Skip to content

Engine Architecture

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

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

Layers

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.

Plugins

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_world folder, see Data-Configs.
  • Configurables read one JSON file.
  • Commands are COMMAND(Class, "name") or CMDRUN(name) objects registered with the command manager. Their definition (aliases, flags, help) is an XML file in dreamland_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.

Reload

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 most keeps loaded (for instance descriptor), and another plugin starts calling it, most loads the new caller against the old callee and fails. Treat the change as a reboot change.
  • changed can 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 with plugin 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 for undefined symbol and Exception loading after 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_flags and apply_flags live in core for this reason.

Startup

main() creates DreamLand, calls load() and run(), then saves and leaves.

  1. load(): read the main config, open the log, open the Fenia database environment, open and load the Fenia manager, open the API port, then pluginManager->loadAll() (the list in libexec/plugin.xml).
  2. 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
  1. 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.

The world model

  • 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 inherit DLObject virtually.
  • Flags and enumerations are tables declared in .conf files (src/bits.conf, per-plugin *.conf). makebits.pl generates a header, a source file and a registry entry for each table. A FlagTable is found by name from Fenia as .tables.<table>. The generated header is not rebuilt when only the .conf is newer in a stale tree, regenerate it by hand if a new flag does not appear.
  • Object::extra_flags is 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 through behavior_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 from dreamland_world/prio, then by name. Every alias that is not in the priority file has priority 1000. In OLC states 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_raw finds the command and calls its entry point directly, with no gate at all. Use interpret_cmd when 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 in dreamland_world/grammar/synonyms.json. A keyword that is unknown to the table logs an error on every call, so add it first. Use arg_is_strict for 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.

Fenia in the engine

  • 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 with NMI_GET, NMI_SET, NMI_INVOKE macros whose description string feeds the generated API page. The root object (.) is defined only in root.cpp.
  • A C++ class that wants a wrapper inherits WrapperTarget. The wrapper manager lives in core and links targets at load. Never call extractWrapper() 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_trigger for behaviors, and gprog("onName", ...) for global triggers. The macro returns from the enclosing function when a handler answers. gprog swallows exceptions, gprog_nocatch does not and must be used on combat paths. See Fenia-Triggers.
  • A C++ skill or spell class that has a Fenia run method 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.

Storage

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 name has one typeless record. Repair works on a copy of the database: dump the objects sub-database, replace the bad record with an empty <object/>, load into a fresh file, verify, swap. See Troubleshooting.

HTTP API

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.

See also

Clone this wiki locally