diff --git a/apps/desktop/src/SongSelect.cpp b/apps/desktop/src/SongSelect.cpp index 00d5b33..84536e1 100644 --- a/apps/desktop/src/SongSelect.cpp +++ b/apps/desktop/src/SongSelect.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -360,6 +361,18 @@ bool readFile(const fs::path& path, std::vector* 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 selected{ - "jf_m_simple_ini", "jf_m_normal_ini", "jf_m_hard_ini", "jf_m_extra_ini" - }; - static constexpr std::array visible{ "jf_m_simple_on", "jf_m_normal_on", "jf_m_hard_on", "jf_m_extra_on" }; - static constexpr std::array unavailable{ + static constexpr std::array visible{ "jf_m_simple_off", "jf_m_normal_off", "jf_m_hard_off", "jf_m_extra_off" }; + static constexpr std::array 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(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 textures; - const std::array 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 visible; std::vector 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(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(genreSlot) + delta) % static_cast(genres.size()); - if (next < 0) next += static_cast(genres.size()); - genreSlot = static_cast(next); - selection = 0; + auto changeSort = [&](int delta) { + const size_t selectedSong = visible.empty() ? 0 : visible[selection]; + int next = (static_cast(sortSlot) + delta) % 8; + if (next < 0) next += 8; + sortSlot = static_cast(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(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 sortNames{ + "NEW", "MONTHLY THEME", "GENRE", "DIFFICULTY", + "SCORE AVERAGE", "TITLE", "FAVORITE", "AT RANDOM" + }; + const glm::vec4 activeColor = genreColor(static_cast(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; } } diff --git a/docs/re_gc_song_select.md b/docs/re_gc_song_select.md index ffb290b..8d19618 100644 --- a/docs/re_gc_song_select.md +++ b/docs/re_gc_song_select.md @@ -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