FossilOrigin-Name: b81e2948d74e606b374ad69956719d748340eb128dc5e4c732781a2de71c2ba2
575 lines
12 KiB
Text
575 lines
12 KiB
Text
// loading and saving of savegames & demos, dumps the spawn state of all
|
|
// mapents, the full state of all dynents (monsters + player)
|
|
|
|
#include "cube.h"
|
|
|
|
#import "DynamicEntity.h"
|
|
|
|
#ifdef OF_BIG_ENDIAN
|
|
static const int islittleendian = 0;
|
|
#else
|
|
static const int islittleendian = 1;
|
|
#endif
|
|
|
|
static gzFile f = NULL;
|
|
bool demorecording = false;
|
|
bool demoplayback = false;
|
|
bool demoloading = false;
|
|
static OFMutableArray<DynamicEntity *> *playerhistory;
|
|
int democlientnum = 0;
|
|
|
|
void startdemo();
|
|
|
|
void
|
|
gzput(int i)
|
|
{
|
|
gzputc(f, i);
|
|
}
|
|
|
|
void
|
|
gzputi(int i)
|
|
{
|
|
gzwrite(f, &i, sizeof(int));
|
|
}
|
|
|
|
void
|
|
gzputv(OFVector3D &v)
|
|
{
|
|
gzwrite(f, &v, sizeof(OFVector3D));
|
|
}
|
|
|
|
void
|
|
gzcheck(int a, int b)
|
|
{
|
|
if (a != b)
|
|
fatal(@"savegame file corrupt (short)");
|
|
}
|
|
|
|
int
|
|
gzget()
|
|
{
|
|
char c = gzgetc(f);
|
|
return c;
|
|
}
|
|
|
|
int
|
|
gzgeti()
|
|
{
|
|
int i;
|
|
gzcheck(gzread(f, &i, sizeof(int)), sizeof(int));
|
|
return i;
|
|
}
|
|
|
|
void
|
|
gzgetv(OFVector3D &v)
|
|
{
|
|
gzcheck(gzread(f, &v, sizeof(OFVector3D)), sizeof(OFVector3D));
|
|
}
|
|
|
|
void
|
|
stop()
|
|
{
|
|
if (f) {
|
|
if (demorecording)
|
|
gzputi(-1);
|
|
gzclose(f);
|
|
}
|
|
f = NULL;
|
|
demorecording = false;
|
|
demoplayback = false;
|
|
demoloading = false;
|
|
[playerhistory removeAllObjects];
|
|
}
|
|
|
|
void
|
|
stopifrecording()
|
|
{
|
|
if (demorecording)
|
|
stop();
|
|
}
|
|
|
|
void
|
|
savestate(OFIRI *IRI)
|
|
{
|
|
@autoreleasepool {
|
|
stop();
|
|
f = gzopen([IRI.fileSystemRepresentation
|
|
cStringWithEncoding:OFLocale.encoding],
|
|
"wb9");
|
|
if (!f) {
|
|
conoutf(@"could not write %@", IRI.string);
|
|
return;
|
|
}
|
|
gzwrite(f, (void *)"CUBESAVE", 8);
|
|
gzputc(f, islittleendian);
|
|
gzputi(SAVEGAMEVERSION);
|
|
OFData *data = [player1 dataBySerializing];
|
|
gzputi(data.count);
|
|
char map[_MAXDEFSTR] = { 0 };
|
|
memcpy(map, getclientmap().UTF8String,
|
|
min(getclientmap().UTF8StringLength, _MAXDEFSTR - 1));
|
|
gzwrite(f, map, _MAXDEFSTR);
|
|
gzputi(gamemode);
|
|
gzputi(ents.length());
|
|
loopv(ents) gzputc(f, ents[i].spawned);
|
|
gzwrite(f, data.items, data.count);
|
|
OFArray<DynamicEntity *> *monsters = getmonsters();
|
|
gzputi(monsters.count);
|
|
for (DynamicEntity *monster in monsters) {
|
|
data = [monster dataBySerializing];
|
|
gzwrite(f, data.items, data.count);
|
|
}
|
|
gzputi(players.count);
|
|
for (id player in players) {
|
|
gzput(player == [OFNull null]);
|
|
data = [player dataBySerializing];
|
|
gzwrite(f, data.items, data.count);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
savegame(OFString *name)
|
|
{
|
|
if (!m_classicsp) {
|
|
conoutf(@"can only save classic sp games");
|
|
return;
|
|
}
|
|
|
|
@autoreleasepool {
|
|
OFString *path =
|
|
[OFString stringWithFormat:@"savegames/%@.csgz", name];
|
|
OFIRI *IRI = [Cube.sharedInstance.userDataIRI
|
|
IRIByAppendingPathComponent:path];
|
|
savestate(IRI);
|
|
stop();
|
|
conoutf(@"wrote %@", IRI.string);
|
|
}
|
|
}
|
|
COMMAND(savegame, ARG_1STR)
|
|
|
|
void
|
|
loadstate(OFIRI *IRI)
|
|
{
|
|
@autoreleasepool {
|
|
stop();
|
|
if (multiplayer())
|
|
return;
|
|
f = gzopen([IRI.fileSystemRepresentation
|
|
cStringWithEncoding:OFLocale.encoding],
|
|
"rb9");
|
|
if (!f) {
|
|
conoutf(@"could not open %@", IRI.string);
|
|
return;
|
|
}
|
|
|
|
char mapname[_MAXDEFSTR] = { 0 };
|
|
char buf[8];
|
|
gzread(f, buf, 8);
|
|
if (strncmp(buf, "CUBESAVE", 8))
|
|
goto out;
|
|
if (gzgetc(f) != islittleendian)
|
|
goto out; // not supporting save->load accross
|
|
// incompatible architectures simpifies things
|
|
// a LOT
|
|
if (gzgeti() != SAVEGAMEVERSION ||
|
|
gzgeti() != DynamicEntity.serializedSize)
|
|
goto out;
|
|
gzread(f, mapname, _MAXDEFSTR);
|
|
nextmode = gzgeti();
|
|
@autoreleasepool {
|
|
// continue below once map has been loaded and client &
|
|
// server have updated
|
|
changemap(@(mapname));
|
|
}
|
|
return;
|
|
out:
|
|
conoutf(@"aborting: savegame/demo from a different version of "
|
|
@"cube or cpu architecture");
|
|
stop();
|
|
}
|
|
}
|
|
|
|
void
|
|
loadgame(OFString *name)
|
|
{
|
|
@autoreleasepool {
|
|
OFString *path =
|
|
[OFString stringWithFormat:@"savegames/%@.csgz", name];
|
|
OFIRI *IRI = [Cube.sharedInstance.userDataIRI
|
|
IRIByAppendingPathComponent:path];
|
|
loadstate(IRI);
|
|
}
|
|
}
|
|
COMMAND(loadgame, ARG_1STR)
|
|
|
|
void
|
|
loadgameout()
|
|
{
|
|
stop();
|
|
conoutf(@"loadgame incomplete: savegame from a different version of "
|
|
@"this map");
|
|
}
|
|
|
|
void
|
|
loadgamerest()
|
|
{
|
|
if (demoplayback || !f)
|
|
return;
|
|
|
|
if (gzgeti() != ents.length())
|
|
return loadgameout();
|
|
loopv(ents)
|
|
{
|
|
ents[i].spawned = gzgetc(f) != 0;
|
|
if (ents[i].type == CARROT && !ents[i].spawned)
|
|
trigger(ents[i].attr1, ents[i].attr2, true);
|
|
}
|
|
restoreserverstate(ents);
|
|
|
|
OFMutableData *data =
|
|
[OFMutableData dataWithCapacity:DynamicEntity.serializedSize];
|
|
[data increaseCountBy:DynamicEntity.serializedSize];
|
|
gzread(f, data.mutableItems, data.count);
|
|
[player1 setFromSerializedData:data];
|
|
player1.lastaction = lastmillis;
|
|
|
|
int nmonsters = gzgeti();
|
|
OFArray<DynamicEntity *> *monsters = getmonsters();
|
|
if (nmonsters != monsters.count)
|
|
return loadgameout();
|
|
|
|
for (DynamicEntity *monster in monsters) {
|
|
gzread(f, data.mutableItems, data.count);
|
|
[monster setFromSerializedData:data];
|
|
// lazy, could save id of enemy instead
|
|
monster.enemy = player1;
|
|
// also lazy, but no real noticable effect on game
|
|
monster.lastaction = monster.trigger = lastmillis + 500;
|
|
if (monster.state == CS_DEAD)
|
|
monster.lastaction = 0;
|
|
}
|
|
restoremonsterstate();
|
|
|
|
int nplayers = gzgeti();
|
|
loopi(nplayers) if (!gzget())
|
|
{
|
|
DynamicEntity *d = getclient(i);
|
|
assert(d);
|
|
gzread(f, data.mutableItems, data.count);
|
|
[d setFromSerializedData:data];
|
|
}
|
|
|
|
conoutf(@"savegame restored");
|
|
if (demoloading)
|
|
startdemo();
|
|
else
|
|
stop();
|
|
}
|
|
|
|
// demo functions
|
|
|
|
int starttime = 0;
|
|
int playbacktime = 0;
|
|
int ddamage, bdamage;
|
|
OFVector3D dorig;
|
|
|
|
void
|
|
record(OFString *name)
|
|
{
|
|
if (m_sp) {
|
|
conoutf(@"cannot record singleplayer games");
|
|
return;
|
|
}
|
|
|
|
int cn = getclientnum();
|
|
if (cn < 0)
|
|
return;
|
|
|
|
@autoreleasepool {
|
|
OFString *path =
|
|
[OFString stringWithFormat:@"demos/%@.cdgz", name];
|
|
OFIRI *IRI = [Cube.sharedInstance.userDataIRI
|
|
IRIByAppendingPathComponent:path];
|
|
savestate(IRI);
|
|
gzputi(cn);
|
|
conoutf(@"started recording demo to %@", IRI.string);
|
|
demorecording = true;
|
|
starttime = lastmillis;
|
|
ddamage = bdamage = 0;
|
|
}
|
|
}
|
|
COMMAND(record, ARG_1STR)
|
|
|
|
void
|
|
demodamage(int damage, const OFVector3D &o)
|
|
{
|
|
ddamage = damage;
|
|
dorig = o;
|
|
}
|
|
|
|
void
|
|
demoblend(int damage)
|
|
{
|
|
bdamage = damage;
|
|
}
|
|
|
|
void
|
|
incomingdemodata(uchar *buf, int len, bool extras)
|
|
{
|
|
if (!demorecording)
|
|
return;
|
|
gzputi(lastmillis - starttime);
|
|
gzputi(len);
|
|
gzwrite(f, buf, len);
|
|
gzput(extras);
|
|
if (extras) {
|
|
gzput(player1.gunselect);
|
|
gzput(player1.lastattackgun);
|
|
gzputi(player1.lastaction - starttime);
|
|
gzputi(player1.gunwait);
|
|
gzputi(player1.health);
|
|
gzputi(player1.armour);
|
|
gzput(player1.armourtype);
|
|
loopi(NUMGUNS) gzput(player1.ammo[i]);
|
|
gzput(player1.state);
|
|
gzputi(bdamage);
|
|
bdamage = 0;
|
|
gzputi(ddamage);
|
|
if (ddamage) {
|
|
gzputv(dorig);
|
|
ddamage = 0;
|
|
}
|
|
// FIXME: add all other client state which is not send through
|
|
// the network
|
|
}
|
|
}
|
|
|
|
void
|
|
demo(OFString *name)
|
|
{
|
|
@autoreleasepool {
|
|
OFString *path =
|
|
[OFString stringWithFormat:@"demos/%@.cdgz", name];
|
|
OFIRI *IRI = [Cube.sharedInstance.userDataIRI
|
|
IRIByAppendingPathComponent:path];
|
|
loadstate(IRI);
|
|
demoloading = true;
|
|
}
|
|
}
|
|
COMMAND(demo, ARG_1STR)
|
|
|
|
void
|
|
stopreset()
|
|
{
|
|
conoutf(@"demo stopped (%d msec elapsed)", lastmillis - starttime);
|
|
stop();
|
|
[players removeAllObjects];
|
|
disconnect(0, 0);
|
|
}
|
|
|
|
VAR(demoplaybackspeed, 10, 100, 1000);
|
|
int
|
|
scaletime(int t)
|
|
{
|
|
return (int)(t * (100.0f / demoplaybackspeed)) + starttime;
|
|
}
|
|
|
|
void
|
|
readdemotime()
|
|
{
|
|
if (gzeof(f) || (playbacktime = gzgeti()) == -1) {
|
|
stopreset();
|
|
return;
|
|
}
|
|
playbacktime = scaletime(playbacktime);
|
|
}
|
|
|
|
void
|
|
startdemo()
|
|
{
|
|
democlientnum = gzgeti();
|
|
demoplayback = true;
|
|
starttime = lastmillis;
|
|
conoutf(@"now playing demo");
|
|
setclient(democlientnum, [player1 copy]);
|
|
readdemotime();
|
|
}
|
|
|
|
VAR(demodelaymsec, 0, 120, 500);
|
|
|
|
// spline interpolation
|
|
#define catmulrom(z, a, b, c, s, dest) \
|
|
{ \
|
|
OFVector3D t1 = b, t2 = c; \
|
|
\
|
|
vsub(t1, z); \
|
|
vmul(t1, 0.5f); \
|
|
vsub(t2, a); \
|
|
vmul(t2, 0.5f); \
|
|
\
|
|
float s2 = s * s; \
|
|
float s3 = s * s2; \
|
|
\
|
|
dest = a; \
|
|
OFVector3D t = b; \
|
|
\
|
|
vmul(dest, 2 * s3 - 3 * s2 + 1); \
|
|
vmul(t, -2 * s3 + 3 * s2); \
|
|
vadd(dest, t); \
|
|
vmul(t1, s3 - 2 * s2 + s); \
|
|
vadd(dest, t1); \
|
|
vmul(t2, s3 - s2); \
|
|
vadd(dest, t2); \
|
|
}
|
|
|
|
void
|
|
fixwrap(DynamicEntity *a, DynamicEntity *b)
|
|
{
|
|
while (b.yaw - a.yaw > 180)
|
|
a.yaw += 360;
|
|
while (b.yaw - a.yaw < -180)
|
|
a.yaw -= 360;
|
|
}
|
|
|
|
void
|
|
demoplaybackstep()
|
|
{
|
|
while (demoplayback && lastmillis >= playbacktime) {
|
|
int len = gzgeti();
|
|
if (len < 1 || len > MAXTRANS) {
|
|
conoutf(
|
|
@"error: huge packet during demo play (%d)", len);
|
|
stopreset();
|
|
return;
|
|
}
|
|
uchar buf[MAXTRANS];
|
|
gzread(f, buf, len);
|
|
localservertoclient(buf, len); // update game state
|
|
|
|
DynamicEntity *target = players[democlientnum];
|
|
assert(target);
|
|
|
|
int extras;
|
|
// read additional client side state not present in normal
|
|
// network stream
|
|
if ((extras = gzget())) {
|
|
target.gunselect = gzget();
|
|
target.lastattackgun = gzget();
|
|
target.lastaction = scaletime(gzgeti());
|
|
target.gunwait = gzgeti();
|
|
target.health = gzgeti();
|
|
target.armour = gzgeti();
|
|
target.armourtype = gzget();
|
|
loopi(NUMGUNS) target.ammo[i] = gzget();
|
|
target.state = gzget();
|
|
target.lastmove = playbacktime;
|
|
if ((bdamage = gzgeti()))
|
|
damageblend(bdamage);
|
|
if ((ddamage = gzgeti())) {
|
|
gzgetv(dorig);
|
|
particle_splash(3, ddamage, 1000, dorig);
|
|
}
|
|
// FIXME: set more client state here
|
|
}
|
|
|
|
// insert latest copy of player into history
|
|
if (extras &&
|
|
(playerhistory.count == 0 ||
|
|
playerhistory.lastObject.lastupdate != playbacktime)) {
|
|
DynamicEntity *d = [target copy];
|
|
d.lastupdate = playbacktime;
|
|
|
|
if (playerhistory == nil)
|
|
playerhistory = [[OFMutableArray alloc] init];
|
|
|
|
[playerhistory addObject:d];
|
|
|
|
if (playerhistory.count > 20)
|
|
[playerhistory removeObjectAtIndex:0];
|
|
}
|
|
|
|
readdemotime();
|
|
}
|
|
|
|
if (!demoplayback)
|
|
return;
|
|
|
|
int itime = lastmillis - demodelaymsec;
|
|
// find 2 positions in history that surround interpolation time point
|
|
size_t count = playerhistory.count;
|
|
for (ssize_t i = count - 1; i >= 0; i--) {
|
|
if (playerhistory[i].lastupdate < itime) {
|
|
DynamicEntity *a = playerhistory[i];
|
|
DynamicEntity *b = a;
|
|
|
|
if (i + 1 < playerhistory.count)
|
|
b = playerhistory[i + 1];
|
|
|
|
player1 = b;
|
|
// interpolate pos & angles
|
|
if (a != b) {
|
|
DynamicEntity *c = b;
|
|
if (i + 2 < playerhistory.count)
|
|
c = playerhistory[i + 2];
|
|
DynamicEntity *z = a;
|
|
if (i - 1 >= 0)
|
|
z = playerhistory[i - 1];
|
|
// if(a==z || b==c)
|
|
// printf("* %d\n", lastmillis);
|
|
float bf = (itime - a.lastupdate) /
|
|
(float)(b.lastupdate - a.lastupdate);
|
|
fixwrap(a, player1);
|
|
fixwrap(c, player1);
|
|
fixwrap(z, player1);
|
|
vdist(dist, v, z.o, c.o);
|
|
// if teleport or spawn, don't interpolate
|
|
if (dist < 16) {
|
|
catmulrom(
|
|
z.o, a.o, b.o, c.o, bf, player1.o);
|
|
OFVector3D vz = OFMakeVector3D(
|
|
z.yaw, z.pitch, z.roll);
|
|
OFVector3D va = OFMakeVector3D(
|
|
a.yaw, a.pitch, a.roll);
|
|
OFVector3D vb = OFMakeVector3D(
|
|
b.yaw, b.pitch, b.roll);
|
|
OFVector3D vc = OFMakeVector3D(
|
|
c.yaw, c.pitch, c.roll);
|
|
OFVector3D vp1 =
|
|
OFMakeVector3D(player1.yaw,
|
|
player1.pitch, player1.roll);
|
|
catmulrom(vz, va, vb, vc, bf, vp1);
|
|
z.yaw = vz.x;
|
|
z.pitch = vz.y;
|
|
z.roll = vz.z;
|
|
a.yaw = va.x;
|
|
a.pitch = va.y;
|
|
a.roll = va.z;
|
|
b.yaw = vb.x;
|
|
b.pitch = vb.y;
|
|
b.roll = vb.z;
|
|
c.yaw = vc.x;
|
|
c.pitch = vc.y;
|
|
c.roll = vc.z;
|
|
player1.yaw = vp1.x;
|
|
player1.pitch = vp1.y;
|
|
player1.roll = vp1.z;
|
|
}
|
|
fixplayer1range();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
// if(player1->state!=CS_DEAD) showscores(false);
|
|
}
|
|
|
|
void
|
|
stopn()
|
|
{
|
|
if (demoplayback)
|
|
stopreset();
|
|
else
|
|
stop();
|
|
conoutf(@"demo stopped");
|
|
}
|
|
COMMANDN(stop, stopn, ARG_NONE)
|