Full guide to server-side plugin development.
Server Plugins
Server plugins run inside the game server with full access to game events, entities, zones, and player communication.
PluginContext API
The ctx object provides:
Event Subscription
Subscribe to any of the 50+ typed game events:
// Combat events
ctx.eventBus.on("mob:killed", (e) => {
// e.killerId, e.mobId, e.mobDefinitionId,
// e.posX, e.posY, e.zoneId, e.mobTier
});
ctx.eventBus.on("damage:dealt", (e) => {
// e.attackerId, e.targetId, e.abilityId,
// e.damage, e.isCrit, e.damageType, e.targetHpLeft
});
ctx.eventBus.on("player:killed", (e) => {
// e.victimId, e.killerId, e.isPvpKill,
// e.pvpRule, e.zoneId
});
// Crafting & Harvesting
ctx.eventBus.on("craft:completed", (e) => {
// e.playerId, e.recipeId, e.outputItemDefinitionId,
// e.outputQuantity, e.quality
});
ctx.eventBus.on("harvest:yield", (e) => {
// e.playerId, e.nodeId, e.resourceType,
// e.itemDefinitionId, e.quantity, e.zoneId
});
// Progression
ctx.eventBus.on("skill:levelUp", (e) => {
// e.playerId, e.skillId, e.newLevel
});
ctx.eventBus.on("quest:turnedIn", (e) => {
// e.playerId, e.questId, e.questName,
// e.rewardGold, e.rewardXp, e.rewardItemIds
});
// Server lifecycle
ctx.eventBus.on("server:ready", () => {
ctx.log.info("Server is ready!");
});
All subscriptions auto-clean when the plugin unloads.
Chat Commands
Register slash commands players type in chat:
ctx.registerCommand("spawn", (entityId, args, rawMessage) => {
const [mobType, countStr] = args;
const count = parseInt(countStr) || 1;
ctx.sendSystemMessage(entityId, Spawning ${count} ${mobType});
}, {
description: "Spawn mobs (admin only)",
usage: "/spawn <mobType> [count]",
minArgs: 1,
adminOnly: true,
});CommandOptions:
| Option | Type | Description |
|---|---|---|
description | string | Shown in /help |
usage | string | Usage hint |
minArgs | number | Required argument count |
adminOnly | boolean | Admin/GM only |
Player Messaging
ctx.sendSystemMessage(entityId, "You found a rare item!");
ctx.broadcastSystemMessage("World boss spawning in 60 seconds!");Custom Intents
Handle custom client-to-server actions:
ctx.registerIntent("MyCustomAction", (entityId, data) => {
ctx.log.info(Player ${entityId} sent: ${JSON.stringify(data)});
});Timers
const cancel = ctx.setInterval(() => {
ctx.broadcastSystemMessage("Event reminder!");
}, 5 60 1000);
ctx.setTimeout(() => {
ctx.broadcastSystemMessage("Event starting NOW!");
}, 60 * 1000);
All timers auto-cancel on plugin unload.
Data Store
In-memory key-value storage scoped to your plugin:
ctx.setData("leaderboard", new Map<string, number>());
const board = ctx.getData<Map<string, number>>("leaderboard");Persists while the server runs, resets on restart.
Entity & Zone Access
const entity = ctx.entities.get(entityId);
const zone = ctx.zones.getZone("starting_zone");Logging
ctx.log.info("Plugin started");
ctx.log.warn("Low resources");
ctx.log.error("Something failed", error);
ctx.log.debug("Trace info");All logs are auto-prefixed with the plugin name.
Complete Example: Kill Tracker
import type { PluginDefinition } from "../src/core/plugin-system.js";
const killCounts = new Map<string, number>();
const plugin: PluginDefinition = {
name: "kill-tracker",
version: "1.0.0",
description: "Tracks mob kills and awards milestones.",
register(ctx) {
ctx.eventBus.on("mob:killed", (event) => {
const count = (killCounts.get(event.killerId) ?? 0) + 1;
killCounts.set(event.killerId, count);
if (count === 10) {
ctx.sendSystemMessage(event.killerId, "Novice Slayer — 10 kills!");
} else if (count === 100) {
ctx.sendSystemMessage(event.killerId, "Veteran Hunter — 100 kills!");
} else if (count === 1000) {
ctx.sendSystemMessage(event.killerId, "Legendary Champion — 1000 kills!");
}
});
ctx.registerCommand("kills", (entityId) => {
const count = killCounts.get(entityId) ?? 0;
ctx.sendSystemMessage(entityId, You have ${count} mob kills.);
}, { description: "Show your kill count" });
ctx.registerCommand("killboard", (entityId) => {
const sorted = Array.from(killCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
const lines = sorted.map(([id, count], i) =>
${i + 1}. ${id.slice(0, 8)}... — ${count} kills
);
ctx.sendSystemMessage(entityId,
Kill Leaderboard:\n${lines.join("\n")});
}, { description: "Top 5 killers" });
ctx.setInterval(() => {
const total = Array.from(killCounts.values())
.reduce((a, b) => a + b, 0);
if (total > 0) {
ctx.broadcastSystemMessage(${total} mobs slain this session!);
}
}, 10 60 1000);
},
unregister() {
killCounts.clear();
},
};
export default plugin;