1
0
mirror of synced 2025-02-02 12:27:20 +01:00

Add SongList class

This commit is contained in:
Stepland 2019-10-29 18:42:21 +01:00
parent 7d8aa5e265
commit b2bd5d3e30
18 changed files with 395 additions and 139 deletions

View File

@ -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
View File

@ -0,0 +1,8 @@
#pragma once
namespace Data {
class Chart {
public:
Chart(/* args */);
};
}

73
src/Data/Note.cpp Normal file
View 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
View 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
View File

@ -0,0 +1,8 @@
#pragma once
namespace Data {
class Score {
public:
Score();
};
};

116
src/Data/SongList.cpp Normal file
View 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
View 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);
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -1,3 +0,0 @@
#include "MusicList.hpp"
MusicList::MusicList()

View File

@ -1,12 +0,0 @@
#pragma once
#include <vector>
#include "../Textures/JacketPack.hpp"
class MusicList {
public:
MusicList();
private:
Textures::JacketPack jacket_previews;
};

View File

@ -1,6 +0,0 @@
#pragma once
class Score {
public:
Score();
};

View File

@ -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;
};
};

View File

@ -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;

View 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;
}

View 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);
};

View File

@ -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};
}

View File

@ -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);
}