Build HUD widgets, chat commands, and client features.

Client Plugins

Client plugins are JavaScript files that run in the player's browser. They can create HUD widgets, register local chat commands, and communicate with server plugins.

Plugin editor — client and server plugins share the same managed editor with file creation, enable toggle, and code editing
Plugin editor — client and server plugins share the same managed editor with file creation, enable toggle, and code editing

How They Work

1. Create a .js file in the Plugin editor (Client tab) or the client-plugins/ folder 2. When a player connects, the server sends all client plugins to their browser 3. Each plugin runs with a ctx object and interacts with the game only through it

Important: Client plugins must be plain JavaScript (.js).

> Sandbox: by default (sandboxedClientPlugins, on), a client plugin runs inside a QuickJS-WASM isolate with no DOM, window, or globals — it reaches the page only through ctx. In this mode import lines are stripped and modern (ES2020) syntax is fine. If the sandbox flag is turned off, a legacy path runs the code with new Function and full DOM access — only there does ES5-only advice apply. Write for the sandbox: use ctx, not the DOM. Unlike server plugins, the client ctx is not capability-gated today.

Game State

var state = ctx.getState();
// Returns: selfId, selfName, selfPosition, selfHp, selfMaxHp,
// selfMp, selfMaxMp, selfSilver, accountGold, currentZoneId,
// currentZoneName, connected, isDead, isMounted, playerLevel,
// inventory, equipment, skills, activeEffects, activeQuests,
// party, guild, chatMessages, entities, survival, environment

ctx.onStateChange(function() { var state = ctx.getState(); // Update your widgets here... });

HUD Widgets

Create floating UI elements on screen:

var widgetId = ctx.createWidget({
  html: "<div>My Widget</div>",
  anchor: "top-right",
  position: { x: 16, y: 80 },
  width: 200,
  height: 100,
  visible: true,
  draggable: true,
  zIndex: 100,
  css: "background:rgba(0,0,0,0.8);color:white;" +
       "padding:10px;border-radius:8px;",
  onClick: function() {
    ctx.addLocalChatMessage("Widget clicked!");
  },
});

ctx.updateWidget(widgetId, { html: "<div>Updated!</div>" }); ctx.removeWidget(widgetId);

Widget Options:

PropertyTypeDefaultDescription
htmlstringrequiredInner HTML content
cssstringInline CSS style
anchorstring"top-left"Screen anchor: top-left, top-right, bottom-left, bottom-right, center
position{x, y}{0, 0}Offset from anchor
widthnumberautoWidth in pixels
heightnumberautoHeight in pixels
visiblebooleantrueShow/hide
draggablebooleanfalseUser can drag the widget
zIndexnumber100Layering order
onClickfunctionClick handler

Chat Commands

ctx.addChatCommand("pos", function(args) {
  var state = ctx.getState();
  ctx.addLocalChatMessage(
    "[Plugin] X=" + Math.round(state.selfPosition.x) +
    " Y=" + Math.round(state.selfPosition.y)
  );
});

Server Communication

Send and receive data from server plugins:

// Send to server
ctx.send("my-channel", { action: "request-data" });

// Receive from server ctx.on("my-channel", function(data) { ctx.addLocalChatMessage("Server says: " + data.message); });

Timers

var cancel = ctx.setInterval(function() {
  // runs every second
}, 1000);

ctx.setTimeout(function() { ctx.addLocalChatMessage("Timer fired!"); }, 5000);

More client APIs

Beyond the above, the client ctx also gives you:

ctx.pluginName;                       // this plugin's name (read-only)

ctx.log.info("loaded"); // client logger: info / warn / error (no debug)

// Listen to raw server messages by type (not just plugin channels): var off = ctx.onServerMessage("EntityUpdate", function (msg) { / ... / }); off(); // every subscribe returns an unsubscribe fn

ctx.on, ctx.onStateChange, ctx.addChatCommand, ctx.setInterval, and ctx.setTimeout all return a teardown function too — call it to stop listening.

Restrictions

  • No direct DOM access — build UI only through createWidget. Widget HTML is sanitized: <script>, inline on* handlers, javascript: URLs, and <iframe>/<object>/<embed>/<form>/<meta>/<link>/<base> are stripped. Use the onClick option for interactivity, not inline handlers.
  • No access to game engine internals — only the ctx API.
  • Server communication is named channels carrying arbitrary JSON — not schema-validated, so validate payloads on the server.
  • In the sandboxed path, a widget's onClick receives no arguments (the MouseEvent is only available on the legacy non-sandboxed path).