ui: scan games in a background thread. hide unknown zips and naomi chds
This commit is contained in:
parent
786c8e7744
commit
381f0f0f95
|
@ -1,9 +1,6 @@
|
|||
#pragma once
|
||||
#include "types.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
void os_SetWindowText(const char* text);
|
||||
double os_GetSeconds();
|
||||
|
||||
|
@ -28,13 +25,3 @@ u32 static INLINE bitscanrev(u32 v)
|
|||
}
|
||||
|
||||
void os_DebugBreak();
|
||||
|
||||
static inline std::string get_file_extension(const std::string& s)
|
||||
{
|
||||
size_t dot = s.find_last_of('.');
|
||||
if (dot >= s.length())
|
||||
return "";
|
||||
std::string ext = s.substr(dot + 1, s.length() - dot - 1);
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c){ return std::tolower(c); });
|
||||
return ext;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
Copyright 2020 flyinghead
|
||||
|
||||
This file is part of flycast.
|
||||
|
||||
flycast is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
flycast is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with flycast. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#pragma once
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "types.h"
|
||||
#include "stdclass.h"
|
||||
#include "hw/naomi/naomi_roms.h"
|
||||
|
||||
struct GameMedia {
|
||||
std::string name;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
static bool operator<(const GameMedia &left, const GameMedia &right)
|
||||
{
|
||||
return left.name < right.name;
|
||||
}
|
||||
|
||||
class GameScanner
|
||||
{
|
||||
std::vector<GameMedia> game_list;
|
||||
std::mutex mutex;
|
||||
std::unique_ptr<std::thread> scan_thread;
|
||||
bool scan_done = false;
|
||||
bool running = false;
|
||||
std::unordered_map<std::string, const Game*> arcade_games;
|
||||
std::unordered_set<std::string> arcade_gdroms;
|
||||
|
||||
void insert_game(const GameMedia& game)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
game_list.insert(std::upper_bound(game_list.begin(), game_list.end(), game), game);
|
||||
}
|
||||
|
||||
void add_game_directory(const std::string& path)
|
||||
{
|
||||
//printf("Exploring %s\n", path.c_str());
|
||||
DIR *dir = opendir(path.c_str());
|
||||
if (dir == NULL)
|
||||
return;
|
||||
while (running)
|
||||
{
|
||||
struct dirent *entry = readdir(dir);
|
||||
if (entry == NULL)
|
||||
break;
|
||||
std::string name(entry->d_name);
|
||||
if (name == "." || name == "..")
|
||||
continue;
|
||||
std::string child_path = path + "/" + name;
|
||||
bool is_dir = false;
|
||||
#ifndef _WIN32
|
||||
if (entry->d_type == DT_DIR)
|
||||
is_dir = true;
|
||||
if (entry->d_type == DT_UNKNOWN || entry->d_type == DT_LNK)
|
||||
#endif
|
||||
{
|
||||
struct stat st;
|
||||
if (stat(child_path.c_str(), &st) != 0)
|
||||
continue;
|
||||
if (S_ISDIR(st.st_mode))
|
||||
is_dir = true;
|
||||
}
|
||||
if (is_dir)
|
||||
{
|
||||
add_game_directory(child_path);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string extension = get_file_extension(name);
|
||||
if (extension == "zip" || extension == "7z")
|
||||
{
|
||||
std::string basename = get_file_basename(name);
|
||||
string_tolower(basename);
|
||||
auto it = arcade_games.find(basename);
|
||||
if (it == arcade_games.end())
|
||||
continue;
|
||||
name = name + " (" + std::string(it->second->description) + ")";
|
||||
}
|
||||
else if (extension == "chd" || extension == "gdi")
|
||||
{
|
||||
// Hide arcade gdroms
|
||||
std::string basename = get_file_basename(name);
|
||||
string_tolower(basename);
|
||||
if (arcade_gdroms.count(basename) != 0)
|
||||
continue;
|
||||
}
|
||||
else if ((settings.dreamcast.HideLegacyNaomiRoms
|
||||
|| (extension != "bin" && extension != "lst" && extension != "dat"))
|
||||
&& extension != "cdi" && extension != "cue")
|
||||
continue;
|
||||
insert_game(GameMedia{ name, child_path });
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
public:
|
||||
~GameScanner()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
void refresh()
|
||||
{
|
||||
stop();
|
||||
scan_done = false;
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
running = false;
|
||||
if (scan_thread && scan_thread->joinable())
|
||||
scan_thread->join();
|
||||
}
|
||||
|
||||
void fetch_game_list()
|
||||
{
|
||||
if (scan_done || running)
|
||||
return;
|
||||
running = true;
|
||||
scan_thread = std::unique_ptr<std::thread>(
|
||||
new std::thread([this]()
|
||||
{
|
||||
if (arcade_games.empty())
|
||||
for (int gameid = 0; Games[gameid].name != nullptr; gameid++)
|
||||
{
|
||||
const Game *game = &Games[gameid];
|
||||
arcade_games[game->name] = game;
|
||||
if (game->gdrom_name != nullptr)
|
||||
arcade_gdroms.insert(game->gdrom_name);
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
game_list.clear();
|
||||
}
|
||||
for (const auto& path : settings.dreamcast.ContentPath)
|
||||
{
|
||||
add_game_directory(path);
|
||||
if (!running)
|
||||
break;
|
||||
}
|
||||
if (running)
|
||||
scan_done = true;
|
||||
running = false;
|
||||
}));
|
||||
}
|
||||
|
||||
std::mutex& get_mutex() { return mutex; }
|
||||
const std::vector<GameMedia>& get_game_list() { return game_list; }
|
||||
};
|
|
@ -16,18 +16,12 @@
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with reicast. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "gui.h"
|
||||
#include "oslib/oslib.h"
|
||||
#include "cfg/cfg.h"
|
||||
#include "hw/maple/maple_if.h"
|
||||
#include "hw/maple/maple_devs.h"
|
||||
#include "hw/naomi/naomi_cart.h"
|
||||
#include "hw/naomi/naomi_roms.h"
|
||||
#include "imgui/imgui.h"
|
||||
#include "gles/imgui_impl_opengl3.h"
|
||||
#include "imgui/roboto_medium.h"
|
||||
|
@ -36,6 +30,7 @@
|
|||
#include "input/keyboard_device.h"
|
||||
#include "gui_util.h"
|
||||
#include "gui_android.h"
|
||||
#include "game_scanner.h"
|
||||
#include "version.h"
|
||||
#include "oslib/audiostream.h"
|
||||
#include "imgread/common.h"
|
||||
|
@ -44,9 +39,7 @@
|
|||
extern void dc_loadstate();
|
||||
extern void dc_savestate();
|
||||
extern void dc_stop();
|
||||
extern void dc_reset(bool manual);
|
||||
extern void dc_resume();
|
||||
extern void dc_term();
|
||||
extern void dc_start_game(const char *path);
|
||||
extern void UpdateInputState(u32 port);
|
||||
extern bool game_started;
|
||||
|
@ -70,6 +63,8 @@ static void display_vmus();
|
|||
static void reset_vmus();
|
||||
static void term_vmus();
|
||||
|
||||
GameScanner scanner;
|
||||
|
||||
float gui_get_scaling()
|
||||
{
|
||||
return scaling;
|
||||
|
@ -248,6 +243,7 @@ void ImGui_Impl_NewFrame()
|
|||
}
|
||||
|
||||
#if 0
|
||||
#include "oslib/timeseries.h"
|
||||
TimeSeries renderTimes;
|
||||
TimeSeries vblankTimes;
|
||||
|
||||
|
@ -292,6 +288,7 @@ void gui_open_settings()
|
|||
|
||||
static void gui_start_game(const std::string& path)
|
||||
{
|
||||
scanner.stop();
|
||||
try {
|
||||
dc_start_game(path.empty() ? NULL : path.c_str());
|
||||
} catch (ReicastException& ex) {
|
||||
|
@ -646,14 +643,12 @@ static void error_popup()
|
|||
}
|
||||
}
|
||||
|
||||
static bool game_list_done; // Set to false to refresh the game list
|
||||
|
||||
void directory_selected_callback(bool cancelled, std::string selection)
|
||||
{
|
||||
if (!cancelled)
|
||||
{
|
||||
settings.dreamcast.ContentPath.push_back(selection);
|
||||
game_list_done = false;
|
||||
scanner.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -815,7 +810,7 @@ static void gui_display_settings()
|
|||
if (to_delete >= 0)
|
||||
{
|
||||
settings.dreamcast.ContentPath.erase(settings.dreamcast.ContentPath.begin() + to_delete);
|
||||
game_list_done = false;
|
||||
scanner.refresh();
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
@ -833,9 +828,9 @@ static void gui_display_settings()
|
|||
ImGui::ListBoxFooter();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ShowHelpMarker("The directory where reicast saves configuration files and VMUs. BIOS files should be in the data subdirectory");
|
||||
ShowHelpMarker("The directory where reicast saves configuration files and VMUs. BIOS files should be in a subfolder named \"data\"");
|
||||
if (ImGui::Checkbox("Hide Legacy Naomi Roms", &settings.dreamcast.HideLegacyNaomiRoms))
|
||||
game_list_done = false;
|
||||
scanner.refresh();
|
||||
ImGui::SameLine();
|
||||
ShowHelpMarker("Hide .bin, .dat and .lst files from the content browser");
|
||||
|
||||
|
@ -1126,7 +1121,7 @@ static void gui_display_settings()
|
|||
{
|
||||
ImGui::SliderInt("Texture Upscaling", (int *)&settings.rend.TextureUpscale, 1, 8);
|
||||
ImGui::SameLine();
|
||||
ShowHelpMarker("Upscale textures with the xBRZ algorithm. Only on fast platforms and for certain games");
|
||||
ShowHelpMarker("Upscale textures with the xBRZ algorithm. Only on fast platforms and for certain 2D games");
|
||||
ImGui::SliderInt("Upscaled Texture Max Size", (int *)&settings.rend.MaxFilteredTextureSize, 8, 1024);
|
||||
ImGui::SameLine();
|
||||
ShowHelpMarker("Textures larger than this dimension squared will not be upscaled");
|
||||
|
@ -1148,7 +1143,7 @@ static void gui_display_settings()
|
|||
ShowHelpMarker("Disable the emulator sound output");
|
||||
ImGui::Checkbox("Enable DSP", &settings.aica.DSPEnabled);
|
||||
ImGui::SameLine();
|
||||
ShowHelpMarker("Enable the Dreamcast Digital Sound Processor. Only recommended on fast and arm64 platforms");
|
||||
ShowHelpMarker("Enable the Dreamcast Digital Sound Processor. Only recommended on fast platforms");
|
||||
ImGui::Checkbox("Limit Emulator Speed", &settings.aica.LimitFPS);
|
||||
ImGui::SameLine();
|
||||
ShowHelpMarker("Whether to limit the emulator speed using the audio output. Recommended");
|
||||
|
@ -1291,10 +1286,10 @@ static void gui_display_settings()
|
|||
{
|
||||
ImGui::Checkbox("HLE BIOS", &settings.bios.UseReios);
|
||||
ImGui::SameLine();
|
||||
ShowHelpMarker("Use high-level BIOS emulation if BIOS files are not found");
|
||||
ShowHelpMarker("Force high-level BIOS emulation");
|
||||
ImGui::Checkbox("Force Windows CE", &settings.dreamcast.ForceWindowsCE);
|
||||
ImGui::SameLine();
|
||||
ShowHelpMarker("Enable full MMU emulation and other Windows CE settings");
|
||||
ShowHelpMarker("Enable full MMU emulation and other Windows CE settings. Do not enable unless necessary");
|
||||
#ifndef __ANDROID
|
||||
ImGui::Checkbox("Serial Console", &settings.debug.SerialConsole);
|
||||
ImGui::SameLine();
|
||||
|
@ -1421,79 +1416,6 @@ static void gui_display_settings()
|
|||
settings.dynarec.Enable = (bool)dynarec_enabled;
|
||||
}
|
||||
|
||||
struct GameMedia {
|
||||
std::string name;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
static bool operator<(const GameMedia &left, const GameMedia &right)
|
||||
{
|
||||
return left.name < right.name;
|
||||
}
|
||||
|
||||
static void add_game_directory(const std::string& path, std::vector<GameMedia>& game_list)
|
||||
{
|
||||
//printf("Exploring %s\n", path.c_str());
|
||||
DIR *dir = opendir(path.c_str());
|
||||
if (dir == NULL)
|
||||
return;
|
||||
while (true)
|
||||
{
|
||||
struct dirent *entry = readdir(dir);
|
||||
if (entry == NULL)
|
||||
break;
|
||||
std::string name(entry->d_name);
|
||||
if (name == "." || name == "..")
|
||||
continue;
|
||||
std::string child_path = path + "/" + name;
|
||||
bool is_dir = false;
|
||||
#ifndef _WIN32
|
||||
if (entry->d_type == DT_DIR)
|
||||
is_dir = true;
|
||||
if (entry->d_type == DT_UNKNOWN || entry->d_type == DT_LNK)
|
||||
#endif
|
||||
{
|
||||
struct stat st;
|
||||
if (stat(child_path.c_str(), &st) != 0)
|
||||
continue;
|
||||
if (S_ISDIR(st.st_mode))
|
||||
is_dir = true;
|
||||
}
|
||||
if (is_dir)
|
||||
{
|
||||
add_game_directory(child_path, game_list);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string::size_type dotpos = name.find_last_of('.');
|
||||
if (dotpos == std::string::npos || dotpos == name.size() - 1)
|
||||
continue;
|
||||
std::string extension = name.substr(dotpos);
|
||||
if (stricmp(extension.c_str(), ".zip") && stricmp(extension.c_str(), ".7z")
|
||||
&& (settings.dreamcast.HideLegacyNaomiRoms
|
||||
|| (stricmp(extension.c_str(), ".bin") && stricmp(extension.c_str(), ".lst")
|
||||
&& stricmp(extension.c_str(), ".dat")))
|
||||
&& stricmp(extension.c_str(), ".cdi") && stricmp(extension.c_str(), ".gdi")
|
||||
&& stricmp(extension.c_str(), ".chd") && stricmp(extension.c_str(), ".cue"))
|
||||
continue;
|
||||
game_list.push_back({ name, child_path });
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
static std::vector<GameMedia> game_list;
|
||||
|
||||
static void fetch_game_list()
|
||||
{
|
||||
if (game_list_done)
|
||||
return;
|
||||
game_list.clear();
|
||||
for (auto& path : settings.dreamcast.ContentPath)
|
||||
add_game_directory(path, game_list);
|
||||
std::stable_sort(game_list.begin(), game_list.end());
|
||||
game_list_done = true;
|
||||
}
|
||||
|
||||
static void gui_display_demo()
|
||||
{
|
||||
|
@ -1505,22 +1427,6 @@ static void gui_display_demo()
|
|||
ImGui_impl_RenderDrawData(ImGui::GetDrawData(), false);
|
||||
}
|
||||
|
||||
static std::string get_game_description(std::string name)
|
||||
{
|
||||
size_t dot = name.find_last_of('.');
|
||||
if (dot != std::string::npos && dot != name.size() - 1)
|
||||
name = name.substr(0, dot);
|
||||
|
||||
int gameid = 0;
|
||||
for (; Games[gameid].name != NULL; gameid++)
|
||||
if (!stricmp(Games[gameid].name, name.c_str()))
|
||||
break;
|
||||
if (Games[gameid].name == NULL)
|
||||
return std::string();
|
||||
|
||||
return std::string(Games[gameid].description);
|
||||
}
|
||||
|
||||
static void gui_display_content()
|
||||
{
|
||||
ImGui_Impl_NewFrame();
|
||||
|
@ -1550,7 +1456,7 @@ static void gui_display_content()
|
|||
}
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
fetch_game_list();
|
||||
scanner.fetch_game_list();
|
||||
|
||||
// Only if Filter and Settings aren't focused... ImGui::SetNextWindowFocus();
|
||||
ImGui::BeginChild(ImGui::GetID("library"), ImVec2(0, 0), true);
|
||||
|
@ -1566,29 +1472,22 @@ static void gui_display_content()
|
|||
}
|
||||
ImGui::PopID();
|
||||
|
||||
for (const auto& game : game_list)
|
||||
{
|
||||
scanner.get_mutex().lock();
|
||||
for (const auto& game : scanner.get_game_list())
|
||||
{
|
||||
if (gui_state == SelectDisk)
|
||||
{
|
||||
std::string::size_type dotpos = game.name.find_last_of('.');
|
||||
if (dotpos == std::string::npos || dotpos == game.name.size() - 1)
|
||||
continue;
|
||||
std::string extension = game.name.substr(dotpos);
|
||||
if (stricmp(extension.c_str(), ".gdi") && stricmp(extension.c_str(), ".chd")
|
||||
&& stricmp(extension.c_str(), ".cdi") && stricmp(extension.c_str(), ".cue"))
|
||||
std::string extension = get_file_extension(game.path);
|
||||
if (extension != "gdi" && extension != "chd"
|
||||
&& extension != "cdi" && extension != "cue")
|
||||
// Only dreamcast disks
|
||||
continue;
|
||||
}
|
||||
std::string description,selection;
|
||||
description = get_game_description(game.name);
|
||||
if (description.empty())
|
||||
selection = game.name;
|
||||
else
|
||||
selection = game.name + " (" + description + ")";
|
||||
if (filter.PassFilter(selection.c_str()))
|
||||
if (filter.PassFilter(game.name.c_str()))
|
||||
{
|
||||
ImGui::PushID(game.path.c_str());
|
||||
if (ImGui::Selectable(selection.c_str()))
|
||||
if (ImGui::Selectable(game.name.c_str()))
|
||||
{
|
||||
if (gui_state == SelectDisk)
|
||||
{
|
||||
|
@ -1598,13 +1497,17 @@ static void gui_display_content()
|
|||
}
|
||||
else
|
||||
{
|
||||
scanner.get_mutex().unlock();
|
||||
gui_state = ClosedNoResume;
|
||||
gui_start_game(game.path);
|
||||
scanner.get_mutex().lock();
|
||||
}
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
scanner.get_mutex().unlock();
|
||||
}
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
@ -1812,7 +1715,7 @@ extern bool subfolders_read;
|
|||
|
||||
void gui_refresh_files()
|
||||
{
|
||||
game_list_done = false;
|
||||
scanner.refresh();
|
||||
subfolders_read = false;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
#ifndef _WIN32
|
||||
#include <pthread.h>
|
||||
|
@ -111,3 +113,26 @@ public:
|
|||
return data[i];
|
||||
}
|
||||
};
|
||||
|
||||
static inline void string_tolower(std::string& s)
|
||||
{
|
||||
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return std::tolower(c); });
|
||||
}
|
||||
|
||||
static inline std::string get_file_extension(const std::string& s)
|
||||
{
|
||||
size_t dot = s.find_last_of('.');
|
||||
if (dot == std::string::npos)
|
||||
return "";
|
||||
std::string ext = s.substr(dot + 1, s.length() - dot - 1);
|
||||
string_tolower(ext);
|
||||
return ext;
|
||||
}
|
||||
|
||||
static inline std::string get_file_basename(const std::string& s)
|
||||
{
|
||||
size_t dot = s.find_last_of('.');
|
||||
if (dot == std::string::npos)
|
||||
return s;
|
||||
return s.substr(0, dot);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue