Fix song selector sorting and difficulty focus

This commit is contained in:
2026-08-15 14:08:07 +02:00
parent 623ef9a733
commit 3090555ab3
2 changed files with 113 additions and 51 deletions
+110 -49
View File
@@ -18,6 +18,7 @@
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <random>
#include <unordered_map>
#include <unordered_set>
#include <vector>
@@ -360,6 +361,18 @@ bool readFile(const fs::path& path, std::vector<uint8_t>* bytes) {
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) {
if (symbol.rfind("Image", 0) != 0 || symbol.size() <= 5) return SIZE_MAX;
size_t number = 0;
@@ -496,7 +509,8 @@ gc::RvbSnapshotState selectMusicRowState() {
return state;
}
gc::RvbSnapshotState selectMusicSceneState(const Song* song = nullptr) {
gc::RvbSnapshotState selectMusicSceneState(const Song* song = nullptr,
int activeSort = 3) {
gc::RvbSnapshotState state;
auto& frame = state.frameByPath;
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_tri_btm"] = "lf_tri_off";
frame["/imc_focus/imc_tri_top"] = "lf_tri_off";
// CSelectMusicTask stores sort kinds in executable order, not their
// left-to-right tab order. Internal kind 0 (genre) maps through the
// "34621857" table to visual tab 3.
frame["/imc_sort"] = "jf_sort3_ini";
// CSelectMusicTask stores sort kinds in executable order and maps them
// through "34621857". Callers pass the resulting visual tab (1..8).
activeSort = std::clamp(activeSort, 1, 8);
frame["/imc_sort"] = "jf_sort" + std::to_string(activeSort) + "_ini";
// Child clips carry each tab's enabled label independently of the active
// 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.
@@ -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"
};
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"
};
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"
};
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) {
const bool exists = !song || !song->stages[i].empty();
frame[paths[i]] = static_cast<int>(i) == difficulty && exists
@@ -619,19 +633,6 @@ gc::RvbSnapshotState navigatorSelectSceneState() {
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) {
// s_j[_eng].dds is the executable-owned 256x256 genre-label atlas.
// Each pseudo song ID 50000+n selects one 256x32 row.
@@ -800,25 +801,74 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
}
std::unordered_map<size_t, DdsTexture> textures;
const std::array<int, 8> genres{-1, 1, 2, 7, 3, 4, 5, 6};
size_t genreSlot = 0;
// Visual order in selectmusic2: New, Monthly, Genre, Difficulty,
// Score Average, Title, Favorite, At random.
constexpr size_t kGenreSort = 2;
size_t sortSlot = kGenreSort;
std::vector<size_t> visible;
std::vector<CarouselEntry> carousel;
std::mt19937 random(std::random_device{}());
auto rebuildVisible = [&] {
visible.clear();
carousel.clear();
const int genre = genres[genreSlot];
for (size_t i = 0; i < songs.size(); ++i) {
if (genre < 0 || songs[i].catalog.genre == genre) visible.push_back(i);
visible.reserve(songs.size());
for (size_t i = 0; i < songs.size(); ++i) 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;
}
int previousGenre = -1;
for (const size_t songIndex : visible) {
const int songGenre = songs[songIndex].catalog.genre;
if (songGenre != previousGenre) {
carousel.push_back({true, 0, songGenre});
previousGenre = songGenre;
if (sortSlot == kGenreSort) {
int previousGenre = -1;
for (const size_t songIndex : visible) {
const int songGenre = songs[songIndex].catalog.genre;
if (songGenre != previousGenre) {
carousel.push_back({true, 0, songGenre});
previousGenre = songGenre;
}
carousel.push_back({false, songIndex, songGenre});
}
} else {
for (const size_t songIndex : visible) {
carousel.push_back({false, songIndex, songs[songIndex].catalog.genre});
}
carousel.push_back({false, songIndex, songGenre});
}
};
rebuildVisible();
@@ -828,7 +878,9 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
auto refreshOriginalScenes = [&] {
if (visible.empty()) return;
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()) {
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);
refreshOriginalScenes();
};
auto changeGenre = [&](int delta) {
int next = (static_cast<int>(genreSlot) + delta) % static_cast<int>(genres.size());
if (next < 0) next += static_cast<int>(genres.size());
genreSlot = static_cast<size_t>(next);
selection = 0;
auto changeSort = [&](int delta) {
const size_t selectedSong = visible.empty() ? 0 : visible[selection];
int next = (static_cast<int>(sortSlot) + delta) % 8;
if (next < 0) next += 8;
sortSlot = static_cast<size_t>(next);
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();
};
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_PAGEUP: changeSong(-8); break;
case SDLK_PAGEDOWN: changeSong(8); break;
case SDLK_Q: changeGenre(-1); break;
case SDLK_E: case SDLK_TAB: changeGenre(1); break;
case SDLK_Q: changeSort(-1); break;
case SDLK_E: case SDLK_TAB: changeSort(1); break;
case SDLK_RETURN: case SDLK_SPACE:
task = SelectTask::Difficulty;
break;
@@ -1007,7 +1064,7 @@ bool runSongSelect(SDL_Window* window, const fs::path& gcRoot, std::string* sele
auto selectedTexture = textures.find(selectedSongIndex);
if (selectedTexture == textures.end()) {
DdsTexture loaded;
if (loadDdsTexture(selected.menuTexture.string(), loaded, nullptr)) {
if (loadSongMenuTexture(selected.menuTexture.string(), loaded, nullptr)) {
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, 62, 5, std::to_string(selection + 1) + "/" + std::to_string(visible.size()), dark);
const int activeGenre = genres[genreSlot];
const glm::vec4 activeColor = genreColor(activeGenre);
static constexpr std::array<const char*, 8> sortNames{
"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.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 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);
if (found == textures.end()) {
DdsTexture loaded;
if (loadDdsTexture(songs[songIndex].menuTexture.string(), loaded, nullptr)) {
if (loadSongMenuTexture(songs[songIndex].menuTexture.string(), loaded, nullptr)) {
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);
if (texture == textures.end()) {
DdsTexture loaded;
if (loadDdsTexture(songs[songIndex].menuTexture.string(), loaded, nullptr)) {
if (loadSongMenuTexture(songs[songIndex].menuTexture.string(), loaded, nullptr)) {
texture = textures.emplace(songIndex, loaded).first;
}
}
+3 -2
View File
@@ -26,7 +26,8 @@ What is already wired:
- the original `common_eng` controller HUD and animated Yume navigator scenes;
- the executable's purple-to-pink background strip, rotating wire sphere,
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
slots with their executable positions and opacity table, including original
`s_j_eng.dds` genre headings as real scrolling entries;
@@ -48,7 +49,7 @@ Controls:
```text
Up / Down, W / S previous / next song
PageUp / PageDown jump by eight songs
Q / E or Tab genre
Q / E or Tab song order
Enter or Space enter difficulty screen
Escape close selector