Skip to content

Output Format

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

TL;DR: Every message a player sees goes through one custom formatter (MsgFormatter), not libc printf. Fenia act, recho, room.echo, fmt and the C++ pecho, recho, echo, fmt all share it. It adds positional arguments, name declension, gender and plural endings, and renders per viewer. Use when: you write or review any text with % codes, add a message to C++ or Fenia, or debug a stray letter, a missing capital or "Not enough arguments". Source of truth: plug-ins/output/msgformatter.cpp (MsgFormatter::run), plug-ins/output/act.cpp, plug-ins/output/character.cpp, plug-ins/feniaroot/dlprintf.cpp (regfmt). Verified: 2026-11-02 against dreamland_code 88da8302, dreamland_world 43880467, dreamland_fenia_public c28874b

Which function to call

All of these format with the same % codes. They differ only in who receives the line.

Receiver C++ (Character) Fenia
Only the character pecho(fmt, ...), pecho(pos, fmt, ...) ch.act(fmt, ...)
Everyone in the room except the character recho(fmt, ...), recho(pos, fmt, ...) ch.recho(fmt, ...)
Room except the character and vch recho(vch, fmt, ...) ch.rvecho(vch, fmt, ...)
Anyone by type (TO_CHAR, TO_ROOM, TO_VICT, TO_NOTVICT, TO_ALL) echo(pos, type, vch, fmt, ...) none, use the rows above
Everyone awake in a room loop over room->people room.echo(fmt, ...)
Same, adjacent rooms none room.echoAround(fmt, ...)
Format only, no output fmt(to, fmt, ...) ch.print(fmt, ...), .fmt(fmt, ...)
Raw text, no formatting send_to(text) (no newline added) ch.ptc(text) (goes through the pager, see below)

Rules that hold for every row:

  • pecho and recho add the line end. send_to does not.
  • The formatter runs once per recipient, with that recipient as the viewer (to). One recho can show a name differently to each reader (invisible, disguised, other language).
  • A recipient is skipped if their position is below pos (default POS_RESTING, so sleepers get nothing). recho and echo of type room, notvict and all also skip anyone who cannot sense the actor.
  • Fenia argument names and signatures come from the API lookup (scripts/fenia-api.py in the main tree, or api() in game), not from this page.

Positions are the position_table in src/bits.conf: dead 0, mort 1, incap 2, stun 3, sleep 4, rest 5, sit 6, fight 7, stand 8.

Format codes

A code is %, optional parts, then a conversion letter. Order of the optional parts:

