Core API Reference

Everything documented here is exported from jsr:@ursamu/mush.

Package Structure

UrsaMU ships three packages on JSR:

Package Purpose
jsr:@ursamu/mush Full MUSH engine API — the preferred import for plugins and game projects
jsr:@ursamu/core Raw server infrastructure only (transports, pipeline, sessions) — no MUSH concepts; use this when building a custom game engine on top of the same infrastructure
jsr:@ursamu/mush Backwards-compatibility shim — re-exports everything from @ursamu/mush; existing plugins using this import continue to work unchanged

@ursamu/core exports

@ursamu/core provides the low-level primitives that @ursamu/mush builds on:

import {
  createServer,
  websocketTransport, telnetTransport, httpTransport,
  registerFallback, addHandler, runPipeline,
  sessions, registerSender,
} from "jsr:@ursamu/core";

Use @ursamu/core directly only when you are building a non-MUSH server
(custom protocol, alternative command syntax, etc.) on the UrsaMU
infrastructure. For standard MUSH game development import from
jsr:@ursamu/mush.

Contents


Imports

import {
  mu, startTelnetServer, createObj, checkAndCreateSuperuser,
  addCmd, cmds, registerScript, registerCmdMiddleware,
  DBO, dbojs, send, joinSocketToRoom, wsService,
  gameHooks,
  registerLockFunc, evaluateLock, validateLock,
  registerFormatHandler, registerFormatTemplate, unregisterFormatHandler,
  resolveFormat, resolveFormatOr, resolveGlobalFormat, resolveGlobalFormatOr,
  header, divider, footer,
  registerSoftcodeFunc, registerSoftcodeSub, softcodeService,
  registerPluginRoute, registerUIComponent, unregisterUIComponent, getRegisteredUIComponents,
  registerStatSystem, getStatSystem, getDefaultStatSystem, getStatSystemNames,
  buildContext, PluginConfigManager,
} from "jsr:@ursamu/mush";

import type {
  IUrsamuSDK, IDBObj, IDBOBJ, ICmd, IPlugin, IPluginDependency,
  IContext, IMiddlewareFunction, IStatSystem, IUIComponent,
  GameHookMap, SessionEvent, SayEvent, PoseEvent, PageEvent, MoveEvent,
  ChannelMessageEvent, ObjectCreatedEvent, ObjectDestroyedEvent, ObjectModifiedEvent,
  SceneCreatedEvent, ScenePoseEvent, SceneSetEvent, SceneTitleEvent, SceneClearEvent,
  MailReceivedEvent, FormatHandler, FormatSlot, GameContext, UserSocket,
  LockFunc, SoftcodeFn, SoftcodeSubHandler, SoftcodeContext,
} from "jsr:@ursamu/mush";

Engine entry points

mu(config?)

Boots the engine. Call once from src/main.ts.

import { mu } from "jsr:@ursamu/mush";
await mu();

startTelnetServer(port?)

Spawn a standalone Telnet listener that talks to the same hub.

createObj(template)

Create a DB object outside of a command handler (migrations, seeders).

const room = await createObj({
  name: "The Void",
  flags: new Set(["room"]),
  state: { desc: "An empty room." },
  contents: [],
});

checkAndCreateSuperuser()

Idempotent — ensures #1 exists with the superuser flag.


Commands

addCmd(...cmds)

Registers one or more ICmd objects. Safe at module load.

addCmd({
  name: "+greet",
  pattern: /^\+greet\s+(.+)/i,
  lock: "connected",
  category: "Social",
  help: "+greet <name> — Say hello.",
  exec: (u) => u.send(`Hello, ${u.cmd.args[0]}!`),
});

ICmd shape:

interface ICmd {
  name: string;
  pattern: string | RegExp;
  lock?: string;
  category?: string;
  help?: string;
  hidden?: boolean;
  exec: (u: IUrsamuSDK) => void | Promise<void>;
}

