Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,47 @@ if(BUILD_TESTING)
COMMAND $<TARGET_FILE:recomp-ui-launcher-discs-test>
"${CMAKE_CURRENT_BINARY_DIR}")

add_executable(recomp-ui-launcher-mod-gate-test
tests/launcher_mod_gate_test.c src/common/crc32.c src/common/sha1.c
src/common/launcher_mod_visibility.c
src/common/sha256.c src/common/ips_patch.c src/consoles/psx/memcard_format.c)
target_include_directories(recomp-ui-launcher-mod-gate-test PRIVATE src src/common src/consoles/psx)
target_link_libraries(recomp-ui-launcher-mod-gate-test PRIVATE ${RUI_SDL_TARGET})
# Keep assertions active in Release, matching the intended policy fixture.
if(MSVC)
target_compile_options(recomp-ui-launcher-mod-gate-test PRIVATE /UNDEBUG)
else()
target_compile_options(recomp-ui-launcher-mod-gate-test PRIVATE -UNDEBUG)
endif()
add_test(NAME recomp-ui-launcher-mod-gate COMMAND recomp-ui-launcher-mod-gate-test)

add_executable(recomp-ui-launcher-mod-button-test
tests/launcher_mod_button_test.cpp
src/third_party/imgui/imgui.cpp src/third_party/imgui/imgui_draw.cpp
src/third_party/imgui/imgui_tables.cpp src/third_party/imgui/imgui_widgets.cpp)
target_include_directories(recomp-ui-launcher-mod-button-test PRIVATE
src/common src/third_party/imgui)
if(MSVC)
target_compile_options(recomp-ui-launcher-mod-button-test PRIVATE /UNDEBUG)
else()
target_compile_options(recomp-ui-launcher-mod-button-test PRIVATE -UNDEBUG)
endif()
add_test(NAME recomp-ui-launcher-mod-button COMMAND recomp-ui-launcher-mod-button-test)

# Cover folders: per-disc preference, relocation, Unicode paths, invalid
# and empty folders, ordering and aspect fit. Writes only under the test dir.
add_executable(recomp-ui-launcher-artwork-folder-test tests/launcher_artwork_folder_test.cpp)
target_include_directories(recomp-ui-launcher-artwork-folder-test PRIVATE src/common)
target_compile_features(recomp-ui-launcher-artwork-folder-test PRIVATE cxx_std_17)
if(MSVC)
target_compile_options(recomp-ui-launcher-artwork-folder-test PRIVATE /UNDEBUG)
else()
target_compile_options(recomp-ui-launcher-artwork-folder-test PRIVATE -UNDEBUG)
endif()
add_test(NAME recomp-ui-launcher-artwork-folder
COMMAND recomp-ui-launcher-artwork-folder-test
"${CMAKE_CURRENT_BINARY_DIR}/artwork-folder-test-$<CONFIG>")

