mirror of
https://github.com/KartKrewDev/RingRacers.git
synced 2025-10-30 08:01:28 +00:00
Add Egg TV menu
This commit is contained in:
parent
7ed356d620
commit
3ccfb71f04
10 changed files with 2434 additions and 1 deletions
|
|
@ -411,6 +411,9 @@ extern menu_t EXTRAS_ReplayHutDef;
|
||||||
extern menuitem_t EXTRAS_ReplayStart[];
|
extern menuitem_t EXTRAS_ReplayStart[];
|
||||||
extern menu_t EXTRAS_ReplayStartDef;
|
extern menu_t EXTRAS_ReplayStartDef;
|
||||||
|
|
||||||
|
extern menuitem_t EXTRAS_EggTV[];
|
||||||
|
extern menu_t EXTRAS_EggTVDef;
|
||||||
|
|
||||||
// PAUSE
|
// PAUSE
|
||||||
extern menuitem_t PAUSE_Main[];
|
extern menuitem_t PAUSE_Main[];
|
||||||
extern menu_t PAUSE_MainDef;
|
extern menu_t PAUSE_MainDef;
|
||||||
|
|
@ -1039,6 +1042,10 @@ boolean M_QuitReplayHut(void);
|
||||||
void M_HutStartReplay(INT32 choice);
|
void M_HutStartReplay(INT32 choice);
|
||||||
void M_PrepReplayList(void);
|
void M_PrepReplayList(void);
|
||||||
|
|
||||||
|
// Extras: Egg TV
|
||||||
|
void M_EggTV(INT32 choice);
|
||||||
|
void M_EggTV_RefreshButtonLabels(void);
|
||||||
|
|
||||||
|
|
||||||
// Pause menu:
|
// Pause menu:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ target_sources(SRB2SDL2 PRIVATE
|
||||||
extras-1.c
|
extras-1.c
|
||||||
extras-addons.c
|
extras-addons.c
|
||||||
extras-challenges.c
|
extras-challenges.c
|
||||||
|
extras-egg-tv.cpp
|
||||||
extras-replay-hut.c
|
extras-replay-hut.c
|
||||||
extras-statistics.c
|
extras-statistics.c
|
||||||
main-1.c
|
main-1.c
|
||||||
|
|
@ -40,4 +41,5 @@ target_sources(SRB2SDL2 PRIVATE
|
||||||
play-online-server-browser.c
|
play-online-server-browser.c
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_subdirectory(class-egg-tv)
|
||||||
add_subdirectory(transient)
|
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
|
||||||
|
)
|
||||||
1156
src/menus/class-egg-tv/EggTV.cpp
Normal file
1156
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__
|
||||||
369
src/menus/class-egg-tv/EggTVData.cpp
Normal file
369
src/menus/class-egg-tv/EggTVData.cpp
Normal file
|
|
@ -0,0 +1,369 @@
|
||||||
|
// 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(favoritesPath_) >> 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()))
|
||||||
|
{
|
||||||
|
if (!entry.is_regular_file())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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) :
|
||||||
|
size_(std::distance(fs::directory_iterator(entry.path()), fs::directory_iterator())),
|
||||||
|
time_(time_point_conv<decltype(time_)>(entry.last_write_time())),
|
||||||
|
tv_(&tv),
|
||||||
|
name_(entry.path().filename().string())
|
||||||
|
{
|
||||||
|
SRB2_ASSERT(entry.path().parent_path() == tv_->root_);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
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},
|
NULL, {.routine = M_Statistics}, 0, 0},
|
||||||
|
|
||||||
{IT_STRING | IT_CALL, NULL, NULL,
|
{IT_STRING | IT_CALL, NULL, NULL,
|
||||||
NULL, {.routine = M_ReplayHut}, 0, 0},
|
NULL, {.routine = M_EggTV}, 0, 0},
|
||||||
|
|
||||||
{IT_STRING | IT_CALL, NULL, NULL,
|
{IT_STRING | IT_CALL, NULL, NULL,
|
||||||
NULL, {.routine = M_SoundTest}, 0, 0},
|
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";
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue