blankart/src/g_demo.c
2025-06-19 13:05:47 -04:00

4203 lines
104 KiB
C

// SONIC ROBO BLAST 2
//-----------------------------------------------------------------------------
// 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 "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_ArchiveDemo and LUA_UnArchiveDemo
#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"
#include "g_party.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);
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 0x0008
#define DEMOHEADER "\xF0" "KartReplay" "\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
#define ZT_BOT 0x8000
// Ziptics are UINT16 now, go nuts
#define ZT_BOT_TURN 0x0001
#define ZT_BOT_ITEM 0x0002
#define ZT_BOT_RESPAWN 0x0004
#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
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_ReadDemoExtraData(void)
{
INT32 p, extradata, i;
char name[17];
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));
}
}
memset(name, '\0', 17);
p = READUINT8(demobuf.p);
while (p < DW_EXTRASTUFF)
{
extradata = READUINT8(demobuf.p);
if (extradata & DXD_JOINDATA)
{
if (!playeringame[p])
{
G_AddPlayer(p, p);
}
players[p].bot = !!(READUINT8(demobuf.p));
if (players[p].bot)
{
players[p].botvars.difficulty = READUINT8(demobuf.p);
players[p].botvars.diffincrease = READUINT8(demobuf.p); // needed to avoid having to duplicate logic
players[p].botvars.rival = (boolean)READUINT8(demobuf.p);
}
}
if (extradata & DXD_PLAYSTATE)
{
i = READUINT8(demobuf.p);
switch (i) {
case DXD_PST_PLAYING:
if (players[p].spectator == true)
{
if (players[p].bot)
{
players[p].spectator = false;
}
else
{
players[p].pflags |= PF_WANTSTOJOIN;
}
}
//CONS_Printf("player %s is despectating on tic %d\n", player_names[p], leveltime);
break;
case DXD_PST_SPECTATING:
if (players[p].spectator)
{
players[p].pflags &= ~PF_WANTSTOJOIN;
}
else
{
if (players[p].mo)
{
P_DamageMobj(players[p].mo, NULL, NULL, 1, DMG_SPECTATOR);
}
P_SetPlayerSpectator(p);
}
break;
case DXD_PST_LEFT:
CL_RemovePlayer(p, 0);
break;
}
G_ResetViews();
// maybe these are necessary?
K_CheckBumpers();
P_CheckRacers();
}
if (extradata & DXD_NAME)
{
// Name
M_Memcpy(player_names[p],demobuf.p,16);
demobuf.p += 16;
}
if (extradata & DXD_SKIN)
{
UINT8 kartspeed, kartweight;
// Skin
M_Memcpy(name, demobuf.p, 16);
demobuf.p += 16;
SetPlayerSkin(p, name);
kartspeed = READUINT8(demobuf.p);
kartweight = READUINT8(demobuf.p);
if (stricmp(skins[players[p].skin].name, name) != 0)
FindClosestSkinForStats(p, kartspeed, kartweight);
players[p].kartspeed = kartspeed;
players[p].kartweight = kartweight;
}
if (extradata & DXD_COLOR)
{
// Color
M_Memcpy(name, demobuf.p, 16);
demobuf.p += 16;
for (i = 0; i < numskincolors; i++)
if (!stricmp(skincolors[i].name, name)) // SRB2kart
{
players[p].skincolor = i;
if (players[p].mo)
players[p].mo->color = i;
break;
}
}
if (extradata & DXD_FOLLOWER)
{
// Set our follower
M_Memcpy(name, demobuf.p, 16);
demobuf.p += 16;
K_SetFollowerByName(p, name);
// Follower's color
M_Memcpy(name, demobuf.p, 16);
demobuf.p += 16;
for (i = 0; i < numskincolors +2; i++) // +2 because of Match and Opposite
{
if (!stricmp(Followercolor_cons_t[i].strvalue, name))
{
players[p].followercolor = i;
break;
}
}
}
if (extradata & DXD_RESPAWN)
{
if (players[p].mo)
{
// Is this how this should work..?
P_DamageMobj(players[p].mo, NULL, NULL, 1, DMG_INSTAKILL);
}
}
if (extradata & DXD_WEAPONPREF)
{
WeaponPref_Parse(&demobuf.p, p);
//CONS_Printf("weaponpref is %d for player %d\n", i, p);
}
p = READUINT8(demobuf.p);
}
while (p != DW_END)
{
UINT32 rng;
switch (p)
{
case DW_RNG:
rng = READUINT32(demobuf.p);
if (P_GetRandSeed() != rng)
{
P_SetRandSeed(rng);
if (demosynced)
CONS_Alert(CONS_WARNING, M_GetText("Demo playback has desynced (RNG)!\n"));
demosynced = false;
}
}
p = READUINT8(demobuf.p);
}
if (!(demoflags & DF_GHOST) && *demobuf.p == DEMOMARKER)
{
// end of demo data stream
G_CheckDemoStatus();
return;
}
}
void G_WriteDemoExtraData(void)
{
INT32 i;
char name[16];
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)
{
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
memset(name, 0, 16);
strncpy(name, player_names[i], 16);
M_Memcpy(demobuf.p,name,16);
demobuf.p += 16;
}
if (demo_extradata[i] & DXD_SKIN)
{
// Skin
memset(name, 0, 16);
strncpy(name, skins[players[i].skin].name, 16);
M_Memcpy(demobuf.p,name,16);
demobuf.p += 16;
WRITEUINT8(demobuf.p, skins[players[i].skin].kartspeed);
WRITEUINT8(demobuf.p, skins[players[i].skin].kartweight);
}
if (demo_extradata[i] & DXD_COLOR)
{
// Color
memset(name, 0, 16);
strncpy(name, skincolors[players[i].skincolor].name, 16);
M_Memcpy(demobuf.p,name,16);
demobuf.p += 16;
}
if (demo_extradata[i] & DXD_FOLLOWER)
{
// write follower
memset(name, 0, 16);
if (players[i].followerskin == -1)
strncpy(name, "None", 16);
else
strncpy(name, followers[players[i].followerskin].skinname, 16);
M_Memcpy(demobuf.p, name, 16);
demobuf.p += 16;
// write follower color
memset(name, 0, 16);
strncpy(name, Followercolor_cons_t[(UINT16)(players[i].followercolor+2)].strvalue, 16); // Not KartColor_Names because followercolor has extra values such as "Match"
M_Memcpy(demobuf.p,name,16);
demobuf.p += 16;
}
//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)
{
UINT16 ziptic;
if (!demobuf.p || !demo.deferstart)
return;
ziptic = READUINT16(demobuf.p);
if (ziptic & ZT_FWD)
oldcmd[playernum].forwardmove = READSINT8(demobuf.p);
if (ziptic & ZT_SIDE)
oldcmd[playernum].sidemove = READSINT8(demobuf.p);
if (ziptic & ZT_TURNING)
oldcmd[playernum].turning = READINT16(demobuf.p);
if (ziptic & ZT_ANGLE)
oldcmd[playernum].angle = READINT16(demobuf.p);
if (ziptic & ZT_THROWDIR)
oldcmd[playernum].throwdir = READINT16(demobuf.p);
if (ziptic & ZT_BUTTONS)
oldcmd[playernum].buttons = READUINT16(demobuf.p);
if (ziptic & ZT_AIMING)
oldcmd[playernum].aiming = READINT16(demobuf.p);
if (ziptic & ZT_LATENCY)
oldcmd[playernum].latency = READUINT8(demobuf.p);
if (ziptic & ZT_FLAGS)
oldcmd[playernum].flags = READUINT8(demobuf.p);
if (ziptic & ZT_BOT)
{
UINT16 botziptic = READUINT16(demobuf.p);
if (botziptic & ZT_BOT_TURN)
oldcmd[playernum].bot.turnconfirm = READSINT8(demobuf.p);
if (botziptic & ZT_BOT_ITEM)
oldcmd[playernum].bot.itemconfirm = READSINT8(demobuf.p);
if (botziptic & ZT_BOT_RESPAWN)
oldcmd[playernum].bot.respawnconfirm = READSINT8(demobuf.p);
}
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;
}
if (cmd->flags & TICCMD_BOT)
{
ziptic |= ZT_BOT;
}
WRITEUINT16(ziptic_p, ziptic);
if (ziptic & ZT_BOT)
{
UINT16 botziptic = 0;
UINT8 *botziptic_p;
botziptic_p = demobuf.p; // the ziptic, written at the end of this function
demobuf.p += 2;
if (cmd->bot.turnconfirm != oldcmd[playernum].bot.turnconfirm)
{
WRITESINT8(demobuf.p, cmd->bot.turnconfirm);
oldcmd[playernum].bot.turnconfirm = cmd->bot.turnconfirm;
botziptic |= ZT_BOT_TURN;
}
if (cmd->bot.itemconfirm != oldcmd[playernum].bot.itemconfirm)
{
WRITESINT8(demobuf.p, cmd->bot.itemconfirm);
oldcmd[playernum].bot.itemconfirm = cmd->bot.itemconfirm;
botziptic |= ZT_BOT_ITEM;
}
if (cmd->bot.respawnconfirm != oldcmd[playernum].bot.respawnconfirm)
{
WRITESINT8(demobuf.p, cmd->bot.respawnconfirm);
oldcmd[playernum].bot.respawnconfirm = cmd->bot.respawnconfirm;
botziptic |= ZT_BOT_RESPAWN;
}
WRITEUINT16(botziptic_p, botziptic);
}
// 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++;
ghostext[playernum].hitlist = Z_Realloc(ghostext[playernum].hitlist, ghostext[playernum].hits * sizeof(mobj_t *), PU_LEVEL, &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);
}
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)
{
UINT8 ziptic;
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;
// Grab ghost data.
ziptic = READUINT8(demobuf.p);
if (ziptic & GZT_XYZ)
{
oldghost[playernum].x = READFIXED(demobuf.p);
oldghost[playernum].y = READFIXED(demobuf.p);
oldghost[playernum].z = READFIXED(demobuf.p);
syncleeway = 0;
}
else
{
if (ziptic & GZT_MOMXY)
{
oldghost[playernum].momx = READFIXED(demobuf.p);
oldghost[playernum].momy = READFIXED(demobuf.p);
}
if (ziptic & GZT_MOMZ)
oldghost[playernum].momz = READFIXED(demobuf.p);
oldghost[playernum].x += oldghost[playernum].momx;
oldghost[playernum].y += oldghost[playernum].momy;
oldghost[playernum].z += oldghost[playernum].momz;
syncleeway = FRACUNIT;
}
if (ziptic & GZT_ANGLE)
demobuf.p++;
if (ziptic & GZT_FRAME)
demobuf.p++;
if (ziptic & GZT_SPR2)
demobuf.p++;
if (ziptic & GZT_EXTRA)
{ // But wait, there's more!
UINT8 xziptic = READUINT8(demobuf.p);
if (xziptic & EZT_COLOR)
demobuf.p += sizeof(UINT16);
if (xziptic & EZT_SCALE)
demobuf.p += sizeof(fixed_t);
if (xziptic & EZT_HIT)
{ // Resync mob damage.
UINT16 i, count = READUINT16(demobuf.p);
thinker_t *th;
mobj_t *mobj;
UINT32 type;
UINT16 health;
fixed_t x;
fixed_t y;
fixed_t z;
for (i = 0; i < count; i++)
{
//demobuf.p += 4; // reserved.
type = READUINT32(demobuf.p);
health = READUINT16(demobuf.p);
x = READFIXED(demobuf.p);
y = READFIXED(demobuf.p);
z = READFIXED(demobuf.p);
demobuf.p += sizeof(angle_t); // angle, unnecessary for cons.
mobj = NULL;
for (th = thlist[THINK_MOBJ].next; th != &thlist[THINK_MOBJ]; th = th->next)
{
if (th->function.acp1 == (actionf_p1)P_RemoveThinkerDelayed)
continue;
mobj = (mobj_t *)th;
if (mobj->type == (mobjtype_t)type && mobj->x == x && mobj->y == y && mobj->z == z)
break;
}
if (th != &thlist[THINK_MOBJ] && mobj->health != 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 (xziptic & EZT_SPRITE)
demobuf.p += sizeof(UINT16);
if (xziptic & EZT_KART)
{
ghostext[playernum].kartitem = READINT32(demobuf.p);
ghostext[playernum].kartamount = READINT32(demobuf.p);
ghostext[playernum].kartbumpers = READINT32(demobuf.p);
}
}
if (ziptic & GZT_FOLLOW)
{ // Even more...
UINT8 followtic = READUINT8(demobuf.p);
if (followtic & FZT_SPAWNED)
{
demobuf.p += sizeof(INT16);
if (followtic & FZT_SKIN)
demobuf.p += sizeof(UINT16);
}
if (followtic & FZT_SCALE)
demobuf.p += sizeof(fixed_t);
// momx, momy and momz
demobuf.p += sizeof(fixed_t) * 3;
if (followtic & FZT_SKIN)
demobuf.p += sizeof(UINT16);
demobuf.p += sizeof(UINT16);
demobuf.p++;
demobuf.p += sizeof(UINT16);
}
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 (abs(px-gx) > syncleeway || abs(py-gy) > syncleeway || 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 (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;
}
}
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.
UINT16 ziptic = READUINT8(g->p);
UINT8 xziptic = 0;
if (g->done)
{
continue;
}
while (ziptic != DW_END) // Get rid of extradata stuff
{
if (ziptic < MAXPLAYERS)
{
#ifdef DEVELOP
UINT8 playerid = ziptic;
#endif
// 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.
ziptic = READUINT8(g->p);
if (ziptic & DXD_JOINDATA)
{
if (READUINT8(g->p) != 0)
I_Error("Ghost is not a record attack ghost (bot JOINDATA)");
}
if (ziptic & DXD_PLAYSTATE)
{
UINT8 playstate = READUINT8(g->p);
if (playstate != DXD_PST_PLAYING)
{
#ifdef DEVELOP
CONS_Alert(CONS_WARNING, "Ghost demo has non-playing playstate for player %d\n", playerid + 1);
#endif
;
}
}
if (ziptic & DXD_NAME)
g->p += 16; // yea
if (ziptic & DXD_SKIN)
g->p += 16; // We _could_ read this info, but it shouldn't change anything in record attack...
if (ziptic & DXD_COLOR)
g->p += 16; // Same tbh
if (ziptic & DXD_FOLLOWER)
g->p += 32; // ok (32 because there's both the skin and the colour)
if (ziptic & DXD_WEAPONPREF)
g->p++; // ditto
}
else if (ziptic == DW_RNG)
g->p += 4; // RNG seed
else
{
I_Error("Ghost is not a record attack ghost DXD (ziptic = %u)", ziptic); //@TODO lmao don't blow up like this
}
ziptic = READUINT8(g->p);
}
ziptic = READUINT16(g->p);
if (ziptic & ZT_FWD)
g->p++;
if (ziptic & ZT_SIDE)
g->p++;
if (ziptic & ZT_TURNING)
g->p += 2;
if (ziptic & ZT_ANGLE)
g->p += 2;
if (ziptic & ZT_THROWDIR)
g->p += 2;
if (ziptic & ZT_BUTTONS)
g->p += 2;
if (ziptic & ZT_AIMING)
g->p += 2;
if (ziptic & ZT_LATENCY)
g->p++;
if (ziptic & ZT_FLAGS)
g->p++;
if (ziptic & ZT_BOT)
{
UINT16 botziptic = READUINT16(g->p);
if (botziptic & ZT_BOT_TURN)
g->p++;
if (botziptic & ZT_BOT_ITEM)
g->p++;
}
// Grab ghost data.
ziptic = READUINT8(g->p);
if (ziptic == 0xFF)
goto skippedghosttic; // Didn't write ghost info this frame
else if (ziptic != 0)
I_Error("Ghost is not a record attack ghost ZIPTIC"); //@TODO lmao don't blow up like this
ziptic = READUINT8(g->p);
if (ziptic & GZT_XYZ)
{
g->oldmo.x = READFIXED(g->p);
g->oldmo.y = READFIXED(g->p);
g->oldmo.z = READFIXED(g->p);
}
else
{
if (ziptic & GZT_MOMXY)
{
g->oldmo.momx = READFIXED(g->p);
g->oldmo.momy = READFIXED(g->p);
}
if (ziptic & GZT_MOMZ)
g->oldmo.momz = READFIXED(g->p);
g->oldmo.x += g->oldmo.momx;
g->oldmo.y += g->oldmo.momy;
g->oldmo.z += g->oldmo.momz;
}
if (ziptic & GZT_ANGLE)
g->oldmo.angle = READUINT8(g->p)<<24;
if (ziptic & GZT_FRAME)
g->oldmo.frame = READUINT8(g->p);
if (ziptic & GZT_SPR2)
g->oldmo.sprite2 = READUINT8(g->p);
// 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 (ziptic & GZT_EXTRA)
{ // But wait, there's more!
xziptic = READUINT8(g->p);
if (xziptic & EZT_COLOR)
{
g->color = READUINT16(g->p);
switch(g->color)
{
default:
case GHC_RETURNSKIN:
g->mo->skin = 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 (xziptic & EZT_FLIP)
g->mo->eflags ^= MFE_VERTICALFLIP;
if (xziptic & EZT_SCALE)
{
g->mo->destscale = READFIXED(g->p);
if (g->mo->destscale != g->mo->scale)
P_SetScale(g->mo, g->mo->destscale);
}
if (xziptic & EZT_HIT)
{ // Spawn hit poofs for killing things!
UINT16 i, count = READUINT16(g->p), health;
UINT32 type;
fixed_t x,y,z;
angle_t angle;
mobj_t *poof;
for (i = 0; i < count; i++)
{
//g->p += 4; // reserved
type = READUINT32(g->p);
health = READUINT16(g->p);
x = READFIXED(g->p);
y = READFIXED(g->p);
z = READFIXED(g->p);
angle = READANGLE(g->p);
if (!(mobjinfo[type].flags & MF_SHOOTABLE)
|| !(mobjinfo[type].flags & (MF_ENEMY|MF_MONITOR))
|| health != 0 || i >= 4) // only spawn for the first 4 hits per frame, to prevent ghosts from splode-spamming too bad.
continue;
poof = P_SpawnMobj(x, y, z, MT_GHOST);
poof->angle = 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 (xziptic & EZT_SPRITE)
g->mo->sprite = READUINT16(g->p);
if (xziptic & EZT_KART)
g->p += 12; // kartitem, kartamount, kartbumpers
}
#define follow g->mo->tracer
if (ziptic & GZT_FOLLOW)
{ // Even more...
UINT8 followtic = READUINT8(g->p);
fixed_t temp;
if (followtic & FZT_SPAWNED)
{
if (follow)
P_RemoveMobj(follow);
P_SetTarget(&follow, P_SpawnMobjFromMobj(g->mo, 0, 0, 0, MT_GHOST));
P_SetTarget(&follow->tracer, g->mo);
follow->tics = -1;
temp = READINT16(g->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)
follow->skin = &skins[READUINT16(g->p)];
}
if (follow)
{
if (followtic & FZT_SCALE)
follow->destscale = READFIXED(g->p);
else
follow->destscale = g->mo->destscale;
if (follow->destscale != follow->scale)
P_SetScale(follow, follow->destscale);
P_UnsetThingPosition(follow);
temp = (g->version < 0x000e) ? READINT16(g->p)<<8 : READFIXED(g->p);
follow->x = g->mo->x + temp;
temp = (g->version < 0x000e) ? READINT16(g->p)<<8 : READFIXED(g->p);
follow->y = g->mo->y + temp;
temp = (g->version < 0x000e) ? READINT16(g->p)<<8 : READFIXED(g->p);
follow->z = g->mo->z + temp;
P_SetThingPosition(follow);
if (followtic & FZT_SKIN)
follow->sprite2 = READUINT16(g->p);
else
follow->sprite2 = 0;
follow->sprite = READUINT16(g->p);
follow->frame = (READUINT8(g->p)) | (g->mo->frame & FF_TRANSMASK);
follow->angle = g->mo->angle;
follow->color = READUINT16(g->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);
}
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 (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 (follow)
follow->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();
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)
follow->skin = &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);
strcat(demoname, ".lmp");
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, 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.
M_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;
snprintf(demo.titlename, 64, "%s - %s", G_BuildMapTitle(gamemap), modeattacking ? "Record Attack" : connectedservername);
// demo checksum
demobuf.p += sizeof(UINT64);
// game data
M_Memcpy(demobuf.p, "PLAY", 4); demobuf.p += 4;
WRITESTRINGN(demobuf.p, mapheaderinfo[gamemap-1]->lumpname, MAXMAPLUMPNAME);
WRITEUINT64(demobuf.p, maphash);
WRITEUINT8(demobuf.p, demoflags);
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.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
memset(name, 0, 16);
strncpy(name, player_names[p], 16);
M_Memcpy(demobuf.p,name,16);
demobuf.p += 16;
// Skin
memset(name, 0, 16);
strncpy(name, skins[player->skin].name, 16);
M_Memcpy(demobuf.p,name,16);
demobuf.p += 16;
// Color
memset(name, 0, 16);
strncpy(name, skincolors[player->skincolor].name, 16);
M_Memcpy(demobuf.p,name,16);
demobuf.p += 16;
// 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.
memset(name, 0, 16);
if (player->follower)
strncpy(name, followers[player->followerskin].skinname, 16);
else
strncpy(name, "None", 16); // Say we don't have one, then.
M_Memcpy(demobuf.p,name,16);
demobuf.p += 16;
// Save follower's colour
memset(name, 0, 16);
strncpy(name, Followercolor_cons_t[(UINT16)(player->followercolor+2)].strvalue, 16); // Not KartColor_Names because followercolor has extra values such as "Match"
M_Memcpy(demobuf.p, name, 16);
demobuf.p += 16;
// 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);
}
}
WRITEUINT8(demobuf.p, 0xFF); // Denote the end of the player listing
// player lua vars, always saved even if empty
if (demoflags & DF_LUAVARS)
LUA_Archive(&demobuf, 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.
M_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);
M_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)
{
char temp[16];
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
memset(temp, 0, 16);
strncpy(temp, name, 16);
M_Memcpy(demobuf.p,temp,16);
demobuf.p += 16;
// Skin
memset(temp, 0, 16);
strncpy(temp, skins[skinnum].name, 16);
M_Memcpy(demobuf.p,temp,16);
demobuf.p += 16;
// Color
memset(temp, 0, 16);
strncpy(temp, skincolors[color].name, 16);
M_Memcpy(demobuf.p,temp,16);
demobuf.p += 16;
// 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(UINT8 **pp)
{
UINT8 totalfiles;
char filename[MAX_WADPATH];
UINT64 filehash;
filestatus_t ncs;
boolean toomany = false;
boolean alreadyloaded;
UINT8 i, j;
boolean compatmode;
totalfiles = READUINT8((*pp));
for (i = 0; i < totalfiles; ++i)
{
if (toomany)
SKIPSTRING((*pp));
else
{
strlcpy(filename, (char *)(*pp), sizeof filename);
SKIPSTRING((*pp));
}
filehash = READUINT64((*pp));
compatmode = READUINT8((*pp));
if (!toomany)
{
alreadyloaded = false;
for (j = 0; j < numwadfiles; ++j)
{
if (filehash == wadfiles[j]->hash)
{
alreadyloaded = true;
break;
}
}
if (alreadyloaded)
continue;
if (numwadfiles >= MAX_WADFILES)
toomany = true;
else
ncs = findfile(filename, 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"), filename);
else if (ncs == FS_BADHASH)
CONS_Alert(CONS_NOTICE, M_GetText("Checksum mismatch on %s\n"), filename);
else
CONS_Alert(CONS_NOTICE, M_GetText("Unknown error finding file %s\n"), 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(filename, compatmode ? WC_ON : WC_OFF);
}
}
}
if (P_PartialAddGetStage() >= 0)
P_MultiSetupWadFiles(true); // in case any partial adds were done
}
static void G_SkipDemoExtraFiles(UINT8 **pp)
{
UINT8 totalfiles;
UINT8 i;
totalfiles = READUINT8((*pp));
for (i = 0; i < totalfiles; ++i)
{
SKIPSTRING((*pp));// file name
(*pp) += sizeof(UINT64); // hash
(*pp) += 1; // compatmode
}
}
// 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(UINT8 **pp, boolean quick)
{
UINT8 totalfiles, filesloaded, nmusfilecount;
char filename[MAX_WADPATH];
UINT64 filehash;
boolean toomany = false;
boolean alreadyloaded;
UINT8 i, j;
UINT8 error = 0;
totalfiles = READUINT8((*pp));
filesloaded = 0;
for (i = 0; i < totalfiles; ++i)
{
if (toomany)
SKIPSTRING((*pp));
else
{
strlcpy(filename, (char *)(*pp), sizeof filename);
SKIPSTRING((*pp));
}
filehash = READUINT64((*pp));
(void)READUINT8((*pp)); // compatmode
if (!toomany)
{
alreadyloaded = false;
nmusfilecount = 0;
for (j = 0; j < numwadfiles; ++j)
{
if (wadfiles[j]->important && j >= NUMMAINWADS)
nmusfilecount++;
else
continue;
if (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(filename, 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,*p;
UINT8 flags;
UINT32 oldtime, newtime, oldlap, newlap;
UINT16 oldversion;
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);
p = buffer;
// read demo header
I_Assert(!memcmp(p, DEMOHEADER, 12));
p += 12; // DEMOHEADER
c = READUINT8(p); // VERSION
I_Assert(c == VERSION);
c = READUINT8(p); // SUBVERSION
I_Assert(c == SUBVERSION);
s = READUINT16(p);
I_Assert(s == DEMOVERSION);
p += 64; // full demo title
p += sizeof(UINT64); // demo checksum
I_Assert(!memcmp(p, "PLAY", 4));
p += 4; // PLAY
SKIPSTRING(p); // gamemap
p += sizeof(UINT64); // map hash
flags = READUINT8(p); // demoflags
p++; // gametype
p++; // numlaps
G_SkipDemoExtraFiles(&p);
aflags = flags & (DF_TIMEATTACK|DF_ITEMBREAKER);
I_Assert(aflags);
if (flags & DF_TIMEATTACK)
uselaps = true; // get around uninitalized error
newtime = READUINT32(p);
if (uselaps)
newlap = READUINT32(p);
else
newlap = UINT32_MAX;
Z_Free(buffer);
// 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;
}
p = buffer;
// read demo header
if (memcmp(p, DEMOHEADER, 12))
{
CONS_Alert(CONS_NOTICE, M_GetText("File '%s' invalid format. It will be overwritten.\n"), oldname);
Z_Free(buffer);
return UINT8_MAX;
} p += 12; // DEMOHEADER
p++; // VERSION
p++; // SUBVERSION
oldversion = READUINT16(p);
switch(oldversion) // demoversion
{
case DEMOVERSION: // latest always supported
break;
// too old, cannot support.
default:
CONS_Alert(CONS_NOTICE, M_GetText("File '%s' invalid format. It will be overwritten.\n"), oldname);
Z_Free(buffer);
return UINT8_MAX;
}
p += 64; // full demo title
p += sizeof(UINT64); // demo checksum
if (memcmp(p, "PLAY", 4))
{
CONS_Alert(CONS_NOTICE, M_GetText("File '%s' invalid format. It will be overwritten.\n"), oldname);
Z_Free(buffer);
return UINT8_MAX;
} p += 4; // "PLAY"
SKIPSTRING(p); // gamemap
p += sizeof(UINT64); // maphash
flags = READUINT8(p);
p++; // gametype
p++; // numlaps
G_SkipDemoExtraFiles(&p);
if (!(flags & aflags))
{
CONS_Alert(CONS_NOTICE, M_GetText("File '%s' not from same game mode. It will be overwritten.\n"), oldname);
Z_Free(buffer);
return UINT8_MAX;
}
oldtime = READUINT32(p);
if (uselaps)
oldlap = READUINT32(p);
else
oldlap = 0;
Z_Free(buffer);
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)
{
UINT8 *infobuffer, *info_p, *extrainfo_p;
UINT8 version, subversion, pdemoflags;
UINT16 pdemoversion, count;
char mapname[MAXMAPLUMPNAME];
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;
}
info_p = infobuffer;
if (memcmp(info_p, DEMOHEADER, 12))
{
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");
Z_Free(infobuffer);
return;
}
pdemo->type = MD_LOADED;
info_p += 12; // DEMOHEADER
version = READUINT8(info_p);
subversion = READUINT8(info_p);
pdemoversion = READUINT16(info_p);
switch(pdemoversion)
{
case DEMOVERSION: // latest always supported
// demo title
M_Memcpy(pdemo->title, info_p, 64);
info_p += 64;
break;
// too old, cannot support.
default:
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");
Z_Free(infobuffer);
return;
}
if (version != VERSION || subversion != SUBVERSION)
pdemo->type = MD_OUTDATED;
info_p += sizeof(UINT64); // demo checksum
if (memcmp(info_p, "PLAY", 4))
{
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");
Z_Free(infobuffer);
return;
}
info_p += 4; // "PLAY"
READSTRINGN(info_p, mapname, sizeof(mapname));
pdemo->map = G_MapNumber(mapname);
info_p += sizeof(UINT64); // maphash
pdemoflags = READUINT8(info_p);
// temp?
if (!(pdemoflags & 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);
Z_Free(infobuffer);
return;
}
pdemo->gametype = READUINT8(info_p);
pdemo->numlaps = READUINT8(info_p);
pdemo->addonstatus = G_CheckDemoExtraFiles(&info_p, true);
info_p += 4; // RNG seed
extrainfo_p = infobuffer + READUINT32(info_p); // 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
count = READUINT16(info_p);
while (count--)
{
UINT16 netid;
char *svalue;
netid = READUINT16(info_p);
svalue = (char *)info_p;
SKIPSTRING(info_p);
info_p++; // stealth
if (netid == cv_kartspeed.netid)
{
UINT8 j;
for (j = 0; kartspeed_cons_t[j].strvalue; j++)
if (!stricmp(kartspeed_cons_t[j].strvalue, svalue))
pdemo->kartspeed = kartspeed_cons_t[j].value;
}
}
if (pdemoflags & DF_ENCORE)
pdemo->kartspeed |= DF_ENCORE;
// Read standings!
count = 0;
while (READUINT8(extrainfo_p) == DW_STANDING) // Assume standings are always first in the extrainfo
{
INT32 i;
char temp[16];
pdemo->standings[count].ranking = READUINT8(extrainfo_p);
// Name
M_Memcpy(pdemo->standings[count].name, extrainfo_p, 16);
extrainfo_p += 16;
// Skin
M_Memcpy(temp,extrainfo_p,16);
extrainfo_p += 16;
pdemo->standings[count].skin = UINT8_MAX;
for (i = 0; i < numskins; i++)
if (stricmp(skins[i].name, temp) == 0)
{
pdemo->standings[count].skin = i;
break;
}
// Color
M_Memcpy(temp,extrainfo_p,16);
extrainfo_p += 16;
for (i = 0; i < numskincolors; i++)
if (!stricmp(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
}
// I think that's everything we need?
Z_Free(infobuffer);
}
//
// 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
void G_DoPlayDemo(char *defdemoname)
{
UINT8 i, p;
lumpnum_t l;
char skin[17],color[MAXCOLORNAME+1],follower[17],mapname[MAXMAPLUMPNAME],*n,*pdemoname;
UINT8 version,subversion;
UINT32 randseed;
char msg[1024];
boolean spectator, bot;
UINT8 slots[MAXPLAYERS], kartspeed[MAXPLAYERS], kartweight[MAXPLAYERS], numslots = 0;
#if defined(SKIPERRORS) && !defined(DEVELOP)
boolean skiperrors = false;
#endif
G_InitDemoRewind();
skin[16] = '\0';
follower[16] = '\0';
color[MAXCOLORNAME] = '\0';
// 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);
CONS_Alert(CONS_ERROR, "%s", msg);
gameaction = ga_nothing;
M_StartMessage(msg, NULL, MM_NOTHING);
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);
CONS_Alert(CONS_ERROR, "%s", msg);
Z_Free(pdemoname);
gameaction = ga_nothing;
M_StartMessage(msg, NULL, MM_NOTHING);
return;
}
P_SaveBufferFromLump(&demobuf, l);
}
else
{
// vres GHOST_%u
virtres_t *vRes;
virtlump_t *vLump;
UINT16 mapnum;
size_t step = 0;
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);
CONS_Alert(CONS_ERROR, "%s", msg);
Z_Free(pdemoname);
gameaction = ga_nothing;
M_StartMessage(msg, NULL, MM_NOTHING);
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);
CONS_Alert(CONS_ERROR, "%s", msg);
Z_Free(pdemoname);
gameaction = ga_nothing;
M_StartMessage(msg, NULL, MM_NOTHING);
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
gameaction = ga_nothing;
demo.playback = true;
demo.buffer = &demobuf;
if (memcmp(demobuf.p, DEMOHEADER, 12))
{
snprintf(msg, 1024, M_GetText("%s is not a SRB2Kart replay file.\n"), pdemoname);
CONS_Alert(CONS_ERROR, "%s", msg);
Z_Free(pdemoname);
P_SaveBufferFree(&demobuf);
demo.playback = false;
demo.title = false;
M_StartMessage(msg, NULL, MM_NOTHING);
return;
}
demobuf.p += 12; // DEMOHEADER
version = READUINT8(demobuf.p);
subversion = READUINT8(demobuf.p);
demo.version = READUINT16(demobuf.p);
switch(demo.version)
{
case DEMOVERSION: // latest always supported
break;
// too old, cannot support.
default:
snprintf(msg, 1024, M_GetText("%s is an incompatible replay format and cannot be played.\n"), pdemoname);
CONS_Alert(CONS_ERROR, "%s", msg);
Z_Free(pdemoname);
P_SaveBufferFree(&demobuf);
demo.playback = false;
demo.title = false;
M_StartMessage(msg, NULL, MM_NOTHING);
return;
}
// demo title
M_Memcpy(demo.titlename, demobuf.p, 64);
demobuf.p += 64;
demobuf.p += sizeof(UINT64); // demo checksum
if (memcmp(demobuf.p, "PLAY", 4))
{
snprintf(msg, 1024, M_GetText("%s is the wrong type of recording and cannot be played.\n"), pdemoname);
CONS_Alert(CONS_ERROR, "%s", msg);
Z_Free(pdemoname);
P_SaveBufferFree(&demobuf);
demo.playback = false;
demo.title = false;
M_StartMessage(msg, NULL, MM_NOTHING);
return;
}
demobuf.p += 4; // "PLAY"
READSTRINGN(demobuf.p, mapname, sizeof(mapname)); // gamemap
gamemap = G_MapNumber(mapname)+1;
demobuf.p += sizeof(UINT64); // maphash
demoflags = READUINT8(demobuf.p);
gametype = READUINT8(demobuf.p);
G_SetGametype(gametype);
numlaps = READUINT8(demobuf.p);
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(&demobuf.p);
else if (demo.ignorefiles)
G_SkipDemoExtraFiles(&demobuf.p);
else
{
UINT8 error = G_CheckDemoExtraFiles(&demobuf.p, false);
if (error)
{
switch (error)
{
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);
break;
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);
break;
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);
break;
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);
break;
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);
break;
}
CONS_Alert(CONS_ERROR, "%s", msg);
Z_Free(pdemoname);
P_SaveBufferFree(&demobuf);
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);
return;
}
}
modeattacking = (demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT;
multiplayer = !!(demoflags & DF_MULTIPLAYER);
demo.netgame = (multiplayer && !(demoflags & DF_NONETMP));
CON_ToggleOff();
hu_demotime = UINT32_MAX;
hu_demolap = UINT32_MAX;
switch (modeattacking)
{
case ATTACKING_NONE: // 0
break;
case ATTACKING_TIME: // 1
hu_demotime = READUINT32(demobuf.p);
hu_demolap = READUINT32(demobuf.p);
break;
case ATTACKING_ITEMBREAK: // 2
hu_demotime = READUINT32(demobuf.p);
break;
default: // 3
modeattacking = ATTACKING_NONE;
break;
}
// Random seed
randseed = READUINT32(demobuf.p);
demobuf.p += 4; // Extrainfo location
// ...*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);
CONS_Alert(CONS_ERROR, "%s", msg);
Z_Free(pdemoname);
P_SaveBufferFree(&demobuf);
demo.playback = false;
demo.title = false;
M_StartMessage(msg, NULL, MM_NOTHING);
return;
}
// net var data
CV_LoadDemoVars(&demobuf.p);
memset(&grandprixinfo, 0, sizeof grandprixinfo);
if ((demoflags & DF_GRANDPRIX))
{
grandprixinfo.gp = true;
grandprixinfo.gamespeed = READUINT8(demobuf.p);
grandprixinfo.masterbots = READUINT8(demobuf.p) != 0;
grandprixinfo.eventmode = READUINT8(demobuf.p);
}
// Load "mapmusrng" used for altmusic selection
mapmusrng = READUINT8(demobuf.p);
// Sigh ... it's an empty demo.
if (*demobuf.p == DEMOMARKER)
{
snprintf(msg, 1024, M_GetText("%s contains no data to be played.\n"), pdemoname);
CONS_Alert(CONS_ERROR, "%s", msg);
Z_Free(pdemoname);
P_SaveBufferFree(&demobuf);
demo.playback = false;
demo.title = false;
M_StartMessage(msg, NULL, MM_NOTHING);
return;
}
Z_Free(pdemoname);
memset(&oldcmd,0,sizeof(oldcmd));
memset(&oldghost,0,sizeof(oldghost));
memset(&ghostext,0,sizeof(ghostext));
#if defined(SKIPERRORS) && !defined(DEVELOP)
if ((VERSION != version || SUBVERSION != subversion) && !skiperrors)
#else
if (VERSION != version || SUBVERSION != 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
p = READUINT8(demobuf.p);
while (p != 0xFF)
{
UINT8 flags = READUINT8(demobuf.p);
spectator = !!(flags & DEMO_SPECTATOR);
bot = !!(flags & DEMO_BOT);
if ((spectator || bot))
{
if (modeattacking)
{
snprintf(msg, 1024, M_GetText("%s is a Record Attack replay with %s, and is thus invalid.\n"), pdemoname, (bot ? "bots" : "spectators"));
CONS_Alert(CONS_ERROR, "%s", msg);
Z_Free(pdemoname);
P_SaveBufferFree(&demobuf);
demo.playback = false;
demo.title = false;
M_StartMessage(msg, NULL, MM_NOTHING);
return;
}
}
slots[numslots] = p;
numslots++;
if (modeattacking && numslots > 1)
{
snprintf(msg, 1024, M_GetText("%s is a Record Attack replay with multiple players, and is thus invalid.\n"), pdemoname);
CONS_Alert(CONS_ERROR, "%s", msg);
Z_Free(pdemoname);
P_SaveBufferFree(&demobuf);
demo.playback = false;
demo.title = false;
M_StartMessage(msg, NULL, MM_NOTHING);
return;
}
if (!playeringame[displayplayers[0]] || players[displayplayers[0]].spectator)
displayplayers[0] = consoleplayer = serverplayer = p;
G_AddPlayer(p, p);
players[p].spectator = spectator;
if (flags & DEMO_KICKSTART)
players[p].pflags |= PF_KICKSTARTACCEL;
else
players[p].pflags &= ~PF_KICKSTARTACCEL;
if (flags & DEMO_SHRINKME)
players[p].pflags |= PF_SHRINKME;
else
players[p].pflags &= ~PF_SHRINKME;
K_UpdateShrinkCheat(&players[p]);
if ((players[p].bot = bot) == true)
{
players[p].botvars.difficulty = READUINT8(demobuf.p);
players[p].botvars.diffincrease = READUINT8(demobuf.p); // needed to avoid having to duplicate logic
players[p].botvars.rival = (boolean)READUINT8(demobuf.p);
}
// Name
M_Memcpy(player_names[p],demobuf.p,16);
demobuf.p += 16;
/*if (players[p].spectator)
{
CONS_Printf("player %s is spectator at start\n", player_names[p]);
}*/
// Skin
M_Memcpy(skin,demobuf.p,16);
demobuf.p += 16;
SetPlayerSkin(p, skin);
// Color
M_Memcpy(color,demobuf.p,16);
demobuf.p += 16;
for (i = 0; i < numskincolors; i++)
if (!stricmp(skincolors[i].name,color)) // SRB2kart
{
players[p].skincolor = i;
break;
}
// Follower
M_Memcpy(follower, demobuf.p, 16);
demobuf.p += 16;
K_SetFollowerByName(p, follower);
// Follower colour
M_Memcpy(color, demobuf.p, 16);
demobuf.p += 16;
for (i = 0; i < numskincolors +2; i++) // +2 because of Match and Opposite
{
if (!stricmp(Followercolor_cons_t[i].strvalue, color))
{
players[p].followercolor = i;
break;
}
}
// Score, since Kart uses this to determine where you start on the map
players[p].score = READUINT32(demobuf.p);
// Power Levels
clientpowerlevels[p][gametype == GT_BATTLE ? PWRLV_BATTLE : PWRLV_RACE] = READUINT16(demobuf.p);
// Kart stats, temporarily
kartspeed[p] = READUINT8(demobuf.p);
kartweight[p] = READUINT8(demobuf.p);
if (stricmp(skins[players[p].skin].name, skin) != 0)
FindClosestSkinForStats(p, kartspeed[p], kartweight[p]);
// Followitem
players[p].followitem = READUINT32(demobuf.p);
// Look for the next player
p = READUINT8(demobuf.p);
}
// 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.
LUA_UnArchive(&demobuf, false);
}
splitscreen = 0;
if (demo.title)
{
splitscreen = M_RandomKey(6)-1;
splitscreen = min(min(3, numslots-1), splitscreen); // Bias toward 1p and 4p views
for (p = 0; p <= splitscreen; p++)
G_ResetView(p+1, slots[M_RandomKey(numslots)], false);
}
R_ExecuteSetViewSize();
P_SetRandSeed(randseed);
G_InitNew(demoflags & DF_ENCORE, gamemap, true, true, false); // Doesn't matter whether you reset or not here, given changes to resetplayer.
for (i = 0; i < MAXPLAYERS; i++)
{
if (players[i].mo)
{
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;
}
// Set saved attribute values
// No cheat checking here, because even if they ARE wrong...
// it would only break the replay if we clipped them.
players[i].kartspeed = kartspeed[i];
players[i].kartweight = kartweight[i];
}
demo.deferstart = true;
}
void G_AddGhost(char *defdemoname)
{
INT32 i;
lumpnum_t l;
char name[17],skin[17],color[MAXCOLORNAME+1],*n,*pdemoname;
UINT64 demohash;
demoghost *gh;
UINT8 flags;
UINT8 *buffer,*p;
mapthing_t *mthing;
UINT16 count, ghostversion;
skin_t *ghskin = &skins[0];
UINT8 kartspeed = UINT8_MAX, kartweight = UINT8_MAX;
name[16] = '\0';
skin[16] = '\0';
color[16] = '\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);
Z_Free(pdemoname);
return;
}
p = buffer;
}
// 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);
Z_Free(pdemoname);
return;
}
else // it's an internal demo
buffer = p = W_CacheLumpNum(l, PU_LEVEL);
// read demo header
if (memcmp(p, DEMOHEADER, 12))
{
CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Not a SRB2 replay.\n"), pdemoname);
Z_Free(pdemoname);
Z_Free(buffer);
return;
} p += 12; // DEMOHEADER
p++; // VERSION
p++; // SUBVERSION
ghostversion = READUINT16(p);
switch(ghostversion)
{
case DEMOVERSION: // latest always supported
break;
// too old, cannot support.
default:
CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Demo version incompatible.\n"), pdemoname);
Z_Free(pdemoname);
Z_Free(buffer);
return;
}
p += 64; // title
demohash = READUINT64(p); // demo checksum
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);
Z_Free(pdemoname);
Z_Free(buffer);
return;
}
if (memcmp(p, "PLAY", 4))
{
CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: Demo format unacceptable.\n"), pdemoname);
Z_Free(pdemoname);
Z_Free(buffer);
return;
} p += 4; // "PLAY"
SKIPSTRING(p); // gamemap
p += sizeof(UINT64); // maphash (possibly check for consistency?)
flags = READUINT8(p);
if (!(flags & DF_GHOST))
{
CONS_Alert(CONS_NOTICE, M_GetText("Ghost %s: No ghost data in this demo.\n"), pdemoname);
Z_Free(pdemoname);
Z_Free(buffer);
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);
Z_Free(pdemoname);
Z_Free(buffer);
return;
}
p++; // gametype
p++; // numlaps
G_SkipDemoExtraFiles(&p); // Don't wanna modify the file list for ghosts.
switch ((flags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
{
case ATTACKING_NONE: // 0
break;
case ATTACKING_TIME: // 1
p += 8; // demo time, lap
break;
case ATTACKING_ITEMBREAK: // 2
p += 4; // demo time
break;
default: // 3
break;
}
p += 4; // random seed
p += 4; // Extra data location reference
// net var data
count = READUINT16(p);
while (count--)
{
SKIPSTRING(p);
SKIPSTRING(p);
p++;
}
if ((flags & DF_GRANDPRIX))
{
p += 3;
}
// Skip mapmusrng
p++;
if (*p == DEMOMARKER)
{
CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Replay is empty.\n"), pdemoname);
Z_Free(pdemoname);
Z_Free(buffer);
return;
}
p++; // player number - doesn't really need to be checked, TODO maybe support adding multiple players' ghosts at once
// any invalidating flags?
i = READUINT8(p);
if ((i & (DEMO_SPECTATOR|DEMO_BOT)) != 0)
{
CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Invalid player slot (spectator/bot)\n"), defdemoname);
Z_Free(pdemoname);
Z_Free(buffer);
return;
}
// Player name (TODO: Display this somehow if it doesn't match cv_playername!)
M_Memcpy(name, p, 16);
p += 16;
// Skin
M_Memcpy(skin, p, 16);
p += 16;
// Color
M_Memcpy(color, p, 16);
p += 16;
// Follower data was here, skip it, we don't care about it for ghosts.
p += 32; // followerskin (16) + followercolor (16)
p += 4; // score
p += 2; // powerlevel
kartspeed = READUINT8(p);
kartweight = READUINT8(p);
p += 4; // followitem (maybe change later)
if (READUINT8(p) != 0xFF)
{
CONS_Alert(CONS_NOTICE, M_GetText("Failed to add ghost %s: Invalid player slot. (Bad terminator)\n"), pdemoname);
Z_Free(pdemoname);
Z_Free(buffer);
return;
}
for (i = 0; i < numskins; i++)
if (!stricmp(skins[i].name,skin))
{
ghskin = &skins[i];
break;
}
if (i == numskins)
{
if (kartspeed != UINT8_MAX && kartweight != UINT8_MAX)
ghskin = &skins[GetSkinNumClosestToStats(kartspeed, 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 = p;
ghosts = gh;
gh->version = ghostversion;
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
gh->mo->skin = gh->oldmo.skin = ghskin;
// Set color
gh->mo->color = ((skin_t*)gh->mo->skin)->prefcolor;
for (i = 0; i < numskincolors; i++)
if (!stricmp(skincolors[i].name,color))
{
gh->mo->color = (UINT16)i;
break;
}
gh->oldmo.color = gh->mo->color;
CONS_Printf(M_GetText("Added ghost %s from %s\n"), name, pdemoname);
Z_Free(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)
{
UINT8 *buffer,*p;
UINT16 ghostversion;
UINT8 flags;
buffer = p = W_CacheLumpNum(l, PU_CACHE);
// read demo header
if (memcmp(p, DEMOHEADER, 12))
{
goto fail;
}
p += 12; // DEMOHEADER
p++; // VERSION
p++; // SUBVERSION
ghostversion = READUINT16(p);
switch(ghostversion)
{
case DEMOVERSION: // latest always supported
break;
// too old, cannot support.
default:
goto fail;
}
p += 64; // full demo title
p += sizeof(UINT64); // demo checksum
if (memcmp(p, "PLAY", 4))
{
goto fail;
}
p += 4; // "PLAY"
SKIPSTRING(p); // gamemap
p += sizeof(UINT64); // maphash (possibly check for consistency?)
flags = READUINT8(p);
if (!(flags & DF_GHOST))
{
goto fail; // we don't NEED to do it here, but whatever
}
p++; // Gametype
p++; // numlaps
G_SkipDemoExtraFiles(&p);
switch ((flags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
{
case ATTACKING_NONE: // 0
break;
case ATTACKING_TIME: // 1
p += 8; // demo time, lap
break;
case ATTACKING_ITEMBREAK: // 2
p += 4; // demo time
break;
default: // 3
break;
}
p += 4; // random seed
p += 4; // Extrainfo location marker
// Ehhhh don't need ghostversion here (?) so I'll reuse the var here
ghostversion = READUINT16(p);
while (ghostversion--)
{
p += 2;
SKIPSTRING(p);
p++; // stealth
}
if ((flags & DF_GRANDPRIX))
{
p += 3;
}
// Assert first player is in and then read name
if (READUINT8(p) != 0)
goto fail;
if (READUINT8(p) & (DEMO_SPECTATOR|DEMO_BOT))
goto fail;
M_Memcpy(dummystaffname, p,16);
dummystaffname[16] = '\0';
// Ok, no longer any reason to care, bye
fail:
Z_Free(buffer);
return;
}
//
// 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.acp1 == (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))
{
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.
M_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++)
{
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 (!dash)
{
demo_slug[strindex] = '-';
strindex++;
dash = true;
}
}
demo_slug[strindex] = 0;
if (dash) demo_slug[strindex-1] = 0;
writepoint = strstr(strrchr(demoname, *PATHSEP), "-") + 1;
demo_slug[128 - (writepoint - demoname) - 4] = 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)
{
size_t len;
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;
}
if ((ch >= HU_FONTSTART && ch <= HU_FONTEND && fontv[HU_FONT].font[ch-HU_FONTSTART])
|| ch == ' ') // Allow spaces, of course
{
len = strlen(demo.titlename);
if (len < 64)
{
demo.titlename[len+1] = 0;
demo.titlename[len] = CON_ShiftChar(ch);
}
}
else if (ch == KEY_BACKSPACE)
{
if (shiftdown)
memset(demo.titlename, 0, sizeof(demo.titlename));
else
{
len = strlen(demo.titlename);
if (len > 0)
demo.titlename[len-1] = 0;
}
}
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
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();
}