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