Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,11 @@ try {
$sub = new \DF\Plugin\PluginToHost();
$sub->setPluginId($pluginId);
$subscribe = new \DF\Plugin\EventSubscribe();
$subscribe->setEvents(['PLAYER_JOIN', 'COMMAND', 'BLOCK_BREAK']);
$subscribe->setEvents([
\DF\Plugin\EventType::PLAYER_JOIN,
\DF\Plugin\EventType::COMMAND,
\DF\Plugin\EventType::PLAYER_BLOCK_BREAK,
]);
$sub->setSubscribe($subscribe);
$stream->write($sub);
continue;
Expand All @@ -136,7 +140,7 @@ try {
$event = $message->getEvent();

// Handle /mine command
if ($event->getType() === 'COMMAND' && $event->hasCommand()) {
if ($event->getType() === \DF\Plugin\EventType::COMMAND && $event->hasCommand()) {
$cmd = $event->getCommand();
if ($cmd->getCommand() === 'mine') {
// Send message to player
Expand Down
7 changes: 4 additions & 3 deletions docs/plugin-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,13 @@ plugins:

## 4. Event Routing

The manager sends events to plugins based on their subscriptions. Current events include:
The manager sends events to plugins based on their subscriptions. Current events include values from the
`EventType` enum, such as:

* `PLAYER_JOIN` / `PLAYER_QUIT`
* `CHAT`
* `COMMAND`
* `BLOCK_BREAK`
* `PLAYER_BLOCK_BREAK`
* `WORLD_CLOSE`

Events carry minimal data required for action correlation (player UUID, name, coordinates). Plugins can correlate
Expand Down Expand Up @@ -147,7 +148,7 @@ respect Dragonfly’s threading model.
* `api_version`
* Optional command registrations (shown in `/help`).
3. Dragonfly identifies the plugin by `plugin_id` and sends `HostHello(api_version="v1")`.
4. Plugin sends `EventSubscribe` listing uppercase event names (`["PLAYER_JOIN", "COMMAND"]`).
4. Plugin sends `EventSubscribe` listing `EventType` values (for example, `[EventType.PLAYER_JOIN, EventType.COMMAND]`).
5. Stream enters steady state: host pushes events; plugin sends actions/logs as needed.

## 8. Backpressure & Fault Handling
Expand Down
4 changes: 3 additions & 1 deletion examples/plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,13 @@ Host ←→ Plugin (EventStream)

### 4. Example Event Types

Values come from the `EventType` enum:

- `PLAYER_JOIN` - Player connected
- `PLAYER_QUIT` - Player disconnected
- `CHAT` - Player sent chat message
- `COMMAND` - Player executed command
- `BLOCK_BREAK` - Player broke a block
- `PLAYER_BLOCK_BREAK` - Player broke a block
- `WORLD_CLOSE` - World is closing

### 5. Example Actions
Expand Down
13 changes: 9 additions & 4 deletions examples/plugins/php/src/HelloPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Df\Plugin\CommandSpec;
use Df\Plugin\EventResult;
use Df\Plugin\EventSubscribe;
use Df\Plugin\EventType;
use Df\Plugin\PluginClient;
use Df\Plugin\PluginHello;
use Df\Plugin\PluginToHost;
Expand Down Expand Up @@ -51,7 +52,11 @@
$subscribeMsg = new PluginToHost();
$subscribeMsg->setPluginId($pluginId);
$subscribe = new EventSubscribe();
$subscribe->setEvents(['PLAYER_JOIN', 'COMMAND', 'CHAT']);
$subscribe->setEvents([
EventType::PLAYER_JOIN,
EventType::COMMAND,
EventType::CHAT,
]);
$subscribeMsg->setSubscribe($subscribe);
$call->write($subscribeMsg);

Expand All @@ -76,12 +81,12 @@
$event = $message->getEvent();
$eventId = $event->getEventId();

if ($event->getType() === 'PLAYER_JOIN' && $event->hasPlayerJoin()) {
if ($event->getType() === EventType::PLAYER_JOIN && $event->hasPlayerJoin()) {
acknowledgeEvent($call, $pluginId, $eventId);
continue;
}

if ($event->getType() === 'CHAT' && $event->hasChat()) {
if ($event->getType() === EventType::CHAT && $event->hasChat()) {
$chat = $event->getChat();
$text = $chat->getMessage();

Expand All @@ -101,7 +106,7 @@
continue;
}

if ($event->getType() === 'COMMAND' && $event->hasCommand()) {
if ($event->getType() === EventType::COMMAND && $event->hasCommand()) {
$commandEvent = $event->getCommand();
if ($commandEvent->getRaw() === '/cheers') {
$action = new Action();
Expand Down
21 changes: 14 additions & 7 deletions examples/plugins/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
HostToPlugin,
PluginToHost,
GameMode,
EventType,
} from '@dragonfly/proto';

const pluginId = process.env.DF_PLUGIN_ID || 'typescript-plugin';
Expand Down Expand Up @@ -46,7 +47,7 @@ function handleEvent(
event: NonNullable<HostToPlugin['event']>
) {
switch (event.type) {
case 'PLAYER_JOIN': {
case EventType.PLAYER_JOIN: {
const player = event.playerJoin;
if (!player) break;

Expand All @@ -72,7 +73,7 @@ function handleEvent(
break;
}

case 'PLAYER_QUIT': {
case EventType.PLAYER_QUIT: {
const player = event.playerQuit;
if (!player) break;
console.log(`[ts] player left ${player.name}`);
Expand All @@ -89,7 +90,7 @@ function handleEvent(
break;
}

case 'COMMAND': {
case EventType.COMMAND: {
const cmd = event.command;
if (!cmd) break;

Expand Down Expand Up @@ -228,7 +229,7 @@ function handleEvent(
break;
}

case 'CHAT': {
case EventType.CHAT: {
const chat = event.chat;
if (!chat) break;

Expand Down Expand Up @@ -292,7 +293,7 @@ function handleEvent(
break;
}

case 'BLOCK_BREAK': {
case EventType.PLAYER_BLOCK_BREAK: {
const blockBreak = event.blockBreak;
if (!blockBreak) break;

Expand Down Expand Up @@ -328,7 +329,7 @@ function handleEvent(
}

default:
console.log('[ts] unhandled event type:', event.type);
console.log('[ts] unhandled event type:', EventType[event.type] ?? event.type);
}
}

Expand Down Expand Up @@ -371,7 +372,13 @@ call.write(helloMessage);
const initialSubscribe: PluginToHost = {
pluginId,
subscribe: {
events: ['PLAYER_JOIN', 'PLAYER_QUIT', 'COMMAND', 'CHAT', 'BLOCK_BREAK'],
events: [
EventType.PLAYER_JOIN,
EventType.PLAYER_QUIT,
EventType.COMMAND,
EventType.CHAT,
EventType.PLAYER_BLOCK_BREAK,
],
},
};
call.write(initialSubscribe);
Expand Down
2 changes: 1 addition & 1 deletion plugin/adapters/handlers/world.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func (h *WorldHandler) HandleClose(tx *world.Tx) {
}
evt := &pb.EventEnvelope{
EventId: h.broadcaster.GenerateEventID(),
Type: "WORLD_CLOSE",
Type: pb.EventType_WORLD_CLOSE,
Payload: &pb.EventEnvelope_WorldClose{
WorldClose: &pb.WorldCloseEvent{},
},
Expand Down
16 changes: 8 additions & 8 deletions plugin/adapters/plugin/emitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ func (m *Emitter) detachPlayer(p *player.Player) {
func (m *Emitter) EmitPlayerJoin(p *player.Player) {
evt := &pb.EventEnvelope{
EventId: m.generateEventID(),
Type: "PLAYER_JOIN",
Type: pb.EventType_PLAYER_JOIN,
Payload: &pb.EventEnvelope_PlayerJoin{
PlayerJoin: &pb.PlayerJoinEvent{
PlayerUuid: p.UUID().String(),
Expand All @@ -234,7 +234,7 @@ func (m *Emitter) EmitPlayerJoin(p *player.Player) {
func (m *Emitter) EmitPlayerQuit(p *player.Player) {
evt := &pb.EventEnvelope{
EventId: m.generateEventID(),
Type: "PLAYER_QUIT",
Type: pb.EventType_PLAYER_QUIT,
Payload: &pb.EventEnvelope_PlayerQuit{
PlayerQuit: &pb.PlayerQuitEvent{
PlayerUuid: p.UUID().String(),
Expand All @@ -251,7 +251,7 @@ func (m *Emitter) EmitChat(ctx *player.Context, p *player.Player, msg *string) {
}
evt := &pb.EventEnvelope{
EventId: m.generateEventID(),
Type: "CHAT",
Type: pb.EventType_CHAT,
Payload: &pb.EventEnvelope_Chat{
Chat: &pb.ChatEvent{
PlayerUuid: p.UUID().String(),
Expand Down Expand Up @@ -285,7 +285,7 @@ func (m *Emitter) EmitCommand(ctx *player.Context, p *player.Player, cmdName str
}
evt := &pb.EventEnvelope{
EventId: m.generateEventID(),
Type: "COMMAND",
Type: pb.EventType_COMMAND,
Payload: &pb.EventEnvelope_Command{
Command: &pb.CommandEvent{
PlayerUuid: p.UUID().String(),
Expand All @@ -308,7 +308,7 @@ func (m *Emitter) EmitCommand(ctx *player.Context, p *player.Player, cmdName str
func (m *Emitter) EmitBlockBreak(ctx *player.Context, p *player.Player, pos cube.Pos, drops *[]item.Stack, xp *int, worldDim string) {
evt := &pb.EventEnvelope{
EventId: m.generateEventID(),
Type: "BLOCK_BREAK",
Type: pb.EventType_PLAYER_BLOCK_BREAK,
Payload: &pb.EventEnvelope_BlockBreak{
BlockBreak: &pb.BlockBreakEvent{
PlayerUuid: p.UUID().String(),
Expand Down Expand Up @@ -351,7 +351,7 @@ func (m *Emitter) dispatchEvent(envelope *pb.EventEnvelope, expectResult bool) [
if envelope == nil {
return nil
}
eventType := strings.ToUpper(envelope.Type)
eventType := envelope.Type
m.mu.RLock()
procs := make([]*pluginProcess, 0, len(m.plugins))
for _, proc := range m.plugins {
Expand Down Expand Up @@ -385,14 +385,14 @@ func (m *Emitter) dispatchEvent(envelope *pb.EventEnvelope, expectResult bool) [
res, err := proc.waitEventResult(waitCh, eventResponseTimeout)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
proc.log.Warn("plugin did not respond to event", "event_id", envelope.EventId, "type", envelope.Type)
proc.log.Warn("plugin did not respond to event", "event_id", envelope.EventId, "type", envelope.Type.String())
}
proc.discardEventResult(envelope.EventId)
continue
}
if res != nil {
results = append(results, res)
if envelope.Type == "CHAT" {
if envelope.Type == pb.EventType_CHAT {
if chatEvt := envelope.GetChat(); chatEvt != nil {
if chatMut := res.GetChat(); chatMut != nil {
chatEvt.Message = chatMut.Message
Expand Down
15 changes: 8 additions & 7 deletions plugin/adapters/plugin/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"log/slog"
"os"
"os/exec"
"strings"
"sync"
"sync/atomic"
"time"
Expand Down Expand Up @@ -226,25 +225,27 @@ func (p *pluginProcess) recvLoop() {
}
}

func (p *pluginProcess) HasSubscription(event string) bool {
func (p *pluginProcess) HasSubscription(event pb.EventType) bool {
if !p.ready.Load() {
return false
}
if _, ok := p.subscriptions.Load("*"); ok {
if _, ok := p.subscriptions.Load(pb.EventType_EVENT_TYPE_ALL); ok {
return true
}
_, ok := p.subscriptions.Load(strings.ToUpper(event))
if event == pb.EventType_EVENT_TYPE_UNSPECIFIED {
return false
}
_, ok := p.subscriptions.Load(event)
return ok
}

func (p *pluginProcess) updateSubscriptions(events []string) {
func (p *pluginProcess) updateSubscriptions(events []pb.EventType) {
p.subscriptions.Range(func(key, value any) bool {
p.subscriptions.Delete(key)
return true
})
for _, evt := range events {
evt = strings.ToUpper(strings.TrimSpace(evt))
if evt == "" {
if evt == pb.EventType_EVENT_TYPE_UNSPECIFIED {
continue
}
p.subscriptions.Store(evt, struct{}{})
Expand Down
2 changes: 1 addition & 1 deletion plugin/ports/ports.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type PluginManager interface {
type PluginProcess interface {
Start(ctx context.Context) error
Stop()
HasSubscription(eventType string) bool
HasSubscription(eventType pb.EventType) bool
Queue(msg *pb.HostToPlugin)
}

Expand Down
4 changes: 2 additions & 2 deletions proto/generated/php/Df/Plugin/BlockBreakEvent.php

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions proto/generated/php/Df/Plugin/ChatEvent.php

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions proto/generated/php/Df/Plugin/CommandEvent.php

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading