OpenLib

✅ 可玩

openlib

更新 fdf581a 2026-09-12 源码 下载 ZIP 上游 tmcintos/OpenLib

▶ 开始游玩 · Play Now

A real [MudOS](https://en.wikipedia.org/wiki/LPMud#Server_software) v22.1a6-era mudlib by Tim McIntosh, formerly called "UltraLib". Released as a single alpha snapshot (v0.1) in late 1996 before development ceased.

English

A real MudOS v22.1a6-era mudlib (formerly "UltraLib") by Tim McIntosh, released as a single alpha snapshot (v0.1) in late 1996 before development ceased -- genuinely distinct from every other lib in this collection: a from-scratch English-language codebase built around a UNIX-flavored security and shell model rather than the usual Chinese-wuxia lineage. Login bootstraps by connecting as the literal username "root" (a privileged bootstrap account defined in security.h), then uses in-game commands (mkwiz/addmem/chmem) to promote a real admin character before retiring root's own privileges. Access control is entirely privilege-based (a custom D_ACCESS-style security_d.lpc consulting per-directory read/write protection tables walked up the path hierarchy) with no driver uid usage at all. Players get a real UNIX-like shell (nmsh, a "new mud shell" supporting cd/pwd/pushd/popd/aliases/history) as their command interpreter rather than a flat verb table, and the archive ships a working FTP daemon, finger daemon, and HTTP daemon as real listening services alongside the game itself. The shipped content is deliberately minimal (GETTING_STARTED admits "the lib is incomplete") -- a single starting room at the exact center of the mudlib's universe with two exits, and a handful of unfinished example object classes (a generic shop, money, a zombie) that reference the original author's own never-shipped personal test items under his own /u/ wizard directory. English-language content throughout.

README

Distinct from the rest of this collection in almost every way: an English-language, from-scratch codebase built around a UNIX-flavored security and shell model instead of the usual wuxia lineage. Login bootstraps as the literal username root, players get a real UNIX-like shell (nmsh) as their command interpreter, and the archive even ships working FTP/finger/HTTP daemons alongside the game itself.

Source: tmcintos/OpenLib on GitHub.

Highlights

Play online

https://mudlibs.fluffos.info/openlib/

Admin account

Warning: RootPass2026! is a public default password for local play
only. Change it before exposing this host publicly.

Status

WASM status: playable. The FTP/finger/HTTP daemons and their shared socket-server base class fail to compile without the sockets package -- a normal, non-fatal preload skip, not a boot blocker. Login and gameplay verified clean under WASM. See NOTES.md \S14.

Local run

cd libs/openlib
~/src/fluffos/build-debug/src/driver config.fluffos

Game port: 40245.

NOTES · 移植与修复记录

OpenLib -- porting notes

Source: git clone https://github.com/tmcintos/OpenLib (commit 044c60a5266e40f9b469503cf73fa06534d67df7, cloned 2026-08-27). Real MudOS v22.1a6 mudlib by Tim McIntosh, formerly called "UltraLib" -- released as a single alpha snapshot (v0.1) in late 1996, per the repo's own README ("There was an alpha release in late 1996 (v0.1) after which development ceased"). Top level bundles LICENSE and a short README.md alongside the real mudlib root, mudlib/. 776 raw files (LICENSE + README.md + mudlib/), 3.8MB excluding .git. Slug openlib, number 943, port 40245.

Genuinely distinct from everything else in this collection: an English-language, from-scratch codebase (adm/obj/daemon/master.c confirms real MudOS structure) built around a UNIX-flavored security/shell model rather than the usual Chinese-wuxia lineage. ls libs/ was checked for anything similarly named before starting; nothing close exists.

1. Conversion

Pure ASCII/English archive -- convert_lib.sh reported already_utf8=685 converted=1 lossy=0 skipped_binary=88 (the "88 binary" count is real: www/ has a handful of image assets, doc/man/ has troff-formatted man pages the text-extension sweep still handled fine, and a couple of true 0-byte/binary placeholders). Renamed 242 .c files to .lpc, fixed 62 literal .c" references, converted 16 local angle-bracket #includes to quotes, and ran the static->nosave sweep across 41 files with zero collisions (no "static path-literal hits, no #define nosave static compatibility shim).

One rename casualty needing a manual fix: adm/cmd/player/unsetenv.lpc was a raw filesystem symlink (unsetenv.c -> setenv.c) in the original archive -- the rename script renamed the real target file (setenv.c -> setenv.lpc) but left the symlink itself pointing at the now-nonexistent setenv.c, since symlink targets aren't text content the rename sweep rewrites. Fixed by recreating the symlink to point at setenv.lpc. Two more symlinks exist (doc/man/man2..man7 -> cat2..cat7) but those target directories, unaffected by the file rename.

2. include/runtime_config.h divergence (AGENTS.md §7.89), found and

fixed before ever booting

This archive bundles its own include/runtime_config.h (a copy of the driver's own header, since OpenLib shipped alongside driver source historically) with its own from-scratch get_config() slot numbering: BASE_CONFIG_INT = BASE_CONFIG_STR + 14 (i.e. CFG_INT(0) == 14), versus this driver build's real internal numbering (~/src/fluffos/src/include/runtime_config.h), where RC_BASE_CONFIG_INT = RC_LAST_CONFIG_STR + 1 == 256. Since this driver's #include <...> resolution would pick up the mudlib's own copy under include/ (per the config's include directories list) rather than the driver-canonical one, every get_config() call using this header's macros would silently read the WRONG config slot. adm/include/config.h's mud_name()/mud_port() macros are the only two symbols this archive actually calls get_config() through (confirmed via a corpus-wide grep for get_config() -- both map to index 0 in either header (CFG_STR(0)/CFG_INT(0)), so this particular archive would likely have degraded rather than crashed outright, but per the established fix (ds386, zjdywzb, yhwhpublicfi), the header itself was replaced wholesale with the driver's canonical copy rather than trusting the coincidence. Diffed the two headers' symbol sets first: the mudlib's own extra symbols (__ADDR_SERVER_IP__, __SAVE_BINARIES_DIR__, __ADDR_SERVER_PORT__, __RESERVED_MEM_SIZE__, __COMPILER_STACK_SIZE__, __EVALUATOR_STACK_SIZE__, __MAX_LOCAL_VARIABLES__) are used nowhere outside the header itself, so no alias/reconciliation was needed.

3. get_root_uid()/get_bb_uid()/creator_file() (AGENTS.md §7.2)

This driver build has PACKAGE_UIDS on, which requires these applies on master.lpc or the whole process exit(-1)s at boot. This mudlib's own access-control model is entirely privilege-based (a custom security_d.lpc consulting per-directory read/write protection tables, walked up the path hierarchy -- see §5 below) and never touches getuid()/seteuid() anywhere (corpus-wide grep: zero hits). Added the same flat "everyone is Root" stub used for nightmare3/nightmare4/ residuum:

string get_root_uid() { return "Root"; }
string get_bb_uid() { return "Backbone"; }
string creator_file(string file) { return get_root_uid(); }

4. Master's lazy SECURITY_D auto-load recurses forever (AGENTS.md

§7.1) -- 40MB of debug.log on the very first boot attempt

adm/obj/daemon/master/valid.lpc's valid_read()/valid_write() do check_priv(SECURITY_D->get_file_protection(file, N)) unconditionally on every single file access -- an implicit -> call that lazily compiles /adm/obj/daemon/security_d the very first time either apply runs (this daemon isn't preloaded). While security_d.lpc is mid- compile, the driver calls valid_read() again for every file IT needs (its own source, its #includes), which again finds SECURITY_D still not resident and tries to load it again -- unbounded recursion, exactly AGENTS.md's §7.1 shape (previously seen as an explicit load_object()/find_object() pair; here it's the same trap but triggered through a bare -> auto-load instead). Symptom on the first raw boot attempt: the driver never crashed outright, but spammed the identical valid_read error trace to log/debug for the full 8-second test window, producing a 40MB, 800,000-line log file before being killed -- a genuine near-miss for the kind of runaway-log/disk-fill incident this session's safety notes warn about, caught immediately by watching the log size rather than assuming "no crash" meant "fine".

Fixed with the standard re-entrancy-flag guard, applied to BOTH valid_read() and valid_write() (the archive's valid_write() has the identical SECURITY_D->get_file_protection() call on its own fall-through path, so it carries the exact same latent bug even though it wasn't the one that fired first):

private nosave int loading_security_d;
// ...
if( !find_object(SECURITY_D) ) {
  if( loading_security_d ) return 1;
  loading_security_d = 1;
  catch(load_object(SECURITY_D));
  loading_security_d = 0;
  if( !find_object(SECURITY_D) ) return 1;
}

SAVE_D (the other daemon valid_write/valid_read reference) is NOT at the same risk: it's only consulted when func == "save_object"/ "restore_object" specifically, so a save_d.lpc compile (itself with no #includes and no inherit) never re-enters that specific branch the way SECURITY_D's unconditional else-path did. Confirmed via a clean post-fix boot (774-line debug.log, only preload compile warnings, no error-trace spam) and via grep -c "valid_read\|recursion" staying at 1 (the fix's own guard-comment text, not a real hit).

5. §7.118 .c->.lpc filename-slice bug -- broke the entire

command-dispatch registry

adm/obj/daemon/cmd_d.lpc's hash_path() builds the per-directory command lookup table every wizard/player command dispatches through (find_cmd(), called from adm/obj/clone/user.lpc's cmd_hook() -- the single command dispatcher this whole mudlib routes through):

cmdpathmap[path] = map(filter(files, (: $1[<2..<1] == ".lpc" :)),
                       (: $1[0..<3] :));

The filter guard slices the LAST 2 characters of each filename and compares to the 4-character literal ".lpc" -- a 2-character slice can never equal a 4-character string, so the filter always returned an EMPTY array, and cmdpathmap[path] for any freshly-hashed directory was always ({}). find_cmd()'s member_array(name, cmdpathmap[path]) then always returns -1, so it always returns 0 -- every ordinary command would silently fail to dispatch, project-wide -- this archive ships NO pre-populated adm/data/cmd_d.o cache at all (confirmed: no such file exists anywhere in the raw clone), so cmdpathmap starts empty on every fresh boot and EVERY command directory gets hashed fresh, via this exact buggy function, the first time anything in it is ever looked up. Found and fixed via static code inspection before the first live boot, so the broken behavior itself was never directly observed live -- but the shape matches AGENTS.md's §7.118 catalog entry exactly: [<2..<1]/[0..<3] are correct only for the archive's original 2-character .c extension, silently wrong after this project's .c->.lpc rename (a 4-character extension).

Fixed by widening both slices from 2 to 4 characters:

cmdpathmap[path] = map(filter(files, (: $1[<4..<1] == ".lpc" :)),
                       (: $1[0..<5] :));

Verified via lpcc (clean compile, no change in pass/fail count) and live: every command dispatched during testing (look/score/who/ tell/save/quit/eval, both as root and as a fresh non-admin registration) exercised this exact function on its directory's first lookup, and adm/data/cmd_d.o (freshly written by reset()'s save_object() after the fix, not shipped by the archive) shows the correctly-stripped, extensionless command names for every hashed directory.

6. ed_start()/ed_cmd()/query_ed_mode() on the real player body

(AGENTS.md §6.2, same class as residuum's writeup)

This driver build has __OLD_ED__ (single-arg classic ed(), not the new synchronous ed_start/ed_cmd/query_ed_mode API this archive's adm/obj/clone/user.lpc was written against). This mattered far more here than on most archives with this same gap: adm/obj/clone/user.lpc (DEFAULT_BODY, login.h's SECURE_CLONE_DIR "/user") is the REAL player body class every login actually clones (a naive grep for the literal string "clone/user" misses this -- the only real reference is through the DEFAULT_BODY macro, used by login_d.lpc's player_enter_world2(): "guaranteed to load *grin*", per the original author's own comment). Fixed the same way as residuum:

Verified via lpcc (the file's only remaining warning is a pre-existing, harmless remove() return-type mismatch against living.lpc, unrelated to this fix) and live play (see §8).

7. SEVERE: password verification used a classic 2-character DES

crypt() salt against this driver's modern $6$ SHA-512 hashes -- no account could ever log back in a second time

Found live, during the very first restart-and-reconnect verification pass (exactly the class of bug that a single continuous test session can't catch -- see AGENTS.md §7.120's precedent). root registered fine, played, and quit cleanly in the first session; reconnecting with the exact same password on the very next connection (same driver process, no restart even needed to reproduce it) failed with "login incorrect."

Root cause, in adm/obj/daemon/login_d.lpc:

This is the single most severe bug found on this lib: it didn't just degrade some rarely-exercised feature, it made the mudlib's own registration/login flow completely non-functional for returning players -- every account was permanently a one-time-use account.

Fixed by passing the WHOLE stored hash back in as the salt argument, which is the standard POSIX crypt() verification idiom (crypt(3) itself only reads as much of the string as it needs for the salt and ignores the rest): ``lpc return ( crypt(passwd, crypted_pass) == crypted_pass ); ` Verified live end-to-end: killed and restarted the driver process (a genuine process restart, not just a reconnect within the same session), reconnected as root with the exact original password, and logged in successfully; repeated a second restart-and-reconnect cycle afterward to confirm stability. Also registered a brand-new, non-admin account (testuser) through the full name/password/email/ real-name/gender flow and confirmed look/score/quit` all produce correct output.

8. Verification -- full registration through world entry, both admin

and ordinary accounts, across two genuine process restarts

9. Known content gaps -- NOT fixed (author's own incomplete v0.1

alpha content, not driver-compat)

Per the archive's own GETTING_STARTED ("Since the lib is incomplete, you will have to 'ls' /adm/cmd/* and /cmd/* to find out what commands are available"), several files were shipped unfinished by the original 1996 author and fail lpcc_check.sh for reasons unrelated to this project's conversion -- confirmed genuinely pre-existing, not touched, per this project's standing "never fix content/design, never invent missing files" policy:

10. Other checklist items checked, no bug found

11. RAM safety

This archive is small (774 files, 3.8MB). lpcc_check.sh's batch sweep was run under a ps -o rss= watch the whole time; peak RSS stayed under 4MB throughout (finished in under 4 seconds both times it was run).

12. Deep functional test (round two, AGENTS.md §10.7)

One continuous session (a raw Python socket client against ~/src/fluffos/build-debug/src/driver config.fluffos), covering the newbie GETTING_STARTED flow end-to-end plus the mandatory restart-and-reconnect verification. root's password from the onboarding pass was not recorded anywhere retrievable, so its saved connection.o/body.o were backed up and reset to force a fresh registration; root's current password for future testing is RootPass2026! (documenting this now so the next tester doesn't hit the same gap).

Found and fixed: sprintf() missing its %s argument in

obj/clone/monster.lpc's die() -- crashed every undead-monster kill

tell_room(environment(this_object()),
	  sprintf("%s turns to dust before your eyes.\n"));

sprintf() with a %s directive and zero arguments throws a driver runtime error ("Not enough arguments to sprintf") on every call -- this fires unconditionally whenever any monster.lpc instance with undead set dies, a real driver-API misuse (efun argument-count mismatch), not a content/balance question. Fixed by passing the monster's own name:

tell_room(environment(this_object()),
	  sprintf("%s turns to dust before your eyes.\n",
		  query_cap_name()));

Verified live via eval: cloned a /obj/clone/monster, named it, set_undead(), then die() -- correct message ("Testzombie2 turns to dust before your eyes.") printed, monster's inventory dropped, object destructed, no runtime-error trace in log/errors/runtime or log/debug. (An earlier attempt in the same session hit the driver's "Bad argument 1 to EFUN call_other()" trace inside security_d.lpc's eval_unguarded() -- root-caused to a test-script mistake, not a mudlib bug: present() searches an object's id() list, set by set_name(), not the capitalized set_cap_name() string used in the first attempt's search term.)

Registration, privilege commands, and reconnect verification

Thirteen standing cross-cutting patterns, explicitly checked

Eight were already confirmed clean during onboarding (§10 above): §7.121 float-in-declared-int, §8.3a private dispatch-target, §7.123 bare file-scope initializer, §7.124 fraction/percentage mismatch, §7.126 stale .c-extension save data, §7.129 tell_room/message omitted-arg-as-0, §7.130 unconditional post-non-interactive liveness check, §7.131 find_living/find_player registration. This pass additionally checked the five newer ones:

Verification character/state left as evidence

root (password RootPass2026!, documented above) is the only account left in the saved data after this pass -- the throwaway walker (promoted wizard/Admin-domain member) and its wizard home directory, plus the runtime-only adm/data/security.o domain/priv save it created, were deleted before committing so security_d.lpc falls back to its pristine create() defaults (root-only admin) on next boot, matching the state this lib shipped in.

13. Round-two follow-up: ls crash for non-wizards, plus an

independent cross-check of §12

This session dispatched a research subagent mid-pass (originally scoped to just extracting the AGENTS.md §7.121-135/§8.3a pattern definitions) that ended up independently re-running this exact §10.7 pass itself in parallel on the same lib -- same driver, overlapping connections (a stray "Walker" registration and a mid-session quit visible in this session's own test transcripts), converging on the identical root password and the identical sprintf() fix in obj/clone/monster.lpc documented in §12 above, which it committed first (62e0d51659c). Confirmed via git log/git show that no work was lost or double-applied; this section is a genuine follow-up, not a redo.

One item in §12's writeup benefits from a small correction: the non-wizard pwd/ls/cwd gap was described there as "not a crash". That's true for pwd (bare cmd/wiz/pwd.lpc prints a literal 0, since int + string concatenation doesn't error on this driver) but NOT true for ls: a non-wizard's bare ls (no filespec) hit do_ls() in adm/cmd/wiz/ls.lpc:88, which called file_size(dir) on dir still holding its uninitialized int 0 value (the same this_player()->query_cwd()-returns-unset root cause documented in §12 -- nmsh.lpc's shell_init() only sets CurrentWorkingDirectory if(wizardp(owner))) -- confirmed live, freshly reproduced against a brand-new non-wizard registration (morgan), with the driver's own log/errors/runtime capturing **Bad argument 1 to file_size() Expected: string Got: 0. This is a real, unguarded crash (the player sees a bare runtime: error: Check /log/errors/runtime for more information. with no listing at all), not a graceful degraded message, and squarely a "missing stringp() guard" case per this project's own §10.7 scope boundary -- independent of whatever the correct answer turns out to be on the separate, genuinely-uncertain question §12 already flagged (whether non-wizards are meant to have real filesystem-navigation commands at all in this lib's design, which this fix does NOT attempt to resolve either way).

Fixed with a narrow guard in do_ls(), added right after path resolution:

if( !stringp(dir) )
  return write("ls: unable to resolve current directory.\n");

This only changes the failure mode from a raw driver crash to a clean one-line message; it does not touch wizardp() gating, ACL behavior, or grant non-wizards any new capability. Verified live: fresh non-wizard registration (morgan) hit the exact same code path post-fix and got the clean message instead of a crash; log/errors/runtime did not grow (stayed at 413 lines, unchanged from before the test); log/debug stayed warning-only. morgan's test save was deleted before committing, along with a leftover testuser.o save/connection pair from the original onboarding pass (§1-11) that had never been cleaned up -- root remains the only saved account.

The other wiz-only file commands sharing the same RESOLVE_PATH/file_size() shape (rm, rmdir, cp, mv, du, cat, head, tail, more, touch) all require an explicit filename/path argument rather than defaulting to the bare, no-arg do_ls(0, ...) shape that triggers this specific crash, and an absolute path argument bypasses query_cwd() entirely -- not independently re-verified live one by one this pass, but flagged here in case a future relative-path-as-non-wizard test surfaces the same shape elsewhere in this file family.

14. WASM status audit (2026-09-01)

playable. Booted ~/src/fluffos/build-wasm/src via scripts/wasm_client.js -- clean boot; the sockets-dependent daemons (adm/obj/daemon/net/services.lpc, .../intermud.lpc, adm/obj/inherit/server.lpc, adm/obj/daemon/net/http_d.lpc -- the FTP/finger/HTTP daemons and their shared server-socket base class) all fail to *compile* without the sockets package, but these are normal non-fatal preload failures the driver logs and moves past, not fatal to boot -- those network-service features are simply absent under WASM. Reproduced first as an apparent login failure that turned out to be unrelated to WASM at all: logging in as root with the README's documented password (Mud@2026) got login incorrect. on *both* the WASM and a fresh native boot -- \S12 above already documents that root's password was reset to RootPass2026! during the round-two deep-functional-test pass and the README was never updated to match. Fixed the README. With the correct password, a full WASM session verified clean: login, score, quit. (A compile: error: Check .../compile for more information notice appears on screen a few times during any session, on both native and WASM -- this is adm/obj/daemon/master.lpc's log_error() broadcasting a background daemon's periodic recompile-retry failure to whichever player happens to be connected at that tick; cosmetic noise, not a sign that the connecting player's own login/commands failed.)