From 370c282240f97d36032d5fa0caa2524dd0633886 Mon Sep 17 00:00:00 2001 From: k21971 Date: Tue, 18 Aug 2026 00:30:23 +0000 Subject: [PATCH] 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 .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. --- src/allmain.c | 2 +- src/restore.c | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/allmain.c b/src/allmain.c index 8f747564a..4177cd0cd 100644 --- a/src/allmain.c +++ b/src/allmain.c @@ -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); diff --git a/src/restore.c b/src/restore.c index 8a7ad8743..48e851dc6 100644 --- a/src/restore.c +++ b/src/restore.c @@ -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) { /*