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
CC?=gcc
CCFLAGS+=-std=c99 -D _DEFAULT_SOURCE
CCFLAGS+=-std=c99 -D _DEFAULT_SOURCE -g
LIBS=
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)

1
NEWS
View file

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

View file

@ -41,8 +41,8 @@ There have been no gameplay changes.
== BUGS ==
The binary save file format is fragile and unlikely to survive
through version bumps.
The binary save file format is fragile, dependent on your machine word
size and endianness, and unlikely to survive through version bumps.
== REPORTING BUGS ==
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,
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 ==
[bibliography]

View file

@ -1,217 +1,94 @@
#include <stdlib.h>
#include <string.h>
#include "advent.h"
#include "database.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 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)
#define VRSION 26 /* bump on save format change */
/* Suspend/resume I/O routines (SAVWDS, SAVARR, SAVWRD) */
static void fSAVWDS(long *W1, long *W2, long *W3, long *W4,
long *W5, long *W6, long *W7)
/* Write or read 7 variables. See SAVWRD. */
{
SAVWRD(0,(*W1));
SAVWRD(0,(*W2));
SAVWRD(0,(*W3));
SAVWRD(0,(*W4));
SAVWRD(0,(*W5));
SAVWRD(0,(*W6));
SAVWRD(0,(*W7));
}
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;
}
}
/*
* If you change the first three members, the resume function may not properly
* reject saves from older versions. Yes, this glues us to a hardware-
* dependent length of long. Later members can change, but bump the version
* when you do that.
*/
struct save_t {
long savetime;
long mode; /* not used, must be present for version detection */
long version;
struct game_t game;
long bird;
long bivalve;
};
struct save_t save;
int saveresume(FILE *input, bool resume)
/* Suspend and resume */
{
int kk;
long i;
long i, k;
FILE *fp = NULL;
char *name;
if (!resume) {
/* Suspend. Offer to save things in a file, but charging
* some points (so can't win by using saved games to retry
* battles or to start over after learning zzword). */
SPK=201;
RSPEAK(260);
if (!YES(input,200,54,54)) return(2012);
game.saved=game.saved+5;
kk= -1;
}
else
{
/* Resume. Read a suspended game back from a file. */
kk=1;
if (game.loc != 1 || game.abbrev[1] != 1) {
RSPEAK(268);
if (!YES(input,200,54,54)) return(2012);
}
}
/* Suspend vs resume cases are distinguished by the value of kk
* (-1 for suspend, +1 for resume). */
/*
* FIXME: This is way more complicated than it needs to be in C.
* What we ought to do is define a save-block structure that
* includes a game state block and then use a single fread/fwrite
* for I/O. All the SAV* functions can be scrapped.
*/
DATIME(&i,&K);
K=i+650*K;
SAVWRD(kk,K);
K=VRSION;
SAVWRD(0,K);
if (K != VRSION) {
SETPRM(1,K/10,MOD(K,10));
SETPRM(3,VRSION/10,MOD(VRSION,10));
RSPEAK(269);
while (fp == NULL) {
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);
k=i+650*k;
if (!resume)
{
save.savetime = k;
save.mode = -1;
save.version = VRSION;
memcpy(&save.game, &game, sizeof(struct game_t));
save.bird = OBJSND[BIRD];
save.bivalve = OBJTXT[OYSTER];
IGNORE(fwrite(&save, sizeof(struct save_t), 1, fp));
fclose(fp);
RSPEAK(266);
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));
RSPEAK(269);
return(2000);
}
memcpy(&game, &save.game, sizeof(struct game_t));
OBJSND[BIRD] = save.bird;
OBJTXT[OYSTER] = save.bivalve;
game.zzword=RNDVOC(3,game.zzword);
return(2000);
}
/* Herewith are all the variables whose values can change during a game,
* omitting a few (such as I, J) whose values between turns are
* irrelevant and some whose values when a game is
* 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);
if (kk > 0) return(8);
RSPEAK(266);
exit(0);
}
/* end */