Repository navigation
Engine Architecture
TL;DR: A small core library (
src/) plus about fifty plugins (plug-ins/, shared objects) make one process. Plugins load in the order of a list file and can be reloaded one by one while the game runs. The core cannot. Fenia, the scripting language, is the core's interpreter plus a wrapper plugin. Fenia's state lives in a BerkeleyDB database, players in flat files, areas in XML read at boot. Use when: you navigate the C++ code, add a plugin, decide between a hot reload and a reboot, or touch storage. Source of truth:src/dreamland.cpp,src/main.cpp,src/plugin/pluginmanager.cpp,libexec/plugin.xml.template,plug-ins/Makefile.am,src/bits.conf,src/makebits.pl,plug-ins/feniaroot. Verified: 2026-11-02 against dreamland_code 88da8302, dreamland_world 43880467, dreamland_fenia_public c28874b
bin/dreamland main(): load config, run the pulse loop, save
libdreamland core library: src/{regex,lang,alloc,util,l10n,xml,flags,gref,process,plugin,scheduler,io,fenia,core}
libdreamland_impl the DreamLand object and the plugin-facing glue
plug-ins/<name>/*.so gameplay: commands, fight, OLC, scripting wrappers, web, ...
dreamland_world XML and JSON game data, read by plugins
Fenia scripts scripts loaded from the Fenia database and script directories
src/ builds before plug-ins/ and plugins link against it. The dependency only goes one way.
src/ directory |
What lives there |
|---|---|
lang |
strings (DLString), lists, maps, the intrusive Pointer<T>, bit helpers |
util, regex, alloc
|
dates, crypto, a bundled regex engine, a class registry that allocates objects by name |
xml, moc
|
XML variables and the meta-object compiler. A class marked XML_OBJECT with XML_VARIABLE fields gets load and save code generated from its header |
flags, gref
|
flag and enumeration tables, and "global references" that name a registry entry in XML (a race, a skill) |
l10n |
grammar entities, noun declension (Flexer), inflected strings |
plugin, scheduler, process
|
the plugin manager, the priority scheduler, threads and monitors (Fenia threads sit on these) |
io |
files, directories, sockets, HTTP, BerkeleyDB wrapper |
fenia |
the Fenia language: scanner, parser, tree, runtime, codesources |
core |
the world model: Character, PCharacter, NPCharacter, Object, Room, Area, Affect, Race, behaviors, calendar, auction, help manager |
Plugins are grouped by job. A short map:
| Group | Plugins |
|---|---|
| input and output |
descriptor, iomanager, output, interpret, command, web, gmcp, servlets
|
| world and persistence |
areas, loadsave, updates, system, traverse
|
| combat |
fight, fight_core, ai, groups, skills_impl, skills_loader
|
| characters |
profession, race, race_aptitude, religion, clan, remort, hometown, learn
|
| content |
comm, communication, note, quest, questreward, gquest, cards, languages, help, social, services
|
| building |
olc, ed
|
| scripting | feniaroot |
Every plugin directory is listed in plug-ins/Makefile.am. occupations is headers only.
A plugin is a shared object that exports initialize_<name>(), which returns a list of Plugin objects. Each plugin implements initialization() and destruction(). A PluginInitializer<T> static registers a plugin at load. Plugin::isCritical() is true for plugins whose reload has serious side effects (only descriptor, which holds every connection).
libexec/plugin.xml lists the modules to load, in order. It is generated at configure time from libexec/plugin.xml.template. Order matters: a plugin that registers a class used by another must come first. A plugin can also be linked against another (LIBADD), which makes the linked one a dependency and unloads it later.
Typical plugin contents:
-
Table loaders read a
dreamland_worldfolder, see Data-Configs. - Configurables read one JSON file.
-
Commands are
COMMAND(Class, "name")orCMDRUN(name)objects registered with the command manager. Their definition (aliases, flags, help) is an XML file indreamland_world/commands. - Scheduler tasks run each pulse at a priority, see Engine-Time-and-Pulses.
- Servlets answer HTTP requests under a prefix on the API port.
-
Skills and spells are
SKILL(name)and spell classes found by name from the skill XML.
The plugin command: plugin list, plugin reload all|most|changed|<name>, plugin load <name>, plugin unload <name>. A reload request is stored and executed between pulses.
| Request | Does |
|---|---|
<name> |
unloads one plugin and calls loadAll, which loads whatever is missing |
most |
unloads every plugin except the critical ones, then loads all |
changed |
the same, for plugins whose .so file changed since load (and are not critical) |
all |
unloads everything including descriptor, which disconnects every player |
Safe and unsafe, in one place:
- A change under
src/(any core header) is never hot-reloadable. The running process holds the old core library. A rebuilt plugin that calls a new core symbol or relies on a new class layout fails to load against it ("undefined symbol") or corrupts memory. Everything after the failing plugin stays unloaded and Fenia is gone. Reboot. - The same happens plugin to plugin. If a new exported function is added to a plugin that
mostkeeps loaded (for instancedescriptor), and another plugin starts calling it,mostloads the new caller against the old callee and fails. Treat the change as a reboot change. -
changedcan half-load for the same reason when somebody else changed the core between your build and your reload. - Data-only changes (XML and JSON in
dreamland_world, translation catalogs) go withplugin reload most. Area files do not reload at all. - The server does not catch a crash in a plugin's
initialization: it aborts that reload with a log line. Check the log forundefined symbolandException loadingafter every reload. - After a reload that unloads
feniaroot, the Fenia database is recovered from disk. A record that cannot be read (a typeless<object>) then stops the whole load. Never keep a._()message in a persistent Fenia field, because messages are not persisted and leave an empty record on the next database sync. - Flag tables that a long-lived object can point at (affects store a raw table pointer) belong in
src/bits.conf, never in a plugin.conf, because a plugin's tables unmap on reload.weapon_type2,raffect_flagsandapply_flagslive in core for this reason.
main() creates DreamLand, calls load() and run(), then saves and leaves.
-
load(): read the main config, open the log, open the Fenia database environment, open and load the Fenia manager, open the API port, thenpluginManager->loadAll()(the list inlibexec/plugin.xml). - Tick 0 of the scheduler runs the boot tasks, in priority order:
| Priority | Task |
|---|---|
SCDP_BOOT + 5 |
load all area XML files |
+ 6 |
fix-ups on loaded areas: exits, resets, door levels, vnum check |
+ 7 |
item economy: derive the weight and cost of every object prototype from the model |
+ 10 |
load every player profile into an in-memory index (name and attribute scans over the playerbase are cheap afterwards). Counts limited items held by offline players |
+ 15 |
accounts |
+ 20 |
saved items on room floors, saved mobs, charmed summons, the report on saved mobs |
SCDP_INITIAL |
weather init and the first forced area reset |
- The pulse loop starts. Players can connect once the port plugin has opened its sockets.
Order is the reason for two facts. Players load before saved room items, so an item's owner can be resolved when the item is read. Areas load before both, so a saved file for a room that no longer exists is skipped and logged as NULL room N.
Shutdown: the reboot command or a shutdown file sets a flag, the loop ends, players are saved, the config and Fenia database are committed, and the process exits at once without tearing down plugins. Every Fenia write is a synchronous database transaction and the environment opens with recovery, so an unclean exit is recovered on next boot.
- Every kind of entity has a prototype (
MOB_INDEX_DATA,OBJ_INDEX_DATA,RoomIndexData,AreaIndexData) and instances (NPCharacter,Object,Room,Area) with a pointer back to the prototype. Prototypes come from area XML. Instances are created by resets, commands and scripts. Global lists:char_list,object_list,roomInstances,areaInstances. - A mob prototype's hit points, damage and armor are derived at boot from level, tier, race and form. The area XML numbers are ignored. See OLC-Mobiles.
- Reference counting is intrusive:
::Pointer<T>. Many engine classes inheritDLObjectvirtually. - Flags and enumerations are tables declared in
.conffiles (src/bits.conf, per-plugin*.conf).makebits.plgenerates a header, a source file and a registry entry for each table. AFlagTableis found by name from Fenia as.tables.<table>. The generated header is not rebuilt when only the.confis newer in a stale tree, regenerate it by hand if a new flag does not appear. -
Object::extra_flagsis a signed 32-bit integer. The last usable single bit is 2^30. There is no room for another object extra flag without widening the field. To add a concept, repurpose a bit or use a script property. - Behaviors (
dreamland_world/behaviors) are classes attached to mob, object and room prototypes. The engine calls them throughbehavior_trigger(entity, "Name", ...)and C++ macros, before and after the Fenia handlers. A behavior with an empty node in area XML (<behavior type="X"/>) used to crash the loader and is now logged and replaced by the basic behavior. Do not write one. - Command lookup (
plug-ins/command): a typed word matches any command or alias it is a prefix of. Candidates are tried in priority order fromdreamland_world/prio, then by name. Every alias that is not in the priority file has priority 1000. InOLCstates the first chain entry the word is a prefix of wins, and commands defined later in the file are tried first, so give a longer command name earlier in the file than the shorter one it extends. -
interpret(ch, line)runs a full command line with every gate.interpret_cmd(ch, name, fmt, ...)finds a command and runs its dispatch (position, stun, charm, ghost checks) with a printf-style argument.interpret_rawfinds the command and calls its entry point directly, with no gate at all. Useinterpret_cmdwhen you delegate on behalf of a player. The format argument of both is a libc format, never pass player text as the format. -
arg_is(arg, "keyword")is true when the typed argument is a prefix of the keyword or of any synonym indreamland_world/grammar/synonyms.json. A keyword that is unknown to the table logs an error on every call, so add it first. Usearg_is_strictfor whole words. - Any C++ loop that hands items one by one to a handler that fires triggers must recheck that the actor, the target and the room are still alive after every item, because a trigger may extract either party. Compare the room as well as the dead flag: an extracted character has its room pointer cleared and its dead flag reset.
- Parsing a player-typed number:
DLString::toInt()throws on overflow, and nothing on the command path catches it, so an oversized number typed as an argument must never reach it unchecked. Bound the length (seven digits cover every vnum) before you convert.isNumber()accepts a leading minus.
- The language itself is in
src/fenia: scanner (scan.l++), grammar (parse.y++), trees, runtime, codesources. See Fenia-Language and Fenia-Grammar. - The API objects are in
plug-ins/feniaroot. A wrapper is the Fenia-visible face of a C++ entity:CharacterWrapper,ObjectWrapper,RoomWrapper, and so on. Fields and methods are declared withNMI_GET,NMI_SET,NMI_INVOKEmacros whose description string feeds the generated API page. The root object (.) is defined only inroot.cpp. - A C++ class that wants a wrapper inherits
WrapperTarget. The wrapper manager lives in core and links targets at load. Never callextractWrapper()in a destructor without checking that the wrapper handler is alive, and note that adding a library link to feniaroot reorders unload as well as load. - Triggers: C++ code calls a script with
FENIA_CALL(entity, "Name", ...)for the instance,FENIA_NDX_CALL(index, ...)for the prototype,behavior_triggerfor behaviors, andgprog("onName", ...)for global triggers. The macro returns from the enclosing function when a handler answers.gprogswallows exceptions,gprog_nocatchdoes not and must be used on combat paths. See Fenia-Triggers. - A C++ skill or spell class that has a Fenia
runmethod posted calls the script first and replaces the C++ body at once, even while its XML still says C++. A script that replaces a C++ skill must work under the XML's old argument type. - Object identity for the Fenia database: two live objects with the same saved id bind to one persisted wrapper and corrupt it. A guard renumbers a newcomer only when a persisted wrapper already exists for that id, so duplicates in saved item files have to be fixed by hand.
- A persisted Fenia object of a wrapper class that an older binary does not know stops the boot of that binary. Do not use a new wrapper constructor on a live server until the change is final.
| Data | Format and place |
|---|---|
Fenia state (.tmp, fields on characters and objects, codesources) |
BerkeleyDB environment in feniaDbDir, two sub-databases, objects and codesouces (sic). Every write is a synchronous transaction |
| Players | one flat profile per character in playerDir, plus an XML attribute file per character under the database directory. Loaded into an in-memory index at boot |
| Account data | JSON files read by the account manager |
| Area files | XML, areaDir, listed in area.lst.xml. Read at boot only. In-game editors rewrite them |
| Saved rooms | per-room files for floor items, mobs and charmed creatures under savedDir, in 16 subdirectories by vnum modulo 16 |
| Game data | XML and JSON in dreamland_world, see Data-Configs
|
Rules:
- Pfile fields set by Fenia as plain integer attributes on a character are lost at remort. Use string attributes for anything that must survive.
- Items are saved when they move. See Mechanics-World.
- A Fenia database that fails to recover with
Attempt to allocate a class with empty namehas one typeless record. Repair works on a copy of the database: dump theobjectssub-database, replace the bad record with an empty<object/>, load into a fresh file, verify, swap. See Troubleshooting.
If apiPort is non-zero the core opens an HTTP server and a ServletManager. Plugins register servlets under a URL prefix. Several are public read-only JSON (who is online, a character profile, the bug list). Others are administrative and need a token that is kept in the misc directory. The administrative endpoints are not documented here.
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