Basis

✅ 可玩

basis

更新 85d6a35 2026-09-02 源码 下载 ZIP

▶ 开始游玩 · Play Now

Basis is a small, historically significant English-language LPMud mudlib written by John Garnett ("Truilkan") -- the same author as the already-onboarded `tmi2` (TMI-2), but an earlier, deliberately smaller 1993 project. Per its own `README`: "Basis was conceived as an experiment... It has some good features in it but is far from a finished polished mudlib... Basis is for experimenters who actually want to start with something smaller and less complete than TMI-2." Basis's `std/` base classes, groups/access permission model, and `bin` command-dispatch system were later inherited by TMI-2 itself (per Basis's own README: "TMI-2 uses Basis's emoted system... uses the groups/access model originated by Portals as does TMI-2... Both TMI-2 and Basis use some variant of the 'bin' command system").

README

This is a "core infrastructure" mudlib, not a game with content: a tiny two-room test area (/room/start, /room/void, connected by go east/go west), a working registration/login flow, and a real bin/-command dispatch tree (movement, communication, inventory, wizard tooling) -- but no quests, no combat, no score command (never shipped one). "Definition of done" here means every command the archive actually implements works, not a rich game world.

Provenance

Recovered from the live ftp.lysator.liu.se LPMud archive mirror (pub/lpmud/mudlibs/MudOS/basis-20.tar.gz, unchanged since 2010 per its HTTP Last-Modified header, itself an untouched copy of the original 1993 upload) -- the file happened to still be reachable directly over HTTPS, so no Wayback Machine recovery was needed. 233 files, real adm/obj/master.lpc tree.

Highlights (see NOTES.md for full detail)

Registration flow

name -> confirm (y/n) -> (first-time only) news/credits text -> password -> confirm password -> straight into /room/start. No gender/race prompts (this thin mudlib never implemented character attributes beyond a name).

Admin account

