Handle a bare numeric token on the command line a bit more gracefully.
[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  28      /* 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 long.  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 bool is_valid(struct game_t*);
116
117 int restore(FILE* fp)
118 {
119     /*  Read and restore game state from file, assuming
120      *  sane initial state.
121      *  If ADVENT_NOSAVE is defined, do nothing instead. */
122 #ifdef ADVENT_NOSAVE
123     return GO_UNKNOWN;
124 #endif
125
126     IGNORE(fread(&save, sizeof(struct save_t), 1, fp));
127     fclose(fp);
128     if (save.version != VRSION) {
129         rspeak(VERSION_SKEW, save.version / 10, MOD(save.version, 10), VRSION / 10, MOD(VRSION, 10));
130     } else if (is_valid(&save.game)) {
131         game = save.game;
132     }
133     return GO_TOP;
134 }
135
136 bool is_valid(struct game_t* valgame)
137 {
138     /*  Save files can be roughly grouped into three groups:
139      *  With valid, reaceable state, with valid, but unreachable
140      *  state and with invaild state. We check that state is
141      *  valid: no states are outside minimal or maximal value
142      */
143
144     /* Prevent division by zero */
145     if (valgame->abbnum == 0) {
146         return false;
147     }
148
149     /* Check for RNG overflow. Truncate */
150     if (valgame->lcg_x >= LCG_M) {
151         valgame->lcg_x %= LCG_M;
152     }
153
154     /* Check for RNG underflow. Transpose */
155     if (valgame->lcg_x < LCG_M) {
156         valgame->lcg_x = LCG_M + (valgame->lcg_x % LCG_M);
157     }
158
159     /*  Bounds check for locations */
160     if ( valgame->chloc  < -1 || valgame->chloc  > NLOCATIONS ||
161          valgame->chloc2 < -1 || valgame->chloc2 > NLOCATIONS ||
162          valgame->loc    <  0 || valgame->loc    > NLOCATIONS ||
163          valgame->newloc <  0 || valgame->newloc > NLOCATIONS ||
164          valgame->oldloc <  0 || valgame->oldloc > NLOCATIONS ||
165          valgame->oldlc2 <  0 || valgame->oldlc2 > NLOCATIONS) {
166         return false;
167     }
168     /*  Bounds check for location arrays */
169     for (int i = 0; i <= NDWARVES; i++) {
170         if (valgame->dloc[i]  < -1 || valgame->dloc[i]  > NLOCATIONS  ||
171             valgame->odloc[i] < -1 || valgame->odloc[i] > NLOCATIONS) {
172             return false;
173         }
174     }
175
176     for (int i = 0; i <= NOBJECTS; i++) {
177         if (valgame->place[i] < -1 || valgame->place[i] > NLOCATIONS  ||
178             valgame->fixed[i] < -1 || valgame->fixed[i] > NLOCATIONS) {
179             return false;
180         }
181     }
182
183     /*  Bounds check for dwarves */
184     if (valgame->dtotal < 0 || valgame->dtotal > NDWARVES ||
185         valgame->dkill < 0  || valgame->dkill  > NDWARVES) {
186         return false;
187     }
188
189     /*  Validate that we didn't die too many times in save */
190     if (valgame->numdie >= NDEATHS) {
191         return false;
192     }
193
194     /* Recalculate tally, throw the towel if in disagreement */
195     long temp_tally = 0;
196     for (int treasure = 1; treasure <= NOBJECTS; treasure++) {
197         if (objects[treasure].is_treasure) {
198             if (valgame->prop[treasure] == STATE_NOTFOUND) {
199                 ++temp_tally;
200             }
201         }
202     }
203     if (temp_tally != valgame->tally) {
204         return false;
205     }
206
207     /* Check that properties of objects aren't beyond expected */
208     for (obj_t obj = 0; obj <= NOBJECTS; obj++) {
209         if (valgame->prop[obj] < STATE_NOTFOUND || valgame->prop[obj] > 1) {
210             switch (obj) {
211             case RUG:
212             case DRAGON:
213             case BIRD:
214             case BOTTLE:
215             case PLANT:
216             case PLANT2:
217             case TROLL:
218             case URN:
219             case EGGS:
220             case VASE:
221             case CHAIN:
222                 if (valgame->prop[obj] == 2) // There are multiple different states, but it's convenient to clump them together
223                     continue;
224             /* FALLTHRU */
225             case BEAR:
226                 if (valgame->prop[BEAR] == CONTENTED_BEAR || valgame->prop[BEAR] == BEAR_DEAD)
227                     continue;
228             /* FALLTHRU */
229             default:
230                 return false;
231             }
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->atloc[loc] < NO_OBJECT || valgame->atloc[loc] > NOBJECTS * 2) {
238             return false;
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;
244         }
245     }
246
247     return true;
248 }
249
250 /* end */