This document covers internal architecture and conventions for contributors working on ENiGMA½ core systems. It supplements CONTRIBUTING.md, which covers style and process.
ENiGMA½ is a multi-client BBS server. Each connected user gets a dedicated client object that holds their terminal state, session, and current menu module. All user-facing logic runs through menu modules driven by a menu.hjson configuration file.
Key directories:
| Path | Purpose |
|---|---|
core/ |
Server engine, view system, menu/session management |
mods/ |
User-supplied mods — ENiGMA ships nothing here; sysops drop local modules in |
art/general/ |
General art files — theme-agnostic or used as fallback across themes |
art/themes/<name>/ |
Per-theme art files; ENiGMA ships with the luciano_blocktronics default theme |
misc/menu_templates/ |
Canonical menu.hjson templates — oputil uses these for initial deployment; sysops then modify their own copies |
docs/ |
End-user and sysop documentation |
test/ |
Mocha test suite |
🚧 Full documentation lives in
docs/_docs/art/. This section is a quick map for contributors.
Art files are ANSI or UTF-8 files with SAUCE metadata stored under art/themes/<theme-name>/. They contain MCI codes (%SB1, %TL1, %VM1, etc.) which are placeholders for interactive views.
The pipeline from art to live view:
MenuModule.displayAsset()renders the art file and runs the ANSI parser- The parser emits MCI positions (screen row/col for each
%XX#code) MCIViewFactory.createFromMCI()instantiates the correct view classViewController.loadFromMenuConfig()applies properties frommenu.hjsonviaview.setPropertyValue()
Theme overrides: theme.hjson can override any MCI property using the same config path as menu.hjson.
Double-MCI convention: art files encode normal and focus SGR for a view by placing the MCI code twice (%TL1%TL1) — first occurrence captures normal SGR, second captures focus SGR.
Templates: misc/menu_templates/*.in.hjson are the canonical defaults used by oputil when a sysop first deploys their system. After that, sysops own their copies and modify them freely. When adding or changing MCI config, update the template and any local dev config.
A menu module is a class that extends MenuModule (or a mixin thereof), exported as getModule:
exports.moduleInfo = { name: 'My Module', desc: '...', author: '...' };
exports.getModule = class MyModule extends MenuModule {
constructor(options) {
super(options);
this.menuMethods = {
doAction: (formData, extraArgs, cb) => {
// handle form submit
return cb(null);
},
};
}
initSequence() {
async.series(
[
callback => this.beforeArt(callback),
callback => this.displayViewScreen(false, callback),
],
err => { if (err) { /* ... */ } }
);
}
};Module-specific config comes from the config: block in the menu entry via this.menuConfig.config.
All view properties flow through view.setPropertyValue(propName, value) — never set instance variables directly from outside the view. The factory creates views with minimal options; ViewController applies the full config after creation.
Views have acceptsFocus and acceptsInput (both false by default). Any view that owns a timer must implement destroy() to clear it — ViewController.detachClientEvents() calls destroy() on all views.
See docs/_docs/art/views/ for per-view documentation.
- System config:
Config.get()— the singleton loaded fromconfig.hjson - Module config:
this.menuConfig.config— theconfig:block of the current menu entry - Safe access idiom:
_.get(this.menuConfig.config, 'someKey', defaultValue)
Config files use hjson (comments, unquoted keys, trailing commas are all valid).
npm test # run full suite
npm test -- --grep "pattern" # run matching tests- Framework: Mocha + Node
assert(assert.strictEqual, notexpect/should) test/setup.jspatchesConfig.getso view constructors work in isolation — always included via--require test/setup.js- Async tests: wrap callback APIs in a small Promise helper, then use
async/awaitin the test body - Tests for a module live in
test/<module-name>.test.js