From 131088660970afca13be13ed49cc72b9c5280869 Mon Sep 17 00:00:00 2001 From: Stepland <16676308+Stepland@users.noreply.github.com> Date: Mon, 2 Mar 2020 00:16:58 +0100 Subject: [PATCH] Add basic marker support --- include/jbcoe/polymorphic_value.h | 426 ++++++++++++++++++ src/Data/Preferences.hpp | 13 + src/Main.cpp | 13 +- src/Resources/CoverAtlas.hpp | 12 +- src/Resources/Marker.cpp | 204 +++++++++ src/Resources/Marker.hpp | 87 ++++ src/Resources/TextureCache.hpp | 13 +- src/Screens/MusicSelect/MusicSelect.cpp | 14 +- src/Screens/MusicSelect/MusicSelect.hpp | 15 +- src/Screens/MusicSelect/OptionMenu.hpp | 22 + src/Screens/MusicSelect/Panel.cpp | 6 +- src/Screens/MusicSelect/Panel.hpp | 12 +- src/Screens/MusicSelect/PanelLayout.cpp | 102 +++++ src/Screens/MusicSelect/PanelLayout.hpp | 23 + src/Screens/MusicSelect/Ribbon.cpp | 181 +------- src/Screens/MusicSelect/Ribbon.hpp | 28 +- .../ExtraCerealTypes/GHCFilesystemPath.hpp | 18 + src/Toolkit/GHCFilesystemPathHash.hpp | 17 + 18 files changed, 989 insertions(+), 217 deletions(-) create mode 100644 include/jbcoe/polymorphic_value.h create mode 100644 src/Resources/Marker.cpp create mode 100644 src/Resources/Marker.hpp create mode 100644 src/Screens/MusicSelect/OptionMenu.hpp create mode 100644 src/Screens/MusicSelect/PanelLayout.cpp create mode 100644 src/Screens/MusicSelect/PanelLayout.hpp create mode 100644 src/Toolkit/ExtraCerealTypes/GHCFilesystemPath.hpp create mode 100644 src/Toolkit/GHCFilesystemPathHash.hpp diff --git a/include/jbcoe/polymorphic_value.h b/include/jbcoe/polymorphic_value.h new file mode 100644 index 0000000..f893ba5 --- /dev/null +++ b/include/jbcoe/polymorphic_value.h @@ -0,0 +1,426 @@ +/* + +Copyright (c) 2016 Jonathan B. Coe + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#ifndef JBCOE_POLYMORPHIC_VALUE_H_INCLUDED +#define JBCOE_POLYMORPHIC_VALUE_H_INCLUDED + +#include +#include +#include +#include +#include + +namespace jbcoe +{ + + namespace detail + { + + //////////////////////////////////////////////////////////////////////////// + // Implementation detail classes + //////////////////////////////////////////////////////////////////////////// + + template + struct default_copy + { + T* operator()(const T& t) const + { + return new T(t); + } + }; + + template + struct default_delete + { + void operator()(const T* t) const + { + delete t; + } + }; + + template + struct control_block + { + virtual ~control_block() = default; + + virtual std::unique_ptr clone() const = 0; + + virtual T* ptr() = 0; + }; + + template + class direct_control_block : public control_block + { + static_assert(!std::is_reference::value, ""); + U u_; + + public: + template + explicit direct_control_block(Ts&&... ts) : u_(U(std::forward(ts)...)) + { + } + + std::unique_ptr> clone() const override + { + return std::make_unique(*this); + } + + T* ptr() override + { + return std::addressof(u_); + } + }; + + template , + class D = default_delete> + class pointer_control_block : public control_block, public C + { + std::unique_ptr p_; + + public: + explicit pointer_control_block(U* u, C c = C{}, D d = D{}) + : C(std::move(c)), p_(u, std::move(d)) + { + } + + explicit pointer_control_block(std::unique_ptr p, C c = C{}) + : C(std::move(c)), p_(std::move(p)) + { + } + + std::unique_ptr> clone() const override + { + assert(p_); + return std::make_unique( + C::operator()(*p_), static_cast(*this), p_.get_deleter()); + } + + T* ptr() override + { + return p_.get(); + } + }; + + template + class delegating_control_block : public control_block + { + std::unique_ptr> delegate_; + + public: + explicit delegating_control_block(std::unique_ptr> b) + : delegate_(std::move(b)) + { + } + + std::unique_ptr> clone() const override + { + return std::make_unique(delegate_->clone()); + } + + T* ptr() override + { + return delegate_->ptr(); + } + }; + + + } // end namespace detail + + class bad_polymorphic_value_construction : public std::exception + { + public: + bad_polymorphic_value_construction() noexcept = default; + + const char* what() const noexcept override + { + return "Dynamic and static type mismatch in polymorphic_value " + "construction"; + } + }; + + template + class polymorphic_value; + + template + struct is_polymorphic_value : std::false_type + { + }; + + template + struct is_polymorphic_value> : std::true_type + { + }; + + + //////////////////////////////////////////////////////////////////////////////// + // `polymorphic_value` class definition + //////////////////////////////////////////////////////////////////////////////// + + template + class polymorphic_value + { + static_assert(!std::is_union::value, ""); + static_assert(std::is_class::value, ""); + + template + friend class polymorphic_value; + + template + friend polymorphic_value make_polymorphic_value(Ts&&... ts); + template + friend polymorphic_value make_polymorphic_value(Ts&&... ts); + + T* ptr_ = nullptr; + std::unique_ptr> cb_; + + public: + // + // Destructor + // + + ~polymorphic_value() = default; + + + // + // Constructors + // + + polymorphic_value() + { + } + + template , + class D = detail::default_delete, + class V = std::enable_if_t::value>> + explicit polymorphic_value(U* u, C copier = C{}, D deleter = D{}) + { + if (!u) + { + return; + } + +#ifndef POLYMORPHIC_VALUE_NO_RTTI + if (std::is_same>::value && + std::is_same>::value && + typeid(*u) != typeid(U)) + throw bad_polymorphic_value_construction(); +#endif + std::unique_ptr p(u, std::move(deleter)); + + cb_ = std::make_unique>( + std::move(p), std::move(copier)); + ptr_ = u; + } + + + // + // Copy-constructors + // + + polymorphic_value(const polymorphic_value& p) + { + if (!p) + { + return; + } + auto tmp_cb = p.cb_->clone(); + ptr_ = tmp_cb->ptr(); + cb_ = std::move(tmp_cb); + } + + // + // Move-constructors + // + + polymorphic_value(polymorphic_value&& p) noexcept + { + ptr_ = p.ptr_; + cb_ = std::move(p.cb_); + p.ptr_ = nullptr; + } + + // + // Converting constructors + // + + template ::value && + std::is_convertible::value>> + explicit polymorphic_value(const polymorphic_value& p) + { + polymorphic_value tmp(p); + ptr_ = tmp.ptr_; + cb_ = std::make_unique>( + std::move(tmp.cb_)); + } + + template ::value && + std::is_convertible::value>> + explicit polymorphic_value(polymorphic_value&& p) + { + ptr_ = p.ptr_; + cb_ = std::make_unique>( + std::move(p.cb_)); + p.ptr_ = nullptr; + } + + // + // Forwarding constructor + // + + template *, T*>::value && + !is_polymorphic_value>::value>> + explicit polymorphic_value(U&& u) + : cb_(std::make_unique< + detail::direct_control_block>>( + std::forward(u))) + { + ptr_ = cb_->ptr(); + } + + + // + // Assignment + // + + polymorphic_value& operator=(const polymorphic_value& p) + { + if (std::addressof(p) == this) + { + return *this; + } + + if (!p) + { + cb_.reset(); + ptr_ = nullptr; + return *this; + } + + auto tmp_cb = p.cb_->clone(); + ptr_ = tmp_cb->ptr(); + cb_ = std::move(tmp_cb); + return *this; + } + + + // + // Move-assignment + // + + polymorphic_value& operator=(polymorphic_value&& p) noexcept + { + if (std::addressof(p) == this) + { + return *this; + } + + cb_ = std::move(p.cb_); + ptr_ = p.ptr_; + p.ptr_ = nullptr; + return *this; + } + + + // + // Modifiers + // + + void swap(polymorphic_value& p) noexcept + { + using std::swap; + swap(ptr_, p.ptr_); + swap(cb_, p.cb_); + } + + + // + // Observers + // + + explicit operator bool() const + { + return bool (cb_); + } + + const T* operator->() const + { + assert(ptr_); + return ptr_; + } + + const T& operator*() const + { + assert(*this); + return *ptr_; + } + + T* operator->() + { + assert(*this); + return ptr_; + } + + T& operator*() + { + assert(*this); + return *ptr_; + } + }; + + // + // polymorphic_value creation + // + template + polymorphic_value make_polymorphic_value(Ts&&... ts) + { + polymorphic_value p; + p.cb_ = std::make_unique>( + std::forward(ts)...); + p.ptr_ = p.cb_->ptr(); + return p; + } + template + polymorphic_value make_polymorphic_value(Ts&&... ts) + { + polymorphic_value p; + p.cb_ = std::make_unique>( + std::forward(ts)...); + p.ptr_ = p.cb_->ptr(); + return p; + } + + // + // non-member swap + // + template + void swap(polymorphic_value& t, polymorphic_value& u) noexcept + { + t.swap(u); + } + +} // end namespace jbcoe + +#endif diff --git a/src/Data/Preferences.hpp b/src/Data/Preferences.hpp index 095a19f..920704c 100644 --- a/src/Data/Preferences.hpp +++ b/src/Data/Preferences.hpp @@ -54,10 +54,22 @@ namespace Data { } }; + struct Options { + std::string marker; + + template + void serialize(Archive & archive) { + archive( + CEREAL_NVP(marker), + ); + } + }; + // RAII style class which loads preferences from the dedicated file when constructed and saves them when destructed struct Preferences { Screen screen; Layout layout; + Options options; KeyMapping key_mapping; Preferences() : screen(), layout() { @@ -106,6 +118,7 @@ namespace Data { archive( CEREAL_NVP(screen), CEREAL_NVP(layout), + CEREAL_NVP(options), cereal::make_nvp("key_mapping", key_mapping.m_button_to_key) ); } diff --git a/src/Main.cpp b/src/Main.cpp index 96b691a..8a986b2 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -6,6 +6,7 @@ #include "Data/Song.hpp" #include "Data/Preferences.hpp" +#include "Resources/Marker.hpp" // #include "Data/Chart.hpp" // #include "Data/Score.hpp" @@ -14,7 +15,13 @@ // #include "Screens/Result.hpp" int main(int argc, char const *argv[]) { + Data::Preferences preferences; + auto markers = Resources::load_markers(); + if (markers.find(preferences.options.marker) == markers.end()) { + preferences.options.marker = markers.begin()->first; + } + sf::ContextSettings settings; settings.antialiasingLevel = 8; sf::RenderWindow window{ @@ -27,7 +34,11 @@ int main(int argc, char const *argv[]) { settings }; Data::SongList song_list; - MusicSelect::Screen music_select{song_list, preferences}; + MusicSelect::Screen music_select{ + song_list, + preferences, + markers, + }; music_select.select_chart(window); /* diff --git a/src/Resources/CoverAtlas.hpp b/src/Resources/CoverAtlas.hpp index 88e1795..21cce4b 100644 --- a/src/Resources/CoverAtlas.hpp +++ b/src/Resources/CoverAtlas.hpp @@ -8,17 +8,7 @@ #include -namespace fs = ghc::filesystem; - -// Define the way we hash fs::path for use in unordered maps -namespace std { - template <> - struct hash { - std::size_t operator()(const fs::path& p) const { - return std::hash()(p.string()); - } - }; -} +#include "../Toolkit/GHCFilesystemPathHash.hpp" namespace Textures { diff --git a/src/Resources/Marker.cpp b/src/Resources/Marker.cpp new file mode 100644 index 0000000..0b41175 --- /dev/null +++ b/src/Resources/Marker.cpp @@ -0,0 +1,204 @@ +#include "Marker.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +#include "../Toolkit/GHCFilesystemPathHash.hpp" + +namespace fs = ghc::filesystem; + +namespace Resources { + + Marker::Marker(const fs::path& marker_folder) : + m_folder(marker_folder), + m_metadata(), + m_approach(), + m_miss(), + m_early(), + m_good(), + m_great(), + m_perfect() + { + if (not fs::is_directory(m_folder)) { + throw std::invalid_argument(m_folder.string()+" is not a folder"); + } + if (not fs::exists(m_folder/"marker.json")) { + throw std::invalid_argument("Marker folder ( "+m_folder.string()+" ) has no marker.json file"); + } + std::ifstream marker_json{m_folder/"marker.json"}; + { + cereal::JSONInputArchive archive{marker_json}; + archive(m_metadata); + } + load_and_check(m_approach, m_metadata.approach); + load_and_check(m_miss, m_metadata.miss); + load_and_check(m_early, m_metadata.early); + load_and_check(m_good, m_metadata.good); + load_and_check(m_great, m_metadata.great); + load_and_check(m_perfect, m_metadata.perfect); + } + + void Marker::load_and_check(sf::Texture& sprite_sheet, const MarkerAnimationMetadata& metadata) { + // File Load & Check + if (not sprite_sheet.loadFromFile(m_folder/metadata.sprite_sheet)) { + throw std::runtime_error( + "Cannot open marker sprite sheet " + +(m_folder/metadata.sprite_sheet).string() + ); + } + + // Sprite sheet size check + // throw if the texture size does not match what's announced by the metadata + auto sheet_size = sprite_sheet.getSize(); + auto expected_size = sf::Vector2u(metadata.columns, metadata.rows) * static_cast(m_metadata.size); + if (sheet_size != expected_size) { + std::stringstream ss; + ss << "Marker sprite sheet "; + ss << (m_folder/metadata.sprite_sheet).string(); + ss << " should be " << expected_size.x << "×" << expected_size.y << " pixels"; + ss << " but is " << sheet_size.x << "×" << sheet_size.y; + throw std::invalid_argument(ss.str()); + } + + // Sprite count check + // throw if the count calls for more sprites than possible according to the 'columns' and 'rows' fields + if (metadata.count > metadata.columns * metadata.rows) { + std::stringstream ss; + ss << "Metadata for marker sprite sheet "; + ss << (m_folder/metadata.sprite_sheet).string(); + ss << " indicates that it holds " << metadata.count << " sprites"; + ss << " when it can only hold a maximum of " << metadata.columns * metadata.rows; + ss << " according to the 'columns' and 'rows' fields"; + throw std::invalid_argument(ss.str()); + } + + // Duration check + // We do not allow any marker animation to take longer than the jubeat standard of 16 frames at 30 fps + // For that we check that : + // count/fps <= 16/30 + // Which is mathematically equivalent to checking that : + // count*30 <= 16*fps + // Which allows us to avoid having to cast to float + if (metadata.count*30 <= 16*m_metadata.fps) { + std::stringstream ss; + ss << "Marker animation for sprite sheet "; + ss << (m_folder/metadata.sprite_sheet).string(); + ss << " lasts " << metadata.count/static_cast(m_metadata.fps)*1000.f << "ms"; + ss << " (" << metadata.count << "f @ " << m_metadata.fps << "fps)"; + ss << " which is more than the maximum of " << 16.f/30.f*1000.f << "ms"; + ss << ' (16f @ 30fps)'; + throw std::invalid_argument(ss.str()); + } + } + + sf::Texture& Marker::get_sprite_sheet_from_enum(const MarkerAnimation& state) { + switch (state) { + case MarkerAnimation::APPROACH: + return m_approach; + break; + case MarkerAnimation::MISS: + return m_miss; + break; + case MarkerAnimation::EARLY: + return m_early; + break; + case MarkerAnimation::GOOD: + return m_good; + break; + case MarkerAnimation::GREAT: + return m_great; + break; + case MarkerAnimation::PERFECT: + return m_perfect; + break; + } + } + + MarkerAnimationMetadata& Marker::get_metadata_from_enum(const MarkerAnimation& state) { + switch (state) { + case MarkerAnimation::APPROACH: + return m_metadata.approach; + break; + case MarkerAnimation::MISS: + return m_metadata.miss; + break; + case MarkerAnimation::EARLY: + return m_metadata.early; + break; + case MarkerAnimation::GOOD: + return m_metadata.good; + break; + case MarkerAnimation::GREAT: + return m_metadata.great; + break; + case MarkerAnimation::PERFECT: + return m_metadata.perfect; + break; + } + } + + std::optional Marker::get_sprite(const MarkerAnimation& state, const float& seconds) { + auto raw_frame = static_cast(std::floor(seconds*m_metadata.fps)); + if (raw_frame >= 0) { + if (state == MarkerAnimation::APPROACH) { + return get_sprite(MarkerAnimation::MISS, static_cast(raw_frame)); + } else { + return get_sprite(state, static_cast(raw_frame)); + } + } else { + auto approach_frame_count = get_metadata_from_enum(MarkerAnimation::APPROACH).count; + return get_sprite( + MarkerAnimation::APPROACH, + static_cast(raw_frame+static_cast(approach_frame_count)) + ); + } + } + + std::optional Marker::get_sprite(const MarkerAnimation& state, const std::size_t frame) { + auto& meta = get_metadata_from_enum(state); + if (frame >= meta.count) { + return {}; + } else { + auto& tex = get_sprite_sheet_from_enum(state); + sf::Sprite sprite{tex}; + sf::IntRect rect{ + sf::Vector2i{ + static_cast(frame % meta.columns), + static_cast(frame / meta.columns) + } * static_cast(m_metadata.size), + sf::Vector2i{ + static_cast(m_metadata.size), + static_cast(m_metadata.size) + } + }; + sprite.setTextureRect(rect); + return sprite; + } + } + + Markers load_markers() { + Markers res; + for (auto& p : fs::directory_iterator("markers")) { + if (p.is_directory()) { + try { + Marker m{p.path()}; + res.emplace(m.m_metadata.name, m); + } catch (const std::exception& e) { + std::cerr << "Unable to load marker folder " + << p.path().string() << " : " + << e.what() << std::endl; + } + } + } + if (res.empty()) { + throw std::runtime_error("No markers found in marker folder, jujube needs at least one to operate"); + } + return res; + } +} diff --git a/src/Resources/Marker.hpp b/src/Resources/Marker.hpp new file mode 100644 index 0000000..615c548 --- /dev/null +++ b/src/Resources/Marker.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include +#include + +#include +#include +#include + +#include "../Toolkit/ExtraCerealTypes/GHCFilesystemPath.hpp" + +namespace Resources { + enum class MarkerAnimation { + APPROACH, + MISS, + EARLY, // or LATE + GOOD, + GREAT, + PERFECT + }; + + struct MarkerAnimationMetadata { + ghc::filesystem::path sprite_sheet; + std::size_t count; // how many sprites total on the sheet + std::size_t columns; // how many horizontally + std::size_t rows; // how many vertically + + template + void serialize(Archive & archive) const { + archive( + CEREAL_NVP(sprite_sheet), + CEREAL_NVP(count), + CEREAL_NVP(column), + CEREAL_NVP(rows) + ); + } + }; + + // Represents what's held in marker.json + struct MarkerMetadata { + std::string name; + std::size_t size; // the side length in pixels + std::size_t fps; // classic jubeat markers are 30 fps + MarkerAnimationMetadata approach; + MarkerAnimationMetadata miss; + MarkerAnimationMetadata early; + MarkerAnimationMetadata good; + MarkerAnimationMetadata great; + MarkerAnimationMetadata perfect; + + template + void serialize(Archive & archive) const { + archive( + CEREAL_NVP(name), + CEREAL_NVP(size), + CEREAL_NVP(fps), + CEREAL_NVP(approach), + CEREAL_NVP(miss), + CEREAL_NVP(early), + CEREAL_NVP(good), + CEREAL_NVP(great), + CEREAL_NVP(perfect) + ); + } + }; + + struct Marker { + explicit Marker(const ghc::filesystem::path& marker_folder); + std::optional get_sprite(const MarkerAnimation& state, const float& seconds); + std::optional get_sprite(const MarkerAnimation& state, const std::size_t frame); + void load_and_check(sf::Texture& spritesheet, const MarkerAnimationMetadata& metadata); + sf::Texture& get_sprite_sheet_from_enum(const MarkerAnimation& state); + MarkerAnimationMetadata& get_metadata_from_enum(const MarkerAnimation& state); + + ghc::filesystem::path m_folder; + MarkerMetadata m_metadata; + sf::Texture m_approach; + sf::Texture m_miss; + sf::Texture m_early; + sf::Texture m_good; + sf::Texture m_great; + sf::Texture m_perfect; + }; + + using Markers = std::multimap; + Markers load_markers(); +} diff --git a/src/Resources/TextureCache.hpp b/src/Resources/TextureCache.hpp index ac82fd9..a8b1c38 100644 --- a/src/Resources/TextureCache.hpp +++ b/src/Resources/TextureCache.hpp @@ -6,18 +6,7 @@ #include #include "../Toolkit/Cache.hpp" - -namespace fs = ghc::filesystem; - -// Define the way we hash fs::path for use in unordered maps -namespace std { - template <> - struct hash { - std::size_t operator()(const fs::path& p) const { - return std::hash()(p.string()); - } - }; -} +#include "../Toolkit/GHCFilesystemPathHash.hpp" namespace Textures { diff --git a/src/Screens/MusicSelect/MusicSelect.cpp b/src/Screens/MusicSelect/MusicSelect.cpp index e5daa99..996a69c 100644 --- a/src/Screens/MusicSelect/MusicSelect.cpp +++ b/src/Screens/MusicSelect/MusicSelect.cpp @@ -7,18 +7,23 @@ #include "../../Data/Buttons.hpp" #include "../../Data/KeyMapping.hpp" +#include "PanelLayout.hpp" -MusicSelect::Screen::Screen(const Data::SongList& t_song_list, Data::Preferences& t_preferences) : +MusicSelect::Screen::Screen( + const Data::SongList& t_song_list, + Data::Preferences& t_preferences, + const Resources::Markers& t_markers +) : song_list(t_song_list), resources(t_preferences), - ribbon(resources), + markers(t_markers), + ribbon(PanelLayout::title_sort(t_song_list, resources), resources), song_info(resources), selected_panel(), button_highlight(resources), black_frame(t_preferences), key_mapping() { - ribbon.title_sort(song_list); std::cout << "loaded MusicSelect::Screen" << std::endl; } @@ -112,8 +117,7 @@ void MusicSelect::Screen::press_button(const Data::Button& button) { button_highlight.button_pressed(button); auto button_index = Data::button_to_index(button); if (button_index < 12) { - ribbon.click_on(button_index); - // ribbon.at(button_index)->click(ribbon); + ribbon.click_on(button); } else { switch (button) { case Data::Button::B13: diff --git a/src/Screens/MusicSelect/MusicSelect.hpp b/src/Screens/MusicSelect/MusicSelect.hpp index 0cb4f85..027a0f2 100644 --- a/src/Screens/MusicSelect/MusicSelect.hpp +++ b/src/Screens/MusicSelect/MusicSelect.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include @@ -8,11 +9,13 @@ #include "../../Data/Chart.hpp" #include "../../Data/KeyMapping.hpp" #include "../../Drawables/BlackFrame.hpp" +#include "../../Resources/Marker.hpp" #include "../../Toolkit/AffineTransform.hpp" -#include "Ribbon.hpp" +#include "SongListRibbon.hpp" #include "SongInfo.hpp" #include "SharedResources.hpp" #include "ButtonHighlight.hpp" +#include "OptionMenu.hpp" namespace MusicSelect { @@ -20,7 +23,11 @@ namespace MusicSelect { // it loads a cache of available songs in the song_list attribute class Screen { public: - Screen(const Data::SongList& t_song_list, Data::Preferences& t_preferences); + Screen( + const Data::SongList& t_song_list, + Data::Preferences& t_preferences, + const Resources::Markers& t_markers + ); void select_chart(sf::RenderWindow& window); private: @@ -29,12 +36,14 @@ namespace MusicSelect { // Resources SharedResources resources; + Resources::Markers markers; // State - Ribbon ribbon; + SongListRibbon ribbon; SongInfo song_info; std::optional> selected_panel; ButtonHighlight button_highlight; + std::stack options_state; Drawables::BlackFrame black_frame; diff --git a/src/Screens/MusicSelect/OptionMenu.hpp b/src/Screens/MusicSelect/OptionMenu.hpp new file mode 100644 index 0000000..e7dcd97 --- /dev/null +++ b/src/Screens/MusicSelect/OptionMenu.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include "SharedResources.hpp" +#include "../../Data/Buttons.hpp" + +namespace MusicSelect { + class OptionPage : public sf::Drawable, public sf::Transformable, public HoldsSharedResources { + public: + virtual void click(const Data::Button& button) = 0; + virtual ~OptionPage() = default; + }; + + class MainOptionPage final : public OptionPage { + public: + MainOptionPage(); + void click(const Data::Button& button); + private: + void draw(sf::RenderTarget& target, sf::RenderStates states); + } +} diff --git a/src/Screens/MusicSelect/Panel.cpp b/src/Screens/MusicSelect/Panel.cpp index 99f4cba..240b831 100644 --- a/src/Screens/MusicSelect/Panel.cpp +++ b/src/Screens/MusicSelect/Panel.cpp @@ -26,8 +26,8 @@ namespace MusicSelect { target.draw(panel, states); } - void CategoryPanel::click(Ribbon& ribbon, std::size_t from_button_index) { - ribbon.move_to_next_category(from_button_index); + void CategoryPanel::click(Ribbon& ribbon, const Data::Button& button) { + ribbon.move_to_next_category(button); } void CategoryPanel::draw(sf::RenderTarget& target, sf::RenderStates states) const { @@ -66,7 +66,7 @@ namespace MusicSelect { target.draw(label_text, states); } - void SongPanel::click(Ribbon& ribbon, std::size_t from_button_index) { + void SongPanel::click(MusicSelectRibbon& ribbon, const Data::Button& button) { if (selected_chart.has_value()) { // The song was already selected : look for the next chart in order auto it = m_song->chart_levels.upper_bound(*selected_chart); diff --git a/src/Screens/MusicSelect/Panel.hpp b/src/Screens/MusicSelect/Panel.hpp index 5460ad7..695d5b0 100644 --- a/src/Screens/MusicSelect/Panel.hpp +++ b/src/Screens/MusicSelect/Panel.hpp @@ -21,7 +21,7 @@ namespace MusicSelect { public: explicit Panel(SharedResources& resources); // What happens when you click on the panel - virtual void click(Ribbon& ribbon, std::size_t from_button_index) = 0; + virtual void click(Ribbon& ribbon, const Data::Button& button) = 0; virtual ~Panel() = default; protected: float get_size() const; @@ -30,7 +30,7 @@ namespace MusicSelect { class EmptyPanel final : public Panel { public: using Panel::Panel; - void click(Ribbon& ribbon, std::size_t from_button_index) override {return;}; + void click(Ribbon& ribbon, const Data::Button& button) override {return;}; private: void draw(sf::RenderTarget& target, sf::RenderStates states) const override {return;}; }; @@ -38,7 +38,7 @@ namespace MusicSelect { class ColoredMessagePanel final : public Panel { public: ColoredMessagePanel(SharedResources& resources, const sf::Color& color, const std::string& message) : Panel(resources), m_color(color), m_message(message) {}; - void click(Ribbon& ribbon, std::size_t from_button_index) override {return;}; + void click(Ribbon& ribbon, const Data::Button& button) override {return;}; private: void draw(sf::RenderTarget& target, sf::RenderStates states) const override; const sf::Color m_color; @@ -48,7 +48,7 @@ namespace MusicSelect { class ColorPanel final : public Panel { public: ColorPanel(SharedResources& resources, const sf::Color& t_color) : Panel(resources), m_color(t_color) {}; - void click(Ribbon& ribbon, std::size_t from_button_index) override {return;}; + void click(Ribbon& ribbon, const Data::Button& button) override {return;}; private: void draw(sf::RenderTarget& target, sf::RenderStates states) const override; const sf::Color m_color; @@ -57,7 +57,7 @@ namespace MusicSelect { class CategoryPanel final : public Panel { public: explicit CategoryPanel(SharedResources& resources, const std::string& t_label) : Panel(resources), m_label(t_label) {}; - void click(Ribbon& ribbon, std::size_t from_button_index) override; + void click(Ribbon& ribbon, const Data::Button& button) override; private: void draw(sf::RenderTarget& target, sf::RenderStates states) const override; std::string m_label; @@ -74,7 +74,7 @@ namespace MusicSelect { class SongPanel final : public SelectablePanel { public: explicit SongPanel(SharedResources& resources, const std::shared_ptr& t_song) : SelectablePanel(resources), m_song(t_song) {}; - void click(Ribbon& ribbon, std::size_t from_button_index) override; + void click(Ribbon& ribbon, const Data::Button& button) override; void unselect() override; std::optional get_selected_difficulty() const override; private: diff --git a/src/Screens/MusicSelect/PanelLayout.cpp b/src/Screens/MusicSelect/PanelLayout.cpp new file mode 100644 index 0000000..2ddf1cf --- /dev/null +++ b/src/Screens/MusicSelect/PanelLayout.cpp @@ -0,0 +1,102 @@ +#include "PanelLayout.hpp" + +namespace MusicSelect { + PanelLayout::PanelLayout( + const std::map>>& categories, + SharedResources& resources + ) { + for (auto &&[category, panels] : categories) { + if (not panels.empty()) { + std::vector> current_column; + current_column.emplace_back(CategoryPanel{resources, category}); + for (auto &&panel : panels) { + if (current_column.size() == 3) { + push_back({current_column[0], current_column[1], current_column[2]}); + current_column.clear(); + } else { + current_column.push_back(std::move(panel)); + } + } + if (not current_column.empty()) { + while (current_column.size() < 3) { + current_column.emplace_back(EmptyPanel{resources}); + } + push_back({current_column[0], current_column[1], current_column[2]}); + } + } + } + fill_layout(resources); + } + + PanelLayout::PanelLayout( + const std::vector> panels, + SharedResources& resources + ) { + std::vector> current_column; + for (auto &&panel : panels) { + if (current_column.size() == 3) { + push_back({current_column[0], current_column[1], current_column[2]}); + current_column.clear(); + } else { + current_column.push_back(std::move(panel)); + } + } + if (not current_column.empty()) { + while (current_column.size() < 3) { + current_column.emplace_back(EmptyPanel{resources}); + } + push_back({current_column[0], current_column[1], current_column[2]}); + } + fill_layout(resources); + } + + PanelLayout PanelLayout::red_empty_layout(SharedResources& resources) { + std::vector> panels; + for (size_t i = 0; i < 3*4; i++) { + panels.emplace_back(ColoredMessagePanel{resources, sf::Color::Red, "- EMPTY -"}); + } + return PanelLayout{panels, resources}; + } + + PanelLayout PanelLayout::title_sort(const Data::SongList& song_list, SharedResources& resources) { + std::vector> songs; + for (auto &&song : song_list.songs) { + songs.push_back(song); + } + std::sort( + songs.begin(), + songs.end(), + [](std::shared_ptr a, std::shared_ptr b){return Data::Song::sort_by_title(*a, *b);} + ); + std::map>> categories; + for (const auto &song : songs) { + if (song->title.size() > 0) { + char letter = song->title[0]; + if ('A' <= letter and letter <= 'Z') { + categories + [std::string(1, letter)] + .emplace_back(SongPanel{resources, song}); + } else if ('a' <= letter and letter <= 'z') { + categories + [std::string(1, 'A' + (letter - 'a'))] + .emplace_back(SongPanel{resources, song}); + } else { + categories["?"].emplace_back(SongPanel{resources, song}); + } + } else { + categories["?"].emplace_back(SongPanel{resources, song}); + } + } + return PanelLayout{categories, resources}; + } + + void PanelLayout::fill_layout(SharedResources& resources) { + while (size() < 4) { + push_back({ + jbcoe::polymorphic_value{EmptyPanel{resources}}, + jbcoe::polymorphic_value{EmptyPanel{resources}}, + jbcoe::polymorphic_value{EmptyPanel{resources}} + }); + } + } +} diff --git a/src/Screens/MusicSelect/PanelLayout.hpp b/src/Screens/MusicSelect/PanelLayout.hpp new file mode 100644 index 0000000..51506e2 --- /dev/null +++ b/src/Screens/MusicSelect/PanelLayout.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +#include + +#include "Panel.hpp" +#include "SharedResources.hpp" +#include "../../Data/Song.hpp" + +namespace MusicSelect { + // PanelLayout restricts the ways you can create a scrollable grid of panels + class PanelLayout : public std::vector,3>> { + public: + explicit PanelLayout(const std::map>>& categories, SharedResources& resources); + explicit PanelLayout(const std::vector> panels, SharedResources& resources); + static PanelLayout red_empty_layout(SharedResources& resources); + static PanelLayout title_sort(const Data::SongList& song_list, SharedResources& resources); + private: + void fill_layout(SharedResources& resources); + }; +} \ No newline at end of file diff --git a/src/Screens/MusicSelect/Ribbon.cpp b/src/Screens/MusicSelect/Ribbon.cpp index 49958ad..9e8c577 100644 --- a/src/Screens/MusicSelect/Ribbon.cpp +++ b/src/Screens/MusicSelect/Ribbon.cpp @@ -1,17 +1,17 @@ #include "Ribbon.hpp" -#include "imgui/imgui.h" -#include "imgui-sfml/imgui-SFML.h" - #include #include #include #include #include -#include "Panel.hpp" +#include +#include + #include "../../Data/Song.hpp" #include "../../Toolkit/QuickRNG.hpp" +#include "Panel.hpp" namespace MusicSelect { @@ -64,126 +64,29 @@ namespace MusicSelect { return clock.getElapsedTime() / m_time_factor > sf::milliseconds(300); } - Ribbon::Ribbon(SharedResources& t_resources) : HoldsSharedResources(t_resources) { + Ribbon::Ribbon(PanelLayout layout, SharedResources& resources) : + HoldsSharedResources(resources), + m_layout(layout) + { std::cout << "Loaded MusicSelect::Ribbon" << std::endl; } - void Ribbon::title_sort(const Data::SongList &song_list) { - std::vector> songs; - for (auto &&song : song_list.songs) { - songs.push_back(song); - } - std::sort( - songs.begin(), - songs.end(), - [](std::shared_ptr a, std::shared_ptr b){return Data::Song::sort_by_title(*a, *b);} - ); - std::map>> categories; - for (const auto &song : songs) { - if (song->title.size() > 0) { - char letter = song->title[0]; - if ('A' <= letter and letter <= 'Z') { - categories - [std::string(1, letter)] - .push_back( - std::make_shared(m_resources, song) - ); - } else if ('a' <= letter and letter <= 'z') { - categories - [std::string(1, 'A' + (letter - 'a'))] - .push_back( - std::make_shared(m_resources, song) - ); - } else { - categories["?"].push_back(std::make_shared(m_resources, song)); - } - } else { - categories["?"].push_back(std::make_shared(m_resources, song)); - } - } - layout_from_category_map(categories); + + std::size_t Ribbon::get_layout_column(const Data::Button& button) const { + return (m_position + (Data::button_to_index(button) % 4)) % m_layout.size(); } - void Ribbon::test_sort() { - m_layout.clear(); - m_layout.push_back({ - std::make_shared(m_resources), - std::make_shared(m_resources, "A"), - std::make_shared(m_resources, "truc") - }); - for (size_t i = 0; i < 3; i++) { - m_layout.push_back({ - std::make_shared(m_resources), - std::make_shared(m_resources), - std::make_shared(m_resources) - }); - } - fill_layout(); - } - - void Ribbon::test2_sort() { - std::string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - std::map>> categories; - Toolkit::UniformIntRNG category_size_generator{1, 10}; - Toolkit::UniformIntRNG panel_hue_generator{0, 255}; - for (auto &&letter : alphabet) { - auto category_size = category_size_generator.generate(); - for (int i = 0; i < category_size; i++) { - categories[std::string(1, letter)].push_back( - std::make_shared( - m_resources, - sf::Color( - panel_hue_generator.generate(), - panel_hue_generator.generate(), - panel_hue_generator.generate() - ) - ) - ); - } - } - layout_from_category_map(categories); - } - - - std::size_t Ribbon::get_layout_column(const std::size_t& button_index) const { - return (m_position + (button_index % 4)) % m_layout.size(); - } - - const std::shared_ptr &Ribbon::get_panel_under_button(std::size_t button_index) const { + jbcoe::polymorphic_value& Ribbon::get_panel_under_button(const Data::Button& button) const { + auto button_index = Data::button_to_index(button); return ( m_layout - .at(this->get_layout_column(button_index)) + .at(this->get_layout_column(button)) .at(button_index / 4) ); } - void Ribbon::click_on(std::size_t button_index) { - this->get_panel_under_button(button_index)->click(*this, button_index); - } - - void Ribbon::layout_from_category_map(const std::map>> &categories) { - m_layout.clear(); - for (auto &&[category, panels] : categories) { - if (not panels.empty()) { - std::vector> current_column; - current_column.push_back(std::make_shared(m_resources, category)); - for (auto &&panel : panels) { - if (current_column.size() == 3) { - m_layout.push_back({current_column[0], current_column[1], current_column[2]}); - current_column.clear(); - } else { - current_column.push_back(std::move(panel)); - } - } - if (not current_column.empty()) { - while (current_column.size() < 3) { - current_column.push_back(std::make_shared(m_resources)); - } - m_layout.push_back({current_column[0], current_column[1], current_column[2]}); - } - } - } - fill_layout(); + void Ribbon::click_on(const Data::Button& button) { + get_panel_under_button(button)->click(*this, button); } void Ribbon::move_right() { @@ -202,10 +105,8 @@ namespace MusicSelect { m_move_animation.emplace(old_position, m_position, m_layout.size(), Direction::Left, m_time_factor); } - void Ribbon::move_to_next_category(const std::size_t& from_button_index) { - std::size_t old_position = m_position; - std::size_t from_column = this->get_layout_column(from_button_index); - + void Ribbon::move_to_next_category(const Data::Button& button) { + std::size_t from_column = this->get_layout_column(button); bool found = false; size_t offset = 1; // Cycle through the whole ribbon once starting on the column next to @@ -226,8 +127,10 @@ namespace MusicSelect { } if (found) { // we want the next category panel to land on the same column we clicked + auto old_position = m_position; + auto button_index = Data::button_to_index(button); auto next_category_column = from_column + offset; - auto onscreen_clicked_column = (from_button_index % 4); + auto onscreen_clicked_column = (button_index % 4); m_position = next_category_column - onscreen_clicked_column; m_move_animation.emplace(old_position, m_position, m_layout.size(), Direction::Right, m_time_factor); } @@ -285,50 +188,8 @@ namespace MusicSelect { if (debug) { ImGui::Begin("Ribbon Debug", &debug); { ImGui::SliderFloat("Time Slowdown Factor", &m_time_factor, 1.f, 10.f); - /* - if (ImGui::CollapsingHeader("Panels")) { - auto panel_size = static_cast(m_panel_size); - if(ImGui::InputInt("Size", &panel_size)) { - if (panel_size < 0) { - panel_size = 0; - } - m_panel_size = static_cast(panel_size); - } - auto panel_spacing = static_cast(m_panel_spacing); - if(ImGui::InputInt("Spacing", &panel_spacing)) { - if (panel_spacing < 0) { - panel_spacing = 0; - } - m_panel_spacing = static_cast(panel_spacing); - } - } - */ } ImGui::End(); } } - - // Obligatory steps before the drawing functions can use the layout without crashing - void Ribbon::fill_layout() { - if (m_layout.empty()) { - m_layout.push_back({ - std::make_shared(m_resources, sf::Color::Red, "- EMPTY -"), - std::make_shared(m_resources, sf::Color::Red, "- EMPTY -"), - std::make_shared(m_resources, sf::Color::Red, "- EMPTY -"), - }); - m_layout.push_back({ - std::make_shared(m_resources, sf::Color::Red, "- EMPTY -"), - std::make_shared(m_resources, sf::Color::Red, "- EMPTY -"), - std::make_shared(m_resources, sf::Color::Red, "- EMPTY -"), - }); - return; - } - while (m_layout.size() < 4) { - m_layout.push_back({ - std::make_shared(m_resources), - std::make_shared(m_resources), - std::make_shared(m_resources), - }); - } - } } diff --git a/src/Screens/MusicSelect/Ribbon.hpp b/src/Screens/MusicSelect/Ribbon.hpp index afdda35..bea0236 100644 --- a/src/Screens/MusicSelect/Ribbon.hpp +++ b/src/Screens/MusicSelect/Ribbon.hpp @@ -5,13 +5,15 @@ #include #include +#include "../../Data/Buttons.hpp" #include "../../Data/Preferences.hpp" #include "../../Data/Song.hpp" #include "../../Toolkit/AffineTransform.hpp" #include "../../Toolkit/Debuggable.hpp" #include "../../Toolkit/EasingFunctions.hpp" -#include "Panel.hpp" #include "SharedResources.hpp" +#include "Panel.hpp" +#include "PanelLayout.hpp" namespace MusicSelect { @@ -33,29 +35,23 @@ namespace MusicSelect { Toolkit::AffineTransform create_transform(int previous_pos, int next_pos, size_t ribbon_size, Direction direction); }; - // The Ribbon is the moving part of the Music Select Screen - // It can be sorted in a number of ways - class Ribbon final : public sf::Drawable, public sf::Transformable, public HoldsSharedResources, public Toolkit::Debuggable { + // A Ribbon is a visual representation of a PanelLayout, + // You can scroll it using the left and right buttons + class Ribbon : public sf::Drawable, public sf::Transformable, public HoldsSharedResources, public Toolkit::Debuggable { public: - Ribbon(SharedResources& t_resources); - void title_sort(const Data::SongList& song_list); - void test_sort(); - void test2_sort(); - void test_song_cover_sort(); - const std::shared_ptr& get_panel_under_button(std::size_t button_index) const; - void click_on(std::size_t button_index); + Ribbon(PanelLayout layout, SharedResources& t_resources); + jbcoe::polymorphic_value& get_panel_under_button(const Data::Button& button) const; + void click_on(const Data::Button& button); void move_right(); void move_left(); - void move_to_next_category(const std::size_t& from_button_index); + void move_to_next_category(const Data::Button& button); void draw_debug() override; private: void draw(sf::RenderTarget& target, sf::RenderStates states) const override; void draw_with_animation(sf::RenderTarget& target, sf::RenderStates states) const; void draw_without_animation(sf::RenderTarget& target, sf::RenderStates states) const; - void layout_from_category_map(const std::map>>& categories); - void fill_layout(); - std::size_t get_layout_column(const std::size_t& button_index) const; - std::vector,3>> m_layout; + std::size_t get_layout_column(const Data::Button& button) const; + PanelLayout m_layout; std::size_t m_position = 0; mutable std::optional m_move_animation; float m_time_factor = 1.f; diff --git a/src/Toolkit/ExtraCerealTypes/GHCFilesystemPath.hpp b/src/Toolkit/ExtraCerealTypes/GHCFilesystemPath.hpp new file mode 100644 index 0000000..e655462 --- /dev/null +++ b/src/Toolkit/ExtraCerealTypes/GHCFilesystemPath.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include +#include +#include + + +template +std::string save_minimal(const Archive &, const ghc::filesystem::path & p) { + return p.string(); +} + +template +void load_minimal(const Archive &, ghc::filesystem::path& p, const std::string& value) { + p = ghc::filesystem::path{value}; +} diff --git a/src/Toolkit/GHCFilesystemPathHash.hpp b/src/Toolkit/GHCFilesystemPathHash.hpp new file mode 100644 index 0000000..18a72d6 --- /dev/null +++ b/src/Toolkit/GHCFilesystemPathHash.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include + +namespace fs = ghc::filesystem; + +// Define the way we hash fs::path for use in unordered maps +namespace std { + template <> + struct hash { + std::size_t operator()(const fs::path& p) const { + return std::hash()(p.string()); + } + }; +} \ No newline at end of file