Scripting Guide
UrsaMU scripts are TypeScript/JavaScript files that run inside Web Workers — completely isolated from the host process. Every script receives a typed SDK object called u that provides everything a script needs to interact with the game world.
How It Works
When a player runs a command (or @trigger fires an attribute), the Sandbox Service:
- Spawns a Web Worker
- Injects the
uSDK object as a global - Runs the script (ESM module or legacy block)
- Worker posts results/messages back to the server
- Worker terminates
Scripts are fully isolated — they cannot crash the server, access the filesystem, or make network requests. All interaction happens through u.
The u Object
Every script has access to a global u object. It contains both the current execution context and all SDK methods.
Context Properties
u.me // IDBObj — the actor (player or object running the script)
u.here // IDBObj — the room the actor is in; also has .broadcast()
u.target // IDBObj | undefined — an optional target object
u.cmd // { name, args, switches } — the command that triggered this
u.state // Record<string, unknown> — reactive per-execution state
u.socketId // string | undefined — socket ID of the triggering connection
IDBObj shape
{
id: string;
name?: string;
location?: string;
flags: Set<string>; // e.g. new Set(["player", "connected"])
state: Record<string, unknown>; // player/object data (desc, attrs, etc.)
contents: IDBObj[];
}
u.cmd shape
u.cmd.name // "look", "score", "bbpost", etc.
u.cmd.original // raw command string typed by the player
u.cmd.args // string[] — [rawArgString, ...splitArgs]
u.cmd.switches // string[] | undefined — ["edit"] from "@bbpost/edit"
Messaging
// Send to the triggering player only
u.send("You look around the room.");
// Send to a specific player by ID
u.send("You have new mail.", targetPlayerId);
// Send with options (e.g. quit)
u.send("Goodbye!", undefined, { quit: true });
// Broadcast to ALL connected players (use sparingly)
u.broadcast("The server will reboot in 5 minutes.");
// Broadcast to all players in the current room
u.here.broadcast("Alice waves cheerfully.");
// Broadcast to all in target's room
u.target?.broadcast("Someone enters the room.");
Database (u.db)
// Search by query object (returns SDK IDBObj array)
const players = await u.db.search({ flags: /player/i });
const here = await u.db.search({ id: u.me.location });
const byName = await u.db.search({ "data.name": /alice/i });
// Create a new object
const coin = await u.db.create({
flags: new Set(["thing"]),
location: u.me.id, // put it in actor's inventory
state: { name: "Gold Coin", description: "A shiny coin." }
});
// Modify an object (always spread state to avoid clobbering)
await u.db.modify(u.me.id, "$set", {
data: { ...u.me.state, score: (u.me.state.score as number || 0) + 10 }
});
// Destroy an object
await u.db.destroy(coin.id);
Important: always spread existing state when modifying:
{ data: { ...u.me.state, myField: value } }— omitting the spread
will wipe all other fields.
Finding Objects (u.util.target)
// Find by name, ID, alias, "me", "here", etc.
const target = await u.util.target(u.me, u.cmd.args[0]);
if (!target) {
u.send("I can't find that.");
return;
}
Flags & Permissions
// Check flags
u.me.flags.has("wizard") // true/false
u.me.flags.has("connected")
// Check edit permission (owner, admin, wizard)
if (!(await u.canEdit(u.me, target))) {
u.send("Permission denied.");
return;
}
// Set or remove flags
await u.setFlags(target.id, "dark"); // add flag
await u.setFlags(target.id, "!dark"); // remove flag
await u.setFlags(target.id, "wizard"); // grant wizard
Locks
// Evaluate a stored lock expression
const canPass = await u.checkLock(target, "player+");
Movement
// Move any object to a destination
u.teleport(objectId, destinationRoomId);
Executing Commands
// Run a command as the actor (fires the full command pipeline)
u.execute("look");
u.execute("say Hello, world!");
// Force a command (bypasses some permission checks — use carefully)
u.force("@set me=dark");
Attributes (u.attr)
// Read a soft-coded &ATTR from any object (case-insensitive)
const bio = await u.attr.get(u.me.id, "FINGER-INFO"); // null if not set
const desc = await u.attr.get(objectId, "SHORT-DESC");
if (bio) u.send(bio);
Evaluate Attribute (u.eval)
// Run &ATTR on an object and capture its output as a string
const result = await u.eval("#42", "SCORE-FORMULA");
u.send(`Result: ${result}`);
// Pass arguments
const out = await u.eval(u.me.id, "CALC", ["10", "20"]);
Force As (u.forceAs)
// Execute a command as another object (wizard/admin only — check flags first)
if (!u.me.flags.has("wizard") && !u.me.flags.has("admin")) {
u.send("Permission denied."); return;
}
await u.forceAs(npcId, "say Greetings, traveler!");
Text (u.text)
Server-side named text blobs — used for MOTD, help files, etc.
// Read a stored text entry
const motd = await u.text.read("motd");
// Write/update a text entry (admin scripts)
await u.text.set("motd", "Welcome to the game! Events tonight at 8pm.");
Bulletin Boards (u.bb)
// List all boards (includes unread counts for current player)
const boards = await u.bb.listBoards();
// List posts on a board
const posts = await u.bb.listPosts("announcements");
// Read a specific post
const post = await u.bb.readPost("announcements", 1);
if (post) u.send(`${post.subject}\n${post.body}`);
// Post to a board
await u.bb.post("announcements", "Server Update", "Maintenance tonight at midnight.");
// Mark a board as read
await u.bb.markRead("announcements");
// Get unread count totals
const newCount = await u.bb.totalNewCount();
Mail (u.mail)
// Send a message
await u.mail.send({
id: crypto.randomUUID(),
from: `#${u.me.id}`,
to: [`#${recipientId}`],
subject: "Hello",
message: "Nice to meet you!",
read: false,
date: Date.now()
});
// Read mail (query by recipient ID)
const inbox = await u.mail.read({ to: { $in: [`#${u.me.id}`] } });
// Delete a message
await u.mail.delete(messageId);
Channels (u.chan)
// Join a channel
await u.chan.join("public", "pub");
// Leave by alias
await u.chan.leave("pub");
// List all channels
const channels = await u.chan.list();
// Admin: create a channel
await u.chan.create("events", { header: "[EVENTS]", hidden: false });
// Admin: destroy a channel
await u.chan.destroy("events");
// Admin: change channel properties
await u.chan.set("public", { header: "[PUB]", lock: "player+", masking: false });
Attribute Triggers (u.trigger)
Fire a stored &ATTR on any object as a script:
// Fire &OPEN on the target object, passing args
await u.trigger(target.id, "OPEN", ["force"]);
// Fire &ACONNECT on the player
await u.trigger(u.me.id, "ACONNECT");
Authentication (u.auth)
Primarily used in connect.ts and registration scripts:
// Verify login credentials
const ok = await u.auth.verify(name, password);
// Complete login (sets connected flag, joins socket rooms)
await u.auth.login(playerId);
// Hash a password for storage
const hashed = await u.auth.hash("mysecretpassword");
// Change a player's password
await u.auth.setPassword(playerId, "newpassword");
System (u.sys)
Admin/wizard-only controls:
// Set a config key at runtime
await u.sys.setConfig("game.masterRoom", "42");
// Disconnect a socket by socket ID
await u.sys.disconnect(socketId);
// Get server uptime in milliseconds
const ms = await u.sys.uptime();
const minutes = Math.floor(ms / 60000);
// Reboot or shut down
await u.sys.reboot(); // exits with code 75 — daemon restart loop restarts the server
await u.sys.shutdown(); // exits with code 0 — clean stop
// Pull latest code from git and restart
await u.sys.update(); // pull origin/main
await u.sys.update("develop"); // pull a specific branch
Exit code 75 is caught by scripts/main-loop.sh which restarts the process
with exponential backoff on rapid exits. Telnet connections survive reboots.
Game Time (u.sys.gameTime)
// Read the in-game calendar
const t = await u.sys.gameTime();
// { year: number, month: 1-12, day: 1-28, hour: 0-23, minute: 0-59 }
// Set the in-game calendar (wizard/admin only)
await u.sys.setGameTime({ year: 1340, month: 6, day: 15, hour: 8, minute: 0 });
Channel History (u.chan.history)
// Get recent messages from a channel (default last 20)
const history = await u.chan.history("public");
const history = await u.chan.history("public", 50);
// → [{ id, playerName, message, timestamp }, ...]
Events (u.events)
Pub/sub events across the server:
// Emit an event
await u.events.emit("player.levelup", { id: u.me.id, newLevel: 5 });
// Subscribe a handler attribute (the handler receives the event data)
const subId = await u.events.on("player.levelup", "LEVELUP_HANDLER");
Formatting Utilities (u.util)
// Display name (uses moniker if set, falls back to name)
const name = u.util.displayName(u.me, u.me);
// Text alignment
u.util.ljust("Left", 20) // "Left "
u.util.rjust("Right", 20) // " Right"
u.util.center("Center", 20, "-") // "-------Center-------"
// printf-style sprintf
u.util.sprintf("%-10s %5d", "Score", 1200)
// Column template (multi-row data)
u.util.template(
"[NNN] [TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT]",
{ N: "42", T: "Some long title here" }
)
// Strip MUSH color codes and ANSI escapes (for storage/validation)
u.util.stripSubs("%chHello %cgWorld%cn") // → "Hello World"
Structured UI (u.ui)
For web client panels (JSON output, not text):
// Layout — posts a structured result to the web client
u.ui.layout({
components: [
{ type: "header", title: "Character Sheet" },
{ type: "table", content: [["Name", u.util.displayName(u.me, u.me)]] }
]
});
// Render a template string
const html = u.ui.render("<b>{{name}}</b>", { name: "Alice" });
Writing a Script
Scripts live in system/scripts/. The file name becomes the command name (minus any @ prefix).
ESM Style (recommended)
// system/scripts/greet.ts
import { IUrsamuSDK } from "../../src/@types/UrsamuSDK.ts";
export default async (u: IUrsamuSDK) => {
const targetName = (u.cmd.args[0] || "").trim();
if (!targetName) {
u.send("Usage: greet <player>");
return;
}
const target = await u.util.target(u.me, targetName);
if (!target) {
u.send(`I can't find '${targetName}'.`);
return;
}
const myName = u.util.displayName(u.me, u.me);
u.send(`You wave to ${u.util.displayName(target, u.me)}.`);
u.send(`${myName} waves to you.`, target.id);
u.here.broadcast(`${myName} waves to ${u.util.displayName(target, u.me)}.`);
};
Players run it as greet <name> or @greet <name>.
Legacy Block Style
Files without export run as a plain async block. The u global is injected directly:
// system/scripts/gold.ts
const gold = (u.me.state.gold as number) || 0;
u.send(`You have ${gold} gold coins.`);
Aliases
Export a const aliases array to register additional trigger names:
export const aliases = ["greet", "wave"];
Switches
Commands like @bbpost/edit or @motd/set pass the portion after / as u.cmd.switches:
// u.cmd.switches = ["edit"] when player types @mycommand/edit
const switches = u.cmd.switches || [];
if (switches.includes("edit")) {
// handle edit mode
}
Security Model
| Capability | Available? |
|---|---|
| File system access | No |
Network requests (fetch) |
No |
Deno.* API |
No |
Database (via u.db) |
Yes |
Messaging (via u.send, u.broadcast) |
Yes |
Channel ops (via u.chan) |
Yes |
Mail (via u.mail) |
Yes |
Bulletin boards (via u.bb) |
Yes |
System commands (via u.sys) |
Wizard/admin only — enforced by scripts |
| ESM imports within same worker | Yes |
| JSR sub-path imports | Limited (import maps don’t resolve in workers) |
Scripts run inside Worker with /// <reference no-default-lib="true" />, which blocks Deno, fetch, XMLHttpRequest, and the filesystem.
MUSH Color Codes
UrsaMU processes traditional MUSH substitution codes in all outgoing text:
| Code | Effect |
|---|---|
%ch |
Bold / bright |
%cn |
Reset to normal |
%cr |
Red |
%cg |
Green |
%cb |
Blue |
%cy |
Yellow |
%cw |
White |
%cc |
Cyan |
%cm |
Magenta |
%n |
Actor’s name |
%r / %R |
Newline |
%t |
Tab |
%b |
Space |
u.send("%ch%cyWelcome to Arenthia!%cn");
u.send(`%ch%cr${u.util.ljust("ALERT", 10)}%cn Server restarting in 5 minutes.`);
Use u.util.stripSubs(text) to remove all codes before storing or comparing strings.