feat(playgo): accept a chunk id list for dumps without metadata

This commit is contained in:
Eduardo Ruiz committed 2026-10-31 00:06:25 +01:00
1 parent 6b48aee5d2
commit 3cb12a6210
6 files changed
+127 -8

No files matched your search

+34 -4
View File
@@ -75,6 +75,30 @@ static void RejectUnreadablePlayGoFiles() {
}
}
static bool ReadExplicitChunks(ChunkTable& table) {
const char* text = std::getenv("APS5_PLAYGO_UNLISTED_CHUNKS");
if (text == nullptr || text[0] == '\0') return false;
const std::string value(text);
if (value == "installed" || value == "absent") return false;
std::size_t start = 0;
while (start <= value.size()) {
const auto end = value.find(',', start);
const auto token = value.substr(start, end == std::string::npos ? std::string::npos : end - start);
try {
std::size_t consumed = 0;
const auto parsed = std::stoul(token, &consumed);
if (token.empty() || consumed != token.size() || parsed > 0xFFFF) throw std::invalid_argument("chunk");
table.ids.insert(static_cast<uint16_t>(parsed));
} catch (const std::exception&) {
throw std::runtime_error("scePlayGo: APS5_PLAYGO_UNLISTED_CHUNKS must be installed, absent, or a comma-separated list of chunk ids from 0 to 65535, not \"" + value + "\"");
}
if (end == std::string::npos) break;
start = end + 1;
}
table.defined = true;
return true;
}
static const ChunkTable& Chunks() {
static const ChunkTable chunks = [] {
ChunkTable table;
@@ -82,8 +106,12 @@ static const ChunkTable& Chunks() {
if (!file) {
if (ReadPackageChunks(table)) return table;
table.scenario = std::ifstream(ResolvePath_nid_no_patch("/app0/sce_sys/playgo-scenario.json")).good();
if (!table.scenario) RejectUnreadablePlayGoFiles();
else for (uint32_t id = 1; id < PLAYGO_SCENARIO_CHUNK_COUNT; ++id) table.ids.insert(static_cast<uint16_t>(id));
if (table.scenario) {
for (uint32_t id = 1; id < PLAYGO_SCENARIO_CHUNK_COUNT; ++id) table.ids.insert(static_cast<uint16_t>(id));
return table;
}
RejectUnreadablePlayGoFiles();
ReadExplicitChunks(table);
return table;
}
table.defined = true;
@@ -112,7 +140,9 @@ static UnlistedChunkAnswer UnlistedChunks() {
const std::string value(text);
if (value == "installed") return UnlistedChunkAnswer::Installed;
if (value == "absent") return UnlistedChunkAnswer::Absent;
throw std::runtime_error("scePlayGo: APS5_PLAYGO_UNLISTED_CHUNKS must be installed or absent, not \"" + value + "\"");
ChunkTable explicitChunks;
ReadExplicitChunks(explicitChunks);
return UnlistedChunkAnswer::Stop;
}();
return answer;
}
@@ -128,7 +158,7 @@ static bool IsKnownChunk(uint16_t id) {
}
if (chunks.scenario)
throw std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat is missing and sce_sys/playgo-scenario.json lists no chunks, so chunk ID " + std::to_string(id) + " outside the listed ids 0 to " + std::to_string(PLAYGO_SCENARIO_CHUNK_COUNT - 1) + " cannot be checked; set APS5_PLAYGO_UNLISTED_CHUNKS=installed or =absent to answer for this dump");
throw std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat and sce_sys/playgo-scenario.json are missing, so chunk ID " + std::to_string(id) + " cannot be checked; set APS5_PLAYGO_UNLISTED_CHUNKS=installed or =absent to answer for this dump");
throw std::runtime_error("scePlayGo: sce_sys/playgo-chunk.dat and sce_sys/playgo-scenario.json are missing, so chunk ID " + std::to_string(id) + " cannot be checked; set APS5_PLAYGO_UNLISTED_CHUNKS=installed, =absent, or a comma-separated list of chunk ids to answer for this dump");
}
static int ValidateChunks(const uint16_t* chunk_ids, uint32_t number_of_entries) {
+88 -1
View File
@@ -291,6 +291,79 @@ static int CheckNoPlayGoFileAnswered(const char* root, const char* answer) {
return 0;
}
static int CheckNoPlayGoFileExplicit(const char* root, const char* value) {
constexpr int badChunkId = static_cast<int>(0x80B2000C);
SetUnlistedChunks(value);
std::filesystem::current_path(root);
PlayGoInitParams init{};
int handle = 0;
Require(scePlayGoInitialize(&init) == 0);
Require(scePlayGoOpen(&handle, nullptr) == 0);
const std::uint16_t listed[2] = {0, 1};
std::int8_t loci[2] = {0, 0};
Require(scePlayGoGetLocus(handle, listed, 2, loci) == 0 && loci[0] == 3 && loci[1] == 3);
const std::uint16_t unlisted[2] = {2, 999};
Require(scePlayGoGetLocus(handle, unlisted, 2, loci) == badChunkId);
std::uint32_t chunkCount = 0;
Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 2);
std::uint16_t ids[2] = {0xFFFF, 0xFFFF};
Require(scePlayGoGetChunkId(handle, ids, 2, &chunkCount) == 0 && chunkCount == 2 && ids[0] == 0 && ids[1] == 1);
PlayGoProgress progress{};
Require(scePlayGoGetProgress(handle, listed, 2, &progress) == 0 && progress.progress_size == 1 && progress.total_size == 1);
Require(scePlayGoGetProgress(handle, unlisted, 2, &progress) == badChunkId);
Require(scePlayGoClose(handle) == 0);
return 0;
}
static int CheckNoPlayGoFileExplicitInvalid(const char* root, const char* value) {
SetUnlistedChunks(value);
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("APS5_PLAYGO_UNLISTED_CHUNKS must be installed, absent, or a comma-separated list") != std::string_view::npos;
}
Require(threw);
return 0;
}
static int CheckPackageChunkTableExplicit(const char* root) {
SetUnlistedChunks("0,1");
return CheckPackageChunkTable(root);
}
static int CheckScenarioWithoutChunkTableExplicit(const char* root) {
constexpr int badChunkId = static_cast<int>(0x80B2000C);
SetUnlistedChunks("0,1");
std::filesystem::current_path(root);
PlayGoInitParams init{};
int handle = 0;
Require(scePlayGoInitialize(&init) == 0);
Require(scePlayGoOpen(&handle, nullptr) == 0);
const std::uint16_t listed[2] = {0, 999};
std::int8_t loci[2] = {0, 0};
Require(scePlayGoGetLocus(handle, listed, 2, loci) == 0 && loci[0] == 3 && loci[1] == 3);
std::uint32_t chunkCount = 0;
Require(scePlayGoGetChunkId(handle, nullptr, 0, &chunkCount) == 0 && chunkCount == 1000);
const std::uint16_t unlisted = 1000;
bool threw = false;
try {
scePlayGoGetLocus(handle, &unlisted, 1, loci);
} catch (const std::runtime_error& error) {
threw = std::string_view(error.what()).find("APS5_PLAYGO_UNLISTED_CHUNKS=installed or =absent") != std::string_view::npos;
}
Require(threw);
Require(scePlayGoClose(handle) == 0);
return 0;
}
static int CheckScenarioWithoutChunkTableAnswered(const char* root, const char* answer) {
constexpr int badChunkId = static_cast<int>(0x80B2000C);
SetUnlistedChunks(answer);
@@ -326,7 +399,7 @@ static int CheckUnknownUnlistedChunkAnswer(const char* root) {
scePlayGoGetLocus(handle, &unlisted, 1, &locus);
} catch (const std::runtime_error& error) {
threw = std::string_view(error.what()).find("APS5_PLAYGO_UNLISTED_CHUNKS must be installed or absent") != std::string_view::npos;
threw = std::string_view(error.what()).find("APS5_PLAYGO_UNLISTED_CHUNKS must be installed, absent, or a comma-separated list") != std::string_view::npos;
}
Require(threw);
return 0;
@@ -362,7 +435,14 @@ int main(int argc, char** argv) {
if (mode == "--no-file-installed") return CheckNoPlayGoFileAnswered(argv[2], "installed");
if (mode == "--no-file-absent") return CheckNoPlayGoFileAnswered(argv[2], "absent");
if (mode == "--no-file-unknown-answer") return CheckUnknownUnlistedChunkAnswer(argv[2]);
if (mode == "--no-file-explicit") return CheckNoPlayGoFileExplicit(argv[2], "0,1");
if (mode == "--no-file-explicit-one") return CheckNoPlayGoFileExplicit(argv[2], "1");
if (mode == "--no-file-explicit-empty") return CheckNoPlayGoFileExplicitInvalid(argv[2], "0,");
if (mode == "--no-file-explicit-range") return CheckNoPlayGoFileExplicitInvalid(argv[2], "65536");
if (mode == "--no-file-explicit-text") return CheckNoPlayGoFileExplicitInvalid(argv[2], "0,x");
if (mode == "--package-chunk-table" || mode == "--scenario-with-chunk-table") return CheckPackageChunkTable(argv[2]);
if (mode == "--package-chunk-table-explicit") return CheckPackageChunkTableExplicit(argv[2]);
if (mode == "--scenario-only-explicit") return CheckScenarioWithoutChunkTableExplicit(argv[2]);
if (mode == "--scenario-only") return CheckScenarioWithoutChunkTable(argv[2]);
if (mode == "--scenario-only-installed") return CheckScenarioWithoutChunkTableAnswered(argv[2], "installed");
if (mode == "--scenario-only-absent") return CheckScenarioWithoutChunkTableAnswered(argv[2], "absent");
@@ -374,7 +454,14 @@ int main(int argc, char** argv) {
RunLayout(argv[0], "--no-file-installed", {});
RunLayout(argv[0], "--no-file-absent", {});
RunLayout(argv[0], "--no-file-unknown-answer", {});
RunLayout(argv[0], "--no-file-explicit", {});
RunLayout(argv[0], "--no-file-explicit-one", {});
RunLayout(argv[0], "--no-file-explicit-empty", {});
RunLayout(argv[0], "--no-file-explicit-range", {});
RunLayout(argv[0], "--no-file-explicit-text", {});
RunLayout(argv[0], "--package-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}});
RunLayout(argv[0], "--package-chunk-table-explicit", {{"playgo-chunk.dat", ChunkTableFile()}});
RunLayout(argv[0], "--scenario-only-explicit", {{"playgo-scenario.json", "{}"}});
RunLayout(argv[0], "--package-chunk-table", {{"playgo-chunk.dat", ChunkTableFile("plgo")}});
RunLayout(argv[0], "--scenario-with-chunk-table", {{"playgo-chunk.dat", ChunkTableFile()}, {"playgo-scenario.json", "{}"}});
std::int64_t eta = -1;
+1 -1
View File
@@ -550,7 +550,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti
| Name | Kind | Source | Default | Description |
|------|------|--------|---------|-------------|
| `APS5_PLAYGO_UNLISTED_CHUNKS` | string | `core/libs/prx/libScePlayGo/Export.cpp` | | answer for a chunk ID the playgo manifest does not list: installed or absent; unset throws instead of answering |
| `APS5_PLAYGO_UNLISTED_CHUNKS` | string | `core/libs/prx/libScePlayGo/Export.cpp` | | answer for a chunk ID the playgo manifest does not list: installed, absent, or a comma-separated list of installed chunk ids; unset throws instead of answering |
### libSceSaveData.native
+1 -1
View File
@@ -95,7 +95,7 @@ cmd /c '.\app.exe > run.log 2>&1'
## Title notes
- **PlayGo:** dumps without PlayGo metadata stop at start unless `APS5_PLAYGO_UNLISTED_CHUNKS` is set; report the value used.
- **PlayGo:** dumps without PlayGo metadata stop at start unless `APS5_PLAYGO_UNLISTED_CHUNKS` is set (installed, absent, or a comma-separated list of installed chunk ids); report the value used.
- **UE4 titles:** keep the startup movies in place. Renaming them to skip the intro has made a level render without its 3D view, which looked like an emulator bug and wasn't.
## Input
+1
View File
@@ -6,5 +6,6 @@
- [scePlayGoRequestOptionalChunk, scePlayGoGetRequiredDisc](../../../core/libs/prx/libScePlayGo/Export.cpp) (libScePlayGo) - the package's scenario count is not read: scenario (type 1) bits 0 to 4 are accepted for every package and bits from 5 up return `SCE_PLAYGO_ERROR_BAD_SCENARIO_CONFIGURATION`, and `scePlayGoGetSupportedOptionalChunk` and `scePlayGoGetOptionalChunk` report bits 0 to 4 for type 1 too. `scePlayGoGetRequiredDisc` always reports disc 0. Only `scePlayGoSetInstallSpeed2` checks initialization, and unlike both sources `scePlayGoInitialize` accepts a null or small buffer and a second call, and `scePlayGoTerminate` succeeds without it. These checks follow [pgo_stub](https://github.com/drakmor/pgo_stub/blob/c95dec777563/library.cpp#L559-L596) and [rps5e](https://github.com/keklick1337/rps5e/blob/7028192c3246/crates/ps5hle/src/playgo.rs#L313-L344), which may not be independent; none is measured
- [scePlayGoSetLanguageMask](../../../core/libs/prx/libScePlayGo/Export.cpp) (libScePlayGo) - stores the mask that `scePlayGoGetLanguageMask` then reports, and returns `SCE_PLAYGO_ERROR_BAD_HANDLE` for another handle, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/8d50471b7dc141dd700b51276b8edd1f293bce26/src/core/libraries/playgo/playgo.cpp#L346-L359) does on the PS4. shadPS4 also returns `SCE_PLAYGO_ERROR_NOT_INITIALIZED` before `scePlayGoInitialize` and starts from the system language's mask; here the mask starts with every language and initialization is not tracked. Not measured on a PS5
- [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 ids 0 to 999 as installed, and `scePlayGoGetChunkId` lists the same ids. A title that builds its set of installed chunks from that list skips any chunk it does not see. The console's chunk limit is not known here, so an id from 1000 up is answered like an unlisted id of a package with no `playgo-*` file: it throws unless `APS5_PLAYGO_UNLISTED_CHUNKS` answers it (`=absent` ends PPSA02630's id walk, which otherwise asks for every id until one is rejected and wraps the 16-bit id). A package with no `playgo-*` file in `sce_sys` (PPSA03001, PPSA03490 and PPSA01285 here) accepts chunk 0; querying any other id throws with a message naming the missing `playgo-chunk.dat` and `playgo-scenario.json`, since the incomplete dump cannot establish whether that id exists; `APS5_PLAYGO_UNLISTED_CHUNKS=installed` answers every such id as installed (`LOCAL_FAST`, which PPSA15388 needs to load its saves) and `=absent` answers `SCE_PLAYGO_ERROR_BAD_CHUNK_ID` (which ends PPSA03001's id walk), as the user's choice for that dump. 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. These answers are unverified on a console, as is the console's answer for an id outside a package's chunk table.
- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) (libScePlayGo) - when the dump has no `playgo-chunk.dat` or `playgo-scenario.json`, `APS5_PLAYGO_UNLISTED_CHUNKS` may list installed chunk ids (for example `0,1`); the listed ids plus chunk 0 become the chunk set and every other unlisted id is absent. The dump cannot say which ids the title expects; no console run verifies the user's choice.
- [libScePlayGo](../../../core/libs/prx/libScePlayGo/Export.cpp) - `scePlayGoGetSupportedOptionalChunk` and `scePlayGoGetOptionalChunk` report every bit set for `type` 0 or 2 and bits 0 to 4 for `type` 1, i.e. every optional chunk (type 0 languages, type 1 scenarios) supported and installed, like `scePlayGoGetLanguageMask`. Native requests confirm types 0 through 2 and an eight-byte result; their meanings and bit layout are still determined by the external service, not this module; PPSA01748 calls both with type 0 and hides every text language listed in `playgo-scenario.json` `chunkSupportedLanguages` when the bitmask is 0. The console's answer for a package that ships no optional chunks is unverified.
- [scePlayGoPrefetchOptionalChunk](../../../core/libs/prx/libScePlayGo/Export.cpp) (libScePlayGo) - checks only the handle and returns 0, ignoring `type` and the option. Unlike `scePlayGoGetOptionalChunk` and `scePlayGoGetSupportedOptionalChunk`, it rejects neither a null option nor a type above 2. With every optional chunk reported installed, there is nothing to prefetch. Borderlands 4 (PPSA01494), MXGP 26 (PPSA30839) and Super Meat Boy 3D (PPSA31162) each call it from two wrappers, one with type 0 and one with type 1, passing a stack copy of an eight-byte option and returning the result to their caller (read statically from each `eboot.bin`). pgo_stub and rps5e reject a null option, a type above 2 and scenario bits from 5 up here as in `scePlayGoRequestOptionalChunk`; that is not applied, and the console's answer is unverified.
+2 -1
View File
@@ -362,8 +362,9 @@ When a dump has neither `sce_sys/playgo-chunk.dat` nor `sce_sys/playgo-scenario.
|-------|---------------------------------|------------------------------|
| `installed` | Report it as installed (`LOCAL_FAST`). | High on Life (PPSA15388) needs this to load its saves. |
| `absent` | Return `SCE_PLAYGO_ERROR_BAD_CHUNK_ID`. | Sifu (PPSA03001) and Demon Slayer: Kimetsu no Yaiba - The Hinokami Chronicles (PPSA03490) use this result to end their chunk-ID search. With `installed`, PPSA03490 keeps asking about higher chunk IDs forever, so its chunk list grows until startup fails with a `Ran out of memory allocating 5565054976 bytes` error. |
| A comma-separated list of IDs, e.g. `0,1` | Report those IDs as installed (`LOCAL_FAST`) and every other unlisted ID as absent (`SCE_PLAYGO_ERROR_BAD_CHUNK_ID`). | The Medium (PPSA03718) needs chunks 0 and 1; with `installed` it asks about every ID and UE4 runs out of memory. |
Leave the variable unset to keep the explicit error. Any other value also stops with an error. Dumps that include PlayGo metadata are unaffected. These title-specific results come from emulator runs and have not been verified against a console.
Leave the variable unset to keep the explicit error. A list holds decimal chunk IDs from 0 to 65535 and is added to chunk 0; it only applies when the dump has neither `playgo-chunk.dat` nor `playgo-scenario.json`. Any other value also stops with an error. Dumps that include `playgo-chunk.dat` or `playgo-scenario.json` are unaffected. These title-specific results come from emulator runs and have not been verified against a console.
### Screenshots