ATDT Demo board online

Documentation · for developers

The Door API

A door is one folder with a manifest and a PHP class. The board provides screen, input, storage and identity; the door returns frames. Nothing a door does can take the board down.

API version 1 · chapter 16 of the SysOp's Manual · ATDT 1.8.1

Section 01

#Anatomy of a door

doors/dicerun/ door.json the manifest door.php the game class cron.php optional: daily housekeeping admin.php optional: a panel inside the console

The manifest:

{ "api": 1, "slug": "dicerun", "title": "Dice Run", "entry": "door.php", "class": "\\Doors\\DiceRun\\Door", "cron": "cron.php", "admin": "admin.php", "cursor": false }
  • api must be 1. slug is the unique lowercase id naming the door's storage and its DOOR menu argument.
  • entry is the PHP file to include; class is the fully qualified class it defines. cron, admin and cursor are optional.
  • cursor (new in 1.7.6) sets the door's default for every hotkey() prompt. Absent, or anything other than false, means on. See section 03.
  • Install by uploading the folder into the doors/ directory of the board, alongside hilo and starmerchant, 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.
Section 02

#The lifecycle

The class implements three methods:

final class Door implements \Atdt\DoorInterface { public function enter(\Atdt\DoorContext $c): array; // first frames public function input(\Atdt\DoorContext $c, \Atdt\In $in): array; public function leave(\Atdt\DoorContext $c): void; // cleanup }
  • enter runs when the caller opens the door; return the opening frames.
  • input receives keys, lines, or editor payloads exactly like board screens. Check $in->kind for key, line or editor, then read the matching field.
  • To exit, include $c->exitDoor() in your returned frames; the board pops the caller to the menu and calls leave.
Section 03

#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 honors ESC[s and ESC[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

{ "api": 1, "slug": "yourgame", ..., "cursor": false }

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

public function hotkey(?bool $cursor = null): array
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[s and ESC[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.
Section 04

#DoorContext: every service

CallGives 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->statePer-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.
Section 05

#Storage: tables that scope themselves

A door never touches the board's tables. $c->table() creates and returns door_<slug>_<name>:

$scores = $c->table('scores', [ 'id' => 'pk', 'user' => ['int'], 'points' => ['int'], 'name' => ['text', 'short' => true], ], [['user']]);

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.

$t->rows(['sector' => 9]); // = $t->rows(['points >' => 100]); // > (also != < <= >=) $t->rows(['name LIKE' => 'A%']); // LIKE $t->rows(['id IN' => [1, 2, 3]]); // IN

Everything is prepared statements underneath; identifiers are validated.

Section 06

#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.

A door can be wrong; it cannot be fatal. Write freely. The worst outcome of a bug is one annoyed caller and one log line.
Section 07

#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.

CallEffect
$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.
One rule: never call 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.
Section 08

#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.

Section 09

#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.

Section 10

#Learn from HILO, then ship

  • Read doors/hilo/door.php: about 120 lines using the state bag, kv, line mode and award. doors/README.md walks 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.

↑ Back to the top