From 51c7206fd0ab6cb291ff9317280cda8ca211f5cc Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 13 Oct 2026 11:09:21 +0200 Subject: [PATCH] fix(libScePlayGo): answer chunk ids from the package layout A package with no playgo-* file in sce_sys has no PlayGo data, so only chunk 0 is valid and any other id returns SCE_PLAYGO_ERROR_BAD_CHUNK_ID. A package with playgo-scenario.json but no chunk table (PPSA01748) keeps accepting every id. A chunk table (playgo-chunk.dat or playgo-chunkdefs.xml) still decides when it exists. A sce_sys that holds other playgo-* files without a chunk table or a scenario file, or a playgo-chunk.dat that is not a plgx table with a nonzero count, throws naming the file, since no such dump is known. Without this a title that walks the ids until the first error (Sifu PPSA03001) never gets the error. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libScePlayGo/Export.cpp | 25 +++++- core/libs/tests/GuestPlayGo.cpp | 110 +++++++++++++++++++++----- docs/dev/debt/libScePlayGo.md | 2 +- 3 files changed, 111 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libScePlayGo/Export.cpp b/core/libs/prx/libScePlayGo/Export.cpp index bf70cc09d61..64420b32d33 100644 --- a/core/libs/prx/libScePlayGo/Export.cpp +++ b/core/libs/prx/libScePlayGo/Export.cpp @@ -3,10 +3,12 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include +#include #include #include #include #include +#include #include // The game is fully installed on the host, so every chunk is local and nothing is pending. @@ -28,27 +30,42 @@ static constexpr int SCE_PLAYGO_ERROR_BAD_LOCUS = static_cast(0x80B20010); struct ChunkTable { std::set ids{0}; bool defined = false; + bool scenario = false; }; static bool ReadPackageChunks(ChunkTable& table) { constexpr std::size_t CountOffset = 0x0A; std::ifstream file(ResolvePath_nid_no_patch("/app0/sce_sys/playgo-chunk.dat"), std::ios::binary); char header[CountOffset + sizeof(uint16_t)]{}; - if (!file.read(header, sizeof(header)) || std::memcmp(header, "plgx", 4) != 0) return false; + if (!file) return false; + const auto unreadable = [] { return std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat is not a chunk table this library reads (expected a plgx header with a nonzero chunk count)"); }; + if (!file.read(header, sizeof(header)) || std::memcmp(header, "plgx", 4) != 0) throw unreadable(); uint16_t count = 0; std::memcpy(&count, header + CountOffset, sizeof(count)); - if (count == 0) return false; + if (count == 0) throw unreadable(); for (uint32_t id = 0; id < count; ++id) table.ids.insert(static_cast(id)); table.defined = true; return true; } +static void RejectUnreadablePlayGoFiles() { + std::error_code error; + const std::filesystem::path directory = ResolvePath_nid_no_patch("/app0/sce_sys"); + for (const auto& entry : std::filesystem::directory_iterator(directory, error)) { + const auto name = entry.path().filename().string(); + if (name.rfind("playgo-", 0) == 0) + throw std::runtime_error("scePlayGo: sce_sys/" + name + " is present but no chunk table (playgo-chunk.dat) or scenario file (playgo-scenario.json) is, so the chunk ids are unknown"); + } +} + static const ChunkTable& Chunks() { static const ChunkTable chunks = [] { ChunkTable table; std::ifstream file(ResolvePath_nid_no_patch("/app0/playgo-chunkdefs.xml")); if (!file) { - ReadPackageChunks(table); + if (ReadPackageChunks(table)) return table; + table.scenario = std::ifstream(ResolvePath_nid_no_patch("/app0/sce_sys/playgo-scenario.json")).good(); + if (!table.scenario) RejectUnreadablePlayGoFiles(); return table; } table.defined = true; @@ -70,7 +87,7 @@ static const ChunkTable& Chunks() { static bool IsKnownChunk(uint16_t id) { const auto& chunks = Chunks(); - return !chunks.defined || chunks.ids.contains(id); + return chunks.defined ? chunks.ids.contains(id) : chunks.scenario || id == 0; } static int ValidateChunks(const uint16_t* chunk_ids, uint32_t number_of_entries) { diff --git a/core/libs/tests/GuestPlayGo.cpp b/core/libs/tests/GuestPlayGo.cpp index 2cea5041e18..cf93ef9eb2a 100644 --- a/core/libs/tests/GuestPlayGo.cpp +++ b/core/libs/tests/GuestPlayGo.cpp @@ -4,9 +4,12 @@ #include #include #include +#include #include +#include #include #include +#include extern "C" { int APS5_VABI scePlayGoInitialize(const PlayGoInitParams*); @@ -39,31 +42,30 @@ static int CheckPackageChunkTable(const char* root) { return 0; } -static void RunWithPackageChunkTable(const char* self) { - const auto root = std::filesystem::temp_directory_path() / ("guest_playgo_" + std::to_string(std::random_device{}())); - std::filesystem::remove_all(root); - std::filesystem::create_directories(root / "app0" / "sce_sys"); - char header[0x100] = {'p', 'l', 'g', 'x'}; - header[0x0A] = 3; - std::ofstream(root / "app0" / "sce_sys" / "playgo-chunk.dat", std::ios::binary).write(header, sizeof(header)); - const std::string arguments = "\"" + std::string(self) + "\" --package-chunk-table \"" + root.string() + "\""; -#ifdef _WIN32 - const std::string command = "\"" + arguments + "\""; -#else - const std::string command = arguments; -#endif - const int status = std::system(command.c_str()); - std::filesystem::remove_all(root); - Require(status == 0); +static int CheckScenarioWithoutChunkTable(const char* root) { + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t anyIds[3] = {1, 10, 0xFFFF}; + std::int8_t loci[3] = {0, 0, 0}; + Require(scePlayGoGetLocus(handle, anyIds, 3, loci) == 0 && loci[0] == 3 && loci[1] == 3 && loci[2] == 3); + const PlayGoToDo todo[2] = {{0, 3, 0}, {0xFFFF, 3, 0}}; + Require(scePlayGoSetToDoList(handle, todo, 2) == 0); + std::uint32_t chunkCount = 0; + Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1); + Require(scePlayGoClose(handle) == 0); + return 0; } -int main(int argc, char** argv) { - if (argc == 3 && std::string_view(argv[1]) == "--package-chunk-table") return CheckPackageChunkTable(argv[2]); - RunWithPackageChunkTable(argv[0]); +static int CheckNoPlayGoFile(const char* root) { + std::filesystem::current_path(root); constexpr int badHandle = static_cast(0x80B20009); constexpr int badPointer = static_cast(0x80B2000A); constexpr int badSize = static_cast(0x80B2000B); constexpr int badLocus = static_cast(0x80B20010); + constexpr int badChunkId = static_cast(0x80B2000C); PlayGoInitParams init{}; int handle = 0; @@ -84,8 +86,10 @@ int main(int argc, char** argv) { Require(scePlayGoSetToDoList(handle, todo, 2) == 0); todo[1].locus = 1; Require(scePlayGoSetToDoList(handle, todo, 2) == badLocus); + todo[1] = {1, 3, 0}; + Require(scePlayGoSetToDoList(handle, todo, 2) == badChunkId); todo[1] = {0xFFFF, 3, 0}; - Require(scePlayGoSetToDoList(handle, todo, 2) == 0); + Require(scePlayGoSetToDoList(handle, todo, 2) == badChunkId); std::uint32_t entries = 1; Require(scePlayGoGetToDoList(handle, todo, 2, &entries) == 0 && entries == 0); @@ -95,9 +99,73 @@ int main(int argc, char** argv) { Require(scePlayGoGetLocus(handle, &chunk, 1, &locus) == 0 && locus == 3); const std::uint16_t undeclared[2] = {1, 10}; std::int8_t loci[2] = {0, 0}; - Require(scePlayGoGetLocus(handle, undeclared, 2, loci) == 0 && loci[0] == 3 && loci[1] == 3); + Require(scePlayGoGetLocus(handle, undeclared, 2, loci) == badChunkId); + const std::uint16_t onlyDeclared[2] = {0, 0}; + Require(scePlayGoGetLocus(handle, onlyDeclared, 2, loci) == 0 && loci[0] == 3 && loci[1] == 3); std::uint32_t chunkCount = 0; Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1); Require(scePlayGoClose(handle + 1) == badHandle); Require(scePlayGoClose(handle) == 0); + return 0; +} + +static int CheckRejectedLayout(const char* root, const char* fileName) { + std::filesystem::current_path(root); + PlayGoInitParams init{}; + int handle = 0; + Require(scePlayGoInitialize(&init) == 0); + Require(scePlayGoOpen(&handle, nullptr) == 0); + const std::uint16_t chunk = 0; + std::int8_t locus = 0; + bool threw = false; + try { + scePlayGoGetLocus(handle, &chunk, 1, &locus); + } catch (const std::runtime_error& error) { + threw = std::string_view(error.what()).find(fileName) != std::string_view::npos; + } + Require(threw); + return 0; +} + +static void RunLayout(const char* self, const char* mode, std::initializer_list> files) { + const auto root = std::filesystem::temp_directory_path() / ("guest_playgo_" + std::to_string(std::random_device{}())); + std::filesystem::remove_all(root); + std::filesystem::create_directories(root / "app0" / "sce_sys"); + for (const auto& [name, content] : files) std::ofstream(root / "app0" / "sce_sys" / name, std::ios::binary).write(content.data(), static_cast(content.size())); + const std::string arguments = "\"" + std::string(self) + "\" " + mode + " \"" + root.string() + "\""; +#ifdef _WIN32 + const std::string command = "\"" + arguments + "\""; +#else + const std::string command = arguments; +#endif + const int status = std::system(command.c_str()); + std::filesystem::remove_all(root); + Require(status == 0); +} + +static std::string ChunkTableFile() { + std::string header(0x100, '\0'); + header.replace(0, 4, "plgx"); + header[0x0A] = 3; + return header; +} + +int main(int argc, char** argv) { + if (argc == 3) { + const std::string_view mode = argv[1]; + if (mode == "--no-file") return CheckNoPlayGoFile(argv[2]); + if (mode == "--package-chunk-table" || mode == "--scenario-with-chunk-table") return CheckPackageChunkTable(argv[2]); + if (mode == "--scenario-only") return CheckScenarioWithoutChunkTable(argv[2]); + if (mode == "--ficm-only") return CheckRejectedLayout(argv[2], "playgo-ficm.dat"); + if (mode == "--hash-table-only") return CheckRejectedLayout(argv[2], "playgo-hash-table.dat"); + if (mode == "--invalid-chunk-table") return CheckRejectedLayout(argv[2], "playgo-chunk.dat"); + } + RunLayout(argv[0], "--no-file", {}); + RunLayout(argv[0], "--package-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}}); + RunLayout(argv[0], "--scenario-with-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}, {"playgo-scenario.json", "{}"}}); + RunLayout(argv[0], "--scenario-only", {{"playgo-scenario.json", "{}"}}); + RunLayout(argv[0], "--ficm-only", {{"playgo-ficm.dat", "x"}}); + RunLayout(argv[0], "--hash-table-only", {{"playgo-hash-table.dat", "x"}}); + RunLayout(argv[0], "--invalid-chunk-table", {{"playgo-chunk.dat", "not a chunk table"}}); + return 0; } diff --git a/docs/dev/debt/libScePlayGo.md b/docs/dev/debt/libScePlayGo.md index 3a13df5e033..ecf89fb032c 100644 --- a/docs/dev/debt/libScePlayGo.md +++ b/docs/dev/debt/libScePlayGo.md @@ -2,4 +2,4 @@ ### Unknown function info -- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file every chunk id is accepted while `scePlayGoGetChunkId` lists only chunk 0; the console's answer for an id outside the package's chunk table is not checked. +- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - the chunk set comes from `playgo-chunkdefs.xml` (every listed chunk plus chunks 0 through the default chunk), otherwise from `sce_sys/playgo-chunk.dat`, whose `plgx` header is read as holding the chunk count as a 16-bit value at offset 0x0A and ids 0 to count - 1 are taken as valid. The field was identified on four dumps, where exactly that many 0x20-byte entries follow from offset 0x100 (RIDE 4 PPSA01599: 35, Persona 5 Royal PPSA05110: 25, LEGO Horizon Adventures PPSA14632 and Kena PPSA01802: 1); the rest of the format is not decoded and ids are assumed to be contiguous. Every listed chunk is reported as installed (`LOCAL_FAST`). Without either file, a package with `sce_sys/playgo-scenario.json` (PPSA01748, whose dump lists no chunks) accepts every chunk id as installed while `scePlayGoGetChunkId` lists only chunk 0, and a package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts only chunk 0 and returns `SCE_PLAYGO_ERROR_BAD_CHUNK_ID` for any other id; a `sce_sys` that holds other `playgo-*` files (`playgo-ficm.dat`, `playgo-hash-table.dat`) without `playgo-chunk.dat` or `playgo-scenario.json`, or a `playgo-chunk.dat` without a `plgx` header and a nonzero count, throws naming the file, because no such dump is known; the two answers above are unverified on a console, as is the console's answer for an id outside a package's chunk table.