← MegaMOO Guide

Text, Substitution & Colour

Writing one line that reads correctly to everybody who sees it.

Two layers, one sigil

Everything a player reads passes through two passes, in this order: substitution, which replaces &s and friends with names and pronouns, and then colour, which turns &r and &<245> into escape codes. Both use the same prefix character, &, so the letter after it is the only thing that decides which pass claims a token.

That sharing has one consequence worth knowing before you write anything. Substitution only fills a token when it was given something to fill it with — send a message with no indirect object and its &i survives untouched, reaches the colour pass, and is read as a colour code. This is why i and u are deliberately absent from the colour table: &i is the indirect object and &u the noun, and before they were removed an &i without an iobj inverted the rest of the line. Nothing was lost — &<underline> and &<reverse> spell those attributes unambiguously.

Why & and not %

% is Python's string-formatting operator, so "%<245>%s" % name raises ValueError — the most common formatting idiom in the language collided with the most common display idiom in the engine. Counted across 5,217 literal output strings in a real world, % appeared 1,132 times, | 14, and & zero. It is the Diku/Merc convention besides, so MUD authors arrive already knowing it.

Sending text: msg and msg_room

Two calls cover almost everything. Use them rather than the raw notify() builtin:

pobj.msg("You pick up &d.", dob=item) pobj.location.msg_room("&S picks up &d.", exclude=[pobj], sub=pobj, dob=item)

msg sends to one object. msg_room sends to every player in a location, and exclude is how you keep the actor from hearing the third-person version of their own action.

Both are native methods on every object, which matters for speed: as a method msg costs about 1.7µs against 168µs for the same thing written as a verb, and because msg_room calls msg once per listener, a forty-person room emit went from 6.7ms to 70µs when it stopped being a verb.

They are still overridable. Define a msg verb on any object — a deafened character, a filtered channel — and msg dispatches to it instead. The lookup happens per call, so a set_verb at runtime takes effect immediately. Every msg_room implementation deliberately delivers through msg rather than notify for exactly this reason.

Substitution runs once per recipient

msg_room walks the room and calls msg on each listener, so the tokens are resolved separately for every player. That is what makes viewer-aware emits possible, and it is also why &S capitalises correctly for everyone rather than being baked once and shared.

The substitution tokens

Three objects can be named in a message, plus a noun: the subject (who acted), the direct object (what they acted on), and the indirect object (what they used or whom they acted toward). Pass them as sub, dob and iob.

TokenFills withNeeds
&s / &Sthe subject's namesub=
&d / &Dthe direct object's namedob=
&i / &Ithe indirect object's nameiob=
&u / &Ua noun, not a full nameuob=
&pshe / she / they / itsub=
&pohim / her / them / itsub=
&pphis / her / their / itssub=
&pahis / hers / theirs / itssub=
&prhimself / herself / themself / itselfsub=

Capitalise by upper-casing the token's first letter. One rule, everywhere: &S, &D, &Ps, &Pp. There is no separate set of capitalised tokens to learn.

Pronouns come from the subject's gender property — male, female, neutral or ambiguous. Anything else, including an object with no gender at all, falls back to ambiguous: they/them/their.

Raw slots, and escaping

Numbers, coin words and any other plain string go in through numbered slots rather than as objects. The keyword keeps its s; the token drops it:

pobj.msg("You pay &1 for &d.", dob=item, s1="three silver")

The convention is worth following: objects go through &d/&i, strings through &1/&2. Slots are numbered from &0, and higher indices are replaced first so &1 can never clobber &10.

Two escaping rules matter, and they pull in opposite directions.

Names are protected for you. A name containing a sigil would otherwise be re-read by a later pass — an object called a &d inserted at the subject step had its &d replaced by the direct object's name at the next one. Names inserted through &s/&d/&i are neutralised automatically, so you never think about this.

Raw slots are not. Slot values are inserted verbatim and are never rescanned, which is deliberate — but it also means a slot is the right place for text you do not control, and the wrong place for text you want substituted. If a verb echoes something a player typed, double the sigil yourself:

spoken = args.replace('&', '&&') pobj.msg('You say, "%s"' % spoken)

That is what say and act do. Without it, anybody could put colour codes — or a stray &S naming somebody else — into a line attributed to them. A doubled && survives substitution intact so the colour pass still has its own half of the escape to consume.

Raw-display verbs

A verb that prints a property's literal value — @examine, a decompiler, anything showing you what is stored — has to double & on the way out, or a message template stored in a property will render instead of being displayed.

Viewer-aware emits

An emit has three audiences: the one who acted, the one it happened to, and everybody watching. A combat line needs all three —

You attack Bramble. Malifax attacks you. Malifax attacks Bramble.

— and writing that as three strings means three things to keep in step, with nothing checking that they agree. Two token families collapse it into one string. Because substitution runs once per recipient, each of them can ask who is reading this.

&y… is the subject, &t… the target, and both take the same five cases as the pronoun tokens:

TokenTo that personTo anyone else
&ysyouMalifax (the name)
&yoyouhim
&ypyourhis
&yayourshis
&yryourselfhimself
&ts &toyouBramble (the name)
&tpyourher
&tayourshers
&tryourselfherself

So the three lines above are one string:

here.msg_room("&Ys &v(attack) &to.", sub=pobj, dob=target)

Capitalisation is the same rule as everywhere else: &Ys, &Tp.

Two deliberate asymmetries

&ys renders the name in the third person, not "he". Subject position is where a line says who it is about, and "He smiles at Bramble" arriving cold has no antecedent. The other cases take pronouns because by then the name has been said.

