RingRacers/src/menus/play-local-race-time-attack.c
toaster b70e59755f Tie SPB Attack and Encore together
- A method to access Encore in Time Attack
    - so we have SOME Encore singleplayer content on launch
- Also available for Versus mode (in Special Attack)
    - Finally, Encore rematches have a way to access them!
    - Obviously will not spawn a chasing SPB, it's just a signal for "hard mode"
- Relevant gametype + unlock checks have been abstracted into M_EncoreAttackTogglePermitted
2024-02-17 23:11:58 +00:00

625 lines
16 KiB
C

/// \file menus/play-local-race-time-attack.c
/// \brief Race Time Attack Menu
#include "../k_menu.h"
#include "../r_local.h" // SplitScreen_OnChange
#include "../s_sound.h"
#include "../f_finale.h" // F_WipeStartScreen
#include "../v_video.h"
#include "../d_main.h" // srb2home
#include "../m_misc.h" // M_MkdirEach
#include "../z_zone.h" // Z_StrDup/Z_Free
#include "../m_cond.h"
struct timeattackmenu_s timeattackmenu;
void M_TimeAttackTick(void)
{
timeattackmenu.ticker++;
if (timeattackmenu.spbflicker > 0)
{
timeattackmenu.spbflicker--;
}
}
boolean M_EncoreAttackTogglePermitted(void)
{
if ((gametypes[levellist.newgametype]->rules & GTR_ENCORE) == 0) //levellist.newgametype != GT_RACE
return false;
return M_SecretUnlocked(SECRET_SPBATTACK, true);
}
boolean M_TimeAttackInputs(INT32 ch)
{
const UINT8 pid = 0;
const boolean buttonR = M_MenuButtonPressed(pid, MBT_R);
(void) ch;
if (buttonR && M_EncoreAttackTogglePermitted())
{
CV_AddValue(&cv_dummyspbattack, 1);
timeattackmenu.spbflicker = TICRATE/6;
if (cv_dummyspbattack.value)
{
S_StartSound(NULL, sfx_s3k9f);
S_StopSoundByID(NULL, sfx_s3k92);
}
else
{
S_StartSound(NULL, sfx_s3k92);
S_StopSoundByID(NULL, sfx_s3k9f);
}
return true;
}
if (menucmd[pid].dpad_lr != 0
&& !((currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_ARROWS
|| (currentMenu->menuitems[itemOn].status & IT_TYPE) == IT_CVAR)
)
{
if (menucmd[pid].dpad_lr > 0)
{
levellist.cursor++;
if (levellist.cursor >= levellist.mapcount)
{
M_LevelSelectCupSwitch(true, false);
levellist.cursor = 0;
}
}
else
{
levellist.cursor--;
if (levellist.cursor < 0)
{
M_LevelSelectCupSwitch(false, false);
levellist.cursor = levellist.mapcount-1;
}
}
M_LevelSelectScrollDest();
levellist.slide.start = 0;
M_LevelSelected(levellist.cursor, false);
itemOn = ta_start;
S_StartSound(NULL, sfx_s3k5b);
M_SetMenuDelay(pid);
return true;
}
return false;
}
// see ta_e
menuitem_t PLAY_TimeAttack[] =
{
{IT_STRING | IT_SUBMENU, "Replay...", NULL, "MENUI006", {.submenu = &PLAY_TAReplayDef}, 0, 0},
{IT_STRING | IT_SUBMENU, "Guest...", NULL, "MENUI006", {.submenu = &PLAY_TAReplayGuestDef}, 0, 0},
{IT_STRING | IT_SUBMENU, "Ghosts...", NULL, "MENUI006", {.submenu = &PLAY_TAGhostsDef}, 0, 0},
{IT_HEADERTEXT|IT_HEADER, "", NULL, NULL, {NULL}, 0, 0},
{IT_STRING | IT_CALL, "Start", NULL, "MENUI006", {.routine = M_StartTimeAttack}, 0, 0},
};
menu_t PLAY_TimeAttackDef = {
sizeof(PLAY_TimeAttack) / sizeof(menuitem_t),
&PLAY_LevelSelectDef,
0,
PLAY_TimeAttack,
0, 0,
0, 0,
0,
NULL,
2, 5,
M_DrawTimeAttack,
NULL,
M_TimeAttackTick,
NULL,
NULL,
M_TimeAttackInputs
};
typedef enum
{
tareplay_header = 0,
tareplay_besttime,
tareplay_bestlap,
tareplay_last,
tareplay_gap1,
tareplay_guest,
tareplay_staff,
tareplay_gap2,
tareplay_back
} tareplay_e;
menuitem_t PLAY_TAReplay[] =
{
{IT_HEADERTEXT|IT_HEADER, "<!>", NULL, NULL, {NULL}, 0, 0},
{IT_STRING | IT_CALL, "Replay Best Time", NULL, "MENUI006", {.routine = M_ReplayTimeAttack}, 0, 0},
{IT_STRING | IT_CALL, "Replay Best Lap", NULL, "MENUI006", {.routine = M_ReplayTimeAttack}, 0, 0},
{IT_STRING | IT_CALL, "Replay Last", NULL, "MENUI006", {.routine = M_ReplayTimeAttack}, 0, 0},
{IT_HEADERTEXT|IT_HEADER, "", NULL, NULL, {NULL}, 0, 0},
{IT_STRING | IT_CALL, "Replay Guest", NULL, "MENUI006", {.routine = M_ReplayTimeAttack}, 0, 0},
{IT_STRING | IT_ARROWS, "Replay Staff", NULL, "MENUI006", {.routine = M_HandleStaffReplay}, 0, 0},
{IT_HEADERTEXT|IT_HEADER, "", NULL, NULL, {NULL}, 0, 0},
{IT_STRING | IT_SUBMENU, "Back", NULL, "MENUI006", {.submenu = &PLAY_TimeAttackDef}, 0, 0},
};
menu_t PLAY_TAReplayDef = {
sizeof(PLAY_TAReplay) / sizeof(menuitem_t),
&PLAY_TimeAttackDef,
0,
PLAY_TAReplay,
0, 0,
0, 0,
0,
NULL,
2, 5,
M_DrawTimeAttack,
NULL,
M_TimeAttackTick,
NULL,
NULL,
NULL
};
typedef enum
{
taguest_header = 0,
taguest_besttime,
taguest_bestlap,
taguest_last,
taguest_gap1,
taguest_delete,
taguest_gap2,
taguest_back
} taguest_e;
menuitem_t PLAY_TAReplayGuest[] =
{
{IT_HEADERTEXT|IT_HEADER, "Save as guest...", NULL, "MENUI006", {NULL}, 0, 0},
{IT_STRING | IT_CALL, "Best Time", NULL, "MENUI006", {.routine = M_SetGuestReplay}, 0, 0},
{IT_STRING | IT_CALL, "Best Lap", NULL, "MENUI006", {.routine = M_SetGuestReplay}, 0, 0},
{IT_STRING | IT_CALL, "Last Run", NULL, "MENUI006", {.routine = M_SetGuestReplay}, 0, 0},
{IT_HEADERTEXT|IT_HEADER, "", NULL, NULL, {NULL}, 0, 0},
{IT_STRING | IT_CALL, "Delete Guest", NULL, "MENUI006", {.routine = M_SetGuestReplay}, 0, 0},
{IT_HEADERTEXT|IT_HEADER, "", NULL, NULL, {NULL}, 0, 0},
{IT_STRING | IT_SUBMENU, "Back", NULL, "MENUI006", {.submenu = &PLAY_TimeAttackDef}, 0, 0},
};
menu_t PLAY_TAReplayGuestDef = {
sizeof(PLAY_TAReplayGuest) / sizeof(menuitem_t),
&PLAY_TimeAttackDef,
0,
PLAY_TAReplayGuest,
0, 0,
0, 0,
0,
NULL,
2, 5,
M_DrawTimeAttack,
NULL,
M_TimeAttackTick,
NULL,
NULL,
NULL
};
typedef enum
{
taghost_besttime = 0,
taghost_bestlap,
taghost_last,
taghost_guest,
taghost_staff,
taghost_gap1,
taghost_back
} taghost_e;
menuitem_t PLAY_TAGhosts[] =
{
{IT_STRING | IT_CVAR, "Best Time", NULL, "MENUI006", {.cvar = &cv_ghost_besttime}, 0, 0},
{IT_STRING | IT_CVAR, "Best Lap", NULL, "MENUI006", {.cvar = &cv_ghost_bestlap}, 0, 0},
{IT_STRING | IT_CVAR, "Last", NULL, "MENUI006", {.cvar = &cv_ghost_last}, 0, 0},
{IT_DISABLED, "Guest", NULL, "MENUI006", {.cvar = &cv_ghost_guest}, 0, 0},
{IT_DISABLED, "Staff", NULL, "MENUI006", {.cvar = &cv_ghost_staff}, 0, 0},
{IT_HEADERTEXT|IT_HEADER, "", NULL, NULL, {NULL}, 0, 0},
{IT_STRING | IT_SUBMENU, "Back", NULL, "MENUI006", {.submenu = &PLAY_TimeAttackDef}, 0, 0},
};
menu_t PLAY_TAGhostsDef = {
sizeof(PLAY_TAGhosts) / sizeof(menuitem_t),
&PLAY_TimeAttackDef,
0,
PLAY_TAGhosts,
0, 0,
0, 0,
0,
NULL,
2, 5,
M_DrawTimeAttack,
NULL,
M_TimeAttackTick,
NULL,
NULL,
NULL
};
void CV_SPBAttackChanged(void);
void CV_SPBAttackChanged(void)
{
G_UpdateTimeStickerMedals(levellist.choosemap, false);
// Menu options
{
// see also p_setup.c's P_LoadRecordGhosts
char *gpath = Z_StrDup(va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s", srb2home, timeattackfolder, G_BuildMapName(levellist.choosemap+1)));
const char *modeprefix = "";
UINT8 active;
if (!gpath)
return;
if (cv_dummyspbattack.value)
modeprefix = "spb-";
active = false;
PLAY_TimeAttack[ta_guest].status = IT_DISABLED;
PLAY_TimeAttack[ta_replay].status = IT_DISABLED;
PLAY_TimeAttack[ta_ghosts].status = IT_DISABLED;
PLAY_TAReplay[tareplay_header].status = IT_DISABLED;
// Check if file exists, if not, disable options
PLAY_TAReplay[tareplay_besttime].status =
PLAY_TAReplayGuest[taguest_besttime].status = IT_DISABLED;
if (FIL_FileExists(va("%s-%s-%stime-best.lmp", gpath, cv_skin[0].string, modeprefix)))
{
PLAY_TAReplay[tareplay_besttime].status = IT_STRING|IT_CALL;
PLAY_TAReplayGuest[taguest_besttime].status = IT_STRING|IT_CALL;
active |= (1|2|4|8);
}
else if (PLAY_TAReplayGuestDef.lastOn == taguest_besttime)
PLAY_TAReplayGuestDef.lastOn = taguest_back;
PLAY_TAReplay[tareplay_bestlap].status =
PLAY_TAReplayGuest[taguest_bestlap].status =
PLAY_TAGhosts[taghost_bestlap].status = IT_DISABLED;
if ((gametypes[levellist.newgametype]->rules & GTR_CIRCUIT)
&& (mapheaderinfo[levellist.choosemap]->numlaps != 1)
&& FIL_FileExists(va("%s-%s-%slap-best.lmp", gpath, cv_skin[0].string, modeprefix)))
{
PLAY_TAReplay[tareplay_bestlap].status = IT_STRING|IT_CALL;
PLAY_TAReplayGuest[taguest_bestlap].status = IT_STRING|IT_CALL;
PLAY_TAGhosts[taghost_bestlap].status = IT_STRING|IT_CVAR;
active |= (1|2|4|8);
}
else if (PLAY_TAReplayGuestDef.lastOn == taguest_bestlap)
PLAY_TAReplayGuestDef.lastOn = taguest_back;
PLAY_TAReplay[tareplay_last].status =
PLAY_TAReplayGuest[taguest_last].status = IT_DISABLED;
if (FIL_FileExists(va("%s-%s-%slast.lmp", gpath, cv_skin[0].string, modeprefix)))
{
PLAY_TAReplay[tareplay_last].status = IT_STRING|IT_CALL;
PLAY_TAReplayGuest[taguest_last].status = IT_STRING|IT_CALL;
active |= (1|2|4|8);
}
else if (PLAY_TAReplayGuestDef.lastOn == taguest_last)
PLAY_TAReplayGuestDef.lastOn = taguest_back;
PLAY_TAReplay[tareplay_guest].status =
PLAY_TAGhosts[taghost_guest].status =
PLAY_TAReplayGuest[taguest_delete].status = IT_DISABLED;
if (FIL_FileExists(va("%s-%sguest.lmp", gpath, modeprefix)))
{
PLAY_TAReplay[tareplay_guest].status = IT_STRING|IT_CALL;
PLAY_TAReplayGuest[taguest_delete].status = IT_STRING|IT_CALL;
PLAY_TAGhosts[taghost_guest].status = IT_STRING|IT_CVAR;
active |= (1|2|4);
}
else if (PLAY_TAReplayGuestDef.lastOn == taguest_delete)
PLAY_TAReplayGuestDef.lastOn = taguest_back;
PLAY_TAReplay[tareplay_staff].status =
PLAY_TAGhosts[taghost_staff].status = IT_DISABLED;
if (mapheaderinfo[levellist.choosemap]->ghostCount > 0 && !modeprefix[0])
{
PLAY_TAReplay[tareplay_staff].status = IT_STRING|IT_ARROWS;
PLAY_TAGhosts[taghost_staff].status = IT_STRING|IT_CVAR;
CV_SetValue(&cv_dummystaff, 0);
active |= 1|4;
}
if (currentMenu == &PLAY_TimeAttackDef)
PLAY_TimeAttackDef.lastOn = itemOn;
if (active & 1)
PLAY_TimeAttack[ta_replay].status = IT_STRING|IT_SUBMENU;
else if (PLAY_TimeAttackDef.lastOn == ta_replay)
PLAY_TimeAttackDef.lastOn = ta_start;
if (active & 2)
PLAY_TimeAttack[ta_guest].status = IT_STRING|IT_SUBMENU;
else if (PLAY_TimeAttackDef.lastOn == ta_guest)
PLAY_TimeAttackDef.lastOn = ta_start;
//if (active & 4) -- for possible future use
PLAY_TimeAttack[ta_ghosts].status = IT_STRING|IT_SUBMENU;
/*else if (PLAY_TimeAttackDef.lastOn == ta_ghosts)
PLAY_TimeAttackDef.lastOn = ta_start;*/
if ((active & 8) && M_EncoreAttackTogglePermitted())
{
PLAY_TAReplay[tareplay_header].status = IT_HEADER;
PLAY_TAReplay[tareplay_header].text = cv_dummyspbattack.value ? "SPB Attack..." : "Time Attack...";
}
if (currentMenu == &PLAY_TimeAttackDef)
itemOn = PLAY_TimeAttackDef.lastOn;
Z_Free(gpath);
}
}
/// time attack stuff...
void M_PrepareTimeAttack(boolean menuupdate)
{
if (menuupdate)
{
timeattackmenu.ticker = 0;
}
// Gametype guess
if (levellist.guessgt != MAXGAMETYPES)
{
levellist.newgametype = levellist.guessgt;
if (!(gametypes[levellist.newgametype]->tol & mapheaderinfo[levellist.choosemap]->typeoflevel))
{
INT32 guess = G_GuessGametypeByTOL(mapheaderinfo[levellist.choosemap]->typeoflevel);
if (guess != -1)
levellist.newgametype = guess;
}
}
if (cv_dummyspbattack.value
&& (levellist.levelsearch.timeattack == false || !M_EncoreAttackTogglePermitted()))
{
CV_StealthSetValue(&cv_dummyspbattack, 0);
if (!menuupdate)
{
timeattackmenu.spbflicker = TICRATE/6;
S_StartSound(NULL, sfx_s3k92);
S_StopSoundByID(NULL, sfx_s3k9f);
}
}
// Menu options / Time-sticker medals
CV_SPBAttackChanged();
}
void M_HandleStaffReplay(INT32 choice)
{
if (choice == 2)
{
mapheader_t *mapheader;
staffbrief_t *staffbrief;
const char* lumpname = NULL;
restoreMenu = &PLAY_TimeAttackDef;
M_ClearMenus(true);
demo.loadfiles = false;
demo.ignorefiles = true; // Just assume that record attack replays have the files needed
mapheader = mapheaderinfo[levellist.choosemap];
staffbrief = mapheader->ghostBrief[cv_dummystaff.value];
lumpname = W_CheckNameForNumPwad(staffbrief->wad, staffbrief->lump);
G_DoPlayDemo(lumpname);
return;
}
M_ChangeCvarDirect(choice, &cv_dummystaff);
}
void M_ReplayTimeAttack(INT32 choice)
{
const char *which;
restoreMenu = &PLAY_TimeAttackDef;
M_ClearMenus(true);
demo.loadfiles = false;
demo.ignorefiles = true; // Just assume that record attack replays have the files needed
const char *modeprefix = "";
if (cv_dummyspbattack.value)
modeprefix = "spb-";
switch (choice)
{
default:
case tareplay_besttime:
which = "time-best";
break;
case tareplay_bestlap:
which = "lap-best";
break;
case tareplay_last:
which = "last";
break;
case tareplay_guest:
G_DoPlayDemo(va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%sguest.lmp", srb2home, timeattackfolder, G_BuildMapName(levellist.choosemap+1), modeprefix));
return;
}
G_DoPlayDemo(va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%s-%s%s.lmp", srb2home, timeattackfolder, G_BuildMapName(levellist.choosemap+1), cv_skin[0].string, modeprefix, which));
}
static const char *TA_GuestReplay_Str = NULL;
static void M_WriteGuestReplay(INT32 ch)
{
char *gpath, *rguest;
UINT8 *buf;
size_t len = 0;
if (ch != MA_YES)
return;
gpath = Z_StrDup(va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s", srb2home, timeattackfolder, G_BuildMapName(levellist.choosemap+1)));
const char *modeprefix = "";
if (cv_dummyspbattack.value)
modeprefix = "spb-";
if (TA_GuestReplay_Str != NULL)
{
len = FIL_ReadFile(va("%s-%s-%s%s.lmp", gpath, cv_skin[0].string, modeprefix, TA_GuestReplay_Str), &buf);
if (!len)
{
M_StartMessage("Guest Replay", "Replay to copy no longer exists!", NULL, MM_NOTHING, NULL, NULL);
Z_Free(gpath);
return;
}
}
rguest = Z_StrDup(va("%s-%sguest.lmp", gpath, modeprefix));
if (FIL_FileExists(rguest))
{
remove(rguest);
}
if (len)
{
FIL_WriteFile(rguest, buf, len);
}
Z_Free(rguest);
Z_Free(gpath);
M_PrepareTimeAttack(false);
M_SetupNextMenu(&PLAY_TimeAttackDef, false);
// TODO the following isn't showing up and I'm not sure why
//M_StartMessage("Guest Replay", va("Guest replay data %s.", (len ? "saved" : "erased")), NULL, MM_NOTHING, NULL, NULL);
}
void M_SetGuestReplay(INT32 choice)
{
switch (choice)
{
case taguest_besttime:
TA_GuestReplay_Str = "time-best";
break;
case taguest_bestlap:
TA_GuestReplay_Str = "lap-best";
break;
case taguest_last:
TA_GuestReplay_Str = "last";
break;
case taguest_delete:
default:
TA_GuestReplay_Str = NULL;
break;
}
const char *modeprefix = "";
if (cv_dummyspbattack.value)
modeprefix = "spb-";
if (FIL_FileExists(va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s"PATHSEP"%s-%sguest.lmp", srb2home, timeattackfolder, G_BuildMapName(levellist.choosemap+1), modeprefix)))
{
M_StartMessage("Guest Replay", va("Are you sure you want to\n%s the guest replay data?\n", (TA_GuestReplay_Str != NULL ? "overwrite" : "delete")), &M_WriteGuestReplay, MM_YESNO, NULL, NULL);
}
else if (TA_GuestReplay_Str != NULL)
{
M_WriteGuestReplay(MA_YES);
}
}
void M_StartTimeAttack(INT32 choice)
{
char *gpath;
char nameofdemo[256];
const char *modeprefix = "";
(void)choice;
modeattacking = ATTACKING_TIME;
if ((gametypes[levellist.newgametype]->rules & GTR_CIRCUIT)
&& (mapheaderinfo[levellist.choosemap]->numlaps != 1))
{
modeattacking |= ATTACKING_LAP;
}
if (cv_dummyspbattack.value)
{
if (levellist.newgametype == GT_RACE)
{
modeattacking |= ATTACKING_SPB;
}
modeprefix = "spb-";
}
// Still need to reset devmode
cht_debug = 0;
if (demo.playback)
G_StopDemo();
splitscreen = 0;
SplitScreen_OnChange();
S_StartSound(NULL, sfx_s3k63);
paused = false;
S_StopMusicCredit();
// Early fadeout to let the sound finish playing
F_WipeStartScreen();
V_DrawFill(0, 0, BASEVIDWIDTH, BASEVIDHEIGHT, 31);
F_WipeEndScreen();
F_RunWipe(wipe_level_toblack, wipedefs[wipe_level_toblack], false, "FADEMAP0", false, false);
SV_StartSinglePlayerServer(levellist.newgametype, false);
gpath = va("%s"PATHSEP"media"PATHSEP"replay"PATHSEP"%s",
srb2home, timeattackfolder);
M_MkdirEach(gpath, M_PathParts(gpath) - 3, 0755);
strcat(gpath, PATHSEP);
strcat(gpath, G_BuildMapName(levellist.choosemap+1));
snprintf(nameofdemo, sizeof nameofdemo, "%s-%s-%slast", gpath, cv_skin[0].string, modeprefix);
if (!cv_autorecord.value)
remove(va("%s"PATHSEP"%s.lmp", srb2home, nameofdemo));
else
G_RecordDemo(nameofdemo);
restoreMenu = &PLAY_TimeAttackDef;
M_ClearMenus(true);
D_MapChange(levellist.choosemap+1, levellist.newgametype, (cv_dummyspbattack.value == 1), 1, 1, false, false);
G_UpdateTimeStickerMedals(levellist.choosemap, true);
}