mirror of
https://github.com/mihawk-99/PS5_RetroArch.git
synced 2026-10-22 00:22:29 -05:00
From mihawk-99/PS5_RetroArch#34 (Martin Pham). A forwarder, a small app
with a home screen tile of its own, starts the title with --rom <file>
[--core <core>] [--exit-after-game] (each also as --x=<value>). On a
launch that names no mode, src/frontend_mode_ps5.cpp takes it as game
mode takes a frontend's request: the content (absolute, or inside
/app0/content/ and never above it) and the core are checked by
ps5_game_check, RetroArch starts with -L, and afterwards the title
opens as from the home screen, or closes with --exit-after-game.
Without --core the playlists choose, else the one core whose info file
lists the extension (Genesis Plus GX for the Mega Drive extensions both
Sega cores list) or, for a folder, the one core that opens folders. A
refusal goes to trace.txt and the launch goes on as from the home
screen; a handover's --ps5-mode wins over --rom.
Changed from the pull request: its README examples and test fixtures no
longer name games and use the ES-DE folder names content/ has now; one
header comment is moved to its declaration. Its line-ending rewrites of
the PowerShell scripts are dropped (they are gone, 8f14452), and its
GitHub workflow is parked in parked/github-build-workflow, with what
would finish it at the top of build.yml: it cannot run here, and it
does not fetch PS5_MihawkGen, which the frontend compiles now.
To prove it on a console, whose control payload launches without
arguments: the relaunch test takes its restarts' arguments from the
lines after the arm line, and tools/run-title.sh --relaunch-arg writes
them.
Evidence: tests/frontend_mode_ps5_test.cpp (parsing, paths, cores by
playlist, extension and folder, refusals, after the game);
tests/relaunch_ps5_test.cpp (arguments carried; a control character or
a ninth argument arms nothing). Gates: format, unit (204), build
(staged cores reused), integration, evidence PASS. Console: with this
series' deploy.
582 lines
28 KiB
C++
582 lines
28 KiB
C++
/* Which frontend a launch of eboot.bin starts (src/frontend_mode_ps5.cpp): the
|
|
* decision for every launch, the mode argument as the console gives argv, and the
|
|
* LoadExec each decision makes, against a LoadExec that replaces the process (a
|
|
* thrown Replaced), refuses, or is accepted and ignored. argv[1] is a scratch
|
|
* directory for the files the decision reads. */
|
|
#include <sys/stat.h>
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <vector>
|
|
#include "../src/frontend_mode_ps5.cpp"
|
|
|
|
namespace
|
|
{
|
|
struct Replaced
|
|
{
|
|
};
|
|
enum class Exec
|
|
{
|
|
replace,
|
|
refuse,
|
|
ignore
|
|
};
|
|
Exec exec_mode = Exec::replace;
|
|
int exec_calls = 0;
|
|
std::string exec_path, exec_argument;
|
|
std::vector<std::string> marks;
|
|
bool l1_held = false;
|
|
int l1_reads = 0;
|
|
bool read_l1()
|
|
{
|
|
l1_reads++;
|
|
return l1_held;
|
|
}
|
|
|
|
std::string read_file(const std::string &path)
|
|
{
|
|
std::string text;
|
|
if (std::FILE *file = std::fopen(path.c_str(), "rb"))
|
|
{
|
|
char buffer[256];
|
|
size_t n;
|
|
while ((n = std::fread(buffer, 1, sizeof(buffer), file)) > 0)
|
|
text.append(buffer, n);
|
|
std::fclose(file);
|
|
}
|
|
return text;
|
|
}
|
|
|
|
void write_file(const std::string &path, const std::string &text)
|
|
{
|
|
std::FILE *file = std::fopen(path.c_str(), "wb");
|
|
assert(file);
|
|
std::fputs(text.c_str(), file);
|
|
std::fclose(file);
|
|
}
|
|
|
|
void touch(const std::string &path, bool present)
|
|
{
|
|
if (!present)
|
|
{
|
|
std::remove(path.c_str());
|
|
return;
|
|
}
|
|
std::FILE *file = std::fopen(path.c_str(), "wb");
|
|
assert(file);
|
|
std::fclose(file);
|
|
}
|
|
|
|
/* One launch: what it LoadExecs, or "retroarch" when it returns to run RetroArch. */
|
|
std::string launch(const ps5::frontend_mode::Paths &paths, std::vector<const char *> argv)
|
|
{
|
|
argv.push_back(nullptr);
|
|
exec_path.clear();
|
|
try
|
|
{
|
|
ps5::frontend_mode::run(paths, static_cast<int>(argv.size() - 1),
|
|
const_cast<char **>(argv.data()), 0);
|
|
return "retroarch";
|
|
}
|
|
catch (const Replaced &)
|
|
{
|
|
return exec_path;
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
/* src/permissions_ps5.cpp: the repair is the title's; here it has nothing to wait for. */
|
|
extern "C" void ps5_permissions_settle()
|
|
{
|
|
}
|
|
extern "C" int sceSystemServiceLoadExec(const char *path, const char *const *argv)
|
|
{
|
|
exec_calls++;
|
|
exec_path = path;
|
|
exec_argument = argv && argv[0] ? argv[0] : "(none)";
|
|
/* "exit" closes the title, with no arguments; a restart has exactly one. */
|
|
assert(std::string(path) == "exit" ? !argv : argv && argv[0] && !argv[1]);
|
|
if (exec_mode == Exec::replace)
|
|
throw Replaced{};
|
|
return exec_mode == Exec::refuse ? -1 : 0;
|
|
}
|
|
|
|
/* The title's pad read (src/frontend_hold_ps5.cpp); the launches here use read_l1. */
|
|
extern "C" bool ps5_frontend_reopen_held(void)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void ps5::debug::mark(const char *step) noexcept
|
|
{
|
|
marks.emplace_back(step);
|
|
}
|
|
void ps5::debug::mark_value(const char *step, long long value) noexcept
|
|
{
|
|
marks.emplace_back(std::string(step) + " " + std::to_string(value));
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
using ps5::frontend_mode::decide;
|
|
using ps5::frontend_mode::Launch;
|
|
using ps5::frontend_mode::Next;
|
|
assert(argc == 2);
|
|
const std::string dir = argv[1];
|
|
|
|
/* The decision, for every launch. */
|
|
for (unsigned state = 0; state < 32; state++)
|
|
{
|
|
for (const char *mode : {"", "retroarch", "es-de", "picker", "game", "quit", "other"})
|
|
for (const char *choice : {"ask", "retroarch", "es-de"})
|
|
{
|
|
Launch launch;
|
|
launch.mode = mode;
|
|
launch.test_run = state & 1;
|
|
launch.picker_test = state & 2;
|
|
launch.picker_present = state & 4;
|
|
launch.es_de_present = state & 8;
|
|
launch.reopen_held = state & 16;
|
|
launch.choice = choice;
|
|
const Next next = decide(launch);
|
|
const std::string m = mode, c = choice;
|
|
if (m == "game")
|
|
assert(next == Next::game);
|
|
else if (m == "es-de")
|
|
assert(next == (launch.es_de_present ? Next::es_de : Next::retroarch));
|
|
else if (m == "picker")
|
|
assert(next == (launch.picker_present ? Next::picker : Next::retroarch));
|
|
else if (m == "quit") /* a frontend quit: the picker, or closed when remembered */
|
|
assert(next ==
|
|
(c == "ask" && launch.picker_present ? Next::picker : Next::close));
|
|
else if (!m.empty())
|
|
assert(next == Next::retroarch);
|
|
else if (launch.picker_test && launch.picker_present)
|
|
assert(next == Next::picker);
|
|
else if (launch.test_run)
|
|
assert(next == Next::retroarch);
|
|
else if (!launch.reopen_held && c == "retroarch")
|
|
assert(next == Next::retroarch);
|
|
else if (!launch.reopen_held && c == "es-de" && launch.es_de_present)
|
|
assert(next == Next::es_de);
|
|
else
|
|
assert(next == (launch.picker_present ? Next::picker : Next::retroarch));
|
|
}
|
|
}
|
|
|
|
/* The mode argument, as the console gives argv: no program name, and an empty
|
|
* argument from the home screen. */
|
|
{
|
|
char empty[] = "", mode[] = "--ps5-mode=retroarch", other[] = "--ps5-relaunch=2";
|
|
char *home[] = {empty, nullptr};
|
|
char *chosen[] = {mode, nullptr};
|
|
char *both[] = {other, mode, nullptr};
|
|
assert(ps5::frontend_mode::mode_argument(1, home).empty());
|
|
assert(ps5::frontend_mode::mode_argument(1, chosen) == "retroarch");
|
|
assert(ps5::frontend_mode::mode_argument(2, both) == "retroarch");
|
|
assert(ps5::frontend_mode::mode_argument(0, nullptr).empty());
|
|
/* A session starts with no argument of this port: the home screen, a test run. */
|
|
assert(ps5::frontend_mode::session_start(1, home) &&
|
|
ps5::frontend_mode::session_start(0, nullptr));
|
|
assert(!ps5::frontend_mode::session_start(1, chosen) &&
|
|
!ps5::frontend_mode::session_start(2, both));
|
|
}
|
|
|
|
/* The launches, against files in the scratch directory. */
|
|
const ps5::frontend_mode::Paths paths{
|
|
dir + "/picker.bin", dir + "/es-de.bin", dir + "/test-run.txt",
|
|
dir + "/picker-test.txt", dir + "/eboot.bin", dir + "/game-request.txt",
|
|
dir + "/game-result.txt", dir + "/playlists", dir + "/content/",
|
|
dir + "/info/", dir + "/frontend.cfg", dir + "/trace.txt",
|
|
dir + "/retroarch.log", dir + "/retroarch-game.log", read_l1};
|
|
touch(paths.picker, true);
|
|
touch(paths.es_de, true);
|
|
touch(paths.test_run, false);
|
|
touch(paths.picker_test, false);
|
|
assert(launch(paths, {""}) == paths.picker && exec_argument.empty()); /* the home screen */
|
|
assert(marks.back() == "frontend: mode '', test run 0, picker test 0, picker 1, es-de 1, "
|
|
"remembered ask, L1 0 -> picker");
|
|
assert(l1_reads == 0); /* nothing remembered: the pad is not read */
|
|
assert(launch(paths, {"--ps5-mode=retroarch"}) == "retroarch");
|
|
assert(launch(paths, {"--ps5-mode=es-de"}) == paths.es_de);
|
|
touch(paths.test_run, true);
|
|
assert(launch(paths, {""}) == "retroarch"); /* a test run stays RetroArch */
|
|
touch(paths.picker_test, true);
|
|
assert(launch(paths, {""}) == paths.picker); /* unless the picker test is armed */
|
|
touch(paths.picker, false);
|
|
assert(launch(paths, {""}) == "retroarch"); /* a title without the picker */
|
|
touch(paths.es_de, false);
|
|
assert(launch(paths, {"--ps5-mode=es-de"}) == "retroarch");
|
|
|
|
/* A LoadExec refused, or accepted but not replacing the process, runs RetroArch. */
|
|
touch(paths.picker, true);
|
|
touch(paths.test_run, false);
|
|
touch(paths.picker_test, false);
|
|
for (Exec mode : {Exec::refuse, Exec::ignore})
|
|
{
|
|
exec_mode = mode;
|
|
const int before = exec_calls;
|
|
assert(launch(paths, {""}) == "retroarch" && exec_calls == before + 1 &&
|
|
exec_path == paths.picker);
|
|
assert(marks.back().rfind(
|
|
"frontend: LoadExec did not replace the process; RetroArch runs; result", 0) ==
|
|
0);
|
|
}
|
|
/* A frontend remembered (config/frontend.cfg): a launch from the home screen starts
|
|
* it, unless L1 is held; a frontend quitting then closes the title. */
|
|
touch(paths.test_run, false);
|
|
touch(paths.picker_test, false);
|
|
touch(paths.es_de, true);
|
|
exec_mode = Exec::replace;
|
|
assert(ps5_frontend_choice_write(paths.choice.c_str(), "es-de") == 0);
|
|
assert(read_file(paths.choice) == "frontend_start = \"es-de\"\n");
|
|
assert(launch(paths, {""}) == paths.es_de && l1_reads == 1);
|
|
l1_held = true;
|
|
assert(launch(paths, {""}) == paths.picker && l1_reads == 2); /* L1: the picker anyway */
|
|
l1_held = false;
|
|
assert(launch(paths, {"--ps5-mode=quit"}) == "exit" && exec_argument == "(none)");
|
|
assert(launch(paths, {"--ps5-mode=es-de"}) == paths.es_de); /* a mode is not overridden */
|
|
assert(ps5_frontend_choice_write(paths.choice.c_str(), "retroarch") == 0);
|
|
assert(launch(paths, {""}) == "retroarch");
|
|
touch(paths.test_run, true);
|
|
const int reads_before = l1_reads;
|
|
assert(launch(paths, {""}) == "retroarch" && l1_reads == reads_before); /* a test run */
|
|
touch(paths.test_run, false);
|
|
assert(ps5_frontend_choice_write(paths.choice.c_str(), "ask") == 0);
|
|
assert(launch(paths, {"--ps5-mode=quit"}) == paths.picker);
|
|
/* A file that says anything else is "ask"; an invalid choice is not written. */
|
|
write_file(paths.choice, "frontend_start = \"evil\"\nother = \"1\"\n");
|
|
assert(std::string(ps5_frontend_choice_read(paths.choice.c_str())) == "ask");
|
|
assert(ps5_frontend_choice_write(paths.choice.c_str(), "evil") != 0);
|
|
std::remove(paths.choice.c_str());
|
|
assert(std::string(ps5_frontend_choice_read(paths.choice.c_str())) == "ask");
|
|
/* The shell refusing to close the title leaves RetroArch to run. */
|
|
assert(ps5_frontend_choice_write(paths.choice.c_str(), "es-de") == 0);
|
|
exec_mode = Exec::refuse;
|
|
assert(launch(paths, {"--ps5-mode=quit"}) == "retroarch" &&
|
|
marks.back().rfind("frontend: the shell did not close the title", 0) == 0);
|
|
exec_mode = Exec::replace;
|
|
std::remove(paths.choice.c_str());
|
|
|
|
/* Each session's trace is kept once, at its start only; RetroArch's log by mode. */
|
|
write_file(paths.trace, "last session\n");
|
|
{
|
|
char handover[] = "--ps5-mode=quit", empty[] = "";
|
|
char *carried[] = {handover, nullptr};
|
|
char *home[] = {empty, nullptr};
|
|
ps5::frontend_mode::start_session_logs(paths, 1, carried);
|
|
assert(read_file(paths.trace) == "last session\n");
|
|
ps5::frontend_mode::start_session_logs(paths, 1, home);
|
|
assert(!ps5::frontend_mode::exists(paths.trace) &&
|
|
read_file(dir + "/trace.1.txt") == "last session\n");
|
|
}
|
|
write_file(paths.retroarch_log, "retroarch\n");
|
|
assert(ps5::frontend_mode::retroarch_log(paths) == paths.retroarch_log);
|
|
assert(!ps5::frontend_mode::exists(paths.retroarch_log) &&
|
|
read_file(dir + "/retroarch.1.log") == "retroarch\n");
|
|
|
|
/* After RetroArch quits: back to the picker only when the picker started it. */
|
|
for (const char *mode : {"", "retroarch", "es-de", "picker"})
|
|
for (bool present : {false, true})
|
|
assert(ps5::frontend_mode::back_to_picker(mode, present) ==
|
|
(present && std::string(mode) == "retroarch"));
|
|
const auto quit = [&](const char *mode)
|
|
{
|
|
exec_path.clear();
|
|
try
|
|
{
|
|
ps5::frontend_mode::after_retroarch(paths, mode, 0, 0, false);
|
|
return std::string("closes");
|
|
}
|
|
catch (const Replaced &)
|
|
{
|
|
return exec_path;
|
|
}
|
|
};
|
|
exec_mode = Exec::replace;
|
|
assert(quit("retroarch") == paths.eboot && exec_argument == "--ps5-mode=quit");
|
|
assert(quit("") == "closes"); /* a test run's RetroArch closes the title */
|
|
touch(paths.picker, false);
|
|
assert(quit("retroarch") == "closes");
|
|
touch(paths.picker, true);
|
|
exec_mode = Exec::refuse;
|
|
assert(quit("retroarch") == "closes" &&
|
|
marks.back().rfind("frontend: LoadExec did not replace the process; the title closes",
|
|
0) == 0);
|
|
/* Game mode: the request taken once, its core from the request or the playlists,
|
|
* refused back to the frontend, and the result and the frontend after RetroArch. */
|
|
exec_mode = Exec::replace;
|
|
const std::string content = dir + "/game.zip",
|
|
core = std::string(PS5_GAME_CORES) + "snes9x_libretro.so",
|
|
frontend = paths.es_de;
|
|
touch(paths.es_de, true); /* the frontend to come back to */
|
|
mkdir(PS5_GAME_CORES, 0777);
|
|
touch(content, true);
|
|
touch(core, true);
|
|
const auto request = [&](const char *core_path, const char *content_path)
|
|
{
|
|
struct ps5_game game = {};
|
|
std::snprintf(game.core, sizeof(game.core), "%s", core_path);
|
|
std::snprintf(game.content, sizeof(game.content), "%s", content_path);
|
|
std::snprintf(game.frontend, sizeof(game.frontend), "%s", frontend.c_str());
|
|
std::snprintf(game.state, sizeof(game.state), "%s", "snes\tgame.zip");
|
|
assert(ps5_game_write(paths.request.c_str(), &game, 0) == 0);
|
|
};
|
|
const auto result = [&]()
|
|
{
|
|
struct ps5_game game;
|
|
int kind = -1;
|
|
assert(ps5_game_read(paths.result.c_str(), &game, &kind) == 0 && kind == 1);
|
|
std::remove(paths.result.c_str());
|
|
return game;
|
|
};
|
|
request(core.c_str(), content.c_str());
|
|
assert(launch(paths, {"--ps5-mode=game"}) == "retroarch");
|
|
const struct ps5_game *running = ps5::frontend_mode::running_game();
|
|
assert(running && core == running->core && content == running->content);
|
|
assert(!ps5::frontend_mode::exists(paths.request)); /* a request runs once */
|
|
/* A game's RetroArch logs apart from RetroArch's own. */
|
|
write_file(paths.game_log, "last game\n");
|
|
assert(ps5::frontend_mode::retroarch_log(paths) == paths.game_log &&
|
|
read_file(dir + "/retroarch-game.1.log") == "last game\n");
|
|
assert(quit("") == frontend); /* RetroArch quit: back to the frontend, with the result */
|
|
struct ps5_game back = result();
|
|
assert(back.status == 0 && back.seconds >= 0 && core == back.core &&
|
|
std::string(back.state) == "snes\tgame.zip");
|
|
assert(!ps5::frontend_mode::running_game());
|
|
|
|
/* An update installed as RetroArch quit: no handover, in game mode or from the picker. */
|
|
request(core.c_str(), content.c_str());
|
|
assert(launch(paths, {"--ps5-mode=game"}) == "retroarch" && ps5::frontend_mode::running_game());
|
|
exec_path.clear();
|
|
ps5::frontend_mode::after_retroarch(paths, "game", 0, 0, true);
|
|
assert(exec_path.empty() && !ps5::frontend_mode::running_game() &&
|
|
marks.back() == "frontend: an update was installed; the title closes, no handover");
|
|
ps5::frontend_mode::after_retroarch(paths, "retroarch", 0, 0, true);
|
|
assert(exec_path.empty());
|
|
|
|
/* RetroArch's playlist association wins over the frontend's core, if it is the title's. */
|
|
const std::string other = std::string(PS5_GAME_CORES) + "bsnes_libretro.so";
|
|
touch(other, true);
|
|
request(core.c_str(), content.c_str());
|
|
mkdir(paths.playlists.c_str(), 0777);
|
|
std::FILE *associated = std::fopen((paths.playlists + "/SNES.lpl").c_str(), "w");
|
|
assert(associated);
|
|
std::fprintf(associated, "{\"items\":[{\"path\":\"%s\",\"core_path\":\"%s\"}]}",
|
|
content.c_str(), other.c_str());
|
|
std::fclose(associated);
|
|
assert(launch(paths, {"--ps5-mode=game"}) == "retroarch" &&
|
|
other == ps5::frontend_mode::running_game()->core);
|
|
assert(quit("") == frontend);
|
|
result();
|
|
associated = std::fopen((paths.playlists + "/SNES.lpl").c_str(), "w");
|
|
std::fprintf(associated,
|
|
"{\"items\":[{\"path\":\"%s\",\"core_path\":\"/elsewhere/x_libretro.so\"}]}",
|
|
content.c_str());
|
|
std::fclose(associated);
|
|
request(core.c_str(), content.c_str());
|
|
assert(launch(paths, {"--ps5-mode=game"}) == "retroarch" &&
|
|
core == ps5::frontend_mode::running_game()->core);
|
|
assert(quit("") == frontend);
|
|
result();
|
|
|
|
request("", content.c_str()); /* no core: the playlists' */
|
|
std::FILE *playlist = std::fopen((paths.playlists + "/SNES.lpl").c_str(), "w");
|
|
assert(playlist);
|
|
std::fprintf(playlist, "{\"items\":[{\"path\":\"%s\",\"core_path\":\"%s\"}]}", content.c_str(),
|
|
core.c_str());
|
|
std::fclose(playlist);
|
|
assert(launch(paths, {"--ps5-mode=game"}) == "retroarch" &&
|
|
ps5::frontend_mode::running_game() && core == ps5::frontend_mode::running_game()->core);
|
|
assert(quit("") == frontend);
|
|
result();
|
|
|
|
std::remove((paths.playlists + "/SNES.lpl").c_str());
|
|
request("", content.c_str()); /* no core anywhere: refused back to the frontend */
|
|
assert(launch(paths, {"--ps5-mode=game"}) == frontend && !ps5::frontend_mode::running_game());
|
|
back = result();
|
|
assert(back.status == -1 && std::string(back.error).find("no core") == 0);
|
|
|
|
request(core.c_str(), (dir + "/missing.zip").c_str()); /* content that is not there */
|
|
assert(launch(paths, {"--ps5-mode=game"}) == frontend);
|
|
assert(result().status == -1);
|
|
|
|
assert(launch(paths, {"--ps5-mode=game"}) == "retroarch" &&
|
|
!ps5::frontend_mode::running_game());
|
|
assert(marks.back() == "game mode: no request; RetroArch runs");
|
|
|
|
/* A home screen forwarder: --rom and --core, run as a game; afterwards the title as from
|
|
* the home screen, or closed with --exit-after-game. */
|
|
{
|
|
using ps5::frontend_mode::forward_arguments;
|
|
using ps5::frontend_mode::forward_content;
|
|
using ps5::frontend_mode::forward_core;
|
|
char rom[] = "--rom", rom_value[] = "snes/Game (USA).zip", core_flag[] = "--core=snes9x",
|
|
exit_flag[] = "--exit-after-game", dangling[] = "--core";
|
|
char *full[] = {rom, rom_value, core_flag, exit_flag, nullptr};
|
|
const ps5::frontend_mode::Forward parsed = forward_arguments(4, full);
|
|
assert(parsed.rom == "snes/Game (USA).zip" && parsed.core == "snes9x" &&
|
|
parsed.exit_after_game);
|
|
char *missing[] = {dangling, nullptr};
|
|
assert(forward_arguments(1, missing).core.empty() &&
|
|
forward_arguments(0, nullptr).rom.empty());
|
|
assert(forward_content("snes/Game.zip", "/app0/content/") == "/app0/content/snes/Game.zip");
|
|
assert(forward_content("/mnt/usb0/Game.zip\r\n", "/c/") == "/mnt/usb0/Game.zip");
|
|
assert(forward_content("../Game.zip", "/c/").empty() &&
|
|
forward_content("a/../../x", "/c/").empty());
|
|
assert(forward_content("a/..b.zip", "/c/") == "/c/a/..b.zip" &&
|
|
forward_content("", "/c/").empty());
|
|
for (const char *name : {"snes9x", "snes9x_libretro", "snes9x_libretro.so"})
|
|
assert(forward_core(name, "/app0/cores/") == "/app0/cores/snes9x_libretro.so");
|
|
assert(forward_core("/app0/cores/mgba_libretro.so", "/x/") ==
|
|
"/app0/cores/mgba_libretro.so");
|
|
assert(forward_core("", "/app0/cores/").empty() &&
|
|
forward_core("_libretro.so", "/c/").empty());
|
|
}
|
|
mkdir((dir + "/content").c_str(), 0777);
|
|
mkdir((dir + "/content/snes").c_str(), 0777);
|
|
const std::string forwarded = dir + "/content/snes/Game.zip";
|
|
touch(forwarded, true);
|
|
touch(paths.eboot, true); /* the title, which game mode's check takes as the frontend */
|
|
touch(paths.picker, true);
|
|
std::remove(paths.choice.c_str());
|
|
exec_mode = Exec::replace;
|
|
/* --core named: the game, with that core; the playlists do not override it */
|
|
assert(launch(paths, {"--rom", "snes/Game.zip", "--core", "snes9x"}) == "retroarch");
|
|
const struct ps5_game *forward_game = ps5::frontend_mode::running_game();
|
|
assert(forward_game && forwarded == forward_game->content && core == forward_game->core);
|
|
assert(marks.back() == "forwarder: " + forwarded + " with " + core);
|
|
assert(ps5::frontend_mode::retroarch_log(paths) == paths.game_log);
|
|
/* RetroArch quit: the title as from the home screen, with no result for a frontend */
|
|
std::remove(paths.result.c_str());
|
|
assert(quit("") == paths.eboot && exec_argument.empty());
|
|
assert(!ps5::frontend_mode::exists(paths.result) && !ps5::frontend_mode::running_game());
|
|
/* --exit-after-game: the title closes (catchReturnFromMain's LoadExec("exit")) */
|
|
{
|
|
const std::string absolute = "--rom=" + forwarded;
|
|
assert(launch(paths, {absolute.c_str(), "--core=snes9x_libretro.so",
|
|
"--exit-after-game"}) == "retroarch");
|
|
assert(ps5::frontend_mode::running_game() && quit("") == "closes");
|
|
assert(marks.back().find("the title closes") != std::string::npos);
|
|
}
|
|
/* no --core: RetroArch's playlists choose, as for a frontend's game */
|
|
playlist = std::fopen((paths.playlists + "/SNES.lpl").c_str(), "w");
|
|
assert(playlist);
|
|
std::fprintf(playlist, "{\"items\":[{\"path\":\"%s\",\"core_path\":\"%s\"}]}",
|
|
forwarded.c_str(), other.c_str());
|
|
std::fclose(playlist);
|
|
assert(launch(paths, {"--rom", "snes/Game.zip"}) == "retroarch" &&
|
|
other == ps5::frontend_mode::running_game()->core);
|
|
assert(quit("") == paths.eboot);
|
|
std::remove((paths.playlists + "/SNES.lpl").c_str());
|
|
/* not runnable: the trace says why, and the launch goes on as from the home screen */
|
|
assert(launch(paths, {"--rom", "snes/Game.zip"}) == paths.picker &&
|
|
!ps5::frontend_mode::running_game());
|
|
assert(std::find_if(marks.begin(), marks.end(), [](const std::string &m)
|
|
{ return m.find("no core: neither --core") != std::string::npos; }) !=
|
|
marks.end());
|
|
/* nor a playlist: the core whose .info lists the extension (a hack no playlist has) */
|
|
mkdir(paths.info.c_str(), 0777);
|
|
write_file(paths.info + "snes9x_libretro.info",
|
|
"display_name = \"Snes9x\"\nsupported_extensions = \"smc|SFC|swc\"\n");
|
|
write_file(paths.info + "bsnes_libretro.info", "supported_extensions = \"sfc|bs\"\n");
|
|
write_file(paths.info + "genesis_plus_gx_libretro.info",
|
|
" supported_extensions = \"md|gen|cue\"\n");
|
|
write_file(paths.info + "picodrive_libretro.info", "supported_extensions = \"md|32x|cue\"\n");
|
|
write_file(paths.info + "notes.txt", "supported_extensions = \"sfc\"\n");
|
|
const std::string hack = dir + "/content/snes/Hack (patched).SFC";
|
|
touch(hack, true);
|
|
assert((ps5::frontend_mode::forward_extension_cores(paths.info, hack) ==
|
|
std::vector<std::string>{"bsnes", "snes9x"}));
|
|
assert(ps5::frontend_mode::forward_extension_cores(paths.info, dir + "/content/snes/x.smc") ==
|
|
std::vector<std::string>{"snes9x"});
|
|
assert(ps5::frontend_mode::forward_extension_cores(paths.info, dir + "/content/a.b/noext")
|
|
.empty());
|
|
assert(ps5::frontend_mode::forward_extension_cores(paths.info, dir + "/content/x.sf").empty());
|
|
assert(ps5::frontend_mode::forward_preferred_core(
|
|
"/x/Racer.md", {"genesis_plus_gx", "picodrive"}) == "genesis_plus_gx");
|
|
assert(
|
|
ps5::frontend_mode::forward_preferred_core("/x/Game.cue", {"genesis_plus_gx", "picodrive"})
|
|
.empty());
|
|
/* bsnes lists .sfc but is not one of the title's cores (no library): snes9x runs it */
|
|
std::remove(other.c_str());
|
|
assert(launch(paths, {"--rom", "snes/Hack (patched).SFC"}) == "retroarch" &&
|
|
core == ps5::frontend_mode::running_game()->core);
|
|
assert(quit("") == paths.eboot);
|
|
/* both are cores of the title: --core has to choose, and the trace names them */
|
|
touch(other, true);
|
|
assert(launch(paths, {"--rom", "snes/Hack (patched).SFC"}) == paths.picker &&
|
|
!ps5::frontend_mode::running_game());
|
|
assert(std::find_if(marks.begin(), marks.end(),
|
|
[](const std::string &m)
|
|
{
|
|
return m.find("several cores list its extension (bsnes, snes9x)") !=
|
|
std::string::npos;
|
|
}) != marks.end());
|
|
/* a Mega Drive cartridge both Sega cores list: Genesis Plus GX */
|
|
const std::string genesis = std::string(PS5_GAME_CORES) + "genesis_plus_gx_libretro.so";
|
|
touch(genesis, true);
|
|
touch(std::string(PS5_GAME_CORES) + "picodrive_libretro.so", true);
|
|
touch(dir + "/content/Racer.md", true);
|
|
assert(launch(paths, {"--rom", "Racer.md"}) == "retroarch" &&
|
|
genesis == ps5::frontend_mode::running_game()->core);
|
|
assert(quit("") == paths.eboot);
|
|
/* a folder, for a core that opens folders (its .info lists "/"): DOSBox Pure's DOS or
|
|
* Windows installation */
|
|
write_file(paths.info + "dosbox_pure_libretro.info",
|
|
"supported_extensions = \"zip|dosz|exe|/\"\n");
|
|
const std::string dosbox = std::string(PS5_GAME_CORES) + "dosbox_pure_libretro.so";
|
|
touch(dosbox, true);
|
|
mkdir((dir + "/content/dos").c_str(), 0777);
|
|
mkdir((dir + "/content/dos/w95").c_str(), 0777);
|
|
assert(ps5::frontend_mode::forward_folder_cores(paths.info) ==
|
|
std::vector<std::string>{"dosbox_pure"});
|
|
assert(ps5::frontend_mode::forward_content("dos/w95/", "/c/") == "/c/dos/w95");
|
|
{
|
|
const std::string w95 = "--rom=" + dir + "/content/dos/w95";
|
|
assert(launch(paths, {w95.c_str(), "--core", "dosbox_pure_libretro",
|
|
"--exit-after-game"}) == "retroarch" &&
|
|
dosbox == ps5::frontend_mode::running_game()->core &&
|
|
dir + "/content/dos/w95" == ps5::frontend_mode::running_game()->content);
|
|
assert(quit("") == "closes");
|
|
}
|
|
/* without --core: the one core that opens folders */
|
|
assert(launch(paths, {"--rom", "dos/w95/"}) == "retroarch" &&
|
|
dosbox == ps5::frontend_mode::running_game()->core);
|
|
assert(quit("") == paths.eboot);
|
|
/* a core that does not open folders is refused, naming those that do */
|
|
assert(launch(paths, {"--rom", "dos/w95", "--core", "snes9x"}) == paths.picker &&
|
|
!ps5::frontend_mode::running_game());
|
|
const auto marked = [&](const char *text)
|
|
{
|
|
return std::find_if(marks.begin(), marks.end(), [&](const std::string &m)
|
|
{ return m.find(text) != std::string::npos; }) != marks.end();
|
|
};
|
|
assert(
|
|
marked("snes9x does not open folders (its .info lists no \"/\"; these do: dosbox_pure)"));
|
|
/* a folder that is not there is still "not a file" */
|
|
assert(launch(paths, {"--rom", "dos/w98", "--core", "dosbox_pure"}) == paths.picker &&
|
|
marked("--rom dos/w98 --core dosbox_pure not run: the content is not a file"));
|
|
for (const std::vector<const char *> &args :
|
|
std::vector<std::vector<const char *>>{{"--rom", "snes/Missing.zip", "--core", "snes9x"},
|
|
{"--rom", "../Game.zip", "--core", "snes9x"},
|
|
{"--rom", "snes/Game.zip", "--core", "nosuchcore"}})
|
|
assert(launch(paths, args) == paths.picker && !ps5::frontend_mode::running_game());
|
|
/* a handover's mode wins over --rom */
|
|
assert(launch(paths, {"--ps5-mode=es-de", "--rom", "snes/Game.zip", "--core", "snes9x"}) ==
|
|
paths.es_de &&
|
|
!ps5::frontend_mode::running_game());
|
|
|
|
std::puts(
|
|
"frontend_mode_ps5: forwarder --rom/--core launches, cores by extension, folders, refusals "
|
|
"and "
|
|
"after-game PASS; "
|
|
"decisions, mode argument, LoadExec targets, test runs and "
|
|
"refused or ignored LoadExec PASS; the remembered frontend, L1, quit and closing PASS; "
|
|
"session and per-mode logs PASS; back to the picker after RetroArch, no handover after "
|
|
"an update; game mode requests, cores, refusals and results PASS");
|
|
return 0;
|
|
}
|