Add SongList class
This commit is contained in:
parent
7d8aa5e265
commit
b2bd5d3e30
15
meson.build
15
meson.build
@ -15,19 +15,20 @@ filesystem = cpp.find_library('stdc++fs')
|
||||
|
||||
sources = [
|
||||
'src/Main.cpp',
|
||||
'src/Model/Chart.hpp',
|
||||
'src/Model/MusicList.hpp',
|
||||
'src/Model/MusicList.cpp',
|
||||
'src/Model/Score.hpp',
|
||||
'src/Data/Note.hpp',
|
||||
'src/Data/Note.cpp',
|
||||
'srx/Data/Score.hpp',
|
||||
'src/Data/SongList.hpp',
|
||||
'src/Data/SongList.cpp',
|
||||
'src/Screens/Gameplay.hpp',
|
||||
'src/Screens/MusicSelect.hpp',
|
||||
'src/Screens/Result.hpp',
|
||||
'src/Textures/TexturePack.hpp',
|
||||
'src/Textures/TexturePack.cpp',
|
||||
]
|
||||
|
||||
executable(
|
||||
'jujube',
|
||||
sources,
|
||||
'src/Test.cpp',
|
||||
'src/Textures/CoverPack.hpp',
|
||||
'src/Textures/CoverPack.cpp',
|
||||
dependencies: [sfml, filesystem]
|
||||
)
|
8
src/Data/Chart.hpp
Normal file
8
src/Data/Chart.hpp
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
namespace Data {
|
||||
class Chart {
|
||||
public:
|
||||
Chart(/* args */);
|
||||
};
|
||||
}
|
73
src/Data/Note.cpp
Normal file
73
src/Data/Note.cpp
Normal file
@ -0,0 +1,73 @@
|
||||
#include "Note.hpp"
|
||||
|
||||
|
||||
Data::Note::Note(
|
||||
unsigned int t_position,
|
||||
sf::Time t_timing,
|
||||
sf::Time t_length,
|
||||
unsigned int t_tail_position
|
||||
) :
|
||||
position(t_position),
|
||||
timing(t_timing),
|
||||
length(t_length),
|
||||
tail_position(t_tail_position)
|
||||
{
|
||||
if (t_position > 15) {
|
||||
throw std::out_of_range("Tried creating a note with invalid position : "+std::to_string(t_position));
|
||||
}
|
||||
|
||||
if (t_length.asMicroseconds < 0) {
|
||||
throw std::out_of_range("Tried creating a long note with negative length : "+std::to_string(t_length.asSeconds)+ "s");
|
||||
|
||||
}
|
||||
if (t_length.asMicroseconds > 0) {
|
||||
if (t_tail_position > 5) {
|
||||
throw std::out_of_range("Tried creating a long note with invalid tail position : "+std::to_string(t_tail_position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Data::Note::operator==(const Data::Note &rhs) const {
|
||||
return timing == rhs.timing && position == rhs.position;
|
||||
}
|
||||
|
||||
bool Data::Note::operator!=(const Data::Note &rhs) const {
|
||||
return !(rhs == *this);
|
||||
}
|
||||
|
||||
bool Data::Note::operator<(const Data::Note &rhs) const {
|
||||
if (timing < rhs.timing) {
|
||||
return true;
|
||||
}
|
||||
if (rhs.timing < timing) {
|
||||
return false;
|
||||
}
|
||||
return position < rhs.position;
|
||||
}
|
||||
|
||||
bool Data::Note::operator>(const Data::Note &rhs) const {
|
||||
return rhs < *this;
|
||||
}
|
||||
|
||||
bool Data::Note::operator<=(const Data::Note &rhs) const {
|
||||
return !(rhs < *this);
|
||||
}
|
||||
|
||||
bool Data::Note::operator>=(const Data::Note &rhs) const {
|
||||
return !(*this < rhs);
|
||||
}
|
||||
const unsigned int Data::Note::getPosition() const {
|
||||
return position;
|
||||
}
|
||||
|
||||
const sf::Time& Data::Note::getTiming() const {
|
||||
return timing;
|
||||
}
|
||||
|
||||
const sf::Time& Data::Note::getLength() const {
|
||||
return length;
|
||||
}
|
||||
|
||||
const unsigned int Data::Note::getTailPosition() const {
|
||||
return tail_position;
|
||||
}
|
30
src/Data/Note.hpp
Normal file
30
src/Data/Note.hpp
Normal file
@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <SFML/System.hpp>
|
||||
|
||||
namespace Data {
|
||||
class Note {
|
||||
public:
|
||||
Note(unsigned int t_position, sf::Time t_timing, sf::Time t_length, unsigned int t_tail_position);
|
||||
|
||||
bool operator==(const Note &rhs) const;
|
||||
bool operator!=(const Note &rhs) const;
|
||||
bool operator<(const Note &rhs) const;
|
||||
bool operator>(const Note &rhs) const;
|
||||
bool operator<=(const Note &rhs) const;
|
||||
bool operator>=(const Note &rhs) const;
|
||||
|
||||
const unsigned int getPosition() const;
|
||||
const sf::Time& getTiming() const;
|
||||
const sf::Time& getLength() const;
|
||||
const unsigned int getTailPosition() const;
|
||||
|
||||
private:
|
||||
|
||||
const unsigned int position;
|
||||
const sf::Time timing;
|
||||
const sf::Time length = sf::Time{};
|
||||
const unsigned int tail_position = 0;
|
||||
|
||||
};
|
||||
};
|
8
src/Data/Score.hpp
Normal file
8
src/Data/Score.hpp
Normal file
@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
namespace Data {
|
||||
class Score {
|
||||
public:
|
||||
Score();
|
||||
};
|
||||
};
|
116
src/Data/SongList.cpp
Normal file
116
src/Data/SongList.cpp
Normal file
@ -0,0 +1,116 @@
|
||||
#include "SongList.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <list>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
Data::SongList::SongList::SongList() {
|
||||
|
||||
// Loading all song metadata
|
||||
for (const auto& folder : getSongFolders()) {
|
||||
try {
|
||||
songs.emplace_back(folder);
|
||||
} catch (const std::invalid_argument& e) {
|
||||
std::cerr << "Exception while parsing song folder "
|
||||
<< folder.string() << " : " << e.what() << '\n';
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Loading all cover previews
|
||||
for (const auto& song : songs) {
|
||||
if (song.cover) {
|
||||
try {
|
||||
cover_previews.emplace_back(*song.cover);
|
||||
} catch (const std::invalid_argument& e) {
|
||||
std::cerr << "Exception while loading song cover "
|
||||
<< song.cover->string() << " : " << e.what() << '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
const std::vector<fs::path> Data::getSongFolders() {
|
||||
|
||||
std::vector<fs::path> song_folders;
|
||||
|
||||
for (const auto& dir_item : fs::directory_iterator("songs/")) {
|
||||
if (dir_item.is_directory()) {
|
||||
for (auto& path : recursiveSongSearch(dir_item.path())) {
|
||||
song_folders.push_back(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return song_folders;
|
||||
}
|
||||
|
||||
std::list<fs::path> Data::recursiveSongSearch(fs::path song_or_pack) {
|
||||
std::list<fs::path> res;
|
||||
fs::directory_iterator folder{song_or_pack};
|
||||
if (
|
||||
std::any_of(
|
||||
fs::begin(folder),
|
||||
fs::end(folder),
|
||||
[](const fs::directory_entry& de) {
|
||||
de.path().extension() == ".memo" or
|
||||
de.path().extension() == ".memon";
|
||||
}
|
||||
)
|
||||
) {
|
||||
res.push_back(song_or_pack);
|
||||
return res;
|
||||
} else {
|
||||
for (auto& p : fs::directory_iterator(song_or_pack)) {
|
||||
if (p.is_directory()) {
|
||||
res.splice(res.end(), recursiveSongSearch(p));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
Data::Song::Song(fs::path song_folder) {
|
||||
|
||||
// .memon ?
|
||||
auto memon_files = getMemonFiles(song_folder);
|
||||
if (not memon_files.empty()) {
|
||||
if (memon_files.size() > 1) {
|
||||
throw std::invalid_argument("Multiple .memon files");
|
||||
} else {
|
||||
|
||||
}
|
||||
} else {
|
||||
// .memo ?
|
||||
auto memo_files = getMemoFiles(song_folder);
|
||||
if (not memo_files.empty()) {
|
||||
|
||||
} else {
|
||||
throw std::invalid_argument("No valid file found in song folder");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<fs::path>& Data::getMemoFiles(fs::path song_folder) {
|
||||
std::vector<fs::path> res;
|
||||
for (const auto& p : fs::directory_iterator(song_folder)) {
|
||||
if (p.path().extension() == ".memo") {
|
||||
res.push_back(p.path());
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
const std::vector<fs::path>& Data::getMemonFiles(fs::path song_folder) {
|
||||
std::vector<fs::path> res;
|
||||
for (const auto& p : fs::directory_iterator(song_folder)) {
|
||||
if (p.path().extension() == ".memon") {
|
||||
res.push_back(p.path());
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
47
src/Data/SongList.hpp
Normal file
47
src/Data/SongList.hpp
Normal file
@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "../Textures/CoverPack.hpp"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace Data {
|
||||
|
||||
// Basic metadata about a song
|
||||
struct Song {
|
||||
Song() = default;
|
||||
explicit Song(fs::path song_folder);
|
||||
std::string title;
|
||||
std::string artist;
|
||||
// Path to the album cover
|
||||
std::optional<fs::path> cover;
|
||||
// Path the the audio file
|
||||
std::optional<fs::path> audio;
|
||||
// Mapping from chart difficulty (BSC, ADV, EXT ...) to the numeric level,
|
||||
// the level is stored multiplied by 10 and displayed divided by 10
|
||||
// to allow for decimal levels (introduced in jubeat ... festo ?)
|
||||
std::unordered_map<std::string, unsigned int> chart_levels;
|
||||
};
|
||||
|
||||
// Class holding all the necessary data to run the Music Select screen
|
||||
class SongList {
|
||||
public:
|
||||
SongList();
|
||||
private:
|
||||
Textures::CoverPack cover_previews;
|
||||
std::vector<const Song> songs;
|
||||
};
|
||||
|
||||
// Returns the folders conscidered to contain a valid song
|
||||
// classic memo files should have the .memo extension
|
||||
// .memon files also accepted
|
||||
const std::vector<fs::path> getSongFolders();
|
||||
std::list<fs::path> recursiveSongSearch(fs::path song_or_pack);
|
||||
const std::vector<fs::path>& getMemoFiles(fs::path song_folder);
|
||||
const std::vector<fs::path>& getMemonFiles(fs::path song_folder);
|
||||
}
|
@ -22,8 +22,8 @@ int main(int argc, char const *argv[]) {
|
||||
Screen::Gameplay gameplay(selected_chart);
|
||||
Score score = gameplay.play_chart(window);
|
||||
|
||||
Screen::Result result(score);
|
||||
result.display(window);
|
||||
Screen::Result result_screen(score);
|
||||
result_screen.display(window);
|
||||
|
||||
}
|
||||
|
||||
|
@ -1,20 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
class Chart {
|
||||
public:
|
||||
Chart();
|
||||
};
|
||||
|
||||
class ChartMetadata {
|
||||
public:
|
||||
ChartMetadata(
|
||||
std::string title,
|
||||
std::string artist
|
||||
);
|
||||
private:
|
||||
std::string title;
|
||||
std::string artist;
|
||||
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
#include "MusicList.hpp"
|
||||
|
||||
MusicList::MusicList()
|
@ -1,12 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "../Textures/JacketPack.hpp"
|
||||
|
||||
class MusicList {
|
||||
public:
|
||||
MusicList();
|
||||
private:
|
||||
Textures::JacketPack jacket_previews;
|
||||
};
|
@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
class Score {
|
||||
public:
|
||||
Score();
|
||||
};
|
@ -1,14 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <SFML/Window.hpp>
|
||||
#include "../Model/Chart.hpp"
|
||||
#include "../Model/Score.hpp"
|
||||
#include "../Data/Chart.hpp"
|
||||
#include "../Data/Score.hpp"
|
||||
|
||||
namespace Screen {
|
||||
class Gameplay {
|
||||
const Chart& chart;
|
||||
const Data::Chart& chart;
|
||||
public:
|
||||
explicit Gameplay(const Chart& selected_chart);
|
||||
explicit Gameplay(const Data::Chart& selected_chart);
|
||||
Score play_chart(sf::Window& window) const;
|
||||
};
|
||||
};
|
||||
|
@ -1,16 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <SFML/Window.hpp>
|
||||
#include "../Model/MusicList.hpp"
|
||||
#include "../Model/Chart.hpp"
|
||||
#include "../Data/SongList.hpp"
|
||||
#include "../Data/Chart.hpp"
|
||||
|
||||
namespace Screen {
|
||||
/*
|
||||
The music select screen is created only once and loads a cache of available songs
|
||||
in the music_list attribute
|
||||
*/
|
||||
|
||||
// The music select screen is created only once and loads a cache of available songs
|
||||
// in the music_list attribute
|
||||
class MusicSelect {
|
||||
MusicList music_list;
|
||||
Data::SongList music_list;
|
||||
public:
|
||||
MusicSelect() = default;
|
||||
Chart& select_chart(sf::Window& window) const;
|
||||
|
48
src/Textures/CoverPack.cpp
Normal file
48
src/Textures/CoverPack.cpp
Normal file
@ -0,0 +1,48 @@
|
||||
#include "CoverPack.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <sstream>
|
||||
|
||||
sf::Sprite Textures::CoverPack::at(const std::filesystem::path& path) const{
|
||||
auto location = get_detailed_location(locations.at(path));
|
||||
sf::IntRect rect(location.column, location.row, 128, 128);
|
||||
sf::Sprite cover;
|
||||
cover.setTexture(textures.at(location.texture_index)->getTexture());
|
||||
cover.setTextureRect(rect);
|
||||
return cover;
|
||||
}
|
||||
|
||||
void Textures::CoverPack::emplace_back(const std::filesystem::path& cover) {
|
||||
|
||||
sf::Texture cover_texture;
|
||||
if (!cover_texture.loadFromFile(cover)) {
|
||||
std::stringstream ss;
|
||||
ss << "Unable to load cover image : " << cover;
|
||||
throw std::invalid_argument(ss.str());
|
||||
} else {
|
||||
locations[cover] = next_location;
|
||||
}
|
||||
auto size = cover_texture.getSize();
|
||||
auto location = get_detailed_location(next_location);
|
||||
sf::Sprite new_cover;
|
||||
new_cover.setTexture(cover_texture);
|
||||
new_cover.setScale(128.0f/size.x, 128.0f/size.y);
|
||||
new_cover.setPosition(128.0f * location.column, 128.0f * location.row);
|
||||
if (location.column == 0 and location.row == 0) {
|
||||
// first cover means it's a new texture
|
||||
textures.push_back(std::make_shared<sf::RenderTexture>());
|
||||
textures.back()->create(1024, 1024);
|
||||
}
|
||||
textures.at(location.texture_index)->draw(new_cover);
|
||||
textures.at(location.texture_index)->display();
|
||||
next_location++;
|
||||
}
|
||||
|
||||
Textures::DetailedLocation Textures::get_detailed_location(unsigned int location) {
|
||||
return {location / 64, (location % 64) / 8, location % 8};
|
||||
}
|
||||
|
||||
const std::vector<std::shared_ptr<sf::RenderTexture>>& Textures::CoverPack::getTextures() const {
|
||||
return textures;
|
||||
}
|
45
src/Textures/CoverPack.hpp
Normal file
45
src/Textures/CoverPack.hpp
Normal file
@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <SFML/Graphics.hpp>
|
||||
|
||||
// Define the way we hash std::filesystem::path for use in unordered maps
|
||||
namespace std {
|
||||
template <>
|
||||
struct hash<std::filesystem::path> {
|
||||
std::size_t operator()(const std::filesystem::path& p) const {
|
||||
return std::hash<std::string>()(p.string());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
namespace Textures {
|
||||
|
||||
/*
|
||||
A CoverPack stores 128x128 cover previews in a vector of 1024x1024 textures
|
||||
*/
|
||||
class CoverPack {
|
||||
public:
|
||||
CoverPack() = default;
|
||||
sf::Sprite at(const std::filesystem::path& path) const;
|
||||
void emplace_back(const std::filesystem::path& cover);
|
||||
const std::vector<std::shared_ptr<sf::RenderTexture>>& getTextures() const;
|
||||
private:
|
||||
std::unordered_map<std::filesystem::path, unsigned int> locations;
|
||||
std::vector<std::shared_ptr<sf::RenderTexture>> textures;
|
||||
unsigned int next_location = 0;
|
||||
};
|
||||
|
||||
struct DetailedLocation {
|
||||
unsigned int texture_index;
|
||||
unsigned int row;
|
||||
unsigned int column;
|
||||
};
|
||||
|
||||
DetailedLocation get_detailed_location(unsigned int location);
|
||||
};
|
@ -1,44 +0,0 @@
|
||||
#include "JacketPack.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <sstream>
|
||||
|
||||
sf::Sprite Textures::JacketPack::operator[](const std::string& path) const{
|
||||
auto location = get_detailed_location(locations.at(path));
|
||||
sf::IntRect rect(location.column, location.row, 128, 128);
|
||||
sf::Sprite jacket;
|
||||
jacket.setTexture(textures.at(location.texture_index)->getTexture());
|
||||
jacket.setTextureRect(rect);
|
||||
return jacket;
|
||||
}
|
||||
|
||||
void Textures::JacketPack::push_back(const std::filesystem::path& jacket) {
|
||||
|
||||
sf::Texture jacket_texture;
|
||||
if (!jacket_texture.loadFromFile(jacket)) {
|
||||
std::stringstream ss;
|
||||
ss << "Unable to load jacket image : " << jacket;
|
||||
throw std::runtime_error(ss.str());
|
||||
} else {
|
||||
locations[jacket] = next_location;
|
||||
}
|
||||
auto size = jacket_texture.getSize();
|
||||
auto location = get_detailed_location(next_location);
|
||||
sf::Sprite new_jacket;
|
||||
new_jacket.setTexture(jacket_texture);
|
||||
new_jacket.setScale(128.0f/size.x, 128.0f/size.y);
|
||||
new_jacket.setPosition(128.0f * location.column, 128.0f * location.row);
|
||||
if (location.column == 0 and location.row == 0) {
|
||||
// first jacket means it's a new texture
|
||||
textures.push_back(std::make_shared<sf::RenderTexture>());
|
||||
textures.back()->create(1024, 1024);
|
||||
}
|
||||
textures.at(location.texture_index)->draw(new_jacket);
|
||||
textures.at(location.texture_index)->display();
|
||||
next_location++;
|
||||
}
|
||||
|
||||
Textures::DetailedLocation Textures::get_detailed_location(unsigned int location) {
|
||||
return {location / 64, (location % 64) / 8, location % 8};
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <SFML/Graphics.hpp>
|
||||
|
||||
namespace Textures {
|
||||
|
||||
/*
|
||||
A JacketPack stores 128x128 jacket previews in 1024x1024 textures
|
||||
*/
|
||||
class JacketPack {
|
||||
public:
|
||||
JacketPack() = default;
|
||||
sf::Sprite operator[](const std::string& path) const;
|
||||
void push_back(const std::filesystem::path& jacket);
|
||||
//private:
|
||||
std::unordered_map<std::filesystem::path, unsigned int> locations;
|
||||
std::vector<std::shared_ptr<sf::RenderTexture>> textures;
|
||||
unsigned int next_location = 0;
|
||||
};
|
||||
|
||||
struct DetailedLocation {
|
||||
unsigned int texture_index;
|
||||
unsigned int row;
|
||||
unsigned int column;
|
||||
};
|
||||
|
||||
DetailedLocation get_detailed_location(unsigned int location);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user