Allow Record Attack in modified games

Replays now store & validate the add-on list before loading them. Custom characters can now be allowed in Record Attack.
This commit is contained in:
Sally Coolatta 2023-07-25 08:40:36 -04:00
parent 1a2f66acd9
commit 77576c3c5d
5 changed files with 487 additions and 35 deletions

View file

@ -1643,9 +1643,14 @@ static void Command_Playdemo_f(void)
{
char name[256];
if (COM_Argc() != 2)
if (COM_Argc() < 2)
{
CONS_Printf(M_GetText("playdemo <demoname>: playback a demo\n"));
CONS_Printf("playdemo <demoname> [-addfiles / -force]:\n");
CONS_Printf(M_GetText(
"Play back a demo file. The full path from your SRB2 directory must be given.\n\n"
"* With \"-addfiles\", any required files are added from a list contained within the demo file.\n"
"* With \"-force\", the demo is played even if the necessary files have not been added.\n"));
return;
}
@ -1667,6 +1672,16 @@ static void Command_Playdemo_f(void)
CONS_Printf(M_GetText("Playing back demo '%s'.\n"), name);
demofileoverride = DFILE_OVERRIDE_NONE;
if (strcmp(COM_Argv(2), "-addfiles") == 0)
{
demofileoverride = DFILE_OVERRIDE_LOAD;
}
else if (strcmp(COM_Argv(2), "-force") == 0)
{
demofileoverride = DFILE_OVERRIDE_SKIP;
}
// Internal if no extension, external if one exists
// If external, convert the file name to a path in SRB2's home directory
if (FIL_CheckExtension(name))

View file

@ -3509,6 +3509,7 @@ void F_TitleScreenTicker(boolean run)
}
titledemo = true;
demofileoverride = DFILE_OVERRIDE_NONE;
G_DoPlayDemo(dname);
}
}

View file

