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
25 changes: 21 additions & 4 deletions core/libs/prx/libScePlayGo/Export.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <regex>
#include <set>
#include <stdexcept>
#include <string>

// The game is fully installed on the host, so every chunk is local and nothing is pending.
Expand All @@ -28,27 +30,42 @@ static constexpr int SCE_PLAYGO_ERROR_BAD_LOCUS = static_cast<int>(0x80B20010);
struct ChunkTable {
std::set<uint16_t> 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<uint16_t>(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;
Expand All @@ -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) {
Expand Down
110 changes: 89 additions & 21 deletions core/libs/tests/GuestPlayGo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <initializer_list>
#include <random>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>

extern "C" {
int APS5_VABI scePlayGoInitialize(const PlayGoInitParams*);
Expand Down Expand Up @@ -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<int>(0x80B20009);
constexpr int badPointer = static_cast<int>(0x80B2000A);
constexpr int badSize = static_cast<int>(0x80B2000B);
constexpr int badLocus = static_cast<int>(0x80B20010);
constexpr int badChunkId = static_cast<int>(0x80B2000C);

PlayGoInitParams init{};
int handle = 0;
Expand All @@ -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);
Expand All @@ -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<std::pair<const char*, std::string>> 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<std::streamsize>(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;
}
2 changes: 1 addition & 1 deletion docs/dev/debt/libScePlayGo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading