This page covers scripting in the Shattered Archive game client (web and mobile). Still on MUSHclient? See the MUSHclient scripting page (legacy). If you have a specific request, please submit site-feedback.

Scripting in the Game Client

Introduction

The Shattered Archive game client lets you tell it when THIS happens, do THAT: eat when you are hungry, turn a short command into a long one, send a command every few seconds. Nothing to install and no files to manage: scripts live in the client, run in your browser or on your phone, and can be saved to your account.

There are three kinds of script, and a shared library that all of them can use:

  • Triggers run when a line of game text matches
  • Aliases run when you type a short command
  • Timers run again and again on an interval
  • Global scripts are shared functions your scripts call, in any language, from any language

Each script is written in Commands (plain text), JavaScript, TypeScript, Lua or Python. You do not need to be a programmer: a Commands script is just a list of things to type.

Quick Start

In the web client open Game > Script Sandbox…. On the phone, open the Scripts tab. The sandbox has a tab each for Triggers, Aliases, Timers, Globals and Variables.

1. A trigger that eats when you are hungry

  • Triggers tab, add a trigger
  • Match text: You are hungry
  • Language: Commands, and type eat bread as the script

2. An alias that attacks a target

  • Aliases tab, add an alias
  • Alias: kk {TARGET}
  • Language: Commands, script kill {TARGET}
  • Type kk goblin in the command bar and the client sends kill goblin

3. A timer

  • Timers tab, add a timer with an interval of 30000 (milliseconds)
  • Script look and it sends that every 30 seconds
  • Untick the script's checkbox to switch it off. The script stays in your list.

Languages

LanguageUse it for
CommandsOne game command per line. A line that is only a global script's name (global.lua.heal) runs that global instead of being sent.
JavaScriptThe general-purpose choice. Runs natively in the browser.
TypeScriptJavaScript with types. Types are removed before running; they are not checked.
LuaIf you prefer Lua (version 5.3 semantics, so 5 and 5.0 are different numbers).
PythonIf you prefer Python (core language and the standard library Skulpt ships).

All five run on the web client and on mobile, so a script written on one works on the other.

Triggers, Aliases and Timers

Triggers

A trigger listens for an event (normally the raw text coming from the game) and runs its script when the line contains the text you gave it. A trigger can also hide the matching line from your screen (omit from output), or fire on every line without any match text.

The matched line is in event.payload.text, the event's name in event.name. Match text can contain a variable, You hit {target}, which is replaced by that variable's current value every time.

Aliases

An alias catches what you type before it reaches the game. Put {NAME} in the alias to capture part of what you typed: kk {TARGET} matches kk goblin and gives the script TARGET = goblin. The last capture takes the rest of the line. In JavaScript and TypeScript the capture is a ready-made constant (TARGET); in every language you can read it with getNamedVar("TARGET").

Several commands can be typed in one go with the command separator (; by default), and each part is checked against your aliases. doAfter(5000, world, "jump") can be typed anywhere in such a chain to send a command later.

Timers

A timer runs its script every N milliseconds. The first run is one full interval after the timer is switched on. A timer that is disabled does nothing but stays in your list.

What a Script Can Do

Every language gets the same calls, spelled the same way. In JavaScript, TypeScript, Lua and Python they are available by name (sendCommand("north")); JavaScript and TypeScript can also reach them through the api object (api.sendCommand("north")).

