From 951156327897e5ba15015c40370d675386407c1f Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Mon, 24 Nov 2025 17:09:30 -0500 Subject: [PATCH 01/22] Think its there tbh, need to make proc macro and a few readmes and test it. --- packages/rust/Cargo.toml | 10 +- packages/rust/rust-plugin-macro/Cargo.toml | 6 + packages/rust/rust-plugin-macro/src/main.rs | 3 + packages/rust/rust-plugin-test/Cargo.toml | 6 + packages/rust/rust-plugin-test/src/main.rs | 3 + packages/rust/src/event/context.rs | 55 ++ packages/rust/src/event/handler.rs | 608 ++++++++++++++++++ packages/rust/src/event/mod.rs | 6 + packages/rust/src/event/mutations.rs | 200 ++++++ packages/rust/src/lib.rs | 119 +++- packages/rust/src/server/helpers.rs | 416 ++++++++++++ packages/rust/src/server/server.rs | 94 +++ packages/rust/xtask/Cargo.toml | 14 + packages/rust/xtask/src/generate_actions.rs | 76 +++ packages/rust/xtask/src/generate_handlers.rs | 74 +++ packages/rust/xtask/src/generate_mutations.rs | 83 +++ packages/rust/xtask/src/main.rs | 39 ++ packages/rust/xtask/src/utils.rs | 171 +++++ 18 files changed, 1981 insertions(+), 2 deletions(-) create mode 100644 packages/rust/rust-plugin-macro/Cargo.toml create mode 100644 packages/rust/rust-plugin-macro/src/main.rs create mode 100644 packages/rust/rust-plugin-test/Cargo.toml create mode 100644 packages/rust/rust-plugin-test/src/main.rs create mode 100644 packages/rust/src/event/context.rs create mode 100644 packages/rust/src/event/handler.rs create mode 100644 packages/rust/src/event/mod.rs create mode 100644 packages/rust/src/event/mutations.rs create mode 100644 packages/rust/src/server/helpers.rs create mode 100644 packages/rust/src/server/server.rs create mode 100644 packages/rust/xtask/Cargo.toml create mode 100644 packages/rust/xtask/src/generate_actions.rs create mode 100644 packages/rust/xtask/src/generate_handlers.rs create mode 100644 packages/rust/xtask/src/generate_mutations.rs create mode 100644 packages/rust/xtask/src/main.rs create mode 100644 packages/rust/xtask/src/utils.rs diff --git a/packages/rust/Cargo.toml b/packages/rust/Cargo.toml index 0f66308..5bdb9a9 100644 --- a/packages/rust/Cargo.toml +++ b/packages/rust/Cargo.toml @@ -1,3 +1,7 @@ +[workspace] +resolver = "3" +members = [".", "rust-plugin-macro", "rust-plugin-test", "xtask"] + [package] name = "dragonfly-plugin" version = "0.1.0" @@ -10,5 +14,9 @@ description = "Dragonfly gRPC plugin SDK for Rust" path = "src/lib.rs" [dependencies] +async-trait = "0.1.89" +prettyplease = "0.2.37" prost = "0.13" -tonic = { version = "0.12", features = ["transport"] } \ No newline at end of file +tokio = "1.48.0" +tokio-stream = "0.1.17" +tonic = { version = "0.12", features = ["transport"] } diff --git a/packages/rust/rust-plugin-macro/Cargo.toml b/packages/rust/rust-plugin-macro/Cargo.toml new file mode 100644 index 0000000..958b5b7 --- /dev/null +++ b/packages/rust/rust-plugin-macro/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "rust-plugin-macro" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/packages/rust/rust-plugin-macro/src/main.rs b/packages/rust/rust-plugin-macro/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/packages/rust/rust-plugin-macro/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/packages/rust/rust-plugin-test/Cargo.toml b/packages/rust/rust-plugin-test/Cargo.toml new file mode 100644 index 0000000..1d4876a --- /dev/null +++ b/packages/rust/rust-plugin-test/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "rust-plugin-test" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/packages/rust/rust-plugin-test/src/main.rs b/packages/rust/rust-plugin-test/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/packages/rust/rust-plugin-test/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/packages/rust/src/event/context.rs b/packages/rust/src/event/context.rs new file mode 100644 index 0000000..8a015cf --- /dev/null +++ b/packages/rust/src/event/context.rs @@ -0,0 +1,55 @@ +use crate::types; + +/// This enum is used internally by `dispatch_event` to +/// determine what action to take after an event handler runs. +#[doc(hidden)] +pub enum EventResultUpdate { + /// Do nothing, let the default server behavior happen. just sends ack. + None, + /// Cancel the event, stopping default server behavior. + Cancelled, + /// Mutate the event, which is sent back to the server. + Mutated(types::event_result::Update), +} + +/// A smart wrapper for a server event. +/// +/// This struct provides read-only access to the event's data +/// and methods to mutate or cancel it. +pub struct EventContext<'a, T> { + pub data: &'a T, + + event_id: &'a str, + result: EventResultUpdate, +} + +impl<'a, T> EventContext<'a, T> { + #[doc(hidden)] + pub fn new(event_id: &'a str, data: &'a T) -> Self { + Self { + event_id, + data, + result: EventResultUpdate::None, + } + } + + /// Consumes the context and returns the final result. + #[doc(hidden)] + pub fn into_result(self) -> (String, EventResultUpdate) { + (self.event_id.to_string(), self.result) + } + + /// Cancels the event. + /// + /// The server's default handler will not run. + pub fn cancel(&mut self) { + self.result = EventResultUpdate::Cancelled; + } + + /// Internal helper to set a mutation. + /// This is called by the auto-generated helper methods. + #[doc(hidden)] + pub fn set_mutation(&mut self, update: types::event_result::Update) { + self.result = EventResultUpdate::Mutated(update); + } +} diff --git a/packages/rust/src/event/handler.rs b/packages/rust/src/event/handler.rs new file mode 100644 index 0000000..d54b577 --- /dev/null +++ b/packages/rust/src/event/handler.rs @@ -0,0 +1,608 @@ +#![allow(unused_variables)] +use crate::{event::EventContext, types, PluginSubscriptions, Server}; +use async_trait::async_trait; +#[async_trait] +pub trait PluginEventHandler: PluginSubscriptions + Send + Sync { + ///Handler for the `PlayerJoin` event. + async fn on_player_join( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerQuit` event. + async fn on_player_quit( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerMove` event. + async fn on_player_move( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerJump` event. + async fn on_player_jump( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerTeleport` event. + async fn on_player_teleport( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerChangeWorld` event. + async fn on_player_change_world( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerToggleSprint` event. + async fn on_player_toggle_sprint( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerToggleSneak` event. + async fn on_player_toggle_sneak( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `Chat` event. + async fn on_chat(&self, _server: &Server, _event: &mut EventContext) {} + ///Handler for the `PlayerFoodLoss` event. + async fn on_player_food_loss( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerHeal` event. + async fn on_player_heal( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerHurt` event. + async fn on_player_hurt( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerDeath` event. + async fn on_player_death( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerRespawn` event. + async fn on_player_respawn( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerSkinChange` event. + async fn on_player_skin_change( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerFireExtinguish` event. + async fn on_player_fire_extinguish( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerStartBreak` event. + async fn on_player_start_break( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `BlockBreak` event. + async fn on_block_break( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerBlockPlace` event. + async fn on_player_block_place( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerBlockPick` event. + async fn on_player_block_pick( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerItemUse` event. + async fn on_player_item_use( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerItemUseOnBlock` event. + async fn on_player_item_use_on_block( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerItemUseOnEntity` event. + async fn on_player_item_use_on_entity( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerItemRelease` event. + async fn on_player_item_release( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerItemConsume` event. + async fn on_player_item_consume( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerAttackEntity` event. + async fn on_player_attack_entity( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerExperienceGain` event. + async fn on_player_experience_gain( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerPunchAir` event. + async fn on_player_punch_air( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerSignEdit` event. + async fn on_player_sign_edit( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerLecternPageTurn` event. + async fn on_player_lectern_page_turn( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerItemDamage` event. + async fn on_player_item_damage( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerItemPickup` event. + async fn on_player_item_pickup( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerHeldSlotChange` event. + async fn on_player_held_slot_change( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerItemDrop` event. + async fn on_player_item_drop( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `PlayerTransfer` event. + async fn on_player_transfer( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `Command` event. + async fn on_command(&self, _server: &Server, _event: &mut EventContext) {} + ///Handler for the `PlayerDiagnostics` event. + async fn on_player_diagnostics( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `WorldLiquidFlow` event. + async fn on_world_liquid_flow( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `WorldLiquidDecay` event. + async fn on_world_liquid_decay( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `WorldLiquidHarden` event. + async fn on_world_liquid_harden( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `WorldSound` event. + async fn on_world_sound( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `WorldFireSpread` event. + async fn on_world_fire_spread( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `WorldBlockBurn` event. + async fn on_world_block_burn( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `WorldCropTrample` event. + async fn on_world_crop_trample( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `WorldLeavesDecay` event. + async fn on_world_leaves_decay( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `WorldEntitySpawn` event. + async fn on_world_entity_spawn( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `WorldEntityDespawn` event. + async fn on_world_entity_despawn( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `WorldExplosion` event. + async fn on_world_explosion( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } + ///Handler for the `WorldClose` event. + async fn on_world_close( + &self, + _server: &Server, + _event: &mut EventContext, + ) { + } +} +#[doc(hidden)] +pub async fn dispatch_event( + server: &Server, + handler: &impl PluginEventHandler, + envelope: &types::EventEnvelope, +) { + let Some(payload) = &envelope.payload else { + return; + }; + match payload { + types::event_envelope::Payload::PlayerJoin(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_join(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerQuit(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_quit(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerMove(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_move(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerJump(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_jump(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerTeleport(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_teleport(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerChangeWorld(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_change_world(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerToggleSprint(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_toggle_sprint(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerToggleSneak(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_toggle_sneak(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::Chat(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_chat(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerFoodLoss(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_food_loss(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerHeal(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_heal(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerHurt(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_hurt(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerDeath(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_death(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerRespawn(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_respawn(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerSkinChange(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_skin_change(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerFireExtinguish(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler + .on_player_fire_extinguish(server, &mut context) + .await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerStartBreak(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_start_break(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::BlockBreak(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_block_break(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerBlockPlace(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_block_place(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerBlockPick(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_block_pick(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerItemUse(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_item_use(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerItemUseOnBlock(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler + .on_player_item_use_on_block(server, &mut context) + .await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerItemUseOnEntity(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler + .on_player_item_use_on_entity(server, &mut context) + .await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerItemRelease(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_item_release(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerItemConsume(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_item_consume(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerAttackEntity(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_attack_entity(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerExperienceGain(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler + .on_player_experience_gain(server, &mut context) + .await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerPunchAir(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_punch_air(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerSignEdit(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_sign_edit(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerLecternPageTurn(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler + .on_player_lectern_page_turn(server, &mut context) + .await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerItemDamage(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_item_damage(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerItemPickup(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_item_pickup(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerHeldSlotChange(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler + .on_player_held_slot_change(server, &mut context) + .await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerItemDrop(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_item_drop(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerTransfer(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_transfer(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::Command(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_command(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::PlayerDiagnostics(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_player_diagnostics(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::WorldLiquidFlow(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_world_liquid_flow(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::WorldLiquidDecay(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_world_liquid_decay(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::WorldLiquidHarden(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_world_liquid_harden(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::WorldSound(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_world_sound(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::WorldFireSpread(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_world_fire_spread(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::WorldBlockBurn(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_world_block_burn(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::WorldCropTrample(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_world_crop_trample(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::WorldLeavesDecay(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_world_leaves_decay(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::WorldEntitySpawn(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_world_entity_spawn(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::WorldEntityDespawn(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_world_entity_despawn(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::WorldExplosion(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_world_explosion(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::WorldClose(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_world_close(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + } +} diff --git a/packages/rust/src/event/mod.rs b/packages/rust/src/event/mod.rs new file mode 100644 index 0000000..da975ef --- /dev/null +++ b/packages/rust/src/event/mod.rs @@ -0,0 +1,6 @@ +pub mod context; +pub mod handler; +pub mod mutations; + +pub use context::*; +pub use handler::*; diff --git a/packages/rust/src/event/mutations.rs b/packages/rust/src/event/mutations.rs new file mode 100644 index 0000000..13ac041 --- /dev/null +++ b/packages/rust/src/event/mutations.rs @@ -0,0 +1,200 @@ +use crate::types; +use crate::event::EventContext; +impl<'a> EventContext<'a, types::ChatEvent> { + ///Sets the `message` for this event. + pub fn set_message(&mut self, message: String) { + let mutation = types::ChatMutation { + message: Some(message.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::Chat(mutation)); + } +} +impl<'a> EventContext<'a, types::BlockBreakEvent> { + ///Sets the `drops` for this event. + pub fn set_drops(&mut self, drops: Vec) { + let mutation = types::BlockBreakMutation { + drops: Some(types::ItemStackList { + items: drops.into_iter().map(|s| s.into()).collect(), + }), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::BlockBreak(mutation)); + } + ///Sets the `xp` for this event. + pub fn set_xp(&mut self, xp: i32) { + let mutation = types::BlockBreakMutation { + xp: Some(xp.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::BlockBreak(mutation)); + } +} +impl<'a> EventContext<'a, types::PlayerFoodLossEvent> { + ///Sets the `to` for this event. + pub fn set_to(&mut self, to: i32) { + let mutation = types::PlayerFoodLossMutation { + to: Some(to.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::PlayerFoodLoss(mutation)); + } +} +impl<'a> EventContext<'a, types::PlayerHealEvent> { + ///Sets the `amount` for this event. + pub fn set_amount(&mut self, amount: f64) { + let mutation = types::PlayerHealMutation { + amount: Some(amount.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::PlayerHeal(mutation)); + } +} +impl<'a> EventContext<'a, types::PlayerHurtEvent> { + ///Sets the `damage` for this event. + pub fn set_damage(&mut self, damage: f64) { + let mutation = types::PlayerHurtMutation { + damage: Some(damage.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::PlayerHurt(mutation)); + } + ///Sets the `attack_immunity_ms` for this event. + pub fn set_attack_immunity_ms(&mut self, attack_immunity_ms: i64) { + let mutation = types::PlayerHurtMutation { + attack_immunity_ms: Some(attack_immunity_ms.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::PlayerHurt(mutation)); + } +} +impl<'a> EventContext<'a, types::PlayerDeathEvent> { + ///Sets the `keep_inventory` for this event. + pub fn set_keep_inventory(&mut self, keep_inventory: bool) { + let mutation = types::PlayerDeathMutation { + keep_inventory: Some(keep_inventory.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::PlayerDeath(mutation)); + } +} +impl<'a> EventContext<'a, types::PlayerRespawnEvent> { + ///Sets the `position` for this event. + pub fn set_position(&mut self, position: types::Vec3) { + let mutation = types::PlayerRespawnMutation { + position: Some(position.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::PlayerRespawn(mutation)); + } + ///Sets the `world` for this event. + pub fn set_world(&mut self, world: types::WorldRef) { + let mutation = types::PlayerRespawnMutation { + world: Some(world.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::PlayerRespawn(mutation)); + } +} +impl<'a> EventContext<'a, types::PlayerAttackEntityEvent> { + ///Sets the `force` for this event. + pub fn set_force(&mut self, force: f64) { + let mutation = types::PlayerAttackEntityMutation { + force: Some(force.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::PlayerAttackEntity(mutation)); + } + ///Sets the `height` for this event. + pub fn set_height(&mut self, height: f64) { + let mutation = types::PlayerAttackEntityMutation { + height: Some(height.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::PlayerAttackEntity(mutation)); + } + ///Sets the `critical` for this event. + pub fn set_critical(&mut self, critical: bool) { + let mutation = types::PlayerAttackEntityMutation { + critical: Some(critical.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::PlayerAttackEntity(mutation)); + } +} +impl<'a> EventContext<'a, types::PlayerExperienceGainEvent> { + ///Sets the `amount` for this event. + pub fn set_amount(&mut self, amount: i32) { + let mutation = types::PlayerExperienceGainMutation { + amount: Some(amount.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::PlayerExperienceGain(mutation)); + } +} +impl<'a> EventContext<'a, types::PlayerLecternPageTurnEvent> { + ///Sets the `new_page` for this event. + pub fn set_new_page(&mut self, new_page: i32) { + let mutation = types::PlayerLecternPageTurnMutation { + new_page: Some(new_page.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::PlayerLecternPageTurn(mutation)); + } +} +impl<'a> EventContext<'a, types::PlayerItemPickupEvent> { + ///Sets the `item` for this event. + pub fn set_item(&mut self, item: types::ItemStack) { + let mutation = types::PlayerItemPickupMutation { + item: Some(item.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::PlayerItemPickup(mutation)); + } +} +impl<'a> EventContext<'a, types::PlayerTransferEvent> { + ///Sets the `address` for this event. + pub fn set_address(&mut self, address: types::Address) { + let mutation = types::PlayerTransferMutation { + address: Some(address.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::PlayerTransfer(mutation)); + } +} +impl<'a> EventContext<'a, types::WorldExplosionEvent> { + ///Sets the `entity_uuids` for this event. + pub fn set_entity_uuids(&mut self, entity_uuids: Vec) { + let mutation = types::WorldExplosionMutation { + entity_uuids: Some(types::StringList { + values: entity_uuids.into_iter().map(|s| s.into()).collect(), + }), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::WorldExplosion(mutation)); + } + ///Sets the `blocks` for this event. + pub fn set_blocks(&mut self, blocks: types::BlockPosList) { + let mutation = types::WorldExplosionMutation { + blocks: Some(blocks.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::WorldExplosion(mutation)); + } + ///Sets the `item_drop_chance` for this event. + pub fn set_item_drop_chance(&mut self, item_drop_chance: f64) { + let mutation = types::WorldExplosionMutation { + item_drop_chance: Some(item_drop_chance.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::WorldExplosion(mutation)); + } + ///Sets the `spawn_fire` for this event. + pub fn set_spawn_fire(&mut self, spawn_fire: bool) { + let mutation = types::WorldExplosionMutation { + spawn_fire: Some(spawn_fire.into()), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::WorldExplosion(mutation)); + } +} diff --git a/packages/rust/src/lib.rs b/packages/rust/src/lib.rs index 302c57e..59022a6 100644 --- a/packages/rust/src/lib.rs +++ b/packages/rust/src/lib.rs @@ -3,4 +3,121 @@ #[path = "generated/df.plugin.rs"] mod df_plugin; -pub use df_plugin::*; \ No newline at end of file +pub mod types { + pub use super::df_plugin::plugin_client::PluginClient; + pub use super::df_plugin::*; + pub use super::df_plugin::{ + action::Kind as ActionKind, event_envelope::Payload as EventPayload, + event_result::Update as EventResultUpdate, host_to_plugin::Payload as HostPayload, + plugin_to_host::Payload as PluginPayload, + }; +} + +pub mod event; +#[path = "server/server.rs"] +pub mod server; + +use std::error::Error; + +// internal uses. +pub(crate) use server::*; + +// main usage stuff for plugin devs: +pub use async_trait::async_trait; +pub use event::PluginEventHandler; +use tokio::sync::mpsc; +use tokio_stream::{wrappers::ReceiverStream, StreamExt}; +// TODO: pub use rust_plugin_macro::bedrock_plugin; + +pub struct Plugin { + id: String, + name: String, + version: String, + api_version: String, +} + +impl Plugin { + pub fn new(id: &str, name: &str, version: &str, api_version: &str) -> Self { + Self { + id: id.to_string(), + name: name.to_string(), + version: version.to_string(), + api_version: api_version.to_string(), + } + } + + /// Runs the plugin, connecting to the server and starting the event loop. + pub async fn run( + self, + handler: impl PluginEventHandler + PluginSubscriptions + 'static, + addr: A, + ) -> Result<(), Box> + where + // Yeah this was AI, but holy hell is it good at doing dynamic type stuff. + A: TryInto, + A::Error: Into>, + { + let mut raw_client = types::PluginClient::connect(addr) + .await + .map_err(|e| Box::new(e) as Box)?; + + let (tx, rx) = mpsc::channel(128); + + let request_stream = ReceiverStream::new(rx); + + let mut event_stream = raw_client.event_stream(request_stream).await?.into_inner(); + + let server = Server { + plugin_id: self.id.clone(), + sender: tx.clone(), + }; + + let hello_msg = types::PluginToHost { + plugin_id: self.id.clone(), + payload: Some(types::PluginPayload::Hello(types::PluginHello { + name: self.name.clone(), + version: self.version.clone(), + api_version: self.api_version.clone(), + commands: vec![], + custom_items: vec![], + })), + }; + tx.send(hello_msg).await?; + + let events = handler.get_subscriptions(); + if !events.is_empty() { + println!("Subscribing to {} event types...", events.len()); + server.subscribe(events).await?; + } + + println!("Plugin '{}' connected and listening.", self.name); + + // 8. Run the main event loop + while let Some(Ok(msg)) = event_stream.next().await { + match msg.payload { + // We received a game event + Some(types::HostPayload::Event(envelope)) => { + event::dispatch_event(&server, &handler, &envelope).await; + } + // The server is shutting us down + Some(types::HostPayload::Shutdown(shutdown)) => { + println!("Server shutting down plugin: {}", shutdown.reason); + break; // Break the loop + } + _ => { /* Ignore other payloads */ } + } + } + + println!("Plugin '{}' disconnected.", self.name); + Ok(()) + } +} + +/// A trait that defines which events your plugin will receive. +/// +/// You can implement this trait manually, or you can use the +/// `#[bedrock_plugin]` macro on your `PluginEventHandler` +/// implementation to generate it for you. +pub trait PluginSubscriptions { + fn get_subscriptions(&self) -> Vec; +} diff --git a/packages/rust/src/server/helpers.rs b/packages/rust/src/server/helpers.rs new file mode 100644 index 0000000..e8652d5 --- /dev/null +++ b/packages/rust/src/server/helpers.rs @@ -0,0 +1,416 @@ +use crate::{types, Server}; +use tokio::sync::mpsc; +impl Server { + ///Sends a `SendChat` action to the server. + pub async fn send_chat( + &self, + target_uuid: String, + message: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::SendChat(types::SendChatAction { + target_uuid: target_uuid.into(), + message: message.into(), + }), + ) + .await + } + ///Sends a `Teleport` action to the server. + pub async fn teleport( + &self, + player_uuid: String, + position: Option, + rotation: Option, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::Teleport(types::TeleportAction { + player_uuid: player_uuid.into(), + position: position.map(|v| v.into()), + rotation: rotation.map(|v| v.into()), + }), + ) + .await + } + ///Sends a `Kick` action to the server. + pub async fn kick( + &self, + player_uuid: String, + reason: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::Kick(types::KickAction { + player_uuid: player_uuid.into(), + reason: reason.into(), + }), + ) + .await + } + ///Sends a `SetGameMode` action to the server. + pub async fn set_game_mode( + &self, + player_uuid: String, + game_mode: i32, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::SetGameMode(types::SetGameModeAction { + player_uuid: player_uuid.into(), + game_mode: game_mode.into(), + }), + ) + .await + } + ///Sends a `GiveItem` action to the server. + pub async fn give_item( + &self, + player_uuid: String, + item: Option, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::GiveItem(types::GiveItemAction { + player_uuid: player_uuid.into(), + item: item.map(|v| v.into()), + }), + ) + .await + } + ///Sends a `ClearInventory` action to the server. + pub async fn clear_inventory( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::ClearInventory(types::ClearInventoryAction { + player_uuid: player_uuid.into(), + }), + ) + .await + } + ///Sends a `SetHeldItem` action to the server. + pub async fn set_held_item( + &self, + player_uuid: String, + main: Option, + offhand: Option, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::SetHeldItem(types::SetHeldItemAction { + player_uuid: player_uuid.into(), + main: main.map(|v| v.into()), + offhand: offhand.map(|v| v.into()), + }), + ) + .await + } + ///Sends a `SetHealth` action to the server. + pub async fn set_health( + &self, + player_uuid: String, + health: f64, + max_health: Option, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::SetHealth(types::SetHealthAction { + player_uuid: player_uuid.into(), + health: health.into(), + max_health: max_health.map(|v| v.into()), + }), + ) + .await + } + ///Sends a `SetFood` action to the server. + pub async fn set_food( + &self, + player_uuid: String, + food: i32, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::SetFood(types::SetFoodAction { + player_uuid: player_uuid.into(), + food: food.into(), + }), + ) + .await + } + ///Sends a `SetExperience` action to the server. + pub async fn set_experience( + &self, + player_uuid: String, + level: Option, + progress: Option, + amount: Option, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::SetExperience(types::SetExperienceAction { + player_uuid: player_uuid.into(), + level: level.map(|v| v.into()), + progress: progress.map(|v| v.into()), + amount: amount.map(|v| v.into()), + }), + ) + .await + } + ///Sends a `SetVelocity` action to the server. + pub async fn set_velocity( + &self, + player_uuid: String, + velocity: Option, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::SetVelocity(types::SetVelocityAction { + player_uuid: player_uuid.into(), + velocity: velocity.map(|v| v.into()), + }), + ) + .await + } + ///Sends a `AddEffect` action to the server. + pub async fn add_effect( + &self, + player_uuid: String, + effect_type: i32, + level: i32, + duration_ms: i64, + show_particles: bool, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::AddEffect(types::AddEffectAction { + player_uuid: player_uuid.into(), + effect_type: effect_type.into(), + level: level.into(), + duration_ms: duration_ms.into(), + show_particles: show_particles.into(), + }), + ) + .await + } + ///Sends a `RemoveEffect` action to the server. + pub async fn remove_effect( + &self, + player_uuid: String, + effect_type: i32, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::RemoveEffect(types::RemoveEffectAction { + player_uuid: player_uuid.into(), + effect_type: effect_type.into(), + }), + ) + .await + } + ///Sends a `SendTitle` action to the server. + pub async fn send_title( + &self, + player_uuid: String, + title: String, + subtitle: Option, + fade_in_ms: Option, + duration_ms: Option, + fade_out_ms: Option, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::SendTitle(types::SendTitleAction { + player_uuid: player_uuid.into(), + title: title.into(), + subtitle: subtitle.map(|v| v.into()), + fade_in_ms: fade_in_ms.map(|v| v.into()), + duration_ms: duration_ms.map(|v| v.into()), + fade_out_ms: fade_out_ms.map(|v| v.into()), + }), + ) + .await + } + ///Sends a `SendPopup` action to the server. + pub async fn send_popup( + &self, + player_uuid: String, + message: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::SendPopup(types::SendPopupAction { + player_uuid: player_uuid.into(), + message: message.into(), + }), + ) + .await + } + ///Sends a `SendTip` action to the server. + pub async fn send_tip( + &self, + player_uuid: String, + message: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::SendTip(types::SendTipAction { + player_uuid: player_uuid.into(), + message: message.into(), + }), + ) + .await + } + ///Sends a `PlaySound` action to the server. + pub async fn play_sound( + &self, + player_uuid: String, + sound: i32, + position: Option, + volume: Option, + pitch: Option, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::PlaySound(types::PlaySoundAction { + player_uuid: player_uuid.into(), + sound: sound.into(), + position: position.map(|v| v.into()), + volume: volume.map(|v| v.into()), + pitch: pitch.map(|v| v.into()), + }), + ) + .await + } + ///Sends a `ExecuteCommand` action to the server. + pub async fn execute_command( + &self, + player_uuid: String, + command: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::ExecuteCommand(types::ExecuteCommandAction { + player_uuid: player_uuid.into(), + command: command.into(), + }), + ) + .await + } + ///Sends a `WorldSetDefaultGameMode` action to the server. + pub async fn world_set_default_game_mode( + &self, + world: Option, + game_mode: i32, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::WorldSetDefaultGameMode(types::WorldSetDefaultGameModeAction { + world: world.map(|v| v.into()), + game_mode: game_mode.into(), + }), + ) + .await + } + ///Sends a `WorldSetDifficulty` action to the server. + pub async fn world_set_difficulty( + &self, + world: Option, + difficulty: i32, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::WorldSetDifficulty(types::WorldSetDifficultyAction { + world: world.map(|v| v.into()), + difficulty: difficulty.into(), + }), + ) + .await + } + ///Sends a `WorldSetTickRange` action to the server. + pub async fn world_set_tick_range( + &self, + world: Option, + tick_range: i32, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::WorldSetTickRange(types::WorldSetTickRangeAction { + world: world.map(|v| v.into()), + tick_range: tick_range.into(), + }), + ) + .await + } + ///Sends a `WorldSetBlock` action to the server. + pub async fn world_set_block( + &self, + world: Option, + position: Option, + block: Option, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::WorldSetBlock(types::WorldSetBlockAction { + world: world.map(|v| v.into()), + position: position.map(|v| v.into()), + block: block.map(|v| v.into()), + }), + ) + .await + } + ///Sends a `WorldPlaySound` action to the server. + pub async fn world_play_sound( + &self, + world: Option, + sound: i32, + position: Option, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::WorldPlaySound(types::WorldPlaySoundAction { + world: world.map(|v| v.into()), + sound: sound.into(), + position: position.map(|v| v.into()), + }), + ) + .await + } + ///Sends a `WorldAddParticle` action to the server. + pub async fn world_add_particle( + &self, + world: Option, + position: Option, + particle: i32, + block: Option, + face: Option, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::WorldAddParticle(types::WorldAddParticleAction { + world: world.map(|v| v.into()), + position: position.map(|v| v.into()), + particle: particle.into(), + block: block.map(|v| v.into()), + face: face.map(|v| v.into()), + }), + ) + .await + } + ///Sends a `WorldQueryEntities` action to the server. + pub async fn world_query_entities( + &self, + world: Option, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::WorldQueryEntities(types::WorldQueryEntitiesAction { + world: world.map(|v| v.into()), + }), + ) + .await + } + ///Sends a `WorldQueryPlayers` action to the server. + pub async fn world_query_players( + &self, + world: Option, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::WorldQueryPlayers(types::WorldQueryPlayersAction { + world: world.map(|v| v.into()), + }), + ) + .await + } + ///Sends a `WorldQueryEntitiesWithin` action to the server. + pub async fn world_query_entities_within( + &self, + world: Option, + r#box: Option, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::WorldQueryEntitiesWithin(types::WorldQueryEntitiesWithinAction { + world: world.map(|v| v.into()), + r#box: r#box.map(|v| v.into()), + }), + ) + .await + } +} diff --git a/packages/rust/src/server/server.rs b/packages/rust/src/server/server.rs new file mode 100644 index 0000000..3efe1ca --- /dev/null +++ b/packages/rust/src/server/server.rs @@ -0,0 +1,94 @@ +use tokio::sync::mpsc; + +use crate::{ + event::{EventContext, EventResultUpdate}, + types::{self, PluginToHost}, +}; + +#[derive(Clone)] +pub struct Server { + pub plugin_id: String, + pub sender: mpsc::Sender, +} + +impl Server { + /// Helper to build and send a single action. + pub async fn send_action( + &self, + kind: types::action::Kind, + ) -> Result<(), mpsc::error::SendError> { + let action = types::Action { + correlation_id: None, + kind: Some(kind), + }; + let batch = types::ActionBatch { + actions: vec![action], + }; + let msg = PluginToHost { + plugin_id: self.plugin_id.clone(), + payload: Some(types::PluginPayload::Actions(batch)), + }; + self.sender.send(msg).await + } + + /// Helper to send a batch of actions. + pub async fn send_actions( + &self, + actions: Vec, + ) -> Result<(), mpsc::error::SendError> { + let batch = types::ActionBatch { actions }; + let msg = PluginToHost { + plugin_id: self.plugin_id.clone(), + payload: Some(types::PluginPayload::Actions(batch)), + }; + self.sender.send(msg).await + } + + /// Subscribe to a list of game events. + pub async fn subscribe( + &self, + events: Vec, + ) -> Result<(), mpsc::error::SendError> { + let sub = types::EventSubscribe { + events: events.into_iter().map(|e| e.into()).collect(), + }; + let msg = PluginToHost { + plugin_id: self.plugin_id.clone(), + payload: Some(types::PluginPayload::Subscribe(sub)), + }; + self.sender.send(msg).await + } + + /// Internal helper to send an event result (cancel/mutate) + /// This is called by the auto-generated `dispatch_event` function. + #[doc(hidden)] + pub(crate) async fn send_event_result( + &self, + context: EventContext<'_, impl Sized>, + ) -> Result<(), mpsc::error::SendError> { + let (event_id, result) = context.into_result(); + + let payload = match result { + // Do nothing if the handler didn't mutate or cancel + EventResultUpdate::None => return Ok(()), + EventResultUpdate::Cancelled => types::EventResult { + event_id, + cancel: Some(true), + update: None, + }, + EventResultUpdate::Mutated(update) => types::EventResult { + event_id, + cancel: None, + update: Some(update), + }, + }; + + let msg = types::PluginToHost { + plugin_id: self.plugin_id.clone(), + payload: Some(types::PluginPayload::EventResult(payload)), + }; + self.sender.send(msg).await + } +} + +mod helpers; diff --git a/packages/rust/xtask/Cargo.toml b/packages/rust/xtask/Cargo.toml new file mode 100644 index 0000000..614b497 --- /dev/null +++ b/packages/rust/xtask/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2021" + +publish = false + +[dependencies] +syn = { version = "2.0", features = ["full", "parsing", "visit"] } +quote = "1.0" +heck = "0.5" # For to_snake_case +anyhow = "1.0" +prettyplease = "0.2.37" +proc-macro2 = "1.0.103" diff --git a/packages/rust/xtask/src/generate_actions.rs b/packages/rust/xtask/src/generate_actions.rs new file mode 100644 index 0000000..4017e55 --- /dev/null +++ b/packages/rust/xtask/src/generate_actions.rs @@ -0,0 +1,76 @@ +use anyhow::Result; +use heck::ToSnakeCase; +use quote::{format_ident, quote}; +use std::{collections::HashMap, path::PathBuf}; +use syn::{File, Ident, ItemStruct}; + +use crate::utils::{ + clean_type, find_nested_enum, get_variant_type_path, unwrap_option_path, write_formatted_file, +}; + +pub(crate) fn generate_server_helpers( + ast: &File, + all_structs: &HashMap, + output_path: &PathBuf, +) -> Result<()> { + let action_kind_enum = find_nested_enum(ast, "action", "Kind")?; + let mut server_helpers = Vec::new(); + + for variant in &action_kind_enum.variants { + let variant_ident = &variant.ident; + let action_struct_path = get_variant_type_path(variant)?; + let action_struct_name = action_struct_path.segments.last().unwrap().ident.clone(); + let fn_name = format_ident!("{}", variant_ident.to_string().to_snake_case()); + let doc_string = format!("Sends a `{}` action to the server.", variant_ident); + + // Find the struct definition + let action_struct_def = all_structs.get(&action_struct_name).ok_or_else(|| { + anyhow::anyhow!("Struct definition not found for {}", action_struct_name) + })?; + + let mut fn_args = Vec::new(); + let mut struct_fields = Vec::new(); + + for field in &action_struct_def.fields { + let field_name = field.ident.as_ref().unwrap(); + let (_inner_type, is_option) = unwrap_option_path(&field.ty); + let arg_type = clean_type(&field.ty); + + if is_option { + // If it's an Option, we must map the inner value + struct_fields.push(quote! { #field_name: #field_name.map(|v| v.into()) }); + } else { + // If it's not an Option, we can call .into() directly + struct_fields.push(quote! { #field_name: #field_name.into() }); + } + + fn_args.push(quote! { #field_name: #arg_type }); + } + + server_helpers.push(quote! { + #[doc = #doc_string] + pub async fn #fn_name( + &self, + #( #fn_args ),* + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::#variant_ident( + types::#action_struct_name { + #( #struct_fields ),* + } + )).await + } + }); + } + + let final_file = quote! { + // This file is auto-generated by `xtask`. Do not edit manually. + use crate::{types, Server}; + use tokio::sync::mpsc; + + impl Server { + #( #server_helpers )* + } + }; + + write_formatted_file(output_path, final_file.to_string()) +} diff --git a/packages/rust/xtask/src/generate_handlers.rs b/packages/rust/xtask/src/generate_handlers.rs new file mode 100644 index 0000000..ca79c17 --- /dev/null +++ b/packages/rust/xtask/src/generate_handlers.rs @@ -0,0 +1,74 @@ +use anyhow::Result; +use heck::ToSnakeCase; +use quote::{format_ident, quote}; +use std::path::PathBuf; +use syn::File; + +use crate::utils::{find_nested_enum, get_variant_type_path, write_formatted_file}; + +pub fn generate_handler_trait(ast: &File, output_path: &PathBuf) -> Result<()> { + println!( + "Generating Event Handler trait in: {}...", + output_path.to_string_lossy() + ); + + let event_payload_enum = find_nested_enum(ast, "event_envelope", "Payload")?; + + let mut event_handler_fns = Vec::new(); + let mut dispatch_fn_match_arms = Vec::new(); + + for variant in &event_payload_enum.variants { + let ident = &variant.ident; + let ty_path = get_variant_type_path(variant)?.segments.last(); + let ident_formatted = ident.to_string().to_snake_case(); + let handler_fn_name = format_ident!("on_{}", ident_formatted); + let doc_string = format!("Handler for the `{}` event.", ident); + + event_handler_fns.push(quote! { + #[doc = #doc_string] + async fn #handler_fn_name( + &self, + _server: &Server, + _event: &mut EventContext, + ) {} + }); + + dispatch_fn_match_arms.push(quote! { + types::event_envelope::Payload::#ident(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.#handler_fn_name(server, &mut context).await; + server.send_event_result(context).await.ok(); + }, + }) + } + + let file = quote! { + // This file is auto-generated by `xtask`. Do not edit manually. + #![allow(unused_variables)] + use crate::{event::EventContext, types, Server, PluginSubscriptions}; + use async_trait::async_trait; + + #[async_trait] + pub trait PluginEventHandler: PluginSubscriptions + Send + Sync { + #(#event_handler_fns)* + } + + #[doc(hidden)] + pub async fn dispatch_event(server: &Server, handler: &impl PluginEventHandler, envelope: &types::EventEnvelope) { + let Some(payload) = &envelope.payload else { + return; + }; + match payload { + #(#dispatch_fn_match_arms)* + } + } + }; + + // write our generated code. + write_formatted_file(output_path, file.to_string())?; + println!( + "Successfully generated Event Handler Trait in: {}.", + output_path.to_string_lossy() + ); + Ok(()) +} diff --git a/packages/rust/xtask/src/generate_mutations.rs b/packages/rust/xtask/src/generate_mutations.rs new file mode 100644 index 0000000..6abaed7 --- /dev/null +++ b/packages/rust/xtask/src/generate_mutations.rs @@ -0,0 +1,83 @@ +use anyhow::Result; +use quote::{format_ident, quote}; +use std::{collections::HashMap, path::PathBuf}; +use syn::{File, Ident, ItemStruct}; + +use crate::utils::{ + clean_type, find_nested_enum, generate_conversion_code, get_variant_type_path, + unwrap_option_path, write_formatted_file, +}; + +pub(crate) fn generate_event_mutations( + ast: &File, + all_structs: &HashMap, + output_path: &PathBuf, +) -> Result<()> { + let mutation_enum = find_nested_enum(ast, "event_result", "Update")?; + let event_payload_enum = find_nested_enum(ast, "event_envelope", "Payload")?; + let mut mutation_impls = Vec::new(); + + for variant in &mutation_enum.variants { + let mutation_variant_name = &variant.ident; + let mutation_struct_path = get_variant_type_path(variant)?; + let mutation_struct_name = mutation_struct_path.segments.last().unwrap().ident.clone(); + + let event_variant = event_payload_enum + .variants + .iter() + .find(|v| v.ident == *mutation_variant_name) + .ok_or_else(|| { + anyhow::anyhow!("No event payload for mutation {}", mutation_variant_name) + })?; + let event_struct_path = get_variant_type_path(event_variant)?; + let event_struct_name = event_struct_path.segments.last().unwrap().ident.clone(); + + // Find mutation struct definition + let mutation_struct_def = all_structs.get(&mutation_struct_name).ok_or_else(|| { + anyhow::anyhow!("Struct definition not found for {}", mutation_struct_name) + })?; + + let mut helper_methods = Vec::new(); + for field in &mutation_struct_def.fields { + let field_name = field.ident.as_ref().unwrap(); + let (inner_type, is_option) = unwrap_option_path(&field.ty); + + if !is_option { + continue; + } + + let arg_type = clean_type(inner_type); + let setter_fn_name = format_ident!("set_{}", field_name); + let doc_string = format!("Sets the `{}` for this event.", field_name); + + let convert_code = generate_conversion_code("e! { #field_name }, inner_type); + + helper_methods.push(quote! { + #[doc = #doc_string] + pub fn #setter_fn_name(&mut self, #field_name: #arg_type) { + let mutation = types::#mutation_struct_name { + #field_name: Some(#convert_code), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::#mutation_variant_name(mutation)); + } + }); + } + + mutation_impls.push(quote! { + impl<'a> EventContext<'a, types::#event_struct_name> { + #( #helper_methods )* + } + }); + } + + let final_file = quote! { + // This file is auto-generated by `xtask`. Do not edit manually. + use crate::types; + use crate::event::EventContext; + + #( #mutation_impls )* + }; + + write_formatted_file(output_path, final_file.to_string()) +} diff --git a/packages/rust/xtask/src/main.rs b/packages/rust/xtask/src/main.rs new file mode 100644 index 0000000..52ce030 --- /dev/null +++ b/packages/rust/xtask/src/main.rs @@ -0,0 +1,39 @@ +pub mod generate_actions; +pub mod generate_handlers; +pub mod generate_mutations; +pub mod utils; + +use anyhow::Result; +use std::{fs, path::PathBuf}; +use syn::parse_file; + +use crate::utils::find_all_structs; + +fn main() -> Result<()> { + println!("Starting 'xtask' code generation..."); + let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .to_path_buf(); + + let generated_file_path = root_dir.join("src/generated/df.plugin.rs"); + let handler_output_path = root_dir.join("src/event/handler.rs"); + let mutations_output_path = root_dir.join("src/event/mutations.rs"); + let server_output_path = root_dir.join("src/server/helpers.rs"); + + // only read the generated code file once we can just reference it out so we don't waste resources. + println!("Parsing generated proto file..."); + let generated_code = fs::read_to_string(generated_file_path)?; + let ast = parse_file(&generated_code)?; + + // just cache all structs, we can hashmap look them up em. + let all_structs = find_all_structs(&ast); + + // first generate event handlers: + generate_handlers::generate_handler_trait(&ast, &handler_output_path)?; + + generate_mutations::generate_event_mutations(&ast, &all_structs, &mutations_output_path)?; + + generate_actions::generate_server_helpers(&ast, &all_structs, &server_output_path)?; + Ok(()) +} diff --git a/packages/rust/xtask/src/utils.rs b/packages/rust/xtask/src/utils.rs new file mode 100644 index 0000000..0d9b018 --- /dev/null +++ b/packages/rust/xtask/src/utils.rs @@ -0,0 +1,171 @@ +use anyhow::Result; +use quote::quote; +use std::{collections::HashMap, fs, path::PathBuf}; +use syn::{ + parse_file, visit::Visit, File, Ident, Item, ItemEnum, ItemMod, ItemStruct, Path, Type, + TypePath, +}; + +pub(crate) fn write_formatted_file(path: &PathBuf, content: String) -> Result<()> { + let ast = parse_file(&content)?; + let formatted_code = prettyplease::unparse(&ast); + fs::write(path, formatted_code)?; + Ok(()) +} + +pub(crate) fn find_all_structs(ast: &File) -> HashMap { + struct StructVisitor<'a> { + structs: HashMap, + } + impl<'a> Visit<'a> for StructVisitor<'a> { + fn visit_item_struct(&mut self, i: &'a ItemStruct) { + self.structs.insert(i.ident.clone(), i); + } + fn visit_item_mod(&mut self, i: &'a ItemMod) { + if let Some((_, items)) = &i.content { + for item in items { + self.visit_item(item); + } + } + } + } + let mut visitor = StructVisitor { + structs: Default::default(), + }; + visitor.visit_file(ast); + visitor.structs +} + +pub(crate) fn find_nested_enum<'a>( + ast: &'a File, + mod_name: &str, + enum_name: &str, +) -> Result<&'a ItemEnum> { + for item in &ast.items { + if let Item::Mod(item_mod) = item { + if item_mod.ident == mod_name { + if let Some((_, items)) = &item_mod.content { + for item in items { + if let Item::Enum(item_enum) = item { + if item_enum.ident == enum_name { + return Ok(item_enum); + } + } + } + } + } + } + } + anyhow::bail!("Enum `{}::{}` not found in AST", mod_name, enum_name) +} + +pub(crate) fn get_variant_type_path(variant: &syn::Variant) -> Result<&Path> { + if let syn::Fields::Unnamed(fields) = &variant.fields { + if let Some(field) = fields.unnamed.first() { + if let Type::Path(type_path) = &field.ty { + return Ok(&type_path.path); + } + } + } + anyhow::bail!("Variant `{}` is not a single-tuple struct", variant.ident) +} + +pub(crate) fn clean_type(ty: &Type) -> proc_macro2::TokenStream { + let (inner_type, is_option) = unwrap_option_path(ty); + if is_option { + let inner_cleaned = clean_type(inner_type); + return quote! { Option<#inner_cleaned> }; + } + + if let Type::Path(type_path) = inner_type { + if let Some(segment) = type_path.path.segments.last() { + let ident_str = segment.ident.to_string(); + + match ident_str.as_str() { + "String" => return quote! { String }, + + "StringList" => return quote! { Vec }, + "ItemStackList" => return quote! { Vec }, + + "Vec" if is_prost_bytes(type_path) => return quote! { Vec }, + + // Primitives (safe, no path) + "f64" => return quote! { f64 }, + "f32" => return quote! { f32 }, + "i64" => return quote! { i64 }, + "u64" => return quote! { u64 }, + "i32" => return quote! { i32 }, + "u32" => return quote! { u32 }, + "bool" => return quote! { bool }, + + // Fallback for complex types (Vec3, WorldRef, etc.) + // This is now correct. If `ident_str` is `Vec3`, it + // will correctly become `types::Vec3`. + _ => { + let ident = &segment.ident; + return quote! { types::#ident }; + } + } + } + } + + // Last resort (e.g., tuples, arrays) + quote! { #ty } +} + +/// Helper for clean_type to detect `prost` bytes (Vec) +fn is_prost_bytes(type_path: &TypePath) -> bool { + let path_str = quote!(#type_path).to_string(); + path_str.contains("Vec < u8 > ") +} +/// Unwraps `Option` or `::core::option::Option` and returns `T` +pub(crate) fn unwrap_option_path(ty: &Type) -> (&Type, bool) { + if let Type::Path(type_path) = ty { + if let Some(segment) = type_path.path.segments.last() { + if segment.ident == "Option" { + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() { + return (inner_ty, true); + } + } + } + } + } + (ty, false) +} + +pub(crate) fn generate_conversion_code( + var_name: &proc_macro2::TokenStream, + original_ty: &Type, +) -> proc_macro2::TokenStream { + let (inner_type, is_option) = unwrap_option_path(original_ty); + if is_option { + let inner_var = quote! { v }; + let inner_conversion = generate_conversion_code(&inner_var, inner_type); + return quote! { #var_name.map(|v| #inner_conversion) }; + } + + if let Type::Path(type_path) = inner_type { + if let Some(segment) = type_path.path.segments.last() { + let ident_str = segment.ident.to_string(); + + if ident_str == "StringList" { + return quote! { + types::StringList { + values: #var_name.into_iter().map(|s| s.into()).collect(), + } + }; + } + if ident_str == "ItemStackList" { + return quote! { + types::ItemStackList { + items: #var_name.into_iter().map(|s| s.into()).collect(), + } + }; + } + } + } + + // Default case: just use .into() + quote! { #var_name.into() } +} From 6d02528fee1946e2a4329707c96f8d7932491363 Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Mon, 24 Nov 2025 17:15:04 -0500 Subject: [PATCH 02/22] AI wrote example plugin for testing. --- packages/rust/rust-plugin-test/Cargo.toml | 2 + packages/rust/rust-plugin-test/src/main.rs | 94 +++++++++++++++++++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/packages/rust/rust-plugin-test/Cargo.toml b/packages/rust/rust-plugin-test/Cargo.toml index 1d4876a..44459d9 100644 --- a/packages/rust/rust-plugin-test/Cargo.toml +++ b/packages/rust/rust-plugin-test/Cargo.toml @@ -4,3 +4,5 @@ version = "0.1.0" edition = "2024" [dependencies] +dragonfly-plugin = { path = "../"} +tokio = { version = "1", features = ["full"] } diff --git a/packages/rust/rust-plugin-test/src/main.rs b/packages/rust/rust-plugin-test/src/main.rs index e7a11a9..e9348da 100644 --- a/packages/rust/rust-plugin-test/src/main.rs +++ b/packages/rust/rust-plugin-test/src/main.rs @@ -1,3 +1,93 @@ -fn main() { - println!("Hello, world!"); +use my_bedrock_api::{ + Plugin, // The plugin runner + Server, // The handle to the server + async_trait, // Required by PluginEventHandler + bedrock_plugin, // The "magic" macro + event_context::EventContext, // The event wrapper + event_handler::PluginEventHandler, // The main trait + types, // All the raw prost/tonic types +}; + +// --- 2. Define a struct for your plugin's state --- +// It can be empty, or it can hold databases, configs, etc. +// We add `Default` so it's easy to create. +#[derive(Default)] +struct MyExamplePlugin; + +// --- 3. Implement the event handlers --- +// +// * We add `#[bedrock_plugin]` to this block. +// This macro will scan for every `on_...` function we implement +// and automatically generate the `impl PluginSubscriptions` for us. +// +// * We add `#[async_trait]` because the trait uses async functions. + +#[bedrock_plugin] +#[async_trait] +impl PluginEventHandler for MyExamplePlugin { + /// This handler runs when a player joins the server. + /// We'll use it to send our "hello world" message. + async fn on_player_join( + &self, + server: &Server, + event: &mut EventContext, + ) { + // Log to the plugin's console + println!("Player '{}' has joined the server.", event.data.name); + + // Send a public, broadcasted chat message. + // We assume `send_chat` was generated by `xtask` from a `SendChatAction`. + let welcome_message = format!( + "Welcome, {}! This server is running MyExamplePlugin.", + event.data.name + ); + + // We call the auto-generated `server.send_chat` helper. + // `.await.ok()` sends the message and ignores any potential + // (but rare) connection errors. + server.send_chat(welcome_message).await.ok(); + } + + /// This handler runs every time a player sends a chat message. + /// We'll use it to edit the message. + async fn on_chat( + &self, + _server: &Server, // We don't need the server handle for this + event: &mut EventContext, + ) { + // Get the original message from the event's data + let original_message = &event.data.message; + + // Create the new message + let new_message = format!("[Plugin] {}", original_message); + + // Use the auto-generated `set_message` helper to + // mutate the event before the server processes it. + event.set_message(new_message); + } + + // We don't implement `on_player_hurt`, `on_block_break`, etc., + // so the `#[bedrock_plugin]` macro will not subscribe to them. +} + +// --- 4. The main function to run the plugin --- +#[tokio::main] +async fn main() -> Result<(), Box> { + // 1. Define the plugin's metadata + let plugin = Plugin::new( + "example-plugin", // A unique ID for your plugin + "My Example Plugin", // A human-readable name + "1.0.0", // Your plugin's version + "1.0.0", // The API version you're built against + ); + + // 2. Connect to the server and run the plugin + println!("Connecting to Bedrock server..."); + + plugin + .run( + MyExamplePlugin::default(), // Pass in an instance of our handler + "http://[::1]:50051", // The server address + ) + .await } From 243a360cf5ef5abe00546fb883fd789184e5ac58 Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Mon, 24 Nov 2025 17:22:24 -0500 Subject: [PATCH 03/22] should be runnable now example, need to now do the proc macro --- packages/rust/rust-plugin-test/src/main.rs | 33 +++++++++++++--------- packages/rust/src/lib.rs | 3 +- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/rust/rust-plugin-test/src/main.rs b/packages/rust/rust-plugin-test/src/main.rs index e9348da..42e8a12 100644 --- a/packages/rust/rust-plugin-test/src/main.rs +++ b/packages/rust/rust-plugin-test/src/main.rs @@ -1,11 +1,10 @@ -use my_bedrock_api::{ - Plugin, // The plugin runner - Server, // The handle to the server - async_trait, // Required by PluginEventHandler - bedrock_plugin, // The "magic" macro - event_context::EventContext, // The event wrapper - event_handler::PluginEventHandler, // The main trait - types, // All the raw prost/tonic types +use dragonfly_plugin::{ + Plugin, + PluginSubscriptions, + Server, + async_trait, + event::{EventContext, PluginEventHandler}, + types, // All the raw prost/tonic types }; // --- 2. Define a struct for your plugin's state --- @@ -22,7 +21,6 @@ struct MyExamplePlugin; // // * We add `#[async_trait]` because the trait uses async functions. -#[bedrock_plugin] #[async_trait] impl PluginEventHandler for MyExamplePlugin { /// This handler runs when a player joins the server. @@ -35,7 +33,6 @@ impl PluginEventHandler for MyExamplePlugin { // Log to the plugin's console println!("Player '{}' has joined the server.", event.data.name); - // Send a public, broadcasted chat message. // We assume `send_chat` was generated by `xtask` from a `SendChatAction`. let welcome_message = format!( "Welcome, {}! This server is running MyExamplePlugin.", @@ -45,7 +42,10 @@ impl PluginEventHandler for MyExamplePlugin { // We call the auto-generated `server.send_chat` helper. // `.await.ok()` sends the message and ignores any potential // (but rare) connection errors. - server.send_chat(welcome_message).await.ok(); + server + .send_chat(event.data.player_uuid.clone(), welcome_message) + .await + .ok(); } /// This handler runs every time a player sends a chat message. @@ -70,6 +70,13 @@ impl PluginEventHandler for MyExamplePlugin { // so the `#[bedrock_plugin]` macro will not subscribe to them. } +// THIS IS AN OPTIONAL THING USUALLY USE the proc macro. +impl PluginSubscriptions for MyExamplePlugin { + fn get_subscriptions(&self) -> Vec { + vec![types::EventType::Chat] + } +} + // --- 4. The main function to run the plugin --- #[tokio::main] async fn main() -> Result<(), Box> { @@ -86,8 +93,8 @@ async fn main() -> Result<(), Box> { plugin .run( - MyExamplePlugin::default(), // Pass in an instance of our handler - "http://[::1]:50051", // The server address + MyExamplePlugin, // Pass in an instance of our handler + "http://[::1]:50051", // The server address ) .await } diff --git a/packages/rust/src/lib.rs b/packages/rust/src/lib.rs index 59022a6..8ac092b 100644 --- a/packages/rust/src/lib.rs +++ b/packages/rust/src/lib.rs @@ -19,8 +19,7 @@ pub mod server; use std::error::Error; -// internal uses. -pub(crate) use server::*; +pub use server::*; // main usage stuff for plugin devs: pub use async_trait::async_trait; From a56075764d119c76b06baf45f973e3dfc8b84c8a Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 25 Nov 2025 01:30:17 +0300 Subject: [PATCH 04/22] a --- cmd/plugins/plugins.yaml | 42 +++++----- examples/plugins/php/composer.json | 2 +- examples/plugins/php/src/CircleCommand.php | 98 ++++++++++++++++++++++ 3 files changed, 120 insertions(+), 22 deletions(-) create mode 100644 examples/plugins/php/src/CircleCommand.php diff --git a/cmd/plugins/plugins.yaml b/cmd/plugins/plugins.yaml index 80eba06..f4b0c7f 100644 --- a/cmd/plugins/plugins.yaml +++ b/cmd/plugins/plugins.yaml @@ -19,16 +19,16 @@ plugins: # args: ["../examples/plugins/node/hello.js"] # env: # NODE_ENV: development - - id: example-typescript - name: Example TypeScript Plugin - #command: "node" - #args: ["../examples/plugins/typescript/dist/index.js"] - command: "npm" - args: ["run", "dev"] - work_dir: - path: "../examples/plugins/typescript" - env: - NODE_ENV: production + # - id: example-typescript + # name: Example TypeScript Plugin + # #command: "node" + # #args: ["../examples/plugins/typescript/dist/index.js"] + # command: "npm" + # args: ["run", "dev"] + # work_dir: + # path: "../examples/plugins/typescript" + # env: + # NODE_ENV: production - id: example-php name: Example PHP Plugin command: "../examples/plugins/php/bin/php7/bin/php" @@ -43,14 +43,14 @@ plugins: env: PHP_ENV: production - - id: example-go - name: Example Go Plugin - command: "go" - args: ["run", "cmd/main.go"] - work_dir: - git: - enabled: true - #persistent: true # persistent can be set to true if you don't - # want the plugin to be cloned at every startup - #version: tags/v0.0.1 # can also specify commit hashes - path: https://github.com/secmc/plugin-go + # - id: example-go + # name: Example Go Plugin + # command: "go" + # args: ["run", "cmd/main.go"] + # work_dir: + # git: + # enabled: true + # #persistent: true # persistent can be set to true if you don't + # # want the plugin to be cloned at every startup + # #version: tags/v0.0.1 # can also specify commit hashes + # path: https://github.com/secmc/plugin-go diff --git a/examples/plugins/php/composer.json b/examples/plugins/php/composer.json index 2033478..3b4bda5 100644 --- a/examples/plugins/php/composer.json +++ b/examples/plugins/php/composer.json @@ -4,7 +4,7 @@ "type": "project", "require": { "php": ">=8.1", - "dragonfly-plugins/plugin-sdk": "^0.0.5", + "dragonfly-plugins/plugin-sdk": "dev-main", "grpc/grpc": "1.74.0", "google/protobuf": "^3.25" }, diff --git a/examples/plugins/php/src/CircleCommand.php b/examples/plugins/php/src/CircleCommand.php new file mode 100644 index 0000000..2876dc8 --- /dev/null +++ b/examples/plugins/php/src/CircleCommand.php @@ -0,0 +1,98 @@ + */ + public Optional $particle; + + public function execute(CommandSender $sender, EventContext $ctx): void { + if (!$sender instanceof Player) { + $sender->sendMessage("§cThis command can only be run by a player."); + return; + } + + if ($this->particle->hasValue()) { + $particleName = $this->particle->get(); + $particleId = $this->resolveParticleId($particleName); + if ($particleId === null) { + $sender->sendMessage("§cUnknown particle: {$particleName}"); + return; + } + } else { + $particleId = ParticleType::PARTICLE_FLAME; + $particleName = 'flame'; + } + + $world = $sender->getWorld(); + if ($world === null) { + $sender->sendMessage("§cCannot determine your world."); + return; + } + + $correlationId = uniqid('circle_', true); + $ctx->onActionResult($correlationId, function (ActionResult $result) use ($ctx, $world, $particleId) { + $playersResult = $result->getWorldPlayers(); + if ($playersResult === null) { + return; + } + + $radius = 3.0; + $points = 16; + + foreach ($playersResult->getPlayers() as $player) { + $pos = $player->getPosition(); + if ($pos === null) { + continue; + } + + $cx = $pos->getX(); + $cy = $pos->getY(); + $cz = $pos->getZ(); + + for ($i = 0; $i < $points; $i++) { + $angle = (2 * M_PI / $points) * $i; + $x = $cx + $radius * cos($angle); + $z = $cz + $radius * sin($angle); + + $particlePos = new Vec3(); + $particlePos->setX($x); + $particlePos->setY($cy + 0.5); + $particlePos->setZ($z); + + $ctx->worldAddParticle($world, $particlePos, $particleId); + } + } + }); + + $ctx->worldQueryPlayers($world, $correlationId); + $sender->sendMessage("§aSpawning {$particleName} circles around all players!"); + } + + /** + * @return array}> + */ + public function serializeParamSpec(): array { + $names = EnumResolver::lowerNames(ParticleType::class, ['PARTICLE_TYPE_UNSPECIFIED']); + return $this->withEnum(parent::serializeParamSpec(), 'particle', $names); + } + + private function resolveParticleId(string $input): ?int { + return EnumResolver::value(ParticleType::class, $input, 'PARTICLE_'); + } +} + From 30f9e4d5ab3525f565881c9160a1110f5cb50f23 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 25 Nov 2025 01:37:05 +0300 Subject: [PATCH 05/22] fix: socket address --- packages/rust/rust-plugin-test/src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/rust/rust-plugin-test/src/main.rs b/packages/rust/rust-plugin-test/src/main.rs index 42e8a12..ce659e3 100644 --- a/packages/rust/rust-plugin-test/src/main.rs +++ b/packages/rust/rust-plugin-test/src/main.rs @@ -82,8 +82,8 @@ impl PluginSubscriptions for MyExamplePlugin { async fn main() -> Result<(), Box> { // 1. Define the plugin's metadata let plugin = Plugin::new( - "example-plugin", // A unique ID for your plugin - "My Example Plugin", // A human-readable name + "example-rust", // A unique ID for your plugin (matches plugins.yaml) + "Example Rust Plugin", // A human-readable name "1.0.0", // Your plugin's version "1.0.0", // The API version you're built against ); @@ -93,8 +93,8 @@ async fn main() -> Result<(), Box> { plugin .run( - MyExamplePlugin, // Pass in an instance of our handler - "http://[::1]:50051", // The server address + MyExamplePlugin, // Pass in an instance of our handler + "unix:///tmp/dragonfly_plugin.sock", // The server address (Unix socket) ) .await } From 9e100d490274c4b06ec664084c7bd6ba813be782 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Tue, 25 Nov 2025 02:06:37 +0300 Subject: [PATCH 06/22] fix: connection logic --- packages/rust/Cargo.toml | 4 +- packages/rust/src/lib.rs | 80 ++++++++++++++++++++++++++++++---------- 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/packages/rust/Cargo.toml b/packages/rust/Cargo.toml index 5bdb9a9..03bdbb2 100644 --- a/packages/rust/Cargo.toml +++ b/packages/rust/Cargo.toml @@ -17,6 +17,8 @@ path = "src/lib.rs" async-trait = "0.1.89" prettyplease = "0.2.37" prost = "0.13" -tokio = "1.48.0" +tokio = { version = "1.48.0", features = ["net"] } tokio-stream = "0.1.17" tonic = { version = "0.12", features = ["transport"] } +tower = "0.5" +hyper-util = { version = "0.1", features = ["tokio"] } diff --git a/packages/rust/src/lib.rs b/packages/rust/src/lib.rs index 8ac092b..7d0fb1c 100644 --- a/packages/rust/src/lib.rs +++ b/packages/rust/src/lib.rs @@ -26,8 +26,53 @@ pub use async_trait::async_trait; pub use event::PluginEventHandler; use tokio::sync::mpsc; use tokio_stream::{wrappers::ReceiverStream, StreamExt}; +use tower::service_fn; // TODO: pub use rust_plugin_macro::bedrock_plugin; +#[cfg(unix)] +use tokio::net::UnixStream; +#[cfg(unix)] +use hyper_util::rt::TokioIo; + +/// Helper function to connect to the server, supporting both Unix sockets and TCP. +async fn connect_to_server(addr: &str) -> Result, Box> { + // Check if it's a Unix socket address (starts with "unix:" or is a path starting with "/") + if addr.starts_with("unix:") || addr.starts_with('/') { + #[cfg(unix)] + { + // Extract the path and convert to owned String for the closure + let path: String = if addr.starts_with("unix://") { + addr[7..].to_string() + } else if addr.starts_with("unix:") { + addr[5..].to_string() + } else { + addr.to_string() + }; + + // Create a lazy channel that uses Unix sockets. + // Lazy is required so the hello message gets sent as part of stream + // establishment, avoiding a deadlock with the Go server which waits + // for the hello before sending response headers. + let channel = tonic::transport::Endpoint::try_from("http://[::1]:50051")? + .connect_with_connector_lazy(service_fn(move |_: tonic::transport::Uri| { + let path = path.clone(); + async move { + let stream = UnixStream::connect(&path).await?; + Ok::<_, std::io::Error>(TokioIo::new(stream)) + } + })); + Ok(types::PluginClient::new(channel)) + } + #[cfg(not(unix))] + { + Err("Unix sockets are not supported on this platform".into()) + } + } else { + // Regular TCP connection + Ok(types::PluginClient::connect(addr.to_string()).await?) + } +} + pub struct Plugin { id: String, name: String, @@ -46,31 +91,18 @@ impl Plugin { } /// Runs the plugin, connecting to the server and starting the event loop. - pub async fn run( + pub async fn run( self, handler: impl PluginEventHandler + PluginSubscriptions + 'static, - addr: A, - ) -> Result<(), Box> - where - // Yeah this was AI, but holy hell is it good at doing dynamic type stuff. - A: TryInto, - A::Error: Into>, - { - let mut raw_client = types::PluginClient::connect(addr) - .await - .map_err(|e| Box::new(e) as Box)?; + addr: &str, + ) -> Result<(), Box> { + let mut raw_client = connect_to_server(addr).await?; let (tx, rx) = mpsc::channel(128); - let request_stream = ReceiverStream::new(rx); - - let mut event_stream = raw_client.event_stream(request_stream).await?.into_inner(); - - let server = Server { - plugin_id: self.id.clone(), - sender: tx.clone(), - }; - + // Pre-buffer the hello message so it's sent immediately when stream opens. + // This is required because the Go server blocks on Recv() waiting for the + // hello before sending response headers. let hello_msg = types::PluginToHost { plugin_id: self.id.clone(), payload: Some(types::PluginPayload::Hello(types::PluginHello { @@ -83,6 +115,14 @@ impl Plugin { }; tx.send(hello_msg).await?; + let request_stream = ReceiverStream::new(rx); + let mut event_stream = raw_client.event_stream(request_stream).await?.into_inner(); + + let server = Server { + plugin_id: self.id.clone(), + sender: tx.clone(), + }; + let events = handler.get_subscriptions(); if !events.is_empty() { println!("Subscribing to {} event types...", events.len()); From b8791a0833bfc4dc9c4127ce3f15f6df68092d07 Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Mon, 24 Nov 2025 18:34:20 -0500 Subject: [PATCH 07/22] merge --- cmd/plugins/plugins.yaml | 56 --- packages/rust/Cargo.toml | 4 +- packages/rust/rust-plugin-test/src/main.rs | 10 +- packages/rust/src/event/handler.rs | 1 + packages/rust/src/event/mutations.rs | 3 +- packages/rust/src/lib.rs | 81 +++- packages/rust/src/server/helpers.rs | 385 ++++++++---------- packages/rust/xtask/src/generate_actions.rs | 2 +- packages/rust/xtask/src/generate_handlers.rs | 2 +- packages/rust/xtask/src/generate_mutations.rs | 2 +- 10 files changed, 254 insertions(+), 292 deletions(-) delete mode 100644 cmd/plugins/plugins.yaml diff --git a/cmd/plugins/plugins.yaml b/cmd/plugins/plugins.yaml deleted file mode 100644 index 80eba06..0000000 --- a/cmd/plugins/plugins.yaml +++ /dev/null @@ -1,56 +0,0 @@ -# Plugin Server Configuration -# Dragonfly runs a gRPC server that plugins connect to -# Use Unix socket for best performance -server_port: "unix:///tmp/dragonfly_plugin.sock" -# Or use TCP for remote: "127.0.0.1:50050" - -# List of plugin IDs that must connect before server starts -# This ensures custom items are registered before the resource pack is built -required_plugins: - - example-php - -# Maximum time to wait for required plugins to connect (milliseconds) -hello_timeout_ms: 5000 - -plugins: - # - id: example-node - # name: Example Node Plugin - # command: "node" - # args: ["../examples/plugins/node/hello.js"] - # env: - # NODE_ENV: development - - id: example-typescript - name: Example TypeScript Plugin - #command: "node" - #args: ["../examples/plugins/typescript/dist/index.js"] - command: "npm" - args: ["run", "dev"] - work_dir: - path: "../examples/plugins/typescript" - env: - NODE_ENV: production - - id: example-php - name: Example PHP Plugin - command: "../examples/plugins/php/bin/php7/bin/php" - args: ["../examples/plugins/php/src/HelloPlugin.php"] - # command: "docker" - # args: - # - "compose" - # - "-f" - # - "../examples/plugins/php/docker-compose.yml" - # - "up" - # - "--abort-on-container-exit" - env: - PHP_ENV: production - - - id: example-go - name: Example Go Plugin - command: "go" - args: ["run", "cmd/main.go"] - work_dir: - git: - enabled: true - #persistent: true # persistent can be set to true if you don't - # want the plugin to be cloned at every startup - #version: tags/v0.0.1 # can also specify commit hashes - path: https://github.com/secmc/plugin-go diff --git a/packages/rust/Cargo.toml b/packages/rust/Cargo.toml index 5bdb9a9..03bdbb2 100644 --- a/packages/rust/Cargo.toml +++ b/packages/rust/Cargo.toml @@ -17,6 +17,8 @@ path = "src/lib.rs" async-trait = "0.1.89" prettyplease = "0.2.37" prost = "0.13" -tokio = "1.48.0" +tokio = { version = "1.48.0", features = ["net"] } tokio-stream = "0.1.17" tonic = { version = "0.12", features = ["transport"] } +tower = "0.5" +hyper-util = { version = "0.1", features = ["tokio"] } diff --git a/packages/rust/rust-plugin-test/src/main.rs b/packages/rust/rust-plugin-test/src/main.rs index ce659e3..928da6c 100644 --- a/packages/rust/rust-plugin-test/src/main.rs +++ b/packages/rust/rust-plugin-test/src/main.rs @@ -82,10 +82,10 @@ impl PluginSubscriptions for MyExamplePlugin { async fn main() -> Result<(), Box> { // 1. Define the plugin's metadata let plugin = Plugin::new( - "example-rust", // A unique ID for your plugin (matches plugins.yaml) + "example-rust", // A unique ID for your plugin (matches plugins.yaml) "Example Rust Plugin", // A human-readable name - "1.0.0", // Your plugin's version - "1.0.0", // The API version you're built against + "1.0.0", // Your plugin's version + "1.0.0", // The API version you're built against ); // 2. Connect to the server and run the plugin @@ -93,8 +93,8 @@ async fn main() -> Result<(), Box> { plugin .run( - MyExamplePlugin, // Pass in an instance of our handler - "unix:///tmp/dragonfly_plugin.sock", // The server address (Unix socket) + MyExamplePlugin, // Pass in an instance of our handler + "tcp://127.0.0.1:50050", // The server address (Unix socket) ) .await } diff --git a/packages/rust/src/event/handler.rs b/packages/rust/src/event/handler.rs index d54b577..f3c7411 100644 --- a/packages/rust/src/event/handler.rs +++ b/packages/rust/src/event/handler.rs @@ -1,3 +1,4 @@ +//! This file is auto-generated by `xtask`. Do not edit manually. #![allow(unused_variables)] use crate::{event::EventContext, types, PluginSubscriptions, Server}; use async_trait::async_trait; diff --git a/packages/rust/src/event/mutations.rs b/packages/rust/src/event/mutations.rs index 13ac041..5004978 100644 --- a/packages/rust/src/event/mutations.rs +++ b/packages/rust/src/event/mutations.rs @@ -1,5 +1,6 @@ -use crate::types; +//! This file is auto-generated by `xtask`. Do not edit manually. use crate::event::EventContext; +use crate::types; impl<'a> EventContext<'a, types::ChatEvent> { ///Sets the `message` for this event. pub fn set_message(&mut self, message: String) { diff --git a/packages/rust/src/lib.rs b/packages/rust/src/lib.rs index 8ac092b..33a6764 100644 --- a/packages/rust/src/lib.rs +++ b/packages/rust/src/lib.rs @@ -28,6 +28,52 @@ use tokio::sync::mpsc; use tokio_stream::{wrappers::ReceiverStream, StreamExt}; // TODO: pub use rust_plugin_macro::bedrock_plugin; +#[cfg(unix)] +use hyper_util::rt::TokioIo; +#[cfg(unix)] +use tokio::net::UnixStream; + +/// Helper function to connect to the server, supporting both Unix sockets and TCP. +async fn connect_to_server( + addr: &str, +) -> Result, Box> { + // Check if it's a Unix socket address (starts with "unix:" or is a path starting with "/") + if addr.starts_with("unix:") || addr.starts_with('/') { + #[cfg(unix)] + { + // Extract the path and convert to owned String for the closure + let path: String = if addr.starts_with("unix://") { + addr[7..].to_string() + } else if addr.starts_with("unix:") { + addr[5..].to_string() + } else { + addr.to_string() + }; + + // Create a lazy channel that uses Unix sockets. + // Lazy is required so the hello message gets sent as part of stream + // establishment, avoiding a deadlock with the Go server which waits + // for the hello before sending response headers. + let channel = tonic::transport::Endpoint::try_from("http://[::1]:50051")? + .connect_with_connector_lazy(service_fn(move |_: tonic::transport::Uri| { + let path = path.clone(); + async move { + let stream = UnixStream::connect(&path).await?; + Ok::<_, std::io::Error>(TokioIo::new(stream)) + } + })); + Ok(types::PluginClient::new(channel)) + } + #[cfg(not(unix))] + { + Err("Unix sockets are not supported on this platform".into()) + } + } else { + // Regular TCP connection + Ok(types::PluginClient::connect(addr.to_string()).await?) + } +} + pub struct Plugin { id: String, name: String, @@ -46,31 +92,18 @@ impl Plugin { } /// Runs the plugin, connecting to the server and starting the event loop. - pub async fn run( + pub async fn run( self, handler: impl PluginEventHandler + PluginSubscriptions + 'static, - addr: A, - ) -> Result<(), Box> - where - // Yeah this was AI, but holy hell is it good at doing dynamic type stuff. - A: TryInto, - A::Error: Into>, - { - let mut raw_client = types::PluginClient::connect(addr) - .await - .map_err(|e| Box::new(e) as Box)?; + addr: &str, + ) -> Result<(), Box> { + let mut raw_client = connect_to_server(addr).await?; let (tx, rx) = mpsc::channel(128); - let request_stream = ReceiverStream::new(rx); - - let mut event_stream = raw_client.event_stream(request_stream).await?.into_inner(); - - let server = Server { - plugin_id: self.id.clone(), - sender: tx.clone(), - }; - + // Pre-buffer the hello message so it's sent immediately when stream opens. + // This is required because the Go server blocks on Recv() waiting for the + // hello before sending response headers. let hello_msg = types::PluginToHost { plugin_id: self.id.clone(), payload: Some(types::PluginPayload::Hello(types::PluginHello { @@ -83,6 +116,14 @@ impl Plugin { }; tx.send(hello_msg).await?; + let request_stream = ReceiverStream::new(rx); + let mut event_stream = raw_client.event_stream(request_stream).await?.into_inner(); + + let server = Server { + plugin_id: self.id.clone(), + sender: tx.clone(), + }; + let events = handler.get_subscriptions(); if !events.is_empty() { println!("Subscribing to {} event types...", events.len()); diff --git a/packages/rust/src/server/helpers.rs b/packages/rust/src/server/helpers.rs index e8652d5..07aa162 100644 --- a/packages/rust/src/server/helpers.rs +++ b/packages/rust/src/server/helpers.rs @@ -1,3 +1,4 @@ +//! This file is auto-generated by `xtask`. Do not edit manually. use crate::{types, Server}; use tokio::sync::mpsc; impl Server { @@ -7,13 +8,11 @@ impl Server { target_uuid: String, message: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SendChat(types::SendChatAction { - target_uuid: target_uuid.into(), - message: message.into(), - }), - ) - .await + self.send_action(types::action::Kind::SendChat(types::SendChatAction { + target_uuid: target_uuid.into(), + message: message.into(), + })) + .await } ///Sends a `Teleport` action to the server. pub async fn teleport( @@ -22,14 +21,12 @@ impl Server { position: Option, rotation: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::Teleport(types::TeleportAction { - player_uuid: player_uuid.into(), - position: position.map(|v| v.into()), - rotation: rotation.map(|v| v.into()), - }), - ) - .await + self.send_action(types::action::Kind::Teleport(types::TeleportAction { + player_uuid: player_uuid.into(), + position: position.map(|v| v.into()), + rotation: rotation.map(|v| v.into()), + })) + .await } ///Sends a `Kick` action to the server. pub async fn kick( @@ -37,13 +34,11 @@ impl Server { player_uuid: String, reason: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::Kick(types::KickAction { - player_uuid: player_uuid.into(), - reason: reason.into(), - }), - ) - .await + self.send_action(types::action::Kind::Kick(types::KickAction { + player_uuid: player_uuid.into(), + reason: reason.into(), + })) + .await } ///Sends a `SetGameMode` action to the server. pub async fn set_game_mode( @@ -51,13 +46,11 @@ impl Server { player_uuid: String, game_mode: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetGameMode(types::SetGameModeAction { - player_uuid: player_uuid.into(), - game_mode: game_mode.into(), - }), - ) - .await + self.send_action(types::action::Kind::SetGameMode(types::SetGameModeAction { + player_uuid: player_uuid.into(), + game_mode: game_mode.into(), + })) + .await } ///Sends a `GiveItem` action to the server. pub async fn give_item( @@ -65,25 +58,23 @@ impl Server { player_uuid: String, item: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::GiveItem(types::GiveItemAction { - player_uuid: player_uuid.into(), - item: item.map(|v| v.into()), - }), - ) - .await + self.send_action(types::action::Kind::GiveItem(types::GiveItemAction { + player_uuid: player_uuid.into(), + item: item.map(|v| v.into()), + })) + .await } ///Sends a `ClearInventory` action to the server. pub async fn clear_inventory( &self, player_uuid: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::ClearInventory(types::ClearInventoryAction { - player_uuid: player_uuid.into(), - }), - ) - .await + self.send_action(types::action::Kind::ClearInventory( + types::ClearInventoryAction { + player_uuid: player_uuid.into(), + }, + )) + .await } ///Sends a `SetHeldItem` action to the server. pub async fn set_held_item( @@ -92,14 +83,12 @@ impl Server { main: Option, offhand: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetHeldItem(types::SetHeldItemAction { - player_uuid: player_uuid.into(), - main: main.map(|v| v.into()), - offhand: offhand.map(|v| v.into()), - }), - ) - .await + self.send_action(types::action::Kind::SetHeldItem(types::SetHeldItemAction { + player_uuid: player_uuid.into(), + main: main.map(|v| v.into()), + offhand: offhand.map(|v| v.into()), + })) + .await } ///Sends a `SetHealth` action to the server. pub async fn set_health( @@ -108,14 +97,12 @@ impl Server { health: f64, max_health: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetHealth(types::SetHealthAction { - player_uuid: player_uuid.into(), - health: health.into(), - max_health: max_health.map(|v| v.into()), - }), - ) - .await + self.send_action(types::action::Kind::SetHealth(types::SetHealthAction { + player_uuid: player_uuid.into(), + health: health.into(), + max_health: max_health.map(|v| v.into()), + })) + .await } ///Sends a `SetFood` action to the server. pub async fn set_food( @@ -123,13 +110,11 @@ impl Server { player_uuid: String, food: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetFood(types::SetFoodAction { - player_uuid: player_uuid.into(), - food: food.into(), - }), - ) - .await + self.send_action(types::action::Kind::SetFood(types::SetFoodAction { + player_uuid: player_uuid.into(), + food: food.into(), + })) + .await } ///Sends a `SetExperience` action to the server. pub async fn set_experience( @@ -139,15 +124,15 @@ impl Server { progress: Option, amount: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetExperience(types::SetExperienceAction { - player_uuid: player_uuid.into(), - level: level.map(|v| v.into()), - progress: progress.map(|v| v.into()), - amount: amount.map(|v| v.into()), - }), - ) - .await + self.send_action(types::action::Kind::SetExperience( + types::SetExperienceAction { + player_uuid: player_uuid.into(), + level: level.map(|v| v.into()), + progress: progress.map(|v| v.into()), + amount: amount.map(|v| v.into()), + }, + )) + .await } ///Sends a `SetVelocity` action to the server. pub async fn set_velocity( @@ -155,13 +140,11 @@ impl Server { player_uuid: String, velocity: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetVelocity(types::SetVelocityAction { - player_uuid: player_uuid.into(), - velocity: velocity.map(|v| v.into()), - }), - ) - .await + self.send_action(types::action::Kind::SetVelocity(types::SetVelocityAction { + player_uuid: player_uuid.into(), + velocity: velocity.map(|v| v.into()), + })) + .await } ///Sends a `AddEffect` action to the server. pub async fn add_effect( @@ -172,16 +155,14 @@ impl Server { duration_ms: i64, show_particles: bool, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::AddEffect(types::AddEffectAction { - player_uuid: player_uuid.into(), - effect_type: effect_type.into(), - level: level.into(), - duration_ms: duration_ms.into(), - show_particles: show_particles.into(), - }), - ) - .await + self.send_action(types::action::Kind::AddEffect(types::AddEffectAction { + player_uuid: player_uuid.into(), + effect_type: effect_type.into(), + level: level.into(), + duration_ms: duration_ms.into(), + show_particles: show_particles.into(), + })) + .await } ///Sends a `RemoveEffect` action to the server. pub async fn remove_effect( @@ -189,13 +170,13 @@ impl Server { player_uuid: String, effect_type: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::RemoveEffect(types::RemoveEffectAction { - player_uuid: player_uuid.into(), - effect_type: effect_type.into(), - }), - ) - .await + self.send_action(types::action::Kind::RemoveEffect( + types::RemoveEffectAction { + player_uuid: player_uuid.into(), + effect_type: effect_type.into(), + }, + )) + .await } ///Sends a `SendTitle` action to the server. pub async fn send_title( @@ -207,17 +188,15 @@ impl Server { duration_ms: Option, fade_out_ms: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SendTitle(types::SendTitleAction { - player_uuid: player_uuid.into(), - title: title.into(), - subtitle: subtitle.map(|v| v.into()), - fade_in_ms: fade_in_ms.map(|v| v.into()), - duration_ms: duration_ms.map(|v| v.into()), - fade_out_ms: fade_out_ms.map(|v| v.into()), - }), - ) - .await + self.send_action(types::action::Kind::SendTitle(types::SendTitleAction { + player_uuid: player_uuid.into(), + title: title.into(), + subtitle: subtitle.map(|v| v.into()), + fade_in_ms: fade_in_ms.map(|v| v.into()), + duration_ms: duration_ms.map(|v| v.into()), + fade_out_ms: fade_out_ms.map(|v| v.into()), + })) + .await } ///Sends a `SendPopup` action to the server. pub async fn send_popup( @@ -225,13 +204,11 @@ impl Server { player_uuid: String, message: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SendPopup(types::SendPopupAction { - player_uuid: player_uuid.into(), - message: message.into(), - }), - ) - .await + self.send_action(types::action::Kind::SendPopup(types::SendPopupAction { + player_uuid: player_uuid.into(), + message: message.into(), + })) + .await } ///Sends a `SendTip` action to the server. pub async fn send_tip( @@ -239,13 +216,11 @@ impl Server { player_uuid: String, message: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SendTip(types::SendTipAction { - player_uuid: player_uuid.into(), - message: message.into(), - }), - ) - .await + self.send_action(types::action::Kind::SendTip(types::SendTipAction { + player_uuid: player_uuid.into(), + message: message.into(), + })) + .await } ///Sends a `PlaySound` action to the server. pub async fn play_sound( @@ -256,16 +231,14 @@ impl Server { volume: Option, pitch: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::PlaySound(types::PlaySoundAction { - player_uuid: player_uuid.into(), - sound: sound.into(), - position: position.map(|v| v.into()), - volume: volume.map(|v| v.into()), - pitch: pitch.map(|v| v.into()), - }), - ) - .await + self.send_action(types::action::Kind::PlaySound(types::PlaySoundAction { + player_uuid: player_uuid.into(), + sound: sound.into(), + position: position.map(|v| v.into()), + volume: volume.map(|v| v.into()), + pitch: pitch.map(|v| v.into()), + })) + .await } ///Sends a `ExecuteCommand` action to the server. pub async fn execute_command( @@ -273,13 +246,13 @@ impl Server { player_uuid: String, command: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::ExecuteCommand(types::ExecuteCommandAction { - player_uuid: player_uuid.into(), - command: command.into(), - }), - ) - .await + self.send_action(types::action::Kind::ExecuteCommand( + types::ExecuteCommandAction { + player_uuid: player_uuid.into(), + command: command.into(), + }, + )) + .await } ///Sends a `WorldSetDefaultGameMode` action to the server. pub async fn world_set_default_game_mode( @@ -287,13 +260,13 @@ impl Server { world: Option, game_mode: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldSetDefaultGameMode(types::WorldSetDefaultGameModeAction { - world: world.map(|v| v.into()), - game_mode: game_mode.into(), - }), - ) - .await + self.send_action(types::action::Kind::WorldSetDefaultGameMode( + types::WorldSetDefaultGameModeAction { + world: world.map(|v| v.into()), + game_mode: game_mode.into(), + }, + )) + .await } ///Sends a `WorldSetDifficulty` action to the server. pub async fn world_set_difficulty( @@ -301,13 +274,13 @@ impl Server { world: Option, difficulty: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldSetDifficulty(types::WorldSetDifficultyAction { - world: world.map(|v| v.into()), - difficulty: difficulty.into(), - }), - ) - .await + self.send_action(types::action::Kind::WorldSetDifficulty( + types::WorldSetDifficultyAction { + world: world.map(|v| v.into()), + difficulty: difficulty.into(), + }, + )) + .await } ///Sends a `WorldSetTickRange` action to the server. pub async fn world_set_tick_range( @@ -315,13 +288,13 @@ impl Server { world: Option, tick_range: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldSetTickRange(types::WorldSetTickRangeAction { - world: world.map(|v| v.into()), - tick_range: tick_range.into(), - }), - ) - .await + self.send_action(types::action::Kind::WorldSetTickRange( + types::WorldSetTickRangeAction { + world: world.map(|v| v.into()), + tick_range: tick_range.into(), + }, + )) + .await } ///Sends a `WorldSetBlock` action to the server. pub async fn world_set_block( @@ -330,14 +303,14 @@ impl Server { position: Option, block: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldSetBlock(types::WorldSetBlockAction { - world: world.map(|v| v.into()), - position: position.map(|v| v.into()), - block: block.map(|v| v.into()), - }), - ) - .await + self.send_action(types::action::Kind::WorldSetBlock( + types::WorldSetBlockAction { + world: world.map(|v| v.into()), + position: position.map(|v| v.into()), + block: block.map(|v| v.into()), + }, + )) + .await } ///Sends a `WorldPlaySound` action to the server. pub async fn world_play_sound( @@ -346,14 +319,14 @@ impl Server { sound: i32, position: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldPlaySound(types::WorldPlaySoundAction { - world: world.map(|v| v.into()), - sound: sound.into(), - position: position.map(|v| v.into()), - }), - ) - .await + self.send_action(types::action::Kind::WorldPlaySound( + types::WorldPlaySoundAction { + world: world.map(|v| v.into()), + sound: sound.into(), + position: position.map(|v| v.into()), + }, + )) + .await } ///Sends a `WorldAddParticle` action to the server. pub async fn world_add_particle( @@ -364,40 +337,40 @@ impl Server { block: Option, face: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldAddParticle(types::WorldAddParticleAction { - world: world.map(|v| v.into()), - position: position.map(|v| v.into()), - particle: particle.into(), - block: block.map(|v| v.into()), - face: face.map(|v| v.into()), - }), - ) - .await + self.send_action(types::action::Kind::WorldAddParticle( + types::WorldAddParticleAction { + world: world.map(|v| v.into()), + position: position.map(|v| v.into()), + particle: particle.into(), + block: block.map(|v| v.into()), + face: face.map(|v| v.into()), + }, + )) + .await } ///Sends a `WorldQueryEntities` action to the server. pub async fn world_query_entities( &self, world: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldQueryEntities(types::WorldQueryEntitiesAction { - world: world.map(|v| v.into()), - }), - ) - .await + self.send_action(types::action::Kind::WorldQueryEntities( + types::WorldQueryEntitiesAction { + world: world.map(|v| v.into()), + }, + )) + .await } ///Sends a `WorldQueryPlayers` action to the server. pub async fn world_query_players( &self, world: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldQueryPlayers(types::WorldQueryPlayersAction { - world: world.map(|v| v.into()), - }), - ) - .await + self.send_action(types::action::Kind::WorldQueryPlayers( + types::WorldQueryPlayersAction { + world: world.map(|v| v.into()), + }, + )) + .await } ///Sends a `WorldQueryEntitiesWithin` action to the server. pub async fn world_query_entities_within( @@ -405,12 +378,12 @@ impl Server { world: Option, r#box: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldQueryEntitiesWithin(types::WorldQueryEntitiesWithinAction { - world: world.map(|v| v.into()), - r#box: r#box.map(|v| v.into()), - }), - ) - .await + self.send_action(types::action::Kind::WorldQueryEntitiesWithin( + types::WorldQueryEntitiesWithinAction { + world: world.map(|v| v.into()), + r#box: r#box.map(|v| v.into()), + }, + )) + .await } } diff --git a/packages/rust/xtask/src/generate_actions.rs b/packages/rust/xtask/src/generate_actions.rs index 4017e55..6258c82 100644 --- a/packages/rust/xtask/src/generate_actions.rs +++ b/packages/rust/xtask/src/generate_actions.rs @@ -63,7 +63,7 @@ pub(crate) fn generate_server_helpers( } let final_file = quote! { - // This file is auto-generated by `xtask`. Do not edit manually. + //! This file is auto-generated by `xtask`. Do not edit manually. use crate::{types, Server}; use tokio::sync::mpsc; diff --git a/packages/rust/xtask/src/generate_handlers.rs b/packages/rust/xtask/src/generate_handlers.rs index ca79c17..425a09c 100644 --- a/packages/rust/xtask/src/generate_handlers.rs +++ b/packages/rust/xtask/src/generate_handlers.rs @@ -43,7 +43,7 @@ pub fn generate_handler_trait(ast: &File, output_path: &PathBuf) -> Result<()> { } let file = quote! { - // This file is auto-generated by `xtask`. Do not edit manually. + //! This file is auto-generated by `xtask`. Do not edit manually. #![allow(unused_variables)] use crate::{event::EventContext, types, Server, PluginSubscriptions}; use async_trait::async_trait; diff --git a/packages/rust/xtask/src/generate_mutations.rs b/packages/rust/xtask/src/generate_mutations.rs index 6abaed7..1c9f41d 100644 --- a/packages/rust/xtask/src/generate_mutations.rs +++ b/packages/rust/xtask/src/generate_mutations.rs @@ -72,7 +72,7 @@ pub(crate) fn generate_event_mutations( } let final_file = quote! { - // This file is auto-generated by `xtask`. Do not edit manually. + //! This file is auto-generated by `xtask`. Do not edit manually. use crate::types; use crate::event::EventContext; From aa65c739b3bbe62a9dc54830cec1f349b943bd4b Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Mon, 24 Nov 2025 19:02:55 -0500 Subject: [PATCH 08/22] proc macro made --- packages/rust/rust-plugin-macro/Cargo.toml | 6 ++ packages/rust/rust-plugin-macro/src/lib.rs | 65 ++++++++++++++++++++++ packages/rust/rust-plugin-test/Cargo.toml | 1 + packages/rust/rust-plugin-test/src/main.rs | 9 +-- 4 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 packages/rust/rust-plugin-macro/src/lib.rs diff --git a/packages/rust/rust-plugin-macro/Cargo.toml b/packages/rust/rust-plugin-macro/Cargo.toml index 958b5b7..4cce8e4 100644 --- a/packages/rust/rust-plugin-macro/Cargo.toml +++ b/packages/rust/rust-plugin-macro/Cargo.toml @@ -3,4 +3,10 @@ name = "rust-plugin-macro" version = "0.1.0" edition = "2024" +[lib] +proc-macro = true + [dependencies] +quote = "1.0" +syn = { version = "2.0", features = ["full", "parsing", "visit"] } +heck = "0.5" diff --git a/packages/rust/rust-plugin-macro/src/lib.rs b/packages/rust/rust-plugin-macro/src/lib.rs new file mode 100644 index 0000000..e2d996f --- /dev/null +++ b/packages/rust/rust-plugin-macro/src/lib.rs @@ -0,0 +1,65 @@ +use heck::ToPascalCase; +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{ImplItem, ItemImpl, Type, parse_macro_input}; + +#[proc_macro_attribute] +pub fn bedrock_plugin(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemImpl); + let original_impl = input.clone(); + + let self_ty: &Type = &input.self_ty; + + let trait_path = match &input.trait_ { + Some((_, path, _)) => path, + None => { + let msg = "The #[bedrock_plugin] attribute can only be used on an `impl PluginEventHandler for ...` block."; + return syn::Error::new_spanned(&input.self_ty, msg) + .to_compile_error() + .into(); + } + }; + + if trait_path + .segments + .last() + .is_some_and(|s| s.ident != "PluginEventHandler") + { + let msg = "The #[bedrock_plugin] attribute must be on an `impl PluginEventHandler for ...` block."; + return syn::Error::new_spanned(trait_path, msg) + .to_compile_error() + .into(); + } + + let mut subscriptions = Vec::new(); + for item in &input.items { + if let ImplItem::Fn(method) = item { + let fn_name_str = method.sig.ident.to_string(); + + if let Some(event_name_snake) = fn_name_str.strip_prefix("on_") { + let event_name_pascal = event_name_snake.to_pascal_case(); + + let event_type_ident = format_ident!("{}", event_name_pascal); + + subscriptions.push(quote! { types::EventType::#event_type_ident }); + } + } + } + + let subscription_impl = quote! { + impl PluginSubscriptions for #self_ty { + fn get_subscriptions(&self) -> Vec { + vec![ + #( #subscriptions ),* + ] + } + } + }; + + let output = quote! { + #original_impl + #subscription_impl + }; + + output.into() +} diff --git a/packages/rust/rust-plugin-test/Cargo.toml b/packages/rust/rust-plugin-test/Cargo.toml index 44459d9..8bafe5a 100644 --- a/packages/rust/rust-plugin-test/Cargo.toml +++ b/packages/rust/rust-plugin-test/Cargo.toml @@ -5,4 +5,5 @@ edition = "2024" [dependencies] dragonfly-plugin = { path = "../"} +rust-plugin-macro = { path = "../rust-plugin-macro"} tokio = { version = "1", features = ["full"] } diff --git a/packages/rust/rust-plugin-test/src/main.rs b/packages/rust/rust-plugin-test/src/main.rs index 928da6c..44567a4 100644 --- a/packages/rust/rust-plugin-test/src/main.rs +++ b/packages/rust/rust-plugin-test/src/main.rs @@ -6,6 +6,7 @@ use dragonfly_plugin::{ event::{EventContext, PluginEventHandler}, types, // All the raw prost/tonic types }; +use rust_plugin_macro::bedrock_plugin; // --- 2. Define a struct for your plugin's state --- // It can be empty, or it can hold databases, configs, etc. @@ -22,6 +23,7 @@ struct MyExamplePlugin; // * We add `#[async_trait]` because the trait uses async functions. #[async_trait] +#[bedrock_plugin] impl PluginEventHandler for MyExamplePlugin { /// This handler runs when a player joins the server. /// We'll use it to send our "hello world" message. @@ -70,13 +72,6 @@ impl PluginEventHandler for MyExamplePlugin { // so the `#[bedrock_plugin]` macro will not subscribe to them. } -// THIS IS AN OPTIONAL THING USUALLY USE the proc macro. -impl PluginSubscriptions for MyExamplePlugin { - fn get_subscriptions(&self) -> Vec { - vec![types::EventType::Chat] - } -} - // --- 4. The main function to run the plugin --- #[tokio::main] async fn main() -> Result<(), Box> { From 1a7cf73361f53dca0bd7d5ae806f43a03f5928ef Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Mon, 24 Nov 2025 20:58:26 -0500 Subject: [PATCH 09/22] some small changes, and attempts to make LSP integration better --- packages/rust/Cargo.toml | 2 +- packages/rust/rust-plugin-macro/src/main.rs | 3 - packages/rust/src/event/handler.rs | 2 +- packages/rust/src/event/mutations.rs | 2 +- packages/rust/src/lib.rs | 2 +- packages/rust/src/server/helpers.rs | 384 +++++++++++--------- 6 files changed, 210 insertions(+), 185 deletions(-) delete mode 100644 packages/rust/rust-plugin-macro/src/main.rs diff --git a/packages/rust/Cargo.toml b/packages/rust/Cargo.toml index 03bdbb2..834e7fe 100644 --- a/packages/rust/Cargo.toml +++ b/packages/rust/Cargo.toml @@ -5,7 +5,7 @@ members = [".", "rust-plugin-macro", "rust-plugin-test", "xtask"] [package] name = "dragonfly-plugin" version = "0.1.0" -edition = "2021" +edition = "2024" license = "MIT OR Apache-2.0" repository = "https://github.com/secmc/dragonfly-plugins" description = "Dragonfly gRPC plugin SDK for Rust" diff --git a/packages/rust/rust-plugin-macro/src/main.rs b/packages/rust/rust-plugin-macro/src/main.rs deleted file mode 100644 index e7a11a9..0000000 --- a/packages/rust/rust-plugin-macro/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -} diff --git a/packages/rust/src/event/handler.rs b/packages/rust/src/event/handler.rs index f3c7411..f71c80b 100644 --- a/packages/rust/src/event/handler.rs +++ b/packages/rust/src/event/handler.rs @@ -1,6 +1,6 @@ //! This file is auto-generated by `xtask`. Do not edit manually. #![allow(unused_variables)] -use crate::{event::EventContext, types, PluginSubscriptions, Server}; +use crate::{PluginSubscriptions, Server, event::EventContext, types}; use async_trait::async_trait; #[async_trait] pub trait PluginEventHandler: PluginSubscriptions + Send + Sync { diff --git a/packages/rust/src/event/mutations.rs b/packages/rust/src/event/mutations.rs index 5004978..4485006 100644 --- a/packages/rust/src/event/mutations.rs +++ b/packages/rust/src/event/mutations.rs @@ -1,6 +1,6 @@ //! This file is auto-generated by `xtask`. Do not edit manually. -use crate::event::EventContext; use crate::types; +use crate::event::EventContext; impl<'a> EventContext<'a, types::ChatEvent> { ///Sets the `message` for this event. pub fn set_message(&mut self, message: String) { diff --git a/packages/rust/src/lib.rs b/packages/rust/src/lib.rs index 0a371e9..a077642 100644 --- a/packages/rust/src/lib.rs +++ b/packages/rust/src/lib.rs @@ -25,7 +25,7 @@ pub use server::*; pub use async_trait::async_trait; pub use event::PluginEventHandler; use tokio::sync::mpsc; -use tokio_stream::{wrappers::ReceiverStream, StreamExt}; +use tokio_stream::{StreamExt, wrappers::ReceiverStream}; // TODO: pub use rust_plugin_macro::bedrock_plugin; #[cfg(unix)] diff --git a/packages/rust/src/server/helpers.rs b/packages/rust/src/server/helpers.rs index 07aa162..47397b4 100644 --- a/packages/rust/src/server/helpers.rs +++ b/packages/rust/src/server/helpers.rs @@ -8,11 +8,13 @@ impl Server { target_uuid: String, message: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SendChat(types::SendChatAction { - target_uuid: target_uuid.into(), - message: message.into(), - })) - .await + self.send_action( + types::action::Kind::SendChat(types::SendChatAction { + target_uuid: target_uuid.into(), + message: message.into(), + }), + ) + .await } ///Sends a `Teleport` action to the server. pub async fn teleport( @@ -21,12 +23,14 @@ impl Server { position: Option, rotation: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::Teleport(types::TeleportAction { - player_uuid: player_uuid.into(), - position: position.map(|v| v.into()), - rotation: rotation.map(|v| v.into()), - })) - .await + self.send_action( + types::action::Kind::Teleport(types::TeleportAction { + player_uuid: player_uuid.into(), + position: position.map(|v| v.into()), + rotation: rotation.map(|v| v.into()), + }), + ) + .await } ///Sends a `Kick` action to the server. pub async fn kick( @@ -34,11 +38,13 @@ impl Server { player_uuid: String, reason: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::Kick(types::KickAction { - player_uuid: player_uuid.into(), - reason: reason.into(), - })) - .await + self.send_action( + types::action::Kind::Kick(types::KickAction { + player_uuid: player_uuid.into(), + reason: reason.into(), + }), + ) + .await } ///Sends a `SetGameMode` action to the server. pub async fn set_game_mode( @@ -46,11 +52,13 @@ impl Server { player_uuid: String, game_mode: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SetGameMode(types::SetGameModeAction { - player_uuid: player_uuid.into(), - game_mode: game_mode.into(), - })) - .await + self.send_action( + types::action::Kind::SetGameMode(types::SetGameModeAction { + player_uuid: player_uuid.into(), + game_mode: game_mode.into(), + }), + ) + .await } ///Sends a `GiveItem` action to the server. pub async fn give_item( @@ -58,23 +66,25 @@ impl Server { player_uuid: String, item: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::GiveItem(types::GiveItemAction { - player_uuid: player_uuid.into(), - item: item.map(|v| v.into()), - })) - .await + self.send_action( + types::action::Kind::GiveItem(types::GiveItemAction { + player_uuid: player_uuid.into(), + item: item.map(|v| v.into()), + }), + ) + .await } ///Sends a `ClearInventory` action to the server. pub async fn clear_inventory( &self, player_uuid: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::ClearInventory( - types::ClearInventoryAction { - player_uuid: player_uuid.into(), - }, - )) - .await + self.send_action( + types::action::Kind::ClearInventory(types::ClearInventoryAction { + player_uuid: player_uuid.into(), + }), + ) + .await } ///Sends a `SetHeldItem` action to the server. pub async fn set_held_item( @@ -83,12 +93,14 @@ impl Server { main: Option, offhand: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SetHeldItem(types::SetHeldItemAction { - player_uuid: player_uuid.into(), - main: main.map(|v| v.into()), - offhand: offhand.map(|v| v.into()), - })) - .await + self.send_action( + types::action::Kind::SetHeldItem(types::SetHeldItemAction { + player_uuid: player_uuid.into(), + main: main.map(|v| v.into()), + offhand: offhand.map(|v| v.into()), + }), + ) + .await } ///Sends a `SetHealth` action to the server. pub async fn set_health( @@ -97,12 +109,14 @@ impl Server { health: f64, max_health: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SetHealth(types::SetHealthAction { - player_uuid: player_uuid.into(), - health: health.into(), - max_health: max_health.map(|v| v.into()), - })) - .await + self.send_action( + types::action::Kind::SetHealth(types::SetHealthAction { + player_uuid: player_uuid.into(), + health: health.into(), + max_health: max_health.map(|v| v.into()), + }), + ) + .await } ///Sends a `SetFood` action to the server. pub async fn set_food( @@ -110,11 +124,13 @@ impl Server { player_uuid: String, food: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SetFood(types::SetFoodAction { - player_uuid: player_uuid.into(), - food: food.into(), - })) - .await + self.send_action( + types::action::Kind::SetFood(types::SetFoodAction { + player_uuid: player_uuid.into(), + food: food.into(), + }), + ) + .await } ///Sends a `SetExperience` action to the server. pub async fn set_experience( @@ -124,15 +140,15 @@ impl Server { progress: Option, amount: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SetExperience( - types::SetExperienceAction { - player_uuid: player_uuid.into(), - level: level.map(|v| v.into()), - progress: progress.map(|v| v.into()), - amount: amount.map(|v| v.into()), - }, - )) - .await + self.send_action( + types::action::Kind::SetExperience(types::SetExperienceAction { + player_uuid: player_uuid.into(), + level: level.map(|v| v.into()), + progress: progress.map(|v| v.into()), + amount: amount.map(|v| v.into()), + }), + ) + .await } ///Sends a `SetVelocity` action to the server. pub async fn set_velocity( @@ -140,11 +156,13 @@ impl Server { player_uuid: String, velocity: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SetVelocity(types::SetVelocityAction { - player_uuid: player_uuid.into(), - velocity: velocity.map(|v| v.into()), - })) - .await + self.send_action( + types::action::Kind::SetVelocity(types::SetVelocityAction { + player_uuid: player_uuid.into(), + velocity: velocity.map(|v| v.into()), + }), + ) + .await } ///Sends a `AddEffect` action to the server. pub async fn add_effect( @@ -155,14 +173,16 @@ impl Server { duration_ms: i64, show_particles: bool, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::AddEffect(types::AddEffectAction { - player_uuid: player_uuid.into(), - effect_type: effect_type.into(), - level: level.into(), - duration_ms: duration_ms.into(), - show_particles: show_particles.into(), - })) - .await + self.send_action( + types::action::Kind::AddEffect(types::AddEffectAction { + player_uuid: player_uuid.into(), + effect_type: effect_type.into(), + level: level.into(), + duration_ms: duration_ms.into(), + show_particles: show_particles.into(), + }), + ) + .await } ///Sends a `RemoveEffect` action to the server. pub async fn remove_effect( @@ -170,13 +190,13 @@ impl Server { player_uuid: String, effect_type: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::RemoveEffect( - types::RemoveEffectAction { - player_uuid: player_uuid.into(), - effect_type: effect_type.into(), - }, - )) - .await + self.send_action( + types::action::Kind::RemoveEffect(types::RemoveEffectAction { + player_uuid: player_uuid.into(), + effect_type: effect_type.into(), + }), + ) + .await } ///Sends a `SendTitle` action to the server. pub async fn send_title( @@ -188,15 +208,17 @@ impl Server { duration_ms: Option, fade_out_ms: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SendTitle(types::SendTitleAction { - player_uuid: player_uuid.into(), - title: title.into(), - subtitle: subtitle.map(|v| v.into()), - fade_in_ms: fade_in_ms.map(|v| v.into()), - duration_ms: duration_ms.map(|v| v.into()), - fade_out_ms: fade_out_ms.map(|v| v.into()), - })) - .await + self.send_action( + types::action::Kind::SendTitle(types::SendTitleAction { + player_uuid: player_uuid.into(), + title: title.into(), + subtitle: subtitle.map(|v| v.into()), + fade_in_ms: fade_in_ms.map(|v| v.into()), + duration_ms: duration_ms.map(|v| v.into()), + fade_out_ms: fade_out_ms.map(|v| v.into()), + }), + ) + .await } ///Sends a `SendPopup` action to the server. pub async fn send_popup( @@ -204,11 +226,13 @@ impl Server { player_uuid: String, message: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SendPopup(types::SendPopupAction { - player_uuid: player_uuid.into(), - message: message.into(), - })) - .await + self.send_action( + types::action::Kind::SendPopup(types::SendPopupAction { + player_uuid: player_uuid.into(), + message: message.into(), + }), + ) + .await } ///Sends a `SendTip` action to the server. pub async fn send_tip( @@ -216,11 +240,13 @@ impl Server { player_uuid: String, message: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SendTip(types::SendTipAction { - player_uuid: player_uuid.into(), - message: message.into(), - })) - .await + self.send_action( + types::action::Kind::SendTip(types::SendTipAction { + player_uuid: player_uuid.into(), + message: message.into(), + }), + ) + .await } ///Sends a `PlaySound` action to the server. pub async fn play_sound( @@ -231,14 +257,16 @@ impl Server { volume: Option, pitch: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::PlaySound(types::PlaySoundAction { - player_uuid: player_uuid.into(), - sound: sound.into(), - position: position.map(|v| v.into()), - volume: volume.map(|v| v.into()), - pitch: pitch.map(|v| v.into()), - })) - .await + self.send_action( + types::action::Kind::PlaySound(types::PlaySoundAction { + player_uuid: player_uuid.into(), + sound: sound.into(), + position: position.map(|v| v.into()), + volume: volume.map(|v| v.into()), + pitch: pitch.map(|v| v.into()), + }), + ) + .await } ///Sends a `ExecuteCommand` action to the server. pub async fn execute_command( @@ -246,13 +274,13 @@ impl Server { player_uuid: String, command: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::ExecuteCommand( - types::ExecuteCommandAction { - player_uuid: player_uuid.into(), - command: command.into(), - }, - )) - .await + self.send_action( + types::action::Kind::ExecuteCommand(types::ExecuteCommandAction { + player_uuid: player_uuid.into(), + command: command.into(), + }), + ) + .await } ///Sends a `WorldSetDefaultGameMode` action to the server. pub async fn world_set_default_game_mode( @@ -260,13 +288,13 @@ impl Server { world: Option, game_mode: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldSetDefaultGameMode( - types::WorldSetDefaultGameModeAction { - world: world.map(|v| v.into()), - game_mode: game_mode.into(), - }, - )) - .await + self.send_action( + types::action::Kind::WorldSetDefaultGameMode(types::WorldSetDefaultGameModeAction { + world: world.map(|v| v.into()), + game_mode: game_mode.into(), + }), + ) + .await } ///Sends a `WorldSetDifficulty` action to the server. pub async fn world_set_difficulty( @@ -274,13 +302,13 @@ impl Server { world: Option, difficulty: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldSetDifficulty( - types::WorldSetDifficultyAction { - world: world.map(|v| v.into()), - difficulty: difficulty.into(), - }, - )) - .await + self.send_action( + types::action::Kind::WorldSetDifficulty(types::WorldSetDifficultyAction { + world: world.map(|v| v.into()), + difficulty: difficulty.into(), + }), + ) + .await } ///Sends a `WorldSetTickRange` action to the server. pub async fn world_set_tick_range( @@ -288,13 +316,13 @@ impl Server { world: Option, tick_range: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldSetTickRange( - types::WorldSetTickRangeAction { - world: world.map(|v| v.into()), - tick_range: tick_range.into(), - }, - )) - .await + self.send_action( + types::action::Kind::WorldSetTickRange(types::WorldSetTickRangeAction { + world: world.map(|v| v.into()), + tick_range: tick_range.into(), + }), + ) + .await } ///Sends a `WorldSetBlock` action to the server. pub async fn world_set_block( @@ -303,14 +331,14 @@ impl Server { position: Option, block: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldSetBlock( - types::WorldSetBlockAction { - world: world.map(|v| v.into()), - position: position.map(|v| v.into()), - block: block.map(|v| v.into()), - }, - )) - .await + self.send_action( + types::action::Kind::WorldSetBlock(types::WorldSetBlockAction { + world: world.map(|v| v.into()), + position: position.map(|v| v.into()), + block: block.map(|v| v.into()), + }), + ) + .await } ///Sends a `WorldPlaySound` action to the server. pub async fn world_play_sound( @@ -319,14 +347,14 @@ impl Server { sound: i32, position: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldPlaySound( - types::WorldPlaySoundAction { - world: world.map(|v| v.into()), - sound: sound.into(), - position: position.map(|v| v.into()), - }, - )) - .await + self.send_action( + types::action::Kind::WorldPlaySound(types::WorldPlaySoundAction { + world: world.map(|v| v.into()), + sound: sound.into(), + position: position.map(|v| v.into()), + }), + ) + .await } ///Sends a `WorldAddParticle` action to the server. pub async fn world_add_particle( @@ -337,40 +365,40 @@ impl Server { block: Option, face: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldAddParticle( - types::WorldAddParticleAction { - world: world.map(|v| v.into()), - position: position.map(|v| v.into()), - particle: particle.into(), - block: block.map(|v| v.into()), - face: face.map(|v| v.into()), - }, - )) - .await + self.send_action( + types::action::Kind::WorldAddParticle(types::WorldAddParticleAction { + world: world.map(|v| v.into()), + position: position.map(|v| v.into()), + particle: particle.into(), + block: block.map(|v| v.into()), + face: face.map(|v| v.into()), + }), + ) + .await } ///Sends a `WorldQueryEntities` action to the server. pub async fn world_query_entities( &self, world: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldQueryEntities( - types::WorldQueryEntitiesAction { - world: world.map(|v| v.into()), - }, - )) - .await + self.send_action( + types::action::Kind::WorldQueryEntities(types::WorldQueryEntitiesAction { + world: world.map(|v| v.into()), + }), + ) + .await } ///Sends a `WorldQueryPlayers` action to the server. pub async fn world_query_players( &self, world: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldQueryPlayers( - types::WorldQueryPlayersAction { - world: world.map(|v| v.into()), - }, - )) - .await + self.send_action( + types::action::Kind::WorldQueryPlayers(types::WorldQueryPlayersAction { + world: world.map(|v| v.into()), + }), + ) + .await } ///Sends a `WorldQueryEntitiesWithin` action to the server. pub async fn world_query_entities_within( @@ -378,12 +406,12 @@ impl Server { world: Option, r#box: Option, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldQueryEntitiesWithin( - types::WorldQueryEntitiesWithinAction { - world: world.map(|v| v.into()), - r#box: r#box.map(|v| v.into()), - }, - )) - .await + self.send_action( + types::action::Kind::WorldQueryEntitiesWithin(types::WorldQueryEntitiesWithinAction { + world: world.map(|v| v.into()), + r#box: r#box.map(|v| v.into()), + }), + ) + .await } } From 55a26ab6f821936e6edbd9498d2a53df421d4dde Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Mon, 24 Nov 2025 20:59:31 -0500 Subject: [PATCH 10/22] update cargo.toml --- packages/rust/Cargo.toml | 2 +- packages/rust/rust-plugin-test/src/main.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/rust/Cargo.toml b/packages/rust/Cargo.toml index 834e7fe..03bdbb2 100644 --- a/packages/rust/Cargo.toml +++ b/packages/rust/Cargo.toml @@ -5,7 +5,7 @@ members = [".", "rust-plugin-macro", "rust-plugin-test", "xtask"] [package] name = "dragonfly-plugin" version = "0.1.0" -edition = "2024" +edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/secmc/dragonfly-plugins" description = "Dragonfly gRPC plugin SDK for Rust" diff --git a/packages/rust/rust-plugin-test/src/main.rs b/packages/rust/rust-plugin-test/src/main.rs index 44567a4..319b42f 100644 --- a/packages/rust/rust-plugin-test/src/main.rs +++ b/packages/rust/rust-plugin-test/src/main.rs @@ -20,7 +20,6 @@ struct MyExamplePlugin; // This macro will scan for every `on_...` function we implement // and automatically generate the `impl PluginSubscriptions` for us. // -// * We add `#[async_trait]` because the trait uses async functions. #[async_trait] #[bedrock_plugin] From 7be5f51b4538063be63b34e4f6c43faabc87f25f Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Mon, 24 Nov 2025 23:42:12 -0500 Subject: [PATCH 11/22] change how the macro works for better LSP interop. Add readmes --- packages/rust/MAINTAINING.md | 87 +++++ packages/rust/README.md | 169 +++++++++ packages/rust/rust-plugin-macro/Cargo.toml | 1 - packages/rust/rust-plugin-macro/src/lib.rs | 96 ++--- packages/rust/rust-plugin-test/src/main.rs | 22 +- packages/rust/src/event/handler.rs | 350 +++++++++++-------- packages/rust/src/lib.rs | 3 +- packages/rust/xtask/src/generate_handlers.rs | 11 +- 8 files changed, 527 insertions(+), 212 deletions(-) create mode 100644 packages/rust/MAINTAINING.md create mode 100644 packages/rust/README.md diff --git a/packages/rust/MAINTAINING.md b/packages/rust/MAINTAINING.md new file mode 100644 index 0000000..76af5c9 --- /dev/null +++ b/packages/rust/MAINTAINING.md @@ -0,0 +1,87 @@ +# Maintaining the Plugin API + +This document is for developers who need to update, extend, or maintain the `dragonfly-plugin` API. This is **not** for plugin _users_; that documentation is in the main `README.md`. + +## The Golden Rule: Do Not Edit Generated Files + +This entire API is code-generated from a "Single Source of Truth." **You must not edit the generated Rust files by hand.** Any manual changes will be overwritten. + +The _only_ files you will ever edit manually are: + +1. The `.proto` files (Phase 1). +2. The `xtask` generator code (Phase 3's _generator_). +3. The `dragonfly-plugin-macros` crate code. +4. The `src/*` code in case of BC changes or etc. + +## The 3-Phase Generation Pipeline + +Our architecture is a 3-phase pipeline. Understanding this flow is critical to maintaining the API. + +--- + +### Phase 1: The Protocol (Source of Truth) + +- **What it is:** The `.proto` files that define all our gRPC services, messages, and events. +- **Location:** `../proto` directory. +- **How to change:** This is the **only place** you should start a change. To add a new event (e.g., `PlayerSleepEvent`), you add it to the `EventEnvelope` message in the `.proto` file. + +--- + +### Phase 2: The Raw Bindings (Intermediate) + +- **What it is:** The "raw" Rust code (structs, enums, and gRPC clients) generated directly from the `.proto` files. This code is often ugly, un-idiomatic, and not user-friendly. +- **How it's made:** This code is generated by `buf` which uses `prost` (via the makefile). +- **How to update:** When you have made changes to the proto buf files run the buf tool via `make proto` +- **Warning:** **NEVER EDIT THESE FILES BY HAND.** + +--- + +### Phase 3: The Friendly API (The `xtask` Generator) + +- **What it is:** This is the **public-facing, idiomatic Rust API** that our users love. It includes: + - The `PluginEventHandler` trait. + - The `Server` struct with its friendly helper methods (e.g., `server.send_chat(...)`). + - The `EventContext` struct and its helpers (`.cancel()`, `.set_message()`). + - The `types::EventType` enum. +- **How it's made:** The `xtask` crate is a custom Rust program that **parses the output of Phase 2** and generates this beautiful API. +- **How to update:** You run the `xtask` crate from the terminal. +- **Warning:** **NEVER EDIT THESE FILES BY HAND.** + +--- + +## How to Add a New Event + +This is the most common maintenance task. Here is the complete workflow. + +**Goal:** Add a new `PlayerSleepEvent`. + +1. **Phase 1: Edit the Protocol** + - Open the relevant `.proto` file (e.g., `events.proto`). + - Add the `PlayerSleepEvent` message. + - Add `PlayerSleepEvent player_sleep = 15;` (or the next available ID) to your main `EventEnvelope`'s `payload` oneof. + +2. **Phase 2: Generate the Raw Bindings** + - Run `make proto` to use the buf tool to generate all the needed `generated/*` files. + +3. **Phase 3: Generate the Friendly API** + - Now that the raw types exist, we can generate the friendly API for them. + - `cd` into the `xtask` directory. + - Run `cargo run`. + - The `xtask` generator will see the new `PlayerSleep` variant and automatically: + - Add `async fn on_player_sleep(...)` to the `PluginEventHandler` trait. + - Add the `case PlayerSleep` arm to the `dispatch_event` function. + +4. **Review and Commit** + - You are done. Review the changes in `git`. + - You should see changes in: + 1. The `.proto` file you edited. + 2. The Phase 2 raw bindings file. + 3. The Phase 3 friendly API files (e.g., `event_handler.rs`, `types.rs`). + - Commit all of them. + +## The `Handler` Proc-Macro + +- **Crate:** `dragonfly-plugin-macros` +- **Purpose:** Provides `#[derive(Handler)]` and its helper attribute `#[subscriptions(...)]`. +- **How it works:** This macro generates the `impl PluginSubscriptions` block for the user, turning their `#[subscriptions(Chat, PlayerSleep)]` list into a `Vec`. +- **Maintenance:** You do not need to touch this crate during the normal "add an event" workflow. The macro doesn't need to know _what_ events exist; it just converts the identifiers the user provides (e.g., `PlayerSleep`) into the corresponding enum variant (`types::EventType::PlayerSleep`). If the user makes a typo, the Rust compiler throws the error for us, which is the intended design. diff --git a/packages/rust/README.md b/packages/rust/README.md new file mode 100644 index 0000000..d80b1cf --- /dev/null +++ b/packages/rust/README.md @@ -0,0 +1,169 @@ +# Dragonfly Rust Plugin API + +Welcome to the Rust Plugin API for Dragonfly server software. This library provides the tools to build high-performance, safe, and asynchronous plugins using native Rust. + +The API is designed to be simple and explicit. You define your plugin's logic by implementing the `PluginEventHandler` trait and register your event subscriptions using the `#[derive(Handler)]` macro. + +## Features + +- **Asynchronous by Default:** Built on `tokio` and native Rust `async/await`. +- **Type-Safe:** All events and actions are strongly typed, catching bugs at compile time. +- **Simple Subscriptions:** A clean `#[derive(Handler)]` macro handles all event subscription logic. + +--- + +## Quick Start Guide + +Here is the complete process for creating a "Hello, World\!" plugin. + +### 1\. Create a New Plugin + +First, create a new binary crate for your plugin: + +```sh +cargo new my_plugin --bin +``` + +### 2\. Update `Cargo.toml` + +Next, add `dragonfly-plugin` and `tokio` to your dependencies. + +```toml +[package] +name = "my_plugin" +version = "0.1.0" +edition = "2021" + +[dependencies] +# This is the main API library +dragonfly-plugin = "0.1" # Or use a version number + +# Tokio is required for the async runtime +tokio = { version = "1", features = ["full"] } +``` + +### 3\. Write Your Plugin (`src/main.rs`) + +This is the complete code for a simple plugin that greets players on join and adds a prefix to their chat messages. + +```rust +// --- Import all the necessary items --- +use dragonfly-plugin::{ + event_context::EventContext, + event_handler::PluginEventHandler, + types, // Contains all event data structs + Handler, // The derive macro + Plugin, // The main plugin runner + Server, +}; + +// --- 1. Define Your Plugin Struct --- +// +// `#[derive(Handler)]` is the "trigger" that runs the macro. +// `#[derive(Default)]` is common for simple, stateless plugins. +#[derive(Handler, Default)] +// +// `#[subscriptions(...)]` is the "helper attribute" that lists +// all the events this plugin needs to listen for. +#[subscriptions(PlayerJoin, Chat)] +struct MyPlugin; + +// --- 2. Implement the Event Handlers --- +// +// This is where all your plugin's logic lives. +// You only need to implement the `async fn` handlers +// for the events you subscribed to. +impl PluginEventHandler for MyPlugin { + /// This handler runs when a player joins the server. + async fn on_player_join( + &self, + server: &Server, + event: &mut EventContext<'_, types::PlayerJoinEvent>, + ) { + let player_name = &event.data.name; + println!("Player '{}' has joined.", player_name); + + let welcome_message = format!( + "Welcome, {}! This server is running a Rust plugin.", + player_name + ); + + // Use the `server` handle to send actions + // (This assumes a `send_chat` action helper exists) + server.send_chat(welcome_message).await.ok(); + } + + /// This handler runs when a player sends a chat message. + async fn on_chat( + &self, + _server: &Server, // We don't need the server for this + event: &mut EventContext<'_, types::ChatEvent>, + ) { + let new_message = format!("[Plugin] {}", event.data.message); + + // Use helper methods on the `event` to mutate it + event.set_message(new_message); + } +} + +// --- 3. Start the Plugin --- +// +// This is the entry point that connects to the server. +#[tokio::main] +async fn main() { + println!("Starting my-plugin..."); + + // Create an instance of your plugin + let plugin = MyPlugin::default(); + + // Run the plugin. This will connect to the server + // and block forever, processing events. + Plugin::run(plugin, "127.0.0.1:50051") + .await + .expect("Plugin failed to run"); +} +``` + +--- + +## Writing Event Handlers + +All plugin logic is built by implementing functions from the `PluginEventHandler` trait. + +### `async fn` and Lifetimes + +Because this API uses native `async fn` in traits, you **must** include the anonymous lifetime (`'_`) annotation in the `EventContext` type: + +- **Correct:** `event: &mut EventContext<'_, types::ChatEvent>` +- **Incorrect:** `event: &mut EventContext` + +### Reading Event Data + +You can read immutable data directly from the event: + +```rust +let player_name = &event.data.name; +println!("Player name: {}", player_name); +``` + +### Mutating Events + +Some events are mutable. The `EventContext` provides helper methods (like `set_message`) to modify the event before the server processes it: + +```rust +event.set_message(format!("New message: {}", event.data.message)); +``` + +### Cancelling Events + +You can also cancel compatible events to stop them from happening: + +```rust +event.cancel(); +``` + +## Available Events + +The `#[subscriptions(...)]` macro accepts any variant from the `types::EventType` enum. + +You can find a complete list of all available event names (e.g., `PlayerJoin`, `Chat`, `BlockBreak`) and their corresponding data structs (e.g., `types::PlayerJoinEvent`) by looking in the `./src/types.rs` file or by consulting the API documentation. diff --git a/packages/rust/rust-plugin-macro/Cargo.toml b/packages/rust/rust-plugin-macro/Cargo.toml index 4cce8e4..b3c050f 100644 --- a/packages/rust/rust-plugin-macro/Cargo.toml +++ b/packages/rust/rust-plugin-macro/Cargo.toml @@ -9,4 +9,3 @@ proc-macro = true [dependencies] quote = "1.0" syn = { version = "2.0", features = ["full", "parsing", "visit"] } -heck = "0.5" diff --git a/packages/rust/rust-plugin-macro/src/lib.rs b/packages/rust/rust-plugin-macro/src/lib.rs index e2d996f..99f1581 100644 --- a/packages/rust/rust-plugin-macro/src/lib.rs +++ b/packages/rust/rust-plugin-macro/src/lib.rs @@ -1,65 +1,73 @@ -use heck::ToPascalCase; use proc_macro::TokenStream; -use quote::{format_ident, quote}; -use syn::{ImplItem, ItemImpl, Type, parse_macro_input}; +use quote::quote; +use syn::{ + Data, DeriveInput, Ident, Token, + parse::{Parse, ParseStream}, + parse_macro_input, + punctuated::Punctuated, +}; -#[proc_macro_attribute] -pub fn bedrock_plugin(_attr: TokenStream, item: TokenStream) -> TokenStream { - let input = parse_macro_input!(item as ItemImpl); - let original_impl = input.clone(); - - let self_ty: &Type = &input.self_ty; +#[proc_macro_derive(Handler, attributes(subscriptions))] +pub fn handler_derive(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + if !matches!(&ast.data, Data::Struct(_)) { + let msg = "The #[derive(Handler)] macro can only be used on a `struct`."; + return syn::Error::new_spanned(&ast.ident, msg) + .to_compile_error() + .into(); + }; - let trait_path = match &input.trait_ { - Some((_, path, _)) => path, + let attr = match ast + .attrs + .iter() + .find(|a| a.path().is_ident("subscriptions")) + { + Some(attr) => attr, None => { - let msg = "The #[bedrock_plugin] attribute can only be used on an `impl PluginEventHandler for ...` block."; - return syn::Error::new_spanned(&input.self_ty, msg) + let msg = "Missing #[subscriptions(...)] attribute. Please list the events to subscribe to, e.g., #[subscriptions(Chat, PlayerJoin)]"; + return syn::Error::new_spanned(&ast.ident, msg) .to_compile_error() .into(); } }; - if trait_path - .segments - .last() - .is_some_and(|s| s.ident != "PluginEventHandler") - { - let msg = "The #[bedrock_plugin] attribute must be on an `impl PluginEventHandler for ...` block."; - return syn::Error::new_spanned(trait_path, msg) - .to_compile_error() - .into(); - } - - let mut subscriptions = Vec::new(); - for item in &input.items { - if let ImplItem::Fn(method) = item { - let fn_name_str = method.sig.ident.to_string(); + let subscriptions = match attr.parse_args::() { + Ok(list) => list.events, + Err(e) => { + return e.to_compile_error().into(); + } + }; - if let Some(event_name_snake) = fn_name_str.strip_prefix("on_") { - let event_name_pascal = event_name_snake.to_pascal_case(); + let subscription_variants = subscriptions.iter().map(|ident| { + quote! { types::EventType::#ident } + }); - let event_type_ident = format_ident!("{}", event_name_pascal); + let struct_name = &ast.ident; - subscriptions.push(quote! { types::EventType::#event_type_ident }); - } - } - } - - let subscription_impl = quote! { - impl PluginSubscriptions for #self_ty { + let output = quote! { + impl dragonfly_plugin::PluginSubscriptions for #struct_name { fn get_subscriptions(&self) -> Vec { vec![ - #( #subscriptions ),* + #( #subscription_variants ),* ] } } }; - let output = quote! { - #original_impl - #subscription_impl - }; - output.into() } + +struct SubscriptionsListParser { + events: Vec, +} + +impl Parse for SubscriptionsListParser { + fn parse(input: ParseStream) -> syn::Result { + let punctuated_list: Punctuated = + Punctuated::parse_terminated(input)?; + + let events = punctuated_list.into_iter().collect(); + + Ok(Self { events }) + } +} diff --git a/packages/rust/rust-plugin-test/src/main.rs b/packages/rust/rust-plugin-test/src/main.rs index 319b42f..1f43bc5 100644 --- a/packages/rust/rust-plugin-test/src/main.rs +++ b/packages/rust/rust-plugin-test/src/main.rs @@ -1,35 +1,29 @@ use dragonfly_plugin::{ Plugin, - PluginSubscriptions, Server, - async_trait, event::{EventContext, PluginEventHandler}, types, // All the raw prost/tonic types }; -use rust_plugin_macro::bedrock_plugin; +use rust_plugin_macro::Handler; // --- 2. Define a struct for your plugin's state --- // It can be empty, or it can hold databases, configs, etc. +// Note `Handler` is what enables the auto regisration feature +// `subscriptions(xx)` is the events that your handle will sub +// to from the server. // We add `Default` so it's easy to create. -#[derive(Default)] +#[derive(Handler, Default)] +#[subscriptions(PlayerJoin, Chat)] struct MyExamplePlugin; // --- 3. Implement the event handlers --- -// -// * We add `#[bedrock_plugin]` to this block. -// This macro will scan for every `on_...` function we implement -// and automatically generate the `impl PluginSubscriptions` for us. -// - -#[async_trait] -#[bedrock_plugin] impl PluginEventHandler for MyExamplePlugin { /// This handler runs when a player joins the server. /// We'll use it to send our "hello world" message. async fn on_player_join( &self, server: &Server, - event: &mut EventContext, + event: &mut EventContext<'_, types::PlayerJoinEvent>, ) { // Log to the plugin's console println!("Player '{}' has joined the server.", event.data.name); @@ -54,7 +48,7 @@ impl PluginEventHandler for MyExamplePlugin { async fn on_chat( &self, _server: &Server, // We don't need the server handle for this - event: &mut EventContext, + event: &mut EventContext<'_, types::ChatEvent>, ) { // Get the original message from the event's data let original_message = &event.data.message; diff --git a/packages/rust/src/event/handler.rs b/packages/rust/src/event/handler.rs index f71c80b..09756fc 100644 --- a/packages/rust/src/event/handler.rs +++ b/packages/rust/src/event/handler.rs @@ -1,341 +1,399 @@ //! This file is auto-generated by `xtask`. Do not edit manually. #![allow(unused_variables)] -use crate::{PluginSubscriptions, Server, event::EventContext, types}; -use async_trait::async_trait; -#[async_trait] +use crate::{event::EventContext, types, PluginSubscriptions, Server}; +use std::future::Future; pub trait PluginEventHandler: PluginSubscriptions + Send + Sync { ///Handler for the `PlayerJoin` event. - async fn on_player_join( + fn on_player_join( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerJoinEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerQuit` event. - async fn on_player_quit( + fn on_player_quit( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerQuitEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerMove` event. - async fn on_player_move( + fn on_player_move( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerMoveEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerJump` event. - async fn on_player_jump( + fn on_player_jump( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerJumpEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerTeleport` event. - async fn on_player_teleport( + fn on_player_teleport( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerTeleportEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerChangeWorld` event. - async fn on_player_change_world( + fn on_player_change_world( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerChangeWorldEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerToggleSprint` event. - async fn on_player_toggle_sprint( + fn on_player_toggle_sprint( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerToggleSprintEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerToggleSneak` event. - async fn on_player_toggle_sneak( + fn on_player_toggle_sneak( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerToggleSneakEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `Chat` event. - async fn on_chat(&self, _server: &Server, _event: &mut EventContext) {} + fn on_chat( + &self, + _server: &Server, + _event: &mut EventContext<'_, types::ChatEvent>, + ) -> impl Future + Send { + std::future::ready(()) + } ///Handler for the `PlayerFoodLoss` event. - async fn on_player_food_loss( + fn on_player_food_loss( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerFoodLossEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerHeal` event. - async fn on_player_heal( + fn on_player_heal( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerHealEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerHurt` event. - async fn on_player_hurt( + fn on_player_hurt( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerHurtEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerDeath` event. - async fn on_player_death( + fn on_player_death( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerDeathEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerRespawn` event. - async fn on_player_respawn( + fn on_player_respawn( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerRespawnEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerSkinChange` event. - async fn on_player_skin_change( + fn on_player_skin_change( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerSkinChangeEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerFireExtinguish` event. - async fn on_player_fire_extinguish( + fn on_player_fire_extinguish( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerFireExtinguishEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerStartBreak` event. - async fn on_player_start_break( + fn on_player_start_break( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerStartBreakEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `BlockBreak` event. - async fn on_block_break( + fn on_block_break( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::BlockBreakEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerBlockPlace` event. - async fn on_player_block_place( + fn on_player_block_place( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerBlockPlaceEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerBlockPick` event. - async fn on_player_block_pick( + fn on_player_block_pick( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerBlockPickEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerItemUse` event. - async fn on_player_item_use( + fn on_player_item_use( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerItemUseEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerItemUseOnBlock` event. - async fn on_player_item_use_on_block( + fn on_player_item_use_on_block( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerItemUseOnBlockEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerItemUseOnEntity` event. - async fn on_player_item_use_on_entity( + fn on_player_item_use_on_entity( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerItemUseOnEntityEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerItemRelease` event. - async fn on_player_item_release( + fn on_player_item_release( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerItemReleaseEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerItemConsume` event. - async fn on_player_item_consume( + fn on_player_item_consume( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerItemConsumeEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerAttackEntity` event. - async fn on_player_attack_entity( + fn on_player_attack_entity( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerAttackEntityEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerExperienceGain` event. - async fn on_player_experience_gain( + fn on_player_experience_gain( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerExperienceGainEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerPunchAir` event. - async fn on_player_punch_air( + fn on_player_punch_air( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerPunchAirEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerSignEdit` event. - async fn on_player_sign_edit( + fn on_player_sign_edit( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerSignEditEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerLecternPageTurn` event. - async fn on_player_lectern_page_turn( + fn on_player_lectern_page_turn( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerLecternPageTurnEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerItemDamage` event. - async fn on_player_item_damage( + fn on_player_item_damage( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerItemDamageEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerItemPickup` event. - async fn on_player_item_pickup( + fn on_player_item_pickup( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerItemPickupEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerHeldSlotChange` event. - async fn on_player_held_slot_change( + fn on_player_held_slot_change( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerHeldSlotChangeEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerItemDrop` event. - async fn on_player_item_drop( + fn on_player_item_drop( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerItemDropEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `PlayerTransfer` event. - async fn on_player_transfer( + fn on_player_transfer( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerTransferEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `Command` event. - async fn on_command(&self, _server: &Server, _event: &mut EventContext) {} + fn on_command( + &self, + _server: &Server, + _event: &mut EventContext<'_, types::CommandEvent>, + ) -> impl Future + Send { + std::future::ready(()) + } ///Handler for the `PlayerDiagnostics` event. - async fn on_player_diagnostics( + fn on_player_diagnostics( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::PlayerDiagnosticsEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `WorldLiquidFlow` event. - async fn on_world_liquid_flow( + fn on_world_liquid_flow( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::WorldLiquidFlowEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `WorldLiquidDecay` event. - async fn on_world_liquid_decay( + fn on_world_liquid_decay( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::WorldLiquidDecayEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `WorldLiquidHarden` event. - async fn on_world_liquid_harden( + fn on_world_liquid_harden( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::WorldLiquidHardenEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `WorldSound` event. - async fn on_world_sound( + fn on_world_sound( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::WorldSoundEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `WorldFireSpread` event. - async fn on_world_fire_spread( + fn on_world_fire_spread( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::WorldFireSpreadEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `WorldBlockBurn` event. - async fn on_world_block_burn( + fn on_world_block_burn( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::WorldBlockBurnEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `WorldCropTrample` event. - async fn on_world_crop_trample( + fn on_world_crop_trample( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::WorldCropTrampleEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `WorldLeavesDecay` event. - async fn on_world_leaves_decay( + fn on_world_leaves_decay( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::WorldLeavesDecayEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `WorldEntitySpawn` event. - async fn on_world_entity_spawn( + fn on_world_entity_spawn( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::WorldEntitySpawnEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `WorldEntityDespawn` event. - async fn on_world_entity_despawn( + fn on_world_entity_despawn( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::WorldEntityDespawnEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `WorldExplosion` event. - async fn on_world_explosion( + fn on_world_explosion( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::WorldExplosionEvent>, + ) -> impl Future + Send { + std::future::ready(()) } ///Handler for the `WorldClose` event. - async fn on_world_close( + fn on_world_close( &self, _server: &Server, - _event: &mut EventContext, - ) { + _event: &mut EventContext<'_, types::WorldCloseEvent>, + ) -> impl Future + Send { + std::future::ready(()) } } #[doc(hidden)] diff --git a/packages/rust/src/lib.rs b/packages/rust/src/lib.rs index a077642..bcebe68 100644 --- a/packages/rust/src/lib.rs +++ b/packages/rust/src/lib.rs @@ -22,10 +22,9 @@ use std::error::Error; pub use server::*; // main usage stuff for plugin devs: -pub use async_trait::async_trait; pub use event::PluginEventHandler; use tokio::sync::mpsc; -use tokio_stream::{StreamExt, wrappers::ReceiverStream}; +use tokio_stream::{wrappers::ReceiverStream, StreamExt}; // TODO: pub use rust_plugin_macro::bedrock_plugin; #[cfg(unix)] diff --git a/packages/rust/xtask/src/generate_handlers.rs b/packages/rust/xtask/src/generate_handlers.rs index 425a09c..4df2e89 100644 --- a/packages/rust/xtask/src/generate_handlers.rs +++ b/packages/rust/xtask/src/generate_handlers.rs @@ -26,11 +26,13 @@ pub fn generate_handler_trait(ast: &File, output_path: &PathBuf) -> Result<()> { event_handler_fns.push(quote! { #[doc = #doc_string] - async fn #handler_fn_name( + fn #handler_fn_name( &self, _server: &Server, - _event: &mut EventContext, - ) {} + _event: &mut EventContext<'_, types::#ty_path>, + ) -> impl Future + Send { + std::future::ready(()) + } }); dispatch_fn_match_arms.push(quote! { @@ -46,9 +48,8 @@ pub fn generate_handler_trait(ast: &File, output_path: &PathBuf) -> Result<()> { //! This file is auto-generated by `xtask`. Do not edit manually. #![allow(unused_variables)] use crate::{event::EventContext, types, Server, PluginSubscriptions}; - use async_trait::async_trait; + use std::future::Future; - #[async_trait] pub trait PluginEventHandler: PluginSubscriptions + Send + Sync { #(#event_handler_fns)* } From 6e6eb9056c6c3392cf67017a942121cd151699bc Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Mon, 24 Nov 2025 23:55:39 -0500 Subject: [PATCH 12/22] reexport the macro from main lib crate, update some examples to not reference old code. --- packages/rust/Cargo.toml | 1 + packages/rust/rust-plugin-test/Cargo.toml | 1 - packages/rust/rust-plugin-test/src/main.rs | 5 ++--- packages/rust/src/lib.rs | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/rust/Cargo.toml b/packages/rust/Cargo.toml index 03bdbb2..05eed66 100644 --- a/packages/rust/Cargo.toml +++ b/packages/rust/Cargo.toml @@ -22,3 +22,4 @@ tokio-stream = "0.1.17" tonic = { version = "0.12", features = ["transport"] } tower = "0.5" hyper-util = { version = "0.1", features = ["tokio"] } +rust-plugin-macro = { path = "rust-plugin-macro" } diff --git a/packages/rust/rust-plugin-test/Cargo.toml b/packages/rust/rust-plugin-test/Cargo.toml index 8bafe5a..44459d9 100644 --- a/packages/rust/rust-plugin-test/Cargo.toml +++ b/packages/rust/rust-plugin-test/Cargo.toml @@ -5,5 +5,4 @@ edition = "2024" [dependencies] dragonfly-plugin = { path = "../"} -rust-plugin-macro = { path = "../rust-plugin-macro"} tokio = { version = "1", features = ["full"] } diff --git a/packages/rust/rust-plugin-test/src/main.rs b/packages/rust/rust-plugin-test/src/main.rs index 1f43bc5..024e23d 100644 --- a/packages/rust/rust-plugin-test/src/main.rs +++ b/packages/rust/rust-plugin-test/src/main.rs @@ -1,10 +1,10 @@ use dragonfly_plugin::{ + Handler, Plugin, Server, event::{EventContext, PluginEventHandler}, types, // All the raw prost/tonic types }; -use rust_plugin_macro::Handler; // --- 2. Define a struct for your plugin's state --- // It can be empty, or it can hold databases, configs, etc. @@ -62,7 +62,6 @@ impl PluginEventHandler for MyExamplePlugin { } // We don't implement `on_player_hurt`, `on_block_break`, etc., - // so the `#[bedrock_plugin]` macro will not subscribe to them. } // --- 4. The main function to run the plugin --- @@ -77,7 +76,7 @@ async fn main() -> Result<(), Box> { ); // 2. Connect to the server and run the plugin - println!("Connecting to Bedrock server..."); + println!("Connecting to df-mc server..."); plugin .run( diff --git a/packages/rust/src/lib.rs b/packages/rust/src/lib.rs index bcebe68..c921098 100644 --- a/packages/rust/src/lib.rs +++ b/packages/rust/src/lib.rs @@ -23,9 +23,9 @@ pub use server::*; // main usage stuff for plugin devs: pub use event::PluginEventHandler; +pub use rust_plugin_macro::Handler; use tokio::sync::mpsc; use tokio_stream::{wrappers::ReceiverStream, StreamExt}; -// TODO: pub use rust_plugin_macro::bedrock_plugin; #[cfg(unix)] use hyper_util::rt::TokioIo; From e4698c1949085b4019835c9d60072e949c036535 Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Tue, 25 Nov 2025 00:14:36 -0500 Subject: [PATCH 13/22] published. --- packages/rust/Cargo.toml | 9 +++++++-- packages/rust/rust-plugin-macro/Cargo.toml | 9 ++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/rust/Cargo.toml b/packages/rust/Cargo.toml index 05eed66..4f226d0 100644 --- a/packages/rust/Cargo.toml +++ b/packages/rust/Cargo.toml @@ -2,13 +2,18 @@ resolver = "3" members = [".", "rust-plugin-macro", "rust-plugin-test", "xtask"] +[workspace.dependencies] +rust-plugin-macro = { path = "rust-plugin-macro", version = "0.1" } + [package] name = "dragonfly-plugin" version = "0.1.0" edition = "2021" -license = "MIT OR Apache-2.0" +license = "MIT" repository = "https://github.com/secmc/dragonfly-plugins" description = "Dragonfly gRPC plugin SDK for Rust" +homepage = "https://github.com/secmc/dragonfly-plugins" +keywords = ["dragonfly", "plugin", "grpc", "macro"] [lib] path = "src/lib.rs" @@ -22,4 +27,4 @@ tokio-stream = "0.1.17" tonic = { version = "0.12", features = ["transport"] } tower = "0.5" hyper-util = { version = "0.1", features = ["tokio"] } -rust-plugin-macro = { path = "rust-plugin-macro" } +rust-plugin-macro = { workspace = true, version = "0.1" } diff --git a/packages/rust/rust-plugin-macro/Cargo.toml b/packages/rust/rust-plugin-macro/Cargo.toml index b3c050f..d4649f9 100644 --- a/packages/rust/rust-plugin-macro/Cargo.toml +++ b/packages/rust/rust-plugin-macro/Cargo.toml @@ -1,7 +1,14 @@ [package] name = "rust-plugin-macro" version = "0.1.0" -edition = "2024" +edition = "2021" +license = "MIT" +repository = "https://github.com/secmc/dragonfly-plugins" +documentation = "https://docs.rs/rust-plugin-macro" +description = "Procedural macros for the Dragonfly gRPC plugin SDK" +homepage = "https://github.com/secmc/dragonfly-plugins" +keywords = ["dragonfly", "plugin", "grpc", "macro"] +categories = ["development-tools::procedural-macro-helpers"] [lib] proc-macro = true From 98a4c41b0e21d5a941df9b6d607f34f47441b113 Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Tue, 25 Nov 2025 11:11:49 -0500 Subject: [PATCH 14/22] new macro and changes to naming way more logical. Need to update docs doctests aren't passing. Going to attempt to replace #[events()] with a #[event_handler] proc macro impl block again. --- packages/rust/README.md | 14 +- packages/rust/rust-plugin-macro/Cargo.toml | 1 + packages/rust/rust-plugin-macro/src/events.rs | 46 ++++++ packages/rust/rust-plugin-macro/src/lib.rs | 92 ++++-------- packages/rust/rust-plugin-macro/src/plugin.rs | 127 ++++++++++++++++ packages/rust/rust-plugin-test/src/main.rs | 41 +++-- packages/rust/src/event/handler.rs | 6 +- packages/rust/src/event/mutations.rs | 1 + packages/rust/src/lib.rs | 105 ++++++++----- packages/rust/src/server/helpers.rs | 142 +++++++++--------- packages/rust/xtask/src/generate_actions.rs | 14 +- packages/rust/xtask/src/generate_handlers.rs | 4 +- packages/rust/xtask/src/generate_mutations.rs | 1 + 13 files changed, 381 insertions(+), 213 deletions(-) create mode 100644 packages/rust/rust-plugin-macro/src/events.rs create mode 100644 packages/rust/rust-plugin-macro/src/plugin.rs diff --git a/packages/rust/README.md b/packages/rust/README.md index d80b1cf..aa89570 100644 --- a/packages/rust/README.md +++ b/packages/rust/README.md @@ -48,9 +48,8 @@ This is the complete code for a simple plugin that greets players on join and ad ```rust // --- Import all the necessary items --- -use dragonfly-plugin::{ - event_context::EventContext, - event_handler::PluginEventHandler, +use dragonfly_plugin::{ + event::{EventContext, PluginEventHandler}, types, // Contains all event data structs Handler, // The derive macro Plugin, // The main plugin runner @@ -89,8 +88,7 @@ impl PluginEventHandler for MyPlugin { ); // Use the `server` handle to send actions - // (This assumes a `send_chat` action helper exists) - server.send_chat(welcome_message).await.ok(); + server.send_chat(event.data.player_uuid.clone(), welcome_message).await.ok(); } /// This handler runs when a player sends a chat message. @@ -141,7 +139,7 @@ Because this API uses native `async fn` in traits, you **must** include the anon You can read immutable data directly from the event: -```rust +```rust,ignore let player_name = &event.data.name; println!("Player name: {}", player_name); ``` @@ -150,7 +148,7 @@ println!("Player name: {}", player_name); Some events are mutable. The `EventContext` provides helper methods (like `set_message`) to modify the event before the server processes it: -```rust +```rust,ignore event.set_message(format!("New message: {}", event.data.message)); ``` @@ -158,7 +156,7 @@ event.set_message(format!("New message: {}", event.data.message)); You can also cancel compatible events to stop them from happening: -```rust +```rust,ignore event.cancel(); ``` diff --git a/packages/rust/rust-plugin-macro/Cargo.toml b/packages/rust/rust-plugin-macro/Cargo.toml index d4649f9..8096be8 100644 --- a/packages/rust/rust-plugin-macro/Cargo.toml +++ b/packages/rust/rust-plugin-macro/Cargo.toml @@ -14,5 +14,6 @@ categories = ["development-tools::procedural-macro-helpers"] proc-macro = true [dependencies] +proc-macro2 = "1.0.103" quote = "1.0" syn = { version = "2.0", features = ["full", "parsing", "visit"] } diff --git a/packages/rust/rust-plugin-macro/src/events.rs b/packages/rust/rust-plugin-macro/src/events.rs new file mode 100644 index 0000000..34024dd --- /dev/null +++ b/packages/rust/rust-plugin-macro/src/events.rs @@ -0,0 +1,46 @@ +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Attribute, Ident, Token, +}; + +struct SubscriptionsListParser { + events: Vec, +} + +impl Parse for SubscriptionsListParser { + fn parse(input: ParseStream) -> syn::Result { + let punctuated_list: Punctuated = Punctuated::parse_terminated(input)?; + + let events = punctuated_list.into_iter().collect(); + + Ok(Self { events }) + } +} + +pub(crate) fn generate_event_subscriptions_inp( + attr: &Attribute, + derive_name: &Ident, +) -> proc_macro2::TokenStream { + let subscriptions = match attr.parse_args::() { + Ok(list) => list.events, + Err(e) => { + return e.to_compile_error(); + } + }; + + let subscription_variants = subscriptions.iter().map(|ident| { + quote! { types::EventType::#ident } + }); + + quote! { + impl dragonfly_plugin::EventSubscriptions for #derive_name { + fn get_subscriptions(&self) -> Vec { + vec![ + #( #subscription_variants ),* + ] + } + } + } +} diff --git a/packages/rust/rust-plugin-macro/src/lib.rs b/packages/rust/rust-plugin-macro/src/lib.rs index 99f1581..61c74d5 100644 --- a/packages/rust/rust-plugin-macro/src/lib.rs +++ b/packages/rust/rust-plugin-macro/src/lib.rs @@ -1,73 +1,47 @@ +mod events; +mod plugin; + use proc_macro::TokenStream; use quote::quote; -use syn::{ - Data, DeriveInput, Ident, Token, - parse::{Parse, ParseStream}, - parse_macro_input, - punctuated::Punctuated, -}; +use syn::{parse_macro_input, Attribute, DeriveInput}; + +use crate::{events::generate_event_subscriptions_inp, plugin::generate_plugin_impl}; -#[proc_macro_derive(Handler, attributes(subscriptions))] +#[proc_macro_derive(Plugin, attributes(plugin, events))] pub fn handler_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); - if !matches!(&ast.data, Data::Struct(_)) { - let msg = "The #[derive(Handler)] macro can only be used on a `struct`."; - return syn::Error::new_spanned(&ast.ident, msg) - .to_compile_error() - .into(); - }; - let attr = match ast - .attrs - .iter() - .find(|a| a.path().is_ident("subscriptions")) - { - Some(attr) => attr, - None => { - let msg = "Missing #[subscriptions(...)] attribute. Please list the events to subscribe to, e.g., #[subscriptions(Chat, PlayerJoin)]"; - return syn::Error::new_spanned(&ast.ident, msg) - .to_compile_error() - .into(); - } - }; + let derive_name = &ast.ident; - let subscriptions = match attr.parse_args::() { - Ok(list) => list.events, - Err(e) => { - return e.to_compile_error().into(); - } - }; + let info_attr = find_attribute( + &ast, + "plugin", + "Missing `#[plugin(...)]` attribute with metadata.", + ); - let subscription_variants = subscriptions.iter().map(|ident| { - quote! { types::EventType::#ident } - }); + // generate the code for impling Plugin. + let plugin_impl = generate_plugin_impl(info_attr, derive_name); - let struct_name = &ast.ident; + let subscription_attr = find_attribute( + &ast, + "events", + "Missing #[events(...)] attribute. Please list the events to \ + subscribe to, e.g., #[events(Chat, PlayerJoin)]", + ); - let output = quote! { - impl dragonfly_plugin::PluginSubscriptions for #struct_name { - fn get_subscriptions(&self) -> Vec { - vec![ - #( #subscription_variants ),* - ] - } - } - }; - - output.into() -} + let event_subscriptions_impl = generate_event_subscriptions_inp(subscription_attr, derive_name); -struct SubscriptionsListParser { - events: Vec, + quote! { + #plugin_impl + #event_subscriptions_impl + } + .into() } -impl Parse for SubscriptionsListParser { - fn parse(input: ParseStream) -> syn::Result { - let punctuated_list: Punctuated = - Punctuated::parse_terminated(input)?; - - let events = punctuated_list.into_iter().collect(); - - Ok(Self { events }) - } +fn find_attribute<'a>(ast: &'a syn::DeriveInput, name: &str, error: &str) -> &'a Attribute { + ast.attrs + .iter() + .find(|a| a.path().is_ident(name)) + .ok_or_else(|| syn::Error::new(ast.ident.span(), error)) + .unwrap() } diff --git a/packages/rust/rust-plugin-macro/src/plugin.rs b/packages/rust/rust-plugin-macro/src/plugin.rs new file mode 100644 index 0000000..30976ac --- /dev/null +++ b/packages/rust/rust-plugin-macro/src/plugin.rs @@ -0,0 +1,127 @@ +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Attribute, Expr, Ident, Lit, LitStr, Meta, Token, +}; + +struct PluginInfoParser { + pub id: LitStr, + pub name: LitStr, + pub version: LitStr, + pub api: LitStr, +} + +impl Parse for PluginInfoParser { + fn parse(input: ParseStream) -> syn::Result { + let metas = Punctuated::::parse_terminated(input)?; + + let mut id = None; + let mut name = None; + let mut version = None; + let mut api = None; + + for meta in metas { + match meta { + Meta::NameValue(nv) => { + let key_ident = nv.path.get_ident().ok_or_else(|| { + syn::Error::new_spanned(&nv.path, "Expected an identifier (e.g., 'id')") + })?; + + let value_str = match &nv.value { + Expr::Lit(expr_lit) => match &expr_lit.lit { + // clone out the reference as we will now + // be owning it and putting it into our generated code + Lit::Str(lit_str) => lit_str.clone(), + _ => { + return Err(syn::Error::new_spanned( + &nv.value, + "Expected a string literal", + )); + } + }, + _ => { + return Err(syn::Error::new_spanned( + &nv.value, + "Expected a string literal", + )); + } + }; + + // Store the value + if key_ident == "id" { + id = Some(value_str); + } else if key_ident == "name" { + name = Some(value_str); + } else if key_ident == "version" { + version = Some(value_str); + } else if key_ident == "api" { + api = Some(value_str); + } else { + return Err(syn::Error::new_spanned( + key_ident, + "Unknown key. Expected 'id', 'name', 'version', or 'api'", + )); + } + } + _ => { + return Err(syn::Error::new_spanned( + meta, + "Expected `key = \"value\"` format", + )); + } + }; + } + + // Validate that all required fields were found + // We use `input.span()` to point the error at the whole `#[plugin(...)]` + // attribute if a field is missing. + let id = id.ok_or_else(|| syn::Error::new(input.span(), "Missing required field 'id'"))?; + let name = + name.ok_or_else(|| syn::Error::new(input.span(), "Missing required field 'name'"))?; + let version = version + .ok_or_else(|| syn::Error::new(input.span(), "Missing required field 'version'"))?; + let api = + api.ok_or_else(|| syn::Error::new(input.span(), "Missing required field 'api'"))?; + + Ok(Self { + id, + name, + version, + api, + }) + } +} + +pub(crate) fn generate_plugin_impl( + attr: &Attribute, + derive_name: &Ident, +) -> proc_macro2::TokenStream { + let plugin_info = match attr.parse_args::() { + Ok(info) => info, + Err(e) => return e.to_compile_error(), + }; + + // gotta define these outside because of quote rules with the . access. + let id_lit = &plugin_info.id; + let name_lit = &plugin_info.name; + let version_lit = &plugin_info.version; + let api_lit = &plugin_info.api; + + quote! { + impl dragonfly_plugin::Plugin for #derive_name { + fn get_info(&self) -> dragonfly_plugin::PluginInfo<'static> { + dragonfly_plugin::PluginInfo::<'static> { + id: #id_lit, + name: #name_lit, + version: #version_lit, + api_version: #api_lit + } + } + fn get_id(&self) -> &'static str { #id_lit } + fn get_name(&self) -> &'static str { #name_lit } + fn get_version(&self) -> &'static str { #version_lit } + fn get_api_version(&self) -> &'static str { #api_lit } + } + } +} diff --git a/packages/rust/rust-plugin-test/src/main.rs b/packages/rust/rust-plugin-test/src/main.rs index 024e23d..c59a3a4 100644 --- a/packages/rust/rust-plugin-test/src/main.rs +++ b/packages/rust/rust-plugin-test/src/main.rs @@ -1,23 +1,30 @@ use dragonfly_plugin::{ - Handler, Plugin, + PluginRunner, Server, - event::{EventContext, PluginEventHandler}, + event::{EventContext, EventHandler}, types, // All the raw prost/tonic types }; // --- 2. Define a struct for your plugin's state --- // It can be empty, or it can hold databases, configs, etc. -// Note `Handler` is what enables the auto regisration feature +// Note `Plugin` is what enables the auto regisration feature +// and the plugin details via plugin(...) // `subscriptions(xx)` is the events that your handle will sub // to from the server. // We add `Default` so it's easy to create. -#[derive(Handler, Default)] -#[subscriptions(PlayerJoin, Chat)] +#[derive(Plugin, Default)] +#[plugin( + id = "example-rust", // A unique ID for your plugin (matches plugins.yaml) + name = "Example Rust Plugin", // A human-readable name + version = "1.0.0", // Your plugin's version + api = "1.0.0", // The API version you're built against +)] +#[events(PlayerJoin, Chat)] // A list of the events that you want to handle. struct MyExamplePlugin; // --- 3. Implement the event handlers --- -impl PluginEventHandler for MyExamplePlugin { +impl EventHandler for MyExamplePlugin { /// This handler runs when a player joins the server. /// We'll use it to send our "hello world" message. async fn on_player_join( @@ -67,21 +74,11 @@ impl PluginEventHandler for MyExamplePlugin { // --- 4. The main function to run the plugin --- #[tokio::main] async fn main() -> Result<(), Box> { - // 1. Define the plugin's metadata - let plugin = Plugin::new( - "example-rust", // A unique ID for your plugin (matches plugins.yaml) - "Example Rust Plugin", // A human-readable name - "1.0.0", // Your plugin's version - "1.0.0", // The API version you're built against - ); + println!("Starting the rust plugin..."); - // 2. Connect to the server and run the plugin - println!("Connecting to df-mc server..."); - - plugin - .run( - MyExamplePlugin, // Pass in an instance of our handler - "tcp://127.0.0.1:50050", // The server address (Unix socket) - ) - .await + PluginRunner::run( + MyExamplePlugin, // Pass in an instance of our plugin + "tcp://127.0.0.1:50050", // The server address (Unix socket) + ) + .await } diff --git a/packages/rust/src/event/handler.rs b/packages/rust/src/event/handler.rs index 09756fc..209b473 100644 --- a/packages/rust/src/event/handler.rs +++ b/packages/rust/src/event/handler.rs @@ -1,8 +1,8 @@ //! This file is auto-generated by `xtask`. Do not edit manually. #![allow(unused_variables)] -use crate::{event::EventContext, types, PluginSubscriptions, Server}; +use crate::{event::EventContext, types, EventSubscriptions, Server}; use std::future::Future; -pub trait PluginEventHandler: PluginSubscriptions + Send + Sync { +pub trait EventHandler: EventSubscriptions + Send + Sync { ///Handler for the `PlayerJoin` event. fn on_player_join( &self, @@ -399,7 +399,7 @@ pub trait PluginEventHandler: PluginSubscriptions + Send + Sync { #[doc(hidden)] pub async fn dispatch_event( server: &Server, - handler: &impl PluginEventHandler, + handler: &impl EventHandler, envelope: &types::EventEnvelope, ) { let Some(payload) = &envelope.payload else { diff --git a/packages/rust/src/event/mutations.rs b/packages/rust/src/event/mutations.rs index 4485006..e32216d 100644 --- a/packages/rust/src/event/mutations.rs +++ b/packages/rust/src/event/mutations.rs @@ -1,4 +1,5 @@ //! This file is auto-generated by `xtask`. Do not edit manually. +#![allow(clippy::all)] use crate::types; use crate::event::EventContext; impl<'a> EventContext<'a, types::ChatEvent> { diff --git a/packages/rust/src/lib.rs b/packages/rust/src/lib.rs index c921098..dd4c0b2 100644 --- a/packages/rust/src/lib.rs +++ b/packages/rust/src/lib.rs @@ -1,5 +1,4 @@ -#![allow(clippy::all)] - +#[doc = include_str!("../README.md")] #[path = "generated/df.plugin.rs"] mod df_plugin; @@ -22,8 +21,8 @@ use std::error::Error; pub use server::*; // main usage stuff for plugin devs: -pub use event::PluginEventHandler; -pub use rust_plugin_macro::Handler; +pub use event::EventHandler; +pub use rust_plugin_macro::Plugin; use tokio::sync::mpsc; use tokio_stream::{wrappers::ReceiverStream, StreamExt}; @@ -72,29 +71,11 @@ async fn connect_to_server( } } -pub struct Plugin { - id: String, - name: String, - version: String, - api_version: String, -} - -impl Plugin { - pub fn new(id: &str, name: &str, version: &str, api_version: &str) -> Self { - Self { - id: id.to_string(), - name: name.to_string(), - version: version.to_string(), - api_version: api_version.to_string(), - } - } +pub struct PluginRunner {} +impl PluginRunner { /// Runs the plugin, connecting to the server and starting the event loop. - pub async fn run( - self, - handler: impl PluginEventHandler + PluginSubscriptions + 'static, - addr: &str, - ) -> Result<(), Box> { + pub async fn run(plugin: impl Plugin + 'static, addr: &str) -> Result<(), Box> { let mut raw_client = connect_to_server(addr).await?; let (tx, rx) = mpsc::channel(128); @@ -103,11 +84,11 @@ impl Plugin { // This is required because the Go server blocks on Recv() waiting for the // hello before sending response headers. let hello_msg = types::PluginToHost { - plugin_id: self.id.clone(), + plugin_id: plugin.get_id().to_owned(), payload: Some(types::PluginPayload::Hello(types::PluginHello { - name: self.name.clone(), - version: self.version.clone(), - api_version: self.api_version.clone(), + name: plugin.get_name().to_owned(), + version: plugin.get_version().to_owned(), + api_version: plugin.get_api_version().to_owned(), commands: vec![], custom_items: vec![], })), @@ -118,24 +99,24 @@ impl Plugin { let mut event_stream = raw_client.event_stream(request_stream).await?.into_inner(); let server = Server { - plugin_id: self.id.clone(), + plugin_id: plugin.get_id().to_owned(), sender: tx.clone(), }; - let events = handler.get_subscriptions(); + let events = plugin.get_subscriptions(); if !events.is_empty() { println!("Subscribing to {} event types...", events.len()); server.subscribe(events).await?; } - println!("Plugin '{}' connected and listening.", self.name); + println!("Plugin '{}' connected and listening.", plugin.get_name()); // 8. Run the main event loop while let Some(Ok(msg)) = event_stream.next().await { match msg.payload { // We received a game event Some(types::HostPayload::Event(envelope)) => { - event::dispatch_event(&server, &handler, &envelope).await; + event::dispatch_event(&server, &plugin, &envelope).await; } // The server is shutting us down Some(types::HostPayload::Shutdown(shutdown)) => { @@ -146,7 +127,7 @@ impl Plugin { } } - println!("Plugin '{}' disconnected.", self.name); + println!("Plugin '{}' disconnected.", plugin.get_name()); Ok(()) } } @@ -154,8 +135,60 @@ impl Plugin { /// A trait that defines which events your plugin will receive. /// /// You can implement this trait manually, or you can use the -/// `#[bedrock_plugin]` macro on your `PluginEventHandler` +/// `#[derive(Plugin)]` along with `#[events(Event1, Event2)` /// implementation to generate it for you. -pub trait PluginSubscriptions { +pub trait EventSubscriptions { fn get_subscriptions(&self) -> Vec; } + +/// A struct that defines the details of your plugin. +pub struct PluginInfo<'a> { + pub id: &'a str, + pub name: &'a str, + pub version: &'a str, + pub api_version: &'a str, +} + +/// The final trait required for our plugin to be runnable. +/// +/// These functions get impled automatically by +/// `#[derive(Plugin)` like so: +/// ```rust +/// use dragonfly_plugin::{ +/// Dragonfly, // Our runtime, clearly named +/// EventHandler, // The logic trait +/// Plugin, // The derive macro +/// event_context::EventContext, +/// types, +/// Server, +/// }; +/// +/// #[derive(Plugin, Default)] +/// #[plugin( +/// id = "example-rust", +/// name = "Example Rust Plugin", +/// version = "1.0.0", +/// api = "1.0.0" +/// )] +///#[events(PlayerJoin, Chat)] +///struct MyPlugin {} +/// +///impl EventHandler for MyPlugin { +/// async fn on_player_join( +/// &self, +/// server: &Server, +/// event: &mut EventContext<'_, types::PlayerJoinEvent>, +/// ) { +/// } +/// ``` +pub trait Plugin: EventHandler + EventSubscriptions { + fn get_info(&self) -> PluginInfo<'_>; + + fn get_id(&self) -> &str; + + fn get_name(&self) -> &str; + + fn get_version(&self) -> &str; + + fn get_api_version(&self) -> &str; +} diff --git a/packages/rust/src/server/helpers.rs b/packages/rust/src/server/helpers.rs index 47397b4..851fc36 100644 --- a/packages/rust/src/server/helpers.rs +++ b/packages/rust/src/server/helpers.rs @@ -10,8 +10,8 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::SendChat(types::SendChatAction { - target_uuid: target_uuid.into(), - message: message.into(), + target_uuid, + message, }), ) .await @@ -25,9 +25,9 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::Teleport(types::TeleportAction { - player_uuid: player_uuid.into(), - position: position.map(|v| v.into()), - rotation: rotation.map(|v| v.into()), + player_uuid, + position, + rotation, }), ) .await @@ -40,8 +40,8 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::Kick(types::KickAction { - player_uuid: player_uuid.into(), - reason: reason.into(), + player_uuid, + reason, }), ) .await @@ -54,8 +54,8 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::SetGameMode(types::SetGameModeAction { - player_uuid: player_uuid.into(), - game_mode: game_mode.into(), + player_uuid, + game_mode, }), ) .await @@ -68,8 +68,8 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::GiveItem(types::GiveItemAction { - player_uuid: player_uuid.into(), - item: item.map(|v| v.into()), + player_uuid, + item, }), ) .await @@ -81,7 +81,7 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::ClearInventory(types::ClearInventoryAction { - player_uuid: player_uuid.into(), + player_uuid, }), ) .await @@ -95,9 +95,9 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::SetHeldItem(types::SetHeldItemAction { - player_uuid: player_uuid.into(), - main: main.map(|v| v.into()), - offhand: offhand.map(|v| v.into()), + player_uuid, + main, + offhand, }), ) .await @@ -111,9 +111,9 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::SetHealth(types::SetHealthAction { - player_uuid: player_uuid.into(), - health: health.into(), - max_health: max_health.map(|v| v.into()), + player_uuid, + health, + max_health, }), ) .await @@ -126,8 +126,8 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::SetFood(types::SetFoodAction { - player_uuid: player_uuid.into(), - food: food.into(), + player_uuid, + food, }), ) .await @@ -142,10 +142,10 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::SetExperience(types::SetExperienceAction { - player_uuid: player_uuid.into(), - level: level.map(|v| v.into()), - progress: progress.map(|v| v.into()), - amount: amount.map(|v| v.into()), + player_uuid, + level, + progress, + amount, }), ) .await @@ -158,8 +158,8 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::SetVelocity(types::SetVelocityAction { - player_uuid: player_uuid.into(), - velocity: velocity.map(|v| v.into()), + player_uuid, + velocity, }), ) .await @@ -175,11 +175,11 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::AddEffect(types::AddEffectAction { - player_uuid: player_uuid.into(), - effect_type: effect_type.into(), - level: level.into(), - duration_ms: duration_ms.into(), - show_particles: show_particles.into(), + player_uuid, + effect_type, + level, + duration_ms, + show_particles, }), ) .await @@ -192,8 +192,8 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::RemoveEffect(types::RemoveEffectAction { - player_uuid: player_uuid.into(), - effect_type: effect_type.into(), + player_uuid, + effect_type, }), ) .await @@ -210,12 +210,12 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::SendTitle(types::SendTitleAction { - player_uuid: player_uuid.into(), - title: title.into(), - subtitle: subtitle.map(|v| v.into()), - fade_in_ms: fade_in_ms.map(|v| v.into()), - duration_ms: duration_ms.map(|v| v.into()), - fade_out_ms: fade_out_ms.map(|v| v.into()), + player_uuid, + title, + subtitle, + fade_in_ms, + duration_ms, + fade_out_ms, }), ) .await @@ -228,8 +228,8 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::SendPopup(types::SendPopupAction { - player_uuid: player_uuid.into(), - message: message.into(), + player_uuid, + message, }), ) .await @@ -242,8 +242,8 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::SendTip(types::SendTipAction { - player_uuid: player_uuid.into(), - message: message.into(), + player_uuid, + message, }), ) .await @@ -259,11 +259,11 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::PlaySound(types::PlaySoundAction { - player_uuid: player_uuid.into(), - sound: sound.into(), - position: position.map(|v| v.into()), - volume: volume.map(|v| v.into()), - pitch: pitch.map(|v| v.into()), + player_uuid, + sound, + position, + volume, + pitch, }), ) .await @@ -276,8 +276,8 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::ExecuteCommand(types::ExecuteCommandAction { - player_uuid: player_uuid.into(), - command: command.into(), + player_uuid, + command, }), ) .await @@ -290,8 +290,8 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::WorldSetDefaultGameMode(types::WorldSetDefaultGameModeAction { - world: world.map(|v| v.into()), - game_mode: game_mode.into(), + world, + game_mode, }), ) .await @@ -304,8 +304,8 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::WorldSetDifficulty(types::WorldSetDifficultyAction { - world: world.map(|v| v.into()), - difficulty: difficulty.into(), + world, + difficulty, }), ) .await @@ -318,8 +318,8 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::WorldSetTickRange(types::WorldSetTickRangeAction { - world: world.map(|v| v.into()), - tick_range: tick_range.into(), + world, + tick_range, }), ) .await @@ -333,9 +333,9 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::WorldSetBlock(types::WorldSetBlockAction { - world: world.map(|v| v.into()), - position: position.map(|v| v.into()), - block: block.map(|v| v.into()), + world, + position, + block, }), ) .await @@ -349,9 +349,9 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::WorldPlaySound(types::WorldPlaySoundAction { - world: world.map(|v| v.into()), - sound: sound.into(), - position: position.map(|v| v.into()), + world, + sound, + position, }), ) .await @@ -367,11 +367,11 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::WorldAddParticle(types::WorldAddParticleAction { - world: world.map(|v| v.into()), - position: position.map(|v| v.into()), - particle: particle.into(), - block: block.map(|v| v.into()), - face: face.map(|v| v.into()), + world, + position, + particle, + block, + face, }), ) .await @@ -383,7 +383,7 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::WorldQueryEntities(types::WorldQueryEntitiesAction { - world: world.map(|v| v.into()), + world, }), ) .await @@ -395,7 +395,7 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::WorldQueryPlayers(types::WorldQueryPlayersAction { - world: world.map(|v| v.into()), + world, }), ) .await @@ -408,8 +408,8 @@ impl Server { ) -> Result<(), mpsc::error::SendError> { self.send_action( types::action::Kind::WorldQueryEntitiesWithin(types::WorldQueryEntitiesWithinAction { - world: world.map(|v| v.into()), - r#box: r#box.map(|v| v.into()), + world, + r#box, }), ) .await diff --git a/packages/rust/xtask/src/generate_actions.rs b/packages/rust/xtask/src/generate_actions.rs index 6258c82..e6cd01c 100644 --- a/packages/rust/xtask/src/generate_actions.rs +++ b/packages/rust/xtask/src/generate_actions.rs @@ -4,9 +4,7 @@ use quote::{format_ident, quote}; use std::{collections::HashMap, path::PathBuf}; use syn::{File, Ident, ItemStruct}; -use crate::utils::{ - clean_type, find_nested_enum, get_variant_type_path, unwrap_option_path, write_formatted_file, -}; +use crate::utils::{clean_type, find_nested_enum, get_variant_type_path, write_formatted_file}; pub(crate) fn generate_server_helpers( ast: &File, @@ -33,17 +31,9 @@ pub(crate) fn generate_server_helpers( for field in &action_struct_def.fields { let field_name = field.ident.as_ref().unwrap(); - let (_inner_type, is_option) = unwrap_option_path(&field.ty); let arg_type = clean_type(&field.ty); - if is_option { - // If it's an Option, we must map the inner value - struct_fields.push(quote! { #field_name: #field_name.map(|v| v.into()) }); - } else { - // If it's not an Option, we can call .into() directly - struct_fields.push(quote! { #field_name: #field_name.into() }); - } - + struct_fields.push(quote! { #field_name }); fn_args.push(quote! { #field_name: #arg_type }); } diff --git a/packages/rust/xtask/src/generate_handlers.rs b/packages/rust/xtask/src/generate_handlers.rs index 4df2e89..b7e4a2f 100644 --- a/packages/rust/xtask/src/generate_handlers.rs +++ b/packages/rust/xtask/src/generate_handlers.rs @@ -47,10 +47,10 @@ pub fn generate_handler_trait(ast: &File, output_path: &PathBuf) -> Result<()> { let file = quote! { //! This file is auto-generated by `xtask`. Do not edit manually. #![allow(unused_variables)] - use crate::{event::EventContext, types, Server, PluginSubscriptions}; + use crate::{event::EventContext, types, Server, EventSubscriptions}; use std::future::Future; - pub trait PluginEventHandler: PluginSubscriptions + Send + Sync { + pub trait EventHandler: EventSubscriptions + Send + Sync { #(#event_handler_fns)* } diff --git a/packages/rust/xtask/src/generate_mutations.rs b/packages/rust/xtask/src/generate_mutations.rs index 1c9f41d..ac9f1f5 100644 --- a/packages/rust/xtask/src/generate_mutations.rs +++ b/packages/rust/xtask/src/generate_mutations.rs @@ -73,6 +73,7 @@ pub(crate) fn generate_event_mutations( let final_file = quote! { //! This file is auto-generated by `xtask`. Do not edit manually. + #![allow(clippy::all)] use crate::types; use crate::event::EventContext; From 0ba8e41be78eb960f9e737de7bbf0703b656c8dd Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Tue, 25 Nov 2025 12:08:21 -0500 Subject: [PATCH 15/22] Replace #[events()] with #[event_handler] on impl block, refactor crates, change names, LSP support working with proc macro etc. --- packages/rust/Cargo.toml | 15 +- packages/rust/README.md | 47 ++-- .../{rust-plugin-test => example}/Cargo.toml | 4 +- .../{rust-plugin-test => example}/src/main.rs | 8 +- .../{rust-plugin-macro => macro}/Cargo.toml | 3 +- packages/rust/macro/src/lib.rs | 109 ++++++++ .../src/plugin.rs | 0 packages/rust/rust-plugin-macro/src/events.rs | 46 ---- packages/rust/rust-plugin-macro/src/lib.rs | 47 ---- packages/rust/src/event/handler.rs | 253 +++++++----------- packages/rust/src/lib.rs | 16 +- packages/rust/xtask/src/generate_handlers.rs | 11 +- 12 files changed, 269 insertions(+), 290 deletions(-) rename packages/rust/{rust-plugin-test => example}/Cargo.toml (71%) rename packages/rust/{rust-plugin-test => example}/src/main.rs (90%) rename packages/rust/{rust-plugin-macro => macro}/Cargo.toml (92%) create mode 100644 packages/rust/macro/src/lib.rs rename packages/rust/{rust-plugin-macro => macro}/src/plugin.rs (100%) delete mode 100644 packages/rust/rust-plugin-macro/src/events.rs delete mode 100644 packages/rust/rust-plugin-macro/src/lib.rs diff --git a/packages/rust/Cargo.toml b/packages/rust/Cargo.toml index 4f226d0..9dd181d 100644 --- a/packages/rust/Cargo.toml +++ b/packages/rust/Cargo.toml @@ -1,13 +1,13 @@ [workspace] resolver = "3" -members = [".", "rust-plugin-macro", "rust-plugin-test", "xtask"] +members = [".", "macro", "example", "xtask"] [workspace.dependencies] -rust-plugin-macro = { path = "rust-plugin-macro", version = "0.1" } +dragonfly-plugin-macro = { path = "macro", version = "0.1" } [package] name = "dragonfly-plugin" -version = "0.1.0" +version = "1.0.0" edition = "2021" license = "MIT" repository = "https://github.com/secmc/dragonfly-plugins" @@ -27,4 +27,11 @@ tokio-stream = "0.1.17" tonic = { version = "0.12", features = ["transport"] } tower = "0.5" hyper-util = { version = "0.1", features = ["tokio"] } -rust-plugin-macro = { workspace = true, version = "0.1" } +dragonfly-plugin-macro = { workspace = true, version = "0.1", optional = true } + +[dev-dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } + +[features] +default = ["macros"] +macros = ["dep:dragonfly-plugin-macro"] diff --git a/packages/rust/README.md b/packages/rust/README.md index aa89570..a391b60 100644 --- a/packages/rust/README.md +++ b/packages/rust/README.md @@ -46,25 +46,26 @@ tokio = { version = "1", features = ["full"] } This is the complete code for a simple plugin that greets players on join and adds a prefix to their chat messages. -```rust +```rust,no_run // --- Import all the necessary items --- use dragonfly_plugin::{ - event::{EventContext, PluginEventHandler}, + event::{EventContext, EventHandler}, types, // Contains all event data structs - Handler, // The derive macro - Plugin, // The main plugin runner + Plugin, // The derive macro + event_handler, + PluginRunner, // The runner struct. Server, }; -// --- 1. Define Your Plugin Struct --- -// -// `#[derive(Handler)]` is the "trigger" that runs the macro. -// `#[derive(Default)]` is common for simple, stateless plugins. -#[derive(Handler, Default)] -// -// `#[subscriptions(...)]` is the "helper attribute" that lists -// all the events this plugin needs to listen for. -#[subscriptions(PlayerJoin, Chat)] +// make sure to derive Plugin, (Default isn't required but is used in this example code only.) +// when deriving Plugin you must include plugin attribute: +#[derive(Plugin, Default)] +#[plugin( + id = "example-rust", // A unique ID for your plugin (matches plugins.yaml) + name = "Example Rust Plugin", // A human-readable name + version = "1.0.0", // Your plugin's version + api = "1.0.0", // The API version you're built against +)] struct MyPlugin; // --- 2. Implement the Event Handlers --- @@ -72,7 +73,14 @@ struct MyPlugin; // This is where all your plugin's logic lives. // You only need to implement the `async fn` handlers // for the events you subscribed to. -impl PluginEventHandler for MyPlugin { +// note your LSP will probably fill them in as fn on_xx() -> Future<()> +// just delete the Future<()> + ... and put the keyword async before fn. +// +// #[event_handler] is a proc macro that detects which ever events you +// are overriding and thus setups a list of events to compile to +// as soon as your plugin is ran then it subscribes to them. +#[event_handler] +impl EventHandler for MyPlugin { /// This handler runs when a player joins the server. async fn on_player_join( &self, @@ -111,12 +119,11 @@ impl PluginEventHandler for MyPlugin { async fn main() { println!("Starting my-plugin..."); - // Create an instance of your plugin - let plugin = MyPlugin::default(); - - // Run the plugin. This will connect to the server - // and block forever, processing events. - Plugin::run(plugin, "127.0.0.1:50051") + // Here we default construct our Plugin. + // note you can use it almost like a Context variable as long + // as its Send / Sync. + // so you can not impl default and have it hold like a PgPool or etc. + PluginRunner::run(MyPlugin, "127.0.0.1:50051") .await .expect("Plugin failed to run"); } diff --git a/packages/rust/rust-plugin-test/Cargo.toml b/packages/rust/example/Cargo.toml similarity index 71% rename from packages/rust/rust-plugin-test/Cargo.toml rename to packages/rust/example/Cargo.toml index 44459d9..f7cc96a 100644 --- a/packages/rust/rust-plugin-test/Cargo.toml +++ b/packages/rust/example/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "rust-plugin-test" -version = "0.1.0" +name = "dragon-plugin-example" +version = "1.0.0" edition = "2024" [dependencies] diff --git a/packages/rust/rust-plugin-test/src/main.rs b/packages/rust/example/src/main.rs similarity index 90% rename from packages/rust/rust-plugin-test/src/main.rs rename to packages/rust/example/src/main.rs index c59a3a4..ad71c79 100644 --- a/packages/rust/rust-plugin-test/src/main.rs +++ b/packages/rust/example/src/main.rs @@ -3,6 +3,7 @@ use dragonfly_plugin::{ PluginRunner, Server, event::{EventContext, EventHandler}, + event_handler, types, // All the raw prost/tonic types }; @@ -20,10 +21,15 @@ use dragonfly_plugin::{ version = "1.0.0", // Your plugin's version api = "1.0.0", // The API version you're built against )] -#[events(PlayerJoin, Chat)] // A list of the events that you want to handle. struct MyExamplePlugin; // --- 3. Implement the event handlers --- +// #[event_handler] is our magic proc macro that +// detects which handlers you are implementing and +// automatically gathers which events to subscribe to. +// This replaces the prior #[events()] and impl EventSubscriptions +// and manually writing the list in the function definition. +#[event_handler] impl EventHandler for MyExamplePlugin { /// This handler runs when a player joins the server. /// We'll use it to send our "hello world" message. diff --git a/packages/rust/rust-plugin-macro/Cargo.toml b/packages/rust/macro/Cargo.toml similarity index 92% rename from packages/rust/rust-plugin-macro/Cargo.toml rename to packages/rust/macro/Cargo.toml index 8096be8..79a146c 100644 --- a/packages/rust/rust-plugin-macro/Cargo.toml +++ b/packages/rust/macro/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "rust-plugin-macro" +name = "dragonfly-plugin-macro" version = "0.1.0" edition = "2021" license = "MIT" @@ -14,6 +14,7 @@ categories = ["development-tools::procedural-macro-helpers"] proc-macro = true [dependencies] +heck = "0.5.0" proc-macro2 = "1.0.103" quote = "1.0" syn = { version = "2.0", features = ["full", "parsing", "visit"] } diff --git a/packages/rust/macro/src/lib.rs b/packages/rust/macro/src/lib.rs new file mode 100644 index 0000000..d059b1f --- /dev/null +++ b/packages/rust/macro/src/lib.rs @@ -0,0 +1,109 @@ +mod plugin; + +use heck::ToPascalCase; +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, Attribute, DeriveInput, ImplItem, ItemImpl}; + +use crate::plugin::generate_plugin_impl; + +#[proc_macro_derive(Plugin, attributes(plugin, events))] +pub fn handler_derive(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + + let derive_name = &ast.ident; + + let info_attr = match find_attribute( + &ast, + "plugin", + "Missing `#[plugin(...)]` attribute with metadata.", + ) { + Ok(attr) => attr, + Err(e) => return e.to_compile_error().into(), + }; + + let plugin_impl = generate_plugin_impl(info_attr, derive_name); + + quote! { + #plugin_impl + } + .into() +} + +fn find_attribute<'a>( + ast: &'a syn::DeriveInput, + name: &str, + error: &str, +) -> Result<&'a Attribute, syn::Error> { + ast.attrs + .iter() + .find(|a| a.path().is_ident(name)) + .ok_or_else(|| syn::Error::new(ast.ident.span(), error)) +} + +#[proc_macro_attribute] +pub fn event_handler(_attr: TokenStream, item: TokenStream) -> TokenStream { + let item_clone = item.clone(); + + // Try to parse the input tokens as an `impl` block + let impl_block = match syn::parse::(item) { + Ok(block) => block, + Err(_) => { + // Parse failed, which means the user is probably in the + // middle of typing. Return the original, un-parsed tokens + // to keep the LSP alive + return item_clone; + } + }; + + // ensure its our EventHandler. + let is_event_handler_impl = if let Some((_, trait_path, _)) = &impl_block.trait_ { + trait_path + .segments + .last() + .is_some_and(|segment| segment.ident == "EventHandler") + } else { + return item_clone; + }; + + if !is_event_handler_impl { + // This is an `impl` for some *other* trait. + // We shouldn't touch it. Return the original tokens. + return item_clone; + } + + let mut event_variants = Vec::new(); + for item in &impl_block.items { + if let ImplItem::Fn(method) = item { + let fn_name = method.sig.ident.to_string(); + + if let Some(event_name_snake) = fn_name.strip_prefix("on_") { + let event_name_pascal = event_name_snake.to_pascal_case(); + + let variant_ident = format_ident!("{}", event_name_pascal); + event_variants.push(quote! { types::EventType::#variant_ident }); + } + } + } + + let self_ty = &impl_block.self_ty; + + let subscriptions_impl = quote! { + impl dragonfly_plugin::EventSubscriptions for #self_ty { + fn get_subscriptions(&self) -> Vec { + vec![ + #( #event_variants ),* + ] + } + } + }; + + let original_impl_tokens = quote! { #impl_block }; + + let final_output = quote! { + #original_impl_tokens + #subscriptions_impl + }; + + final_output.into() +} diff --git a/packages/rust/rust-plugin-macro/src/plugin.rs b/packages/rust/macro/src/plugin.rs similarity index 100% rename from packages/rust/rust-plugin-macro/src/plugin.rs rename to packages/rust/macro/src/plugin.rs diff --git a/packages/rust/rust-plugin-macro/src/events.rs b/packages/rust/rust-plugin-macro/src/events.rs deleted file mode 100644 index 34024dd..0000000 --- a/packages/rust/rust-plugin-macro/src/events.rs +++ /dev/null @@ -1,46 +0,0 @@ -use quote::quote; -use syn::{ - parse::{Parse, ParseStream}, - punctuated::Punctuated, - Attribute, Ident, Token, -}; - -struct SubscriptionsListParser { - events: Vec, -} - -impl Parse for SubscriptionsListParser { - fn parse(input: ParseStream) -> syn::Result { - let punctuated_list: Punctuated = Punctuated::parse_terminated(input)?; - - let events = punctuated_list.into_iter().collect(); - - Ok(Self { events }) - } -} - -pub(crate) fn generate_event_subscriptions_inp( - attr: &Attribute, - derive_name: &Ident, -) -> proc_macro2::TokenStream { - let subscriptions = match attr.parse_args::() { - Ok(list) => list.events, - Err(e) => { - return e.to_compile_error(); - } - }; - - let subscription_variants = subscriptions.iter().map(|ident| { - quote! { types::EventType::#ident } - }); - - quote! { - impl dragonfly_plugin::EventSubscriptions for #derive_name { - fn get_subscriptions(&self) -> Vec { - vec![ - #( #subscription_variants ),* - ] - } - } - } -} diff --git a/packages/rust/rust-plugin-macro/src/lib.rs b/packages/rust/rust-plugin-macro/src/lib.rs deleted file mode 100644 index 61c74d5..0000000 --- a/packages/rust/rust-plugin-macro/src/lib.rs +++ /dev/null @@ -1,47 +0,0 @@ -mod events; -mod plugin; - -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, Attribute, DeriveInput}; - -use crate::{events::generate_event_subscriptions_inp, plugin::generate_plugin_impl}; - -#[proc_macro_derive(Plugin, attributes(plugin, events))] -pub fn handler_derive(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - - let derive_name = &ast.ident; - - let info_attr = find_attribute( - &ast, - "plugin", - "Missing `#[plugin(...)]` attribute with metadata.", - ); - - // generate the code for impling Plugin. - let plugin_impl = generate_plugin_impl(info_attr, derive_name); - - let subscription_attr = find_attribute( - &ast, - "events", - "Missing #[events(...)] attribute. Please list the events to \ - subscribe to, e.g., #[events(Chat, PlayerJoin)]", - ); - - let event_subscriptions_impl = generate_event_subscriptions_inp(subscription_attr, derive_name); - - quote! { - #plugin_impl - #event_subscriptions_impl - } - .into() -} - -fn find_attribute<'a>(ast: &'a syn::DeriveInput, name: &str, error: &str) -> &'a Attribute { - ast.attrs - .iter() - .find(|a| a.path().is_ident(name)) - .ok_or_else(|| syn::Error::new(ast.ident.span(), error)) - .unwrap() -} diff --git a/packages/rust/src/event/handler.rs b/packages/rust/src/event/handler.rs index 209b473..0f721f4 100644 --- a/packages/rust/src/event/handler.rs +++ b/packages/rust/src/event/handler.rs @@ -1,399 +1,344 @@ //! This file is auto-generated by `xtask`. Do not edit manually. -#![allow(unused_variables)] -use crate::{event::EventContext, types, EventSubscriptions, Server}; -use std::future::Future; +#![allow(async_fn_in_trait)] +use crate::{EventSubscriptions, Server, event::EventContext, types}; pub trait EventHandler: EventSubscriptions + Send + Sync { ///Handler for the `PlayerJoin` event. - fn on_player_join( + async fn on_player_join( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerJoinEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerQuit` event. - fn on_player_quit( + async fn on_player_quit( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerQuitEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerMove` event. - fn on_player_move( + async fn on_player_move( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerMoveEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerJump` event. - fn on_player_jump( + async fn on_player_jump( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerJumpEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerTeleport` event. - fn on_player_teleport( + async fn on_player_teleport( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerTeleportEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerChangeWorld` event. - fn on_player_change_world( + async fn on_player_change_world( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerChangeWorldEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerToggleSprint` event. - fn on_player_toggle_sprint( + async fn on_player_toggle_sprint( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerToggleSprintEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerToggleSneak` event. - fn on_player_toggle_sneak( + async fn on_player_toggle_sneak( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerToggleSneakEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `Chat` event. - fn on_chat( - &self, - _server: &Server, - _event: &mut EventContext<'_, types::ChatEvent>, - ) -> impl Future + Send { - std::future::ready(()) - } + async fn on_chat(&self, _server: &Server, _event: &mut EventContext<'_, types::ChatEvent>) {} ///Handler for the `PlayerFoodLoss` event. - fn on_player_food_loss( + async fn on_player_food_loss( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerFoodLossEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerHeal` event. - fn on_player_heal( + async fn on_player_heal( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerHealEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerHurt` event. - fn on_player_hurt( + async fn on_player_hurt( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerHurtEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerDeath` event. - fn on_player_death( + async fn on_player_death( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerDeathEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerRespawn` event. - fn on_player_respawn( + async fn on_player_respawn( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerRespawnEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerSkinChange` event. - fn on_player_skin_change( + async fn on_player_skin_change( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerSkinChangeEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerFireExtinguish` event. - fn on_player_fire_extinguish( + async fn on_player_fire_extinguish( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerFireExtinguishEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerStartBreak` event. - fn on_player_start_break( + async fn on_player_start_break( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerStartBreakEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `BlockBreak` event. - fn on_block_break( + async fn on_block_break( &self, _server: &Server, _event: &mut EventContext<'_, types::BlockBreakEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerBlockPlace` event. - fn on_player_block_place( + async fn on_player_block_place( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerBlockPlaceEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerBlockPick` event. - fn on_player_block_pick( + async fn on_player_block_pick( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerBlockPickEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerItemUse` event. - fn on_player_item_use( + async fn on_player_item_use( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerItemUseEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerItemUseOnBlock` event. - fn on_player_item_use_on_block( + async fn on_player_item_use_on_block( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerItemUseOnBlockEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerItemUseOnEntity` event. - fn on_player_item_use_on_entity( + async fn on_player_item_use_on_entity( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerItemUseOnEntityEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerItemRelease` event. - fn on_player_item_release( + async fn on_player_item_release( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerItemReleaseEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerItemConsume` event. - fn on_player_item_consume( + async fn on_player_item_consume( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerItemConsumeEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerAttackEntity` event. - fn on_player_attack_entity( + async fn on_player_attack_entity( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerAttackEntityEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerExperienceGain` event. - fn on_player_experience_gain( + async fn on_player_experience_gain( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerExperienceGainEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerPunchAir` event. - fn on_player_punch_air( + async fn on_player_punch_air( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerPunchAirEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerSignEdit` event. - fn on_player_sign_edit( + async fn on_player_sign_edit( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerSignEditEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerLecternPageTurn` event. - fn on_player_lectern_page_turn( + async fn on_player_lectern_page_turn( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerLecternPageTurnEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerItemDamage` event. - fn on_player_item_damage( + async fn on_player_item_damage( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerItemDamageEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerItemPickup` event. - fn on_player_item_pickup( + async fn on_player_item_pickup( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerItemPickupEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerHeldSlotChange` event. - fn on_player_held_slot_change( + async fn on_player_held_slot_change( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerHeldSlotChangeEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerItemDrop` event. - fn on_player_item_drop( + async fn on_player_item_drop( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerItemDropEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerTransfer` event. - fn on_player_transfer( + async fn on_player_transfer( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerTransferEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `Command` event. - fn on_command( + async fn on_command( &self, _server: &Server, _event: &mut EventContext<'_, types::CommandEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `PlayerDiagnostics` event. - fn on_player_diagnostics( + async fn on_player_diagnostics( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerDiagnosticsEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `WorldLiquidFlow` event. - fn on_world_liquid_flow( + async fn on_world_liquid_flow( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldLiquidFlowEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `WorldLiquidDecay` event. - fn on_world_liquid_decay( + async fn on_world_liquid_decay( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldLiquidDecayEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `WorldLiquidHarden` event. - fn on_world_liquid_harden( + async fn on_world_liquid_harden( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldLiquidHardenEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `WorldSound` event. - fn on_world_sound( + async fn on_world_sound( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldSoundEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `WorldFireSpread` event. - fn on_world_fire_spread( + async fn on_world_fire_spread( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldFireSpreadEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `WorldBlockBurn` event. - fn on_world_block_burn( + async fn on_world_block_burn( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldBlockBurnEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `WorldCropTrample` event. - fn on_world_crop_trample( + async fn on_world_crop_trample( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldCropTrampleEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `WorldLeavesDecay` event. - fn on_world_leaves_decay( + async fn on_world_leaves_decay( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldLeavesDecayEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `WorldEntitySpawn` event. - fn on_world_entity_spawn( + async fn on_world_entity_spawn( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldEntitySpawnEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `WorldEntityDespawn` event. - fn on_world_entity_despawn( + async fn on_world_entity_despawn( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldEntityDespawnEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `WorldExplosion` event. - fn on_world_explosion( + async fn on_world_explosion( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldExplosionEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } ///Handler for the `WorldClose` event. - fn on_world_close( + async fn on_world_close( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldCloseEvent>, - ) -> impl Future + Send { - std::future::ready(()) + ) { } } #[doc(hidden)] diff --git a/packages/rust/src/lib.rs b/packages/rust/src/lib.rs index dd4c0b2..385f747 100644 --- a/packages/rust/src/lib.rs +++ b/packages/rust/src/lib.rs @@ -21,10 +21,10 @@ use std::error::Error; pub use server::*; // main usage stuff for plugin devs: +pub use dragonfly_plugin_macro::{Plugin, event_handler}; pub use event::EventHandler; -pub use rust_plugin_macro::Plugin; use tokio::sync::mpsc; -use tokio_stream::{wrappers::ReceiverStream, StreamExt}; +use tokio_stream::{StreamExt, wrappers::ReceiverStream}; #[cfg(unix)] use hyper_util::rt::TokioIo; @@ -155,10 +155,10 @@ pub struct PluginInfo<'a> { /// `#[derive(Plugin)` like so: /// ```rust /// use dragonfly_plugin::{ -/// Dragonfly, // Our runtime, clearly named -/// EventHandler, // The logic trait +/// PluginRunner, // Our runtime, clearly named /// Plugin, // The derive macro -/// event_context::EventContext, +/// event::{EventContext, EventHandler}, +/// event_handler, /// types, /// Server, /// }; @@ -170,16 +170,16 @@ pub struct PluginInfo<'a> { /// version = "1.0.0", /// api = "1.0.0" /// )] -///#[events(PlayerJoin, Chat)] ///struct MyPlugin {} /// +///#[event_handler] ///impl EventHandler for MyPlugin { /// async fn on_player_join( /// &self, /// server: &Server, /// event: &mut EventContext<'_, types::PlayerJoinEvent>, -/// ) { -/// } +/// ) { } +/// } /// ``` pub trait Plugin: EventHandler + EventSubscriptions { fn get_info(&self) -> PluginInfo<'_>; diff --git a/packages/rust/xtask/src/generate_handlers.rs b/packages/rust/xtask/src/generate_handlers.rs index b7e4a2f..500d0f9 100644 --- a/packages/rust/xtask/src/generate_handlers.rs +++ b/packages/rust/xtask/src/generate_handlers.rs @@ -26,13 +26,11 @@ pub fn generate_handler_trait(ast: &File, output_path: &PathBuf) -> Result<()> { event_handler_fns.push(quote! { #[doc = #doc_string] - fn #handler_fn_name( + async fn #handler_fn_name( &self, _server: &Server, _event: &mut EventContext<'_, types::#ty_path>, - ) -> impl Future + Send { - std::future::ready(()) - } + ) { } }); dispatch_fn_match_arms.push(quote! { @@ -46,16 +44,15 @@ pub fn generate_handler_trait(ast: &File, output_path: &PathBuf) -> Result<()> { let file = quote! { //! This file is auto-generated by `xtask`. Do not edit manually. - #![allow(unused_variables)] + #![allow(async_fn_in_trait)] use crate::{event::EventContext, types, Server, EventSubscriptions}; - use std::future::Future; pub trait EventHandler: EventSubscriptions + Send + Sync { #(#event_handler_fns)* } #[doc(hidden)] - pub async fn dispatch_event(server: &Server, handler: &impl PluginEventHandler, envelope: &types::EventEnvelope) { + pub async fn dispatch_event(server: &Server, handler: &impl EventHandler, envelope: &types::EventEnvelope) { let Some(payload) = &envelope.payload else { return; }; From 9df4daac7aa21d9b62b1d2ab1e498cfcda83eb41 Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Tue, 25 Nov 2025 23:20:14 -0500 Subject: [PATCH 16/22] a lot: refactor xtask, unit and snapshot test xtask, bring back the old proc macro, different code gen etc. --- packages/rust/Cargo.toml | 10 +- packages/rust/example/Cargo.toml | 11 +- packages/rust/macro/Cargo.toml | 2 +- packages/rust/src/event/handler.rs | 2 +- packages/rust/src/event/mutations.rs | 90 ++-- packages/rust/src/lib.rs | 4 +- packages/rust/src/server/helpers.rs | 464 ++++++++-------- packages/rust/xtask/Cargo.toml | 3 + packages/rust/xtask/assets/mock_prost.rs | 88 +++ packages/rust/xtask/src/generate_actions.rs | 66 ++- packages/rust/xtask/src/generate_handlers.rs | 51 +- packages/rust/xtask/src/generate_mutations.rs | 63 ++- ...nerate_actions__tests__server_actions.snap | 37 ++ ...nerate_handlers__tests__event_handler.snap | 43 ++ ...ate_mutations__tests__event_mutations.snap | 28 + packages/rust/xtask/src/utils.rs | 510 +++++++++++++++--- 16 files changed, 1052 insertions(+), 420 deletions(-) create mode 100644 packages/rust/xtask/assets/mock_prost.rs create mode 100644 packages/rust/xtask/src/snapshots/xtask__generate_actions__tests__server_actions.snap create mode 100644 packages/rust/xtask/src/snapshots/xtask__generate_handlers__tests__event_handler.snap create mode 100644 packages/rust/xtask/src/snapshots/xtask__generate_mutations__tests__event_mutations.snap diff --git a/packages/rust/Cargo.toml b/packages/rust/Cargo.toml index 9dd181d..84b50a5 100644 --- a/packages/rust/Cargo.toml +++ b/packages/rust/Cargo.toml @@ -2,12 +2,16 @@ resolver = "3" members = [".", "macro", "example", "xtask"] +[profile.dev.package] +insta.opt-level = 3 +similar.opt-level = 3 + [workspace.dependencies] -dragonfly-plugin-macro = { path = "macro", version = "0.1" } +dragonfly-plugin-macro = { path = "macro", version = "0.2" } [package] name = "dragonfly-plugin" -version = "1.0.0" +version = "0.2.0" edition = "2021" license = "MIT" repository = "https://github.com/secmc/dragonfly-plugins" @@ -27,7 +31,7 @@ tokio-stream = "0.1.17" tonic = { version = "0.12", features = ["transport"] } tower = "0.5" hyper-util = { version = "0.1", features = ["tokio"] } -dragonfly-plugin-macro = { workspace = true, version = "0.1", optional = true } +dragonfly-plugin-macro = { workspace = true, optional = true } [dev-dependencies] tokio = { version = "1", features = ["rt-multi-thread", "macros"] } diff --git a/packages/rust/example/Cargo.toml b/packages/rust/example/Cargo.toml index f7cc96a..8f8c52c 100644 --- a/packages/rust/example/Cargo.toml +++ b/packages/rust/example/Cargo.toml @@ -1,8 +1,11 @@ [package] -name = "dragon-plugin-example" -version = "1.0.0" -edition = "2024" +name = "dragonfly-plugin-example" +version = "0.1.0" +edition = "2021" [dependencies] -dragonfly-plugin = { path = "../"} +# for your own plugins change this to be like: +# dragonfly-plugin = version or +# use cargo add dragonfly-plugin to get the latest one. +dragonfly-plugin = { path = "../" } tokio = { version = "1", features = ["full"] } diff --git a/packages/rust/macro/Cargo.toml b/packages/rust/macro/Cargo.toml index 79a146c..69dd178 100644 --- a/packages/rust/macro/Cargo.toml +++ b/packages/rust/macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dragonfly-plugin-macro" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "MIT" repository = "https://github.com/secmc/dragonfly-plugins" diff --git a/packages/rust/src/event/handler.rs b/packages/rust/src/event/handler.rs index 0f721f4..cb22f1f 100644 --- a/packages/rust/src/event/handler.rs +++ b/packages/rust/src/event/handler.rs @@ -1,6 +1,6 @@ //! This file is auto-generated by `xtask`. Do not edit manually. #![allow(async_fn_in_trait)] -use crate::{EventSubscriptions, Server, event::EventContext, types}; +use crate::{event::EventContext, types, EventSubscriptions, Server}; pub trait EventHandler: EventSubscriptions + Send + Sync { ///Handler for the `PlayerJoin` event. async fn on_player_join( diff --git a/packages/rust/src/event/mutations.rs b/packages/rust/src/event/mutations.rs index e32216d..ced2deb 100644 --- a/packages/rust/src/event/mutations.rs +++ b/packages/rust/src/event/mutations.rs @@ -1,12 +1,12 @@ //! This file is auto-generated by `xtask`. Do not edit manually. #![allow(clippy::all)] -use crate::types; use crate::event::EventContext; +use crate::types; impl<'a> EventContext<'a, types::ChatEvent> { ///Sets the `message` for this event. - pub fn set_message(&mut self, message: String) { + pub fn set_message(&mut self, message: impl Into>) { let mutation = types::ChatMutation { - message: Some(message.into()), + message: message.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::Chat(mutation)); @@ -14,19 +14,17 @@ impl<'a> EventContext<'a, types::ChatEvent> { } impl<'a> EventContext<'a, types::BlockBreakEvent> { ///Sets the `drops` for this event. - pub fn set_drops(&mut self, drops: Vec) { + pub fn set_drops(&mut self, drops: impl Into>) { let mutation = types::BlockBreakMutation { - drops: Some(types::ItemStackList { - items: drops.into_iter().map(|s| s.into()).collect(), - }), + drops: drops.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::BlockBreak(mutation)); } ///Sets the `xp` for this event. - pub fn set_xp(&mut self, xp: i32) { + pub fn set_xp(&mut self, xp: impl Into>) { let mutation = types::BlockBreakMutation { - xp: Some(xp.into()), + xp: xp.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::BlockBreak(mutation)); @@ -34,9 +32,9 @@ impl<'a> EventContext<'a, types::BlockBreakEvent> { } impl<'a> EventContext<'a, types::PlayerFoodLossEvent> { ///Sets the `to` for this event. - pub fn set_to(&mut self, to: i32) { + pub fn set_to(&mut self, to: impl Into>) { let mutation = types::PlayerFoodLossMutation { - to: Some(to.into()), + to: to.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::PlayerFoodLoss(mutation)); @@ -44,9 +42,9 @@ impl<'a> EventContext<'a, types::PlayerFoodLossEvent> { } impl<'a> EventContext<'a, types::PlayerHealEvent> { ///Sets the `amount` for this event. - pub fn set_amount(&mut self, amount: f64) { + pub fn set_amount(&mut self, amount: impl Into>) { let mutation = types::PlayerHealMutation { - amount: Some(amount.into()), + amount: amount.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::PlayerHeal(mutation)); @@ -54,17 +52,17 @@ impl<'a> EventContext<'a, types::PlayerHealEvent> { } impl<'a> EventContext<'a, types::PlayerHurtEvent> { ///Sets the `damage` for this event. - pub fn set_damage(&mut self, damage: f64) { + pub fn set_damage(&mut self, damage: impl Into>) { let mutation = types::PlayerHurtMutation { - damage: Some(damage.into()), + damage: damage.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::PlayerHurt(mutation)); } ///Sets the `attack_immunity_ms` for this event. - pub fn set_attack_immunity_ms(&mut self, attack_immunity_ms: i64) { + pub fn set_attack_immunity_ms(&mut self, attack_immunity_ms: impl Into>) { let mutation = types::PlayerHurtMutation { - attack_immunity_ms: Some(attack_immunity_ms.into()), + attack_immunity_ms: attack_immunity_ms.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::PlayerHurt(mutation)); @@ -72,9 +70,9 @@ impl<'a> EventContext<'a, types::PlayerHurtEvent> { } impl<'a> EventContext<'a, types::PlayerDeathEvent> { ///Sets the `keep_inventory` for this event. - pub fn set_keep_inventory(&mut self, keep_inventory: bool) { + pub fn set_keep_inventory(&mut self, keep_inventory: impl Into>) { let mutation = types::PlayerDeathMutation { - keep_inventory: Some(keep_inventory.into()), + keep_inventory: keep_inventory.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::PlayerDeath(mutation)); @@ -82,17 +80,17 @@ impl<'a> EventContext<'a, types::PlayerDeathEvent> { } impl<'a> EventContext<'a, types::PlayerRespawnEvent> { ///Sets the `position` for this event. - pub fn set_position(&mut self, position: types::Vec3) { + pub fn set_position(&mut self, position: impl Into>) { let mutation = types::PlayerRespawnMutation { - position: Some(position.into()), + position: position.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::PlayerRespawn(mutation)); } ///Sets the `world` for this event. - pub fn set_world(&mut self, world: types::WorldRef) { + pub fn set_world(&mut self, world: impl Into>) { let mutation = types::PlayerRespawnMutation { - world: Some(world.into()), + world: world.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::PlayerRespawn(mutation)); @@ -100,25 +98,25 @@ impl<'a> EventContext<'a, types::PlayerRespawnEvent> { } impl<'a> EventContext<'a, types::PlayerAttackEntityEvent> { ///Sets the `force` for this event. - pub fn set_force(&mut self, force: f64) { + pub fn set_force(&mut self, force: impl Into>) { let mutation = types::PlayerAttackEntityMutation { - force: Some(force.into()), + force: force.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::PlayerAttackEntity(mutation)); } ///Sets the `height` for this event. - pub fn set_height(&mut self, height: f64) { + pub fn set_height(&mut self, height: impl Into>) { let mutation = types::PlayerAttackEntityMutation { - height: Some(height.into()), + height: height.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::PlayerAttackEntity(mutation)); } ///Sets the `critical` for this event. - pub fn set_critical(&mut self, critical: bool) { + pub fn set_critical(&mut self, critical: impl Into>) { let mutation = types::PlayerAttackEntityMutation { - critical: Some(critical.into()), + critical: critical.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::PlayerAttackEntity(mutation)); @@ -126,9 +124,9 @@ impl<'a> EventContext<'a, types::PlayerAttackEntityEvent> { } impl<'a> EventContext<'a, types::PlayerExperienceGainEvent> { ///Sets the `amount` for this event. - pub fn set_amount(&mut self, amount: i32) { + pub fn set_amount(&mut self, amount: impl Into>) { let mutation = types::PlayerExperienceGainMutation { - amount: Some(amount.into()), + amount: amount.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::PlayerExperienceGain(mutation)); @@ -136,9 +134,9 @@ impl<'a> EventContext<'a, types::PlayerExperienceGainEvent> { } impl<'a> EventContext<'a, types::PlayerLecternPageTurnEvent> { ///Sets the `new_page` for this event. - pub fn set_new_page(&mut self, new_page: i32) { + pub fn set_new_page(&mut self, new_page: impl Into>) { let mutation = types::PlayerLecternPageTurnMutation { - new_page: Some(new_page.into()), + new_page: new_page.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::PlayerLecternPageTurn(mutation)); @@ -146,9 +144,9 @@ impl<'a> EventContext<'a, types::PlayerLecternPageTurnEvent> { } impl<'a> EventContext<'a, types::PlayerItemPickupEvent> { ///Sets the `item` for this event. - pub fn set_item(&mut self, item: types::ItemStack) { + pub fn set_item(&mut self, item: impl Into>) { let mutation = types::PlayerItemPickupMutation { - item: Some(item.into()), + item: item.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::PlayerItemPickup(mutation)); @@ -156,9 +154,9 @@ impl<'a> EventContext<'a, types::PlayerItemPickupEvent> { } impl<'a> EventContext<'a, types::PlayerTransferEvent> { ///Sets the `address` for this event. - pub fn set_address(&mut self, address: types::Address) { + pub fn set_address(&mut self, address: impl Into>) { let mutation = types::PlayerTransferMutation { - address: Some(address.into()), + address: address.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::PlayerTransfer(mutation)); @@ -166,35 +164,33 @@ impl<'a> EventContext<'a, types::PlayerTransferEvent> { } impl<'a> EventContext<'a, types::WorldExplosionEvent> { ///Sets the `entity_uuids` for this event. - pub fn set_entity_uuids(&mut self, entity_uuids: Vec) { + pub fn set_entity_uuids(&mut self, entity_uuids: impl Into>) { let mutation = types::WorldExplosionMutation { - entity_uuids: Some(types::StringList { - values: entity_uuids.into_iter().map(|s| s.into()).collect(), - }), + entity_uuids: entity_uuids.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::WorldExplosion(mutation)); } ///Sets the `blocks` for this event. - pub fn set_blocks(&mut self, blocks: types::BlockPosList) { + pub fn set_blocks(&mut self, blocks: impl Into>) { let mutation = types::WorldExplosionMutation { - blocks: Some(blocks.into()), + blocks: blocks.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::WorldExplosion(mutation)); } ///Sets the `item_drop_chance` for this event. - pub fn set_item_drop_chance(&mut self, item_drop_chance: f64) { + pub fn set_item_drop_chance(&mut self, item_drop_chance: impl Into>) { let mutation = types::WorldExplosionMutation { - item_drop_chance: Some(item_drop_chance.into()), + item_drop_chance: item_drop_chance.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::WorldExplosion(mutation)); } ///Sets the `spawn_fire` for this event. - pub fn set_spawn_fire(&mut self, spawn_fire: bool) { + pub fn set_spawn_fire(&mut self, spawn_fire: impl Into>) { let mutation = types::WorldExplosionMutation { - spawn_fire: Some(spawn_fire.into()), + spawn_fire: spawn_fire.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::WorldExplosion(mutation)); diff --git a/packages/rust/src/lib.rs b/packages/rust/src/lib.rs index 385f747..c157ad6 100644 --- a/packages/rust/src/lib.rs +++ b/packages/rust/src/lib.rs @@ -21,10 +21,10 @@ use std::error::Error; pub use server::*; // main usage stuff for plugin devs: -pub use dragonfly_plugin_macro::{Plugin, event_handler}; +pub use dragonfly_plugin_macro::{event_handler, Plugin}; pub use event::EventHandler; use tokio::sync::mpsc; -use tokio_stream::{StreamExt, wrappers::ReceiverStream}; +use tokio_stream::{wrappers::ReceiverStream, StreamExt}; #[cfg(unix)] use hyper_util::rt::TokioIo; diff --git a/packages/rust/src/server/helpers.rs b/packages/rust/src/server/helpers.rs index 851fc36..9d66a16 100644 --- a/packages/rust/src/server/helpers.rs +++ b/packages/rust/src/server/helpers.rs @@ -8,29 +8,25 @@ impl Server { target_uuid: String, message: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SendChat(types::SendChatAction { - target_uuid, - message, - }), - ) - .await + self.send_action(types::action::Kind::SendChat(types::SendChatAction { + target_uuid, + message, + })) + .await } ///Sends a `Teleport` action to the server. pub async fn teleport( &self, player_uuid: String, - position: Option, - rotation: Option, + position: impl Into>, + rotation: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::Teleport(types::TeleportAction { - player_uuid, - position, - rotation, - }), - ) - .await + self.send_action(types::action::Kind::Teleport(types::TeleportAction { + player_uuid, + position: position.into(), + rotation: rotation.into(), + })) + .await } ///Sends a `Kick` action to the server. pub async fn kick( @@ -38,85 +34,73 @@ impl Server { player_uuid: String, reason: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::Kick(types::KickAction { - player_uuid, - reason, - }), - ) - .await + self.send_action(types::action::Kind::Kick(types::KickAction { + player_uuid, + reason, + })) + .await } ///Sends a `SetGameMode` action to the server. pub async fn set_game_mode( &self, player_uuid: String, - game_mode: i32, + game_mode: types::GameMode, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetGameMode(types::SetGameModeAction { - player_uuid, - game_mode, - }), - ) - .await + self.send_action(types::action::Kind::SetGameMode(types::SetGameModeAction { + player_uuid, + game_mode: game_mode.into(), + })) + .await } ///Sends a `GiveItem` action to the server. pub async fn give_item( &self, player_uuid: String, - item: Option, + item: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::GiveItem(types::GiveItemAction { - player_uuid, - item, - }), - ) - .await + self.send_action(types::action::Kind::GiveItem(types::GiveItemAction { + player_uuid, + item: item.into(), + })) + .await } ///Sends a `ClearInventory` action to the server. pub async fn clear_inventory( &self, player_uuid: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::ClearInventory(types::ClearInventoryAction { - player_uuid, - }), - ) - .await + self.send_action(types::action::Kind::ClearInventory( + types::ClearInventoryAction { player_uuid }, + )) + .await } ///Sends a `SetHeldItem` action to the server. pub async fn set_held_item( &self, player_uuid: String, - main: Option, - offhand: Option, + main: impl Into>, + offhand: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetHeldItem(types::SetHeldItemAction { - player_uuid, - main, - offhand, - }), - ) - .await + self.send_action(types::action::Kind::SetHeldItem(types::SetHeldItemAction { + player_uuid, + main: main.into(), + offhand: offhand.into(), + })) + .await } ///Sends a `SetHealth` action to the server. pub async fn set_health( &self, player_uuid: String, health: f64, - max_health: Option, + max_health: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetHealth(types::SetHealthAction { - player_uuid, - health, - max_health, - }), - ) - .await + self.send_action(types::action::Kind::SetHealth(types::SetHealthAction { + player_uuid, + health, + max_health: max_health.into(), + })) + .await } ///Sends a `SetFood` action to the server. pub async fn set_food( @@ -124,101 +108,93 @@ impl Server { player_uuid: String, food: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetFood(types::SetFoodAction { - player_uuid, - food, - }), - ) - .await + self.send_action(types::action::Kind::SetFood(types::SetFoodAction { + player_uuid, + food, + })) + .await } ///Sends a `SetExperience` action to the server. pub async fn set_experience( &self, player_uuid: String, - level: Option, - progress: Option, - amount: Option, + level: impl Into>, + progress: impl Into>, + amount: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetExperience(types::SetExperienceAction { - player_uuid, - level, - progress, - amount, - }), - ) - .await + self.send_action(types::action::Kind::SetExperience( + types::SetExperienceAction { + player_uuid, + level: level.into(), + progress: progress.into(), + amount: amount.into(), + }, + )) + .await } ///Sends a `SetVelocity` action to the server. pub async fn set_velocity( &self, player_uuid: String, - velocity: Option, + velocity: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetVelocity(types::SetVelocityAction { - player_uuid, - velocity, - }), - ) - .await + self.send_action(types::action::Kind::SetVelocity(types::SetVelocityAction { + player_uuid, + velocity: velocity.into(), + })) + .await } ///Sends a `AddEffect` action to the server. pub async fn add_effect( &self, player_uuid: String, - effect_type: i32, + effect_type: types::EffectType, level: i32, duration_ms: i64, show_particles: bool, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::AddEffect(types::AddEffectAction { - player_uuid, - effect_type, - level, - duration_ms, - show_particles, - }), - ) - .await + self.send_action(types::action::Kind::AddEffect(types::AddEffectAction { + player_uuid, + effect_type: effect_type.into(), + level, + duration_ms, + show_particles, + })) + .await } ///Sends a `RemoveEffect` action to the server. pub async fn remove_effect( &self, player_uuid: String, - effect_type: i32, + effect_type: types::EffectType, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::RemoveEffect(types::RemoveEffectAction { - player_uuid, - effect_type, - }), - ) - .await + self.send_action(types::action::Kind::RemoveEffect( + types::RemoveEffectAction { + player_uuid, + effect_type: effect_type.into(), + }, + )) + .await } ///Sends a `SendTitle` action to the server. pub async fn send_title( &self, player_uuid: String, title: String, - subtitle: Option, - fade_in_ms: Option, - duration_ms: Option, - fade_out_ms: Option, + subtitle: impl Into>, + fade_in_ms: impl Into>, + duration_ms: impl Into>, + fade_out_ms: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SendTitle(types::SendTitleAction { - player_uuid, - title, - subtitle, - fade_in_ms, - duration_ms, - fade_out_ms, - }), - ) - .await + self.send_action(types::action::Kind::SendTitle(types::SendTitleAction { + player_uuid, + title, + subtitle: subtitle.into(), + fade_in_ms: fade_in_ms.into(), + duration_ms: duration_ms.into(), + fade_out_ms: fade_out_ms.into(), + })) + .await } ///Sends a `SendPopup` action to the server. pub async fn send_popup( @@ -226,13 +202,11 @@ impl Server { player_uuid: String, message: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SendPopup(types::SendPopupAction { - player_uuid, - message, - }), - ) - .await + self.send_action(types::action::Kind::SendPopup(types::SendPopupAction { + player_uuid, + message, + })) + .await } ///Sends a `SendTip` action to the server. pub async fn send_tip( @@ -240,33 +214,29 @@ impl Server { player_uuid: String, message: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SendTip(types::SendTipAction { - player_uuid, - message, - }), - ) - .await + self.send_action(types::action::Kind::SendTip(types::SendTipAction { + player_uuid, + message, + })) + .await } ///Sends a `PlaySound` action to the server. pub async fn play_sound( &self, player_uuid: String, - sound: i32, - position: Option, - volume: Option, - pitch: Option, + sound: types::Sound, + position: impl Into>, + volume: impl Into>, + pitch: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::PlaySound(types::PlaySoundAction { - player_uuid, - sound, - position, - volume, - pitch, - }), - ) - .await + self.send_action(types::action::Kind::PlaySound(types::PlaySoundAction { + player_uuid, + sound: sound.into(), + position: position.into(), + volume: volume.into(), + pitch: pitch.into(), + })) + .await } ///Sends a `ExecuteCommand` action to the server. pub async fn execute_command( @@ -274,144 +244,144 @@ impl Server { player_uuid: String, command: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::ExecuteCommand(types::ExecuteCommandAction { - player_uuid, - command, - }), - ) - .await + self.send_action(types::action::Kind::ExecuteCommand( + types::ExecuteCommandAction { + player_uuid, + command, + }, + )) + .await } ///Sends a `WorldSetDefaultGameMode` action to the server. pub async fn world_set_default_game_mode( &self, - world: Option, - game_mode: i32, + world: impl Into>, + game_mode: types::GameMode, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldSetDefaultGameMode(types::WorldSetDefaultGameModeAction { - world, - game_mode, - }), - ) - .await + self.send_action(types::action::Kind::WorldSetDefaultGameMode( + types::WorldSetDefaultGameModeAction { + world: world.into(), + game_mode: game_mode.into(), + }, + )) + .await } ///Sends a `WorldSetDifficulty` action to the server. pub async fn world_set_difficulty( &self, - world: Option, - difficulty: i32, + world: impl Into>, + difficulty: types::Difficulty, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldSetDifficulty(types::WorldSetDifficultyAction { - world, - difficulty, - }), - ) - .await + self.send_action(types::action::Kind::WorldSetDifficulty( + types::WorldSetDifficultyAction { + world: world.into(), + difficulty: difficulty.into(), + }, + )) + .await } ///Sends a `WorldSetTickRange` action to the server. pub async fn world_set_tick_range( &self, - world: Option, + world: impl Into>, tick_range: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldSetTickRange(types::WorldSetTickRangeAction { - world, - tick_range, - }), - ) - .await + self.send_action(types::action::Kind::WorldSetTickRange( + types::WorldSetTickRangeAction { + world: world.into(), + tick_range, + }, + )) + .await } ///Sends a `WorldSetBlock` action to the server. pub async fn world_set_block( &self, - world: Option, - position: Option, - block: Option, + world: impl Into>, + position: impl Into>, + block: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldSetBlock(types::WorldSetBlockAction { - world, - position, - block, - }), - ) - .await + self.send_action(types::action::Kind::WorldSetBlock( + types::WorldSetBlockAction { + world: world.into(), + position: position.into(), + block: block.into(), + }, + )) + .await } ///Sends a `WorldPlaySound` action to the server. pub async fn world_play_sound( &self, - world: Option, - sound: i32, - position: Option, + world: impl Into>, + sound: types::Sound, + position: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldPlaySound(types::WorldPlaySoundAction { - world, - sound, - position, - }), - ) - .await + self.send_action(types::action::Kind::WorldPlaySound( + types::WorldPlaySoundAction { + world: world.into(), + sound: sound.into(), + position: position.into(), + }, + )) + .await } ///Sends a `WorldAddParticle` action to the server. pub async fn world_add_particle( &self, - world: Option, - position: Option, - particle: i32, - block: Option, - face: Option, + world: impl Into>, + position: impl Into>, + particle: types::ParticleType, + block: impl Into>, + face: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldAddParticle(types::WorldAddParticleAction { - world, - position, - particle, - block, - face, - }), - ) - .await + self.send_action(types::action::Kind::WorldAddParticle( + types::WorldAddParticleAction { + world: world.into(), + position: position.into(), + particle: particle.into(), + block: block.into(), + face: face.into(), + }, + )) + .await } ///Sends a `WorldQueryEntities` action to the server. pub async fn world_query_entities( &self, - world: Option, + world: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldQueryEntities(types::WorldQueryEntitiesAction { - world, - }), - ) - .await + self.send_action(types::action::Kind::WorldQueryEntities( + types::WorldQueryEntitiesAction { + world: world.into(), + }, + )) + .await } ///Sends a `WorldQueryPlayers` action to the server. pub async fn world_query_players( &self, - world: Option, + world: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldQueryPlayers(types::WorldQueryPlayersAction { - world, - }), - ) - .await + self.send_action(types::action::Kind::WorldQueryPlayers( + types::WorldQueryPlayersAction { + world: world.into(), + }, + )) + .await } ///Sends a `WorldQueryEntitiesWithin` action to the server. pub async fn world_query_entities_within( &self, - world: Option, - r#box: Option, + world: impl Into>, + r#box: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldQueryEntitiesWithin(types::WorldQueryEntitiesWithinAction { - world, - r#box, - }), - ) - .await + self.send_action(types::action::Kind::WorldQueryEntitiesWithin( + types::WorldQueryEntitiesWithinAction { + world: world.into(), + r#box: r#box.into(), + }, + )) + .await } } diff --git a/packages/rust/xtask/Cargo.toml b/packages/rust/xtask/Cargo.toml index 614b497..5977ef1 100644 --- a/packages/rust/xtask/Cargo.toml +++ b/packages/rust/xtask/Cargo.toml @@ -12,3 +12,6 @@ heck = "0.5" # For to_snake_case anyhow = "1.0" prettyplease = "0.2.37" proc-macro2 = "1.0.103" + +[dev-dependencies] +insta = { version = "1.44.1" } diff --git a/packages/rust/xtask/assets/mock_prost.rs b/packages/rust/xtask/assets/mock_prost.rs new file mode 100644 index 0000000..e9ea681 --- /dev/null +++ b/packages/rust/xtask/assets/mock_prost.rs @@ -0,0 +1,88 @@ +pub struct Vec3 { + x: f64, + y: f64, + z: f64, +} + +// We need `Clone` for the test helpers +#[derive(Clone)] +pub struct ItemStack { + name: String, + count: i32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum GameMode { + Survival = 0, + Creative = 1, +} + +// A mock action struct +#[derive(Clone, PartialEq)] +pub struct SetGameModeAction { + #[prost(string, tag = "1")] + pub player_uuid: String, + #[prost(enumeration = "GameMode", tag = "2")] + pub game_mode: i32, +} + +// Another mock action struct +#[derive(Clone, PartialEq)] +pub struct GiveItemAction { + #[prost(string, tag = "1")] + pub player_uuid: String, + #[prost(message, optional, tag = "2")] + pub item: ::core::option::Option, +} + +// Mock action enum +#[allow(dead_code)] +mod action { + pub enum Kind { + SetGameMode(super::SetGameModeAction), + GiveItem(super::GiveItemAction), + } +} + +#[derive(Clone, PartialEq)] +pub struct ChatEvent { + #[prost(string, tag = "1")] + pub player_uuid: String, + #[prost(string, tag = "2")] + pub message: String, +} + +#[derive(Clone, PartialEq)] +pub struct BlockBreakEvent { + #[prost(string, tag = "1")] + pub player_uuid: String, +} + +#[allow(dead_code)] +mod event_envelope { + pub enum Payload { + Chat(super::ChatEvent), + BlockBreak(super::BlockBreakEvent), + } +} + +#[derive(Clone, PartialEq)] +pub struct ChatMutation { + #[prost(message, optional, tag = "1")] + pub message: ::core::option::Option, +} + +#[derive(Clone, PartialEq)] +pub struct BlockBreakMutation { + #[prost(message, optional, tag = "1")] + pub drops: ::core::option::Option, +} + +// The other missing enum your test is looking for! +#[allow(dead_code)] +mod event_result { + pub enum Update { + Chat(super::ChatMutation), + BlockBreak(super::BlockBreakMutation), + } +} diff --git a/packages/rust/xtask/src/generate_actions.rs b/packages/rust/xtask/src/generate_actions.rs index e6cd01c..9b0565e 100644 --- a/packages/rust/xtask/src/generate_actions.rs +++ b/packages/rust/xtask/src/generate_actions.rs @@ -4,13 +4,29 @@ use quote::{format_ident, quote}; use std::{collections::HashMap, path::PathBuf}; use syn::{File, Ident, ItemStruct}; -use crate::utils::{clean_type, find_nested_enum, get_variant_type_path, write_formatted_file}; +use crate::utils::{ + find_nested_enum, get_action_conversion_logic, get_api_type, get_variant_type_path, + prettify_code, ConversionLogic, +}; pub(crate) fn generate_server_helpers( ast: &File, all_structs: &HashMap, output_path: &PathBuf, ) -> Result<()> { + let code = generate_server_helpers_tokens(ast, all_structs)?; + + let file = prettify_code(code)?; + + std::fs::write(output_path, file)?; + + Ok(()) +} + +fn generate_server_helpers_tokens( + ast: &File, + all_structs: &HashMap, +) -> Result { let action_kind_enum = find_nested_enum(ast, "action", "Kind")?; let mut server_helpers = Vec::new(); @@ -31,10 +47,21 @@ pub(crate) fn generate_server_helpers( for field in &action_struct_def.fields { let field_name = field.ident.as_ref().unwrap(); - let arg_type = clean_type(&field.ty); + let arg_type = get_api_type(field); + let conversion_logic = get_action_conversion_logic(field); + + let struct_field_code = match conversion_logic { + ConversionLogic::Direct => { + quote! { #field_name } + } + // It's an enum or Option. Use the explicit conversion + ConversionLogic::Into => { + quote! { #field_name: #field_name.into() } + } + }; - struct_fields.push(quote! { #field_name }); fn_args.push(quote! { #field_name: #arg_type }); + struct_fields.push(quote! { #struct_field_code }); } server_helpers.push(quote! { @@ -52,7 +79,7 @@ pub(crate) fn generate_server_helpers( }); } - let final_file = quote! { + Ok(quote! { //! This file is auto-generated by `xtask`. Do not edit manually. use crate::{types, Server}; use tokio::sync::mpsc; @@ -60,7 +87,34 @@ pub(crate) fn generate_server_helpers( impl Server { #( #server_helpers )* } - }; + } + .to_string()) +} - write_formatted_file(output_path, final_file.to_string()) +#[cfg(test)] +mod tests { + use crate::utils::find_all_structs; + + use super::*; // Import your generator functions + use syn::{parse_file, File}; + + fn setup_test_ast() -> File { + let mock_code = include_str!("../assets/mock_prost.rs"); + + parse_file(mock_code).expect("Failed to parse mock AST") + } + + #[test] + fn snapshot_test_generate_server_actions() { + let ast = setup_test_ast(); + + let all_structs = find_all_structs(&ast); + + let generated_code = + generate_server_helpers_tokens(&ast, &all_structs).expect("Generator function failed"); + + let prettified_code = prettify_code(generated_code).expect("Invalid code being produced."); + + insta::assert_snapshot!("server_actions", prettified_code); + } } diff --git a/packages/rust/xtask/src/generate_handlers.rs b/packages/rust/xtask/src/generate_handlers.rs index 500d0f9..55ce1fe 100644 --- a/packages/rust/xtask/src/generate_handlers.rs +++ b/packages/rust/xtask/src/generate_handlers.rs @@ -4,7 +4,7 @@ use quote::{format_ident, quote}; use std::path::PathBuf; use syn::File; -use crate::utils::{find_nested_enum, get_variant_type_path, write_formatted_file}; +use crate::utils::{find_nested_enum, get_variant_type_path, prettify_code}; pub fn generate_handler_trait(ast: &File, output_path: &PathBuf) -> Result<()> { println!( @@ -12,6 +12,21 @@ pub fn generate_handler_trait(ast: &File, output_path: &PathBuf) -> Result<()> { output_path.to_string_lossy() ); + let code = generate_handler_trait_tokens(ast)?; + + // write our generated code. + let file = prettify_code(code)?; + + std::fs::write(output_path, file)?; + + println!( + "Successfully generated Event Handler Trait in: {}.", + output_path.to_string_lossy() + ); + Ok(()) +} + +fn generate_handler_trait_tokens(ast: &File) -> Result { let event_payload_enum = find_nested_enum(ast, "event_envelope", "Payload")?; let mut event_handler_fns = Vec::new(); @@ -42,7 +57,7 @@ pub fn generate_handler_trait(ast: &File, output_path: &PathBuf) -> Result<()> { }) } - let file = quote! { + Ok(quote! { //! This file is auto-generated by `xtask`. Do not edit manually. #![allow(async_fn_in_trait)] use crate::{event::EventContext, types, Server, EventSubscriptions}; @@ -60,13 +75,29 @@ pub fn generate_handler_trait(ast: &File, output_path: &PathBuf) -> Result<()> { #(#dispatch_fn_match_arms)* } } - }; + }.to_string()) +} - // write our generated code. - write_formatted_file(output_path, file.to_string())?; - println!( - "Successfully generated Event Handler Trait in: {}.", - output_path.to_string_lossy() - ); - Ok(()) +#[cfg(test)] +mod tests { + use super::*; // Import your generator functions + use syn::{parse_file, File}; + + fn setup_test_ast() -> File { + let mock_code = include_str!("../assets/mock_prost.rs"); + + parse_file(mock_code).expect("Failed to parse mock AST") + } + + #[test] + fn snapshot_test_generate_event_handler() { + let ast = setup_test_ast(); + + let generated_code = + generate_handler_trait_tokens(&ast).expect("Generator function failed"); + + let prettified_code = prettify_code(generated_code).expect("Invalid code being produced."); + + insta::assert_snapshot!("event_handler", prettified_code); + } } diff --git a/packages/rust/xtask/src/generate_mutations.rs b/packages/rust/xtask/src/generate_mutations.rs index ac9f1f5..b5e18dc 100644 --- a/packages/rust/xtask/src/generate_mutations.rs +++ b/packages/rust/xtask/src/generate_mutations.rs @@ -3,16 +3,26 @@ use quote::{format_ident, quote}; use std::{collections::HashMap, path::PathBuf}; use syn::{File, Ident, ItemStruct}; -use crate::utils::{ - clean_type, find_nested_enum, generate_conversion_code, get_variant_type_path, - unwrap_option_path, write_formatted_file, -}; +use crate::utils::{find_nested_enum, get_api_type, get_variant_type_path, prettify_code}; pub(crate) fn generate_event_mutations( ast: &File, all_structs: &HashMap, output_path: &PathBuf, ) -> Result<()> { + let code = generate_event_mutations_tokens(ast, all_structs)?; + + let file = prettify_code(code)?; + + std::fs::write(output_path, file)?; + + Ok(()) +} + +fn generate_event_mutations_tokens( + ast: &File, + all_structs: &HashMap, +) -> Result { let mutation_enum = find_nested_enum(ast, "event_result", "Update")?; let event_payload_enum = find_nested_enum(ast, "event_envelope", "Payload")?; let mut mutation_impls = Vec::new(); @@ -40,23 +50,15 @@ pub(crate) fn generate_event_mutations( let mut helper_methods = Vec::new(); for field in &mutation_struct_def.fields { let field_name = field.ident.as_ref().unwrap(); - let (inner_type, is_option) = unwrap_option_path(&field.ty); - - if !is_option { - continue; - } - - let arg_type = clean_type(inner_type); + let arg_type = get_api_type(field); let setter_fn_name = format_ident!("set_{}", field_name); let doc_string = format!("Sets the `{}` for this event.", field_name); - let convert_code = generate_conversion_code("e! { #field_name }, inner_type); - helper_methods.push(quote! { #[doc = #doc_string] pub fn #setter_fn_name(&mut self, #field_name: #arg_type) { let mutation = types::#mutation_struct_name { - #field_name: Some(#convert_code), + #field_name: #field_name.into(), ..Default::default() }; self.set_mutation(types::EventResultUpdate::#mutation_variant_name(mutation)); @@ -71,14 +73,41 @@ pub(crate) fn generate_event_mutations( }); } - let final_file = quote! { + Ok(quote! { //! This file is auto-generated by `xtask`. Do not edit manually. #![allow(clippy::all)] use crate::types; use crate::event::EventContext; #( #mutation_impls )* - }; + } + .to_string()) +} + +#[cfg(test)] +mod tests { + use crate::utils::find_all_structs; + + use super::*; // Import your generator functions + use syn::{parse_file, File}; + + fn setup_test_ast() -> File { + let mock_code = include_str!("../assets/mock_prost.rs"); + + parse_file(mock_code).expect("Failed to parse mock AST") + } - write_formatted_file(output_path, final_file.to_string()) + #[test] + fn snapshot_test_generate_event_mutations() { + let ast = setup_test_ast(); + + let all_structs = find_all_structs(&ast); + + let generated_code = + generate_event_mutations_tokens(&ast, &all_structs).expect("Generator function failed"); + + let prettified_code = prettify_code(generated_code).expect("Invalid code being produced."); + + insta::assert_snapshot!("event_mutations", prettified_code); + } } diff --git a/packages/rust/xtask/src/snapshots/xtask__generate_actions__tests__server_actions.snap b/packages/rust/xtask/src/snapshots/xtask__generate_actions__tests__server_actions.snap new file mode 100644 index 0000000..4e23c25 --- /dev/null +++ b/packages/rust/xtask/src/snapshots/xtask__generate_actions__tests__server_actions.snap @@ -0,0 +1,37 @@ +--- +source: xtask/src/generate_actions.rs +expression: prettified_code +--- +//! This file is auto-generated by `xtask`. Do not edit manually. +use crate::{types, Server}; +use tokio::sync::mpsc; +impl Server { + ///Sends a `SetGameMode` action to the server. + pub async fn set_game_mode( + &self, + player_uuid: String, + game_mode: types::GameMode, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::SetGameMode(types::SetGameModeAction { + player_uuid, + game_mode: game_mode.into(), + }), + ) + .await + } + ///Sends a `GiveItem` action to the server. + pub async fn give_item( + &self, + player_uuid: String, + item: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action( + types::action::Kind::GiveItem(types::GiveItemAction { + player_uuid, + item: item.into(), + }), + ) + .await + } +} diff --git a/packages/rust/xtask/src/snapshots/xtask__generate_handlers__tests__event_handler.snap b/packages/rust/xtask/src/snapshots/xtask__generate_handlers__tests__event_handler.snap new file mode 100644 index 0000000..8c642eb --- /dev/null +++ b/packages/rust/xtask/src/snapshots/xtask__generate_handlers__tests__event_handler.snap @@ -0,0 +1,43 @@ +--- +source: xtask/src/generate_handlers.rs +expression: prettified_code +--- +//! This file is auto-generated by `xtask`. Do not edit manually. +#![allow(async_fn_in_trait)] +use crate::{event::EventContext, types, Server, EventSubscriptions}; +pub trait EventHandler: EventSubscriptions + Send + Sync { + ///Handler for the `Chat` event. + async fn on_chat( + &self, + _server: &Server, + _event: &mut EventContext<'_, types::ChatEvent>, + ) {} + ///Handler for the `BlockBreak` event. + async fn on_block_break( + &self, + _server: &Server, + _event: &mut EventContext<'_, types::BlockBreakEvent>, + ) {} +} +#[doc(hidden)] +pub async fn dispatch_event( + server: &Server, + handler: &impl EventHandler, + envelope: &types::EventEnvelope, +) { + let Some(payload) = &envelope.payload else { + return; + }; + match payload { + types::event_envelope::Payload::Chat(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_chat(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + types::event_envelope::Payload::BlockBreak(e) => { + let mut context = EventContext::new(&envelope.event_id, e); + handler.on_block_break(server, &mut context).await; + server.send_event_result(context).await.ok(); + } + } +} diff --git a/packages/rust/xtask/src/snapshots/xtask__generate_mutations__tests__event_mutations.snap b/packages/rust/xtask/src/snapshots/xtask__generate_mutations__tests__event_mutations.snap new file mode 100644 index 0000000..923c45d --- /dev/null +++ b/packages/rust/xtask/src/snapshots/xtask__generate_mutations__tests__event_mutations.snap @@ -0,0 +1,28 @@ +--- +source: xtask/src/generate_mutations.rs +expression: prettified_code +--- +//! This file is auto-generated by `xtask`. Do not edit manually. +#![allow(clippy::all)] +use crate::types; +use crate::event::EventContext; +impl<'a> EventContext<'a, types::ChatEvent> { + ///Sets the `message` for this event. + pub fn set_message(&mut self, message: impl Into>) { + let mutation = types::ChatMutation { + message: message.into(), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::Chat(mutation)); + } +} +impl<'a> EventContext<'a, types::BlockBreakEvent> { + ///Sets the `drops` for this event. + pub fn set_drops(&mut self, drops: impl Into>) { + let mutation = types::BlockBreakMutation { + drops: drops.into(), + ..Default::default() + }; + self.set_mutation(types::EventResultUpdate::BlockBreak(mutation)); + } +} diff --git a/packages/rust/xtask/src/utils.rs b/packages/rust/xtask/src/utils.rs index 0d9b018..c1f1a74 100644 --- a/packages/rust/xtask/src/utils.rs +++ b/packages/rust/xtask/src/utils.rs @@ -1,16 +1,14 @@ use anyhow::Result; use quote::quote; -use std::{collections::HashMap, fs, path::PathBuf}; +use std::collections::HashMap; use syn::{ - parse_file, visit::Visit, File, Ident, Item, ItemEnum, ItemMod, ItemStruct, Path, Type, - TypePath, + parse::Parse, parse_file, visit::Visit, Field, File, GenericArgument, Ident, Item, ItemEnum, + ItemMod, ItemStruct, Meta, Path, PathArguments, Type, TypePath, }; -pub(crate) fn write_formatted_file(path: &PathBuf, content: String) -> Result<()> { +pub(crate) fn prettify_code(content: String) -> Result { let ast = parse_file(&content)?; - let formatted_code = prettyplease::unparse(&ast); - fs::write(path, formatted_code)?; - Ok(()) + Ok(prettyplease::unparse(&ast)) } pub(crate) fn find_all_structs(ast: &File) -> HashMap { @@ -70,102 +68,450 @@ pub(crate) fn get_variant_type_path(variant: &syn::Variant) -> Result<&Path> { anyhow::bail!("Variant `{}` is not a single-tuple struct", variant.ident) } -pub(crate) fn clean_type(ty: &Type) -> proc_macro2::TokenStream { - let (inner_type, is_option) = unwrap_option_path(ty); - if is_option { - let inner_cleaned = clean_type(inner_type); - return quote! { Option<#inner_cleaned> }; +/// Helper for clean_type to detect `Vec` variants by parsing +/// the syn::TypePath instead of string-matching. +fn is_vec_u8(type_path: &TypePath) -> bool { + // Check if the last segment's identifier is "Vec" + if let Some(segment) = type_path.path.segments.last() { + if segment.ident == "Vec" { + // Check if its type argument is `` + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + if args.args.len() == 1 { + // Check for `Vec` + if let Some(syn::GenericArgument::Type(Type::Path(inner_type_path))) = + args.args.first() + { + // `is_ident` is robust for primitives + if inner_type_path.path.is_ident("u8") { + return true; + } + } + } + } + } } + false +} - if let Type::Path(type_path) = inner_type { - if let Some(segment) = type_path.path.segments.last() { - let ident_str = segment.ident.to_string(); - - match ident_str.as_str() { - "String" => return quote! { String }, - - "StringList" => return quote! { Vec }, - "ItemStackList" => return quote! { Vec }, +enum ProstTypeInfo<'a> { + Option(&'a Type), + Vec(&'a Type), + VecU8, + HashMap(&'a Type, &'a Type), + String, + Primitive(&'a Ident), + #[allow(dead_code)] + Model(&'a Ident), // Any other struct/enum like Vec3, ItemStack + Unknown(&'a Type), +} - "Vec" if is_prost_bytes(type_path) => return quote! { Vec }, +fn classify_prost_type(ty: &Type) -> ProstTypeInfo<'_> { + if let Type::Path(type_path) = ty { + let segment = type_path.path.segments.last().unwrap(); + let ident = &segment.ident; + let ident_str = ident.to_string(); - // Primitives (safe, no path) - "f64" => return quote! { f64 }, - "f32" => return quote! { f32 }, - "i64" => return quote! { i64 }, - "u64" => return quote! { u64 }, - "i32" => return quote! { i32 }, - "u32" => return quote! { u32 }, - "bool" => return quote! { bool }, + if ident_str == "Option" { + if let PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(GenericArgument::Type(inner_ty)) = args.args.first() { + return ProstTypeInfo::Option(inner_ty); + } + } + } + if ident_str == "Vec" { + if is_vec_u8(type_path) { + return ProstTypeInfo::VecU8; + } + if let PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(GenericArgument::Type(inner_ty)) = args.args.first() { + return ProstTypeInfo::Vec(inner_ty); + } + } + } - // Fallback for complex types (Vec3, WorldRef, etc.) - // This is now correct. If `ident_str` is `Vec3`, it - // will correctly become `types::Vec3`. - _ => { - let ident = &segment.ident; - return quote! { types::#ident }; + if ident_str == "HashMap" { + if let PathArguments::AngleBracketed(args) = &segment.arguments { + if let (Some(GenericArgument::Type(k_ty)), Some(GenericArgument::Type(v_ty))) = + (args.args.first(), args.args.last()) + { + return ProstTypeInfo::HashMap(k_ty, v_ty); } } } + + if ident_str == "String" { + return ProstTypeInfo::String; + } + if ["i32", "i64", "u32", "u64", "f32", "f64", "bool"].contains(&ident_str.as_str()) { + return ProstTypeInfo::Primitive(ident); + } + + return ProstTypeInfo::Model(ident); } - // Last resort (e.g., tuples, arrays) - quote! { #ty } + ProstTypeInfo::Unknown(ty) } -/// Helper for clean_type to detect `prost` bytes (Vec) -fn is_prost_bytes(type_path: &TypePath) -> bool { - let path_str = quote!(#type_path).to_string(); - path_str.contains("Vec < u8 > ") -} -/// Unwraps `Option` or `::core::option::Option` and returns `T` -pub(crate) fn unwrap_option_path(ty: &Type) -> (&Type, bool) { - if let Type::Path(type_path) = ty { - if let Some(segment) = type_path.path.segments.last() { - if segment.ident == "Option" { - if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { - if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() { - return (inner_ty, true); +pub(crate) fn get_prost_enumeration(field: &Field) -> Option { + for attr in &field.attrs { + // We only care about `#[prost(...)]` attributes + if !attr.path().is_ident("prost") { + continue; + } + + // We expect the attribute to be a list, like `prost(string, tag="1")` + if let Ok(list) = attr.meta.require_list() { + // We need to parse the tokens inside the `(...)` + // This is complex, so we parse them as a series of Meta items + let result = list.parse_args_with(|input: syn::parse::ParseStream| { + input.parse_terminated(Meta::parse, syn::Token![,]) + }); + + if let Ok(parsed_metas) = result { + for meta in parsed_metas { + // We are looking for a `NameValue` pair + if let Meta::NameValue(name_value) = meta { + // Specifically, one where the name is `enumeration` + if name_value.path.is_ident("enumeration") { + // The value should be a string literal, e.g., "GameMode" + if let syn::Expr::Lit(expr_lit) = name_value.value { + if let syn::Lit::Str(lit_str) = expr_lit.lit { + return Some(lit_str.parse().unwrap()); + } + } + } } } } } } - (ty, false) + + // No `enumeration` attribute found + None } -pub(crate) fn generate_conversion_code( - var_name: &proc_macro2::TokenStream, - original_ty: &Type, -) -> proc_macro2::TokenStream { - let (inner_type, is_option) = unwrap_option_path(original_ty); - if is_option { - let inner_var = quote! { v }; - let inner_conversion = generate_conversion_code(&inner_var, inner_type); - return quote! { #var_name.map(|v| #inner_conversion) }; - } - - if let Type::Path(type_path) = inner_type { - if let Some(segment) = type_path.path.segments.last() { - let ident_str = segment.ident.to_string(); - - if ident_str == "StringList" { - return quote! { - types::StringList { - values: #var_name.into_iter().map(|s| s.into()).collect(), - } - }; +pub(crate) fn get_api_type(field: &Field) -> proc_macro2::TokenStream { + if let Some(enum_ident) = get_prost_enumeration(field) { + // We found #[prost(enumeration="GameMode")] + // The type is `prost_gen::GameMode` + // (We assume prost_gen is imported in the generated file) + return quote! { types::#enum_ident }; + } + + // 2. Not an enum, so resolve the type normally + resolve_recursive_api_type(&field.ty) +} + +/// Recursive helper for get_api_type. +/// Maps a prost type to its "clean" public-facing type. +fn resolve_recursive_api_type(ty: &Type) -> proc_macro2::TokenStream { + match classify_prost_type(ty) { + ProstTypeInfo::Option(inner) => { + let inner_ty = resolve_recursive_api_type(inner); // Recurse + quote! { impl Into> } + } + ProstTypeInfo::Vec(inner) => { + let inner_ty = resolve_recursive_api_type(inner); // Recurse + quote! { Vec<#inner_ty> } + } + ProstTypeInfo::HashMap(k, v) => { + let k_ty = resolve_recursive_api_type(k); // Recurse + let v_ty = resolve_recursive_api_type(v); // Recurse + quote! { std::collections::HashMap<#k_ty, #v_ty> } + } + ProstTypeInfo::VecU8 => quote! { Vec }, + ProstTypeInfo::String => quote! { String }, + ProstTypeInfo::Primitive(ident) => quote! { #ident }, + + ProstTypeInfo::Model(_) => quote! { types::#ty }, + ProstTypeInfo::Unknown(ty) => quote! { #ty }, + } +} + +pub(crate) enum ConversionLogic { + /// No conversion needed, just use the name. + Direct, + /// Needs `.into()`. + Into, +} + +pub(crate) fn get_action_conversion_logic(field: &Field) -> ConversionLogic { + if get_prost_enumeration(field).is_some() { + return ConversionLogic::Into; + } + + match classify_prost_type(&field.ty) { + ProstTypeInfo::Option(_) => ConversionLogic::Into, + + _ => ConversionLogic::Direct, + } +} + +#[cfg(test)] +mod tests { + use super::*; // Import all functions from the parent module + use anyhow::Result; + use quote::{format_ident, quote}; + use syn::{parse_file, parse_str, File, ItemEnum, Type}; + + // --- Helper: Parse a string into a syn::File --- + fn parse_test_file(code: &str) -> File { + parse_file(code).expect("Failed to parse test code") + } + + // --- Helper: Parse a string into a syn::Type --- + fn parse_test_type(type_str: &str) -> Type { + parse_str(type_str).expect("Failed to parse test type") + } + + // --- Helper: Get realistic &Field objects from a mock struct --- + fn get_test_fields() -> HashMap { + let code = r#" + struct MockAction { + #[prost(string, tag = "1")] + player_uuid: String, + + #[prost(enumeration = "GameMode", tag = "2")] + game_mode: i32, + + #[prost(message, optional, tag = "3")] + item: ::core::option::Option, + + #[prost(message, tag = "4")] + position: types::Vec3, } - if ident_str == "ItemStackList" { - return quote! { - types::ItemStackList { - items: #var_name.into_iter().map(|s| s.into()).collect(), - } - }; + "#; + let ast = parse_test_file(code); + let structs = find_all_structs(&ast); + let mock_struct = structs + .get(&format_ident!("MockAction")) + .expect("MockAction struct not found"); + + mock_struct + .fields + .iter() + .map(|f| (f.ident.as_ref().unwrap().to_string(), f.clone())) + .collect() + } + + // --- Tests for find_all_structs --- + #[test] + fn test_find_all_structs_nested() { + let code = r#" + struct TopLevel {} + mod my_mod { + struct NestedStruct {} } - } + "#; + let ast = parse_test_file(code); + let structs = find_all_structs(&ast); + assert_eq!(structs.len(), 2); + assert!(structs.contains_key(&format_ident!("TopLevel"))); + assert!(structs.contains_key(&format_ident!("NestedStruct"))); + } + + // --- Tests for find_nested_enum --- + #[test] + fn test_find_nested_enum_success() { + let code = r#" + mod event_envelope { + enum Payload { PlayerJoin(String) } + } + "#; + let ast = parse_test_file(code); + let result = find_nested_enum(&ast, "event_envelope", "Payload"); + assert!(result.is_ok()); + } + + #[test] + fn test_find_nested_enum_fail() { + let code = r#" + mod event_envelope { + enum Payload {} + } + "#; + let ast = parse_test_file(code); + let result = find_nested_enum(&ast, "wrong_mod", "Payload"); + assert!(result.is_err()); + } + + // --- Tests for get_variant_type_path --- + #[test] + fn test_get_variant_type_path() -> Result<()> { + let item_enum: ItemEnum = parse_str(r#"enum E { Simple(String), Empty, Struct{f: i32} }"#)?; + let simple_variant = item_enum.variants.first().unwrap(); + assert_eq!(quote!(#simple_variant).to_string(), "Simple (String)"); + let path = get_variant_type_path(simple_variant)?; + assert_eq!(quote!(#path).to_string(), "String"); + + let empty_variant = &item_enum.variants[1]; + assert!(get_variant_type_path(empty_variant).is_err()); + + let struct_variant = &item_enum.variants[2]; + assert!(get_variant_type_path(struct_variant).is_err()); + Ok(()) } - // Default case: just use .into() - quote! { #var_name.into() } + // --- Tests for is_vec_u8 --- + // first time ive ever had to actually use ref p some real ball knowledge here. + // AI didn't even get it right + #[test] + fn test_is_vec_u8_helper() { + let ty_vec_u8 = parse_test_type("Vec"); + let ty_prost_vec = parse_test_type("::prost::alloc::vec::Vec"); + let ty_vec_string = parse_test_type("Vec"); + assert!(is_vec_u8(match ty_vec_u8 { + Type::Path(ref p) => p, + _ => panic!(), + })); + assert!(is_vec_u8(match ty_prost_vec { + Type::Path(ref p) => p, + _ => panic!(), + })); + assert!(!is_vec_u8(match ty_vec_string { + Type::Path(ref p) => p, + _ => panic!(), + })); + } + + // --- Tests for classify_prost_type --- + #[test] + fn test_classify_prost_type() { + let ty_opt = parse_test_type("Option"); + let ty_vec = parse_test_type("Vec"); + let ty_bytes = parse_test_type("::prost::alloc::vec::Vec"); + let ty_map = parse_test_type("::std::collections::HashMap"); + let ty_str = parse_test_type("::prost::alloc::string::String"); + let ty_prim = parse_test_type("i32"); + let ty_model = parse_test_type("types::ItemStack"); + + assert!(matches!( + classify_prost_type(&ty_opt), + ProstTypeInfo::Option(_) + )); + assert!(matches!( + classify_prost_type(&ty_vec), + ProstTypeInfo::Vec(_) + )); + assert!(matches!( + classify_prost_type(&ty_bytes), + ProstTypeInfo::VecU8 + )); + assert!(matches!( + classify_prost_type(&ty_map), + ProstTypeInfo::HashMap(_, _) + )); + assert!(matches!( + classify_prost_type(&ty_str), + ProstTypeInfo::String + )); + assert!(matches!( + classify_prost_type(&ty_prim), + ProstTypeInfo::Primitive(_) + )); + assert!(matches!( + classify_prost_type(&ty_model), + ProstTypeInfo::Model(_) + )); + } + + // --- Tests for get_prost_enumeration --- + #[test] + fn test_get_prost_enumeration() { + let fields = get_test_fields(); + let field_game_mode = fields.get("game_mode").unwrap(); + let field_uuid = fields.get("player_uuid").unwrap(); + let field_item = fields.get("item").unwrap(); + + assert_eq!( + get_prost_enumeration(field_game_mode).unwrap().to_string(), + "GameMode" + ); + assert!(get_prost_enumeration(field_uuid).is_none()); + assert!(get_prost_enumeration(field_item).is_none()); + } + + // --- Tests for resolve_recursive_api_type --- + fn assert_resolve_api_type(input_type: &str, expected_output: &str) { + let ty = parse_test_type(input_type); + let resolved = resolve_recursive_api_type(&ty); + assert_eq!( + resolved.to_string().replace(' ', ""), + expected_output.replace(' ', "") + ); + } + + #[test] + fn test_resolve_recursive_api_type() { + assert_resolve_api_type("i32", "i32"); + assert_resolve_api_type("::prost::alloc::string::String", "String"); + assert_resolve_api_type("::prost::alloc::vec::Vec", "Vec"); + assert_resolve_api_type("ItemStack", "types::ItemStack"); + assert_resolve_api_type("Option", "implInto>"); + assert_resolve_api_type("Vec", "Vec"); + assert_resolve_api_type( + "std::collections::HashMap", + "std::collections::HashMap", + ); + } + + // --- Tests for get_api_type --- + fn assert_api_type(field: &Field, expected_output: &str) { + let resolved = get_api_type(field); + assert_eq!( + resolved.to_string().replace(' ', ""), + expected_output.replace(' ', "") + ); + } + + #[test] + fn test_get_api_type() { + let fields = get_test_fields(); + + // Test enum field + assert_api_type(fields.get("game_mode").unwrap(), "types::GameMode"); + + // Test string field + assert_api_type(fields.get("player_uuid").unwrap(), "String"); + + // Test optional model field + assert_api_type( + fields.get("item").unwrap(), + "implInto>", + ); + } + + // --- Tests for get_action_conversion_logic --- + #[test] + fn test_get_action_conversion_logic() { + let fields = get_test_fields(); + let field_game_mode = fields.get("game_mode").unwrap(); + let field_uuid = fields.get("player_uuid").unwrap(); + let field_item = fields.get("item").unwrap(); + let field_pos = fields.get("position").unwrap(); + + // Enum -> Into + assert!(matches!( + get_action_conversion_logic(field_game_mode), + ConversionLogic::Into + )); + + // Option -> Into + assert!(matches!( + get_action_conversion_logic(field_item), + ConversionLogic::Into + )); + + // String -> Direct + assert!(matches!( + get_action_conversion_logic(field_uuid), + ConversionLogic::Direct + )); + + // Model (types::Vec3) -> Direct + assert!(matches!( + get_action_conversion_logic(field_pos), + ConversionLogic::Direct + )); + } } From ae6785e3584d6d208ccff7b0014951e37e4335cd Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Wed, 26 Nov 2025 00:23:38 -0500 Subject: [PATCH 17/22] Start on rustic-economy examples/plugins/rusts. Revealing some less than wanted behavour and thus I will be adding command API and maybe better error handling to the handler fns. --- examples/plugins/rust/.gitignore | 16 ++++ examples/plugins/rust/Cargo.toml | 12 +++ examples/plugins/rust/src/main.rs | 153 ++++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 examples/plugins/rust/.gitignore create mode 100644 examples/plugins/rust/Cargo.toml create mode 100644 examples/plugins/rust/src/main.rs diff --git a/examples/plugins/rust/.gitignore b/examples/plugins/rust/.gitignore new file mode 100644 index 0000000..3972d0e --- /dev/null +++ b/examples/plugins/rust/.gitignore @@ -0,0 +1,16 @@ +# Cargo build artifacts +target/ + +# Cargo.lock for libraries (keep for applications) +# See: https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# IDE files +.idea/ +.vscode/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db diff --git a/examples/plugins/rust/Cargo.toml b/examples/plugins/rust/Cargo.toml new file mode 100644 index 0000000..a90d7a5 --- /dev/null +++ b/examples/plugins/rust/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rustic-economy" +version = "0.1.0" +edition = "2024" + +[dependencies] +# required for any base plugin. +dragonfly-plugin = "0.2.0" +tokio = { version = "1.48.0", features = ["full"] } + +# used in this plugin specifically but isn't required for all plugins. +sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } diff --git a/examples/plugins/rust/src/main.rs b/examples/plugins/rust/src/main.rs new file mode 100644 index 0000000..8cbbd50 --- /dev/null +++ b/examples/plugins/rust/src/main.rs @@ -0,0 +1,153 @@ +/// This is a semi advanced example of a simple economy plugin. +/// we are gonna use sqlite, to store user money. +/// two commands: +/// pay: pay yourself money +/// bal: view your balance / money +use dragonfly_plugin::{ + Plugin, PluginRunner, Server, + event::{EventContext, EventHandler}, + event_handler, types, +}; +use sqlx::{SqlitePool, sqlite::SqlitePoolOptions}; + +#[derive(Plugin)] +#[plugin( + id = "rustic-economy", + name = "Rustic Economy", + version = "0.1.0", + api = "1.0.0" +)] +struct RusticEconomy { + db: SqlitePool, +} + +/// This impl is just a helper for dealing with our SQL stuff. +impl RusticEconomy { + async fn new() -> Result> { + // Create database connection + let db = SqlitePoolOptions::new() + .max_connections(5) + .connect("sqlite:economy.db") + .await?; + + // Create table if it doesn't exist + sqlx::query( + "CREATE TABLE IF NOT EXISTS users ( + uuid TEXT PRIMARY KEY, + balance REAL NOT NULL DEFAULT 0.0 + )", + ) + .execute(&db) + .await?; + + Ok(Self { db }) + } + + async fn get_balance(&self, uuid: &str) -> Result { + let result: Option<(f64,)> = sqlx::query_as("SELECT balance FROM users WHERE uuid = ?") + .bind(uuid) + .fetch_optional(&self.db) + .await?; + + Ok(result.map(|(bal,)| bal).unwrap_or(0.0)) + } + + async fn add_money(&self, uuid: &str, amount: f64) -> Result { + // Insert or update user balance + sqlx::query( + "INSERT INTO users (uuid, balance) VALUES (?, ?) + ON CONFLICT(uuid) DO UPDATE SET balance = balance + ?", + ) + .bind(uuid) + .bind(amount) + .bind(amount) + .execute(&self.db) + .await?; + + self.get_balance(uuid).await + } +} + +#[event_handler] +impl EventHandler for RusticEconomy { + async fn on_chat(&self, server: &Server, event: &mut EventContext<'_, types::ChatEvent>) { + let message = &event.data.message; + let player_uuid = &event.data.player_uuid; + + // Handle commands + if message.starts_with("!pay") { + event.cancel(); + let parts: Vec<&str> = message.split_whitespace().collect(); + if parts.len() != 2 { + server + .send_chat(player_uuid.clone(), "Usage: !pay ".to_string()) + .await + .expect("Bad error handling womp."); + return; + } + + let amount: f64 = match parts[1].parse() { + Ok(amt) if amt > 0.0 => amt, + _ => { + server + .send_chat( + player_uuid.clone(), + "Please provide a valid positive amount!".to_string(), + ) + .await + .expect("Bad error handling sad."); + return; + } + }; + + match self.add_money(player_uuid, amount).await { + Ok(new_balance) => { + server + .send_chat( + player_uuid.clone(), + format!("Added ${:.2}! New balance: ${:.2}", amount, new_balance), + ) + .await + .expect("again error handling is bad"); + } + Err(e) => { + eprintln!("Database error: {}", e); + server + .send_chat(player_uuid.clone(), "Error processing payment!".to_string()) + .await + .expect("Bad error handling"); + } + } + } else if message.starts_with("!bal") { + event.cancel(); + match self.get_balance(player_uuid).await { + Ok(balance) => { + server + .send_chat( + player_uuid.clone(), + format!("Your balance: ${:.2}", balance), + ) + .await + .expect("oh shit i need to handle this better."); + } + Err(e) => { + eprintln!("Database error: {}", e); + server + .send_chat(player_uuid.clone(), "Error checking balance!".to_string()) + .await + .expect("again bad error handling"); + } + } + } + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("Starting the plugin..."); + println!("Initializing database..."); + + let plugin = RusticEconomy::new().await?; + + PluginRunner::run(plugin, "tcp://127.0.0.1:50050").await +} From ef579aea029b1d70ca055d21efdcb4390cdafb1e Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Wed, 26 Nov 2025 16:28:54 -0500 Subject: [PATCH 18/22] midway to 3.0. Fixing critical issue. --- examples/plugins/rust/Cargo.toml | 2 +- examples/plugins/rust/src/main.rs | 10 +- packages/rust/src/event/context.rs | 94 +++- packages/rust/src/event/handler.rs | 441 ++++++++++++---- packages/rust/src/event/mutations.rs | 482 +++++++++++++----- packages/rust/src/server/helpers.rs | 382 +++++++------- packages/rust/src/server/server.rs | 36 +- packages/rust/xtask/src/generate_handlers.rs | 9 +- packages/rust/xtask/src/generate_mutations.rs | 23 +- 9 files changed, 1018 insertions(+), 461 deletions(-) diff --git a/examples/plugins/rust/Cargo.toml b/examples/plugins/rust/Cargo.toml index a90d7a5..2c2e8a5 100644 --- a/examples/plugins/rust/Cargo.toml +++ b/examples/plugins/rust/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] # required for any base plugin. -dragonfly-plugin = "0.2.0" +dragonfly-plugin = { path = "../../../packages/rust/"} tokio = { version = "1.48.0", features = ["full"] } # used in this plugin specifically but isn't required for all plugins. diff --git a/examples/plugins/rust/src/main.rs b/examples/plugins/rust/src/main.rs index 8cbbd50..d7dc898 100644 --- a/examples/plugins/rust/src/main.rs +++ b/examples/plugins/rust/src/main.rs @@ -76,7 +76,8 @@ impl EventHandler for RusticEconomy { // Handle commands if message.starts_with("!pay") { - event.cancel(); + // ALWAYS RESPOND TO THE SERVER ASAP. + // THEN DO SIDE WORK THAT MIGHT TAKE A GOOD BIT. let parts: Vec<&str> = message.split_whitespace().collect(); if parts.len() != 2 { server @@ -118,10 +119,15 @@ impl EventHandler for RusticEconomy { .expect("Bad error handling"); } } + event.cancel().await; + println!("I HAVE SENT THE CANCEL FULLY"); } else if message.starts_with("!bal") { - event.cancel(); + // YET AGAIN RESPOND ASAP TO SERVER. + event.cancel().await; match self.get_balance(player_uuid).await { Ok(balance) => { + // These are fine to happen later. + // as they aren't bounded by the timout of server responses. server .send_chat( player_uuid.clone(), diff --git a/packages/rust/src/event/context.rs b/packages/rust/src/event/context.rs index 8a015cf..e69c67a 100644 --- a/packages/rust/src/event/context.rs +++ b/packages/rust/src/event/context.rs @@ -1,9 +1,12 @@ -use crate::types; +use tokio::sync::mpsc; + +use crate::types::{self, PluginToHost}; /// This enum is used internally by `dispatch_event` to /// determine what action to take after an event handler runs. #[doc(hidden)] -pub enum EventResultUpdate { +#[derive(Debug)] +pub enum EventResult { /// Do nothing, let the default server behavior happen. just sends ack. None, /// Cancel the event, stopping default server behavior. @@ -18,38 +21,101 @@ pub enum EventResultUpdate { /// and methods to mutate or cancel it. pub struct EventContext<'a, T> { pub data: &'a T, + pub result: EventResult, event_id: &'a str, - result: EventResultUpdate, + sender: mpsc::Sender, + plugin_id: String, + sent: bool, } impl<'a, T> EventContext<'a, T> { #[doc(hidden)] - pub fn new(event_id: &'a str, data: &'a T) -> Self { + pub fn new( + event_id: &'a str, + data: &'a T, + sender: mpsc::Sender, + plugin_id: String, + ) -> Self { Self { event_id, data, - result: EventResultUpdate::None, + result: EventResult::None, + sender, + plugin_id, + sent: false, } } /// Consumes the context and returns the final result. #[doc(hidden)] - pub fn into_result(self) -> (String, EventResultUpdate) { + pub fn into_result(self) -> (String, EventResult) { (self.event_id.to_string(), self.result) } /// Cancels the event. /// - /// The server's default handler will not run. - pub fn cancel(&mut self) { - self.result = EventResultUpdate::Cancelled; + pub async fn cancel(&mut self) { + self.result = EventResult::Cancelled; + self.send().await } - /// Internal helper to set a mutation. - /// This is called by the auto-generated helper methods. - #[doc(hidden)] - pub fn set_mutation(&mut self, update: types::event_result::Update) { - self.result = EventResultUpdate::Mutated(update); + pub(crate) async fn send_ack_if_needed(&mut self) { + if self.sent { + return; + } + self.sent = true; + // result is still EventResultUpdate::None, which sends ack + self.send().await; + } + + pub async fn send(&mut self) { + if self.sent { + #[cfg(debug_assertions)] + panic!("Attempted to respond twice to the same event!"); + + #[cfg(not(debug_assertions))] + { + eprintln!("Warning: send() called after response already sent"); + return; + } + } + + self.sent = true; + + let event_id = self.event_id.to_owned(); + + let payload = match &self.result { + // If nothing was changed just send ack. + EventResult::None => types::EventResult { + event_id, + cancel: None, + update: None, + }, + EventResult::Cancelled => types::EventResult { + event_id, + cancel: Some(true), + update: None, + }, + EventResult::Mutated(update) => types::EventResult { + event_id, + cancel: None, + // TODO: later try to fix this clone. + // this gives us best API usage but is memory semantically wrong. + // calling this func or like .cancel should consume event. + // + // but for newbies thats hard to understand. + update: Some(update.clone()), + }, + }; + + let msg = types::PluginToHost { + plugin_id: self.plugin_id.clone(), + payload: Some(types::PluginPayload::EventResult(payload)), + }; + + if let Err(e) = self.sender.send(msg).await { + eprintln!("Failed to send event response: {}", e); + } } } diff --git a/packages/rust/src/event/handler.rs b/packages/rust/src/event/handler.rs index cb22f1f..f865579 100644 --- a/packages/rust/src/event/handler.rs +++ b/packages/rust/src/event/handler.rs @@ -352,261 +352,506 @@ pub async fn dispatch_event( }; match payload { types::event_envelope::Payload::PlayerJoin(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_join(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerQuit(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_quit(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerMove(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_move(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerJump(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_jump(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerTeleport(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_teleport(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerChangeWorld(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_change_world(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerToggleSprint(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_toggle_sprint(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerToggleSneak(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_toggle_sneak(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::Chat(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_chat(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerFoodLoss(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_food_loss(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerHeal(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_heal(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerHurt(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_hurt(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerDeath(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_death(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerRespawn(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_respawn(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerSkinChange(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_skin_change(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerFireExtinguish(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler .on_player_fire_extinguish(server, &mut context) .await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerStartBreak(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_start_break(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::BlockBreak(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_block_break(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerBlockPlace(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_block_place(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerBlockPick(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_block_pick(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerItemUse(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_item_use(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerItemUseOnBlock(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler .on_player_item_use_on_block(server, &mut context) .await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerItemUseOnEntity(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler .on_player_item_use_on_entity(server, &mut context) .await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerItemRelease(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_item_release(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerItemConsume(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_item_consume(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerAttackEntity(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_attack_entity(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerExperienceGain(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler .on_player_experience_gain(server, &mut context) .await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerPunchAir(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_punch_air(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerSignEdit(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_sign_edit(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerLecternPageTurn(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler .on_player_lectern_page_turn(server, &mut context) .await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerItemDamage(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_item_damage(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerItemPickup(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_item_pickup(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerHeldSlotChange(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler .on_player_held_slot_change(server, &mut context) .await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerItemDrop(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_item_drop(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerTransfer(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_transfer(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::Command(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_command(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerDiagnostics(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_player_diagnostics(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::WorldLiquidFlow(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_world_liquid_flow(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::WorldLiquidDecay(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_world_liquid_decay(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::WorldLiquidHarden(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_world_liquid_harden(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::WorldSound(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_world_sound(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::WorldFireSpread(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_world_fire_spread(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::WorldBlockBurn(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_world_block_burn(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::WorldCropTrample(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_world_crop_trample(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::WorldLeavesDecay(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_world_leaves_decay(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::WorldEntitySpawn(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_world_entity_spawn(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::WorldEntityDespawn(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_world_entity_despawn(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::WorldExplosion(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_world_explosion(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::WorldClose(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_world_close(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } } } diff --git a/packages/rust/src/event/mutations.rs b/packages/rust/src/event/mutations.rs index ced2deb..5b36b3a 100644 --- a/packages/rust/src/event/mutations.rs +++ b/packages/rust/src/event/mutations.rs @@ -1,198 +1,426 @@ //! This file is auto-generated by `xtask`. Do not edit manually. #![allow(clippy::all)] -use crate::event::EventContext; use crate::types; +use crate::event::{EventContext, EventResult}; impl<'a> EventContext<'a, types::ChatEvent> { + fn ensure_mutation_exists(&mut self) { + if !matches!( + self.result, EventResult::Mutated(types::EventResultUpdate::Chat(_)) + ) { + self.result = EventResult::Mutated( + types::EventResultUpdate::Chat(types::ChatMutation::default()), + ); + } + } ///Sets the `message` for this event. - pub fn set_message(&mut self, message: impl Into>) { - let mutation = types::ChatMutation { - message: message.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::Chat(mutation)); + pub fn set_message(&mut self, message: impl Into>) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated(types::EventResultUpdate::Chat(ref mut m)) = self + .result + { + m.message = message.into(); + } + self } } impl<'a> EventContext<'a, types::BlockBreakEvent> { + fn ensure_mutation_exists(&mut self) { + if !matches!( + self.result, EventResult::Mutated(types::EventResultUpdate::BlockBreak(_)) + ) { + self.result = EventResult::Mutated( + types::EventResultUpdate::BlockBreak( + types::BlockBreakMutation::default(), + ), + ); + } + } ///Sets the `drops` for this event. - pub fn set_drops(&mut self, drops: impl Into>) { - let mutation = types::BlockBreakMutation { - drops: drops.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::BlockBreak(mutation)); + pub fn set_drops( + &mut self, + drops: impl Into>, + ) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated(types::EventResultUpdate::BlockBreak(ref mut m)) = self + .result + { + m.drops = drops.into(); + } + self } ///Sets the `xp` for this event. - pub fn set_xp(&mut self, xp: impl Into>) { - let mutation = types::BlockBreakMutation { - xp: xp.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::BlockBreak(mutation)); + pub fn set_xp(&mut self, xp: impl Into>) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated(types::EventResultUpdate::BlockBreak(ref mut m)) = self + .result + { + m.xp = xp.into(); + } + self } } impl<'a> EventContext<'a, types::PlayerFoodLossEvent> { + fn ensure_mutation_exists(&mut self) { + if !matches!( + self.result, + EventResult::Mutated(types::EventResultUpdate::PlayerFoodLoss(_)) + ) { + self.result = EventResult::Mutated( + types::EventResultUpdate::PlayerFoodLoss( + types::PlayerFoodLossMutation::default(), + ), + ); + } + } ///Sets the `to` for this event. - pub fn set_to(&mut self, to: impl Into>) { - let mutation = types::PlayerFoodLossMutation { - to: to.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::PlayerFoodLoss(mutation)); + pub fn set_to(&mut self, to: impl Into>) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated( + types::EventResultUpdate::PlayerFoodLoss(ref mut m), + ) = self.result + { + m.to = to.into(); + } + self } } impl<'a> EventContext<'a, types::PlayerHealEvent> { + fn ensure_mutation_exists(&mut self) { + if !matches!( + self.result, EventResult::Mutated(types::EventResultUpdate::PlayerHeal(_)) + ) { + self.result = EventResult::Mutated( + types::EventResultUpdate::PlayerHeal( + types::PlayerHealMutation::default(), + ), + ); + } + } ///Sets the `amount` for this event. - pub fn set_amount(&mut self, amount: impl Into>) { - let mutation = types::PlayerHealMutation { - amount: amount.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::PlayerHeal(mutation)); + pub fn set_amount(&mut self, amount: impl Into>) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated(types::EventResultUpdate::PlayerHeal(ref mut m)) = self + .result + { + m.amount = amount.into(); + } + self } } impl<'a> EventContext<'a, types::PlayerHurtEvent> { + fn ensure_mutation_exists(&mut self) { + if !matches!( + self.result, EventResult::Mutated(types::EventResultUpdate::PlayerHurt(_)) + ) { + self.result = EventResult::Mutated( + types::EventResultUpdate::PlayerHurt( + types::PlayerHurtMutation::default(), + ), + ); + } + } ///Sets the `damage` for this event. - pub fn set_damage(&mut self, damage: impl Into>) { - let mutation = types::PlayerHurtMutation { - damage: damage.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::PlayerHurt(mutation)); + pub fn set_damage(&mut self, damage: impl Into>) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated(types::EventResultUpdate::PlayerHurt(ref mut m)) = self + .result + { + m.damage = damage.into(); + } + self } ///Sets the `attack_immunity_ms` for this event. - pub fn set_attack_immunity_ms(&mut self, attack_immunity_ms: impl Into>) { - let mutation = types::PlayerHurtMutation { - attack_immunity_ms: attack_immunity_ms.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::PlayerHurt(mutation)); + pub fn set_attack_immunity_ms( + &mut self, + attack_immunity_ms: impl Into>, + ) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated(types::EventResultUpdate::PlayerHurt(ref mut m)) = self + .result + { + m.attack_immunity_ms = attack_immunity_ms.into(); + } + self } } impl<'a> EventContext<'a, types::PlayerDeathEvent> { + fn ensure_mutation_exists(&mut self) { + if !matches!( + self.result, EventResult::Mutated(types::EventResultUpdate::PlayerDeath(_)) + ) { + self.result = EventResult::Mutated( + types::EventResultUpdate::PlayerDeath( + types::PlayerDeathMutation::default(), + ), + ); + } + } ///Sets the `keep_inventory` for this event. - pub fn set_keep_inventory(&mut self, keep_inventory: impl Into>) { - let mutation = types::PlayerDeathMutation { - keep_inventory: keep_inventory.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::PlayerDeath(mutation)); + pub fn set_keep_inventory( + &mut self, + keep_inventory: impl Into>, + ) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated(types::EventResultUpdate::PlayerDeath(ref mut m)) = self + .result + { + m.keep_inventory = keep_inventory.into(); + } + self } } impl<'a> EventContext<'a, types::PlayerRespawnEvent> { + fn ensure_mutation_exists(&mut self) { + if !matches!( + self.result, EventResult::Mutated(types::EventResultUpdate::PlayerRespawn(_)) + ) { + self.result = EventResult::Mutated( + types::EventResultUpdate::PlayerRespawn( + types::PlayerRespawnMutation::default(), + ), + ); + } + } ///Sets the `position` for this event. - pub fn set_position(&mut self, position: impl Into>) { - let mutation = types::PlayerRespawnMutation { - position: position.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::PlayerRespawn(mutation)); + pub fn set_position( + &mut self, + position: impl Into>, + ) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated( + types::EventResultUpdate::PlayerRespawn(ref mut m), + ) = self.result + { + m.position = position.into(); + } + self } ///Sets the `world` for this event. - pub fn set_world(&mut self, world: impl Into>) { - let mutation = types::PlayerRespawnMutation { - world: world.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::PlayerRespawn(mutation)); + pub fn set_world(&mut self, world: impl Into>) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated( + types::EventResultUpdate::PlayerRespawn(ref mut m), + ) = self.result + { + m.world = world.into(); + } + self } } impl<'a> EventContext<'a, types::PlayerAttackEntityEvent> { + fn ensure_mutation_exists(&mut self) { + if !matches!( + self.result, + EventResult::Mutated(types::EventResultUpdate::PlayerAttackEntity(_)) + ) { + self.result = EventResult::Mutated( + types::EventResultUpdate::PlayerAttackEntity( + types::PlayerAttackEntityMutation::default(), + ), + ); + } + } ///Sets the `force` for this event. - pub fn set_force(&mut self, force: impl Into>) { - let mutation = types::PlayerAttackEntityMutation { - force: force.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::PlayerAttackEntity(mutation)); + pub fn set_force(&mut self, force: impl Into>) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated( + types::EventResultUpdate::PlayerAttackEntity(ref mut m), + ) = self.result + { + m.force = force.into(); + } + self } ///Sets the `height` for this event. - pub fn set_height(&mut self, height: impl Into>) { - let mutation = types::PlayerAttackEntityMutation { - height: height.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::PlayerAttackEntity(mutation)); + pub fn set_height(&mut self, height: impl Into>) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated( + types::EventResultUpdate::PlayerAttackEntity(ref mut m), + ) = self.result + { + m.height = height.into(); + } + self } ///Sets the `critical` for this event. - pub fn set_critical(&mut self, critical: impl Into>) { - let mutation = types::PlayerAttackEntityMutation { - critical: critical.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::PlayerAttackEntity(mutation)); + pub fn set_critical(&mut self, critical: impl Into>) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated( + types::EventResultUpdate::PlayerAttackEntity(ref mut m), + ) = self.result + { + m.critical = critical.into(); + } + self } } impl<'a> EventContext<'a, types::PlayerExperienceGainEvent> { + fn ensure_mutation_exists(&mut self) { + if !matches!( + self.result, + EventResult::Mutated(types::EventResultUpdate::PlayerExperienceGain(_)) + ) { + self.result = EventResult::Mutated( + types::EventResultUpdate::PlayerExperienceGain( + types::PlayerExperienceGainMutation::default(), + ), + ); + } + } ///Sets the `amount` for this event. - pub fn set_amount(&mut self, amount: impl Into>) { - let mutation = types::PlayerExperienceGainMutation { - amount: amount.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::PlayerExperienceGain(mutation)); + pub fn set_amount(&mut self, amount: impl Into>) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated( + types::EventResultUpdate::PlayerExperienceGain(ref mut m), + ) = self.result + { + m.amount = amount.into(); + } + self } } impl<'a> EventContext<'a, types::PlayerLecternPageTurnEvent> { + fn ensure_mutation_exists(&mut self) { + if !matches!( + self.result, + EventResult::Mutated(types::EventResultUpdate::PlayerLecternPageTurn(_)) + ) { + self.result = EventResult::Mutated( + types::EventResultUpdate::PlayerLecternPageTurn( + types::PlayerLecternPageTurnMutation::default(), + ), + ); + } + } ///Sets the `new_page` for this event. - pub fn set_new_page(&mut self, new_page: impl Into>) { - let mutation = types::PlayerLecternPageTurnMutation { - new_page: new_page.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::PlayerLecternPageTurn(mutation)); + pub fn set_new_page(&mut self, new_page: impl Into>) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated( + types::EventResultUpdate::PlayerLecternPageTurn(ref mut m), + ) = self.result + { + m.new_page = new_page.into(); + } + self } } impl<'a> EventContext<'a, types::PlayerItemPickupEvent> { + fn ensure_mutation_exists(&mut self) { + if !matches!( + self.result, + EventResult::Mutated(types::EventResultUpdate::PlayerItemPickup(_)) + ) { + self.result = EventResult::Mutated( + types::EventResultUpdate::PlayerItemPickup( + types::PlayerItemPickupMutation::default(), + ), + ); + } + } ///Sets the `item` for this event. - pub fn set_item(&mut self, item: impl Into>) { - let mutation = types::PlayerItemPickupMutation { - item: item.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::PlayerItemPickup(mutation)); + pub fn set_item(&mut self, item: impl Into>) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated( + types::EventResultUpdate::PlayerItemPickup(ref mut m), + ) = self.result + { + m.item = item.into(); + } + self } } impl<'a> EventContext<'a, types::PlayerTransferEvent> { + fn ensure_mutation_exists(&mut self) { + if !matches!( + self.result, + EventResult::Mutated(types::EventResultUpdate::PlayerTransfer(_)) + ) { + self.result = EventResult::Mutated( + types::EventResultUpdate::PlayerTransfer( + types::PlayerTransferMutation::default(), + ), + ); + } + } ///Sets the `address` for this event. - pub fn set_address(&mut self, address: impl Into>) { - let mutation = types::PlayerTransferMutation { - address: address.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::PlayerTransfer(mutation)); + pub fn set_address( + &mut self, + address: impl Into>, + ) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated( + types::EventResultUpdate::PlayerTransfer(ref mut m), + ) = self.result + { + m.address = address.into(); + } + self } } impl<'a> EventContext<'a, types::WorldExplosionEvent> { + fn ensure_mutation_exists(&mut self) { + if !matches!( + self.result, + EventResult::Mutated(types::EventResultUpdate::WorldExplosion(_)) + ) { + self.result = EventResult::Mutated( + types::EventResultUpdate::WorldExplosion( + types::WorldExplosionMutation::default(), + ), + ); + } + } ///Sets the `entity_uuids` for this event. - pub fn set_entity_uuids(&mut self, entity_uuids: impl Into>) { - let mutation = types::WorldExplosionMutation { - entity_uuids: entity_uuids.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::WorldExplosion(mutation)); + pub fn set_entity_uuids( + &mut self, + entity_uuids: impl Into>, + ) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated( + types::EventResultUpdate::WorldExplosion(ref mut m), + ) = self.result + { + m.entity_uuids = entity_uuids.into(); + } + self } ///Sets the `blocks` for this event. - pub fn set_blocks(&mut self, blocks: impl Into>) { - let mutation = types::WorldExplosionMutation { - blocks: blocks.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::WorldExplosion(mutation)); + pub fn set_blocks( + &mut self, + blocks: impl Into>, + ) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated( + types::EventResultUpdate::WorldExplosion(ref mut m), + ) = self.result + { + m.blocks = blocks.into(); + } + self } ///Sets the `item_drop_chance` for this event. - pub fn set_item_drop_chance(&mut self, item_drop_chance: impl Into>) { - let mutation = types::WorldExplosionMutation { - item_drop_chance: item_drop_chance.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::WorldExplosion(mutation)); + pub fn set_item_drop_chance( + &mut self, + item_drop_chance: impl Into>, + ) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated( + types::EventResultUpdate::WorldExplosion(ref mut m), + ) = self.result + { + m.item_drop_chance = item_drop_chance.into(); + } + self } ///Sets the `spawn_fire` for this event. - pub fn set_spawn_fire(&mut self, spawn_fire: impl Into>) { - let mutation = types::WorldExplosionMutation { - spawn_fire: spawn_fire.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::WorldExplosion(mutation)); + pub fn set_spawn_fire(&mut self, spawn_fire: impl Into>) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated( + types::EventResultUpdate::WorldExplosion(ref mut m), + ) = self.result + { + m.spawn_fire = spawn_fire.into(); + } + self } } diff --git a/packages/rust/src/server/helpers.rs b/packages/rust/src/server/helpers.rs index 9d66a16..85f836b 100644 --- a/packages/rust/src/server/helpers.rs +++ b/packages/rust/src/server/helpers.rs @@ -8,11 +8,13 @@ impl Server { target_uuid: String, message: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SendChat(types::SendChatAction { - target_uuid, - message, - })) - .await + self.send_action( + types::action::Kind::SendChat(types::SendChatAction { + target_uuid, + message, + }), + ) + .await } ///Sends a `Teleport` action to the server. pub async fn teleport( @@ -21,12 +23,14 @@ impl Server { position: impl Into>, rotation: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::Teleport(types::TeleportAction { - player_uuid, - position: position.into(), - rotation: rotation.into(), - })) - .await + self.send_action( + types::action::Kind::Teleport(types::TeleportAction { + player_uuid, + position: position.into(), + rotation: rotation.into(), + }), + ) + .await } ///Sends a `Kick` action to the server. pub async fn kick( @@ -34,11 +38,13 @@ impl Server { player_uuid: String, reason: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::Kick(types::KickAction { - player_uuid, - reason, - })) - .await + self.send_action( + types::action::Kind::Kick(types::KickAction { + player_uuid, + reason, + }), + ) + .await } ///Sends a `SetGameMode` action to the server. pub async fn set_game_mode( @@ -46,11 +52,13 @@ impl Server { player_uuid: String, game_mode: types::GameMode, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SetGameMode(types::SetGameModeAction { - player_uuid, - game_mode: game_mode.into(), - })) - .await + self.send_action( + types::action::Kind::SetGameMode(types::SetGameModeAction { + player_uuid, + game_mode: game_mode.into(), + }), + ) + .await } ///Sends a `GiveItem` action to the server. pub async fn give_item( @@ -58,21 +66,25 @@ impl Server { player_uuid: String, item: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::GiveItem(types::GiveItemAction { - player_uuid, - item: item.into(), - })) - .await + self.send_action( + types::action::Kind::GiveItem(types::GiveItemAction { + player_uuid, + item: item.into(), + }), + ) + .await } ///Sends a `ClearInventory` action to the server. pub async fn clear_inventory( &self, player_uuid: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::ClearInventory( - types::ClearInventoryAction { player_uuid }, - )) - .await + self.send_action( + types::action::Kind::ClearInventory(types::ClearInventoryAction { + player_uuid, + }), + ) + .await } ///Sends a `SetHeldItem` action to the server. pub async fn set_held_item( @@ -81,12 +93,14 @@ impl Server { main: impl Into>, offhand: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SetHeldItem(types::SetHeldItemAction { - player_uuid, - main: main.into(), - offhand: offhand.into(), - })) - .await + self.send_action( + types::action::Kind::SetHeldItem(types::SetHeldItemAction { + player_uuid, + main: main.into(), + offhand: offhand.into(), + }), + ) + .await } ///Sends a `SetHealth` action to the server. pub async fn set_health( @@ -95,12 +109,14 @@ impl Server { health: f64, max_health: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SetHealth(types::SetHealthAction { - player_uuid, - health, - max_health: max_health.into(), - })) - .await + self.send_action( + types::action::Kind::SetHealth(types::SetHealthAction { + player_uuid, + health, + max_health: max_health.into(), + }), + ) + .await } ///Sends a `SetFood` action to the server. pub async fn set_food( @@ -108,11 +124,13 @@ impl Server { player_uuid: String, food: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SetFood(types::SetFoodAction { - player_uuid, - food, - })) - .await + self.send_action( + types::action::Kind::SetFood(types::SetFoodAction { + player_uuid, + food, + }), + ) + .await } ///Sends a `SetExperience` action to the server. pub async fn set_experience( @@ -122,15 +140,15 @@ impl Server { progress: impl Into>, amount: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SetExperience( - types::SetExperienceAction { - player_uuid, - level: level.into(), - progress: progress.into(), - amount: amount.into(), - }, - )) - .await + self.send_action( + types::action::Kind::SetExperience(types::SetExperienceAction { + player_uuid, + level: level.into(), + progress: progress.into(), + amount: amount.into(), + }), + ) + .await } ///Sends a `SetVelocity` action to the server. pub async fn set_velocity( @@ -138,11 +156,13 @@ impl Server { player_uuid: String, velocity: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SetVelocity(types::SetVelocityAction { - player_uuid, - velocity: velocity.into(), - })) - .await + self.send_action( + types::action::Kind::SetVelocity(types::SetVelocityAction { + player_uuid, + velocity: velocity.into(), + }), + ) + .await } ///Sends a `AddEffect` action to the server. pub async fn add_effect( @@ -153,14 +173,16 @@ impl Server { duration_ms: i64, show_particles: bool, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::AddEffect(types::AddEffectAction { - player_uuid, - effect_type: effect_type.into(), - level, - duration_ms, - show_particles, - })) - .await + self.send_action( + types::action::Kind::AddEffect(types::AddEffectAction { + player_uuid, + effect_type: effect_type.into(), + level, + duration_ms, + show_particles, + }), + ) + .await } ///Sends a `RemoveEffect` action to the server. pub async fn remove_effect( @@ -168,13 +190,13 @@ impl Server { player_uuid: String, effect_type: types::EffectType, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::RemoveEffect( - types::RemoveEffectAction { - player_uuid, - effect_type: effect_type.into(), - }, - )) - .await + self.send_action( + types::action::Kind::RemoveEffect(types::RemoveEffectAction { + player_uuid, + effect_type: effect_type.into(), + }), + ) + .await } ///Sends a `SendTitle` action to the server. pub async fn send_title( @@ -186,15 +208,17 @@ impl Server { duration_ms: impl Into>, fade_out_ms: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SendTitle(types::SendTitleAction { - player_uuid, - title, - subtitle: subtitle.into(), - fade_in_ms: fade_in_ms.into(), - duration_ms: duration_ms.into(), - fade_out_ms: fade_out_ms.into(), - })) - .await + self.send_action( + types::action::Kind::SendTitle(types::SendTitleAction { + player_uuid, + title, + subtitle: subtitle.into(), + fade_in_ms: fade_in_ms.into(), + duration_ms: duration_ms.into(), + fade_out_ms: fade_out_ms.into(), + }), + ) + .await } ///Sends a `SendPopup` action to the server. pub async fn send_popup( @@ -202,11 +226,13 @@ impl Server { player_uuid: String, message: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SendPopup(types::SendPopupAction { - player_uuid, - message, - })) - .await + self.send_action( + types::action::Kind::SendPopup(types::SendPopupAction { + player_uuid, + message, + }), + ) + .await } ///Sends a `SendTip` action to the server. pub async fn send_tip( @@ -214,11 +240,13 @@ impl Server { player_uuid: String, message: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::SendTip(types::SendTipAction { - player_uuid, - message, - })) - .await + self.send_action( + types::action::Kind::SendTip(types::SendTipAction { + player_uuid, + message, + }), + ) + .await } ///Sends a `PlaySound` action to the server. pub async fn play_sound( @@ -229,14 +257,16 @@ impl Server { volume: impl Into>, pitch: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::PlaySound(types::PlaySoundAction { - player_uuid, - sound: sound.into(), - position: position.into(), - volume: volume.into(), - pitch: pitch.into(), - })) - .await + self.send_action( + types::action::Kind::PlaySound(types::PlaySoundAction { + player_uuid, + sound: sound.into(), + position: position.into(), + volume: volume.into(), + pitch: pitch.into(), + }), + ) + .await } ///Sends a `ExecuteCommand` action to the server. pub async fn execute_command( @@ -244,13 +274,13 @@ impl Server { player_uuid: String, command: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::ExecuteCommand( - types::ExecuteCommandAction { - player_uuid, - command, - }, - )) - .await + self.send_action( + types::action::Kind::ExecuteCommand(types::ExecuteCommandAction { + player_uuid, + command, + }), + ) + .await } ///Sends a `WorldSetDefaultGameMode` action to the server. pub async fn world_set_default_game_mode( @@ -258,13 +288,13 @@ impl Server { world: impl Into>, game_mode: types::GameMode, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldSetDefaultGameMode( - types::WorldSetDefaultGameModeAction { - world: world.into(), - game_mode: game_mode.into(), - }, - )) - .await + self.send_action( + types::action::Kind::WorldSetDefaultGameMode(types::WorldSetDefaultGameModeAction { + world: world.into(), + game_mode: game_mode.into(), + }), + ) + .await } ///Sends a `WorldSetDifficulty` action to the server. pub async fn world_set_difficulty( @@ -272,13 +302,13 @@ impl Server { world: impl Into>, difficulty: types::Difficulty, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldSetDifficulty( - types::WorldSetDifficultyAction { - world: world.into(), - difficulty: difficulty.into(), - }, - )) - .await + self.send_action( + types::action::Kind::WorldSetDifficulty(types::WorldSetDifficultyAction { + world: world.into(), + difficulty: difficulty.into(), + }), + ) + .await } ///Sends a `WorldSetTickRange` action to the server. pub async fn world_set_tick_range( @@ -286,13 +316,13 @@ impl Server { world: impl Into>, tick_range: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldSetTickRange( - types::WorldSetTickRangeAction { - world: world.into(), - tick_range, - }, - )) - .await + self.send_action( + types::action::Kind::WorldSetTickRange(types::WorldSetTickRangeAction { + world: world.into(), + tick_range, + }), + ) + .await } ///Sends a `WorldSetBlock` action to the server. pub async fn world_set_block( @@ -301,14 +331,14 @@ impl Server { position: impl Into>, block: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldSetBlock( - types::WorldSetBlockAction { - world: world.into(), - position: position.into(), - block: block.into(), - }, - )) - .await + self.send_action( + types::action::Kind::WorldSetBlock(types::WorldSetBlockAction { + world: world.into(), + position: position.into(), + block: block.into(), + }), + ) + .await } ///Sends a `WorldPlaySound` action to the server. pub async fn world_play_sound( @@ -317,14 +347,14 @@ impl Server { sound: types::Sound, position: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldPlaySound( - types::WorldPlaySoundAction { - world: world.into(), - sound: sound.into(), - position: position.into(), - }, - )) - .await + self.send_action( + types::action::Kind::WorldPlaySound(types::WorldPlaySoundAction { + world: world.into(), + sound: sound.into(), + position: position.into(), + }), + ) + .await } ///Sends a `WorldAddParticle` action to the server. pub async fn world_add_particle( @@ -335,40 +365,40 @@ impl Server { block: impl Into>, face: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldAddParticle( - types::WorldAddParticleAction { - world: world.into(), - position: position.into(), - particle: particle.into(), - block: block.into(), - face: face.into(), - }, - )) - .await + self.send_action( + types::action::Kind::WorldAddParticle(types::WorldAddParticleAction { + world: world.into(), + position: position.into(), + particle: particle.into(), + block: block.into(), + face: face.into(), + }), + ) + .await } ///Sends a `WorldQueryEntities` action to the server. pub async fn world_query_entities( &self, world: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldQueryEntities( - types::WorldQueryEntitiesAction { - world: world.into(), - }, - )) - .await + self.send_action( + types::action::Kind::WorldQueryEntities(types::WorldQueryEntitiesAction { + world: world.into(), + }), + ) + .await } ///Sends a `WorldQueryPlayers` action to the server. pub async fn world_query_players( &self, world: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldQueryPlayers( - types::WorldQueryPlayersAction { - world: world.into(), - }, - )) - .await + self.send_action( + types::action::Kind::WorldQueryPlayers(types::WorldQueryPlayersAction { + world: world.into(), + }), + ) + .await } ///Sends a `WorldQueryEntitiesWithin` action to the server. pub async fn world_query_entities_within( @@ -376,12 +406,12 @@ impl Server { world: impl Into>, r#box: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action(types::action::Kind::WorldQueryEntitiesWithin( - types::WorldQueryEntitiesWithinAction { - world: world.into(), - r#box: r#box.into(), - }, - )) - .await + self.send_action( + types::action::Kind::WorldQueryEntitiesWithin(types::WorldQueryEntitiesWithinAction { + world: world.into(), + r#box: r#box.into(), + }), + ) + .await } } diff --git a/packages/rust/src/server/server.rs b/packages/rust/src/server/server.rs index 3efe1ca..b4281df 100644 --- a/packages/rust/src/server/server.rs +++ b/packages/rust/src/server/server.rs @@ -1,9 +1,6 @@ use tokio::sync::mpsc; -use crate::{ - event::{EventContext, EventResultUpdate}, - types::{self, PluginToHost}, -}; +use crate::types::{self, PluginToHost}; #[derive(Clone)] pub struct Server { @@ -58,37 +55,6 @@ impl Server { }; self.sender.send(msg).await } - - /// Internal helper to send an event result (cancel/mutate) - /// This is called by the auto-generated `dispatch_event` function. - #[doc(hidden)] - pub(crate) async fn send_event_result( - &self, - context: EventContext<'_, impl Sized>, - ) -> Result<(), mpsc::error::SendError> { - let (event_id, result) = context.into_result(); - - let payload = match result { - // Do nothing if the handler didn't mutate or cancel - EventResultUpdate::None => return Ok(()), - EventResultUpdate::Cancelled => types::EventResult { - event_id, - cancel: Some(true), - update: None, - }, - EventResultUpdate::Mutated(update) => types::EventResult { - event_id, - cancel: None, - update: Some(update), - }, - }; - - let msg = types::PluginToHost { - plugin_id: self.plugin_id.clone(), - payload: Some(types::PluginPayload::EventResult(payload)), - }; - self.sender.send(msg).await - } } mod helpers; diff --git a/packages/rust/xtask/src/generate_handlers.rs b/packages/rust/xtask/src/generate_handlers.rs index 55ce1fe..a738ba9 100644 --- a/packages/rust/xtask/src/generate_handlers.rs +++ b/packages/rust/xtask/src/generate_handlers.rs @@ -50,9 +50,14 @@ fn generate_handler_trait_tokens(ast: &File) -> Result { dispatch_fn_match_arms.push(quote! { types::event_envelope::Payload::#ident(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.#handler_fn_name(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; }, }) } diff --git a/packages/rust/xtask/src/generate_mutations.rs b/packages/rust/xtask/src/generate_mutations.rs index b5e18dc..57854d7 100644 --- a/packages/rust/xtask/src/generate_mutations.rs +++ b/packages/rust/xtask/src/generate_mutations.rs @@ -48,6 +48,16 @@ fn generate_event_mutations_tokens( })?; let mut helper_methods = Vec::new(); + helper_methods.push(quote! { + fn ensure_mutation_exists(&mut self) { + if !matches!(self.result, EventResult::Mutated(types::EventResultUpdate::#mutation_variant_name(_))) { + self.result = EventResult::Mutated( + types::EventResultUpdate::#mutation_variant_name(types::#mutation_struct_name::default()) + ); + } + } + }); + for field in &mutation_struct_def.fields { let field_name = field.ident.as_ref().unwrap(); let arg_type = get_api_type(field); @@ -56,12 +66,13 @@ fn generate_event_mutations_tokens( helper_methods.push(quote! { #[doc = #doc_string] - pub fn #setter_fn_name(&mut self, #field_name: #arg_type) { - let mutation = types::#mutation_struct_name { - #field_name: #field_name.into(), - ..Default::default() + pub fn #setter_fn_name(&mut self, #field_name: #arg_type) -> &mut Self { + self.ensure_mutation_exists(); + + if let EventResult::Mutated(types::EventResultUpdate::#mutation_variant_name(ref mut m)) = self.result { + m.#field_name = #field_name.into() }; - self.set_mutation(types::EventResultUpdate::#mutation_variant_name(mutation)); + self } }); } @@ -77,7 +88,7 @@ fn generate_event_mutations_tokens( //! This file is auto-generated by `xtask`. Do not edit manually. #![allow(clippy::all)] use crate::types; - use crate::event::EventContext; + use crate::event::{EventContext, EventResult}; #( #mutation_impls )* } From ab09772001edf81b39b1abb5f6ba53b051049130 Mon Sep 17 00:00:00 2001 From: HashimTheArab Date: Thu, 27 Nov 2025 00:58:48 +0300 Subject: [PATCH 19/22] feat: more debug logs --- plugin/adapters/plugin/manager.go | 20 ++++++++++++++++++++ plugin/adapters/plugin/process.go | 3 +++ 2 files changed, 23 insertions(+) diff --git a/plugin/adapters/plugin/manager.go b/plugin/adapters/plugin/manager.go index cf049de..5106e88 100644 --- a/plugin/adapters/plugin/manager.go +++ b/plugin/adapters/plugin/manager.go @@ -280,6 +280,7 @@ func (m *Manager) dispatchEvent(envelope *pb.EventEnvelope, expectResult bool) [ Event: envelope, }, } + proc.log.Debug("sending event", "event_id", envelope.EventId, "type", envelope.Type.String()) proc.queue(msg) if !expectResult { @@ -309,6 +310,13 @@ func (m *Manager) dispatchEvent(envelope *pb.EventEnvelope, expectResult bool) [ "event_id", envelope.EventId, "plugin_response_ms", pluginResponseTime.Milliseconds(), "plugin_response_us", pluginResponseTime.Microseconds()) + } else { + // General timing for non-command events + proc.log.Debug("plugin event response received", + "event_id", envelope.EventId, + "type", envelope.Type.String(), + "plugin_response_ms", pluginResponseTime.Milliseconds(), + "plugin_response_us", pluginResponseTime.Microseconds()) } } } @@ -347,6 +355,7 @@ func (m *Manager) dispatchEventParallel(envelope *pb.EventEnvelope, expectResult if expectResult { waitCh = proc.expectEventResult(envelope.EventId) } + proc.log.Debug("sending event", "event_id", envelope.EventId, "type", envelope.Type.String()) proc.queue(&pb.HostToPlugin{ PluginId: proc.id, Payload: &pb.HostToPlugin_Event{ @@ -356,6 +365,7 @@ func (m *Manager) dispatchEventParallel(envelope *pb.EventEnvelope, expectResult if !expectResult { return } + waitStart := time.Now() res, err := proc.waitEventResult(waitCh, eventResponseTimeout) if err != nil { if errors.Is(err, context.DeadlineExceeded) { @@ -364,6 +374,14 @@ func (m *Manager) dispatchEventParallel(envelope *pb.EventEnvelope, expectResult proc.discardEventResult(envelope.EventId) return } + pluginResponseTime := time.Since(waitStart) + if envelope.Type != pb.EventType_COMMAND { + proc.log.Debug("plugin event response received", + "event_id", envelope.EventId, + "type", envelope.Type.String(), + "plugin_response_ms", pluginResponseTime.Milliseconds(), + "plugin_response_us", pluginResponseTime.Microseconds()) + } results[idx] = res }) } @@ -386,8 +404,10 @@ func (m *Manager) emitCancellable(ctx cancelContext, envelope *pb.EventEnvelope) } // If any plugin cancelled, do not apply any mutations. if cancelled { + m.log.Debug("event cancelled by plugin", "event_id", envelope.EventId, "type", envelope.Type.String()) return nil } + m.log.Debug("event completed", "event_id", envelope.EventId, "type", envelope.Type.String(), "responses", len(results)) return results } diff --git a/plugin/adapters/plugin/process.go b/plugin/adapters/plugin/process.go index 6797622..a3051ba 100644 --- a/plugin/adapters/plugin/process.go +++ b/plugin/adapters/plugin/process.go @@ -379,6 +379,7 @@ func (p *pluginProcess) expectEventResult(eventID string) chan *pb.EventResult { p.pendingMu.Lock() p.pending[eventID] = ch p.pendingMu.Unlock() + p.log.Debug("waiting for event result", "event_id", eventID) return ch } @@ -401,6 +402,7 @@ func (p *pluginProcess) discardEventResult(eventID string) { close(ch) } p.pendingMu.Unlock() + p.log.Debug("discarded event result waiter", "event_id", eventID) } func (p *pluginProcess) deliverEventResult(res *pb.EventResult) { @@ -422,4 +424,5 @@ func (p *pluginProcess) deliverEventResult(res *pb.EventResult) { default: } close(ch) + p.log.Debug("delivered event result", "event_id", res.EventId) } From 7dd2943f040bec986d4ea2d00b1954819ad4ca82 Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Thu, 27 Nov 2025 00:14:45 -0500 Subject: [PATCH 20/22] all core features of v0.3.0 made and functioning. Need to cleanup some rough edges, add more testing and then will be good for release --- cmd/economy.db | Bin 12288 -> 0 bytes examples/plugins/rust/src/main.rs | 116 +- packages/rust/Cargo.toml | 4 +- packages/rust/macro/Cargo.toml | 2 +- packages/rust/macro/src/command.rs | 484 ++++++ packages/rust/macro/src/lib.rs | 159 +- packages/rust/macro/src/plugin.rs | 59 +- packages/rust/src/command.rs | 86 ++ packages/rust/src/event/context.rs | 1 - packages/rust/src/event/handler.rs | 185 +-- packages/rust/src/lib.rs | 16 +- packages/rust/src/server/helpers.rs | 1423 +++++++++++++++--- packages/rust/xtask/src/generate_actions.rs | 3 + packages/rust/xtask/src/generate_handlers.rs | 53 +- packages/rust/xtask/src/utils.rs | 45 +- 15 files changed, 2198 insertions(+), 438 deletions(-) delete mode 100644 cmd/economy.db create mode 100644 packages/rust/macro/src/command.rs create mode 100644 packages/rust/src/command.rs diff --git a/cmd/economy.db b/cmd/economy.db deleted file mode 100644 index c97043c5d1659bbc4dd1686b31f91db8cdc85edc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI$&r8EF6bJC66?KDA@zU$Nxq%(s>R5LeSha#!r_L7KX{22SgSqL}f_T&a&#U;S zcr+^v{K3GZgYP4+c?n5qKezPeIvAyrhSS-dNXcP4jB|EK#2726)KxA+Q^m$|pnCqL zUR4%Y_36H3d@#*$nc*6*s}^D&0uX=z1Rwwb2tWV=5P$##An+dro_G0{X`1{+PenYI z*<8-%`9nMI`d;W$=p6+<fWv)luN$fZow(6GKNQC7yhczpXg=;kv yr!E`PZXGzTRaD=_jd!LR0s;_#00bZa0SG_<0uX=z1Rwx`H51s=qrzW)0DJ;if>X-? diff --git a/examples/plugins/rust/src/main.rs b/examples/plugins/rust/src/main.rs index d7dc898..8c5c10f 100644 --- a/examples/plugins/rust/src/main.rs +++ b/examples/plugins/rust/src/main.rs @@ -4,8 +4,9 @@ /// pay: pay yourself money /// bal: view your balance / money use dragonfly_plugin::{ - Plugin, PluginRunner, Server, - event::{EventContext, EventHandler}, + Plugin, PluginRunner, + command::{Command, Ctx, command_handlers}, + event::EventHandler, event_handler, types, }; use sqlx::{SqlitePool, sqlite::SqlitePoolOptions}; @@ -15,7 +16,8 @@ use sqlx::{SqlitePool, sqlite::SqlitePoolOptions}; id = "rustic-economy", name = "Rustic Economy", version = "0.1.0", - api = "1.0.0" + api = "1.0.0", + commands(Eco) )] struct RusticEconomy { db: SqlitePool, @@ -68,86 +70,52 @@ impl RusticEconomy { } } -#[event_handler] -impl EventHandler for RusticEconomy { - async fn on_chat(&self, server: &Server, event: &mut EventContext<'_, types::ChatEvent>) { - let message = &event.data.message; - let player_uuid = &event.data.player_uuid; +#[derive(Command)] +#[command(name = "eco", description = "Rustic Economy commands.")] +pub enum Eco { + Pay { amount: f64 }, + Bal, +} - // Handle commands - if message.starts_with("!pay") { - // ALWAYS RESPOND TO THE SERVER ASAP. - // THEN DO SIDE WORK THAT MIGHT TAKE A GOOD BIT. - let parts: Vec<&str> = message.split_whitespace().collect(); - if parts.len() != 2 { - server - .send_chat(player_uuid.clone(), "Usage: !pay ".to_string()) +#[command_handlers] +impl Eco { + async fn pay(state: &RusticEconomy, ctx: Ctx<'_>, amount: f64) { + match state.add_money(&ctx.sender, amount).await { + Ok(new_balance) => ctx + .reply(format!( + "Added ${:.2}! New balance: ${:.2}", + amount, new_balance + )) + .await + .unwrap(), + Err(e) => { + eprintln!("Database error: {}", e); + ctx.reply("Error processing payment!".to_string()) .await - .expect("Bad error handling womp."); - return; + .unwrap() } + } + } - let amount: f64 = match parts[1].parse() { - Ok(amt) if amt > 0.0 => amt, - _ => { - server - .send_chat( - player_uuid.clone(), - "Please provide a valid positive amount!".to_string(), - ) - .await - .expect("Bad error handling sad."); - return; - } - }; - - match self.add_money(player_uuid, amount).await { - Ok(new_balance) => { - server - .send_chat( - player_uuid.clone(), - format!("Added ${:.2}! New balance: ${:.2}", amount, new_balance), - ) - .await - .expect("again error handling is bad"); - } - Err(e) => { - eprintln!("Database error: {}", e); - server - .send_chat(player_uuid.clone(), "Error processing payment!".to_string()) - .await - .expect("Bad error handling"); - } - } - event.cancel().await; - println!("I HAVE SENT THE CANCEL FULLY"); - } else if message.starts_with("!bal") { - // YET AGAIN RESPOND ASAP TO SERVER. - event.cancel().await; - match self.get_balance(player_uuid).await { - Ok(balance) => { - // These are fine to happen later. - // as they aren't bounded by the timout of server responses. - server - .send_chat( - player_uuid.clone(), - format!("Your balance: ${:.2}", balance), - ) - .await - .expect("oh shit i need to handle this better."); - } - Err(e) => { - eprintln!("Database error: {}", e); - server - .send_chat(player_uuid.clone(), "Error checking balance!".to_string()) - .await - .expect("again bad error handling"); - } + async fn bal(state: &RusticEconomy, ctx: Ctx<'_>) { + match state.get_balance(&ctx.sender).await { + Ok(balance) => ctx + .reply(format!("Your balance: ${:.2}", balance)) + .await + .unwrap(), + Err(e) => { + eprintln!("Database error: {}", e); + ctx.reply("Error checking balance!".to_string()) + .await + .unwrap() } } } } +#[event_handler] +impl EventHandler for RusticEconomy {} + #[tokio::main] async fn main() -> Result<(), Box> { println!("Starting the plugin..."); diff --git a/packages/rust/Cargo.toml b/packages/rust/Cargo.toml index 84b50a5..e1f3f43 100644 --- a/packages/rust/Cargo.toml +++ b/packages/rust/Cargo.toml @@ -7,11 +7,11 @@ insta.opt-level = 3 similar.opt-level = 3 [workspace.dependencies] -dragonfly-plugin-macro = { path = "macro", version = "0.2" } +dragonfly-plugin-macro = { path = "macro", version = "0.3" } [package] name = "dragonfly-plugin" -version = "0.2.0" +version = "0.3.0" edition = "2021" license = "MIT" repository = "https://github.com/secmc/dragonfly-plugins" diff --git a/packages/rust/macro/Cargo.toml b/packages/rust/macro/Cargo.toml index 69dd178..d90231d 100644 --- a/packages/rust/macro/Cargo.toml +++ b/packages/rust/macro/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dragonfly-plugin-macro" -version = "0.2.0" +version = "0.3.0" edition = "2021" license = "MIT" repository = "https://github.com/secmc/dragonfly-plugins" diff --git a/packages/rust/macro/src/command.rs b/packages/rust/macro/src/command.rs new file mode 100644 index 0000000..b5027a9 --- /dev/null +++ b/packages/rust/macro/src/command.rs @@ -0,0 +1,484 @@ +use heck::ToSnakeCase; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + spanned::Spanned, + Attribute, Data, DeriveInput, Expr, ExprLit, Fields, GenericArgument, Ident, Lit, LitStr, Meta, + PathArguments, Token, Type, TypePath, +}; + +struct CommandInfoParser { + pub name: LitStr, + pub description: LitStr, + pub aliases: Vec, +} + +impl Parse for CommandInfoParser { + fn parse(input: ParseStream) -> syn::Result { + let metas = Punctuated::::parse_terminated(input)?; + + let mut name = None; + let mut description = None; + let mut aliases = Vec::new(); + + for meta in metas { + match meta { + Meta::NameValue(nv) if nv.path.is_ident("name") => { + if let Expr::Lit(ExprLit { + lit: Lit::Str(s), .. + }) = nv.value + { + name = Some(s); + } else { + return Err(syn::Error::new( + nv.value.span(), + "expected string literal for `name`", + )); + } + } + Meta::NameValue(nv) if nv.path.is_ident("description") => { + if let Expr::Lit(ExprLit { + lit: Lit::Str(s), .. + }) = nv.value + { + description = Some(s); + } else { + return Err(syn::Error::new( + nv.value.span(), + "expected string literal for `description`", + )); + } + } + Meta::List(list) if list.path.is_ident("aliases") => { + aliases = list + .parse_args_with(Punctuated::::parse_terminated)? + .into_iter() + .collect(); + } + _ => { + return Err(syn::Error::new( + meta.span(), + "unrecognized command attribute", + )); + } + } + } + + Ok(Self { + name: name.ok_or_else(|| { + syn::Error::new(input.span(), "missing required attribute `name`") + })?, + description: description.unwrap_or_else(|| LitStr::new("", input.span())), + aliases, + }) + } +} + +pub fn generate_command_impls(ast: &DeriveInput, attr: &Attribute) -> TokenStream { + let command_info = match attr.parse_args::() { + Ok(info) => info, + Err(e) => return e.to_compile_error(), + }; + + let cmd_ident = &ast.ident; + let cmd_name_lit = &command_info.name; + let cmd_desc_lit = &command_info.description; + let aliases_lits = &command_info.aliases; + + let shape = match collect_command_shape(ast) { + Ok(s) => s, + Err(e) => return e.to_compile_error(), + }; + + let spec_impl = generate_spec_impl(cmd_ident, cmd_name_lit, cmd_desc_lit, aliases_lits, &shape); + let try_from_impl = generate_try_from_impl(cmd_ident, cmd_name_lit, &shape); + + quote! { + #spec_impl + #try_from_impl + } +} + +fn generate_spec_impl( + cmd_ident: &Ident, + cmd_name_lit: &LitStr, + cmd_desc_lit: &LitStr, + aliases_lits: &[LitStr], + shape: &CommandShape, +) -> TokenStream { + let params_tokens = match shape { + CommandShape::Struct { params } => { + let specs = params.iter().map(param_to_spec); + quote! { vec![ #( #specs ),* ] } + } + CommandShape::Enum { variants } => { + // First param: subcommand enum + let variant_names: Vec = + variants.iter().map(|v| v.name_snake.clone()).collect(); + let subcommand_spec = quote! { + dragonfly_plugin::types::ParamSpec { + name: "subcommand".to_string(), + r#type: dragonfly_plugin::types::ParamType::ParamEnum as i32, + optional: false, + suffix: String::new(), + enum_values: vec![ #( #variant_names.to_string() ),* ], + } + }; + + // Merge all variant params, marking as optional if not present in all variants + let merged = merge_variant_params(variants); + let merged_specs = merged.iter().map(param_to_spec); + + quote! { vec![ #subcommand_spec, #( #merged_specs ),* ] } + } + }; + + quote! { + impl #cmd_ident { + pub fn spec() -> dragonfly_plugin::types::CommandSpec { + dragonfly_plugin::types::CommandSpec { + name: #cmd_name_lit.to_string(), + description: #cmd_desc_lit.to_string(), + aliases: vec![ #( #aliases_lits.to_string() ),* ], + params: #params_tokens, + } + } + } + } +} + +fn param_to_spec(p: &ParamMeta) -> TokenStream { + let name_str = p.field_ident.to_string(); + let param_type_expr = &p.param_type_expr; + let is_optional = p.is_optional; + + quote! { + dragonfly_plugin::types::ParamSpec { + name: #name_str.to_string(), + r#type: #param_type_expr as i32, + optional: #is_optional, + suffix: String::new(), + enum_values: Vec::new(), + } + } +} + +/// Merge params from all variants: a param is optional if not present in every variant. +fn merge_variant_params(variants: &[VariantMeta]) -> Vec { + use std::collections::HashMap; + + // Collect all unique param names with their metadata + let mut seen: HashMap = HashMap::new(); // name -> (meta, count) + + for variant in variants { + for param in &variant.params { + let name = param.field_ident.to_string(); + seen.entry(name) + .and_modify(|(_, count)| *count += 1) + .or_insert_with(|| (param.clone(), 1)); + } + } + + let variant_count = variants.len(); + + seen.into_iter() + .map(|(_, (mut meta, count))| { + // If not present in all variants, mark as optional + if count < variant_count { + meta.is_optional = true; + } + meta + }) + .collect() +} + +fn generate_try_from_impl( + cmd_ident: &Ident, + cmd_name_lit: &LitStr, + shape: &CommandShape, +) -> TokenStream { + let body = match shape { + CommandShape::Struct { params } => { + let field_inits = params.iter().map(|p| struct_field_init(p, 0)); + quote! { + Ok(Self { + #( #field_inits, )* + }) + } + } + CommandShape::Enum { variants } => { + let match_arms = variants.iter().map(|v| { + let variant_ident = &v.ident; + let name_snake = &v.name_snake; + + if v.params.is_empty() { + // Unit-like variant + quote! { + #name_snake => Ok(Self::#variant_ident), + } + } else { + // Struct-like variant with fields + // Note: arg index starts at 1 (index 0 is the subcommand itself) + let field_inits = v.params.iter().map(enum_field_init); + quote! { + #name_snake => Ok(Self::#variant_ident { + #( #field_inits, )* + }), + } + } + }); + + quote! { + let subcommand = event.args.first() + .ok_or(dragonfly_plugin::command::CommandParseError::Missing("subcommand"))? + .as_str(); + + match subcommand { + #( #match_arms )* + _ => Err(dragonfly_plugin::command::CommandParseError::UnknownSubcommand), + } + } + } + }; + + quote! { + impl ::core::convert::TryFrom<&dragonfly_plugin::types::CommandEvent> for #cmd_ident { + type Error = dragonfly_plugin::command::CommandParseError; + + fn try_from(event: &dragonfly_plugin::types::CommandEvent) -> Result { + if event.command != #cmd_name_lit { + return Err(dragonfly_plugin::command::CommandParseError::NoMatch); + } + + #body + } + } + } +} + +/// Generate field init for struct commands (args start at index 0). +fn struct_field_init(p: &ParamMeta, offset: usize) -> TokenStream { + let ident = &p.field_ident; + let idx = p.index + offset; + let name_str = ident.to_string(); + let ty = &p.field_ty; + + if p.is_optional { + quote! { + #ident: dragonfly_plugin::command::parse_optional_arg::<#ty>(&event.args, #idx, #name_str)? + } + } else { + quote! { + #ident: dragonfly_plugin::command::parse_required_arg::<#ty>(&event.args, #idx, #name_str)? + } + } +} + +/// Generate field init for enum variant (args start at index 1, after subcommand). +fn enum_field_init(p: &ParamMeta) -> TokenStream { + let ident = &p.field_ident; + let idx = p.index + 1; // +1 because index 0 is the subcommand + let name_str = ident.to_string(); + let ty = &p.field_ty; + + if p.is_optional { + quote! { + #ident: dragonfly_plugin::command::parse_optional_arg::<#ty>(&event.args, #idx, #name_str)? + } + } else { + quote! { + #ident: dragonfly_plugin::command::parse_required_arg::<#ty>(&event.args, #idx, #name_str)? + } + } +} + +/// Metadata for a single command parameter (struct field or enum variant field). +#[derive(Clone)] +struct ParamMeta { + /// Rust field identifier, e.g. `amount` + field_ident: Ident, + /// Full Rust type of the field, e.g. `f64` or `Option` + field_ty: Type, + /// Expression for ParamType (e.g. `ParamType::ParamFloat`) + param_type_expr: TokenStream, + /// Whether this is optional in the spec + is_optional: bool, + /// Position index in args (0-based, relative to this variant/struct) + index: usize, +} + +/// Metadata for a single enum variant (subcommand). +struct VariantMeta { + /// Variant identifier, e.g. `Pay` + ident: Ident, + /// Snake-case name for matching, e.g. `"pay"` + name_snake: String, + /// Parameters (fields) for this variant + params: Vec, +} + +/// The shape of a command: either a struct or an enum with variants. +enum CommandShape { + Struct { params: Vec }, + Enum { variants: Vec }, +} +fn collect_command_shape(ast: &DeriveInput) -> syn::Result { + match &ast.data { + Data::Struct(data) => { + let params = collect_params_from_fields(&data.fields)?; + Ok(CommandShape::Struct { params }) + } + Data::Enum(data) => { + let mut variants = Vec::new(); + + for variant in &data.variants { + let ident = variant.ident.clone(); + let name_snake = ident.to_string().to_snake_case(); + + // Collect params for this variant's fields + // Note: index is relative to args AFTER the subcommand arg + let params = collect_params_from_fields(&variant.fields)?; + + variants.push(VariantMeta { + ident, + name_snake, + params, + }); + } + + if variants.is_empty() { + return Err(syn::Error::new_spanned( + &ast.ident, + "enum commands must have at least one variant", + )); + } + + Ok(CommandShape::Enum { variants }) + } + Data::Union(_) => Err(syn::Error::new_spanned( + ast, + "unions are not supported for #[derive(Command)]", + )), + } +} + +fn collect_params_from_fields(fields: &Fields) -> syn::Result> { + let fields = match fields { + Fields::Named(named) => &named.named, + Fields::Unit => { + // No params + return Ok(Vec::new()); + } + Fields::Unnamed(_) => { + // TODO: maybe do support this but typenames are used as param names? + return Err(syn::Error::new_spanned( + fields, + "tuple structs are not supported for commands; use named fields", + )); + } + }; + + let mut out = Vec::new(); + for (index, field) in fields.iter().enumerate() { + let field_ident = field + .ident + .clone() + .expect("command struct fields must be named"); + let field_ty = field.ty.clone(); + + let (param_type_expr, is_optional) = get_param_type(&field_ty); + + out.push(ParamMeta { + field_ident, + field_ty, + param_type_expr, + is_optional, + index, + }); + } + + Ok(out) +} + +fn get_param_type(ty: &Type) -> (TokenStream, bool) { + if let Some(inner) = option_inner(ty) { + let (inner_param, _inner_opt) = get_param_type(inner); + return (inner_param, true); + } + + if let Type::Reference(r) = ty { + return get_param_type(&r.elem); + } + + if let Type::Path(TypePath { path, .. }) = ty { + if let Some(seg) = path.segments.last() { + let ident = seg.ident.to_string(); + + // Floats + if ident == "f32" || ident == "f64" { + return ( + quote! { dragonfly_plugin::types::ParamType::ParamFloat }, + false, + ); + } + + if matches!( + ident.as_str(), + "i8" | "i16" + | "i32" + | "i64" + | "i128" + | "u8" + | "u16" + | "u32" + | "u64" + | "u128" + | "isize" + | "usize" + ) { + return ( + quote! { dragonfly_plugin::types::ParamType::ParamInt }, + false, + ); + } + + if ident == "bool" { + return ( + quote! { dragonfly_plugin::types::ParamType::ParamBool }, + false, + ); + } + + if ident == "String" { + return ( + quote! { dragonfly_plugin::types::ParamType::ParamString }, + false, + ); + } + } + } + + ( + quote! { dragonfly_plugin::types::ParamType::ParamString }, + false, + ) +} + +fn option_inner(ty: &Type) -> Option<&Type> { + if let Type::Path(TypePath { path, .. }) = ty { + if let Some(seg) = path.segments.last() { + if seg.ident == "Option" { + if let PathArguments::AngleBracketed(args) = &seg.arguments { + for arg in &args.args { + if let GenericArgument::Type(inner) = arg { + return Some(inner); + } + } + } + } + } + } + None +} diff --git a/packages/rust/macro/src/lib.rs b/packages/rust/macro/src/lib.rs index d059b1f..2a573b3 100644 --- a/packages/rust/macro/src/lib.rs +++ b/packages/rust/macro/src/lib.rs @@ -1,11 +1,14 @@ +mod command; mod plugin; use heck::ToPascalCase; use proc_macro::TokenStream; use quote::{format_ident, quote}; -use syn::{parse_macro_input, Attribute, DeriveInput, ImplItem, ItemImpl}; +use syn::{ + parse_macro_input, Attribute, DeriveInput, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, Type, +}; -use crate::plugin::generate_plugin_impl; +use crate::{command::generate_command_impls, plugin::generate_plugin_impl}; #[proc_macro_derive(Plugin, attributes(plugin, events))] pub fn handler_derive(input: TokenStream) -> TokenStream { @@ -107,3 +110,155 @@ pub fn event_handler(_attr: TokenStream, item: TokenStream) -> TokenStream { final_output.into() } + +#[proc_macro_derive(Command, attributes(command))] +pub fn command_derive(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + + let command_attr = match find_attribute( + &ast, + "command", + "Missing `#[command(...)]` attribute with metadata.", + ) { + Ok(attr) => attr, + Err(e) => return e.to_compile_error().into(), + }; + + generate_command_impls(&ast, command_attr).into() +} + +#[proc_macro_attribute] +pub fn command_handlers(_attr: TokenStream, item: TokenStream) -> TokenStream { + let item_clone = item.clone(); + + let impl_block = match syn::parse::(item) { + Ok(block) => block, + Err(_) => return item_clone, // Keep LSP happy during typing + }; + + // Must be an inherent impl (not a trait impl) + if impl_block.trait_.is_some() { + return syn::Error::new_spanned( + &impl_block, + "#[command_handlers] must be on an inherent impl, not a trait impl", + ) + .to_compile_error() + .into(); + } + + let cmd_ty = &impl_block.self_ty; + + // Collect handler methods and extract state type + let mut state_ty: Option = None; + let mut handlers: Vec = Vec::new(); + + for item in &impl_block.items { + let ImplItem::Fn(method) = item else { continue }; + + // Skip non-async methods + if method.sig.asyncness.is_none() { + continue; + } + + let method_name = &method.sig.ident; + let variant_name = format_ident!("{}", method_name.to_string().to_pascal_case()); + + // Extract params: expect (state: &T, ctx: Ctx, ...fields) + let mut inputs = method.sig.inputs.iter(); + + // First param: state: &SomePlugin + let Some(FnArg::Typed(state_pat)) = inputs.next() else { + return syn::Error::new_spanned( + &method.sig, + "handler must have at least (state: &Plugin, ctx: Ctx)", + ) + .to_compile_error() + .into(); + }; + + // Extract and validate state type + let extracted_state_ty = (*state_pat.ty).clone(); + match &state_ty { + None => state_ty = Some(extracted_state_ty), + Some(prev) => { + // All handlers must use the same state type + if quote!(#prev).to_string() != quote!(#extracted_state_ty).to_string() { + return syn::Error::new_spanned( + &state_pat.ty, + "all handlers must use the same state type", + ) + .to_compile_error() + .into(); + } + } + } + + // Second param: ctx: Ctx (skip validation for now) + let _ = inputs.next(); + + // Remaining params are the variant fields + let field_idents: Vec = inputs + .filter_map(|arg| { + if let FnArg::Typed(PatType { pat, .. }) = arg { + if let Pat::Ident(pat_ident) = &**pat { + return Some(pat_ident.ident.clone()); + } + } + None + }) + .collect(); + + handlers.push(HandlerMeta { + method_name: method_name.clone(), + variant_name, + field_idents, + }); + } + + let Some(state_ty) = state_ty else { + return syn::Error::new_spanned(&impl_block, "no handler methods found") + .to_compile_error() + .into(); + }; + + // Generate match arms + let match_arms = handlers.iter().map(|h| { + let method_name = &h.method_name; + let variant_name = &h.variant_name; + let fields = &h.field_idents; + + if fields.is_empty() { + quote! { + Self::#variant_name => Self::#method_name(state, ctx).await, + } + } else { + quote! { + Self::#variant_name { #( #fields ),* } => Self::#method_name(state, ctx, #( #fields ),*).await, + } + } + }); + + let execute_impl = quote! { + impl #cmd_ty { + pub async fn __execute(self, state: #state_ty, ctx: dragonfly_plugin::command::Ctx<'_>) { + match self { + #( #match_arms )* + } + } + } + }; + + let original = quote! { #impl_block }; + + quote! { + #original + #execute_impl + } + .into() +} + +struct HandlerMeta { + method_name: Ident, + variant_name: Ident, + field_idents: Vec, +} diff --git a/packages/rust/macro/src/plugin.rs b/packages/rust/macro/src/plugin.rs index 30976ac..109c75a 100644 --- a/packages/rust/macro/src/plugin.rs +++ b/packages/rust/macro/src/plugin.rs @@ -10,6 +10,7 @@ struct PluginInfoParser { pub name: LitStr, pub version: LitStr, pub api: LitStr, + pub commands: Vec, } impl Parse for PluginInfoParser { @@ -20,6 +21,7 @@ impl Parse for PluginInfoParser { let mut name = None; let mut version = None; let mut api = None; + let mut commands = Vec::new(); for meta in metas { match meta { @@ -64,10 +66,17 @@ impl Parse for PluginInfoParser { )); } } + Meta::List(list) if list.path.is_ident("commands") => { + // Parse commands(Ident, Ident, ...) + commands = list + .parse_args_with(Punctuated::::parse_terminated)? + .into_iter() + .collect(); + } _ => { return Err(syn::Error::new_spanned( meta, - "Expected `key = \"value\"` format", + "Expected `key = \"value\"` or `commands(...)` format", )); } }; @@ -89,6 +98,7 @@ impl Parse for PluginInfoParser { name, version, api, + commands, }) } } @@ -108,6 +118,51 @@ pub(crate) fn generate_plugin_impl( let version_lit = &plugin_info.version; let api_lit = &plugin_info.api; + let commands = &plugin_info.commands; + + let command_registry_impl = if commands.is_empty() { + // No commands: empty impl uses default (returns empty vec) + quote! { + impl dragonfly_plugin::command::CommandRegistry for #derive_name {} + } + } else { + // Has commands: generate get_commands() and __dispatch_commands() + let spec_calls = commands.iter().map(|cmd| { + quote! { #cmd::spec() } + }); + + let dispatch_arms = commands.iter().map(|cmd| { + quote! { + if let Ok(cmd) = #cmd::try_from(event.data) { + event.cancel().await; + cmd.__execute(self, ctx).await; + return true; + } + } + }); + + quote! { + impl dragonfly_plugin::command::CommandRegistry for #derive_name { + fn get_commands(&self) -> Vec { + vec![ #( #spec_calls ),* ] + } + + async fn dispatch_commands( + &self, + server: &dragonfly_plugin::Server, + event: &mut dragonfly_plugin::event::EventContext<'_, dragonfly_plugin::types::CommandEvent>, + ) -> bool { + use ::core::convert::TryFrom; + let ctx = dragonfly_plugin::command::Ctx::new(server, event.data.player_uuid.clone()); + + #( #dispatch_arms )* + + false + } + } + } + }; + quote! { impl dragonfly_plugin::Plugin for #derive_name { fn get_info(&self) -> dragonfly_plugin::PluginInfo<'static> { @@ -123,5 +178,7 @@ pub(crate) fn generate_plugin_impl( fn get_version(&self) -> &'static str { #version_lit } fn get_api_version(&self) -> &'static str { #api_lit } } + + #command_registry_impl } } diff --git a/packages/rust/src/command.rs b/packages/rust/src/command.rs new file mode 100644 index 0000000..01644cc --- /dev/null +++ b/packages/rust/src/command.rs @@ -0,0 +1,86 @@ +use crate::{server::Server, types}; + +pub use dragonfly_plugin_macro::{command_handlers, Command}; +use tokio::sync::mpsc; + +/// Per-command execution context. +pub struct Ctx<'a> { + pub server: &'a Server, + pub sender: String, +} + +impl<'a> Ctx<'a> { + pub fn new(server: &'a Server, player_uuid: String) -> Self { + Self { + server, + sender: player_uuid, + } + } + + pub async fn reply( + &self, + msg: impl Into, + ) -> Result<(), mpsc::error::SendError> { + self.server.send_chat(self.sender.clone(), msg.into()).await + } +} + +/// Trait plugins use to expose commands to the host. +pub trait CommandRegistry { + fn get_commands(&self) -> Vec { + Vec::new() + } + + /// Dispatch to registered commands. Returns true if a command was handled. + #[allow(async_fn_in_trait)] + async fn dispatch_commands( + &self, + _server: &crate::Server, + _event: &mut crate::event::EventContext<'_, types::CommandEvent>, + ) -> bool { + false + } +} + +#[derive(Debug)] +pub enum CommandParseError { + NoMatch, + Missing(&'static str), + Invalid(&'static str), + UnknownSubcommand, +} + +/// Parse a required argument at the given index. +pub fn parse_required_arg( + args: &[String], + index: usize, + name: &'static str, +) -> Result +where + T: std::str::FromStr, +{ + let s = args.get(index).ok_or(CommandParseError::Missing(name))?; + s.parse().map_err(|_| CommandParseError::Invalid(name)) +} + +/// Parse an optional argument at the given index. +/// Returns Ok(None) if the argument is missing. +/// Returns Ok(Some(value)) if present and parseable. +/// Returns Err if present but invalid. +pub fn parse_optional_arg( + args: &[String], + index: usize, + name: &'static str, +) -> Result, CommandParseError> +where + T: std::str::FromStr, +{ + match args.get(index) { + None => Ok(None), + Some(s) if s.is_empty() => Ok(None), + Some(s) => s + .parse() + .map(Some) + .map_err(|_| CommandParseError::Invalid(name)), + } +} diff --git a/packages/rust/src/event/context.rs b/packages/rust/src/event/context.rs index e69c67a..31c988c 100644 --- a/packages/rust/src/event/context.rs +++ b/packages/rust/src/event/context.rs @@ -64,7 +64,6 @@ impl<'a, T> EventContext<'a, T> { if self.sent { return; } - self.sent = true; // result is still EventResultUpdate::None, which sends ack self.send().await; } diff --git a/packages/rust/src/event/handler.rs b/packages/rust/src/event/handler.rs index f865579..63e63cb 100644 --- a/packages/rust/src/event/handler.rs +++ b/packages/rust/src/event/handler.rs @@ -1,350 +1,308 @@ //! This file is auto-generated by `xtask`. Do not edit manually. #![allow(async_fn_in_trait)] -use crate::{event::EventContext, types, EventSubscriptions, Server}; +use crate::{ + event::EventContext, types, Server, EventSubscriptions, command::CommandRegistry, +}; pub trait EventHandler: EventSubscriptions + Send + Sync { ///Handler for the `PlayerJoin` event. async fn on_player_join( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerJoinEvent>, - ) { - } + ) {} ///Handler for the `PlayerQuit` event. async fn on_player_quit( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerQuitEvent>, - ) { - } + ) {} ///Handler for the `PlayerMove` event. async fn on_player_move( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerMoveEvent>, - ) { - } + ) {} ///Handler for the `PlayerJump` event. async fn on_player_jump( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerJumpEvent>, - ) { - } + ) {} ///Handler for the `PlayerTeleport` event. async fn on_player_teleport( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerTeleportEvent>, - ) { - } + ) {} ///Handler for the `PlayerChangeWorld` event. async fn on_player_change_world( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerChangeWorldEvent>, - ) { - } + ) {} ///Handler for the `PlayerToggleSprint` event. async fn on_player_toggle_sprint( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerToggleSprintEvent>, - ) { - } + ) {} ///Handler for the `PlayerToggleSneak` event. async fn on_player_toggle_sneak( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerToggleSneakEvent>, - ) { - } + ) {} ///Handler for the `Chat` event. - async fn on_chat(&self, _server: &Server, _event: &mut EventContext<'_, types::ChatEvent>) {} + async fn on_chat( + &self, + _server: &Server, + _event: &mut EventContext<'_, types::ChatEvent>, + ) {} ///Handler for the `PlayerFoodLoss` event. async fn on_player_food_loss( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerFoodLossEvent>, - ) { - } + ) {} ///Handler for the `PlayerHeal` event. async fn on_player_heal( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerHealEvent>, - ) { - } + ) {} ///Handler for the `PlayerHurt` event. async fn on_player_hurt( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerHurtEvent>, - ) { - } + ) {} ///Handler for the `PlayerDeath` event. async fn on_player_death( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerDeathEvent>, - ) { - } + ) {} ///Handler for the `PlayerRespawn` event. async fn on_player_respawn( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerRespawnEvent>, - ) { - } + ) {} ///Handler for the `PlayerSkinChange` event. async fn on_player_skin_change( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerSkinChangeEvent>, - ) { - } + ) {} ///Handler for the `PlayerFireExtinguish` event. async fn on_player_fire_extinguish( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerFireExtinguishEvent>, - ) { - } + ) {} ///Handler for the `PlayerStartBreak` event. async fn on_player_start_break( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerStartBreakEvent>, - ) { - } + ) {} ///Handler for the `BlockBreak` event. async fn on_block_break( &self, _server: &Server, _event: &mut EventContext<'_, types::BlockBreakEvent>, - ) { - } + ) {} ///Handler for the `PlayerBlockPlace` event. async fn on_player_block_place( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerBlockPlaceEvent>, - ) { - } + ) {} ///Handler for the `PlayerBlockPick` event. async fn on_player_block_pick( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerBlockPickEvent>, - ) { - } + ) {} ///Handler for the `PlayerItemUse` event. async fn on_player_item_use( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerItemUseEvent>, - ) { - } + ) {} ///Handler for the `PlayerItemUseOnBlock` event. async fn on_player_item_use_on_block( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerItemUseOnBlockEvent>, - ) { - } + ) {} ///Handler for the `PlayerItemUseOnEntity` event. async fn on_player_item_use_on_entity( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerItemUseOnEntityEvent>, - ) { - } + ) {} ///Handler for the `PlayerItemRelease` event. async fn on_player_item_release( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerItemReleaseEvent>, - ) { - } + ) {} ///Handler for the `PlayerItemConsume` event. async fn on_player_item_consume( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerItemConsumeEvent>, - ) { - } + ) {} ///Handler for the `PlayerAttackEntity` event. async fn on_player_attack_entity( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerAttackEntityEvent>, - ) { - } + ) {} ///Handler for the `PlayerExperienceGain` event. async fn on_player_experience_gain( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerExperienceGainEvent>, - ) { - } + ) {} ///Handler for the `PlayerPunchAir` event. async fn on_player_punch_air( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerPunchAirEvent>, - ) { - } + ) {} ///Handler for the `PlayerSignEdit` event. async fn on_player_sign_edit( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerSignEditEvent>, - ) { - } + ) {} ///Handler for the `PlayerLecternPageTurn` event. async fn on_player_lectern_page_turn( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerLecternPageTurnEvent>, - ) { - } + ) {} ///Handler for the `PlayerItemDamage` event. async fn on_player_item_damage( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerItemDamageEvent>, - ) { - } + ) {} ///Handler for the `PlayerItemPickup` event. async fn on_player_item_pickup( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerItemPickupEvent>, - ) { - } + ) {} ///Handler for the `PlayerHeldSlotChange` event. async fn on_player_held_slot_change( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerHeldSlotChangeEvent>, - ) { - } + ) {} ///Handler for the `PlayerItemDrop` event. async fn on_player_item_drop( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerItemDropEvent>, - ) { - } + ) {} ///Handler for the `PlayerTransfer` event. async fn on_player_transfer( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerTransferEvent>, - ) { - } + ) {} ///Handler for the `Command` event. async fn on_command( &self, _server: &Server, _event: &mut EventContext<'_, types::CommandEvent>, - ) { - } + ) {} ///Handler for the `PlayerDiagnostics` event. async fn on_player_diagnostics( &self, _server: &Server, _event: &mut EventContext<'_, types::PlayerDiagnosticsEvent>, - ) { - } + ) {} ///Handler for the `WorldLiquidFlow` event. async fn on_world_liquid_flow( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldLiquidFlowEvent>, - ) { - } + ) {} ///Handler for the `WorldLiquidDecay` event. async fn on_world_liquid_decay( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldLiquidDecayEvent>, - ) { - } + ) {} ///Handler for the `WorldLiquidHarden` event. async fn on_world_liquid_harden( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldLiquidHardenEvent>, - ) { - } + ) {} ///Handler for the `WorldSound` event. async fn on_world_sound( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldSoundEvent>, - ) { - } + ) {} ///Handler for the `WorldFireSpread` event. async fn on_world_fire_spread( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldFireSpreadEvent>, - ) { - } + ) {} ///Handler for the `WorldBlockBurn` event. async fn on_world_block_burn( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldBlockBurnEvent>, - ) { - } + ) {} ///Handler for the `WorldCropTrample` event. async fn on_world_crop_trample( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldCropTrampleEvent>, - ) { - } + ) {} ///Handler for the `WorldLeavesDecay` event. async fn on_world_leaves_decay( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldLeavesDecayEvent>, - ) { - } + ) {} ///Handler for the `WorldEntitySpawn` event. async fn on_world_entity_spawn( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldEntitySpawnEvent>, - ) { - } + ) {} ///Handler for the `WorldEntityDespawn` event. async fn on_world_entity_despawn( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldEntityDespawnEvent>, - ) { - } + ) {} ///Handler for the `WorldExplosion` event. async fn on_world_explosion( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldExplosionEvent>, - ) { - } + ) {} ///Handler for the `WorldClose` event. async fn on_world_close( &self, _server: &Server, _event: &mut EventContext<'_, types::WorldCloseEvent>, - ) { - } + ) {} } #[doc(hidden)] pub async fn dispatch_event( server: &Server, - handler: &impl EventHandler, + handler: &(impl EventHandler + CommandRegistry), envelope: &types::EventEnvelope, ) { let Some(payload) = &envelope.payload else { @@ -508,9 +466,7 @@ pub async fn dispatch_event( server.sender.clone(), server.plugin_id.clone(), ); - handler - .on_player_fire_extinguish(server, &mut context) - .await; + handler.on_player_fire_extinguish(server, &mut context).await; context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerStartBreak(e) => { @@ -570,9 +526,7 @@ pub async fn dispatch_event( server.sender.clone(), server.plugin_id.clone(), ); - handler - .on_player_item_use_on_block(server, &mut context) - .await; + handler.on_player_item_use_on_block(server, &mut context).await; context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerItemUseOnEntity(e) => { @@ -582,9 +536,7 @@ pub async fn dispatch_event( server.sender.clone(), server.plugin_id.clone(), ); - handler - .on_player_item_use_on_entity(server, &mut context) - .await; + handler.on_player_item_use_on_entity(server, &mut context).await; context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerItemRelease(e) => { @@ -624,9 +576,7 @@ pub async fn dispatch_event( server.sender.clone(), server.plugin_id.clone(), ); - handler - .on_player_experience_gain(server, &mut context) - .await; + handler.on_player_experience_gain(server, &mut context).await; context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerPunchAir(e) => { @@ -656,9 +606,7 @@ pub async fn dispatch_event( server.sender.clone(), server.plugin_id.clone(), ); - handler - .on_player_lectern_page_turn(server, &mut context) - .await; + handler.on_player_lectern_page_turn(server, &mut context).await; context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerItemDamage(e) => { @@ -688,9 +636,7 @@ pub async fn dispatch_event( server.sender.clone(), server.plugin_id.clone(), ); - handler - .on_player_held_slot_change(server, &mut context) - .await; + handler.on_player_held_slot_change(server, &mut context).await; context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerItemDrop(e) => { @@ -720,7 +666,10 @@ pub async fn dispatch_event( server.sender.clone(), server.plugin_id.clone(), ); - handler.on_command(server, &mut context).await; + let handled = handler.dispatch_commands(server, &mut context).await; + if !handled { + handler.on_command(server, &mut context).await; + } context.send_ack_if_needed().await; } types::event_envelope::Payload::PlayerDiagnostics(e) => { diff --git a/packages/rust/src/lib.rs b/packages/rust/src/lib.rs index c157ad6..42f3a4d 100644 --- a/packages/rust/src/lib.rs +++ b/packages/rust/src/lib.rs @@ -12,6 +12,7 @@ pub mod types { }; } +pub mod command; pub mod event; #[path = "server/server.rs"] pub mod server; @@ -31,6 +32,8 @@ use hyper_util::rt::TokioIo; #[cfg(unix)] use tokio::net::UnixStream; +use crate::command::CommandRegistry; + /// Helper function to connect to the server, supporting both Unix sockets and TCP. async fn connect_to_server( addr: &str, @@ -89,8 +92,9 @@ impl PluginRunner { name: plugin.get_name().to_owned(), version: plugin.get_version().to_owned(), api_version: plugin.get_api_version().to_owned(), - commands: vec![], + commands: plugin.get_commands(), custom_items: vec![], + custom_blocks: vec![], })), }; tx.send(hello_msg).await?; @@ -103,7 +107,13 @@ impl PluginRunner { sender: tx.clone(), }; - let events = plugin.get_subscriptions(); + let mut events = plugin.get_subscriptions(); + + // Auto-subscribe to Command if plugin has registered commands + if !plugin.get_commands().is_empty() && !events.contains(&types::EventType::Command) { + events.push(types::EventType::Command); + } + if !events.is_empty() { println!("Subscribing to {} event types...", events.len()); server.subscribe(events).await?; @@ -181,7 +191,7 @@ pub struct PluginInfo<'a> { /// ) { } /// } /// ``` -pub trait Plugin: EventHandler + EventSubscriptions { +pub trait Plugin: EventHandler + EventSubscriptions + CommandRegistry { fn get_info(&self) -> PluginInfo<'_>; fn get_id(&self) -> &str; diff --git a/packages/rust/src/server/helpers.rs b/packages/rust/src/server/helpers.rs index 85f836b..7eb2699 100644 --- a/packages/rust/src/server/helpers.rs +++ b/packages/rust/src/server/helpers.rs @@ -8,13 +8,11 @@ impl Server { target_uuid: String, message: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SendChat(types::SendChatAction { - target_uuid, - message, - }), - ) - .await + self.send_action(types::action::Kind::SendChat(types::SendChatAction { + target_uuid, + message, + })) + .await } ///Sends a `Teleport` action to the server. pub async fn teleport( @@ -23,14 +21,12 @@ impl Server { position: impl Into>, rotation: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::Teleport(types::TeleportAction { - player_uuid, - position: position.into(), - rotation: rotation.into(), - }), - ) - .await + self.send_action(types::action::Kind::Teleport(types::TeleportAction { + player_uuid, + position: position.into(), + rotation: rotation.into(), + })) + .await } ///Sends a `Kick` action to the server. pub async fn kick( @@ -38,13 +34,11 @@ impl Server { player_uuid: String, reason: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::Kick(types::KickAction { - player_uuid, - reason, - }), - ) - .await + self.send_action(types::action::Kind::Kick(types::KickAction { + player_uuid, + reason, + })) + .await } ///Sends a `SetGameMode` action to the server. pub async fn set_game_mode( @@ -52,13 +46,11 @@ impl Server { player_uuid: String, game_mode: types::GameMode, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetGameMode(types::SetGameModeAction { - player_uuid, - game_mode: game_mode.into(), - }), - ) - .await + self.send_action(types::action::Kind::SetGameMode(types::SetGameModeAction { + player_uuid, + game_mode: game_mode.into(), + })) + .await } ///Sends a `GiveItem` action to the server. pub async fn give_item( @@ -66,25 +58,21 @@ impl Server { player_uuid: String, item: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::GiveItem(types::GiveItemAction { - player_uuid, - item: item.into(), - }), - ) - .await + self.send_action(types::action::Kind::GiveItem(types::GiveItemAction { + player_uuid, + item: item.into(), + })) + .await } ///Sends a `ClearInventory` action to the server. pub async fn clear_inventory( &self, player_uuid: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::ClearInventory(types::ClearInventoryAction { - player_uuid, - }), - ) - .await + self.send_action(types::action::Kind::ClearInventory( + types::ClearInventoryAction { player_uuid }, + )) + .await } ///Sends a `SetHeldItem` action to the server. pub async fn set_held_item( @@ -93,14 +81,76 @@ impl Server { main: impl Into>, offhand: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetHeldItem(types::SetHeldItemAction { - player_uuid, - main: main.into(), - offhand: offhand.into(), - }), - ) - .await + self.send_action(types::action::Kind::SetHeldItem(types::SetHeldItemAction { + player_uuid, + main: main.into(), + offhand: offhand.into(), + })) + .await + } + ///Sends a `PlayerSetArmour` action to the server. + pub async fn player_set_armour( + &self, + player_uuid: String, + helmet: impl Into>, + chestplate: impl Into>, + leggings: impl Into>, + boots: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSetArmour( + types::PlayerSetArmourAction { + player_uuid, + helmet: helmet.into(), + chestplate: chestplate.into(), + leggings: leggings.into(), + boots: boots.into(), + }, + )) + .await + } + ///Sends a `PlayerOpenBlockContainer` action to the server. + pub async fn player_open_block_container( + &self, + player_uuid: String, + position: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerOpenBlockContainer( + types::PlayerOpenBlockContainerAction { + player_uuid, + position: position.into(), + }, + )) + .await + } + ///Sends a `PlayerDropItem` action to the server. + pub async fn player_drop_item( + &self, + player_uuid: String, + item: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerDropItem( + types::PlayerDropItemAction { + player_uuid, + item: item.into(), + }, + )) + .await + } + ///Sends a `PlayerSetItemCooldown` action to the server. + pub async fn player_set_item_cooldown( + &self, + player_uuid: String, + item: impl Into>, + duration_ms: i64, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSetItemCooldown( + types::PlayerSetItemCooldownAction { + player_uuid, + item: item.into(), + duration_ms, + }, + )) + .await } ///Sends a `SetHealth` action to the server. pub async fn set_health( @@ -109,14 +159,12 @@ impl Server { health: f64, max_health: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetHealth(types::SetHealthAction { - player_uuid, - health, - max_health: max_health.into(), - }), - ) - .await + self.send_action(types::action::Kind::SetHealth(types::SetHealthAction { + player_uuid, + health, + max_health: max_health.into(), + })) + .await } ///Sends a `SetFood` action to the server. pub async fn set_food( @@ -124,13 +172,11 @@ impl Server { player_uuid: String, food: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetFood(types::SetFoodAction { - player_uuid, - food, - }), - ) - .await + self.send_action(types::action::Kind::SetFood(types::SetFoodAction { + player_uuid, + food, + })) + .await } ///Sends a `SetExperience` action to the server. pub async fn set_experience( @@ -140,15 +186,15 @@ impl Server { progress: impl Into>, amount: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetExperience(types::SetExperienceAction { - player_uuid, - level: level.into(), - progress: progress.into(), - amount: amount.into(), - }), - ) - .await + self.send_action(types::action::Kind::SetExperience( + types::SetExperienceAction { + player_uuid, + level: level.into(), + progress: progress.into(), + amount: amount.into(), + }, + )) + .await } ///Sends a `SetVelocity` action to the server. pub async fn set_velocity( @@ -156,13 +202,11 @@ impl Server { player_uuid: String, velocity: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SetVelocity(types::SetVelocityAction { - player_uuid, - velocity: velocity.into(), - }), - ) - .await + self.send_action(types::action::Kind::SetVelocity(types::SetVelocityAction { + player_uuid, + velocity: velocity.into(), + })) + .await } ///Sends a `AddEffect` action to the server. pub async fn add_effect( @@ -173,16 +217,14 @@ impl Server { duration_ms: i64, show_particles: bool, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::AddEffect(types::AddEffectAction { - player_uuid, - effect_type: effect_type.into(), - level, - duration_ms, - show_particles, - }), - ) - .await + self.send_action(types::action::Kind::AddEffect(types::AddEffectAction { + player_uuid, + effect_type: effect_type.into(), + level, + duration_ms, + show_particles, + })) + .await } ///Sends a `RemoveEffect` action to the server. pub async fn remove_effect( @@ -190,13 +232,13 @@ impl Server { player_uuid: String, effect_type: types::EffectType, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::RemoveEffect(types::RemoveEffectAction { - player_uuid, - effect_type: effect_type.into(), - }), - ) - .await + self.send_action(types::action::Kind::RemoveEffect( + types::RemoveEffectAction { + player_uuid, + effect_type: effect_type.into(), + }, + )) + .await } ///Sends a `SendTitle` action to the server. pub async fn send_title( @@ -208,17 +250,15 @@ impl Server { duration_ms: impl Into>, fade_out_ms: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SendTitle(types::SendTitleAction { - player_uuid, - title, - subtitle: subtitle.into(), - fade_in_ms: fade_in_ms.into(), - duration_ms: duration_ms.into(), - fade_out_ms: fade_out_ms.into(), - }), - ) - .await + self.send_action(types::action::Kind::SendTitle(types::SendTitleAction { + player_uuid, + title, + subtitle: subtitle.into(), + fade_in_ms: fade_in_ms.into(), + duration_ms: duration_ms.into(), + fade_out_ms: fade_out_ms.into(), + })) + .await } ///Sends a `SendPopup` action to the server. pub async fn send_popup( @@ -226,13 +266,11 @@ impl Server { player_uuid: String, message: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SendPopup(types::SendPopupAction { - player_uuid, - message, - }), - ) - .await + self.send_action(types::action::Kind::SendPopup(types::SendPopupAction { + player_uuid, + message, + })) + .await } ///Sends a `SendTip` action to the server. pub async fn send_tip( @@ -240,13 +278,109 @@ impl Server { player_uuid: String, message: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::SendTip(types::SendTipAction { - player_uuid, - message, - }), - ) - .await + self.send_action(types::action::Kind::SendTip(types::SendTipAction { + player_uuid, + message, + })) + .await + } + ///Sends a `PlayerSendToast` action to the server. + pub async fn player_send_toast( + &self, + player_uuid: String, + title: String, + message: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSendToast( + types::PlayerSendToastAction { + player_uuid, + title, + message, + }, + )) + .await + } + ///Sends a `PlayerSendJukeboxPopup` action to the server. + pub async fn player_send_jukebox_popup( + &self, + player_uuid: String, + message: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSendJukeboxPopup( + types::PlayerSendJukeboxPopupAction { + player_uuid, + message, + }, + )) + .await + } + ///Sends a `PlayerShowCoordinates` action to the server. + pub async fn player_show_coordinates( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerShowCoordinates( + types::PlayerShowCoordinatesAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerHideCoordinates` action to the server. + pub async fn player_hide_coordinates( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerHideCoordinates( + types::PlayerHideCoordinatesAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerEnableInstantRespawn` action to the server. + pub async fn player_enable_instant_respawn( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerEnableInstantRespawn( + types::PlayerEnableInstantRespawnAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerDisableInstantRespawn` action to the server. + pub async fn player_disable_instant_respawn( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerDisableInstantRespawn( + types::PlayerDisableInstantRespawnAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerSetNameTag` action to the server. + pub async fn player_set_name_tag( + &self, + player_uuid: String, + name_tag: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSetNameTag( + types::PlayerSetNameTagAction { + player_uuid, + name_tag, + }, + )) + .await + } + ///Sends a `PlayerSetScoreTag` action to the server. + pub async fn player_set_score_tag( + &self, + player_uuid: String, + score_tag: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSetScoreTag( + types::PlayerSetScoreTagAction { + player_uuid, + score_tag, + }, + )) + .await } ///Sends a `PlaySound` action to the server. pub async fn play_sound( @@ -257,16 +391,142 @@ impl Server { volume: impl Into>, pitch: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::PlaySound(types::PlaySoundAction { - player_uuid, - sound: sound.into(), - position: position.into(), - volume: volume.into(), - pitch: pitch.into(), - }), - ) - .await + self.send_action(types::action::Kind::PlaySound(types::PlaySoundAction { + player_uuid, + sound: sound.into(), + position: position.into(), + volume: volume.into(), + pitch: pitch.into(), + })) + .await + } + ///Sends a `PlayerShowParticle` action to the server. + pub async fn player_show_particle( + &self, + player_uuid: String, + position: impl Into>, + particle: types::ParticleType, + block: impl Into>, + face: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerShowParticle( + types::PlayerShowParticleAction { + player_uuid, + position: position.into(), + particle: particle.into(), + block: block.into(), + face: face.into(), + }, + )) + .await + } + ///Sends a `PlayerSendScoreboard` action to the server. + pub async fn player_send_scoreboard( + &self, + player_uuid: String, + title: String, + lines: Vec, + padding: impl Into>, + descending: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSendScoreboard( + types::PlayerSendScoreboardAction { + player_uuid, + title, + lines, + padding: padding.into(), + descending: descending.into(), + }, + )) + .await + } + ///Sends a `PlayerRemoveScoreboard` action to the server. + pub async fn player_remove_scoreboard( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerRemoveScoreboard( + types::PlayerRemoveScoreboardAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerSendMenuForm` action to the server. + pub async fn player_send_menu_form( + &self, + player_uuid: String, + title: String, + body: impl Into>, + buttons: Vec, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSendMenuForm( + types::PlayerSendMenuFormAction { + player_uuid, + title, + body: body.into(), + buttons, + }, + )) + .await + } + ///Sends a `PlayerSendModalForm` action to the server. + pub async fn player_send_modal_form( + &self, + player_uuid: String, + title: String, + body: String, + yes_text: String, + no_text: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSendModalForm( + types::PlayerSendModalFormAction { + player_uuid, + title, + body, + yes_text, + no_text, + }, + )) + .await + } + ///Sends a `PlayerSendDialogue` action to the server. + pub async fn player_send_dialogue( + &self, + player_uuid: String, + title: String, + body: impl Into>, + buttons: Vec, + entity: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSendDialogue( + types::PlayerSendDialogueAction { + player_uuid, + title, + body: body.into(), + buttons, + entity: entity.into(), + }, + )) + .await + } + ///Sends a `PlayerCloseDialogue` action to the server. + pub async fn player_close_dialogue( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerCloseDialogue( + types::PlayerCloseDialogueAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerCloseForm` action to the server. + pub async fn player_close_form( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerCloseForm( + types::PlayerCloseFormAction { player_uuid }, + )) + .await } ///Sends a `ExecuteCommand` action to the server. pub async fn execute_command( @@ -274,13 +534,478 @@ impl Server { player_uuid: String, command: String, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::ExecuteCommand(types::ExecuteCommandAction { - player_uuid, - command, - }), - ) - .await + self.send_action(types::action::Kind::ExecuteCommand( + types::ExecuteCommandAction { + player_uuid, + command, + }, + )) + .await + } + ///Sends a `PlayerStartSprinting` action to the server. + pub async fn player_start_sprinting( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerStartSprinting( + types::PlayerStartSprintingAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerStopSprinting` action to the server. + pub async fn player_stop_sprinting( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerStopSprinting( + types::PlayerStopSprintingAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerStartSneaking` action to the server. + pub async fn player_start_sneaking( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerStartSneaking( + types::PlayerStartSneakingAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerStopSneaking` action to the server. + pub async fn player_stop_sneaking( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerStopSneaking( + types::PlayerStopSneakingAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerStartSwimming` action to the server. + pub async fn player_start_swimming( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerStartSwimming( + types::PlayerStartSwimmingAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerStopSwimming` action to the server. + pub async fn player_stop_swimming( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerStopSwimming( + types::PlayerStopSwimmingAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerStartCrawling` action to the server. + pub async fn player_start_crawling( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerStartCrawling( + types::PlayerStartCrawlingAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerStopCrawling` action to the server. + pub async fn player_stop_crawling( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerStopCrawling( + types::PlayerStopCrawlingAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerStartGliding` action to the server. + pub async fn player_start_gliding( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerStartGliding( + types::PlayerStartGlidingAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerStopGliding` action to the server. + pub async fn player_stop_gliding( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerStopGliding( + types::PlayerStopGlidingAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerStartFlying` action to the server. + pub async fn player_start_flying( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerStartFlying( + types::PlayerStartFlyingAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerStopFlying` action to the server. + pub async fn player_stop_flying( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerStopFlying( + types::PlayerStopFlyingAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerSetImmobile` action to the server. + pub async fn player_set_immobile( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSetImmobile( + types::PlayerSetImmobileAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerSetMobile` action to the server. + pub async fn player_set_mobile( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSetMobile( + types::PlayerSetMobileAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerSetSpeed` action to the server. + pub async fn player_set_speed( + &self, + player_uuid: String, + speed: f64, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSetSpeed( + types::PlayerSetSpeedAction { player_uuid, speed }, + )) + .await + } + ///Sends a `PlayerSetFlightSpeed` action to the server. + pub async fn player_set_flight_speed( + &self, + player_uuid: String, + flight_speed: f64, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSetFlightSpeed( + types::PlayerSetFlightSpeedAction { + player_uuid, + flight_speed, + }, + )) + .await + } + ///Sends a `PlayerSetVerticalFlightSpeed` action to the server. + pub async fn player_set_vertical_flight_speed( + &self, + player_uuid: String, + vertical_flight_speed: f64, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSetVerticalFlightSpeed( + types::PlayerSetVerticalFlightSpeedAction { + player_uuid, + vertical_flight_speed, + }, + )) + .await + } + ///Sends a `PlayerSetAbsorption` action to the server. + pub async fn player_set_absorption( + &self, + player_uuid: String, + absorption: f64, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSetAbsorption( + types::PlayerSetAbsorptionAction { + player_uuid, + absorption, + }, + )) + .await + } + ///Sends a `PlayerSetOnFire` action to the server. + pub async fn player_set_on_fire( + &self, + player_uuid: String, + duration_ms: i64, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSetOnFire( + types::PlayerSetOnFireAction { + player_uuid, + duration_ms, + }, + )) + .await + } + ///Sends a `PlayerExtinguish` action to the server. + pub async fn player_extinguish( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerExtinguish( + types::PlayerExtinguishAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerSetInvisible` action to the server. + pub async fn player_set_invisible( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSetInvisible( + types::PlayerSetInvisibleAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerSetVisible` action to the server. + pub async fn player_set_visible( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSetVisible( + types::PlayerSetVisibleAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerSetScale` action to the server. + pub async fn player_set_scale( + &self, + player_uuid: String, + scale: f64, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSetScale( + types::PlayerSetScaleAction { player_uuid, scale }, + )) + .await + } + ///Sends a `PlayerSetHeldSlot` action to the server. + pub async fn player_set_held_slot( + &self, + player_uuid: String, + slot: i32, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSetHeldSlot( + types::PlayerSetHeldSlotAction { player_uuid, slot }, + )) + .await + } + ///Sends a `PlayerRespawn` action to the server. + pub async fn player_respawn( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerRespawn( + types::PlayerRespawnAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerTransfer` action to the server. + pub async fn player_transfer( + &self, + player_uuid: String, + address: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerTransfer( + types::PlayerTransferAction { + player_uuid, + address: address.into(), + }, + )) + .await + } + ///Sends a `PlayerKnockBack` action to the server. + pub async fn player_knock_back( + &self, + player_uuid: String, + source: impl Into>, + force: f64, + height: f64, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerKnockBack( + types::PlayerKnockBackAction { + player_uuid, + source: source.into(), + force, + height, + }, + )) + .await + } + ///Sends a `PlayerSwingArm` action to the server. + pub async fn player_swing_arm( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSwingArm( + types::PlayerSwingArmAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerPunchAir` action to the server. + pub async fn player_punch_air( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerPunchAir( + types::PlayerPunchAirAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerSendBossBar` action to the server. + pub async fn player_send_boss_bar( + &self, + player_uuid: String, + text: String, + health_percentage: impl Into>, + colour: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerSendBossBar( + types::PlayerSendBossBarAction { + player_uuid, + text, + health_percentage: health_percentage.into(), + colour: colour.into().map(|x| x.into()), + }, + )) + .await + } + ///Sends a `PlayerRemoveBossBar` action to the server. + pub async fn player_remove_boss_bar( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerRemoveBossBar( + types::PlayerRemoveBossBarAction { player_uuid }, + )) + .await + } + ///Sends a `PlayerShowHudElement` action to the server. + pub async fn player_show_hud_element( + &self, + player_uuid: String, + element: types::HudElement, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerShowHudElement( + types::PlayerShowHudElementAction { + player_uuid, + element: element.into(), + }, + )) + .await + } + ///Sends a `PlayerHideHudElement` action to the server. + pub async fn player_hide_hud_element( + &self, + player_uuid: String, + element: types::HudElement, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerHideHudElement( + types::PlayerHideHudElementAction { + player_uuid, + element: element.into(), + }, + )) + .await + } + ///Sends a `PlayerOpenSign` action to the server. + pub async fn player_open_sign( + &self, + player_uuid: String, + position: impl Into>, + front_side: bool, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerOpenSign( + types::PlayerOpenSignAction { + player_uuid, + position: position.into(), + front_side, + }, + )) + .await + } + ///Sends a `PlayerEditSign` action to the server. + pub async fn player_edit_sign( + &self, + player_uuid: String, + position: impl Into>, + front_text: String, + back_text: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerEditSign( + types::PlayerEditSignAction { + player_uuid, + position: position.into(), + front_text, + back_text, + }, + )) + .await + } + ///Sends a `PlayerTurnLecternPage` action to the server. + pub async fn player_turn_lectern_page( + &self, + player_uuid: String, + position: impl Into>, + page: i32, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerTurnLecternPage( + types::PlayerTurnLecternPageAction { + player_uuid, + position: position.into(), + page, + }, + )) + .await + } + ///Sends a `PlayerHidePlayer` action to the server. + pub async fn player_hide_player( + &self, + player_uuid: String, + target_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerHidePlayer( + types::PlayerHidePlayerAction { + player_uuid, + target_uuid, + }, + )) + .await + } + ///Sends a `PlayerShowPlayer` action to the server. + pub async fn player_show_player( + &self, + player_uuid: String, + target_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerShowPlayer( + types::PlayerShowPlayerAction { + player_uuid, + target_uuid, + }, + )) + .await + } + ///Sends a `PlayerRemoveAllDebugShapes` action to the server. + pub async fn player_remove_all_debug_shapes( + &self, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::PlayerRemoveAllDebugShapes( + types::PlayerRemoveAllDebugShapesAction { player_uuid }, + )) + .await } ///Sends a `WorldSetDefaultGameMode` action to the server. pub async fn world_set_default_game_mode( @@ -288,13 +1013,13 @@ impl Server { world: impl Into>, game_mode: types::GameMode, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldSetDefaultGameMode(types::WorldSetDefaultGameModeAction { - world: world.into(), - game_mode: game_mode.into(), - }), - ) - .await + self.send_action(types::action::Kind::WorldSetDefaultGameMode( + types::WorldSetDefaultGameModeAction { + world: world.into(), + game_mode: game_mode.into(), + }, + )) + .await } ///Sends a `WorldSetDifficulty` action to the server. pub async fn world_set_difficulty( @@ -302,13 +1027,13 @@ impl Server { world: impl Into>, difficulty: types::Difficulty, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldSetDifficulty(types::WorldSetDifficultyAction { - world: world.into(), - difficulty: difficulty.into(), - }), - ) - .await + self.send_action(types::action::Kind::WorldSetDifficulty( + types::WorldSetDifficultyAction { + world: world.into(), + difficulty: difficulty.into(), + }, + )) + .await } ///Sends a `WorldSetTickRange` action to the server. pub async fn world_set_tick_range( @@ -316,13 +1041,13 @@ impl Server { world: impl Into>, tick_range: i32, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldSetTickRange(types::WorldSetTickRangeAction { - world: world.into(), - tick_range, - }), - ) - .await + self.send_action(types::action::Kind::WorldSetTickRange( + types::WorldSetTickRangeAction { + world: world.into(), + tick_range, + }, + )) + .await } ///Sends a `WorldSetBlock` action to the server. pub async fn world_set_block( @@ -331,14 +1056,14 @@ impl Server { position: impl Into>, block: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldSetBlock(types::WorldSetBlockAction { - world: world.into(), - position: position.into(), - block: block.into(), - }), - ) - .await + self.send_action(types::action::Kind::WorldSetBlock( + types::WorldSetBlockAction { + world: world.into(), + position: position.into(), + block: block.into(), + }, + )) + .await } ///Sends a `WorldPlaySound` action to the server. pub async fn world_play_sound( @@ -347,14 +1072,14 @@ impl Server { sound: types::Sound, position: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldPlaySound(types::WorldPlaySoundAction { - world: world.into(), - sound: sound.into(), - position: position.into(), - }), - ) - .await + self.send_action(types::action::Kind::WorldPlaySound( + types::WorldPlaySoundAction { + world: world.into(), + sound: sound.into(), + position: position.into(), + }, + )) + .await } ///Sends a `WorldAddParticle` action to the server. pub async fn world_add_particle( @@ -365,40 +1090,158 @@ impl Server { block: impl Into>, face: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldAddParticle(types::WorldAddParticleAction { - world: world.into(), - position: position.into(), - particle: particle.into(), - block: block.into(), - face: face.into(), - }), - ) - .await + self.send_action(types::action::Kind::WorldAddParticle( + types::WorldAddParticleAction { + world: world.into(), + position: position.into(), + particle: particle.into(), + block: block.into(), + face: face.into(), + }, + )) + .await + } + ///Sends a `WorldSetTime` action to the server. + pub async fn world_set_time( + &self, + world: impl Into>, + time: i32, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldSetTime( + types::WorldSetTimeAction { + world: world.into(), + time, + }, + )) + .await + } + ///Sends a `WorldStopTime` action to the server. + pub async fn world_stop_time( + &self, + world: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldStopTime( + types::WorldStopTimeAction { + world: world.into(), + }, + )) + .await + } + ///Sends a `WorldStartTime` action to the server. + pub async fn world_start_time( + &self, + world: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldStartTime( + types::WorldStartTimeAction { + world: world.into(), + }, + )) + .await + } + ///Sends a `WorldSetSpawn` action to the server. + pub async fn world_set_spawn( + &self, + world: impl Into>, + spawn: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldSetSpawn( + types::WorldSetSpawnAction { + world: world.into(), + spawn: spawn.into(), + }, + )) + .await + } + ///Sends a `WorldSetBiome` action to the server. + pub async fn world_set_biome( + &self, + world: impl Into>, + position: impl Into>, + biome_id: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldSetBiome( + types::WorldSetBiomeAction { + world: world.into(), + position: position.into(), + biome_id, + }, + )) + .await + } + ///Sends a `WorldSetLiquid` action to the server. + pub async fn world_set_liquid( + &self, + world: impl Into>, + position: impl Into>, + liquid: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldSetLiquid( + types::WorldSetLiquidAction { + world: world.into(), + position: position.into(), + liquid: liquid.into(), + }, + )) + .await + } + ///Sends a `WorldScheduleBlockUpdate` action to the server. + pub async fn world_schedule_block_update( + &self, + world: impl Into>, + position: impl Into>, + block: impl Into>, + delay_ms: i64, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldScheduleBlockUpdate( + types::WorldScheduleBlockUpdateAction { + world: world.into(), + position: position.into(), + block: block.into(), + delay_ms, + }, + )) + .await + } + ///Sends a `WorldBuildStructure` action to the server. + pub async fn world_build_structure( + &self, + world: impl Into>, + origin: impl Into>, + structure: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldBuildStructure( + types::WorldBuildStructureAction { + world: world.into(), + origin: origin.into(), + structure: structure.into(), + }, + )) + .await } ///Sends a `WorldQueryEntities` action to the server. pub async fn world_query_entities( &self, world: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldQueryEntities(types::WorldQueryEntitiesAction { - world: world.into(), - }), - ) - .await + self.send_action(types::action::Kind::WorldQueryEntities( + types::WorldQueryEntitiesAction { + world: world.into(), + }, + )) + .await } ///Sends a `WorldQueryPlayers` action to the server. pub async fn world_query_players( &self, world: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldQueryPlayers(types::WorldQueryPlayersAction { - world: world.into(), - }), - ) - .await + self.send_action(types::action::Kind::WorldQueryPlayers( + types::WorldQueryPlayersAction { + world: world.into(), + }, + )) + .await } ///Sends a `WorldQueryEntitiesWithin` action to the server. pub async fn world_query_entities_within( @@ -406,12 +1249,180 @@ impl Server { world: impl Into>, r#box: impl Into>, ) -> Result<(), mpsc::error::SendError> { - self.send_action( - types::action::Kind::WorldQueryEntitiesWithin(types::WorldQueryEntitiesWithinAction { - world: world.into(), - r#box: r#box.into(), - }), - ) - .await + self.send_action(types::action::Kind::WorldQueryEntitiesWithin( + types::WorldQueryEntitiesWithinAction { + world: world.into(), + r#box: r#box.into(), + }, + )) + .await + } + ///Sends a `WorldQueryPlayerSpawn` action to the server. + pub async fn world_query_player_spawn( + &self, + world: impl Into>, + player_uuid: String, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldQueryPlayerSpawn( + types::WorldQueryPlayerSpawnAction { + world: world.into(), + player_uuid, + }, + )) + .await + } + ///Sends a `WorldQueryBlock` action to the server. + pub async fn world_query_block( + &self, + world: impl Into>, + position: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldQueryBlock( + types::WorldQueryBlockAction { + world: world.into(), + position: position.into(), + }, + )) + .await + } + ///Sends a `WorldQueryBiome` action to the server. + pub async fn world_query_biome( + &self, + world: impl Into>, + position: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldQueryBiome( + types::WorldQueryBiomeAction { + world: world.into(), + position: position.into(), + }, + )) + .await + } + ///Sends a `WorldQueryLight` action to the server. + pub async fn world_query_light( + &self, + world: impl Into>, + position: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldQueryLight( + types::WorldQueryLightAction { + world: world.into(), + position: position.into(), + }, + )) + .await + } + ///Sends a `WorldQuerySkyLight` action to the server. + pub async fn world_query_sky_light( + &self, + world: impl Into>, + position: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldQuerySkyLight( + types::WorldQuerySkyLightAction { + world: world.into(), + position: position.into(), + }, + )) + .await + } + ///Sends a `WorldQueryTemperature` action to the server. + pub async fn world_query_temperature( + &self, + world: impl Into>, + position: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldQueryTemperature( + types::WorldQueryTemperatureAction { + world: world.into(), + position: position.into(), + }, + )) + .await + } + ///Sends a `WorldQueryHighestBlock` action to the server. + pub async fn world_query_highest_block( + &self, + world: impl Into>, + x: i32, + z: i32, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldQueryHighestBlock( + types::WorldQueryHighestBlockAction { + world: world.into(), + x, + z, + }, + )) + .await + } + ///Sends a `WorldQueryRainingAt` action to the server. + pub async fn world_query_raining_at( + &self, + world: impl Into>, + position: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldQueryRainingAt( + types::WorldQueryRainingAtAction { + world: world.into(), + position: position.into(), + }, + )) + .await + } + ///Sends a `WorldQuerySnowingAt` action to the server. + pub async fn world_query_snowing_at( + &self, + world: impl Into>, + position: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldQuerySnowingAt( + types::WorldQuerySnowingAtAction { + world: world.into(), + position: position.into(), + }, + )) + .await + } + ///Sends a `WorldQueryThunderingAt` action to the server. + pub async fn world_query_thundering_at( + &self, + world: impl Into>, + position: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldQueryThunderingAt( + types::WorldQueryThunderingAtAction { + world: world.into(), + position: position.into(), + }, + )) + .await + } + ///Sends a `WorldQueryLiquid` action to the server. + pub async fn world_query_liquid( + &self, + world: impl Into>, + position: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldQueryLiquid( + types::WorldQueryLiquidAction { + world: world.into(), + position: position.into(), + }, + )) + .await + } + ///Sends a `WorldQueryDefaultGameMode` action to the server. + pub async fn world_query_default_game_mode( + &self, + world: impl Into>, + ) -> Result<(), mpsc::error::SendError> { + self.send_action(types::action::Kind::WorldQueryDefaultGameMode( + types::WorldQueryDefaultGameModeAction { + world: world.into(), + }, + )) + .await } } diff --git a/packages/rust/xtask/src/generate_actions.rs b/packages/rust/xtask/src/generate_actions.rs index 9b0565e..bd94edc 100644 --- a/packages/rust/xtask/src/generate_actions.rs +++ b/packages/rust/xtask/src/generate_actions.rs @@ -58,6 +58,9 @@ fn generate_server_helpers_tokens( ConversionLogic::Into => { quote! { #field_name: #field_name.into() } } + ConversionLogic::OptionInnerInto => { + quote! { #field_name: #field_name.into().map(|x| x.into()) } + } }; fn_args.push(quote! { #field_name: #arg_type }); diff --git a/packages/rust/xtask/src/generate_handlers.rs b/packages/rust/xtask/src/generate_handlers.rs index a738ba9..5498bfb 100644 --- a/packages/rust/xtask/src/generate_handlers.rs +++ b/packages/rust/xtask/src/generate_handlers.rs @@ -48,31 +48,56 @@ fn generate_handler_trait_tokens(ast: &File) -> Result { ) { } }); - dispatch_fn_match_arms.push(quote! { - types::event_envelope::Payload::#ident(e) => { - let mut context = EventContext::new( - &envelope.event_id, - e, - server.sender.clone(), - server.plugin_id.clone(), - ); - handler.#handler_fn_name(server, &mut context).await; - context.send_ack_if_needed().await; - }, - }) + let match_arm = if ident == "Command" { + quote! { + types::event_envelope::Payload::#ident(e) => { + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); + + // Try registered commands first + let handled = handler.dispatch_commands(server, &mut context).await; + + // Fall back to on_command for unregistered/dynamic commands + if !handled { + handler.#handler_fn_name(server, &mut context).await; + } + + context.send_ack_if_needed().await; + }, + } + } else { + quote! { + types::event_envelope::Payload::#ident(e) => { + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); + handler.#handler_fn_name(server, &mut context).await; + context.send_ack_if_needed().await; + }, + } + }; + + dispatch_fn_match_arms.push(match_arm); } Ok(quote! { //! This file is auto-generated by `xtask`. Do not edit manually. #![allow(async_fn_in_trait)] - use crate::{event::EventContext, types, Server, EventSubscriptions}; + use crate::{event::EventContext, types, Server, EventSubscriptions, command::CommandRegistry}; pub trait EventHandler: EventSubscriptions + Send + Sync { #(#event_handler_fns)* } #[doc(hidden)] - pub async fn dispatch_event(server: &Server, handler: &impl EventHandler, envelope: &types::EventEnvelope) { + pub async fn dispatch_event(server: &Server, handler: &(impl EventHandler + CommandRegistry), envelope: &types::EventEnvelope) { let Some(payload) = &envelope.payload else { return; }; diff --git a/packages/rust/xtask/src/utils.rs b/packages/rust/xtask/src/utils.rs index c1f1a74..31d939f 100644 --- a/packages/rust/xtask/src/utils.rs +++ b/packages/rust/xtask/src/utils.rs @@ -191,15 +191,24 @@ pub(crate) fn get_prost_enumeration(field: &Field) -> Option { } pub(crate) fn get_api_type(field: &Field) -> proc_macro2::TokenStream { - if let Some(enum_ident) = get_prost_enumeration(field) { - // We found #[prost(enumeration="GameMode")] - // The type is `prost_gen::GameMode` - // (We assume prost_gen is imported in the generated file) - return quote! { types::#enum_ident }; - } + let enum_ident = get_prost_enumeration(field); - // 2. Not an enum, so resolve the type normally - resolve_recursive_api_type(&field.ty) + match (enum_ident, classify_prost_type(&field.ty)) { + // Enum field (prost represents as i32) + (Some(ident), ProstTypeInfo::Primitive(_)) => { + quote! { types::#ident } + } + // Option field (prost represents as Option) + (Some(ident), ProstTypeInfo::Option(_)) => { + quote! { impl Into> } + } + // Enum with unexpected wrapper - just use the enum type + (Some(ident), _) => { + quote! { types::#ident } + } + // Not an enum, resolve normally + (None, _) => resolve_recursive_api_type(&field.ty), + } } /// Recursive helper for get_api_type. @@ -233,17 +242,21 @@ pub(crate) enum ConversionLogic { Direct, /// Needs `.into()`. Into, + /// Option: needs `.into().map(|x| x.into())` + OptionInnerInto, } pub(crate) fn get_action_conversion_logic(field: &Field) -> ConversionLogic { - if get_prost_enumeration(field).is_some() { - return ConversionLogic::Into; - } - - match classify_prost_type(&field.ty) { - ProstTypeInfo::Option(_) => ConversionLogic::Into, - - _ => ConversionLogic::Direct, + let is_enum = get_prost_enumeration(field).is_some(); + let is_option = matches!(classify_prost_type(&field.ty), ProstTypeInfo::Option(_)); + + match (is_enum, is_option) { + // Option needs: .into().map(|x| x.into()) + (true, true) => ConversionLogic::OptionInnerInto, + // Plain enum or plain Option needs: .into() + (true, false) | (false, true) => ConversionLogic::Into, + // Everything else: direct + (false, false) => ConversionLogic::Direct, } } From 76c140e2e225f8e58779fa4a8f01f44cbdd98b02 Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Thu, 27 Nov 2025 12:57:21 -0500 Subject: [PATCH 21/22] some more semi BC changes thankfully i didnt publish yet. Added subcommand attr, removed the extra proc macro, cleaner code overall imo. --- examples/plugins/rust/src/main.rs | 24 +- packages/rust/macro/src/command.rs | 243 +++++++++++++++++-- packages/rust/macro/src/lib.rs | 142 +---------- packages/rust/macro/src/plugin.rs | 2 +- packages/rust/src/command.rs | 1 - packages/rust/src/lib.rs | 2 +- packages/rust/xtask/src/generate_handlers.rs | 4 +- 7 files changed, 238 insertions(+), 180 deletions(-) diff --git a/examples/plugins/rust/src/main.rs b/examples/plugins/rust/src/main.rs index 8c5c10f..d3f4958 100644 --- a/examples/plugins/rust/src/main.rs +++ b/examples/plugins/rust/src/main.rs @@ -4,10 +4,7 @@ /// pay: pay yourself money /// bal: view your balance / money use dragonfly_plugin::{ - Plugin, PluginRunner, - command::{Command, Ctx, command_handlers}, - event::EventHandler, - event_handler, types, + Command, Plugin, PluginRunner, command::Ctx, event::EventHandler, event_handler, types, }; use sqlx::{SqlitePool, sqlite::SqlitePoolOptions}; @@ -71,16 +68,21 @@ impl RusticEconomy { } #[derive(Command)] -#[command(name = "eco", description = "Rustic Economy commands.")] +#[command( + name = "eco", + description = "Rustic Economy commands.", + aliases("economy", "rustic_eco") +)] pub enum Eco { + #[subcommand(aliases("donate"))] Pay { amount: f64 }, + #[subcommand(aliases("balance", "money"))] Bal, } -#[command_handlers] -impl Eco { - async fn pay(state: &RusticEconomy, ctx: Ctx<'_>, amount: f64) { - match state.add_money(&ctx.sender, amount).await { +impl EcoHandler for RusticEconomy { + async fn pay(&self, ctx: Ctx<'_>, amount: f64) { + match self.add_money(&ctx.sender, amount).await { Ok(new_balance) => ctx .reply(format!( "Added ${:.2}! New balance: ${:.2}", @@ -97,8 +99,8 @@ impl Eco { } } - async fn bal(state: &RusticEconomy, ctx: Ctx<'_>) { - match state.get_balance(&ctx.sender).await { + async fn bal(&self, ctx: Ctx<'_>) { + match self.get_balance(&ctx.sender).await { Ok(balance) => ctx .reply(format!("Your balance: ${:.2}", balance)) .await diff --git a/packages/rust/macro/src/command.rs b/packages/rust/macro/src/command.rs index b5027a9..0e647e1 100644 --- a/packages/rust/macro/src/command.rs +++ b/packages/rust/macro/src/command.rs @@ -1,6 +1,6 @@ use heck::ToSnakeCase; use proc_macro2::TokenStream; -use quote::quote; +use quote::{format_ident, quote}; use syn::{ parse::{Parse, ParseStream}, punctuated::Punctuated, @@ -76,6 +76,60 @@ impl Parse for CommandInfoParser { } } +#[derive(Default)] +struct SubcommandAttr { + pub name: Option, + pub aliases: Vec, +} + +fn parse_subcommand_attr(attrs: &[Attribute]) -> syn::Result { + let mut out = SubcommandAttr::default(); + + for attr in attrs { + if !attr.path().is_ident("subcommand") { + continue; + } + + let metas = attr.parse_args_with(Punctuated::::parse_terminated)?; + + for meta in metas { + match meta { + // name = "pay" + Meta::NameValue(nv) if nv.path.is_ident("name") => { + if let Expr::Lit(ExprLit { + lit: Lit::Str(s), .. + }) = nv.value + { + out.name = Some(s); + } else { + return Err(syn::Error::new_spanned( + nv.value, + "subcommand `name` must be a string literal", + )); + } + } + + // aliases("give", "send") + Meta::List(list) if list.path.is_ident("aliases") => { + out.aliases = list + .parse_args_with(Punctuated::::parse_terminated)? + .into_iter() + .collect(); + } + + _ => { + return Err(syn::Error::new_spanned( + meta, + "unknown subcommand attribute; expected `name = \"...\"` or `aliases(..)`", + )); + } + } + } + } + + Ok(out) +} + pub fn generate_command_impls(ast: &DeriveInput, attr: &Attribute) -> TokenStream { let command_info = match attr.parse_args::() { Ok(info) => info, @@ -93,11 +147,15 @@ pub fn generate_command_impls(ast: &DeriveInput, attr: &Attribute) -> TokenStrea }; let spec_impl = generate_spec_impl(cmd_ident, cmd_name_lit, cmd_desc_lit, aliases_lits, &shape); - let try_from_impl = generate_try_from_impl(cmd_ident, cmd_name_lit, &shape); + let try_from_impl = generate_try_from_impl(cmd_ident, cmd_name_lit, aliases_lits, &shape); + let trait_impl = generate_handler_trait(cmd_ident, &shape); + let exec_impl = generate_execute_impl(cmd_ident, &shape); quote! { #spec_impl #try_from_impl + #trait_impl + #exec_impl } } @@ -115,15 +173,19 @@ fn generate_spec_impl( } CommandShape::Enum { variants } => { // First param: subcommand enum - let variant_names: Vec = - variants.iter().map(|v| v.name_snake.clone()).collect(); + let variant_names_iter = variants.iter().map(|v| &v.canonical); + let subcommand_names: Vec<&LitStr> = variants + .iter() + .flat_map(|v| &v.aliases) + .chain(variant_names_iter) + .collect(); let subcommand_spec = quote! { dragonfly_plugin::types::ParamSpec { name: "subcommand".to_string(), r#type: dragonfly_plugin::types::ParamType::ParamEnum as i32, optional: false, suffix: String::new(), - enum_values: vec![ #( #variant_names.to_string() ),* ], + enum_values: vec![ #( #subcommand_names.to_string() ),* ], } }; @@ -197,6 +259,7 @@ fn merge_variant_params(variants: &[VariantMeta]) -> Vec { fn generate_try_from_impl( cmd_ident: &Ident, cmd_name_lit: &LitStr, + cmd_aliases: &[LitStr], shape: &CommandShape, ) -> TokenStream { let body = match shape { @@ -211,19 +274,25 @@ fn generate_try_from_impl( CommandShape::Enum { variants } => { let match_arms = variants.iter().map(|v| { let variant_ident = &v.ident; - let name_snake = &v.name_snake; + let params = &v.params; - if v.params.is_empty() { - // Unit-like variant + // Build patterns: "canonical" | "alias1" | "alias2" + let mut name_lits = Vec::new(); + name_lits.push(&v.canonical); + for alias in &v.aliases { + name_lits.push(alias); + } + + let subcommand_patterns = quote! { #( #name_lits )|* }; + + if params.is_empty() { quote! { - #name_snake => Ok(Self::#variant_ident), + #subcommand_patterns => Ok(Self::#variant_ident), } } else { - // Struct-like variant with fields - // Note: arg index starts at 1 (index 0 is the subcommand itself) - let field_inits = v.params.iter().map(enum_field_init); + let field_inits = params.iter().map(enum_field_init); quote! { - #name_snake => Ok(Self::#variant_ident { + #subcommand_patterns => Ok(Self::#variant_ident { #( #field_inits, )* }), } @@ -243,12 +312,19 @@ fn generate_try_from_impl( } }; + let mut conditions = Vec::with_capacity(1 + cmd_aliases.len()); + conditions.push(quote! { event.command != #cmd_name_lit }); + + for alias in cmd_aliases { + conditions.push(quote! { && event.command != #alias }); + } + quote! { impl ::core::convert::TryFrom<&dragonfly_plugin::types::CommandEvent> for #cmd_ident { type Error = dragonfly_plugin::command::CommandParseError; fn try_from(event: &dragonfly_plugin::types::CommandEvent) -> Result { - if event.command != #cmd_name_lit { + if #(#conditions)* { return Err(dragonfly_plugin::command::CommandParseError::NoMatch); } @@ -313,8 +389,10 @@ struct ParamMeta { struct VariantMeta { /// Variant identifier, e.g. `Pay` ident: Ident, - /// Snake-case name for matching, e.g. `"pay"` - name_snake: String, + /// name for matching, e.g. `"pay"` + canonical: LitStr, + /// Aliases e.g give, donate. + aliases: Vec, /// Parameters (fields) for this variant params: Vec, } @@ -331,31 +409,38 @@ fn collect_command_shape(ast: &DeriveInput) -> syn::Result { Ok(CommandShape::Struct { params }) } Data::Enum(data) => { - let mut variants = Vec::new(); + let mut variants_meta = Vec::new(); for variant in &data.variants { let ident = variant.ident.clone(); - let name_snake = ident.to_string().to_snake_case(); + let default_name = + LitStr::new(ident.to_string().to_snake_case().as_str(), ident.span()); + + let sub_attr = parse_subcommand_attr(&variant.attrs)?; + let canonical = sub_attr.name.unwrap_or(default_name); + + let aliases = sub_attr.aliases.into_iter().collect::>(); - // Collect params for this variant's fields - // Note: index is relative to args AFTER the subcommand arg let params = collect_params_from_fields(&variant.fields)?; - variants.push(VariantMeta { + variants_meta.push(VariantMeta { ident, - name_snake, + canonical, + aliases, params, }); } - if variants.is_empty() { + if variants_meta.is_empty() { return Err(syn::Error::new_spanned( &ast.ident, "enum commands must have at least one variant", )); } - Ok(CommandShape::Enum { variants }) + Ok(CommandShape::Enum { + variants: variants_meta, + }) } Data::Union(_) => Err(syn::Error::new_spanned( ast, @@ -482,3 +567,113 @@ fn option_inner(ty: &Type) -> Option<&Type> { } None } + +fn generate_handler_trait(cmd_ident: &Ident, shape: &CommandShape) -> TokenStream { + let trait_ident = format_ident!("{}Handler", cmd_ident); + + match shape { + CommandShape::Struct { params } => { + // method name = struct name in snake_case, e.g. Ping -> ping + let method_ident = format_ident!("{}", cmd_ident.to_string().to_snake_case()); + + // args from struct fields if you want + let args = params.iter().map(|p| { + let ident = &p.field_ident; + let ty = &p.field_ty; + quote! { #ident: #ty } + }); + + quote! { + #[allow(async_fn_in_trait)] + pub trait #trait_ident: Send + Sync { + async fn #method_ident( + &self, + ctx: dragonfly_plugin::command::Ctx<'_>, + #( #args ),* + ); + } + } + } + CommandShape::Enum { variants } => { + let methods = variants.iter().map(|v| { + let method_ident = format_ident!("{}", v.ident.to_string().to_snake_case()); + let args = v.params.iter().map(|p| { + let ident = &p.field_ident; + let ty = &p.field_ty; + quote! { #ident: #ty } + }); + quote! { + async fn #method_ident( + &self, + ctx: dragonfly_plugin::command::Ctx<'_>, + #( #args ),* + ); + } + }); + + quote! { + #[allow(async_fn_in_trait)] + pub trait #trait_ident: Send + Sync { + #( #methods )* + } + } + } + } +} + +fn generate_execute_impl(cmd_ident: &Ident, shape: &CommandShape) -> TokenStream { + let trait_ident = format_ident!("{}Handler", cmd_ident); + + match shape { + CommandShape::Struct { params } => { + let method_ident = format_ident!("{}", cmd_ident.to_string().to_snake_case()); + let field_idents: Vec<_> = params.iter().map(|p| &p.field_ident).collect(); + + quote! { + impl #cmd_ident { + pub async fn __execute( + self, + handler: &H, + ctx: dragonfly_plugin::command::Ctx<'_>, + ) { + let Self { #( #field_idents ),* } = self; + handler.#method_ident(ctx, #( #field_idents ),*).await; + } + } + } + } + CommandShape::Enum { variants } => { + let match_arms = variants.iter().map(|v| { + let variant_ident = &v.ident; + let method_ident = format_ident!("{}", v.ident.to_string().to_snake_case()); + let field_idents: Vec<_> = v.params.iter().map(|p| &p.field_ident).collect(); + + if field_idents.is_empty() { + quote! { + Self::#variant_ident => handler.#method_ident(ctx).await, + } + } else { + quote! { + Self::#variant_ident { #( #field_idents ),* } => { + handler.#method_ident(ctx, #( #field_idents ),*).await + } + } + } + }); + + quote! { + impl #cmd_ident { + pub async fn __execute( + self, + handler: &H, + ctx: dragonfly_plugin::command::Ctx<'_>, + ) { + match self { + #( #match_arms )* + } + } + } + } + } + } +} diff --git a/packages/rust/macro/src/lib.rs b/packages/rust/macro/src/lib.rs index 2a573b3..1dcaa6e 100644 --- a/packages/rust/macro/src/lib.rs +++ b/packages/rust/macro/src/lib.rs @@ -4,9 +4,7 @@ mod plugin; use heck::ToPascalCase; use proc_macro::TokenStream; use quote::{format_ident, quote}; -use syn::{ - parse_macro_input, Attribute, DeriveInput, FnArg, Ident, ImplItem, ItemImpl, Pat, PatType, Type, -}; +use syn::{parse_macro_input, Attribute, DeriveInput, ImplItem, ItemImpl}; use crate::{command::generate_command_impls, plugin::generate_plugin_impl}; @@ -111,7 +109,7 @@ pub fn event_handler(_attr: TokenStream, item: TokenStream) -> TokenStream { final_output.into() } -#[proc_macro_derive(Command, attributes(command))] +#[proc_macro_derive(Command, attributes(command, subcommand))] pub fn command_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); @@ -126,139 +124,3 @@ pub fn command_derive(input: TokenStream) -> TokenStream { generate_command_impls(&ast, command_attr).into() } - -#[proc_macro_attribute] -pub fn command_handlers(_attr: TokenStream, item: TokenStream) -> TokenStream { - let item_clone = item.clone(); - - let impl_block = match syn::parse::(item) { - Ok(block) => block, - Err(_) => return item_clone, // Keep LSP happy during typing - }; - - // Must be an inherent impl (not a trait impl) - if impl_block.trait_.is_some() { - return syn::Error::new_spanned( - &impl_block, - "#[command_handlers] must be on an inherent impl, not a trait impl", - ) - .to_compile_error() - .into(); - } - - let cmd_ty = &impl_block.self_ty; - - // Collect handler methods and extract state type - let mut state_ty: Option = None; - let mut handlers: Vec = Vec::new(); - - for item in &impl_block.items { - let ImplItem::Fn(method) = item else { continue }; - - // Skip non-async methods - if method.sig.asyncness.is_none() { - continue; - } - - let method_name = &method.sig.ident; - let variant_name = format_ident!("{}", method_name.to_string().to_pascal_case()); - - // Extract params: expect (state: &T, ctx: Ctx, ...fields) - let mut inputs = method.sig.inputs.iter(); - - // First param: state: &SomePlugin - let Some(FnArg::Typed(state_pat)) = inputs.next() else { - return syn::Error::new_spanned( - &method.sig, - "handler must have at least (state: &Plugin, ctx: Ctx)", - ) - .to_compile_error() - .into(); - }; - - // Extract and validate state type - let extracted_state_ty = (*state_pat.ty).clone(); - match &state_ty { - None => state_ty = Some(extracted_state_ty), - Some(prev) => { - // All handlers must use the same state type - if quote!(#prev).to_string() != quote!(#extracted_state_ty).to_string() { - return syn::Error::new_spanned( - &state_pat.ty, - "all handlers must use the same state type", - ) - .to_compile_error() - .into(); - } - } - } - - // Second param: ctx: Ctx (skip validation for now) - let _ = inputs.next(); - - // Remaining params are the variant fields - let field_idents: Vec = inputs - .filter_map(|arg| { - if let FnArg::Typed(PatType { pat, .. }) = arg { - if let Pat::Ident(pat_ident) = &**pat { - return Some(pat_ident.ident.clone()); - } - } - None - }) - .collect(); - - handlers.push(HandlerMeta { - method_name: method_name.clone(), - variant_name, - field_idents, - }); - } - - let Some(state_ty) = state_ty else { - return syn::Error::new_spanned(&impl_block, "no handler methods found") - .to_compile_error() - .into(); - }; - - // Generate match arms - let match_arms = handlers.iter().map(|h| { - let method_name = &h.method_name; - let variant_name = &h.variant_name; - let fields = &h.field_idents; - - if fields.is_empty() { - quote! { - Self::#variant_name => Self::#method_name(state, ctx).await, - } - } else { - quote! { - Self::#variant_name { #( #fields ),* } => Self::#method_name(state, ctx, #( #fields ),*).await, - } - } - }); - - let execute_impl = quote! { - impl #cmd_ty { - pub async fn __execute(self, state: #state_ty, ctx: dragonfly_plugin::command::Ctx<'_>) { - match self { - #( #match_arms )* - } - } - } - }; - - let original = quote! { #impl_block }; - - quote! { - #original - #execute_impl - } - .into() -} - -struct HandlerMeta { - method_name: Ident, - variant_name: Ident, - field_idents: Vec, -} diff --git a/packages/rust/macro/src/plugin.rs b/packages/rust/macro/src/plugin.rs index 109c75a..0e33382 100644 --- a/packages/rust/macro/src/plugin.rs +++ b/packages/rust/macro/src/plugin.rs @@ -126,7 +126,7 @@ pub(crate) fn generate_plugin_impl( impl dragonfly_plugin::command::CommandRegistry for #derive_name {} } } else { - // Has commands: generate get_commands() and __dispatch_commands() + // Has commands: generate get_commands() and dispatch_commands() let spec_calls = commands.iter().map(|cmd| { quote! { #cmd::spec() } }); diff --git a/packages/rust/src/command.rs b/packages/rust/src/command.rs index 01644cc..2cec22d 100644 --- a/packages/rust/src/command.rs +++ b/packages/rust/src/command.rs @@ -1,6 +1,5 @@ use crate::{server::Server, types}; -pub use dragonfly_plugin_macro::{command_handlers, Command}; use tokio::sync::mpsc; /// Per-command execution context. diff --git a/packages/rust/src/lib.rs b/packages/rust/src/lib.rs index 42f3a4d..dc8506f 100644 --- a/packages/rust/src/lib.rs +++ b/packages/rust/src/lib.rs @@ -22,7 +22,7 @@ use std::error::Error; pub use server::*; // main usage stuff for plugin devs: -pub use dragonfly_plugin_macro::{event_handler, Plugin}; +pub use dragonfly_plugin_macro::{event_handler, Command, Plugin}; pub use event::EventHandler; use tokio::sync::mpsc; use tokio_stream::{wrappers::ReceiverStream, StreamExt}; diff --git a/packages/rust/xtask/src/generate_handlers.rs b/packages/rust/xtask/src/generate_handlers.rs index 5498bfb..f0b3e9e 100644 --- a/packages/rust/xtask/src/generate_handlers.rs +++ b/packages/rust/xtask/src/generate_handlers.rs @@ -58,6 +58,8 @@ fn generate_handler_trait_tokens(ast: &File) -> Result { server.plugin_id.clone(), ); + context.send_ack_if_needed().await; + // Try registered commands first let handled = handler.dispatch_commands(server, &mut context).await; @@ -65,8 +67,6 @@ fn generate_handler_trait_tokens(ast: &File) -> Result { if !handled { handler.#handler_fn_name(server, &mut context).await; } - - context.send_ack_if_needed().await; }, } } else { From 7cf9bdeac3413bc00597cd4443361cdb30239a98 Mon Sep 17 00:00:00 2001 From: Jeremy Ianne Date: Thu, 27 Nov 2025 16:11:25 -0500 Subject: [PATCH 22/22] v0.3.0 ready for release and merge --- examples/plugins/rust/README.md | 88 +++++ examples/plugins/rust/src/main.rs | 68 ++-- packages/rust/MAINTAINING.md | 60 +++- packages/rust/README.md | 252 ++++++++----- packages/rust/example/src/main.rs | 11 +- packages/rust/macro/Cargo.toml | 3 + .../src/{command.rs => command/codegen.rs} | 336 +----------------- packages/rust/macro/src/command/mod.rs | 7 + packages/rust/macro/src/command/model.rs | 203 +++++++++++ packages/rust/macro/src/command/parse.rs | 129 +++++++ packages/rust/macro/src/lib.rs | 14 + packages/rust/macro/src/plugin.rs | 22 +- .../macro/tests/fixtures/command_basic.rs | 9 + .../rust/macro/tests/fixtures/plugin_basic.rs | 22 ++ packages/rust/macro/tests/macros_compile.rs | 8 + packages/rust/src/command.rs | 110 ++++++ packages/rust/src/event/mod.rs | 11 + packages/rust/src/server/server.rs | 6 + packages/rust/tests/command_derive.rs | 137 +++++++ packages/rust/tests/event_pipeline.rs | 221 ++++++++++++ packages/rust/tests/server_helpers.rs | 99 ++++++ packages/rust/xtask/src/generate_actions.rs | 28 ++ packages/rust/xtask/src/generate_handlers.rs | 14 + packages/rust/xtask/src/generate_mutations.rs | 36 ++ packages/rust/xtask/src/main.rs | 12 + ...nerate_handlers__tests__event_handler.snap | 24 +- ...ate_mutations__tests__event_mutations.snap | 53 ++- packages/rust/xtask/src/utils.rs | 15 + 28 files changed, 1523 insertions(+), 475 deletions(-) create mode 100644 examples/plugins/rust/README.md rename packages/rust/macro/src/{command.rs => command/codegen.rs} (53%) create mode 100644 packages/rust/macro/src/command/mod.rs create mode 100644 packages/rust/macro/src/command/model.rs create mode 100644 packages/rust/macro/src/command/parse.rs create mode 100644 packages/rust/macro/tests/fixtures/command_basic.rs create mode 100644 packages/rust/macro/tests/fixtures/plugin_basic.rs create mode 100644 packages/rust/macro/tests/macros_compile.rs create mode 100644 packages/rust/tests/command_derive.rs create mode 100644 packages/rust/tests/event_pipeline.rs create mode 100644 packages/rust/tests/server_helpers.rs diff --git a/examples/plugins/rust/README.md b/examples/plugins/rust/README.md new file mode 100644 index 0000000..bd287e0 --- /dev/null +++ b/examples/plugins/rust/README.md @@ -0,0 +1,88 @@ +## Rustic Economy – Rust example plugin + +`rustic-economy` is a **Rust example plugin** for Dragonfly that demonstrates: + +- A simple **SQLite-backed economy** using `sqlx`. +- The Rust SDK macros `#[derive(Plugin)]` and `#[derive(Command)]`. +- The generated **command system** (`Eco` enum + `EcoHandler` trait). +- Using `Ctx` to reply to the invoking player. + +It is meant as a learning/reference plugin, not a production-ready economy. + +### What this plugin does + +- Stores each player’s balance in a local `economy.db` SQLite database. +- Exposes one command, `/eco` (with aliases `/economy` and `/rustic_eco`): + - `/eco pay ` (`/eco donate `): add money to your own balance. + - `/eco bal` (aliases `/eco balance`, `/eco money`): show your current balance. + +Balances are stored as `REAL`/`f64` for simplicity. For real money, you should use +an integer representation (e.g. cents as `i64`) to avoid floating‑point issues. + +### Files and structure + +- `Cargo.toml`: Rust crate metadata for the example plugin. +- `src/main.rs`: The entire plugin implementation: + - `RusticEconomy` struct holding a `SqlitePool`. + - `impl RusticEconomy { new, get_balance, add_money }` – DB helpers. + - `Eco` command enum + `EcoHandler` impl with `pay` and `bal` handlers. + - `main` function that initialises the DB and runs `PluginRunner`. + +### Requirements + +- Rust (stable) and `cargo`. +- A Dragonfly host that has the Rust SDK wired in (this repo’s Go host). +- SQLite available on the host machine (the plugin writes `economy.db` + next to where it is run). + +### Building the plugin + +From the repo root: + +```bash +cd examples/plugins/rust +cargo build --release +``` + +The compiled binary will be in `target/release/rustic-economy` (or `.exe` on Windows). +Point your Dragonfly `plugins.yaml` at that binary. + +### Example `plugins.yaml` entry + +```yaml +plugins: + - id: rustic-economy + name: Rustic Economy + command: "./examples/plugins/rust/target/release/rustic-economy" + address: "tcp://127.0.0.1:50050" +``` + +Ensure the `id` matches the `#[plugin(id = "rustic-economy", ...)]` attribute in +`src/main.rs`. + +### Running and testing + +1. Start Dragonfly with the plugin enabled via `plugins.yaml`. +2. Join the server as a player. +3. Run economy commands in chat: + - `/eco pay 10` – adds 10 to your balance and shows the new total. + - `/eco bal` – prints your current balance. +4. Check that `economy.db` is created and populated in the working directory. + +If any DB or send‑chat errors occur, the plugin logs them to stderr and replies +with a generic error message so players aren’t exposed to internals. + +### How it uses the Rust SDK + +- `#[derive(Plugin)]` + `#[plugin(...)]` describe plugin metadata and register + the `Eco` command with the host. +- `#[derive(Command)]` generates a `EcoHandler` trait and argument parsing from + `types::CommandEvent` into the `Eco` enum. +- `Ctx<'_>` is used to send replies: `ctx.reply("...".to_string()).await`. +- `PluginRunner::run(plugin, "tcp://127.0.0.1:50050")` connects the plugin + process to the Dragonfly host and runs the event loop. + +Use this example as a starting point when building stateful Rust plugins that +compose the SDK’s command and event systems with your own storage layer. + + diff --git a/examples/plugins/rust/src/main.rs b/examples/plugins/rust/src/main.rs index d3f4958..b191066 100644 --- a/examples/plugins/rust/src/main.rs +++ b/examples/plugins/rust/src/main.rs @@ -1,8 +1,12 @@ -/// This is a semi advanced example of a simple economy plugin. -/// we are gonna use sqlite, to store user money. -/// two commands: -/// pay: pay yourself money -/// bal: view your balance / money +/// Rustic Economy: a small example plugin backed by SQLite. +/// +/// This example demonstrates how to: +/// - Use `#[derive(Plugin)]` to declare plugin metadata and register commands. +/// - Use `#[derive(Command)]` to define a typed command enum. +/// - Hold state (a `SqlitePool`) inside your plugin struct. +/// - Use `Ctx` to reply to the invoking player. +/// - Use `#[event_handler]` for event subscriptions (even when you don't +/// implement any event methods yet). use dragonfly_plugin::{ Command, Plugin, PluginRunner, command::Ctx, event::EventHandler, event_handler, types, }; @@ -12,7 +16,7 @@ use sqlx::{SqlitePool, sqlite::SqlitePoolOptions}; #[plugin( id = "rustic-economy", name = "Rustic Economy", - version = "0.1.0", + version = "0.3.0", api = "1.0.0", commands(Eco) )] @@ -20,7 +24,7 @@ struct RusticEconomy { db: SqlitePool, } -/// This impl is just a helper for dealing with our SQL stuff. +/// Database helpers for the Rustic Economy example. impl RusticEconomy { async fn new() -> Result> { // Create database connection @@ -29,7 +33,11 @@ impl RusticEconomy { .connect("sqlite:economy.db") .await?; - // Create table if it doesn't exist + // Create table if it doesn't exist. + // + // NOTE: This example stores balances as REAL/f64 for simplicity. + // For real-world money you should use an integer representation + // (e.g. cents as i64) to avoid floating point rounding issues. sqlx::query( "CREATE TABLE IF NOT EXISTS users ( uuid TEXT PRIMARY KEY, @@ -83,33 +91,47 @@ pub enum Eco { impl EcoHandler for RusticEconomy { async fn pay(&self, ctx: Ctx<'_>, amount: f64) { match self.add_money(&ctx.sender, amount).await { - Ok(new_balance) => ctx - .reply(format!( - "Added ${:.2}! New balance: ${:.2}", - amount, new_balance - )) - .await - .unwrap(), + Ok(new_balance) => { + if let Err(e) = ctx + .reply(format!( + "Added ${:.2}! New balance: ${:.2}", + amount, new_balance + )) + .await + { + eprintln!("Failed to send payment reply: {}", e); + } + } Err(e) => { eprintln!("Database error: {}", e); - ctx.reply("Error processing payment!".to_string()) + if let Err(send_err) = ctx + .reply("Error processing payment!".to_string()) .await - .unwrap() + { + eprintln!("Failed to send error reply: {}", send_err); + } } } } async fn bal(&self, ctx: Ctx<'_>) { match self.get_balance(&ctx.sender).await { - Ok(balance) => ctx - .reply(format!("Your balance: ${:.2}", balance)) - .await - .unwrap(), + Ok(balance) => { + if let Err(e) = ctx + .reply(format!("Your balance: ${:.2}", balance)) + .await + { + eprintln!("Failed to send balance reply: {}", e); + } + } Err(e) => { eprintln!("Database error: {}", e); - ctx.reply("Error checking balance!".to_string()) + if let Err(send_err) = ctx + .reply("Error checking balance!".to_string()) .await - .unwrap() + { + eprintln!("Failed to send error reply: {}", send_err); + } } } } diff --git a/packages/rust/MAINTAINING.md b/packages/rust/MAINTAINING.md index 76af5c9..fc8a37f 100644 --- a/packages/rust/MAINTAINING.md +++ b/packages/rust/MAINTAINING.md @@ -39,17 +39,20 @@ Our architecture is a 3-phase pipeline. Understanding this flow is critical to m ### Phase 3: The Friendly API (The `xtask` Generator) - **What it is:** This is the **public-facing, idiomatic Rust API** that our users love. It includes: - - The `PluginEventHandler` trait. + - The `EventHandler` trait and its `async fn on_*` methods. - The `Server` struct with its friendly helper methods (e.g., `server.send_chat(...)`). - - The `EventContext` struct and its helpers (`.cancel()`, `.set_message()`). + - The `EventContext` struct and its helpers (`.cancel()`, mutation helpers like `.set_message()`). - The `types::EventType` enum. -- **How it's made:** The `xtask` crate is a custom Rust program that **parses the output of Phase 2** and generates this beautiful API. +- **How it's made:** The `xtask` crate is a custom Rust program that **parses the output of Phase 2** (the prost Rust code in `src/generated/df.plugin.rs`) and generates this API via: + - `generate_actions.rs` → `server/helpers.rs` + - `generate_handlers.rs` → `event/handler.rs` + - `generate_mutations.rs` → `event/mutations.rs` - **How to update:** You run the `xtask` crate from the terminal. - **Warning:** **NEVER EDIT THESE FILES BY HAND.** --- -## How to Add a New Event +## How to add a new event This is the most common maintenance task. Here is the complete workflow. @@ -71,7 +74,7 @@ This is the most common maintenance task. Here is the complete workflow. - Add `async fn on_player_sleep(...)` to the `PluginEventHandler` trait. - Add the `case PlayerSleep` arm to the `dispatch_event` function. -4. **Review and Commit** +4. **Review and commit** - You are done. Review the changes in `git`. - You should see changes in: 1. The `.proto` file you edited. @@ -79,9 +82,46 @@ This is the most common maintenance task. Here is the complete workflow. 3. The Phase 3 friendly API files (e.g., `event_handler.rs`, `types.rs`). - Commit all of them. -## The `Handler` Proc-Macro +- **What xtask generates for events** + - Adds `async fn on_player_sleep(...)` (and similar) to the `EventHandler` trait in `event/handler.rs`. + - Adds the `PlayerSleep` arm to the `dispatch_event` function. + - Adds any required mutation helpers in `event/mutations.rs`. -- **Crate:** `dragonfly-plugin-macros` -- **Purpose:** Provides `#[derive(Handler)]` and its helper attribute `#[subscriptions(...)]`. -- **How it works:** This macro generates the `impl PluginSubscriptions` block for the user, turning their `#[subscriptions(Chat, PlayerSleep)]` list into a `Vec`. -- **Maintenance:** You do not need to touch this crate during the normal "add an event" workflow. The macro doesn't need to know _what_ events exist; it just converts the identifiers the user provides (e.g., `PlayerSleep`) into the corresponding enum variant (`types::EventType::PlayerSleep`). If the user makes a typo, the Rust compiler throws the error for us, which is the intended design. +## The `event_handler`, `Plugin`, and `Command` proc-macros + +- **Crate:** `dragonfly-plugin-macro` + +### `#[event_handler]` + +- **Purpose:** Attached to an `impl EventHandler for MyPlugin` block. +- **How it works:** Scans the methods you define (`on_player_join`, `on_chat`, etc.) and generates an `impl EventSubscriptions for MyPlugin` that returns a `Vec` with the corresponding variants. +- **Maintenance:** This macro is intentionally simple: it only cares about method names and does not need to change when new events are added. If a user types a non-existent event method, the trait mismatch causes a normal Rust compiler error. + +### `#[derive(Plugin)]` + `#[plugin(...)]` + +- **Purpose:** Implements the `Plugin` trait and wires the plugin metadata into the runtime. +- **Attributes:** `#[plugin(id = \"...\", name = \"...\", version = \"...\", api = \"...\", commands(Foo, Bar))]`. +- **How it works:** + - Generates `Plugin` trait methods based on the literals. + - Generates a `CommandRegistry` implementation that: + - Collects command specs from the listed command types. + - Dispatches `types::CommandEvent` into those command types. + +### `#[derive(Command)]` + `#[subcommand(...)]` + +- **Purpose:** Generates a strongly-typed command parser and handler trait for a struct/enum. +- **How it works:** + - Produces a `spec()` function returning `types::CommandSpec`. + - Implements `TryFrom<&types::CommandEvent>` using the SDK’s `parse_required_arg` / `parse_optional_arg` helpers. + - Generates an `XxxHandler` trait and an `__execute` method that calls into the appropriate handler method on the plugin. +- **Maintenance:** When adding new `ParamType` support (e.g., enums) or changing how arguments map from `String` → Rust types, update the command macro implementation in `macro/src/command/` (parse/model/codegen) to keep codegen consistent with the runtime helpers in `src/command.rs`. + +## Testing xtask and macros + +- **xtask tests:** The `xtask` crate has: + - Unit tests in `utils.rs` for AST helpers. + - Snapshot tests in the generator modules (`generate_actions.rs`, `generate_handlers.rs`, `generate_mutations.rs`) using `insta`, with snapshots under `xtask/src/snapshots/`. + - Edge-case tests that verify generators fail with clear error messages when prost structures are inconsistent (e.g., missing structs or payloads). +- **Macro tests:** The macro crate uses `trybuild` fixtures under `macro/tests/fixtures` to ensure: + - Basic usage of `#[derive(Plugin)]`, `#[event_handler]`, and `#[derive(Command)]` compiles against the public SDK. + - Any breaking change to macro signatures or expectations shows up as a compile error in these tests. diff --git a/packages/rust/README.md b/packages/rust/README.md index a391b60..28f3fe8 100644 --- a/packages/rust/README.md +++ b/packages/rust/README.md @@ -1,32 +1,49 @@ -# Dragonfly Rust Plugin API +## Dragonfly Rust Plugin SDK -Welcome to the Rust Plugin API for Dragonfly server software. This library provides the tools to build high-performance, safe, and asynchronous plugins using native Rust. +The `dragonfly-plugin` crate is the **Rust SDK for Dragonfly gRPC plugins**. It gives you: -The API is designed to be simple and explicit. You define your plugin's logic by implementing the `PluginEventHandler` trait and register your event subscriptions using the `#[derive(Handler)]` macro. +- **Derive macros** to describe your plugin (`#[derive(Plugin)]`) and commands (`#[derive(Command)]`). +- A simple **event system** based on an `EventHandler` trait and an `#[event_handler]` macro. +- A `Server` handle with high‑level helpers (like `send_chat`, `teleport`, `world_set_block`, …). +- A `PluginRunner` that connects your process to the Dragonfly host and runs the event loop. -## Features +### Crate and directory layout -- **Asynchronous by Default:** Built on `tokio` and native Rust `async/await`. -- **Type-Safe:** All events and actions are strongly typed, catching bugs at compile time. -- **Simple Subscriptions:** A clean `#[derive(Handler)]` macro handles all event subscription logic. +The Rust SDK lives under `packages/rust` as a small workspace: ---- +- **`dragonfly-plugin` (this crate)**: Public SDK surface used by plugin authors. + - `src/lib.rs`: Re-exports core modules and pulls in this README as crate-level docs. + - `src/command.rs`: Command context (`Ctx`), parsing helpers, and `CommandRegistry` trait. + - `src/event/`: Event system (`EventContext`, `EventHandler`, mutation helpers). + - `src/server/`: `Server` handle and generated helpers for sending actions to the host. + - `src/generated/df.plugin.rs`: Prost/tonic types generated from `proto/types/*.proto` (do not edit). +- **`macro/` (`dragonfly-plugin-macro`)**: Procedural macros for `#[derive(Plugin)]`, `#[derive(Command)]`, + and `#[event_handler]`. This crate is re-exported by `dragonfly-plugin` and is not used directly by plugins. +- **`xtask/`**: Internal code generation tooling that reads `df.plugin.rs` and regenerates + `event/handler.rs`, `event/mutations.rs`, and `server/helpers.rs`. It is not published. +- **`example/`**: A minimal example plugin crate showing recommended usage patterns for the SDK. +- **`tests/`**: Integration tests covering command derivation, event dispatch, server helpers, + and the interaction between the runtime and macros. + +All APIs in this README reflect the **0.3.x line**. Within 0.3.x we intend to keep: + +- The `Plugin`, `EventHandler`, `EventSubscriptions`, and `CommandRegistry` trait shapes. +- The `event_handler`, `Plugin`, and `Command` macros and their attribute syntax. +- The `Server` helpers and `event::EventContext` semantics (including `cancel` and mutation helpers). -## Quick Start Guide +Breaking changes may still happen in a future 0.4.0, but not within 0.3.x. -Here is the complete process for creating a "Hello, World\!" plugin. +--- -### 1\. Create a New Plugin +## Quick start -First, create a new binary crate for your plugin: +### 1. Create a new plugin crate ```sh cargo new my_plugin --bin ``` -### 2\. Update `Cargo.toml` - -Next, add `dragonfly-plugin` and `tokio` to your dependencies. +### 2. Add dependencies ```toml [package] @@ -35,53 +52,36 @@ version = "0.1.0" edition = "2021" [dependencies] -# This is the main API library -dragonfly-plugin = "0.1" # Or use a version number - -# Tokio is required for the async runtime -tokio = { version = "1", features = ["full"] } +dragonfly-plugin = "0.3" +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] } # optional, for DB-backed examples ``` -### 3\. Write Your Plugin (`src/main.rs`) +Only `dragonfly-plugin` and `tokio` are required; other crates (like `sqlx`) are up to your plugin. -This is the complete code for a simple plugin that greets players on join and adds a prefix to their chat messages. +### 3. Define your plugin ```rust,no_run -// --- Import all the necessary items --- use dragonfly_plugin::{ event::{EventContext, EventHandler}, - types, // Contains all event data structs - Plugin, // The derive macro event_handler, - PluginRunner, // The runner struct. + types, + Plugin, + PluginRunner, Server, }; -// make sure to derive Plugin, (Default isn't required but is used in this example code only.) -// when deriving Plugin you must include plugin attribute: #[derive(Plugin, Default)] #[plugin( - id = "example-rust", // A unique ID for your plugin (matches plugins.yaml) - name = "Example Rust Plugin", // A human-readable name - version = "1.0.0", // Your plugin's version - api = "1.0.0", // The API version you're built against + id = "example-rust", // must match plugins.yaml + name = "Example Rust Plugin", + version = "0.3.0", + api = "1.0.0" )] struct MyPlugin; -// --- 2. Implement the Event Handlers --- -// -// This is where all your plugin's logic lives. -// You only need to implement the `async fn` handlers -// for the events you subscribed to. -// note your LSP will probably fill them in as fn on_xx() -> Future<()> -// just delete the Future<()> + ... and put the keyword async before fn. -// -// #[event_handler] is a proc macro that detects which ever events you -// are overriding and thus setups a list of events to compile to -// as soon as your plugin is ran then it subscribes to them. #[event_handler] impl EventHandler for MyPlugin { - /// This handler runs when a player joins the server. async fn on_player_join( &self, server: &Server, @@ -90,85 +90,163 @@ impl EventHandler for MyPlugin { let player_name = &event.data.name; println!("Player '{}' has joined.", player_name); - let welcome_message = format!( + let welcome = format!( "Welcome, {}! This server is running a Rust plugin.", player_name ); - // Use the `server` handle to send actions - server.send_chat(event.data.player_uuid.clone(), welcome_message).await.ok(); + // Ignore send errors; they usually mean the host shut down. + let _ = server + .send_chat(event.data.player_uuid.clone(), welcome) + .await; } - /// This handler runs when a player sends a chat message. async fn on_chat( &self, - _server: &Server, // We don't need the server for this + _server: &Server, event: &mut EventContext<'_, types::ChatEvent>, ) { let new_message = format!("[Plugin] {}", event.data.message); - - // Use helper methods on the `event` to mutate it event.set_message(new_message); } } -// --- 3. Start the Plugin --- -// -// This is the entry point that connects to the server. #[tokio::main] -async fn main() { - println!("Starting my-plugin..."); - - // Here we default construct our Plugin. - // note you can use it almost like a Context variable as long - // as its Send / Sync. - // so you can not impl default and have it hold like a PgPool or etc. - PluginRunner::run(MyPlugin, "127.0.0.1:50051") - .await - .expect("Plugin failed to run"); +async fn main() -> Result<(), Box> { + println!("Starting example-rust plugin..."); + PluginRunner::run(MyPlugin, "tcp://127.0.0.1:50050").await } ``` ---- +The `#[event_handler]` macro: -## Writing Event Handlers +- Detects which `on_*` methods you implement. +- Generates an `EventSubscriptions` impl that subscribes to the corresponding `types::EventType` variants. +- Wires those events into `event::dispatch_event`. -All plugin logic is built by implementing functions from the `PluginEventHandler` trait. +--- -### `async fn` and Lifetimes +## Commands -Because this API uses native `async fn` in traits, you **must** include the anonymous lifetime (`'_`) annotation in the `EventContext` type: +The 0.3.x series introduces a **first‑class command system**. -- **Correct:** `event: &mut EventContext<'_, types::ChatEvent>` -- **Incorrect:** `event: &mut EventContext` +### Declaring a command -### Reading Event Data +```rust,no_run +use dragonfly_plugin::{command::Ctx, Command}; -You can read immutable data directly from the event: +#[derive(Command)] +#[command( + name = "eco", + description = "Economy commands.", + aliases("economy", "rustic_eco") +)] +pub enum Eco { + #[subcommand(aliases("donate"))] + Pay { amount: f64 }, -```rust,ignore -let player_name = &event.data.name; -println!("Player name: {}", player_name); + #[subcommand(aliases("balance", "money"))] + Bal, +} ``` -### Mutating Events +This generates: + +- A static `Eco::spec() -> types::CommandSpec`. +- A `TryFrom<&types::CommandEvent>` impl that parses args into `Eco`. +- An `EcoHandler` trait with async methods (`pay`, `bal`) and an `__execute` helper. -Some events are mutable. The `EventContext` provides helper methods (like `set_message`) to modify the event before the server processes it: +### Handling commands in your plugin + +Add the command type to your plugin’s `#[plugin]` attribute, and implement the generated handler trait for your plugin type: ```rust,ignore -event.set_message(format!("New message: {}", event.data.message)); -``` +use dragonfly_plugin::{command::Ctx, Command, Plugin}; -### Cancelling Events +#[derive(Plugin)] +#[plugin( + id = "rustic-economy", + name = "Rustic Economy", + version = "0.1.0", + api = "1.0.0", + commands(Eco) +)] +struct RusticEconomy { + // your state here, e.g. DB pools +} -You can also cancel compatible events to stop them from happening: +impl EcoHandler for RusticEconomy { + async fn pay(&self, ctx: Ctx<'_>, amount: f64) { + // ... + let _ = ctx + .reply(format!("You paid yourself ${:.2}.", amount)) + .await; + } -```rust,ignore -event.cancel(); + async fn bal(&self, ctx: Ctx<'_>) { + // ... + let _ = ctx.reply("Your balance is $0.00".to_string()).await; + } +} ``` -## Available Events +The `#[derive(Plugin)]` macro then: + +- Reports the command specs in the initial hello handshake. +- Generates a `CommandRegistry` impl that: + - Parses `CommandEvent`s into your command types. + - Cancels the event if a command matches. + - Dispatches into your `EcoHandler` implementation. + +Within 0.3.x the **shape of the command API** (`Ctx`, `CommandRegistry`, `CommandParseError`, and the `Command` derive attributes) is considered stable. + +--- + +## Events, context, and mutations + +- `event::EventContext<'_, T>` wraps each incoming event: + - `data: &T` gives read‑only access. + - `cancel().await` marks the event as cancelled and immediately sends a response. + - Event‑specific methods (like `set_message` for `ChatEvent`) live in generated extensions. +- `event::EventHandler` is a trait with an async method per event type; you usually never write `impl EventHandler` by hand except inside an `#[event_handler]` block. + +You generally do not construct `EventContext` yourself; the runtime does it for you. + +--- + +## Connection and runtime + +Use `PluginRunner::run(plugin, addr)` from your `main` function: + +- For TCP, pass e.g. `"tcp://127.0.0.1:50050"` or `"127.0.0.1:50050"`. +- On Unix hosts you may also pass: + - `"unix:///tmp/dragonfly_plugin.sock"` or + - an absolute path (`"/tmp/dragonfly_plugin.sock"`). + +On non‑Unix platforms, Unix socket addresses will return an error. + +`PluginRunner`: + +- Connects to the host. +- Sends an initial hello (`PluginHello`) with your plugin ID, name, version, API version and commands. +- Subscribes to your `EventSubscriptions`. +- Drives the main event loop until the host sends a shutdown message or closes the stream. + +--- + +## Stability policy for 0.3.x + +Within the 0.3.x series we aim to keep: + +- Trait surfaces for `Plugin`, `EventHandler`, `EventSubscriptions`, `CommandRegistry`. +- Macro names and high‑level attribute syntax (`#[plugin(...)]`, `#[event_handler]`, `#[derive(Command)]`, `#[subcommand(...)]`). +- `Server` helper method names and argument shapes. +- `EventContext` behavior for `cancel`, mutation helpers, and double‑send (panic in debug, log in release). + +We may still: -The `#[subscriptions(...)]` macro accepts any variant from the `types::EventType` enum. +- Add new events and actions. +- Add new helpers or mutation methods. +- Improve error messages and diagnostics. -You can find a complete list of all available event names (e.g., `PlayerJoin`, `Chat`, `BlockBreak`) and their corresponding data structs (e.g., `types::PlayerJoinEvent`) by looking in the `./src/types.rs` file or by consulting the API documentation. +For details on how the code is generated and how to maintain it, see `MAINTAINING.md`. diff --git a/packages/rust/example/src/main.rs b/packages/rust/example/src/main.rs index ad71c79..f54cf6a 100644 --- a/packages/rust/example/src/main.rs +++ b/packages/rust/example/src/main.rs @@ -48,12 +48,13 @@ impl EventHandler for MyExamplePlugin { ); // We call the auto-generated `server.send_chat` helper. - // `.await.ok()` sends the message and ignores any potential - // (but rare) connection errors. - server + // On failure we log the error, but otherwise keep behavior unchanged. + if let Err(e) = server .send_chat(event.data.player_uuid.clone(), welcome_message) .await - .ok(); + { + eprintln!("Failed to send welcome message: {}", e); + } } /// This handler runs every time a player sends a chat message. @@ -84,7 +85,7 @@ async fn main() -> Result<(), Box> { PluginRunner::run( MyExamplePlugin, // Pass in an instance of our plugin - "tcp://127.0.0.1:50050", // The server address (Unix socket) + "tcp://127.0.0.1:50050", // The server address (e.g., TCP or Unix socket) ) .await } diff --git a/packages/rust/macro/Cargo.toml b/packages/rust/macro/Cargo.toml index d90231d..d0b2f45 100644 --- a/packages/rust/macro/Cargo.toml +++ b/packages/rust/macro/Cargo.toml @@ -18,3 +18,6 @@ heck = "0.5.0" proc-macro2 = "1.0.103" quote = "1.0" syn = { version = "2.0", features = ["full", "parsing", "visit"] } + +[dev-dependencies] +trybuild = "1.0" diff --git a/packages/rust/macro/src/command.rs b/packages/rust/macro/src/command/codegen.rs similarity index 53% rename from packages/rust/macro/src/command.rs rename to packages/rust/macro/src/command/codegen.rs index 0e647e1..7155c48 100644 --- a/packages/rust/macro/src/command.rs +++ b/packages/rust/macro/src/command/codegen.rs @@ -1,134 +1,12 @@ use heck::ToSnakeCase; use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{ - parse::{Parse, ParseStream}, - punctuated::Punctuated, - spanned::Spanned, - Attribute, Data, DeriveInput, Expr, ExprLit, Fields, GenericArgument, Ident, Lit, LitStr, Meta, - PathArguments, Token, Type, TypePath, -}; - -struct CommandInfoParser { - pub name: LitStr, - pub description: LitStr, - pub aliases: Vec, -} - -impl Parse for CommandInfoParser { - fn parse(input: ParseStream) -> syn::Result { - let metas = Punctuated::::parse_terminated(input)?; - - let mut name = None; - let mut description = None; - let mut aliases = Vec::new(); - - for meta in metas { - match meta { - Meta::NameValue(nv) if nv.path.is_ident("name") => { - if let Expr::Lit(ExprLit { - lit: Lit::Str(s), .. - }) = nv.value - { - name = Some(s); - } else { - return Err(syn::Error::new( - nv.value.span(), - "expected string literal for `name`", - )); - } - } - Meta::NameValue(nv) if nv.path.is_ident("description") => { - if let Expr::Lit(ExprLit { - lit: Lit::Str(s), .. - }) = nv.value - { - description = Some(s); - } else { - return Err(syn::Error::new( - nv.value.span(), - "expected string literal for `description`", - )); - } - } - Meta::List(list) if list.path.is_ident("aliases") => { - aliases = list - .parse_args_with(Punctuated::::parse_terminated)? - .into_iter() - .collect(); - } - _ => { - return Err(syn::Error::new( - meta.span(), - "unrecognized command attribute", - )); - } - } - } - - Ok(Self { - name: name.ok_or_else(|| { - syn::Error::new(input.span(), "missing required attribute `name`") - })?, - description: description.unwrap_or_else(|| LitStr::new("", input.span())), - aliases, - }) - } -} - -#[derive(Default)] -struct SubcommandAttr { - pub name: Option, - pub aliases: Vec, -} - -fn parse_subcommand_attr(attrs: &[Attribute]) -> syn::Result { - let mut out = SubcommandAttr::default(); - - for attr in attrs { - if !attr.path().is_ident("subcommand") { - continue; - } - - let metas = attr.parse_args_with(Punctuated::::parse_terminated)?; - - for meta in metas { - match meta { - // name = "pay" - Meta::NameValue(nv) if nv.path.is_ident("name") => { - if let Expr::Lit(ExprLit { - lit: Lit::Str(s), .. - }) = nv.value - { - out.name = Some(s); - } else { - return Err(syn::Error::new_spanned( - nv.value, - "subcommand `name` must be a string literal", - )); - } - } - - // aliases("give", "send") - Meta::List(list) if list.path.is_ident("aliases") => { - out.aliases = list - .parse_args_with(Punctuated::::parse_terminated)? - .into_iter() - .collect(); - } +use syn::{Attribute, DeriveInput, Ident, LitStr}; - _ => { - return Err(syn::Error::new_spanned( - meta, - "unknown subcommand attribute; expected `name = \"...\"` or `aliases(..)`", - )); - } - } - } - } - - Ok(out) -} +use crate::command::{ + model::{collect_command_shape, CommandShape, ParamMeta, VariantMeta}, + parse::CommandInfoParser, +}; pub fn generate_command_impls(ast: &DeriveInput, attr: &Attribute) -> TokenStream { let command_info = match attr.parse_args::() { @@ -146,7 +24,8 @@ pub fn generate_command_impls(ast: &DeriveInput, attr: &Attribute) -> TokenStrea Err(e) => return e.to_compile_error(), }; - let spec_impl = generate_spec_impl(cmd_ident, cmd_name_lit, cmd_desc_lit, aliases_lits, &shape); + let spec_impl = + generate_spec_impl(cmd_ident, cmd_name_lit, cmd_desc_lit, aliases_lits, &shape); let try_from_impl = generate_try_from_impl(cmd_ident, cmd_name_lit, aliases_lits, &shape); let trait_impl = generate_handler_trait(cmd_ident, &shape); let exec_impl = generate_execute_impl(cmd_ident, &shape); @@ -370,204 +249,6 @@ fn enum_field_init(p: &ParamMeta) -> TokenStream { } } -/// Metadata for a single command parameter (struct field or enum variant field). -#[derive(Clone)] -struct ParamMeta { - /// Rust field identifier, e.g. `amount` - field_ident: Ident, - /// Full Rust type of the field, e.g. `f64` or `Option` - field_ty: Type, - /// Expression for ParamType (e.g. `ParamType::ParamFloat`) - param_type_expr: TokenStream, - /// Whether this is optional in the spec - is_optional: bool, - /// Position index in args (0-based, relative to this variant/struct) - index: usize, -} - -/// Metadata for a single enum variant (subcommand). -struct VariantMeta { - /// Variant identifier, e.g. `Pay` - ident: Ident, - /// name for matching, e.g. `"pay"` - canonical: LitStr, - /// Aliases e.g give, donate. - aliases: Vec, - /// Parameters (fields) for this variant - params: Vec, -} - -/// The shape of a command: either a struct or an enum with variants. -enum CommandShape { - Struct { params: Vec }, - Enum { variants: Vec }, -} -fn collect_command_shape(ast: &DeriveInput) -> syn::Result { - match &ast.data { - Data::Struct(data) => { - let params = collect_params_from_fields(&data.fields)?; - Ok(CommandShape::Struct { params }) - } - Data::Enum(data) => { - let mut variants_meta = Vec::new(); - - for variant in &data.variants { - let ident = variant.ident.clone(); - let default_name = - LitStr::new(ident.to_string().to_snake_case().as_str(), ident.span()); - - let sub_attr = parse_subcommand_attr(&variant.attrs)?; - let canonical = sub_attr.name.unwrap_or(default_name); - - let aliases = sub_attr.aliases.into_iter().collect::>(); - - let params = collect_params_from_fields(&variant.fields)?; - - variants_meta.push(VariantMeta { - ident, - canonical, - aliases, - params, - }); - } - - if variants_meta.is_empty() { - return Err(syn::Error::new_spanned( - &ast.ident, - "enum commands must have at least one variant", - )); - } - - Ok(CommandShape::Enum { - variants: variants_meta, - }) - } - Data::Union(_) => Err(syn::Error::new_spanned( - ast, - "unions are not supported for #[derive(Command)]", - )), - } -} - -fn collect_params_from_fields(fields: &Fields) -> syn::Result> { - let fields = match fields { - Fields::Named(named) => &named.named, - Fields::Unit => { - // No params - return Ok(Vec::new()); - } - Fields::Unnamed(_) => { - // TODO: maybe do support this but typenames are used as param names? - return Err(syn::Error::new_spanned( - fields, - "tuple structs are not supported for commands; use named fields", - )); - } - }; - - let mut out = Vec::new(); - for (index, field) in fields.iter().enumerate() { - let field_ident = field - .ident - .clone() - .expect("command struct fields must be named"); - let field_ty = field.ty.clone(); - - let (param_type_expr, is_optional) = get_param_type(&field_ty); - - out.push(ParamMeta { - field_ident, - field_ty, - param_type_expr, - is_optional, - index, - }); - } - - Ok(out) -} - -fn get_param_type(ty: &Type) -> (TokenStream, bool) { - if let Some(inner) = option_inner(ty) { - let (inner_param, _inner_opt) = get_param_type(inner); - return (inner_param, true); - } - - if let Type::Reference(r) = ty { - return get_param_type(&r.elem); - } - - if let Type::Path(TypePath { path, .. }) = ty { - if let Some(seg) = path.segments.last() { - let ident = seg.ident.to_string(); - - // Floats - if ident == "f32" || ident == "f64" { - return ( - quote! { dragonfly_plugin::types::ParamType::ParamFloat }, - false, - ); - } - - if matches!( - ident.as_str(), - "i8" | "i16" - | "i32" - | "i64" - | "i128" - | "u8" - | "u16" - | "u32" - | "u64" - | "u128" - | "isize" - | "usize" - ) { - return ( - quote! { dragonfly_plugin::types::ParamType::ParamInt }, - false, - ); - } - - if ident == "bool" { - return ( - quote! { dragonfly_plugin::types::ParamType::ParamBool }, - false, - ); - } - - if ident == "String" { - return ( - quote! { dragonfly_plugin::types::ParamType::ParamString }, - false, - ); - } - } - } - - ( - quote! { dragonfly_plugin::types::ParamType::ParamString }, - false, - ) -} - -fn option_inner(ty: &Type) -> Option<&Type> { - if let Type::Path(TypePath { path, .. }) = ty { - if let Some(seg) = path.segments.last() { - if seg.ident == "Option" { - if let PathArguments::AngleBracketed(args) = &seg.arguments { - for arg in &args.args { - if let GenericArgument::Type(inner) = arg { - return Some(inner); - } - } - } - } - } - } - None -} - fn generate_handler_trait(cmd_ident: &Ident, shape: &CommandShape) -> TokenStream { let trait_ident = format_ident!("{}Handler", cmd_ident); @@ -576,7 +257,6 @@ fn generate_handler_trait(cmd_ident: &Ident, shape: &CommandShape) -> TokenStrea // method name = struct name in snake_case, e.g. Ping -> ping let method_ident = format_ident!("{}", cmd_ident.to_string().to_snake_case()); - // args from struct fields if you want let args = params.iter().map(|p| { let ident = &p.field_ident; let ty = &p.field_ty; @@ -677,3 +357,5 @@ fn generate_execute_impl(cmd_ident: &Ident, shape: &CommandShape) -> TokenStream } } } + + diff --git a/packages/rust/macro/src/command/mod.rs b/packages/rust/macro/src/command/mod.rs new file mode 100644 index 0000000..f3a87da --- /dev/null +++ b/packages/rust/macro/src/command/mod.rs @@ -0,0 +1,7 @@ +mod parse; +mod model; +mod codegen; + +pub use codegen::generate_command_impls; + + diff --git a/packages/rust/macro/src/command/model.rs b/packages/rust/macro/src/command/model.rs new file mode 100644 index 0000000..2f804ae --- /dev/null +++ b/packages/rust/macro/src/command/model.rs @@ -0,0 +1,203 @@ +use heck::ToSnakeCase; +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + Data, DeriveInput, Fields, GenericArgument, Ident, LitStr, PathArguments, Type, TypePath, +}; + +use crate::command::parse::{parse_subcommand_attr, SubcommandAttr}; + +/// Metadata for a single command parameter (struct field or enum variant field). +#[derive(Clone)] +pub(crate) struct ParamMeta { + /// Rust field identifier, e.g. `amount` + pub field_ident: Ident, + /// Full Rust type of the field, e.g. `f64` or `Option` + pub field_ty: Type, + /// Expression for ParamType (e.g. `ParamType::ParamFloat`) + pub param_type_expr: TokenStream, + /// Whether this is optional in the spec + pub is_optional: bool, + /// Position index in args (0-based, relative to this variant/struct) + pub index: usize, +} + +/// Metadata for a single enum variant (subcommand). +pub(crate) struct VariantMeta { + /// Variant identifier, e.g. `Pay` + pub ident: Ident, + /// name for matching, e.g. `"pay"` + pub canonical: LitStr, + /// Aliases e.g give, donate. + pub aliases: Vec, + /// Parameters (fields) for this variant + pub params: Vec, +} + +/// The shape of a command: either a struct or an enum with variants. +pub(crate) enum CommandShape { + Struct { params: Vec }, + Enum { variants: Vec }, +} + +pub(crate) fn collect_command_shape(ast: &DeriveInput) -> syn::Result { + match &ast.data { + Data::Struct(data) => { + let params = collect_params_from_fields(&data.fields)?; + Ok(CommandShape::Struct { params }) + } + Data::Enum(data) => { + let mut variants_meta = Vec::new(); + + for variant in &data.variants { + let ident = variant.ident.clone(); + let default_name = + LitStr::new(ident.to_string().to_snake_case().as_str(), ident.span()); + + let SubcommandAttr { name, aliases } = parse_subcommand_attr(&variant.attrs)?; + let canonical = name.unwrap_or(default_name); + let params = collect_params_from_fields(&variant.fields)?; + + variants_meta.push(VariantMeta { + ident, + canonical, + aliases, + params, + }); + } + + if variants_meta.is_empty() { + return Err(syn::Error::new_spanned( + &ast.ident, + "enum commands must have at least one variant", + )); + } + + Ok(CommandShape::Enum { + variants: variants_meta, + }) + } + Data::Union(_) => Err(syn::Error::new_spanned( + ast, + "unions are not supported for #[derive(Command)]", + )), + } +} + +fn collect_params_from_fields(fields: &Fields) -> syn::Result> { + let fields = match fields { + Fields::Named(named) => &named.named, + Fields::Unit => { + // No params + return Ok(Vec::new()); + } + Fields::Unnamed(_) => { + return Err(syn::Error::new_spanned( + fields, + "tuple structs are not supported for commands; use named fields", + )); + } + }; + + let mut out = Vec::new(); + for (index, field) in fields.iter().enumerate() { + let field_ident = field + .ident + .clone() + .expect("command struct fields must be named"); + let field_ty = field.ty.clone(); + + let (param_type_expr, is_optional) = get_param_type(&field_ty); + + out.push(ParamMeta { + field_ident, + field_ty, + param_type_expr, + is_optional, + index, + }); + } + + Ok(out) +} + +fn get_param_type(ty: &Type) -> (TokenStream, bool) { + if let Some(inner) = option_inner(ty) { + let (inner_param, _inner_opt) = get_param_type(inner); + return (inner_param, true); + } + + if let Type::Reference(r) = ty { + return get_param_type(&r.elem); + } + + if let Type::Path(TypePath { path, .. }) = ty { + if let Some(seg) = path.segments.last() { + let ident = seg.ident.to_string(); + + // Floats + if ident == "f32" || ident == "f64" { + return ( + quote! { dragonfly_plugin::types::ParamType::ParamFloat }, + false, + ); + } + + if matches!( + ident.as_str(), + "i8" | "i16" + | "i32" + | "i64" + | "i128" + | "u8" + | "u16" + | "u32" + | "u64" + | "u128" + | "isize" + | "usize" + ) { + return ( + quote! { dragonfly_plugin::types::ParamType::ParamInt }, + false, + ); + } + + if ident == "bool" { + return ( + quote! { dragonfly_plugin::types::ParamType::ParamBool }, + false, + ); + } + + if ident == "String" { + return ( + quote! { dragonfly_plugin::types::ParamType::ParamString }, + false, + ); + } + } + } + + ( + quote! { dragonfly_plugin::types::ParamType::ParamString }, + false, + ) +} + +fn option_inner(ty: &Type) -> Option<&Type> { + if let Type::Path(TypePath { path, .. }) = ty { + if let Some(seg) = path.segments.last() { + if seg.ident == "Option" { + if let PathArguments::AngleBracketed(args) = &seg.arguments { + for arg in &args.args { + if let GenericArgument::Type(inner) = arg { + return Some(inner); + } + } + } + } + } + } + None +} diff --git a/packages/rust/macro/src/command/parse.rs b/packages/rust/macro/src/command/parse.rs new file mode 100644 index 0000000..cb29573 --- /dev/null +++ b/packages/rust/macro/src/command/parse.rs @@ -0,0 +1,129 @@ +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + spanned::Spanned, + Attribute, Expr, ExprLit, Lit, LitStr, Meta, Token, +}; + +pub(crate) struct CommandInfoParser { + pub name: LitStr, + pub description: LitStr, + pub aliases: Vec, +} + +impl Parse for CommandInfoParser { + fn parse(input: ParseStream) -> syn::Result { + let metas = Punctuated::::parse_terminated(input)?; + + let mut name = None; + let mut description = None; + let mut aliases = Vec::new(); + + for meta in metas { + match meta { + Meta::NameValue(nv) if nv.path.is_ident("name") => { + if let Expr::Lit(ExprLit { + lit: Lit::Str(s), .. + }) = nv.value + { + name = Some(s); + } else { + return Err(syn::Error::new( + nv.value.span(), + "expected string literal for `name`", + )); + } + } + Meta::NameValue(nv) if nv.path.is_ident("description") => { + if let Expr::Lit(ExprLit { + lit: Lit::Str(s), .. + }) = nv.value + { + description = Some(s); + } else { + return Err(syn::Error::new( + nv.value.span(), + "expected string literal for `description`", + )); + } + } + Meta::List(list) if list.path.is_ident("aliases") => { + aliases = list + .parse_args_with(Punctuated::::parse_terminated)? + .into_iter() + .collect(); + } + _ => { + return Err(syn::Error::new( + meta.span(), + "unrecognized command attribute", + )); + } + } + } + + Ok(Self { + name: name.ok_or_else(|| { + syn::Error::new(input.span(), "missing required attribute `name`") + })?, + description: description.unwrap_or_else(|| LitStr::new("", input.span())), + aliases, + }) + } +} + +#[derive(Default)] +pub(crate) struct SubcommandAttr { + pub name: Option, + pub aliases: Vec, +} + +pub(crate) fn parse_subcommand_attr(attrs: &[Attribute]) -> syn::Result { + let mut out = SubcommandAttr::default(); + + for attr in attrs { + if !attr.path().is_ident("subcommand") { + continue; + } + + let metas = attr.parse_args_with(Punctuated::::parse_terminated)?; + + for meta in metas { + match meta { + // name = "pay" + Meta::NameValue(nv) if nv.path.is_ident("name") => { + if let Expr::Lit(ExprLit { + lit: Lit::Str(s), .. + }) = nv.value + { + out.name = Some(s); + } else { + return Err(syn::Error::new_spanned( + nv.value, + "subcommand `name` must be a string literal", + )); + } + } + + // aliases("give", "send") + Meta::List(list) if list.path.is_ident("aliases") => { + out.aliases = list + .parse_args_with(Punctuated::::parse_terminated)? + .into_iter() + .collect(); + } + + _ => { + return Err(syn::Error::new_spanned( + meta, + "unknown subcommand attribute; expected `name = \"...\"` or `aliases(..)`", + )); + } + } + } + } + + Ok(out) +} + + diff --git a/packages/rust/macro/src/lib.rs b/packages/rust/macro/src/lib.rs index 1dcaa6e..304a90f 100644 --- a/packages/rust/macro/src/lib.rs +++ b/packages/rust/macro/src/lib.rs @@ -1,3 +1,17 @@ +//! Procedural macros for the `dragonfly-plugin` Rust SDK. +//! +//! This crate exposes three main macros: +//! - `#[derive(Plugin)]` with a `#[plugin(...)]` attribute to describe +//! the plugin metadata and registered commands. +//! - `#[event_handler]` to generate an `EventSubscriptions` implementation +//! based on the `on_*` methods you override in an `impl EventHandler`. +//! - `#[derive(Command)]` together with `#[command(...)]` / +//! `#[subcommand(...)]` to generate strongly-typed command parsers and +//! handler traits. +//! +//! These macros are re-exported by the `dragonfly-plugin` crate, so plugin +//! authors should depend on that crate directly. + mod command; mod plugin; diff --git a/packages/rust/macro/src/plugin.rs b/packages/rust/macro/src/plugin.rs index 0e33382..5b13dab 100644 --- a/packages/rust/macro/src/plugin.rs +++ b/packages/rust/macro/src/plugin.rs @@ -133,10 +133,24 @@ pub(crate) fn generate_plugin_impl( let dispatch_arms = commands.iter().map(|cmd| { quote! { - if let Ok(cmd) = #cmd::try_from(event.data) { - event.cancel().await; - cmd.__execute(self, ctx).await; - return true; + match #cmd::try_from(event.data) { + Ok(cmd) => { + event.cancel().await; + cmd.__execute(self, ctx).await; + return true; + } + Err(dragonfly_plugin::command::CommandParseError::NoMatch) => { + // Try the next registered command. + } + Err( + err @ dragonfly_plugin::command::CommandParseError::Missing(_) + | err @ dragonfly_plugin::command::CommandParseError::Invalid(_) + | err @ dragonfly_plugin::command::CommandParseError::UnknownSubcommand, + ) => { + // Surface parse errors to the player as a friendly message. + let _ = ctx.reply(err.to_string()).await; + return true; + } } } }); diff --git a/packages/rust/macro/tests/fixtures/command_basic.rs b/packages/rust/macro/tests/fixtures/command_basic.rs new file mode 100644 index 0000000..75d2beb --- /dev/null +++ b/packages/rust/macro/tests/fixtures/command_basic.rs @@ -0,0 +1,9 @@ +use dragonfly_plugin_macro::Command; + +#[derive(Command)] +#[command(name = "ping", description = "Ping command", aliases("p"))] +pub struct Ping { + times: i32, +} + + diff --git a/packages/rust/macro/tests/fixtures/plugin_basic.rs b/packages/rust/macro/tests/fixtures/plugin_basic.rs new file mode 100644 index 0000000..611b398 --- /dev/null +++ b/packages/rust/macro/tests/fixtures/plugin_basic.rs @@ -0,0 +1,22 @@ +use dragonfly_plugin_macro::{event_handler, Plugin}; + +struct MyPlugin; + +#[event_handler] +impl dragonfly_plugin::EventHandler for MyPlugin { + async fn on_chat( + &self, + _server: &dragonfly_plugin::Server, + _event: &mut dragonfly_plugin::event::EventContext< + '_, + dragonfly_plugin::types::ChatEvent, + >, + ) { + } +} + +#[derive(Plugin)] +#[plugin(id = "example-rust", name = "Example Rust Plugin", version = "0.3.0", api = "1.0.0")] +struct PluginInfoPlugin; + + diff --git a/packages/rust/macro/tests/macros_compile.rs b/packages/rust/macro/tests/macros_compile.rs new file mode 100644 index 0000000..5879271 --- /dev/null +++ b/packages/rust/macro/tests/macros_compile.rs @@ -0,0 +1,8 @@ +#[test] +fn macros_compile_on_basic_examples() { + let t = trybuild::TestCases::new(); + t.pass("tests/fixtures/plugin_basic.rs"); + t.pass("tests/fixtures/command_basic.rs"); +} + + diff --git a/packages/rust/src/command.rs b/packages/rust/src/command.rs index 2cec22d..be5df3f 100644 --- a/packages/rust/src/command.rs +++ b/packages/rust/src/command.rs @@ -1,8 +1,19 @@ +//! Command helpers and traits used by the `#[derive(Command)]` macro. +//! +//! Plugin authors usually interact with: +//! - `Ctx`, the per-command execution context (for replying to the sender). +//! - `CommandRegistry`, which is implemented for you by `#[derive(Plugin)]`. +//! - `CommandParseError`, surfaced as friendly messages to players. + use crate::{server::Server, types}; use tokio::sync::mpsc; /// Per-command execution context. +/// +/// This context is constructed by the runtime when a command matches, +/// and exposes the `Server` handle plus the UUID of the player that +/// issued the command. pub struct Ctx<'a> { pub server: &'a Server, pub sender: String, @@ -16,6 +27,9 @@ impl<'a> Ctx<'a> { } } + /// Sends a chat message back to the command sender. + /// + /// This is a convenience wrapper around `Server::send_chat`. pub async fn reply( &self, msg: impl Into, @@ -49,6 +63,27 @@ pub enum CommandParseError { UnknownSubcommand, } +impl std::fmt::Display for CommandParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CommandParseError::NoMatch => { + write!(f, "command did not match") + } + CommandParseError::Missing(name) => { + write!(f, "missing required argument `{name}`") + } + CommandParseError::Invalid(name) => { + write!(f, "invalid value for argument `{name}`") + } + CommandParseError::UnknownSubcommand => { + write!(f, "unknown subcommand") + } + } + } +} + +impl std::error::Error for CommandParseError {} + /// Parse a required argument at the given index. pub fn parse_required_arg( args: &[String], @@ -83,3 +118,78 @@ where .map_err(|_| CommandParseError::Invalid(name)), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_required_arg_ok() { + let args = vec!["42".to_string()]; + let value: i32 = parse_required_arg(&args, 0, "amount").unwrap(); + assert_eq!(value, 42); + } + + #[test] + fn parse_required_arg_missing() { + let args: Vec = Vec::new(); + let err = parse_required_arg::(&args, 0, "amount").unwrap_err(); + match err { + CommandParseError::Missing(name) => assert_eq!(name, "amount"), + e => panic!("expected Missing, got {e:?}"), + } + } + + #[test] + fn parse_required_arg_invalid() { + let args = vec!["not-a-number".to_string()]; + let err = parse_required_arg::(&args, 0, "amount").unwrap_err(); + match err { + CommandParseError::Invalid(name) => assert_eq!(name, "amount"), + e => panic!("expected Invalid, got {e:?}"), + } + } + + #[test] + fn parse_optional_arg_none_when_missing_or_empty() { + // Missing index + let args: Vec = Vec::new(); + let value: Option = parse_optional_arg(&args, 0, "amount").unwrap(); + assert!(value.is_none()); + + // Present but empty string + let args = vec!["".to_string()]; + let value: Option = parse_optional_arg(&args, 0, "amount").unwrap(); + assert!(value.is_none()); + } + + #[test] + fn parse_optional_arg_some_when_valid() { + let args = vec!["7".to_string()]; + let value: Option = parse_optional_arg(&args, 0, "amount").unwrap(); + assert_eq!(value, Some(7)); + } + + #[test] + fn parse_optional_arg_error_when_invalid() { + let args = vec!["nope".to_string()]; + let err = parse_optional_arg::(&args, 0, "amount").unwrap_err(); + match err { + CommandParseError::Invalid(name) => assert_eq!(name, "amount"), + e => panic!("expected Invalid, got {e:?}"), + } + } + + #[test] + fn display_messages_are_human_friendly() { + let err = CommandParseError::Missing("amount"); + assert!(err.to_string().contains("missing required argument")); + assert!(err.to_string().contains("amount")); + + let err = CommandParseError::Invalid("amount"); + assert!(err.to_string().contains("invalid value for argument")); + + let err = CommandParseError::UnknownSubcommand; + assert!(err.to_string().contains("unknown subcommand")); + } +} diff --git a/packages/rust/src/event/mod.rs b/packages/rust/src/event/mod.rs index da975ef..ee2cf18 100644 --- a/packages/rust/src/event/mod.rs +++ b/packages/rust/src/event/mod.rs @@ -1,3 +1,14 @@ +//! Core event types and helpers for the Rust plugin SDK. +//! +//! Most plugin authors will work with: +//! - [`EventContext`], which wraps each incoming event and lets you cancel +//! or mutate it before the host processes it. +//! - [`EventHandler`], a trait with an async method per event type. You +//! typically implement this inside an `#[event_handler]` block. +//! +//! The concrete event structs (`ChatEvent`, `PlayerJoinEvent`, …) live in +//! [`crate::types`], generated from the protobuf definitions. + pub mod context; pub mod handler; pub mod mutations; diff --git a/packages/rust/src/server/server.rs b/packages/rust/src/server/server.rs index b4281df..2a1f90f 100644 --- a/packages/rust/src/server/server.rs +++ b/packages/rust/src/server/server.rs @@ -1,3 +1,9 @@ +//! Lightweight handle for sending actions and subscriptions to the host. +//! +//! Plugin authors receive a [`Server`] reference in every event handler. +//! It can be cloned freely and used to send actions like `send_chat`, +//! `teleport`, or `world_set_block` back to the Dragonfly host. + use tokio::sync::mpsc; use crate::types::{self, PluginToHost}; diff --git a/packages/rust/tests/command_derive.rs b/packages/rust/tests/command_derive.rs new file mode 100644 index 0000000..6e6c068 --- /dev/null +++ b/packages/rust/tests/command_derive.rs @@ -0,0 +1,137 @@ +use std::convert::TryFrom; + +use dragonfly_plugin::{command::CommandParseError, types, Command}; + +fn make_event(player_uuid: &str, command: &str, args: &[&str]) -> types::CommandEvent { + types::CommandEvent { + player_uuid: player_uuid.to_string(), + name: format!("/{command} {}", args.join(" ")), + raw: format!("/{command} {}", args.join(" ")), + command: command.to_string(), + args: args.iter().map(|s| s.to_string()).collect(), + } +} + +#[derive(Debug, Command)] +#[command(name = "ping", description = "Ping command", aliases("p"))] +struct Ping { + times: i32, +} + +#[derive(Debug, Command)] +#[command(name = "eco", description = "Economy command")] +enum Eco { + #[subcommand(aliases("donate"))] + Pay { + amount: f64, + }, + Bal, +} + +#[derive(Debug, Command)] +#[command(name = "flaggy", description = "Flag-style command")] +enum Flaggy { + Enable { + #[allow(dead_code)] + #[command(name = "value")] + value: bool, + }, +} + +#[test] +fn struct_command_parses_ok() { + let event = make_event("player-uuid", "ping", &["3"]); + let cmd = Ping::try_from(&event).expect("expected ping command to parse"); + assert_eq!(cmd.times, 3); +} + +#[test] +fn struct_command_respects_aliases() { + let event = make_event("player-uuid", "p", &["5"]); + let cmd = Ping::try_from(&event).expect("expected alias to parse"); + assert_eq!(cmd.times, 5); +} + +#[test] +fn struct_command_errors_when_name_does_not_match() { + let event = make_event("player-uuid", "other", &["3"]); + let err = Ping::try_from(&event).unwrap_err(); + assert!(matches!(err, CommandParseError::NoMatch)); +} + +#[test] +fn struct_command_reports_missing_and_invalid_args() { + // Missing required arg. + let event = make_event("player-uuid", "ping", &[]); + let err = Ping::try_from(&event).unwrap_err(); + assert!(matches!(err, CommandParseError::Missing("times"))); + + // Invalid arg type. + let event = make_event("player-uuid", "ping", &["not-a-number"]); + let err = Ping::try_from(&event).unwrap_err(); + assert!(matches!(err, CommandParseError::Invalid("times"))); +} + +#[test] +fn enum_command_parses_subcommands_and_args() { + // canonical subcommand name + let event = make_event("player-uuid", "eco", &["pay", "10.5"]); + let cmd = Eco::try_from(&event).expect("expected eco pay to parse"); + match cmd { + Eco::Pay { amount } => assert!((amount - 10.5).abs() < f64::EPSILON), + other => panic!("expected Pay variant, got {other:?}"), + } + + // alias subcommand + let event = make_event("player-uuid", "eco", &["donate", "2"]); + let cmd = Eco::try_from(&event).expect("expected eco donate to parse"); + match cmd { + Eco::Pay { amount } => assert!((amount - 2.0).abs() < f64::EPSILON), + other => panic!("expected Pay variant, got {other:?}"), + } + + // unit-like subcommand + let event = make_event("player-uuid", "eco", &["bal"]); + let cmd = Eco::try_from(&event).expect("expected eco bal to parse"); + matches!(cmd, Eco::Bal); +} + +#[test] +fn enum_command_reports_missing_or_unknown_subcommand() { + // No args -> missing subcommand. + let event = make_event("player-uuid", "eco", &[]); + let err = Eco::try_from(&event).unwrap_err(); + assert!(matches!(err, CommandParseError::Missing("subcommand"))); + + // Unrecognised subcommand string. + let event = make_event("player-uuid", "eco", &["nope"]); + let err = Eco::try_from(&event).unwrap_err(); + assert!(matches!(err, CommandParseError::UnknownSubcommand)); +} + +#[test] +fn bool_flags_parse_as_expected() { + // NOTE: rust FromStr of bools is case sensitive to literally the word. + // maybe later we add blanket impls for our own trait to parse from commands. + // TODO: this enables a good amount of flexibility so 0.3.1 + // we could add that as its not a BC. + + // true-like values + let event = make_event("player-uuid", "flaggy", &["enable", "true"]); + let cmd = Flaggy::try_from(&event).expect("expected flaggy enable to parse"); + match cmd { + Flaggy::Enable { value } => assert!(value, "expected true to parse as true"), + } + + // false-like values + let event = make_event("player-uuid", "flaggy", &["enable", "false"]); + let cmd = Flaggy::try_from(&event).expect("expected flaggy enable to parse"); + match cmd { + Flaggy::Enable { value } => assert!(!value, "expected false to parse as false"), + } + + // invalid values should surface a parse error + let event = make_event("player-uuid", "flaggy", &["enable", "not-a-bool"]); + let err = Flaggy::try_from(&event).unwrap_err(); + assert!(matches!(err, CommandParseError::Invalid("value"))); +} diff --git a/packages/rust/tests/event_pipeline.rs b/packages/rust/tests/event_pipeline.rs new file mode 100644 index 0000000..abceda9 --- /dev/null +++ b/packages/rust/tests/event_pipeline.rs @@ -0,0 +1,221 @@ +use dragonfly_plugin::{ + command::Ctx, + event::{EventContext, EventHandler}, + event_handler, + server::Server, + types, Command, EventSubscriptions, Plugin, +}; +use tokio::sync::{mpsc, Mutex}; + +#[tokio::test] +async fn event_context_cancel_sends_cancelled_result() { + let (tx, mut rx) = mpsc::channel(1); + + let chat = types::ChatEvent { + player_uuid: "player-uuid".to_string(), + name: "Player".to_string(), + message: "hello".to_string(), + }; + + let mut ctx = EventContext::new("event-1", &chat, tx, "plugin-id".to_string()); + ctx.cancel().await; + + let msg = rx.recv().await.expect("expected event result message"); + assert_eq!(msg.plugin_id, "plugin-id"); + + match msg.payload.expect("missing payload") { + types::PluginPayload::EventResult(result) => { + assert_eq!(result.event_id, "event-1"); + assert_eq!(result.cancel, Some(true)); + assert!(result.update.is_none()); + } + other => panic!("unexpected payload: {:?}", other), + } +} + +#[tokio::test] +async fn event_context_mutation_helper_sets_update() { + let (tx, mut rx) = mpsc::channel(1); + + let chat = types::ChatEvent { + player_uuid: "player-uuid".to_string(), + name: "Player".to_string(), + message: "before".to_string(), + }; + + let mut ctx = EventContext::new("event-mutate", &chat, tx, "plugin-id".to_string()); + ctx.set_message("after".to_string()); + ctx.send().await; + + let msg = rx.recv().await.expect("expected mutation result"); + + match msg.payload.expect("missing payload") { + types::PluginPayload::EventResult(result) => { + assert_eq!(result.event_id, "event-mutate"); + assert!(result.cancel.is_none()); + + let update = result.update.expect("missing update"); + match update { + types::EventResultUpdate::Chat(mutation) => { + assert_eq!(mutation.message.as_deref(), Some("after")); + } + other => panic!("unexpected update variant: {:?}", other), + } + } + other => panic!("unexpected payload: {:?}", other), + } +} + +#[derive(Default)] +struct RecordingPlugin { + calls: Mutex>, +} + +impl EventSubscriptions for RecordingPlugin { + fn get_subscriptions(&self) -> Vec { + vec![types::EventType::Chat] + } +} + +impl dragonfly_plugin::command::CommandRegistry for RecordingPlugin {} + +impl EventHandler for RecordingPlugin { + async fn on_chat(&self, _server: &Server, _event: &mut EventContext<'_, types::ChatEvent>) { + self.calls.lock().await.push("chat"); + } +} + +#[tokio::test] +async fn dispatch_event_routes_chat_to_handler() { + let (tx, mut rx) = mpsc::channel(1); + + let server = Server { + plugin_id: "plugin-id".to_string(), + sender: tx, + }; + + let plugin = RecordingPlugin::default(); + + let chat = types::ChatEvent { + player_uuid: "player-uuid".to_string(), + name: "Player".to_string(), + message: "hello".to_string(), + }; + + let envelope = types::EventEnvelope { + event_id: "chat-event".to_string(), + r#type: types::EventType::Chat as i32, + expects_response: true, + payload: Some(types::EventPayload::Chat(chat)), + }; + + dragonfly_plugin::event::dispatch_event(&server, &plugin, &envelope).await; + + // Handler was called. + let calls = plugin.calls.lock().await; + assert_eq!(calls.as_slice(), &["chat"]); + drop(calls); + + // Ack was sent. + let msg = rx.recv().await.expect("expected ack from dispatch_event"); + assert_eq!(msg.plugin_id, "plugin-id"); +} + +#[tokio::test] +#[should_panic(expected = "Attempted to respond twice to the same event!")] +async fn event_context_double_send_panics_in_debug() { + let (tx, _rx) = mpsc::channel(1); + + let chat = types::ChatEvent { + player_uuid: "player-uuid".to_string(), + name: "Player".to_string(), + message: "hello".to_string(), + }; + + let mut ctx = EventContext::new("event-double", &chat, tx, "plugin-id".to_string()); + + // First send is fine. + ctx.send().await; + // Second send should panic in debug builds. + ctx.send().await; +} + +#[derive(Default, Plugin)] +#[plugin( + id = "test-plugin", + name = "Test Plugin", + version = "0.0.0", + api = "1.0.0", + commands(PingCommand) +)] +struct CommandPlugin { + calls: Mutex>, +} + +#[derive(Debug, Command)] +#[command(name = "ping", description = "Ping command")] +struct PingCommand { + value: i32, +} + +#[event_handler] +impl EventHandler for CommandPlugin { + async fn on_command( + &self, + _server: &Server, + _event: &mut EventContext<'_, types::CommandEvent>, + ) { + self.calls + .lock() + .await + .push("on_command_fallback".to_string()); + } +} + +impl PingCommandHandler for CommandPlugin { + async fn ping_command(&self, ctx: Ctx<'_>, value: i32) { + self.calls + .lock() + .await + .push(format!("handled:{}:{value}", ctx.sender)); + } +} + +#[tokio::test] +async fn dispatch_event_dispatches_command_before_on_command() { + let (tx, mut rx) = mpsc::channel(1); + + let server = Server { + plugin_id: "plugin-id".to_string(), + sender: tx, + }; + + let plugin = CommandPlugin::default(); + + let cmd_event = types::CommandEvent { + player_uuid: "player-uuid".to_string(), + name: "/ping 5".to_string(), + raw: "/ping 5".to_string(), + command: "ping".to_string(), + args: vec!["5".to_string()], + }; + + let envelope = types::EventEnvelope { + event_id: "cmd-event".to_string(), + r#type: types::EventType::Command as i32, + expects_response: true, + payload: Some(types::EventPayload::Command(cmd_event)), + }; + + dragonfly_plugin::event::dispatch_event(&server, &plugin, &envelope).await; + + // Command handler should have run, but on_command fallback should not. + let calls = plugin.calls.lock().await; + assert_eq!(calls.len(), 1); + assert!(calls[0].starts_with("handled:player-uuid:5")); + drop(calls); + + // An EventResult ack should have been sent. + let msg = rx.recv().await.expect("expected command EventResult"); + assert_eq!(msg.plugin_id, "plugin-id"); +} diff --git a/packages/rust/tests/server_helpers.rs b/packages/rust/tests/server_helpers.rs new file mode 100644 index 0000000..3c6f2f5 --- /dev/null +++ b/packages/rust/tests/server_helpers.rs @@ -0,0 +1,99 @@ +use dragonfly_plugin::{server::Server, types}; +use tokio::sync::mpsc; + +#[tokio::test] +async fn send_action_wraps_single_action_in_batch() { + let (tx, mut rx) = mpsc::channel(1); + + let server = Server { + plugin_id: "plugin-id".to_string(), + sender: tx, + }; + + let kind = types::action::Kind::SendChat(types::SendChatAction { + target_uuid: "player-uuid".to_string(), + message: "hello".to_string(), + }); + + server.send_action(kind).await.expect("send_action failed"); + + let msg = rx.recv().await.expect("expected PluginToHost message"); + assert_eq!(msg.plugin_id, "plugin-id"); + + match msg.payload.expect("missing payload") { + types::PluginPayload::Actions(batch) => { + assert_eq!(batch.actions.len(), 1); + let action = &batch.actions[0]; + assert!(action.correlation_id.is_none()); + match action.kind.as_ref().expect("missing action kind") { + types::ActionKind::SendChat(chat) => { + assert_eq!(chat.target_uuid, "player-uuid"); + assert_eq!(chat.message, "hello"); + } + other => panic!("unexpected action kind: {:?}", other), + } + } + other => panic!("unexpected payload: {:?}", other), + } +} + +#[tokio::test] +async fn send_chat_helper_builds_correct_action() { + let (tx, mut rx) = mpsc::channel(1); + + let server = Server { + plugin_id: "plugin-id".to_string(), + sender: tx, + }; + + server + .send_chat("player-uuid".to_string(), "hi there".to_string()) + .await + .expect("send_chat failed"); + + let msg = rx.recv().await.expect("expected PluginToHost message"); + match msg.payload.expect("missing payload") { + types::PluginPayload::Actions(batch) => { + assert_eq!(batch.actions.len(), 1); + let action = &batch.actions[0]; + match action.kind.as_ref().expect("missing action kind") { + types::ActionKind::SendChat(chat) => { + assert_eq!(chat.target_uuid, "player-uuid"); + assert_eq!(chat.message, "hi there"); + } + other => panic!("unexpected action kind: {:?}", other), + } + } + other => panic!("unexpected payload: {:?}", other), + } +} + +#[tokio::test] +async fn subscribe_sends_subscribe_payload() { + let (tx, mut rx) = mpsc::channel(1); + + let server = Server { + plugin_id: "plugin-id".to_string(), + sender: tx, + }; + + server + .subscribe(vec![types::EventType::Chat, types::EventType::Command]) + .await + .expect("subscribe failed"); + + let msg = rx.recv().await.expect("expected PluginToHost message"); + assert_eq!(msg.plugin_id, "plugin-id"); + + match msg.payload.expect("missing payload") { + types::PluginPayload::Subscribe(sub) => { + // Order is preserved from the vec we passed in. + assert_eq!(sub.events.len(), 2); + assert_eq!(sub.events[0], types::EventType::Chat as i32); + assert_eq!(sub.events[1], types::EventType::Command as i32); + } + other => panic!("unexpected payload: {:?}", other), + } +} + + diff --git a/packages/rust/xtask/src/generate_actions.rs b/packages/rust/xtask/src/generate_actions.rs index bd94edc..e89f0b8 100644 --- a/packages/rust/xtask/src/generate_actions.rs +++ b/packages/rust/xtask/src/generate_actions.rs @@ -1,3 +1,11 @@ +//! Generate `Server` helper methods for each `action::Kind` variant. +//! +//! This module inspects the prost-generated `action::Kind` enum from +//! `df.plugin.rs` and produces a single `impl Server { ... }` block in +//! `src/server/helpers.rs`. Each action variant gets a corresponding async +//! helper method that takes ergonomic parameters and forwards them into the +//! raw `types::Action` wire format. + use anyhow::Result; use heck::ToSnakeCase; use quote::{format_ident, quote}; @@ -9,6 +17,8 @@ use crate::utils::{ prettify_code, ConversionLogic, }; +/// Generate the `impl Server { .. }` block with one helper per `action::Kind` +/// variant in the prost-generated API. pub(crate) fn generate_server_helpers( ast: &File, all_structs: &HashMap, @@ -120,4 +130,22 @@ mod tests { insta::assert_snapshot!("server_actions", prettified_code); } + + #[test] + fn generate_server_helpers_fails_when_struct_missing() { + // Create a tiny AST with an action::Kind enum referring to a non-existent struct. + let code = r#" + mod action { + pub enum Kind { + Missing(MissingAction), + } + } + "#; + let ast: File = parse_file(code).expect("Failed to parse test AST"); + let all_structs = find_all_structs(&ast); + + let err = generate_server_helpers_tokens(&ast, &all_structs).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("Struct definition not found for MissingAction")); + } } diff --git a/packages/rust/xtask/src/generate_handlers.rs b/packages/rust/xtask/src/generate_handlers.rs index f0b3e9e..f22d313 100644 --- a/packages/rust/xtask/src/generate_handlers.rs +++ b/packages/rust/xtask/src/generate_handlers.rs @@ -1,3 +1,15 @@ +//! Generate the `EventHandler` trait and `dispatch_event` function. +//! +//! This module walks the prost-generated `event_envelope::Payload` enum and +//! produces: +//! - A trait method per event (e.g. `async fn on_chat(...)`). +//! - A single `dispatch_event` function that decodes envelopes and forwards +//! them into the correct handler method. +//! +//! The generated code lives in `src/event/handler.rs` and is considered +//! part of the public SDK surface, so changes here must keep the trait +//! shape and dispatch semantics stable. + use anyhow::Result; use heck::ToSnakeCase; use quote::{format_ident, quote}; @@ -6,6 +18,8 @@ use syn::File; use crate::utils::{find_nested_enum, get_variant_type_path, prettify_code}; +/// Generate the `EventHandler` trait and the central `dispatch_event` +/// function from the prost-generated `EventEnvelope::Payload` enum. pub fn generate_handler_trait(ast: &File, output_path: &PathBuf) -> Result<()> { println!( "Generating Event Handler trait in: {}...", diff --git a/packages/rust/xtask/src/generate_mutations.rs b/packages/rust/xtask/src/generate_mutations.rs index 57854d7..e8c148f 100644 --- a/packages/rust/xtask/src/generate_mutations.rs +++ b/packages/rust/xtask/src/generate_mutations.rs @@ -1,3 +1,11 @@ +//! Generate mutation helpers on `EventContext` for event updates. +//! +//! This module inspects the prost-generated `event_result::Update` enum and +//! corresponding mutation structs and generates setter-style helper methods +//! on `EventContext` (e.g. `set_message`, `set_damage`). The resulting +//! code is written to `src/event/mutations.rs` and used directly by plugin +//! authors when mutating events. + use anyhow::Result; use quote::{format_ident, quote}; use std::{collections::HashMap, path::PathBuf}; @@ -5,6 +13,8 @@ use syn::{File, Ident, ItemStruct}; use crate::utils::{find_nested_enum, get_api_type, get_variant_type_path, prettify_code}; +/// Generate mutation helper methods on `EventContext` for each +/// `event_result::Update` variant in the prost-generated API. pub(crate) fn generate_event_mutations( ast: &File, all_structs: &HashMap, @@ -121,4 +131,30 @@ mod tests { insta::assert_snapshot!("event_mutations", prettified_code); } + + #[test] + fn generate_event_mutations_errors_when_payload_missing() { + // Mutation has a variant with no corresponding payload variant. + let code = r#" + mod event_result { + pub enum Update { + Chat(ChatMutation), + } + } + mod event_envelope { + pub enum Payload { + // Intentionally do not include Chat here. + } + } + pub struct ChatMutation { + pub message: ::prost::alloc::string::String, + } + "#; + let ast: File = parse_file(code).expect("Failed to parse test AST"); + let all_structs = find_all_structs(&ast); + + let err = generate_event_mutations_tokens(&ast, &all_structs).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("No event payload for mutation Chat")); + } } diff --git a/packages/rust/xtask/src/main.rs b/packages/rust/xtask/src/main.rs index 52ce030..56061f4 100644 --- a/packages/rust/xtask/src/main.rs +++ b/packages/rust/xtask/src/main.rs @@ -1,3 +1,15 @@ +//! Code generation entrypoint for the Rust `dragonfly-plugin` SDK. +//! +//! This `xtask` binary: +//! - Parses the prost-generated `src/generated/df.plugin.rs` file. +//! - Builds an in-memory AST of all events, actions, and result types. +//! - Regenerates three helper files in the main SDK crate: +//! - `src/event/handler.rs` (`generate_handlers`) +//! - `src/event/mutations.rs` (`generate_mutations`) +//! - `src/server/helpers.rs` (`generate_actions`) +//! The public API of the SDK is considered stable; this task should only +//! be changed in ways that preserve the shape of the generated code. + pub mod generate_actions; pub mod generate_handlers; pub mod generate_mutations; diff --git a/packages/rust/xtask/src/snapshots/xtask__generate_handlers__tests__event_handler.snap b/packages/rust/xtask/src/snapshots/xtask__generate_handlers__tests__event_handler.snap index 8c642eb..239cc28 100644 --- a/packages/rust/xtask/src/snapshots/xtask__generate_handlers__tests__event_handler.snap +++ b/packages/rust/xtask/src/snapshots/xtask__generate_handlers__tests__event_handler.snap @@ -4,7 +4,9 @@ expression: prettified_code --- //! This file is auto-generated by `xtask`. Do not edit manually. #![allow(async_fn_in_trait)] -use crate::{event::EventContext, types, Server, EventSubscriptions}; +use crate::{ + event::EventContext, types, Server, EventSubscriptions, command::CommandRegistry, +}; pub trait EventHandler: EventSubscriptions + Send + Sync { ///Handler for the `Chat` event. async fn on_chat( @@ -22,7 +24,7 @@ pub trait EventHandler: EventSubscriptions + Send + Sync { #[doc(hidden)] pub async fn dispatch_event( server: &Server, - handler: &impl EventHandler, + handler: &(impl EventHandler + CommandRegistry), envelope: &types::EventEnvelope, ) { let Some(payload) = &envelope.payload else { @@ -30,14 +32,24 @@ pub async fn dispatch_event( }; match payload { types::event_envelope::Payload::Chat(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_chat(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } types::event_envelope::Payload::BlockBreak(e) => { - let mut context = EventContext::new(&envelope.event_id, e); + let mut context = EventContext::new( + &envelope.event_id, + e, + server.sender.clone(), + server.plugin_id.clone(), + ); handler.on_block_break(server, &mut context).await; - server.send_event_result(context).await.ok(); + context.send_ack_if_needed().await; } } } diff --git a/packages/rust/xtask/src/snapshots/xtask__generate_mutations__tests__event_mutations.snap b/packages/rust/xtask/src/snapshots/xtask__generate_mutations__tests__event_mutations.snap index 923c45d..8caa6eb 100644 --- a/packages/rust/xtask/src/snapshots/xtask__generate_mutations__tests__event_mutations.snap +++ b/packages/rust/xtask/src/snapshots/xtask__generate_mutations__tests__event_mutations.snap @@ -5,24 +5,51 @@ expression: prettified_code //! This file is auto-generated by `xtask`. Do not edit manually. #![allow(clippy::all)] use crate::types; -use crate::event::EventContext; +use crate::event::{EventContext, EventResult}; impl<'a> EventContext<'a, types::ChatEvent> { + fn ensure_mutation_exists(&mut self) { + if !matches!( + self.result, EventResult::Mutated(types::EventResultUpdate::Chat(_)) + ) { + self.result = EventResult::Mutated( + types::EventResultUpdate::Chat(types::ChatMutation::default()), + ); + } + } ///Sets the `message` for this event. - pub fn set_message(&mut self, message: impl Into>) { - let mutation = types::ChatMutation { - message: message.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::Chat(mutation)); + pub fn set_message(&mut self, message: impl Into>) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated(types::EventResultUpdate::Chat(ref mut m)) = self + .result + { + m.message = message.into(); + } + self } } impl<'a> EventContext<'a, types::BlockBreakEvent> { + fn ensure_mutation_exists(&mut self) { + if !matches!( + self.result, EventResult::Mutated(types::EventResultUpdate::BlockBreak(_)) + ) { + self.result = EventResult::Mutated( + types::EventResultUpdate::BlockBreak( + types::BlockBreakMutation::default(), + ), + ); + } + } ///Sets the `drops` for this event. - pub fn set_drops(&mut self, drops: impl Into>) { - let mutation = types::BlockBreakMutation { - drops: drops.into(), - ..Default::default() - }; - self.set_mutation(types::EventResultUpdate::BlockBreak(mutation)); + pub fn set_drops( + &mut self, + drops: impl Into>, + ) -> &mut Self { + self.ensure_mutation_exists(); + if let EventResult::Mutated(types::EventResultUpdate::BlockBreak(ref mut m)) = self + .result + { + m.drops = drops.into(); + } + self } } diff --git a/packages/rust/xtask/src/utils.rs b/packages/rust/xtask/src/utils.rs index 31d939f..404a894 100644 --- a/packages/rust/xtask/src/utils.rs +++ b/packages/rust/xtask/src/utils.rs @@ -1,3 +1,15 @@ +//! Shared AST helpers used by the Rust SDK `xtask` generators. +//! +//! This module wraps common syn-based utilities for: +//! - Formatting generated Rust code (`prettify_code`). +//! - Scanning the prost-generated AST for structs and nested enums. +//! - Mapping prost field/attribute shapes into public-facing SDK types. +//! - Deciding how to convert helper method arguments into wire types. +//! +//! The functions here are internal to `xtask`, but the behavior they encode +//! (e.g. how `GameMode` enums or `Option` fields are surfaced) is part of +//! the stable shape of the generated SDK API. + use anyhow::Result; use quote::quote; use std::collections::HashMap; @@ -6,6 +18,9 @@ use syn::{ ItemMod, ItemStruct, Meta, Path, PathArguments, Type, TypePath, }; +/// Pretty-print a Rust source string using `prettyplease`. +/// +/// This is applied to all generated files to keep diffs readable. pub(crate) fn prettify_code(content: String) -> Result { let ast = parse_file(&content)?; Ok(prettyplease::unparse(&ast))