Capture groups in pattern populate u.cmd.args[0], [1], etc.

cmds

The registered command map. Use to introspect or override an existing
command:

import { cmds } from "jsr:@ursamu/mush";
const existing = cmds.get("look");

registerScript(name, content)

Register a softcode/system script. Lookup order: local file override →
plugin registry → engine bundled.

registerScript("custom-look", "say You ran the custom look script.");

registerCmdMiddleware(fn)

Insert middleware into the command pipeline. Runs before exec. Return
false to abort dispatch.

registerCmdMiddleware(async (u) => {
  if (u.me.flags.has("frozen")) { u.send("You're frozen."); return false; }
});

Plugin SDK (IUrsamuSDK)

The u object passed to every addCmd exec and every sandbox script.

interface IUrsamuSDK {
  state: Record<string, unknown>;
  socketId?: string;
  me: IDBObj;
  here: IDBObj & { broadcast(msg: string): void };
  target?: IDBObj & { broadcast(msg: string): void };
  cmd: { name: string; original?: string; args: string[]; switches?: string[] };

  send(message: string, target?: string): void;
  broadcast(message: string): void;
  execute(command: string): Promise<void>;
  force(command: string): Promise<void>;
  forceAs(targetId: string, command: string): Promise<void>;
  canEdit(actor: IDBObj, target: IDBObj): Promise<boolean>;
  checkLock(target: IDBObj, lock: string): Promise<boolean>;
  setFlags(targetId: string, flags: string): Promise<void>;
  trigger(targetId: string, attr: string, args?: string[]): Promise<void>;
  eval(targetId: string, attr: string, args?: string[]): Promise<string>;
  evalString(source: string): Promise<string>;
  intercept?: (input: string) => Promise<boolean>;

  db:    { search, create, modify, destroy };
  util:  { target, displayName, stripSubs, center, ljust, rjust, sprintf,
           template, parseDesc, resolveFormat, resolveFormatOr,
           resolveGlobalFormat, resolveGlobalFormatOr };
  auth:  { verify, login, hash, setPassword };
  sys:   { setConfig, disconnect, reboot, shutdown, uptime, update,
           gameTime, setGameTime };
  chan:  { join, leave, list, create, destroy, set, history };
  attr:  { get, set, clear };
  events:{ emit, on };
  ui:    { panel, render, layout };
}

u.db

All async. op must be "$set", "$inc", "$unset", or "$push".

const list = await u.db.search({ flags: ["room"] });
const sword = await u.db.create({ name: "Sword", flags: new Set(["thing"]),
  location: u.me.id, state: {}, contents: [] });
await u.db.modify(u.me.id, "$set", { "data.gold": 100 });
await u.db.modify(u.me.id, "$inc", { "data.deaths": 1 });
await u.db.modify(u.me.id, "$unset", { "data.tempFlag": "" });
await u.db.modify(u.here.id, "$push", { "data.log": "Alice arrived." });
await u.db.destroy(sword.id);

u.util

const obj = await u.util.target(u.me, u.cmd.args[0], true);
const name = u.util.displayName(target, u.me);
const plain = u.util.stripSubs("%chBold%cn");
u.send(u.util.center("TITLE", 78, "="));
u.send(u.util.ljust("Name", 20) + u.util.rjust("100", 10));
u.send(u.util.sprintf("%-20s %5d gp", player.name!, gold));
const text = await u.util.resolveFormat(u.me, "NAMEFORMAT", defaultName);

u.auth / u.sys / u.chan / u.attr / u.events

await u.auth.verify(u.me.name!, "pw");
await u.auth.setPassword(u.me.id, "newpw");

await u.sys.setConfig("server.name", "My Game");
const t = await u.sys.gameTime();

await u.chan.join("Public", "pub");
await u.chan.create("Staff", { header: "%ch[STAFF]%cn", hidden: true });