Basis's file-permission system is a /adm/etc/groups + /adm/etc/access named-group scheme (same lineage as TMI-2's), fully independent from the ordinary "which player-body class did you get" question (std/user.lpc / std/maker.lpc / std/admin.lpc, resolved once at first login from a per-account "pith file" at /data/users/<letter>/<name>.o, itself unrelated to the player's own save file). To seed fluffos as an admin:

1. Registered fluffos (password Mud@2026) through the normal flow -- this always creates a plain user-class body on first login. 2. Added fluffos to the (admin) group in adm/etc/groups (alongside the original author accounts, not replacing them) -- grants file read/write ACL. 3. Edited the account's pith file (data/users/f/fluffos.o) to change its stored body-class attribute from "user" to "admin", matching exactly how the archive's own pre-shipped truilkan account (John Garnett's own, still present with its original 1992-era crypt password we can't recover) is set up. 4. On the next login, the account gets destructed and rebuilt fresh as an /obj/i/admin body (since no admin-class save exists yet); seeded its resulting save file's a_permissions (2999, ADMIN(999)) and a_position ("Admin") attributes, again matching truilkan's own pre-shipped values exactly. 5. Verified: update /bin/user/senses/look succeeds (write ACL confirmed), and permission-gated player commands like emote (which ordinary new players cannot use at all -- see NOTES.md) also work.

Local play

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

Port 40261.

NOTES · 移植与修复记录

Basis -- porting notes

Source: basis-20.tar.gz, recovered directly from the still-live ftp.lysator.liu.se LPMud archive mirror (https://ftp.lysator.liu.se/pub/lpmud/mudlibs/MudOS/basis-20.tar.gz, Last-Modified: 05 Aug 2010, itself an untouched 2010-era copy of the original file). No Wayback Machine recovery was actually needed -- the CDX API had no capture of the file itself (only of its parent directory listing), but the live mirror still serves the exact file. Slug basis, number 959, port 40261. 233 files, real adm/obj/master.lpc tree, mudlib root at the archive's top-level basis/ directory.

Author: John Garnett ("Truilkan"), same person as the already-onboarded tmi2 (TMI-2), but this is a distinct, earlier (1993), and much smaller/thinner project -- explicitly described by its own author as "an experiment," "far from a finished polished mudlib," intended "for experimenters who actually want to start with something smaller and less complete than TMI-2." Basis's own std/access/emoted conventions were later inherited by TMI-2 (confirmed by the archive's own README).

1. Extraction and conversion

Pure ASCII/English archive, 0 lossy conversions (as expected -- convert_lib.sh's encoding pass was a no-op). 233 files renamed .c->.lpc, 43 literal .c" references fixed, 3 local angle-bracket includes converted to quotes, 33 files static->nosave.

2. Proactive on-sight checklist findings

3. Compile-time driver-compat fixes

4. Compile-sweep triage: known-acceptable remaining failures (11 of 237)

All confirmed either (a) fragment-only files meant to compile ONLY as part of adm/obj/simul_efun.lpc's monolithic #include chain (existence.lpc, communications.lpc, to_object.lpc, writef.lpc -- all reference macros/globals that only exist once pulled in via the parent file's own earlier includes; the parent file itself compiles and loads cleanly, confirmed live), or (b) genuinely unreachable dead/incomplete original content, none inherited by anything live and none reachable from any in-game command:

5. Live playthrough (native driver)

Full flow verified in one continuous session, port 40261: fresh registration (fluffos / Mud@2026) reaching /room/start -> look (correct room description) -> go east (moves to the second room, correct room-broadcast message to the departed room) -> look again (correct new-room description) -> quit (clean disconnect message). Re-login (the restore/existing-password path) verified separately: password accepted, look shows /room/start again (player position is not part of the minimal save data this archive implements -- confirmed intentional/pre-existing scope, not a regression, since core::attrs/save_data() never captured environment in the first place). debug.log fully clean (zero error lines) across every test session after all fixes above.

Also spot-checked: help (usage message with no args, as expected), who, uptime, version, inventory, say. All correct.

One additional finding, likely intentional design rather than a bug (no error signature; see AGENTS.md's "no error signature = design" standard): emote <text> (free-form emote) and mail require a_permissions >= 500 / >= 100 respectively, but NO code path ever initializes a new player's a_permissions attribute at all -- it silently defaults to 0. Every OTHER bin/user/* command (look, go, say, inventory, drop, get, tell, etc. -- 30 of 33 total) requires exactly permission 0 and works fine for a brand-new player; only these two are gated. Given the MOTD's own text ("added a security system (admin/promote, position, and so commands)") and that emote is the one command letting a player type completely unmoderated free text (unlike the canned-message temote/semote, which both require only 0), this reads as a deliberate 1990s-era anti-abuse gate requiring an admin to promote a vetted player before they get free-text emote/mail access -- not a registration bug. Left unmodified; documented here per the review standard rather than "fixed."

6. WASM

Boots and plays identically under the WASM build (~/src/fluffos/build-wasm/src, via scripts/wasm_client.js): full password-login (reusing the same seeded account) -> look -> quit, byte-for-byte the same output as native (modulo the expected WASM startup noise/mojibake prefix before the listener is ready). wasm_status: playable.

7. Admin account seeding detail

See README.md's "Admin account" section for the procedure. Key files touched: adm/etc/groups (added fluffos to the (admin) group, alongside the original truilkan:cynosure:sulam -- not replacing them), data/users/f/fluffos.o (the per-account "pith file" PLAYER_D maintains independently of the main save; its a_filename attribute, key 300, decides which player-body class std/user/std/maker/std/admin a RETURNING login gets -- critically, ONLY on return-login via check_password(), not on the very first login, since a brand-new account has no pith file yet and always gets "user"), and data/obj/i/admin/fluffos.o (the actual admin-class player save, seeded with a_permissions=2999 (ADMIN(999)) and a_position="Admin", matching the archive's own pre-shipped truilkan admin account's exact values byte-for-byte). Verified: update /bin/user/senses/look succeeds (file write ACL via the groups/access system), and emote (permission-gated, see §5 above) also works once promoted, confirming both independent permission axes (file ACL vs. per-command a_permissions gate) are correctly seeded.

Note: the archive ships a real 1992-era admin account (truilkan, data/users/t/truilkan.o, crypted password unrecoverable) -- left untouched, not reused, per this project's standard policy of never guessing or discarding a pre-existing real account.

8. New AGENTS.md catalog entries added this session

1. §6.2 addition: (: obj_or_path_expr, "method" :) closures only build a real call_other pointer on this driver when the head is a bare compile-time NAME; anything else (string constant, called function) silently becomes a no-op comma-expression. Fix: route through (: call_other, target, "method" :). 2. §6.2 addition: new/class as ordinary identifiers are unconditionally reserved keywords on this driver (object instantiation / typed-class syntax), unlike the compile-flag-gated ref (§6.7). Grep \b(new|class)\b in declaration position on any new archive. 3. §6.2 additions: extract(), log_file(), privp(), cat() are not real efuns on this driver (join the existing tail() entry) -- all reimplementable as simul_efuns per old MudOS manual semantics. 4. §7 candidate: a vestigial private inherit of an otherwise correctly-inherited base class can create an ambiguous SECOND copy of a needed public method, with this driver resolving unqualified calls to the wrong (private, uninitialized) copy -- check whether the private inherit is actually used before "fixing" by loosening its visibility. 5. §4.2 item 4 confirmed on new content: a convert_lib.sh .c->.lpc literal-string rename left a fixed-width extension slice ([len-2..len-1]) unwidened, permanently breaking a hand-rolled command-file cache -- every command in the game failed. Worth a proactive grep (\[\(?len\s*-\s*[0-9]+\)?\.\.) on any archive with a similar directory-scanning command cache.

9. §10.7 deep functional test (2026-08-31)

Full continuous-session playthrough on the native driver (port 40261), exercising literally every implemented bin/user/* command (not just the register/look/go/quit path already verified in §5), plus a maker and admin pass logged in as the seeded fluffos account, plus a WASM long-sit boot watch (scripts/wasm_boot_watch.sh basis 200). This is a genuinely thin lib by design (per the archive author's own README), so "deep test" here meant "does every implemented feature actually work," not a full RPG progression. Found and fixed 7 distinct, previously undetected programming bugs -- more than any prior pass on this lib, all invisible to the boot-log/registration-smoke-test bar because they only fire when a specific command is actually typed.

Bugs found and fixed

1. bin/user/senses/look.lpc: look <item> (without the literal at) crashed on literally every attempt, look.lpc:19-20. sscanf(args, "at %s", something) leaves something unassigned (0) whenever args doesn't start with "at " (i.e. any plain look dictionary instead of look at dictionary); the unguarded present(something, this_player()) right after then hard-errors *Bad argument 1 to present(), Expected: string or object Got: 0, swallowed silently for the non-wizard player (§7.171) -- the client saw NOTHING at all, not even a fail message. Fixed by only attempting the present() lookup when sscanf actually matched (== 1 && something), falling through to the existing "Nothing like that around here." message otherwise -- preserves the room-description's own documented UX ('look at dictionary') while eliminating the crash. New AGENTS.md class, §7.181. 2. 13 bin//std/bin/ command files silently locked at MAX_PERMISSION (999) for every player, including ones whose own permissions() correctly declares 0: finger.lpc, alias.lpc, unalias.lpc, tell.lpc (user-level); clone.lpc, update.lpc, ref.lpc (maker-level); new_master.lpc, galias.lpc, gunalias.lpc (admin-level); bug.lpc, idea.lpc, typo.lpc (report-based). Each overrides create() without calling ::create(), so /adm/std/security/bin.lpc's own create() (seteuid(ROOT_UID); set_permission((int)this_object()->permissions());) never runs -- permission stays at its raw declared default (999) instead of the command's own intended value, and the command's euid stays unset (0) instead of ROOT_UID. Symptom: tell, alias, unalias, finger all failed with "bin: Permission denied." for a brand-new player; bug/idea/typo failed the same way AND (even after the permission fix) their reports never got logged because of bug #6 below. Fixed by adding ::create(); as the first statement in each. New AGENTS.md class, §7.182. 3. 20 command files' actual entry point was named something other than do_command, the one name /adm/std/security/bin.lpc's execute() calls -- every one of these was 100% dead for every player, failing with *Undefined function called: do_command (swallowed for non-wizards, so the player just saw the driver's generic "Errmm?" fail message, indistinguishable from a genuinely unrecognized command). converse.lpc (do_converse); maker-level force.lpc (cmd_force), review.lpc (cmd_review), gauge.lpc (cmd_gauge), popd.lpc/pushd.lpc (cmd_popd/cmd_pushd), snoop.lpc (cmd_snoop), tsh.lpc (cmd_tsh), load.lpc (cmd_load), home.lpc (cmd_home), whatis.lpc (cmd_whatis), origin.lpc (cmd_origin), apropos.lpc (cmd_apropos), man.lpc (cmd_man); user-level give.lpc/remove.lpc (cmd_give/cmd_remove), nickname.lpc/unnickname.lpc (cmd_nickname/cmd_unnickname), history.lpc (cmd_history). Confirmed each renamed identifier is used nowhere else in the archive before renaming (safe, no external callers). This is clearly leftover TMI/Portals-lineage naming convention (the archive's own README says "many of the bin commands are based on the ones from the old TMI") that never got reconciled with bin.lpc's actual do_command contract -- worth checking on sight in any TMI-derived archive. New AGENTS.md class, §7.183. 4. bin/user/objects/ready.lpc: bare ready (no argument) crashed the same present()-with-0 way as bug #1, ready.lpc:15 (arg is 0 when the player types no argument at all, not merely sscanf-unassigned). Fixed with a stringp(arg) guard printing "Ready what?\n" first (matching drop.lpc/get.lpc's existing, correct pattern for the same efun). 5. log_file() (the simul_efun reimplementing the classic MudOS log-append efun, adm/obj/simul_efun/log_file.lpc) silently wrote NOTHING, every single call, project-wide -- confirmed via instrumentation: write_file() inside it always returned 0 (denied) with zero visible symptom (no catchable error; classic MudOS write_file()/its simul_efun reimplementation just returns false on denial). Root cause: this driver's valid_write() master apply receives current_object() as its "user" argument (AGENTS.md §7.176) -- since log_file() calls write_file() directly from ITS OWN code, current_object() at that point is the simul_efun object itself (/adm/obj/simul_efun), whose own euid is 0 (never set), not the actually-privileged calling command (confirmed ROOT_UID euid) that previous_object() would have shown. Every caller was silently affected: bug/idea/typo reports (never appended to log/Bugs/Ideas/Typos despite the interactive editor completing normally), tell.lpc's own error log, master.lpc, and 5+ adm/daemon/inet/* daemons' logging. Fixed by adding seteuid(ROOT_UID); inside log_file() itself before the write_file() call, replicating the "trusted native primitive" semantics the real MudOS efun had. New AGENTS.md class, §7.184 (a direct, simul_efun-specific consequence of §7.176). 6. bin/maker/help/apropos.lpc: APROPOS_D pointed at the stale path /bin/daemon/aproposd, while the real file lives at /adm/daemon/aproposd.lpc -- the exact same archive-internal path-drift shape already documented in §3 for the 19-file bin_m//adm/std/security/bin case, just a single fresh instance. Symptom: apropos <anything> crashed the driver's own master/virtuald fallback into the self-referential *Inherit chain too deep: > 30 recursion (same mechanism as §3's closure bug) on literally every invocation, for every maker. Fixed by correcting the macro's path. 7. std/i/shell.lpc never inherited std/i/nicknames.lpc -- unlike the genuinely-incomplete, never-finished std/i/history.lpc and std/i/tsh.lpc siblings already documented in §4 (both reference undefined variables or #include headers that never existed), nicknames.lpc is a complete, internally-consistent, correctly implemented mixin (query_nicknames/set_nicknames/ query_nickname/remove_nickname/empty_nicknames, all self-contained) that was simply never added to the inherit chain. Symptom: nickname <nick> <real> crashed with *Value being indexed is zero (call_other of an undefined function on the player returns 0, then nicknames[nn] indexes that 0), and unnickname <nick> always reported "No such nickname defined." even right after a (crashing) nickname attempt. Fixed by adding inherit "/std/i/nicknames"; to std/i/shell.lpc alongside the existing pager/edit/alias mixins. Left deliberately incomplete: the mixin's own do_nicknames() substitution helper (meant to actually replace a nickname with the real name in a typed command line) is private and still never called from process_input() anywhere -- wiring that in would mean completing a never-finished feature integration rather than fixing an omission, so nickname/unnickname now correctly manage the nickname list but the substitution effect itself remains unverified/unimplemented. Documented here rather than guessed at.

Confirmed NOT bugs -- genuinely incomplete pre-existing content (left alone)

WASM

scripts/wasm_boot_watch.sh basis 200 ran the full 200s with no driver crash, reaching the login prompt cleanly. One expected, already-documented WASM-only limitation fired exactly as AGENTS.md predicts (line ~349, "WASM builds ship without sockets... package"): adm/daemon/inet/inetd.lpc fails to compile under WASM (Undefined function socket_close/socket_write/socket_create/ socket_error), so /adm/daemon/inet/inetd never loads and the intermud-tell/finger feature is unavailable under WASM specifically -- master.lpc's own socket_preload() already wraps this in catch(), so it's non-fatal and the rest of the game is unaffected. Native-only limitation, not a bug; not fixed.

Regression check

All 7 fixes re-verified together in one final continuous session (register deeptest16, exercise every fixed command, quit) plus a separate maker/admin pass logged in as the seeded fluffos account exercising every one of the 13 previously-dead maker commands (apropos, review, force, gauge, popd, pushd, snoop) -- zero errors in the driver's own captured stdout across the whole session (native builds' debug.log is dead for the process's whole life per §10.9, so stdout is the only reliable error channel).