Reissue 1.18 - same code, corrected metadata.
[open-adventure.git] / saveresume.c
1 /*
2  * Saving and resuming.
3  *
4  * (ESR) This replaces  a bunch of particularly nasty FORTRAN-derived code;
5  * see the history.adoc file in the source distribution for discussion.
6  *
7  * SPDX-FileCopyrightText: (C) 1977, 2005 by Will Crowther and Don Woods
8  * SPDX-License-Identifier: BSD-2-Clause
9  */
10
11 #include <ctype.h>
12 #include <inttypes.h>
13 #include <stdlib.h>
14 #include <string.h>
15 #include <time.h>
16
17 #include "advent.h"
18
19 /*
20  * Use this to detect endianness mismatch.  Can't be unchanged by byte-swapping.
21  */
22 #define ENDIAN_MAGIC 2317
23
24 struct save_t save;
25
26 #define IGNORE(r)                                                              \
27         do {                                                                   \
28                 if (r) {                                                       \
29                 }                                                              \
30         } while (0)
31
32 int savefile(FILE *fp) {
33         /* Save game to file. No input or output from user. */
34         memcpy(&save.magic, ADVENT_MAGIC, sizeof(ADVENT_MAGIC));
35         if (save.version == 0) {
36                 save.version = SAVE_VERSION;
37         }
38         if (save.canary == 0) {
39                 save.canary = ENDIAN_MAGIC;
40         }
41         save.game = game;
42         IGNORE(fwrite(&save, sizeof(struct save_t), 1, fp));
43         return (0);
44 }
45
46 /* Suspend and resume */
47
48 static char *strip(char *name) {
49         // Trim leading whitespace
50         while (isspace((unsigned char)*name)) {
51                 name++; // LCOV_EXCL_LINE
52         }
53         if (*name != '\0') {
54                 // Trim trailing whitespace;
55                 // might be left there by autocomplete
56                 char *end = name + strlen(name) - 1;
57                 while (end > name && isspace((unsigned char)*end)) {
58                         end--;
59                 }
60                 // Write new null terminator character
61                 end[1] = '\0';
62         }
63
64         return name;
65 }
66
67 int suspend(void) {
68         /*  Suspend.  Offer to save things in a file, but charging
69          *  some points (so can't win by using saved games to retry
70          *  battles or to start over after learning zzword).
71          *  If ADVENT_NOSAVE is defined, gripe instead. */
72
73 #if defined ADVENT_NOSAVE || defined ADVENT_AUTOSAVE
74         rspeak(SAVERESUME_DISABLED);
75         return GO_TOP;
76 #endif
77         FILE *fp = NULL;
78
79         rspeak(SUSPEND_WARNING);
80         if (!yes_or_no(arbitrary_messages[THIS_ACCEPTABLE],
81                        arbitrary_messages[OK_MAN],
82                        arbitrary_messages[OK_MAN])) {
83                 return GO_CLEAROBJ;
84         }
85         game.saved = game.saved + 5;
86
87         while (fp == NULL) {
88                 char *name = myreadline("\nFile name: ");
89                 if (name == NULL) {
90                         return GO_TOP;
91                 }
92                 name = strip(name);
93                 if (strlen(name) == 0) {
94                         return GO_TOP; // LCOV_EXCL_LINE
95                 }
96                 fp = fopen(strip(name), WRITE_MODE);
97                 if (fp == NULL) {
98                         printf("Can't open file %s, try again.\n", name);
99                 }
100                 free(name);
101         }
102
103         savefile(fp);
104         fclose(fp);
105         rspeak(RESUME_HELP);
106         exit(EXIT_SUCCESS);
107 }
108
109 int resume(void) {
110         /*  Resume.  Read a suspended game back from a file.
111          *  If ADVENT_NOSAVE is defined, gripe instead. */
112
113 #if defined ADVENT_NOSAVE || defined ADVENT_AUTOSAVE
114         rspeak(SAVERESUME_DISABLED);
115         return GO_TOP;
116 #endif
117         FILE *fp = NULL;
118
119         if (game.loc != LOC_START || game.locs[LOC_START].abbrev != 1) {
120                 rspeak(RESUME_ABANDON);
121                 if (!yes_or_no(arbitrary_messages[THIS_ACCEPTABLE],
122                                arbitrary_messages[OK_MAN],
123                                arbitrary_messages[OK_MAN])) {
124                         return GO_CLEAROBJ;
125                 }
126         }
127
128         while (fp == NULL) {
129                 char *name = myreadline("\nFile name: ");
130                 if (name == NULL) {
131                         return GO_TOP;
132                 }
133                 name = strip(name);
134                 if (strlen(name) == 0) {
135                         return GO_TOP; // LCOV_EXCL_LINE
136                 }
137                 fp = fopen(name, READ_MODE);
138                 if (fp == NULL) {
139                         printf("Can't open file %s, try again.\n", name);
140                 }
141                 free(name);
142         }
143
144         return restore(fp);
145 }
146
147 int restore(FILE *fp) {
148         /*  Read and restore game state from file, assuming
149          *  sane initial state.
150          *  If ADVENT_NOSAVE is defined, gripe instead. */
151 #ifdef ADVENT_NOSAVE
152         rspeak(SAVERESUME_DISABLED);
153         return GO_TOP;
154 #endif
155
156         IGNORE(fread(&save, sizeof(struct save_t), 1, fp));
157         fclose(fp);
158         if (memcmp(save.magic, ADVENT_MAGIC, sizeof(ADVENT_MAGIC)) != 0 ||
159             save.canary != ENDIAN_MAGIC) {
160                 rspeak(BAD_SAVE);
161         } else if (save.version != SAVE_VERSION) {
162                 rspeak(VERSION_SKEW, save.version / 10, MOD(save.version, 10),
163                        SAVE_VERSION / 10, MOD(SAVE_VERSION, 10));
164         } else if (!is_valid(save.game)) {
165                 rspeak(SAVE_TAMPERING);
166                 exit(EXIT_SUCCESS);
167         } else {
168                 game = save.game;
169         }
170         return GO_TOP;
171 }
172
173 bool is_valid(struct game_t valgame) {
174         /*  Save files can be roughly grouped into three groups:
175          *  With valid, reachable state, with valid, but unreachable
176          *  state and with invalid state. We check that state is
177          *  valid: no states are outside minimal or maximal value
178          */
179
180         /* Prevent division by zero */
181         if (valgame.abbnum == 0) {
182                 return false; // LCOV_EXCL_LINE
183         }
184
185         /* Check for RNG overflow. Truncate */
186         if (valgame.lcg_x >= LCG_M) {
187                 return false;
188         }
189
190         /*  Bounds check for locations */
191         if (valgame.chloc < -1 || valgame.chloc > NLOCATIONS ||
192             valgame.chloc2 < -1 || valgame.chloc2 > NLOCATIONS ||
193             valgame.loc < 0 || valgame.loc > NLOCATIONS || valgame.newloc < 0 ||
194             valgame.newloc > NLOCATIONS || valgame.oldloc < 0 ||
195             valgame.oldloc > NLOCATIONS || valgame.oldlc2 < 0 ||
196             valgame.oldlc2 > NLOCATIONS) {
197                 return false; // LCOV_EXCL_LINE
198         }
199         /*  Bounds check for location arrays */
200         for (int i = 0; i <= NDWARVES; i++) {
201                 if (valgame.dwarves[i].loc < -1 ||
202                     valgame.dwarves[i].loc > NLOCATIONS ||
203                     valgame.dwarves[i].oldloc < -1 ||
204                     valgame.dwarves[i].oldloc > NLOCATIONS) {
205                         return false; // LCOV_EXCL_LINE
206                 }
207         }
208
209         for (int i = 0; i <= NOBJECTS; i++) {
210                 if (valgame.objects[i].place < -1 ||
211                     valgame.objects[i].place > NLOCATIONS ||
212                     valgame.objects[i].fixed < -1 ||
213                     valgame.objects[i].fixed > NLOCATIONS) {
214                         return false; // LCOV_EXCL_LINE
215                 }
216         }
217
218         /*  Bounds check for dwarves */
219         if (valgame.dtotal < 0 || valgame.dtotal > NDWARVES ||
220             valgame.dkill < 0 || valgame.dkill > NDWARVES) {
221                 return false; // LCOV_EXCL_LINE
222         }
223
224         /*  Validate that we didn't die too many times in save */
225         if (valgame.numdie >= NDEATHS) {
226                 return false; // LCOV_EXCL_LINE
227         }
228
229         /* Recalculate tally, throw the towel if in disagreement */
230         int temp_tally = 0;
231         for (int treasure = 1; treasure <= NOBJECTS; treasure++) {
232                 if (objects[treasure].is_treasure) {
233                         if (PROP_IS_NOTFOUND2(valgame, treasure)) {
234                                 ++temp_tally;
235                         }
236                 }
237         }
238         if (temp_tally != valgame.tally) {
239                 return false; // LCOV_EXCL_LINE
240         }
241
242         /* Check that properties of objects aren't beyond expected */
243         for (obj_t obj = 0; obj <= NOBJECTS; obj++) {
244                 if (PROP_IS_INVALID(valgame.objects[obj].prop)) {
245                         return false; // LCOV_EXCL_LINE
246                 }
247         }
248
249         /* Check that values in linked lists for objects in locations are inside
250          * bounds */
251         for (loc_t loc = LOC_NOWHERE; loc <= NLOCATIONS; loc++) {
252                 if (valgame.locs[loc].atloc < NO_OBJECT ||
253                     valgame.locs[loc].atloc > NOBJECTS * 2) {
254                         return false; // LCOV_EXCL_LINE
255                 }
256         }
257         for (obj_t obj = 0; obj <= NOBJECTS * 2; obj++) {
258                 if (valgame.link[obj] < NO_OBJECT ||
259                     valgame.link[obj] > NOBJECTS * 2) {
260                         return false; // LCOV_EXCL_LINE
261                 }
262         }
263
264         return true;
265 }
266
267 /* end */