@ -39,6 +39,7 @@
#include "v_video.h"
#include "lua_hook.h"
#include "md5.h" // demo checksums
#include "d_netfil.h" // G_CheckDemoExtraFiles
boolean timingdemo; // if true, exit with report on completion
boolean nodrawers; // for comparative timing purposes
@ -49,6 +50,7 @@ static char demoname[64];
boolean demorecording;
boolean demoplayback;
boolean titledemo; // Title Screen demo can be cancelled by any key
demo_file_override_e demofileoverride;
static UINT8 *demobuffer = NULL;
static UINT8 *demo_p, *demotime_p;
static UINT8 *demoend;
@ -95,7 +97,7 @@ demoghost *ghosts = NULL;
// DEMO RECORDING
//
#define DEMOVERSION 0x000f
#define DEMOVERSION 0x0010
#define DEMOHEADER "\xF0" "SRB2Replay" "\x0F"
#define DF_GHOST 0x01 // This demo contains ghost data too!
@ -1413,6 +1415,10 @@ void G_BeginRecording(void)
char name[MAXCOLORNAME+1];
player_t *player = &players[consoleplayer];
char *filename;
UINT8 totalfiles;
UINT8 *m;
if (demo_p)
return;
memset(name,0,sizeof(name));
@ -1435,6 +1441,26 @@ void G_BeginRecording(void)
M_Memcpy(demo_p, mapmd5, 16); demo_p += 16;
WRITEUINT8(demo_p,demoflags);
// file list
m = demo_p;/* file count */
demo_p += 1;
totalfiles = 0;
for (i = mainwads; ++i < numwadfiles; )
{
if (wadfiles[i]->important)
{
nameonly(( filename = va("%s", wadfiles[i]->filename) ));
WRITESTRINGL(demo_p, filename, MAX_WADPATH);
WRITEMEM(demo_p, wadfiles[i]->md5sum, 16);
totalfiles++;
}
}
WRITEUINT8(m, totalfiles);
switch ((demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
{
case ATTACKING_NONE: // 0
@ -1590,6 +1616,177 @@ void G_BeginMetal(void)
oldmetal.angle = mo->angle>>24;
}
static void G_LoadDemoExtraFiles(UINT8 **pp)
{
UINT8 totalfiles;
char filename[MAX_WADPATH];
UINT8 md5sum[16];
filestatus_t ncs;
boolean toomany = false;
boolean alreadyloaded;
UINT8 i, j;
totalfiles = READUINT8((*pp));
for (i = 0; i < totalfiles; ++i)
{
if (toomany)
SKIPSTRING((*pp));
else
{
strlcpy(filename, (char *)(*pp), sizeof filename);
SKIPSTRING((*pp));
}
READMEM((*pp), md5sum, 16);
if (!toomany)
{
alreadyloaded = false;
for (j = 0; j < numwadfiles; ++j)
{
if (memcmp(md5sum, wadfiles[j]->md5sum, 16) == 0)
{
alreadyloaded = true;
break;
}
}
if (alreadyloaded)
continue;
if (numwadfiles >= MAX_WADFILES)
toomany = true;
else
ncs = findfile(filename, md5sum, 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_MD5SUMBAD)
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_AddWadFile(filename);
}
}
}
}
static void G_SkipDemoExtraFiles(UINT8 **pp)
{
UINT8 totalfiles;
UINT8 i;
if (demoversion < 0x0010)
{
// demo has no file list
return;
}
totalfiles = READUINT8((*pp));
for (i = 0; i < totalfiles; ++i)
{
SKIPSTRING((*pp));// file name
(*pp) += 16;// md5
}
}
// 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];
UINT8 md5sum[16];
boolean toomany = false;
boolean alreadyloaded;
UINT8 i, j;
UINT8 error = DFILE_ERROR_NONE;
if (demoversion < 0x0010)
{
// demo has no file list
return DFILE_ERROR_NONE;
}
totalfiles = READUINT8((*pp));
filesloaded = 0;
for (i = 0; i < totalfiles; ++i)
{
if (toomany)
SKIPSTRING((*pp));
else
{
strlcpy(filename, (char *)(*pp), sizeof filename);
SKIPSTRING((*pp));
}
READMEM((*pp), md5sum, 16);
if (!toomany)
{
alreadyloaded = false;
nmusfilecount = 0;
for (j = 0; j < numwadfiles; ++j)
{
if (wadfiles[j]->important && j > mainwads)
nmusfilecount++;
else
continue;
if (memcmp(md5sum, wadfiles[j]->md5sum, 16) == 0)
{
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, md5sum, 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 > mainwads)
nmusfilecount++;
if (!error && filesloaded < nmusfilecount)
error = DFILE_ERROR_EXTRAFILES;
return error;
}
void G_SetDemoTime(UINT32 ptime, UINT32 pscore, UINT16 prings)
{
if (!demorecording || !demotime_p)
@ -1645,7 +1842,7 @@ UINT8 G_CmpDemoTime(char *oldname, char *newname)
p += 2; // gamemap
p += 16; // map md5
flags = READUINT8(p); // demoflags
G_SkipDemoExtraFiles(&p);
aflags = flags & (DF_RECORDATTACK|DF_NIGHTSATTACK);
I_Assert(aflags);
if (flags & DF_RECORDATTACK)
@ -1710,6 +1907,7 @@ UINT8 G_CmpDemoTime(char *oldname, char *newname)
p += 2; // gamemap
p += 16; // mapmd5
flags = READUINT8(p);
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);
@ -1871,6 +2069,73 @@ void G_DoPlayDemo(char *defdemoname)
demo_p += 16; // mapmd5
demoflags = READUINT8(demo_p);
if (demoversion < 0x0010)
{
; // Don't do anything with files.
}
else if (titledemo)
{
// Titledemos should always play and ought to always be compatible with whatever wadlist is running.
G_SkipDemoExtraFiles(&demo_p);
}
else if (demofileoverride == DFILE_OVERRIDE_LOAD)
{
G_LoadDemoExtraFiles(&demo_p);
}
else if (demofileoverride == DFILE_OVERRIDE_SKIP)
{
G_SkipDemoExtraFiles(&demo_p);
}
else
{
UINT8 error = G_CheckDemoExtraFiles(&demo_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);
M_StartMessage(msg, NULL, MM_NOTHING);
Z_Free(pdemoname);
Z_Free(demobuffer);
demoplayback = false;
titledemo = false;
return;
}
}
modeattacking = (demoflags & DF_ATTACKMASK)>>DF_ATTACKSHIFT;
CON_ToggleOff();
@ -2026,6 +2291,91 @@ void G_DoPlayDemo(char *defdemoname)
demo_start = true;
}
//
// Check if a replay can be loaded from the menu
//
UINT8 G_CheckDemoForError(char *defdemoname)
{
lumpnum_t l;
char *n,*pdemoname;
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_ReadFile(defdemoname, &demobuffer))
{
return DFILE_ERROR_NOTDEMO;
}
demo_p = demobuffer;
}
// load demo resource from WAD
else if ((l = W_CheckNumForName(defdemoname)) == LUMPERROR)
{
return DFILE_ERROR_NOTDEMO;
}
else // it's an internal demo
{
demobuffer = demo_p = W_CacheLumpNum(l, PU_STATIC);
}
// read demo header
if (memcmp(demo_p, DEMOHEADER, 12))
{
return DFILE_ERROR_NOTDEMO;
}
demo_p += 12; // DEMOHEADER
demo_p++; // version
demo_p++; // subversion
demoversion = READUINT16(demo_p);
switch(demoversion)
{
case 0x000d:
case 0x000e:
case 0x000f:
case DEMOVERSION: // latest always supported
break;
#ifdef OLD22DEMOCOMPAT
case 0x000c:
break;
#endif
// too old, cannot support.
default:
return DFILE_ERROR_NOTDEMO;
}
demo_p += 16; // demo checksum
if (memcmp(demo_p, "PLAY", 4))
{
return DFILE_ERROR_NOTDEMO;
}
demo_p += 4; // "PLAY"
demo_p += 2; // gamemap
demo_p += 16; // mapmd5
demo_p++; // demoflags
// Don't do anything with files.
if (demoversion < 0x0010)
{
return DFILE_ERROR_NONE;
}
else if (titledemo)
{
return DFILE_ERROR_NONE;
}
return G_CheckDemoExtraFiles(&demo_p, true);
}
void G_AddGhost(char *defdemoname)
{
INT32 i;
@ -2130,6 +2480,9 @@ void G_AddGhost(char *defdemoname)
Z_Free(buffer);
return;
}
G_SkipDemoExtraFiles(&p); // Don't wanna modify the file list for ghosts.
switch ((flags & DF_ATTACKMASK)>>DF_ATTACKSHIFT)
{
case ATTACKING_NONE: // 0

View file

@ -26,6 +26,15 @@
extern boolean demoplayback, titledemo, demorecording, timingdemo;
extern tic_t demostarttime;
typedef enum
{
DFILE_OVERRIDE_NONE = 0, // Show errors normally
DFILE_OVERRIDE_LOAD, // Forcefully load demo, add files beforehand
DFILE_OVERRIDE_SKIP, // Forcefully load demo, skip file list
} demo_file_override_e;
extern demo_file_override_e demofileoverride;
// Quit after playing a demo from cmdline.
extern boolean singledemo;
extern boolean demo_start;
@ -53,6 +62,18 @@ typedef enum
GHC_RETURNSKIN // ditto
} ghostcolor_t;
// G_CheckDemoExtraFiles: checks if our loaded WAD list matches the demo's.
typedef enum
{
DFILE_ERROR_NONE = 0, // No file error
DFILE_ERROR_NOTLOADED, // Files are not loaded, but can be without a restart.
DFILE_ERROR_OUTOFORDER, // Files are loaded, but out of order.
DFILE_ERROR_INCOMPLETEOUTOFORDER, // Some files are loaded out of order, but others are not.
DFILE_ERROR_CANNOTLOAD, // Files are missing and cannot be loaded.
DFILE_ERROR_EXTRAFILES, // Extra files outside of the replay's file list are loaded.
DFILE_ERROR_NOTDEMO = UINT8_MAX, // This replay isn't even a replay...
} demo_file_error_e;
// Record/playback tics
void G_ReadDemoTiccmd(ticcmd_t *cmd, INT32 playernum);
void G_WriteDemoTiccmd(ticcmd_t *cmd, INT32 playernum);
@ -83,5 +104,6 @@ ATTRNORETURN void FUNCNORETURN G_StopMetalRecording(boolean kill);
void G_StopDemo(void);
boolean G_CheckDemoStatus(void);
INT32 G_ConvertOldFrameFlags(INT32 frame);
UINT8 G_CheckDemoForError(char *defdemoname);
#endif // __G_DEMO__

