/*  savegame.c-- Routines for saving and restoring games */
/* Copyright (C) 1996-1999,2001  Robert Masenten          */
/* This program may be redistributed under the terms of the
   GNU General Public License, version 2; see agility.h for details. */
/*                                                        */
/* This is part of the source for AGiliTy                 */

#define SAVE_UNDO

#include <assert.h>
#include <stdio.h>
#include <stddef.h>

#include "agility.h"
#include "interp.h"
#include "exec.h"

#define DEBUG_SAVE_SIZE 0

long state_size;


/*-------------------------------------------------------------------*/
/*  INITIALISATION ROUTINES  */
/* These initialize all of the values that can be derived from */
/*   other data in the game file or that are reset when a game */
/*   is restored */
/* See parser.c for the interpreter's main initialisation routines */

void init_vals(void)
/* Compute quantities that can be deduced from existing data */
{
  int i;

  quitflag=winflag=deadflag=endflag=0;
  cmd_saveable=0;
  last_he=last_she=last_it=0;
  totwt=totsize=0;
  for(i=0;i<=maxroom-first_room;i++)
    room[i].contents=0;
  player_contents=player_worn=0;
  for(i=0;i<=maxnoun-first_noun;i++) {
    if (player_has(i+first_noun)) totwt+=noun[i].weight;
    if (noun[i].location==1) totsize+=noun[i].size;
    noun[i].something_pos_near_noun=0;
    noun[i].contents=noun[i].next=0;
  }
  for(i=0;i<=maxcreat-first_creat;i++) 
    creature[i].contents=creature[i].next=0;
  for(i=0;i<=maxnoun-first_noun;i++) {
    add_object(noun[i].location,i+first_noun);
    if (noun[i].nearby_noun>=first_noun && 
	noun[i].nearby_noun<=maxnoun)
      noun[noun[i].nearby_noun-first_noun].something_pos_near_noun=1;
  }
  for(i=0;i<=maxcreat-first_creat;i++)
    add_object(creature[i].location,i+first_creat);
  objscore=0; /* Will need to recompute this ... */
}




/*-------------------------------------------------------------------*/
/*  ROUTINES TO SAVE/RESTORE THE GAME STATE */
/* These are used by RESTART and UNDO as well as SAVE and RESTORE */

/* Game State format: */ 
/* The first two bytes indicate the length of the block (unsigned).*/ 
/* The next two bytes indicate the game file somehow (so we don't try to */
/* restore to a different game). */
/* After this comes the game information itself. */
/* All values are still little-endian (that is, LSB first) */

/* These are the macros for writing game information to the state block */
/* There is no difference between signed and unsigned when storing them;
 there will be problems when recovering them again. */

#define g(ft,var) {ft,DT_DEFAULT,&var,0}
#define r(ft,str,f) {ft,DT_DEFAULT,NULL,offsetof(str,f)}
#define dptype   {FT_DESCPTR,DT_DESCPTR,NULL,0}

static file_info fi_savehead[]={
  g(FT_INT16,loc), g(FT_INT32,tscore), g(FT_INT16,turncnt),
  g(FT_BYTE,statusmode),
  g(FT_BOOL,first_visit_flag), g(FT_BOOL,newlife_flag),
  g(FT_BOOL,room_firstdesc), g(FT_BOOL,verboseflag),
  g(FT_BOOL,notify_flag), g(FT_BOOL,listexit_flag),
  g(FT_BOOL,menu_mode), g(FT_BOOL,sound_on),
  g(FT_BOOL,agt_answer),g(FT_INT32,agt_number),
  g(FT_INT16,curr_time),g(FT_INT16,curr_lives),
  g(FT_INT16,delta_time),
  endrec
};

static file_info fi_saveroom[]={
  dptype,
  r(FT_BOOL,room_rec,seen),
  r(FT_BOOL,room_rec,locked_door),
  r(FT_INT16,room_rec,oclass),
  r(FT_INT16,room_rec,points),
  r(FT_INT16,room_rec,light),
  r(FT_PATHARRAY,room_rec,path),
  r(FT_UINT32,room_rec,flag_noun_bits),
  endrec
};

