Skip to content

Commit ff2f446

Browse files
authored
Merge pull request #28 from secmc/features/rust
Rust client.
2 parents 4b63adc + 7cf9bde commit ff2f446

43 files changed

Lines changed: 6951 additions & 21 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/plugins/php/src/CircleCommand.php

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@
1313
use Dragonfly\PluginLib\Events\EventContext;
1414
use Dragonfly\PluginLib\Util\EnumResolver;
1515

16-
class CircleCommand extends Command {
17-
protected string $name = 'circle';
18-
protected string $description = 'Spawn particles in a circle around all players';
16+
class CircleCommand extends Command
17+
{
18+
protected string $name = "circle";
19+
protected string $description = "Spawn particles in a circle around all players";
1920

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

23-
public function execute(CommandSender $sender, EventContext $ctx): void {
24+
public function execute(CommandSender $sender, EventContext $ctx): void
25+
{
2426
if (!$sender instanceof Player) {
2527
$sender->sendMessage("§cThis command can only be run by a player.");
2628
return;
@@ -35,12 +37,14 @@ public function execute(CommandSender $sender, EventContext $ctx): void {
3537
}
3638
} else {
3739
$particleId = ParticleType::PARTICLE_FLAME;
38-
$particleName = 'flame';
40+
$particleName = "flame";
3941
}
4042

4143
$world = $sender->getWorld();
42-
$correlationId = uniqid('circle_', true);
43-
$ctx->onActionResult($correlationId, function (ActionResult $result) use ($ctx, $world, $particleId) {
44+
$correlationId = uniqid("circle_", true);
45+
$ctx->onActionResult($correlationId, function (
46+
ActionResult $result,
47+
) use ($ctx, $world, $particleId) {
4448
$playersResult = $result->getWorldPlayers();
4549
if ($playersResult === null) {
4650
return;
@@ -58,7 +62,7 @@ public function execute(CommandSender $sender, EventContext $ctx): void {
5862
$cy = $pos->getY();
5963
$cz = $pos->getZ();
6064
for ($i = 0; $i < $points; $i++) {
61-
$angle = (2 * M_PI / $points) * $i;
65+
$angle = ((2 * M_PI) / $points) * $i;
6266
$x = $cx + $radius * cos($angle);
6367
$z = $cz + $radius * sin($angle);
6468

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

7579
$ctx->worldQueryPlayers($world, $correlationId);
76-
$sender->sendMessage("§aSpawning {$particleName} circles around all players!");
80+
$sender->sendMessage(
81+
"§aSpawning {$particleName} circles around all players!",
82+
);
7783
}
7884

7985
/**
8086
* @return array<int, array{name:string,type:string,optional?:bool,enum_values?:array<int,string>}>
8187
*/
82-
public function serializeParamSpec(): array {
83-
$names = EnumResolver::lowerNames(ParticleType::class, ['PARTICLE_TYPE_UNSPECIFIED']);
84-
return $this->withEnum(parent::serializeParamSpec(), 'particle', $names);
88+
public function serializeParamSpec(): array
89+
{
90+
$names = EnumResolver::lowerNames(ParticleType::class, [
91+
"PARTICLE_TYPE_UNSPECIFIED",
92+
]);
93+
return $this->withEnum(
94+
parent::serializeParamSpec(),
95+
"particle",
96+
$names,
97+
);
8598
}
8699

87-
private function resolveParticleId(string $input): ?int {
88-
return EnumResolver::value(ParticleType::class, $input, 'PARTICLE_');
100+
private function resolveParticleId(string $input): ?int
101+
{
102+
return EnumResolver::value(ParticleType::class, $input, "PARTICLE_");
89103
}
90104
}
91-

examples/plugins/rust/.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Cargo build artifacts
2+
target/
3+
4+
# Cargo.lock for libraries (keep for applications)
5+
# See: https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
6+
Cargo.lock
7+
8+
# IDE files
9+
.idea/
10+
.vscode/
11+
*.swp
12+
*.swo
13+
14+
# OS files
15+
.DS_Store
16+
Thumbs.db

examples/plugins/rust/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "rustic-economy"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
# required for any base plugin.
8+
dragonfly-plugin = { path = "../../../packages/rust/"}
9+
tokio = { version = "1.48.0", features = ["full"] }
10+
11+
# used in this plugin specifically but isn't required for all plugins.
12+
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }

examples/plugins/rust/README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
## Rustic Economy – Rust example plugin
2+
3+
`rustic-economy` is a **Rust example plugin** for Dragonfly that demonstrates:
4+
5+
- A simple **SQLite-backed economy** using `sqlx`.
6+
- The Rust SDK macros `#[derive(Plugin)]` and `#[derive(Command)]`.
7+
- The generated **command system** (`Eco` enum + `EcoHandler` trait).
8+
- Using `Ctx` to reply to the invoking player.
9+
10+
It is meant as a learning/reference plugin, not a production-ready economy.
11+
12+
### What this plugin does
13+
14+
- Stores each player’s balance in a local `economy.db` SQLite database.
15+
- Exposes one command, `/eco` (with aliases `/economy` and `/rustic_eco`):
16+
- `/eco pay <amount>` (`/eco donate <amount>`): add money to your own balance.
17+
- `/eco bal` (aliases `/eco balance`, `/eco money`): show your current balance.
18+
19+
Balances are stored as `REAL`/`f64` for simplicity. For real money, you should use
20+
an integer representation (e.g. cents as `i64`) to avoid floating‑point issues.
21+
22+
### Files and structure
23+
24+
- `Cargo.toml`: Rust crate metadata for the example plugin.
25+
- `src/main.rs`: The entire plugin implementation:
26+
- `RusticEconomy` struct holding a `SqlitePool`.
27+
- `impl RusticEconomy { new, get_balance, add_money }` – DB helpers.
28+
- `Eco` command enum + `EcoHandler` impl with `pay` and `bal` handlers.
29+
- `main` function that initialises the DB and runs `PluginRunner`.
30+
31+
### Requirements
32+
33+
- Rust (stable) and `cargo`.
34+
- A Dragonfly host that has the Rust SDK wired in (this repo’s Go host).
35+
- SQLite available on the host machine (the plugin writes `economy.db`
36+
next to where it is run).
37+
38+
### Building the plugin
39+
40+
From the repo root:
41+
42+
```bash
43+
cd examples/plugins/rust
44+
cargo build --release
45+
```
46+
47+
The compiled binary will be in `target/release/rustic-economy` (or `.exe` on Windows).
48+
Point your Dragonfly `plugins.yaml` at that binary.
49+
50+
### Example `plugins.yaml` entry
51+
52+
```yaml
53+
plugins:
54+
- id: rustic-economy
55+
name: Rustic Economy
56+
command: "./examples/plugins/rust/target/release/rustic-economy"
57+
address: "tcp://127.0.0.1:50050"
58+
```
59+
60+
Ensure the `id` matches the `#[plugin(id = "rustic-economy", ...)]` attribute in
61+
`src/main.rs`.
62+
63+
### Running and testing
64+
65+
1. Start Dragonfly with the plugin enabled via `plugins.yaml`.
66+
2. Join the server as a player.
67+
3. Run economy commands in chat:
68+
- `/eco pay 10` – adds 10 to your balance and shows the new total.
69+
- `/eco bal` – prints your current balance.
70+
4. Check that `economy.db` is created and populated in the working directory.
71+
72+
If any DB or send‑chat errors occur, the plugin logs them to stderr and replies
73+
with a generic error message so players aren’t exposed to internals.
74+
75+
### How it uses the Rust SDK
76+
77+
- `#[derive(Plugin)]` + `#[plugin(...)]` describe plugin metadata and register
78+
the `Eco` command with the host.
79+
- `#[derive(Command)]` generates a `EcoHandler` trait and argument parsing from
80+
`types::CommandEvent` into the `Eco` enum.
81+
- `Ctx<'_>` is used to send replies: `ctx.reply("...".to_string()).await`.
82+
- `PluginRunner::run(plugin, "tcp://127.0.0.1:50050")` connects the plugin
83+
process to the Dragonfly host and runs the event loop.
84+
85+
Use this example as a starting point when building stateful Rust plugins that
86+
compose the SDK’s command and event systems with your own storage layer.
87+
88+

examples/plugins/rust/src/main.rs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/// Rustic Economy: a small example plugin backed by SQLite.
2+
///
3+
/// This example demonstrates how to:
4+
/// - Use `#[derive(Plugin)]` to declare plugin metadata and register commands.
5+
/// - Use `#[derive(Command)]` to define a typed command enum.
6+
/// - Hold state (a `SqlitePool`) inside your plugin struct.
7+
/// - Use `Ctx` to reply to the invoking player.
8+
/// - Use `#[event_handler]` for event subscriptions (even when you don't
9+
/// implement any event methods yet).
10+
use dragonfly_plugin::{
11+
Command, Plugin, PluginRunner, command::Ctx, event::EventHandler, event_handler, types,
12+
};
13+
use sqlx::{SqlitePool, sqlite::SqlitePoolOptions};
14+
15+
#[derive(Plugin)]
16+
#[plugin(
17+
id = "rustic-economy",
18+
name = "Rustic Economy",
19+
version = "0.3.0",
20+
api = "1.0.0",
21+
commands(Eco)
22+
)]
23+
struct RusticEconomy {
24+
db: SqlitePool,
25+
}
26+
27+
/// Database helpers for the Rustic Economy example.
28+
impl RusticEconomy {
29+
async fn new() -> Result<Self, Box<dyn std::error::Error>> {
30+
// Create database connection
31+
let db = SqlitePoolOptions::new()
32+
.max_connections(5)
33+
.connect("sqlite:economy.db")
34+
.await?;
35+
36+
// Create table if it doesn't exist.
37+
//
38+
// NOTE: This example stores balances as REAL/f64 for simplicity.
39+
// For real-world money you should use an integer representation
40+
// (e.g. cents as i64) to avoid floating point rounding issues.
41+
sqlx::query(
42+
"CREATE TABLE IF NOT EXISTS users (
43+
uuid TEXT PRIMARY KEY,
44+
balance REAL NOT NULL DEFAULT 0.0
45+
)",
46+
)
47+
.execute(&db)
48+
.await?;
49+
50+
Ok(Self { db })
51+
}
52+
53+
async fn get_balance(&self, uuid: &str) -> Result<f64, sqlx::Error> {
54+
let result: Option<(f64,)> = sqlx::query_as("SELECT balance FROM users WHERE uuid = ?")
55+
.bind(uuid)
56+
.fetch_optional(&self.db)
57+
.await?;
58+
59+
Ok(result.map(|(bal,)| bal).unwrap_or(0.0))
60+
}
61+
62+
async fn add_money(&self, uuid: &str, amount: f64) -> Result<f64, sqlx::Error> {
63+
// Insert or update user balance
64+
sqlx::query(
65+
"INSERT INTO users (uuid, balance) VALUES (?, ?)
66+
ON CONFLICT(uuid) DO UPDATE SET balance = balance + ?",
67+
)
68+
.bind(uuid)
69+
.bind(amount)
70+
.bind(amount)
71+
.execute(&self.db)
72+
.await?;
73+
74+
self.get_balance(uuid).await
75+
}
76+
}
77+
78+
#[derive(Command)]
79+
#[command(
80+
name = "eco",
81+
description = "Rustic Economy commands.",
82+
aliases("economy", "rustic_eco")
83+
)]
84+
pub enum Eco {
85+
#[subcommand(aliases("donate"))]
86+
Pay { amount: f64 },
87+
#[subcommand(aliases("balance", "money"))]
88+
Bal,
89+
}
90+
91+
impl EcoHandler for RusticEconomy {
92+
async fn pay(&self, ctx: Ctx<'_>, amount: f64) {
93+
match self.add_money(&ctx.sender, amount).await {
94+
Ok(new_balance) => {
95+
if let Err(e) = ctx
96+
.reply(format!(
97+
"Added ${:.2}! New balance: ${:.2}",
98+
amount, new_balance
99+
))
100+
.await
101+
{
102+
eprintln!("Failed to send payment reply: {}", e);
103+
}
104+
}
105+
Err(e) => {
106+
eprintln!("Database error: {}", e);
107+
if let Err(send_err) = ctx
108+
.reply("Error processing payment!".to_string())
109+
.await
110+
{
111+
eprintln!("Failed to send error reply: {}", send_err);
112+
}
113+
}
114+
}
115+
}
116+
117+
async fn bal(&self, ctx: Ctx<'_>) {
118+
match self.get_balance(&ctx.sender).await {
119+
Ok(balance) => {
120+
if let Err(e) = ctx
121+
.reply(format!("Your balance: ${:.2}", balance))
122+
.await
123+
{
124+
eprintln!("Failed to send balance reply: {}", e);
125+
}
126+
}
127+
Err(e) => {
128+
eprintln!("Database error: {}", e);
129+
if let Err(send_err) = ctx
130+
.reply("Error checking balance!".to_string())
131+
.await
132+
{
133+
eprintln!("Failed to send error reply: {}", send_err);
134+
}
135+
}
136+
}
137+
}
138+
}
139+
140+
#[event_handler]
141+
impl EventHandler for RusticEconomy {}
142+
143+
#[tokio::main]
144+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
145+
println!("Starting the plugin...");
146+
println!("Initializing database...");
147+
148+
let plugin = RusticEconomy::new().await?;
149+
150+
PluginRunner::run(plugin, "tcp://127.0.0.1:50050").await
151+
}

0 commit comments

Comments
 (0)