diff --git a/src/Resources/LNMarker.cpp b/src/Resources/LNMarker.cpp new file mode 100644 index 0000000..c7ab552 --- /dev/null +++ b/src/Resources/LNMarker.cpp @@ -0,0 +1,40 @@ +#include "LNMarker.hpp" + +#include + +namespace Resources { + LNMarker::LNMarker(const fs::path& t_folder) : + folder(t_folder) + { + if (not fs::is_directory(folder)) { + throw std::invalid_argument(folder.string()+" is not a folder"); + } + if (not fs::exists(folder/"marker.json")) { + throw std::invalid_argument("LNMarker folder ( "+folder.string()+" ) has no long.json file"); + } + std::ifstream marker_json{folder/"long.json"}; + nlohmann::json j; + marker_json >> j; + j.get_to(*this); + background.load_and_check(folder, size, fps, {16, 30}); + outline.load_and_check(folder, size, fps, {16, 30}); + highlight.load_and_check(folder, size, fps, {16, 30}); + tail.load_and_check(folder, size, fps, {16, 30}); + tip_appearance.load_and_check(folder, size, fps, {16, 30}); + tip_begin_cycle.load_and_check(folder, size, fps, {8, 30}); + tip_cycle.load_and_check(folder, size, fps, {16, 30}); + } + + void from_json(const nlohmann::json& j, LNMarker& m) { + j.at("name").get_to(m.name); + j.at("fps").get_to(m.fps); + j.at("size").get_to(m.size); + j.at("note").at("background").get_to(m.background); + j.at("note").at("outline").get_to(m.outline); + j.at("note").at("highlight").get_to(m.highlight); + j.at("tail").get_to(m.tail); + j.at("tip").at("appearance").get_to(m.tip_appearance); + j.at("tip").at("begin cycle").get_to(m.tip_begin_cycle); + j.at("tip").at("cycle").get_to(m.tip_cycle); + } +} diff --git a/src/Resources/LNMarker.hpp b/src/Resources/LNMarker.hpp new file mode 100644 index 0000000..c7f06a4 --- /dev/null +++ b/src/Resources/LNMarker.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#include "SpriteSheet.hpp" + +namespace fs = ghc::filesystem; + +namespace Resources { + struct LNMarker { + LNMarker(const fs::path& folder); + + fs::path folder; + std::string name; + std::size_t fps; + std::size_t size; + SpriteSheet background; + SpriteSheet outline; + SpriteSheet highlight; + SpriteSheet tail; + SpriteSheet tip_appearance; + SpriteSheet tip_begin_cycle; + SpriteSheet tip_cycle; + }; + + void from_json(const nlohmann::json& j, LNMarker& m); +} \ No newline at end of file diff --git a/src/Resources/Marker.cpp b/src/Resources/Marker.cpp index a790f9c..7c80e9f 100644 --- a/src/Resources/Marker.cpp +++ b/src/Resources/Marker.cpp @@ -11,174 +11,58 @@ namespace fs = ghc::filesystem; namespace Resources { - void to_json(nlohmann::json& j, const MarkerAnimationMetadata& mam) { - j = nlohmann::json{ - {"sprite_sheet", mam.sprite_sheet.string()}, - {"count", mam.count}, - {"columns", mam.columns}, - {"rows", mam.rows} - }; + void from_json(const nlohmann::json& j, Marker& m) { + j.at("name").get_to(m.name); + j.at("fps").get_to(m.fps); + j.at("size").get_to(m.size); + j.at("approach").get_to(m.approach); + j.at("miss").get_to(m.miss); + j.at("poor").get_to(m.poor); + j.at("good").get_to(m.good); + j.at("great").get_to(m.great); + j.at("perfect").get_to(m.perfect); } - - void from_json(const nlohmann::json& j, MarkerAnimationMetadata& mam) { - mam.sprite_sheet = ghc::filesystem::path{j.at("sprite_sheet").get()}; - j.at("count").get_to(mam.count); - j.at("columns").get_to(mam.columns); - j.at("rows").get_to(mam.rows); - } - - void to_json(nlohmann::json& j, const MarkerMetadata& mm) { - j = nlohmann::json{ - {"name", mm.name}, - {"size", mm.size}, - {"fps", mm.fps}, - {"approach", mm.approach}, - {"miss", mm.miss}, - {"poor", mm.poor}, - {"good", mm.good}, - {"great", mm.great}, - {"perfect", mm.perfect} - }; - } - - void from_json(const nlohmann::json& j, MarkerMetadata& mm) { - j.at("name").get_to(mm.name); - j.at("size").get_to(mm.size); - j.at("fps").get_to(mm.fps); - j.at("approach").get_to(mm.approach); - j.at("miss").get_to(mm.miss); - j.at("poor").get_to(mm.poor); - j.at("good").get_to(mm.good); - j.at("great").get_to(mm.great); - j.at("perfect").get_to(mm.perfect); - } - + Marker::Marker(const fs::path& marker_folder) : - m_folder(marker_folder), - m_metadata(), - m_approach(), - m_miss(), - m_poor(), - m_good(), - m_great(), - m_perfect() + folder(marker_folder) { - if (not fs::is_directory(m_folder)) { - throw std::invalid_argument(m_folder.string()+" is not a folder"); + if (not fs::is_directory(folder)) { + throw std::invalid_argument(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"); + if (not fs::exists(folder/"marker.json")) { + throw std::invalid_argument("Marker folder ( "+folder.string()+" ) has no marker.json file"); } - std::ifstream marker_json{m_folder/"marker.json"}; + std::ifstream marker_json{folder/"marker.json"}; nlohmann::json j; marker_json >> j; - j.get_to(m_metadata); - load_and_check(m_approach, m_metadata.approach); - load_and_check(m_miss, m_metadata.miss); - load_and_check(m_poor, m_metadata.poor); - load_and_check(m_good, m_metadata.good); - load_and_check(m_great, m_metadata.great); - load_and_check(m_perfect, m_metadata.perfect); + j.get_to(*this); + approach.load_and_check(folder, size, fps, {16, 30}); + miss.load_and_check(folder, size, fps, {16, 30}); + poor.load_and_check(folder, size, fps, {16, 30}); + good.load_and_check(folder, size, fps, {16, 30}); + great.load_and_check(folder, size, fps, {16, 30}); + perfect.load_and_check(folder, size, fps, {16, 30}); } - 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.setSmooth(true); - - // 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 make sure 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()); - } - } - - const sf::Texture& Marker::get_sprite_sheet_from_enum(const MarkerAnimation& state) const { + const SpriteSheet& Marker::get_sprite_sheet_from_enum(const MarkerAnimation& state) const { switch (state) { case MarkerAnimation::APPROACH: - return m_approach; + return approach; break; case MarkerAnimation::MISS: - return m_miss; + return miss; break; case MarkerAnimation::POOR: - return m_poor; + return poor; break; case MarkerAnimation::GOOD: - return m_good; + return good; break; case MarkerAnimation::GREAT: - return m_great; + return great; break; case MarkerAnimation::PERFECT: - return m_perfect; - break; - default: - throw std::runtime_error("wtf ?"); - } - } - - const MarkerAnimationMetadata& Marker::get_metadata_from_enum(const MarkerAnimation& state) const { - switch (state) { - case MarkerAnimation::APPROACH: - return m_metadata.approach; - break; - case MarkerAnimation::MISS: - return m_metadata.miss; - break; - case MarkerAnimation::POOR: - return m_metadata.poor; - break; - case MarkerAnimation::GOOD: - return m_metadata.good; - break; - case MarkerAnimation::GREAT: - return m_metadata.great; - break; - case MarkerAnimation::PERFECT: - return m_metadata.perfect; + return perfect; break; default: throw std::runtime_error("wtf ?"); @@ -191,7 +75,7 @@ namespace Resources { } std::optional Marker::get_sprite(const MarkerAnimation& state, const float seconds) const { - auto raw_frame = static_cast(std::floor(seconds*m_metadata.fps)); + auto raw_frame = static_cast(std::floor(seconds*fps)); if (raw_frame >= 0) { if (state == MarkerAnimation::APPROACH) { return get_sprite(MarkerAnimation::MISS, static_cast(raw_frame)); @@ -199,29 +83,27 @@ namespace Resources { 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)) + static_cast(raw_frame+static_cast(approach.count)) ); } } std::optional Marker::get_sprite(const MarkerAnimation& state, const std::size_t frame) const { - auto& meta = get_metadata_from_enum(state); - if (frame >= meta.count) { + auto& sprite_sheet = get_sprite_sheet_from_enum(state); + if (frame >= sprite_sheet.count) { return {}; } else { - auto& tex = get_sprite_sheet_from_enum(state); - sf::Sprite sprite{tex}; + sf::Sprite sprite{sprite_sheet.tex}; sf::IntRect rect{ sf::Vector2i{ - static_cast(frame % meta.columns), - static_cast(frame / meta.columns) - } * static_cast(m_metadata.size), + static_cast(frame % sprite_sheet.columns), + static_cast(frame / sprite_sheet.columns) + } * static_cast(size), sf::Vector2i{ - static_cast(m_metadata.size), - static_cast(m_metadata.size) + static_cast(size), + static_cast(size) } }; sprite.setTextureRect(rect); @@ -237,7 +119,7 @@ namespace Resources { if (p.is_directory()) { try { Marker m{p.path()}; - res.emplace(m.m_metadata.name, m); + res.emplace(m.name, m); } catch (const std::exception& e) { std::cerr << "Unable to load marker folder " << p.path().filename().string() << " : " diff --git a/src/Resources/Marker.hpp b/src/Resources/Marker.hpp index e63fffa..64f7e0b 100644 --- a/src/Resources/Marker.hpp +++ b/src/Resources/Marker.hpp @@ -7,6 +7,10 @@ #include #include +#include "SpriteSheet.hpp" + +namespace fs = ghc::filesystem; + namespace Resources { enum class MarkerAnimation { APPROACH, @@ -17,51 +21,27 @@ namespace Resources { 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 - }; - - void to_json(nlohmann::json& j, const MarkerAnimationMetadata& mam); - void from_json(const nlohmann::json& j, MarkerAnimationMetadata& mam); - - // 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 poor; - MarkerAnimationMetadata good; - MarkerAnimationMetadata great; - MarkerAnimationMetadata perfect; - }; - - void to_json(nlohmann::json& j, const MarkerMetadata& mm); - void from_json(const nlohmann::json& j, MarkerMetadata& mm); - struct Marker { - explicit Marker(const ghc::filesystem::path& marker_folder); + explicit Marker(const fs::path& marker_folder); std::optional get_sprite(const MarkerAnimation& state, const sf::Time seconds) const; std::optional get_sprite(const MarkerAnimation& state, const float seconds) const; std::optional get_sprite(const MarkerAnimation& state, const std::size_t frame) const; - void load_and_check(sf::Texture& spritesheet, const MarkerAnimationMetadata& metadata); - const sf::Texture& get_sprite_sheet_from_enum(const MarkerAnimation& state) const; - const MarkerAnimationMetadata& get_metadata_from_enum(const MarkerAnimation& state) const; + const SpriteSheet& get_sprite_sheet_from_enum(const MarkerAnimation& state) const; - ghc::filesystem::path m_folder; - MarkerMetadata m_metadata; - sf::Texture m_approach; - sf::Texture m_miss; - sf::Texture m_poor; - sf::Texture m_good; - sf::Texture m_great; - sf::Texture m_perfect; + fs::path folder; + std::string name; + std::size_t size; // the side length in pixels + std::size_t fps; // classic jubeat markers are 30 fps + SpriteSheet approach; + SpriteSheet miss; + SpriteSheet poor; + SpriteSheet good; + SpriteSheet great; + SpriteSheet perfect; }; + void from_json(const nlohmann::json& j, Marker& m); + using Markers = std::map; Markers load_markers(const ghc::filesystem::path& jujube_path); } diff --git a/src/Resources/SpriteSheet.cpp b/src/Resources/SpriteSheet.cpp new file mode 100644 index 0000000..3c3f789 --- /dev/null +++ b/src/Resources/SpriteSheet.cpp @@ -0,0 +1,67 @@ +#include "SpriteSheet.hpp" + +#include + +namespace Resources { + void from_json(const nlohmann::json& j, SpriteSheet& s) { + s.tex_path = fs::path{j.at("sprite_sheet").get()}; + j.at("count").get_to(s.count); + j.at("columns").get_to(s.columns); + j.at("rows").get_to(s.rows); + } + + void SpriteSheet::load_and_check(const fs::path& folder, std::size_t size, std::size_t fps, const DurationInFrames& max_duration) { + // File Load & Check + if (not tex.loadFromFile(folder/tex_path)) { + throw std::runtime_error( + "Cannot open marker sprite sheet " + +(folder/tex_path).string() + ); + } + tex.setSmooth(true); + + // Sprite sheet size check + // throw if the texture size does not match what's announced by the metadata + auto sheet_size = tex.getSize(); + auto expected_size = sf::Vector2u(columns, rows) * static_cast(size); + if (sheet_size != expected_size) { + std::stringstream ss; + ss << "Marker sprite sheet "; + ss << (folder/tex_path).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 (count > columns * rows) { + std::stringstream ss; + ss << "Metadata for marker sprite sheet "; + ss << (folder/tex_path).string(); + ss << " indicates that it holds " << count << " sprites"; + ss << " when it can only hold a maximum of " << columns * 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 make sure that : + // frames/fps <= max_frames/reference_fps + // Which is mathematically equivalent to checking that : + // count*reference_fps <= max_frames*fps + // Which allows us to avoid having to cast to float + if (count*max_duration.fps > max_duration.frames*fps) { + std::stringstream ss; + ss << "Marker animation for sprite sheet "; + ss << (folder/tex_path).string(); + ss << " lasts " << count/static_cast(fps)*1000.f << "ms"; + ss << " (" << count << "f @ " << fps << "fps)"; + ss << " which is more than the maximum of "; + ss << max_duration.frames/static_cast(max_duration.fps)*1000.f << "ms"; + ss << " (16f @ 30fps)"; + throw std::invalid_argument(ss.str()); + } + } +} diff --git a/src/Resources/SpriteSheet.hpp b/src/Resources/SpriteSheet.hpp new file mode 100644 index 0000000..8c7e367 --- /dev/null +++ b/src/Resources/SpriteSheet.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include + +namespace fs = ghc::filesystem; + +namespace Resources { + struct SpriteSheet { + sf::Texture tex; + fs::path tex_path; + std::size_t count; + std::size_t columns; + std::size_t rows; + + void load_and_check( + const fs::path& folder, + std::size_t size, + std::size_t fps, + const DurationInFrames& max_duration + ); + }; + + struct DurationInFrames { + std::size_t frames; + std::size_t fps; + }; + + void from_json(const nlohmann::json& j, SpriteSheet& s); +} diff --git a/utils/spritesheet_maker.py b/utils/spritesheet_maker.py new file mode 100644 index 0000000..fe2a87e --- /dev/null +++ b/utils/spritesheet_maker.py @@ -0,0 +1,27 @@ +from math import sqrt, ceil +from PIL import Image +from path import Path +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument("files", type=Path, nargs="+") +args = parser.parse_args() + +images = [Image.open(file) for file in args.files] +assert len(set(image.size for image in images)) == 1 +sprite_size = images[0].size +sheet_square_side = ceil(sqrt(len(images))) +sheet = Image.new( + 'RGBA', + ( + sheet_square_side*sprite_size[0], + sheet_square_side*sprite_size[1] + ), + (0,0,0,0) +) +for index, sprite in enumerate(images): + x = (index % sheet_square_side)*sprite_size[0] + y = (index // sheet_square_side)*sprite_size[1] + sheet.paste(sprite, (x,y)) +sheet.save("sheet.png") +