Meme Wars · Plugin System · Design

How should a plugin
draw a screen?

Plugins run server-side only — they can't ship a line of client code. So a UI framework can't be "call these draw functions." It has to be a declarative vocabulary: the plugin describes what it wants in data, and the engine's client renders it — exactly how a plugin item already picks an icon key and a tint instead of shipping art.

Four directions for that vocabulary, from a one-liner HUD bar to a full retained widget tree. Each shows the Lua a plugin writes and what the player would see. Pick one (or a layering) and we build it.

constraint  server-only plugins precedent  setStat → HUD bar, icon palette routing  presses ride the input channel (Set 12.2)
01 · Minimal

AReactive HUD bindings

Declare a readout once and bind it to a value. The plugin never "draws" — it writes a number with setStat (which hunger already does), and the bound widget updates itself. A tiny fixed widget set: bar, counter, timer, icon-value.

the plugin writes
-- declare once, at load
plugin.hud("food", {
  kind = "bar", label = "Food",
  max = 100, color = "#7bd88f", icon = "food",
})
plugin.hud("wave", { kind = "counter", label = "Wave" })

-- then just push values (per player)
engine.setStat(pid, "food", 62)   -- bar fills itself
engine.setStat(pid, "wave", 3)
what the player sees
◈ Food62 / 100
WAVE 3
🍖 × 4
engine renders · plugin only sets numbers
Strength

Almost no API and impossible to misuse. It's the food bar generalised — ships in an afternoon.

Limit

Read-only. No buttons, menus, or layout — purely a corner of status meters.

Cost / wire

Trivial. Values already ride the snapshot's hud; only the widget declarations are new.


02 · Familiar

BImmediate-mode panels

Every tick, the plugin re-describes the whole panel — the Dear ImGui shape. A button call both draws the button and returns whether it was clicked. Flexible and code-like; you write UI as plain control flow.

the plugin writes
-- runs every tick, for each player near a campfire
plugin.ui(pid, function(u)
  u.panel("Campfire", function(p)
    p.text("Nearby produce: " .. n)
    if n > 0 and p.button("Cook all") then
      cook_all(pid)
    end
    if p.button("Leave") then u.close() end
  end)
end)
what the player sees
🔥 Campfire
Nearby produce3
re-sent every frame
Strength

Maximum flexibility with the least ceremony — arbitrary layout as normal Lua, no ids to manage.

Limit

Chatty: the panel is re-declared and re-diffed every tick, and a click is a server round-trip, so buttons feel a frame behind.

Cost / wire

Medium–high. Needs a per-tick UI channel + retained-on-client diffing to stay affordable.


03 · Scalable

CRetained widget tree

Build the panel once, by id; push updates only when something changes; interactions come back as named actions routed like a control. The general answer — efficient on the wire and genuinely stateful.

the plugin writes
-- build once; opens on a key (a control)
local fire = plugin.panel("campfire", {
  title = "Campfire", open = "KeyF",
})
fire.text("status", "Nothing to cook")
fire.button("cook", { label = "Cook all", action = "cook" })

-- update only what changed
fire.set("status", n .. " produce ready")

plugin.on("ui:campfire:cook", function(pid) cook_all(pid) end)
what the player sees
🔥 CampfireF
3 produce ready
held on the client · only diffs sent
Strength

Cheap after the first frame, stateful, and interactions are just controls — the same routing as a keybind. Scales to real menus.

Limit

The largest API surface: ids, an element vocabulary, a diff protocol, action wiring. Most to design and get right.

Cost / wire

Higher up front, lowest at runtime. A create/update/remove protocol over welcome + snapshot.


04 · Safest

DTemplate slots

The engine owns a handful of prefab UIs — a radial menu (the block wheel already is one), a list menu, a toast, a bar group. The plugin picks a template and fills its slots with data. The look stays consistent because the plugin never lays anything out.

the plugin writes
plugin.menu("campfire", {
  template = "radial", title = "Campfire", open = "KeyF",
  options = {
    { id = "cook",  label = "Cook all", icon = "fire" },
    { id = "plant", label = "Plant",    icon = "seed" },
    { id = "douse", label = "Douse",    icon = "crop" },
  },
})

plugin.on("menu:campfire", function(pid, choice)
  if choice == "cook" then cook_all(pid) end
end)
what the player sees
Campfire
🔥Cook all
🌱Plant
🌿Douse
engine owns the look · plugin fills data
Strength

Consistent, polished, hard to make ugly — every plugin's menu matches the game. Small API, reuses the block wheel.

Limit

Only as expressive as the prefab set. Anything outside the templates needs a new engine-side template first.

Cost / wire

Low–medium. One selection message back; templates are built once, in the client.


Recommendation

Layer them, don't pick one

These aren't mutually exclusive — they're rungs. Each is useful on its own, and later ones subsume earlier needs. A staged path gets food's UI shipping now while leaving room to grow.

A pragmatic order

  1. Ship A (reactive bindings) first. It's tiny, it's the food/ammo/wave HUD we already half-have, and it clears the most common need — showing a number — with near-zero risk.
  2. Add D (template menus) for interaction. Reuse the block-wheel renderer as the first template. That covers cooking's "pick an action" and agriculture's "choose a seed" without inventing a layout engine — and its selection routing is the same control channel as keybinds (Set 12.2).
  3. Grow to C (retained tree) only if a plugin outgrows the templates. It's the real framework, but it's the biggest build; defer it until a concrete UI actually needs free-form layout.
  4. Skip B (immediate-mode) unless we want a debug/dev overlay. Its per-tick chattiness and click latency don't earn their keep for player-facing UI.

Everything here rides one shared idea from Set 12.2: a declared control (a name + a default key, rebindable and conflict-checked in the settings menu) whose presses route to the server. A menu is just a control that opens a template; a button is a control fired from inside one. Build that spine and all four directions become the same plumbing wearing different clothes.

Meme Wars · plugin UI framework — design directions for review. Companion to the Plugin Developer Guide and the Field Codex. Nothing here is built yet; this is the menu we order from.