Fix: first INSURANCE checkpoint is never written.

savestateinlock() returns early unless
program_state.something_worth_saving is set, but both call sites that
are meant to lay down the initial checkpoint run before that flag is
set: newgame() calls save_currentstate() two lines early, and
dorecover() calls savestateinlock() 73 lines before it, ahead of the
pass that writes out the level files.

Both calls are therefore no-ops, and the <uid><plname>.0 lock file
holds nothing but the pid written by getlock() until the hero first
changes dungeon level. A game that dies without a chance to save
during that window (SIGKILL, OOM killer, watchdog, host reboot)
cannot be rebuilt: recover has no save file name, no current level
number and no game state, and reports "Checkpointing was not in
effect", which is true of the outcome but misleading about the cause.
The window covers the whole of a long stay on one level, and in
particular the entire period right after a restore.

Set the flag before the call in newgame(). In dorecover(), drop the
ineffective call and checkpoint once the restore is complete instead;
at the original spot no level file for this session has been written
yet, so a checkpoint there would name a current level and save file
that are not on disk. The new call goes after program_state.restoring
is cleared, so that stairs and traps are written with the same
relative dlevel encoding a normal save uses, and it is
save_currentstate() rather than savestateinlock() so that
in_checkpoint is set while savegamestate() writes u.ustuck_mid and
u.usteed_mid.

This arrived with the something_worth_saving guard; 3.4.3, which has
no guard, is unaffected. Looks like this issue has been around since
version 3.6.0.
This commit is contained in:
k21971
2026-08-18 00:30:23 +00:00
parent d8ea2087bc
commit 370c282240
2 changed files with 7 additions and 4 deletions
+1 -1
View File
@@ -872,10 +872,10 @@ newgame(void)
urealtime.realtime = 0L;
urealtime.start_timing = getnow();
program_state.something_worth_saving++; /* useful data now exists */
#ifdef INSURANCE
save_currentstate();
#endif
program_state.something_worth_saving++; /* useful data now exists */
/* Success! */
welcome(TRUE);
+6 -3
View File
@@ -843,9 +843,6 @@ dorecover(NHFILE *nhfp)
init_oclass_probs(); /* recompute go.oclass_prob_totals[] */
restlevelstate();
#ifdef INSURANCE
savestateinlock();
#endif
rtmp = restlevelfile(ledger_no(&u.uz));
if (rtmp < 2)
return rtmp; /* dorecover called recursively */
@@ -953,6 +950,12 @@ dorecover(NHFILE *nhfp)
run_timers(); /* expire all timers that have gone off while away */
program_state.restoring = 0; /* affects bot() so clear before docrt() */
#ifdef INSURANCE
/* first checkpoint of the restored session; every level file has been
written and the current level has been read back in, so recover has
something to work with even if the player never changes level */
save_currentstate();
#endif
if (ge.early_raw_messages && !program_state.beyond_savefile_load) {
/*