static file_info fi_savenoun[]={
  dptype,
  r(FT_INT16,noun_rec,location),
  r(FT_INT16,noun_rec,nearby_noun),
  r(FT_INT16,noun_rec,num_shots),
  r(FT_INT16,noun_rec,initdesc),
  r(FT_INT16,noun_rec,oclass),
  r(FT_INT16,noun_rec,points),
  r(FT_INT16,noun_rec,weight),
  r(FT_INT16,noun_rec,size),
  r(FT_BOOL,noun_rec,on),
  r(FT_BOOL,noun_rec,open),
  r(FT_BOOL,noun_rec,locked),
  r(FT_BOOL,noun_rec,movable),
  r(FT_BOOL,noun_rec,seen),
  r(FT_WORD,noun_rec,pos_prep),
  r(FT_WORD,noun_rec,pos_name),
  endrec
};

static file_info fi_savecreat[]={
  dptype,
  r(FT_INT16,creat_rec,location),
  r(FT_INT16,creat_rec,counter),
  r(FT_INT16,creat_rec,timecounter),
  r(FT_INT16,creat_rec,initdesc),
  r(FT_INT16,creat_rec,oclass),
  r(FT_INT16,creat_rec,points),
  r(FT_BOOL,creat_rec,groupmemb),
  r(FT_BOOL,creat_rec,hostile),
  r(FT_BOOL,creat_rec,seen),
  endrec
};

static file_info fi_saveustr[]={
  {FT_TLINE,DT_DEFAULT,NULL,0},
  endrec
};



uchar *getstate(uchar *gs) 
/* Returns block containing game state.  
  If gs!=NULL, uses that space as a buffer; 
  if gs==NULL, we malloc a new block and return it */
{
  rbool new_block; /* True if we allocate a new block */
  long bp;

  if (gs==NULL) {
    rm_trap=0; /* Don't exit on out-of-memory condition */
    gs=rmalloc(state_size); /* This should be enough. */
    rm_trap=1;
    if (gs==NULL) /* This is why we set rm_trap to 0 before calling rmalloc */
      return NULL;
    new_block=1;
  } else new_block=0;

  /* First two bytes reserved for block size, which we don't know yet.*/
  gs[4]=game_sig&0xFF;
  gs[5]=(game_sig>>8)&0xFF;

  tscore-=objscore;  /* Only include "permanent" part of score;
			objscore we can recompute on RESTORE */
  
  /* Need to setup here */
  set_internal_buffer(gs);
  fi_saveroom[0].ptr=room_ptr;
  fi_savenoun[0].ptr=noun_ptr;
  fi_savecreat[0].ptr=creat_ptr;

  bp=6;
  bp+=write_globalrec(fi_savehead,bp);
  bp+=write_recblock(flag,FT_BYTE,FLAG_NUM+1,bp);
  bp+=write_recblock(agt_counter,FT_INT16,CNT_NUM+1,bp);
  bp+=write_recblock(agt_var,FT_INT32,VAR_NUM+1,bp);
  bp+=write_recarray(room,sizeof(room_rec),rangefix(maxroom-first_room+1),
		     fi_saveroom,bp);
  bp+=write_recarray(noun,sizeof(noun_rec),rangefix(maxnoun-first_noun+1),
		     fi_savenoun,bp);
  bp+=write_recarray(creature,sizeof(creat_rec),
		     rangefix(maxcreat-first_creat+1),
		     fi_savecreat,bp);
  if (userstr!=NULL)
    bp+=write_recarray(userstr,sizeof(tline),MAX_USTR,fi_saveustr,bp);
  if (objflag!=NULL)
    bp+=write_recblock(objflag,FT_BYTE, objextsize(0), bp);
  if (objprop!=NULL)
    bp+=write_recblock(objprop,FT_INT32, objextsize(1), bp);
  set_internal_buffer(NULL);
  gs[0]=bp & 0xFF;
  gs[1]=(bp>>8) & 0xFF;
  gs[2]=(bp>>16) & 0xFF;
  gs[3]=(bp>>24) & 0x7F;  /* Don't trust top bit */
  if (new_block)
    gs=rrealloc(gs,bp); 
  tscore+=objscore;
  return gs;
}



