diff --git a/docs/MOD_PACKAGES.md b/docs/MOD_PACKAGES.md index 655f39cf..4a6b9631 100644 --- a/docs/MOD_PACKAGES.md +++ b/docs/MOD_PACKAGES.md @@ -603,6 +603,13 @@ remain black. Enabling the feature without choosing artwork is also a no-op. The package supplies only the declaration and trusted plugin selection; archives still cannot load native code. +A title's own trusted plugin can call `psx_mod_set_bezel_artwork(path)` directly +to ship its artwork. An absolute path (such as the built-in package's selected +resource) is used as-is; a relative path like `"bezels/qirex.png"` is resolved +against the executable's directory when the artwork loads, never the current +working directory, so artwork the title stages beside its binary is found +however the game was launched. + ### Retained-scene loading presentation (native-wide opt-in) A trusted game plugin can register diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index 30887ac5..9de40626 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -231,6 +231,10 @@ int psx_mod_set_auto_skip_fmv(int enabled); * Draw still artwork behind the game image in OpenGL letterbox/pillarbox * margins. The image path is an owner-selected mod resource; with no enabled * mod/resource path, the margins remain the historical black clear. + * An absolute path is used unchanged. A relative path (e.g. "bezels/x.png" + * for artwork a title stages beside its binary) is resolved against the + * executable's directory, never the current working directory, when the + * artwork is loaded. */ int psx_mod_set_bezel_artwork(const char* path); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index f8b352fe..1cfb76f0 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -15240,18 +15240,26 @@ session_reboot: /* Bezel artwork (Mods): load after the GL context exists. */ if (!g_bezel_path.empty() && g_gl_active) { - std::filesystem::path bp(g_bezel_path); + /* A relative artwork path names a file shipped beside the + * executable (e.g. a title's staged bezels/), so anchor it on the + * exe directory -- never cwd (see exe_dir_from_argv). An absolute + * path, such as the builtin bezel's owner-selected resource, is + * used unchanged. */ + const std::filesystem::path bp = PSXRecompV4::host_resolve( + exe_dir_from_argv(argv[0]), std::filesystem::path(g_bezel_path)); std::vector file; - if (FILE *bf = std::fopen(bp.string().c_str(), "rb")) { - std::fseek(bf, 0, SEEK_END); - const long len = std::ftell(bf); - std::fseek(bf, 0, SEEK_SET); + /* fs::path stream: the exe directory is a wide Windows path, which + * a narrow fopen() of bp.string() cannot open when it is non-ASCII. */ + std::ifstream bf(bp, std::ios::binary | std::ios::ate); + if (bf.is_open()) { + const std::streamoff len = bf.tellg(); if (len > 0) { file.resize((size_t)len); - if (std::fread(file.data(), 1, file.size(), bf) != file.size()) + bf.seekg(0, std::ios::beg); + if (!bf.read(reinterpret_cast(file.data()), + (std::streamsize)file.size())) file.clear(); } - std::fclose(bf); } int bw = 0, bh = 0, bc = 0; unsigned char *px = file.empty() ? nullptr diff --git a/runtime/tests/test_mod_bezel.py b/runtime/tests/test_mod_bezel.py index 6e82f9f1..7515fc64 100644 --- a/runtime/tests/test_mod_bezel.py +++ b/runtime/tests/test_mod_bezel.py @@ -9,7 +9,8 @@ MAIN = (ROOT / "runtime/src/main.cpp").read_text(encoding="utf-8") HEADER = (ROOT / "runtime/include/mod_plugins.h").read_text(encoding="utf-8") MOD_RUNTIME = (ROOT / "runtime/src/mod_runtime.cpp").read_text(encoding="utf-8") BUILTIN = (ROOT / "runtime/src/mod_builtin_bezel.c").read_text(encoding="utf-8") -CONFIG_H = (ROOT / "recompiler/src/config_loader.h").read_text(encoding="utf-8") +HOST_PATH = (ROOT / "recompiler/include/host_path.h").read_text(encoding="utf-8") +CONFIG_H =(ROOT / "recompiler/src/config_loader.h").read_text(encoding="utf-8") CONFIG_CPP = (ROOT / "recompiler/src/config_loader.cpp").read_text( encoding="utf-8" ) @@ -26,6 +27,18 @@ assert "psx_mod_current_resource_path" in HEADER assert "psx_mod_set_bezel_artwork" in MAIN assert "g_video_renderer = 1;" in MAIN assert "mod-owned OpenGL margin artwork" in MAIN +# Relative artwork paths resolve against the exe directory at load time, never +# cwd; absolute paths (the builtin resource) pass through host_resolve as-is. +LOADER = MAIN[MAIN.index("/* Bezel artwork (Mods): load after the GL context exists. */"):] +LOADER = LOADER[: LOADER.index("gl_renderer_set_bezel(")] +assert ( + "PSXRecompV4::host_resolve(\n" + " exe_dir_from_argv(argv[0]), std::filesystem::path(g_bezel_path));" +) in LOADER +assert "host_path_is_absolute(path) ? path : root / path" in HOST_PATH +assert "std::fopen" not in LOADER +assert "std::filesystem::path bp(g_bezel_path)" not in MAIN +assert "resolved against the\n * executable's directory, never the current working directory" in HEADER assert "current_plugin" in MOD_RUNTIME assert 'psx_mod_current_resource_path("artwork"' in BUILTIN assert 'psx_mod_register_activation_plugin("psx.bezel"' in BUILTIN