Repository navigation
Fenia Language
TL;DR: Fenia is an untyped, JavaScript-like language. A script is one expression. It has six value types, C-style
for,for ... in,try/catch, and first-class functions. It has nowhile, no ternary, no++or+=, and no floats. Use when: you write, review or generate Fenia code, or need to know whether a construct parses and what it does at runtime. Source of truth:dreamland_code/src/fenia/scan.l++(tokens),parse.y++(grammar),register-impl.h(operators, casts),stmt-tree.cpp,scope.cpp,closure.cpp,plug-ins/feniaroot/feniastringext.cpp,regcontainerext.cpp,reglistext.cppVerified: 2026-11-02 against dreamland_code 88da8302, dreamland_world 43880467
Everything on this page was read from the parser and the interpreter source unless marked (measured live ...) or (unverified). The formal grammar is on Fenia-Grammar. Runtime traps that come out of these rules are collected on Fenia-Gotchas.
The grammar entry rule is input: exp. A file posted with cs post or cs load, and a line typed after eval, must each be exactly one expression. Statements only exist inside a function body.
- Wrap anything with more than one statement in
.apply(function() { ... }). - Put no
;after the closing}). A trailing;is a parse error and the whole file is rejected. The previous version keeps running and nothing tells the player or the log reader why. - Inside the function body every statement ends with
;, including the last one. - A top-level
{ ... }block is not an expression and does not parse.
.apply(function() {
.get_mob_index(3090).onSpec = function(mob) {
mob.interpret("emote purrs.");
};
})A single assignment is already one expression, so a file that holds only .get_mob_index(3090).onSpec = function(mob) { ... } also works without .apply.
eval <expression> runs one expression with this bound to the acting character. Assignments work (eval hit = max_hit). var, statements and a trailing ; do not.
The interpreter calls a value a register. A register has one of six types.
| Type | Holds | Notes |
|---|---|---|
NONE |
nothing | The value of null, of a missing key, of an unset variable, of a function without return. |
NUMBER |
32-bit signed integer | No floats. true is 1, false is 0. |
STRING |
byte string | Single or double quotes. Methods are listed below. |
OBJECT |
a handle to an engine or script object | Characters, rooms, items, .List(), .Array(), .Map(), affects, and so on. Compared by identity. |
FUNCTION |
a closure | First-class. Can be stored in fields, passed and returned. |
IDENTIFIER |
an interned name | Used internally as a key type. Scripts rarely see it. |
- Identifiers start with a Latin letter, an underscore or a Cyrillic letter, and continue with those plus digits. Cyrillic identifiers are legal.
-
Numbers are decimal digits only. The lexer calls
atoi, so a literal above 2147483647 silently truncates. Long ids (14 digits) cannot be written as numbers, keep them as strings. -
Keywords:
null if in else for break continue return function var try catch throw, plusthis,trueandfalse. There is nowhile,do,switch,case,newortypeof. -
Strings use
'...'or"...". The escapes are\n,\r,\t. Any other\xyieldsx. A literal newline inside a string is allowed. Two string literals written next to each other are joined into one. -
Comments:
// to end of lineand/* ... */. Block comments nest. -
Stray characters such as
?,:,#,@,$and a backtick outside strings and comments are a lexer error (unexpected char). -
Encoding: script text is converted from UTF-8 to KOI8-U when it is loaded. Stay with ASCII and Cyrillic. A character with no KOI8-U form, an emoji for example, made a whole file fail to post even when it sat in a comment
(measured live). -
Operators and punctuation:
+ - * / % & | ^ ! ~ < > = == != -> && || . , ; ( ) { } [ ].>=and<=are lexed as two tokens, so> =with a space is also accepted.
| Form | Notes |
|---|---|
var a, b; |
Declares names, value NONE. No initializer: var x = 5; does not parse. Write var x; x = 5;. |
if (cond) stmt / if (cond) stmt else stmt
|
No else if keyword, nest an if after else. |
for (init; cond; step) stmt |
init and step are comma-separated expressions, var is not allowed in init. cond may be empty. It is re-evaluated each iteration. |
for (name in expr) stmt |
Calls expr.iterator(), which lists have. To walk an .Array() use a.keys or a.values, for a .Map() use m.fieldKeys. name needs no var. |
break; continue;
|
continue in a C-style for still runs the step expression. |
return; return expr;
|
A function that falls off the end returns NONE. |
throw expr; |
Throws the value as a message. |
try stmt catch (e) stmt |
e is the message as a STRING. |
{ ... } |
Block. Opens a new scope. |
expr; |
Expression statement. A call needs the ;. |
From lowest to highest precedence. Within a row, grouping is left to right unless noted.
| Operators | Meaning |
|---|---|
-> |
Lambda, see Functions. |
= |
Assignment. Right to left. a = b = 0 works. if (a = b) assigns and tests, it is not a comparison. |
&& ||
|
Logical, short-circuit, result is 1 or 0. See the note below on how they group. |
| |
Bitwise or. |
^ |
Bitwise xor. |
& |
Bitwise and. |
== !=
|
Equality. |
< > <= >=
|
Order. Defined for numbers and strings only. |
+ -
|
Add, subtract. |
* / %
|
Multiply, integer divide, remainder. Division or remainder by zero throws. |
! ~ -
|
Unary not, bitwise not, minus. |
.name [expr] (args)
|
Member, index, call. |
Operator behaviour by type, from register-impl.h:
-
+looks at the left operand.NUMBER + xconvertsxto a number.STRING + xconvertsxto a string and concatenates. Anything else throws. So"2" + 2is"22"and2 + "2"is4. A string that does not start with digits throws when converted to a number ("abc","")."12abc"converts to12. -
-*/%&|^need aNUMBERon the left, otherwise they throwOperation ... is not defined. -
==and!=: if either side isNONE, the result only compares the two types. Numbers compare numerically, strings by bytes, objects and functions by identity. ANUMBERcompared with aSTRINGconverts the right side to the type of the left one. -
!and conditions use truthiness, see below.
&& and || do not group like C. The grammar gives the one-character tokens | and & a higher precedence than the logical rules, so a chain groups to the right: a && b || c parses as a && (b || c), and a | b && c as a | (b && c). This comes from replaying the precedence table of parse.y++ in an LALR parser, not from running the engine (unverified on the engine). Always parenthesize mixed && and ||.
~: bitwise not, as in C. ~4 is -5 and 12 & ~4 is 8 (measured live 2026-11-02), so flags & ~bit clears a bit. .unset_bit(mask, bit) does the same and reads clearer.
A condition converts its value to a boolean. Only two types convert: a NUMBER is true when it is not 0, and a STRING is true when it is not empty. NONE, OBJECT and FUNCTION throw Invalid cast: expected 'boolean'. So if (obj) throws when obj is null and also when it is a live object.
Write explicit comparisons: if (obj != null), if (obj == null). Truthiness is safe on comparisons, .is_npc(), and other calls that return a number.
- Each function call starts a fresh root scope. The scope holds the parameters,
this, and thevarnames declared in it. It does not hold the caller's locals. - Each
{ }block, and eachfor ... inloop, opens a nested scope. Avarinside a block is not visible after it. - Redeclaring a
varin a loop body resets it toNONEon each pass. -
Reading a name nobody declared looks for a field of
thiswith that name. It givesNONEon a character, item, room or.Map()without that field, so a typo in a variable name is silent there. Engine objects such as the spell context throwGet method not foundinstead. IfthisisNONE, as in the body of a bare.apply(function() { ... }), the read throws. -
Assigning a name nobody declared writes a field on
this. Inside a trigger that is the mob, item or room, and the field persists in the Fenia database. Declare every local withvar. -
thisis a local. You can read it, pass it, or assign to it. Helper files usethis = .tmp.system;and then writesleep = function(t) { ... };to define fields on that map. - Inside spell and skill hooks,
thisis a context object. The namesch,vict,leveland so on resolve through the same field fallback. See Fenia-Skills-and-Spells. -
originis the object a top-level expression was evaluated against. Inevalit is the acting character. - A leading
.names a field of the root object:.get_mob_index(3090),.tables.item_table.furniture,.tmp.
var add;
add = function(a, b) { return a + b; };-
Arity is strict in both directions. Too few arguments throws
NotEnoughArguments, too many throwsTooManyArguments. There are no defaults and noargumentsobject. -
A call needs a reference on its left.
f(x),obj.method(x)and.root(x)are calls.f(x)(y)and(function(){})()are not valid. Use.apply(fn, this, argList)to call a function value with an explicitthisand a.List()of arguments. -
function() { }captures nothing. It sees its own parameters and locals, then falls back to fields ofthis. A local of the enclosing function is not visible inside it. -
x -> expr,() -> exprand(a, b) -> { ... }parse as lambdas. A lambda copies the enclosing scope's variables when it is created. No script in the shipped repositories uses lambdas, and their runtime behaviour has not been checked on the running game(unverified). - Numbered forms such as
function 12 (a) { }appear when the engine prints a stored function. You do not write them. - A function value cast to a string prints its decompiled source.
throw "text"; throws a message. catch (e) receives the message as a string for scripted throws and for engine exceptions such as a missing method or a bad cast.
An exception that nobody catches ends the handler. The engine logs it and warns online immortals. The command or trigger that called the handler carries on as if nothing happened. In a skill's run the exception is swallowed and the skill is still counted as handled, see Fenia-Skills-and-Spells.
A try also catches the engine's "victim is dead" unwinding in combat code. A handler that kills its target inside a try can leave the fight round running on a corpse. Keep kills outside try blocks (see the ai_trigger comment in plug-ins/ai/aitrigger.h).
Reading a field of NONE throws Cannot call operator * on NONE. Calling a method on NONE throws Null pointer dereference. Assigning a field on a non-object throws.
| Operation | From | Result |
|---|---|---|
| to number | NUMBER |
itself |
| to number | STRING |
leading integer, or throws |
| to number | other | throws |
| to string | NUMBER |
decimal text |
| to string | STRING |
itself |
| to string | FUNCTION |
decompiled source |
| to string |
NONE, OBJECT
|
throws |
| to boolean |
NUMBER, STRING
|
see Truthiness |
| to boolean | other | throws |
Because ptc and string concatenation need a string, ptc(null) throws. Test with != null before printing a value that may be missing.
A string has methods and an index, no fields. Always call a method with parentheses: "abc".size(). A bare s.size is read as a character index and throws Index out of bounds.
-
s[i]ands.at(i)give the character at positioni. -
==is an exact byte comparison. A Cyrillic literal in a UTF-8 script file and a runtime name stored in KOI8-R do not compare equal(measured live 2026-09-10). Match players by a non-name property, or let engine helpers compare. - Methods in the engine (from
feniastringext.cpp):size,empty,at,substr(offset[, length]),split(sep)(returns a list, default separator is a newline),replace(old, new)(replaces every occurrence),trim,capitalize,upperFirstChar,toLower,toUpper,format(width),quote,noun,ruscase(case),arguments,getOneArgument,cutOneArgument,strPrefix(str),match(regex),matchCase(regex),matchGroups(pattern),is_name(arg),isName(arg),contains(words),stripColour,stripTags,isCyrillic,isRussian(deprecated),isNumber,api. -
There is no substring search.
contains(words)treats its argument as a list of keywords and checks word prefixes, so"abcdef".contains("cde")is false. Usematch("regex")to test for a substring. -
is_nameandisNameare different.a.is_name(b)asks whether the words ofaare prefixes of words inb(the keyword test used for command words).a.isName(b)asks whether the words ofbare whole words ina. - Regular expressions are POSIX extended.
matchignores case,matchCasedoes not.matchGroups(pattern)returns groups 1 to n in reverse order, the last group first. - A string method that is not in this list fails with
Invoke method not found.
._("text") and .mm(en, ru, ua) return a message object, not a string. The object is rendered in each reader's language when it reaches act, say, recho and the other output functions. Do not compare it with ==, concatenate it, or print it with ptc: comparing it with "" throws and ends the handler. To keep a refusal text for a later test, store a formatted string, for example msg = ch.print(._("...")). Details are on L10n-Fenia.
| Constructor | Kind | Use |
|---|---|---|
.List() |
ordered list | Iterate with for (x in list). |
.Array() |
key to value table | Any key type. Safe to persist. |
.Map() |
struct with named fields | Keys must be names. Persisted as XML. |
.List()
- Create it empty and fill it. The constructor takes no arguments, so
.List(a, b)does not putaandbin the list. - Methods:
add(a, b, ...),addAll(list),push_back,push_front,pop_back,pop_front,front,back,at(n),random,size,has(x),find(x),find_reverse(x),sub(a, ...),unique,clone,clear,join(sep),sort([fn]),filter(fn),forEach(fn[, args]),api. - Index a list with
at(n)orfor ... in. Square-bracket indexing of a list throws(measured live). Config arrays returned by.config(...)are the opposite: they takearr[i + 0]but cannot be walked withfor ... in. -
sub(a, ...)removes every occurrence of the given elements in place and returns the list. Getters such asroom.pplreturn a fresh list on each read, soroom.ppl.sub(ch)is a safe copy withoutch. -
forEach(fn, extra...)callsfnonce per element withthisset to the element and the extra arguments passed as parameters. The count of extra arguments must match the parameter list. It returns a list of the non-null results. -
add(a, b, c)returns the list. A maintainer note says a chained.List().add(a, b, c)failed at runtime in one script(unverified). Build the list in a variable, then iterate.
.Array() (RegContainer)
- Read or write any key:
a["key"],a[ch.id]. A missing key readsNONE. Assigningnullremoves the key. - Members:
keys,values(lists),size(),add(k, v, ...),addAll(array),clone(),clear(),api(). - The built-in names win over data. A key called
keys,sizeorclearis shadowed by the member.
.Map() (IdContainer)
- Fields by name:
m.hp = 5,m["some key"],m[nameVar]with a string variable. - A missing field reads
NONE. Members:fieldKeys,size(),clear(),api(). There is nokeys. Use.Array()when you need to list keys. - Numeric keys (
m[vnum]) throwInvalid cast: expected 'number' got 'IDENTIFIER'. Convert to a string first, or use.Array()(measured live). - A
.Map()is saved as XML element names. A key that is not a legal XML name (starts with a digit, contains a space or Cyrillic) saves fine but fails to load after a reboot. The object then reads asObject not recovered(measured live). This does not show on a hot reload. Use only Latin identifier keys in a persisted.Map(), and.Array()for everything else.
| Missing | Use instead |
|---|---|
while, do
|
for (i = 0; i < n; i = i + 1) with an explicit bound |
a ? b : c |
var t; if (a) t = b; else t = c; |
i++, x += 1
|
i = i + 1; |
switch |
chained if / else
|
| floats | integers, scaled up (centi-units). Division truncates. |
var x = 1; |
var x; x = 1; |
new, classes |
.Map() and closures |
| template strings |
"" + count + "x " + name or .fmt("%dx %s", count, name)
|
A construct the parser does not know is not always a loud error. while (cond) { ... } lexes as a plain call to a function named while, fails to parse, and the file is rejected as a whole.
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