void putstate(uchar *gs) /* Restores games state. */
{
  long size, bp, numrec, i;


  size=gs[0]+(((long)gs[1])<<8)+(((long)gs[2])<<16)+(((long)gs[3])<<24); 
  if (size!=state_size) {
    writeln("Size difference in save files!");
    agt_delay(3);
    return;
  }
  if (gs[4]+(((long)gs[5])<<8)!=game_sig) {
    writestr("This appears to be a save file for a different game. Is this"
	     " from an earlier chapter in a multi-part game such as"
	     " Klaustrophobia");     
    if (yesno("?")) 
      skip_descr=1; /* We don't want to overwrite the descriptions
		       with the pointers from the save file. */ 
    else {
      writestr("Do you want to try using it anyhow (WARNING: This could"
	       " crash the interpreter)");
      if (!(yesno("?"))) {
	writeln("Command cancelled!");
	agt_delay(3);
	return;
      }
    }
  }


  /* setup... */
  set_internal_buffer(gs);
  fi_saveroom[0].ptr=room_ptr;
  fi_savenoun[0].ptr=noun_ptr;
  fi_savecreat[0].ptr=creat_ptr;
  bp=6;

  read_globalrec(fi_savehead,0,bp,0);
    bp+=compute_recsize(fi_savehead);
  read_recblock(flag,FT_BYTE,FLAG_NUM+1,bp,0);
    bp+=ft_leng[FT_BYTE]*(FLAG_NUM+1);
  read_recblock(agt_counter,FT_INT16,CNT_NUM+1,bp,0);
    bp+=ft_leng[FT_INT16]*(CNT_NUM+1);
  read_recblock(agt_var,FT_INT32,VAR_NUM+1,bp,0);
    bp+=ft_leng[FT_INT32]*(VAR_NUM+1);
  
  numrec=rangefix(maxroom-first_room+1);
   read_recarray(room,sizeof(room_rec),numrec,fi_saveroom,0,bp,0);
   bp+=compute_recsize(fi_saveroom)*numrec;
  numrec=rangefix(maxnoun-first_noun+1);
   read_recarray(noun,sizeof(noun_rec),numrec,fi_savenoun,0,bp,0);
   bp+=compute_recsize(fi_savenoun)*numrec;
  numrec=rangefix(maxcreat-first_creat+1); 
   read_recarray(creature,sizeof(creat_rec),numrec,fi_savecreat,0,bp,0);
   bp+=compute_recsize(fi_savecreat)*numrec;
  if (userstr!=NULL) {
    read_recarray(userstr,sizeof(tline),MAX_USTR,fi_saveustr,0,bp,0);
    bp+=ft_leng[FT_TLINE]*MAX_USTR;
  }
  if (objflag!=NULL) {
    i=objextsize(0);
    read_recblock(objflag,FT_BYTE,i,bp,0);
    bp+=ft_leng[FT_BYTE]*i;
  }
  if (objprop!=NULL) {
    i=objextsize(1);
    read_recblock(objprop,FT_INT32,i,bp,0);
    bp+=ft_leng[FT_INT32]*i;
  }
  set_internal_buffer(NULL);

  if (skip_descr)   /* Need to "fix" position information. This is a hack. */
    /* Basically, this sets the position of each object to its default */
    /* The problem here is that the usual position info is invalid-- we've
       changed games, and hence dictionaries */
    for(i=0;i<maxnoun-first_noun;i++) {
      if (noun[i].position!=NULL && noun[i].position[0]!=0)
	noun[i].pos_prep=-1;
      else noun[i].pos_prep=0;
    }
  else   /* Rebuild position information */
    for(i=0;i<maxnoun-first_noun;i++) 
      if (noun[i].pos_prep==-1)
	noun[i].position=noun[i].initpos;
      else
	noun[i].position=NULL;

  init_vals();
  skip_descr=0; /* If we set this to 1, restore it to its original state */
/* Now do some simple consistancy checking on major variables */
  if (loc>maxroom || loc<0 || turncnt<0 || 
      curr_lives<0 || curr_lives>max_lives) {
    writeln("Error: Save file inconsistent.");
    exit(EXIT_FAILURE);
  }
}

void init_state_sys(void)
/* Initializes the state saving mechanisms */
/* Mainly it just computes the size of a state block */
{
  state_size=compute_recsize(fi_savehead) 
    + compute_recsize(fi_saveroom)*rangefix(maxroom-first_room+1) 
    + compute_recsize(fi_savenoun)*rangefix(maxnoun-first_noun+1)
    + compute_recsize(fi_savecreat)*rangefix(maxcreat-first_creat+1)
    + ft_leng[FT_BYTE]*(FLAG_NUM+1)
    + ft_leng[FT_INT16]*(CNT_NUM+1)
    + ft_leng[FT_INT32]*(VAR_NUM+1)
    + ft_leng[FT_BYTE]*objextsize(0)
    + ft_leng[FT_INT32]*objextsize(1)
    + 6;  /* Six bytes in header */
  if (userstr!=NULL) state_size+=ft_leng[FT_TLINE]*MAX_USTR; 
}


/*-------------------------------------------------------------------*/
/*  SAVE FILE ROUTINES    */

void savegame(void)
{
  genfile savefile;
  uchar *gs;
  long size;

#ifndef UNDO_SAVE
  gs=getstate(NULL);
#else
  gs=undo_state;
#endif
  if (gs==NULL) {
    writeln("Insufficiant memory to support SAVE.");
    return;
  }
  savefile=get_user_file(1);
  if (!filevalid(savefile,fSAV)) {
    writeln("That is not a valid save file.");
    return;
  }
  size=gs[0]+(((long)gs[1])<<8)+(((long)gs[2])<<16)+(((long)gs[3])<<24); 
  if (!binwrite(savefile,gs,size,1,0))
    writeln("Error writing save file.");
#ifndef UNDO_SAVE
  rfree(gs);
#endif
  writeclose(savefile,NO_FILE_ID);
}


rbool loadgame(void)
/* 1=success, 0=failure */
{
  genfile loadfile;
  long size;
  uchar *gs;
  char *errstr;

  loadfile=get_user_file(2);
  if (!filevalid(loadfile,fSAV)) {
    writeln("Unable to open file.");
    return 0;
  }
  size=binsize(loadfile);
  if (size==-1) {
    writeln("Could not access file.");
    readclose(loadfile);
    return 0;
  }
  gs=rmalloc(size);
  if (!binread(loadfile,gs,size,1,&errstr)) {
    writeln("Error reading file.");
    rfree(gs);readclose(loadfile);
    return 0;
  }
  readclose(loadfile);
  if (size!=gs[0]+(((long)gs[1])<<8)+(((long)gs[2])<<16)+(((long)gs[3])<<24)){
    if (size==gs[0]+(((long)gs[1])<<8)) { 
      /* Old save file format; patch to look like new format */
      gs=rrealloc(gs,size+2);
      memmove(gs+4,gs+2,size-2);
      gs[2]=gs[3]=0;
    } else {
      writeln("Save file corrupted or invalid.");
      rfree(gs);
      return 0;
    }
  }
  putstate(gs);
  rfree(gs);
  set_statline();
  look_room();
  return 1;
}

void restart_game(void)
{
  putstate(restart_state);
  agt_clrscr();	
  set_statline();
  do_look=do_autoverb=1;
  if (intro_ptr.size>0) {
    print_descr(intro_ptr,1);
    wait_return();
    agt_clrscr();
  }
  newroom();
}

