forked from tsuki/openroller
Fix song selector sorting and difficulty focus
This commit is contained in:
+103
-42
@@ -18,6 +18,7 @@
|
|||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <random>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -360,6 +361,18 @@ bool readFile(const fs::path& path, std::vector<uint8_t>* bytes) {
|
|||||||
return file.good() || file.eof();
|
return file.good() || file.eof();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool loadSongMenuTexture(const std::string& path, DdsTexture& texture,
|
||||||
|
std::string* error = nullptr) {
|
||||||
|
if (!loadDdsTexture(path, texture, error)) return false;
|
||||||
|
// The title and artist glyphs are already antialiased in the atlas.
|
||||||
|
// Nearest magnification keeps their transparent outline texels intact at
|
||||||
|
// the arcade's authored 1:1 size; minified carousel copies remain linear.
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture.id);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
size_t imageTextureIndex(const std::string& symbol) {
|
size_t imageTextureIndex(const std::string& symbol) {
|
||||||
if (symbol.rfind("Image", 0) != 0 || symbol.size() <= 5) return SIZE_MAX;
|
if (symbol.rfind("Image", 0) != 0 || symbol.size() <= 5) return SIZE_MAX;
|
||||||
size_t number = 0;
|
size_t number = 0;
|
||||||
@@ -496,7 +509,8 @@ gc::RvbSnapshotState selectMusicRowState() {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
gc::RvbSnapshotState selectMusicSceneState(const Song* song = nullptr) {
|
gc::RvbSnapshotState selectMusicSceneState(const Song* song = nullptr,
|
||||||
|
int activeSort = 3) {
|
||||||
gc::RvbSnapshotState state;
|
gc::RvbSnapshotState state;
|
||||||
auto& frame = state.frameByPath;
|
auto& frame = state.frameByPath;
|
||||||
frame["/"] = "jf_slmusic_start";
|
frame["/"] = "jf_slmusic_start";
|
||||||
@@ -531,10 +545,10 @@ gc::RvbSnapshotState selectMusicSceneState(const Song* song = nullptr) {
|
|||||||
frame["/imc_focus/imc_tag_new"] = "jf_tag_new_on";
|
frame["/imc_focus/imc_tag_new"] = "jf_tag_new_on";
|
||||||
frame["/imc_focus/imc_tri_btm"] = "lf_tri_off";
|
frame["/imc_focus/imc_tri_btm"] = "lf_tri_off";
|
||||||
frame["/imc_focus/imc_tri_top"] = "lf_tri_off";
|
frame["/imc_focus/imc_tri_top"] = "lf_tri_off";
|
||||||
// CSelectMusicTask stores sort kinds in executable order, not their
|
// CSelectMusicTask stores sort kinds in executable order and maps them
|
||||||
// left-to-right tab order. Internal kind 0 (genre) maps through the
|
// through "34621857". Callers pass the resulting visual tab (1..8).
|
||||||
// "34621857" table to visual tab 3.
|
activeSort = std::clamp(activeSort, 1, 8);
|
||||||
frame["/imc_sort"] = "jf_sort3_ini";
|
frame["/imc_sort"] = "jf_sort" + std::to_string(activeSort) + "_ini";
|
||||||
// Child clips carry each tab's enabled label independently of the active
|
// Child clips carry each tab's enabled label independently of the active
|
||||||
// chevron. The yellow 40x18 NEW marker is a separate root child authored
|
// chevron. The yellow 40x18 NEW marker is a separate root child authored
|
||||||
// into both jf_sort3_ini and jf_sort3, so it is intentionally preserved.
|
// into both jf_sort3_ini and jf_sort3, so it is intentionally preserved.
|
||||||
@@ -564,14 +578,14 @@ gc::RvbSnapshotState selectModeSceneState(const Song* song = nullptr, int diffic
|
|||||||
"/imc_slmode/imc_mode/imc_m_hard", "/imc_slmode/imc_mode/imc_m_extra"
|
"/imc_slmode/imc_mode/imc_m_hard", "/imc_slmode/imc_mode/imc_m_extra"
|
||||||
};
|
};
|
||||||
static constexpr std::array<const char*, 4> selected{
|
static constexpr std::array<const char*, 4> selected{
|
||||||
"jf_m_simple_ini", "jf_m_normal_ini", "jf_m_hard_ini", "jf_m_extra_ini"
|
|
||||||
};
|
|
||||||
static constexpr std::array<const char*, 4> visible{
|
|
||||||
"jf_m_simple_on", "jf_m_normal_on", "jf_m_hard_on", "jf_m_extra_on"
|
"jf_m_simple_on", "jf_m_normal_on", "jf_m_hard_on", "jf_m_extra_on"
|
||||||
};
|
};
|
||||||
static constexpr std::array<const char*, 4> unavailable{
|
static constexpr std::array<const char*, 4> visible{
|
||||||
"jf_m_simple_off", "jf_m_normal_off", "jf_m_hard_off", "jf_m_extra_off"
|
"jf_m_simple_off", "jf_m_normal_off", "jf_m_hard_off", "jf_m_extra_off"
|
||||||
};
|
};
|
||||||
|
static constexpr std::array<const char*, 4> unavailable{
|
||||||
|
"jf_m_simple_xxx", "jf_m_normal_xxx", "jf_m_hard_xxx", "jf_m_extra_xxx"
|
||||||
|
};
|
||||||
for (size_t i = 0; i < paths.size(); ++i) {
|
for (size_t i = 0; i < paths.size(); ++i) {
|
||||||
const bool exists = !song || !song->stages[i].empty();
|
const bool exists = !song || !song->stages[i].empty();
|
||||||
frame[paths[i]] = static_cast<int>(i) == difficulty && exists
|
frame[paths[i]] = static_cast<int>(i) == difficulty && exists
|
||||||
@@ -619,19 +633,6 @@ gc::RvbSnapshotState navigatorSelectSceneState() {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* genreName(int genre) {
|
|
||||||
switch (genre) {
|
|
||||||
case 1: return "ANIME AND POPS";
|
|
||||||
case 2: return "VOCALOID";
|
|
||||||
case 3: return "RHYTHM GAME";
|
|
||||||
case 4: return "GAME";
|
|
||||||
case 5: return "VARIETY";
|
|
||||||
case 6: return "ORIGINAL";
|
|
||||||
case 7: return "TOUHOU";
|
|
||||||
default: return "ALL SONGS";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int genreLabelRow(int genre) {
|
int genreLabelRow(int genre) {
|
||||||
// s_j[_eng].dds is the executable-owned 256x256 genre-label atlas.
|
// s_j[_eng].dds is the executable-owned 256x256 genre-label atlas.
|
||||||
// Each pseudo song ID 50000+n selects one 256x32 row.
|
// Each pseudo song ID 50000+n selects one 256x32 row.
|
||||||
@@ -800,17 +801,61 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
|||||||
}
|
}
|
||||||
std::unordered_map<size_t, DdsTexture> textures;
|
std::unordered_map<size_t, DdsTexture> textures;
|
||||||
|
|
||||||
const std::array<int, 8> genres{-1, 1, 2, 7, 3, 4, 5, 6};
|
// Visual order in selectmusic2: New, Monthly, Genre, Difficulty,
|
||||||
size_t genreSlot = 0;
|
// Score Average, Title, Favorite, At random.
|
||||||
|
constexpr size_t kGenreSort = 2;
|
||||||
|
size_t sortSlot = kGenreSort;
|
||||||
std::vector<size_t> visible;
|
std::vector<size_t> visible;
|
||||||
std::vector<CarouselEntry> carousel;
|
std::vector<CarouselEntry> carousel;
|
||||||
|
std::mt19937 random(std::random_device{}());
|
||||||
auto rebuildVisible = [&] {
|
auto rebuildVisible = [&] {
|
||||||
visible.clear();
|
visible.clear();
|
||||||
carousel.clear();
|
carousel.clear();
|
||||||
const int genre = genres[genreSlot];
|
visible.reserve(songs.size());
|
||||||
for (size_t i = 0; i < songs.size(); ++i) {
|
for (size_t i = 0; i < songs.size(); ++i) visible.push_back(i);
|
||||||
if (genre < 0 || songs[i].catalog.genre == genre) visible.push_back(i);
|
|
||||||
|
const auto titleLess = [&](size_t lhs, size_t rhs) {
|
||||||
|
const auto& a = songs[lhs].catalog;
|
||||||
|
const auto& b = songs[rhs].catalog;
|
||||||
|
const std::string& aKey = a.sortKey.empty() ? a.title : a.sortKey;
|
||||||
|
const std::string& bKey = b.sortKey.empty() ? b.title : b.sortKey;
|
||||||
|
return aKey == bKey ? a.id < b.id : aKey < bKey;
|
||||||
|
};
|
||||||
|
switch (sortSlot) {
|
||||||
|
case 0: // Newest catalog entries first.
|
||||||
|
std::stable_sort(visible.begin(), visible.end(), [&](size_t lhs, size_t rhs) {
|
||||||
|
return songs[lhs].catalog.id > songs[rhs].catalog.id;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case kGenreSort:
|
||||||
|
std::stable_sort(visible.begin(), visible.end(), [&](size_t lhs, size_t rhs) {
|
||||||
|
const int a = genreLabelRow(songs[lhs].catalog.genre);
|
||||||
|
const int b = genreLabelRow(songs[rhs].catalog.genre);
|
||||||
|
return a == b ? titleLess(lhs, rhs) : a < b;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 3: // Highest chart rating first.
|
||||||
|
std::stable_sort(visible.begin(), visible.end(), [&](size_t lhs, size_t rhs) {
|
||||||
|
const auto& a = songs[lhs].catalog.difficultyRatings;
|
||||||
|
const auto& b = songs[rhs].catalog.difficultyRatings;
|
||||||
|
const uint8_t aMax = *std::max_element(a.begin(), a.end());
|
||||||
|
const uint8_t bMax = *std::max_element(b.begin(), b.end());
|
||||||
|
return aMax == bMax ? titleLess(lhs, rhs) : aMax > bMax;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
std::stable_sort(visible.begin(), visible.end(), titleLess);
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
std::shuffle(visible.begin(), visible.end(), random);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Theme, score and favorite data live in the arcade profile
|
||||||
|
// services. Preserve catalog order when no profile is loaded.
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sortSlot == kGenreSort) {
|
||||||
int previousGenre = -1;
|
int previousGenre = -1;
|
||||||
for (const size_t songIndex : visible) {
|
for (const size_t songIndex : visible) {
|
||||||
const int songGenre = songs[songIndex].catalog.genre;
|
const int songGenre = songs[songIndex].catalog.genre;
|
||||||
@@ -820,6 +865,11 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
|||||||
}
|
}
|
||||||
carousel.push_back({false, songIndex, songGenre});
|
carousel.push_back({false, songIndex, songGenre});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
for (const size_t songIndex : visible) {
|
||||||
|
carousel.push_back({false, songIndex, songs[songIndex].catalog.genre});
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
rebuildVisible();
|
rebuildVisible();
|
||||||
|
|
||||||
@@ -828,7 +878,9 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
|||||||
auto refreshOriginalScenes = [&] {
|
auto refreshOriginalScenes = [&] {
|
||||||
if (visible.empty()) return;
|
if (visible.empty()) return;
|
||||||
const Song& song = songs[visible[selection]];
|
const Song& song = songs[visible[selection]];
|
||||||
if (originalMusic.ready()) originalMusic.setState(selectMusicSceneState(&song));
|
if (originalMusic.ready()) {
|
||||||
|
originalMusic.setState(selectMusicSceneState(&song, static_cast<int>(sortSlot + 1)));
|
||||||
|
}
|
||||||
if (originalDifficulty.ready()) {
|
if (originalDifficulty.ready()) {
|
||||||
originalDifficulty.setState(selectModeSceneState(&song, difficulty));
|
originalDifficulty.setState(selectModeSceneState(&song, difficulty));
|
||||||
}
|
}
|
||||||
@@ -843,13 +895,18 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
|||||||
difficulty = firstPlayableDifficulty(songs[visible[selection]], difficulty);
|
difficulty = firstPlayableDifficulty(songs[visible[selection]], difficulty);
|
||||||
refreshOriginalScenes();
|
refreshOriginalScenes();
|
||||||
};
|
};
|
||||||
auto changeGenre = [&](int delta) {
|
auto changeSort = [&](int delta) {
|
||||||
int next = (static_cast<int>(genreSlot) + delta) % static_cast<int>(genres.size());
|
const size_t selectedSong = visible.empty() ? 0 : visible[selection];
|
||||||
if (next < 0) next += static_cast<int>(genres.size());
|
int next = (static_cast<int>(sortSlot) + delta) % 8;
|
||||||
genreSlot = static_cast<size_t>(next);
|
if (next < 0) next += 8;
|
||||||
selection = 0;
|
sortSlot = static_cast<size_t>(next);
|
||||||
rebuildVisible();
|
rebuildVisible();
|
||||||
if (!visible.empty()) difficulty = firstPlayableDifficulty(songs[visible[selection]]);
|
const auto selected = std::find(visible.begin(), visible.end(), selectedSong);
|
||||||
|
selection = selected == visible.end()
|
||||||
|
? 0 : static_cast<size_t>(std::distance(visible.begin(), selected));
|
||||||
|
if (!visible.empty()) {
|
||||||
|
difficulty = firstPlayableDifficulty(songs[visible[selection]], difficulty);
|
||||||
|
}
|
||||||
refreshOriginalScenes();
|
refreshOriginalScenes();
|
||||||
};
|
};
|
||||||
auto changeDifficulty = [&](int delta) {
|
auto changeDifficulty = [&](int delta) {
|
||||||
@@ -890,8 +947,8 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
|||||||
case SDLK_DOWN: case SDLK_S: changeSong(1); break;
|
case SDLK_DOWN: case SDLK_S: changeSong(1); break;
|
||||||
case SDLK_PAGEUP: changeSong(-8); break;
|
case SDLK_PAGEUP: changeSong(-8); break;
|
||||||
case SDLK_PAGEDOWN: changeSong(8); break;
|
case SDLK_PAGEDOWN: changeSong(8); break;
|
||||||
case SDLK_Q: changeGenre(-1); break;
|
case SDLK_Q: changeSort(-1); break;
|
||||||
case SDLK_E: case SDLK_TAB: changeGenre(1); break;
|
case SDLK_E: case SDLK_TAB: changeSort(1); break;
|
||||||
case SDLK_RETURN: case SDLK_SPACE:
|
case SDLK_RETURN: case SDLK_SPACE:
|
||||||
task = SelectTask::Difficulty;
|
task = SelectTask::Difficulty;
|
||||||
break;
|
break;
|
||||||
@@ -1007,7 +1064,7 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
|||||||
auto selectedTexture = textures.find(selectedSongIndex);
|
auto selectedTexture = textures.find(selectedSongIndex);
|
||||||
if (selectedTexture == textures.end()) {
|
if (selectedTexture == textures.end()) {
|
||||||
DdsTexture loaded;
|
DdsTexture loaded;
|
||||||
if (loadDdsTexture(selected.menuTexture.string(), loaded, nullptr)) {
|
if (loadSongMenuTexture(selected.menuTexture.string(), loaded, nullptr)) {
|
||||||
selectedTexture = textures.emplace(selectedSongIndex, loaded).first;
|
selectedTexture = textures.emplace(selectedSongIndex, loaded).first;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1024,10 +1081,14 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
|||||||
ui.text(550, 32, 3, "LOCAL", dark);
|
ui.text(550, 32, 3, "LOCAL", dark);
|
||||||
ui.text(550, 62, 5, std::to_string(selection + 1) + "/" + std::to_string(visible.size()), dark);
|
ui.text(550, 62, 5, std::to_string(selection + 1) + "/" + std::to_string(visible.size()), dark);
|
||||||
|
|
||||||
const int activeGenre = genres[genreSlot];
|
static constexpr std::array<const char*, 8> sortNames{
|
||||||
const glm::vec4 activeColor = genreColor(activeGenre);
|
"NEW", "MONTHLY THEME", "GENRE", "DIFFICULTY",
|
||||||
|
"SCORE AVERAGE", "TITLE", "FAVORITE", "AT RANDOM"
|
||||||
|
};
|
||||||
|
const glm::vec4 activeColor = genreColor(static_cast<int>(sortSlot));
|
||||||
ui.rect(18, 136, 684, 48, activeColor);
|
ui.rect(18, 136, 684, 48, activeColor);
|
||||||
ui.text(38, 149, 3, "Q < " + std::string(genreName(activeGenre)) + " > E", glm::vec4(1.0f));
|
ui.text(38, 149, 3,
|
||||||
|
"Q < " + std::string(sortNames[sortSlot]) + " > E", glm::vec4(1.0f));
|
||||||
|
|
||||||
constexpr int rows = 8;
|
constexpr int rows = 8;
|
||||||
constexpr float rowY = 200.0f;
|
constexpr float rowY = 200.0f;
|
||||||
@@ -1047,7 +1108,7 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
|||||||
auto found = textures.find(songIndex);
|
auto found = textures.find(songIndex);
|
||||||
if (found == textures.end()) {
|
if (found == textures.end()) {
|
||||||
DdsTexture loaded;
|
DdsTexture loaded;
|
||||||
if (loadDdsTexture(songs[songIndex].menuTexture.string(), loaded, nullptr)) {
|
if (loadSongMenuTexture(songs[songIndex].menuTexture.string(), loaded, nullptr)) {
|
||||||
found = textures.emplace(songIndex, loaded).first;
|
found = textures.emplace(songIndex, loaded).first;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1129,7 +1190,7 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
|
|||||||
auto texture = textures.find(songIndex);
|
auto texture = textures.find(songIndex);
|
||||||
if (texture == textures.end()) {
|
if (texture == textures.end()) {
|
||||||
DdsTexture loaded;
|
DdsTexture loaded;
|
||||||
if (loadDdsTexture(songs[songIndex].menuTexture.string(), loaded, nullptr)) {
|
if (loadSongMenuTexture(songs[songIndex].menuTexture.string(), loaded, nullptr)) {
|
||||||
texture = textures.emplace(songIndex, loaded).first;
|
texture = textures.emplace(songIndex, loaded).first;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ What is already wired:
|
|||||||
- the original `common_eng` controller HUD and animated Yume navigator scenes;
|
- the original `common_eng` controller HUD and animated Yume navigator scenes;
|
||||||
- the executable's purple-to-pink background strip, rotating wire sphere,
|
- the executable's purple-to-pink background strip, rotating wire sphere,
|
||||||
translucent menu geometry, and `balloon.dds` lower backing layer;
|
translucent menu geometry, and `balloon.dds` lower backing layer;
|
||||||
- genre filtering from the catalog genre byte;
|
- the eight original sort tabs, with genre headings and catalog-backed title,
|
||||||
|
difficulty, newest and random ordering;
|
||||||
- the twelve dynamically linked `mc_music_link`/`mc_index_link` carousel
|
- the twelve dynamically linked `mc_music_link`/`mc_index_link` carousel
|
||||||
slots with their executable positions and opacity table, including original
|
slots with their executable positions and opacity table, including original
|
||||||
`s_j_eng.dds` genre headings as real scrolling entries;
|
`s_j_eng.dds` genre headings as real scrolling entries;
|
||||||
@@ -48,7 +49,7 @@ Controls:
|
|||||||
```text
|
```text
|
||||||
Up / Down, W / S previous / next song
|
Up / Down, W / S previous / next song
|
||||||
PageUp / PageDown jump by eight songs
|
PageUp / PageDown jump by eight songs
|
||||||
Q / E or Tab genre
|
Q / E or Tab song order
|
||||||
Enter or Space enter difficulty screen
|
Enter or Space enter difficulty screen
|
||||||
Escape close selector
|
Escape close selector
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user