1TBS reflow with clang-format.
[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                 name = strip(name);
133                 if (strlen(name) == 0)
134                         return GO_TOP; // LCOV_EXCL_LINE
135                 fp = fopen(name, READ_MODE);
136                 if (fp == NULL) {
137                         printf("Can't open file %s, try again.\n", name);
138                 }
139                 free(name);
140         }
141
142         return restore(fp);
143 }
144
145 int restore(FILE *fp) {
146         /*  Read and restore game state from file, assuming
147          *  sane initial state.
148          *  If ADVENT_NOSAVE is defined, gripe instead. */
149 #ifdef ADVENT_NOSAVE
150         rspeak(SAVERESUME_DISABLED);
151         return GO_TOP;
152 #endif
153
154         IGNORE(fread(&save, sizeof(struct save_t), 1, fp));
155         fclose(fp);
156         if (memcmp(save.magic, ADVENT_MAGIC, sizeof(ADVENT_MAGIC)) != 0 ||
157             save.canary != ENDIAN_MAGIC) {
158                 rspeak(BAD_SAVE);
159         } else if (save.version != SAVE_VERSION) {
160                 rspeak(VERSION_SKEW, save.version / 10, MOD(save.version, 10),
161                        SAVE_VERSION / 10, MOD(SAVE_VERSION, 10));
162         } else if (!is_valid(save.game)) {
163                 rspeak(SAVE_TAMPERING);
164                 exit(EXIT_SUCCESS);
165         } else {
166                 game = save.game;
167         }
168         return GO_TOP;
169 }
170
171 bool is_valid(struct game_t valgame) {
172         /*  Save files can be roughly grouped into three groups:
173          *  With valid, reachable state, with valid, but unreachable
174          *  state and with invalid state. We check that state is
175          *  valid: no states are outside minimal or maximal value
176          */
177
178         /* Prevent division by zero */
179         if (valgame.abbnum == 0) {
180                 return false; // LCOV_EXCL_LINE
181         }
182
183         /* Check for RNG overflow. Truncate */
184         if (valgame.lcg_x >= LCG_M) {
185                 valgame.lcg_x %= LCG_M; // LCOV_EXCL_LINE
186         }
187
188         /* Check for RNG underflow. Transpose */
189         if (valgame.lcg_x < LCG_M) {
190                 valgame.lcg_x = LCG_M + (valgame.lcg_x % LCG_M);
191         }
192
193         /*  Bounds check for locations */
194         if (valgame.chloc < -1 || valgame.chloc > NLOCATIONS ||
195             valgame.chloc2 < -1 || valgame.chloc2 > NLOCATIONS ||
196             valgame.loc < 0 || valgame.loc > NLOCATIONS || valgame.newloc < 0 ||
197             valgame.newloc > NLOCATIONS || valgame.oldloc < 0 ||
198             valgame.oldloc > NLOCATIONS || valgame.oldlc2 < 0 ||
199             valgame.oldlc2 > NLOCATIONS) {
200                 return false; // LCOV_EXCL_LINE
201         }
202         /*  Bounds check for location arrays */
203         for (int i = 0; i <= NDWARVES; i++) {
204                 if (valgame.dwarves[i].loc < -1 ||
205                     valgame.dwarves[i].loc > NLOCATIONS ||
206                     valgame.dwarves[i].oldloc < -1 ||
207                     valgame.dwarves[i].oldloc > NLOCATIONS) {
208                         return false; // LCOV_EXCL_LINE
209                 }
210         }
211
212         for (int i = 0; i <= NOBJECTS; i++) {
213                 if (valgame.objects[i].place < -1 ||
214                     valgame.objects[i].place > NLOCATIONS ||
215                     valgame.objects[i].fixed < -1 ||
216                     valgame.objects[i].fixed > NLOCATIONS) {
217                         return false; // LCOV_EXCL_LINE
218                 }
219         }
220
221         /*  Bounds check for dwarves */
222         if (valgame.dtotal < 0 || valgame.dtotal > NDWARVES ||
223             valgame.dkill < 0 || valgame.dkill > NDWARVES) {
224                 return false; // LCOV_EXCL_LINE
225         }
226
227         /*  Validate that we didn't die too many times in save */
228         if (valgame.numdie >= NDEATHS) {
229                 return false; // LCOV_EXCL_LINE
230         }
231
232         /* Recalculate tally, throw the towel if in disagreement */
233         int temp_tally = 0;
234         for (int treasure = 1; treasure <= NOBJECTS; treasure++) {
235                 if (objects[treasure].is_treasure) {
236                         if (PROP_IS_NOTFOUND2(valgame, treasure)) {
237                                 ++temp_tally;
238                         }
239                 }
240         }
241         if (temp_tally != valgame.tally) {
242                 return false; // LCOV_EXCL_LINE
243         }
244
245         /* Check that properties of objects aren't beyond expected */
246         for (obj_t obj = 0; obj <= NOBJECTS; obj++) {
247                 if (PROP_IS_INVALID(valgame.objects[obj].prop)) {
248                         return false; // LCOV_EXCL_LINE
249                 }
250         }
251
252         /* Check that values in linked lists for objects in locations are inside
253          * bounds */
254         for (loc_t loc = LOC_NOWHERE; loc <= NLOCATIONS; loc++) {
255                 if (valgame.locs[loc].atloc < NO_OBJECT ||
256                     valgame.locs[loc].atloc > NOBJECTS * 2) {
257                         return false; // LCOV_EXCL_LINE
258                 }
259         }
260         for (obj_t obj = 0; obj <= NOBJECTS * 2; obj++) {
261                 if (valgame.link[obj] < NO_OBJECT ||
262                     valgame.link[obj] > NOBJECTS * 2) {
263                         return false; // LCOV_EXCL_LINE
264                 }
265         }
266
267         return true;
268 }
269
270 /* end */