Skip to content

Commit 74edff7

Browse files
committed
Fix 2 bugs found in shujian2008's deep functional test (existing §7.16/§7.12 classes)
Round-two deep functional test (AGENTS.md §10.7), including two real full 16-minute NET_DEAD_TIMEOUT waits. 1. AGENTS.md §7.16 class: cmds/usr/top.lpc's five add_rank_*() functions ran an unbounded per-hour decay loop against the shipped /log/rank.o's real ~2008 timestamps -- crashed the very first quit attempted (quit.lpc calls TOP_CMD->add_rank(me) unconditionally). Fixed with the established pattern: cap each loop at 240 iterations, unconditionally resync the timestamp afterward. 2. AGENTS.md §7.12 class: adm/simul_efun/message.lpc's tell_room() forwarded a raw int 0 as message()'s 4th argument on 2-arg calls -- reached from user_dump()'s NET_DEAD_TIMEOUT force-quit path, reproduced live via a real 16-minute net-dead wait: silently disabled the entire net-dead force-quit safety net (a reconnect 20+ minutes later found the stale character object still alive and just relinked instead of a fresh login). Fixed with the standard `exclude || ({})` pattern; re-verified with a second full 16-minute wait producing a clean force-quit post-fix.
1 parent 1fb4be8 commit 74edff7

5 files changed

Lines changed: 124 additions & 21 deletions

File tree

libs/shujian2008/NOTES.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,61 @@ treated as untrusted/remote and subject to the gate normally, not
264264
silently allowed through. Retested: fluffos login (127.0.0.1, real
265265
value under the current driver) still passes every gate; debug.log
266266
stayed clean of `denied`/`undefined function`/`error in error handler`.
267+
268+
## 深度功能测试 / Deep functional test (2026-07-24)
269+
270+
Round-two deep functional test per AGENTS.md §10.7. Read `doc/help/
271+
newbie` and `doc/help/help_wuguan`/`map_wuguan` first, then played one
272+
continuous native-driver session as a fresh Chinese-named character
273+
through registration → exploration → safe sparring → organic skill
274+
learning → sect join → clean quit → prompt net-dead reconnect → a real
275+
~16-minute full `NET_DEAD_TIMEOUT` wait (done twice) → post-quit
276+
state-persistence verification. This lib's "adm-single" family is
277+
confirmed distinct from the "书剑"/`bxsj` lineage already deep-tested
278+
earlier this round, despite the similar title (AGENTS.md §11).
279+
280+
**Bugs found and fixed** (both already-cataloged AGENTS.md classes,
281+
first occurrence on this specific lib — no new class needed):
282+
283+
1. **AGENTS.md §7.16**`cmds/usr/top.lpc`: five `add_rank_*()`
284+
functions (`add_rank_beauty`, `add_rank_pk`, `add_rank_rich`,
285+
`add_rank_worker`, `add_rank`) ran an unbounded per-hour decay loop
286+
against the shipped `/log/rank.o`'s genuine ~2008 timestamps. Since
287+
`cmds/usr/quit.lpc:218` calls `TOP_CMD->add_rank(me)` unconditionally
288+
on every `quit`, this crashed the very first `quit` attempted
289+
(`Too long evaluation. Execution aborted.`), silently dropping an
290+
in-flight `startroom` update since the crash landed before
291+
`do_quit()`'s own `save()`. Fixed with the established §7.16
292+
pattern: cap each loop at 240 iterations, then unconditionally
293+
resync the timestamp to `t` afterward.
294+
2. **AGENTS.md §7.12**`adm/simul_efun/message.lpc`'s `tell_room()`
295+
wrapper forwarded a bare `int 0` as `message()`'s 4th argument when
296+
called with only 2 args. Reached from `clone/user/user.lpc`'s
297+
`user_dump()` on the `NET_DEAD_TIMEOUT` (900s) force-quit path —
298+
reproduced live via a real 16-minute net-dead wait: the crash
299+
aborted `user_dump()` before it ever reached `"/cmds/usr/quit"->
300+
main(me)`, silently disabling the entire net-dead force-quit safety
301+
net (confirmed live: a reconnect >20 minutes later found the old
302+
character object still alive/un-destructed and just relinked to it
303+
via "重新连线完毕。" instead of a fresh login). Fixed with the
304+
standard `exclude || ({})` pattern from §7.12, then re-verified with
305+
a second full 16-minute wait producing a clean force-quit and
306+
fresh-login banner on reconnect.
307+
308+
**Test character left behind:** id `qinshiyu`, Chinese name 秦时雨,
309+
password `Sj2026Test` — 武当派 (Wudang) disciple, 17 skills, standing
310+
at 西练武场. Saves: `work/data/{login,user}/q/qinshiyu.o`.
311+
312+
**Verified working:** registration with a real Chinese name; multi-room
313+
exploration; safe sparring vs. the training dummy; organic sect join;
314+
organic skill learning (17 skills total); clean `quit` + immediate
315+
`debug.log` grep; prompt net-dead reconnect; two full real 16-minute
316+
`NET_DEAD_TIMEOUT` waits (the second post-fix, confirming the fix);
317+
state persistence (location/sect/skills) across all of the above.
318+
319+
**Explicitly not verified live** (documented, not silently skipped):
320+
a shop purchase (no shop in the starting zone; real commerce is in a
321+
distant Yangzhou city zone not mapped this pass) and a full
322+
combat-to-death cycle (core combat mechanics already exercised via the
323+
safe-sparring dummy fight; the two real 16-minute net-dead waits
324+
consumed most of the remaining time budget).

