Book and Sword World

✅ 可玩

书剑天下 (ShuJian MUD)

bxsj

🔑 fluffos / Mud@2026 更新 1a05443 2026-09-01 源码 下载 ZIP

▶ 开始游玩 · Play Now

重庆电信 ShuJian MUD Wizard Group 自 1997 年起开发维护的经典武侠 MUD,与 bxsj1《书剑·经典》同源,共享大部分核心引擎代码。新人从"武馆前院"起步,沿东练武场、设有木人陪练的东武场、兵器库一路探索,是一套完整成型的新手引导区;木人会原样复制对手自身的属性技能来对练,是真正意义上的安全陪练。拜师系统有两条路:既可以循序渐进地拜入武当宋远桥等正统门派掌门门下,也可以直接找游戏内建的"礼物使者"报出门派名,瞬间加入十五个门派中的任意一个并领取满级绝技全套装备——这是官方明示的新手鼓励捷径。`ask <npc> about name/rumors/here` 等江湖打探指令几乎对所有 NPC 都有效,`rumor` 频道匿名传话、`chat` 频道显示真实姓名,社交玩法比较成熟。

English

A classic wuxia MUD developed and maintained since 1997 by the ShuJian MUD Wizard Group at Chongqing Telecom. New players train in a full beginner region at the martial hall, complete with a training dummy that mirrors an attacker's own stats for safe sparring, and can join any of fifteen sects either through formal apprenticeship or a built-in "gift envoy" shortcut for newcomers.

README

内容亮点

在线试玩

https://mudlibs.fluffos.info/bxsj/

管理员账号 / Admin account

警告:Mud@2026 是本地游玩用的公开默认密码。若要正式对外开放主机,
请先修改此密码。

本地运行

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

游戏端口:40004

NOTES · 移植与修复记录

bxsj.rar → bxsj

Status: DONE — boots clean, playable over telnet

Reached name entry, length validation, existing-ID detection, and the password prompt — full login flow confirmed working. This lib surfaced three genuinely new (to this project) driver-interaction bugs, all generalizable patterns now cataloged in AGENTS.md.

What was fixed

1. Encoding: 10865 converted, 737 already UTF-8/ASCII, 75 skipped binary, 36 lossy. Found the convert_lib.sh binary-detection bug here (AGENTS.md, "Encoding — file's text/binary guess is not reliable enough to gate on"): file misclassified several genuine GBK source files as data (unusual CRCRLF line endings apparently confuse its heuristic) and the script silently skipped converting them, leaving raw GBK bytes in work/. Fixed the shared script (forces known text extensions regardless of file's guess) AND manually converted the 9 affected files found here (feature/save.lpc, clone/user/ olduser{,2}.lpc, adm/daemons/bbsd.lpc, cmds/std/testsjmud{1,2}.lpc, cmds/arch/ucp.lpc, cmds/wiz/movedata{2,cq}.lpc) plus re-scanned libs #1-3 for the same issue (clean, 0 hits — this bug's impact was specific to whatever tool/era produced this particular archive). 2. .c.lpc: 9936 files. 834 literal .c" refs auto-fixed, 0 left over. 3. staticnosave: 199 files. 4. No §4 (load_object in valid_read) issue in this master — checked, valid_write/valid_read call SECURITY_D via find_object only (no lazy load_object), so no recursion risk there. 5. New bug (AGENTS.md §8, extended): master.lpc's log_error() — the driver's own error-reporting callback — calls CHANNEL_D->do_channel(...) unconditionally. If CHANNEL_D isn't actually loaded at the moment a PRELOADED file fails to compile (which is exactly when log_error() runs), that call needs a fresh compile, which is forbidden mid-compile → Object cannot be loaded during compilation → which re-enters log_error() → cascades. This fired repeatedly during boot for multiple different failing preload entries. Fixed with the same reentrancy-guard + catch() shape as AGENTS.md §4. 6. New bug (AGENTS.md §14 cousin): master.lpc's valid_override() was 2-arg (missing the main_file parameter needed for efun:: calls inside #included simul_efun fragments) AND defaulted to deny (return 0) rather than allow for anything not explicitly listed — stricter than lib #1's version. Added the 3rd parameter, plus an explicit allow-list for the simul_efun message-formatting wrappers (shout/write/tell_object/tell_room/say) that cmds/adm/ad.lpc needed via efun::shout(...). 7. New bug, the real blocker for this lib (AGENTS.md §8c) — took the most effort to find: securityd.lpc's valid_read() had if (this_player()) user = this_player(); unconditionally, which wrongly attributed a SYSTEM-initiated lazy load_object() (master loading BAN_D/band and UPTIME_CMD/cmds/usr/uptime, neither preloaded, both first touched from inside a brand-new connection's own login sequence) to that connecting player's own unprivileged permissions instead of the real (root-euid) caller — an exclude_read rule protecting /adm from ordinary players then denied it, which permanently stranded every single new connection at the login banner (no compile errors anywhere — looked like a total, silent hang). Diagnosed via a temporary instrumented master.lpc valid_read() (logged file/user/func/result to a scratch file) rather than guessing from the generic *Read access denied. message — found the exact two denied load_object calls in minutes. Fixed: only override with this_player() when the passed-in user doesn't already have a resolvable euid/uid.

