mirror of
https://github.com/KartKrewDev/RingRacers.git
synced 2026-04-22 02:00:11 +00:00
Merge branch 'egg-tv' into 'master'
Egg TV See merge request KartKrew/Kart!1209
This commit is contained in:
commit
1392b04b7a
18 changed files with 2470 additions and 727 deletions
|
|
@ -254,6 +254,8 @@ if(SRB2_CONFIG_ENABLE_WEBM_MOVIES)
|
|||
target_compile_definitions(SRB2SDL2 PRIVATE -DSRB2_CONFIG_ENABLE_WEBM_MOVIES)
|
||||
endif()
|
||||
|
||||
target_link_libraries(SRB2SDL2 PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
set(SRB2_HAVE_THREADS ON)
|
||||
target_compile_definitions(SRB2SDL2 PRIVATE -DHAVE_THREADS)
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ struct demovars_s {
|
|||
boolean rewinding; // Rewind in progress
|
||||
|
||||
boolean loadfiles, ignorefiles; // Demo file loading options
|
||||
boolean inreplayhut; // Go back to replayhut after demos
|
||||
boolean quitafterplaying; // quit after playing a demo from cmdline
|
||||
boolean deferstart; // don't start playing demo right away
|
||||
boolean netgame; // multiplayer netgame
|
||||
|
|
|
|||
19
src/k_menu.h
19
src/k_menu.h
|
|
@ -405,11 +405,9 @@ extern menu_t OPTIONS_DataProfileEraseDef;
|
|||
extern menuitem_t EXTRAS_Main[];
|
||||
extern menu_t EXTRAS_MainDef;
|
||||
|
||||
extern menuitem_t EXTRAS_ReplayHut[];
|
||||
extern menu_t EXTRAS_ReplayHutDef;
|
||||
|
||||
extern menuitem_t EXTRAS_ReplayStart[];
|
||||
extern menu_t EXTRAS_ReplayStartDef;
|
||||
extern menuitem_t EXTRAS_EggTV[];
|
||||
extern menu_t EXTRAS_EggTVDef;
|
||||
|
||||
// PAUSE
|
||||
extern menuitem_t PAUSE_Main[];
|
||||
|
|
@ -1033,11 +1031,10 @@ void M_ExtrasTick(void);
|
|||
boolean M_ExtrasInputs(INT32 ch);
|
||||
boolean M_ExtrasQuit(void); // resets buttons when you quit
|
||||
|
||||
// Extras: Replay Hut
|
||||
void M_HandleReplayHutList(INT32 choice);
|
||||
boolean M_QuitReplayHut(void);
|
||||
void M_HutStartReplay(INT32 choice);
|
||||
void M_PrepReplayList(void);
|
||||
|
||||
// Extras: Egg TV
|
||||
void M_EggTV(INT32 choice);
|
||||
void M_EggTV_RefreshButtonLabels(void);
|
||||
|
||||
|
||||
// Pause menu:
|
||||
|
|
@ -1083,8 +1080,6 @@ void M_PlaybackAdjustView(INT32 choice);
|
|||
void M_PlaybackToggleFreecam(INT32 choice);
|
||||
void M_PlaybackQuit(INT32 choice);
|
||||
|
||||
void M_ReplayHut(INT32 choice);
|
||||
|
||||
// Misc menus:
|
||||
#define numaddonsshown 4
|
||||
void M_Addons(INT32 choice);
|
||||
|
|
@ -1149,8 +1144,6 @@ extern tic_t shitsfree;
|
|||
// Extras menu:
|
||||
void M_DrawExtrasMovingButton(void);
|
||||
void M_DrawExtras(void);
|
||||
void M_DrawReplayHut(void);
|
||||
void M_DrawReplayStartMenu(void);
|
||||
|
||||
// Misc menus:
|
||||
#define LOCATIONSTRING1 "Visit \x83SRB2.ORG/MODS\x80 to get & make addons!"
|
||||
|
|
|
|||
402
src/k_menudraw.c
402
src/k_menudraw.c
|
|
@ -4160,408 +4160,6 @@ void M_DrawPlaybackMenu(void)
|
|||
}
|
||||
|
||||
|
||||
// replay hut...
|
||||
// ...dear lord this is messy, but Ima be real I ain't fixing this.
|
||||
|
||||
#define SCALEDVIEWWIDTH (vid.width/vid.dupx)
|
||||
#define SCALEDVIEWHEIGHT (vid.height/vid.dupy)
|
||||
static void M_DrawReplayHutReplayInfo(menudemo_t *demoref)
|
||||
{
|
||||
patch_t *patch = NULL;
|
||||
UINT8 *colormap;
|
||||
INT32 x, y;
|
||||
|
||||
switch (demoref->type)
|
||||
{
|
||||
case MD_NOTLOADED:
|
||||
V_DrawCenteredString(160, 40, 0, "Loading replay information...");
|
||||
break;
|
||||
|
||||
case MD_INVALID:
|
||||
V_DrawCenteredString(160, 40, warningflags, "This replay cannot be played.");
|
||||
break;
|
||||
|
||||
case MD_SUBDIR:
|
||||
break; // Can't think of anything to draw here right now
|
||||
|
||||
case MD_OUTDATED:
|
||||
V_DrawThinString(17, 64, V_ALLOWLOWERCASE|V_TRANSLUCENT|highlightflags, "Recorded on an outdated version.");
|
||||
/* FALLTHRU */
|
||||
default:
|
||||
// Draw level stuff
|
||||
x = 15; y = 15;
|
||||
|
||||
K_DrawMapThumbnail(
|
||||
x<<FRACBITS, y<<FRACBITS,
|
||||
80<<FRACBITS,
|
||||
((demoref->kartspeed & DF_ENCORE) ? V_FLIP : 0),
|
||||
demoref->map,
|
||||
NULL);
|
||||
|
||||
if (demoref->kartspeed & DF_ENCORE)
|
||||
{
|
||||
static angle_t rubyfloattime = 0;
|
||||
const fixed_t rubyheight = FINESINE(rubyfloattime>>ANGLETOFINESHIFT);
|
||||
V_DrawFixedPatch((x+40)<<FRACBITS, ((y+25)<<FRACBITS) - (rubyheight<<1), FRACUNIT, 0, W_CachePatchName("RUBYICON", PU_CACHE), NULL);
|
||||
rubyfloattime += FixedMul(ANGLE_MAX/NEWTICRATE, renderdeltatics);
|
||||
}
|
||||
|
||||
x += 85;
|
||||
|
||||
if (demoref->map < nummapheaders && mapheaderinfo[demoref->map])
|
||||
{
|
||||
char *title = G_BuildMapTitle(demoref->map+1);
|
||||
V_DrawString(x, y, 0, title);
|
||||
Z_Free(title);
|
||||
}
|
||||
else
|
||||
V_DrawString(x, y, V_ALLOWLOWERCASE|V_TRANSLUCENT, "Level is not loaded.");
|
||||
|
||||
if (demoref->numlaps)
|
||||
V_DrawThinString(x, y+9, V_ALLOWLOWERCASE, va("(%d laps)", demoref->numlaps));
|
||||
|
||||
{
|
||||
const char *gtstring;
|
||||
if (demoref->gametype < 0)
|
||||
{
|
||||
gtstring = "Custom (not loaded)";
|
||||
}
|
||||
else
|
||||
{
|
||||
gtstring = gametypes[demoref->gametype]->name;
|
||||
|
||||
if ((gametypes[demoref->gametype]->rules & GTR_CIRCUIT))
|
||||
gtstring = va("%s (%s)", gtstring, kartspeed_cons_t[(demoref->kartspeed & ~DF_ENCORE) + 1].strvalue);
|
||||
}
|
||||
|
||||
V_DrawString(x, y+20, V_ALLOWLOWERCASE, gtstring);
|
||||
}
|
||||
|
||||
if (!demoref->standings[0].ranking)
|
||||
{
|
||||
// No standings were loaded!
|
||||
V_DrawString(x, y+39, V_ALLOWLOWERCASE|V_TRANSLUCENT, "No standings available.");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
V_DrawThinString(x, y+29, highlightflags, "WINNER");
|
||||
V_DrawString(x+38, y+30, V_ALLOWLOWERCASE, demoref->standings[0].name);
|
||||
|
||||
if (demoref->gametype >= 0)
|
||||
{
|
||||
if (gametypes[demoref->gametype]->rules & GTR_POINTLIMIT)
|
||||
{
|
||||
V_DrawThinString(x, y+39, highlightflags, "SCORE");
|
||||
}
|
||||
else
|
||||
{
|
||||
V_DrawThinString(x, y+39, highlightflags, "TIME");
|
||||
}
|
||||
|
||||
if (demoref->standings[0].timeorscore == (UINT32_MAX-1))
|
||||
{
|
||||
V_DrawThinString(x+32, y+39, 0, "NO CONTEST");
|
||||
}
|
||||
else if (gametypes[demoref->gametype]->rules & GTR_POINTLIMIT)
|
||||
{
|
||||
V_DrawString(x+32, y+40, 0, va("%d", demoref->standings[0].timeorscore));
|
||||
}
|
||||
else
|
||||
{
|
||||
V_DrawRightAlignedString(x+84, y+40, 0, va("%d'%02d\"%02d",
|
||||
G_TicsToMinutes(demoref->standings[0].timeorscore, true),
|
||||
G_TicsToSeconds(demoref->standings[0].timeorscore),
|
||||
G_TicsToCentiseconds(demoref->standings[0].timeorscore)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Character face!
|
||||
|
||||
// Lat: 08/06/2020: For some reason missing skins have their value set to 255 (don't even ask me why I didn't write this)
|
||||
// and for an even STRANGER reason this passes the first check below, so we're going to make sure that the skin here ISN'T 255 before we do anything stupid.
|
||||
|
||||
if (demoref->standings[0].skin < numskins)
|
||||
{
|
||||
patch = faceprefix[demoref->standings[0].skin][FACE_WANTED];
|
||||
colormap = R_GetTranslationColormap(
|
||||
demoref->standings[0].skin,
|
||||
demoref->standings[0].color,
|
||||
GTC_MENUCACHE);
|
||||
}
|
||||
else
|
||||
{
|
||||
patch = W_CachePatchName("M_NOWANT", PU_CACHE);
|
||||
colormap = R_GetTranslationColormap(
|
||||
TC_RAINBOW,
|
||||
demoref->standings[0].color,
|
||||
GTC_MENUCACHE);
|
||||
}
|
||||
|
||||
V_DrawMappedPatch(BASEVIDWIDTH-15 - SHORT(patch->width), y+20, 0, patch, colormap);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void M_DrawReplayHut(void)
|
||||
{
|
||||
INT32 x, y, cursory = 0;
|
||||
INT16 i;
|
||||
INT16 replaylistitem = currentMenu->numitems-2;
|
||||
boolean processed_one_this_frame = false;
|
||||
|
||||
static UINT16 replayhutmenuy = 0;
|
||||
|
||||
M_DrawEggaChannel();
|
||||
|
||||
// Draw menu choices
|
||||
x = currentMenu->x;
|
||||
y = currentMenu->y;
|
||||
|
||||
if (itemOn > replaylistitem)
|
||||
{
|
||||
itemOn = replaylistitem;
|
||||
dir_on[menudepthleft] = sizedirmenu-1;
|
||||
extrasmenu.replayScrollTitle = 0; extrasmenu.replayScrollDelay = TICRATE; extrasmenu.replayScrollDir = 1;
|
||||
}
|
||||
else if (itemOn < replaylistitem)
|
||||
{
|
||||
dir_on[menudepthleft] = 0;
|
||||
extrasmenu.replayScrollTitle = 0; extrasmenu.replayScrollDelay = TICRATE; extrasmenu.replayScrollDir = 1;
|
||||
}
|
||||
|
||||
if (itemOn == replaylistitem)
|
||||
{
|
||||
INT32 maxy;
|
||||
// Scroll menu items if needed
|
||||
cursory = y + currentMenu->menuitems[replaylistitem].mvar1 + dir_on[menudepthleft]*10;
|
||||
maxy = y + currentMenu->menuitems[replaylistitem].mvar1 + sizedirmenu*10;
|
||||
|
||||
if (cursory > maxy - 20)
|
||||
cursory = maxy - 20;
|
||||
|
||||
if (cursory - replayhutmenuy > SCALEDVIEWHEIGHT-50)
|
||||
replayhutmenuy += (cursory-SCALEDVIEWHEIGHT-replayhutmenuy + 51)/2;
|
||||
else if (cursory - replayhutmenuy < 110)
|
||||
replayhutmenuy += (max(0, cursory-110)-replayhutmenuy - 1)/2;
|
||||
}
|
||||
else
|
||||
replayhutmenuy /= 2;
|
||||
|
||||
y -= replayhutmenuy;
|
||||
|
||||
// Draw static menu items
|
||||
for (i = 0; i < replaylistitem; i++)
|
||||
{
|
||||
INT32 localy = y + currentMenu->menuitems[i].mvar1;
|
||||
|
||||
if (localy < 65)
|
||||
continue;
|
||||
|
||||
if (i == itemOn)
|
||||
cursory = localy;
|
||||
|
||||
if ((currentMenu->menuitems[i].status & IT_DISPLAY)==IT_STRING)
|
||||
V_DrawString(x, localy, 0, currentMenu->menuitems[i].text);
|
||||
else
|
||||
V_DrawString(x, localy, highlightflags, currentMenu->menuitems[i].text);
|
||||
}
|
||||
|
||||
y += currentMenu->menuitems[replaylistitem].mvar1;
|
||||
|
||||
for (i = 0; i < (INT16)sizedirmenu; i++)
|
||||
{
|
||||
INT32 localy = y+i*10;
|
||||
INT32 localx = x;
|
||||
|
||||
if (localy < 65)
|
||||
continue;
|
||||
if (localy >= SCALEDVIEWHEIGHT)
|
||||
break;
|
||||
|
||||
if (extrasmenu.demolist[i].type == MD_NOTLOADED && !processed_one_this_frame)
|
||||
{
|
||||
processed_one_this_frame = true;
|
||||
G_LoadDemoInfo(&extrasmenu.demolist[i]);
|
||||
}
|
||||
|
||||
if (extrasmenu.demolist[i].type == MD_SUBDIR)
|
||||
{
|
||||
localx += 8;
|
||||
V_DrawScaledPatch(x - 4, localy, 0, W_CachePatchName(dirmenu[i][DIR_TYPE] == EXT_UP ? "M_RBACK" : "M_RFLDR", PU_CACHE));
|
||||
}
|
||||
|
||||
if (itemOn == replaylistitem && i == (INT16)dir_on[menudepthleft])
|
||||
{
|
||||
cursory = localy;
|
||||
|
||||
if (extrasmenu.replayScrollDelay)
|
||||
extrasmenu.replayScrollDelay--;
|
||||
else if (extrasmenu.replayScrollDir > 0)
|
||||
{
|
||||
if (extrasmenu.replayScrollTitle < (V_StringWidth(extrasmenu.demolist[i].title, 0) - (SCALEDVIEWWIDTH - (x<<1)))<<1)
|
||||
extrasmenu.replayScrollTitle++;
|
||||
else
|
||||
{
|
||||
extrasmenu.replayScrollDelay = TICRATE;
|
||||
extrasmenu.replayScrollDir = -1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (extrasmenu.replayScrollTitle > 0)
|
||||
extrasmenu.replayScrollTitle--;
|
||||
else
|
||||
{
|
||||
extrasmenu.replayScrollDelay = TICRATE;
|
||||
extrasmenu.replayScrollDir = 1;
|
||||
}
|
||||
}
|
||||
|
||||
V_DrawString(localx - (extrasmenu.replayScrollTitle>>1), localy, highlightflags|V_ALLOWLOWERCASE, extrasmenu.demolist[i].title);
|
||||
}
|
||||
else
|
||||
V_DrawString(localx, localy, V_ALLOWLOWERCASE, extrasmenu.demolist[i].title);
|
||||
}
|
||||
|
||||
// Draw scrollbar
|
||||
y = sizedirmenu*10 + currentMenu->menuitems[replaylistitem].mvar1 + 30;
|
||||
if (y > SCALEDVIEWHEIGHT-80)
|
||||
{
|
||||
V_DrawFill(BASEVIDWIDTH-4, 75, 4, SCALEDVIEWHEIGHT-80, 159);
|
||||
V_DrawFill(BASEVIDWIDTH-3, 76 + (SCALEDVIEWHEIGHT-80) * replayhutmenuy / y, 2, (((SCALEDVIEWHEIGHT-80) * (SCALEDVIEWHEIGHT-80))-1) / y - 1, 149);
|
||||
}
|
||||
|
||||
// Draw the cursor
|
||||
V_DrawScaledPatch(currentMenu->x - 24, cursory, 0,
|
||||
W_CachePatchName("M_CURSOR", PU_CACHE));
|
||||
V_DrawString(currentMenu->x, cursory, highlightflags, currentMenu->menuitems[itemOn].text);
|
||||
|
||||
// Now draw some replay info!
|
||||
V_DrawFill(10, 10, 300, 60, 159);
|
||||
|
||||
if (itemOn == replaylistitem)
|
||||
{
|
||||
M_DrawReplayHutReplayInfo(&extrasmenu.demolist[dir_on[menudepthleft]]);
|
||||
}
|
||||
}
|
||||
|
||||
void M_DrawReplayStartMenu(void)
|
||||
{
|
||||
const char *warning;
|
||||
UINT8 i;
|
||||
menudemo_t *demoref = &extrasmenu.demolist[dir_on[menudepthleft]];
|
||||
|
||||
M_DrawEggaChannel();
|
||||
M_DrawGenericMenu();
|
||||
|
||||
#define STARTY 62-(extrasmenu.replayScrollTitle>>1)
|
||||
// Draw rankings beyond first
|
||||
for (i = 1; i < MAXPLAYERS && demoref->standings[i].ranking; i++)
|
||||
{
|
||||
patch_t *patch;
|
||||
UINT8 *colormap;
|
||||
|
||||
V_DrawRightAlignedString(BASEVIDWIDTH-100, STARTY + i*20,highlightflags, va("%2d", demoref->standings[i].ranking));
|
||||
V_DrawThinString(BASEVIDWIDTH-96, STARTY + i*20, V_ALLOWLOWERCASE, demoref->standings[i].name);
|
||||
|
||||
if (demoref->standings[i].timeorscore == UINT32_MAX-1)
|
||||
V_DrawThinString(BASEVIDWIDTH-92, STARTY + i*20 + 9, 0, "NO CONTEST");
|
||||
else if (demoref->gametype < 0)
|
||||
;
|
||||
else if (gametypes[demoref->gametype]->rules & GTR_POINTLIMIT)
|
||||
V_DrawString(BASEVIDWIDTH-92, STARTY + i*20 + 9, 0, va("%d", demoref->standings[i].timeorscore));
|
||||
else
|
||||
V_DrawRightAlignedString(BASEVIDWIDTH-40, STARTY + i*20 + 9, 0, va("%d'%02d\"%02d",
|
||||
G_TicsToMinutes(demoref->standings[i].timeorscore, true),
|
||||
G_TicsToSeconds(demoref->standings[i].timeorscore),
|
||||
G_TicsToCentiseconds(demoref->standings[i].timeorscore)
|
||||
));
|
||||
|
||||
// Character face!
|
||||
|
||||
// Lat: 08/06/2020: For some reason missing skins have their value set to 255 (don't even ask me why I didn't write this)
|
||||
// and for an even STRANGER reason this passes the first check below, so we're going to make sure that the skin here ISN'T 255 before we do anything stupid.
|
||||
|
||||
if (demoref->standings[i].skin < numskins)
|
||||
{
|
||||
patch = faceprefix[demoref->standings[i].skin][FACE_RANK];
|
||||
colormap = R_GetTranslationColormap(
|
||||
demoref->standings[i].skin,
|
||||
demoref->standings[i].color,
|
||||
GTC_MENUCACHE);
|
||||
}
|
||||
else
|
||||
{
|
||||
patch = W_CachePatchName("M_NORANK", PU_CACHE);
|
||||
colormap = R_GetTranslationColormap(
|
||||
TC_RAINBOW,
|
||||
demoref->standings[i].color,
|
||||
GTC_MENUCACHE);
|
||||
}
|
||||
|
||||
V_DrawMappedPatch(BASEVIDWIDTH-5 - SHORT(patch->width), STARTY + i*20, 0, patch, colormap);
|
||||
}
|
||||
#undef STARTY
|
||||
|
||||
// Handle scrolling rankings
|
||||
if (extrasmenu.replayScrollDelay)
|
||||
extrasmenu.replayScrollDelay--;
|
||||
else if (extrasmenu.replayScrollDir > 0)
|
||||
{
|
||||
if (extrasmenu.replayScrollTitle < (i*20 - SCALEDVIEWHEIGHT + 100)<<1)
|
||||
extrasmenu.replayScrollTitle++;
|
||||
else
|
||||
{
|
||||
extrasmenu.replayScrollDelay = TICRATE;
|
||||
extrasmenu.replayScrollDir = -1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (extrasmenu.replayScrollTitle > 0)
|
||||
extrasmenu.replayScrollTitle--;
|
||||
else
|
||||
{
|
||||
extrasmenu.replayScrollDelay = TICRATE;
|
||||
extrasmenu.replayScrollDir = 1;
|
||||
}
|
||||
}
|
||||
|
||||
V_DrawFill(10, 10, 300, 60, 159);
|
||||
M_DrawReplayHutReplayInfo(demoref);
|
||||
|
||||
V_DrawString(10, 72, highlightflags|V_ALLOWLOWERCASE, demoref->title);
|
||||
|
||||
// Draw a warning prompt if needed
|
||||
switch (demoref->addonstatus)
|
||||
{
|
||||
case DFILE_ERROR_CANNOTLOAD:
|
||||
warning = "Some addons in this replay cannot be loaded.\nYou can watch anyway, but desyncs may occur.";
|
||||
break;
|
||||
|
||||
case DFILE_ERROR_NOTLOADED:
|
||||
case DFILE_ERROR_INCOMPLETEOUTOFORDER:
|
||||
warning = "Loading addons will mark your game as modified, and Record Attack may be unavailable.\nYou can watch without loading addons, but desyncs may occur.";
|
||||
break;
|
||||
|
||||
case DFILE_ERROR_EXTRAFILES:
|
||||
warning = "You have addons loaded that were not present in this replay.\nYou can watch anyway, but desyncs may occur.";
|
||||
break;
|
||||
|
||||
case DFILE_ERROR_OUTOFORDER:
|
||||
warning = "You have this replay's addons loaded, but they are out of order.\nYou can watch anyway, but desyncs may occur.";
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
V_DrawSmallString(4, BASEVIDHEIGHT-14, V_ALLOWLOWERCASE, warning);
|
||||
}
|
||||
|
||||
// Draw misc menus:
|
||||
|
||||
// Addons
|
||||
|
|
|
|||
|
|
@ -481,16 +481,6 @@ menu_t *M_SpecificMenuRestore(menu_t *torestore)
|
|||
M_SetupRaceMenu(-1);
|
||||
M_SetupDifficultyOptions((cupgrid.grandprix == false));
|
||||
}
|
||||
else if (torestore == &EXTRAS_ReplayHutDef)
|
||||
{
|
||||
// Handle modifications to the folder while playing
|
||||
M_ReplayHut(0);
|
||||
|
||||
if (demo.inreplayhut == false)
|
||||
{
|
||||
torestore = &EXTRAS_MainDef;
|
||||
}
|
||||
}
|
||||
else if (torestore == &PLAY_MP_OptSelectDef)
|
||||
{
|
||||
// Ticker init
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ target_sources(SRB2SDL2 PRIVATE
|
|||
extras-1.c
|
||||
extras-addons.c
|
||||
extras-challenges.c
|
||||
extras-replay-hut.c
|
||||
extras-egg-tv.cpp
|
||||
extras-statistics.c
|
||||
main-1.c
|
||||
main-profile-select.c
|
||||
|
|
@ -40,4 +40,5 @@ target_sources(SRB2SDL2 PRIVATE
|
|||
play-online-server-browser.c
|
||||
)
|
||||
|
||||
add_subdirectory(class-egg-tv)
|
||||
add_subdirectory(transient)
|
||||
|
|
|
|||
4
src/menus/class-egg-tv/CMakeLists.txt
Normal file
4
src/menus/class-egg-tv/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
target_sources(SRB2SDL2 PRIVATE
|
||||
EggTV.cpp
|
||||
EggTVData.cpp
|
||||
)
|
||||
1159
src/menus/class-egg-tv/EggTV.cpp
Normal file
1159
src/menus/class-egg-tv/EggTV.cpp
Normal file
File diff suppressed because it is too large
Load diff
335
src/menus/class-egg-tv/EggTV.hpp
Normal file
335
src/menus/class-egg-tv/EggTV.hpp
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
// DR. ROBOTNIK'S RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __EGGTV_HPP__
|
||||
#define __EGGTV_HPP__
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
#include "EggTVData.hpp"
|
||||
#include "EggTVGraphics.hpp"
|
||||
|
||||
#include "../../doomdef.h" // TICRATE
|
||||
#include "../../i_time.h"
|
||||
#include "../../k_menu.h"
|
||||
#include "../../m_fixed.h"
|
||||
#include "../../sounds.h"
|
||||
#include "../../v_draw.hpp"
|
||||
|
||||
namespace srb2::menus::egg_tv
|
||||
{
|
||||
|
||||
class EggTV : private EggTVData, private EggTVGraphics
|
||||
{
|
||||
public:
|
||||
struct InputReaction
|
||||
{
|
||||
bool bypass = false; // use default menu controls
|
||||
bool effect = false; // input was not ignored
|
||||
sfxenum_t sound = sfx_s3k5b;
|
||||
};
|
||||
|
||||
explicit EggTV() : EggTVData() {}
|
||||
|
||||
InputReaction input(int pid);
|
||||
|
||||
bool select();
|
||||
void back();
|
||||
|
||||
void watch() const;
|
||||
void erase();
|
||||
void toggle_favorite();
|
||||
void standings();
|
||||
|
||||
bool favorited() const;
|
||||
|
||||
void draw() const;
|
||||
|
||||
private:
|
||||
// TODO: separate some of these classes into separate files for general use
|
||||
class Animation
|
||||
{
|
||||
public:
|
||||
class Value
|
||||
{
|
||||
public:
|
||||
Value(fixed_t n) : n_(n) {}
|
||||
|
||||
Value invert() const { return FRACUNIT - n_; }
|
||||
Value invert_if(bool yes) const { return yes ? invert() : *this; }
|
||||
|
||||
operator fixed_t() const { return n_; }
|
||||
|
||||
private:
|
||||
fixed_t n_;
|
||||
};
|
||||
|
||||
enum Timing
|
||||
{
|
||||
kOnce,
|
||||
kLooping,
|
||||
};
|
||||
|
||||
explicit Animation(unsigned int duration, Timing kind = kOnce) : kind_(kind), defaultDuration_(duration) {}
|
||||
|
||||
void start(int offset = 0) { start_at(now() + offset); }
|
||||
void stop() { start_at(now() - duration()); }
|
||||
void start_with(const Animation& b) { start_at(b.starting_point()); }
|
||||
void start_after(const Animation& b) { start_at(b.stopping_point()); }
|
||||
void end_with(const Animation& b) { start_at(b.stopping_point() - duration()); }
|
||||
void end_before(const Animation& b) { start_at(b.starting_point() - duration()); }
|
||||
|
||||
void delay() { start(1 - elapsed()); }
|
||||
void extend(float f) { duration_ += defaultDuration_ * f; }
|
||||
|
||||
tic_t starting_point() const { return epoch_; }
|
||||
tic_t stopping_point() const { return epoch_ + duration(); }
|
||||
|
||||
bool running() const
|
||||
{
|
||||
switch (kind_)
|
||||
{
|
||||
case kOnce:
|
||||
return elapsed() < duration();
|
||||
|
||||
case kLooping:
|
||||
return !delayed();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool delayed() const { return starting_point() > now(); }
|
||||
|
||||
unsigned int elapsed() const { return delayed() ? 0 : now() - starting_point(); }
|
||||
unsigned int duration() const { return duration_; }
|
||||
|
||||
Value variable() const { return variable_or(0); }
|
||||
Value reverse() const { return FRACUNIT - variable_or(FRACUNIT); }
|
||||
Value reverse_if(bool yes) const { return yes ? reverse() : variable(); }
|
||||
|
||||
Animation operator +(int offset) const
|
||||
{
|
||||
Animation anim = *this;
|
||||
anim.epoch_ += offset;
|
||||
return anim;
|
||||
}
|
||||
|
||||
Animation operator -(int offset) const { return *this + -(offset); }
|
||||
|
||||
private:
|
||||
Timing kind_;
|
||||
unsigned int defaultDuration_;
|
||||
unsigned int duration_ = defaultDuration_;
|
||||
tic_t epoch_ = 0;
|
||||
|
||||
void start_at(tic_t when)
|
||||
{
|
||||
epoch_ = when;
|
||||
duration_ = defaultDuration_;
|
||||
}
|
||||
|
||||
fixed_t variable_or(fixed_t alt) const
|
||||
{
|
||||
switch (kind_)
|
||||
{
|
||||
case kOnce:
|
||||
return running() ? count() : alt;
|
||||
|
||||
case kLooping:
|
||||
return count() % FRACUNIT;
|
||||
}
|
||||
|
||||
return alt;
|
||||
}
|
||||
|
||||
fixed_t count() const { return (elapsed() * FRACUNIT) / std::max(1u, duration()); }
|
||||
|
||||
static tic_t now() { return gametic; }
|
||||
};
|
||||
|
||||
class Mode
|
||||
{
|
||||
public:
|
||||
enum Value
|
||||
{
|
||||
kFolders,
|
||||
kGrid,
|
||||
kReplay,
|
||||
kStandings,
|
||||
};
|
||||
|
||||
explicit Mode(Value mode) : next_(mode) {}
|
||||
|
||||
void change(Value mode, tic_t when)
|
||||
{
|
||||
prev_ = next_;
|
||||
next_ = mode;
|
||||
stop_ = when;
|
||||
}
|
||||
|
||||
Value get() const { return (stop_ <= gametic ? next_ : prev_); }
|
||||
Value next() const { return next_; }
|
||||
|
||||
bool changing() const { return gametic < stop_; }
|
||||
bool changing_to(Value type) const { return changing() && next_ == type; }
|
||||
|
||||
bool operator ==(Value b) const { return get() == b; }
|
||||
operator Value() const { return get(); }
|
||||
|
||||
private:
|
||||
Value next_;
|
||||
Value prev_ = next_;
|
||||
tic_t stop_ = 0;
|
||||
};
|
||||
|
||||
class Cursor
|
||||
{
|
||||
public:
|
||||
using limiter_t = std::function<int()>;
|
||||
using anims_t = std::vector<Animation*>;
|
||||
|
||||
explicit Cursor(anims_t anims, limiter_t limiter) : limiter_(limiter), anims_(anims) {}
|
||||
|
||||
int pos() const { return pos_; }
|
||||
int end() const { return std::max(1, limiter_()); }
|
||||
float fraction() const { return pos() / static_cast<float>(end()); }
|
||||
int change() const { return change_; }
|
||||
|
||||
bool wrap() const
|
||||
{
|
||||
const int prev = pos() - change();
|
||||
|
||||
return prev < 0 || prev >= end();
|
||||
}
|
||||
|
||||
Cursor& operator +=(int n)
|
||||
{
|
||||
if (n != 0)
|
||||
{
|
||||
pos_ += n;
|
||||
change_ = n;
|
||||
}
|
||||
|
||||
if (pos_ < 0)
|
||||
{
|
||||
pos_ = end() - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
pos_ %= end();
|
||||
}
|
||||
|
||||
if (n != 0)
|
||||
{
|
||||
// TODO?
|
||||
for (Animation* anim : anims_)
|
||||
{
|
||||
if (anim->running())
|
||||
{
|
||||
anim->delay();
|
||||
}
|
||||
else
|
||||
{
|
||||
anim->start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
Cursor& operator =(int n)
|
||||
{
|
||||
pos_ = n;
|
||||
pos_ += 0;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
limiter_t limiter_;
|
||||
|
||||
int pos_ = 0;
|
||||
int change_ = 0;
|
||||
|
||||
anims_t anims_;
|
||||
};
|
||||
|
||||
Mode mode_{Mode::kFolders};
|
||||
|
||||
Animation rowSlide_{4};
|
||||
Animation folderSlide_{8};
|
||||
Animation dashRise_{6};
|
||||
Animation dashTextRise_{8};
|
||||
Animation dashTextScroll_{12*TICRATE, Animation::kLooping};
|
||||
Animation gridFade_{4};
|
||||
Animation gridPopulate_{16};
|
||||
Animation gridSelectX_{3};
|
||||
Animation gridSelectY_{3};
|
||||
Animation gridSelectShake_{6};
|
||||
Animation enhanceMove_{2};
|
||||
Animation enhanceZoom_{4};
|
||||
Animation replaySlide_{6};
|
||||
Animation buttonSlide_{6};
|
||||
Animation buttonHover_{4};
|
||||
Animation replayTitleScroll_{24*TICRATE, Animation::kLooping};
|
||||
Animation favSlap_{8};
|
||||
Animation standingsRowSlide_{6};
|
||||
|
||||
static constexpr int kGridColsPerRow = 5;
|
||||
|
||||
Cursor folderRow_{{&rowSlide_}, [this]{ return folders_.size(); }};
|
||||
Cursor gridCol_{{&gridSelectX_}, []{ return kGridColsPerRow; }};
|
||||
Cursor gridRow_{{&gridSelectY_, &rowSlide_}, [this] { return grid_rows(); }};
|
||||
Cursor standingsRow_{{&standingsRowSlide_}, [this]{ return standings_rows(); }};
|
||||
|
||||
std::size_t grid_index() const { return gridCol_.pos() + (gridRow_.pos() * kGridColsPerRow); }
|
||||
std::size_t grid_rows() const;
|
||||
|
||||
std::size_t standings_rows() const;
|
||||
|
||||
PatchManager::patch gametype_graphic(const Replay& replay) const;
|
||||
|
||||
void draw_background() const;
|
||||
void draw_overlay() const;
|
||||
void draw_folder_header() const;
|
||||
void draw_folders() const;
|
||||
void draw_scroll_bar() const;
|
||||
void draw_dash() const;
|
||||
void draw_dash_text(const Replay& replay) const;
|
||||
|
||||
enum class GridMode
|
||||
{
|
||||
kPopulating,
|
||||
kFinal,
|
||||
};
|
||||
|
||||
struct GridOffsets;
|
||||
|
||||
template <GridMode Mode>
|
||||
void draw_grid() const;
|
||||
|
||||
void draw_grid_mesh(const GridOffsets& grid) const;
|
||||
void draw_grid_select(const GridOffsets& grid) const;
|
||||
void draw_grid_enhance(const GridOffsets& grid) const;
|
||||
|
||||
void draw_replay(const Replay& replay) const;
|
||||
void draw_replay_photo(const Replay& replay, Draw pic) const;
|
||||
void draw_replay_buttons() const;
|
||||
|
||||
void draw_standings(const Replay& replay) const;
|
||||
};
|
||||
|
||||
}; // namespace srb2::menus::egg_tv
|
||||
|
||||
#endif // __EGGTV_HPP__
|
||||
390
src/menus/class-egg-tv/EggTVData.cpp
Normal file
390
src/menus/class-egg-tv/EggTVData.cpp
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
// DR. ROBOTNIK'S RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <fmt/std.h> // std::filesystem::path formatter
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "../../cxxutil.hpp"
|
||||
#include "EggTVData.hpp"
|
||||
|
||||
#include "../../doomdef.h" // CONS_Alert
|
||||
#include "../../doomstat.h" // gametypes
|
||||
#include "../../g_demo.h"
|
||||
#include "../../k_menu.h" // DF_ENCORE
|
||||
#include "../../r_skins.h"
|
||||
|
||||
using namespace srb2::menus::egg_tv;
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
using nlohmann::json;
|
||||
|
||||
template <>
|
||||
struct fmt::formatter<fs::filesystem_error> : formatter<std::string>
|
||||
{
|
||||
template <typename FormatContext>
|
||||
auto format(const fs::filesystem_error& ex, FormatContext& ctx) const
|
||||
{
|
||||
return formatter<std::string>::format(
|
||||
fmt::format("{}, path1={}, path2={}", ex.what(), ex.path1(), ex.path2()),
|
||||
ctx
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
template <class... Args>
|
||||
void print_error(fmt::format_string<Args...> format, Args&&... args)
|
||||
{
|
||||
CONS_Alert(CONS_ERROR, "Egg TV: %s\n", fmt::format(format, args...).c_str());
|
||||
}
|
||||
|
||||
template <class To, class From>
|
||||
To time_point_conv(From time)
|
||||
{
|
||||
// https://stackoverflow.com/a/58237530/10850779
|
||||
return std::chrono::time_point_cast<typename To::duration>(To::clock::now() + (time - From::clock::now()));
|
||||
}
|
||||
|
||||
json& ensure_array(json& object, const char* key)
|
||||
{
|
||||
json& array = object[key];
|
||||
|
||||
if (!array.is_array())
|
||||
{
|
||||
array = json::array();
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
}; // namespace
|
||||
|
||||
EggTVData::EggTVData() : favorites_(ensure_array(favoritesFile_, "favorites"))
|
||||
{
|
||||
try
|
||||
{
|
||||
cache_folders();
|
||||
}
|
||||
catch (const fs::filesystem_error& ex)
|
||||
{
|
||||
print_error("{}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
json EggTVData::cache_favorites() const
|
||||
{
|
||||
json object;
|
||||
|
||||
try
|
||||
{
|
||||
std::ifstream f(favoritesPath_);
|
||||
|
||||
if (f.is_open())
|
||||
{
|
||||
f >> object;
|
||||
}
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
print_error("{}", ex.what());
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
EggTVData::Folder::Cache::Cache(Folder& folder) : folder_(folder)
|
||||
{
|
||||
try
|
||||
{
|
||||
for (const fs::directory_entry& entry : fs::directory_iterator(folder_.path()))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!entry.is_regular_file())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
replays_.emplace_back(
|
||||
*this,
|
||||
entry.path().filename(),
|
||||
time_point_conv<time_point_t>(entry.last_write_time())
|
||||
);
|
||||
}
|
||||
catch (const fs::filesystem_error& ex)
|
||||
{
|
||||
print_error("{}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const fs::filesystem_error& ex)
|
||||
{
|
||||
print_error("{}", ex);
|
||||
}
|
||||
|
||||
auto predicate = [](const ReplayRef& a, const ReplayRef& b)
|
||||
{
|
||||
// Favorites come first
|
||||
if (a.favorited() != b.favorited())
|
||||
{
|
||||
return a.favorited();
|
||||
}
|
||||
|
||||
return a.time() > b.time(); // sort newest to oldest
|
||||
};
|
||||
|
||||
std::sort(replays_.begin(), replays_.end(), predicate);
|
||||
|
||||
// Refresh folder size
|
||||
folder_.size_ = replays_.size();
|
||||
}
|
||||
|
||||
std::shared_ptr<EggTVData::Replay> EggTVData::Folder::Cache::replay(std::size_t idx)
|
||||
{
|
||||
if (idx >= size())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return replays_[idx].replay();
|
||||
}
|
||||
|
||||
void EggTVData::Folder::Cache::release(const ReplayRef& ref)
|
||||
{
|
||||
const auto& it = std::find_if(replays_.begin(), replays_.end(), [&ref](const ReplayRef& b) { return &b == &ref; });
|
||||
|
||||
SRB2_ASSERT(it != replays_.end());
|
||||
|
||||
replays_.erase(it);
|
||||
folder_.size_--;
|
||||
}
|
||||
|
||||
EggTVData::Folder::Folder(EggTVData& tv, const fs::directory_entry& entry) :
|
||||
tv_(&tv),
|
||||
name_(entry.path().filename().string())
|
||||
{
|
||||
SRB2_ASSERT(entry.path().parent_path() == tv_->root_);
|
||||
|
||||
time_ = time_point_t::min();
|
||||
size_ = 0;
|
||||
|
||||
for (const fs::directory_entry& entry : fs::directory_iterator(entry.path()))
|
||||
{
|
||||
const time_point_t t = time_point_conv<time_point_t>(entry.last_write_time());
|
||||
|
||||
if (time_ < t)
|
||||
time_ = t;
|
||||
|
||||
size_++;
|
||||
}
|
||||
}
|
||||
|
||||
EggTVData::Replay::Title::operator const std::string() const
|
||||
{
|
||||
return second().empty() ? first() : fmt::format("{} - {}", first(), second());
|
||||
}
|
||||
|
||||
EggTVData::Replay::Replay(Folder::Cache::ReplayRef& ref) : ref_(&ref)
|
||||
{
|
||||
const fs::path path = this->path();
|
||||
|
||||
menudemo_t info = {};
|
||||
|
||||
if (path.native().size() >= sizeof info.filepath)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::copy_n(path.string().c_str(), path.native().size() + 1, info.filepath);
|
||||
|
||||
G_LoadDemoInfo(&info);
|
||||
|
||||
if (info.type != MD_LOADED)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
constexpr std::string_view kDelimiter = " - ";
|
||||
|
||||
const std::string_view str = info.title;
|
||||
const std::size_t mid = str.find(kDelimiter);
|
||||
|
||||
title_ = Title(str.substr(0, mid), mid == std::string::npos ? "" : str.substr(mid + kDelimiter.size()));
|
||||
//title_ = Title("WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW", "WWWWWWWWWWWWWWWWWWWWWWWWWWW");
|
||||
}
|
||||
|
||||
map_ = info.map;
|
||||
|
||||
if (info.gametype == GT_RACE)
|
||||
{
|
||||
gametype_ = Gametype(GT_RACE, Gametype::Race {
|
||||
info.numlaps,
|
||||
kartspeed_cons_t[(info.kartspeed & ~(DF_ENCORE)) + 1].strvalue,
|
||||
(info.kartspeed & DF_ENCORE) != 0,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
gametype_ = Gametype(info.gametype);
|
||||
}
|
||||
|
||||
for (const auto& data : info.standings)
|
||||
{
|
||||
if (!data.ranking)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Standing& standing = standings_.emplace_back();
|
||||
|
||||
standing.name = data.name;
|
||||
|
||||
if (data.skin < numskins)
|
||||
{
|
||||
standing.skin = data.skin;
|
||||
}
|
||||
|
||||
standing.color = data.color;
|
||||
|
||||
if (data.timeorscore != UINT32_MAX - 1) // NO CONTEST
|
||||
{
|
||||
if (gametype_.ranks_time())
|
||||
{
|
||||
standing.time = data.timeorscore;
|
||||
}
|
||||
else if (gametype_.ranks_points())
|
||||
{
|
||||
standing.score = data.timeorscore;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invalid_ = false;
|
||||
}
|
||||
|
||||
EggTVData::Replay::~Replay()
|
||||
{
|
||||
if (!erased_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Delayed erase function ensures there are no references
|
||||
// left before ReplayRef is removed from cache.
|
||||
|
||||
try
|
||||
{
|
||||
fs::remove(path());
|
||||
}
|
||||
catch (const fs::filesystem_error& ex)
|
||||
{
|
||||
// catch inside loop so individual errors don't bail completely
|
||||
print_error("{}", ex);
|
||||
}
|
||||
|
||||
// Clear deleted replays from favorites too!
|
||||
if (favorited())
|
||||
{
|
||||
toggle_favorite();
|
||||
}
|
||||
|
||||
ref_->cache().release(*ref_);
|
||||
}
|
||||
|
||||
void EggTVData::Replay::toggle_favorite() const
|
||||
{
|
||||
const auto& it = ref_->iterator_to_favorite();
|
||||
|
||||
if (it != ref_->favorites().end())
|
||||
{
|
||||
ref_->favorites().erase(it);
|
||||
}
|
||||
else
|
||||
{
|
||||
ref_->favorites().emplace_back(ref_->favorites_path());
|
||||
}
|
||||
|
||||
ref_->cache().folder().tv().save_favorites();
|
||||
}
|
||||
|
||||
void EggTVData::cache_folders()
|
||||
{
|
||||
for (const fs::directory_entry& entry : fs::directory_iterator(root_))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!entry.is_directory())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Folder folder(*this, entry);
|
||||
|
||||
if (!folder.empty())
|
||||
{
|
||||
folders_.push_back(folder);
|
||||
}
|
||||
}
|
||||
catch (const fs::filesystem_error& ex)
|
||||
{
|
||||
// catch inside loop so individual errors don't bail completely
|
||||
print_error("{}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
sort_folders();
|
||||
}
|
||||
|
||||
void EggTVData::sort_folders()
|
||||
{
|
||||
auto predicate = [this](const Folder& a, const Folder& b)
|
||||
{
|
||||
switch (folderSort_)
|
||||
{
|
||||
case FolderSort::kDate:
|
||||
return a.time() > b.time();
|
||||
|
||||
case FolderSort::kName:
|
||||
return a.name() < b.name(); // ascending order
|
||||
|
||||
case FolderSort::kSize:
|
||||
return a.size() > b.size();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
std::sort(folders_.begin(), folders_.end(), predicate);
|
||||
}
|
||||
|
||||
void EggTVData::save_favorites() const
|
||||
{
|
||||
try
|
||||
{
|
||||
std::ofstream(favoritesPath_) << favoritesFile_;
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
print_error("{}", ex.what());
|
||||
}
|
||||
}
|
||||
268
src/menus/class-egg-tv/EggTVData.hpp
Normal file
268
src/menus/class-egg-tv/EggTVData.hpp
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
// DR. ROBOTNIK'S RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __EGGTVDATA_HPP__
|
||||
#define __EGGTVDATA_HPP__
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "../../cxxutil.hpp"
|
||||
|
||||
#include "../../d_main.h" // srb2home
|
||||
#include "../../doomstat.h" // gametype_t
|
||||
#include "../../doomtype.h"
|
||||
|
||||
namespace srb2::menus::egg_tv
|
||||
{
|
||||
|
||||
class EggTVData
|
||||
{
|
||||
private:
|
||||
const std::filesystem::path root_ = std::filesystem::path{srb2home} / "media/replay/online";
|
||||
const std::filesystem::path favoritesPath_ = root_ / "favorites.json";
|
||||
|
||||
nlohmann::json favoritesFile_ = cache_favorites();
|
||||
nlohmann::json& favorites_;
|
||||
|
||||
nlohmann::json cache_favorites() const;
|
||||
|
||||
void cache_folders();
|
||||
void save_favorites() const;
|
||||
|
||||
public:
|
||||
using time_point_t = std::chrono::time_point<std::chrono::system_clock>;
|
||||
|
||||
explicit EggTVData();
|
||||
|
||||
class Replay;
|
||||
|
||||
class Folder
|
||||
{
|
||||
public:
|
||||
class Cache
|
||||
{
|
||||
public:
|
||||
class ReplayRef
|
||||
{
|
||||
public:
|
||||
explicit ReplayRef(Cache& cache, std::filesystem::path filename, time_point_t time) :
|
||||
cache_(&cache), filename_(filename), time_(time)
|
||||
{
|
||||
}
|
||||
|
||||
Cache& cache() const { return *cache_; }
|
||||
const std::filesystem::path& filename() const { return filename_; }
|
||||
const time_point_t& time() const { return time_; }
|
||||
|
||||
std::shared_ptr<Replay> replay()
|
||||
{
|
||||
SRB2_ASSERT(!released_); // do not call after released
|
||||
|
||||
if (!replay_)
|
||||
{
|
||||
replay_ = std::make_shared<Replay>(*this);
|
||||
}
|
||||
|
||||
return replay_;
|
||||
}
|
||||
|
||||
void release()
|
||||
{
|
||||
replay_.reset();
|
||||
released_ = true;
|
||||
}
|
||||
|
||||
bool favorited() const { return iterator_to_favorite() != favorites().end(); }
|
||||
nlohmann::json& favorites() const { return cache().folder().tv().favorites_; }
|
||||
|
||||
std::string favorites_path() const
|
||||
{
|
||||
// path::generic_string converts to forward
|
||||
// slashes on Windows. This should suffice to make
|
||||
// the JSON file portable across installations.
|
||||
return (std::filesystem::path{cache().folder().name()} / filename()).generic_string();
|
||||
}
|
||||
|
||||
nlohmann::json::const_iterator iterator_to_favorite() const
|
||||
{
|
||||
return std::find(favorites().begin(), favorites().end(), favorites_path());
|
||||
}
|
||||
|
||||
private:
|
||||
Cache* cache_;
|
||||
std::filesystem::path filename_;
|
||||
time_point_t time_;
|
||||
std::shared_ptr<Replay> replay_;
|
||||
bool released_ = false;
|
||||
};
|
||||
|
||||
explicit Cache(Folder& folder);
|
||||
|
||||
std::shared_ptr<Replay> replay(std::size_t idx);
|
||||
void release(const ReplayRef& ref);
|
||||
|
||||
std::size_t size() const { return folder_.size(); }
|
||||
Folder& folder() const { return folder_; }
|
||||
|
||||
private:
|
||||
Folder& folder_;
|
||||
std::vector<ReplayRef> replays_;
|
||||
};
|
||||
|
||||
explicit Folder(EggTVData& tv, const std::filesystem::directory_entry& entry);
|
||||
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
|
||||
bool empty() { return size() == 0; }
|
||||
std::filesystem::path path() const { return tv_->root_ / name_; }
|
||||
|
||||
EggTVData& tv() const { return *tv_; }
|
||||
|
||||
std::size_t size() const { return size_; }
|
||||
const time_point_t& time() const { return time_; }
|
||||
const std::string& name() const { return name_; }
|
||||
|
||||
std::unique_ptr<Cache> load() { return std::make_unique<Cache>(*this); };
|
||||
|
||||
bool operator ==(const Folder& b) const { return this == &b; }
|
||||
|
||||
private:
|
||||
std::size_t size_;
|
||||
time_point_t time_;
|
||||
EggTVData* tv_;
|
||||
std::string name_;
|
||||
};
|
||||
|
||||
class Replay
|
||||
{
|
||||
public:
|
||||
class Title
|
||||
{
|
||||
public:
|
||||
explicit Title() {}
|
||||
explicit Title(const std::string_view& first, const std::string_view& second) :
|
||||
first_(first), second_(second)
|
||||
{
|
||||
}
|
||||
|
||||
const std::string& first() const { return first_; }
|
||||
const std::string& second() const { return second_; }
|
||||
|
||||
operator const std::string() const;
|
||||
|
||||
private:
|
||||
std::string first_, second_;
|
||||
};
|
||||
|
||||
struct Standing
|
||||
{
|
||||
std::string name;
|
||||
std::optional<std::size_t> skin;
|
||||
std::size_t color;
|
||||
std::optional<tic_t> time;
|
||||
std::optional<UINT32> score;
|
||||
};
|
||||
|
||||
class Gametype
|
||||
{
|
||||
public:
|
||||
struct Race
|
||||
{
|
||||
int laps;
|
||||
std::string_view speed;
|
||||
bool encore;
|
||||
};
|
||||
|
||||
explicit Gametype() {}
|
||||
explicit Gametype(INT16 gt, Race race) : gametype_(get(gt)), var_(race) {}
|
||||
explicit Gametype(INT16 gt) : gametype_(get(gt)) {}
|
||||
|
||||
bool valid() const { return gametype_; }
|
||||
|
||||
std::string_view name() const { return valid() ? gametype_->name : "<Unknown gametype>"; }
|
||||
UINT32 rules() const { return valid() ? gametype_->rules : 0u; }
|
||||
|
||||
bool ranks_time() const { return !ranks_points(); }
|
||||
bool ranks_points() const { return rules() & GTR_POINTLIMIT; }
|
||||
|
||||
const Race* race() const { return std::get_if<Race>(&var_); }
|
||||
|
||||
private:
|
||||
const gametype_t* gametype_ = nullptr;
|
||||
std::variant<std::monostate, Race> var_;
|
||||
|
||||
static gametype_t* get(INT16 gt) { return gt >= 0 && gt < numgametypes ? gametypes[gt] : nullptr; }
|
||||
};
|
||||
|
||||
explicit Replay(Folder::Cache::ReplayRef& ref);
|
||||
~Replay();
|
||||
|
||||
void mark_for_deletion()
|
||||
{
|
||||
erased_ = true;
|
||||
ref_->release();
|
||||
}
|
||||
|
||||
void toggle_favorite() const;
|
||||
|
||||
bool invalid() const { return invalid_; }
|
||||
bool favorited() const { return ref_->iterator_to_favorite() != ref_->favorites().end(); }
|
||||
|
||||
std::filesystem::path path() const { return ref_->cache().folder().path() / ref_->filename(); }
|
||||
const time_point_t& date() const { return ref_->time(); }
|
||||
|
||||
std::size_t map() const { return map_; }
|
||||
const Title& title() const { return title_; }
|
||||
const Gametype& gametype() const { return gametype_; }
|
||||
|
||||
const std::vector<Standing>& standings() const { return standings_; }
|
||||
const Standing* winner() const { return standings_.empty() ? nullptr : &standings_.front(); }
|
||||
|
||||
private:
|
||||
Folder::Cache::ReplayRef* ref_;
|
||||
|
||||
bool invalid_ = true;
|
||||
bool erased_ = false;
|
||||
|
||||
std::vector<Standing> standings_;
|
||||
std::size_t map_;
|
||||
Title title_;
|
||||
Gametype gametype_;
|
||||
};
|
||||
|
||||
enum class FolderSort
|
||||
{
|
||||
kDate,
|
||||
kName,
|
||||
kSize,
|
||||
};
|
||||
|
||||
std::vector<Folder> folders_;
|
||||
std::unique_ptr<Folder::Cache> cache_;
|
||||
FolderSort folderSort_ = FolderSort::kDate;
|
||||
|
||||
void sort_folders();
|
||||
};
|
||||
|
||||
}; // namsepace srb2::menus::egg_tv
|
||||
|
||||
#endif // __EGGTVDATA_HPP__
|
||||
161
src/menus/class-egg-tv/EggTVGraphics.hpp
Normal file
161
src/menus/class-egg-tv/EggTVGraphics.hpp
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
// DR. ROBOTNIK'S RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __EGGTVGRAPHICS_HPP__
|
||||
#define __EGGTVGRAPHICS_HPP__
|
||||
|
||||
#include <array>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "../../doomdef.h" // skincolornum_t
|
||||
#include "../../v_draw.hpp"
|
||||
|
||||
namespace srb2::menus::egg_tv
|
||||
{
|
||||
|
||||
class EggTVGraphics
|
||||
{
|
||||
public:
|
||||
struct PatchManager
|
||||
{
|
||||
using patch = const char*;
|
||||
|
||||
template <int Size>
|
||||
using array = std::array<patch, Size>;
|
||||
|
||||
template <int Size>
|
||||
struct Animation
|
||||
{
|
||||
array<Size> data;
|
||||
|
||||
patch animate(int interval) const;
|
||||
};
|
||||
|
||||
Animation<2> bg = {
|
||||
"RHETSCB1",
|
||||
"RHETSCB2",
|
||||
};
|
||||
|
||||
patch mod = "RHTVMOD";
|
||||
patch overlay = "RHETTV";
|
||||
|
||||
patch bar = "RH_BAR";
|
||||
|
||||
array<2> folder = {
|
||||
"RHFLDR02",
|
||||
"RHFLDR03",
|
||||
};
|
||||
|
||||
patch dash = "RHMQBR";
|
||||
patch grid = "RHGRID";
|
||||
|
||||
patch empty = "RHTVSQN0";
|
||||
|
||||
Animation<4> tv = {
|
||||
"RHTVSTC1",
|
||||
"RHTVSTC2",
|
||||
"RHTVSTC3",
|
||||
"RHTVSTC4",
|
||||
};
|
||||
|
||||
std::array<Animation<2>, 2> nodata = {
|
||||
Animation<2> {
|
||||
"RHTVSQN1",
|
||||
"RHTVSQN2",
|
||||
},
|
||||
Animation<2> {
|
||||
"RHTVSQN3",
|
||||
"RHTVSQN4",
|
||||
},
|
||||
};
|
||||
|
||||
std::array<Animation<2>, 2> corrupt = {
|
||||
Animation<2> {
|
||||
"RHTVSQN5",
|
||||
"RHTVSQN6",
|
||||
},
|
||||
Animation<2> {
|
||||
"RHTVSQN7",
|
||||
"RHTVSQN8",
|
||||
},
|
||||
};
|
||||
|
||||
patch select = "RHTVSQSL";
|
||||
|
||||
std::unordered_map<std::string_view, patch> gametype = {
|
||||
{"Race", "RHGT1"},
|
||||
{"Battle", "RHGT2"},
|
||||
|
||||
// TODO: requires support in the demo format
|
||||
//{"Prisons", "RHGT3"},
|
||||
//{"Special", "RHGT4"},
|
||||
};
|
||||
|
||||
patch fav = "RHFAV";
|
||||
|
||||
patch button = "RHMNBR";
|
||||
|
||||
struct
|
||||
{
|
||||
struct
|
||||
{
|
||||
patch up = "RHSBRU";
|
||||
patch down = "RHSBRD";
|
||||
}
|
||||
arrow;
|
||||
|
||||
struct
|
||||
{
|
||||
patch top = "RHSBR01";
|
||||
patch mid = "RHSBR02";
|
||||
patch bottom = "RHSBR03";
|
||||
}
|
||||
bead;
|
||||
}
|
||||
scroll;
|
||||
};
|
||||
|
||||
struct ColorManager
|
||||
{
|
||||
using color = skincolornum_t;
|
||||
|
||||
template <int Size>
|
||||
using array = std::array<color, Size>;
|
||||
|
||||
array<2> bar = {
|
||||
SKINCOLOR_JET,
|
||||
SKINCOLOR_BANANA,
|
||||
};
|
||||
|
||||
array<2> folder = {
|
||||
SKINCOLOR_NONE,
|
||||
SKINCOLOR_THUNDER,
|
||||
};
|
||||
|
||||
array<2> button = {
|
||||
SKINCOLOR_BLACK,
|
||||
SKINCOLOR_THUNDER,
|
||||
};
|
||||
|
||||
color scroll = SKINCOLOR_THUNDER;
|
||||
|
||||
array<2> select = {
|
||||
SKINCOLOR_NONE,
|
||||
SKINCOLOR_MAROON,
|
||||
};
|
||||
};
|
||||
|
||||
static const PatchManager patches_;
|
||||
static const ColorManager colors_;
|
||||
};
|
||||
|
||||
}; // namespace srb2::menus::egg_tv
|
||||
|
||||
#endif // __EGGTVGRAPHICS_HPP__
|
||||
|
|
@ -24,7 +24,7 @@ menuitem_t EXTRAS_Main[] =
|
|||
NULL, {.routine = M_Statistics}, 0, 0},
|
||||
|
||||
{IT_STRING | IT_CALL, NULL, NULL,
|
||||
NULL, {.routine = M_ReplayHut}, 0, 0},
|
||||
NULL, {.routine = M_EggTV}, 0, 0},
|
||||
|
||||
{IT_STRING | IT_CALL, NULL, NULL,
|
||||
NULL, {.routine = M_SoundTest}, 0, 0},
|
||||
|
|
|
|||
131
src/menus/extras-egg-tv.cpp
Normal file
131
src/menus/extras-egg-tv.cpp
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/// \brief Extras Menu: Egg TV
|
||||
|
||||
#include "class-egg-tv/EggTV.hpp"
|
||||
|
||||
#include "../k_menu.h"
|
||||
#include "../s_sound.h"
|
||||
|
||||
using namespace srb2::menus::egg_tv;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
std::unique_ptr<EggTV> g_egg_tv;
|
||||
|
||||
void M_DrawEggTV()
|
||||
{
|
||||
g_egg_tv->draw();
|
||||
}
|
||||
|
||||
boolean M_QuitEggTV()
|
||||
{
|
||||
g_egg_tv = {};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean M_HandleEggTV(INT32 choice)
|
||||
{
|
||||
(void)choice;
|
||||
|
||||
const UINT8 pid = 0;
|
||||
const EggTV::InputReaction reaction = g_egg_tv->input(pid);
|
||||
|
||||
if (reaction.bypass)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reaction.effect)
|
||||
{
|
||||
S_StartSound(nullptr, reaction.sound);
|
||||
M_SetMenuDelay(pid);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void M_DeleteReplayChoice(INT32 choice)
|
||||
{
|
||||
if (choice == MA_YES)
|
||||
{
|
||||
g_egg_tv->erase();
|
||||
|
||||
//S_StartSound(nullptr, sfx_s3k4e); // BOOM
|
||||
S_StartSound(nullptr, sfx_monch); // :)
|
||||
}
|
||||
}
|
||||
|
||||
void M_DeleteReplay(INT32 c)
|
||||
{
|
||||
(void)c;
|
||||
M_StartMessage(
|
||||
"Are you sure you want to\n"
|
||||
"delete this replay?\n"
|
||||
"\n"
|
||||
"\x85" "This cannot be undone.\n" "\x80"
|
||||
"\n"
|
||||
"Press (A) to confirm or (B) to cancel",
|
||||
FUNCPTRCAST(M_DeleteReplayChoice),
|
||||
MM_YESNO
|
||||
);
|
||||
S_StartSound(nullptr, sfx_s3k36); // lel skid
|
||||
}
|
||||
|
||||
void M_FavoriteReplay(INT32 c)
|
||||
{
|
||||
(void)c;
|
||||
|
||||
g_egg_tv->toggle_favorite();
|
||||
|
||||
S_StartSound(nullptr, sfx_s1c9);
|
||||
}
|
||||
|
||||
}; // namespace
|
||||
|
||||
// extras menu: replay hut
|
||||
menuitem_t EXTRAS_EggTV[] =
|
||||
{
|
||||
{IT_STRING | IT_CALL, "WATCH REPLAY", NULL, NULL, {.routine = [](auto) { g_egg_tv->watch(); }}, 0, 0},
|
||||
{IT_STRING | IT_CALL, "STANDINGS", NULL, NULL, {.routine = [](auto) { g_egg_tv->standings(); }}, 0, 0},
|
||||
{IT_STRING | IT_CALL, "FAVORITE", NULL, NULL, {.routine = M_FavoriteReplay}, 0, 0},
|
||||
|
||||
{IT_SPACE},
|
||||
|
||||
{IT_STRING | IT_CALL, "DELETE REPLAY", NULL, NULL, {.routine = M_DeleteReplay}, 0, 0},
|
||||
|
||||
{IT_SPACE},
|
||||
|
||||
{IT_STRING | IT_CALL, "GO BACK", NULL, NULL, {.routine = [](auto) { g_egg_tv->back(); }}, 0, 0},
|
||||
};
|
||||
|
||||
menu_t EXTRAS_EggTVDef =
|
||||
{
|
||||
sizeof (EXTRAS_EggTV)/sizeof (menuitem_t),
|
||||
&EXTRAS_MainDef,
|
||||
0,
|
||||
EXTRAS_EggTV,
|
||||
30, 80,
|
||||
0, 0,
|
||||
0,
|
||||
"REPLAY", // music
|
||||
41, 1,
|
||||
M_DrawEggTV,
|
||||
NULL,
|
||||
NULL,
|
||||
M_QuitEggTV,
|
||||
M_HandleEggTV
|
||||
};
|
||||
|
||||
// Call this to construct Egg TV menu
|
||||
void M_EggTV(INT32 choice)
|
||||
{
|
||||
g_egg_tv = std::make_unique<EggTV>();
|
||||
|
||||
M_SetupNextMenu(&EXTRAS_EggTVDef, false);
|
||||
}
|
||||
|
||||
void M_EggTV_RefreshButtonLabels()
|
||||
{
|
||||
EXTRAS_EggTV[2].text = g_egg_tv->favorited() ? "UNFAVORITE" : "FAVORITE";
|
||||
}
|
||||
|
|
@ -1,296 +0,0 @@
|
|||
/// \file menus/extras-replay-hut.c
|
||||
/// \brief Extras Menu: Replay Hut
|
||||
|
||||
#include "../k_menu.h"
|
||||
#include "../filesrch.h" // Addfile
|
||||
#include "../d_main.h"
|
||||
#include "../s_sound.h"
|
||||
#include "../v_video.h"
|
||||
#include "../z_zone.h"
|
||||
|
||||
// extras menu: replay hut
|
||||
menuitem_t EXTRAS_ReplayHut[] =
|
||||
{
|
||||
{IT_KEYHANDLER|IT_NOTHING, "", "", // Dummy menuitem for the replay list
|
||||
NULL, {.routine = M_HandleReplayHutList}, 0, 0},
|
||||
|
||||
{IT_NOTHING, "", "", // Dummy for handling wrapping to the top of the menu..
|
||||
NULL, {NULL}, 0, 0},
|
||||
};
|
||||
|
||||
menu_t EXTRAS_ReplayHutDef =
|
||||
{
|
||||
sizeof (EXTRAS_ReplayHut)/sizeof (menuitem_t),
|
||||
&EXTRAS_MainDef,
|
||||
0,
|
||||
EXTRAS_ReplayHut,
|
||||
30, 80,
|
||||
0, 0,
|
||||
0,
|
||||
"REPLAY",
|
||||
41, 1,
|
||||
M_DrawReplayHut,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL
|
||||
};
|
||||
|
||||
menuitem_t EXTRAS_ReplayStart[] =
|
||||
{
|
||||
{IT_CALL |IT_STRING, "Load Addons and Watch", NULL,
|
||||
NULL, {.routine = M_HutStartReplay}, 0, 0},
|
||||
|
||||
{IT_CALL |IT_STRING, "Load Without Addons", NULL,
|
||||
NULL, {.routine = M_HutStartReplay}, 10, 0},
|
||||
|
||||
{IT_CALL |IT_STRING, "Watch Replay", NULL,
|
||||
NULL, {.routine = M_HutStartReplay}, 10, 0},
|
||||
|
||||
{IT_SUBMENU |IT_STRING, "Go Back", NULL,
|
||||
NULL, {.submenu = &EXTRAS_ReplayHutDef}, 30, 0},
|
||||
};
|
||||
|
||||
|
||||
menu_t EXTRAS_ReplayStartDef =
|
||||
{
|
||||
sizeof (EXTRAS_ReplayStart)/sizeof (menuitem_t),
|
||||
&EXTRAS_ReplayHutDef,
|
||||
0,
|
||||
EXTRAS_ReplayStart,
|
||||
27, 80,
|
||||
0, 0,
|
||||
0,
|
||||
"REPLAY",
|
||||
41, 1,
|
||||
M_DrawReplayStartMenu,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
NULL
|
||||
};
|
||||
|
||||
void M_PrepReplayList(void)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
if (extrasmenu.demolist)
|
||||
Z_Free(extrasmenu.demolist);
|
||||
|
||||
extrasmenu.demolist = Z_Calloc(sizeof(menudemo_t) * sizedirmenu, PU_STATIC, NULL);
|
||||
|
||||
for (i = 0; i < sizedirmenu; i++)
|
||||
{
|
||||
if (dirmenu[i][DIR_TYPE] == EXT_UP)
|
||||
{
|
||||
extrasmenu.demolist[i].type = MD_SUBDIR;
|
||||
sprintf(extrasmenu.demolist[i].title, "UP");
|
||||
}
|
||||
else if (dirmenu[i][DIR_TYPE] == EXT_FOLDER)
|
||||
{
|
||||
extrasmenu.demolist[i].type = MD_SUBDIR;
|
||||
strncpy(extrasmenu.demolist[i].title, dirmenu[i] + DIR_STRING, 64);
|
||||
}
|
||||
else
|
||||
{
|
||||
extrasmenu.demolist[i].type = MD_NOTLOADED;
|
||||
snprintf(extrasmenu.demolist[i].filepath, sizeof extrasmenu.demolist[i].filepath,
|
||||
// 255 = UINT8 limit. dirmenu entries are restricted to this length (see DIR_LEN).
|
||||
"%s%.255s", menupath, dirmenu[i] + DIR_STRING);
|
||||
sprintf(extrasmenu.demolist[i].title, ".....");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void M_ReplayHut(INT32 choice)
|
||||
{
|
||||
(void)choice;
|
||||
|
||||
if (demo.inreplayhut)
|
||||
{
|
||||
demo.rewinding = false;
|
||||
CL_ClearRewinds();
|
||||
}
|
||||
else
|
||||
{
|
||||
snprintf(menupath, 1024, "%s"PATHSEP"media"PATHSEP"replay"PATHSEP"online"PATHSEP, srb2home);
|
||||
menupathindex[(menudepthleft = menudepth-1)] = strlen(menupath);
|
||||
}
|
||||
|
||||
if (!preparefilemenu(false, true))
|
||||
{
|
||||
M_StartMessage("No replays found.\n\nPress (B)\n", NULL, MM_NOTHING);
|
||||
demo.inreplayhut = false;
|
||||
return;
|
||||
}
|
||||
else if (!demo.inreplayhut)
|
||||
{
|
||||
dir_on[menudepthleft] = 0;
|
||||
}
|
||||
|
||||
extrasmenu.replayScrollTitle = 0;
|
||||
extrasmenu.replayScrollDelay = TICRATE;
|
||||
extrasmenu.replayScrollDir = 1;
|
||||
|
||||
M_PrepReplayList();
|
||||
|
||||
if (!demo.inreplayhut)
|
||||
M_SetupNextMenu(&EXTRAS_ReplayHutDef, false);
|
||||
|
||||
demo.inreplayhut = true;
|
||||
}
|
||||
|
||||
// key handler
|
||||
void M_HandleReplayHutList(INT32 choice)
|
||||
{
|
||||
|
||||
const UINT8 pid = 0;
|
||||
(void) choice;
|
||||
|
||||
if (menucmd[pid].dpad_ud < 0)
|
||||
{
|
||||
if (dir_on[menudepthleft])
|
||||
dir_on[menudepthleft]--;
|
||||
else
|
||||
return;
|
||||
//M_PrevOpt();
|
||||
|
||||
S_StartSound(NULL, sfx_s3k5b);
|
||||
M_SetMenuDelay(pid);
|
||||
extrasmenu.replayScrollTitle = 0; extrasmenu.replayScrollDelay = TICRATE; extrasmenu.replayScrollDir = 1;
|
||||
}
|
||||
|
||||
else if (menucmd[pid].dpad_ud > 0)
|
||||
{
|
||||
if (dir_on[menudepthleft] < sizedirmenu-1)
|
||||
dir_on[menudepthleft]++;
|
||||
else
|
||||
return;
|
||||
//itemOn = 0; // Not M_NextOpt because that would take us to the extra dummy item
|
||||
|
||||
S_StartSound(NULL, sfx_s3k5b);
|
||||
M_SetMenuDelay(pid);
|
||||
extrasmenu.replayScrollTitle = 0; extrasmenu.replayScrollDelay = TICRATE; extrasmenu.replayScrollDir = 1;
|
||||
}
|
||||
|
||||
else if (M_MenuBackPressed(pid))
|
||||
{
|
||||
M_SetMenuDelay(pid);
|
||||
M_QuitReplayHut();
|
||||
}
|
||||
|
||||
else if (M_MenuConfirmPressed(pid))
|
||||
{
|
||||
M_SetMenuDelay(pid);
|
||||
switch (dirmenu[dir_on[menudepthleft]][DIR_TYPE])
|
||||
{
|
||||
case EXT_FOLDER:
|
||||
strcpy(&menupath[menupathindex[menudepthleft]],dirmenu[dir_on[menudepthleft]]+DIR_STRING);
|
||||
if (menudepthleft)
|
||||
{
|
||||
menupathindex[--menudepthleft] = strlen(menupath);
|
||||
menupath[menupathindex[menudepthleft]] = 0;
|
||||
|
||||
if (!preparefilemenu(false, true))
|
||||
{
|
||||
S_StartSound(NULL, sfx_s224);
|
||||
M_StartMessage(va("%c%s\x80\nThis folder is empty.\n\nPress (B)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), M_AddonsHeaderPath()),NULL,MM_NOTHING);
|
||||
menupath[menupathindex[++menudepthleft]] = 0;
|
||||
|
||||
if (!preparefilemenu(true, true))
|
||||
{
|
||||
M_QuitReplayHut();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
S_StartSound(NULL, sfx_s3k5b);
|
||||
dir_on[menudepthleft] = 1;
|
||||
M_PrepReplayList();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
S_StartSound(NULL, sfx_s26d);
|
||||
M_StartMessage(va("%c%s\x80\nThis folder is too deep to navigate to!\n\nPress (B)\n", ('\x80' + (highlightflags>>V_CHARCOLORSHIFT)), M_AddonsHeaderPath()),NULL,MM_NOTHING);
|
||||
menupath[menupathindex[menudepthleft]] = 0;
|
||||
}
|
||||
break;
|
||||
case EXT_UP:
|
||||
S_StartSound(NULL, sfx_s3k5b);
|
||||
menupath[menupathindex[++menudepthleft]] = 0;
|
||||
if (!preparefilemenu(false, true))
|
||||
{
|
||||
M_QuitReplayHut();
|
||||
return;
|
||||
}
|
||||
M_PrepReplayList();
|
||||
break;
|
||||
default:
|
||||
M_SetupNextMenu(&EXTRAS_ReplayStartDef, true);
|
||||
|
||||
extrasmenu.replayScrollTitle = 0;
|
||||
extrasmenu.replayScrollDelay = TICRATE;
|
||||
extrasmenu.replayScrollDir = 1;
|
||||
|
||||
switch (extrasmenu.demolist[dir_on[menudepthleft]].addonstatus)
|
||||
{
|
||||
case DFILE_ERROR_CANNOTLOAD:
|
||||
// Only show "Watch Replay Without Addons"
|
||||
EXTRAS_ReplayStart[0].status = IT_DISABLED;
|
||||
EXTRAS_ReplayStart[1].status = IT_CALL|IT_STRING;
|
||||
//EXTRAS_ReplayStart[1].alphaKey = 0;
|
||||
EXTRAS_ReplayStart[2].status = IT_DISABLED;
|
||||
itemOn = 1;
|
||||
break;
|
||||
|
||||
case DFILE_ERROR_NOTLOADED:
|
||||
case DFILE_ERROR_INCOMPLETEOUTOFORDER:
|
||||
// Show "Load Addons and Watch Replay" and "Watch Replay Without Addons"
|
||||
EXTRAS_ReplayStart[0].status = IT_CALL|IT_STRING;
|
||||
EXTRAS_ReplayStart[1].status = IT_CALL|IT_STRING;
|
||||
//EXTRAS_ReplayStart[1].alphaKey = 10;
|
||||
EXTRAS_ReplayStart[2].status = IT_DISABLED;
|
||||
itemOn = 0;
|
||||
break;
|
||||
|
||||
case DFILE_ERROR_EXTRAFILES:
|
||||
case DFILE_ERROR_OUTOFORDER:
|
||||
default:
|
||||
// Show "Watch Replay"
|
||||
EXTRAS_ReplayStart[0].status = IT_DISABLED;
|
||||
EXTRAS_ReplayStart[1].status = IT_DISABLED;
|
||||
EXTRAS_ReplayStart[2].status = IT_CALL|IT_STRING;
|
||||
//EXTRAS_ReplayStart[2].alphaKey = 0;
|
||||
itemOn = 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean M_QuitReplayHut(void)
|
||||
{
|
||||
if (extrasmenu.demolist)
|
||||
Z_Free(extrasmenu.demolist);
|
||||
extrasmenu.demolist = NULL;
|
||||
|
||||
demo.inreplayhut = false;
|
||||
|
||||
M_GoBack(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void M_HutStartReplay(INT32 choice)
|
||||
{
|
||||
(void)choice;
|
||||
|
||||
restoreMenu = &EXTRAS_ReplayHutDef;
|
||||
|
||||
M_ClearMenus(false);
|
||||
demo.loadfiles = (itemOn == 0);
|
||||
demo.ignorefiles = (itemOn != 0);
|
||||
|
||||
G_DoPlayDemo(extrasmenu.demolist[dir_on[menudepthleft]].filepath);
|
||||
}
|
||||
|
|
@ -257,10 +257,10 @@ void M_PlaybackQuit(INT32 choice)
|
|||
(void)choice;
|
||||
G_StopDemo();
|
||||
|
||||
if (demo.inreplayhut)
|
||||
M_StartControlPanel();
|
||||
else if (modeattacking)
|
||||
if (modeattacking)
|
||||
M_EndModeAttackRun();
|
||||
else if (restoreMenu)
|
||||
M_StartControlPanel();
|
||||
else
|
||||
D_StartTitle();
|
||||
}
|
||||
|
|
|
|||
2
thirdparty/CMakeLists.txt
vendored
2
thirdparty/CMakeLists.txt
vendored
|
|
@ -34,6 +34,8 @@ if (SRB2_CONFIG_ENABLE_WEBM_MOVIES)
|
|||
include("cpm-libyuv.cmake")
|
||||
endif()
|
||||
|
||||
include("cpm-nlohmann-json.cmake")
|
||||
|
||||
add_subdirectory(tcbrindle_span)
|
||||
add_subdirectory(stb_vorbis)
|
||||
add_subdirectory(stb_rect_pack)
|
||||
|
|
|
|||
6
thirdparty/cpm-nlohmann-json.cmake
vendored
Normal file
6
thirdparty/cpm-nlohmann-json.cmake
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
CPMAddPackage(
|
||||
NAME nlohmann_json
|
||||
VERSION 3.11.2
|
||||
URL "https://github.com/nlohmann/json/releases/download/v3.11.2/json.tar.xz"
|
||||
EXCLUDE_FROM_ALL ON
|
||||
)
|
||||
Loading…
Add table
Reference in a new issue