For the same reason &to renders the name while &yo renders "him" — nothing has named the target yet at that point. &ts and &to are identical, since "you" is caseless; both exist so the two families are learned once.

Verb agreement

Once one string serves both "you" and "Malifax", the verb has to move with it — "you smile" but "Malifax smiles". Wrap the bare verb:

TokenAgrees with
&v(smile)the subject (sub)
&vd(dangle)the direct object (dob)

&vd exists because the two are not always the same. In "&D &vd(dangle) from &yp ear." the possessive belongs to the wearer and the verb to the earring, so sub stays the character while dob drives the agreement.

The bare form is used when any of these is true, and the -s form otherwise:

Spelling follows the ordinary rules: add -s, -es after s x z ch sh o, y becomes -ies after a consonant. Only be and have are table entries; do and go come out right from the -o rule.

Two things the article rule gets right on purpose

a pair of is singular — "a pair of boots is slung over your shoulder" — and an item with no article at all is singular too, because it is more often proper-named ("Excalibur gleams") than plural. Set plural where the article misleads.

Gender is not consulted. A they/them character takes the bare form behind the pronoun — "they smile" — but not behind their name, and "Robin smiles" is what English wants. A sentence built on &ps rather than &ys has to set plural or write its own agreement.

Both are available directly when a verb is assembling text rather than emitting it:

su.conjugate('smile') # smiles su.conjugate('smile', plural=True) # smile su.takes_plural_verb(obj, viewer) # True / False

psub1 and psub2

Where esub thinks in terms of subject and objects, psub1 and psub2 think in terms of an enactor and a target. They are the right tool for a message template stored on an object — a social, a weapon's hit line — where there is no verb call supplying sub= and dob=.

TokenFills withFunction
&N / &CNthe enactor's namepsub1
&EPS &EPO &EPP &EPRthe enactor's pronounspsub1
&CEPS &CEPO &CEPP &CEPRthe same, capitalisedpsub1
&T / &CTthe target's namepsub2
&OPS &OPO &OPP &OPRthe target's pronounspsub2
&COPS &COPO &COPP &COPRthe same, capitalisedpsub2
su.psub1("&CN draws &EPP sword.", eobj=player) su.psub2("&CN bows to &T.", eobj=player, tobj=other)

These families spell capitalisation with a C prefix rather than by upper-casing the first letter, which is the one place the two conventions differ. Replacement runs longest-token-first — reflexive, then possessive, then objective, then subjective — so &EPS cannot match inside a longer token.

psub1a and psub2a are the same with three positional strings, filling &1, &2 and &3.

The rest of su

su is available in every verb without importing anything. Most of it is the small English machinery that message-building needs.

CallDoes
su.listtoenglish([…])"a sword, a shield and a helm" from a list of strings
su.tlisttoenglish([…])the same from a list of objects, using their names
su.english_list(…)as above with the separators spelled out, and a word for the empty case
su.capitalise(s)upper-case the first character, leave the rest alone
su.a_or_an(word)"a" or "an", by what the word starts with
su.pluralise(word, count)naive English plural
su.ordinal(n)1 → "1st", 2 → "2nd"
su.listtomenu([…])a numbered menu
su.columnize([…])two side-by-side numbered columns
su.wrapstringlist([…], width)wrap each string and join with newlines
su.collapse(block)a pasted block — a sign, a menu board — as one newline-joined string
su.find_prefix(p, candidates)index of the one candidate p matches, else −1
su.index_delimited(s, target)index of a word within a delimited string, else −1
su.msg_list(text, targets)send one string to every object in a list
Use su.capitalise, never .capitalize()

Python's str.capitalize() lower-cases everything after the first letter, so "a LOOT sack" becomes "A loot sack". su.capitalise raises the first character and leaves the rest alone. su.capitalize is spelled both ways and does the same correct thing.

Colour

Single letters for the basic sixteen, lower-case for normal and upper-case for bright:

CodeColour
&x &r &g &y &b &m &c &wblack, red, green, yellow, blue, magenta, cyan, white
&X &R &G &Y &B &M &C &Wthe bright variants
&nreset — clears everything
&h &fbold, blink
&<245>xterm-256 index, foreground
&<dim>a name for 245, the house grey; &<bgdim> behind
&<bg21>xterm-256 index, background
&<#FF0000>hex RGB, and &<bg#FF0000> behind
&<underline> &<reverse>attributes, spelled out

Always close with &n. For interface chrome — labels, borders, anything that should recede — &<245> is the house grey, and &<dim> is a readable name for exactly that index; &<240> is too dark to read on some terminals.

&<dim> is a colour, not the ANSI faint attribute. It sets the foreground to one specific grey rather than fading whatever colour you were already using, so it cannot be layered over another colour. The composable attribute is deliberately unavailable: the browser client defines no rule for it, and a fair share of terminals draw it as ordinary text.

Codes are stripped rather than rendered for clients that cannot show colour, and width calculations strip them before measuring, so a coloured line still boxes and centres correctly.

Wrapping

Long lines are wrapped at WRAP_WIDTH, 121 characters, on the telnet path only. The browser client never wraps server-side — it wraps in CSS, so the same text reflows when the window changes size. Setting WRAP_WIDTH to 0 turns server-side wrapping off entirely.

This is why anything drawn as art — a banner, a box, a map — should be sent as a grid rather than as prose, and why the browser client lays text out on a character cell instead of leaving it to the font.