Rewrite save/resume in idiomatic C. Savefile version bumped to 26.

This commit is contained in:
Eric S. Raymond 2017-06-10 07:13:23 -04:00
parent 3af993abca
commit 9e8e0893dc
5 changed files with 73 additions and 188 deletions

View file

@ -14,7 +14,7 @@
VERS=1.0 VERS=1.0
CC?=gcc CC?=gcc
CCFLAGS+=-std=c99 -D _DEFAULT_SOURCE CCFLAGS+=-std=c99 -D _DEFAULT_SOURCE -g
LIBS= LIBS=
UNAME_S := $(shell uname -s) UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux) ifeq ($(UNAME_S),Linux)

1
NEWS
View file

@ -3,6 +3,7 @@
Repository head:: Repository head::
Include tests directory in generated tarball. Include tests directory in generated tarball.
Support command-line editing with arrow keys and Emacs keystrokes. Support command-line editing with arrow keys and Emacs keystrokes.
Save format has changed.
1.0: 2016-06-05:: 1.0: 2016-06-05::
Forward port of Crowther & Woods's 430-point Adventure 2.5. Forward port of Crowther & Woods's 430-point Adventure 2.5.

View file

@ -41,8 +41,8 @@ There have been no gameplay changes.
== BUGS == == BUGS ==
The binary save file format is fragile and unlikely to survive The binary save file format is fragile, dependent on your machine word
through version bumps. size and endianness, and unlikely to survive through version bumps.
== REPORTING BUGS == == REPORTING BUGS ==
Report bugs to Eric S. Raymond <esr@thyrsus.com>. The project page is Report bugs to Eric S. Raymond <esr@thyrsus.com>. The project page is

View file

@ -173,6 +173,13 @@ The adventure.text file is no longer required at runtime. Instead, it
is compiled at build time to a source module containing C structures, is compiled at build time to a source module containing C structures,
which is then linked to the advent binary. which is then linked to the advent binary.
The game-save format has changed. This was done to simplify
FORTRAN-derived code that formerly implemented these functions;
without C's fread(3)/fwrite() and structs it was necessarily pretty
ugly by modern stabdards. Encryption and checksumming have been
discarded - it's pointless to try tamper-prooing saves when everyone
has the source code.
== Sources == == Sources ==
[bibliography] [bibliography]

View file

