Skip to content

Fenia Scripts

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

TL;DR: A Fenia script is a code source stored in the Fenia database. You load it from the web editor, from a file with cs load, or with eval. Loading runs the file once, and the assignments in it bind triggers and helpers. Nothing re-reads the files at boot. Use when: you add, replace, load or remove Fenia scripts, name a script file, or wonder what survives a reboot. Source of truth: dreamland_code/plug-ins/feniaroot/ccodesource.cpp (cs), ceval.cpp (eval), codesourcerepo.cpp (file layout), cfindref.cpp, src/fenia/codesource.cpp, closure.cpp Verified: 2026-11-02 against dreamland_code a7c4fd10, dreamland_world a35c62ce, dreamland_fenia_public c28874b. Private script repositories were read for behaviour only.

For the language see Fenia-Language. For which events a script can bind to see Fenia-Triggers.

What a script is

A code source has an id, a name (the "subject"), an author, the source text, and a list of functions compiled from it. Loading a code source evaluates the text once. The text is one expression, normally .apply(function() { ... }), and the assignments inside it bind functions to places:

.apply(function() {
    .get_mob_index(3090).onSpec = function(mob) {
        if (.chance(5))
            mob.interpret("emote purrs.");
    };
})

After the load, the function lives in the field onSpec of the prototype of mob 3090. The code source stays registered as long as some function from it is still referenced.

Who can use it

eval, cs, findrefs and the fenia command need Fenia security 110. The engine grants it when one of these holds:

  • the character's trust is 110 or higher
  • the server runs in buildplot mode and the character has a feniasecurity attribute of 110 or more
  • the character wrapper has a numeric feniaSecurity field of 110 or more

The subcommands cs load, cs save, cs del and cs gc are further limited to coders.

Ways to load a script

Way What it does
eval <expression> Evaluates one expression. this is the acting character. ptc(x) prints a string. Creates a throwaway code source named <eval command>.
cs web [id or name] Opens the script editor in the web client with a new or an existing script. Run in the editor evaluates the text and saves it under its subject.
cs post Evaluates the script in your edit buffer (filled by cs paste) and clears the buffer.
cs load <file> Coders only. Reads a file from the Fenia script directory and evaluates it.
cs load all [folder] Coders only. Loads every file under the script directory, or under one folder of it, that matches a known layout pattern.
fenia <trigger> inside medit, oedit, redit, bedit, skedit Opens the web editor with a template for that trigger and the right subject. See OLC-Other-Editors.

Reposting a file with the same subject replaces the stored source in place. It no longer creates a second copy.

The Fenia script directory is set by feniaScriptDir in the server config. The stock template points it at share/DL/fenia (the CI build config still says share/DL/fenia.local). A server that carries the public Fenia repository uses share/DL/fenia and mounts that repository at share/DL/fenia/public (see Repositories). The loader treats a leading public/ in a subject as that folder. The directory is read only by cs load and by the server's HTTP eval interface. Nothing reads it at boot or on a plugin reload. The Fenia database is the source of truth for what runs.

Two consequences:

  • A file that is correct on disk but was never loaded is not running.
  • A script that names a skill or an affect at its top level (.AffectHandler("x").onRefresh = ..., .SkillCommand("x").run = ...) needs that skill to be loaded first. If it is not, the load fails with "skill does not exist", the file is not bound, and nobody reloads it later. Load the world data first, then the script.

cs subcommands

From the command help in ccodesource.cpp.

Subcommand Use
cs list [text] List code sources, all or with the text in the name.
cs read <id or name> Print a script.
cs copy <id or name> Copy a script into your edit buffer.
cs search <text> Find every script whose source contains the text.
cs dups Names that have more than one code source.
cs orphans Counts of unreferenced code sources, objects and functions. Read-only, like the boot check.
cs gc Coders only. Dry run of the duplicate collapse done at boot. Read-only.
cs web [id or name] Web editor.
cs paste, cs show, cs clear Work with the edit buffer.
cs post Evaluate the buffer.
cs load <file>, cs load all [folder] Coders. Load from disk.
cs save <id or name> Coders. Write one script to share/DL/fenia.local/<name>.f++, the fenia.local folder under tablePath. This path ignores feniaScriptDir.
cs save all Coders. Write every script to the script directory using the layout below.
cs del <id> force Coders. Break the cycles of a code source and let the collector take it.
findrefs <id> A separate command. Lists what refers to a code source. Zero lines means nothing refers to it.

