Another typo fix.
[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  * Copyright (c) 1977, 2005 by Will Crowther and Don Woods
8  * Copyright (c) 2017 by Eric S. Raymond
9  * SPDX-License-Identifier: BSD-2-clause
10  */
11
12 #include <stdlib.h>
13 #include <string.h>
14 #include <time.h>
15 #include <inttypes.h>
16
17 #include "advent.h"
18 #include "dungeon.h"
19
20 /*
21  * Bump on save format change.
22  *
23  * Note: Verify that the tests run clean before bumping this, then rebuild the check
24  * files afterwards.  Otherwise you will get a spurious failure due to the old version
25  * having been generated into a check file.
26  */
27 #define VRSION  29
28
29 /*
30  * If you change the first three members, the resume function may not properly
31  * reject saves from older versions.  Yes, this glues us to a hardware-
32  * dependent length of int.  Later members can change, but bump the version
33  * when you do that.
34  */
35 struct save_t {
36     int64_t savetime;
37     int32_t mode;               /* not used, must be present for version detection */
38     int32_t version;
39     struct game_t game;
40 };
41 struct save_t save;
42
43 #define IGNORE(r) do{if (r){}}while(0)
44
45 int savefile(FILE *fp, int32_t version)
46 /* Save game to file. No input or output from user. */
47 {
48     save.savetime = time(NULL);
49     save.mode = -1;
50     save.version = (version == 0) ? VRSION : version;
51
52     save.game = game;
53     IGNORE(fwrite(&save, sizeof(struct save_t), 1, fp));
54     return (0);
55 }
56
57 /* Suspend and resume */
58 int suspend(void)
59 {
60     /*  Suspend.  Offer to save things in a file, but charging
61      *  some points (so can't win by using saved games to retry
62      *  battles or to start over after learning zzword).
63      *  If ADVENT_NOSAVE is defined, do nothing instead. */
64
65 #ifdef ADVENT_NOSAVE
66     return GO_UNKNOWN;
67 #endif
68     FILE *fp = NULL;
69
70     rspeak(SUSPEND_WARNING);
71     if (!yes_or_no(arbitrary_messages[THIS_ACCEPTABLE], arbitrary_messages[OK_MAN], arbitrary_messages[OK_MAN]))
72         return GO_CLEAROBJ;
73     game.saved = game.saved + 5;
74
75     while (fp == NULL) {
76         char* name = myreadline("\nFile name: ");
77         if (name == NULL)
78             return GO_TOP;
79         fp = fopen(name, WRITE_MODE);
80         if (fp == NULL)
81             printf("Can't open file %s, try again.\n", name);
82         free(name);
83     }
84
85     savefile(fp, VRSION);
86     fclose(fp);
87     rspeak(RESUME_HELP);
88     exit(EXIT_SUCCESS);
89 }
90
91 int resume(void)
92 {
93     /*  Resume.  Read a suspended game back from a file.
94      *  If ADVENT_NOSAVE is defined, do nothing instead. */
95
96 #ifdef ADVENT_NOSAVE
97     return GO_UNKNOWN;
98 #endif
99     FILE *fp = NULL;
100
101     if (game.loc != 1 ||
102         game.abbrev[1] != 1) {
103         rspeak(RESUME_ABANDON);
104         if (!yes_or_no(arbitrary_messages[THIS_ACCEPTABLE], arbitrary_messages[OK_MAN], arbitrary_messages[OK_MAN]))
105             return GO_CLEAROBJ;
106     }
107
108     while (fp == NULL) {
109         char* name = myreadline("\nFile name: ");
110         // Autocomplete can leave the input with an extra trailing space.
111         if (name != NULL && strlen(name) > 0 && name[strlen(name) - 1] == ' ')
112             name[strlen(name) - 1] = '\0';
113         if (name == NULL)
114             return GO_TOP;
115         fp = fopen(name, READ_MODE);
116         if (fp == NULL)
117             printf("Can't open file %s, try again.\n", name);
118         free(name);
119     }
120
121     return restore(fp);
122 }
123
124 int restore(FILE* fp)
125 {
126     /*  Read and restore game state from file, assuming
127      *  sane initial state.
128      *  If ADVENT_NOSAVE is defined, do nothing instead. */
129 #ifdef ADVENT_NOSAVE
130     return GO_UNKNOWN;
131 #endif
132
133     IGNORE(fread(&save, sizeof(struct save_t), 1, fp));
134     fclose(fp);
135     if (save.version != VRSION) {
136         rspeak(VERSION_SKEW, save.version / 10, MOD(save.version, 10), VRSION / 10, MOD(VRSION, 10));
137     } else if (is_valid(save.game)) {
138         game = save.game;
139     }
140     return GO_TOP;
141 }
142
143 bool is_valid(struct game_t valgame)
144 {
145     /*  Save files can be roughly grouped into three groups:
146      *  With valid, reaceable state, with valid, but unreachable
147      *  state and with invaild state. We check that state is
148      *  valid: no states are outside minimal or maximal value
149      */
150
151     /* Prevent division by zero */
152     if (valgame.abbnum == 0) {
153         return false;   // LCOV_EXCL_LINE
154     }
155
156     /* Check for RNG overflow. Truncate */
157     if (valgame.lcg_x >= LCG_M) {
158         valgame.lcg_x %= LCG_M; // LCOV_EXCL_LINE
159     }
160
161     /* Check for RNG underflow. Transpose */
162     if (valgame.lcg_x < LCG_M) {
163         valgame.lcg_x = LCG_M + (valgame.lcg_x % LCG_M);
164     }
165
166     /*  Bounds check for locations */
167     if ( valgame.chloc  < -1 || valgame.chloc  > NLOCATIONS ||
168          valgame.chloc2 < -1 || valgame.chloc2 > NLOCATIONS ||
169          valgame.loc    <  0 || valgame.loc    > NLOCATIONS ||
170          valgame.newloc <  0 || valgame.newloc > NLOCATIONS ||
171          valgame.oldloc <  0 || valgame.oldloc > NLOCATIONS ||
172          valgame.oldlc2 <  0 || valgame.oldlc2 > NLOCATIONS) {
173         return false;   // LCOV_EXCL_LINE
174     }
175     /*  Bounds check for location arrays */
176     for (int i = 0; i <= NDWARVES; i++) {
177         if (valgame.dloc[i]  < -1 || valgame.dloc[i]  > NLOCATIONS  ||
178             valgame.odloc[i] < -1 || valgame.odloc[i] > NLOCATIONS) {
179             return false;       // LCOV_EXCL_LINE
180         }
181     }
182
183     for (int i = 0; i <= NOBJECTS; i++) {
184         if (valgame.place[i] < -1 || valgame.place[i] > NLOCATIONS  ||
185             valgame.fixed[i] < -1 || valgame.fixed[i] > NLOCATIONS) {
186             return false;       // LCOV_EXCL_LINE
187         }
188     }
189
190     /*  Bounds check for dwarves */
191     if (valgame.dtotal < 0 || valgame.dtotal > NDWARVES ||
192         valgame.dkill < 0  || valgame.dkill  > NDWARVES) {
193         return false;   // LCOV_EXCL_LINE
194     }
195
196     /*  Validate that we didn't die too many times in save */
197     if (valgame.numdie >= NDEATHS) {
198         return false;   // LCOV_EXCL_LINE
199     }
200
201     /* Recalculate tally, throw the towel if in disagreement */
202     int temp_tally = 0;
203     for (int treasure = 1; treasure <= NOBJECTS; treasure++) {
204         if (objects[treasure].is_treasure) {
205             if (valgame.prop[treasure] == STATE_NOTFOUND) {
206                 ++temp_tally;
207             }
208         }
209     }
210     if (temp_tally != valgame.tally) {
211         return false;   // LCOV_EXCL_LINE
212     }
213
214     /* Check that properties of objects aren't beyond expected */
215     for (obj_t obj = 0; obj <= NOBJECTS; obj++) {
216         if (valgame.prop[obj] < STATE_NOTFOUND || valgame.prop[obj] > 1) {
217             switch (obj) {
218             case RUG:
219             case DRAGON:
220             case BIRD:
221             case BOTTLE:
222             case PLANT:
223             case PLANT2:
224             case TROLL:
225             case URN:
226             case EGGS:
227             case VASE:
228             case CHAIN:
229                 if (valgame.prop[obj] == 2) // There are multiple different states, but it's convenient to clump them together
230                     continue;
231             /* FALLTHRU */
232             case BEAR:
233                 if (valgame.prop[BEAR] == CONTENTED_BEAR || valgame.prop[BEAR] == BEAR_DEAD)
234                     continue;
235             /* FALLTHRU */
236             default:
237                 return false;   // LCOV_EXCL_LINE
238             }
239         }
240     }
241
242     /* Check that values in linked lists for objects in locations are inside bounds */
243     for (loc_t loc = LOC_NOWHERE; loc <= NLOCATIONS; loc++) {
244         if (valgame.atloc[loc] < NO_OBJECT || valgame.atloc[loc] > NOBJECTS * 2) {
245             return false;       // LCOV_EXCL_LINE
246         }
247     }
248     for (obj_t obj = 0; obj <= NOBJECTS * 2; obj++ ) {
249         if (valgame.link[obj] < NO_OBJECT || valgame.link[obj] > NOBJECTS * 2) {
250             return false;       // LCOV_EXCL_LINE
251         }
252     }
253
254     return true;
255 }
256
257 /* end */