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 menu_t EXTRAS_ReplayStartDef;
|
||||
|
||||
extern menuitem_t EXTRAS_EggTV[];
|
||||
extern menu_t EXTRAS_EggTVDef;
|
||||
|
||||
// PAUSE
|
||||
extern menuitem_t PAUSE_Main[];
|
||||
extern menu_t PAUSE_MainDef;
|
||||
|
|
@ -1039,6 +1042,10 @@ 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:
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ target_sources(SRB2SDL2 PRIVATE
|
|||
extras-1.c
|
||||
extras-addons.c
|
||||
extras-challenges.c
|
||||
extras-egg-tv.cpp
|
||||
extras-replay-hut.c
|
||||
extras-statistics.c
|
||||
main-1.c
|
||||
|
|
@ -40,4 +41,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
|
||||
)
|
||||
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},
|
||||
|
||||
{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";
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue