mirror of
https://github.com/yquake2/yquake2remaster.git
synced 2025-03-07 01:50:49 +00:00
Update to: * https://github.com/yquake2/xatrix/releases/tag/XATRIX_2_10 * https://github.com/yquake2/rogue/releases/tag/ROGUE_2_09 * https://github.com/yquake2/ctf/releases/tag/CTF_1_09 Full sync required: * https://github.com/yquake2/rogue/issues/122 * https://github.com/yquake2/xatrix/issues/96
1176 lines
22 KiB
C
1176 lines
22 KiB
C
/*
|
|
* Copyright (c) ZeniMax Media Inc.
|
|
* Licensed under the GNU General Public License 2.0.
|
|
*/
|
|
/*
|
|
* =======================================================================
|
|
*
|
|
* The savegame system.
|
|
*
|
|
* =======================================================================
|
|
*/
|
|
|
|
/*
|
|
* This is the Quake 2 savegame system, fixed by Yamagi
|
|
* based on an idea by Knightmare of kmquake2. This major
|
|
* rewrite of the original g_save.c is much more robust
|
|
* and portable since it doesn't use any function pointers.
|
|
*
|
|
* Inner workings:
|
|
* When the game is saved all function pointers are
|
|
* translated into human readable function definition strings.
|
|
* The same way all mmove_t pointers are translated. This
|
|
* human readable strings are then written into the file.
|
|
* At game load the human readable strings are retranslated
|
|
* into the actual function pointers and struct pointers. The
|
|
* pointers are generated at each compilation / start of the
|
|
* client, thus the pointers are always correct.
|
|
*
|
|
* Limitations:
|
|
* While savegames survive recompilations of the game source
|
|
* and bigger changes in the source, there are some limitation
|
|
* which a nearly impossible to fix without a object orientated
|
|
* rewrite of the game.
|
|
* - If functions or mmove_t structs that a referencenced
|
|
* inside savegames are added or removed (e.g. the files
|
|
* in tables/ are altered) the load functions cannot
|
|
* reconnect all pointers and thus not restore the game.
|
|
* - If the operating system is changed internal structures
|
|
* may change in an unrepairable way.
|
|
* - If the architecture is changed pointer length and
|
|
* other internal datastructures change in an
|
|
* incompatible way.
|
|
* - If the edict_t struct is changed, savegames
|
|
* will break.
|
|
* This is not so bad as it looks since functions and
|
|
* struct won't be added and edict_t won't be changed
|
|
* if no big, sweeping changes are done. The operating
|
|
* system and architecture are in the hands of the user.
|
|
*/
|
|
|
|
#include "../header/local.h"
|
|
|
|
/*
|
|
* When ever the savegame version is changed, q2 will refuse to
|
|
* load older savegames. This should be bumped if the files
|
|
* in tables/ are changed, otherwise strange things may happen.
|
|
*/
|
|
#define SAVEGAMEVER "YQ2-4"
|
|
|
|
|
|
/*
|
|
* This macros are used to prohibit loading of savegames
|
|
* created on other systems or architectures. This will
|
|
* crash q2 in spectacular ways
|
|
*/
|
|
#ifndef YQ2OSTYPE
|
|
#error YQ2OSTYPE should be defined by the build system
|
|
#endif
|
|
|
|
#ifndef YQ2ARCH
|
|
#error YQ2ARCH should be defined by the build system
|
|
#endif
|
|
|
|
/*
|
|
* Older operating system and architecture detection
|
|
* macros, implemented by savegame version YQ2-1.
|
|
*/
|
|
#if defined(__APPLE__)
|
|
#define YQ2OSTYPE_1 "MacOS X"
|
|
#elif defined(__FreeBSD__)
|
|
#define YQ2OSTYPE_1 "FreeBSD"
|
|
#elif defined(__OpenBSD__)
|
|
#define YQ2OSTYPE_1 "OpenBSD"
|
|
#elif defined(__linux__)
|
|
#define YQ2OSTYPE_1 "Linux"
|
|
#elif defined(_WIN32)
|
|
#define YQ2OSTYPE_1 "Windows"
|
|
#else
|
|
#define YQ2OSTYPE_1 "Unknown"
|
|
#endif
|
|
|
|
#if defined(__i386__)
|
|
#define YQ2ARCH_1 "i386"
|
|
#elif defined(__x86_64__)
|
|
#define YQ2ARCH_1 "amd64"
|
|
#elif defined(__sparc__)
|
|
#define YQ2ARCH_1 "sparc64"
|
|
#elif defined(__ia64__)
|
|
#define YQ2ARCH_1 "ia64"
|
|
#else
|
|
#define YQ2ARCH_1 "unknown"
|
|
#endif
|
|
|
|
/*
|
|
* Connects a human readable
|
|
* function signature with
|
|
* the corresponding pointer
|
|
*/
|
|
typedef struct
|
|
{
|
|
char *funcStr;
|
|
byte *funcPtr;
|
|
} functionList_t;
|
|
|
|
/*
|
|
* Connects a human readable
|
|
* mmove_t string with the
|
|
* correspondig pointer
|
|
* */
|
|
typedef struct
|
|
{
|
|
char *mmoveStr;
|
|
mmove_t *mmovePtr;
|
|
} mmoveList_t;
|
|
|
|
typedef struct
|
|
{
|
|
char ver[32];
|
|
char game[32];
|
|
char os[32];
|
|
char arch[32];
|
|
} savegameHeader_t;
|
|
|
|
|
|
/* ========================================================= */
|
|
|
|
/*
|
|
* Prototypes for forward
|
|
* declaration for all game
|
|
* functions.
|
|
*/
|
|
#include "tables/gamefunc_decs.h"
|
|
|
|
/*
|
|
* List with function pointer
|
|
* to each of the functions
|
|
* prototyped above.
|
|
*/
|
|
functionList_t functionList[] = {
|
|
#include "tables/gamefunc_list.h"
|
|
};
|
|
|
|
/*
|
|
* Prtotypes for forward
|
|
* declaration for all game
|
|
* mmove_t functions.
|
|
*/
|
|
#include "tables/gamemmove_decs.h"
|
|
|
|
/*
|
|
* List with pointers to
|
|
* each of the mmove_t
|
|
* functions prototyped
|
|
* above.
|
|
*/
|
|
mmoveList_t mmoveList[] = {
|
|
#include "tables/gamemmove_list.h"
|
|
};
|
|
|
|
/*
|
|
* Fields to be saved
|
|
*/
|
|
field_t fields[] = {
|
|
#include "tables/fields.h"
|
|
};
|
|
|
|
/*
|
|
* Level fields to
|
|
* be saved
|
|
*/
|
|
field_t levelfields[] = {
|
|
#include "tables/levelfields.h"
|
|
};
|
|
|
|
/*
|
|
* Client fields to
|
|
* be saved
|
|
*/
|
|
field_t clientfields[] = {
|
|
#include "tables/clientfields.h"
|
|
};
|
|
|
|
/* ========================================================= */
|
|
|
|
/*
|
|
* This will be called when the dll is first loaded,
|
|
* which only happens when a new game is started or
|
|
* a save game is loaded.
|
|
*/
|
|
void
|
|
InitGame(void)
|
|
{
|
|
gi.dprintf("Game is starting up.\n");
|
|
gi.dprintf("Game is %s built on %s.\n", GAMEVERSION, __DATE__);
|
|
|
|
gun_x = gi.cvar ("gun_x", "0", 0);
|
|
gun_y = gi.cvar ("gun_y", "0", 0);
|
|
gun_z = gi.cvar ("gun_z", "0", 0);
|
|
sv_rollspeed = gi.cvar ("sv_rollspeed", "200", 0);
|
|
sv_rollangle = gi.cvar ("sv_rollangle", "2", 0);
|
|
sv_maxvelocity = gi.cvar ("sv_maxvelocity", "2000", 0);
|
|
sv_gravity = gi.cvar ("sv_gravity", "800", 0);
|
|
|
|
/* noset vars */
|
|
dedicated = gi.cvar ("dedicated", "0", CVAR_NOSET);
|
|
|
|
/* latched vars */
|
|
sv_cheats = gi.cvar ("cheats", "0", CVAR_SERVERINFO|CVAR_LATCH);
|
|
gi.cvar ("gamename", GAMEVERSION , CVAR_SERVERINFO | CVAR_LATCH);
|
|
gi.cvar ("gamedate", __DATE__ , CVAR_SERVERINFO | CVAR_LATCH);
|
|
maxclients = gi.cvar ("maxclients", "4", CVAR_SERVERINFO | CVAR_LATCH);
|
|
maxspectators = gi.cvar ("maxspectators", "4", CVAR_SERVERINFO);
|
|
deathmatch = gi.cvar ("deathmatch", "0", CVAR_LATCH);
|
|
coop = gi.cvar ("coop", "0", CVAR_LATCH);
|
|
coop_elevator_delay = gi.cvar("coop_elevator_delay", "1.0", CVAR_ARCHIVE);
|
|
coop_pickup_weapons = gi.cvar("coop_pickup_weapons", "0", CVAR_ARCHIVE);
|
|
skill = gi.cvar ("skill", "1", CVAR_LATCH);
|
|
maxentities = gi.cvar ("maxentities", "1024", CVAR_LATCH);
|
|
g_footsteps = gi.cvar ("g_footsteps", "1", CVAR_ARCHIVE);
|
|
g_fix_triggered = gi.cvar ("g_fix_triggered", "0", 0);
|
|
|
|
/* change anytime vars */
|
|
dmflags = gi.cvar ("dmflags", "0", CVAR_SERVERINFO);
|
|
fraglimit = gi.cvar ("fraglimit", "0", CVAR_SERVERINFO);
|
|
timelimit = gi.cvar ("timelimit", "0", CVAR_SERVERINFO);
|
|
password = gi.cvar ("password", "", CVAR_USERINFO);
|
|
spectator_password = gi.cvar ("spectator_password", "", CVAR_USERINFO);
|
|
needpass = gi.cvar ("needpass", "0", CVAR_SERVERINFO);
|
|
filterban = gi.cvar ("filterban", "1", 0);
|
|
g_select_empty = gi.cvar ("g_select_empty", "0", CVAR_ARCHIVE);
|
|
run_pitch = gi.cvar ("run_pitch", "0.002", 0);
|
|
run_roll = gi.cvar ("run_roll", "0.005", 0);
|
|
bob_up = gi.cvar ("bob_up", "0.005", 0);
|
|
bob_pitch = gi.cvar ("bob_pitch", "0.002", 0);
|
|
bob_roll = gi.cvar ("bob_roll", "0.002", 0);
|
|
|
|
/* flood control */
|
|
flood_msgs = gi.cvar ("flood_msgs", "4", 0);
|
|
flood_persecond = gi.cvar ("flood_persecond", "4", 0);
|
|
flood_waitdelay = gi.cvar ("flood_waitdelay", "10", 0);
|
|
|
|
/* dm map list */
|
|
sv_maplist = gi.cvar ("sv_maplist", "", 0);
|
|
|
|
/* others */
|
|
aimfix = gi.cvar("aimfix", "0", CVAR_ARCHIVE);
|
|
g_machinegun_norecoil = gi.cvar("g_machinegun_norecoil", "0", CVAR_ARCHIVE);
|
|
|
|
/* items */
|
|
InitItems ();
|
|
|
|
game.helpmessage1[0] = 0;
|
|
game.helpmessage2[0] = 0;
|
|
|
|
/* initialize all entities for this game */
|
|
game.maxentities = maxentities->value;
|
|
g_edicts = gi.TagMalloc (game.maxentities * sizeof(g_edicts[0]), TAG_GAME);
|
|
globals.edicts = g_edicts;
|
|
globals.max_edicts = game.maxentities;
|
|
|
|
/* initialize all clients for this game */
|
|
game.maxclients = maxclients->value;
|
|
game.clients = gi.TagMalloc (game.maxclients * sizeof(game.clients[0]), TAG_GAME);
|
|
globals.num_edicts = game.maxclients+1;
|
|
}
|
|
|
|
/* ========================================================= */
|
|
|
|
/*
|
|
* Helper function to get
|
|
* the human readable function
|
|
* definition by an address.
|
|
* Called by WriteField1 and
|
|
* WriteField2.
|
|
*/
|
|
functionList_t *
|
|
GetFunctionByAddress(byte *adr)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; functionList[i].funcStr; i++)
|
|
{
|
|
if (functionList[i].funcPtr == adr)
|
|
{
|
|
return &functionList[i];
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* Helper function to get the
|
|
* pointer to a function by
|
|
* it's human readable name.
|
|
* Called by WriteField1 and
|
|
* WriteField2.
|
|
*/
|
|
byte *
|
|
FindFunctionByName(char *name)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; functionList[i].funcStr; i++)
|
|
{
|
|
if (!strcmp(name, functionList[i].funcStr))
|
|
{
|
|
return functionList[i].funcPtr;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* Helper function to get the
|
|
* human readable definition of
|
|
* a mmove_t struct by a pointer.
|
|
*/
|
|
mmoveList_t *
|
|
GetMmoveByAddress(mmove_t *adr)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; mmoveList[i].mmoveStr; i++)
|
|
{
|
|
if (mmoveList[i].mmovePtr == adr)
|
|
{
|
|
return &mmoveList[i];
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* Helper function to get the
|
|
* pointer to a mmove_t struct
|
|
* by a human readable definition.
|
|
*/
|
|
mmove_t *
|
|
FindMmoveByName(char *name)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; mmoveList[i].mmoveStr; i++)
|
|
{
|
|
if (!strcmp(name, mmoveList[i].mmoveStr))
|
|
{
|
|
return mmoveList[i].mmovePtr;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
/* ========================================================= */
|
|
|
|
/*
|
|
* The following two functions are
|
|
* doing the dirty work to write the
|
|
* data generated by the functions
|
|
* below this block into files.
|
|
*/
|
|
void
|
|
WriteField1(FILE *f, field_t *field, byte *base)
|
|
{
|
|
void *p;
|
|
int len;
|
|
int index;
|
|
functionList_t *func;
|
|
mmoveList_t *mmove;
|
|
|
|
if (field->flags & FFL_SPAWNTEMP)
|
|
{
|
|
return;
|
|
}
|
|
|
|
p = (void *)(base + field->ofs);
|
|
|
|
switch (field->type)
|
|
{
|
|
case F_INT:
|
|
case F_FLOAT:
|
|
case F_ANGLEHACK:
|
|
case F_VECTOR:
|
|
case F_IGNORE:
|
|
break;
|
|
|
|
case F_LSTRING:
|
|
case F_GSTRING:
|
|
|
|
if (*(char **)p)
|
|
{
|
|
len = strlen(*(char **)p) + 1;
|
|
}
|
|
else
|
|
{
|
|
len = 0;
|
|
}
|
|
|
|
*(int *)p = len;
|
|
break;
|
|
case F_EDICT:
|
|
|
|
if (*(edict_t **)p == NULL)
|
|
{
|
|
index = -1;
|
|
}
|
|
else
|
|
{
|
|
index = *(edict_t **)p - g_edicts;
|
|
}
|
|
|
|
*(int *)p = index;
|
|
break;
|
|
case F_CLIENT:
|
|
|
|
if (*(gclient_t **)p == NULL)
|
|
{
|
|
index = -1;
|
|
}
|
|
else
|
|
{
|
|
index = *(gclient_t **)p - game.clients;
|
|
}
|
|
|
|
*(int *)p = index;
|
|
break;
|
|
case F_ITEM:
|
|
|
|
if (*(edict_t **)p == NULL)
|
|
{
|
|
index = -1;
|
|
}
|
|
else
|
|
{
|
|
index = *(gitem_t **)p - itemlist;
|
|
}
|
|
|
|
*(int *)p = index;
|
|
break;
|
|
case F_FUNCTION:
|
|
|
|
if (*(byte **)p == NULL)
|
|
{
|
|
len = 0;
|
|
}
|
|
else
|
|
{
|
|
func = GetFunctionByAddress (*(byte **)p);
|
|
|
|
if (!func)
|
|
{
|
|
gi.error ("WriteField1: function not in list, can't save game");
|
|
}
|
|
|
|
len = strlen(func->funcStr)+1;
|
|
}
|
|
|
|
*(int *)p = len;
|
|
break;
|
|
case F_MMOVE:
|
|
|
|
if (*(byte **)p == NULL)
|
|
{
|
|
len = 0;
|
|
}
|
|
else
|
|
{
|
|
mmove = GetMmoveByAddress (*(mmove_t **)p);
|
|
|
|
if (!mmove)
|
|
{
|
|
gi.error ("WriteField1: mmove not in list, can't save game");
|
|
}
|
|
|
|
len = strlen(mmove->mmoveStr)+1;
|
|
}
|
|
|
|
*(int *)p = len;
|
|
break;
|
|
default:
|
|
gi.error("WriteEdict: unknown field type");
|
|
}
|
|
}
|
|
|
|
void
|
|
WriteField2(FILE *f, field_t *field, byte *base)
|
|
{
|
|
int len;
|
|
void *p;
|
|
functionList_t *func;
|
|
mmoveList_t *mmove;
|
|
|
|
if (field->flags & FFL_SPAWNTEMP)
|
|
{
|
|
return;
|
|
}
|
|
|
|
p = (void *)(base + field->ofs);
|
|
|
|
switch (field->type)
|
|
{
|
|
case F_LSTRING:
|
|
|
|
if (*(char **)p)
|
|
{
|
|
len = strlen(*(char **)p) + 1;
|
|
fwrite(*(char **)p, len, 1, f);
|
|
}
|
|
|
|
break;
|
|
case F_FUNCTION:
|
|
|
|
if (*(byte **)p)
|
|
{
|
|
func = GetFunctionByAddress (*(byte **)p);
|
|
|
|
if (!func)
|
|
{
|
|
gi.error ("WriteField2: function not in list, can't save game");
|
|
}
|
|
|
|
len = strlen(func->funcStr)+1;
|
|
fwrite (func->funcStr, len, 1, f);
|
|
}
|
|
|
|
break;
|
|
case F_MMOVE:
|
|
|
|
if (*(byte **)p)
|
|
{
|
|
mmove = GetMmoveByAddress (*(mmove_t **)p);
|
|
|
|
if (!mmove)
|
|
{
|
|
gi.error ("WriteField2: mmove not in list, can't save game");
|
|
}
|
|
|
|
len = strlen(mmove->mmoveStr)+1;
|
|
fwrite (mmove->mmoveStr, len, 1, f);
|
|
}
|
|
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* ========================================================= */
|
|
|
|
/*
|
|
* This function does the dirty
|
|
* work to read the data from a
|
|
* file. The processing of the
|
|
* data is done in the functions
|
|
* below
|
|
*/
|
|
void
|
|
ReadField(FILE *f, field_t *field, byte *base)
|
|
{
|
|
void *p;
|
|
int len;
|
|
int index;
|
|
char funcStr[2048];
|
|
|
|
if (field->flags & FFL_SPAWNTEMP)
|
|
{
|
|
return;
|
|
}
|
|
|
|
p = (void *)(base + field->ofs);
|
|
|
|
switch (field->type)
|
|
{
|
|
case F_INT:
|
|
case F_FLOAT:
|
|
case F_ANGLEHACK:
|
|
case F_VECTOR:
|
|
case F_IGNORE:
|
|
break;
|
|
|
|
case F_LSTRING:
|
|
len = *(int *)p;
|
|
|
|
if (!len)
|
|
{
|
|
*(char **)p = NULL;
|
|
}
|
|
else
|
|
{
|
|
*(char **)p = gi.TagMalloc(32 + len, TAG_LEVEL);
|
|
fread(*(char **)p, len, 1, f);
|
|
}
|
|
|
|
break;
|
|
case F_EDICT:
|
|
index = *(int *)p;
|
|
|
|
if (index == -1)
|
|
{
|
|
*(edict_t **)p = NULL;
|
|
}
|
|
else
|
|
{
|
|
*(edict_t **)p = &g_edicts[index];
|
|
}
|
|
|
|
break;
|
|
case F_CLIENT:
|
|
index = *(int *)p;
|
|
|
|
if (index == -1)
|
|
{
|
|
*(gclient_t **)p = NULL;
|
|
}
|
|
else
|
|
{
|
|
*(gclient_t **)p = &game.clients[index];
|
|
}
|
|
|
|
break;
|
|
case F_ITEM:
|
|
index = *(int *)p;
|
|
|
|
if (index == -1)
|
|
{
|
|
*(gitem_t **)p = NULL;
|
|
}
|
|
else
|
|
{
|
|
*(gitem_t **)p = &itemlist[index];
|
|
}
|
|
|
|
break;
|
|
case F_FUNCTION:
|
|
len = *(int *)p;
|
|
|
|
if (!len)
|
|
{
|
|
*(byte **)p = NULL;
|
|
}
|
|
else
|
|
{
|
|
if (len > sizeof(funcStr))
|
|
{
|
|
gi.error ("ReadField: function name is longer than buffer (%i chars)",
|
|
(int)sizeof(funcStr));
|
|
}
|
|
|
|
fread (funcStr, len, 1, f);
|
|
|
|
if ( !(*(byte **)p = FindFunctionByName (funcStr)) )
|
|
{
|
|
gi.error ("ReadField: function %s not found in table, can't load game", funcStr);
|
|
}
|
|
|
|
}
|
|
break;
|
|
case F_MMOVE:
|
|
len = *(int *)p;
|
|
|
|
if (!len)
|
|
{
|
|
*(byte **)p = NULL;
|
|
}
|
|
else
|
|
{
|
|
if (len > sizeof(funcStr))
|
|
{
|
|
gi.error ("ReadField: mmove name is longer than buffer (%i chars)",
|
|
(int)sizeof(funcStr));
|
|
}
|
|
|
|
fread (funcStr, len, 1, f);
|
|
|
|
if ( !(*(mmove_t **)p = FindMmoveByName (funcStr)) )
|
|
{
|
|
gi.error ("ReadField: mmove %s not found in table, can't load game", funcStr);
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
gi.error("ReadEdict: unknown field type");
|
|
}
|
|
}
|
|
|
|
/* ========================================================= */
|
|
|
|
/*
|
|
* Write the client struct into a file.
|
|
*/
|
|
void
|
|
WriteClient(FILE *f, gclient_t *client)
|
|
{
|
|
field_t *field;
|
|
gclient_t temp;
|
|
|
|
/* all of the ints, floats, and vectors stay as they are */
|
|
temp = *client;
|
|
|
|
/* change the pointers to indexes */
|
|
for (field = clientfields; field->name; field++)
|
|
{
|
|
WriteField1(f, field, (byte *)&temp);
|
|
}
|
|
|
|
/* write the block */
|
|
fwrite(&temp, sizeof(temp), 1, f);
|
|
|
|
/* now write any allocated data following the edict */
|
|
for (field = clientfields; field->name; field++)
|
|
{
|
|
WriteField2(f, field, (byte *)client);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Read the client struct from a file
|
|
*/
|
|
void
|
|
ReadClient(FILE *f, gclient_t *client, short save_ver)
|
|
{
|
|
field_t *field;
|
|
|
|
fread(client, sizeof(*client), 1, f);
|
|
|
|
for (field = clientfields; field->name; field++)
|
|
{
|
|
if (field->save_ver <= save_ver)
|
|
{
|
|
ReadField(f, field, (byte *)client);
|
|
}
|
|
}
|
|
|
|
if (save_ver < 3)
|
|
{
|
|
InitClientResp(client);
|
|
}
|
|
}
|
|
|
|
/* ========================================================= */
|
|
|
|
/*
|
|
* Writes the game struct into
|
|
* a file. This is called when
|
|
* ever the games goes to e new
|
|
* level or the user saves the
|
|
* game. Saved informations are:
|
|
* - cross level data
|
|
* - client states
|
|
* - help computer info
|
|
*/
|
|
void
|
|
WriteGame(const char *filename, qboolean autosave)
|
|
{
|
|
savegameHeader_t sv;
|
|
FILE *f;
|
|
int i;
|
|
|
|
if (!autosave)
|
|
{
|
|
SaveClientData();
|
|
}
|
|
|
|
f = fopen(filename, "wb");
|
|
|
|
if (!f)
|
|
{
|
|
gi.error("Couldn't open %s", filename);
|
|
}
|
|
|
|
/* Savegame identification */
|
|
memset(&sv, 0, sizeof(sv));
|
|
|
|
Q_strlcpy(sv.ver, SAVEGAMEVER, sizeof(sv.ver) - 1);
|
|
Q_strlcpy(sv.game, GAMEVERSION, sizeof(sv.game) - 1);
|
|
Q_strlcpy(sv.os, YQ2OSTYPE, sizeof(sv.os) - 1);
|
|
Q_strlcpy(sv.arch, YQ2ARCH, sizeof(sv.arch) - 1);
|
|
|
|
fwrite(&sv, sizeof(sv), 1, f);
|
|
|
|
game.autosaved = autosave;
|
|
fwrite(&game, sizeof(game), 1, f);
|
|
game.autosaved = false;
|
|
|
|
for (i = 0; i < game.maxclients; i++)
|
|
{
|
|
WriteClient(f, &game.clients[i]);
|
|
}
|
|
|
|
fclose(f);
|
|
}
|
|
|
|
/*
|
|
* Read the game structs from
|
|
* a file. Called when ever a
|
|
* savegames is loaded.
|
|
*/
|
|
void
|
|
ReadGame(const char *filename)
|
|
{
|
|
savegameHeader_t sv;
|
|
FILE *f;
|
|
int i;
|
|
|
|
short save_ver = 0;
|
|
|
|
gi.FreeTags(TAG_GAME);
|
|
|
|
f = fopen(filename, "rb");
|
|
|
|
if (!f)
|
|
{
|
|
gi.error("Couldn't open %s", filename);
|
|
}
|
|
|
|
/* Sanity checks */
|
|
fread(&sv, sizeof(sv), 1, f);
|
|
|
|
static const struct {
|
|
const char* verstr;
|
|
int vernum;
|
|
} version_mappings[] = {
|
|
{"YQ2-1", 1},
|
|
{"YQ2-2", 2},
|
|
{"YQ2-3", 3},
|
|
{"YQ2-4", 4},
|
|
};
|
|
|
|
for (i=0; i < sizeof(version_mappings)/sizeof(version_mappings[0]); ++i)
|
|
{
|
|
if (strcmp(version_mappings[i].verstr, sv.ver) == 0)
|
|
{
|
|
save_ver = version_mappings[i].vernum;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (save_ver == 0) // not found in mappings table
|
|
{
|
|
fclose(f);
|
|
gi.error("Savegame from an incompatible version.\n");
|
|
}
|
|
else if (save_ver == 1)
|
|
{
|
|
if (strcmp(sv.game, GAMEVERSION) != 0)
|
|
{
|
|
fclose(f);
|
|
gi.error("Savegame from an other game.so.\n");
|
|
}
|
|
else if (strcmp(sv.os, YQ2OSTYPE_1) != 0)
|
|
{
|
|
fclose(f);
|
|
gi.error("Savegame from an other os.\n");
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
/* Windows was forced to i386 */
|
|
if (strcmp(sv.arch, "i386") != 0)
|
|
{
|
|
fclose(f);
|
|
gi.error("Savegame from another architecture.\n");
|
|
}
|
|
#else
|
|
if (strcmp(sv.arch, YQ2ARCH_1) != 0)
|
|
{
|
|
fclose(f);
|
|
gi.error("Savegame from another architecture.\n");
|
|
}
|
|
#endif
|
|
}
|
|
else // all newer savegame versions
|
|
{
|
|
if (strcmp(sv.game, GAMEVERSION) != 0)
|
|
{
|
|
fclose(f);
|
|
gi.error("Savegame from another game.so.\n");
|
|
}
|
|
else if (strcmp(sv.os, YQ2OSTYPE) != 0)
|
|
{
|
|
fclose(f);
|
|
gi.error("Savegame from another os.\n");
|
|
}
|
|
else if (strcmp(sv.arch, YQ2ARCH) != 0)
|
|
{
|
|
#if defined(_WIN32) && (defined(__i386__) || defined(_M_IX86))
|
|
// before savegame version "YQ2-4" (and after version 1),
|
|
// the official Win32 binaries accidentally had the YQ2ARCH "AMD64"
|
|
// instead of "i386" set due to a bug in the Makefile.
|
|
// This quirk allows loading those savegames anyway
|
|
if (save_ver >= 4 || strcmp(sv.arch, "AMD64") != 0)
|
|
#endif
|
|
{
|
|
fclose(f);
|
|
gi.error("Savegame from another architecture.\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
g_edicts = gi.TagMalloc(game.maxentities * sizeof(g_edicts[0]), TAG_GAME);
|
|
globals.edicts = g_edicts;
|
|
|
|
fread(&game, sizeof(game), 1, f);
|
|
game.clients = gi.TagMalloc(game.maxclients * sizeof(game.clients[0]),
|
|
TAG_GAME);
|
|
|
|
for (i = 0; i < game.maxclients; i++)
|
|
{
|
|
ReadClient(f, &game.clients[i], save_ver);
|
|
}
|
|
|
|
fclose(f);
|
|
}
|
|
|
|
/* ========================================================== */
|
|
|
|
/*
|
|
* Helper function to write the
|
|
* edict into a file. Called by
|
|
* WriteLevel.
|
|
*/
|
|
void
|
|
WriteEdict(FILE *f, edict_t *ent)
|
|
{
|
|
field_t *field;
|
|
edict_t temp;
|
|
|
|
/* all of the ints, floats, and vectors stay as they are */
|
|
temp = *ent;
|
|
|
|
/* change the pointers to lengths or indexes */
|
|
for (field = fields; field->name; field++)
|
|
{
|
|
WriteField1(f, field, (byte *)&temp);
|
|
}
|
|
|
|
/* write the block */
|
|
fwrite(&temp, sizeof(temp), 1, f);
|
|
|
|
/* now write any allocated data following the edict */
|
|
for (field = fields; field->name; field++)
|
|
{
|
|
WriteField2(f, field, (byte *)ent);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Helper fcuntion to write the
|
|
* level local data into a file.
|
|
* Called by WriteLevel.
|
|
*/
|
|
void
|
|
WriteLevelLocals(FILE *f)
|
|
{
|
|
field_t *field;
|
|
level_locals_t temp;
|
|
|
|
/* all of the ints, floats, and vectors stay as they are */
|
|
temp = level;
|
|
|
|
/* change the pointers to lengths or indexes */
|
|
for (field = levelfields; field->name; field++)
|
|
{
|
|
WriteField1(f, field, (byte *)&temp);
|
|
}
|
|
|
|
/* write the block */
|
|
fwrite(&temp, sizeof(temp), 1, f);
|
|
|
|
/* now write any allocated data following the edict */
|
|
for (field = levelfields; field->name; field++)
|
|
{
|
|
WriteField2(f, field, (byte *)&level);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Writes the current level
|
|
* into a file.
|
|
*/
|
|
void
|
|
WriteLevel(const char *filename)
|
|
{
|
|
int i;
|
|
edict_t *ent;
|
|
FILE *f;
|
|
|
|
f = fopen(filename, "wb");
|
|
|
|
if (!f)
|
|
{
|
|
gi.error("Couldn't open %s", filename);
|
|
}
|
|
|
|
/* write out edict size for checking */
|
|
i = sizeof(edict_t);
|
|
fwrite(&i, sizeof(i), 1, f);
|
|
|
|
/* write out level_locals_t */
|
|
WriteLevelLocals(f);
|
|
|
|
/* write out all the entities */
|
|
for (i = 0; i < globals.num_edicts; i++)
|
|
{
|
|
ent = &g_edicts[i];
|
|
|
|
if (!ent->inuse)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
fwrite(&i, sizeof(i), 1, f);
|
|
WriteEdict(f, ent);
|
|
}
|
|
|
|
i = -1;
|
|
fwrite(&i, sizeof(i), 1, f);
|
|
|
|
fclose(f);
|
|
}
|
|
|
|
/* ========================================================== */
|
|
|
|
/*
|
|
* A helper function to
|
|
* read the edict back
|
|
* into the memory. Called
|
|
* by ReadLevel.
|
|
*/
|
|
void
|
|
ReadEdict(FILE *f, edict_t *ent)
|
|
{
|
|
field_t *field;
|
|
|
|
fread(ent, sizeof(*ent), 1, f);
|
|
|
|
for (field = fields; field->name; field++)
|
|
{
|
|
ReadField(f, field, (byte *)ent);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* A helper function to
|
|
* read the level local
|
|
* data from a file.
|
|
* Called by ReadLevel.
|
|
*/
|
|
void
|
|
ReadLevelLocals(FILE *f)
|
|
{
|
|
field_t *field;
|
|
|
|
fread(&level, sizeof(level), 1, f);
|
|
|
|
for (field = levelfields; field->name; field++)
|
|
{
|
|
ReadField(f, field, (byte *)&level);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Reads a level back into the memory.
|
|
* SpawnEntities were allready called
|
|
* in the same way when the level was
|
|
* saved. All world links were cleared
|
|
* befor this function was called. When
|
|
* this function is called, no clients
|
|
* are connected to the server.
|
|
*/
|
|
void
|
|
ReadLevel(const char *filename)
|
|
{
|
|
int entnum;
|
|
FILE *f;
|
|
int i;
|
|
edict_t *ent;
|
|
|
|
f = fopen(filename, "rb");
|
|
|
|
if (!f)
|
|
{
|
|
gi.error("Couldn't open %s", filename);
|
|
}
|
|
|
|
/* free any dynamic memory allocated by
|
|
loading the level base state */
|
|
gi.FreeTags(TAG_LEVEL);
|
|
|
|
/* wipe all the entities */
|
|
memset(g_edicts, 0, game.maxentities * sizeof(g_edicts[0]));
|
|
globals.num_edicts = maxclients->value + 1;
|
|
|
|
/* check edict size */
|
|
fread(&i, sizeof(i), 1, f);
|
|
|
|
if (i != sizeof(edict_t))
|
|
{
|
|
fclose(f);
|
|
gi.error("ReadLevel: mismatched edict size");
|
|
}
|
|
|
|
/* load the level locals */
|
|
ReadLevelLocals(f);
|
|
|
|
/* load all the entities */
|
|
while (1)
|
|
{
|
|
if (fread(&entnum, sizeof(entnum), 1, f) != 1)
|
|
{
|
|
fclose(f);
|
|
gi.error("ReadLevel: failed to read entnum");
|
|
}
|
|
|
|
if (entnum == -1)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (entnum >= globals.num_edicts)
|
|
{
|
|
globals.num_edicts = entnum + 1;
|
|
}
|
|
|
|
ent = &g_edicts[entnum];
|
|
ReadEdict(f, ent);
|
|
|
|
/* let the server rebuild world links for this ent */
|
|
memset(&ent->area, 0, sizeof(ent->area));
|
|
gi.linkentity(ent);
|
|
}
|
|
|
|
fclose(f);
|
|
|
|
/* mark all clients as unconnected */
|
|
for (i = 0; i < maxclients->value; i++)
|
|
{
|
|
ent = &g_edicts[i + 1];
|
|
ent->client = game.clients + i;
|
|
ent->client->pers.connected = false;
|
|
}
|
|
|
|
/* do any load time things at this point */
|
|
for (i = 0; i < globals.num_edicts; i++)
|
|
{
|
|
ent = &g_edicts[i];
|
|
|
|
if (!ent->inuse)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
/* fire any cross-level triggers */
|
|
if (ent->classname)
|
|
{
|
|
if (strcmp(ent->classname, "target_crosslevel_target") == 0)
|
|
{
|
|
ent->nextthink = level.time + ent->delay;
|
|
}
|
|
}
|
|
}
|
|
}
|