Skip to content

Text Markup

Kit Oliynyk edited this page Nov 2, 2026 · 2 revisions

TL;DR: Player text carries inline tags that start with {. One pass resolves the gates (language, gender, alignment, time, weather, client type, links), a second pass turns colours into ANSI or web markup. Help articles add a smaller markup on top (*bold*, _arg_, %FMT%). Use when: you write or review any description, help body, catalog string or message that contains {, or a link, colour or language-dependent word does not render as expected. Source of truth: plug-ins/output/mudtags.cpp (VisibilityTags, ColorTags), plug-ins/help/helpformatter.cpp (HelpFormatter), plug-ins/web/webtextmanip.cpp (decorateExtraDescr), mudjs/src/manip.js (how the web client reads the result). Verified: 2026-11-02 against dreamland_code 88da8302, dreamland_world 43880467, mudjs 990bf6d

How a string is processed

Every line sent to a player goes through Character::send_to, which calls mudtags_convert in two passes:

  1. The visibility pass reads {l, {n, {s, {S, {A, {T, {F, {I and {h. It decides which plain characters survive and, for web clients, wraps links in markup.
  2. The colour pass reads the colour letters and writes ANSI escapes (telnet) or <c c='...'> spans (web client).

The viewer decides everything: language (config lang), sex, alignment, time of day, season, web or telnet, screen reader, immortal or not. A string with no viewer is rendered for a male, neutral-alignment, English, non-web reader, and every {I span is dropped, {IW included (Discord and Telegram relays render this way).

Two facts explain most surprises:

  • Only plain characters are gated. A gate decides whether the letters between tags are printed. The tags themselves are always processed. A colour code inside a hidden {l segment still changes the colour of whatever follows. A {hh inside a hidden segment still opens a link.
  • {x resets every gate. {x (and {X) closes the language, gender, alignment, time, weather, client-type and link state at once, then resets the colour. Text after a {x that sits inside a gated segment is shown to everybody.

Unknown tags are passed through unchanged. Player-typed channel text goes through mudtags_strip_web, which drops {h links (and any command in {hc) but keeps colours and language tags for the readers to resolve.

Colours

{ plus one letter. Uppercase is bright, lowercase is dark.

Code Colour Code Colour
{x {X reset to default (grey-white) {D dark grey
{r {R red, bright red {g {G green, bright green
{y {Y yellow (brown), bright yellow {b {B blue, bright blue
{m {M magenta, bright magenta {c {C cyan, bright cyan
{w {W white (light grey), bright white

Special characters:

Code Effect
{{ a literal {
{/ a new line
{* terminal bell (telnet only)
{+ blink (telnet only)
{1 push: remember the current colour
{2 pop: go back to the remembered colour

{1 and {2 are not colours. Use them around a coloured word inside a longer coloured sentence, so the sentence does not drop to grey after {x:

красивые {1{Bсиние{2 галифе

The stack is empty-safe: {2 with nothing pushed resets to default. The engine uses the same pair to fence declined names (%O, coloured Flexer pads), so a name's own colours cannot leak into the sentence.

An unknown letter after { is ignored by the web client and acts as a reset on telnet. { followed by a Cyrillic letter is therefore always a bug. {G is bright green, never a gender code.

Language tags: {l, {n, {s

Three independent gates, each with the same letters:

Tag Category Typical use
{l command words the command a link or hint tells the player to type
{n names declined or localized names inside a fixed string
{s skill names skill and spell names inside a fixed string

After the category letter: e English, r Russian, u Ukrainian (upper case also works), x close. A text between {lE and the next {l? is printed only if the viewer matches.

{lEcast{lRколдовать{lUчаклувати{lx

Resolution per viewer:

  • English viewer: the E segments.
  • Russian viewer: the R segments.
  • Ukrainian viewer: the U segments of a category, but only if the string contains at least one {lu, {nu or {su for that category. If it has none, the Ukrainian viewer reads the R segments. The check is per category. Old strings that only know {le and {lr therefore keep working for Ukrainian readers.
  • Exit names inside {hs speedwalks are binary: English or localized. Ukrainian rides Russian there.

Rules for authors:

  • Channel text (say, yell, gossip, tell, clan) keeps its {l/{n/{s tags until the final delivery and resolves per listener. A line that uses these tags should carry all three segments ({le, {lr, {lu). The Ukrainian check runs over the whole text blob, so a line with only {le and {lr can lose its Ukrainian reading when it shares a blob with a line that has {lu.
  • Never put a {x inside a language segment. It ends the gate and the remaining segments leak to every reader. Strip colours from a value before embedding it (.stripColour() in Fenia). Declined names from the engine are already fenced with {1...{2 for this reason.
  • Do not use {l to translate whole sentences in new code. Use the catalog (see L10n). The tags are for small command, name and skill fragments inside fixed text.

Gender, alignment, time, weather

All of these gate plain characters and are closed with x.

Tag Values Shown when
{Sm {Sf {Sn male, female, neutral the reader's sex matches
{Ag {An {Ae good, neutral, evil the reader's alignment matches
{AG {AN {AE not good, not neutral, not evil it does not match
{Tn {Td night, day night is dark or sunset, day is everything else
{Ft {Fr {Fc {Fs thunderstorm, rain, cloudy, clear sky the weather matches (upper case: it does not)
{Fw {Fp {Fu {Fa winter, spring, summer, autumn the season matches (upper case: it does not)

Close with {Sx, {Ax, {Tx, {Fx (weather) or {Fy (season).

Months 0 to 3 are winter, 4 to 7 spring, 8 to 11 summer, 12 to 15 autumn, 16 winter again.

{S is resolved against the reader, not against an argument. Any string that reaches a descriptor can carry it, with no %G and no character argument. The usual shape for a verb that changes with the speaker's sex:

Ты вош{Sfла{Smел{Sx в комнату.
Ты попал{Sfа{Sx в ловушку.

Do not glue an а onto a male past tense blindly. Say the female form aloud: if it is the male form plus а, the short marker works, otherwise write both endings. -шёл verbs drop the vowel (вошла, not вошела). In Ukrainian, a perfective verb has no present tense, so a gender marker cannot be dodged by rewriting it in the present.

{Tn{Td{Tx renders different text by day and night. Keep the structure when translating: night text, day text, shared tail.

{TnЛунный свет льется в окно.{TdСолнце льется в окно.{Tx Пахнет пылью.

%G is the printf-style counterpart for argument-driven gender. See Output-Format.

Client and accessibility gates: {I

A span is shown only if the reader has the matching property. Close with {Ix.

Tag Shown to
{Iw web client only
{IW telnet only
{Is readers with config screenreader on
{IS readers with it off
{Ii immortals

A reader counts as a screen-reader user when the screenreader config is set (or the client reports itself as Lyntin). A blind player on the web client is still a web reader, so a {Iw span reaches them and a {IW span does not.

Rules:

  • Only one {I gate is active at a time. They do not nest.
  • Never put a {x between {Iw/{IW and {Ix. It resets the gate and everything after it prints to everyone. Helpers that start with a coloured bullet ({W*{x) break when wrapped as a whole. Gate the value, not the composed line.
  • Anything the player must act on (a code to type, a command) stays outside every {I span. Only the presentation may branch. See Accessibility.
  • Inside a {I span close a hyperlink with {hx, not {x.
  • Put newlines inside the gated branch ({Iw...\n{Ix), not after the closing {Ix. A newline outside the branch prints a stray blank line to the readers who do not see the span.
  • On the web client the characters < and > in text are escaped. Only the engine's own web markup inside {Iw is allowed to carry tags.

Links: {h

Links need a web client to be clickable. On telnet the tag disappears and the plain label stays. Close every link with {hx or {x.

Tag Web client click Notes
{hh<id>text{x help <id> The digits right after {hh are the help id, text is the visible anchor. A title that starts with a digit needs a separator.
{hh<keyword>{x help <keyword> The text is both anchor and lookup. Fragile if the keyword changes.
{hc<command>{x sends the visible text as a command {hcвзять кинжал{x
{hc'<cmd>'label{x sends <cmd> Honoured only for shapes the web client allowlists (mudjs/src/manip.js, SAFE_HC_COMMANDS). Any other cmd falls back to sending the label.
{hl<url>{x opens the URL in a new tab The URL is the visible text. Must start with http. Telnet shows the raw URL.
{hg<name>{x glist <name> skill-group names
{hs<path>{x run <path> speedwalk. Exit letters are localized for non-English readers.

The allowlisted cmd shapes are generated by the server only: path <n>, vault get <n> and vault filter <type>, account switch <name>, craft show <n>, craft <n>, craft filter <type>, and for immortals vault *<Owner> get|filter .... The allowlist exists because player text can carry {h tags and a hidden command would be a phishing tool. The server also strips player-typed {h from channels.

Authoring rules:

  • Link by id ({hh1838Нового Талоса{x). The id is the same in every language and only the anchor text is translated and declined to fit the sentence.
  • The anchor must be in the language of the surrounding text and must match how the target article names the thing in that language.
  • A command link must contain a command the interpreter accepts in the language of the text. Commands resolve across all languages by name or alias, but a Russian word in an English sentence reads wrong. dreamland_world/scripts/lint-hc-command-lang.py checks {hc} links in the catalog.
  • Never write {hh without a target that exists. A dead id renders as a link that opens "no help".

Extra-description markers

A word in parentheses inside a room, object or exit description marks an extra description the player can read:

Над камином висит (картина) в тяжелой раме.

The engine (decorateExtraDescr) turns each marker into {Iw[read=<keywords>,see=<word>]{IW(<word>){Ix. Web readers get a clickable word that sends examine, telnet readers see the parentheses. A marker that matches no extra description but names a visible exit becomes a look link instead. This fallback runs only for room and exit descriptions, not for object descriptions.

Rules:

  • The text between the parentheses may hold only letters, spaces and hyphens. Anything else (code examples, ASCII art, func()) is left alone.
  • A marker matches an extra description when every word of the marker is a prefix of some word in that description's keyword blob. An inflected word (символами) does not match the nominative keyword (символы). Reword the noun or add the inflected form to the keyword blob.
  • The shown word follows the viewer: the author's word is kept if its script matches the reader (Latin for English, Cyrillic for others), otherwise the engine picks a word of the reader's language from the keyword blob. Russian and Ukrainian are told apart by the letters ы э ъ ё and і ї є ґ.
  • Use one marker per extra description, on the noun in the text's own language. Do not add an English word in parentheses after a Russian or Ukrainian noun.
  • The keyword blob of an extra description holds the words of all languages in one element. See L10n-Translating-Content.

Help-body markup

Help bodies run through HelpFormatter first, then through the tags above. The formatter toggles on a character, so every opener needs its closer.

You write Becomes Use for
*word* {y...{w (yellow) commands the player can type
_word_ {D...{w (dark grey) placeholders such as _target_
=word= {c...{w (cyan) the term the article defines, on first mention
[word] {W{hh...{x keyword link, same as {hhword{x in white
(en,ru) {lEen{lRru{lx a language choice, only inside an open *, _, = or [ span

The (en,ru) form has no Ukrainian branch. A Ukrainian reader gets the Russian one. Do not write new ones, the body is already one language per <text> block.

Angle brackets are not safe in help bodies. A <id> placeholder is lost for the reader (reported twice by players, cause not traced, unverified), and a raw < does not parse as XML. Write _id_ and give a concrete example.

%WORD% keywords expand to localized words. An unknown %WORD% is printed as typed.

Keyword Expands to
%FMT% Format: (localized)
%FFF% seven spaces, aligns a continuation line under %FMT%
%A% *, a bullet
%SA% %SEEALSO% See also
%U% %USAGE% Usage
%H% %HELP% the word help in the reader's language, bright white
%CAST% cast (колдовать, чаклувати)
%OBJ% %CHAR% %PLR% %MOB% %VICT% %DIR% argument words: object, character, player, mob, victim, direction
%YES% %NO% %ALL% %SHOW% %ON% %OFF% the literal arguments the parser accepts
%PAUSE% %RESUME% stop and restart help parsing, for pre-rendered blocks
%CMD% the command name in the reader's language (skill and command helps)
%SKILL% the skill name, quoted (skill helps)
%SPELL% the cast prefix plus the quoted spell name, c 'fireball' (skill helps)

Never type the words Format:, See also or their translations in a body. The marker localizes them. The words behind %H%, %CAST% and the argument words are catalog entries, so a word is only offered if the command parser really accepts it. See Help-Articles.

Traps

  • A tag inside a hidden segment still acts. Colour, link and gender state set there carries on.
  • {x is a total reset. It is the usual cause of leaked language segments and leaked {I spans.
  • {hl already handles web and telnet. Do not wrap it in {Iw.
  • Case matters: {Iw web, {IW telnet. {Is screen reader, {IS not.
  • {G, {R, {D are colours. Letters after { that are not in the tables above are dropped or reset the colour. Check every { followed by a letter when proofreading Cyrillic text.
  • Colours in the Flexer pad of a name: the engine fences them. Colours you add by hand in a translated string must be balanced per segment.
  • A catalog translation must carry the same colour tags as its Russian source. A shorter word in the other language moves the leftover letters onto the closing tag.

See also

Clone this wiki locally