QuestMud

✅ 可玩

questmud

更新 fdf581a 2026-09-12 源码 下载 ZIP 上游 vlehtola/questmud

▶ 开始游玩 · Play Now

QuestMud (source: <https://github.com/vlehtola/questmud>) is a real, English-language LPMud restored from a 2012 backup of a game whose "golden age" ran 1996-2000, per its own upstream README: up to 35 concurrent players and over a thousand individual registered accounts at its peak. It's a full game mudlib set in "The Isles of Deleria" -- 20 playable races (from Human and Dwarf through Golem, Lich, Demon, and Spirit rebirth races), 9 base guilds (Fighters, Mages, Clerics, Necromancers, Psionicists, Warlocks, Martial Artists, Abjurers, plus a handful of multiguild combinations like Paladin and Battlemage), a stronghold/ housing system, a mail system, and years of individual wizards' own personal build areas.

README

The port: LDMud, not MudOS

Every other classic-LPMud lib in this collection so far has been MudOS-lineage, close enough to this project's FluffOS target that the compatibility gaps were mostly small dialect differences. QuestMud is different: its original driver was LDMud (bin/ldmud 3000 per the archive's own StartMud script), a genuinely different LPC dialect -- closures (#'name, lambda/unbound_lambda/bind_lambda), a set_driver_hook()-based master-object architecture instead of FluffOS's hardcoded apply conventions, wide/multi-value mappings ((["key": v0; v1; v2]) with map[key, N] column indexing, no FluffOS equivalent at all), and several LDMud-only efuns (cat(), tail(), strstr(), to_string(), object_name(), unshadow()) that simply don't exist on this driver. Porting it was a substantially different job from every other lib here -- see NOTES.md for the full technical writeup.

Severe bugs found

Several genuinely severe, totally-blocking bugs were found and fixed, each of which alone would have made this lib permanently unbootable:

Registration flow

c (create) -> character name (2-10 letters, no digits) -> password (4+ characters) -> confirm password -> race selection room (select <race>, then continue) -> optional special traits (list, select <trait>, continue) -> guild selection (select <guild>) -> the real game world. g logs in as a guest; w lists who's online; q quits from the login menu.

Status

Boots clean under the native build-debug driver with a "lazy resets : 1" config override (see NOTES.md \S1 for why). Full registration verified live and end-to-end with a raw socket client: a brand-new character all the way through race selection, trait selection, and guild selection into the real game world, followed by working look (real room description, "No obvious exits.") and score (a complete, correctly-formatted character sheet -- level, race, guild, stats, HP/SP/EP, bank balance, alignment, age) and a clean quit that saves the character file.

A large compile-sweep tail remains: of the archive's 12,565 .lpc files, 5,829 compile clean and 6,736 do not. The overwhelming majority (6,021 of 6,736, 89%) are inside individual wizards' own personal sandbox directories under wizards/ -- each wizard kept full private copies of core files like player.lpc/living.lpc to experiment on, never loaded by the live game (the canonical obj/player.lpc/ obj/living.lpc etc. are the only copies actually referenced). Of the 715 failures outside wizards/, a large fraction are doc/examples/* tutorial/template content and a handful are the stronghold/ housing subsystem -- none block registration or the verified play session above. See NOTES.md \S3 for the fix patterns that would close most of this tail, if picked up as future work.

WASM status: playable. Shared WASM driver. Full new-character registration (c / wasmqm / password / select human / continue / continue / select fighter) into the Fighter guild of Duranghom (five exits, guildmaster visible), plus look / score ("level 1 Human", "Primary guild: Fighters") / quit ("Saving Wasmqm."), verified with scripts/wasm_client.js. The compile-sweep tail (LDMud leftover syntax in wizard/cmd files) still prints at boot and on quit's drop-all path; none of it blocked the login or play session. Play: https://mudlibs.fluffos.info/questmud/

Local run

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

Game port: 40247.

NOTES · 移植与修复记录

QuestMud -- porting notes

Source: git clone https://github.com/vlehtola/questmud (cloned 2026-08-28). Slug questmud, number 945, port 40247. Mudlib root is the clone's lib/ subdirectory (bin/, ftpd/, share/ alongside it are the archive's own bundled driver/tools, ignored -- this project uses its own FluffOS driver). Confirmed via lib/secure/master.c + lib/secure/ simul_efun.c before cloning; what that early check didn't catch is that this is an LDMud mudlib, not MudOS-lineage like every other classic LPMud in this collection so far -- a materially different porting job. MIT licensed. 22,198 files in the raw clone, 12,565 .c files under lib/.

1. Conversion

scripts/convert_lib.sh on raw/lib -> work: source is English-language and already ~99% ASCII/UTF-8 (735 lossy-conversion files out of ~21,000, mostly Finnish wizard-name comments with accented characters -- the original wizard team, per names throughout the archive like Ahma, Piikki, Nalle, Rag, Siki, was Finnish), 12,565 files renamed .c->.lpc, 11,041 literal .c" references fixed, 6 local angle-bracket includes converted to quotes, 231 files static->nosave.

2. The LDMud-vs-MudOS/FluffOS architecture gap

This is the first LDMud archive onboarded into this collection. LDMud and MudOS/FluffOS share LPC syntax at a surface level but diverge sharply underneath. Closure usage (#'name, lambda/unbound_lambda/ bind_lambda) turned out to be narrow in *scope* (36 files total, mostly secure/master.c and a handful of scattered wizard sandbox files) but every one of the driver-architecture-level gaps below was broad in *impact*, since they sit in secure/master.lpc/secure/simul_efun.lpc or otherwise get exercised by every single connection.

2.1 secure/master.lpc: set_driver_hook() doesn't exist

The entire original create()/inaugurate_master() chain existed to install set_driver_hook()-based behavior for object creation, uid-loading, and command dispatch -- none of which exists on FluffOS. Replaced with a no-op stub, since FluffOS already hardcodes the equivalent behavior directly:

2.2 master::valid_read() never had a case for "load_object"/"include" -- the single highest-impact fix in this whole port

FluffOS's vm/internal/simulate.cc (load_object()/clone_object()) and compiler/internal/lexer_utils.cc (#include resolution) both route through master::valid_read(path, eff_user, call_fun, caller) with call_fun values "load_object"/"recompile_object"/"include" that this LDMud-derived master's valid_read() switch had never needed a case for (LDMud doesn't route object compilation through valid_read() at all). Without a matching case, the switch falls through to its own default (deny), so every single object load and every single #include in the entire mudlib was silently denied from the very first boot -- every preload line failed with a bare "Read access denied", and every file's very first #include <ansi.h>/"log.h" failed with "Cannot #include ...", cascading into hundreds of secondary "Undefined variable"/syntax errors from the missing macros with no indication the real problem was access control, not the files themselves. Fixed by adding both cases to the existing permissive read policy (this lib's valid_read() already allows read_file/read_bytes/file_size to everyone):

case "load_object":
case "recompile_object":
case "include":
case "restore_object": return 1;

2.3 master::creator_file()'s "no special uid" case returned a bare int -- broke loading nearly every non-wizard file

FluffOS's uid system (packages/uids/uids.cc's set_uid()) hard-requires creator_file() to always return a real uid string; any other return type -- including the bare 1/0 LDMud used as its "no special creator, ordinary system object" sentinel -- fatally destructs the object being loaded with "Illegal object to load: return value of master::creator_file() was not a string", *even though the object had compiled cleanly moments earlier*. This hit essentially every non-wizards/-owned file the instant it got past its own compile errors (confirmed live on obj/timer, obj/wizlist, room/house_shop_d, daemons/mastery_d, and would have hit every other ordinary content file in the mudlib). Fixed by returning get_bb_uid() (the existing "Backbone" sentinel) everywhere the original returned a bare int or fell off the end of the function with no return at all.

2.4 move_object() is 1-arg on this driver -- a corpus-wide dialect fix that only partially works

FluffOS's move_object() efun takes one argument and moves this_object() only (void move_object(object | string)); the archive's LDMud/older-MudOS convention is 2-arg (move_object(item, dest), moving an arbitrary object). This project's established mechanical fix for this dialect gap is move_object(A, B) -> A->move_object(B), applied corpus-wide here too (9,873 sites/182 files via a paren-aware script, not a flat regex, so nested-call arguments were handled correctly).

This fix is only reliable when A is already this_object() at the call site. call_other() never falls back to an efun of the same name when the target object has no matching LPC function of its own -- it just returns 0 silently, no error, no exception. Confirmed live: a brand-new character's own myself->move_object("/obj/race_selection") (inside obj/player.lpc's own registration flow, where myself == this_object()) *appeared* to work -- no exception, normal control flow continued, the write()s after it printed fine -- but environment(myself) was still 0 immediately afterward. A bare move_object(dest) call from the same code worked correctly. This was diagnosed by literally inlining a debug_message() with a catch() around the call and comparing find_object()/environment() before and after.

Fixed for the by-far-most-common case: obj/living.lpc (inherited by every player and monster via living.h) now defines a real move_object(dest) method:

mixed move_object(mixed dest) {
    return efun::move_object(dest);
}

This makes X->move_object(dest) a real, correctly-resolving function call for any living (player, monster, NPC) target -- covering the overwhelming majority of ->move_object( call sites in this specific mudlib (players/monsters moving themselves or being moved by daemons). efun::move_object (not a bare recursive call) is required specifically *because* the new function shares the efun's own name; using efun:: here needed its own separate fix, see \S2.5.

Not fixed: any ->move_object( call site whose *target* is not a living (money, weapons, corpses, and other clonable items that don't inherit obj/living.lpc) still silently no-ops. This is a real, confirmed-live, corpus-wide gap that a full future pass should audit -- grep the whole corpus for ->move_object( and, for each target class that isn't already living-derived, either add the same real move_object(dest) wrapper to that class's own base file, or rewrite the call site to run from inside the target's own execution context. Left as documented, unfixed scope for this session given the size of the corpus (12,565 files) and that the verified registration/play path above doesn't depend on any of the remaining cases.

2.5 efun::name(...) outside secure/simul_efun.lpc requires master::valid_override()

The compiler's efun:: override syntax (compiler/internal/ grammar_rules_exprs.cc's rule_efun_override()) gates every use through master::valid_override(calling_file, identifier, main_file); with no valid_override() defined at all, this silently *denies* every use once master_ob is set (a NULL master apply return -> master_approved() returns 0), which happens the moment secure/master.lpc itself finishes loading. secure/simul_efun.lpc's own many efun:: calls (efun:: map_delete, efun::file_name, efun::command, etc., used throughout this file's compat shims) never hit this because simul_efun.lpc loads and compiles *before* master_ob is set (the "no master yet" bootstrap allow case in master_approved()) -- but any efun:: call anywhere else in the mudlib, compiled lazily later during actual play, hit "Invalid simulated efunction override" and failed to compile. First surfaced by obj/living.lpc's new move_object() wrapper (\S2.4) needing efun::move_object. Fixed with a permissive valid_override():

int valid_override(string calling_file, string identifier, string caller) {
  return 1;
}

2.6 Wide/multi-value mappings -- no FluffOS equivalent at all

LDMud mappings can hold multiple values per key ((["key": v0; v1; v2]), accessed via map[key, N] column indexing). FluffOS mappings are always single-value. Every instance found this session was converted to a single array value instead ((["key": ({v0, v1, v2})]), read back as map[key][N]):

daemons/leader_d.lpc's leaders mapping turned out to be a false positive for this pattern: it's genuinely single-valued (leaders[race] = name), but was *read* with leaders[race, 0] LDMud-style column-0 indexing anyway (valid LDMud syntax for a width-1 mapping, equivalent to plain leaders[race]) -- fixed by dropping the , 0 rather than wrapping the write side in an array. Worth checking for this exact false-positive shape (write side is single-value, read side still uses [key, 0]) before assuming every map[key, N] site needs the full array-value conversion.

2.7 FluffOS's reset() is scheduled/lazy, not synchronous-on-load

LDMud calls reset() synchronously right after create() for every new object (this mudlib's original master.c explicitly documented this: "1. reset() will be called first... 4. The game will enter multiuser mode"). FluffOS's reset() only fires on the normal reset-timer cycle ("time to reset", default here 3600s) unless "lazy resets" is enabled in config.fluffos, in which case it fires on the object's *first* apply_low()/move_object() touch instead -- but even with lazy resets : 1 set, several daemons still had their reset()- populated state read before reset() had run, crashing with *Value being indexed is zero on a raw-LPC-default (int 0) mapping. Confirmed live, repeatedly, as the actual blocker for the very first character's own registration flow (each fix below was found by tracing one specific crash during a real registration playthrough, not by static analysis):

This class is confirmed real and repeatable but almost certainly not exhaustively found -- every instance above was discovered by literally playing through registration and reacting to each new crash, not by a systematic corpus grep. A future pass should grep for every mapping/ array global that a file's own reset() populates and cross-check whether anything in that same file (or callable from elsewhere) reads it before reset() could plausibly have run.

2.8 Missing LDMud-only efuns, added as secure/simul_efun.lpc shims

2.9 creator()/domain(): the *other* self-destructing-registration bug

Once \S2.3's creator_file() fix was in place, obj/player.lpc's own reset() had a second, independent bug that also self-destructed every newly-registered character: if (creator(this_object())) { ... destruct(this_object()); } is a "reject this if it looks like a wizard's own ad-hoc clone of player.lpc, not a normal system-created character" guard, and the mudlib's own creator() simul_efun was a bare getuid() passthrough. Under real euid tracking every object always has *some* uid (never LDMud's original int-0/1 "no special uid" sentinel, per \S2.3), so creator() became truthy for literally every object, including perfectly ordinary clone_object("obj/player") calls from master::connect(). Fixed by filtering out the backbone/system uid specifically:

string creator(object ob) {
    string uid = (string)getuid(ob);
    return (uid && uid != (string)MASTER->get_bb_uid()) ? uid : 0;
}

3. Ordinary compile-time bugs (same class as every other lib in this

collection, just discovered via an LDMud lens)

Once the architectural gaps above were fixed, the remaining errors were the same familiar patterns already cataloged in this project's top-level AGENTS.md, just numerous because this is a 12,565-file archive:

4. Compile-sweep results

scripts/lpcc_check.sh: 12,565 files, 5,829 pass, 6,736 fail. Of the failures, 6,021 (89%) are inside individual wizards' own personal wizards/<name>/ sandboxes -- each wizard kept a full private copy of core files like player.lpc/living.lpc/guildrank_d.lpc to experiment on, confirmed via grep to never be referenced from the live game (only the canonical obj/player.lpc, obj/living.lpc, etc. are actually loaded) -- the same "wizard-sandbox clutter" shape this project's AGENTS.md already documents for several other libs, just at a larger absolute scale given this archive's size. Of the remaining 715 failures outside wizards/, the largest identifiable groups are doc/examples/* (114 files, tutorial/template content, several genuinely incomplete even in the original archive per their own comments) and the stronghold/ player-housing subsystem (5 files). None of the 6,736 failures block the registration/play path verified live this session (see README.md). The fix patterns in \S2/\S3 above would likely close a large fraction of this tail if picked up as a future sweep -- the declared-scalar-but-actually-array pattern and the forward-reference script in particular are corpus-wide, mechanical, and already proven safe on the files fixed this session.

5. Known limitations / deliberately left for future work

§10.7 deep functional test (2026-08-31, round two)

Full continuous playthrough against ~/src/fluffos/build-debug/src/ driver config.fluffos (the shared driver, no dedicated worktree needed), several fresh throwaway registrations, following AGENTS.md's own note that \S2.7's lazy-reset class was "confirmed real but not exhaustively audited." It wasn't -- this pass found the single most severe, most far-reaching bug in this whole port, well beyond what \S2/\S7.158 already catalogued. Full technical writeup: new AGENTS.md \S7.192. Summary here.

SEVERE finding: room/room.lpc and obj/monster.lpc were both still missing the \S2.7 lazy-reset create() bridge -- this made essentially the ENTIRE explorable game world unreachable

Registering a fresh character (Bragoth, human Fighter) through the full chargen flow and joining the Fighter guild landed in a room with no description at all and "No obvious exits." -- a hard, silent, total dead end immediately after character creation, for every single new player, every time. Root-caused via vm/internal/base/object.cc's call_create(): it arms next_reset to an hour in the future BEFORE create() even runs, so try_reset()'s lazy-resets check is already false the instant any object finishes loading -- meaning reset() (which is what actually populates short_desc/long_desc/exits via add_exit(), per room/room.lpc's own header comment) never fires until a full hour after that specific room's first load, REGARDLESS of lazy resets : 1. room/room.lpc (inherited by 5,368+ files corpus-wide) and obj/monster.lpc (the base class for every NPC/ monster clone in the game) both had no create() of their own. Fixed both with the same pattern AGENTS.md \S7.177 already established for "universal base class, subclasses each override reset()" (a bare reset(0) can silently no-op on this driver -- route through call_other() instead):

void create() {
    call_other(this_object(), "reset", 0);
}

The reset() gap cascaded into a whole chain of previously-unreachable, independent compile/type bugs -- fixed one link at a time via live reproduction, not static analysis

Once reset() actually started running for the first time ever on this driver, every bug hiding inside it (permanently dead code before the fix) became a real, live, uncaught crash that aborts the calling room's reset() PARTWAY THROUGH -- silently skipping whatever comes textually after the crash point, including the room's own add_exit()/description setup that always follows its initial "populate my furniture/NPCs" clone_object() calls. Traced and fixed via repeated live reproduction (join guild -> crash -> read trace -> fix -> reboot -> repeat), not a static sweep:

Verified live, end to end, after all of the above

Full continuous session, fresh registration each time a fix needed a reboot: name -> password -> race selection (Available races: now lists all 22 real races, not empty) -> special traits -> guild selection (select fighter) -> the Fighter guild room now shows its real description AND all 5 exits -> east onto Gold street (a real citizen and city guard NPC both visible and correctly described, no crash) -> south into Central Square (the game's own central hub -- also showing its real description, all 4 exits, and a patrolling guard NPC) -> north back. quit produced a clean disconnect with log/debug.log never created (no uncaught errors) both before and after every fix in this chain. A ~220s idle long-sit boot watch (real driver stdout capture, not debug.log alone) produced zero new error lines beyond the already-catalogued, unrelated 66-line compile-sweep tail from \S4/\S5.

What was NOT reached, flagged honestly rather than silently skipped

Files modified this pass

room/room.lpc, obj/monster.lpc, obj/living.lpc, obj/monster_data.lpc, obj/base_object.lpc, wizards/siki/base_drink.lpc, cmds/std/_environment.lpc, secure/master.lpc, plus 267 files (guild masters, city NPCs, and wizard-realm monsters/NPCs archive-wide) for the chat_str array-type sweep -- see AGENTS.md §7.192 for the full technical writeup.

WASM measurement (2026-09-03)

meta.json was already playable from the 2026-08-31 deploy-unblock; the README still said "not attempted." Cold-boot under the shared ~/src/fluffos/build-wasm succeeded with no new mudlib-side compile fix. work/ is ~378MB and copies into MEMFS without OOM. Verified with scripts/wasm_client.js (--timeout 300 --idle 0.4): cwasmqm → password → select humancontinuecontinueselect fighter. Landed in the Fighter guild of Duranghom with the real room description, five exits, and Anrax the guildmaster visible. look reprinted that room; score showed "Wasmqm. You are a level 1 Human" / "Primary guild: Fighters" / HP 86(141); quit printed "Saving Wasmqm." The already-catalogued \S4 compile-sweep tail (LDMud leftover #' closures, No program in object on /guilds/obj/skillfun, /cmds/std/_drop, etc.) still prints at preload and on quit's drop-all path; none of it blocked the login or play session. Shop/combat/death were not exercised this pass (same gaps as the native §10.7 writeup). The MEMFS copy does not write player saves back to the host, so wasmqm left no work-tree debris.