Known remaining issues (documented, not fixed)

How to run

cd libs/bxsj
~/src/fluffos/build-debug/src/driver config.fluffos
python3 ../../scripts/mudclient.py 127.0.0.1 40004 --timeout 10 --send "n" --send "testuser" --send "quit"

Post-hoc fix: UTF8-native is_chinese/registration (AGENTS.md §15h)

Applied in a later batch pass across the whole project: is_chinese/is_chinese2 in the shared chinese.lpc simul_efun fragment used GBK byte-range checks that silently never match real Chinese text once strings are UTF-8 (this driver's str[i] returns a Unicode codepoint, not a GBK byte). This broke character registration specifically -- any real Chinese name was rejected. Fixed the range check to test the CJK Unicode block instead, and halved the GBK-byte-calibrated length bounds in check_legal_name to match. See AGENTS.md §15h for the full writeup; confirmed via a real interactive registration test (Chinese surname + given name reaching the next prompt).

Retroactive fix (found via archive #90, jinyongwenzi): this lib was completely command-dead after registration (AGENTS.md §15ae + a new commandd.lpc variant)

Archive #90 (金庸文字版.exe) turned out to be the literal same codebase as this lib (byte-identical master.c/securityd.c/chinese.c after CRLF normalization -- confirmed via diff, not assumed) processed much later in the project, after §15ae (the private nomask command-hook bug) had already been discovered and fixed elsewhere. That later pass found this exact lib had TWO independent bugs compounding into the same symptom -- every single post-login command (even look) silently doing nothing at all, with zero error anywhere:

1. feature/command.lpc's command_hook() was private nomask -- unreachable via add_action's external dispatch on this driver (§15ae). 2. adm/daemons/commandd.lpc's rehash() filtered get_dir() output via sscanf(cmds[i]+"$", "%s.c$", cmds[i]) -- a live sscanf pattern invisible to both the .c.lpc rename's quoted-string fixer and the bare-data-file fixer, since it's neither. After the rename this matched zero files forever, so commandd's command-search table was never populated and find_command() always returned 0 -- a second, independent cause of the exact same symptom. Fixing only one of the two would still have left every command dead.

Both fixed here (dropped private; changed the sscanf pattern to "%s.lpc$"), then re-verified with a full fresh registration (real name, one of "秦风"/"秦岭"/"秦河" tested across a few runs) followed by look, score, and quit all producing correct real output, and debug.log confirmed clean (0 error: lines). This lib's ORIGINAL testing pass (see above) never verified a post-login command -- exactly the blind spot §15ae warns about -- so this had been silently broken since this lib was first marked "done," undetected until the sibling codebase surfaced it. See AGENTS.md §15ae and the new commandd.lpc sscanf-pattern addition for the general writeup.

Re-verification pass (2026-07-23): driver rebuild + LPC formatter + WASM build

WASM-enablement pass (2026-07 standard: loopback-allow, throttle exempt, admin seed)

Gates patched (all short-circuit loopback / malformed-empty IP first, original logic intact below):

Notes:

Admin account: id fluffos / Mud@2026 / 浮浮, granted (admin) via adm/etc/wizlist. Verified update + goto work. Save files for the orchestrator to force-add: libs/bxsj/work/data/user/f/fluffos.o and libs/bxsj/work/data/login/f/fluffos.o (untracked new dirs; not gitignored).

Retest: fresh registration (id fluffos, name 浮浮) reached 武馆前院, look/score correct. Pre-existing content errors unrelated to this pass remain in debug.log (message() bad-arg in user.lpc user_dump(); call_other() bad-arg from NPC 桃花 carry_object).

Retrofit (2026-07-24): fail-closed loopback check (security correction)

The loopback-allow gate patched above originally also treated a non-string/empty/malformed query_ip_number() result as loopback (a defensive stand-in for the WASM driver bug). That driver bug is now fixed upstream, so this was tightened to fail-closed: only an exact "127.0.0.1" / "127."-prefix / "::1" match bypasses the gate; a malformed or non-string address now falls through to the original gate logic (treated as untrusted/remote) instead of being auto-allowed. Re-verified fluffos login still works after tightening.

深度功能测试 / Deep functional test (2026-07-24)

First real *playthrough* pass on this lib (all prior passes verified only registration + look/score/quit + admin login, or watched boot output). Played as an ordinary new player through most systems, native driver (build-debug), with a WASM re-verification of the one fix made. Test character: id qintest, Chinese name 秦风, password Abc12345kept (not cleaned up) as a representative playthrough character; it joined 武当派 (Wudang) via the newbie gift-envoy shortcut and has a reasonably rich state (skills, family, rank-table entry) that's useful evidence of a working system. Save files: work/data/user/q/qintest.o, work/data/login/q/qintest.o.

Bug found and fixed

cmds/usr/top.lpc — unbounded per-hour rank-decay loop crashed on literally every quit (and on top), file:line cmds/usr/top.lpc:395-401 (the live add_rank()) plus four sibling copies of the identical pattern in add_rank_beauty() (~179), add_rank_pk() (~224), add_rank_rich() (~271), add_rank_worker() (~338).

What was tested and confirmed working

Full continuous playthrough as qintest (ordinary new player), plus one wizard-flavored digression through the game's own "newbie gift" NPC (see below) to reach the menpai/skills systems without a long trek:

Methodology notes (for the broader pass this seeds)

WASM 修复摘要(迁移自 meta.json 的 group_note)

书剑 MUD 家族的基础版本。状态已从过时的 limited 修正——这份档案自己的 NOTES.md 记录了一次完整的原生驱动深度功能测试(2026-07-24),以普通新玩家身份走完了注册、移动、人物信息、战斗(木人安全对练机制)、门派/技能系统,并在过程中发现并修复了一个真实 bug(cmds/usr/top.lpc 的 add_rank() 系列函数:一个不设上限的按小时衰减排名循环,一旦这份 2008 年代的存档数据配上现代系统时钟运行,每一次 quit/top 指令都会撑爆求值成本上限——已限制在 240 次迭代以内)。只有商店/经济系统和死亡/复活没有被实测覆盖到(是测试覆盖缺口,不是已知损坏)。这份档案自己的 README 没有记录任何缺陷,本轮也没有发现新的。本轮在当前 WASM 驱动构建下重新验证了预先播种的管理员账号登录(fluffos/Mud@2026,look/score/quit 全部正常,"您目前的权限是:(admin)")——由于 NOTES.md 里 2026-07-24 的深度测试已经完整覆盖过注册流程,本轮没有重跑注册;同一代码家族的手足档案 bxsj1 本轮则重新完整验证过一次注册流程,结果一致。

§7.86 跨库扫描修复(留言板 post 崩溃)

Deep functional test round two (2026-08-14)

Independently re-verified every §10.7 checklist item against current code rather than trusting the 2026-07-24 writeup above. One proactive hardening fix applied; the one real bug found in round one (top.lpc's rank-decay crash) was re-confirmed still fixed via a genuine live regression test on the same character that originally triggered it, not just by grepping the diff.

Fix: adm/simul_efun/file.lpc's cat() had no null-guard on read_file()

Standard proactive hardening for this session's checklist (§7.11-adjacent): cat() called write(read_file(file)) unguarded — read_file() returns 0 for a missing/unreadable file, and write(0) is a latent throw risk.

// BEFORE:
void cat(string file) {
  write(read_file(file));
}
// AFTER:
void cat(string file) {
  write(read_file(file) || "");
}

log_file() in the same file was already correctly hardened — it delegates to LOG_D->log_file() (adm/daemons/logd.lpc), which unconditionally calls assure_file(file) before every write regardless of the optional db flag (the if (db) only guards the preceding dbquery() call — misleading indentation, correct semantics, verified by reading the actual statement boundaries rather than trusting the visual layout). No callers in this lib pass a truthy db flag, so the MySQL dbquery() branch is dead code, not exercised, not a live risk.

Checked and ruled out as not applicable to this lib: no log_error() severity-gate case-mismatch (none found in adm/single/master.lpc), no dbase.lpc password-write-guard shape (this lib's admin seeding doesn't use that pattern), no %-operator-on-corrupted-float shape beyond the already-corpus-fixed quest_times/win_times class (the only unguarded % hits found were unrelated weight-formatting arithmetic in d/city/weapon/weapon*.lpc, not attribute counters), no §8.9 food/water wrong-object read, no printf debug leaks in logind.lpc.

Re-verified live: top.lpc rank-decay crash (round one's fix) still holds

Logged in as qintest (the same character whose quit originally reproduced the crash on 2026-07-24) rather than trusting the diff was still applied. top correctly rendered the real ranking entry (综合评价 3470), and quit — the exact original crash trigger, since cmds/usr/quit.lpc unconditionally calls TOP_CMD->add_rank(me) — completed with zero new work/log/debug.log lines (checked the file's line count before and after: unchanged). The original crash was invisible in the player-facing output even when it was happening, so a clean-looking quit message alone isn't sufficient evidence; the debug.log line-count check is what actually confirms it.

Verification method

Booted native build-debug driver, admin login (fluffos/Mud@2026) with update /adm/daemons/logind as the real privileged-action check (succeeded — "重新编译 ... 成功!"; adm/simul_efun/file.lpc itself is #include'd into adm/single/simul_efun.lpc, a container object, so it can't be update'd directly — same limitation documented elsewhere this campaign). Two rapid consecutive admin reconnects, both clean, both landing a full fresh login (上次连线 timestamp updated each time). One login attempt as qintest timed out mid-password-entry on the login procedure's overall timer (clone/user/login.lpc's time_check()/time_out()) when sent via too many separate tool calls; resolved by sending the whole id+password sequence as a single batched tmux_mud.sh multi call with a slightly longer per-line wait — not a mudlib bug, a test-harness pacing issue (this lib's overall login-procedure timeout is comparatively tight). Noted a stale FATAL ERROR: SIGTERM: Process terminated line at the tail of work/log/debug.log predating this session's own boot — the driver (confirmed via readlink /proc/<pid>/cwd) stayed alive and responsive throughout testing, so this is leftover content from an earlier, unrelated process instance sharing the same log path, not a live crash; not investigated further since it didn't correlate with any actual failure during this pass. Driver killed by exact PID after testing; incidental fluffos.o/qintest.o save-timestamp churn reverted before commit.

Files modified this pass

Round three deep functional test (2026-08-18)

Went deeper than rounds one/two per the standing checklist: real combat death/reincarnation (not just code review), board post/read, shop dispatch, and a persistent-guild command smoke test — plus a project-wide sweep for three newly-catalogued bug patterns (AGENTS.md §7.111/§7.112, and the §8 logind.lpc enter_world() missing-save shape).

Bug found and fixed: §7.112 unguarded reincarnation-chain init() (2 live instances + 1 hardening)

Grepped every init() containing a call_out( corpus-wide (2,278 files have init(), 543 also schedule a call_out; narrowed to 81 candidates lacking any existing remove_call_out/query_temp guard, then to the ones whose scheduled function recursively re-schedules itself — a genuine multi-stage chain, not a one-shot timer). Two real hits, both in the death/reincarnation zone (/d/death/), exactly matching the reference shape from haiyang2/jym's wgargoyle.lpc:

1. work/d/death/npc/death.h (included by mengpo.lpc/孟婆, yanluo.lpc/阎罗, pusa.lpc/菩萨 — the three reincarnation-guide NPCs a dead player's ghost meets) — init() unconditionally scheduled a 5-stage death_stage() message/teleport chain (first stage 20-40s out, then 5s between stages) with no guard. Since the driver re-broadcasts init() to every NPC in a room whenever a living object's commands are enabled (including on a player reconnect), a player who disconnects and reconnects while standing in front of one of these NPCs — very plausible given the 20-60s window — would stack a second parallel chain: doubled dialogue, and a second reincarnate()/move at the end. 2. work/d/shaolin/npc/yu-zu2.lpc (狱卒, a Shaolin-jail guard) — same unguarded shape, its own private 5-stage death_stage() chain (60s between stages) ending in ob->move("/d/shaolin/woshi1"). Grep found no room anywhere in the corpus that currently places this NPC (a sibling yu-zu.lpc, no "2", is the one actually used in d/shaolin/jianyu1.lpc, and it already has its own correctly-guarded remove_call_out+call_out pattern) — yu-zu2.lpc looks like dead/orphaned code today, but fixed it anyway since the shape is real and harmless to guard.

Fix (matching the documented reference shape): a death_stage_active per-player set_temp()/query_temp() guard around the call_out() scheduling call in init(), cleared at every exit point of death_stage() (the ghost-already-resurrected early return, the target-left-the-room early return, and the final completion branch).

Proactive hardening in the same zone: work/d/death/gate.lpc (地门/鬼门关 — DEATH_ROOM, the very first room a dying player lands in) had the identical unguarded shape one level up — its own init() unconditionally scheduled a call_out("run", 1, me) that randomly routes the ghost into gateway/mpting (yanluo/mengpo). Narrower window (1s) and more benign outcome (a duplicated flavor-text line, and a possible unwanted second random reroute) than the death.h chain, but same class of bug and directly in the path being live-tested here, so hardened it with the same gate_run_active guard shape for consistency.

Live-verified end-to-end, twice:

Not a match — checked and ruled out: work/adm/single/master.lpc's standard_trace() (§7.111) formats error["object"] with %O (safe, never throws on a non-object), not an unguarded file_name(error["object"]) — this lib's error handler doesn't have the §7.111 shape at all. adm/daemons/logind.lpc's enter_world() (the §8 logind save-guard check) already unconditionally calls both user->save() and ob->save() right after setup() completes — not commented out, not missing.

Live-verified: economy/shop (closes another round-one gap, partially)

Round one flagged shop transactions as reviewed-but-never-actually-run. This pass: goto'd directly to /d/xiangyang/zahuopu (牛老板's general store) as admin, ran list (correct item/price/stock table), then buy deng long — item-id resolution worked correctly (vendor.lpc's is_vendor_good()/do_buy() dispatch fired without error), and since admin had no money, MONEY_D->player_pay()'s real insufficient-funds path executed and returned the correct in-character rejection ("穷光蛋,一边呆 着去!"). Confirms the shop's full command-dispatch and payment-check code path executes cleanly end-to-end; a successful purchase (money actually changing hands) is still not verified — no in-game money-granting wizard command exists in this lib (checked cmds/wiz/*, cmds/adm/*; no clone/cash/give tool), and legitimately earning coin would require a genuine trek/job. Left as a to-do, same as round one.

Live-verified: board post/read (new coverage)

As admin on the 巫师留言簿 (/d/wizard/wizard_room's wizard board): post <title> (title must be given inline, then drops into the real ed-style multiline editor via me->edit(...)) → wrote a body line → . to end — got "留言完毕" (posting complete), board count went from 124 to 125, and read 125 played back the exact posted title/body/author/timestamp correctly. Confirms bboard.lpc's post/read path (including the §7.86-fixed inherit/replace_program shape from the earlier corpus sweep) is still fully live-functional, not just "loads without crashing."

Smoke-tested: persistent-guild commands (cmds/group/*, new coverage)

Distinct from the menpai/sect system (bai/apprentice, tested in round one) — this lib also has a separate persistent-faction system (gcreate/glist/gmove/grant/gforce/abdicate/destory, dispatched by filename via commandd.lpc, not add_action). glist rendered a correctly-formatted (empty) faction leaderboard without error. gcreate with a syntactically-valid Chinese faction name (测试帮, ends in a legal suffix character) correctly rejected with "抱歉,当前只允许贵宾玩家才能创 建帮派" (VIP-only gate) — read gcreate.lpc: this is a genuine, deliberate VIP paywall, not a bug, so left alone per scope discipline. Confirms the command-dispatch and argument-validation paths for this whole subsystem are alive; full faction creation remains unverified (would need a VIP account).

Methodology notes

Flag for cross-lib awareness (not fixed elsewhere, per instructions)

The §7.112 pattern (unguarded reincarnation/staged call_out() chain in a room or NPC's init()) was found here in the death-zone specifically. Any other lib in this corpus with its own /d/death/-style reincarnation-guide NPCs (or any other multi-stage init()-scheduled process) is worth the same grep sweep (call_out( inside init(), minus files that already remove_call_out or check a temp flag first) — this is the third or so independent lineage this exact shape has now been found in per today's (2026-08-18) session notes.

Files modified this pass

§7.100 sub-threshold instance (2026-08-20)

Found during the §7.100 tail-sweep (below the original 166-lib survey's

=100-occurrence threshold, never checked). 24 live

replace_program(ROOM); occurrences across 24 files (d/wanshou/*.lpc, data/group/groom/*.lpc, d/cangzhou/dangpu.lpc) — same fatal redundant-replace-after-inherit shape as the rest of the §7.100 family. This lib's room-building tools (clone/misc/roommaker.lpc, cmds/debug/roommaker.lpc) never had the bug baked into their templates, so no factory-side fix needed. One pre-existing already-//-commented instance (d/xueshan/jlshan.lpc) left untouched. Fixed by deleting the redundant lines (binary-mode script, 24/24 matched git diff --stat). Verified via a clean native driver boot (zero new debug.log errors, port listening, killed by exact PID after ~8s).

``§7.112`` residual-gap closure (2026-08-20)

Corpus re-scan (grep -rl 'call_out("death_stage"' ... | filter for missing guard) found unguarded init()-scheduled death_stage() call_out chain(s) in d/death/npc/death2.h that the original two-wave sweep (see AGENTS.md §7.112) missed -- same reconnect-triggered duplicate-chain bug, different filename/lineage. Added the standard query_temp("death_stage_active")/set_temp/delete_temp re-entry guard, adapted per file's own exit points. Compile-verified via lpcc --batch.

Note: the touched header here (death2.h) turned out to be dead/unreferenced code in this lib -- the live NPCs (yanluo.lpc/mengpo.lpc/pusa.lpc) actually #include a sibling death.h, which was already guarded from an earlier fix. This edit is a harmless no-op; no live vulnerability existed in this lib for this file.

§7.30 uninitialized-mapping accessor sweep (2026-08-20)

Corpus-wide mechanical sweep of the feature/skill.lpc shared-lineage bug (confirmed independently on xiakexing2017/jqxz2015/haiyang2 via round-four testing): 4 accessor(s) in this file returned a raw never-initialized mapping instance variable (defaults to int 0, not ([]), until first assigned), crashing any unguarded keys()/sizeof()/indexing caller for a fresh/untrained character. Fixed at the accessor level (mapp(x) ? x : ([])) per the documented remedy. Verified via lpcc --batch static compile check only (not a live boot) as part of a large mechanical sweep; not individually functionally re-tested live on this lib.

AGENTS.md §7.19 sweep (2026-09-01): enable_player() reentrancy from init()

Same corpus-wide bug class as mhxy/wuhanzhan (AGENTS.md §7.19): this lib's feature/command.lpc enable_player() wrapper (around the raw enable_commands() efun) is reachable from an NPC's init() via a redundant create()-then-init()-calls-setup()/reset_me() chain -- confirmed via a body-aware static scan of every init() in this lib: 5 NPC files call setup()/reset_me() from init() (e.g. data/group/npc/hufa.lpc, d/gb/npc/xixia-wushi.lpc, d/gb/npc/xiaox-wushi.lpc), after create() already made the object living(). Calling enable_commands() a second time on an already-living() object makes the driver re-invoke that object's own init() as a side effect, re-entering the same chain while the original call is still on the stack -- genuine reentrancy, crashing with "Too deep recursion" (most likely on an NPC's first-ever preload/compile).

feature/damage.lpc's revive() and cmds/std/sleep.lpc's wakeup() both call enable_player() again while the object is still living(). This confirms a bare if (living(this_object())) return; guard would be the WRONG fix -- used the same true reentrancy-flag fix as mhxy instead: a nosave private int in_enable_player_now; set for the duration of the wrapper's body, guarding only genuine same-call-stack reentrancy while leaving every legitimate re-enable (revive/wakeup) unaffected. enable_player() had a single fall-through exit (no early returns), so one guard-at-top + one clear-at-bottom pair was sufficient. Verified via a single-file lpcc --batch compile check (PASS) -- not individually live-boot-tested.