ANetBBS Changelog
Versions are internal build numbers. Public releases are tagged
separately. Current release: v1.0b2.58 (July 2026). Full release: August 1 2026.
v287 — Web door terminal: pin to 80x25, real CGA palette (May 2026)
After LORD saved/loaded cleanly in v286.10 there were two remaining
visual issues:
- ANSI art bled into menus. Violet's portrait and her flirt
menu appeared overlapped. Cause:play_terminal.htmlstarted the
xterm at 80×24 but immediately ranfitAddon.fit()to fill the
viewport — typically 150+ columns. LORD'sgotoxy(50, 12)then
landed at the wrong column and the menu overprinted the art. - Gray where colors should be. xterm.js's default red/green/blue
are web-ish midtones, not the CGA primaries doors are written for.
Fixes:
- Pin xterm.js to 80×25 in
play_terminal.html. Removed the fit
addon and the window-resize handler. Wrapped the terminal in a
centered, shrink-to-fit container so it sits cleanly in any
browser width. - Real CGA palette — full 16 colors set on the xterm theme.
Matches what door authors saw on a PC (DOS bright red is
#ff5555, not whatever the browser thought). screen_rows = 25insynchronet_compat.py(was 24).- PTY initial winsize 80×25 in
door_runner.py. Some kernels
default new ptys to (0, 0) — without an explicitTIOCSWINSZ
the door readsconsole.screen_rowsas 0 and gotoxy breaks. - Font bumped to 16px Cascadia Mono / Consolas / Courier New for
a sharper CP437 render.
v286.10 — LORD: File.write at-cursor + exit-hook silence (May 2026)
Critical — File.prototype.write(str, len) in the compat shim
was this._content += str — append to EOF, ignore position.
But recordfile.js calls this.file.write(wr, len) for every
String / Date / Float field at a seeked record offset
(this.file.position = rec * RecordLength then a chain of writes).
Every string field on every record landed at the file's tail
instead of its proper slot. That's why the player-list rankings
showed character-name fragments mixed with timestamps, and
nonsense XP values: not corrupt bytes, just bytes in the wrong
columns.
Fixed write() to honor _pos and overwrite at the cursor
(padding with NULs if seeked past EOF), matching writeBin/writeStr.
writeln() now goes through the same path.
Also silenced the cosmetic [BBS] exit hook failed: player is not
defined line that prints at every clean quit — LORD's
js.on_exit cleanup references variables that are already out
of scope by exit. The hook's purpose was already-redundant
cleanup; swallow the error. Set BBS_DEBUG_EXIT_HOOKS=1 if you
ever want to see them.
Save data after this update: old player.bin / state.bin
files were written with the broken layout. They'll READ wrong
in the new code (and may corrupt further on write). Recommended
to delete and start fresh:
sudo -u stingray rm -f \
/home/stingray/anetbbs/anetbbs/games/sbbs_doors/lord/state.bin \
/home/stingray/anetbbs/anetbbs/games/sbbs_doors/lord/player.bin \
/home/stingray/anetbbs/anetbbs/games/sbbs_doors/lord/*.lock
v286.9 — LORD: check_gameover null-deref guard (May 2026)
Same pattern as v286.8's flirt-with-Violet patch:
check_gameover runs at the top of every door entry. If
state.won_by >= 0 (someone "won"), it does
wb = player_get(state.won_by) then immediately wb.name.
When the referenced player record is gone, the door crashes
with Cannot read property 'name' of null — and the next
entry crashes again because the bad flag is still in state.
Patched lord.js to null-check wb, reset state.won_by = -1,
put_state(), and let the door continue. The game effectively
re-opens for play instead of being stuck on a perpetual "game
over" screen.
v286.8 — LORD: flirt-with-Violet null-deref guard (May 2026)
In-game crash patch: flirt_with_violet() assumed
player_get(state.married_to_violet) always returns a record,
and dereferenced op.name directly. If state.married_to_violet
points at a stale record (record deleted between marriage and
the next flirt; or our fresh state file has a non-(-1) default),
player_get returns null and the door bombs with
Cannot read property 'name' of null.
Patched lord.js to:
- Check op === null after player_get
- Reset state.married_to_violet = -1 and put_state() (so the
flag stays cleared after the door exits)
- Show a generic Grizelda-kisses-you-painfully line instead of
the personalised you curse <name> text
This is an upstream LORD-JS bug we should ideally push back; for
now the patch is marked // ANetBBS patch: in the source for
easy review.
v286.7 — LORD: File.readBin / writeBin / Str + full mode parsing (May 2026)
Next missing piece: RecordFile.writeField → this.file.writeBin(val, N).
Synchronet's File has typed-binary I/O (readBin(N) / writeBin(value, N))
for fixed-width LE unsigned integers, plus readStr(N) / writeStr(s, N)
for fixed-width strings. recordfile.js (the library LORD uses to store
the per-record game state in lord.dat etc.) wires every field through
them.
Added in this round:
File.prototype.readBin(bytes)— read N-byte LE unsigned intFile.prototype.writeBin(value, bytes)— write same, overwriting at posFile.prototype.readStr(n)/writeStr(str, n)— fixed-width strings
with space-pad/truncate semantics
Also overhauled File.open(mode) to honor the full fopen language:
'r', 'w', 'a', 'r+', 'w+', 'a+' (with the 'b' suffix
ignored — same as glibc). Previously only the presence of 'w' was
detected, so opening rb+ was silently treated as read-only and writes
never flushed. Now tracks _can_write separately from the truncate /
append flags, and writeBin/writeStr/flush/close all gate on that
single signal.
v286.6 — LORD: File.lock / unlock / flush / truncate (May 2026)
After file_mutex, LORD's next stop was RecordFile.lock() →
this.file.lock(rec*RecordLength, RecordLength). recordfile.js
uses Synchronet's File range-locking on every record open/close.
Single-node ANetBBS has no contention so:
File.prototype.lock(start, length)→ returnstrue(always grant)File.prototype.unlock(start, length)→ returnstrueFile.prototype.flush()→ persists in-memory_contentto diskFile.prototype.truncate(n)→ trims_contentand the on-disk
file (LORD uses this for the "reset save" path)
All added to synchronet_compat.py File prototype.
v286.5 — LORD: file_mutex stub (May 2026)
After v286.4 cleared the RIP-probe stall, the next missing
built-in tripped: ReferenceError: file_mutex is not defined at
LORD's first get_state() call. Synchronet's file_mutex is an
atomic single-writer lock primitive — creates a .lock file
carrying an owner identity, returns false if a peer holds it.
Single-node BBSes never contend, so a stub that always grants
the lock (and writes contents if provided, since LORD uses
the lock file as a write-once data drop for things like war
reports, mail messages, and fairy logs) is enough.
Stub added to synchronet_compat.py, exposed on globalThis.
v286.4 — LORD: skip the 10-second RIP probe (May 2026)
The DOS LORD had a /NORIP command-line flag to disable the RIP
terminal-detection probe. The JS port doesn't — it unconditionally
sends \x1b[!\x1b[6n and blocks read_str(10000, /RIPSCRIP/) for
up to ten seconds waiting for a RIPSCRIP response that no modern
terminal (xterm.js, SyncTERM, NetRunner, mTelnet) sends. That's
the 10+ second stall users felt before the welcome ANSI appeared.
Patched anetbbs/games/sbbs_doors/lord/lord.js to comment out
the probe + the if-block that loads RIP icons. rip stays false
(its var default), the welcome screen and main menu render
immediately. To re-enable: uncomment the block — it's marked with
// ANetBBS patch: in the source.
v286.3 — LORD: input plumbing actually works now (May 2026)
v286.2 cached Queue("name") so same-name calls returned the same
instance — but missed the case where two scripts pass DIFFERENT
suffixes:
dorkit.js→new Queue("dorkit_input" + bbs.node_num)
→"dorkit_input1"ansi_input.js→new Queue("dorkit_input" + (argv[0] ?? ''))
→"dorkit_input"(argv is empty in our shim)
Different names = different cache entries = still two queues =
bytes still nowhere. Fixed in sbbs_stubs/dorkit/sbbs_input.js by
explicitly re-pointing ai.input_queue = dk.console.input_queue
after loading ansi_input.js, so processed keystrokes definitely
land where dorkit polls.
v286.2 — LORD: input + draw-speed fix (May 2026)
Two issues from a live launch of v286:
-
Input did nothing. LORD's welcome screen drew but
keystrokes were eaten. Cause: Synchronet'sQueue("name")
is a named IPC channel — twonew Queue("dorkit_input"+N)
calls in different scripts (dorkit.js + ansi_input.js) bind
to the same wire. Our shim's Queue was a plain JS class, so
the two became separate objects.ai.add(byte)wrote
processed keystrokes into instance B; dorkit polled instance A.
Forever empty. Fixed by name-caching: same name returns the
same Queue instance. -
Welcome screen drew slowly. ~20 s on the live BBS. Cause:
dk.console.printwrites to BOTHlocal_ioandremote_io.
In sbbs mode local_io is unused — but our compat forces
local_console.jsto load unconditionally (it's required at
the bottom of dorkit.js), leaving local_io defined. Every
print byte went through a 24×80 Screen grid with per-cell
setCellupdates. Patchedsbbs_console.jsto
delete dk.console.local_ioso writes go straight to
remote_io → stdout.
Test instructions if you're scripting smoke tests against the
shim: keep the test window >= 12 s for LORD's start path — it
has a hard-coded 10 s RIP-probe timeout before the welcome
display happens.
v286.1 — Games admin: silent-save fix (May 2026)
Clicking Save on /admin/games/<id>/edit did nothing — appeared
to be a no-op. Cause: the drop_file_type SelectField (and to a
lesser extent category) rejected empty/NULL values via WTForms'
default "must be in choices" validator. LORD's seeded row leaves
drop_file_type NULL, so editing it would silently fail
validation; the page re-rendered identically with no flash, no
error display, no apparent action.
Fixed in two places:
web/games_admin.py:GameForm—drop_file_typeandcategory
now usevalidate_choice=False, accepting empty / NULL as
"no value".templates/games/admin/form.html— added analert-danger
block at the top of the form that lists every field error
when save fails. Future invisible-save bugs become visible
immediately.
v286 — LORD: now boots under Node compat shim (May 2026)
Following v285 which bundled the LORD source and recommended real
Synchronet jsexec, this release closes the remaining gaps so LORD
actually renders under Node — no Synchronet install required. The
welcome ANSI screen draws; the input loop reads keystrokes; play
proceeds.
Compat-shim additions in anetbbs/games/synchronet_compat.py
serverandclientglobal stubs — without those, dorkit's
dk.system.modetest fell through to undefined and no console
driver loaded, so LORD ran to completion with zero output. Now
dorkit picks'sbbs'mode and loadssbbs_console.js.- Beefed-up
bbs.*:logon_time,get_time_left(),online,
sys_status,start_time.system.*:node_dir,data_dir,
text_dir,ctrl_dir,exec_dir,mods_dir,qwk_id,
os_version,matchuser/matchuserdata/usernameno-ops. - Beefed-up
user.*(security.password, stats.bytes_uploaded/
downloaded, laston_date, expiration_date, alias, location, …). console.right/left/up/down(n)aliases — sbbs_console.js calls
these names rather thancursor_rightetc.console.ctrlkey_passthruslot so doors can set the bitmask.load(true, "file.js", args)background form returns a stub
Queue for the on-exit cleanup hook.File.readln()returnsnullat EOF (not'') so LORD's
build_txt_indexloop terminates instead of spinning.File.positionbecomes a real getter/setter (used by
build_txt_indexto record byte offsets).File.lengthis now a property (not a method) — matches what
sauce_lib and others expect.- Load resolver prefers
<stubs_dir>/dorkit/<file>over the flat
<stubs_dir>/<file>so the dorkit-internal Screen/Graphic with
the right prototype methods wins over older bare copies.
Node-side input plumbing (sbbs_stubs/dorkit/sbbs_input.js)
Replaced upstream's busy-loop background-thread with a callback
that registers on dk.console.input_queue_callback and runs from
dorkit's own waitkey() loop. Sets stty min 0 time 1 once at load
so each readSync returns within 100 ms — no per-iteration stty
thrash, no busy-wait, the door is responsive.
Seed Game row flipped to active
_create_default_data inserts LORD with is_active=True now. The
door appears in /games/ ready to play on first start; sysops who
prefer jsexec can install it later and the door_runner auto-prefers
real Synchronet binaries when found.
Honest caveats
- The compat shim handles LORD specifically. Other Synchronet doors
vary; this is the foundation, not a guarantee everything works. - The Node
Queueis a single-process in-memory FIFO; doors that
rely on inter-process communication via named queues will need
more work. - File operations stay in latin-1 binary mode for CP437 fidelity;
pure-text doors that expect UTF-8 might surprise.
v285 — LORD: Synchronet JS port pre-installed (May 2026)
Bundles Synchronet's JavaScript port of Legend of the Red Dragon
inside the BBS, plus the upstream dorkit/ helper library it needs.
What ships
anetbbs/games/sbbs_doors/lord/— full upstreamxtrn/lord/
tree fromgithub.com/SynchronetBBS/sbbs(lord.js, lordsrv.js,
recorddefs.js, IGM subdirs, ANSI art, name lists; 16 MB total).anetbbs/games/sbbs_stubs/dorkit/— the upstreamxtrn/dorkit/
console drivers (screen.js, local_console.js, ansi_console.js,
ansi_input.js, attribute.js, graphic.js, …) so LORD's
require("screen.js")chain resolves.dorkit.js+recordfile.jssynced to current upstream.
Compat-shim improvements
The synchronet_compat.py shim grew the bits LORD (and any other
real Synchronet door) reaches for:
Queueclass — Synchronet's inter-script FIFO; backed by stdin
reads when the queue name starts withdorkit_input.strftime(fmt, unix_seconds)— C-style with the common
conversion specifiers (%H %M %S %Y %m %d %a %A %b %B …).js.load_path_list,js.on_exit(code),js.exec(),js.gc(),
js.global,js.auto_terminate,js.terminate_signaled.require()now accepts the scope-prefix form
(require(scope, "cnflib.js", "CNF")) used by LORD.load()consultsjs.load_path_listfirst, then the new
conventional<exec_dir>/{dorkit,load}/fallbacks.Queueexposed onglobalThisvia the existing global-registry
sweep sovm.runInThisContext'd sub-files see it.sbbs_stubs/cnflib.js: SpiderMonkeyfor each (var p in struct)
→ standardObject.keys(struct).forEach(...)so V8 parses it.
Pre-seeded Game row
_create_default_data inserts a "Legend of the Red Dragon" game
(game_type='door_synchronet') pointing at the bundled LORD. Sysop
must flip is_active=true once Synchronet's jsexec runtime is on
the host — see the updated [[LORD Setup]] wiki page for the three
ways to get jsexec (apt, build-from-source, or point SBBS_JSEXEC
env at an existing install).
Why jsexec instead of Node
The compat shim gets simpler doors running under Node, but LORD's
dorkit library binds its console driver to a full Synchronet
bbs/server/client/user/console global quintet and depends on the
forked input-thread model. Emulating that on top of Node's
single-threaded loop is a much deeper rewrite. Real jsexec is a
small standalone binary that gives upstream behaviour for free.
v284 — Wiki (May 2026)
A full community wiki at /wiki/ — collaborative documentation
with revisions, diff, search, and markdown + [[wiki-links]].
Models
WikiPage— slug-keyed page with current body, title, summary,
view count, lock flag, soft-delete flag, created/updated audit.WikiRevision— every edit gets one. Stores the full body
(no compression — sqlite is fine at this scale), edit summary,
author user-id, author IP, rev-num monotonic per page.- Auto-sweep adds both tables on next
anetbbs-webstart.
Renderer (anetbbs/wiki/render.py)
- Python-markdown with
fenced_code,tables,nl2br,
attr_list,toc,sane_lists. [[Page Title]]→/wiki/page-title. Missing pages render as
red dashed-underline links so editors notice.[[slug|display text]]and[[Page#anchor]]both supported.- Wiki-link preprocessing skips fenced code blocks and inline
\code`` spans so example tokens don't become real links. - Output sanitized by bleach with a whitelist that keeps headings,
tables, code, images, our wiki-link CSS classes, and heading
anchor IDs.
Slug helpers (anetbbs/wiki/slug.py)
- NFKD-fold + lowercase + dash-collapse.
- "Café — édition" →
cafe-edition. - "BinkP & QWK" →
binkp-and-qwk.
Routes (anetbbs/web/wiki.py)
| URL | What |
|---|---|
/wiki/ |
Home page render + recent edits sidebar |
/wiki/<slug> |
View a page (red-link template if missing) |
/wiki/<slug>/edit |
Edit form with live preview |
/wiki/<slug>/preview |
JS-called preview endpoint |
/wiki/<slug>/history |
Revision list w/ compare picker |
/wiki/<slug>/rev/<n> |
View a specific old revision |
/wiki/<slug>/diff/<a>/<b> |
Unified diff between revisions |
/wiki/<slug>/revert/<n> |
Roll back to revision N |
/wiki/<slug>/lock (admin) |
Toggle edit lock |
/wiki/<slug>/delete (admin) |
Soft-delete |
/wiki/<slug>/restore (admin) |
Undo soft-delete |
/wiki/<slug>/rename (admin) |
Change slug |
/wiki/new |
Create-new flow with suggested slug |
/wiki/all |
Alphabetical index |
/wiki/recent |
Every edit, newest first |
/wiki/search?q=… |
Full-text across title + body |
/wiki/wanted |
Pages linked-to but not created |
/wiki/orphans |
Pages no other page links to |
Templates — 13 Jinja templates extending base.html, all
dark-theme-aware. Wiki-specific CSS (red links for missing pages,
diff colourization) lives inline in wiki/_layout.html.
Auth
- Anyone (incl. logged-out) can read.
- Logged-in users can edit and create.
- Locked pages: only admins can edit.
- Lock, delete, restore, rename: admin-only.
Seed content — 41 pages covering: connecting via web /
telnet / SSH / rlogin / gemini / finger; reading and posting in
boards, echomail, netmail, PMs, instant messages, RSS, files;
playing doors (web, rlogin, DOS); sysop guide; door setup; BinkP
setup; LORD-specific recipe; DosBridge architecture; the codepage
story; NodeSpy; backup; full architecture overview; QWK; TIC
processor; IRC and MRC bridges; web terminal; and a glossary of
BBS jargon. 41 revisions in history on day one — each seed entry
gets an r1.
Nav
/wiki/link added to the Help dropdown next to Documentation.
Note on seeded pages — they're a starting point, not the final
word. Anyone with an account can improve them, and the wanted
pages report at /wiki/wanted shows the queue of pages that
existing pages already link to.
v283.7 — Echomail: CP437 body passthrough + Q-skip (May 2026)
Two issues from the Echomail reader:
CP437 / ANSI bodies were mangled. Echomail bodies received via
BinkP from FidoNet/Synchronet networks contain CP437 line-drawing,
block characters, and embedded ANSI color escapes — stored as
latin-1 mojibake (each original byte 0xNN → codepoint U+00NN).
The reader was passing them through session.write() which
re-encodes everything to CP437, scrambling the bytes. Body lines
now go straight to the writer via line.encode('latin-1'), so
the original bytes reach the user's CP437 terminal unchanged.
Falls back to cp437 encoding with replacement if the line has
genuine unicode codepoints above 0xFF.
Q-skip on -- more -- prompts. Walking past 100 areas to
find #22 was painful. Every paging prompt in the echomail flow
(area list under E, message index after picking an area, message
body, area list under C compose) now reads:
-- more (Enter, Q=stop listing) --. Pressing Q at any of them
breaks out of the loop and goes straight to the picker (or back
to the message index, for body view). Mirrors the bulletin
reader's existing [Q]=quit behavior.