4654 lines
116 KiB
C
4654 lines
116 KiB
C
// BLANKART
|
|
//-----------------------------------------------------------------------------
|
|
// Copyright (C) 1993-1996 by id Software, Inc.
|
|
// Copyright (C) 1998-2000 by DooM Legacy Team.
|
|
// Copyright (C) 1999-2020 by Sonic Team Junior.
|
|
//
|
|
// This program is free software distributed under the
|
|
// terms of the GNU General Public License, version 2.
|
|
// See the 'LICENSE' file for more details.
|
|
//-----------------------------------------------------------------------------
|
|
/// \file g_demo.c
|
|
/// \brief Demo recording and playback
|
|
|
|
#include "doomdef.h"
|
|
#include "console.h"
|
|
#include "d_main.h"
|
|
#include "d_player.h"
|
|
#include "d_clisrv.h"
|
|
#include "p_setup.h"
|
|
#include "i_time.h"
|
|
#include "i_system.h"
|
|
#include "m_random.h"
|
|
#include "p_local.h"
|
|
#include "r_draw.h"
|
|
#include "r_main.h"
|
|
#include "g_game.h"
|
|
#include "g_demo.h"
|
|
#include "m_misc.h"
|
|
#include "m_menu.h"
|
|
#include "m_argv.h"
|
|
#include "m_textinput.h"
|
|
#include "hu_stuff.h"
|
|
#include "z_zone.h"
|
|
#include "i_video.h"
|
|
#include "byteptr.h"
|
|
#include "i_joy.h"
|
|
#include "r_local.h"
|
|
#include "r_skins.h"
|
|
#include "y_inter.h"
|
|
#include "v_video.h"
|
|
#include "lua_hook.h"
|
|
#include "p_saveg.h" // savebuffer_t
|
|
#include "g_party.h"
|
|
|
|
// SRB2Kart
|
|
#include "d_netfil.h" // nameonly
|
|
|
|
#include "lua_script.h" // LUA_Sync
|
|
#include "lua_libs.h" // gL (Lua state)
|
|
|
|
#include "k_kart.h"
|
|
#include "k_battle.h"
|
|
#include "k_bot.h"
|
|
#include "k_color.h"
|
|
#include "k_follower.h"
|
|
#include "k_grandprix.h"
|
|
|
|
static CV_PossibleValue_t recordmultiplayerdemos_cons_t[] = {{0, "Disabled"}, {1, "Manual Save"}, {2, "Auto Save"}, {0, NULL}};
|
|
consvar_t cv_recordmultiplayerdemos = CVAR_INIT ("netdemo_record", "Manual Save", CV_SAVE, recordmultiplayerdemos_cons_t, NULL);
|
|
|
|
static CV_PossibleValue_t netdemosyncquality_cons_t[] = {{1, "MIN"}, {35, "MAX"}, {0, NULL}};
|
|
consvar_t cv_netdemosyncquality = CVAR_INIT ("netdemo_syncquality", "1", CV_SAVE, netdemosyncquality_cons_t, NULL);
|
|
|
|
consvar_t cv_netdemosize = CVAR_INIT ("netdemo_size", "6", CV_SAVE, CV_Natural, NULL);
|
|
consvar_t cv_resyncdemo = CVAR_INIT("democonsistency", "On", 0, CV_OnOff, NULL);
|
|
|
|
boolean nodrawers; // for comparative timing purposes
|
|
boolean noblit; // for comparative timing purposes
|
|
tic_t demostarttime; // for comparative timing purposes
|
|
|
|
static char demoname[MAX_WADPATH];
|
|
static savebuffer_t demobuf = {0};
|
|
static UINT8 *demotime_p, *demoinfo_p;
|
|
static UINT8 demoflags;
|
|
boolean demosynced = true; // console warning message
|
|
|
|
struct demovars_s demo;
|
|
|
|
boolean metalrecording; // recording as metal sonic
|
|
mobj_t *metalplayback;
|
|
static UINT8 *metalbuffer = NULL;
|
|
static UINT8 *metal_p;
|
|
static UINT16 metalversion;
|
|
|
|
// extra data stuff (events registered this frame while recording)
|
|
static struct {
|
|
UINT8 flags; // EZT flags
|
|
|
|
// EZT_COLOR
|
|
UINT8 color, lastcolor;
|
|
|
|
// EZT_SCALE
|
|
fixed_t scale, lastscale;
|
|
|
|
// EZT_KART
|
|
INT32 kartitem, kartamount, kartbumpers;
|
|
|
|
UINT8 desyncframes; // Don't try to resync unless we've been off for two frames, to monkeypatch a few trouble spots
|
|
|
|
// EZT_HIT
|
|
UINT16 hits;
|
|
mobj_t **hitlist;
|
|
} ghostext[MAXPLAYERS];
|
|
|
|
// Your naming conventions are stupid and useless.
|
|
// There is no conflict here.
|
|
demoghost *ghosts = NULL;
|
|
|
|
//
|
|
// DEMO RECORDING
|
|
//
|
|
|
|
#define DEMOVERSION 0x0010
|
|
#define DEMOHEADER "\xF0" "BlanReplay" "\x0F"
|
|
|
|
#define DF_GHOST 0x01 // This demo contains ghost data too!
|
|
#define DF_TIMEATTACK 0x02 // This demo is from Time Attack and contains its final completion time & best lap!
|
|
#define DF_ITEMBREAKER 0x04 // This demo is from Item Breaker and contains its final completion time!
|
|
#define DF_ATTACKMASK 0x06 // This demo is from ??? attack and contains ???
|
|
|
|
// 0x08 free
|
|
|
|
#define DF_NONETMP 0x10 // multiplayer but not netgame
|
|
|
|
#define DF_LUAVARS 0x20 // this demo contains extra lua vars
|
|
|
|
#define DF_ATTACKSHIFT 1
|
|
#define DF_ENCORE 0x40
|
|
#define DF_MULTIPLAYER 0x80 // This demo was recorded in multiplayer mode!
|
|
#define DF_GRANDPRIX 0x0100
|
|
|
|
#define DEMO_SPECTATOR 0x01
|
|
#define DEMO_KICKSTART 0x02
|
|
#define DEMO_SHRINKME 0x04
|
|
#define DEMO_BOT 0x08
|
|
|
|
// For demos
|
|
#define ZT_FWD 0x0001
|
|
#define ZT_SIDE 0x0002
|
|
#define ZT_TURNING 0x0004
|
|
#define ZT_ANGLE 0x0008
|
|
#define ZT_THROWDIR 0x0010
|
|
#define ZT_BUTTONS 0x0020
|
|
#define ZT_AIMING 0x0040
|
|
#define ZT_LATENCY 0x0080
|
|
#define ZT_FLAGS 0x0100
|
|
// Ziptics are UINT16 now, go nuts
|
|
|
|
#define DEMOMARKER 0x80 // demobuf.end
|
|
|
|
UINT8 demo_extradata[MAXPLAYERS];
|
|
UINT8 demo_writerng; // 0=no, 1=yes, 2=yes but on a timeout
|
|
static ticcmd_t oldcmd[MAXPLAYERS];
|
|
|
|
#define METALDEATH 0x44
|
|
#define METALSNICE 0x69
|
|
|
|
#define DW_END 0xFF // End of extradata block
|
|
#define DW_RNG 0xFE // Check RNG seed!
|
|
|
|
#define DW_EXTRASTUFF 0xFE // Numbers below this are reserved for writing player slot data
|
|
|
|
// Below consts are only used for demo extrainfo sections
|
|
#define DW_STANDING 0x00
|
|
|
|
// For Metal Sonic and time attack ghosts
|
|
#define GZT_XYZ 0x01
|
|
#define GZT_MOMXY 0x02
|
|
#define GZT_MOMZ 0x04
|
|
#define GZT_ANGLE 0x08
|
|
#define GZT_FRAME 0x10 // Animation frame
|
|
#define GZT_SPR2 0x20 // Player animations
|
|
#define GZT_EXTRA 0x40
|
|
#define GZT_FOLLOW 0x80 // Followmobj
|
|
|
|
// GZT_EXTRA flags
|
|
#define EZT_COLOR 0x001 // Changed color (Super transformation, Mario fireflowers/invulnerability, etc.)
|
|
#define EZT_FLIP 0x002 // Reversed gravity
|
|
#define EZT_SCALE 0x004 // Changed size
|
|
#define EZT_HIT 0x008 // Damaged a mobj
|
|
#define EZT_SPRITE 0x010 // Changed sprite set completely out of PLAY (NiGHTS, SOCs, whatever)
|
|
#define EZT_KART 0x020 // SRB2Kart: Changed current held item/quantity and bumpers for battle
|
|
|
|
// GZT_FOLLOW flags
|
|
#define FZT_SPAWNED 0x01 // just been spawned
|
|
#define FZT_SKIN 0x02 // has skin
|
|
#define FZT_LINKDRAW 0x04 // has linkdraw (combine with spawned only)
|
|
#define FZT_COLORIZED 0x08 // colorized (ditto)
|
|
#define FZT_SCALE 0x10 // different scale to object
|
|
// spare FZT slots 0x20 to 0x80
|
|
|
|
typedef struct
|
|
{
|
|
char *filename;
|
|
UINT64 filehash;
|
|
boolean compatmode;
|
|
} demofile_t;
|
|
|
|
typedef struct
|
|
{
|
|
char *name; // NULL = bad netid
|
|
char *value;
|
|
boolean stealth;
|
|
UINT16 legacynetid;
|
|
} democvar_t;
|
|
|
|
typedef struct
|
|
{
|
|
UINT8 playernum;
|
|
UINT8 flags;
|
|
|
|
struct // DEMO_BOT
|
|
{
|
|
UINT8 difficulty;
|
|
UINT8 diffincrease;
|
|
boolean rival;
|
|
} bot;
|
|
|
|
char name[21+1];
|
|
char skin[16+1];
|
|
char color[16+1];
|
|
char follower[16+1];
|
|
char followercolor[16+1];
|
|
char voice[32+1];
|
|
|
|
UINT32 score;
|
|
UINT16 powerlevel;
|
|
UINT8 kartspeed;
|
|
UINT8 kartweight;
|
|
mobjtype_t followitem;
|
|
UINT8 availabilities[MAXAVAILABILITY];
|
|
} demoplayer_t;
|
|
|
|
typedef struct
|
|
{
|
|
UINT8 version, subversion;
|
|
UINT16 demoversion;
|
|
char demotitle[64];
|
|
UINT64 demochecksum;
|
|
|
|
char maptitle[MAXMAPLUMPNAME];
|
|
UINT64 mapchecksum;
|
|
UINT8 demoflags;
|
|
UINT32 raflags;
|
|
char rapreset[MAXPRESETNAME+1];
|
|
UINT8 rapresetversion;
|
|
UINT8 gametype;
|
|
UINT8 numlaps;
|
|
char servername[MAXSERVERNAME];
|
|
char servercontact[MAXSERVERCONTACT];
|
|
char serverdescription[MAXSERVERDESCRIPTION];
|
|
|
|
UINT8 numfiles;
|
|
demofile_t *files;
|
|
|
|
struct // DF_ATTACKMASK
|
|
{
|
|
UINT32 time;
|
|
UINT32 lap;
|
|
} attacking;
|
|
|
|
UINT32 randseed;
|
|
UINT32 extrainfo;
|
|
|
|
UINT16 numcvars;
|
|
democvar_t *cvars;
|
|
|
|
struct // DF_GRANDPRIX
|
|
{
|
|
boolean masterbots;
|
|
boolean lunaticmode;
|
|
UINT8 gamespeed;
|
|
UINT8 eventmode;
|
|
} grandprix;
|
|
|
|
UINT8 mapmusrng;
|
|
|
|
UINT8 numplayers;
|
|
demoplayer_t *playerdata;
|
|
|
|
boolean empty;
|
|
UINT32 endofs;
|
|
} demoheader_t;
|
|
|
|
typedef struct
|
|
{
|
|
mobjtype_t type;
|
|
UINT16 health;
|
|
fixed_t x, y, z;
|
|
angle_t angle;
|
|
} zipticdamage_t;
|
|
|
|
typedef struct
|
|
{
|
|
UINT8 flags;
|
|
fixed_t x, y, z; // or momx, momy, momz
|
|
angle_t angle;
|
|
UINT8 frame, spr2;
|
|
|
|
// GZT_EXTRA
|
|
UINT8 extraflags;
|
|
UINT16 color;
|
|
fixed_t scale;
|
|
UINT16 damagecount;
|
|
zipticdamage_t *damage;
|
|
UINT16 sprite;
|
|
INT32 kartitem, kartamount, kartbumpers;
|
|
|
|
UINT8 followflags;
|
|
struct // GZT_FOLLOW
|
|
{
|
|
fixed_t height;
|
|
UINT16 skin;
|
|
fixed_t scale;
|
|
fixed_t xofs, yofs, zofs;
|
|
UINT16 sprite2;
|
|
UINT16 sprite;
|
|
UINT8 frame;
|
|
UINT16 color;
|
|
} follow;
|
|
} ziptic_t;
|
|
|
|
typedef struct
|
|
{
|
|
UINT8 playernum; // index of player, or >= DW_EXTRASTUFF for special data
|
|
UINT8 flags; // DXD flags if playernum < DW_EXTRASTUFF
|
|
|
|
// DW_RNG
|
|
UINT32 randseed;
|
|
|
|
struct // DXD_JOINDATA
|
|
{
|
|
boolean bot;
|
|
UINT8 difficulty;
|
|
UINT8 diffincrease;
|
|
boolean rival;
|
|
} joindata;
|
|
|
|
// DXD_PLAYSTATE
|
|
UINT8 playstate;
|
|
|
|
// DXD_NAME
|
|
char playername[21+1];
|
|
|
|
// DXD_SKIN
|
|
char skinname[17];
|
|
char voicename[33];
|
|
UINT8 kartspeed;
|
|
UINT8 kartweight;
|
|
|
|
// DXD_COLOR
|
|
char colorname[17];
|
|
|
|
// DXD_FOLLOWER
|
|
char followername[17];
|
|
char followercolor[17];
|
|
|
|
// DXD_WEAPONPREF
|
|
UINT8 weaponpref;
|
|
} extradata_t;
|
|
|
|
typedef enum
|
|
{
|
|
HEADER_OK,
|
|
HEADER_BADMAGIC, // DEMOHEADER doesn't match
|
|
HEADER_BADVERSION, // unsupported version
|
|
HEADER_BADFORMAT, // "PLAY" doesn't match, or some other error
|
|
} headerstatus_e;
|
|
|
|
static UINT8 *G_ReadZipTic(ticcmd_t *cmd, UINT8 *dp, UINT16 version)
|
|
{
|
|
UINT16 ziptic;
|
|
boolean kart = version <= 0x0002;
|
|
if (kart)
|
|
{
|
|
ziptic = READUINT8(dp);
|
|
ziptic = (ziptic & 3) | ((ziptic & (4|64)) << 1) | ((ziptic & (8|16)) << 2) | ((ziptic & 32) >> 3);
|
|
}
|
|
else
|
|
ziptic = READUINT16(dp);
|
|
|
|
if (ziptic & ZT_FWD)
|
|
cmd->forwardmove = READSINT8(dp);
|
|
if (ziptic & ZT_SIDE)
|
|
cmd->sidemove = READSINT8(dp);
|
|
if (!kart && ziptic & ZT_TURNING)
|
|
cmd->turning = READINT16(dp);
|
|
if (ziptic & ZT_ANGLE)
|
|
cmd->angle = READINT16(dp);
|
|
if (ziptic & ZT_THROWDIR)
|
|
cmd->throwdir = READINT16(dp);
|
|
if (ziptic & ZT_BUTTONS)
|
|
cmd->buttons = READUINT16(dp);
|
|
if (ziptic & ZT_AIMING)
|
|
cmd->aiming = READINT16(dp);
|
|
if (kart && ziptic & ZT_TURNING)
|
|
cmd->turning = READINT16(dp);
|
|
if (ziptic & ZT_LATENCY)
|
|
cmd->latency = READUINT8(dp);
|
|
if (ziptic & ZT_FLAGS)
|
|
cmd->flags = READUINT8(dp);
|
|
|
|
if (version < 0x000a && ziptic & 0x8000) // ZT_BOT
|
|
{
|
|
UINT16 botziptic = READUINT16(dp);
|
|
if (botziptic & 1) (void)READSINT8(dp);
|
|
if (botziptic & 2) (void)READSINT8(dp);
|
|
if (botziptic & 4) (void)READSINT8(dp);
|
|
}
|
|
return dp;
|
|
}
|
|
|
|
// call G_FreeExtraZipTic, you get the idea
|
|
static UINT8 *G_ReadExtraZipTic(ziptic_t *zt, UINT8 *dp, UINT16 version, mobj_t *mo)
|
|
{
|
|
boolean kart = version <= 0x0002;
|
|
UINT8 ziptic = READUINT8(dp);
|
|
|
|
if (kart)
|
|
// shift GZT_EXTRA to its correct position
|
|
// mask out GZT_NIGHTS (this ain't SRB2!)
|
|
ziptic = (ziptic & 0x1f) | ((ziptic & 0x20) << 1);
|
|
|
|
zt->flags = ziptic;
|
|
zt->damagecount = 0;
|
|
|
|
if (ziptic & GZT_XYZ)
|
|
{
|
|
zt->x = READFIXED(dp);
|
|
zt->y = READFIXED(dp);
|
|
zt->z = READFIXED(dp);
|
|
}
|
|
else
|
|
{
|
|
if (ziptic & GZT_MOMXY)
|
|
{
|
|
zt->x = kart ? READINT16(dp)<<8 : READFIXED(dp);
|
|
zt->y = kart ? READINT16(dp)<<8 : READFIXED(dp);
|
|
}
|
|
if (ziptic & GZT_MOMZ)
|
|
zt->z = kart ? READINT16(dp)<<8 : READFIXED(dp);
|
|
}
|
|
|
|
if (ziptic & GZT_ANGLE)
|
|
zt->angle = READUINT8(dp)<<24;
|
|
|
|
if (ziptic & GZT_FRAME)
|
|
{
|
|
if (kart)
|
|
{
|
|
zt->spr2 = P_KartFrameToSprite2(mo->skin, READUINT8(dp), &zt->frame);
|
|
zt->flags |= GZT_SPR2;
|
|
}
|
|
else
|
|
zt->frame = READUINT8(dp);
|
|
}
|
|
|
|
if (ziptic & GZT_SPR2)
|
|
zt->spr2 = READUINT8(dp);
|
|
|
|
if (ziptic & GZT_EXTRA)
|
|
{ // But wait, there's more!
|
|
UINT8 xziptic = READUINT8(dp);
|
|
|
|
// shifted down due to removal of EZT_THOKMASK
|
|
// thoks don't have any extra bytes so just ignore them
|
|
if (kart)
|
|
xziptic >>= 2;
|
|
|
|
zt->extraflags = xziptic;
|
|
|
|
if (xziptic & EZT_COLOR)
|
|
zt->color = kart ? READUINT8(dp) : READUINT16(dp);
|
|
|
|
if (xziptic & EZT_SCALE)
|
|
zt->scale = READFIXED(dp);
|
|
|
|
if (xziptic & EZT_HIT)
|
|
{ // Resync mob damage.
|
|
zt->damagecount = READUINT16(dp);
|
|
if (zt->damagecount)
|
|
zt->damage = malloc(sizeof(zipticdamage_t) * zt->damagecount);
|
|
for (UINT16 i = 0; i < zt->damagecount; i++)
|
|
{
|
|
zipticdamage_t *damage = &zt->damage[i];
|
|
if (kart)
|
|
(void)READUINT32(dp);
|
|
damage->type = READUINT32(dp);
|
|
damage->health = READUINT16(dp);
|
|
damage->x = READFIXED(dp);
|
|
damage->y = READFIXED(dp);
|
|
damage->z = READFIXED(dp);
|
|
damage->angle = READANGLE(dp);
|
|
}
|
|
}
|
|
|
|
if (xziptic & EZT_SPRITE)
|
|
{
|
|
if (kart)
|
|
{
|
|
if (READUINT8(dp) != 3)
|
|
CONS_Alert(CONS_WARNING, "sprite other than SPR_PLAY... oh dear");
|
|
zt->sprite = SPR_PLAY;
|
|
}
|
|
else
|
|
zt->sprite = READUINT16(dp);
|
|
}
|
|
|
|
if (xziptic & EZT_KART)
|
|
{
|
|
zt->kartitem = READINT32(dp);
|
|
zt->kartamount = READINT32(dp);
|
|
zt->kartbumpers = READINT32(dp);
|
|
}
|
|
}
|
|
|
|
if (ziptic & GZT_FOLLOW)
|
|
{ // Even more...
|
|
UINT8 followtic = READUINT8(dp);
|
|
if (followtic & FZT_SPAWNED)
|
|
{
|
|
zt->follow.height = READINT16(dp)<<FRACBITS;
|
|
if (followtic & FZT_SKIN)
|
|
zt->follow.skin = READUINT16(dp);
|
|
}
|
|
|
|
if (followtic & FZT_SCALE)
|
|
zt->follow.scale = READFIXED(dp);
|
|
|
|
zt->follow.xofs = READFIXED(dp);
|
|
zt->follow.yofs = READFIXED(dp);
|
|
zt->follow.zofs = READFIXED(dp);
|
|
if (followtic & FZT_SKIN)
|
|
zt->follow.sprite2 = READUINT16(dp);
|
|
zt->follow.sprite = READUINT16(dp);
|
|
zt->follow.frame = READUINT8(dp);
|
|
zt->follow.color = READUINT16(dp);
|
|
}
|
|
|
|
return dp;
|
|
}
|
|
|
|
static void G_FreeExtraZipTic(ziptic_t *zt)
|
|
{
|
|
if (zt->damagecount)
|
|
free(zt->damage);
|
|
|
|
#ifdef PARANOIA
|
|
memset(zt, 0xff, sizeof(*zt));
|
|
#endif
|
|
}
|
|
|
|
static UINT8 *G_ReadRawExtraData(extradata_t *extra, UINT8 *dp, UINT16 version)
|
|
{
|
|
INT32 i;
|
|
UINT8 extradata;
|
|
extra->playernum = READUINT8(dp);
|
|
boolean kart = version <= 0x0002;
|
|
|
|
if (extra->playernum >= DW_EXTRASTUFF)
|
|
{
|
|
extra->flags = 0;
|
|
if (extra->playernum == DW_RNG)
|
|
{
|
|
extra->randseed = READUINT32(dp);
|
|
}
|
|
// DW_END has no data
|
|
return dp;
|
|
}
|
|
|
|
extradata = READUINT8(dp);
|
|
|
|
if (kart)
|
|
{
|
|
// kart blan
|
|
// DXD_JOINDATA N/A 0
|
|
// DXD_PLAYSTATE 4 1
|
|
// DXD_NAME 2 2
|
|
// DXD_SKIN 1 3
|
|
// DXD_COLOR 3 4
|
|
// DXD_FOLLOWER N/A 5
|
|
// DXD_RESPAWN 0 6
|
|
// completely rearranged... UGH...
|
|
extradata =
|
|
((extradata & 0x01) << 6) // DXD_RESPAWN
|
|
| ((extradata & 0x02) << 2) // DXD_SKIN
|
|
| ((extradata & 0x04) ) // DXD_NAME
|
|
| ((extradata & 0x08) << 1) // DXD_COLOR
|
|
| ((extradata & 0x10) >> 3) // DXD_PLAYSTATE
|
|
;
|
|
}
|
|
|
|
extra->flags = extradata;
|
|
|
|
if (extradata & DXD_JOINDATA)
|
|
{
|
|
if (version >= 0x000F)
|
|
{
|
|
for (i = 0; i < MAXAVAILABILITY; i++)
|
|
{
|
|
players[extra->playernum].availabilities[i] = READUINT8(dp);
|
|
}
|
|
}
|
|
|
|
extra->joindata.bot = !!(READUINT8(dp));
|
|
if (extra->joindata.bot)
|
|
{
|
|
extra->joindata.difficulty = READUINT8(dp);
|
|
extra->joindata.diffincrease = READUINT8(dp); // needed to avoid having to duplicate logic
|
|
extra->joindata.rival = (boolean)READUINT8(dp);
|
|
}
|
|
}
|
|
if (!kart) // READING ORDER MATTERS if that wasn't obvious
|
|
{
|
|
if (extradata & DXD_PLAYSTATE)
|
|
{
|
|
extra->playstate = READUINT8(dp);
|
|
}
|
|
if (extradata & DXD_NAME)
|
|
{
|
|
if (version <= 0x000C)
|
|
{
|
|
READMEM(dp, extra->playername, 16);
|
|
extra->playername[16] = '\0';
|
|
}
|
|
else
|
|
READSTRINGL(dp, extra->playername, 21+1);
|
|
}
|
|
}
|
|
if (extradata & DXD_SKIN)
|
|
{
|
|
if (version <= 0x000C)
|
|
{
|
|
READMEM(dp, extra->skinname, 16);
|
|
extra->skinname[16] = '\0';
|
|
|
|
extra->voicename[0] = '\0';
|
|
}
|
|
else
|
|
{
|
|
READSTRINGL(dp, extra->skinname, 16+1);
|
|
|
|
if (version > 0x000D)
|
|
READSTRINGL(dp, extra->voicename, 32+1);
|
|
else
|
|
extra->voicename[0] = '\0';
|
|
}
|
|
extra->kartspeed = READUINT8(dp);
|
|
extra->kartweight = READUINT8(dp);
|
|
}
|
|
if (extradata & DXD_COLOR)
|
|
{
|
|
if (version <= 0x000C)
|
|
{
|
|
READMEM(dp, extra->colorname, 16);
|
|
extra->colorname[16] = '\0';
|
|
}
|
|
else
|
|
READSTRINGL(dp, extra->colorname, 16+1);
|
|
}
|
|
if (kart)
|
|
{
|
|
if (extradata & DXD_NAME)
|
|
{
|
|
READMEM(dp, extra->playername, 16);
|
|
extra->playername[16] = '\0';
|
|
}
|
|
if (extradata & DXD_PLAYSTATE)
|
|
{
|
|
extra->playstate = READUINT8(dp);
|
|
}
|
|
}
|
|
if (extradata & DXD_FOLLOWER)
|
|
{
|
|
if (version <= 0x000C)
|
|
{
|
|
READMEM(dp, extra->followername, 16);
|
|
extra->followername[16] = '\0';
|
|
READMEM(dp, extra->followercolor, 16);
|
|
extra->followercolor[16] = '\0';
|
|
}
|
|
else
|
|
{
|
|
READSTRINGL(dp, extra->followername, 16+1);
|
|
READSTRINGL(dp, extra->followercolor, 16+1);
|
|
}
|
|
}
|
|
if (extradata & DXD_WEAPONPREF)
|
|
{
|
|
extra->weaponpref = READUINT8(dp);
|
|
}
|
|
|
|
return dp;
|
|
}
|
|
|
|
// parses a demo header from the given byte pointer
|
|
// remember to call G_FreeDemoHeader!
|
|
static headerstatus_e G_ReadDemoHeader(UINT8 *dp, demoheader_t *header)
|
|
{
|
|
UINT8 *startdp = dp;
|
|
UINT8 attack;
|
|
boolean kart = false, oldkart = false;
|
|
boolean raflag = false;
|
|
boolean serverinfo = true;
|
|
boolean rapreset = true; // + extended serverinfo length
|
|
boolean dubs = true; // Multiple voices
|
|
boolean availabilities = true; // Store player availabilities
|
|
boolean gplunatic = true; // Grand Prix: Lunatic mode
|
|
INT32 i;
|
|
|
|
// these may not be present in old demo formats, so initialize them
|
|
// also initialize them so the header can be free'd without issues
|
|
header->numplayers = 0;
|
|
header->numfiles = 0;
|
|
header->numcvars = 0;
|
|
header->mapmusrng = 0;
|
|
header->numlaps = 0;
|
|
header->raflags = 0;
|
|
memset(header->servername, 0, MAXSERVERNAME);
|
|
memset(header->servercontact, 0, MAXSERVERCONTACT);
|
|
memset(header->serverdescription, 0, MAXSERVERDESCRIPTION);
|
|
header->empty = false;
|
|
|
|
dp += 12;
|
|
header->version = READUINT8(dp);
|
|
header->subversion = READUINT8(dp);
|
|
header->demoversion = READUINT16(dp);
|
|
|
|
if (!memcmp(startdp, DEMOHEADER, 12)) // BlanReplay
|
|
{
|
|
switch (header->demoversion)
|
|
{
|
|
case DEMOVERSION: // latest always supported
|
|
break;
|
|
|
|
case 0x0009:
|
|
serverinfo = false;
|
|
/* FALLTHRU */
|
|
case 0x000A:
|
|
case 0x000B:
|
|
case 0x000C:
|
|
rapreset = false;
|
|
raflag = true;
|
|
/* FALLTHRU */
|
|
case 0x000D:
|
|
dubs = false;
|
|
/* FALLTHRU */
|
|
case 0x000E:
|
|
availabilities = false;
|
|
/* FALLTHRU */
|
|
case 0x000F:
|
|
gplunatic = false;
|
|
break;
|
|
|
|
default: // too old, cannot support.
|
|
return HEADER_BADVERSION;
|
|
}
|
|
}
|
|
else if (!memcmp(startdp, "\xF0" "KartReplay" "\x0F", 12))
|
|
{
|
|
dubs = rapreset = raflag = false;
|
|
gplunatic = serverinfo = availabilities = false;
|
|
switch (header->demoversion)
|
|
{
|
|
case 0x0001: // SRB2Kart 1.0.x (only staff ghosts supported)
|
|
oldkart = kart = true;
|
|
break;
|
|
|
|
case 0x0002: // SRB2Kart 1.1+
|
|
kart = true;
|
|
break;
|
|
|
|
case 0x0008: // BlanKart (Pre-RA mod flags)
|
|
break;
|
|
|
|
default: // too old, cannot support.
|
|
return HEADER_BADVERSION;
|
|
}
|
|
}
|
|
else
|
|
return HEADER_BADMAGIC;
|
|
|
|
if (oldkart)
|
|
header->demotitle[0] = '\0';
|
|
else
|
|
READMEM(dp, &header->demotitle, 64);
|
|
|
|
header->demochecksum = READUINT64(dp);
|
|
if (kart) (void)READUINT64(dp); // truncate the md5, this is only used for avoiding duped ghosts
|
|
|
|
if (memcmp(dp, "PLAY", 4))// && memcmp(dp, "METL", 4))
|
|
return HEADER_BADFORMAT;
|
|
dp += 4;
|
|
|
|
if (kart)
|
|
{
|
|
INT16 mapnum = READINT16(dp);
|
|
if (mapnum < 100)
|
|
sprintf(header->maptitle, "MAP%02d", mapnum);
|
|
else
|
|
sprintf(header->maptitle, "MAP%c%c", 'A' + (mapnum - 100)/36, ((mapnum - 100) % 36 < 10 ? '0' : 'A'-10) + (mapnum - 100) % 36);
|
|
}
|
|
else
|
|
{
|
|
READSTRINGL(dp, header->maptitle, MAXMAPLUMPNAME);
|
|
if (dp[-1] != '\0') dp++;
|
|
}
|
|
|
|
header->mapchecksum = READUINT64(dp);
|
|
if (kart) (void)READUINT64(dp); // not even used anywhere
|
|
|
|
header->demoflags = READUINT8(dp);
|
|
|
|
if (raflag)
|
|
{
|
|
header->raflags = READUINT32(dp);
|
|
}
|
|
|
|
if (rapreset)
|
|
{
|
|
READSTRINGL(dp, header->rapreset, sizeof(header->rapreset));
|
|
header->rapresetversion = READUINT8(dp);
|
|
}
|
|
|
|
if (serverinfo)
|
|
{
|
|
size_t maxservercontact = rapreset ? MAXSERVERCONTACT : 320;
|
|
size_t maxdescription = rapreset ? MAXSERVERDESCRIPTION : 320;
|
|
|
|
READSTRINGL(dp, header->servername, MAXSERVERNAME);
|
|
if (dp[-1] != '\0') dp++;
|
|
READSTRINGL(dp, header->servercontact, maxservercontact);
|
|
if (dp[-1] != '\0') dp++;
|
|
READSTRINGL(dp, header->serverdescription, maxdescription);
|
|
if (dp[-1] != '\0') dp++;
|
|
}
|
|
|
|
if (oldkart)
|
|
{
|
|
if (header->demoflags & DF_MULTIPLAYER)
|
|
return HEADER_BADFORMAT; // multiplayer not supported
|
|
|
|
header->gametype = (header->demoflags & 0x30)>>4;
|
|
|
|
if (header->demoflags & 0x08) // DF_FILELIST
|
|
goto readfiles;
|
|
else
|
|
goto skipfiles;
|
|
}
|
|
else
|
|
{
|
|
header->gametype = READUINT8(dp);
|
|
if (!kart)
|
|
header->numlaps = READUINT8(dp);
|
|
}
|
|
|
|
readfiles:
|
|
header->numfiles = READUINT8(dp);
|
|
if (header->numfiles)
|
|
header->files = malloc(sizeof(demofile_t) * header->numfiles);
|
|
for (i = 0; i < header->numfiles; i++)
|
|
{
|
|
char filename[MAX_WADPATH];
|
|
READSTRINGL(dp, filename, MAX_WADPATH);
|
|
header->files[i].filename = strdup(filename);
|
|
|
|
if (kart)
|
|
{
|
|
dp += 16; // md5 is useless anyway
|
|
header->files[i].filehash = 0;
|
|
header->files[i].compatmode = true; // duh...
|
|
}
|
|
else
|
|
{
|
|
header->files[i].filehash = READUINT64(dp);
|
|
header->files[i].compatmode = READUINT8(dp) & 1;
|
|
}
|
|
}
|
|
|
|
skipfiles:
|
|
attack = (header->demoflags & DF_ATTACKMASK) >> DF_ATTACKSHIFT;
|
|
header->attacking.time = attack == ATTACKING_TIME || attack == ATTACKING_ITEMBREAK ? READUINT32(dp) : UINT32_MAX;
|
|
header->attacking.lap = attack == ATTACKING_TIME ? READUINT32(dp) : UINT32_MAX;
|
|
|
|
// Random seed
|
|
header->randseed = READUINT32(dp);
|
|
|
|
if (oldkart)
|
|
{
|
|
header->numplayers = 1;
|
|
header->playerdata = malloc(sizeof(demoplayer_t));
|
|
demoplayer_t *plr = &header->playerdata[0];
|
|
|
|
plr->playernum = 0;
|
|
plr->flags = 0;
|
|
READMEM(dp, plr->name, 16);
|
|
plr->name[16] = '\0';
|
|
READMEM(dp, plr->skin, 16);
|
|
plr->skin[16] = '\0';
|
|
READMEM(dp, plr->color, 16);
|
|
plr->color[16] = '\0';
|
|
strcpy(plr->follower, "None");
|
|
strcpy(plr->followercolor, "Default");
|
|
plr->followitem = MT_NULL;
|
|
|
|
dp += 5; // Backwards compat - some stats
|
|
// SRB2kart
|
|
plr->kartspeed = READUINT8(dp);
|
|
plr->kartweight = READUINT8(dp);
|
|
//
|
|
dp += 9; // Backwards compat - more stats
|
|
}
|
|
else
|
|
header->extrainfo = READUINT32(dp);
|
|
|
|
// net var data
|
|
header->numcvars = READUINT16(dp);
|
|
if (header->numcvars)
|
|
header->cvars = malloc(sizeof(democvar_t) * header->numcvars);
|
|
for (i = 0; i < header->numcvars; i++)
|
|
{
|
|
if (kart)
|
|
{
|
|
UINT16 netid = READUINT16(dp);
|
|
consvar_t *legacycvar = CV_FindLegacyNetVar(netid);
|
|
header->cvars[i].name = legacycvar ? strdup(legacycvar->name) : NULL;
|
|
header->cvars[i].legacynetid = netid;
|
|
}
|
|
else
|
|
{
|
|
header->cvars[i].name = strdup((char *)dp);
|
|
SKIPSTRING(dp);
|
|
}
|
|
header->cvars[i].value = strdup((char *)dp);
|
|
SKIPSTRING(dp);
|
|
header->cvars[i].stealth = READUINT8(dp);
|
|
}
|
|
|
|
if (header->demoflags & DF_GRANDPRIX)
|
|
{
|
|
header->grandprix.gamespeed = READUINT8(dp);
|
|
header->grandprix.masterbots = READUINT8(dp) != 0;
|
|
|
|
if (gplunatic)
|
|
header->grandprix.lunaticmode = READUINT8(dp) != 0;
|
|
|
|
header->grandprix.eventmode = READUINT8(dp);
|
|
}
|
|
|
|
// Load "mapmusrng" used for altmusic selection
|
|
if (!kart)
|
|
header->mapmusrng = READUINT8(dp);
|
|
|
|
if (oldkart)
|
|
goto end;
|
|
|
|
// Sigh ... it's an empty demo.
|
|
if (*dp == DEMOMARKER)
|
|
{
|
|
header->empty = true;
|
|
goto end;
|
|
}
|
|
|
|
// Load players that were in-game when the map started
|
|
UINT8 playernum;
|
|
header->playerdata = NULL;
|
|
while ((playernum = READUINT8(dp)) != 0xff)
|
|
{
|
|
header->playerdata = realloc(header->playerdata, sizeof(demoplayer_t) * ++header->numplayers);
|
|
demoplayer_t *plr = &header->playerdata[header->numplayers - 1];
|
|
plr->playernum = playernum;
|
|
|
|
if (kart)
|
|
{
|
|
plr->flags = playernum & 0x40 ? DEMO_SPECTATOR : 0;
|
|
plr->playernum &= ~0x40;
|
|
}
|
|
else
|
|
plr->flags = READUINT8(dp);
|
|
|
|
if (plr->flags & DEMO_BOT)
|
|
{
|
|
plr->bot.difficulty = READUINT8(dp);
|
|
plr->bot.diffincrease = READUINT8(dp); // needed to avoid having to duplicate logic
|
|
plr->bot.rival = (boolean)READUINT8(dp);
|
|
}
|
|
|
|
if (rapreset)
|
|
{
|
|
READSTRINGL(dp, plr->name, 21+1);
|
|
READSTRINGL(dp, plr->skin, 16+1);
|
|
READSTRINGL(dp, plr->color, 16+1);
|
|
|
|
if (dubs)
|
|
READSTRINGL(dp, plr->voice, 32+1);
|
|
|
|
READSTRINGL(dp, plr->follower, 16+1);
|
|
READSTRINGL(dp, plr->followercolor, 16+1);
|
|
}
|
|
else
|
|
{
|
|
READMEM(dp, plr->name, 16);
|
|
plr->name[16] = '\0';
|
|
READMEM(dp, plr->skin, 16);
|
|
plr->skin[16] = '\0';
|
|
READMEM(dp, plr->color, 16);
|
|
plr->color[16] = '\0';
|
|
if (!kart)
|
|
{
|
|
READMEM(dp, plr->follower, 16);
|
|
plr->follower[16] = '\0';
|
|
READMEM(dp, plr->followercolor, 16);
|
|
plr->followercolor[16] = '\0';
|
|
}
|
|
else
|
|
{
|
|
strcpy(plr->follower, "None");
|
|
strcpy(plr->followercolor, "Default");
|
|
}
|
|
}
|
|
|
|
plr->score = READUINT32(dp);
|
|
plr->powerlevel = !kart ? READUINT16(dp) : 0;
|
|
plr->kartspeed = READUINT8(dp);
|
|
plr->kartweight = READUINT8(dp);
|
|
plr->followitem = !kart ? READUINT32(dp) : MT_NULL;
|
|
|
|
for (i = 0; i < MAXAVAILABILITY; i++)
|
|
{
|
|
plr->availabilities[i] = availabilities ? READUINT8(dp) : 0;
|
|
}
|
|
}
|
|
|
|
// Sigh ... it's an empty demo. Again.
|
|
if (*dp == DEMOMARKER)
|
|
header->empty = true;
|
|
|
|
end:
|
|
header->endofs = dp - startdp;
|
|
return HEADER_OK;
|
|
}
|
|
|
|
static void G_FreeDemoHeader(demoheader_t *header)
|
|
{
|
|
if (header->numfiles)
|
|
{
|
|
for (UINT8 i = 0; i < header->numfiles; i++)
|
|
free(header->files[i].filename);
|
|
free(header->files);
|
|
}
|
|
|
|
if (header->numcvars)
|
|
{
|
|
for (UINT16 i = 0; i < header->numcvars; i++)
|
|
{
|
|
if (header->cvars[i].name)
|
|
free(header->cvars[i].name);
|
|
free(header->cvars[i].value);
|
|
}
|
|
free(header->cvars);
|
|
}
|
|
|
|
if (header->numplayers)
|
|
free(header->playerdata);
|
|
|
|
#ifdef PARANOIA
|
|
memset(header, 0xff, sizeof(*header));
|
|
#endif
|
|
}
|
|
|
|
enum // legacy RA flags
|
|
{
|
|
// 0x0009
|
|
RAF_RINGS = 1<<0,
|
|
RAF_STACKING = 1<<1,
|
|
RAF_CHAINING = 1<<2,
|
|
RAF_SLIPDASH = 1<<3,
|
|
RAF_PURPLEDRIFT = 1<<4,
|
|
RAF_SLOPEBOOST = 1<<5,
|
|
// 0x000C
|
|
RAF_AIRDROP = 1<<6,
|
|
RAF_BUMPDRIFT = 1<<7,
|
|
RAF_BUMPSPARK = 2<<7,
|
|
RAF_BS_RESET100 = 3<<7,
|
|
RAF_BUMPSPARKMASK = 0x3<<7,
|
|
};
|
|
|
|
static strbuf_t *G_GetRecordPresetVersionForDemo(demoheader_t *header)
|
|
{
|
|
if (header->demoversion >= 0x000D)
|
|
return G_GetRecordPresetVersion(header->rapreset, header->rapresetversion);
|
|
|
|
// oh dear... this demo predates softcoded presets.
|
|
// we'll have to try and figure out the corresponding preset
|
|
UINT32 test, raflags = header->raflags;
|
|
|
|
if (raflags == 0)
|
|
return G_GetRecordPresetVersion("kart", 1);
|
|
|
|
test = RAF_STACKING|RAF_CHAINING|RAF_SLOPEBOOST;
|
|
if (header->demoversion == 0x000C)
|
|
test |= RAF_AIRDROP|RAF_BUMPSPARK;
|
|
if (raflags == test)
|
|
return G_GetRecordPresetVersion("tech", header->demoversion == 0x000C ? 2 : 1);
|
|
|
|
test = RAF_RINGS|RAF_STACKING|RAF_CHAINING|RAF_SLIPDASH|RAF_PURPLEDRIFT|RAF_SLOPEBOOST;
|
|
if (header->demoversion == 0x000C)
|
|
test |= RAF_AIRDROP|RAF_BUMPDRIFT;
|
|
if (raflags == test)
|
|
return G_GetRecordPresetVersion("blankart", header->demoversion == 0x000C ? 2 : 1);
|
|
|
|
// supporting custom raflags is a pain in the ass, so no thanks
|
|
// if it really is necessary, just add another preset to the defaults...
|
|
return NULL;
|
|
}
|
|
|
|
// and now, the rest of g_demo.c
|
|
|
|
boolean G_CompatLevel(UINT16 level)
|
|
{
|
|
if (demo.playback)
|
|
{
|
|
// Check gameplay differences for older ghosts
|
|
return (demo.version <= level);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static mobj_t oldmetal, oldghost[MAXPLAYERS];
|
|
|
|
void G_SaveMetal(UINT8 **buffer)
|
|
{
|
|
I_Assert(buffer != NULL && *buffer != NULL);
|
|
|
|
WRITEUINT32(*buffer, metal_p - metalbuffer);
|
|
}
|
|
|
|
void G_LoadMetal(UINT8 **buffer)
|
|
{
|
|
I_Assert(buffer != NULL && *buffer != NULL);
|
|
|
|
G_DoPlayMetal();
|
|
metal_p = metalbuffer + READUINT32(*buffer);
|
|
}
|
|
|
|
// Finds a skin with the closest stats if the expected skin doesn't exist.
|
|
static INT32 GetSkinNumClosestToStats(UINT8 kartspeed, UINT8 kartweight)
|
|
{
|
|
INT32 i, closest_skin = 0;
|
|
UINT8 closest_stats = UINT8_MAX, stat_diff;
|
|
|
|
for (i = 0; i < numskins; i++)
|
|
{
|
|
stat_diff = abs(skins[i].kartspeed - kartspeed) + abs(skins[i].kartweight - kartweight);
|
|
if (stat_diff < closest_stats)
|
|
{
|
|
closest_stats = stat_diff;
|
|
closest_skin = i;
|
|
}
|
|
}
|
|
|
|
return closest_skin;
|
|
}
|
|
|
|
static void FindClosestSkinForStats(UINT32 p, UINT8 kartspeed, UINT8 kartweight)
|
|
{
|
|
INT32 closest_skin = GetSkinNumClosestToStats(kartspeed, kartweight);
|
|
|
|
//CONS_Printf("Using %s instead...\n", skins[closest_skin].name);
|
|
SetPlayerSkinByNum(p, closest_skin);
|
|
}
|
|
|
|
void G_LoadDemoCvars(void **p, void (*setvalue)(consvar_t *, const char *, boolean))
|
|
{
|
|
demoheader_t *header = *p;
|
|
for (UINT16 i = 0; i < header->numcvars; i++)
|
|
{
|
|
if (header->cvars[i].name == NULL)
|
|
{
|
|
CONS_Alert(CONS_WARNING, "Netvar not found with netid %d\n", header->cvars[i].legacynetid);
|
|
continue;
|
|
}
|
|
|
|
consvar_t *cvar = CV_FindVar(header->cvars[i].name);
|
|
if (cvar)
|
|
setvalue(cvar, header->cvars[i].value, header->cvars[i].stealth);
|
|
else
|
|
CONS_Alert(CONS_WARNING, "Netvar not found with name %s\n", header->cvars[i].name);
|
|
}
|
|
}
|
|
|
|
void G_ReadDemoExtraData(void)
|
|
{
|
|
INT32 p, i;
|
|
|
|
if (leveltime > starttime)
|
|
{
|
|
rewind_t *rewind = CL_SaveRewindPoint(demobuf.p - demobuf.buffer);
|
|
if (rewind)
|
|
{
|
|
memcpy(rewind->oldcmd, oldcmd, sizeof (oldcmd));
|
|
memcpy(rewind->oldghost, oldghost, sizeof (oldghost));
|
|
}
|
|
}
|
|
|
|
while (true)
|
|
{
|
|
extradata_t extra;
|
|
demobuf.p = G_ReadRawExtraData(&extra, demobuf.p, demo.version);
|
|
p = extra.playernum;
|
|
|
|
player_t *player = &players[p];
|
|
|
|
if (p == DW_END)
|
|
break;
|
|
|
|
if (p >= DW_EXTRASTUFF) switch (p)
|
|
{
|
|
case DW_RNG:
|
|
if (P_GetRandSeed() != extra.randseed)
|
|
{
|
|
P_SetRandSeed(extra.randseed);
|
|
|
|
if (demosynced)
|
|
CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced (RNG)!\n"));
|
|
demosynced = false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (extra.flags & DXD_JOINDATA)
|
|
{
|
|
if (!playeringame[extra.playernum])
|
|
{
|
|
G_AddPlayer(extra.playernum, extra.playernum);
|
|
}
|
|
|
|
player->bot = extra.joindata.bot;
|
|
if (player->bot)
|
|
{
|
|
player->botvars.difficulty = extra.joindata.difficulty;
|
|
player->botvars.diffincrease = extra.joindata.diffincrease; // needed to avoid having to duplicate logic
|
|
player->botvars.rival = extra.joindata.rival;
|
|
}
|
|
}
|
|
if (extra.flags & DXD_PLAYSTATE)
|
|
{
|
|
switch (extra.playstate) {
|
|
case DXD_PST_PLAYING:
|
|
if (player->spectator == true)
|
|
{
|
|
if (player->bot)
|
|
{
|
|
player->spectator = false;
|
|
}
|
|
else
|
|
{
|
|
player->pflags |= PF_WANTSTOJOIN;
|
|
}
|
|
}
|
|
//CONS_Printf("player %s is despectating on tic %d\n", player_names[p], leveltime);
|
|
break;
|
|
|
|
case DXD_PST_SPECTATING:
|
|
if (player->spectator)
|
|
{
|
|
player->pflags &= ~PF_WANTSTOJOIN;
|
|
}
|
|
else
|
|
{
|
|
if (player->mo)
|
|
{
|
|
P_DamageMobj(player->mo, NULL, NULL, 1, DMG_SPECTATOR);
|
|
}
|
|
P_SetPlayerSpectator(p);
|
|
}
|
|
|
|
break;
|
|
|
|
case DXD_PST_LEFT:
|
|
CL_RemovePlayer(p, 0);
|
|
break;
|
|
}
|
|
|
|
G_ResetViews(false); // dont reset our freecam pls thx!
|
|
|
|
// maybe these are necessary?
|
|
K_CheckBumpers();
|
|
P_CheckRacers();
|
|
}
|
|
if (extra.flags & DXD_NAME)
|
|
{
|
|
// Name
|
|
strcpy(player_names[p], extra.playername);
|
|
}
|
|
if (extra.flags & DXD_SKIN)
|
|
{
|
|
// Skin
|
|
SetPlayerSkin(p, extra.skinname);
|
|
|
|
if (!fasticmp(skins[player->skin].name, extra.skinname))
|
|
FindClosestSkinForStats(p, extra.kartspeed, extra.kartweight);
|
|
|
|
// Voice
|
|
const INT32 vce = R_FindIDForVoice(&skins[player->skin], extra.voicename);
|
|
|
|
if (vce == MAXSKINVOICES)
|
|
{
|
|
// The above function didn't find a voice, so we'll just fall back
|
|
// to the skin's voice.
|
|
player->voice_id = 0;
|
|
}
|
|
else
|
|
{
|
|
player->voice_id = (UINT16)vce;
|
|
}
|
|
|
|
if (!P_MobjWasRemoved(player->mo))
|
|
{
|
|
// This player's mobj exists. So, attach the voice to the mobj!
|
|
player->mo->voice = &skins[player->skin].voices[player->voice_id];
|
|
}
|
|
|
|
player->kartspeed = extra.kartspeed;
|
|
player->kartweight = extra.kartweight;
|
|
}
|
|
if (extra.flags & DXD_COLOR)
|
|
{
|
|
// Color
|
|
for (i = 0; i < numskincolors; i++)
|
|
if (fasticmp(skincolors[i].name, extra.colorname)) // SRB2kart
|
|
{
|
|
player->skincolor = i;
|
|
if (player->mo)
|
|
player->mo->color = i;
|
|
break;
|
|
}
|
|
}
|
|
if (extra.flags & DXD_FOLLOWER)
|
|
{
|
|
// Set our follower
|
|
K_SetFollowerByName(p, extra.followername);
|
|
|
|
// Follower's color
|
|
for (i = 0; i < numskincolors +2; i++) // +2 because of Match and Opposite
|
|
if (fasticmp(Followercolor_cons_t[i].strvalue, extra.followercolor))
|
|
{
|
|
player->followercolor = i;
|
|
break;
|
|
}
|
|
}
|
|
if (extra.flags & DXD_RESPAWN)
|
|
{
|
|
if (player->mo)
|
|
{
|
|
// Is this how this should work..?
|
|
P_DamageMobj(player->mo, NULL, NULL, 1, DMG_INSTAKILL);
|
|
}
|
|
}
|
|
if (extra.flags & DXD_WEAPONPREF)
|
|
{
|
|
UINT8 *bruh = &extra.weaponpref;
|
|
WeaponPref_Parse(&bruh, p);
|
|
|
|
//CONS_Printf("weaponpref is %d for player %d\n", i, p);
|
|
}
|
|
}
|
|
|
|
if (!(demoflags & DF_GHOST) && *demobuf.p == DEMOMARKER)
|
|
{
|
|
// end of demo data stream
|
|
G_CheckDemoStatus();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void G_WriteDemoExtraData(void)
|
|
{
|
|
INT32 i, j;
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (demo_extradata[i])
|
|
{
|
|
WRITEUINT8(demobuf.p, i);
|
|
WRITEUINT8(demobuf.p, demo_extradata[i]);
|
|
|
|
if (demo_extradata[i] & DXD_JOINDATA)
|
|
{
|
|
for (j = 0; j < MAXAVAILABILITY; j++)
|
|
{
|
|
WRITEUINT8(demobuf.p, players[i].availabilities[i]);
|
|
}
|
|
|
|
WRITEUINT8(demobuf.p, (UINT8)players[i].bot);
|
|
if (players[i].bot)
|
|
{
|
|
WRITEUINT8(demobuf.p, players[i].botvars.difficulty);
|
|
WRITEUINT8(demobuf.p, players[i].botvars.diffincrease); // needed to avoid having to duplicate logic
|
|
WRITEUINT8(demobuf.p, (UINT8)players[i].botvars.rival);
|
|
}
|
|
}
|
|
if (demo_extradata[i] & DXD_PLAYSTATE)
|
|
{
|
|
UINT8 pst = DXD_PST_PLAYING;
|
|
|
|
demo_writerng = 1;
|
|
|
|
if (!playeringame[i])
|
|
{
|
|
pst = DXD_PST_LEFT;
|
|
}
|
|
else if (
|
|
players[i].spectator &&
|
|
!(players[i].pflags & PF_WANTSTOJOIN) // <= fuck you specifically
|
|
)
|
|
{
|
|
pst = DXD_PST_SPECTATING;
|
|
}
|
|
|
|
WRITEUINT8(demobuf.p, pst);
|
|
}
|
|
if (demo_extradata[i] & DXD_NAME)
|
|
{
|
|
// Name
|
|
WRITESTRINGL(demobuf.p, player_names[i], 21+1);
|
|
}
|
|
if (demo_extradata[i] & DXD_SKIN)
|
|
{
|
|
// Skin
|
|
WRITESTRINGL(demobuf.p, skins[players[i].skin].name, 16+1);
|
|
|
|
// Voice
|
|
WRITESTRINGL(demobuf.p, skins[players[i].skin].voices[players[i].voice_id].name, 32+1);
|
|
|
|
// Stats
|
|
WRITEUINT8(demobuf.p, skins[players[i].skin].kartspeed);
|
|
WRITEUINT8(demobuf.p, skins[players[i].skin].kartweight);
|
|
|
|
}
|
|
if (demo_extradata[i] & DXD_COLOR)
|
|
{
|
|
// Color
|
|
WRITESTRINGL(demobuf.p, skincolors[players[i].skincolor].name, 16+1);
|
|
}
|
|
if (demo_extradata[i] & DXD_FOLLOWER)
|
|
{
|
|
// write follower
|
|
if (players[i].followerskin == -1)
|
|
WRITESTRINGL(demobuf.p, "None", 16+1);
|
|
else
|
|
WRITESTRINGL(demobuf.p, followers[players[i].followerskin].skinname, 16+1);
|
|
|
|
// write follower color
|
|
WRITESTRINGL(demobuf.p, Followercolor_cons_t[(UINT16)(players[i].followercolor+2)].strvalue, 16+1);
|
|
|
|
}
|
|
//if (demo_extradata[i] & DXD_RESPAWN) has no extra data
|
|
if (demo_extradata[i] & DXD_WEAPONPREF)
|
|
{
|
|
WeaponPref_Save(&demobuf.p, i);
|
|
}
|
|
}
|
|
|
|
demo_extradata[i] = 0;
|
|
}
|
|
|
|
// May not be necessary, but might as well play it safe...
|
|
if ((leveltime & 255) == 128)
|
|
demo_writerng = 1;
|
|
|
|
{
|
|
static UINT8 timeout = 0;
|
|
|
|
if (timeout) timeout--;
|
|
|
|
if (demo_writerng == 1 || (demo_writerng == 2 && timeout == 0))
|
|
{
|
|
demo_writerng = 0;
|
|
timeout = 16;
|
|
WRITEUINT8(demobuf.p, DW_RNG);
|
|
WRITEUINT32(demobuf.p, P_GetRandSeed());
|
|
}
|
|
}
|
|
|
|
WRITEUINT8(demobuf.p, DW_END);
|
|
}
|
|
|
|
void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
|
|
{
|
|
if (!demobuf.p || !demo.deferstart)
|
|
return;
|
|
|
|
demobuf.p = G_ReadZipTic(&oldcmd[playernum], demobuf.p, demo.version);
|
|
G_CopyTiccmd(cmd, &oldcmd[playernum], 1);
|
|
|
|
if (!(demoflags & DF_GHOST) && *demobuf.p == DEMOMARKER)
|
|
{
|
|
// end of demo data stream
|
|
G_CheckDemoStatus();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum)
|
|
{
|
|
UINT16 ziptic = 0;
|
|
UINT8 *ziptic_p;
|
|
//(void)playernum;
|
|
|
|
if (!demobuf.p)
|
|
return;
|
|
|
|
ziptic_p = demobuf.p; // the ziptic, written at the end of this function
|
|
demobuf.p += 2;
|
|
|
|
if (cmd->forwardmove != oldcmd[playernum].forwardmove)
|
|
{
|
|
WRITESINT8(demobuf.p,cmd->forwardmove);
|
|
oldcmd[playernum].forwardmove = cmd->forwardmove;
|
|
ziptic |= ZT_FWD;
|
|
}
|
|
|
|
if (cmd->sidemove != oldcmd[playernum].sidemove)
|
|
{
|
|
WRITESINT8(demobuf.p,cmd->sidemove);
|
|
oldcmd[playernum].sidemove = cmd->sidemove;
|
|
ziptic |= ZT_SIDE;
|
|
}
|
|
|
|
if (cmd->turning != oldcmd[playernum].turning)
|
|
{
|
|
WRITEINT16(demobuf.p,cmd->turning);
|
|
oldcmd[playernum].turning = cmd->turning;
|
|
ziptic |= ZT_TURNING;
|
|
}
|
|
|
|
if (cmd->angle != oldcmd[playernum].angle)
|
|
{
|
|
WRITEINT16(demobuf.p,cmd->angle);
|
|
oldcmd[playernum].angle = cmd->angle;
|
|
ziptic |= ZT_ANGLE;
|
|
}
|
|
|
|
if (cmd->throwdir != oldcmd[playernum].throwdir)
|
|
{
|
|
WRITEINT16(demobuf.p,cmd->throwdir);
|
|
oldcmd[playernum].throwdir = cmd->throwdir;
|
|
ziptic |= ZT_THROWDIR;
|
|
}
|
|
|
|
if (cmd->buttons != oldcmd[playernum].buttons)
|
|
{
|
|
WRITEUINT16(demobuf.p,cmd->buttons);
|
|
oldcmd[playernum].buttons = cmd->buttons;
|
|
ziptic |= ZT_BUTTONS;
|
|
}
|
|
|
|
if (cmd->aiming != oldcmd[playernum].aiming)
|
|
{
|
|
WRITEINT16(demobuf.p,cmd->aiming);
|
|
oldcmd[playernum].aiming = cmd->aiming;
|
|
ziptic |= ZT_AIMING;
|
|
}
|
|
|
|
if (cmd->latency != oldcmd[playernum].latency)
|
|
{
|
|
WRITEUINT8(demobuf.p,cmd->latency);
|
|
oldcmd[playernum].latency = cmd->latency;
|
|
ziptic |= ZT_LATENCY;
|
|
}
|
|
|
|
if (cmd->flags != oldcmd[playernum].flags)
|
|
{
|
|
WRITEUINT8(demobuf.p,cmd->flags);
|
|
oldcmd[playernum].flags = cmd->flags;
|
|
ziptic |= ZT_FLAGS;
|
|
}
|
|
|
|
WRITEUINT16(ziptic_p, ziptic);
|
|
|
|
// attention here for the ticcmd size!
|
|
// latest demos with mouse aiming byte in ticcmd
|
|
if (!(demoflags & DF_GHOST) && ziptic_p > demobuf.end - 9)
|
|
{
|
|
G_CheckDemoStatus(); // no more space
|
|
return;
|
|
}
|
|
}
|
|
|
|
void G_GhostAddFlip(INT32 playernum)
|
|
{
|
|
if (!metalrecording && (!demo.recording || !(demoflags & DF_GHOST)))
|
|
return;
|
|
ghostext[playernum].flags |= EZT_FLIP;
|
|
}
|
|
|
|
void G_GhostAddColor(INT32 playernum, ghostcolor_t color)
|
|
{
|
|
if (!demo.recording || !(demoflags & DF_GHOST))
|
|
return;
|
|
if (ghostext[playernum].lastcolor == (UINT16)color)
|
|
{
|
|
ghostext[playernum].flags &= ~EZT_COLOR;
|
|
return;
|
|
}
|
|
ghostext[playernum].flags |= EZT_COLOR;
|
|
ghostext[playernum].color = (UINT16)color;
|
|
}
|
|
|
|
void G_GhostAddScale(INT32 playernum, fixed_t scale)
|
|
{
|
|
if (!metalrecording && (!demo.recording || !(demoflags & DF_GHOST)))
|
|
return;
|
|
if (ghostext[playernum].lastscale == scale)
|
|
{
|
|
ghostext[playernum].flags &= ~EZT_SCALE;
|
|
return;
|
|
}
|
|
ghostext[playernum].flags |= EZT_SCALE;
|
|
ghostext[playernum].scale = scale;
|
|
}
|
|
|
|
void G_GhostAddHit(INT32 playernum, mobj_t *victim)
|
|
{
|
|
if (!demo.recording || !(demoflags & DF_GHOST))
|
|
return;
|
|
ghostext[playernum].flags |= EZT_HIT;
|
|
ghostext[playernum].hits++;
|
|
Z_Realloc(ghostext[playernum].hitlist, ghostext[playernum].hits * sizeof(mobj_t *), PU_STATIC, &ghostext[playernum].hitlist);
|
|
P_SetTarget(ghostext[playernum].hitlist + (ghostext[playernum].hits-1), victim);
|
|
}
|
|
|
|
void G_WriteAllGhostTics(void)
|
|
{
|
|
boolean toobig = false;
|
|
INT32 i, counter = leveltime;
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (!playeringame[i] || players[i].spectator)
|
|
continue;
|
|
|
|
if (!players[i].mo)
|
|
continue;
|
|
|
|
counter++;
|
|
|
|
if (multiplayer && ((counter % cv_netdemosyncquality.value) != 0)) // Only write 1 in this many ghost datas per tic to cut down on multiplayer replay size.
|
|
continue;
|
|
|
|
WRITEUINT8(demobuf.p, i);
|
|
G_WriteGhostTic(players[i].mo, i);
|
|
|
|
// attention here for the ticcmd size!
|
|
// latest demos with mouse aiming byte in ticcmd
|
|
if (demobuf.p >= demobuf.end - (13 + 9 + 9))
|
|
{
|
|
toobig = true;
|
|
break;
|
|
}
|
|
}
|
|
WRITEUINT8(demobuf.p, 0xFF);
|
|
|
|
if (toobig)
|
|
{
|
|
G_CheckDemoStatus(); // no more space
|
|
return;
|
|
}
|
|
}
|
|
|
|
void G_WriteGhostTic(mobj_t *ghost, INT32 playernum)
|
|
{
|
|
char ziptic = 0;
|
|
UINT8 *ziptic_p;
|
|
UINT32 i;
|
|
|
|
if (!demobuf.p)
|
|
return;
|
|
if (!(demoflags & DF_GHOST))
|
|
return; // No ghost data to write.
|
|
|
|
ziptic_p = demobuf.p++; // the ziptic, written at the end of this function
|
|
|
|
#define MAXMOM (0xFFFF<<8)
|
|
|
|
// GZT_XYZ is only useful if you've moved 256 FRACUNITS or more in a single tic.
|
|
if (abs(ghost->x-oldghost[playernum].x) > MAXMOM
|
|
|| abs(ghost->y-oldghost[playernum].y) > MAXMOM
|
|
|| abs(ghost->z-oldghost[playernum].z) > MAXMOM
|
|
|| ((UINT8)(leveltime & 255) > 0 && (UINT8)(leveltime & 255) <= (UINT8)cv_netdemosyncquality.value)) // Hack to enable slightly nicer resyncing
|
|
{
|
|
oldghost[playernum].x = ghost->x;
|
|
oldghost[playernum].y = ghost->y;
|
|
oldghost[playernum].z = ghost->z;
|
|
ziptic |= GZT_XYZ;
|
|
WRITEFIXED(demobuf.p,oldghost[playernum].x);
|
|
WRITEFIXED(demobuf.p,oldghost[playernum].y);
|
|
WRITEFIXED(demobuf.p,oldghost[playernum].z);
|
|
}
|
|
else
|
|
{
|
|
// For moving normally:
|
|
fixed_t momx = ghost->x-oldghost[playernum].x;
|
|
fixed_t momy = ghost->y-oldghost[playernum].y;
|
|
|
|
if (momx != oldghost[playernum].momx
|
|
|| momy != oldghost[playernum].momy)
|
|
{
|
|
oldghost[playernum].momx = momx;
|
|
oldghost[playernum].momy = momy;
|
|
ziptic |= GZT_MOMXY;
|
|
WRITEFIXED(demobuf.p,momx);
|
|
WRITEFIXED(demobuf.p,momy);
|
|
}
|
|
|
|
momx = ghost->z-oldghost[playernum].z;
|
|
if (momx != oldghost[playernum].momz)
|
|
{
|
|
oldghost[playernum].momz = momx;
|
|
ziptic |= GZT_MOMZ;
|
|
WRITEFIXED(demobuf.p,momx);
|
|
}
|
|
|
|
// This SHOULD set oldghost.x/y/z to match ghost->x/y/z
|
|
oldghost[playernum].x += oldghost[playernum].momx;
|
|
oldghost[playernum].y += oldghost[playernum].momy;
|
|
oldghost[playernum].z +=oldghost[playernum].momz;
|
|
}
|
|
|
|
#undef MAXMOM
|
|
|
|
// Only store the 8 most relevant bits of angle
|
|
// because exact values aren't too easy to discern to begin with when only 8 angles have different sprites
|
|
// and it does not affect this mode of movement at all anyway.
|
|
if (ghost->player && ghost->player->drawangle>>24 != oldghost[playernum].angle)
|
|
{
|
|
oldghost[playernum].angle = ghost->player->drawangle>>24;
|
|
ziptic |= GZT_ANGLE;
|
|
WRITEUINT8(demobuf.p,oldghost[playernum].angle);
|
|
}
|
|
|
|
// Store the sprite frame.
|
|
if ((ghost->frame & FF_FRAMEMASK) != oldghost[playernum].frame)
|
|
{
|
|
oldghost[playernum].frame = (ghost->frame & FF_FRAMEMASK);
|
|
ziptic |= GZT_FRAME;
|
|
WRITEUINT8(demobuf.p,oldghost[playernum].frame);
|
|
}
|
|
|
|
if (ghost->sprite == SPR_PLAY
|
|
&& ghost->sprite2 != oldghost[playernum].sprite2)
|
|
{
|
|
oldghost[playernum].sprite2 = ghost->sprite2;
|
|
ziptic |= GZT_SPR2;
|
|
WRITEUINT8(demobuf.p,oldghost[playernum].sprite2);
|
|
}
|
|
|
|
// Check for sprite set changes
|
|
if (ghost->sprite != oldghost[playernum].sprite)
|
|
{
|
|
oldghost[playernum].sprite = ghost->sprite;
|
|
ghostext[playernum].flags |= EZT_SPRITE;
|
|
}
|
|
|
|
if (ghost->player && (
|
|
ghostext[playernum].kartitem != ghost->player->itemtype ||
|
|
ghostext[playernum].kartamount != ghost->player->itemamount ||
|
|
ghostext[playernum].kartbumpers != ghost->player->bumper
|
|
))
|
|
{
|
|
ghostext[playernum].flags |= EZT_KART;
|
|
ghostext[playernum].kartitem = ghost->player->itemtype;
|
|
ghostext[playernum].kartamount = ghost->player->itemamount;
|
|
ghostext[playernum].kartbumpers = ghost->player->bumper;
|
|
}
|
|
|
|
if (ghostext[playernum].flags)
|
|
{
|
|
ziptic |= GZT_EXTRA;
|
|
|
|
if (ghostext[playernum].color == ghostext[playernum].lastcolor)
|
|
ghostext[playernum].flags &= ~EZT_COLOR;
|
|
if (ghostext[playernum].scale == ghostext[playernum].lastscale)
|
|
ghostext[playernum].flags &= ~EZT_SCALE;
|
|
|
|
WRITEUINT8(demobuf.p,ghostext[playernum].flags);
|
|
if (ghostext[playernum].flags & EZT_COLOR)
|
|
{
|
|
WRITEUINT16(demobuf.p,ghostext[playernum].color);
|
|
ghostext[playernum].lastcolor = ghostext[playernum].color;
|
|
}
|
|
if (ghostext[playernum].flags & EZT_SCALE)
|
|
{
|
|
WRITEFIXED(demobuf.p,ghostext[playernum].scale);
|
|
ghostext[playernum].lastscale = ghostext[playernum].scale;
|
|
}
|
|
if (ghostext[playernum].flags & EZT_HIT)
|
|
{
|
|
WRITEUINT16(demobuf.p,ghostext[playernum].hits);
|
|
for (i = 0; i < ghostext[playernum].hits; i++)
|
|
{
|
|
mobj_t *mo = ghostext[playernum].hitlist[i];
|
|
//WRITEUINT32(demobuf.p,UINT32_MAX); // reserved for some method of determining exactly which mobj this is. (mobjnum doesn't work here.)
|
|
WRITEUINT32(demobuf.p,mo->type);
|
|
WRITEUINT16(demobuf.p,(UINT16)mo->health);
|
|
WRITEFIXED(demobuf.p,mo->x);
|
|
WRITEFIXED(demobuf.p,mo->y);
|
|
WRITEFIXED(demobuf.p,mo->z);
|
|
WRITEANGLE(demobuf.p,mo->angle);
|
|
P_SetTarget(ghostext[playernum].hitlist+i, NULL);
|
|
}
|
|
Z_Free(ghostext[playernum].hitlist);
|
|
ghostext[playernum].hits = 0;
|
|
}
|
|
if (ghostext[playernum].flags & EZT_SPRITE)
|
|
WRITEUINT16(demobuf.p,oldghost[playernum].sprite);
|
|
if (ghostext[playernum].flags & EZT_KART)
|
|
{
|
|
WRITEINT32(demobuf.p, ghostext[playernum].kartitem);
|
|
WRITEINT32(demobuf.p, ghostext[playernum].kartamount);
|
|
WRITEINT32(demobuf.p, ghostext[playernum].kartbumpers);
|
|
}
|
|
|
|
ghostext[playernum].flags = 0;
|
|
}
|
|
|
|
if (ghost->player && ghost->player->followmobj&& !(ghost->player->followmobj->sprite == SPR_NULL || (ghost->player->followmobj->renderflags & RF_DONTDRAW) == RF_DONTDRAW)) // bloats tails runs but what can ya do
|
|
{
|
|
fixed_t temp;
|
|
UINT8 *followtic_p = demobuf.p++;
|
|
UINT8 followtic = 0;
|
|
|
|
ziptic |= GZT_FOLLOW;
|
|
|
|
if (ghost->player->followmobj->skin)
|
|
followtic |= FZT_SKIN;
|
|
|
|
if (!(oldghost[playernum].flags2 & MF2_AMBUSH))
|
|
{
|
|
followtic |= FZT_SPAWNED;
|
|
WRITEINT16(demobuf.p,ghost->player->followmobj->info->height>>FRACBITS);
|
|
if (ghost->player->followmobj->flags2 & MF2_LINKDRAW)
|
|
followtic |= FZT_LINKDRAW;
|
|
if (ghost->player->followmobj->colorized)
|
|
followtic |= FZT_COLORIZED;
|
|
if (followtic & FZT_SKIN)
|
|
WRITEUINT16(demobuf.p,(UINT16)(((skin_t *)(ghost->player->followmobj->skin))-skins));
|
|
oldghost[playernum].flags2 |= MF2_AMBUSH;
|
|
}
|
|
|
|
if (ghost->player->followmobj->scale != ghost->scale)
|
|
{
|
|
followtic |= FZT_SCALE;
|
|
WRITEFIXED(demobuf.p,ghost->player->followmobj->scale);
|
|
}
|
|
|
|
temp = ghost->player->followmobj->x-ghost->x;
|
|
WRITEFIXED(demobuf.p,temp);
|
|
temp = ghost->player->followmobj->y-ghost->y;
|
|
WRITEFIXED(demobuf.p,temp);
|
|
temp = ghost->player->followmobj->z-ghost->z;
|
|
WRITEFIXED(demobuf.p,temp);
|
|
if (followtic & FZT_SKIN)
|
|
WRITEUINT8(demobuf.p,ghost->player->followmobj->sprite2);
|
|
WRITEUINT16(demobuf.p,ghost->player->followmobj->sprite);
|
|
WRITEUINT8(demobuf.p,(ghost->player->followmobj->frame & FF_FRAMEMASK));
|
|
WRITEUINT16(demobuf.p,ghost->player->followmobj->color);
|
|
|
|
*followtic_p = followtic;
|
|
}
|
|
else
|
|
oldghost[playernum].flags2 &= ~MF2_AMBUSH;
|
|
|
|
*ziptic_p = ziptic;
|
|
}
|
|
|
|
void G_ConsAllGhostTics(void)
|
|
{
|
|
UINT8 p;
|
|
|
|
if (!demobuf.p || !demo.deferstart)
|
|
return;
|
|
|
|
p = READUINT8(demobuf.p);
|
|
|
|
while (p != 0xFF)
|
|
{
|
|
G_ConsGhostTic(p);
|
|
p = READUINT8(demobuf.p);
|
|
}
|
|
|
|
if (*demobuf.p == DEMOMARKER)
|
|
{
|
|
// end of demo data stream
|
|
G_CheckDemoStatus();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Uses ghost data to do consistency checks on your position.
|
|
// This fixes desynchronising demos when fighting eggman.
|
|
void G_ConsGhostTic(INT32 playernum)
|
|
{
|
|
INT32 px,py,pz,gx,gy,gz;
|
|
mobj_t *testmo;
|
|
UINT32 syncleeway;
|
|
|
|
if (!(demoflags & DF_GHOST))
|
|
return; // No ghost data to use.
|
|
|
|
testmo = players[playernum].mo;
|
|
|
|
ziptic_t zt;
|
|
demobuf.p = G_ReadExtraZipTic(&zt, demobuf.p, demo.version, testmo);
|
|
|
|
if (!cv_resyncdemo.value)
|
|
goto skip;
|
|
|
|
// Grab ghost data.
|
|
if (zt.flags & GZT_XYZ)
|
|
{
|
|
oldghost[playernum].x = zt.x;
|
|
oldghost[playernum].y = zt.y;
|
|
oldghost[playernum].z = zt.z;
|
|
syncleeway = 0;
|
|
}
|
|
else
|
|
{
|
|
if (zt.flags & GZT_MOMXY)
|
|
{
|
|
oldghost[playernum].momx = zt.x;
|
|
oldghost[playernum].momy = zt.y;
|
|
}
|
|
if (zt.flags & GZT_MOMZ)
|
|
oldghost[playernum].momz = zt.z;
|
|
oldghost[playernum].x += oldghost[playernum].momx;
|
|
oldghost[playernum].y += oldghost[playernum].momy;
|
|
oldghost[playernum].z += oldghost[playernum].momz;
|
|
syncleeway = FRACUNIT;
|
|
}
|
|
|
|
if (zt.flags & GZT_EXTRA)
|
|
{ // But wait, there's more!
|
|
if (zt.extraflags & EZT_HIT)
|
|
{ // Resync mob damage.
|
|
for (UINT16 i = 0; i < zt.damagecount; i++)
|
|
{
|
|
zipticdamage_t *damage = &zt.damage[i];
|
|
mobj_t *mobj = NULL;
|
|
thinker_t *th = NULL;
|
|
for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
|
|
{
|
|
if (th->function == (actionf_p1)P_RemoveThinkerDelayed)
|
|
continue;
|
|
mobj = (mobj_t *)th;
|
|
if (mobj->type == damage->type && mobj->x == damage->x && mobj->y == damage->y && mobj->z == damage->z)
|
|
break;
|
|
}
|
|
if (th != &thlist[THINK_MOBJ] && mobj->health != damage->health) // Wasn't damaged?! This is desync! Fix it!
|
|
{
|
|
if (demosynced)
|
|
CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced (health)!\n"));
|
|
demosynced = false;
|
|
P_DamageMobj(mobj, players[0].mo, players[0].mo, 1, DMG_NORMAL);
|
|
}
|
|
}
|
|
}
|
|
if (zt.extraflags & EZT_KART)
|
|
{
|
|
ghostext[playernum].kartitem = zt.kartitem;
|
|
ghostext[playernum].kartamount = zt.kartamount;
|
|
ghostext[playernum].kartbumpers = zt.kartbumpers;
|
|
}
|
|
}
|
|
|
|
if (testmo)
|
|
{
|
|
// Re-synchronise
|
|
px = testmo->x;
|
|
py = testmo->y;
|
|
pz = testmo->z;
|
|
gx = oldghost[playernum].x;
|
|
gy = oldghost[playernum].y;
|
|
gz = oldghost[playernum].z;
|
|
|
|
if ((UINT32)abs(px-gx) > syncleeway || (UINT32)abs(py-gy) > syncleeway || (UINT32)abs(pz-gz) > syncleeway)
|
|
{
|
|
ghostext[playernum].desyncframes++;
|
|
|
|
if (ghostext[playernum].desyncframes >= 2)
|
|
{
|
|
if (demosynced)
|
|
CONS_Alert(CONS_WARNING, "Demo playback has desynced (player %s)!\n", player_names[playernum]);
|
|
demosynced = false;
|
|
|
|
P_UnsetThingPosition(testmo);
|
|
testmo->x = oldghost[playernum].x;
|
|
testmo->y = oldghost[playernum].y;
|
|
P_SetThingPosition(testmo);
|
|
testmo->z = oldghost[playernum].z;
|
|
|
|
if (abs(testmo->z - testmo->floorz) < 4*FRACUNIT)
|
|
testmo->z = testmo->floorz; // Sync players to the ground when they're likely supposed to be there...
|
|
|
|
ghostext[playernum].desyncframes = 2;
|
|
}
|
|
}
|
|
else
|
|
ghostext[playernum].desyncframes = 0;
|
|
|
|
if (!G_CompatLevel(0x0001) &&
|
|
(players[playernum].itemtype != ghostext[playernum].kartitem
|
|
|| players[playernum].itemamount != ghostext[playernum].kartamount
|
|
|| players[playernum].bumper != ghostext[playernum].kartbumpers))
|
|
{
|
|
if (demosynced)
|
|
CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced (item/bumpers)!\n"));
|
|
demosynced = false;
|
|
|
|
players[playernum].itemtype = ghostext[playernum].kartitem;
|
|
players[playernum].itemamount = ghostext[playernum].kartamount;
|
|
players[playernum].bumper = ghostext[playernum].kartbumpers;
|
|
}
|
|
}
|
|
|
|
skip:
|
|
G_FreeExtraZipTic(&zt);
|
|
if (*demobuf.p == DEMOMARKER)
|
|
{
|
|
// end of demo data stream
|
|
G_CheckDemoStatus();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void G_GhostTicker(void)
|
|
{
|
|
demoghost *g,*p;
|
|
for (g = ghosts, p = NULL; g; g = g->next)
|
|
{
|
|
// Skip normal demo data.
|
|
ticcmd_t dummy;
|
|
ziptic_t zt;
|
|
|
|
if (g->done)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (g->version == 0x0001)
|
|
goto skipextradata;
|
|
|
|
while (true) // Get rid of extradata stuff
|
|
{
|
|
extradata_t extra;
|
|
g->p = G_ReadRawExtraData(&extra, g->p, g->version);
|
|
|
|
if (extra.playernum < MAXPLAYERS)
|
|
{
|
|
// We want to skip *any* player extradata because some demos have extradata for bogus players,
|
|
// but if there is tic data later for those players *then* we'll consider it invalid.
|
|
|
|
if (extra.flags & DXD_JOINDATA && extra.joindata.bot)
|
|
I_Error("Ghost is not a record attack ghost (bot JOINDATA)");
|
|
#ifdef DEVELOP
|
|
if (extra.flags & DXD_PLAYSTATE && extra.playstate != DXD_PST_PLAYING)
|
|
CONS_Alert(CONS_WARNING, "Ghost demo has non-playing playstate for player %d\n", extra.playernum);
|
|
#endif
|
|
}
|
|
else if (extra.playernum == DW_END)
|
|
break;
|
|
else if (extra.playernum < DW_EXTRASTUFF)
|
|
I_Error("Ghost is not a record attack ghost DXD (ziptic = %u)", extra.playernum); //@TODO lmao don't blow up like this
|
|
}
|
|
|
|
skipextradata:
|
|
g->p = G_ReadZipTic(&dummy, g->p, g->version);
|
|
if (g->version == 0x0001)
|
|
goto skipmultiplayer;
|
|
|
|
// Grab ghost data.
|
|
UINT8 pnum = READUINT8(g->p);
|
|
|
|
if (pnum == 0xFF)
|
|
goto skippedghosttic; // Didn't write ghost info this frame
|
|
else if (pnum != 0)
|
|
I_Error("Ghost is not a record attack ghost ZIPTIC"); //@TODO lmao don't blow up like this
|
|
|
|
skipmultiplayer:
|
|
g->p = G_ReadExtraZipTic(&zt, g->p, g->version, g->mo);
|
|
|
|
if (zt.flags & GZT_XYZ)
|
|
{
|
|
g->oldmo.x = zt.x;
|
|
g->oldmo.y = zt.y;
|
|
g->oldmo.z = zt.z;
|
|
}
|
|
else
|
|
{
|
|
if (zt.flags & GZT_MOMXY)
|
|
{
|
|
g->oldmo.momx = zt.x;
|
|
g->oldmo.momy = zt.y;
|
|
}
|
|
if (zt.flags & GZT_MOMZ)
|
|
g->oldmo.momz = zt.z;
|
|
g->oldmo.x += g->oldmo.momx;
|
|
g->oldmo.y += g->oldmo.momy;
|
|
g->oldmo.z += g->oldmo.momz;
|
|
}
|
|
if (zt.flags & GZT_ANGLE)
|
|
g->oldmo.angle = zt.angle;
|
|
if (zt.flags & GZT_FRAME)
|
|
g->oldmo.frame = zt.frame;
|
|
if (zt.flags & GZT_SPR2)
|
|
g->oldmo.sprite2 = zt.spr2;
|
|
|
|
// Update ghost
|
|
P_UnsetThingPosition(g->mo);
|
|
g->mo->x = g->oldmo.x;
|
|
g->mo->y = g->oldmo.y;
|
|
g->mo->z = g->oldmo.z;
|
|
P_SetThingPosition(g->mo);
|
|
g->mo->angle = g->oldmo.angle;
|
|
g->mo->frame = g->oldmo.frame | tr_trans30<<FF_TRANSSHIFT;
|
|
if (g->fadein)
|
|
{
|
|
g->mo->frame += (((--g->fadein)/6)<<FF_TRANSSHIFT); // this calc never exceeds 9 unless g->fadein is bad, and it's only set once, so...
|
|
g->mo->renderflags &= ~RF_DONTDRAW;
|
|
}
|
|
g->mo->sprite2 = g->oldmo.sprite2;
|
|
|
|
if (zt.flags & GZT_EXTRA)
|
|
{ // But wait, there's more!
|
|
if (zt.extraflags & EZT_COLOR)
|
|
{
|
|
g->color = zt.color;
|
|
switch(g->color)
|
|
{
|
|
default:
|
|
case GHC_RETURNSKIN:
|
|
R_ApplySkin(g->mo, g->oldmo.skin);
|
|
/* FALLTHRU */
|
|
case GHC_NORMAL: // Go back to skin color
|
|
g->mo->color = g->oldmo.color;
|
|
break;
|
|
// Handled below
|
|
case GHC_SUPER:
|
|
case GHC_INVINCIBLE:
|
|
break;
|
|
case GHC_FIREFLOWER: // Fireflower
|
|
g->mo->color = SKINCOLOR_WHITE;
|
|
break;
|
|
}
|
|
}
|
|
if (zt.extraflags & EZT_FLIP)
|
|
g->mo->eflags ^= MFE_VERTICALFLIP;
|
|
if (zt.extraflags & EZT_SCALE)
|
|
{
|
|
g->mo->destscale = zt.scale;
|
|
if (g->mo->destscale != g->mo->scale)
|
|
P_SetScale(g->mo, g->mo->destscale);
|
|
}
|
|
if (zt.extraflags & EZT_HIT)
|
|
{ // Spawn hit poofs for killing things!
|
|
for (UINT16 i = 0; i < zt.damagecount; i++)
|
|
{
|
|
zipticdamage_t *damage = &zt.damage[i];
|
|
// MAP15S03 has a broken EZT_HIT at leveltime 3455 which is all 0xff... so bounds check damage->type
|
|
if ((damage->type < nummobjtypes && !(mobjinfo[damage->type].flags & (MF_SHOOTABLE|MF_ENEMY|MF_MONITOR)))
|
|
|| damage->health != 0 || i >= 4) // only spawn for the first 4 hits per frame, to prevent ghosts from splode-spamming too bad.
|
|
continue;
|
|
mobj_t *poof = P_SpawnMobj(damage->x, damage->y, damage->z, MT_GHOST);
|
|
poof->angle = damage->angle;
|
|
poof->flags = MF_NOBLOCKMAP|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_NOGRAVITY; // make an ATTEMPT to curb crazy SOCs fucking stuff up...
|
|
poof->health = 0;
|
|
P_SetMobjStateNF(poof, S_XPLD1);
|
|
}
|
|
}
|
|
if (zt.extraflags & EZT_SPRITE)
|
|
g->mo->sprite = zt.sprite;
|
|
}
|
|
|
|
mobj_t *fmo = g->mo->tracer;
|
|
if (zt.flags & GZT_FOLLOW)
|
|
{ // Even more...
|
|
if (zt.followflags & FZT_SPAWNED)
|
|
{
|
|
if (fmo)
|
|
P_RemoveMobj(fmo);
|
|
P_SetTarget(&fmo, P_SpawnMobjFromMobj(g->mo, 0, 0, 0, MT_GHOST));
|
|
P_SetTarget(&fmo->tracer, g->mo);
|
|
fmo->tics = -1;
|
|
fmo->height = FixedMul(fmo->scale, zt.follow.height);
|
|
|
|
if (zt.followflags & FZT_LINKDRAW)
|
|
fmo->flags2 |= MF2_LINKDRAW;
|
|
|
|
if (zt.followflags & FZT_COLORIZED)
|
|
fmo->colorized = true;
|
|
|
|
if (zt.followflags & FZT_SKIN)
|
|
R_ApplySkin(fmo, &skins[zt.follow.skin]);
|
|
}
|
|
if (fmo)
|
|
{
|
|
if (zt.followflags & FZT_SCALE)
|
|
fmo->destscale = zt.follow.scale;
|
|
else
|
|
fmo->destscale = g->mo->destscale;
|
|
if (fmo->destscale != fmo->scale)
|
|
P_SetScale(fmo, fmo->destscale);
|
|
|
|
P_UnsetThingPosition(fmo);
|
|
fmo->x = g->mo->x + zt.follow.xofs;
|
|
fmo->y = g->mo->y + zt.follow.yofs;
|
|
fmo->z = g->mo->z + zt.follow.zofs;
|
|
P_SetThingPosition(fmo);
|
|
if (zt.followflags & FZT_SKIN)
|
|
fmo->sprite2 = zt.follow.sprite2;
|
|
else
|
|
fmo->sprite2 = 0;
|
|
fmo->sprite = zt.follow.sprite;
|
|
fmo->frame = zt.follow.frame | (g->mo->frame & FF_TRANSMASK);
|
|
fmo->angle = g->mo->angle;
|
|
fmo->color = zt.follow.color;
|
|
|
|
if (!(zt.followflags & FZT_SPAWNED))
|
|
{
|
|
if (zt.extraflags & EZT_FLIP)
|
|
{
|
|
fmo->flags2 ^= MF2_OBJECTFLIP;
|
|
fmo->eflags ^= MFE_VERTICALFLIP;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (fmo)
|
|
{
|
|
P_RemoveMobj(fmo);
|
|
P_SetTarget(&fmo, NULL);
|
|
}
|
|
|
|
G_FreeExtraZipTic(&zt);
|
|
|
|
skippedghosttic:
|
|
// Tick ghost colors (Super and Mario Invincibility flashing)
|
|
switch(g->color)
|
|
{
|
|
case GHC_SUPER: // Super (P_DoSuperStuff)
|
|
if (g->mo->skin)
|
|
{
|
|
skin_t *skin = (skin_t *)g->mo->skin;
|
|
g->mo->color = skin->supercolor;
|
|
}
|
|
else
|
|
g->mo->color = SKINCOLOR_SUPER1;
|
|
g->mo->color += abs( ( (signed)( (unsigned)leveltime >> 1 ) % 9) - 4);
|
|
break;
|
|
case GHC_INVINCIBLE: // Mario invincibility (P_CheckInvincibilityTimer)
|
|
g->mo->color = K_RainbowColor(leveltime); // Passes through all saturated colours
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (g->version != 0x0001 && READUINT8(g->p) != 0xFF) // Make sure there isn't other ghost data here.
|
|
I_Error("Ghost is not a record attack ghost GHOSTEND"); //@TODO lmao don't blow up like this
|
|
|
|
// Demo ends after ghost data.
|
|
if (*g->p == DEMOMARKER)
|
|
{
|
|
g->mo->momx = g->mo->momy = g->mo->momz = 0;
|
|
#if 1 // freeze frame (maybe more useful for time attackers)
|
|
g->mo->colorized = true;
|
|
if (fmo)
|
|
fmo->colorized = true;
|
|
#else // dissapearing act
|
|
g->mo->fuse = TICRATE;
|
|
if (follow)
|
|
follow->fuse = TICRATE;
|
|
#endif
|
|
g->done = true;
|
|
if (p)
|
|
{
|
|
p->next = g->next;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
p = g;
|
|
#undef follow
|
|
}
|
|
}
|
|
|
|
// Demo rewinding functions
|
|
typedef struct rewindinfo_s {
|
|
tic_t leveltime;
|
|
|
|
struct {
|
|
boolean ingame;
|
|
player_t player;
|
|
mobj_t mobj;
|
|
} playerinfo[MAXPLAYERS];
|
|
|
|
struct rewindinfo_s *prev;
|
|
} rewindinfo_t;
|
|
|
|
static tic_t currentrewindnum;
|
|
static rewindinfo_t *rewindhead = NULL; // Reverse chronological order
|
|
|
|
void G_InitDemoRewind(void)
|
|
{
|
|
CL_ClearRewinds();
|
|
|
|
while (rewindhead)
|
|
{
|
|
rewindinfo_t *p = rewindhead->prev;
|
|
Z_Free(rewindhead);
|
|
rewindhead = p;
|
|
}
|
|
|
|
currentrewindnum = 0;
|
|
}
|
|
|
|
void G_StoreRewindInfo(void)
|
|
{
|
|
static UINT8 timetolog = 8;
|
|
rewindinfo_t *info;
|
|
size_t i;
|
|
|
|
if (timetolog-- > 0)
|
|
return;
|
|
timetolog = 8;
|
|
|
|
info = Z_Calloc(sizeof(rewindinfo_t), PU_STATIC, NULL);
|
|
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (!playeringame[i] || players[i].spectator)
|
|
{
|
|
info->playerinfo[i].ingame = false;
|
|
continue;
|
|
}
|
|
|
|
info->playerinfo[i].ingame = true;
|
|
memcpy(&info->playerinfo[i].player, &players[i], sizeof(player_t));
|
|
if (players[i].mo)
|
|
memcpy(&info->playerinfo[i].mobj, players[i].mo, sizeof(mobj_t));
|
|
}
|
|
|
|
info->leveltime = leveltime;
|
|
info->prev = rewindhead;
|
|
rewindhead = info;
|
|
}
|
|
|
|
void G_PreviewRewind(tic_t previewtime)
|
|
{
|
|
SINT8 i;
|
|
//size_t j;
|
|
fixed_t tweenvalue = 0;
|
|
rewindinfo_t *info = rewindhead, *next_info = rewindhead;
|
|
|
|
if (!info)
|
|
return;
|
|
|
|
while (info->leveltime > previewtime && info->prev)
|
|
{
|
|
next_info = info;
|
|
info = info->prev;
|
|
}
|
|
if (info != next_info)
|
|
tweenvalue = FixedDiv(previewtime - info->leveltime, next_info->leveltime - info->leveltime);
|
|
|
|
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
if (!playeringame[i] || players[i].spectator)
|
|
{
|
|
if (info->playerinfo[i].player.mo)
|
|
{
|
|
//@TODO spawn temp object to act as a player display
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (!info->playerinfo[i].ingame || !info->playerinfo[i].player.mo)
|
|
{
|
|
if (players[i].mo)
|
|
players[i].mo->renderflags |= RF_DONTDRAW;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (!players[i].mo)
|
|
continue; //@TODO spawn temp object to act as a player display
|
|
|
|
players[i].mo->renderflags &= ~RF_DONTDRAW;
|
|
|
|
P_UnsetThingPosition(players[i].mo);
|
|
#define TWEEN(pr) info->playerinfo[i].mobj.pr + FixedMul((INT32) (next_info->playerinfo[i].mobj.pr - info->playerinfo[i].mobj.pr), tweenvalue)
|
|
players[i].mo->x = TWEEN(x);
|
|
players[i].mo->y = TWEEN(y);
|
|
players[i].mo->z = TWEEN(z);
|
|
players[i].mo->angle = TWEEN(angle);
|
|
#undef TWEEN
|
|
P_SetThingPosition(players[i].mo);
|
|
|
|
players[i].drawangle = info->playerinfo[i].player.drawangle + FixedMul((INT32) (next_info->playerinfo[i].player.drawangle - info->playerinfo[i].player.drawangle), tweenvalue);
|
|
|
|
players[i].mo->sprite = info->playerinfo[i].mobj.sprite;
|
|
players[i].mo->sprite2 = info->playerinfo[i].mobj.sprite2;
|
|
players[i].mo->frame = info->playerinfo[i].mobj.frame;
|
|
|
|
players[i].realtime = info->playerinfo[i].player.realtime;
|
|
// Genuinely CANNOT be fucked. I can redo lua and I can redo netsaves but I draw the line at this abysmal hack.
|
|
/*for (j = 0; j < NUMKARTSTUFF; j++)
|
|
players[i].kartstuff[j] = info->playerinfo[i].player.kartstuff[j];*/
|
|
}
|
|
|
|
for (i = splitscreen; i >= 0; i--)
|
|
P_ResetCamera(&players[displayplayers[i]], &camera[i]);
|
|
}
|
|
|
|
void G_ConfirmRewind(tic_t rewindtime)
|
|
{
|
|
SINT8 i;
|
|
tic_t j;
|
|
boolean oldsounddisabled = sound_disabled;
|
|
|
|
INT32 olddp1 = displayplayers[0], olddp2 = displayplayers[1], olddp3 = displayplayers[2], olddp4 = displayplayers[3];
|
|
UINT8 oldss = splitscreen;
|
|
|
|
CV_StealthSetValue(&cv_renderview, 0);
|
|
|
|
if (rewindtime <= starttime)
|
|
{
|
|
demo.rewinding = true; // this doesn't APPEAR to cause any misery, and it allows us to prevent running all the wipes again
|
|
G_DoPlayDemo(NULL); // Restart the current demo
|
|
}
|
|
else
|
|
{
|
|
rewind_t *rewind;
|
|
sound_disabled = true; // Prevent sound spam
|
|
demo.rewinding = true;
|
|
|
|
rewind = CL_RewindToTime(rewindtime);
|
|
|
|
if (rewind)
|
|
{
|
|
demobuf.p = demobuf.buffer + rewind->demopos;
|
|
memcpy(oldcmd, rewind->oldcmd, sizeof (oldcmd));
|
|
memcpy(oldghost, rewind->oldghost, sizeof (oldghost));
|
|
paused = false;
|
|
}
|
|
else
|
|
{
|
|
demo.rewinding = true;
|
|
G_DoPlayDemo(NULL); // Restart the current demo
|
|
}
|
|
}
|
|
|
|
for (j = 0; j < rewindtime && leveltime < rewindtime; j++)
|
|
{
|
|
G_Ticker((j % NEWTICRATERATIO) == 0);
|
|
}
|
|
|
|
demo.rewinding = false;
|
|
sound_disabled = oldsounddisabled; // Re-enable SFX
|
|
|
|
wipegamestate = gamestate; // No fading back in!
|
|
|
|
COM_BufInsertText("renderview on\n");
|
|
|
|
splitscreen = oldss;
|
|
displayplayers[0] = olddp1;
|
|
displayplayers[1] = olddp2;
|
|
displayplayers[2] = olddp3;
|
|
displayplayers[3] = olddp4;
|
|
R_ExecuteSetViewSize();
|
|
G_ResetViews(true);
|
|
|
|
for (i = splitscreen; i >= 0; i--)
|
|
P_ResetCamera(&players[displayplayers[i]], &camera[i]);
|
|
}
|
|
|
|
void G_ReadMetalTic(mobj_t *metal)
|
|
{
|
|
UINT8 ziptic;
|
|
UINT8 xziptic = 0;
|
|
|
|
if (!metal_p)
|
|
return;
|
|
|
|
if (!metal->health)
|
|
{
|
|
G_StopMetalDemo();
|
|
return;
|
|
}
|
|
|
|
switch (*metal_p)
|
|
{
|
|
case METALSNICE:
|
|
break;
|
|
case METALDEATH:
|
|
if (metal->tracer)
|
|
P_RemoveMobj(metal->tracer);
|
|
P_KillMobj(metal, NULL, NULL, DMG_NORMAL);
|
|
/* FALLTHRU */
|
|
case DEMOMARKER:
|
|
default:
|
|
// end of demo data stream
|
|
G_StopMetalDemo();
|
|
return;
|
|
}
|
|
metal_p++;
|
|
|
|
ziptic = READUINT8(metal_p);
|
|
|
|
// Read changes from the tic
|
|
if (ziptic & GZT_XYZ)
|
|
{
|
|
// make sure the values are read in the right order
|
|
oldmetal.x = READFIXED(metal_p);
|
|
oldmetal.y = READFIXED(metal_p);
|
|
oldmetal.z = READFIXED(metal_p);
|
|
P_MoveOrigin(metal, oldmetal.x, oldmetal.y, oldmetal.z);
|
|
oldmetal.x = metal->x;
|
|
oldmetal.y = metal->y;
|
|
oldmetal.z = metal->z;
|
|
}
|
|
else
|
|
{
|
|
if (ziptic & GZT_MOMXY)
|
|
{
|
|
oldmetal.momx = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p);
|
|
oldmetal.momy = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p);
|
|
}
|
|
if (ziptic & GZT_MOMZ)
|
|
oldmetal.momz = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p);
|
|
oldmetal.x += oldmetal.momx;
|
|
oldmetal.y += oldmetal.momy;
|
|
oldmetal.z += oldmetal.momz;
|
|
}
|
|
if (ziptic & GZT_ANGLE)
|
|
metal->angle = READUINT8(metal_p)<<24;
|
|
if (ziptic & GZT_FRAME)
|
|
oldmetal.frame = READUINT32(metal_p);
|
|
if (ziptic & GZT_SPR2)
|
|
oldmetal.sprite2 = READUINT8(metal_p);
|
|
|
|
// Set movement, position, and angle
|
|
// oldmetal contains where you're supposed to be.
|
|
metal->momx = oldmetal.momx;
|
|
metal->momy = oldmetal.momy;
|
|
metal->momz = oldmetal.momz;
|
|
P_UnsetThingPosition(metal);
|
|
metal->x = oldmetal.x;
|
|
metal->y = oldmetal.y;
|
|
metal->z = oldmetal.z;
|
|
P_SetThingPosition(metal);
|
|
metal->frame = oldmetal.frame;
|
|
metal->sprite2 = oldmetal.sprite2;
|
|
|
|
if (ziptic & GZT_EXTRA)
|
|
{ // But wait, there's more!
|
|
xziptic = READUINT8(metal_p);
|
|
if (xziptic & EZT_FLIP)
|
|
{
|
|
metal->eflags ^= MFE_VERTICALFLIP;
|
|
metal->flags2 ^= MF2_OBJECTFLIP;
|
|
}
|
|
if (xziptic & EZT_SCALE)
|
|
{
|
|
metal->destscale = READFIXED(metal_p);
|
|
if (metal->destscale != metal->scale)
|
|
P_SetScale(metal, metal->destscale);
|
|
}
|
|
if (xziptic & EZT_SPRITE)
|
|
metal->sprite = READUINT16(metal_p);
|
|
}
|
|
|
|
#define follow metal->tracer
|
|
if (ziptic & GZT_FOLLOW)
|
|
{ // Even more...
|
|
UINT8 followtic = READUINT8(metal_p);
|
|
fixed_t temp;
|
|
if (followtic & FZT_SPAWNED)
|
|
{
|
|
if (follow)
|
|
P_RemoveMobj(follow);
|
|
P_SetTarget(&follow, P_SpawnMobjFromMobj(metal, 0, 0, 0, MT_GHOST));
|
|
P_SetTarget(&follow->tracer, metal);
|
|
follow->tics = -1;
|
|
temp = READINT16(metal_p)<<FRACBITS;
|
|
follow->height = FixedMul(follow->scale, temp);
|
|
|
|
if (followtic & FZT_LINKDRAW)
|
|
follow->flags2 |= MF2_LINKDRAW;
|
|
|
|
if (followtic & FZT_COLORIZED)
|
|
follow->colorized = true;
|
|
|
|
if (followtic & FZT_SKIN)
|
|
R_ApplySkin(follow, &skins[READUINT8(metal_p)]);
|
|
}
|
|
if (follow)
|
|
{
|
|
if (followtic & FZT_SCALE)
|
|
follow->destscale = READFIXED(metal_p);
|
|
else
|
|
follow->destscale = metal->destscale;
|
|
if (follow->destscale != follow->scale)
|
|
P_SetScale(follow, follow->destscale);
|
|
|
|
P_UnsetThingPosition(follow);
|
|
temp = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p);
|
|
follow->x = metal->x + temp;
|
|
temp = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p);
|
|
follow->y = metal->y + temp;
|
|
temp = (metalversion < 0x000e) ? READINT16(metal_p)<<8 : READFIXED(metal_p);
|
|
follow->z = metal->z + temp;
|
|
P_SetThingPosition(follow);
|
|
if (followtic & FZT_SKIN)
|
|
follow->sprite2 = READUINT8(metal_p);
|
|
else
|
|
follow->sprite2 = 0;
|
|
follow->sprite = READUINT16(metal_p);
|
|
follow->frame = READUINT32(metal_p); // NOT & FF_FRAMEMASK here, so 32 bits
|
|
follow->angle = metal->angle;
|
|
follow->color = READUINT16(metal_p);
|
|
|
|
if (!(followtic & FZT_SPAWNED))
|
|
{
|
|
if (xziptic & EZT_FLIP)
|
|
{
|
|
follow->flags2 ^= MF2_OBJECTFLIP;
|
|
follow->eflags ^= MFE_VERTICALFLIP;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (follow)
|
|
{
|
|
P_RemoveMobj(follow);
|
|
P_SetTarget(&follow, NULL);
|
|
}
|
|
#undef follow
|
|
}
|
|
|
|
void G_WriteMetalTic(mobj_t *metal)
|
|
{
|
|
UINT8 ziptic = 0;
|
|
UINT8 *ziptic_p;
|
|
|
|
if (!demobuf.p) // demobuf.p will be NULL until the race start linedef executor is activated!
|
|
return;
|
|
|
|
WRITEUINT8(demobuf.p, METALSNICE);
|
|
ziptic_p = demobuf.p++; // the ziptic, written at the end of this function
|
|
|
|
#define MAXMOM (0xFFFF<<8)
|
|
|
|
// GZT_XYZ is only useful if you've moved 256 FRACUNITS or more in a single tic.
|
|
if (abs(metal->x-oldmetal.x) > MAXMOM
|
|
|| abs(metal->y-oldmetal.y) > MAXMOM
|
|
|| abs(metal->z-oldmetal.z) > MAXMOM)
|
|
{
|
|
oldmetal.x = metal->x;
|
|
oldmetal.y = metal->y;
|
|
oldmetal.z = metal->z;
|
|
ziptic |= GZT_XYZ;
|
|
WRITEFIXED(demobuf.p,oldmetal.x);
|
|
WRITEFIXED(demobuf.p,oldmetal.y);
|
|
WRITEFIXED(demobuf.p,oldmetal.z);
|
|
}
|
|
else
|
|
{
|
|
// For moving normally:
|
|
// Store movement as a fixed value
|
|
fixed_t momx = metal->x-oldmetal.x;
|
|
fixed_t momy = metal->y-oldmetal.y;
|
|
if (momx != oldmetal.momx
|
|
|| momy != oldmetal.momy)
|
|
{
|
|
oldmetal.momx = momx;
|
|
oldmetal.momy = momy;
|
|
ziptic |= GZT_MOMXY;
|
|
WRITEFIXED(demobuf.p,momx);
|
|
WRITEFIXED(demobuf.p,momy);
|
|
}
|
|
momx = metal->z-oldmetal.z;
|
|
if (momx != oldmetal.momz)
|
|
{
|
|
oldmetal.momz = momx;
|
|
ziptic |= GZT_MOMZ;
|
|
WRITEFIXED(demobuf.p,momx);
|
|
}
|
|
|
|
// This SHOULD set oldmetal.x/y/z to match metal->x/y/z
|
|
oldmetal.x += oldmetal.momx;
|
|
oldmetal.y += oldmetal.momy;
|
|
oldmetal.z += oldmetal.momz;
|
|
}
|
|
|
|
#undef MAXMOM
|
|
|
|
// Only store the 8 most relevant bits of angle
|
|
// because exact values aren't too easy to discern to begin with when only 8 angles have different sprites
|
|
// and it does not affect movement at all anyway.
|
|
if (metal->player && metal->player->drawangle>>24 != oldmetal.angle)
|
|
{
|
|
oldmetal.angle = metal->player->drawangle>>24;
|
|
ziptic |= GZT_ANGLE;
|
|
WRITEUINT8(demobuf.p,oldmetal.angle);
|
|
}
|
|
|
|
// Store the sprite frame.
|
|
if ((metal->frame & FF_FRAMEMASK) != oldmetal.frame)
|
|
{
|
|
oldmetal.frame = metal->frame; // NOT & FF_FRAMEMASK here, so 32 bits
|
|
ziptic |= GZT_FRAME;
|
|
WRITEUINT32(demobuf.p,oldmetal.frame);
|
|
}
|
|
|
|
if (metal->sprite == SPR_PLAY
|
|
&& metal->sprite2 != oldmetal.sprite2)
|
|
{
|
|
oldmetal.sprite2 = metal->sprite2;
|
|
ziptic |= GZT_SPR2;
|
|
WRITEUINT8(demobuf.p,oldmetal.sprite2);
|
|
}
|
|
|
|
// Check for sprite set changes
|
|
if (metal->sprite != oldmetal.sprite)
|
|
{
|
|
oldmetal.sprite = metal->sprite;
|
|
ghostext[0].flags |= EZT_SPRITE;
|
|
}
|
|
|
|
if (ghostext[0].flags & ~(EZT_COLOR|EZT_HIT)) // these two aren't handled by metal ever
|
|
{
|
|
ziptic |= GZT_EXTRA;
|
|
|
|
if (ghostext[0].scale == ghostext[0].lastscale)
|
|
ghostext[0].flags &= ~EZT_SCALE;
|
|
|
|
WRITEUINT8(demobuf.p,ghostext[0].flags);
|
|
if (ghostext[0].flags & EZT_SCALE)
|
|
{
|
|
WRITEFIXED(demobuf.p,ghostext[0].scale);
|
|
ghostext[0].lastscale = ghostext[0].scale;
|
|
}
|
|
if (ghostext[0].flags & EZT_SPRITE)
|
|
WRITEUINT16(demobuf.p,oldmetal.sprite);
|
|
ghostext[0].flags = 0;
|
|
}
|
|
|
|
if (metal->player && metal->player->followmobj && !(metal->player->followmobj->sprite == SPR_NULL || (metal->player->followmobj->renderflags & RF_DONTDRAW) == RF_DONTDRAW))
|
|
{
|
|
fixed_t temp;
|
|
UINT8 *followtic_p = demobuf.p++;
|
|
UINT8 followtic = 0;
|
|
|
|
ziptic |= GZT_FOLLOW;
|
|
|
|
if (metal->player->followmobj->skin)
|
|
followtic |= FZT_SKIN;
|
|
|
|
if (!(oldmetal.flags2 & MF2_AMBUSH))
|
|
{
|
|
followtic |= FZT_SPAWNED;
|
|
WRITEINT16(demobuf.p,metal->player->followmobj->info->height>>FRACBITS);
|
|
if (metal->player->followmobj->flags2 & MF2_LINKDRAW)
|
|
followtic |= FZT_LINKDRAW;
|
|
if (metal->player->followmobj->colorized)
|
|
followtic |= FZT_COLORIZED;
|
|
if (followtic & FZT_SKIN)
|
|
WRITEUINT8(demobuf.p,(UINT8)(((skin_t *)(metal->player->followmobj->skin))-skins));
|
|
oldmetal.flags2 |= MF2_AMBUSH;
|
|
}
|
|
|
|
if (metal->player->followmobj->scale != metal->scale)
|
|
{
|
|
followtic |= FZT_SCALE;
|
|
WRITEFIXED(demobuf.p,metal->player->followmobj->scale);
|
|
}
|
|
|
|
temp = metal->player->followmobj->x-metal->x;
|
|
WRITEFIXED(demobuf.p,temp);
|
|
temp = metal->player->followmobj->y-metal->y;
|
|
WRITEFIXED(demobuf.p,temp);
|
|
temp = metal->player->followmobj->z-metal->z;
|
|
WRITEFIXED(demobuf.p,temp);
|
|
if (followtic & FZT_SKIN)
|
|
WRITEUINT8(demobuf.p,metal->player->followmobj->sprite2);
|
|
WRITEUINT16(demobuf.p,metal->player->followmobj->sprite);
|
|
WRITEUINT32(demobuf.p,metal->player->followmobj->frame); // NOT & FF_FRAMEMASK here, so 32 bits
|
|
WRITEUINT16(demobuf.p,metal->player->followmobj->color);
|
|
|
|
*followtic_p = followtic;
|
|
}
|
|
else
|
|
oldmetal.flags2 &= ~MF2_AMBUSH;
|
|
|
|
*ziptic_p = ziptic;
|
|
|
|
// attention here for the ticcmd size!
|
|
// latest demos with mouse aiming byte in ticcmd
|
|
if (demobuf.p >= demobuf.end - 32)
|
|
{
|
|
G_StopMetalRecording(false); // no more space
|
|
return;
|
|
}
|
|
}
|
|
|
|
//
|
|
// G_RecordDemo
|
|
//
|
|
void G_RecordDemo(const char *name)
|
|
{
|
|
INT32 maxsize;
|
|
|
|
strcpy(demoname, name);
|
|
maxsize = 1024 * 1024 * cv_netdemosize.value;
|
|
|
|
P_SaveBufferAlloc(&demobuf, maxsize);
|
|
demobuf.p = NULL;
|
|
|
|
demo.recording = true;
|
|
demo.buffer = &demobuf;
|
|
|
|
/* FIXME: This whole file is in a wretched state. Take a
|
|
look at G_WriteAllGhostTics and G_WriteDemoTiccmd, they
|
|
write a lot of data. It's not realistic to refactor that
|
|
code in order to know exactly HOW MANY bytes it can write
|
|
out. So here's the deal. Reserve a decent block of memory
|
|
at the end of the buffer and never use it. Those bastard
|
|
functions will check if they overran the buffer, but it
|
|
should be safe enough because they'll think there's less
|
|
memory than there actually is and stop early. */
|
|
const size_t deadspace = 1024;
|
|
I_Assert(demobuf.size > deadspace);
|
|
demobuf.size -= deadspace;
|
|
demobuf.end -= deadspace;
|
|
}
|
|
|
|
void G_RecordMetal(void)
|
|
{
|
|
INT32 maxsize;
|
|
maxsize = 1024*1024;
|
|
if (M_CheckParm("-maxdemo") && M_IsNextParm())
|
|
maxsize = atoi(M_GetNextParm()) * 1024;
|
|
|
|
P_SaveBufferAlloc(&demobuf, maxsize);
|
|
demobuf.p = NULL;
|
|
|
|
metalrecording = true;
|
|
}
|
|
|
|
void G_BeginRecording(void)
|
|
{
|
|
UINT8 i, j, p;
|
|
char name[MAXCOLORNAME+1];
|
|
player_t *player = &players[consoleplayer];
|
|
|
|
char *filename;
|
|
UINT8 totalfiles;
|
|
UINT8 *m;
|
|
|
|
if (demobuf.p)
|
|
return;
|
|
memset(name,0,sizeof(name));
|
|
|
|
demobuf.p = demobuf.buffer;
|
|
demoflags = DF_GHOST|(multiplayer ? DF_MULTIPLAYER : (modeattacking<<DF_ATTACKSHIFT));
|
|
|
|
if (multiplayer && !netgame)
|
|
demoflags |= DF_NONETMP;
|
|
|
|
if (encoremode)
|
|
demoflags |= DF_ENCORE;
|
|
|
|
if (multiplayer)
|
|
demoflags |= DF_LUAVARS;
|
|
|
|
// Setup header.
|
|
memcpy(demobuf.p, DEMOHEADER, 12); demobuf.p += 12;
|
|
WRITEUINT8(demobuf.p,VERSION);
|
|
WRITEUINT8(demobuf.p,SUBVERSION);
|
|
WRITEUINT16(demobuf.p,DEMOVERSION);
|
|
|
|
// Full replay title
|
|
demobuf.p += 64;
|
|
{
|
|
char demotitlename[65];
|
|
char *title = G_BuildMapTitle(gamemap);
|
|
// Print to a separate temp buffer instead of demo.titlename, so we can use it in M_TextInputSetString
|
|
snprintf(demotitlename, sizeof(demotitlename), "%s - %s", title, modeattacking ? "Time Attack" : connectedservername);
|
|
|
|
// Init just in case it isn't initialized already
|
|
M_TextInputInit(&demo.titlenameinput, demo.titlename, sizeof(demo.titlename));
|
|
|
|
// This will indirectly assign to demo.titlename too
|
|
M_TextInputSetString(&demo.titlenameinput, demotitlename);
|
|
Z_Free(title);
|
|
}
|
|
|
|
// demo checksum
|
|
demobuf.p += sizeof(UINT64);
|
|
|
|
// game data
|
|
memcpy(demobuf.p, "PLAY", 4); demobuf.p += 4;
|
|
WRITESTRINGL(demobuf.p, mapheaderinfo[gamemap-1]->lumpname, MAXMAPLUMPNAME);
|
|
WRITEUINT64(demobuf.p, maphash);
|
|
|
|
WRITEUINT8(demobuf.p, demoflags);
|
|
WRITESTRINGL(demobuf.p, currentrecordpreset, MAXPRESETNAME+1);
|
|
WRITEUINT8(demobuf.p, currentrecordpresetversion);
|
|
WRITESTRINGL(demobuf.p, connectedservername, MAXSERVERNAME);
|
|
WRITESTRINGL(demobuf.p, connectedservercontact, MAXSERVERCONTACT);
|
|
WRITESTRINGL(demobuf.p, connectedserverdescription, MAXSERVERDESCRIPTION);
|
|
WRITEUINT8(demobuf.p, gametype & 0xFF);
|
|
WRITEUINT8(demobuf.p, numlaps);
|
|
|
|
// file list
|
|
m = demobuf.p;/* file count */
|
|
demobuf.p += 1;
|
|
|
|
totalfiles = 0;
|
|
for (i = NUMMAINWADS-1; ++i < numwadfiles; )
|
|
if (wadfiles[i]->important)
|
|
{
|
|
nameonly(( filename = va("%s", wadfiles[i]->filename) ));
|
|
WRITESTRINGL(demobuf.p, filename, MAX_WADPATH);
|
|
WRITEUINT64(demobuf.p, wadfiles[i]->hash);
|
|
WRITEUINT8(demobuf.p, !!wadfiles[i]->compatmode);
|
|
|
|
totalfiles++;
|
|
}
|
|
|
|
WRITEUINT8(m, totalfiles);
|
|
|
|
switch ((demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
|
|
{
|
|
case ATTACKING_NONE: // 0
|
|
break;
|
|
case ATTACKING_TIME: // 1
|
|
demotime_p = demobuf.p;
|
|
WRITEUINT32(demobuf.p,UINT32_MAX); // time
|
|
WRITEUINT32(demobuf.p,UINT32_MAX); // lap
|
|
break;
|
|
case ATTACKING_ITEMBREAK: // 2
|
|
demotime_p = demobuf.p;
|
|
WRITEUINT32(demobuf.p,UINT32_MAX); // time
|
|
break;
|
|
default: // 3
|
|
break;
|
|
}
|
|
|
|
WRITEUINT32(demobuf.p,P_GetInitSeed());
|
|
|
|
// Reserved for extrainfo location from start of file
|
|
demoinfo_p = demobuf.p;
|
|
WRITEUINT32(demobuf.p, 0);
|
|
|
|
// Save netvar data
|
|
CV_SaveDemoVars(&demobuf.p);
|
|
|
|
if ((demoflags & DF_GRANDPRIX))
|
|
{
|
|
WRITEUINT8(demobuf.p, grandprixinfo.gamespeed);
|
|
WRITEUINT8(demobuf.p, grandprixinfo.masterbots == true);
|
|
WRITEUINT8(demobuf.p, grandprixinfo.lunaticmode == true);
|
|
WRITEUINT8(demobuf.p, grandprixinfo.eventmode);
|
|
}
|
|
|
|
// Save "mapmusrng" used for altmusic selection
|
|
WRITEUINT8(demobuf.p, mapmusrng);
|
|
|
|
// Now store some info for each in-game player
|
|
|
|
// Lat' 12/05/19: Do note that for the first game you load, everything that gets saved here is total garbage;
|
|
// The name will always be Player <n>, the skin sonic, the color None and the follower 0. This is only correct on subsequent games.
|
|
// In the case of said first game, the skin and the likes are updated with Got_NameAndColor, which are then saved in extradata for the demo with DXD_SKIN in r_things.c for instance.
|
|
|
|
|
|
for (p = 0; p < MAXPLAYERS; p++) {
|
|
if (playeringame[p]) {
|
|
player = &players[p];
|
|
WRITEUINT8(demobuf.p, p);
|
|
|
|
i = 0;
|
|
if (player->spectator)
|
|
i |= DEMO_SPECTATOR;
|
|
if (player->pflags & PF_KICKSTARTACCEL)
|
|
i |= DEMO_KICKSTART;
|
|
if (player->pflags & PF_SHRINKME)
|
|
i |= DEMO_SHRINKME;
|
|
if (player->bot == true)
|
|
i |= DEMO_BOT;
|
|
WRITEUINT8(demobuf.p, i);
|
|
|
|
if (i & DEMO_BOT)
|
|
{
|
|
WRITEUINT8(demobuf.p, player->botvars.difficulty);
|
|
WRITEUINT8(demobuf.p, player->botvars.diffincrease); // needed to avoid having to duplicate logic
|
|
WRITEUINT8(demobuf.p, (UINT8)player->botvars.rival);
|
|
}
|
|
|
|
// Name
|
|
WRITESTRINGL(demobuf.p, player_names[p], 21+1);
|
|
|
|
// Skin
|
|
WRITESTRINGL(demobuf.p, skins[player->skin].name, 16+1);
|
|
|
|
// Color
|
|
WRITESTRINGL(demobuf.p, skincolors[player->skincolor].name, 16+1);
|
|
|
|
// Voice
|
|
WRITESTRINGL(demobuf.p, skins[player->skin].voices[player->voice_id].name, 32+1);
|
|
|
|
// Save follower's skin name
|
|
// PS: We must check for 'follower' to determine if the followerskin is valid. It's going to be 0 if we don't have a follower, but 0 is also absolutely a valid follower!
|
|
// Doesn't really matter if the follower mobj is valid so long as it exists in a way or another.
|
|
|
|
if (player->follower)
|
|
WRITESTRINGL(demobuf.p, followers[player->followerskin].skinname, 16+1);
|
|
else
|
|
WRITESTRINGL(demobuf.p, "None", 16+1); // Say we don't have one, then.
|
|
|
|
// Save follower's colour
|
|
// Not KartColor_Names because followercolor has extra values such as "Match"
|
|
WRITESTRINGL(demobuf.p, Followercolor_cons_t[(UINT16)(player->followercolor+2)].strvalue, 16+1);
|
|
|
|
// Score, since Kart uses this to determine where you start on the map
|
|
WRITEUINT32(demobuf.p, player->score);
|
|
|
|
// Power Levels
|
|
WRITEUINT16(demobuf.p, clientpowerlevels[p][gametype == GT_BATTLE ? PWRLV_BATTLE : PWRLV_RACE]);
|
|
|
|
// Kart speed and weight
|
|
WRITEUINT8(demobuf.p, skins[player->skin].kartspeed);
|
|
WRITEUINT8(demobuf.p, skins[player->skin].kartweight);
|
|
|
|
// And mobjtype_t is best with UINT32 too...
|
|
WRITEUINT32(demobuf.p, player->followitem);
|
|
|
|
for (j = 0; j < MAXAVAILABILITY; j++)
|
|
{
|
|
WRITEUINT8(demobuf.p, player->availabilities[j]);
|
|
}
|
|
}
|
|
}
|
|
|
|
WRITEUINT8(demobuf.p, 0xFF); // Denote the end of the player listing
|
|
|
|
// player lua vars, always saved even if empty
|
|
if (demoflags & DF_LUAVARS)
|
|
{
|
|
demobuf.write = true;
|
|
LUA_Sync(&demobuf, false, false);
|
|
}
|
|
|
|
memset(&oldcmd,0,sizeof(oldcmd));
|
|
memset(&oldghost,0,sizeof(oldghost));
|
|
memset(&ghostext,0,sizeof(ghostext));
|
|
|
|
for (i = 0; i < MAXPLAYERS; i++)
|
|
{
|
|
ghostext[i].lastcolor = ghostext[i].color = GHC_NORMAL;
|
|
ghostext[i].lastscale = ghostext[i].scale = FRACUNIT;
|
|
|
|
if (players[i].mo)
|
|
{
|
|
oldghost[i].x = players[i].mo->x;
|
|
oldghost[i].y = players[i].mo->y;
|
|
oldghost[i].z = players[i].mo->z;
|
|
oldghost[i].angle = players[i].mo->angle;
|
|
|
|
// preticker started us gravity flipped
|
|
if (players[i].mo->eflags & MFE_VERTICALFLIP)
|
|
ghostext[i].flags |= EZT_FLIP;
|
|
}
|
|
}
|
|
}
|
|
|
|
void G_BeginMetal(void)
|
|
{
|
|
mobj_t *mo = players[consoleplayer].mo;
|
|
|
|
#if 0
|
|
if (demobuf.p)
|
|
return;
|
|
#endif
|
|
|
|
demobuf.p = demobuf.buffer;
|
|
|
|
// Write header.
|
|
memcpy(demobuf.p, DEMOHEADER, 12); demobuf.p += 12;
|
|
WRITEUINT8(demobuf.p,VERSION);
|
|
WRITEUINT8(demobuf.p,SUBVERSION);
|
|
WRITEUINT16(demobuf.p,DEMOVERSION);
|
|
|
|
// demo checksum
|
|
demobuf.p += sizeof(UINT64);
|
|
|
|
memcpy(demobuf.p, "METL", 4); demobuf.p += 4;
|
|
|
|
memset(&ghostext,0,sizeof(ghostext));
|
|
ghostext[0].lastscale = ghostext[0].scale = FRACUNIT;
|
|
|
|
// Set up our memory.
|
|
memset(&oldmetal,0,sizeof(oldmetal));
|
|
oldmetal.x = mo->x;
|
|
oldmetal.y = mo->y;
|
|
oldmetal.z = mo->z;
|
|
oldmetal.angle = mo->angle>>24;
|
|
}
|
|
|
|
void G_WriteStanding(UINT8 ranking, char *name, INT32 skinnum, UINT16 color, UINT32 val)
|
|
{
|
|
if (demoinfo_p && *(UINT32 *)demoinfo_p == 0)
|
|
{
|
|
WRITEUINT8(demobuf.p, DEMOMARKER); // add the demo end marker
|
|
*(UINT32 *)demoinfo_p = demobuf.p - demobuf.buffer;
|
|
}
|
|
|
|
WRITEUINT8(demobuf.p, DW_STANDING);
|
|
WRITEUINT8(demobuf.p, ranking);
|
|
|
|
// Name
|
|
WRITESTRINGL(demobuf.p, name, 21+1);
|
|
|
|
// Skin
|
|
WRITESTRINGL(demobuf.p, skins[skinnum].name, 16+1);
|
|
|
|
// Color
|
|
WRITESTRINGL(demobuf.p, skincolors[color].name, 16+1);
|
|
|
|
// Score/time/whatever
|
|
WRITEUINT32(demobuf.p, val);
|
|
}
|
|
|
|
void G_SetDemoTime(UINT32 ptime, UINT32 plap)
|
|
{
|
|
if (!demo.recording || !demotime_p)
|
|
return;
|
|
if (demoflags & DF_TIMEATTACK)
|
|
{
|
|
WRITEUINT32(demotime_p, ptime);
|
|
WRITEUINT32(demotime_p, plap);
|
|
demotime_p = NULL;
|
|
}
|
|
else if (demoflags & DF_ITEMBREAKER)
|
|
{
|
|
WRITEUINT32(demotime_p, ptime);
|
|
(void)plap;
|
|
demotime_p = NULL;
|
|
}
|
|
}
|
|
|
|
static void G_LoadDemoExtraFiles(demoheader_t *header)
|
|
{
|
|
filestatus_t ncs;
|
|
boolean toomany = false;
|
|
boolean alreadyloaded;
|
|
UINT8 i, j;
|
|
|
|
for (i = 0; i < header->numfiles; i++)
|
|
{
|
|
demofile_t *file = &header->files[i];
|
|
if (!toomany)
|
|
{
|
|
alreadyloaded = false;
|
|
|
|
for (j = 0; j < numwadfiles; ++j)
|
|
{
|
|
if (file->filehash == wadfiles[j]->hash)
|
|
{
|
|
alreadyloaded = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (alreadyloaded)
|
|
continue;
|
|
|
|
if (numwadfiles >= MAX_WADFILES)
|
|
toomany = true;
|
|
else
|
|
ncs = findfile(file->filename, file->filehash, false);
|
|
|
|
if (toomany)
|
|
{
|
|
CONS_Alert(CONS_WARNING, M_GetText("Too many files loaded to add anymore for demo playback\n"));
|
|
if (!CON_Ready())
|
|
M_StartMessage(M_GetText("There are too many files loaded to add this demo's addons.\n\nDemo playback may desync.\n\nPress ESC\n"), NULL, MM_NOTHING);
|
|
}
|
|
else if (ncs != FS_FOUND)
|
|
{
|
|
if (ncs == FS_NOTFOUND)
|
|
CONS_Alert(CONS_NOTICE, M_GetText("You do not have a copy of %s\n"), file->filename);
|
|
else if (ncs == FS_BADHASH)
|
|
CONS_Alert(CONS_NOTICE, M_GetText("Checksum mismatch on %s\n"), file->filename);
|
|
else
|
|
CONS_Alert(CONS_NOTICE, M_GetText("Unknown error finding file %s\n"), file->filename);
|
|
|
|
if (!CON_Ready())
|
|
M_StartMessage(M_GetText("There were errors trying to add this demo's addons. Check the console for more information.\n\nDemo playback may desync.\n\nPress ESC\n"), NULL, MM_NOTHING);
|
|
}
|
|
else
|
|
{
|
|
P_PartialAddWadFile(file->filename, file->compatmode ? WC_ON : WC_OFF, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (P_PartialAddGetStage() >= 0)
|
|
P_MultiSetupWadFiles(true); // in case any partial adds were done
|
|
}
|
|
|
|
// G_CheckDemoExtraFiles: checks if our loaded WAD list matches the demo's.
|
|
// Enabling quick prevents filesystem checks to see if needed files are available to load.
|
|
static UINT8 G_CheckDemoExtraFiles(demoheader_t *header, boolean quick)
|
|
{
|
|
UINT8 filesloaded, nmusfilecount;
|
|
boolean toomany = false;
|
|
boolean alreadyloaded;
|
|
UINT8 i, j;
|
|
UINT8 error = 0;
|
|
|
|
filesloaded = 0;
|
|
for (i = 0; i < header->numfiles; ++i)
|
|
{
|
|
demofile_t *file = &header->files[i];
|
|
|
|
if (!toomany)
|
|
{
|
|
alreadyloaded = false;
|
|
nmusfilecount = 0;
|
|
|
|
for (j = 0; j < numwadfiles; ++j)
|
|
{
|
|
if (wadfiles[j]->important && j >= NUMMAINWADS)
|
|
nmusfilecount++;
|
|
else
|
|
continue;
|
|
|
|
if (file->filehash == wadfiles[j]->hash)
|
|
{
|
|
alreadyloaded = true;
|
|
|
|
if (i != nmusfilecount-1 && error < DFILE_ERROR_OUTOFORDER)
|
|
error |= DFILE_ERROR_OUTOFORDER;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (alreadyloaded)
|
|
{
|
|
filesloaded++;
|
|
continue;
|
|
}
|
|
|
|
if (numwadfiles >= MAX_WADFILES)
|
|
error = DFILE_ERROR_CANNOTLOAD;
|
|
else if (!quick && findfile(file->filename, file->filehash, false) != FS_FOUND)
|
|
error = DFILE_ERROR_CANNOTLOAD;
|
|
else if (error < DFILE_ERROR_INCOMPLETEOUTOFORDER)
|
|
error |= DFILE_ERROR_NOTLOADED;
|
|
} else
|
|
error = DFILE_ERROR_CANNOTLOAD;
|
|
}
|
|
|
|
// Get final file count
|
|
nmusfilecount = 0;
|
|
|
|
for (j = 0; j < numwadfiles; ++j)
|
|
if (wadfiles[j]->important && j >= NUMMAINWADS)
|
|
nmusfilecount++;
|
|
|
|
if (!error && filesloaded < nmusfilecount)
|
|
error = DFILE_ERROR_EXTRAFILES;
|
|
|
|
return error;
|
|
}
|
|
|
|
// Returns bitfield:
|
|
// 1 == new demo has lower time
|
|
// 2 == new demo has higher score
|
|
// 4 == new demo has higher rings
|
|
UINT8 G_CmpDemoTime(char *oldname, char *newname)
|
|
{
|
|
UINT8 *buffer;
|
|
UINT32 oldtime, newtime, oldlap, newlap;
|
|
size_t bufsize ATTRUNUSED;
|
|
UINT8 c;
|
|
UINT16 s ATTRUNUSED;
|
|
UINT8 aflags = 0;
|
|
boolean uselaps = false;
|
|
|
|
// load the new file
|
|
FIL_DefaultExtension(newname, ".lmp");
|
|
bufsize = FIL_ReadFile(newname, &buffer);
|
|
I_Assert(bufsize != 0);
|
|
|
|
// read demo header
|
|
CLEANUP(G_FreeDemoHeader) demoheader_t header;
|
|
headerstatus_e status = G_ReadDemoHeader(buffer, &header);
|
|
(void)status;
|
|
I_Assert(status == HEADER_OK && header.version == VERSION
|
|
&& header.subversion == SUBVERSION && header.demoversion == DEMOVERSION);
|
|
|
|
Z_Free(buffer);
|
|
|
|
aflags = header.demoflags & (DF_TIMEATTACK|DF_ITEMBREAKER);
|
|
I_Assert(aflags);
|
|
|
|
if (header.demoflags & DF_TIMEATTACK)
|
|
uselaps = true; // get around uninitalized error
|
|
|
|
newtime = header.attacking.time;
|
|
if (uselaps)
|
|
newlap = header.attacking.lap;
|
|
else
|
|
newlap = UINT32_MAX;
|
|
|
|
// load old file
|
|
FIL_DefaultExtension(oldname, ".lmp");
|
|
if (!FIL_ReadFile(oldname, &buffer))
|
|
{
|
|
CONS_Alert(CONS_ERROR, M_GetText("Failed to read file '%s'.\n"), oldname);
|
|
return UINT8_MAX;
|
|
}
|
|
|
|
// read demo header
|
|
CLEANUP(G_FreeDemoHeader) demoheader_t oldheader;
|
|
if (G_ReadDemoHeader(buffer, &oldheader) != HEADER_OK)
|
|
{
|
|
CONS_Alert(CONS_NOTICE, M_GetText("File '%s' invalid format. It will be overwritten.\n"), oldname);
|
|
Z_Free(buffer);
|
|
return UINT8_MAX;
|
|
}
|
|
|
|
Z_Free(buffer);
|
|
|
|
if (!(oldheader.demoflags & aflags))
|
|
{
|
|
CONS_Alert(CONS_NOTICE, M_GetText("File '%s' not from same game mode. It will be overwritten.\n"), oldname);
|
|
return UINT8_MAX;
|
|
}
|
|
|
|
oldtime = oldheader.attacking.time;
|
|
if (uselaps)
|
|
oldlap = oldheader.attacking.lap;
|
|
else
|
|
oldlap = 0;
|
|
|
|
c = 0;
|
|
|
|
if (uselaps)
|
|
{
|
|
if (newtime < oldtime
|
|
|| (newtime == oldtime && (newlap < oldlap)))
|
|
c |= 1; // Better time
|
|
if (newlap < oldlap
|
|
|| (newlap == oldlap && newtime < oldtime))
|
|
c |= 1<<1; // Better lap time
|
|
}
|
|
else
|
|
{
|
|
if (newtime < oldtime)
|
|
c |= 1; // Better time
|
|
}
|
|
|
|
return c;
|
|
}
|
|
|
|
void G_LoadDemoInfo(menudemo_t *pdemo)
|
|
{
|
|
CLEANUP(Z_Pfree) UINT8 *infobuffer = NULL;
|
|
const UINT8 *extrainfo_p;
|
|
|
|
if (!FIL_ReadFile(pdemo->filepath, &infobuffer))
|
|
{
|
|
CONS_Alert(CONS_ERROR, M_GetText("Failed to read file '%s'.\n"), pdemo->filepath);
|
|
pdemo->type = MD_INVALID;
|
|
sprintf(pdemo->title, "INVALID REPLAY");
|
|
return;
|
|
}
|
|
|
|
pdemo->type = MD_LOADED;
|
|
|
|
CLEANUP(G_FreeDemoHeader) demoheader_t header;
|
|
switch (G_ReadDemoHeader(infobuffer, &header))
|
|
{
|
|
case HEADER_OK:
|
|
break;
|
|
|
|
case HEADER_BADMAGIC:
|
|
CONS_Alert(CONS_ERROR, M_GetText("%s is not a SRB2Kart replay file.\n"), pdemo->filepath);
|
|
pdemo->type = MD_INVALID;
|
|
sprintf(pdemo->title, "INVALID REPLAY");
|
|
return;
|
|
|
|
case HEADER_BADVERSION:
|
|
CONS_Alert(CONS_ERROR, M_GetText("%s is an incompatible replay format and cannot be played.\n"), pdemo->filepath);
|
|
pdemo->type = MD_INVALID;
|
|
sprintf(pdemo->title, "INVALID REPLAY");
|
|
return;
|
|
|
|
case HEADER_BADFORMAT:
|
|
CONS_Alert(CONS_ERROR, M_GetText("%s is the wrong type of recording and cannot be played.\n"), pdemo->filepath);
|
|
pdemo->type = MD_INVALID;
|
|
sprintf(pdemo->title, "INVALID REPLAY");
|
|
return;
|
|
}
|
|
|
|
strlcpy(pdemo->title, header.demotitle, sizeof(pdemo->title));
|
|
|
|
if (header.version != VERSION || header.subversion != SUBVERSION)
|
|
pdemo->type = MD_OUTDATED;
|
|
|
|
pdemo->map = G_MapNumber(header.maptitle);
|
|
|
|
// temp?
|
|
if (!(header.demoflags & DF_MULTIPLAYER))
|
|
{
|
|
CONS_Alert(CONS_ERROR, M_GetText("%s is not a multiplayer replay and can't be listed on this menu fully yet.\n"), pdemo->filepath);
|
|
return;
|
|
}
|
|
|
|
pdemo->gametype = header.gametype;
|
|
pdemo->numlaps = header.numlaps;
|
|
|
|
pdemo->addonstatus = G_CheckDemoExtraFiles(&header, true);
|
|
|
|
extrainfo_p = infobuffer + header.extrainfo; // The extra UINT32 read is for a blank 4 bytes?
|
|
|
|
// Pared down version of CV_LoadNetVars to find the kart speed
|
|
pdemo->kartspeed = KARTSPEED_NORMAL; // Default to normal speed
|
|
for (UINT16 i = 0; i < header.numcvars; i++)
|
|
{
|
|
if (header.cvars[i].name == NULL)
|
|
continue;
|
|
if (fastcmp(header.cvars[i].name, cv_kartspeed.name))
|
|
{
|
|
UINT8 j;
|
|
for (j = 0; kartspeed_cons_t[j].strvalue; j++)
|
|
if (fasticmp(kartspeed_cons_t[j].strvalue, header.cvars[i].value))
|
|
pdemo->kartspeed = kartspeed_cons_t[j].value;
|
|
}
|
|
}
|
|
|
|
if (header.demoflags & DF_ENCORE)
|
|
pdemo->kartspeed |= DF_ENCORE;
|
|
|
|
// Read standings!
|
|
UINT16 count = 0;
|
|
|
|
while (READUINT8(extrainfo_p) == DW_STANDING) // Assume standings are always first in the extrainfo
|
|
{
|
|
INT32 i;
|
|
char temp[17];
|
|
|
|
pdemo->standings[count].ranking = READUINT8(extrainfo_p);
|
|
|
|
// Name
|
|
if (header.demoversion <= 0x000C)
|
|
{
|
|
READMEM(extrainfo_p, pdemo->standings[count].name, 16);
|
|
pdemo->standings[count].name[16] = '\0';
|
|
}
|
|
else
|
|
READSTRINGL(extrainfo_p, pdemo->standings[count].name, 21+1);
|
|
|
|
// Skin
|
|
if (header.demoversion <= 0x000C)
|
|
{
|
|
READMEM(extrainfo_p, temp, 16);
|
|
temp[16] = '\0';
|
|
}
|
|
else
|
|
READSTRINGL(extrainfo_p, temp, 16+1);
|
|
pdemo->standings[count].skin = UINT16_MAX;
|
|
for (i = 0; i < numskins; i++)
|
|
{
|
|
if (fasticmp(skins[i].name, temp))
|
|
{
|
|
pdemo->standings[count].skin = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Color
|
|
if (header.demoversion <= 0x000C)
|
|
{
|
|
READMEM(extrainfo_p, temp, 16);
|
|
temp[16] = '\0';
|
|
}
|
|
else
|
|
READSTRINGL(extrainfo_p, temp, 16+1);
|
|
pdemo->standings[count].color = SKINCOLOR_NONE;
|
|
for (i = 0; i < numskincolors; i++)
|
|
if (fasticmp(skincolors[i].name,temp)) // SRB2kart
|
|
{
|
|
pdemo->standings[count].color = i;
|
|
break;
|
|
}
|
|
|
|
// Score/time/whatever
|
|
pdemo->standings[count].timeorscore = READUINT32(extrainfo_p);
|
|
|
|
count++;
|
|
|
|
if (count >= MAXPLAYERS)
|
|
break; //@TODO still cycle through the rest of these if extra demo data is ever used
|
|
}
|
|
}
|
|
|
|
//
|
|
// G_PlayDemo
|
|
//
|
|
void G_DeferedPlayDemo(const char *name)
|
|
{
|
|
COM_BufAddText("playdemo \"");
|
|
COM_BufAddText(name);
|
|
COM_BufAddText("\" -addfiles\n");
|
|
}
|
|
|
|
//
|
|
// Start a demo from a .LMP file or from a wad resource
|
|
//
|
|
|
|
#define SKIPERRORS
|
|
|
|
static void PrintDemoError(const char (*msg)[1024])
|
|
{
|
|
if (*msg[0] == '\0')
|
|
return;
|
|
|
|
CONS_Alert(CONS_ERROR, "%s", *msg);
|
|
demo.playback = false;
|
|
demo.title = false;
|
|
if (!CON_Ready()) // In the console they'll just see the notice there! No point pulling them out.
|
|
M_StartMessage(*msg, NULL, MM_NOTHING);
|
|
}
|
|
|
|
static void FreeDemoBuffer(savebuffer_t **freebuffer)
|
|
{
|
|
if (*freebuffer != NULL)
|
|
P_SaveBufferFree(*freebuffer);
|
|
}
|
|
|
|
void G_DoPlayDemo(char *defdemoname)
|
|
{
|
|
UINT16 i, j;
|
|
lumpnum_t l;
|
|
CLEANUP(Z_Pfree) char *pdemoname = NULL;
|
|
const char *n;
|
|
CLEANUP(PrintDemoError) char msg[1024];
|
|
CLEANUP(FreeDemoBuffer) savebuffer_t *freebuffer = NULL;
|
|
|
|
#if defined(SKIPERRORS) && !defined(DEVELOP)
|
|
boolean skiperrors = false;
|
|
#endif
|
|
|
|
msg[0] = '\0';
|
|
M_ClearMenus(true);
|
|
G_InitDemoRewind();
|
|
gameaction = ga_nothing;
|
|
|
|
// No demo name means we're restarting the current demo
|
|
if (defdemoname == NULL)
|
|
{
|
|
demobuf.p = demobuf.buffer;
|
|
pdemoname = ZZ_Alloc(1); // Easier than adding checks for this everywhere it's freed
|
|
}
|
|
else
|
|
{
|
|
n = defdemoname+strlen(defdemoname);
|
|
while (*n != '/' && *n != '\\' && n != defdemoname)
|
|
n--;
|
|
if (n != defdemoname)
|
|
n++;
|
|
pdemoname = ZZ_Alloc(strlen(n)+1);
|
|
strcpy(pdemoname,n);
|
|
|
|
M_SetPlaybackMenuPointer();
|
|
|
|
// Internal if no extension, external if one exists
|
|
if (FIL_CheckExtension(defdemoname))
|
|
{
|
|
//FIL_DefaultExtension(defdemoname, ".lmp");
|
|
if (P_SaveBufferFromFile(&demobuf, defdemoname) == false)
|
|
{
|
|
snprintf(msg, 1024, M_GetText("Failed to read file '%s'.\n"), defdemoname);
|
|
return;
|
|
}
|
|
}
|
|
// load demo resource from WAD
|
|
else
|
|
{
|
|
if (n == defdemoname)
|
|
{
|
|
// Raw lump.
|
|
if ((l = W_CheckNumForName(defdemoname)) == LUMPERROR)
|
|
{
|
|
snprintf(msg, 1024, M_GetText("Failed to read lump '%s'.\n"), defdemoname);
|
|
return;
|
|
}
|
|
|
|
P_SaveBufferFromLump(&demobuf, l);
|
|
}
|
|
else
|
|
{
|
|
// vres GHOST_%u
|
|
virtres_t *vRes;
|
|
virtlump_t *vLump;
|
|
UINT16 mapnum;
|
|
size_t step = 0;
|
|
char mapname[MAXMAPLUMPNAME];
|
|
|
|
step = 0;
|
|
while (defdemoname+step < n-1)
|
|
{
|
|
mapname[step] = defdemoname[step];
|
|
step++;
|
|
}
|
|
mapname[step] = '\0';
|
|
|
|
mapnum = G_MapNumber(mapname);
|
|
if (mapnum >= nummapheaders || mapheaderinfo[mapnum]->lumpnum == LUMPERROR)
|
|
{
|
|
snprintf(msg, 1024, M_GetText("Failed to read lump '%s (couldn't find map %s)'.\n"), defdemoname, mapname);
|
|
return;
|
|
}
|
|
|
|
vRes = vres_GetMap(mapheaderinfo[mapnum]->lumpnum);
|
|
vLump = vres_Find(vRes, pdemoname);
|
|
|
|
if (vLump == NULL)
|
|
{
|
|
snprintf(msg, 1024, M_GetText("Failed to read lump '%s (couldn't find lump %s in %s)'.\n"), defdemoname, pdemoname, mapname);
|
|
return;
|
|
}
|
|
|
|
P_SaveBufferAlloc(&demobuf, vLump->size);
|
|
memcpy(demobuf.buffer, vLump->data, vLump->size);
|
|
|
|
vres_Free(vRes);
|
|
}
|
|
#if defined(SKIPERRORS) && !defined(DEVELOP)
|
|
skiperrors = true; // RR: Don't print warnings for staff ghosts, since they'll inevitably happen when we make bugfixes/changes...
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// read demo header
|
|
demo.playback = true;
|
|
demo.buffer = freebuffer = &demobuf;
|
|
|
|
CLEANUP(G_FreeDemoHeader) demoheader_t header;
|
|
switch (G_ReadDemoHeader(demobuf.p, &header))
|
|
{
|
|
case HEADER_OK:
|
|
demobuf.p += header.endofs;
|
|
break;
|
|
|
|
case HEADER_BADMAGIC:
|
|
snprintf(msg, 1024, M_GetText("%s is not a SRB2Kart replay file.\n"), pdemoname);
|
|
return;
|
|
|
|
case HEADER_BADVERSION:
|
|
snprintf(msg, 1024, M_GetText("%s is an incompatible replay format and cannot be played.\n"), pdemoname);
|
|
return;
|
|
|
|
case HEADER_BADFORMAT:
|
|
snprintf(msg, 1024, M_GetText("%s is the wrong type of recording and cannot be played.\n"), pdemoname);
|
|
return;
|
|
}
|
|
|
|
demo.version = header.demoversion;
|
|
|
|
// demo title
|
|
strlcpy(demo.titlename, header.demotitle, sizeof(demo.titlename));
|
|
|
|
if (demo.title) // Titledemos should always play and ought to always be compatible with whatever wadlist is running.
|
|
;//G_SkipDemoExtraFiles(&demobuf.p);
|
|
else if (demo.loadfiles)
|
|
{
|
|
G_LoadDemoExtraFiles(&header);
|
|
|
|
// Generate cvar names for missing Kart addon netids.
|
|
for (j = 0; j < header.numcvars; j++)
|
|
{
|
|
if (header.cvars[j].name == NULL && header.cvars[j].legacynetid)
|
|
{
|
|
consvar_t *legacycvar = CV_FindLegacyNetVar(header.cvars[j].legacynetid);
|
|
header.cvars[j].name = legacycvar ? strdup(legacycvar->name) : NULL;
|
|
}
|
|
}
|
|
}
|
|
else if (demo.ignorefiles)
|
|
;//G_SkipDemoExtraFiles(&demobuf.p);
|
|
else switch (G_CheckDemoExtraFiles(&header, false))
|
|
{
|
|
case DFILE_ERROR_NOTLOADED:
|
|
snprintf(msg, 1024,
|
|
M_GetText("Required files for this demo are not loaded.\n\nUse\n\"playdemo %s -addfiles\"\nto load them and play the demo.\n"),
|
|
pdemoname);
|
|
return;
|
|
|
|
case DFILE_ERROR_OUTOFORDER:
|
|
snprintf(msg, 1024,
|
|
M_GetText("Required files for this demo are loaded out of order.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"),
|
|
pdemoname);
|
|
return;
|
|
|
|
case DFILE_ERROR_INCOMPLETEOUTOFORDER:
|
|
snprintf(msg, 1024,
|
|
M_GetText("Required files for this demo are not loaded, and some are out of order.\n\nUse\n\"playdemo %s -addfiles\"\nto load needed files and play the demo.\n"),
|
|
pdemoname);
|
|
return;
|
|
|
|
case DFILE_ERROR_CANNOTLOAD:
|
|
snprintf(msg, 1024,
|
|
M_GetText("Required files for this demo cannot be loaded.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"),
|
|
pdemoname);
|
|
return;
|
|
|
|
case DFILE_ERROR_EXTRAFILES:
|
|
snprintf(msg, 1024,
|
|
M_GetText("You have additional files loaded beyond the demo's file list.\n\nUse\n\"playdemo %s -force\"\nto play the demo anyway.\n"),
|
|
pdemoname);
|
|
return;
|
|
}
|
|
|
|
// Moved here so these init properly.
|
|
gamemap = G_MapNumber(header.maptitle)+1;
|
|
|
|
demoflags = header.demoflags;
|
|
gametype = header.gametype;
|
|
G_SetGametype(gametype);
|
|
numlaps = header.numlaps;
|
|
|
|
// ...*map* not loaded?
|
|
if (!gamemap || (gamemap > nummapheaders) || !mapheaderinfo[gamemap-1] || mapheaderinfo[gamemap-1]->lumpnum == LUMPERROR)
|
|
{
|
|
snprintf(msg, 1024, M_GetText("%s features a course that is not currently loaded.\n"), pdemoname);
|
|
return;
|
|
}
|
|
|
|
// Sigh ... it's an empty demo.
|
|
if (header.empty)
|
|
{
|
|
snprintf(msg, 1024, M_GetText("%s contains no data to be played.\n"), pdemoname);
|
|
return;
|
|
}
|
|
|
|
// extra checks for RA replays
|
|
if (demoflags & DF_ATTACKMASK)
|
|
{
|
|
const char *reason = NULL;
|
|
if (header.numplayers != 1)
|
|
reason = "multiple players";
|
|
else if (header.playerdata[0].flags & DEMO_SPECTATOR)
|
|
reason = "spectators";
|
|
else if (header.playerdata[0].flags & DEMO_BOT)
|
|
reason = "bots";
|
|
|
|
if (reason)
|
|
{
|
|
snprintf(msg, 1024, M_GetText("%s is a Record Attack replay with %s, and is thus invalid.\n"), pdemoname, reason);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// no more error checks at this point, don't free demobuf
|
|
freebuffer = NULL;
|
|
|
|
modeattacking = (demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT;
|
|
multiplayer = !!(demoflags & DF_MULTIPLAYER);
|
|
|
|
demo.netgame = (multiplayer && !(demoflags & DF_NONETMP));
|
|
CON_ToggleOff();
|
|
|
|
hu_demotime = header.attacking.time;
|
|
hu_demolap = header.attacking.lap;
|
|
|
|
if (modeattacking != ATTACKING_TIME && modeattacking != ATTACKING_ITEMBREAK)
|
|
modeattacking = ATTACKING_NONE; // is this really necessary?
|
|
|
|
// net var data
|
|
CV_LoadDemoVars(&header);
|
|
|
|
if (modeattacking != ATTACKING_NONE)
|
|
{
|
|
strbuf_t *pv = G_GetRecordPresetVersionForDemo(&header);
|
|
if (pv)
|
|
{
|
|
const char *faulted = G_CheckPresetCvars(pv);
|
|
if (faulted && !G_CompatLevel(0x000C))
|
|
M_StartMessage(va("Demo cvar %s doesn't match the preset! This may be a mistake, or the demo is cheated!\n", faulted), NULL, MM_NOTHING);
|
|
}
|
|
else
|
|
CONS_Alert(CONS_WARNING, "Couldn't find record preset '%s' version %d\n", header.rapreset, header.rapresetversion);
|
|
}
|
|
|
|
memset(&grandprixinfo, 0, sizeof grandprixinfo);
|
|
if ((demoflags & DF_GRANDPRIX))
|
|
{
|
|
grandprixinfo.gp = true;
|
|
grandprixinfo.gamespeed = header.grandprix.gamespeed;
|
|
grandprixinfo.masterbots = header.grandprix.masterbots;
|
|
grandprixinfo.lunaticmode = header.grandprix.lunaticmode;
|
|
grandprixinfo.eventmode = header.grandprix.eventmode;
|
|
}
|
|
|
|
// Load "mapmusrng" used for altmusic selection
|
|
mapmusrng = header.mapmusrng;
|
|
|
|
memset(&oldcmd,0,sizeof(oldcmd));
|
|
memset(&oldghost,0,sizeof(oldghost));
|
|
memset(&ghostext,0,sizeof(ghostext));
|
|
|
|
#if defined(SKIPERRORS) && !defined(DEVELOP)
|
|
if ((VERSION != header.version || SUBVERSION != header.subversion) && !skiperrors)
|
|
#else
|
|
if (VERSION != header.version || SUBVERSION != header.subversion)
|
|
#endif
|
|
CONS_Alert(CONS_WARNING, M_GetText("Demo version does not match game version. Desyncs may occur.\n"));
|
|
|
|
// console warning messages
|
|
#if defined(SKIPERRORS) && !defined(DEVELOP)
|
|
demosynced = (!skiperrors);
|
|
#else
|
|
demosynced = true;
|
|
#endif
|
|
|
|
// didn't start recording right away.
|
|
demo.deferstart = false;
|
|
|
|
consoleplayer = 0;
|
|
memset(playeringame,0,sizeof(playeringame));
|
|
memset(displayplayers,0,sizeof(displayplayers));
|
|
memset(camera,0,sizeof(camera)); // reset freecam
|
|
|
|
// Load players that were in-game when the map started
|
|
for (UINT8 pnum = 0; pnum < header.numplayers; pnum++)
|
|
{
|
|
demoplayer_t *plr = &header.playerdata[pnum];
|
|
UINT8 p = plr->playernum;
|
|
player_t *player = &players[p];
|
|
|
|
|
|
if (!playeringame[displayplayers[0]] || players[displayplayers[0]].spectator)
|
|
displayplayers[0] = consoleplayer = serverplayer = p;
|
|
|
|
G_AddPlayer(p, p);
|
|
player->spectator = !!(plr->flags & DEMO_SPECTATOR);
|
|
|
|
if (plr->flags & DEMO_KICKSTART)
|
|
player->pflags |= PF_KICKSTARTACCEL;
|
|
else
|
|
player->pflags &= ~PF_KICKSTARTACCEL;
|
|
|
|
if (plr->flags & DEMO_SHRINKME)
|
|
player->pflags |= PF_SHRINKME;
|
|
else
|
|
player->pflags &= ~PF_SHRINKME;
|
|
|
|
K_UpdateShrinkCheat(player);
|
|
|
|
if ((player->bot = !!(plr->flags & DEMO_BOT)) == true)
|
|
{
|
|
player->botvars.difficulty = plr->bot.difficulty;
|
|
player->botvars.diffincrease = plr->bot.diffincrease; // needed to avoid having to duplicate logic
|
|
player->botvars.rival = plr->bot.rival;
|
|
}
|
|
|
|
// Name
|
|
strcpy(player_names[p], plr->name);
|
|
|
|
/*if (player->spectator)
|
|
{
|
|
CONS_Printf("player %s is spectator at start\n", player_names[p]);
|
|
}*/
|
|
|
|
// Skin
|
|
SetPlayerSkin(p, plr->skin);
|
|
|
|
// Color
|
|
for (i = 0; i < numskincolors; i++)
|
|
if (fasticmp(skincolors[i].name, plr->color)) // SRB2kart
|
|
{
|
|
player->skincolor = i;
|
|
break;
|
|
}
|
|
|
|
// Voice
|
|
// We'll only assign IDs, since the actual voice assignment happens during
|
|
// player spawn.
|
|
|
|
const INT32 vce = R_FindIDForVoice(&skins[player->skin], plr->voice);
|
|
|
|
if (vce == MAXSKINVOICES)
|
|
{
|
|
// The above function didn't find a voice, so we'll just fall back to
|
|
// the skin's voice.
|
|
player->voice_id = 0;
|
|
}
|
|
else
|
|
{
|
|
player->voice_id = (UINT16)vce;
|
|
}
|
|
|
|
// Follower
|
|
K_SetFollowerByName(p, plr->follower);
|
|
|
|
// Follower colour
|
|
for (i = 0; i < numskincolors +2; i++) // +2 because of Match and Opposite
|
|
if (fasticmp(Followercolor_cons_t[i].strvalue, plr->followercolor))
|
|
{
|
|
player->followercolor = i;
|
|
break;
|
|
}
|
|
|
|
// Score, since Kart uses this to determine where you start on the map
|
|
player->score = plr->score;
|
|
|
|
// Power Levels
|
|
clientpowerlevels[p][gametype == GT_BATTLE ? PWRLV_BATTLE : PWRLV_RACE] = plr->powerlevel;
|
|
|
|
// Kart stats (set later)
|
|
|
|
if (!fasticmp(skins[player->skin].name, plr->skin))
|
|
FindClosestSkinForStats(p, plr->kartspeed, plr->kartweight);
|
|
|
|
// Followitem
|
|
player->followitem = plr->followitem;
|
|
|
|
for (i = 0; i < MAXAVAILABILITY; i++)
|
|
{
|
|
player->availabilities[i] = plr->availabilities[i];
|
|
}
|
|
}
|
|
|
|
// end of player read (the 0xFF marker)
|
|
// so this is where we are to read our lua variables (if possible!)
|
|
if (demoflags & DF_LUAVARS) // again, used for compability, lua shit will be saved to replays regardless of if it's even been loaded
|
|
{
|
|
if (!gL) // No Lua state! ...I guess we'll just start one...
|
|
LUA_ClearState();
|
|
|
|
// No modeattacking check, DF_LUAVARS won't be present here.
|
|
demobuf.write = false;
|
|
LUA_Sync(&demobuf, false, G_CompatLevel(0x0002));
|
|
}
|
|
|
|
splitscreen = 0;
|
|
|
|
if (demo.title)
|
|
{
|
|
splitscreen = M_RandomKey(6)-1;
|
|
splitscreen = min(min(3, header.numplayers-1), splitscreen); // Bias toward 1p and 4p views
|
|
|
|
for (i = 0; i <= splitscreen; i++)
|
|
G_ResetView(i+1, header.playerdata[M_RandomKey(header.numplayers)].playernum, false);
|
|
}
|
|
|
|
R_ExecuteSetViewSize();
|
|
|
|
P_SetRandSeed(header.randseed);
|
|
G_InitNew(demoflags & DF_ENCORE, gamemap, true, true, false); // Doesn't matter whether you reset or not here, given changes to resetplayer.
|
|
|
|
// Setup server name, contact and description.
|
|
CopyCaretColors(connectedservername, header.servername, MAXSERVERNAME);
|
|
CopyCaretColors(connectedservercontact, header.servercontact, MAXSERVERCONTACT);
|
|
strncpy(connectedserverdescription, header.serverdescription, MAXSERVERDESCRIPTION);
|
|
|
|
for (i = 0; i < header.numplayers; i++)
|
|
{
|
|
// oldghost init doesn't work here, players aren't immediately spawned anymore
|
|
|
|
// Set saved attribute values
|
|
// No cheat checking here, because even if they ARE wrong...
|
|
// it would only break the replay if we clipped them.
|
|
demoplayer_t *plr = &header.playerdata[i];
|
|
players[plr->playernum].kartspeed = plr->kartspeed;
|
|
players[plr->playernum].kartweight = plr->kartweight;
|
|
}
|
|
|
|
demo.deferstart = true;
|
|
}
|
|
|
|
void G_SetupDemoPlayer(INT32 i)
|
|
{
|
|
players[i].mo->color = players[i].skincolor;
|
|
oldghost[i].x = players[i].mo->x;
|
|
oldghost[i].y = players[i].mo->y;
|
|
oldghost[i].z = players[i].mo->z;
|
|
}
|
|
|
|
void G_AddGhost(char *defdemoname)
|
|
{
|
|
INT32 i;
|
|
lumpnum_t l;
|
|
CLEANUP(Z_Pfree) char *pdemoname = NULL;
|
|
const char *n;
|
|
UINT64 demohash;
|
|
demoghost *gh;
|
|
UINT8 flags;
|
|
CLEANUP(Z_Pfree) UINT8 *buffer = NULL;
|
|
mapthing_t *mthing;
|
|
skin_t *ghskin = &skins[0];
|
|
|
|
n = defdemoname+strlen(defdemoname);
|
|
while (*n != '/' && *n != '\\' && n != defdemoname)
|
|
n--;
|
|
if (n != defdemoname)
|
|
n++;
|
|
pdemoname = ZZ_Alloc(strlen(n)+1);
|
|
strcpy(pdemoname,n);
|
|
|
|
// Internal if no extension, external if one exists
|
|
if (FIL_CheckExtension(defdemoname))
|
|
{
|
|
//FIL_DefaultExtension(defdemoname, ".lmp");
|
|
if (!FIL_ReadFileTag(defdemoname, &buffer, PU_LEVEL))
|
|
{
|
|
CONS_Alert(CONS_ERROR, M_GetText("Failed to read file '%s'.\n"), defdemoname);
|
|
return;
|
|
}
|
|
}
|
|
// load demo resource from WAD
|
|
else if ((l = W_CheckNumForName(defdemoname)) == LUMPERROR)
|
|
{
|
|
CONS_Alert(CONS_ERROR, M_GetText("Failed to read lump '%s'.\n"), defdemoname);
|
|
return;
|
|
}
|
|
else // it's an internal demo
|
|
buffer = W_CacheLumpNum(l, PU_LEVEL);
|
|
|
|
CLEANUP(G_FreeDemoHeader) demoheader_t header;
|
|
switch (G_ReadDemoHeader(buffer, &header))
|
|
{
|
|
case HEADER_OK:
|
|
break;
|
|
|
|
case HEADER_BADMAGIC:
|
|
CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Not a SRB2 replay.\n"), pdemoname);
|
|
return;
|
|
|
|
case HEADER_BADVERSION:
|
|
CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Demo version incompatible.\n"), pdemoname);
|
|
return;
|
|
|
|
case HEADER_BADFORMAT:
|
|
CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Demo format unacceptable.\n"), pdemoname);
|
|
return;
|
|
}
|
|
|
|
demohash = header.demochecksum;
|
|
|
|
for (gh = ghosts; gh; gh = gh->next)
|
|
if (demohash == gh->checksum) // another ghost in the game already has this checksum?
|
|
{ // Don't add another one, then!
|
|
CONS_Debug(DBG_SETUP, "Rejecting duplicate ghost %s (hash was matched)\n", pdemoname);
|
|
return;
|
|
}
|
|
|
|
|
|
flags = header.demoflags;
|
|
|
|
if (!(flags & DF_GHOST))
|
|
{
|
|
CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: No ghost data in this demo.\n"), pdemoname);
|
|
return;
|
|
}
|
|
|
|
if (flags & DF_LUAVARS) // can't be arsed to add support for grinding away ported lua material
|
|
{
|
|
CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Replay data contains luavars, cannot continue.\n"), pdemoname);
|
|
return;
|
|
}
|
|
|
|
if (header.empty)
|
|
{
|
|
CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Replay is empty.\n"), pdemoname);
|
|
return;
|
|
}
|
|
|
|
// Check record preset.
|
|
{
|
|
strbuf_t *ghostpre = G_GetRecordPresetVersionForDemo(&header);
|
|
strbuf_t *ourpre = G_GetRecordPresetVersion(currentrecordpreset, currentrecordpresetversion);
|
|
|
|
if (ghostpre == NULL || !G_CompareRecordPresetVersions(ghostpre, ourpre))
|
|
return;
|
|
}
|
|
|
|
demoplayer_t *plr = &header.playerdata[0];
|
|
|
|
// any invalidating flags?
|
|
if ((plr->flags & (DEMO_SPECTATOR|DEMO_BOT)) != 0)
|
|
{
|
|
CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Invalid player slot (spectator/bot)\n"), defdemoname);
|
|
return;
|
|
}
|
|
|
|
for (i = 0; i < numskins; i++)
|
|
if (fasticmp(skins[i].name, plr->skin))
|
|
{
|
|
ghskin = &skins[i];
|
|
break;
|
|
}
|
|
|
|
if (i == numskins)
|
|
{
|
|
if (plr->kartspeed != UINT8_MAX && plr->kartweight != UINT8_MAX)
|
|
ghskin = &skins[GetSkinNumClosestToStats(plr->kartspeed, plr->kartweight)];
|
|
|
|
CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Invalid character. Falling back to %s.\n"), pdemoname, ghskin->name);
|
|
}
|
|
|
|
gh = Z_Calloc(sizeof(demoghost), PU_LEVEL, NULL);
|
|
gh->next = ghosts;
|
|
gh->buffer = buffer;
|
|
gh->checksum = demohash;
|
|
gh->p = buffer + header.endofs;
|
|
buffer = NULL; // buffer can't be freed now!
|
|
|
|
ghosts = gh;
|
|
|
|
gh->version = header.demoversion;
|
|
mthing = playerstarts[0] ? playerstarts[0] : deathmatchstarts[0]; // todo not correct but out of scope
|
|
I_Assert(mthing);
|
|
{ // A bit more complex than P_SpawnPlayer because ghosts aren't solid and won't just push themselves out of the ceiling.
|
|
fixed_t z,f,c;
|
|
fixed_t offset = mthing->z << FRACBITS;
|
|
gh->mo = P_SpawnMobj(mthing->x << FRACBITS, mthing->y << FRACBITS, 0, MT_GHOST);
|
|
gh->mo->angle = FixedAngle(mthing->angle << FRACBITS);
|
|
f = gh->mo->floorz;
|
|
c = gh->mo->ceilingz - mobjinfo[MT_PLAYER].height;
|
|
if (!!(mthing->args[0]) ^ !!(mthing->options & MTF_OBJECTFLIP))
|
|
{
|
|
z = c - offset;
|
|
if (z < f)
|
|
z = f;
|
|
}
|
|
else
|
|
{
|
|
z = f + offset;
|
|
if (z > c)
|
|
z = c;
|
|
}
|
|
gh->mo->z = z;
|
|
}
|
|
|
|
gh->oldmo.x = gh->mo->x;
|
|
gh->oldmo.y = gh->mo->y;
|
|
gh->oldmo.z = gh->mo->z;
|
|
|
|
gh->mo->state = states + S_KART_STILL;
|
|
gh->mo->sprite = gh->mo->state->sprite;
|
|
gh->mo->sprite2 = (gh->mo->state->frame & FF_FRAMEMASK);
|
|
//gh->mo->frame = tr_trans30<<FF_TRANSSHIFT;
|
|
gh->mo->renderflags |= RF_DONTDRAW;
|
|
gh->fadein = (9-3)*6; // fade from invisible to trans30 over as close to 35 tics as possible
|
|
gh->mo->tics = -1;
|
|
|
|
// Set skin
|
|
R_ApplySkin(&gh->oldmo, ghskin);
|
|
R_ApplySkin(gh->mo, gh->oldmo.skin);
|
|
|
|
// Set color
|
|
gh->mo->color = ((skin_t*)gh->mo->skin)->prefcolor;
|
|
for (i = 0; i < numskincolors; i++)
|
|
if (fasticmp(skincolors[i].name, plr->color))
|
|
{
|
|
gh->mo->color = (UINT16)i;
|
|
break;
|
|
}
|
|
gh->oldmo.color = gh->mo->color;
|
|
|
|
CONS_Printf(M_GetText("Added ghost %s from %s\n"), plr->name, pdemoname);
|
|
}
|
|
|
|
// Clean up all ghosts
|
|
void G_FreeGhosts(void)
|
|
{
|
|
while (ghosts)
|
|
{
|
|
demoghost *next = ghosts->next;
|
|
Z_Free(ghosts);
|
|
ghosts = next;
|
|
}
|
|
ghosts = NULL;
|
|
}
|
|
|
|
// A simplified version of G_AddGhost...
|
|
void G_UpdateStaffGhostName(lumpnum_t l)
|
|
{
|
|
CLEANUP(Z_Pfree) UINT8 *buffer = W_CacheLumpNum(l, PU_CACHE);
|
|
|
|
CLEANUP(G_FreeDemoHeader) demoheader_t header;
|
|
if (G_ReadDemoHeader(buffer, &header) != HEADER_OK)
|
|
return;
|
|
|
|
if (!(header.demoflags & DF_GHOST))
|
|
return; // we don't NEED to do it here, but whatever
|
|
|
|
if (header.numplayers == 0 || header.playerdata[0].playernum != 0)
|
|
return;
|
|
if (header.playerdata[0].flags & (DEMO_SPECTATOR|DEMO_BOT))
|
|
return;
|
|
|
|
strcpy(dummystaffname, header.playerdata[0].name);
|
|
}
|
|
|
|
//
|
|
// G_TimeDemo
|
|
// NOTE: name is a full filename for external demos
|
|
//
|
|
static INT32 restorecv_vidwait;
|
|
|
|
void G_TimeDemo(const char *name)
|
|
{
|
|
nodrawers = M_CheckParm("-nodraw");
|
|
noblit = M_CheckParm("-noblit");
|
|
restorecv_vidwait = cv_vidwait.value;
|
|
if (cv_vidwait.value)
|
|
CV_Set(&cv_vidwait, "0");
|
|
demo.timing = true;
|
|
singletics = true;
|
|
framecount = 0;
|
|
demostarttime = I_GetTime();
|
|
G_DeferedPlayDemo(name);
|
|
}
|
|
|
|
void G_DoPlayMetal(void)
|
|
{
|
|
lumpnum_t l;
|
|
mobj_t *mo = NULL;
|
|
thinker_t *th;
|
|
|
|
// it's an internal demo
|
|
// TODO: Use map header to determine lump name
|
|
if ((l = W_CheckNumForName(va("%sMS",G_BuildMapName(gamemap)))) == LUMPERROR)
|
|
{
|
|
CONS_Alert(CONS_WARNING, M_GetText("No bot recording for this map.\n"));
|
|
return;
|
|
}
|
|
else
|
|
metalbuffer = metal_p = W_CacheLumpNum(l, PU_STATIC);
|
|
|
|
// find metal sonic
|
|
for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
|
|
{
|
|
if (th->function == (actionf_p1)P_RemoveThinkerDelayed)
|
|
continue;
|
|
|
|
mo = (mobj_t *)th;
|
|
if (mo->type != MT_METALSONIC_RACE)
|
|
continue;
|
|
|
|
break;
|
|
}
|
|
if (th == &thlist[THINK_MOBJ])
|
|
{
|
|
CONS_Alert(CONS_ERROR, M_GetText("Failed to find bot entity.\n"));
|
|
Z_Free(metalbuffer);
|
|
return;
|
|
}
|
|
|
|
// read demo header
|
|
metal_p += 12; // DEMOHEADER
|
|
metal_p++; // VERSION
|
|
metal_p++; // SUBVERSION
|
|
metalversion = READUINT16(metal_p);
|
|
switch(metalversion)
|
|
{
|
|
case DEMOVERSION: // latest always supported
|
|
break;
|
|
// too old, cannot support.
|
|
default:
|
|
CONS_Alert(CONS_WARNING, M_GetText("Failed to load bot recording for this map, format version incompatible.\n"));
|
|
Z_Free(metalbuffer);
|
|
return;
|
|
}
|
|
metal_p += sizeof(UINT64); // demo checksum
|
|
if (memcmp(metal_p, "METL", 4))
|
|
{
|
|
CONS_Alert(CONS_WARNING, M_GetText("Failed to load bot recording for this map, wasn't recorded in Metal format.\n"));
|
|
Z_Free(metalbuffer);
|
|
return;
|
|
} metal_p += 4; // "METL"
|
|
|
|
// read initial tic
|
|
memset(&oldmetal,0,sizeof(oldmetal));
|
|
oldmetal.x = mo->x;
|
|
oldmetal.y = mo->y;
|
|
oldmetal.z = mo->z;
|
|
metalplayback = mo;
|
|
}
|
|
|
|
void G_DoneLevelLoad(void)
|
|
{
|
|
CONS_Printf(M_GetText("Loaded level in %f sec\n"), (double)(I_GetTime() - demostarttime) / TICRATE);
|
|
framecount = 0;
|
|
demostarttime = I_GetTime();
|
|
}
|
|
|
|
/*
|
|
===================
|
|
=
|
|
= G_CheckDemoStatus
|
|
=
|
|
= Called after a death or level completion to allow demos to be cleaned up
|
|
= Returns true if a new demo loop action will take place
|
|
===================
|
|
*/
|
|
|
|
// Writes the demo's checksum.
|
|
static void WriteDemoChecksum(void)
|
|
{
|
|
UINT8 *p = demobuf.buffer+16; // checksum position
|
|
WRITEUINT64(p, HASH64(p+16, demobuf.p - (p+16)));
|
|
}
|
|
|
|
// Stops metal sonic's demo. Separate from other functions because metal + replays can coexist
|
|
void G_StopMetalDemo(void)
|
|
{
|
|
// Metal Sonic finishing doesn't end the game, dammit.
|
|
Z_Free(metalbuffer);
|
|
metalbuffer = NULL;
|
|
metalplayback = NULL;
|
|
metal_p = NULL;
|
|
}
|
|
|
|
// Stops metal sonic recording.
|
|
ATTRNORETURN void FUNCNORETURN G_StopMetalRecording(boolean kill)
|
|
{
|
|
boolean saved = false;
|
|
if (demobuf.p)
|
|
{
|
|
WRITEUINT8(demobuf.p, (kill) ? METALDEATH : DEMOMARKER); // add the demo end (or metal death) marker
|
|
WriteDemoChecksum();
|
|
saved = FIL_WriteFile(va("%sMS.LMP", G_BuildMapName(gamemap)), demobuf.buffer, demobuf.p - demobuf.buffer); // finally output the file.
|
|
}
|
|
P_SaveBufferFree(&demobuf);
|
|
metalrecording = false;
|
|
if (saved)
|
|
I_Error("Saved to %sMS.LMP", G_BuildMapName(gamemap));
|
|
I_Error("Failed to save demo!");
|
|
}
|
|
|
|
// Stops timing a demo.
|
|
static void G_StopTimingDemo(void)
|
|
{
|
|
INT32 demotime;
|
|
double f1, f2;
|
|
demotime = I_GetTime() - demostarttime;
|
|
if (!demotime)
|
|
return;
|
|
G_StopDemo();
|
|
demo.timing = false;
|
|
f1 = (double)demotime;
|
|
f2 = (double)framecount*TICRATE;
|
|
|
|
CONS_Printf(M_GetText("timed %u gametics in %d realtics - %u frames\n%f seconds, %f avg fps\n"),
|
|
leveltime,demotime,(UINT32)framecount,f1/TICRATE,f2/f1);
|
|
|
|
// CSV-readable timedemo results, for external parsing
|
|
if (timedemo_csv)
|
|
{
|
|
FILE *f;
|
|
const char *csvpath = va("%s"PATHSEP"%s", srb2home, "timedemo.csv");
|
|
const char *header = "id,demoname,seconds,avgfps,leveltime,demotime,framecount,ticrate,rendermode,vidmode,vidwidth,vidheight,procbits\n";
|
|
const char *rowformat = "\"%s\",\"%s\",%f,%f,%u,%d,%u,%u,%u,%u,%u,%u,%u\n";
|
|
boolean headerrow = !FIL_FileExists(csvpath);
|
|
UINT8 procbits = 0;
|
|
|
|
// Bitness
|
|
if (sizeof(void*) == 4)
|
|
procbits = 32;
|
|
else if (sizeof(void*) == 8)
|
|
procbits = 64;
|
|
|
|
f = fopen(csvpath, "a+");
|
|
|
|
if (f)
|
|
{
|
|
if (headerrow)
|
|
fputs(header, f);
|
|
fprintf(f, rowformat,
|
|
timedemo_csv_id,timedemo_name,f1/TICRATE,f2/f1,leveltime,demotime,(UINT32)framecount,TICRATE,rendermode,vid.modenum,vid.width,vid.height,procbits);
|
|
fclose(f);
|
|
CONS_Printf("Timedemo results saved to '%s'\n", csvpath);
|
|
}
|
|
else
|
|
{
|
|
// Just print the CSV output to console
|
|
CON_LogMessage(header);
|
|
CONS_Printf(rowformat,
|
|
timedemo_csv_id,timedemo_name,f1/TICRATE,f2/f1,leveltime,demotime,(UINT32)framecount,TICRATE,rendermode,vid.modenum,vid.width,vid.height,procbits);
|
|
}
|
|
}
|
|
|
|
if (restorecv_vidwait != cv_vidwait.value)
|
|
CV_SetValue(&cv_vidwait, restorecv_vidwait);
|
|
if (timedemo_quit)
|
|
COM_ImmedExecute("quit");
|
|
else
|
|
D_StartTitle();
|
|
}
|
|
|
|
// reset engine variable set for the demos
|
|
// called from stopdemo command, map command, and g_checkdemoStatus.
|
|
void G_StopDemo(void)
|
|
{
|
|
P_SaveBufferFree(&demobuf);
|
|
demo.playback = false;
|
|
if (demo.title)
|
|
modeattacking = false;
|
|
demo.title = false;
|
|
demo.timing = false;
|
|
singletics = false;
|
|
|
|
{
|
|
UINT8 i;
|
|
for (i = 0; i < MAXSPLITSCREENPLAYERS; ++i)
|
|
{
|
|
camera[i].freecam = false;
|
|
}
|
|
}
|
|
|
|
if (gamestate == GS_INTERMISSION)
|
|
Y_EndIntermission(); // cleanup
|
|
|
|
if (gamestate == GS_VOTING)
|
|
Y_EndVote();
|
|
|
|
G_SetGamestate(GS_NULL);
|
|
wipegamestate = GS_NULL;
|
|
SV_StopServer();
|
|
SV_ResetServer();
|
|
}
|
|
|
|
boolean G_CheckDemoStatus(void)
|
|
{
|
|
G_FreeGhosts();
|
|
|
|
// DO NOT end metal sonic demos here
|
|
|
|
if (demo.timing)
|
|
{
|
|
G_StopTimingDemo();
|
|
return true;
|
|
}
|
|
|
|
if (demo.playback)
|
|
{
|
|
if (demo.quitafterplaying)
|
|
I_Quit();
|
|
|
|
if (multiplayer && !demo.title)
|
|
G_FinishExitLevel();
|
|
else
|
|
{
|
|
G_StopDemo();
|
|
|
|
if (timedemo_quit)
|
|
COM_ImmedExecute("quit");
|
|
else if (modeattacking)
|
|
MR_ModeAttackEndGame(0);
|
|
else
|
|
D_StartTitle();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (demo.recording && (modeattacking || demo.savemode != DSM_NOTSAVING))
|
|
{
|
|
if (demobuf.p)
|
|
{
|
|
G_SaveDemo();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (demo.recording)
|
|
P_SaveBufferFree(&demobuf);
|
|
demo.recording = false;
|
|
return false;
|
|
}
|
|
|
|
void G_SaveDemo(void)
|
|
{
|
|
UINT8 *p = demobuf.buffer+16; // after version
|
|
UINT32 length;
|
|
|
|
// Ensure extrainfo pointer is always available, even if no info is present.
|
|
if (demoinfo_p && *(UINT32 *)demoinfo_p == 0)
|
|
{
|
|
WRITEUINT8(demobuf.p, DEMOMARKER); // add the demo end marker
|
|
*(UINT32 *)demoinfo_p = demobuf.p - demobuf.buffer;
|
|
}
|
|
WRITEUINT8(demobuf.p, DW_END); // Mark end of demo extra data.
|
|
|
|
memcpy(p, demo.titlename, 64); // Write demo title here
|
|
p += 64;
|
|
|
|
if (multiplayer)
|
|
{
|
|
// Change the demo's name to be a slug of the title
|
|
char demo_slug[128];
|
|
char *writepoint;
|
|
size_t i, strindex = 0;
|
|
boolean dash = true;
|
|
|
|
//for (i = 0; demo.titlename[i] && i < 127; i++) ?????
|
|
for (i = 0; i < 64 && demo.titlename[i]; i++)
|
|
{
|
|
if ((demo.titlename[i] >= 'a' && demo.titlename[i] <= 'z') ||
|
|
(demo.titlename[i] >= '0' && demo.titlename[i] <= '9'))
|
|
{
|
|
demo_slug[strindex] = demo.titlename[i];
|
|
strindex++;
|
|
dash = false;
|
|
}
|
|
else if (demo.titlename[i] >= 'A' && demo.titlename[i] <= 'Z')
|
|
{
|
|
demo_slug[strindex] = demo.titlename[i] + 'a' - 'A';
|
|
strindex++;
|
|
dash = false;
|
|
}
|
|
else if (strindex && !dash)
|
|
{
|
|
demo_slug[strindex] = '-';
|
|
strindex++;
|
|
dash = true;
|
|
}
|
|
}
|
|
|
|
if (dash && strindex)
|
|
{
|
|
strindex--;
|
|
}
|
|
demo_slug[strindex] = '\0';
|
|
|
|
if (demo_slug[0] != '\0')
|
|
{
|
|
// Slug is valid, write the chosen filename.
|
|
writepoint = strstr(strrchr(demoname, *PATHSEP), "-");
|
|
if (!writepoint)
|
|
return;
|
|
|
|
writepoint++;
|
|
|
|
size_t flen = 128 - (writepoint - demoname) - 4;
|
|
|
|
if (flen > 0 && flen < 128)
|
|
{
|
|
demo_slug[flen] = '\0';
|
|
sprintf(writepoint, "%s.lmp", demo_slug);
|
|
}
|
|
}
|
|
}
|
|
|
|
length = *(UINT32 *)demoinfo_p;
|
|
WRITEUINT32(demoinfo_p, length);
|
|
|
|
// Doesn't seem like I can use WriteDemoChecksum here, correct me if I'm wrong -Sal
|
|
|
|
// Make a checksum of everything after the checksum in the file up to the end of the standard data. Extrainfo is freely modifiable.
|
|
WRITEUINT64(p, HASH64(p+16, (demobuf.buffer + length) - (p+16)));
|
|
|
|
if (FIL_WriteFile(demoname, demobuf.buffer, demobuf.p - demobuf.buffer)) // finally output the file.
|
|
demo.savemode = DSM_SAVED;
|
|
P_SaveBufferFree(&demobuf);
|
|
demo.recording = false;
|
|
|
|
if (!modeattacking)
|
|
{
|
|
if (demo.savemode == DSM_SAVED)
|
|
CONS_Printf(M_GetText("Demo %s recorded\n"), demoname);
|
|
else
|
|
CONS_Alert(CONS_WARNING, M_GetText("Demo %s not saved\n"), demoname);
|
|
}
|
|
}
|
|
|
|
boolean G_DemoTitleResponder(event_t *ev)
|
|
{
|
|
INT32 ch;
|
|
|
|
if (ev->type != ev_keydown)
|
|
return false;
|
|
|
|
ch = (INT32)ev->data1;
|
|
|
|
// Only ESC and non-keyboard keys abort connection
|
|
if (ch == KEY_ESCAPE)
|
|
{
|
|
demo.savemode = (cv_recordmultiplayerdemos.value == 2) ? DSM_WILLAUTOSAVE : DSM_NOTSAVING;
|
|
return true;
|
|
}
|
|
|
|
if (ch == KEY_ENTER || ch >= NUMKEYS)
|
|
{
|
|
demo.savemode = DSM_WILLSAVE;
|
|
return true;
|
|
}
|
|
|
|
M_TextInputHandle(&demo.titlenameinput, ch);
|
|
|
|
return true;
|
|
}
|
|
|
|
void G_SyncDemoParty(INT32 rem, INT32 newsplitscreen)
|
|
{
|
|
int r_splitscreen_copy = r_splitscreen;
|
|
INT32 displayplayers_copy[MAXSPLITSCREENPLAYERS];
|
|
memcpy(displayplayers_copy, displayplayers, sizeof displayplayers);
|
|
|
|
// If we switch away from someone's view, that player
|
|
// should be removed from the party.
|
|
// However, it is valid to have the player on multiple
|
|
// viewports.
|
|
|
|
// Remove this player
|
|
if (playeringame[rem])
|
|
G_LeaveParty(rem);
|
|
|
|
// And reset the rest of the party
|
|
for (int i = 0; i <= r_splitscreen_copy; ++i)
|
|
G_LeaveParty(displayplayers_copy[i]);
|
|
|
|
// Restore the party, without the removed player, and
|
|
// with the order matching displayplayers
|
|
for (int i = 0; i <= newsplitscreen; ++i)
|
|
G_JoinParty(consoleplayer, displayplayers_copy[i]);
|
|
|
|
// memcpy displayplayers back to preserve duplicates
|
|
// (G_JoinParty will not create duplicates itself)
|
|
r_splitscreen = newsplitscreen;
|
|
memcpy(displayplayers, displayplayers_copy, sizeof displayplayers);
|
|
R_ExecuteSetViewSize();
|
|
}
|