libs/shujian2008/work/adm/simul_efun/message.lpc

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,18 @@ void tell_object(object ob, string str) {
4545
}
4646

4747
varargs void tell_room(mixed ob, string str, object *exclude) {
48-
if (ob) message("tell_room", str, ob, exclude);
48+
// FIX (AGENTS.md §7.12): a 2-arg call leaves `exclude` as int 0, but
49+
// the message() efun's 4th argument is spec'd `void | object |
50+
// object *` -- an explicit int 0 is none of those and throws "Bad
51+
// argument 4 to EFUN message()". Reproduced live: this exact wrapper,
52+
// called with 2 args from obj/user.lpc's user_dump() on the
53+
// NET_DEAD_TIMEOUT (900s) force-quit path, crashed before reaching
54+
// the following "/cmds/usr/quit"->main(me) call -- silently
55+
// disabling the whole net-dead force-quit safety net (the character
56+
// stayed alive, unsaved, in server memory indefinitely; a reconnect
57+
// 20+ minutes later found the stale object still live and simply
58+
// relinked to it instead of going through a fresh login).
59+
if (ob) message("tell_room", str, ob, exclude || ({}));
4960
}
5061

5162
void shout(string str) {

libs/shujian2008/work/cmds/usr/top.lpc

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ void goon_upload(mixed ret) {
150150
}
151151

152152
void add_rank_beauty(object ob) {
153-
int i, score, t = time();
153+
int i, score, t = time(), steps;
154154
mapping rank;
155155
string party = ob->query("family/family_name");
156156

@@ -175,12 +175,21 @@ void add_rank_beauty(object ob) {
175175
if (score < 1) return;
176176

177177
for (i = 0; i < sizeof(beauty_ranks); i++)
178-
if (!find_player(beauty_ranks[i]["id"]))
179-
while (beauty_ranks[i]["time"] + 3600 < t) {
178+
if (!find_player(beauty_ranks[i]["id"])) {
179+
// FIX (AGENTS.md §7.16): shipped /log/rank data carries real
180+
// ~2008 timestamps; against today's wall clock this loop would
181+
// need ~157000 iterations per stale entry and blow the eval-cost
182+
// limit on every single quit (TOP_CMD->add_rank() is called
183+
// unconditionally from cmds/usr/quit.lpc). Cap iterations, then
184+
// unconditionally resync the timestamp to "now" below.
185+
steps = 0;
186+
while (beauty_ranks[i]["time"] + 3600 < t && steps++ < 240) {
180187
beauty_ranks[i]["time"] += 3600;
181188
beauty_ranks[i]["score"] *= 95;
182189
beauty_ranks[i]["score"] /= 100;
183-
} else beauty_ranks[i]["time"] = t;
190+
}
191+
beauty_ranks[i]["time"] = t;
192+
} else beauty_ranks[i]["time"] = t;
184193

185194
if (!sizeof(beauty_ranks)) beauty_ranks = ({});
186195
beauty_ranks = sort_array(beauty_ranks, (: sort_rank :));
@@ -202,7 +211,7 @@ void add_rank_beauty(object ob) {
202211
}
203212

204213
void add_rank_pk(object ob) {
205-
int i, score, t = time();
214+
int i, score, t = time(), steps;
206215
int exp = ob->query("combat_exp");
207216
mapping rank;
208217
string party = ob->query("family/family_name");
@@ -220,12 +229,16 @@ void add_rank_pk(object ob) {
220229
if (score < 1) return;
221230

222231
for (i = 0; i < sizeof(pk_ranks); i++)
223-
if (!find_player(pk_ranks[i]["id"]))
224-
while (pk_ranks[i]["time"] + 3600 < t) {
232+
if (!find_player(pk_ranks[i]["id"])) {
233+
// FIX (AGENTS.md §7.16): see add_rank_beauty() above.
234+
steps = 0;
235+
while (pk_ranks[i]["time"] + 3600 < t && steps++ < 240) {
225236
pk_ranks[i]["time"] += 3600;
226237
pk_ranks[i]["score"] *= 97;
227238
pk_ranks[i]["score"] /= 100;
228-
} else pk_ranks[i]["time"] = t;
239+
}
240+
pk_ranks[i]["time"] = t;
241+
} else pk_ranks[i]["time"] = t;
229242

230243
if (!sizeof(pk_ranks)) pk_ranks = ({});
231244
pk_ranks = sort_array(pk_ranks, (: sort_rank :));
@@ -247,7 +260,7 @@ void add_rank_pk(object ob) {
247260
}
248261

249262
void add_rank_rich(object ob) {
250-
int i, score, t = time();
263+
int i, score, t = time(), steps;
251264
int exp = ob->query("combat_exp");
252265
int balance = ob->query("balance");
253266
mapping rank;
@@ -267,12 +280,16 @@ void add_rank_rich(object ob) {
267280
if (score < 1) return;
268281

269282
for (i = 0; i < sizeof(rich_ranks); i++)
270-
if (!find_player(rich_ranks[i]["id"]))
271-
while (rich_ranks[i]["time"] + 3600 < t) {
283+
if (!find_player(rich_ranks[i]["id"])) {
284+
// FIX (AGENTS.md §7.16): see add_rank_beauty() above.
285+
steps = 0;
286+
while (rich_ranks[i]["time"] + 3600 < t && steps++ < 240) {
272287
rich_ranks[i]["time"] += 3600;
273288
rich_ranks[i]["score"] *= 97;
274289
rich_ranks[i]["score"] /= 100;
275-
} else rich_ranks[i]["time"] = t;
290+
}
291+
rich_ranks[i]["time"] = t;
292+
} else rich_ranks[i]["time"] = t;
276293

277294
if (!sizeof(rich_ranks)) rich_ranks = ({});
278295
rich_ranks = sort_array(rich_ranks, (: sort_rank :));
@@ -295,7 +312,7 @@ void add_rank_rich(object ob) {
295312

296313

297314
void add_rank_worker(object ob) {
298-
int i, score, t = time();
315+
int i, score, t = time(), steps;
299316
int exp = ob->query("combat_exp");
300317
mapping rank;
301318
string party = ob->query("family/family_name");
@@ -334,12 +351,18 @@ void add_rank_worker(object ob) {
334351
if (score < 1) return;
335352

336353
for (i = 0; i < sizeof(worker_ranks); i++)
337-
if (!find_player(worker_ranks[i]["id"]))
338-
while (worker_ranks[i]["time"] + 3600 < t) {
354+
if (!find_player(worker_ranks[i]["id"])) {
355+
// FIX (AGENTS.md §7.16): see add_rank_beauty() above. This is
356+
// the copy that actually crashed during live testing: every
357+
// `quit` calls add_rank() -> add_rank_worker() unconditionally.
358+
steps = 0;
359+
while (worker_ranks[i]["time"] + 3600 < t && steps++ < 240) {
339360
worker_ranks[i]["time"] += 3600;
340361
worker_ranks[i]["score"] *= 97;
341362
worker_ranks[i]["score"] /= 100;
342-
} else worker_ranks[i]["time"] = t;
363+
}
364+
worker_ranks[i]["time"] = t;
365+
} else worker_ranks[i]["time"] = t;
343366

344367
if (!sizeof(worker_ranks)) worker_ranks = ({});
345368
worker_ranks = sort_array(worker_ranks, (: sort_rank :));
@@ -361,7 +384,7 @@ void add_rank_worker(object ob) {
361384
}
362385

363386
void add_rank(object ob) {
364-
int i, score, t = time();
387+
int i, score, t = time(), steps;
365388
int exp = ob->query("combat_exp");
366389
int age = ob->query("mud_age") / 3600;
367390
mapping rank;
@@ -393,12 +416,16 @@ void add_rank(object ob) {
393416
if (score < 1) return;
394417

395418
for (i = 0; i < sizeof(score_ranks); i++)
396-
if (!find_player(score_ranks[i]["id"]))
397-
while (score_ranks[i]["time"] + 3600 < t) {
419+
if (!find_player(score_ranks[i]["id"])) {
420+
// FIX (AGENTS.md §7.16): see add_rank_beauty() above.
421+
steps = 0;
422+
while (score_ranks[i]["time"] + 3600 < t && steps++ < 240) {
398423
score_ranks[i]["time"] += 3600;
399424
score_ranks[i]["score"] *= 97;
400425
score_ranks[i]["score"] /= 100;
401-
} else score_ranks[i]["time"] = t;
426+
}
427+
score_ranks[i]["time"] = t;
428+
} else score_ranks[i]["time"] = t;
402429

403430
if (!sizeof(score_ranks)) score_ranks = ({});
404431
score_ranks = sort_array(score_ranks, (: sort_rank :));
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#/clone/user/login.lpc
2+
dbase (["email":"test02@abcd.com","registered":1,"password":"$1$ShuJian$4.jc0AiRlicZV7dUQTZ/3.","last_from":"localhost(127.0.0.1)","id":"qinshiyu","last_on":1784953252,"body":"/clone/user/user","name":"秦时雨",])
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#/clone/user/user.lpc
2+
dbase (["eff_jingli":2618,"max_jing":1143,"qi":1500,"month":2,"max_pot":316,"jingli":2498,"check_point":1784953254,"title":"武当派第三代弟子","dex":20,"jing":800,"water":196,"max_jingli":2500,"per":17,"birthday":1784850411,"channels":({"chat","rumor","party","sj","declare",}),"vip":(["vip_start_time":1784950216,"rules":1,"vip_time":1785555016,]),"race":"人类","env":(["wimpy":50,]),"startroom":"/d/wuguan/wuchang2","gender":"男性","pur":20,"con":17,"eff_jing":800,"online_total":932,"离馆":1,"potential":100,"neili":3800,"max_neili":3800,"job_time":(["丐帮":400,"送信":1000,]),"mud_age":100958,"name":"秦时雨","int":20,"registered":3,"lastroom":"/d/wuguan/shilu3","day":0,"id":"qinshiyu","mud_time":1255,"actions":,"combat_exp":1000000,"score":3106,"food":195,"kar":22,"shizhe_拜师":1,"max_qi":3977,"age":15,"shizhe_newbie":1,"str":23,"family":(["enter_time":1784950219,"title":"弟子","privs":0,"master_id":"shizhe","family_name":"武当派","master_name":"书剑礼物使者","generation":3,]),"shen":100000,"eff_qi":1500,])
3+
skills (["zhenshan-mianzhang":201,"taoism":200,"cuff":201,"literate":200,"taiji-jian":201,"strike":201,"medicine":122,"zhizao":100,"dodge":201,"yinyun-ziqi":201,"duanzao":100,"taiji-quan":201,"force":201,"hand":201,"tiyunzong":201,"sword":201,"parry":201,])
4+
learned ([])
5+
autoload ({})

0 commit comments

Comments
 (0)