From 77576c3c5d526651074ec6b9f4c664671e0ab273 Mon Sep 17 00:00:00 2001 From: Sally Coolatta Date: Tue, 25 Jul 2023 08:40:36 -0400 Subject: [PATCH] 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. --- src/d_netcmd.c | 19 ++- src/f_finale.c | 1 + src/g_demo.c | 387 ++++++++++++++++++++++++++++++++++++++++++++++--- src/g_demo.h | 22 +++ src/m_menu.c | 93 ++++++++++-- 5 files changed, 487 insertions(+), 35 deletions(-) diff --git a/src/d_netcmd.c b/src/d_netcmd.c index 72d020c22..45a394eff 100644 --- a/src/d_netcmd.c +++ b/src/d_netcmd.c @@ -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 : playback a demo\n")); + CONS_Printf("playdemo [-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)) diff --git a/src/f_finale.c b/src/f_finale.c index e59b43472..d03795dc4 100644 --- a/src/f_finale.c +++ b/src/f_finale.c @@ -3509,6 +3509,7 @@ void F_TitleScreenTicker(boolean run) } titledemo = true; + demofileoverride = DFILE_OVERRIDE_NONE; G_DoPlayDemo(dname); } } diff --git a/src/g_demo.c b/src/g_demo.c index 0403da16d..230f6da2d 100644 --- a/src/g_demo.c +++ b/src/g_demo.c @@ -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,23 +1441,43 @@ 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 - break; - case ATTACKING_RECORD: // 1 - demotime_p = demo_p; - WRITEUINT32(demo_p,UINT32_MAX); // time - WRITEUINT32(demo_p,0); // score - WRITEUINT16(demo_p,0); // rings - break; - case ATTACKING_NIGHTS: // 2 - demotime_p = demo_p; - WRITEUINT32(demo_p,UINT32_MAX); // time - WRITEUINT32(demo_p,0); // score - break; - default: // 3 - break; + case ATTACKING_NONE: // 0 + break; + case ATTACKING_RECORD: // 1 + demotime_p = demo_p; + WRITEUINT32(demo_p,UINT32_MAX); // time + WRITEUINT32(demo_p,0); // score + WRITEUINT16(demo_p,0); // rings + break; + case ATTACKING_NIGHTS: // 2 + demotime_p = demo_p; + WRITEUINT32(demo_p,UINT32_MAX); // time + WRITEUINT32(demo_p,0); // score + break; + default: // 3 + break; } WRITEUINT32(demo_p,P_GetInitSeed()); @@ -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 diff --git a/src/g_demo.h b/src/g_demo.h index f25315a58..383d2719b 100644 --- a/src/g_demo.h +++ b/src/g_demo.h @@ -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__ diff --git a/src/m_menu.c b/src/m_menu.c index 12003f945..828fc1aee 100644 --- a/src/m_menu.c +++ b/src/m_menu.c @@ -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 + 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); } - // 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)); } 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))); - return; + snprintf(ra_demoname, 1024, "%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-guest.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value)); + break; } - 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); + 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(demoname)) - demoname = va("%s"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s.lmp", srb2home, timeattackfolder, G_BuildMapName(cv_nextmap.value), which); + 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 - - G_DoPlayDemo(demoname); + } } + + 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; + } + + M_StartTimeAttackReplay(KEY_ENTER); } static void M_EraseGuest(INT32 choice)