1TBS reflow, the bracening.
[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                 valgame.lcg_x %= LCG_M; // LCOV_EXCL_LINE
188         }
189
190         /* Check for RNG underflow. Transpose */
191         if (valgame.lcg_x < LCG_M) {
192                 valgame.lcg_x = LCG_M + (valgame.lcg_x % LCG_M);
193         }
194
195         /*  Bounds check for locations */
196         if (valgame.chloc < -1 || valgame.chloc > NLOCATIONS ||
197             valgame.chloc2 < -1 || valgame.chloc2 > NLOCATIONS ||
198             valgame.loc < 0 || valgame.loc > NLOCATIONS || valgame.newloc < 0 ||
199             valgame.newloc > NLOCATIONS || valgame.oldloc < 0 ||
200             valgame.oldloc > NLOCATIONS || valgame.oldlc2 < 0 ||
201             valgame.oldlc2 > NLOCATIONS) {
202                 return false; // LCOV_EXCL_LINE
203         }
204         /*  Bounds check for location arrays */
205         for (int i = 0; i <= NDWARVES; i++) {
206                 if (valgame.dwarves[i].loc < -1 ||
207                     valgame.dwarves[i].loc > NLOCATIONS ||
208                     valgame.dwarves[i].oldloc < -1 ||
209                     valgame.dwarves[i].oldloc > NLOCATIONS) {
210                         return false; // LCOV_EXCL_LINE
211                 }
212         }
213
214         for (int i = 0; i <= NOBJECTS; i++) {
215                 if (valgame.objects[i].place < -1 ||
216                     valgame.objects[i].place > NLOCATIONS ||
217                     valgame.objects[i].fixed < -1 ||
218                     valgame.objects[i].fixed > NLOCATIONS) {
219                         return false; // LCOV_EXCL_LINE
220                 }
221         }
222
223         /*  Bounds check for dwarves */
224         if (valgame.dtotal < 0 || valgame.dtotal > NDWARVES ||
225             valgame.dkill < 0 || valgame.dkill > NDWARVES) {
226                 return false; // LCOV_EXCL_LINE
227         }
228
229         /*  Validate that we didn't die too many times in save */
230         if (valgame.numdie >= NDEATHS) {
231                 return false; // LCOV_EXCL_LINE
232         }
233
234         /* Recalculate tally, throw the towel if in disagreement */
235         int temp_tally = 0;
236         for (int treasure = 1; treasure <= NOBJECTS; treasure++) {
237                 if (objects[treasure].is_treasure) {
238                         if (PROP_IS_NOTFOUND2(valgame, treasure)) {
239                                 ++temp_tally;
240                         }
241                 }
242         }
243         if (temp_tally != valgame.tally) {
244                 return false; // LCOV_EXCL_LINE
245         }
246
247         /* Check that properties of objects aren't beyond expected */
248         for (obj_t obj = 0; obj <= NOBJECTS; obj++) {
249                 if (PROP_IS_INVALID(valgame.objects[obj].prop)) {
250                         return false; // LCOV_EXCL_LINE
251                 }
252         }
253
254         /* Check that values in linked lists for objects in locations are inside
255          * bounds */
256         for (loc_t loc = LOC_NOWHERE; loc <= NLOCATIONS; loc++) {
257                 if (valgame.locs[loc].atloc < NO_OBJECT ||
258                     valgame.locs[loc].atloc > NOBJECTS * 2) {
259                         return false; // LCOV_EXCL_LINE
260                 }
261         }
262         for (obj_t obj = 0; obj <= NOBJECTS * 2; obj++) {
263                 if (valgame.link[obj] < NO_OBJECT ||
264                     valgame.link[obj] > NOBJECTS * 2) {
265                         return false; // LCOV_EXCL_LINE
266                 }
267         }
268
269         return true;
270 }
271
272 /* end */