#Anatomy of a door
The manifest:
apimust be 1.slugis the unique lowercase id naming the door's storage and its DOOR menu argument.entryis the PHP file to include;classis the fully qualified class it defines.cron,adminandcursorare optional.cursor(new in 1.7.6) sets the door's default for everyhotkey()prompt. Absent, or anything other than false, means on. See section 03.- Install by uploading the folder into the
doors/directory of the board, alongsidehiloandstarmerchant, then pressing Scan on the console Doors page. - Registering does not put the door in front of anyone. Add a DOOR-action item in the menu designer; its argument list offers every registered door.
#The lifecycle
The class implements three methods:
enterruns when the caller opens the door; return the opening frames.inputreceives keys, lines, or editor payloads exactly like board screens. Check$in->kindforkey,lineoreditor, then read the matching field.- To exit, include
$c->exitDoor()in your returned frames; the board pops the caller to the menu and callsleave.
#The cursor and the key echo
As of 1.7.6 the terminal shows its blinking cursor at every hotkey() prompt inside
a native door, and echoes the single key the caller presses at that spot until the door's next
frame repaints over it. This is exactly how the board's own screens have behaved since 1.7.4;
between 1.7.4 and this release every door had both switched off.
Where the cursor sits: wherever your frame's output ended. A door that paints its prompt or hint as the last thing in the frame gets the cursor right after it with no work at all. Two cases need a moment's thought.
- Your last paint is not the prompt. A status line written after the prompt, a table
cell, a note under a picture. End the frame with a cursor move to the prompt cell (an
at(row, col)after your last text), or fence the late paint in a cursor save and restore: the terminal honorsESC[sandESC[u, so a note painted between them leaves the cursor where it was. - Your door repaints only the cells that changed. A screen differ never repaints the cell where the caller's key echoed, so the key stays on screen. Turn the cursor and the echo off for the whole door and keep painting your own prompt marker as before.
Line prompts (line()) are unaffected: the typed field has its own caret, as
ever.
The manifest key
Absent, or any value other than false ("off", "no" and 0
count as false too), means on. Set it to false when your door paints its own prompt marker and
repaints only what changed; it turns both the cursor and the key echo off for the whole door,
which is how every door behaved before 1.7.6.
hotkey() takes a cursor argument
| hotkey() | Follows the door's manifest default (on unless
"cursor": false). |
| hotkey(false) | Hides the cursor and the echo at this one prompt: a screen that paints its own caret in a field, for example. |
| hotkey(true) | Shows them at this one prompt of a door whose manifest turns them off. |
| $c->cursorDefault() | Returns the door's manifest default as a boolean, for a door that wants to reason about it. |
Checklist for an existing door
- Does every hotkey frame end at its prompt? If yes, you are done; the cursor lands after it.
- Is the last paint elsewhere? End the frame with a cursor move, or wrap the late paint
in
ESC[sandESC[u. - Does the door repaint whole screens? Then the echoed key is always covered. If it
paints deltas, set
"cursor": false. - Does a screen paint its own caret? Pass
hotkey(false)there so the caller sees one caret, not two.
#DoorContext: every service
| Call | Gives you |
|---|---|
| $c->out() | ANSI builder: cls(), at(r,c),
txt(), fg(), bg(), reset(),
str(). |
| $c->pipe(s) | Render pipe codes to ANSI. |
| $c->center(s) | Center on the 80-column screen. |
| $c->ansi(s) | Wrap raw ANSI as a frame. |
| $c->bell() | Terminal bell frame. |
| $c->hotkey() | Request single-key input mode. Also
$c->line(...) and $c->editor(...). |
| $c->exitDoor() | The leave-the-door frame. |
| $c->user() | Caller row: id, handle, level. |
| $c->state | Per-visit scratch array, persisted between requests. |
| $c->kv() | The door's private key/value store (get, set, del, all-by-prefix). |
| $c->table(...) | A private database table. See below. |
| $c->files() | A jailed folder under data/doorstore/<slug>/.
$c->path(name) resolves one file inside it. |
| $c->award(points) | Score on the board-wide DOOR CHAMPIONS ladder (Stats screen). |
| $c->log(msg) | An events-log line tagged with your slug. |
| $c->localStamp(utc, fmt) | Format UTC in the board timezone. |
| $c->boardRumors(n) | A few current rumors, for flavor. |
| $c->dropfile() | Path of the JSON drop file written for this session. |
#Storage: tables that scope themselves
A door never touches the board's tables. $c->table() creates and returns
door_<slug>_<name>:
Types: pk, int, text (add
'short' => true for indexable varchars), datetime. Methods:
insert, insertMany, rows(where, order, limit),
one, count, sum, update,
delete.
The where-grammar is an array: a bare column means equals, or suffix an operator in the key.
Everything is prepared statements underneath; identifiers are validated.
#The crash wall
Any uncaught exception or PHP error inside a door is caught: the caller sees a short in-character apology and lands back at the menu; the full error goes to the events log.
#The cron hook
Declare "cron": "cron.php" in the manifest and the board's daily tick includes it
(per enabled door, per board-day, each in its own try/catch) with $doorCron in
scope: kv(), table(), tx(), boardToday(),
log(). Make it idempotent: record the last day you ran in kv and
return early on repeats. Star Merchant's cron.php is the worked example.
Speaking to the board
New in 1.5.5. The cron context can also speak to the board on the door's behalf: two mediated calls, so doors still never touch a board table.
| Call | Effect |
|---|---|
| $doorCron->postMessage($areaTag, $subject, $lines) | Posts a real message into
the message area named by tag (GENERAL, say), authored by the SysOp account, body lines in
pipe color codes. For announcements a game generates on its own clock; Suzerain's
end-of-round Chronicle is the worked example. Returns false, posting nothing, if
the area or the SysOp account cannot be found. |
| $doorCron->boardRumors($n) | A few recent approved rumors, for flavor in rendered briefings. |
postMessage inside your own
tx(). Posting opens its own transaction for the message numbering. Queue the
text in kv during the tick and post after your transaction commits.#The admin panel hook
Declare "admin": "admin.php" and the console Doors page grows a panel link. Your
file renders inside the console with $doorAdmin (kv and tables) and
$csrfField to drop into every form; handle your own POSTs at the top, as Star
Merchant's panel does.
The admin context carries the same postMessage() call as cron, for announcements a
SysOp action should publish: a round opening, a season reset.
#The drop file
On entry the board writes a JSON drop file (path from $c->dropfile()) carrying
board name and version, base URL, node, and the caller's id, handle and level: the spiritual
DOOR.SYS, for doors wanting a file interface.
#Learn from HILO, then ship
- Read
doors/hilo/door.php: about 120 lines using the state bag, kv, line mode and award.doors/README.mdwalks it line by line. - Keep every visible row inside 80 columns and position with
at(). - Never touch superglobals or board tables. Everything you need arrives through the context.
- To distribute: zip the folder. Another SysOp unzips it into their
doors/directory, presses Scan, adds a DOOR menu item, done.