Merge branch 'egg-tv' into 'master'

Egg TV

See merge request KartKrew/Kart!1209
This commit is contained in:
Oni 2023-04-30 23:44:26 +00:00
commit 1392b04b7a
18 changed files with 2470 additions and 727 deletions

View file

@ -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)

View file

@ -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

View file

@ -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!"

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -0,0 +1,4 @@
target_sources(SRB2SDL2 PRIVATE
EggTV.cpp
EggTVData.cpp
)

File diff suppressed because it is too large Load diff

View 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__

View 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());
}
}

View 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__

View 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__

View file

@ -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
View 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";
}

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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
View 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
)