rtapi() on a wrapper prints the fields that scripts set on it at runtime, for example eval ptc(.get_mob_index(3090).rtapi()). Use it to see which triggers a prototype has.

File layout

CodeSourceRepo maps a subject to a file path. The patterns, in the order the engine tries them:

Subject pattern Holds
spell/<spell name>/run<Target> Spell run hooks (runVict, runObj, ...)
affect/<skill name>/<Trigger> Affect handler triggers
skillcommand/<skill name>/<method> Skill command methods (run, preRun, apply)
command/<command name>/<method> Command overrides (runFunc)
areas/<area file>.are/<mob|obj|room>/<vnum>[ label] or <vnum>.<trigger> Triggers on one prototype
areas/<area file>.are/<name> Shared code for one area
<folder>/<name> and <folder>/<sub>/<name> Everything else, for example utils/<topic> helper files

A leading public/ marks a script that belongs to the public repository. Names may contain spaces and, in helper files, a literal colon and space (utils/object: dimensions). Quote such paths in a shell. In game, cs load takes the rest of the line as the name, so type it without quotes.

Several helper files can fill one .tmp.<namespace>. A single trigger file can also arm more than one method on the same prototype, so read a whole file before saying a prototype has "only" one trigger.

What persists

  • Code sources, functions, .tmp and every field set on a character, item, room or prototype live in the Fenia database and come back after a reboot. Fenia fields on a character are not in the player file.
  • Players have a second store: typed attributes, saved in the player file. ch.attribute("name", value) creates one, ch.attribute("name") reads it, ch.hasAttribute(name), ch.eraseAttribute(name). The setter throws if the attribute already exists, so update with erase then set. It works for online players only. For a player who is offline use .Player(name), which has attribute(name), hasAttribute(name), eraseAttribute(name) and setAttribute(name, value[, save]). The last one writes a string attribute, overwrites an existing one, and saves the profile unless save is false.
  • ch.attribute("name") returns a copy. Writing into the returned value changes nothing. Only eraseAttribute removes a whole attribute.
  • A number attribute is dropped at remort. A string attribute survives. Store anything permanent as a string and convert on read.
  • .Map() keys that are not legal XML names break the object at the next boot, see Fenia-Language.

Removing and replacing scripts

There is no cs delete. A code source disappears when its last function is no longer referenced. The collector counts references, so:

  1. Annul before you delete. Set every binding the script made to null first (.get_mob_index(3090).onSpec = null;, .tmp.topic = null;), then remove the file. If you delete the code first, the old binding stays in the database and the next script of the same name can end up duplicated.
  2. Deleting the file does not unregister the script. cs load all re-evaluates whatever is on disk and cs save all writes every registered script back out.
  3. A script whose objects point at each other (a parent object and children registered in a shared map) is a reference cycle. Annulling the bindings does not free it. Use findrefs <id> and cs orphans to see it, and cs del <id> force to break the cycle.
  4. Reposting a body that no longer makes an assignment does not unbind what the old body bound. Annul the bindings instead.
  5. Annul every debug probe you bound while testing: .tmp.test.x = null, temporary onSpec handlers on a prototype. They persist in the database and fire forever.
  6. After you edit a script, cs search <text> finds orphans of the old name.

Posting order and traps

  • Posting a caller before its helper, or the reverse. Fenia arity is strict, so changing a helper's parameter list breaks every caller that still passes the old count until it is posted too. Post the helper and every caller together, or add the parameter under a new helper name.
  • A command override is bound by load order. .FeniaCommand("x").runFunc = ... binds only if the command registered after the Fenia database was recovered. A command registered earlier by a plugin that loads before the Fenia plugin serves its C++ body after every boot. Reposting such an override each boot piles up orphaned wrappers. Declare the command in the world data instead (measured live).
  • A skill command run takes over at once. Posting run for a skill that has a C++ body replaces the body immediately, even if the skill's world data still describes the old argument type. With no argtype, vict is null and only argOne/argAll are set, so the script must find its own target.
  • Affect handlers and skills referenced at the top of a file need the world data first, see above.
  • A script that parses but throws while running shows nothing to the player. Check the server log for lines containing Exception calling Fenia prog.

See also

Clone this wiki locally