# Renderer / VSync as enumerable lists rather than cycle buttons. Same
# include-the-TU shape as the discs test above.
add_executable(recomp-ui-launcher-video-pick-test
Expand Down
282 changes: 247 additions & 35 deletions src/common/backends/imgui/launcher_imgui.cpp

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions src/common/backends/imgui/launcher_mod_button.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once
#include "imgui.h"

/* Only the dashboard entry is gated. The existing Mods page is unchanged. */
static inline bool rui_mods_button_tooltip(bool enabled) {
if (!enabled && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip("%s", "Please select a valid game disc first.");
return true;
}
return false;
}

static inline bool rui_mods_button(bool enabled, const char* label, ImVec2 size) {
ImGui::BeginDisabled(!enabled);
const bool pressed = ImGui::Button(label, size);
ImGui::EndDisabled();
rui_mods_button_tooltip(enabled);
return enabled && pressed;
}
96 changes: 96 additions & 0 deletions src/common/launcher_artwork.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#pragma once
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>

// Optional read-only cover images, one folder per disc serial. Only an explicit
// folder selection writes anything: that disc's cover-folder.txt preference.
namespace launcher_artwork {
namespace fs = std::filesystem;
struct Catalog { std::vector<fs::path> images; };
inline std::string lower(std::string s) {
for (char& c:s) c=static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return s;
}
inline bool supported(const std::string& serial) {
// Any console's disc serial that is a safe single folder name
// (SCES-01330, T-1234G, MK-81000): no separators, no dot names.
if(serial.empty() || serial.size()>64 || serial[0]=='.')return false;
for(char c:serial) {
const bool ok=(c>='A' && c<='Z') || (c>='a' && c<='z') || (c>='0' && c<='9') ||
c=='-' || c=='_' || c=='.';
if(!ok)return false;
}
return true;
}
inline fs::path root(fs::path executable_directory) {
executable_directory=executable_directory.lexically_normal();
// SDL_GetBasePath ends in a separator; remove its empty filename first.
if(executable_directory.filename().empty()) executable_directory=executable_directory.parent_path();
return executable_directory/"artwork";
}
inline fs::path folder(const fs::path& home,const std::string& serial) {
if(!supported(serial))return {};
const auto base=fs::absolute(home/serial).lexically_normal();
std::ifstream in(base/"cover-folder.txt",std::ios::binary);
std::string saved(65537,'\0');in.read(saved.data(),saved.size());saved.resize(size_t(in.gcount()));
if(saved.empty() || saved.size()>65536 || saved.find('\0')!=std::string::npos)return base;
auto selected=fs::u8path(saved);
return (selected.is_absolute()?selected:base/selected).lexically_normal();
}
inline bool select_folder(const fs::path& home,const std::string& serial,const fs::path& path) {
if(!supported(serial))return false;
try {
if(!fs::is_directory(path))return false;
const auto base=fs::absolute(home/serial).lexically_normal();
if(fs::is_symlink(base) || fs::is_symlink(base/"cover-folder.txt"))return false;
const auto chosen=fs::absolute(path).lexically_normal();
fs::create_directories(base);
// Paths within the portable installation follow it when moved.
const auto inside=chosen.lexically_relative(fs::absolute(home).parent_path());
const auto saved=(!inside.empty() && *inside.begin()!=fs::path(".."))
?chosen.lexically_relative(base):chosen;
std::ofstream out(base/"cover-folder.txt",std::ios::binary|std::ios::trunc);
out<<saved.u8string();out.flush();return bool(out);
}catch(...){return false;}
}
inline int rank(const fs::path& p) {
auto name=lower(p.filename().u8string());
if(name.find("front")!=std::string::npos)return 0;
if(name.find("back")!=std::string::npos)return 1;
if(name.find("disc")!=std::string::npos)return 2;
return 3;
}
inline Catalog scan(const fs::path& home,const std::string& serial) {
Catalog c;if(!supported(serial))return c;
try {
std::error_code ec;auto directory=folder(home,serial);
fs::directory_iterator it(directory,fs::directory_options::skip_permission_denied,ec),end;
while(!ec && it!=end) {
const auto p=it->path();
if(!it->is_symlink(ec) && it->is_regular_file(ec)) {
auto ext=lower(p.extension().u8string());
if((ext==".png" || ext==".jpg" || ext==".jpeg" || ext==".tga") &&
it->file_size(ec)<=32*1024*1024 && !ec)c.images.push_back(p);
}
it.increment(ec);
}
std::sort(c.images.begin(),c.images.end(),[](const auto& a,const auto& b){
return rank(a)!=rank(b)?rank(a)<rank(b):a.u8string()<b.u8string();
});
} catch(const std::exception&) { return {}; }
return c;
}
inline size_t step(size_t current,size_t count,int direction) {
if(!count)return 0;
return direction<0?(current+count-1)%count:(current+1)%count;
}
struct Size { float w,h; };
inline Size fit(float w,float h,float max_w,float max_h) {
if(w<=0 || h<=0 || max_w<=0 || max_h<=0)return {0,0};
float scale=std::min(max_w/w,max_h/h);return {w*scale,h*scale};
}
}
36 changes: 27 additions & 9 deletions src/common/launcher_files.c
Original file line number Diff line number Diff line change
Expand Up @@ -564,28 +564,38 @@ static int linux_pick_save_kdialog(const char* title, const char* const* pattern
return linux_spawn_capture(argv, out, out_cap);
}

static int linux_pick_folder_zenity(const char* title, char* out, size_t out_cap) {
static int linux_pick_folder_zenity(const char* title, const char* start_dir,
char* out, size_t out_cap) {
char title_arg[640];
snprintf(title_arg, sizeof(title_arg), "--title=%s",
title && title[0] ? title : "Select folder");
/* zenity opens a directory when --filename ends in a separator. */
char start_arg[4200];
snprintf(start_arg, sizeof(start_arg), "--filename=%s/",
start_dir && start_dir[0] ? start_dir : ".");
char* argv[] = {
"zenity",
"--file-selection",
"--directory",
title_arg,
start_arg,
NULL
};
return linux_spawn_capture(argv, out, out_cap);
}

static int linux_pick_folder_kdialog(const char* title, char* out, size_t out_cap) {
static int linux_pick_folder_kdialog(const char* title, const char* start_dir,
char* out, size_t out_cap) {
char title_buf[512];
snprintf(title_buf, sizeof(title_buf), "%s",
title && title[0] ? title : "Select folder");
char start_buf[4096];
snprintf(start_buf, sizeof(start_buf), "%s",
start_dir && start_dir[0] ? start_dir : ".");
char* argv[] = {
"kdialog",
"--getexistingdirectory",
".",
start_buf,
"--title",
title_buf,
NULL
Expand Down Expand Up @@ -638,21 +648,22 @@ static int linux_pick_save(const char* title, const char* const* patterns,
return -1;
}

static int linux_pick_folder(const char* title, char* out, size_t out_cap) {
static int linux_pick_folder(const char* title, const char* start_dir,
char* out, size_t out_cap) {
const int want_kd = linux_desktop_prefers_kdialog();
const int have_kd = linux_have_cmd("kdialog");
const int have_zn = linux_have_cmd("zenity");
int r;
if (want_kd && have_kd) {
r = linux_pick_folder_kdialog(title, out, out_cap);
r = linux_pick_folder_kdialog(title, start_dir, out, out_cap);
if (r >= 0) return r;
}
if (have_zn) {
r = linux_pick_folder_zenity(title, out, out_cap);
r = linux_pick_folder_zenity(title, start_dir, out, out_cap);
if (r >= 0) return r;
}
if (!want_kd && have_kd) {
r = linux_pick_folder_kdialog(title, out, out_cap);
r = linux_pick_folder_kdialog(title, start_dir, out, out_cap);
if (r >= 0) return r;
}
return -1;
Expand Down Expand Up @@ -702,18 +713,25 @@ bool launcher_pick_rom(char* out_path, size_t out_cap) {
}

int launcher_try_pick_folder(const char* title, char* out_path, size_t out_cap) {
return launcher_try_pick_folder_in(title, NULL, out_path, out_cap);
}

int launcher_try_pick_folder_in(const char* title, const char* start_dir,
char* out_path, size_t out_cap) {
if (!out_path || out_cap == 0) return -1;
out_path[0] = '\0';

#if defined(__ANDROID__)
(void)title;
(void)start_dir;
return -1;
#elif defined(__linux__)
return linux_pick_folder(title, out_path, out_cap);
return linux_pick_folder(title, start_dir, out_path, out_cap);
#else
{
const char* sel =
tinyfd_selectFolderDialog(title ? title : "Select folder", "");
tinyfd_selectFolderDialog(title ? title : "Select folder",
start_dir ? start_dir : "");
if (!sel || !sel[0]) return 0;
snprintf(out_path, out_cap, "%s", sel);
return 1;
Expand Down
3 changes: 3 additions & 0 deletions src/common/launcher_files.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ int launcher_try_pick_file(const char* title, const char* const* patterns,

// Tri-state folder pick. Same contract as launcher_try_pick_file.
int launcher_try_pick_folder(const char* title, char* out_path, size_t out_cap);
/* Same, opening in start_dir (UTF-8; NULL or "" = the picker default). */
int launcher_try_pick_folder_in(const char* title, const char* start_dir,
char* out_path, size_t out_cap);

// Tri-state save-destination pick. Same contract as launcher_try_pick_file.
int launcher_try_pick_save_file(const char* title, const char* const* patterns,
Expand Down
7 changes: 7 additions & 0 deletions src/common/launcher_gl.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// launcher_gl.c — image->GL-texture loader (stb_image + GL 1.1).

#include "launcher_gl.h"
#include <stdint.h>

#include "launcher_sdlcompat.h" // GL header for the active SDL version

Expand All @@ -13,6 +14,8 @@
// glsl_shader.c does — sharing one impl caused LNK2005, and relying on the
// host's format config left TGA box art unloadable). Static = self-contained
// impl with our own STBI_ONLY_* set, identical behaviour standalone or in-process.
#define STBI_WINDOWS_UTF8
#define STBI_MAX_DIMENSIONS 8192
#define STB_IMAGE_STATIC
#define STB_IMAGE_IMPLEMENTATION
#define STBI_ONLY_TGA
Expand All @@ -39,6 +42,10 @@ void launcher_image_free(unsigned char* pixels) {
static LauncherTexture load_impl(const char* path, int colorkey_tol) {
LauncherTexture t = { 0, 0, 0 };
int w = 0, h = 0, comp = 0;
// Bound optional user images before allocating/uploading them.
if(!stbi_info(path,&w,&h,&comp) || w<=0 || h<=0 || (uint64_t)w*h>16*1024*1024)return t;
GLint limit=0;glGetIntegerv(GL_MAX_TEXTURE_SIZE,&limit);
if(w>limit || h>limit)return t;
unsigned char* pixels = stbi_load(path, &w, &h, &comp, 4); // force RGBA
if (!pixels) return t;

Expand Down
14 changes: 14 additions & 0 deletions src/common/launcher_mod_feature_actions.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#pragma once
#include "launcher_model.h"
#include "launcher_mod_visibility.h"

/* One provider operation for the full Mods page and dashboard shortcuts.
* The provider owns dependencies, validation and persisted feature state. */
static inline int launcher_mod_enable_feature(LauncherModel* model,
const RecompLauncherCModFeature* feature,int enabled) {
const RecompLauncherCModProvider* mods=model?model->mods:NULL;
if(!feature || !mods || !mods->feature_enable ||
!launcher_model_can_open_mods(model) ||
!launcher_mod_feature_listed(mods,feature))return 0;
return mods->feature_enable(mods->ctx,feature->package_id,feature->id,enabled);
}
23 changes: 22 additions & 1 deletion src/common/launcher_model.c
Original file line number Diff line number Diff line change
Expand Up @@ -941,10 +941,13 @@ void launcher_model_init(LauncherModel* m,
{
char cached_rom[1024];
const char* seeded_rom = initial_rom;
if (lm_load_cached_rom_path(game, initial_rom, cached_rom,
if ((!game || !game->disc_selection_locked) && lm_load_cached_rom_path(game, initial_rom, cached_rom,
sizeof(cached_rom)))
seeded_rom = cached_rom;
launcher_model_set_rom(m, seeded_rom);
m->disc_selection_locked=game && game->disc_selection_locked;
m->artwork_root=game?game->artwork_root:NULL;
m->dashboard_quick_controls=game && game->dashboard_quick_controls;
}

// Password/mantra save: read the current one-line password file so the
Expand Down Expand Up @@ -1575,6 +1578,7 @@ int launcher_model_rom_report(const LauncherModel* m, char* out, size_t cap) {
}

void launcher_model_set_rom(LauncherModel* m, const char* path) {
if(m->disc_selection_locked && (!path || strcmp(path,m->rom_full)!=0))return;
if (lm_is_sbi_path(path)) {
char mounted[sizeof(m->rom_full)] = {0};
m->setup_error[0] = '\0';
Expand Down Expand Up @@ -2429,6 +2433,16 @@ void launcher_model_cycle_screen_kind(LauncherModel* m) {
int n = 4; (void)screen_kind_vocab(m, &n);
m->s.screen_kind = (clampi(m->s.screen_kind, 0, n - 1) + 1) % n;
}
int launcher_model_screen_kind_count(const LauncherModel* m) {
int n=0;(void)screen_kind_vocab(m,&n);return n;
}
const char* launcher_model_screen_kind_name(const LauncherModel* m,int index) {
int n=0;const char* const* names=screen_kind_vocab(m,&n);
return names[clampi(index,0,n-1)];
}
void launcher_model_set_screen_kind(LauncherModel* m,int index) {
if(m && m->has_screen_kind)m->s.screen_kind=clampi(index,0,launcher_model_screen_kind_count(m)-1);
}

const char* launcher_model_screen_kind_label(const LauncherModel* m) {
int n = 4;
Expand Down Expand Up @@ -3268,6 +3282,13 @@ bool launcher_model_can_finish_setup(const LauncherModel* m) {
return true;
}

bool launcher_model_can_open_mods(const LauncherModel* m) {
if (!m || m->profile_handoff || m->setup_preparing) return false;
if (m->profile && m->profile->verify.mode == 1)
return m->rom_present && m->rom_full[0] && m->verify.verdict == 1;
return true; /* Preserve non-disc hosts. */
}

bool launcher_model_can_launch(const LauncherModel* m) {
if (!m) return false;
if (!m->rom_present || strcmp(m->rom_size, "--") == 0) return false;
Expand Down
10 changes: 10 additions & 0 deletions src/common/launcher_model.h
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,11 @@ typedef struct {
// See VerifyResult above. Untouched (all-zero) for verify.mode==0 systems
// (SNES) — panels branch on m->profile->verify.mode, never on this alone.
VerifyResult verify;
// From GameInfo (RECOMP_LAUNCHER_HAS_ARTWORK_GALLERY): the host chose the
// disc already; cover folder root (borrowed, NULL = default); Quick cards.
bool disc_selection_locked;
const char* artwork_root;
bool dashboard_quick_controls;

// ---- editable settings (working copy of the C ABI struct) ----
RecompLauncherCSettings s;
Expand Down Expand Up @@ -987,6 +992,9 @@ void launcher_model_toggle_perspective_texturing(LauncherModel* m);
// correction rounds back to the native pixel and has no visible effect.
bool launcher_model_geometry_correction_inert(const LauncherModel* m);
void launcher_model_cycle_screen_kind(LauncherModel* m); // Raw/CRT/Composite/Trinitron
int launcher_model_screen_kind_count(const LauncherModel* m);
const char* launcher_model_screen_kind_name(const LauncherModel* m,int index);
void launcher_model_set_screen_kind(LauncherModel* m,int index);
const char* launcher_model_screen_kind_label(const LauncherModel* m);
void launcher_model_toggle_scanlines(LauncherModel* m);
void launcher_model_set_scanline_strength_pct(LauncherModel* m, int pct); // clamps 1..100
Expand Down Expand Up @@ -1216,6 +1224,8 @@ void launcher_model_setup_start_bios_regen(LauncherModel* m);
bool launcher_model_can_finish_setup(const LauncherModel* m);
// True when BIOS (if required) and ROM/disc are ready to launch (incl. fingerprint).
bool launcher_model_can_launch(const LauncherModel* m);
// Dashboard Mods entry requires a fully verified disc and no regional handoff.
bool launcher_model_can_open_mods(const LauncherModel* m);
// True when the mounted disc is OK for online (TOC/cue policy + content).
// Non-disc games (verify.mode!=1) always return true when netplay is supported.
bool launcher_model_netplay_disc_ok(const LauncherModel* m);
Expand Down
Loading