Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9511563
Think its there tbh, need to make proc macro and a few readmes and te…
Jviguy Nov 24, 2025
6d02528
AI wrote example plugin for testing.
Jviguy Nov 24, 2025
243a360
should be runnable now example, need to now do the proc macro
Jviguy Nov 24, 2025
a560757
a
HashimTheArab Nov 24, 2025
30f9e4d
fix: socket address
HashimTheArab Nov 24, 2025
9e100d4
fix: connection logic
HashimTheArab Nov 24, 2025
bb23950
Merge branch 'main' into features/rust
HashimTheArab Nov 24, 2025
b8791a0
merge
Jviguy Nov 24, 2025
b76c87e
finally merged.
Jviguy Nov 24, 2025
aa65c73
proc macro made
Jviguy Nov 25, 2025
1a7cf73
some small changes, and attempts to make LSP integration better
Jviguy Nov 25, 2025
55a26ab
update cargo.toml
Jviguy Nov 25, 2025
7be5f51
change how the macro works for better LSP interop. Add readmes
Jviguy Nov 25, 2025
6e6eb90
reexport the macro from main lib crate, update some examples to not r…
Jviguy Nov 25, 2025
e4698c1
published.
Jviguy Nov 25, 2025
98a4c41
new macro and changes to naming way more logical. Need to update docs…
Jviguy Nov 25, 2025
0ba8e41
Replace #[events()] with #[event_handler] on impl block, refactor cra…
Jviguy Nov 25, 2025
9df4daa
a lot: refactor xtask, unit and snapshot test xtask, bring back the o…
Jviguy Nov 26, 2025
ae6785e
Start on rustic-economy examples/plugins/rusts. Revealing some less t…
Jviguy Nov 26, 2025
ef579ae
midway to 3.0. Fixing critical issue.
Jviguy Nov 26, 2025
60bff4b
merge
Jviguy Nov 26, 2025
ab09772
feat: more debug logs
HashimTheArab Nov 26, 2025
7dd2943
all core features of v0.3.0 made and functioning. Need to cleanup som…
Jviguy Nov 27, 2025
76c140e
some more semi BC changes thankfully i didnt publish yet. Added subco…
Jviguy Nov 27, 2025
7cf9bde
v0.3.0 ready for release and merge
Jviguy Nov 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 28 additions & 15 deletions examples/plugins/php/src/CircleCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@
use Dragonfly\PluginLib\Events\EventContext;
use Dragonfly\PluginLib\Util\EnumResolver;

class CircleCommand extends Command {
protected string $name = 'circle';
protected string $description = 'Spawn particles in a circle around all players';
class CircleCommand extends Command
{
protected string $name = "circle";
protected string $description = "Spawn particles in a circle around all players";

/** @var Optional<string> */
public Optional $particle;

public function execute(CommandSender $sender, EventContext $ctx): void {
public function execute(CommandSender $sender, EventContext $ctx): void
{
if (!$sender instanceof Player) {
$sender->sendMessage("§cThis command can only be run by a player.");
return;
Expand All @@ -35,12 +37,14 @@ public function execute(CommandSender $sender, EventContext $ctx): void {
}
} else {
$particleId = ParticleType::PARTICLE_FLAME;
$particleName = 'flame';
$particleName = "flame";
}

$world = $sender->getWorld();
$correlationId = uniqid('circle_', true);
$ctx->onActionResult($correlationId, function (ActionResult $result) use ($ctx, $world, $particleId) {
$correlationId = uniqid("circle_", true);
$ctx->onActionResult($correlationId, function (
ActionResult $result,
) use ($ctx, $world, $particleId) {
$playersResult = $result->getWorldPlayers();
if ($playersResult === null) {
return;
Expand All @@ -58,7 +62,7 @@ public function execute(CommandSender $sender, EventContext $ctx): void {
$cy = $pos->getY();
$cz = $pos->getZ();
for ($i = 0; $i < $points; $i++) {
$angle = (2 * M_PI / $points) * $i;
$angle = ((2 * M_PI) / $points) * $i;
$x = $cx + $radius * cos($angle);
$z = $cz + $radius * sin($angle);

Expand All @@ -73,19 +77,28 @@ public function execute(CommandSender $sender, EventContext $ctx): void {
});

$ctx->worldQueryPlayers($world, $correlationId);
$sender->sendMessage("§aSpawning {$particleName} circles around all players!");
$sender->sendMessage(
"§aSpawning {$particleName} circles around all players!",
);
}

/**
* @return array<int, array{name:string,type:string,optional?:bool,enum_values?:array<int,string>}>
*/
public function serializeParamSpec(): array {
$names = EnumResolver::lowerNames(ParticleType::class, ['PARTICLE_TYPE_UNSPECIFIED']);
return $this->withEnum(parent::serializeParamSpec(), 'particle', $names);
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_');
private function resolveParticleId(string $input): ?int
{
return EnumResolver::value(ParticleType::class, $input, "PARTICLE_");
}
}

16 changes: 16 additions & 0 deletions examples/plugins/rust/.gitignore
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions examples/plugins/rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "rustic-economy"
version = "0.1.0"
edition = "2024"

[dependencies]
# required for any base plugin.
dragonfly-plugin = { path = "../../../packages/rust/"}
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"] }
88 changes: 88 additions & 0 deletions examples/plugins/rust/README.md
Original file line number Diff line number Diff line change
@@ -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 <amount>` (`/eco donate <amount>`): 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.


151 changes: 151 additions & 0 deletions examples/plugins/rust/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/// 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,
};
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};

#[derive(Plugin)]
#[plugin(
id = "rustic-economy",
name = "Rustic Economy",
version = "0.3.0",
api = "1.0.0",
commands(Eco)
)]
struct RusticEconomy {
db: SqlitePool,
}

/// Database helpers for the Rustic Economy example.
impl RusticEconomy {
async fn new() -> Result<Self, Box<dyn std::error::Error>> {
// Create database connection
let db = SqlitePoolOptions::new()
.max_connections(5)
.connect("sqlite:economy.db")
.await?;

// 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,
balance REAL NOT NULL DEFAULT 0.0
)",
)
.execute(&db)
.await?;

Ok(Self { db })
}

async fn get_balance(&self, uuid: &str) -> Result<f64, sqlx::Error> {
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<f64, sqlx::Error> {
// 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
}
}

#[derive(Command)]
#[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,
}

impl EcoHandler for RusticEconomy {
async fn pay(&self, ctx: Ctx<'_>, amount: f64) {
match self.add_money(&ctx.sender, amount).await {
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);
if let Err(send_err) = ctx
.reply("Error processing payment!".to_string())
.await
{
eprintln!("Failed to send error reply: {}", send_err);
}
}
}
}

async fn bal(&self, ctx: Ctx<'_>) {
match self.get_balance(&ctx.sender).await {
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);
if let Err(send_err) = ctx
.reply("Error checking balance!".to_string())
.await
{
eprintln!("Failed to send error reply: {}", send_err);
}
}
}
}
}

#[event_handler]
impl EventHandler for RusticEconomy {}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Starting the plugin...");
println!("Initializing database...");

let plugin = RusticEconomy::new().await?;

PluginRunner::run(plugin, "tcp://127.0.0.1:50050").await
}
Loading
Loading