Repository navigation
Output Format
TL;DR: Every message a player sees goes through one custom formatter (
MsgFormatter), not libc printf. Feniaact,recho,room.echo,fmtand the C++pecho,recho,echo,fmtall 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
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:
-
pechoandrechoadd the line end.send_todoes not. - The formatter runs once per recipient, with that recipient as the viewer (
to). Onerechocan show a name differently to each reader (invisible, disguised, other language). - A recipient is skipped if their position is below
pos(defaultPOS_RESTING, so sleepers get nothing).rechoandechoof 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.pyin the main tree, orapi()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.
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 %.
| 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) |
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).
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його|його|її|їх.
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.
-
%ld,%lu,%llddo not exist.lis already a full integer conversion, so%ldprints the number and then a literald. Write%d. The same applies to every function in the table above and to the C++ OLCptchelper. Pure log functions (notice,warn,bug) use libcvsnprintfand 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
%Ggender selector), every later unnumbered code in that string slides one slot down. In C++ the argument union overlapsintandchar *, so a%sthat 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.\rand\ndo not stop it, soмонет%3$Iу|ы|\r\nfiles 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
intunion. Pass a plainint, never along. - 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 inargStr. 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 withptcstays a single%. Doubling it (%%) is right only inside a format string. -
%Pand%Tignore the template language. In a message built withlmsgor.mm, an English viewer would see the English frame followed by the Russian word for "her". Use%nit|themfor English and%Gfor 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,{Wand so on pass through untouched. They do not count towardwidthor.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%zsilently disappears. - A string with an unclosed or invalid code does not throw in C++.
fmtlogs the exception and returns what it built so far. In Fenia the exception reaches the script.
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 langsetting (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%sargument. It resolves for each reader.
See Text-Markup for {l, {n, {s language tags inside a string, and L10n for the wider trilingual design.
| 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(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.
Start here
Get running
Contribute
- Contribute
- Contributing Workflow
- Code Style
- Accessibility
- Writing Player Text
- Game Design Principles
Build areas
- Building
- OLC
- OLC Rooms
- OLC Mobiles
- OLC Objects
- OLC Resets
- OLC Line Editor
- OLC Other Editors
- Area Format
- Area Quests
- Mob Forms and Parts
Fenia
- Fenia
- Fenia Language
- Fenia Triggers
- Fenia Gotchas
- Fenia API
- Fenia Scripts
- Fenia Behaviors
- Fenia Skills and Spells
- Fenia Cookbook
- Fenia Grammar
Engine
- Engine Architecture
- Engine Time and Pulses
- Output Format
- Mechanics Combat
- Mechanics World
- Data Configs
- Item Power
Localization
Clients
More