const bio = await u.attr.get(u.me.id, "FINGER-INFO");
await u.attr.set(u.me.id, "BIO", "Tall and lanky.");

await u.events.emit("game:levelup", { id: u.me.id, lvl: 5 });

u.eval / u.evalString / u.trigger

const score = await u.eval(u.me.id, "SCORE-FORMULA");
const rendered = await u.evalString("[name(me)] is here.");
await u.trigger(u.here.id, "ONENTER", [u.me.id]);

u.force / u.forceAs / u.execute

await u.force("look");
await u.forceAs(npcId, "say Welcome.");
await u.execute("@pemit #3=Server message.");

u.forceAs is privileged — guard with u.me.flags.has("wizard") etc.


Database

DBO<T>

Generic database collection. Use for plugin-scoped storage. Always prefix
the namespace with your plugin name.

By default, UrsaMU uses TypeGraph (built on Postgres/PGlite) to store document records inside a local or in-memory PostgreSQL database, with a fallback DenoKvAdapter available.

  • Environment Variable: URSAMU_TYPEGRAPH_DB can be set to customize the database storage file/directory (defaults to ${Deno.cwd()}/data/typegraph.db for production/development, and memory:// during test runs).
  • Methods: Supports standard query and modification operators.
import { DBO } from "jsr:@ursamu/mush";

const scores = new DBO<{ player: string; score: number }>("myplugin.scores");
await scores.create({ player: "Alice", score: 100 });
const all = await scores.all();
const top = await scores.queryOne({ player: "Alice" });
await scores.modify({ player: "Alice" }, "$inc", { score: 1 });
await scores.modify({ player: "Alice" }, "$push", { history: Date.now() });

Ops: $set, $inc, $unset, $push (atomic CAS append).

dbojs

The shared game-object collection (IDBOBJ).

import { dbojs } from "jsr:@ursamu/mush";

const players = await dbojs.find({ flags: { $in: ["player"] } });
const room = await dbojs.queryOne({ id: "1" });

Locks & permissions

Lock strings combine lockfuncs with the operators &&, ||, !, and
(). Legacy & / | still work. Max length 4096 chars / 256 tokens.

Built-in lockfuncs:

Func Example Passes when
flag(name) flag(wizard) enactor has the flag
attr(name) attr(tribe) enactor.state has own property name
attr(name, val) attr(tribe, glasswalker) state[name] === val
type(name) type(player) enactor has the type flag
is(#id) is(#5) enactor.id === "5"
holds(#id) holds(#12) enactor.contents includes #12
perm(level) perm(admin) passes privilege ladder check

registerLockFunc(name, fn)

Register a custom lockfunc. Built-in names are protected.

import { registerLockFunc } from "jsr:@ursamu/mush";

registerLockFunc("tribe", (enactor, _target, args) =>
  String(enactor.state.tribe ?? "").toLowerCase() === args[0]?.toLowerCase()
);

// lock: "tribe(glasswalker) || perm(admin)"

evaluateLock(lock, enactor, target?)

Returns Promise<boolean>. Fail-closed on unknown funcs or parse errors.

validateLock(lock)

Throws on syntax error. Use to validate user-supplied lock strings.


Format handlers

Pluggable display formatters for engine and plugin output. Resolution
priority: per-object softcode attribute → registered TS handler →
registered MUSH template → built-in default.

FormatSlot is an open union (v2.3.4+). The eight engine-known slots
get IDE autocomplete:

NAMEFORMAT  DESCFORMAT  CONFORMAT   EXITFORMAT
WHOFORMAT   WHOROWFORMAT  PSFORMAT  PSROWFORMAT

Plugins may register any UPPERCASE slot (e.g. "MAILFORMAT",
"BBROWFORMAT") without casts.

registerFormatHandler(slot, fn)

registerFormatHandler("NAMEFORMAT", (u, target, defaultName) => {
  if (!target.flags.has("room")) return null;
  return `%ch%cy[${defaultName}]%cn\n`;
});

Return null to fall through. First non-null handler wins. Returns the
registered function so unregisterFormatHandler can remove it.

registerFormatTemplate(slot, mushSource) (v2.4.0)

Install a MUSH-softcode template. %0 binds to the default rendering.

const fn = registerFormatTemplate(
  "NAMEFORMAT",
  "[center(strcat(%cy[ ,%0, ]%cn),78,=)]",
);
// remove() ...
unregisterFormatHandler("NAMEFORMAT", fn);

unregisterFormatHandler(slot, fn)

Remove a handler by reference identity.

resolveFormat(target, slot, defaultArg)

Two-step lookup for target-bound formats: softcode attr on target
registered handler. Returns string | null.

resolveFormatOr(target, slot, defaultArg, fallback)

As above, but always returns a string.

resolveGlobalFormat(enactor, slot, defaultArg) (v2.3.3)

Two-tier lookup for global-list formats (WHO, @ps, +mail, +bb): #0
enactor. Returns string | null.

resolveGlobalFormatOr(enactor, slot, defaultArg, fallback)

Always returns a string.

header(title, width?) / divider(width?) / footer(width?)

Native TS layout helpers for block-style section rules. Distinct from the
softcode helpers of the same name.

u.send(header("Stats", 78));
u.send(divider(78));
u.send(footer(78));

Types

type FormatHandler = (
  u: IUrsamuSDK,
  target: IDBObj,
  defaultArg: string,
) => Promise<string | null> | string | null;

type FormatSlot = "NAMEFORMAT" | "DESCFORMAT" | "CONFORMAT" | "EXITFORMAT"
  | "WHOFORMAT" | "WHOROWFORMAT" | "PSFORMAT" | "PSROWFORMAT"
  | (string & {});

Softcode extension

registerSoftcodeFunc(name, fn)

Register a custom stdlib function callable from softcode as name(args).

import { registerSoftcodeFunc } from "jsr:@ursamu/mush";

registerSoftcodeFunc("double", (_ctx, args) =>
  String(Number(args[0]) * 2)
);

registerSoftcodeSub(char, handler)

Register a custom %X substitution.

registerSoftcodeSub("$", (ctx) => `[$${ctx.enactor.id}]`);

softcodeService

The evaluator instance. Exposed for plugin integration tests.

import { softcodeService } from "jsr:@ursamu/mush";
const out = await softcodeService.eval({ enactor, executor, source: "[add(2,3)]" });

Hooks & events

import { gameHooks } from "jsr:@ursamu/mush";
import type { SessionEvent, SayEvent } from "jsr:@ursamu/mush";

const onLogin = (e: SessionEvent) => console.log(`${e.player.name} logged in`);
gameHooks.on("player:login", onLogin);

// In plugin remove() — same function reference required:
gameHooks.off("player:login", onLogin);

GameHookMap event names:

player:login      player:logout
say               pose             page             move
channel:message
object:created    object:destroyed object:modified  object:moved
scene:created     scene:pose       scene:set
scene:title       scene:clear
mail:received

Each has a typed payload (SessionEvent, SayEvent, PoseEvent,
PageEvent, MoveEvent, ChannelMessageEvent, ObjectCreatedEvent,
ObjectDestroyedEvent, ObjectModifiedEvent, ObjectMovedEvent, SceneCreatedEvent,
ScenePoseEvent, SceneSetEvent, SceneTitleEvent, SceneClearEvent,
MailReceivedEvent).


REST & UI

registerPluginRoute(prefix, handler)

Attach a REST handler. Always return 401 before doing work when userId
is null on a protected route.

registerPluginRoute("/api/v1/my-plugin", async (req, userId) => {
  if (!userId) return Response.json({ error: "Unauthorized" }, { status: 401 });
  return Response.json({ ok: true, userId });
});

Handler signature: (req: Request, userId: string | null) => Promise<Response>.

registerUIComponent(component) / unregisterUIComponent(id) / getRegisteredUIComponents()

Expose a UI element via GET /api/v1/ui-manifest.

import type { IUIComponent } from "jsr:@ursamu/mush";

const comp: IUIComponent = {
  id: "myplugin.panel",
  type: "panel",
  title: "My Panel",
  url: "/plugins/myplugin/panel.html",
  requires: { flag: "builder" },
};
registerUIComponent(comp);

Stat systems

Plugins can register a pluggable stat/skill system, queryable by name.

import { registerStatSystem, getDefaultStatSystem } from "jsr:@ursamu/mush";
import type { IStatSystem } from "jsr:@ursamu/mush";

const system: IStatSystem = { name: "wod5", /* ...impl */ };
registerStatSystem(system, { default: true });

const def = getDefaultStatSystem();

Also: getStatSystem(name), getStatSystemNames(): string[].


WebSocket

wsService

Direct access to the WebSocket hub. Used by channel/notification plugins.

joinSocketToRoom(socketId, room)

Subscribe a connected socket to a broadcast room.

send(message, target?, options?)

Module-level broadcast — sends to a socket ID, room ID, or DB object ID.
Same semantics as u.send but importable from anywhere.

import { send } from "jsr:@ursamu/mush";
send("Server reboot in 5 minutes.", "#all");

notify(actorId, message, options?)

Deliver a message to a single online actor by id. Available as both
u.notify(actorId, msg) (Promise) on the SDK and a standalone
notify import for gameHooks handlers that don’t have a u. Returns
true if at least one live socket was found for the actor, false if the
actor is offline — callers can branch on the result to queue or drop.

Use this when you need to message a third party. u.send addresses the
caller (u.me); here.broadcast is room-scoped; notify is the right
tool when a plugin command boots another player from a seat, or a hook
handler wants to tell the moved actor why something just happened.

// In a plugin command:
const delivered = await u.notify(target.id, "You have been booted from your post.");
if (!delivered) {
  // queue a job / mark unread / fall back to email
}

// In a gameHooks handler (no `u` in scope):
import { notify, gameHooks } from "jsr:@ursamu/mush";
gameHooks.on("player:move", (e) => {
  notify(e.actorId, "Station unmanned — you left the post.");
});

// In a system script (sandboxed) — same shape, also returns a Promise<boolean>:
export default async (u) => {
  await u.notify(targetId, "Heads up.");
};

UserSocket is the typed socket metadata exported for plugin authors.


Engine context

GameContext is the substrate every command runs on (actor, location,
helpers). Most plugin code uses IUrsamuSDK instead — GameContext is
exposed for low-level integrations.

import { buildContext } from "jsr:@ursamu/mush";
const ctx = await buildContext({ socketId, actorId });

Plugin config

PluginConfigManager reads per-plugin scoped values from config.json.

import { PluginConfigManager } from "jsr:@ursamu/mush";

const cfg = new PluginConfigManager("myplugin");
const apiKey = cfg.get<string>("apiKey");
await cfg.set("lastRun", Date.now());

Stdlib (TS)

All functions are pure and deterministic. v2.5.1 promoted these from
softcode-only to TS-importable. v2.5.2 added per-instance Noise.

Noise

import {
  seedNoise, perlin1, perlin2, perlin3,
  simplex2, worley2, fbm2, ridged2, noiseGrid,
  Noise, createNoise, buildPerm,
} from "jsr:@ursamu/mush";

seedNoise(42);
const v = perlin2(0.5, 1.7);

const n = createNoise(123);
const v2 = n.perlin2(0.5, 1.7);
Function Purpose
seedNoise(seed) Reseed the singleton noise stream
perlin1/2/3 Classic Perlin noise (1D/2D/3D), range ~[-1, 1]
simplex2 2D simplex noise
worley2 2D Worley/cellular noise, returns distance to nearest feature
fbm2 Fractional Brownian Motion (octave-summed Perlin)
ridged2 Ridged multifractal noise
noiseGrid(w,h,fn) Sample a function over a grid → number[][]
buildPerm(seed) Build a permutation table for a custom noise impl
Noise class Per-instance independent noise stream
createNoise(seed) Construct a Noise

PRNG (Rng)

import { Rng, createRng } from "jsr:@ursamu/mush";

const r = createRng(123);
r.next();        // [0, 1)
r.int(1, 6);     // dice roll
r.pick(["a", "b", "c"]);

Per-instance mulberry32 — independent of the softcode RNG.

Physics

import { vreflect, pointInAabb, rayAabb } from "jsr:@ursamu/mush";
import type { Vec3 } from "jsr:@ursamu/mush";

const reflected = vreflect([1, 0, 0], [0, 1, 0]);
const inside = pointInAabb([5, 5, 5], [0, 0, 0], [10, 10, 10]);
const hit = rayAabb(origin, dir, min, max);

Spatial scalars

import {
  dist2d, dist3d, distSq2d, distSq3d,
  manhattan, chebyshev, angle2d, bearing,
} from "jsr:@ursamu/mush";

dist2d(0, 0, 3, 4);     // 5
manhattan(0, 0, 3, 4);  // 7
bearing(0, 0, 1, 1);    // radians

Interpolation

import { lerp, inverseLerp, remap, smoothstep, smootherstep, clamp }
  from "jsr:@ursamu/mush";

lerp(0, 100, 0.5);          // 50
inverseLerp(0, 100, 75);    // 0.75
remap(0, 100, 0, 1, 50);    // 0.5
smoothstep(0, 1, 0.5);      // 0.5 (Hermite curve)
clamp(150, 0, 100);         // 100

Vector ops

import { vsize, vsizeSq, vdistance, vdistanceSq, vlerp, vclamp }
  from "jsr:@ursamu/mush";
import type { Vec } from "jsr:@ursamu/mush";

const a: Vec = [3, 4];
vsize(a);              // 5
vdistance([0, 0], a);  // 5
vlerp([0, 0], a, 0.5); // [1.5, 2]

Types

Type Purpose
IUrsamuSDK The u object — full surface
IDBObj Hydrated game object (Set flags, populated contents)
IDBOBJ Raw DB record shape
ICmd Command registration
IPlugin Plugin module export
IPluginDependency Entry in ursamu.plugin.json deps
IContext Low-level command context (legacy)
IMiddlewareFunction (u: IUrsamuSDK) => boolean | Promise<boolean>
IStatSystem Pluggable stat system
IUIComponent UI manifest entry
GameHookMap Event-name → payload mapping
FormatHandler TS format-handler signature
FormatSlot Open uppercase-string union
GameContext Engine substrate
UserSocket WebSocket metadata
LockFunc Custom lockfunc signature
SoftcodeFn Custom stdlib function signature
SoftcodeSubHandler Custom %X handler signature
SoftcodeContext Evaluator context

Internal: plugin install errors

The v2.6.0 plugin installer throws typed errors on failure. They live in
src/utils/pluginErrors.ts and are not currently exported from
mod.ts
— they are used internally by the ursamu plugin install
flow. Listed here for diagnostic visibility.

PluginInstallError              base class
├─ PluginDepNameError           invalid/missing name in deps entry
├─ PluginDepUrlError            invalid/missing url
├─ PluginCloneError             git clone failed
├─ PluginRenameError            move into plugins/ failed
├─ PluginVersionError           installed plugin missing version manifest
├─ PluginSemverError            installed version doesn't satisfy dep range
└─ PluginConflictError          two deps disagree on resolved version

On any throw the installer’s InstallTxn rolls back every directory and
registry mutation made during the run.