CallDoes
sendCommand(cmd)Send a command to the game
writeTerminal(dsl)Write coloured text to your own screen only (the game never sees it). Colour codes: {r {g {y {b {m {c {w, capitals for bright, {x to reset, {{ for a literal {.
log(...) / error(...)Write to the script log
getGlobalVar(key) / setGlobalVar(key, value) / deleteGlobalVar(key)Saved key/value storage, shared by every language
getNamedVar(name)The value of a named variable or an alias capture
doAfter(ms, "world" or "alias", cmd)Do something after a delay
runGlobal(id, args, callback)Call a global script function, in any language. See below.
httpGetJson(url)Fetch JSON from a public web address. Returns it in JavaScript and TypeScript; in Lua and Python it only logs the result.
eventWhat triggered this script: event.name and event.payload. Nothing for most timers.

Variables

There are two kinds, and they are not the same thing.

  • Named variables (the Variables tab) are text values such as TARGET = orc guard. Use {TARGET} in a trigger's match text or an alias, or read one with getNamedVar("TARGET").
  • Global variables are saved values any script can read and write with getGlobalVar / setGlobalVar: counters, the last target, small settings. A value set in one language can be read in another. Lua and Python get anything that is not already text back as JSON text.

Global Scripts

The Globals tab holds one file per language (JavaScript, TypeScript, Lua, Python). Put the functions you use in many places there instead of copying them into a dozen scripts.

  • JavaScript / TypeScript: assign to exports: exports.heal = (api, args) => api.sendCommand("quaff " + args.potion);. A plain function heal() {} is local to the file and is not callable.
  • Lua: a top-level function. A dotted name maps to underscores: global.lua.combat.flee calls combat_flee(argsJson). A Lua global receives its arguments as a JSON string.
  • Python: a top-level function: def combat_flee(args):. Dotted names map to underscores the same way.

A global is named global.<language>.<path>: global.javascript.heal, global.lua.combat.flee.

One Engine per Language

Lua and Python each run as one program for you, not one per script. The Globals file is run first, then every Lua (or Python) trigger, alias and timer runs in that same engine. So:

  • A function or variable defined in Globals, or in any script, can be used directly by every other script in that language: bump(), counter. No runGlobal needed.
  • A variable a Lua script sets without local (kills = (kills or 0) + 1) is still there for the next script. Use local for anything private to one run. In Python, a name set at the top level persists the same way.
  • Saving the Globals file for a language restarts that language's engine: everything it held starts over. Editing a trigger, alias or timer does not.
  • JavaScript and TypeScript scripts are separate from one another (each has its own scope) but share the Globals files by name; see below.

Calling a Global from Any Language

A global written in any language can be called from a script in any language, with the same name, the same arguments and the same kind of result. Arguments and results are plain data: text, numbers, true/false, nothing, lists and dictionaries. A function, a class instance or a value that contains itself comes back as nothing. A Lua table with the keys 1..n is a list; any other table is a dictionary. Whole numbers reach Lua as integers.

You writeYou callYou get
JavaScript / TypeScriptcore.echo({ text }) for a JS/TS global, or await runGlobal("global.lua.x.y", args)The value, or a promise of it
Lualocal r = runGlobal("global.python.x.y", {n = 1})The value if the global finished at once, otherwise nil
Pythonr = runGlobal("global.lua.x.y", {"n": 1})The value if the global finished at once, otherwise None
Lua or Python, a global that takes timerunGlobal(id, args, callback)callback(result, error), always, after your script has finished

"Finished at once" means before runGlobal returned: any Lua function, any JavaScript or TypeScript function that is not async, and any Python function that does not sleep or wait. For the others (an async function, a Python function that calls time.sleep) use the callback.

Scripts that call runGlobal("global.lua.heal") on a line of its own, without using the result, work as they always did. A Commands script can still run a global by putting its name alone on a line.

JavaScript and TypeScript

Everything the JavaScript and TypeScript Globals files export is available in every JavaScript and TypeScript script by name, so you just call it. The calling rule below is applied for you.

// Globals -> JavaScript
exports.state = { kills: 0 };
exports.double = (args) => args.n * 2;
exports.core = {
  hit(api, args) { api.sendCommand("kill " + args.target); return ++exports.state.kills; },
  later: (args) => new Promise((done) => setTimeout(() => done(args.n + 1), 500)),
};

// Any JavaScript or TypeScript trigger, alias or timer
sendCommand("say " + double({ n: 21 }));        // 42
const total = core.hit({ target: "goblin" });   // the number it returned
const n = await core.later({ n: 41 });          // top-level await works
state.kills = 0;                                // the Globals file's own object
  • A global function taking two or more parameters receives (api, args); one taking one parameter receives just args.
  • What the function returns comes back untouched: a value from an ordinary function, a promise from an async one.
  • Objects and lists are shared, not copied, so changing state.kills changes the Globals file's own state. A number or text export is its value at the moment the script starts.
  • A script may still declare its own const core = .... If both Globals files export the same name, the one for the script's own language wins. Names the script already has (log, sendCommand, event), reserved words and names that are not valid identifiers are not bound.
  • Scripts may use await at the top level. A script that never awaits runs to the end in the same moment it was triggered, exactly as before.
  • On mobile, await in a script works where the phone's JavaScript engine supports it; otherwise use runGlobal(...).then(...).

Lua and Python

-- Lua calling a JavaScript, a TypeScript and a Python global
local twice = runGlobal("global.javascript.double", {n = 21})       -- 42
local info  = runGlobal("global.python.combat.info", {who = "orc"}) -- a table
writeTerminal(info.name .. " " .. info.hp .. "\n")

-- a global that takes time: use a callback
runGlobal("global.javascript.core.later", {n = 41}, function(result, err)
  if err then log("failed: " .. err) else writeTerminal(result .. "\n") end
end)
# Python calling a Lua and a JavaScript global
total = runGlobal("global.lua.combat.total", {"hits": 3})        # a number
twice = runGlobal("global.javascript.double", {"n": 21})         # 42

def got(result, err):
    writeTerminal("%s\n" % (err or result))

runGlobal("global.javascript.core.later", {"n": 41}, got)
  • The callback always runs after your script has finished, as callback(result, error). error is nothing when it worked, and result is nothing when it failed.
  • A global that fails is reported in the script log and gives nil / None.
  • A Lua callback is dropped if the Lua engine was restarted (you saved the Lua Globals file) before the global finished.
  • A global's own arguments: a Lua global receives a JSON string (function combat_flee(argsJson)); a Python global receives the value (def combat_flee(args)), a dictionary if you passed one.

Examples

The same alias in every language

Alias kk {TARGET} sends kill <target> and tells you about it.

// JavaScript or TypeScript
sendCommand("kill " + TARGET);
writeTerminal("{rAttacking{x " + TARGET + "\n");
-- Lua
local t = getNamedVar("TARGET")
sendCommand("kill " .. t)
writeTerminal("{rAttacking{x " .. t .. "\n")
# Python
t = getNamedVar("TARGET")
sendCommand("kill " + t)
writeTerminal("{rAttacking{x %s\n" % t)

A trigger that reads the line

-- Trigger on "tells you", Lua
local line = event.payload.text or ""
writeTerminal("{c" .. line .. "{x\n")

A timer that counts, sharing state with a trigger (Lua)

-- Timer, every 1000 ms
ticks = (ticks or 0) + 1
writeTerminal("tick " .. ticks .. "\n")

-- Trigger on "status", in the same engine, sees the same variable
writeTerminal("ticks so far: " .. (ticks or 0) .. "\n")

One library, used from every language

# Globals -> Python
def combat_info(args):
    return {"name": args["who"].upper(), "hp": len(args["who"]) * 10}
// A JavaScript trigger
const info = await runGlobal("global.python.combat.info", { who: "orc" });
writeTerminal(info.name + " " + info.hp + "\n");   // ORC 30

Saving, Syncing and Sharing

  • Scripts, global scripts and variables are saved on the device you are using. Nothing needs an account.
  • With a Shattered Archive account you can Save to cloud and Load from cloud (web: File > Account…, mobile: Settings > Account). Load shows what it will replace and asks first, and an empty cloud copy never wipes what is on your device.
  • The web client and the phone app use the same account storage, so scripts you save on one can be loaded on the other.
  • The Script Sandbox can export your scripts to a file and import them again, which is how you share them with other players.

Treat a script from someone else as a program: read it before you run it.

What Scripts Cannot Do

  • No files, no shell, no interactive prompts (Lua's io.open and os.execute, Python file input/output).
  • On mobile, httpGetJson only reaches public web addresses; local network addresses are refused.
  • A script that never ends (while true do end) freezes the client, as it would anywhere. Use a timer instead of a loop.
  • Plugin-bundled scripts run on their own and cannot see your scripts or Globals.

MUSHclient (Legacy)

Before the game client, many players scripted in MUSHclient with JScript and a TypeScript build step. That guide is kept, unchanged, on its own page: MUSHclient scripting (legacy). Scripts written for MUSHclient use world.Send and friends and do not run in the game client as they are; the ideas carry over (an alias is an alias), the calls are the ones in this page.