@ -1,217 +1,94 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
#include "advent.h" #include "advent.h"
#include "database.h" #include "database.h"
#include "linenoise/linenoise.h" #include "linenoise/linenoise.h"
#define VRSION 25 /* bump on save format change */ /*
* (ESR) This replaces a bunch of particularly nasty FORTRAN-derived code;
* see the history.adoc file in the source distribution for discussion.
*/
static void fSAVWDS(long*,long*,long*,long*,long*,long*,long*); #define VRSION 26 /* bump on save format change */
#define SAVWDS(W1,W2,W3,W4,W5,W6,W7) fSAVWDS(&W1,&W2,&W3,&W4,&W5,&W6,&W7)
static void fSAVARR(long*,long);
#define SAVARR(ARR,N) fSAVARR(ARR,N)
static void fSAVWRD(long,long*);
#define SAVWRD(OP,WORD) fSAVWRD(OP,&WORD)
static void fSAVEIO(long,long,long*);
#define SAVEIO(OP,IN,ARR) fSAVEIO(OP,IN,ARR)
/* Suspend/resume I/O routines (SAVWDS, SAVARR, SAVWRD) */ /*
* If you change the first three members, the resume function may not properly
static void fSAVWDS(long *W1, long *W2, long *W3, long *W4, * reject saves from older versions. Yes, this glues us to a hardware-
long *W5, long *W6, long *W7) * dependent length of long. Later members can change, but bump the version
/* Write or read 7 variables. See SAVWRD. */ * when you do that.
{ */
SAVWRD(0,(*W1)); struct save_t {
SAVWRD(0,(*W2)); long savetime;
SAVWRD(0,(*W3)); long mode; /* not used, must be present for version detection */
SAVWRD(0,(*W4)); long version;
SAVWRD(0,(*W5)); struct game_t game;
SAVWRD(0,(*W6)); long bird;
SAVWRD(0,(*W7)); long bivalve;
} };
struct save_t save;
static void fSAVARR(long arr[], long n)
/* Write or read an array of n words. See SAVWRD. */
{
long i;
for (i=1; i<=n; i++) {
SAVWRD(0,arr[i]);
}
return;
}
static void fSAVWRD(long op, long *pword)
/* If OP<0, start writing a file, using word to initialise encryption; save
* word in the file. If OP>0, start reading a file; read the file to find
* the value with which to decrypt the rest. In either case, if a file is
* already open, finish writing/reading it and don't start a new one. If OP=0,
* read/write a single word. Words are buffered in case that makes for more
* efficient disk use. We also compute a simple checksum to catch elementary
* poking within the saved file. When we finish reading/writing the file,
* we store zero into *PWORD if there's no checksum error, else nonzero. */
{
static long buf[250], cksum = 0, h1, hash = 0, n = 0, state = 0;
if (op != 0)
{
long ifvar = state;
switch (ifvar<0 ? -1 : (ifvar>0 ? 1 : 0))
{
case -1:
case 1:
if (n == 250)SAVEIO(1,state > 0,buf);
n=MOD(n,250)+1;
if (state <= 0) {
n--; buf[n]=cksum; n++;
SAVEIO(1,false,buf);
}
n--; *pword=buf[n]-cksum; n++;
SAVEIO(-1,state > 0,buf);
state=0;
break;
case 0: /* FIXME: Huh? should be impossible */
state=op;
SAVEIO(0,state > 0,buf);
n=1;
if (state <= 0) {
hash=MOD(*pword,1048576L);
buf[0]=1234L*5678L-hash;
}
SAVEIO(1,true,buf);
hash=MOD(1234L*5678L-buf[0],1048576L);
cksum=buf[0];
return;
}
}
if (state == 0)
return;
if (n == 250)
SAVEIO(1,state > 0,buf);
n=MOD(n,250)+1;
h1=MOD(hash*1093L+221573L,1048576L);
hash=MOD(h1*1093L+221573L,1048576L);
h1=MOD(h1,1234)*765432+MOD(hash,123);
n--;
if (state > 0)
*pword=buf[n]+h1;
buf[n]=*pword-h1;
n++;
cksum=MOD(cksum*13+*pword,1000000000L);
}
static void fSAVEIO(long op, long in, long arr[])
/* If OP=0, ask for a file name and open a file. (If IN=true, the file is for
* input, else output.) If OP>0, read/write ARR from/into the previously-opened
* file. (ARR is a 250-integer array.) If OP<0, finish reading/writing the
* file. (Finishing writing can be a no-op if a "stop" statement does it
* automatically. Finishing reading can be a no-op as long as a subsequent
* SAVEIO(0,false,X) will still work.) */
{
static FILE *fp = NULL;
char* name;
switch (op < 0 ? -1 : (op > 0 ? 1 : 0))
{
case -1:
fclose(fp);
break;
case 0:
while (fp == NULL) {
name = linenoise("File name: ");
fp = fopen(name,(in ? READ_MODE : WRITE_MODE));
if (fp == NULL)
printf("Can't open file %s, try again.\n", name);
}
linenoiseFree(name);
break;
case 1:
if (in)
IGNORE(fread(arr,sizeof(long),250,fp));
else
IGNORE(fwrite(arr,sizeof(long),250,fp));
break;
}
}
int saveresume(FILE *input, bool resume) int saveresume(FILE *input, bool resume)
/* Suspend and resume */ /* Suspend and resume */
{ {
int kk; long i, k;
long i; FILE *fp = NULL;
char *name;
if (!resume) { if (!resume) {
/* Suspend. Offer to save things in a file, but charging /* Suspend. Offer to save things in a file, but charging
* some points (so can't win by using saved games to retry * some points (so can't win by using saved games to retry
* battles or to start over after learning zzword). */ * battles or to start over after learning zzword). */
SPK=201;
RSPEAK(260); RSPEAK(260);
if (!YES(input,200,54,54)) return(2012); if (!YES(input,200,54,54)) return(2012);
game.saved=game.saved+5; game.saved=game.saved+5;
kk= -1;
} }
else else
{ {
/* Resume. Read a suspended game back from a file. */ /* Resume. Read a suspended game back from a file. */
kk=1;
if (game.loc != 1 || game.abbrev[1] != 1) { if (game.loc != 1 || game.abbrev[1] != 1) {
RSPEAK(268); RSPEAK(268);
if (!YES(input,200,54,54)) return(2012); if (!YES(input,200,54,54)) return(2012);
} }
} }
/* Suspend vs resume cases are distinguished by the value of kk while (fp == NULL) {
* (-1 for suspend, +1 for resume). */ name = linenoise("File name: ");
fp = fopen(name,(resume ? READ_MODE : WRITE_MODE));
if (fp == NULL)
printf("Can't open file %s, try again.\n", name);
}
linenoiseFree(name);
/* DATIME(&i,&k);
* FIXME: This is way more complicated than it needs to be in C. k=i+650*k;
* What we ought to do is define a save-block structure that if (!resume)
* includes a game state block and then use a single fread/fwrite {
* for I/O. All the SAV* functions can be scrapped. save.savetime = k;
*/ save.mode = -1;
save.version = VRSION;
DATIME(&i,&K); memcpy(&save.game, &game, sizeof(struct game_t));
K=i+650*K; save.bird = OBJSND[BIRD];
SAVWRD(kk,K); save.bivalve = OBJTXT[OYSTER];
K=VRSION; IGNORE(fwrite(&save, sizeof(struct save_t), 1, fp));
SAVWRD(0,K); fclose(fp);
if (K != VRSION) { RSPEAK(266);
SETPRM(1,K/10,MOD(K,10)); exit(0);
} else {
IGNORE(fread(&save, sizeof(struct save_t), 1, fp));
fclose(fp);
if (save.version != VRSION) {
SETPRM(1,k/10,MOD(k,10));
SETPRM(3,VRSION/10,MOD(VRSION,10)); SETPRM(3,VRSION/10,MOD(VRSION,10));
RSPEAK(269); RSPEAK(269);
return(2000); return(2000);
} }
/* Herewith are all the variables whose values can change during a game, memcpy(&game, &save.game, sizeof(struct game_t));
* omitting a few (such as I, J) whose values between turns are OBJSND[BIRD] = save.bird;
* irrelevant and some whose values when a game is OBJTXT[OYSTER] = save.bivalve;
* suspended or resumed are guaranteed to match. If unsure whether a value
* needs to be saved, include it. Overkill can't hurt. Pad the last savwds
* with junk variables to bring it up to 7 values. */
SAVWDS(game.abbnum,game.blklin,game.bonus,game.clock1,game.clock2,game.closed,game.closng);
SAVWDS(game.detail,game.dflag,game.dkill,game.dtotal,game.foobar,game.holdng,game.iwest);
SAVWDS(game.knfloc,game.limit,K,game.lmwarn,game.loc,game.newloc,game.numdie);
SAVWDS(K,game.oldlc2,game.oldloc,game.oldobj,game.panic,game.saved,game.setup);
SAVWDS(SPK,game.tally,game.thresh,game.trndex,game.trnluz,game.turns,OBJTXT[OYSTER]);
SAVWDS(K,WD1,WD1X,WD2,game.wzdark,game.zzword,OBJSND[BIRD]);
SAVWDS(OBJTXT[SIGN],game.clshnt,game.novice,K,K,K,K);
SAVARR(game.abbrev,LOCSIZ);
SAVARR(game.atloc,LOCSIZ);
SAVARR(game.dloc,NDWARVES);
SAVARR(game.dseen,NDWARVES);
SAVARR(game.fixed,NOBJECTS);
SAVARR(game.hinted,HNTSIZ);
SAVARR(game.hintlc,HNTSIZ);
SAVARR(game.link,NOBJECTS*2);
SAVARR(game.odloc,NDWARVES);
SAVARR(game.place,NOBJECTS);
SAVARR(game.prop,NOBJECTS);
SAVWRD(kk,K);
if (K != 0) {
RSPEAK(270);
exit(0);
}
K=NUL;
game.zzword=RNDVOC(3,game.zzword); game.zzword=RNDVOC(3,game.zzword);
if (kk > 0) return(8); return(2000);
RSPEAK(266);
exit(0);
} }
}
/* end */