Repository navigation
Code Style
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.cppVerified: 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.
- 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.
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.
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):
-
chis not null andch.in_roomis not null. -
ch.itemsandch.in_room.itemsare 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.
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;.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.
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
onSpecover 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.
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.
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.
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.
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
nullfirst, 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.
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.
- 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. Passing0as 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) andget.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.jsonandweapon_classes.jsonmust contain the same keys, or the generator fails and an area reset can crash the server (reported crash). Edit both together. See Data-Configs.
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