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