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 <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,17 +801,61 @@ 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;
|
||||
}
|
||||
|
||||
if (sortSlot == kGenreSort) {
|
||||
int previousGenre = -1;
|
||||
for (const size_t songIndex : visible) {
|
||||
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});
|
||||
}
|
||||
} else {
|
||||
for (const size_t songIndex : visible) {
|
||||
carousel.push_back({false, songIndex, songs[songIndex].catalog.genre});
|
||||
}
|
||||
}
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user