@@ -168,3 +168,258 @@ justification. Retrofitted to fail-closed: loopback is now strictly
168168through to the NORMAL gate instead of being treated as local. Retested
169169after tightening: fresh driver boot clean, ` fluffos ` loopback login and
170170its wizard ` update ` command both still work; zero new debug.log errors.
171+
172+ ## 深度功能测试 / Deep functional test (2026-07-24)
173+
174+ First real * playthrough* pass on this lib (all prior passes stopped at
175+ registration + ` look ` /` score ` /` quit ` ; see AGENTS.md §10.7). Native driver
176+ (` build-debug ` ), one continuous ` scripts/mudclient.py ` session per test
177+ step (state persists across steps via the lib's own save/relogin path).
178+
179+ ** Test character** : id ` shenlim ` , Chinese name 沐阳 (Muyang), password
180+ ` MudPass123 ` , email ` shenlim@example.com ` , gender male, gift allocation
181+ accepted as rolled (体格20/根骨20/悟性25/灵性25). Final state left behind:
182+ joined 东方武馆 (Dongfang Martial Hall, via 东方博玉's `ask boyu about
183+ join`), learned 扑击格斗之技 (unarmed, level 1) from teacher NPC 东方聪
184+ via the organic ` learn unarmed from dongfang cong ` path, ` combat_exp ` 6
185+ (from one real fight, see below), one ` 粗布衣 ` (linen robe) in inventory,
186+ died once and was successfully reincarnated (气/血 currently 重伤/受伤
187+ from that), sitting at ` 聚见亭 ` (START_ROOM) after a final clean ` quit ` .
188+ Save files: ` work/data/user/s/shenlim.o ` , ` work/data/login/s/shenlim.o `
189+ — kept intentionally as evidence of a working end-to-end playthrough.
190+
191+ ### What was tested and confirmed working
192+
193+ - ** Read ` help newbie ` first** (` work/doc/help/newbie ` ) — correctly
194+ described the actual starting flow (南城客栈 is flavor-text/legacy;
195+ the real, current onboarding path is 聚见亭 → ` out ` → 仙石 in 花果山)
196+ and named the real command set (` learn X from Y ` , ` wimpy ` , ` skills ` ,
197+ ` fight ` /` kill ` ).
198+ - ** Registration** : real Chinese name (沐阳), full flow (gb/big5 →
199+ age-gate → ` new ` → English id → Chinese name → password×2 → email →
200+ gender → talent roll ` 9 ` +confirm ` y ` ) — reached 聚见亭, ` look ` /` score `
201+ both correct.
202+ - ** Exploration / navigation** : read room ` .lpc ` source
203+ (` d/ourhome/xiaoting.lpc ` , ` d/dntg/hgs/*.lpc ` ) to route from 聚见亭
204+ through ` out ` (→ 仙石/花果山, the actual "leave home, start the
205+ journey" transition — confirmed intentional, not a bug, via
206+ ` work/doc/help/start ` 's narrative text) through 后山小路 → 林间小路 →
207+ 花果山 → 傲来国 → 北城门 → 北菀街 → 傲来台 → 东苑街 → 东方武馆, ~ 15
208+ rooms total. Compound diagonal exit names (` eastup ` , ` southdown ` , …)
209+ initially looked like a rendering bug (`这里明显的出口是 eastup、down
210+ 和 southdown`) but are a confirmed, lib-wide intentional convention
211+ (hundreds of legitimate uses across ` work/d/ ` ), not a bug.
212+ - ** Combat** : no dedicated safe-sparring dummy is actually reachable —
213+ ` d/city/obj/muren.lpc ` and ` d/obj/misc/muren.lpc ` (the stat-mirroring
214+ ` accept_fight() ` training-dummy pattern AGENTS.md §10.7 recommends
215+ looking for) exist but are never placed in any room's ` "objects" ` map
216+ (grepped the whole ` work/d/ ` tree for both paths — zero references);
217+ orphaned/dead content, not chased further (content gap, not a
218+ programming bug). Used ` help newbie ` 's own recommended fallback
219+ instead — the weakest listed mob shape (` 小猴子 ` /monkey,
220+ ` combat_exp ` 50, ` daoxing ` 0) at 后山小路 — ` kill xiao houzi ` produced
221+ a full real fight with correct turn-by-turn narration, skill-up
222+ message, and (since the character had no armor/skill yet) an actual
223+ ** death** , which flowed into a full live test of the death/
224+ reincarnation system (see below) rather than a wimpy-flee.
225+ - ** Death/reincarnation** : ` feature/damage.lpc ` 's ` check_gameover() ` →
226+ ` DEATH_ROOM ` (` /d/death/gate ` ) → ` d/death/npc/pang.lpc ` 's
227+ ` init() ` -triggered ` death_stage() ` ` call_out ` chain (5 staged
228+ messages, 5s apart) → ` reincarnate() ` → move to ` REVIVE_ROOM `
229+ (` /d/ourhome/kedian ` , 荒郊小店) all fired correctly and automatically
230+ (no player input needed, matching the design). Confirmed the
231+ player's saved ` startroom ` field was ** not** touched by any of this
232+ (` grep -o '"startroom"' work/data/user/s/shenlim.o ` → no match, both
233+ before and after) — no instance of the AGENTS.md §7.24 death-code-
234+ overwrites-startroom bug class here.
235+ - ** Sect/faction join** : ` ask dongfang boyu about join ` (东方博玉,
236+ 馆主/hall master, in 前厅) — age/combat_exp gate checked and passed,
237+ ` wuguan/join ` flag set, confirmed via the teacher NPC's ` skills `
238+ command then unlocking (` 只有巫师或有师徒关系的人能察看他人的技能 `
239+ before joining → full skill list after).
240+ - ** Skill learning, organic teacher path** : `learn unarmed from
241+ dongfang cong` in 武馆教练场 — real skill-up message, ` skills`
242+ confirms the new skill persisted.
243+ - ** Shop purchase** : ` buy jitui from xiao er ` in 荒郊小店 (` list `
244+ showed prices in 文钱/两银子, purchase correctly deducted mixed
245+ coin+silver currency) then ` eat jitui ` correctly raised the 食物 stat
246+ (confirmed via ` hp ` ) and correctly refused once full
247+ (` 你已经吃太饱了,再也塞不下任何东西了 ` ).
248+ - ** Quit / relogin / state persistence** : clean ` quit ` → immediate
249+ ` debug.log ` grep (zero new ` 错误 ` /` error ` /` Fatal ` /` recursion ` lines)
250+ → relogin confirms skill + inventory persisted (location resets to
251+ ` START_ROOM ` on a clean quit+relogin, since ` startroom ` was never
252+ explicitly saved by this character — confirmed this is the lib's
253+ actual intended behavior, not a bug: ` cmds/usr/quit.lpc ` never writes
254+ ` "startroom" ` , and no ` valid_startroom ` -flagged room's "make this
255+ home" command was ever invoked in this playthrough).
256+ - ** Net-dead (unclean disconnect) + reconnect** : tested both a prompt
257+ reconnect (well inside the 15s ` do_net_dead ` grace) and a reconnect
258+ after a real ~ 150s wall-clock wait (well past the grace, short of the
259+ 600s ` NET_DEAD_TIMEOUT ` ) — both correctly restored the exact prior
260+ room/state via ` obj/user.lpc ` 's ` reconnect() ` (` remove_call_out ` s the
261+ pending dumps, prints ` 重新连线完毕 ` ). No instance of the AGENTS.md
262+ §7.20/§7.21 void-parking or stuck-wizard-flow classes.
263+ - ** Full-duration net-dead timeout** : happened for real (if
264+ accidentally, from research time between test steps rather than a
265+ deliberate sleep) partway through this pass and ** found the two real
266+ bugs below** — see write-up. Re-verified the fix afterward with a
267+ * deliberate* , short-duration repro (` NET_DEAD_TIMEOUT ` temporarily
268+ patched from 600 to 20 in ` include/user.h ` , reverted immediately
269+ after — confirmed clean via ` git diff ` showing no residual change).
270+
271+ ### Bugs found
272+
273+ ** 1. Fixed — ` adm/simul_efun/message.lpc ` 's ` tell_room() ` wrapper passes
274+ a raw ` int 0 ` as ` message() ` 's exclude argument on every 2-arg call.
275+ This is AGENTS.md §7.12 ("Shared message/wrapper argument bugs"),
276+ recurring on a new lineage — not a new bug class.**
277+
278+ - Symptom, found live: ` debug.log ` /stdout showed
279+ `执行时段错误:* Bad argument 4 to EFUN message() Expected: object,
280+ array, Got: int(0).` , thrown from ` obj/user.lpc:236`'s
281+ ` tell_room(environment(), ...) ` inside ` user_dump(DUMP_NET_DEAD) ` —
282+ the exact same file/function AGENTS.md §7.12 already documents from
283+ ` dtsl ` 's deep functional test. Per that section, this abort happens
284+ in a ` call_out ` -driven context with no enclosing ` catch() ` , so it
285+ doesn't just log an ugly line — it ** aborts the rest of
286+ ` user_dump() ` ** , at minimum ` case DUMP_NET_DEAD ` 's follow-up
287+ ` QUIT_CMD->main(...) ` call right after it (the actual force-quit).
288+ Moments later in the same driver session, the whole native process
289+ ** crashed** (see bug 2) — matching §7.12's already-documented
290+ escalation almost exactly (that section's own dtsl finding: "two
291+ characters hitting the aborted ` user_dump() ` at nearly the same real
292+ moment ... immediately followed by an actual native driver process
293+ crash — a C-level double-free abort ... inside
294+ ` dealloc_object() ` /` free_svalue() ` "). This session had several
295+ overlapping net-dead/abandoned-registration connections in quick
296+ succession right before the crash (multiple registration attempts —
297+ ` linqing ` , ` muyang ` , ` shenlin ` — each left an interactive object
298+ net-dead when ` mudclient.py ` 's connection timed out), so the
299+ "multiple simultaneous aborted ` user_dump() ` s" precondition plausibly
300+ applies here too.
301+ - Root cause: `varargs void tell_room(mixed ob, string str, object
302+ * exclude)` ( ` adm/simul_efun/message.lpc:48` ) forwards ` exclude`
303+ straight into ` message("tell_room", str, ob, exclude) ` — an omitted
304+ ` varargs ` array parameter is the LPC value ` 0 ` , not ` ({}) ` , and
305+ ` message() ` accepts an * omitted* 4th argument but rejects one
306+ explicitly passed as the wrong type. Every 2-arg ` tell_room() ` call
307+ site in the whole lib (717 total call sites of ` tell_room( ` , an
308+ unknown but nonzero fraction of them 2-arg) was a live, if
309+ intermittent, crash risk — not just the two in ` user_dump() ` .
310+ - Fix (` adm/simul_efun/message.lpc:48-56 ` ):
311+ ``` lpc
312+ // BEFORE:
313+ varargs void tell_room(mixed ob, string str, object *exclude) {
314+ if (ob) {
315+ message("tell_room", str, ob, exclude);
316+ // AFTER:
317+ varargs void tell_room(mixed ob, string str, object *exclude) {
318+ if (ob) {
319+ if (!exclude) exclude = ({});
320+ message("tell_room", str, ob, exclude);
321+ ```
322+ Fixing the single shared wrapper (per AGENTS.md §7.12's own
323+ prescribed remedy) covers all 717 call sites at once, rather than
324+ patching individual call sites piecemeal (an earlier draft of this
325+ fix instead null-guarded the three ` tell_room(environment(), ...) `
326+ call sites in ` obj/user.lpc ` directly — reverted once the actual
327+ root cause was traced to the shared wrapper, since a per-call-site
328+ guard would have left every * other* 2-arg ` tell_room() ` call in the
329+ lib equally exposed).
330+ - Verified: reproduced-then-fixed via a deliberate, short repro —
331+ temporarily changed ` include/user.h ` 's ` NET_DEAD_TIMEOUT ` from ` 600 `
332+ to ` 20 ` , rebooted, net-deaded ` shenlim ` , waited ~ 45s (past the 15s
333+ ` do_net_dead ` grace + 20s timeout). Pre-fix this reliably reproduced
334+ the exact ` Bad argument 4 ` trace; post-fix (this wrapper patched),
335+ the same repro produced ** zero** ` 错误 ` /` Bad argument ` output, the
336+ character was cleanly force-quit (confirmed via a normal fresh
337+ relogin working immediately after), and the driver process stayed
338+ alive. Reverted ` NET_DEAD_TIMEOUT ` back to ` 600 ` immediately after
339+ (confirmed via ` git diff ` — no residual change) and did one final
340+ clean reboot + full relogin (` look ` /` score ` /` skills ` /` i ` /` quit ` ) to
341+ confirm the fix in isolation, zero new ` debug.log ` errors.
342+
343+ ** 2. Observed, not independently fixable at the LPC level — a genuine
344+ native driver crash, matching AGENTS.md §10.8's already-cataloged
345+ "driver-fatal ref-count-0 ` free_svalue() ` abort" class (third
346+ corroborating occurrence, and — per bug 1 above — plausibly the exact
347+ mechanism §7.12's own severity-escalation paragraph already predicts,
348+ not a fresh mystery).**
349+
350+ - What happened: partway through this session (naturally, from an
351+ extended real-wall-clock gap between test steps rather than a
352+ deliberate wait — see AGENTS.md §10.7 checklist item 8's honesty
353+ requirement: this was NOT a scripted repro, it just happened), the
354+ driver's own stdout (captured to ` /tmp/xlqy_new2007_stdout.log ` ,
355+ outside the repo) showed:
356+ ```
357+ 执行时段错误:*Bad argument 4 to EFUN message()
358+ ...
359+ 呼叫来自:/obj/user.lpc 的 user_dump() 第 236 行 ...
360+ ******** FATAL ERROR: FATAL: Object 0x55c032a76368 /std/skill ref
361+ count 0, but not destructed (from free_svalue).
362+ (current object was /d/ourhome/kedian2)
363+ ...
364+ crash() in master called successfully. Aborting.
365+ ```
366+ The process exited outright — every connection dropped, ` ss -tlnp `
367+ showed nothing on port 40022, the driver PID was simply gone.
368+ ` debug.log ` showed ** zero** trace of any of this (same total
369+ silence AGENTS.md §10.8 already documents) — the only reason this
370+ was caught at all was having the driver's own stdout redirected to a
371+ file, per that section's own "actionable takeaway."
372+ - Secondary, compounding finding matching AGENTS.md §7.11 (missing
373+ runtime directory): the driver's own crash handler
374+ (` adm/obj/master.lpc ` 's ` crash() ` → ` log_file("nosave/CRASHES", ...) ` )
375+ itself failed with `* Wrong permissions for opening file
376+ /log/nosave/CRASHES for append. "No such file or directory"` because
377+ ` work/log/nosave/ ` doesn't exist in this checkout — meaning even the
378+ driver's own attempt to leave a permanent crash record for next time
379+ silently failed too. ** Not fixed as a committable change** : the
380+ entire ` libs/*/work/log/ ` tree is gitignored project-wide
381+ (` .gitignore:12-14 ` ), so creating this one directory locally doesn't
382+ persist past this session and isn't a real fix at the lib level —
383+ flagging for the orchestrator in case a project-wide fix (e.g.
384+ ` scripts/pack_lib_for_web.sh ` or a boot-prep step always ` mkdir -p `
385+ ` log/nosave/ ` ) is worth doing centrally.
386+ - Not fixed / not independently root-caused at the LPC level, same as
387+ the two prior §10.8 occurrences (` xianjianchuanqi ` , ` shiji ` ) — this
388+ is a driver-level internal-consistency failure, not a mudlib source
389+ pattern. Bug 1's fix removes ONE plausible contributing trigger (the
390+ aborted ` user_dump() ` mid-execution) but is not proven to be the
391+ sole cause; re-verifying bug 1's fix in isolation (see above) did
392+ * not* reproduce the FATAL crash in a short (~ 45s) repro window, which
393+ is weakly consistent with bug 1 being at least a contributing factor,
394+ but 45s of post-fix uptime is nowhere near a rigorous disproof.
395+
396+ ### Observed, deliberately not touched (out of scope: not a crash, not wrong behavior)
397+
398+ - ` feature/damage.lpc:535 ` (and two identical copies,
399+ ` feature/damagedie.lpc:217 ` , ` u/dianel/damagedie.lpc:199 ` ):
400+ ` DEATH_ROOM->start_death(this_object()) ` calls a function that
401+ doesn't exist anywhere in this archive (` /d/death/gate.lpc ` has no
402+ ` start_death() ` , grepped the whole lib). Confirmed harmless: a
403+ ` call_other() ` to an undefined function is a silent no-op on this
404+ driver (confirmed — zero ` debug.log ` /stdout trace during an actual
405+ live death), and the real reincarnation trigger already happens via
406+ a completely different path (` d/death/npc/pang.lpc ` 's
407+ ` init() ` -triggered ` call_out ` chain, fired automatically when the
408+ ghost is moved into the room). Dead/vestigial code from an earlier
409+ version of the death room, not a functional bug — left alone per
410+ this pass's scope (programming bugs that actually crash or misbehave
411+ only, not speculative cleanup).
412+ - One unrelated caught error surfaced ambiently during the same
413+ session, from a completely different, unvisited zone: `* Illegal to
414+ move or destruct an object (/d/moon/obj/luoyi#346) defining actions
415+ from a verb function(give teapot to fei zei) in
416+ object(/d/qujing/wuzhuang/npc/mingyue#144) which returns zero.` —
417+ this fired from ambient NPC/quest simulation in 五庄观
418+ (` d/qujing/wuzhuang/ ` ), a late-game zone this playthrough never
419+ visited. Not chased — out of the tested path, and root-causing it
420+ properly would need a separate deep pass through that zone's own
421+ quest scripts.
422+ - ` d/ourhome/kedian.lpc:20 ` — the ` "exits" ` mapping's ` sizeof() == 2 `
423+ comment doesn't match its actual single ` "south" ` key. Stale comment
424+ from a prior edit, not a functional bug (the room's actual exits
425+ work correctly as coded) — left alone.
0 commit comments