View file

@ -3500,10 +3500,10 @@ boolean M_Responder(event_t *ev)
#ifndef DEVELOP
// TODO: Replays are scary, so I left the remaining instances of this alone.
// It'd be nice to get rid of this once and for all though!
if (((currentMenu->menuitems[itemOn].status & IT_CALLTYPE) & IT_CALL_NOTMODIFIED) && (modifiedgame && !savemoddata) && !usedCheats)
if (((currentMenu->menuitems[itemOn].status & IT_CALLTYPE) & IT_CALL_NOTMODIFIED) && usedCheats)
{
S_StartSound(NULL, sfx_skid);
M_StartMessage(M_GetText("This cannot be done in a modified game.\n\n(Press a key)\n"), NULL, MM_NOTHING);
M_StartMessage(M_GetText("This cannot be done in a cheated game.\n\n(Press a key)\n"), NULL, MM_NOTHING);
return true;
}
#endif
@ -10449,13 +10449,23 @@ static void M_ChooseTimeAttack(INT32 choice)
G_DeferedInitNew(false, G_BuildMapName(cv_nextmap.value), (UINT8)(cv_chooseskin.value-1), false, false);
}
static char ra_demoname[1024];
static void M_StartTimeAttackReplay(INT32 choice)
{
if (choice == 'y' || choice == KEY_ENTER)
{
M_ClearMenus(true);
modeattacking = ATTACKING_RECORD; // set modeattacking before G_DoPlayDemo so the map loader knows
G_DoPlayDemo(ra_demoname);
}
}
// Player has selected the "REPLAY" from the time attack screen
static void M_ReplayTimeAttack(INT32 choice)
{
const char *which;
char *demoname;
M_ClearMenus(true);
modeattacking = ATTACKING_RECORD; // set modeattacking before G_DoPlayDemo so the map loader knows
UINT8 error = DFILE_ERROR_NONE;
if (currentMenu == &SP_ReplayDef)
{
@ -10475,11 +10485,15 @@ static void M_ReplayTimeAttack(INT32 choice)
break;
case 4: // guest
// srb2/replay/main/map01-guest.lmp
G_DoPlayDemo(va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value)));
return;
snprintf(ra_demoname, 1024, "%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value));
break;
}
if (choice != 4)
{
// srb2/replay/main/map01-sonic-time-best.lmp
G_DoPlayDemo(va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), skins[cv_chooseskin.value-1].name, which));
snprintf(ra_demoname, 1024, "%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), skins[cv_chooseskin.value-1].name, which);
}
}
else if (currentMenu == &SP_NightsReplayDef)
{
@ -10495,19 +10509,66 @@ static void M_ReplayTimeAttack(INT32 choice)
which = "last";
break;
case 3: // guest
G_DoPlayDemo(va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value)));
snprintf(ra_demoname, 1024, "%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value));
break;
}
if (choice != 3)
{
snprintf(ra_demoname, 1024, "%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), skins[cv_chooseskin.value-1].name, which);
#ifdef OLDNREPLAYNAME // Check for old style named NiGHTS replay if a new style replay doesn't exist.
if (!FIL_FileExists(ra_demoname))
{
snprintf(ra_demoname, 1024, "%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), which);
}
#endif
}
}
demofileoverride = DFILE_OVERRIDE_NONE;
error = G_CheckDemoForError(ra_demoname);
if (error)
{
S_StartSound(NULL, sfx_skid);
switch (error)
{
case DFILE_ERROR_NOTDEMO:
M_StartMessage(M_GetText("An error occurred loading this replay.\n\n(Press a key)\n"), NULL, MM_NOTHING);
break;
case DFILE_ERROR_NOTLOADED:
demofileoverride = DFILE_OVERRIDE_LOAD;
M_StartMessage(M_GetText("Add-ons for this replay\nhave not been loaded.\n\nAttempt to load files?\n\n(Press 'Y' to confirm)\n"), M_StartTimeAttackReplay, MM_YESNO);
break;
case DFILE_ERROR_OUTOFORDER:
demofileoverride = DFILE_OVERRIDE_SKIP;
M_StartMessage(M_GetText("Add-ons for this replay\nwere loaded out of order.\n\nAttempt to playback anyway?\n\n(Press 'Y' to confirm)\n"), M_StartTimeAttackReplay, MM_YESNO);
break;
case DFILE_ERROR_INCOMPLETEOUTOFORDER:
demofileoverride = DFILE_OVERRIDE_LOAD;
M_StartMessage(M_GetText("Add-ons for this replay\nhave not been loaded,\nand some are in the wrong order.\n\nAttempt to load files?\n\n(Press 'Y' to confirm)\n"), M_StartTimeAttackReplay, MM_YESNO);
break;
case DFILE_ERROR_CANNOTLOAD:
demofileoverride = DFILE_OVERRIDE_SKIP;
M_StartMessage(M_GetText("Add-ons for this replay\ncould not be loaded.\n\nAttempt to playback anyway?\n\n(Press 'Y' to confirm)\n"), M_StartTimeAttackReplay, MM_YESNO);
break;
case DFILE_ERROR_EXTRAFILES:
demofileoverride = DFILE_OVERRIDE_SKIP;
M_StartMessage(M_GetText("You have more files loaded\nthan the replay does.\n\nAttempt to playback anyway?\n\n(Press 'Y' to confirm)\n"), M_StartTimeAttackReplay, MM_YESNO);
break;
}
return;
}
demoname = va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), skins[cv_chooseskin.value-1].name, which);
#ifdef OLDNREPLAYNAME // Check for old style named NiGHTS replay if a new style replay doesn't exist.
if (!FIL_FileExists(demoname))
demoname = va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), which);
#endif
G_DoPlayDemo(demoname);
}
M_StartTimeAttackReplay(KEY_ENTER);
}
static void M_EraseGuest(INT32 choice)