%  [N$]  [#]  [-]  [width]  [.limit]  [^ or _]  conversion
Part Meaning
N$ Use argument number N (1-based). Without it, arguments are taken in order.
# Ignore invisibility and disguise: show the real name even if the viewer cannot see the target.
- Pad on the right (left-align). Default pads on the left.
width Pad to this many visible characters. Colour codes do not count.
.limit Cut to this many visible characters.
^ Capitalise the first letter. ^^ capitalises every letter.
_ Lower-case the first letter. __ lower-cases every letter.

%% prints one %.

Plain values

Code Argument Output
%d %i %l number integer
%u number unsigned integer
%f number float, no width or precision control
%s string the string. In Fenia a ._("...") message is resolved to the viewer's language first.
%S string first word of the string only
%c string first character
%w per-viewer text the word in the viewer's language (C++ LangText)

Names and grammar

The digit after the letter is the grammatical case, 1 to 6: nominative, genitive, dative, accusative, instrumental, prepositional. The viewer's language decides which forms exist. English names have one form for every case.

Code Argument Output
%C1..%C6 character name in that case. Unless #, an unseen character prints as "someone" and a disguised one by the disguise.
%O1..%O6 object short description in that case. Unless #, an unseen object prints as "something". Wrapped in {1..{2 so the name's own colours cannot leak into the sentence.
%N1..%N6 string a Russian declension pad such as стрел|а|ы|е|у|ой|е, in that case. In Fenia it can also be a .mm(...) or ._() message carrying a pad.
%K1..%K6 skill skill name in the viewer's language
%P1..%P7 character or object personal pronoun by gender and number of the argument. Russian table only.
%p1..%p7 number same pronoun, gender given as a number (0 neuter, 1 masculine, 2 feminine)
%T1..%T8 character or object possessive pronoun (Russian твой, твоего, твоей: your). Russian table only.

%P2 is the one to use for "his, her, their" (Russian его, ее, их). %P3 is "to him", %P4 is the form after a preposition (Russian "у него": at him).

Endings chosen by the argument

The alternatives follow the letter and are separated by |. The code ends at a space, punctuation, { or the end of the string.

Code Chooses by Alternatives in order
%G gender of a character or object neuter, masculine, feminine, plural
%g a number (0, 1, 2). A gender index when given a sex, a count class when given a count. first, second, third slot
%n grammatical number of a character or object singular, plural
%I a number, Slavic count rule slot 1 for n ending in 1 (not 11), slot 2 for n ending in 2 to 4 (not 12 to 14), slot 3 for everything else

Examples, with the arguments each one needs:

%2$^C1 отравле%2$Gно|н|на ядом от %3$O2 %1$C2.
У тебя %1$d практи%1$Iка|ки|к и %2$d тренировочн%2$Iая|ые|ых сесси%2$Iя|и|й.
%^C1 стукается лбом о %N4.
Воры ограбили {W%1$#C4{x, и теперь %1$P1 нуждается в помощи.

In order, the lines mean: "The evil werewolf is poisoned by venom from the owner's thorn dagger" (arguments: owner, victim, weapon). "You have 18 practices and 2 training sessions" (arguments: two numbers, the endings follow the count). "A character bumps their forehead on the door" (arguments: character, a door name pad). "Thieves robbed X, and now he needs your help" (argument: the robbed victim, # shows the name even if the viewer cannot see him).

%I is the Russian and Ukrainian rule. English text must not use it: %I is right at 1, 2 and 5 but wrong from 21 up ("21 minute"). For English use %g, which picks slot 0 for 0, slot 1 for 1 and slot 2 for anything above. The Slavic pattern A|B|B becomes B|A|B, so "1 minute, 2 minutes" is written %1$d minute%1$gs||s.

Gender and number come from the single stored value on the argument, in every language. Ukrainian text reads the same gender as Russian does. %P and %T always print Russian, so never use them in an English or Ukrainian template. For English write %1$nit|them. For Ukrainian write %1$Gйого|його|її|їх.

Positional arguments

fmt(to, "На меня напа%1$Gло|л|ла %1$C1!", killer);

The string means "%1$C1 attacked me from ambush" with the verb ending chosen by the gender of argument 1.

%1$ reuses argument 1, so the same character is passed once. Always number the arguments in Fenia. An unnumbered code that runs past the argument list throws NotEnoughArgumentsException ("Not enough arguments"). Numbered codes can also repeat or reorder, which translations need.

Traps

  • %ld, %lu, %lld do not exist. l is already a full integer conversion, so %ld prints the number and then a literal d. Write %d. The same applies to every function in the table above and to the C++ OLC ptc helper. Pure log functions (notice, warn, bug) use libc vsnprintf and do accept %ld.
  • Number every conversion in any string that is translated or goes through a catalog. An unnumbered code takes the next argument in order. If a translation omits a code (English has no use for a %G gender selector), every later unnumbered code in that string slides one slot down. In C++ the argument union overlaps int and char *, so a %s that lands on an integer dereferences a small number as a pointer and the server dies. Numbered codes bind to their own slot, so a dropped selector costs nothing. Fenia's formatter checks types and argument counts, so there the same mistake only garbles the text.
  • Mixing numbered and unnumbered codes differs between the two engines. Fenia's N$ sets the argument cursor, the C++ one only advances it. Number everything and the difference does not matter.
  • Never end a format string with a slot list. The slot scan stops at space, -, ?, !, ,, ., :, {, (, ) and the end of the string. \r and \n do not stop it, so монет%3$Iу|ы|\r\n files the line break into the third slot and the break disappears for most counts. Put the break outside the format call or follow the list with a space or punctuation.
  • The formatter reads integers through an int union. Pass a plain int, never a long.
  • Never pass already-rendered text as the format. pecho(someString) re-parses every % inside it, and a % in a player name, object name or user string crashes the server in argStr. Pass "%s" with the text as an argument. The Fenia room and char methods already do this internally.
  • A literal % in a string that is only concatenated and sent with ptc stays a single %. Doubling it (%%) is right only inside a format string.
  • %P and %T ignore the template language. In a message built with lmsg or .mm, an English viewer would see the English frame followed by the Russian word for "her". Use %nit|them for English and %G for Ukrainian.
  • Capitalisation: ^ upper-cases the first letter of the finished argument, skipping colour codes. A message that starts with a colour code and a name needs %^C1, not a literal capital.
  • Colour codes {x, {W and so on pass through untouched. They do not count toward width or .limit.
  • A % conversion that is not in the tables is not an error. The formatter drops it and goes on, so a typo such as %z silently disappears.
  • A string with an unclosed or invalid code does not throw in C++. fmt logs the exception and returns what it built so far. In Fenia the exception reaches the script.

Languages

Every receiving function resolves the format per viewer. A message can be a plain string or a MultiMessage (C++ _("..."), Fenia ._("...") or .mm(en, ru, ua)).

  • The viewer's language is the config lang setting (en, ru, ua). A mob, an unset player and a missing viewer use the default, Russian. A charmed pet uses its master's language, a switched mob uses the original body's language.
  • A message with no translation falls back to Russian, byte for byte.
  • Names (%C, %O, %K) are declined in the viewer's language, not the template's.
  • For a sink with no viewer, such as the Discord stream, use fmtLang(lang, message, ...) in C++ or .fmtLang(lang, fmt, ...) in Fenia. It renders format and names in one fixed language for everyone. Fenia language numbers are 0 English, 1 Russian, 2 Ukrainian.
  • In a broadcast to a room with mixed languages, pass a .mm(en, ru, ua) as a %s argument. It resolves for each reader.

See Text-Markup for {l, {n, {s language tags inside a string, and L10n for the wider trilingual design.

Paging and C++ helpers

Function Use
send_to(text) Raw text to one player, tags converted, no newline added.
page_to_char(text, ch) Long text. Pushes a pager on the input stack and waits for Enter after each page. Page length is the character's lines config, 0 turns paging off.
hint_fmt(ch, fmt, ...) Newbie hint with a localized [Hint] label. Silent for NPCs, characters at level 50 and above, and anyone who has remorted.
say_fmt, tell_fmt, tell_raw Speech frames ("X says '...'") localized per listener.
echo_char, echo_room, echo_notvict echo with a predicate that filters recipients.
echo_master(ch, fmt, ...) Output for a charmed mob. Goes to the master too when the master gave the order.
print_columns(names, width, columns) Lay out a list in columns.

Legacy: oldact

Legacy: oldact(format, ch, arg1, arg2, type) and oldact_p(..., min_pos) still run in about 115 source files, mostly social, AI and movement code. New code uses pecho, recho and echo. The old function turns $ codes into % codes (act_to_fmt) and calls ch->echo. Arguments become %1$ (ch), %2$ (arg1) and %3$ (arg2).

Old New
$c1..$c6 %1$C1.. (the actor)
$C1..$C6 %3$C1..
$o1..$o6 %2$O1..
$O1..$O6 %3$O1..
$e $s $m $x $y $z $u %1$P1..%1$P7 for the actor
$E $S $M $X $Y $Z $U %3$P1..%3$P7 for arg2
$g, $G %1$G, %3$G
$t, $T %2$s, %3$s
$n, $N %2$N, %3$N
$d %3$S (first word of arg2)
$w, $W %2$w, %3$w

act_to_fmt also adds ^ to the first substitution at the start of each sentence (after !, ? or a single ., not after ...), so legacy strings start with a capital.

Receiver types: TO_CHAR the actor only, TO_VICT only the second character, TO_ROOM everyone but the actor, TO_NOTVICT everyone but actor and victim, TO_ALL everyone in the room. TO_NOBODY drops the message.

See also

Clone this wiki locally