OpenDoors V7 API Design¶
Status: Draft
Target: OpenDoors 7.0
Compatibility: Deliberate source and ABI break from OpenDoors V6
Design Principles¶
- V7 deliberately breaks source and binary compatibility with V6.
- DOS remains a first-class platform. Serial/modem and socket I/O are core features on the platform families where their respective backends apply.
- Mutable library/session state is accessed through functions rather than an exposed global control structure. Small caller-owned value structures may be public where they naturally represent one operation's data.
- C99 sized types (
int32_t,uint8_t, etc.) are the standard. A compatibility header (od_compat.h) provides them for C89 compilers (Turbo C). Plainintis used only for status values, small enumerations, and values whose documented range fits a 16-bit signed integer. No API assumes that plainintis wider than 16 bits. - Every enumeration constant is representable by a 16-bit signed
int, as required by the supported C implementations. Wider flags or numeric constants use explicitly sized integer macros rather than enum members. - A single process serves a single session. There are no session handles; global state is appropriate.
- Initialization is explicit.
od_init()returns an error code. - Niche BBS-system-specific features from V6 are removed. The API surface covers what door authors actually need.
Supported Platforms¶
V7 supports four platform families:
- 16-bit DOS
- 32-bit DOS
- Windows
- POSIX-like systems
Each platform family is first-class: it is built, tested, documented, and included in releases. First-class support is a statement of quality and support, not a promise of identical capabilities. Platform-specific backends are provided where they are technically appropriate; features on other platforms are not limited to the capabilities of 16-bit DOS.
Threading Model¶
V7 has no application-thread affinity: a public function does not depend on which application thread calls it. Platform backends do not create private helper threads. Transport, timer, input, and presentation progress occurs cooperatively inside public API calls. Application callbacks are invoked synchronously on the thread executing the call that reaches their event.
Public API calls must not overlap. An application that uses multiple threads is responsible for serializing all OpenDoors calls; sequential calls may come from different threads. The library does not add internal locking to make concurrent entry safe.
Application callbacks may call back into the public OpenDoors API synchronously. Individual callback contracts may restrict operations that are invalid in that lifecycle phase or would recursively dispatch the same event; such restrictions are documented per callback rather than imposed globally. Synchronous calls made by a callback are nested calls, not overlapping entry from another application thread.
Architectural Layers¶
V7 separates five responsibilities:
- Session bootstrap and metadata.
- Transport I/O and carrier state.
- Character translation and decoded input events.
- Terminal emulation and local presentation.
- One-shot lifecycle and application callbacks.
The IPC path intentionally bundles session bootstrap and transport into one all-or-nothing BBS channel. Raw APIs bypass character decoding or remote terminal emulation where documented, but do not collapse the layer boundaries. Character translation remains an OpenDoors responsibility on every path so IPC, inherited-handle, and DOS sessions expose consistent application semantics.
Drop File Format: opendoor.drp¶
A new positional (line-number-based) drop file format. Lines are ordered by importance so that a minimal BBS can write only the first few lines and a door that reads fewer lines than the file contains simply uses defaults for the rest.
No comments. No key=value. Extension is by appending new lines at the end; old parsers stop at the last line they understand.
Line Definitions¶
Line 1: Transport type 0=LOCAL 1=SERIAL 2=SOCKET 3=STDIO
Line 2: Handle OS handle or socket descriptor; -1 if N/A
Line 3: Time remaining Minutes
Line 4: User ID String; opaque, BBS-assigned, persistent
Line 5: User name Real name
Line 6: Screen rows Terminal height (default 24)
Line 7: Screen cols Terminal width (default 80)
Line 8: ANSI 0 or 1 (default 0)
Line 9: Node number BBS node (default 1)
Line 10: Baud rate DTE rate; 0 if N/A
Line 11: Encoding 0=CP437 1=UTF-8 (default 0)
Line 12: RIP version 0=none 1=RIPscrip 1.x 2=RIP 2.0
Line 13: Security level Integer access level
Line 14: Connect speed DCE rate; 0 if N/A
Line 15: User alias Handle / nickname
Line 16: User location City, State or similar
Line 17: BBS name
Line 18: Sysop name
Design Rationale¶
The format fills three gaps present in every existing drop file format:
- Screen width (columns): only CHAIN.TXT carries this; DOOR.SYS and DOOR32.SYS do not.
- Character encoding: no existing format specifies whether I/O is CP437 or UTF-8.
- RIP version: no text-based format distinguishes RIPscrip 1.x from RIP 2.0.
The user ID field is a string rather than a numeric record number. Numeric record numbers can be recycled when users are deleted and recreated. A string ID lets each BBS use whatever persistent identifier is natural: a UUID, a database key, or a username that is guaranteed unique.
Handle value -1 means the handle is provided by other means (command-line argument, stdio inheritance, etc.).
What This Format Does Not Do¶
There is no return file. The audit of modern BBS software (Synchronet, Mystic, ENiGMA 1/2, WWIV, Talisman, x/84) showed that only Synchronet reads data back from doors in any meaningful way. Standardizing a return format is not justified.
Transport Layer¶
| Transport | DOS | DOS/32 | Windows | POSIX |
|---|---|---|---|---|
| FOSSIL | Yes | Yes | -- | -- |
| Direct serial | -- | -- | Yes | Yes (new) |
| Socket | -- | -- | Yes | Yes |
| Door32 DLL | -- | -- | Yes | -- |
| StdIO | -- | -- | -- | Yes |
| Local | Yes | Yes | Yes | Yes |
StdIO is not supported on Windows. Anonymous pipes on Windows do not
support overlapped I/O, and a blocking ReadFile on a pipe is not
interrupted when the write end closes, making reliable carrier detection
impossible. POSIX ptys and signal delivery do not have this problem.
Windows doors should use socket transport instead.
Windows local presentation uses the console frontend. The V6 GUI frame is removed: maintaining two Windows presentation frontends adds support burden without adding useful door functionality. A console-based frontend also allows increasing reuse of terminal-facing code between Windows and POSIX as the Windows terminal environment evolves. Silent mode performs no local presentation or console management.
The Windows door32.dll transport is retained for Wildcat! compatibility.
Its data-available and offline event handles are observed cooperatively during
public API processing; the backend does not create a helper thread.
POSIX serial support consumes an already-open descriptor supplied by the BBS. OpenDoors does not open the device path or take ownership of serial-port configuration; opening the same device a second time would conflict with the BBS that owns the connection. Carrier state may be queried through the existing descriptor where the platform supports it. Windows direct serial uses the same inherited-handle ownership model. DOS instead addresses the resident FOSSIL driver by its logical port number.
The internal UART ISR driver from V6 is removed. DOS serial I/O uses FOSSIL exclusively. Freely available, mature FOSSIL drivers avoid duplicating hardware-specific interrupt and UART handling in OpenDoors, while drivers such as NetFoss permit configurations that are not tied to a physical serial port.
Drop File Readers¶
V7 reads four formats plus a custom callback:
opendoor.drp(V7 native)DOOR.SYS(52-line variant, widely supported)DOOR32.SYS(socket/handle passing, lean)CHAIN.TXT(commonly supported and carries the configured screen width)- Custom callback (
od_on_no_drop_file) for anything else
All other V6 formats are removed: DORINFO?.DEF, EXITINFO.BBS (all variants), SFDOORS.DAT, CALLINFO.BBS, TRIBBS.SYS, PCBOARD.SYS/USERS.SYS. These formats are niche and rarely used; retaining their readers is not justified by their remaining compatibility value.
Configuration¶
Configuration overrides are made through od_cfg_*() setter functions before
od_init(). Initialization-only setters may
also be called from od_on_session_loaded(), before the resulting state is
frozen, except for bootstrap-only settings that initialization has already
consumed.
Transport¶
od_cfg_transport(int type)
od_cfg_fossil_port(uint16_t port)
od_cfg_baud_rate(uint32_t baud)
od_cfg_handle(uintptr_t handle)
od_cfg_flow_control(method)
od_cfg_transport_encoding(encoding)
UINTPTR_MAX is the invalid in-memory handle value and corresponds to -1 in
the textual drop-file format.
Transport type, FOSSIL port, and reported baud are configured separately. They
are initialization-only, may be supplied before
od_init() or amended by
od_on_session_loaded(), then freeze before the selected transport is opened.
The FOSSIL port occupies the full 16-bit DX register; 0x00ff is reserved by
the FOSSIL specification and does not identify a communications port.
Baud is informational metadata for inherited Windows and POSIX handles;
OpenDoors does not reconfigure the BBS-owned device. On DOS, the configured
rate is supplied to FOSSIL initialization, although a driver may ignore it
(as is common with locked ports).
The inherited handle is also initialization-only and follows the same
ordering. od_on_session_loaded() may replace a handle supplied by the drop
file before OpenDoors begins using it.
od_cfg_handle() borrows the supplied operating-system handle, descriptor, or
socket; it does not transfer ownership.
od_exit() releases OpenDoors'
internal wrapper state but does not call close(), CloseHandle(), or
closesocket() on the borrowed value. The application remains responsible for
closing it when appropriate, while ordinary process teardown retains the
platform's usual responsibility for any handles still open at process exit.
OpenDoors does not automatically alter persistent state on a borrowed object, including serial timeouts or configuration, termios settings, descriptor flags, and socket options. Backends use per-call polling or nonblocking operations where available. A mutation occurs only through an explicit public request whose contract identifies it; if a backend cannot operate without an undeclared mutation, initialization fails rather than silently changing the caller's object.
Flow-control selection, if retained, is initialization-only and freezes before OpenDoors starts using the serial transport. It never reconfigures an inherited Windows or POSIX port.
Open question: Reconsider whether od_cfg_flow_control() belongs in the
public V7 API at all. A door normally should not alter communications policy
owned by the BBS. If retained, inherited Windows/POSIX handles must not be
reconfigured; the remaining possible use is requesting a mode from a DOS
FOSSIL driver during initialization.
Drop File¶
od_cfg_drop_file(path)
od_cfg_drop_type(type)
Drop type constants: OD_DROP_AUTO, OD_DROP_V7, OD_DROP_DOORSYS,
OD_DROP_DOOR32, OD_DROP_CHAIN, OD_DROP_CUSTOM, OD_DROP_NONE.
Drop-file path and type are bootstrap-only. Calls to either setter are rejected
once od_init() has begun, including from
od_on_session_loaded(), because
the selected input has already been consumed by that point.
Program Identity¶
od_cfg_program(name, version)
Program identity is initialization-only. Changing it after
od_init() is rejected; runtime status-line
content is controlled through the status-line API rather than by changing the
door's identity.
Behaviour¶
od_cfg_local_mode(enable)
od_cfg_inactivity(int warn_sec, int timeout_sec)
od_cfg_max_time(uint16_t minutes)
Local mode is initialization-only. It may be set before
od_init() as a default or amended by
od_on_session_loaded(), then is frozen before transport setup begins.
Maximum time is likewise initialization-only: it caps the session during
loading and freezes after od_on_session_loaded(). Runtime grants or penalties
change the caller's remaining time through od_user_set_time_remaining().
Inactivity warning and timeout settings remain live. A door may change or
disable them during an active session, and the new policy applies to subsequent
idle processing. Both values are nonnegative seconds no greater than
INT_MAX; this range is valid even when int is 16 bits.
Callback Registration¶
od_on_exit(func)
od_on_idle(func) -- called from od_kernel()
od_on_hangup(func) -- carrier loss
od_on_time_warning(func) -- receives minutes_left
od_on_no_drop_file(func) -- custom drop file or local prompting
od_on_session_loaded(func) -- validate/amend state after drop-file loading
od_on_statusline(func) -- custom status line renderer
od_on_chat_start(func)
od_on_chat_end(func)
od_on_exit appends a handler to an exit-callback list; registering a new
handler does not replace handlers already registered. Registration is
available both before initialization and during an active session. The list
freezes when shutdown begins; later registration attempts return an error, so
exit callbacks cannot mutate the list being dispatched. Handlers run in
reverse registration order, matching atexit()-style resource cleanup.
They run before transport and presentation teardown while the session object
still exists. The library does not simulate a healthy transport during this
phase: after carrier loss, remote operations return OD_ERR_NOCARRIER, while
local-only operations and stored-value accessors remain available.
od_on_idle controls a single callback slot; registering another function
replaces the previous idle callback rather than accumulating handlers. It may
be replaced or cleared during an active session. A change made from inside the
idle callback takes effect on the next idle event.
od_on_hangup is also a single live callback slot. It may be replaced or
cleared until shutdown begins, at which point the slot freezes.
Carrier loss invokes the registered handler exactly once on the thread
executing the public call that reaches a cooperative processing point. It
wakes a blocking public call, which returns OD_ERR_NOCARRIER. OpenDoors does
not automatically shut down; the application may save its state and then call
od_exit() itself.
od_on_time_warning follows the same single-slot, live-replacement rule.
od_on_statusline is a single live callback slot. Registering NULL restores
the built-in status-line renderer.
od_on_chat_start is a single live callback slot.
od_on_chat_end follows the same rule independently; the two chat hooks do not
have to be registered as a pair.
od_on_session_loaded runs after the standard drop-file reader or
od_on_no_drop_file callback has populated session state, but before the
transport is opened and initialization-only values are frozen. It may inspect
and amend the resulting configuration. Returning a negative error aborts
od_init().
od_on_no_drop_file is bootstrap-only. It must be registered before
od_init() and cannot be installed or replaced
after initialization begins. od_on_session_loaded follows the same
registration rule because its only invocation point is within
od_init().
Initialization¶
int od_init(void) -- returns 0 on success, negative on error
int od_exit(int hangup)
od_exit() shuts down the OpenDoors session and
returns control to its caller; it never terminates the host process. Process
exit status and application-level cleanup remain the door application's
responsibility. It returns
OD_ERR_NOT_INITIALIZED if no session was started and
OD_ERR_ALREADY_EXITED if shutdown already completed, in addition to errors
encountered while cleaning up the active session.
The library is process-lifetime one-shot: a process may call
od_init() only once. A second
od_init() while a session is active returns
OD_ERR_ALREADY_INITIALIZED, and
od_init() after
od_exit() returns OD_ERR_ALREADY_EXITED.
V7 provides no reset or session-restart operation.
Calling od_init() consumes the process's single
initialization attempt even if initialization fails. OpenDoors cleans up any
partially acquired resources and enters a terminal failed state; configuration
and callback registration do not reopen, and another
od_init() cannot retry with altered data.
All od_cfg_*() and od_on_*() functions return an integer status. Each
setting documents whether it is initialization-only or may be changed during
an active session. Initialization-only settings reject late changes with an
error; live settings and callbacks are not frozen merely because
od_init() has completed.
The first call to any od_cfg_*() function initializes the complete internal
configuration to its defaults (screen rows 24, cols 80, encoding CP437, drop
type auto, etc.), after which the setter applies its value. If
od_init() is called without any prior
od_cfg_*() call, it performs the same defaults initialization first. This is
only lazy initialization of in-memory configuration values; it performs no
session initialization, I/O, transport setup, or other externally visible
work.
Because the complete default configuration is established before the first
setter applies its value, an application may explicitly set an option to the
same value as its default without needing a separate per-option "specified"
state. od_init() does not reapply defaults to
an already initialized configuration.
Initialization applies configuration sources in this order:
- Compiled-in defaults.
- Pre-init
od_cfg_*()overrides. - Values supplied by the drop-file reader or
od_on_no_drop_filecallback. - Amendments made by
od_on_session_loaded().
Each layer replaces only the values it supplies. Thus a pre-init override can
provide a default for metadata absent from an older drop-file format, while
od_on_session_loaded() is the deliberate place to override a value supplied
by the BBS.
Initialization then reads the selected session source, invokes
od_on_session_loaded(), validates and freezes initialization-only state,
opens the transport, and starts local presentation and the status line.
Session initialization is never implicit. Operational APIs do not call
od_init() on the application's behalf. This
keeps fallible session setup and the freezing of initialization-only options
explicit, rather than allowing an unrelated operation to trigger them as a
call-order-dependent side effect. Calling an operational API before a
successful od_init() is a contract violation:
the common entry guard invokes assert() in assertion-enabled builds. Release
builds perform no operation and report OD_ERR_NOT_INITIALIZED through the
function's documented error channel.
Simple stored-value accessors remain expression-friendly and return their
fixed-width value directly. If one is called before successful initialization,
an assertion-enabled build asserts; a release build returns zero (or NULL
for a string accessor) and records OD_ERR_NOT_INITIALIZED for
od_last_error().
User Info Accessors¶
const char *od_user_id(void) -- opaque, persistent
const char *od_user_name(void)
const char *od_user_alias(void)
const char *od_user_location(void)
uint16_t od_user_security(void)
uint16_t od_user_time_remaining(void)
uint16_t od_user_screen_rows(void)
uint16_t od_user_screen_cols(void)
int od_user_ansi(void)
int od_user_rip(void) -- 0, 1, or 2
Corresponding od_user_set_*() functions exist for all of the above.
Setters are used by the od_on_no_drop_file callback and for runtime
changes (e.g. granting bonus time).
od_user_set_time_remaining() accepts a uint16_t minute count.
od_user_set_security() accepts a uint16_t level.
The screen-dimension setters accept uint16_t values.
User/session setters modify only OpenDoors' local session state. They do not rewrite a drop file or send an implicit IPC update back to the BBS.
System / Session Accessors¶
const char *od_system_name(void)
const char *od_sysop_name(void)
uint16_t od_node_number(void)
int od_transport_type(void)
int od_transport_encoding(void) -- OD_ENC_CP437 or OD_ENC_UTF8
uint32_t od_baud_rate(void)
uint32_t od_connect_speed(void)
int od_local_mode(void)
int od_drop_file_type(void)
Encoding is a property of the transport, not of the user. A custom drop-file
callback uses od_cfg_transport_encoding() to report it. Like the other
transport configuration, it cannot be changed after initialization-only state
is frozen.
Terminal Output¶
int od_printf(fmt, ...) -- formatted output with backtick colour codes
int od_print(str) -- plain string
int od_putch(int32_t codepoint) -- single Unicode character
int od_write_emulated(buf, len, destinations)
int od_write(buf, len, destinations)
int od_set_color(fg, bg)
int od_set_attrib(attr) -- IBM-PC attribute byte
int od_set_cursor(uint16_t row, uint16_t col)
int od_get_cursor(uint16_t *row, uint16_t *col)
int od_clr_scr()
int od_clr_line()
int od_draw_box(uint16_t left, uint16_t top,
uint16_t right, uint16_t bottom)
int od_scroll(uint16_t left, uint16_t top,
uint16_t right, uint16_t bottom, int distance)
int od_screen_save_size(*size)
int od_screen_save(buf, bufsize)
int od_screen_restore(buf, bufsize)
int od_send_file(filename) -- display/send ASCII, ANSI, or RIP file
All public screen coordinates are zero-based: row 0 and column 0 identify the
upper-left cell. Rectangle APIs use inclusive left, top, right, and
bottom edges. Backends convert to one-based ANSI coordinates internally.
The OpenDoors screen state is the expected result of the terminal sequences the library supports, used for local presentation and screen operations. It is a best-effort mirror, not an authoritative statement about the remote terminal; OpenDoors cannot observe terminal-specific behavior at the other end of the connection.
Open question: Define the V7 color and attribute model before assigning
types to attribute parameters. An 8-bit IBM-PC attribute remains useful for
compatibility, but modern terminals may justify richer colors and styles; the
design must decide whether
od_set_attrib() is the core
representation or a legacy convenience layered over a broader model.
Text-oriented output arguments are UTF-8. The transport encoding is session
state obtained from the BBS or set by a custom drop-file callback; it is
reported by od_transport_encoding(), and OpenDoors performs the required
conversion.
The two counted-buffer functions are byte-oriented escape hatches. They use a common destination mask:
#define OD_OUTPUT_REMOTE 0x01
#define OD_OUTPUT_LOCAL 0x02
od_write_emulated() treats its input as already encoded for the transport
and passes it through terminal emulation. od_write() performs no encoding
conversion and sends remote output byte-for-byte without routing it through
the emulator. When OD_OUTPUT_LOCAL is selected for a raw write, the local
copy is nevertheless passed through terminal emulation so it can be rendered
and tracked; this does not alter bytes sent to the remote destination. Either
function may target the remote transport, the local screen, or both. This
avoids the opposite-polarity Boolean arguments used by V6's
od_disp() and
od_disp_emu().
RIP support remains at its limited V6 scope. OpenDoors obtains or detects the
remote RIP version, emits the RIP clear-screen and default text-window setup,
and lets od_send_file() select and pass
through a .RIP stream while using an ANSI or ASCII companion for local
presentation. It does not parse or render RIP graphics locally and does not
provide a general RIP drawing API.
Colour Syntax¶
Retained from V6. Backtick-delimited colour names within
od_printf():
od_printf("`bright green`Welcome, `cyan`%s`white`!\n", od_user_name());
Input¶
int od_get_input(od_input_t *event, uint32_t timeout_ms)
int od_get_key(int32_t *key, int wait)
int od_read(buf, size, bytes_read, timeout_ms) -- raw transport bytes
int od_get_answer(const char *options)
int od_input_str(char *buf, int maxlen, int min_ch, int max_ch)
int od_edit_str(char *buf, char *format, uint16_t row, uint16_t col,
int normal_attr, int highlight_attr, int flags)
int od_key_pending(void)
int od_clear_keybuffer(void)
The input event structure:
typedef struct {
int type; -- OD_INPUT_CHAR or OD_INPUT_KEY
int remote; -- from remote user?
int32_t key; -- Unicode codepoint or OD_KEY_* code
} od_input_t;
int32_t for the key value handles the full Unicode range on all
platforms including 16-bit DOS where int is only 16 bits.
This caller-owned result structure does not expose mutable library session
state.
od_read() is the consuming byte-stream counterpart to raw od_write(). It
returns transport bytes exactly as received, before character decoding or key
event interpretation.
Raw and decoded input may be mixed within one session and consume the same ordered stream. Decoded input commits consumption only when it has formed one complete input unit: every byte is a boundary in a legacy single-byte encoding, while UTF-8 synchronizes at complete code-point boundaries. A recognized terminal key sequence similarly commits as one key event. A timeout with an incomplete unit leaves its bytes buffered. The decoder does not consume later units speculatively merely to satisfy one event request.
Session Control¶
int od_kernel() -- cooperative processing tick
int od_carrier() -- 1 alive, 0 disconnected, negative on error
int od_set_dtr(high) -- DTR control (serial only)
int od_sleep(int ms) -- 0 yields; positive values sleep, max INT_MAX
int od_chat() -- enter sysop chat mode
int od_autodetect(flags) -- detect ANSI / RIP version
int od_statusline_update() -- force status line refresh
Popup Menus and Text Windows (require ANSI)¶
od_popup_menu(title, items, uint16_t left, uint16_t top, flags)
od_window_create(uint16_t left, uint16_t top,
uint16_t right, uint16_t bottom, title,
border_attr, title_attr, inside_attr)
od_window_remove(win)
Command-Line Parser¶
Built-in parser with long and short options. Standard options:
-l, --local Force local mode
-p, --port NUM COM port number
-b, --baud NUM Baud rate
-H, --handle NUM Pre-opened handle
-d, --drop PATH Drop file path
-n, --node NUM Node number
-t, --time NUM Max time (minutes)
-S, --socket Handle is a socket
-h, --help Show help
API:
int od_parse_args(argc, argv) -- parse and apply standard options
int od_on_arg(func) -- callback for door-specific arguments
int od_cfg_help(text) -- extra help text for --help output
The od_on_arg callback receives (argc, argv, *i) and may advance *i
to consume additional arguments. Returns 0 if handled, -1 if unrecognised.
Error Handling¶
int od_last_error(void)
const char *od_error_string(err)
Every fallible public operation returns an integer status directly. OD_OK
means success and negative values are errors. Functions whose primary result
cannot share that return value use an output parameter, as in
od_screen_save_size(). Simple stored-value accessors are not otherwise
fallible and return fixed-width values directly; contract violations use the
pre-init behavior described above. No operational function is void merely
because its V6 predecessor could report failure only through
od_control.od_error.
Error codes are negative integers: OD_OK (0), OD_ERR_MEMORY,
OD_ERR_NOANSI, OD_ERR_PARAM, OD_ERR_FILE, OD_ERR_NOREMOTE,
OD_ERR_GENERAL, OD_ERR_NOCARRIER, OD_ERR_TIMEOUT,
OD_ERR_NOT_INITIALIZED, OD_ERR_ALREADY_INITIALIZED,
OD_ERR_ALREADY_EXITED.
Status Line¶
The status line occupies one additional local-only row outside the emulated
remote screen; it never reduces or overwrites the caller's reported terminal
dimensions. The built-in renderer places it below the remote screen and shows
user name, time remaining, baud rate, and node number. A custom
od_on_statusline renderer may instead place the row above the remote screen,
in which case local presentation offsets the remote screen downward. The
default works without setup.
Optional Modules¶
Linked only if used:
Process spawning remains supported on every first-class platform, including
16-bit DOS. The DOS16 implementation must release enough conventional memory
to run useful child programs and then resume the door after the child exits.
V7 retains the existing ODSwap implementation, which swaps to EMS when
available and otherwise uses a disk swap file.
od_editor(buffer, bufsize, ...) -- multi-line editor
od_config_load(filename, handler) -- config file parser
od_reserve_init(shared_path) -- named reservation setup
od_reserve_acquire(name, timeout_ms) -- acquire named lock
od_reserve_release() -- release named lock
od_log(message) -- log file entry
od_spawn(cmdline) -- shell out
IPC Protocol¶
An alternative to drop files and handle passing for modern platforms. The BBS and door communicate over a local transport (Unix domain socket, TCP loopback, Windows named pipe) using a simple framing protocol. The BBS handles the user's actual connection (serial, telnet, SSH) and relays I/O to the door. The door never touches the raw user transport.
IPC is all-or-nothing. One IPC connection supplies session metadata, control events, carrier/disconnect state, and user input/output. It cannot be used only as a metadata source alongside a drop file or separate OpenDoors transport, nor only as a byte transport alongside separately loaded session metadata.
For an IPC session, this replaces drop files, socket handle passing, and carrier-detect polling. DOS sessions continue to use FOSSIL plus drop files.
Framing¶
Raw I/O data flows in both directions with no framing. Control and metadata messages are introduced by a NUL byte:
Raw I/O: bytes that are not NUL, passed through as-is
Literal NUL: NUL NUL (escaped)
Message: NUL + Type(1) + Length(2, big-endian) + Value(Length bytes)
NUL never appears in normal terminal I/O (ANSI sequences, text, user input), so the common path has zero framing overhead. Both sides scan outbound I/O for NUL bytes and double them.
Type 0x00 is not a valid message type: NUL followed by 0x00 is the literal NUL escape sequence. The usable type space is 0x01-0xFF.
Message Types¶
BBS to door:
| Type | Name | Value |
|---|---|---|
| 0x01 | SESSION | Tagged values (sub-TLVs, see below) |
| 0x02 | DISCONNECT | Empty; user hung up |
| 0x03 | TIME_UPDATE | New time remaining (minutes, 2 bytes) |
| 0x04 | TERM_RESIZE | Rows (2 bytes) + Cols (2 bytes) |
Door to BBS:
| Type | Name | Value |
|---|---|---|
| 0x81 | LOG | Severity (1 byte) + message text |
Severity values follow syslog levels (0=emergency through 7=debug).
Local changes made through od_user_set_*() do not generate protocol
messages; only explicitly defined door-to-BBS message types cross the IPC
boundary.
Session Message Tagged Values¶
The SESSION message value is a sequence of sub-TLVs:
Tag(1) + Length(2, big-endian) + Value(Length bytes)
Tags not recognised by the door are ignored. Tags not sent by the BBS get default values. This naturally handles missing/optional fields without the ambiguity of empty lines or the namespace collision risk of ad-hoc key=value extension.
| Tag | Name | Type | Default |
|---|---|---|---|
| 0x01 | USER_ID | string | (none) |
| 0x02 | USER_NAME | string | (none) |
| 0x03 | USER_ALIAS | string | (none) |
| 0x04 | USER_LOCATION | string | (none) |
| 0x05 | TIME_LEFT | uint16 | 0 |
| 0x06 | SCREEN_ROWS | uint16 | 24 |
| 0x07 | SCREEN_COLS | uint16 | 80 |
| 0x08 | ANSI | uint8 | 0 |
| 0x09 | RIP | uint8 | 0 |
| 0x0A | ENCODING | uint8 | 0 (CP437) |
| 0x0B | NODE | uint16 | 1 |
| 0x0C | BAUD | uint32 | 0 |
| 0x0D | CONNECT_SPEED | uint32 | 0 |
| 0x0E | SECURITY | uint16 | 0 |
| 0x0F | BBS_NAME | string | (none) |
| 0x10 | SYSOP_NAME | string | (none) |
| 0x11 | LOG_LEVELS | uint8 | 0 (bitmask of accepted syslog levels) |
Multi-byte integers are big-endian.
OpenDoors owns and advances the running time-remaining countdown on every
backend. An IPC TIME_UPDATE is an explicit authoritative correction and
replaces the current local value, including any earlier local adjustment.
IPC Bindings¶
The protocol is transport-agnostic. Defined bindings:
- Unix domain socket: BBS listens, door connects. Path passed via command-line argument or environment variable.
- TCP loopback: BBS listens on localhost, door connects. Port passed via command-line argument.
- Windows named pipe: BBS creates pipe, door opens it. Pipe name passed via command-line argument.
- Inherited fd (Unix): BBS forks door with an already-connected fd. Fd number passed via command-line argument.
Relationship to Drop Files¶
IPC and the legacy drop-file/transport combination are mutually exclusive
session paths. The door library abstracts over both; the door author's code is
identical either way. od_user_name(),
od_printf(), and
od_carrier() all work the same regardless
of which path is active.
| Platform | Legacy path | IPC path |
|---|---|---|
| DOS | FOSSIL + drop file | -- |
| Windows | Socket handle + drop file | Named pipe or TCP loopback |
| POSIX | Socket/stdio + drop file | Unix socket or inherited fd |
What Is Removed from V6¶
- Drop file formats: DORINFO?.DEF, EXITINFO.BBS (QBBS/RA 1.x/RA 2.x/ QuickBBS 2.75+), SFDOORS.DAT, CALLINFO.BBS, TRIBBS.SYS, PCBOARD.SYS/USERS.SYS
- ~90 user fields mapped from dead BBS systems (RA menu stacks, EMSI session data, combined records, timelog data, event scheduling, etc.)
- RA/QBBS control code parsing (
^F/^Ksequences) - Avatar terminal emulation
- FILES.BBS listing (
od_list_files) - BBS-specific status-display personalities (PCBoard, Remote Access, WildCat); the standard local status display is retained
- Registration key system
(
od_reg_key,od_reg_name) - Internal UART ISR driver (replaced by FOSSIL-only on DOS)
- Windows GUI frame window
- ~60 backward-compatibility
#definealiases - Implicit session initialization (lazy in-memory configuration defaults are retained)
- Return file / drop file write-back
The removed user fields include the caller's birth date. Its only substantive application use found in the historical-door audit was initializing game state, principally for features such as a birthday bonus. That narrow use does not justify exposing ambiguous legacy date formats through the core API; a door that needs a birth date can collect and retain it in its own user data.
The caller's sex or gender is also omitted. Real-world gender is not session state that OpenDoors needs to transport, and doors whose gameplay models a character's gender can store that character property in their own data.
RA/QuickBBS ^F and ^K expansion is removed because it is BBS-specific
presentation-file markup, not a terminal or transport facility that belongs in
the core library.
AVATAR terminal emulation is removed. A retained legacy drop format that reports AVATAR support is treated as ANSI-capable; OpenDoors no longer emits the dedicated AVATAR command encoding.
od_list_files() and its FILES.BBS
parser are removed. The function is an unused, fixed-width listing renderer:
it expands filesystem wildcards and prints names, sizes, descriptions, and
offline markers, but provides no file selection or transfer facility. That
presentation belongs in an application that actually owns a file area.
The commercial registration-name/key system and all registration-dependent presentation are removed because an open-source library has no registration state to enforce or display.
V6 compatibility #define aliases are removed. V7 exposes one canonical name
for each public facility rather than preserving names for an API and control
structure that it intentionally replaces.
BBS Software Audit (August 2026)¶
Audited: Synchronet (local source), ENiGMA 1/2, Mystic, WWIV, Talisman, x/84, GameSrv.
Formats generated by all modern BBSes: DOOR.SYS and DOOR32.SYS are universal. CHAIN.TXT is common. Everything else is niche.
Confirmed gaps in all existing formats:
- Screen width (columns): only in CHAIN.TXT
- Character encoding: in no format
- RIP version: only in EXITINFO.BBS (binary)
- DOOR32.SYS has neither rows nor cols
Socket handle passing is platform-fragile: Node.js (ENiGMA 1/2) and Python (x/84) cannot share socket descriptors, writing placeholders (-1 or 0). Talisman warns against DOOR32.SYS on Linux due to stdio/socket conflicts.
Command-line specifiers are a parallel channel: Synchronet (%H, %W, %>), Mystic (%0), WWIV (%H, %Z).
Read-back is rare: only Synchronet does it thoroughly.