Skip to content

Code Style

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

TL;DR: Check for null explicitly, skip guards that the engine already guarantees, guard everything that runs after a wait, put repeated patterns in a helper, roll luck through the shared chance helper, and never leave a loop without an exit. Comments say why. Everything in a source file is ASCII. Use when: you write or review Fenia or C++ for DreamLand, or you want to know why a reviewer asked you to change a guard. Source of truth: dreamland_fenia_public/utils/chance, dreamland_fenia_public/utils/mob, dreamland_fenia_public/utils/system, dreamland_code/src/fenia/scan.l++, dreamland_code/plug-ins/comm/give.cpp Verified: 2026-11-02 against dreamland_code 88da8302, dreamland_fenia_public c28874b, dreamland_world 43880467

Syntax limits of the language are on Fenia-Language. Runtime traps are on Fenia-Gotchas. This page is about habits that keep scripts readable and the server alive.

Both languages

  • Comments explain why, in terms that stay true for a year. Notes from a debugging session ("picked this timer because otherwise it crashes", "the next change will do X") belong in the commit message or the pull request.
  • Comments and strings are English unless a string is player text. See Contributing-Workflow.
  • Use ASCII punctuation in code, comments and strings. The engine converts source to a KOI8 pipeline, and a stray Unicode dash or curly quote breaks rendering. ... for an ellipsis, -- for a dash, straight quotes.
  • Name things for what they are. Do not copy a three-line pattern into a third place, make a helper.

Fenia

Null checks are explicit

Write x != null and x == null. Do not use truthy shortcuts on game objects.

if (obj.in_obj != null && obj.in_obj.vnum == 29115)
    .tmp.system.trueExtract(obj);

Truthy is fine only for expressions that are already boolean, such as comparisons and ch.is_npc(). The codebase uses in_obj != null throughout and has no truthy form of it. Apply the same rule to in_room, carried_by, fighting and any index data pointer.

Do not guard what the engine guarantees

A handler that runs synchronously inside a player command (a skill run, a command override, an on* trigger with no wait before it) is only reached when the character is in a sane state (unverified, maintainer convention):

  • ch is not null and ch.in_room is not null.
  • ch.items and ch.in_room.items are lists, possibly empty.
  • An item in those lists has in_obj == null.

A redundant if (ch == null) return; is clutter. If ch were null, the next call would throw and the surrounding handler would catch it.

Do guard after a wait

Anything that resumes after .scheduler.sleep, .scheduler.yield, or inside a thread may find the world changed. The character can die, be extracted or disconnect.

.tmp.system.sleep(8);
if (!.tmp.mob.valid(ch) || !ch.online)
    return;

.tmp.mob.valid(ch) is true when ch != null && !ch.dead. For an item that earlier loop steps may have removed, use .tmp.object.valid(obj).

The same applies to global triggers with no actor and to callbacks that outlive the entity they were made for. When the guard is repeated, use the helper that does both steps in one call:

if (!.tmp.system.sleep_online(ch, 8))
    return;

Sleeps are in pulses and immortals do not wait

.scheduler.sleep(n) and the helpers built on it count pulses, not seconds. The server runs 4 pulses per second, so 12 is three seconds. Convert before you decide a pause is long.

Immortals test mechanics all day. Put every sleep behind a check so gods skip it, including sleeps inside loops:

if (!ch.is_immortal())
    .tmp.system.sleep(12);

If a global trigger has no ch, find the actor in scope.

Loops need an exit

An unbounded loop or runaway recursion can take the server down. Before you save a loop:

  • Know what ends it.
  • Prefer a periodic trigger such as onSpec over a hand-written wait loop.
  • Yield inside a thread.

The language has no while, so a loop is a C-style for or for (x in list). Declare var above the for. See Fenia-Language.

Luck-aware rolls

Where a character's luck should matter (skill success, dodge, parry, traps), roll with .tmp.chance(base, ch), not with the bare .chance(x). The helper adds race, religion and calendar bonuses and a luck bonus, then clamps the result between 1 and 99 percent.

// Lucky characters avoid the trap more often.
if (.tmp.chance(100 - trapPower, ch))
    return;

For a bad event, roll the avoidance, as above. A character who rolls well is the one who is not hit.

The clamp means there is always a chance and never a certainty. For a true 100 percent event, skip the helper. Use bare .chance(x) only where luck is irrelevant: pure flavour and NPC-internal rolls.

Helpers go where the topic is

A utility used in more than one script goes into the shared library under utils/<topic>. The public library already has helpers for characters, objects, rooms, time, weight, currency and more. Read the folder before writing a new one. Index: Fenia-API.

Layout of nested blocks

The body of if, for and try, and of a function passed as a callback, gets its own indent level. Do not flatten it into the outer block. The interpreter gives no stack trace, and visual structure is how you find the bug.

.get_obj_index(29116).instances.forEach(function() {
    if (this.in_obj != null && this.in_obj.vnum == 29115)
        .tmp.system.trueExtract(this);
});

A single-line if without braces puts its body on the next line with one more indent. Use tabs or four spaces, whichever the surrounding file uses.

State that outlives the session

Everything stored in .tmp and every field set on a character or object is saved in the Fenia database and survives a restart. Two rules follow.

  • Before you remove a function or field, set it to null first, then delete the code. Otherwise the old binding stays and Fenia sees a mismatch with the new file. See Fenia-Scripts.
  • Annul every debug probe when you finish testing. A temporary trigger on a room or mob index keeps firing for everyone.

Item properties of behaviors live on the prototype. On a live instance obj.props is null (unverified, reported by the maintainers), so read them from .get_obj_index(obj.vnum).props.

Review your own script as a whole

Before you submit, reread the whole scenario, not the changed line: null guards, branch logic, loop exits, exemptions. After several rounds of edits to one block, reread the final state, because earlier versions leave artifacts. Then run the placeholder check for any act, echo or fmt you touched. Every placeholder is numbered (%1$s), see Output-Format.

C++

  • A header change affects every plugin that includes it. Grep for the symbol across plug-ins/ and rebuild everything, because a build of one directory does not compile other plugins. See Contributing-Workflow.
  • Player-visible output goes through the localization layer: _("...") for a message, fmt(ch, ...) for formatting with the recipient. Passing 0 as the recipient renders the Russian source text for every viewer. See L10n-Cpp.
  • A loop that hands items to a per-item handler must re-check that both sides are still alive after each item. A trigger can kill or extract either of them mid-loop. give.cpp (still_giving) and get.cpp (still_looting) show the pattern.
if (!still_giving(ch, victim, startRoom))
    break;
  • Never pass a negative vnum to an index lookup (get_obj_index, get_mob_index). The root functions pass the number to the engine lookup with no range check, and a negative vnum has crashed the server, see Troubleshooting.
  • Weapon generator configs weapon_names.json and weapon_classes.json must contain the same keys, or the generator fails and an area reset can crash the server (reported crash). Edit both together. See Data-Configs.

See also

Clone this wiki locally