Files
psxrecomp/runtime/tests/test_mod_runtime.cpp
T
Matt Stanley 13b7f81d29 Add guarded instruction variants fixed during offline mod activation
Keep selector zero stock and seal all choices until the next session restart. Refuse nonzero online activation, scope non-stock save states by frozen choices, and preserve original instruction guards when combined with effective-address adapters. Use overlay ABI34 on current master without the old menu dependency stack.
2026-10-19 16:37:32 -07:00

2295 lines
130 KiB
C++

#include "mod_runtime.h"
#include "mod_netplay.h"
#include "host_launch_timing.h"
#include "host_file_identity.h"
#include "mod_packages.h"
#include "mod_plugins.h"
#include "mod_immediate.h"
#include "psx_sha256.h"
#include "psx_lobby_client.h"
#include "gpu.h"
#include "gpu_hd_textures.h"
#include "cpu_state.h"
#include <array>
#include <csetjmp>
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <stdexcept>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
namespace fs = std::filesystem;
static std::array<uint8_t, 2 * 1024 * 1024> ram;
static int failures;
static int activation_calls;
static int plugin_calls;
static int restore_calls;
static int option_renderer_activations;
static void option_renderer_activate(void) { ++option_renderer_activations; }
static PsxLobbyMatchCaps test_match_caps;
extern "C" const PsxLobbyMatchCaps* psx_lobby_match_caps(void) {
return &test_match_caps;
}
static int game_netplay_entry_hits;
static int game_netplay_filter_hits;
static int test_netplay_active;
extern "C" int psx_netplay_active(void) { return test_netplay_active; }
static void test_game_netplay_entry(CPUState*, uint32_t) {
game_netplay_entry_hits++;
}
static int test_game_netplay_filter(CPUState* cpu, uint32_t) {
game_netplay_filter_hits++;
cpu->gpr[2] = 0x1234u;
return 1;
}
extern "C" uint8_t psx_read_byte(uint32_t address) {
return ram[address & 0x1fffffu];
}
extern "C" void psx_write_byte(uint32_t address, uint8_t value) {
ram[address & 0x1fffffu] = value;
}
extern "C" uint16_t psx_read_half(uint32_t address) {
const uint32_t offset = address & 0x1fffffu;
return (uint16_t)(ram[offset] | ((uint16_t)ram[offset + 1] << 8));
}
extern "C" void psx_write_half(uint32_t address, uint16_t value) {
const uint32_t offset = address & 0x1fffffu;
ram[offset] = (uint8_t)value;
ram[offset + 1] = (uint8_t)(value >> 8);
}
/* Side-effect-free peek (memory.c); host-output observation reads. */
extern "C" int psx_peek_word(uint32_t, uint32_t* out) { *out = 0; return 0; }
extern "C" uint32_t psx_read_word(uint32_t address) {
const uint32_t offset = address & 0x1fffffu;
return (uint32_t)ram[offset] |
((uint32_t)ram[offset + 1] << 8) |
((uint32_t)ram[offset + 2] << 16) |
((uint32_t)ram[offset + 3] << 24);
}
extern "C" void psx_write_word(uint32_t address, uint32_t value) {
const uint32_t offset = address & 0x1fffffu;
ram[offset] = (uint8_t)value;
ram[offset + 1] = (uint8_t)(value >> 8);
ram[offset + 2] = (uint8_t)(value >> 16);
ram[offset + 3] = (uint8_t)(value >> 24);
}
/* Mod writes are host stores (memory.c psx_host_write_*). */
extern "C" void psx_host_write_byte(uint32_t address, uint8_t value) {
psx_write_byte(address, value);
}
extern "C" void psx_host_write_half(uint32_t address, uint16_t value) {
psx_write_half(address, value);
}
extern "C" void psx_host_write_word(uint32_t address, uint32_t value) {
psx_write_word(address, value);
}
extern "C" uint32_t psx_mod_memory_alloc(uint32_t, uint32_t) { return 0; }
extern "C" uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t, uint32_t) {
return 0;
}
extern "C" void psx_ram_reset_size_request(void) {}
static int hd_shutdown_calls, hd_configure_calls, hd_dump_value;
static int hd_replacements_value, hd_reload_calls;
static std::string hd_root;
extern "C" void gpu_hd_textures_shutdown(void) { ++hd_shutdown_calls; hd_root.clear(); }
extern "C" int gpu_hd_textures_configure(const char* root, int replacements, int dump,
char*, size_t) {
++hd_configure_calls;
hd_root = root;
hd_replacements_value = replacements;
hd_dump_value = dump;
return 1;
}
extern "C" void gpu_hd_textures_set_dump_enabled(int enabled) {
hd_dump_value = enabled;
}
extern "C" int gpu_hd_textures_active(void) {
return !hd_root.empty() && (hd_replacements_value || hd_dump_value);
}
extern "C" void gpu_hd_textures_get_diag(GpuHdTextureDiag* out) {
*out = {};
out->root = hd_root.c_str();
}
extern "C" int gpu_hd_textures_reload(char*, size_t) { ++hd_reload_calls; return 1; }
static void test_hd_activation(void) {
char replacements[16], dump[16];
if (!psx_mod_current_option_value("replacements", replacements, sizeof(replacements)) ||
!psx_mod_current_option_value("dump", dump, sizeof(dump))) { ++failures; return; }
if (!psx_mod_set_hd_texture_pack("pack", std::string(replacements) == "true",
std::string(dump) == "true")) ++failures;
}
extern "C" void psx_projection_reset_session(void) {}
extern "C" void gpu_ws_set_native_scene_predicate(int (*)(void)) {}
extern "C" int psx_ws_x_margin(void) { return 0; }
/* Stand-in for the GPU's display geometry. psx_mod_display_width/height must
* report exactly what the presenter reports -- a plugin drawing an overlay
* uses this as the screen edge, so a substituted or rounded value would put
* HUD elements in the wrong place. */
static GpuDisplayInfo g_test_display;
extern "C" void gpu_get_display_info(GpuDisplayInfo* out) {
*out = g_test_display;
}
extern "C" void dirty_ram_mark_executable_range(uint32_t, uint32_t) {}
extern "C" int fntrace_is_game_started(void) { return 1; }
/* Widescreen tag passthroughs mod_runtime forwards to the GPU; unused here. */
extern "C" void gpu_ws_tag_hud_primitive(uint32_t, int) {}
extern "C" void gpu_ws_tag_world_primitive(uint32_t, int) {}
extern "C" void gpu_ws_set_adaptive_backdrop_preload(int) {}
extern "C" int gpu_ws_configured_x_reveal(void) { return 0; }
extern "C" void gpu_ws_tag_hud_prim(uint32_t, int) {}
extern "C" void gpu_ws_tag_screen_mask_quad(uint32_t) {}
extern "C" void gpu_ws_tag_radial_screen_mask_quad(uint32_t, float) {}
extern "C" void gpu_ws_tag_feedback_rect(uint32_t) {}
static CPUState* interrupted_entry_cpu;
static void test_vblank_plugin(void) {
plugin_calls++;
if (interrupted_entry_cpu && psx_mod_finish_function(interrupted_entry_cpu)) failures++;
}
/* Function-entry hooks: one owned by the plan's active plugin, one by a
* plugin whose feature is disabled, one by an id no package selects. */
static int active_entry_hits;
static int disabled_entry_hits;
static int unselected_entry_hits;
static uint32_t active_entry_last;
static int entry_test_mode, nested_result;
static std::jmp_buf callback_escape;
static uint32_t nested_pc;
static void test_active_entry(CPUState* cpu, uint32_t address) {
active_entry_hits++;
active_entry_last = address;
if (entry_test_mode == 1) {
CPUState unrelated{};
if (psx_mod_finish_function(&unrelated) || !psx_mod_finish_function(cpu)) failures++;
cpu->gpr[2] = 0x12345678u;
} else if (entry_test_mode == 2 || entry_test_mode == 3) {
const int mode = entry_test_mode;
if (mode == 3 && !psx_mod_finish_function(cpu)) failures++;
CPUState nested{}; nested.gpr[31] = 0x80004000u;
entry_test_mode = mode == 2 ? 1 : 0;
nested_result = psx_mod_function_entry(&nested, address);
nested_pc = nested.pc;
entry_test_mode = mode;
} else if (entry_test_mode == 4) {
/* A native call made by a hook can cross a guest VBlank. Its
* callbacks must neither inherit nor discard the interrupted hook. */
interrupted_entry_cpu = cpu;
mod_runtime_on_vblank();
interrupted_entry_cpu = nullptr;
if (!psx_mod_finish_function(cpu)) failures++;
cpu->gpr[2] = 0x87654321u;
} else if (entry_test_mode == 5) {
/* Guest scheduling/RFE can abandon the callback's C++ scope. */
std::longjmp(callback_escape, 1);
} else if (entry_test_mode == 6) {
if (!psx_mod_finish_function(cpu)) failures++;
ModFunctionEntryContext interrupted{};
mod_runtime_function_entry_context_save(&interrupted);
if (setjmp(callback_escape) == 0) {
CPUState nested{};
entry_test_mode = 5;
psx_mod_function_entry(&nested, address);
failures++; /* the nested callback must escape */
}
mod_runtime_function_entry_context_restore(&interrupted);
if (!psx_mod_function_entry_active() || !psx_mod_finish_function(cpu)) failures++;
entry_test_mode = 6;
}
}
static void test_disabled_entry(CPUState*, uint32_t) { disabled_entry_hits++; }
static void test_unselected_entry(CPUState*, uint32_t) { unselected_entry_hits++; }
static bool filter_handles;
static bool filter_context_active;
static unsigned active_filter_hits;
static unsigned inactive_filter_hits;
static uint32_t active_filter_last;
static int test_active_filter(CPUState* cpu, uint32_t address) {
++active_filter_hits;
active_filter_last = address;
filter_context_active = psx_mod_function_entry_active() != 0;
if (!filter_handles) return 0;
cpu->gpr[2] = 42;
return 1;
}
static int test_inactive_filter(CPUState*, uint32_t) {
++inactive_filter_hits;
return 1;
}
static int guest_function_hits;
static int instruction_hits;
static int memory_hits;
static uint32_t memory_delta = 0x1000u;
static uint32_t test_memory_address(const CPUState*, uint32_t, uint32_t, uint32_t address) {
++memory_hits;
return address + memory_delta;
}
static void test_instruction(CPUState* cpu, uint32_t) {
instruction_hits++;
cpu->gpr[5] = 0x80301008u;
if (psx_mod_finish_function(cpu)) failures++;
}
static void test_guest_function(CPUState* cpu, uint32_t) {
guest_function_hits++;
const uint32_t ra = cpu->gpr[31];
CPUState nested{};
nested.gpr[31] = 0x80007000u;
const int mode = entry_test_mode;
entry_test_mode = 1;
if (!psx_mod_function_entry(&nested, 0x80003000u) || nested.pc != nested.gpr[31]) failures++;
entry_test_mode = mode;
interrupted_entry_cpu = cpu;
mod_runtime_on_vblank();
interrupted_entry_cpu = nullptr;
if (!psx_mod_finish_function(cpu)) failures++;
cpu->gpr[2] = cpu->gpr[4] + 7u;
if (cpu->gpr[31] != ra) failures++;
}
static void test_activation_plugin(void) {
activation_calls++;
}
static const uint8_t* media_snapshot;
static uint64_t media_size;
static int media_access, foreign_media_access;
static uint32_t mounted_lba;
static int extent_ok, invalid_extent, foreign_extent;
static int audio_ok, audio_invalid, audio_foreign, audio_data_rejected;
static uint32_t audio_lba, audio_lba2;
static void test_disc_audio_plugin() {
PSXModCDDATrack tracks[]={{nullptr,0,0,2},{nullptr,0,0,3}};
uint32_t starts[2]{},counts[2]{};
if(!psx_mod_append_cdda_tracks(tracks,2,starts,counts) ||
starts[0]!=150 || starts[1]!=153 || counts[0]!=3 || counts[1]!=2) failures++;
}
static int immediate_activation_choice, immediate_reset_accepted;
static void test_immediate_reset(void) {
immediate_reset_accepted = psx_mod_set_immediate_selector(1, 2);
}
static void test_media_plugin(void) {
if (immediate_activation_choice) psx_mod_set_immediate_selector(1, immediate_activation_choice);
media_access = psx_mod_current_resource_bytes("rom", &media_snapshot, &media_size);
const uint8_t* foreign = nullptr;
uint64_t size = 0;
foreign_media_access = psx_mod_current_resource_bytes("private", &foreign, &size);
extent_ok=psx_mod_append_disc_extent("xa",0,2,&mounted_lba);
uint32_t bad=99;
invalid_extent=psx_mod_append_disc_extent("xa",1,2,&bad);
foreign_extent=psx_mod_append_disc_extent("private",0,1,&bad);
PSXModCDDATrack sources[]={{"xa",0,1,0},{"xa",2320,1,0}};
uint32_t starts[2]{},counts[2]{};
audio_ok=psx_mod_append_cdda_tracks(sources,2,starts,counts);
audio_lba=starts[0];audio_lba2=starts[1];
sources[1].byte_offset=2321;
audio_invalid=psx_mod_append_cdda_tracks(sources,2,starts,counts);
if(starts[0] || counts[0]) failures++;
sources[1]={"private",0,1,0};
audio_foreign=psx_mod_append_cdda_tracks(sources,2,starts,counts);
sources[1]={nullptr,0,0,1};
audio_data_rejected=!psx_mod_append_cdda_tracks(sources,2,starts,counts);
}
extern "C" {
void* iso_open(const char*);
void iso_close(void*);
int iso_read_raw_sector(void*,uint32_t,uint8_t*,int);
int iso_read_sector(void*,uint32_t,uint8_t*,int);
int iso_read_subq(void*,uint32_t,uint8_t*,int,int*);
uint32_t iso_sector_count(void*);
int iso_track_count(void*);
uint32_t iso_track_start_lba(void*,int);
int iso_track_is_audio(void*,int);
int iso_cdda_track_count(void*);
uint32_t iso_cdda_track_start_lba(void*,int);
uint32_t iso_cdda_sector_count(void*);
int iso_cdda_track_is_audio(void*,int);
int iso_read_cdda_sector(void*,uint32_t,uint8_t*,int);
}
static int big_ram_activations;
static int option_changed_hits = 0;
static std::string option_changed_value;
static int option_changed_refuse = 0; /* 1 = the plugin answers "next start" */
static int test_option_changed(const char* option_id, const char* value) {
char cur[32];
option_changed_hits++;
/* Runs in the plugin's context: current_option_value answers the new value. */
if (std::string(option_id) == "size" &&
psx_mod_current_option_value("size", cur, sizeof(cur)) == 1)
option_changed_value = cur;
(void)value;
return option_changed_refuse ? 0 : 1;
}
static void test_big_ram_activation(void) {
big_ram_activations++;
}
static void test_restore_plugin(void) {
restore_calls++;
if (ram[0x1000] != 0xa1) failures++; /* main plan must be reapplied first */
}
static void check(bool value, const char* message) {
if (!value) {
std::cerr << "FAIL: " << message << "\n";
failures++;
}
}
static void write_text(const fs::path& path, const std::string& text) {
fs::create_directories(path.parent_path());
std::ofstream out(path);
out << text;
}
static void write_bytes(const fs::path& path, const std::vector<uint8_t>& bytes) {
fs::create_directories(path.parent_path());
std::ofstream out(path, std::ios::binary);
out.write((const char*)bytes.data(), (std::streamsize)bytes.size());
}
static std::string sha256_hex(const std::vector<uint8_t>& bytes) {
uint8_t digest[32];
psx_sha256_compute(bytes.data(), bytes.size(), digest);
static const char hex[] = "0123456789abcdef";
std::string out(64, '0');
for (size_t i = 0; i < 32; ++i) {
out[i * 2] = hex[digest[i] >> 4];
out[i * 2 + 1] = hex[digest[i] & 15];
}
return out;
}
static uint64_t launch_timing_total() {
uint64_t total = 0;
host_launch_timing_snapshot(nullptr, 0, &total, nullptr);
return total;
}
static void check_commit_timing(uint64_t before, bool ok, bool hashed) {
HostLaunchTimingEvent events[HOST_LAUNCH_TIMING_CAPACITY]{};
const auto n = host_launch_timing_snapshot(events, HOST_LAUNCH_TIMING_CAPACITY,
nullptr, nullptr);
const HostLaunchTimingEvent* commit = nullptr;
for (uint32_t i = 0; i < n; ++i)
if (events[i].seq > before && events[i].stage == HOST_LAUNCH_COMMIT)
commit = &events[i];
check(commit && commit->ok == unsigned(ok), "real commit outcome retained by C snapshot");
if (!commit) return;
uint32_t stages = 0;
for (uint32_t i = 0; i < n; ++i) {
const auto& e = events[i];
if (e.seq <= before || e.parent_id != commit->scope_id) continue;
stages |= 1u << e.stage;
check(e.start_us >= commit->start_us &&
e.start_us + e.duration_us <= commit->start_us + commit->duration_us,
"real commit child measurement nested inside total");
}
check(bool(stages & (1u << HOST_LAUNCH_DISC_HASH)) == hashed,
"disc hash timing follows actual digest reuse");
const uint32_t required = (1u << HOST_LAUNCH_PREPARE_RESOURCES) |
(1u << HOST_LAUNCH_RESOLVE);
check((stages & required) == required, "real preparation and resolution observed");
if (ok) {
const uint32_t final = (1u << HOST_LAUNCH_OVERLAY_VERIFY) |
(1u << HOST_LAUNCH_DERIVED_DISC) | (1u << HOST_LAUNCH_SAVE_STATE) |
(1u << HOST_LAUNCH_BUILD_DISC_INDEX);
check((stages & final) == final, "real successful commit final stages observed");
} else {
check(!(stages & (1u << HOST_LAUNCH_BUILD_DISC_INDEX)),
"rejected commit does not invent final-stage completion");
}
}
extern "C" int psx_mod_counters_snapshot(const char**, uint64_t*, uint64_t*, int, uint64_t*);
static uint64_t startup_counter(const char* requested) {
const char* names[128]{}; uint64_t counts[128]{}, frames[128]{};
const int count = psx_mod_counters_snapshot(names, counts, frames, 128, nullptr);
for (int i = 0; i < count && i < 128; ++i)
if (std::string(names[i]) == requested) return counts[i];
return 0;
}
int main(int argc, char** argv) {
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
const fs::path root = fs::temp_directory_path() / "psxrecomp-mod-runtime-cache-test";
const auto isolated_cache = root / "test-cache";
#if defined(_WIN32)
check(_wputenv_s(L"LOCALAPPDATA", isolated_cache.c_str()) == 0,
"isolate audited digest receipts from owner cache");
#else
check(setenv("XDG_CACHE_HOME", isolated_cache.c_str(), 1) == 0,
"isolate audited digest receipts from owner cache");
#endif
#if defined(_WIN32)
const char* configured_cache = std::getenv("LOCALAPPDATA");
#else
const char* configured_cache = std::getenv("XDG_CACHE_HOME");
#endif
check(configured_cache && fs::path(configured_cache) == isolated_cache,
"CRT getenv observes the isolated audited cache");
#else
const fs::path root = fs::temp_directory_path() / "psxrecomp-mod-runtime-test";
#endif
std::error_code ec;
fs::remove_all(root, ec);
const std::vector<uint8_t> stock(8 * 2352, 0);
std::vector<uint8_t> overlay(3000);
for (size_t i = 0; i < overlay.size(); ++i)
overlay[i] = (uint8_t)(i * 17u + 3u);
const fs::path stock_path = root / "stock.bin";
write_bytes(stock_path, stock);
write_bytes(root / "audio.bin", std::vector<uint8_t>(2 * 2352, 0x5a));
const fs::path cue_path = root / "stock.cue";
write_text(cue_path,
"FILE \"stock.bin\" BINARY\n"
" TRACK 01 MODE2/2352\n"
" INDEX 01 00:00:00\n"
"FILE \"audio.bin\" BINARY\n"
" TRACK 02 AUDIO\n"
" INDEX 01 00:00:00\n");
write_bytes(root / "packages/runtime.test/1.0.0/assets/overlay.bin",
overlay);
write_text(root / "packages/runtime.test/1.0.0/manifest.toml",
"format_version = 5\n"
"id = \"runtime.test\"\n"
"version = \"1.0.0\"\n"
"name = \"Runtime Test\"\n"
"[[target]]\n"
"game_id = \"SLUS-RUNTIME\"\n"
"disc_sha256 = \"" + sha256_hex(stock) + "\"\n"
"[[feature]]\n"
"id = \"main-code\"\n"
"name = \"Main Code\"\n"
"[[feature]]\n"
"id = \"disc-byte\"\n"
"name = \"Disc Byte\"\n"
"[[feature]]\n"
"id = \"asset-overlay\"\n"
"name = \"Asset Overlay\"\n"
"[[feature]]\n"
"id = \"user-byte\"\n"
"name = \"User Byte\"\n"
"[[feature]]\n"
"id = \"dynamic-main\"\n"
"name = \"Dynamic Main\"\n"
"[[feature]]\n"
"id = \"sparse-main\"\n"
"name = \"Sparse Main\"\n"
"[[feature]]\n"
"id = \"sparse-flag\"\n"
"name = \"Sparse Flag\"\n"
"[[feature]]\n"
"id = \"sparse-disc\"\n"
"name = \"Sparse Disc\"\n"
"[[feature]]\n"
"id = \"vblank-plugin\"\n"
"name = \"VBlank Plugin\"\n"
"[[option]]\nfeature = \"vblank-plugin\"\nid = \"cameras\"\n"
"label = \"Cameras\"\ntype = \"choice\"\ndefault = \"unified\"\n"
"[[option.choice]]\nvalue = \"unified\"\nlabel = \"Unified\"\n"
"[[option.choice]]\nvalue = \"split\"\nlabel = \"Split\"\n"
"[[option]]\n"
"feature = \"dynamic-main\"\n"
"id = \"count\"\n"
"label = \"Count\"\n"
"type = \"integer\"\n"
"min = 0\n"
"max = 254\n"
"default = 42\n"
"[[option]]\n"
"feature = \"sparse-main\"\n"
"id = \"frames\"\n"
"label = \"Frames\"\n"
"type = \"integer\"\n"
"min = 0\n"
"max = 99\n"
"default = 2\n"
"[[patch]]\n"
"feature = \"main-code\"\n"
"target = \"main_exe\"\n"
"address = 2147487744\n"
"expected = \"01020304\"\n"
"replace = \"a1a2a3a4\"\n"
"[[patch]]\n"
"feature = \"disc-byte\"\n"
"target = \"disc_raw\"\n"
"offset = 4714\n"
"expected = \"aa\"\n"
"replace = \"bb\"\n"
"[[patch]]\n"
"feature = \"user-byte\"\n"
"target = \"disc_user\"\n"
"offset = 6154\n"
"expected = \"cc\"\n"
"replace = \"dd\"\n"
"[[patch]]\n"
"feature = \"dynamic-main\"\n"
"target = \"main_exe\"\n"
"address = 2147488000\n"
"expected = \"0000\"\n"
"replace_from = { option = \"count\", encoding = \"u16le\" }\n"
"[[patch]]\n"
"feature = \"dynamic-main\"\n"
"target = \"main_exe\"\n"
"address = 2147488002\n"
"expected = \"0100\"\n"
"replace_from = { option = \"count\", encoding = \"u16le\", addend = 1 }\n"
"[[patch]]\n"
"feature = \"sparse-main\"\n"
"target = \"main_exe\"\n"
"address = 2147488256\n"
"expected = \"02000132\"\n"
"fields = [{ offset = 0, option = \"frames\", encoding = \"u8\" }]\n"
"when_integer = { option = \"frames\", op = \"gt\", value = 0 }\n"
"[[patch]]\n"
"feature = \"sparse-main\"\n"
"target = \"main_exe\"\n"
"address = 2147488256\n"
"expected = \"02000132\"\n"
"fields = [{ offset = 0, replace = \"01\" }, "
"{ offset = 2, replace = \"00\" }]\n"
"when_integer = { option = \"frames\", op = \"eq\", value = 0 }\n"
"[[patch]]\n"
"feature = \"sparse-flag\"\n"
"target = \"main_exe\"\n"
"address = 2147488256\n"
"expected = \"02000132\"\n"
"fields = [{ offset = 1, replace = \"42\" }]\n"
"[[patch]]\n"
"feature = \"sparse-disc\"\n"
"target = \"disc_user\"\n"
"offset = 6164\n"
"expected = \"11223344\"\n"
"fields = [{ offset = 0, replace = \"aa\" }, "
"{ offset = 2, replace = \"bb\" }]\n"
"[[overlay]]\n"
"feature = \"asset-overlay\"\n"
"target = \"disc_raw\"\n"
"offset = 11408\n"
"file = \"assets/overlay.bin\"\n"
"sha256 = \"" + sha256_hex(overlay) + "\"\n"
"expected_sha256 = \"" +
sha256_hex(std::vector<uint8_t>(overlay.size(), 0)) + "\"\n"
"[[plugin]]\n"
"feature = \"vblank-plugin\"\n"
"id = \"runtime.test-vblank\"\n"
"[[feature]]\n"
"id = \"entry-disabled\"\n"
"name = \"Disabled Entry\"\n"
"[[plugin]]\n"
"feature = \"entry-disabled\"\n"
"id = \"runtime.test-disabled-entry\"\n");
write_text(root / "state.toml",
"format_version = 2\n"
"[[package]]\n"
"id = \"runtime.test\"\n"
"version = \"1.0.0\"\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"main-code\"\n"
"enabled = true\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"disc-byte\"\n"
"enabled = true\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"asset-overlay\"\n"
"enabled = true\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"user-byte\"\n"
"enabled = true\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"dynamic-main\"\n"
"enabled = true\n"
"[feature.values]\n"
"count = 42\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"sparse-main\"\n"
"enabled = true\n"
"[feature.values]\n"
"frames = 0\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"sparse-flag\"\n"
"enabled = true\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"sparse-disc\"\n"
"enabled = true\n"
"[[feature]]\n"
"package_id = \"runtime.test\"\n"
"id = \"vblank-plugin\"\n"
"enabled = true\n");
std::string error;
PSXRecompV4::mod_clear_plugins_for_tests();
check(PSXRecompV4::mod_register_activation_plugin(
"runtime.test-vblank", test_activation_plugin),
"runtime test activation hook must register");
check(PSXRecompV4::mod_register_vblank_plugin(
"runtime.test-vblank", test_vblank_plugin),
"runtime test plugin must register");
check(psx_mod_register_function_entry_plugin(
"runtime.test-vblank", 0x80003000u, test_active_entry) == 1,
"active plugin's function-entry hook must register");
check(psx_mod_register_function_entry_plugin(
"runtime.test-disabled-entry", 0x80003000u, test_disabled_entry) == 1,
"disabled feature's function-entry hook must register");
check(psx_mod_register_function_entry_plugin(
"runtime.unselected-entry", 0x80003000u, test_unselected_entry) == 1,
"unselected function-entry hook must register");
CPUState entry_cpu{};
check(psx_mod_register_instruction_plugin("runtime.test-vblank", 0x80003004u,
0x90A30014u, test_instruction), "register guarded instruction callback");
check(!psx_mod_register_instruction_plugin("runtime.test-vblank", 0xA0003004u,
0x90A30014u, test_instruction) &&
!psx_mod_register_instruction_plugin("runtime.other", 0x80003005u,
0x90A30014u, test_instruction), "reject aliased duplicate and unaligned sites");
psx_mod_write_word(0x80003004u, 0x90A30014u);
check(psx_mod_register_memory_address_plugin("runtime.test-vblank", 0x80003008u,
0x8CA30014u, test_memory_address), "register effective address callback");
check(!psx_mod_register_memory_address_plugin("runtime.test-vblank", 0xA0003008u,
0x8CA30014u, test_memory_address) &&
!psx_mod_register_memory_address_plugin("runtime.test-vblank", 0x80003009u,
0x8CA30014u, test_memory_address) &&
!psx_mod_register_memory_address_plugin("runtime.test-vblank", 0x8000300cu,
0x24020001u, test_memory_address), "reject duplicate, unaligned and non-memory adapters");
check(psx_mod_register_memory_address_plugin("runtime.unselected-entry", 0x80003008u,
0x8CA30014u, test_memory_address), "register inactive adapter");
psx_mod_write_word(0x80003008u, 0x8CA30014u);
check(psx_mod_register_guest_function_plugin(
"runtime.test-vblank", 0x8FFF0000u, test_guest_function),
"trusted callback registers outside the hardware map");
check(!psx_mod_register_guest_function_plugin(
"runtime.other", 0xAFFF0000u, test_guest_function) &&
!psx_mod_register_guest_function_plugin("runtime.other", 0x80003000u, test_guest_function) &&
!psx_mod_register_guest_function_plugin("runtime.other", 0x8FFF0001u, test_guest_function) &&
!psx_mod_register_guest_function_plugin("runtime.other", 0xCFFF0000u, test_guest_function),
"guest callbacks refuse alias collisions, game addresses, misalignment and KSEG2");
check(psx_mod_register_guest_function_plugin(
"runtime.unselected-entry", 0x8FFF0004u, test_unselected_entry),
"unselected callback implementation can register");
check(psx_mod_register_savestate_plugin("runtime.test-vblank", test_restore_plugin),
"restore callback must register through the C API");
check(!psx_mod_register_savestate_plugin("runtime.test-vblank", test_restore_plugin),
"duplicate restore callback must be rejected");
check(PSXRecompV4::mod_runtime_initialize(
root, "SLUS-RUNTIME", 0x80002000, {}, &error),
error.c_str());
const auto first_commit_timing = launch_timing_total();
check(PSXRecompV4::mod_runtime_commit(cue_path, &error),
"CUE and its data-track BIN must have the same mod target identity");
check_commit_timing(first_commit_timing, true, true);
/* Committed but not yet activated: no hook may run. */
psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u);
check(instruction_hits == 0 && !g_psx_mod_instruction_hooks, "instruction hooks await activation");
check(psx_mod_memory_address(&entry_cpu, 0x80003008u, 0x8CA30014u, 0x80004000u) == 0x80004000u &&
!memory_hits, "address adapters await activation");
check(!psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0000u) &&
g_psx_mod_guest_functions == 0,
"committed guest functions remain unavailable until activation");
psx_mod_function_entry(&entry_cpu, 0x80003000u);
check(g_psx_mod_function_entry_hooks == 0 && active_entry_hits == 0,
"function-entry hooks must not run before plugin activation");
mod_runtime_activate_plugins();
check(activation_calls == 1,
"resolved trusted plugin must activate before runtime startup");
entry_cpu.pc = 0x80004000u;
const CPUState memory_before = entry_cpu;
check(psx_mod_memory_address(&entry_cpu, 0x80003008u, 0x8CA30014u, 0x80004000u) == 0x80005000u &&
psx_mod_memory_address(&entry_cpu, 0xA0003008u, 0x8CA30014u, 0x80004003u) == 0x80005003u &&
memory_hits == 2 && !std::memcmp(&entry_cpu, &memory_before, sizeof entry_cpu),
"active adapter handles aliases and preserves CPU and merge lanes");
check(psx_mod_memory_address(&entry_cpu, 0x80003008u, 0x8CA30015u, 0x80004000u) == 0x80004000u,
"address adapter rejects wrong emitted word");
psx_mod_write_word(0x80003008u, 0x8CA30015u);
check(psx_mod_memory_address(&entry_cpu, 0x80003008u, 0x8CA30014u, 0x80004000u) == 0x80004000u &&
memory_hits == 2, "address adapter rejects changed overlay word");
psx_mod_write_word(0x80003008u, 0x8CA30014u);
memory_delta = 1;
check(psx_mod_memory_address(&entry_cpu, 0x80003008u, 0x8CA30014u, 0x80004000u) == 0x80004000u,
"misaligned adapter result is ignored");
memory_delta = 0x1000u;
psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u);
psx_mod_instruction(&entry_cpu, 0xA0003004u, 0x90A30014u);
check(instruction_hits == 2 && entry_cpu.gpr[5] == 0x80301008u &&
entry_cpu.pc == 0x80004000u, "instruction aliases preserve control flow and edit registers");
psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30015u);
psx_mod_write_word(0x80003004u, 0x90A30015u);
psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u);
check(instruction_hits == 2, "fetched and live instruction guards reject changed code");
psx_mod_write_word(0x80003004u, 0x90A30014u);
mod_runtime_on_vblank();
check(plugin_calls == 1,
"resolved trusted plugin must run on guest VBlank");
check(g_psx_mod_function_entry_hooks == 1,
"activation must flatten exactly the active plan's entry hooks");
psx_mod_function_entry(&entry_cpu, 0x80003000u);
check(active_entry_hits == 1 && active_entry_last == 0x80003000u,
"active plan's function-entry hook must run at its address");
psx_mod_function_entry(&entry_cpu, 0x00003000u);
psx_mod_function_entry(&entry_cpu, 0xA0003000u);
check(active_entry_hits == 3 && active_entry_last == 0xA0003000u,
"function-entry hooks must match every segment alias of the code address");
psx_mod_function_entry(&entry_cpu, 0x80003004u);
psx_mod_function_entry(&entry_cpu, 0x80203000u);
check(active_entry_hits == 3,
"function-entry hooks must not run for other addresses or RAM mirrors");
check(disabled_entry_hits == 0 && unselected_entry_hits == 0,
"hooks of plugins the plan does not activate must never run");
/* A replaced plan drops every hook until its own activation. */
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), error.c_str());
psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u);
check(instruction_hits == 2 && !g_psx_mod_instruction_hooks, "clearing removes instruction hooks");
check(psx_mod_memory_address(&entry_cpu, 0x80003008u, 0x8CA30014u, 0x80004000u) == 0x80004000u,
"clearing removes address adapters");
check(!psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0000u) &&
g_psx_mod_guest_functions == 0,
"clearing the plan drops guest callback availability");
psx_mod_function_entry(&entry_cpu, 0x80003000u);
check(g_psx_mod_function_entry_hooks == 0 && active_entry_hits == 3,
"clearing the plan must drop its function-entry hooks");
check(PSXRecompV4::mod_runtime_commit(cue_path, &error), error.c_str());
psx_mod_function_entry(&entry_cpu, 0x80003000u);
check(active_entry_hits == 3,
"a re-committed plan must not run hooks before activation");
mod_runtime_activate_plugins();
psx_mod_function_entry(&entry_cpu, 0x80003000u);
check(active_entry_hits == 4 && disabled_entry_hits == 0 &&
unselected_entry_hits == 0,
"re-activation restores exactly the active plan's hooks");
/* The lobby rematch, in the order main.cpp's start_mod_session() drives
* it: a netplay match (plan cleared, then activated) and an offline
* rematch that commits and activates exactly like a first boot. */
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), error.c_str());
mod_runtime_activate_plugins();
mod_runtime_on_vblank();
psx_mod_function_entry(&entry_cpu, 0x80003000u);
check(activation_calls == 2 && plugin_calls == 1 &&
g_psx_mod_function_entry_hooks == 0 && active_entry_hits == 4,
"a netplay session must stay vanilla: no activation, VBlank or hook");
check(PSXRecompV4::mod_runtime_commit(cue_path, &error), error.c_str());
mod_runtime_activate_plugins();
check(activation_calls == 3,
"an offline rematch after netplay must run activation callbacks");
mod_runtime_on_vblank();
check(plugin_calls == 2,
"an offline rematch after netplay must run VBlank plugins");
psx_mod_function_entry(&entry_cpu, 0x80003000u);
check(g_psx_mod_function_entry_hooks == 1 && active_entry_hits == 5 &&
disabled_entry_hits == 0 && unselected_entry_hits == 0,
"an offline rematch after netplay must arm exactly its plan's hooks");
check(psx_mod_register_function_filter_plugin(
"runtime.test-vblank", 0x80003008u, test_active_filter) == 1,
"active plan's return filter must register");
check(psx_mod_register_function_filter_plugin(
"runtime.test-vblank", 0xA0003008u, test_active_filter) == 0,
"filter aliases must not register duplicate hooks");
check(psx_mod_register_function_filter_plugin(
"runtime.test-disabled-entry", 0x80003008u, test_inactive_filter) == 1 &&
psx_mod_register_function_filter_plugin(
"runtime.unselected-entry", 0x80003008u, test_inactive_filter) == 1,
"inactive filter implementations must register without running");
entry_cpu.pc = 0x80003008u;
entry_cpu.gpr[31] = 0x80004000u;
entry_cpu.gpr[2] = 99;
check(!psx_mod_function_entry(&entry_cpu, entry_cpu.pc) && active_filter_hits == 0,
"new filters must wait for active-plan table rebuild");
mod_runtime_activate_plugins();
check(g_psx_mod_function_entry_hooks == 2 &&
!psx_mod_function_entry(&entry_cpu, entry_cpu.pc) &&
active_filter_hits == 1 && filter_context_active &&
entry_cpu.pc == 0x80003008u && entry_cpu.gpr[2] == 99,
"unhandled active filter must preserve native fallthrough");
filter_handles = true;
entry_cpu.pc = 0xA0003008u;
check(psx_mod_function_entry(&entry_cpu, entry_cpu.pc) == 1 &&
active_filter_hits == 2 && active_filter_last == 0xA0003008u &&
entry_cpu.pc == entry_cpu.gpr[31] && entry_cpu.gpr[2] == 42 &&
inactive_filter_hits == 0 && !psx_mod_function_entry_active(),
"handled alias filter must publish return registers and pc=$ra");
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), error.c_str());
check(!psx_mod_function_entry(&entry_cpu, 0x80003008u) &&
active_filter_hits == 2 && g_psx_mod_function_entry_hooks == 0,
"clearing the active plan must also disarm return filters");
check(PSXRecompV4::mod_runtime_commit(cue_path, &error), error.c_str());
mod_runtime_activate_plugins();
entry_cpu.gpr[31] = 0x80005000u;
check(!psx_mod_finish_function(&entry_cpu) && !psx_mod_function_entry(nullptr, 0x80003000u),
"completion outside an entry callback or with no CPU is refused");
entry_test_mode = 1;
check(psx_mod_function_entry(&entry_cpu, 0x80003000u) == 1 &&
entry_cpu.pc == 0x80005000u && entry_cpu.gpr[2] == 0x12345678u,
"explicit completion returns handled and publishes guest return PC/results");
entry_test_mode = 2; entry_cpu.pc = 0;
check(!psx_mod_function_entry(&entry_cpu, 0x80003000u) && entry_cpu.pc == 0 &&
nested_result == 1 && nested_pc == 0x80004000u,
"inner completion cannot finish an ordinary outer callback");
entry_test_mode = 3;
check(psx_mod_function_entry(&entry_cpu, 0x80003000u) == 1 &&
entry_cpu.pc == 0x80005000u && nested_result == 0,
"outer completion survives a nested ordinary callback");
entry_test_mode = 4;
check(psx_mod_function_entry(&entry_cpu, 0x80003000u) == 1 &&
entry_cpu.pc == 0x80005000u && entry_cpu.gpr[2] == 0x87654321u,
"entry completion survives VBlank callbacks during a native call");
entry_test_mode = 0;
entry_test_mode = 5;
if (setjmp(callback_escape) == 0) {
psx_mod_function_entry(&entry_cpu, 0x80003000u);
check(false, "callback must escape without its native cleanup");
}
check(psx_mod_function_entry_active(), "escape reproduces the stranded capture gate");
const ModFunctionEntryContext no_callback{};
mod_runtime_function_entry_context_restore(&no_callback);
check(!psx_mod_function_entry_active() && !psx_mod_finish_function(&entry_cpu),
"scheduler landing releases the gate and clears abandoned completion ownership");
entry_test_mode = 6;
check(psx_mod_function_entry(&entry_cpu, 0x80003000u) == 1 &&
entry_cpu.pc == entry_cpu.gpr[31] && !psx_mod_function_entry_active(),
"nested exception landing preserves the live outer callback and completion");
entry_test_mode = 0;
entry_cpu.gpr[4] = 35u;
const uint32_t guest_sp = entry_cpu.gpr[29];
check(g_psx_mod_guest_functions == 1 &&
psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0000u) &&
entry_cpu.gpr[2] == 42u && entry_cpu.pc == 0x80005000u &&
entry_cpu.gpr[29] == guest_sp,
"active callback returns its result through normal guest PC/stack state");
check(psx_mod_dispatch_guest_function(&entry_cpu, 0x0FFF0000u) &&
psx_mod_dispatch_guest_function(&entry_cpu, 0xAFFF0000u) &&
guest_function_hits == 3,
"guest callback aliases retain nested entry and VBlank scope");
check(!psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0004u) &&
!psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0001u) &&
!psx_mod_dispatch_guest_function(&entry_cpu, 0xCFFF0000u) &&
!psx_mod_dispatch_guest_function(nullptr, 0x8FFF0000u) &&
unselected_entry_hits == 0,
"unselected, unknown, misaligned and invalid callback requests fall through");
ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4;
ram[0x1100] = 0; ram[0x1101] = 0;
ram[0x1102] = 1; ram[0x1103] = 0;
ram[0x1200] = 2; ram[0x1201] = 0;
ram[0x1202] = 1; ram[0x1203] = 0x32;
mod_runtime_on_dispatch(0x80001000);
check(ram[0x1000] == 1, "patch must wait for the configured entry point");
mod_runtime_on_dispatch(0x80002000);
check(ram[0x1000] == 0xa1 && ram[0x1003] == 0xa4,
"main-EXE patch must apply before entry execution");
check(ram[0x1100] == 42 && ram[0x1101] == 0 &&
ram[0x1102] == 43 && ram[0x1103] == 0,
"dynamic main-EXE patches must encode all sites before entry");
check(ram[0x1200] == 1 && ram[0x1201] == 0x42 &&
ram[0x1202] == 0 && ram[0x1203] == 0x32,
"adjacent sparse fields must compose while preserving guard-only "
"bytes");
/* A full-machine savestate replaces main RAM after the entry-point plan
* has already applied. Loading a stock checkpoint must not silently turn
* the currently enabled main-EXE features back off. */
ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4;
ram[0x1100] = 0; ram[0x1101] = 0;
ram[0x1102] = 1; ram[0x1103] = 0;
ram[0x1200] = 2; ram[0x1201] = 0;
ram[0x1202] = 1; ram[0x1203] = 0x32;
const int activation_before_restore = activation_calls;
const int vblank_before_restore = plugin_calls;
mod_runtime_on_savestate_loaded();
check(restore_calls == 1 && activation_calls == activation_before_restore &&
plugin_calls == vblank_before_restore,
"restore must rebind hooks without activation or a gameplay VBlank");
check(ram[0x1000] == 0xa1 && ram[0x1003] == 0xa4 &&
ram[0x1100] == 42 && ram[0x1102] == 43 &&
ram[0x1200] == 1 && ram[0x1201] == 0x42 &&
ram[0x1202] == 0 && ram[0x1203] == 0x32,
"savestate restore must reapply the complete enabled main plan");
std::array<uint8_t, 2352> sector{};
sector[10] = 0xaa;
mod_runtime_patch_disc_sector(2, 1, sector.data(), (uint32_t)sector.size());
check(sector[10] == 0xaa, "disc overlay must stay off during reference reads");
mod_runtime_enable_disc_patches();
mod_runtime_patch_disc_sector(2, 1, sector.data(), (uint32_t)sector.size());
check(sector[10] == 0xbb, "raw disc overlay must patch matching sectors");
std::array<uint8_t, 2352> overlay_sector{};
mod_runtime_patch_disc_sector(
4, 1, overlay_sector.data(), (uint32_t)overlay_sector.size());
check(overlay_sector[1999] == 0 &&
overlay_sector[2000] == overlay[0] &&
overlay_sector[2351] == overlay[351],
"file overlay must patch the tail of its first sector");
overlay_sector.fill(0);
mod_runtime_patch_disc_sector(
5, 1, overlay_sector.data(), (uint32_t)overlay_sector.size());
check(overlay_sector.front() == overlay[352] &&
overlay_sector.back() == overlay[2703],
"file overlay must patch complete middle sectors");
overlay_sector.fill(0);
mod_runtime_patch_disc_sector(
6, 1, overlay_sector.data(), (uint32_t)overlay_sector.size());
check(overlay_sector[0] == overlay[2704] &&
overlay_sector[295] == overlay[2999] &&
overlay_sector[296] == 0,
"file overlay must patch the head of its final sector");
std::array<uint8_t, 2352> mode2_sector{};
mode2_sector[15] = 2;
mode2_sector[18] = 0;
mode2_sector[24 + 10] = 0xcc;
mode2_sector[24 + 20] = 0x11;
mode2_sector[24 + 21] = 0x22;
mode2_sector[24 + 22] = 0x33;
mode2_sector[24 + 23] = 0x44;
mod_runtime_patch_disc_sector(
3, 1, mode2_sector.data(), (uint32_t)mode2_sector.size());
check(mode2_sector[24 + 10] == 0xdd,
"disc_user operations must apply to raw Mode2 Form1 user data");
check(mode2_sector[24 + 20] == 0xaa &&
mode2_sector[24 + 21] == 0x22 &&
mode2_sector[24 + 22] == 0xbb &&
mode2_sector[24 + 23] == 0x44,
"sparse disc writes must validate a complete guard and modify only "
"owned fields");
std::array<uint8_t, 2352> audio_sector{};
audio_sector[24 + 10] = 0xcc;
mod_runtime_patch_disc_sector(
3, 1, audio_sector.data(), (uint32_t)audio_sector.size());
check(audio_sector[24 + 10] == 0xcc,
"disc_user operations must not modify CDDA/non-data sectors");
check(PSXRecompV4::mod_runtime_initialize(
root, "SLUS-RUNTIME", 0x80002000, {}, &error),
error.c_str());
check(PSXRecompV4::mod_runtime_commit(stock_path, &error), error.c_str());
ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4;
ram[0x1100] = 0; ram[0x1101] = 0;
ram[0x1102] = 2; ram[0x1103] = 0; /* second dynamic guard is wrong */
mod_runtime_on_dispatch(0x80002000);
check(ram[0x1000] == 1 && ram[0x1003] == 4 &&
ram[0x1100] == 0 && ram[0x1101] == 0,
"one failed generated guard must leave the complete main plan untouched");
check(PSXRecompV4::mod_runtime_initialize(
root, "SLUS-RUNTIME", 0x80002000, {}, &error),
error.c_str());
check(PSXRecompV4::mod_runtime_commit(stock_path, &error), error.c_str());
ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4;
ram[0x1100] = 0; ram[0x1101] = 0;
ram[0x1102] = 1; ram[0x1103] = 0;
ram[0x1200] = 2; ram[0x1201] = 0;
ram[0x1202] = 1; ram[0x1203] = 0x33; /* guard-only byte is wrong */
mod_runtime_on_dispatch(0x80002000);
check(ram[0x1000] == 1 && ram[0x1003] == 4 &&
ram[0x1200] == 2 && ram[0x1201] == 0 &&
ram[0x1202] == 1 && ram[0x1203] == 0x33,
"a failed sparse guard-only byte must leave the complete main plan "
"untouched");
/* Loading can be requested while the boot executable is still running,
* before the configured game entry point has dispatched. The restored
* checkpoint itself supplies the bytes used to validate and apply the
* plan in that case. */
check(PSXRecompV4::mod_runtime_initialize(
root, "SLUS-RUNTIME", 0x80002000, {}, &error),
error.c_str());
check(PSXRecompV4::mod_runtime_commit(stock_path, &error), error.c_str());
ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4;
ram[0x1100] = 0; ram[0x1101] = 0;
ram[0x1102] = 1; ram[0x1103] = 0;
ram[0x1200] = 2; ram[0x1201] = 0;
ram[0x1202] = 1; ram[0x1203] = 0x32;
mod_runtime_on_savestate_loaded();
check(ram[0x1000] == 0xa1 && ram[0x1003] == 0xa4 &&
ram[0x1100] == 42 && ram[0x1102] == 43 &&
ram[0x1200] == 1 && ram[0x1201] == 0x42 &&
ram[0x1202] == 0 && ram[0x1203] == 0x32,
"pre-entry savestate restore must validate and apply the main plan");
mod_runtime_enable_disc_patches();
std::array<uint8_t, 2352> bad_sparse_disc{};
bad_sparse_disc[15] = 2;
bad_sparse_disc[18] = 0;
bad_sparse_disc[24 + 10] = 0xcc;
bad_sparse_disc[24 + 20] = 0x11;
bad_sparse_disc[24 + 21] = 0x99; /* guard-only byte is wrong */
bad_sparse_disc[24 + 22] = 0x33;
bad_sparse_disc[24 + 23] = 0x44;
mod_runtime_patch_disc_sector(
3, 1, bad_sparse_disc.data(),
(uint32_t)bad_sparse_disc.size());
check(bad_sparse_disc[24 + 10] == 0xcc &&
bad_sparse_disc[24 + 20] == 0x11 &&
bad_sparse_disc[24 + 22] == 0x33,
"a failed sparse disc guard must leave every write in the sector "
"untouched");
/* Implicit requirement through the real commit/activation path: the
* hidden required feature's activation plugin runs, the state.toml commit
* writes never gains the derived activation (it is not a choice),
* and a requirement on an absent package makes the launch commit fail
* rather than boot the requiring build without it. */
{
const fs::path req_root = root / "requirement-runtime";
write_text(req_root / "bundled/runtime.ram/1.0.0/manifest.toml",
"format_version = 5\n"
"id = \"runtime.ram\"\n"
"version = \"1.0.0\"\n"
"name = \"Runtime RAM\"\n"
"[[target]]\n"
"game_id = \"*\"\n"
"[[feature]]\n"
"id = \"big-ram\"\n"
"name = \"Big RAM\"\n"
"hidden = true\n"
"[[option]]\n"
"feature = \"big-ram\"\n"
"id = \"size\"\n"
"label = \"Size\"\n"
"type = \"choice\"\n"
"default = \"eight\"\n"
"[[option.choice]]\nvalue = \"eight\"\nlabel = \"8 MB\"\n"
"[[plugin]]\n"
"feature = \"big-ram\"\n"
"id = \"runtime.big-ram\"\n");
write_text(req_root / "bundled/runtime.mode/1.0.0/manifest.toml",
"format_version = 7\n"
"id = \"runtime.mode\"\n"
"version = \"1.0.0\"\n"
"name = \"Runtime Mode\"\n"
"[[target]]\n"
"game_id = \"SLUS-REQ\"\n"
"[[feature]]\n"
"id = \"mode\"\n"
"name = \"Mode\"\n"
"[[option]]\n"
"feature = \"mode\"\n"
"id = \"extras\"\n"
"label = \"Extras\"\n"
"type = \"choice\"\n"
"default = \"none\"\n"
"[[option.choice]]\nvalue = \"none\"\nlabel = \"None\"\n"
"[[option.choice]]\nvalue = \"full\"\nlabel = \"Full\"\n"
"[[requirement]]\n"
"feature = \"mode\"\n"
"package = \"runtime.ram\"\n"
"requires_feature = \"big-ram\"\n"
"when = { extras = \"full\" }\n");
const std::string state_text =
"format_version = 2\n"
"\n[[feature]]\npackage_id = \"runtime.mode\"\nid = \"mode\"\n"
"enabled = true\n"
"[feature.values]\nextras = \"full\"\n";
write_text(req_root / "state.toml", state_text);
const auto read_state = [&]() {
std::ifstream in(req_root / "state.toml", std::ios::binary);
return std::string((std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>());
};
big_ram_activations = 0;
check(PSXRecompV4::mod_register_activation_plugin(
"runtime.big-ram", test_big_ram_activation),
"big-ram activation hook must register");
check(PSXRecompV4::mod_runtime_initialize(
req_root, "SLUS-REQ", 0x80002000, {}, &error),
error.c_str());
check(PSXRecompV4::mod_runtime_commit(stock_path, &error),
"a satisfied implicit requirement must commit");
mod_runtime_activate_plugins();
check(big_ram_activations == 1,
"the implicitly required feature's plugin must activate");
char value[32];
check(psx_mod_option_value("runtime.ram", "big-ram", "size", value,
sizeof(value)) == 1 &&
std::string(value) == "eight",
"an implicit feature's plugin must read its option values");
/* In-game overlay P5: a live option change reaches the plugin and
* every later read, and is refused while netplay owns the plan. */
check(psx_mod_set_option_live("runtime.ram", "big-ram", "size", "four") == 0,
"without an option-changed callback a live set waits for restart");
check(psx_mod_option_value("runtime.ram", "big-ram", "size", value, sizeof(value)) == 1 &&
std::string(value) == "eight",
"a restart-only change must not reach the running plugin");
check(!psx_mod_option_live_capable("runtime.ram", "big-ram"),
"no callback yet: not live-capable");
check(psx_mod_register_option_changed_plugin("runtime.big-ram", test_option_changed),
"option-changed hook must register");
check(psx_mod_option_live_capable("runtime.ram", "big-ram"),
"a registered callback makes the feature live-capable");
check(psx_mod_set_option_live("runtime.ram", "big-ram", "size", "four") == 1 &&
option_changed_hits == 1 && option_changed_value == "four",
"the plugin must see the change in its own context");
check(psx_mod_option_value("runtime.ram", "big-ram", "size", value, sizeof(value)) == 1 &&
std::string(value) == "four",
"a live override must answer later reads");
/* A callback that refuses ("nothing changes until next start") must
* leave the value the session runs with untouched. */
option_changed_refuse = 1;
check(psx_mod_set_option_live("runtime.ram", "big-ram", "size", "two") == 0 &&
option_changed_hits == 2,
"a refused live set reports refused after asking the plugin");
check(psx_mod_option_value("runtime.ram", "big-ram", "size", value, sizeof(value)) == 1 &&
std::string(value) == "four",
"a refused live set must not change the value later reads see");
option_changed_refuse = 0;
/* A new session (initialise + commit) answers its own committed
* plan, not the previous session's live override. */
check(PSXRecompV4::mod_runtime_initialize(
req_root, "SLUS-REQ", 0x80002000, {}, &error),
error.c_str());
check(PSXRecompV4::mod_runtime_commit(stock_path, &error),
"the second session must commit");
mod_runtime_activate_plugins();
check(psx_mod_option_value("runtime.ram", "big-ram", "size", value, sizeof(value)) == 1 &&
std::string(value) == "eight",
"a live override must not outlive its session's plan");
check(psx_mod_set_option_live("runtime.ram", "no-such-feature", "size", "x") == 0,
"features outside the plan are not live-settable");
check(psx_mod_save_selection() == 1, "saving the selection mid-session must succeed");
test_netplay_active = 1;
check(psx_mod_set_option_live("runtime.ram", "big-ram", "size", "four") == -1,
"netplay must refuse live option changes");
test_netplay_active = 0;
/* commit rewrites state.toml in canonical form; what matters is that
* the player's choice survives and the derived one is never added. */
const std::string after = read_state();
check(after.find("runtime.ram") == std::string::npos &&
after.find("extras = \"full\"") != std::string::npos,
"committing must not write the derived activation to state.toml");
/* Condition false: the required plugin stays off. */
write_text(req_root / "state.toml",
"format_version = 2\n"
"\n[[feature]]\npackage_id = \"runtime.mode\"\nid = \"mode\"\n"
"enabled = true\n"
"[feature.values]\nextras = \"none\"\n");
big_ram_activations = 0;
check(PSXRecompV4::mod_runtime_initialize(
req_root, "SLUS-REQ", 0x80002000, {}, &error),
error.c_str());
check(PSXRecompV4::mod_runtime_commit(stock_path, &error),
error.c_str());
mod_runtime_activate_plugins();
check(big_ram_activations == 0,
"a requirement whose condition is false must not activate");
/* Required package absent (e.g. the title excluded the builtin). */
write_text(req_root / "state.toml", state_text);
fs::remove_all(req_root / "bundled/runtime.ram", ec);
check(PSXRecompV4::mod_runtime_initialize(
req_root, "SLUS-REQ", 0x80002000, {}, &error),
error.c_str());
std::string commit_error;
check(!PSXRecompV4::mod_runtime_commit(stock_path, &commit_error) &&
commit_error.find("runtime.mode/mode requires "
"runtime.ram/big-ram") != std::string::npos,
"an unmet requirement must fail the launch commit loudly");
}
/*
* Display geometry passthrough. Ape Escape scans out 384 while its mode
* width is 368, which is precisely why a plugin cannot derive this from
* GPUSTAT: the horizontal range that makes the difference is write-only.
*/
g_test_display.width = 384;
g_test_display.height = 240;
check(psx_mod_display_width() == 384u,
"psx_mod_display_width must report the presenter's visible width, "
"not the coarse mode width");
check(psx_mod_display_height() == 240u,
"psx_mod_display_height must report the presenter's visible height");
g_test_display.width = 0;
g_test_display.height = 0;
check(psx_mod_display_width() == 0u && psx_mod_display_height() == 0u,
"unestablished display geometry must report zero so callers skip "
"drawing instead of guessing");
/* Source-owned ISO: nested file spanning two sectors, without a derived
* disc. Host reads must choose the original mount and leave sizes honest. */
std::vector<uint8_t> iso(24*2048);
auto le32 = [&](size_t at,uint32_t n) { for(unsigned i=0;i<4;++i) iso[at+i]=(uint8_t)(n>>(i*8)); };
auto record = [&](size_t at,uint32_t lba,uint32_t bytes,bool directory,const std::string& name) {
iso[at]=(uint8_t)((33+name.size()+1)&~size_t(1));
le32(at+2,lba); le32(at+10,bytes); iso[at+25]=directory?2:0;
iso[at+28]=1;iso[at+31]=1;iso[at+32]=(uint8_t)name.size();
std::copy(name.begin(),name.end(),iso.begin()+at+33);
};
iso[16*2048]=1;std::copy_n("CD001",5,iso.begin()+16*2048+1);iso[16*2048+6]=1;
record(16*2048+156,20,2048,true,std::string(1,'\0'));
record(20*2048,21,2048,true,"S0");
record(21*2048,22,3000,false,"LEVEL.NSF;1");
for(unsigned i=0;i<3000;++i)iso[22*2048+i]=(uint8_t)(i*7);
const auto disc_root=root/"host-reader";
const auto iso_path=disc_root/"original.iso";
write_bytes(iso_path,iso);
check(PSXRecompV4::mod_runtime_initialize(disc_root,"READER",0,{},&error),"reader initialize");
check(PSXRecompV4::mod_runtime_commit(iso_path,&error),"reader mount original ISO");
uint32_t bytes=0;
static unsigned host_read_progress = 0;
mod_runtime_set_progress_hook([] { ++host_read_progress; });
check(psx_mod_read_disc_file("S0/LEVEL.NSF",nullptr,0,&bytes) && bytes==3000,"query original nested file size");
std::vector<uint8_t> result(3000);
check(!psx_mod_read_disc_file("S0/LEVEL.NSF",result.data(),2999,&bytes) && bytes==0,"undersized destination rejected");
check(host_read_progress == 0, "size query and rejected destination do not report reading");
check(psx_mod_read_disc_file("S0/LEVEL.NSF",result.data(),(uint32_t)result.size(),&bytes) &&
bytes==3000 && std::equal(result.begin(),result.end(),iso.begin()+22*2048),"complete original file bytes");
check(host_read_progress >= 2, "whole-file plugin reads report progress and completion");
mod_runtime_set_progress_hook(nullptr);
check(!psx_mod_read_disc_file("S0/MISSING.NSF",nullptr,0,&bytes) && bytes==0,"missing file explicit");
check(!psx_mod_read_disc_file("S0",nullptr,0,&bytes),"directory rejected");
std::vector<uint8_t> raw_iso(24*2352);
for(unsigned i=0;i<24;++i) {
/* A raw sector starts with the CD sync pattern; geometry is read from
* it, never from the file length. */
raw_iso[i*2352]=0; std::fill_n(raw_iso.begin()+i*2352+1,10,(uint8_t)0xFF); raw_iso[i*2352+11]=0;
raw_iso[i*2352+15]=2;
std::copy_n(iso.begin()+i*2048,2048,raw_iso.begin()+i*2352+24);
}
const auto raw_path=disc_root/"original.bin";
write_bytes(raw_path,raw_iso);
check(PSXRecompV4::mod_runtime_commit(raw_path,&error),"reader mount raw disc");
check(psx_mod_read_disc_file("S0/LEVEL.NSF",result.data(),(uint32_t)result.size(),&bytes) &&
std::equal(result.begin(),result.end(),iso.begin()+22*2048),"raw and ISO reads identical");
{
const auto folder=root/"prepared-overlay";
const auto pack_path=folder/"local.pack";
std::vector<uint8_t> pack(3016,0x42),payload(3000);
for(size_t i=0;i<payload.size();++i)payload[i]=uint8_t(i*11+5);
std::copy(payload.begin(),payload.end(),pack.begin()+16);
const uint64_t offset=21*2352+2000;
const std::vector<uint8_t> expected(raw_iso.begin()+offset,raw_iso.begin()+offset+payload.size());
check(PSXRecompV4::mod_register_media_preparer("test.overlay.prepare",
[&](const PSXRecompV4::ModPrepareContext& context,std::map<std::string,fs::path>& outputs,std::string&) {
if(context.progress && !context.progress(0.5f,"Building local pack"))return false;
write_bytes(pack_path,pack);outputs["pack"]=pack_path;return true;
}),"register prepared overlay fixture");
write_text(folder/"bundled/prepared.overlay/1.0.0/manifest.toml",
"format_version = 10\nid = \"prepared.overlay\"\nversion = \"1.0.0\"\nname = \"Prepared overlay\"\nprepare = \"test.overlay.prepare\"\nsave_compatibility = \"isolated\"\n"
"[[target]]\ngame_id = \"READER\"\ndisc_sha256 = \""+sha256_hex(raw_iso)+"\"\n"
"[[feature]]\nid = \"audio\"\nname = \"Audio\"\ndefault_enabled = true\n"
"[[resource]]\nfeature = \"audio\"\nid = \"pack\"\nlabel = \"Pack\"\nformat = \"file\"\nhidden = true\nrequired = true\nsize = 3016\nsha256 = \""+sha256_hex(pack)+"\"\n"
"[[overlay]]\nfeature = \"audio\"\ntarget = \"disc_raw\"\noffset = "+std::to_string(offset)+"\n"
"resource = \"pack\"\nresource_offset = 16\nsize = 3000\nsha256 = \""+sha256_hex(payload)+"\"\nexpected_sha256 = \""+sha256_hex(expected)+"\"\n");
check(PSXRecompV4::mod_runtime_initialize(folder,"READER",0,{},&error),"initialize prepared overlay");
int reports=0;
check(!PSXRecompV4::mod_runtime_prepare_for_launcher(raw_path,&error,
[&](float fraction,const std::string&){++reports;return fraction<1.0f;}),"cancel before installing prepared plan");
check(reports>1 && error=="Preparation canceled.","runtime propagates cooperative cancellation");
check(PSXRecompV4::mod_runtime_initialize(folder,"READER",0,{},&error),"cold offer overlay initialize");
std::string save_scope;
check(!PSXRecompV4::mod_runtime_savestate_scope(save_scope,&error) && save_scope.empty(),
"uncommitted isolated plan cannot fall back to shared save states");
check(PSXRecompV4::mod_runtime_prepare_resources(raw_path,&error),"cold offer prepares exact-disc resources without commit");
check(!PSXRecompV4::mod_runtime_savestate_scope(save_scope,&error),
"resource preparation does not install a save-state scope");
const auto offer_fp=PSXRecompV4::mod_runtime_plan_fingerprint_portable();
check(offer_fp.size()==64 && PSXRecompV4::mod_runtime_fingerprint().empty(),"cold offer qualifies disc without installing a plan");
check(PSXRecompV4::mod_runtime_initialize(folder,"READER",0,{},&error),"cold LAN overlay initialize");
test_match_caps={};
check(PSXRecompV4::mod_runtime_commit_for_direct_netplay(raw_path,&error),"cold LAN accepts exact-disc resource overlay");
check(PSXRecompV4::mod_runtime_session_plan_fp()==offer_fp &&
PSXRecompV4::mod_runtime_plan_fingerprint_portable()==offer_fp &&
PSXRecompV4::mod_runtime_fingerprint()==offer_fp,"offer and committed exact-disc identities agree");
check(PSXRecompV4::mod_runtime_savestate_scope(save_scope,&error) && save_scope==offer_fp,
"direct netplay scopes isolated save states by its installed verified plan");
PSXRecompV4::mod_runtime_end_netplay();
check(PSXRecompV4::mod_runtime_savestate_scope(save_scope,&error) && save_scope.empty(),
"intentional netplay clear retires isolated save scope");
const auto changed_disc=folder/"offered.bin";
write_bytes(changed_disc,raw_iso);
check(PSXRecompV4::mod_runtime_initialize(folder,"READER",0,{},&error),"changed offer overlay initialize");
check(PSXRecompV4::mod_runtime_prepare_resources(changed_disc,&error),"prepare source before same-path mutation");
auto changed_bytes=raw_iso;changed_bytes[10]^=1; // Outside every stock overlay guard range.
write_bytes(changed_disc,changed_bytes);
check(!PSXRecompV4::mod_runtime_commit_for_direct_netplay(changed_disc,&error),"final LAN commit rejects same-path changed source identity");
check(PSXRecompV4::mod_runtime_session_plan_fp().empty(),"changed offer cannot install session identity");
check(!PSXRecompV4::mod_runtime_savestate_scope(save_scope,&error),
"failed required plan cannot select shared save-state slots");
check(PSXRecompV4::mod_runtime_initialize(folder,"READER",0,{},&error),"restore original overlay selection");
check(PSXRecompV4::mod_runtime_prepare_for_launcher(raw_path,&error),"prepare verified resource-backed overlay");
check(PSXRecompV4::mod_runtime_savestate_scope(save_scope,&error) && save_scope==offer_fp,
"offline isolated plan restores the same verified save-state scope");
mod_runtime_enable_disc_patches();
fs::remove(pack_path,ec); // Committed bytes must remain immutable and resident.
check(PSXRecompV4::mod_runtime_savestate_scope(save_scope,&error) && save_scope==offer_fp,
"isolated scope uses committed immutable content after source removal");
for(unsigned lba=21;lba<=23;++lba) {
std::array<uint8_t,2352> sector;
std::copy_n(raw_iso.begin()+lba*2352,2352,sector.begin());
auto wanted=sector;
const uint64_t start=std::max<uint64_t>(offset,lba*2352);
const uint64_t end=std::min<uint64_t>(offset+payload.size(),(lba+1)*2352);
std::copy(payload.begin()+start-offset,payload.begin()+end-offset,wanted.begin()+start-lba*2352);
mod_runtime_patch_disc_sector(lba,1,sector.data(),sector.size());
check(sector==wanted,"resource overlay crosses sectors with exact untouched surroundings");
}
}
/* Verified bytes are scoped to the committed feature, and do not depend
* on reopening an owner file after launch. No launcher is needed here. */
const auto media_root = root / "verified-media";
std::vector<uint8_t> rom(64, 0);
rom[0] = 0x80; rom[1] = 0x37; rom[2] = 0x12; rom[3] = 0x40;
rom[32] = 0x5a;
const auto rom_path = media_root / "owner.z64";
write_bytes(rom_path, rom);
std::vector<uint8_t> xa(2336*2);
xa[2]=xa[6]=0x64;xa[8]=0x52;
xa[2336+2]=xa[2336+6]=0xE4;xa[2336+8]=0x63;
const auto xa_path=media_root/"owner.xa";write_bytes(xa_path,xa);
write_text(media_root / "packages/media.test/1.0.0/manifest.toml",
"format_version = 8\nid = \"media.test\"\nversion = \"1.0.0\"\nname = \"Media Test\"\n"
"[[target]]\ngame_id = \"READER\"\n"
"[[feature]]\nid = \"active\"\nname = \"Active\"\ndefault_enabled = true\n"
"[[feature]]\nid = \"private\"\nname = \"Private\"\n"
"[[resource]]\nfeature = \"active\"\nid = \"rom\"\nlabel = \"ROM\"\n"
"format = \"n64-rom\"\nrequired = true\nsize = 64\nsha256 = \"" + sha256_hex(rom) + "\"\n"
"[[resource]]\nfeature = \"active\"\nid = \"xa\"\nlabel = \"XA\"\n"
"format = \"file\"\nrequired = true\nsize = 4672\nsha256 = \"" + sha256_hex(xa) + "\"\n"
"[[resource]]\nfeature = \"private\"\nid = \"private\"\nlabel = \"Private\"\n"
"format = \"n64-rom\"\nrequired = true\nsize = 64\nsha256 = \"" + sha256_hex(rom) + "\"\n"
"[[plugin]]\nfeature = \"active\"\nid = \"media.test.plugin\"\n");
write_text(media_root / "state.toml",
"format_version = 2\n[[feature]]\npackage_id = \"media.test\"\nid = \"active\"\nenabled = true\n"
"[feature.resources]\nrom = \"" + rom_path.generic_string() + "\"\nxa = \"" + xa_path.generic_string() + "\"\n");
check(psx_mod_register_activation_plugin("media.test.plugin", test_media_plugin), "register media consumer");
check(PSXRecompV4::mod_runtime_initialize(media_root, "READER", 0, {}, &error), "media initialize");
check(PSXRecompV4::mod_runtime_commit(iso_path, &error), "verify and commit owner media");
{
std::string save_scope;
check(PSXRecompV4::mod_runtime_savestate_scope(save_scope,&error) && save_scope.empty(),
"shared packages retain historical save-state paths");
const PSXModImmediateSite site = {0x80020000u, 0x28420020u, 1u, 2u, {64, 128}};
psx_mod_immediate_set_sites(&site, 1);
check(psx_mod_register_session_reset_plugin("immediate.test.reset",test_immediate_reset),
"register reset-boundary probe");
immediate_activation_choice = 1;
psx_mod_immediate_begin_session(0);
mod_runtime_activate_plugins();
check(!immediate_reset_accepted && psx_mod_immediate_select(1)==1,
"cleanup cannot select; active plugin selects64");
std::string scope64, scope128;
check(PSXRecompV4::mod_runtime_savestate_scope(scope64,&error) && scope64.size()==64,
"non-stock selectors isolate even shared-package savestates");
check(!psx_mod_set_immediate_selector(1, 2), "live selection refused");
check(PSXRecompV4::mod_runtime_savestate_scope(save_scope,&error) && save_scope==scope64,
"failed live selection preserves current state compatibility");
immediate_activation_choice = 2;
psx_mod_immediate_begin_session(0);
mod_runtime_activate_plugins();
check(psx_mod_immediate_select(1)==2, "next boot selects128");
check(PSXRecompV4::mod_runtime_savestate_scope(scope128,&error) && scope128.size()==64 && scope128!=scope64,
"128-slot session cannot read64-slot state namespace");
psx_mod_immediate_begin_session(1);
mod_runtime_activate_plugins();
check(!psx_mod_immediate_session_valid() && psx_mod_immediate_select(1)==0,
"online activation cannot enable selectors even if plugin ignores refusal");
immediate_activation_choice = 0;
psx_mod_immediate_begin_session(0);
mod_runtime_activate_plugins();
check(PSXRecompV4::mod_runtime_savestate_scope(save_scope,&error) && save_scope.empty(),
"stock selectors restore historical paths");
psx_mod_immediate_reset();
psx_mod_immediate_set_sites(nullptr, 0);
}
const std::string host_media_fp = PSXRecompV4::mod_runtime_plan_fingerprint_portable();
check(host_media_fp.size() == 64, "verified external media has a portable netplay identity");
test_match_caps = {};
check(PSXRecompV4::mod_runtime_commit_for_direct_netplay(iso_path, &error), "LAN retains verified selected content");
check(PSXRecompV4::mod_runtime_session_plan_fp() == host_media_fp, "LAN announces full portable plan identity");
PSXRecompV4::mod_runtime_end_netplay();
check(PSXRecompV4::mod_runtime_session_plan_fp().empty(), "LAN return retires session identity");
const auto guest_rom_path = media_root / "different-machine" / "guest.z64";
write_bytes(guest_rom_path, rom);
const std::string guest_state =
"format_version = 2\n[[feature]]\npackage_id = \"media.test\"\nid = \"active\"\nenabled = false\n"
"[feature.resources]\nrom = \"" + guest_rom_path.generic_string() + "\"\nxa = \"" + xa_path.generic_string() + "\"\n";
write_text(media_root / "state.toml", guest_state);
auto read_guest_state = [&]() {
std::ifstream in(media_root / "state.toml", std::ios::binary);
return std::string(std::istreambuf_iterator<char>(in), {});
};
const std::string guest_state_bytes = read_guest_state();
check(PSXRecompV4::mod_runtime_initialize(media_root, "READER", 0, {}, &error), "guest media initialize");
test_match_caps.valid = 1;
test_match_caps.mod_count = 1;
std::snprintf(test_match_caps.mods[0].id, sizeof(test_match_caps.mods[0].id), "media.test");
std::snprintf(test_match_caps.mods[0].ver, sizeof(test_match_caps.mods[0].ver), "1.0.0");
std::snprintf(test_match_caps.mods[0].feats, sizeof(test_match_caps.mods[0].feats), "active");
std::snprintf(test_match_caps.mod_plan_fp, sizeof(test_match_caps.mod_plan_fp), "%s", host_media_fp.c_str());
check(PSXRecompV4::mod_runtime_commit_for_netplay(iso_path, &error), "host plan enables guest media with different local paths");
check(PSXRecompV4::mod_runtime_plan_fingerprint_portable() == host_media_fp, "guest content identity matches host");
check(read_guest_state() == guest_state_bytes, "online plan preserves offline selection and resource paths byte for byte");
const std::string committed_fp = PSXRecompV4::mod_runtime_fingerprint();
test_match_caps.mod_plan_fp[0] = host_media_fp[0] == 'a' ? 'b' : 'a';
check(!PSXRecompV4::mod_runtime_commit_for_netplay(iso_path, &error), "mismatched host content fingerprint refused");
check(PSXRecompV4::mod_runtime_fingerprint() == committed_fp, "mismatch leaves previous committed plan intact");
test_match_caps.mod_plan_fp[0] = '\0';
check(!PSXRecompV4::mod_runtime_commit_for_netplay(iso_path, &error), "missing host fingerprint refused");
std::snprintf(test_match_caps.mod_plan_fp, sizeof(test_match_caps.mod_plan_fp), "%s", host_media_fp.c_str());
auto bad_rom = rom; bad_rom[33] ^= 1; write_bytes(guest_rom_path, bad_rom);
check(!PSXRecompV4::mod_runtime_commit_for_netplay(iso_path, &error), "changed donor media refused before online launch");
check(read_guest_state() == guest_state_bytes, "failed online launch leaves offline state untouched");
write_bytes(guest_rom_path, rom);
check(PSXRecompV4::mod_runtime_commit_for_netplay(iso_path, &error), "valid donor media recovers after refused launch");
const uint8_t* unavailable = nullptr;
uint64_t unavailable_size = 0;
check(!psx_mod_current_resource_bytes("rom", &unavailable, &unavailable_size), "bytes unavailable outside plugin callback");
mod_runtime_activate_plugins();
check(media_access && media_size == rom.size() &&
std::equal(rom.begin(), rom.end(), media_snapshot), "plugin receives exact verified bytes");
check(!foreign_media_access, "inactive feature cannot supply bytes to another feature");
check(extent_ok && mounted_lba==24 && !invalid_extent && !foreign_extent,"activation mounts only owned bounded resource extents");
check(audio_ok && audio_lba==150 && audio_lba2==151 && !audio_invalid &&
!audio_foreign && audio_data_rejected,"audio playlist validates bounds, ownership and audio source kind");
uint32_t bad_lba=1;
check(!psx_mod_append_disc_extent("xa",0,1,&bad_lba)&&!bad_lba,"late mounting refused");
void* mounted=iso_open(iso_path.string().c_str());
check(mounted&&iso_sector_count(mounted)==24,"BIOS sees original disc before activation is enabled");
check(iso_cdda_track_count(mounted)==1,"audio override is invisible during BIOS boot");
mod_runtime_enable_disc_patches();
check(iso_cdda_track_count(mounted)==3 && iso_cdda_track_start_lba(mounted,2)==150 &&
iso_cdda_track_start_lba(mounted,3)==151 && iso_cdda_sector_count(mounted)==152 &&
!iso_cdda_track_is_audio(mounted,1) && iso_cdda_track_is_audio(mounted,3) &&
!iso_cdda_track_is_audio(mounted,4),"audio-only TOC and lead-out stay separate from XA extents");
check(iso_read_cdda_sector(mounted,150,sector.data(),sector.size()) &&
std::equal(sector.begin(),sector.begin()+2352,xa.begin()),"lossless external CD audio sector");
check(iso_read_cdda_sector(mounted,151,sector.data(),sector.size()) &&
std::equal(sector.begin(),sector.begin()+2352,xa.begin()+2320),"audio track boundary resolves next resource slice");
check(!iso_read_cdda_sector(mounted,152,sector.data(),sector.size()) &&
!iso_read_cdda_sector(mounted,149,sector.data(),sector.size()) &&
!iso_read_cdda_sector(mounted,150,sector.data(),2351),"audio end, data placeholder and short buffer rejected");
std::array<uint8_t,12> subq{};int valid=0;
check(iso_sector_count(mounted)==26&&iso_track_count(mounted)==2&&
iso_track_start_lba(mounted,2)==24&&!iso_track_is_audio(mounted,2),"appended extents expose one data track and leadout");
check(iso_read_raw_sector(mounted,24,sector.data(),sector.size())&&sector[12]==0&&sector[13]==2&&sector[14]==0x24&&sector[15]==2&&
std::equal(xa.begin(),xa.begin()+2336,sector.begin()+16),"native raw sector header and exact XA subheader/audio bytes");
check(iso_read_sector(mounted,25,sector.data(),sector.size())&&sector[0]==0x63,"cooked access resolves the same extent");
check(!iso_read_sector(mounted,26,sector.data(),sector.size())&&
!iso_read_raw_sector(mounted,24,sector.data(),2351),"extent end and short raw buffers rejected");
check(iso_read_subq(mounted,24,subq.data(),subq.size(),&valid)&&valid&&subq[0]==0x41&&subq[1]==2&&subq[5]==0,"appended data track has native subchannel position");
check(iso_read_sector(mounted,22,sector.data(),sector.size())&&sector[1]==7,"base sectors unchanged");
xa[8]=0xff;write_bytes(xa_path,xa);
check(iso_read_raw_sector(mounted,24,sector.data(),sector.size())&&sector[24]==0x52,"streaming uses verified snapshot after external file changes");
mod_runtime_activate_plugins();
check(mounted_lba==24&&iso_sector_count(mounted)==26,"reactivation has stable LBAs and no duplicate extents");
check(iso_cdda_track_count(mounted)==3 && iso_cdda_sector_count(mounted)==152 &&
iso_read_cdda_sector(mounted,150,sector.data(),sector.size()) && sector[8]==0x52,
"reactivation retains audio identity and immutable snapshot");
rom[32] ^= 1;
write_bytes(guest_rom_path, rom);
check(media_snapshot && media_snapshot[32] == 0x5a, "committed bytes survive owner file changes");
check(!PSXRecompV4::mod_runtime_commit({}, &error), "new commit rejects modified media");
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), "netplay clears donor plan");
check(iso_sector_count(mounted)==24&&iso_track_count(mounted)==1&&
!iso_read_raw_sector(mounted,24,sector.data(),sector.size()),"clearing plan removes all donor sectors and restores TOC");
check(iso_cdda_track_count(mounted)==1 && iso_cdda_sector_count(mounted)==24,
"clearing plan also restores original audio TOC");
iso_close(mounted);
PSXRecompV4::mod_runtime_end_netplay();
check(PSXRecompV4::mod_runtime_session_plan_fp().empty(), "leaving online clears host fingerprint");
check(PSXRecompV4::mod_runtime_commit(iso_path, &error), "offline commit after netplay uses original disabled media selection");
mod_runtime_activate_plugins();
const uint8_t* ended_bytes = nullptr; uint64_t ended_size = 0;
check(!psx_mod_current_resource_bytes("rom", &ended_bytes, &ended_size), "online media is retired after session");
{
PSXRecompV4::ModPackageManager offline;
offline.set_root(media_root);
check(offline.scan(&error) && offline.load_state(&error) && !offline.feature_enabled("media.test", "active"),
"later offline save keeps original disabled feature rather than host selection");
}
/* Path-only files are still usable offline, but never portable online. */
const auto loose_root = root / "unverified-media";
write_text(loose_root / "packages/loose.test/1.0.0/manifest.toml",
"format_version = 8\nid = \"loose.test\"\nversion = \"1.0.0\"\nname = \"Loose\"\n"
"[[target]]\ngame_id = \"READER\"\n[[feature]]\nid = \"active\"\nname = \"Active\"\ndefault_enabled = true\n"
"[[resource]]\nfeature = \"active\"\nid = \"file\"\nlabel = \"File\"\nformat = \"file\"\nrequired = true\n");
write_text(loose_root / "state.toml",
"format_version = 2\n[[feature]]\npackage_id = \"loose.test\"\nid = \"active\"\nenabled = true\n"
"[feature.resources]\nfile = \"" + rom_path.generic_string() + "\"\n");
check(PSXRecompV4::mod_runtime_initialize(loose_root, "READER", 0, {}, &error), "loose resource initialize");
check(PSXRecompV4::mod_runtime_commit(iso_path, &error), "unverified file remains usable offline");
check(PSXRecompV4::mod_runtime_plan_fingerprint_portable().empty(), "unverified resource has no online fingerprint");
const auto rejected_commit_timing = launch_timing_total();
check(!PSXRecompV4::mod_runtime_commit(iso_path, &error, false), "online commit rejects unverified resource");
// Netplay remeasures the source in both worker and legacy configurations.
check_commit_timing(rejected_commit_timing, false, true);
check(!PSXRecompV4::mod_runtime_commit_for_direct_netplay(iso_path, &error), "LAN also refuses unverified resources");
test_match_caps = {};
test_match_caps.valid = 1;
check(PSXRecompV4::mod_runtime_commit_for_netplay(iso_path, &error), "explicit empty host plan remains vanilla");
// A guest may own the source without ever enabling/preparing the host's
// feature offline. Preparation must precede the portable hash comparison.
{
const auto prepared_root = root / "prepared-netplay";
const auto prepared_asset = prepared_root / "generated" / "rom.z64";
int prepare_calls = 0;
check(PSXRecompV4::mod_register_media_preparer("test.netplay.prepare",
[&](const PSXRecompV4::ModPrepareContext&,
std::map<std::string, fs::path>& output, std::string&) {
++prepare_calls; write_bytes(prepared_asset, rom);
output["rom"] = prepared_asset; return true;
}), "register netplay preparation fixture");
write_text(prepared_root / "packages/prepared.test/1.0.0/manifest.toml",
"format_version = 9\nid = \"prepared.test\"\nversion = \"1.0.0\"\nname = \"Prepared\"\nprepare = \"test.netplay.prepare\"\n"
"[[target]]\ngame_id = \"READER\"\n[[feature]]\nid = \"active\"\nname = \"Active\"\ndefault_enabled = true\n"
"[[resource]]\nfeature = \"active\"\nid = \"rom\"\nlabel = \"ROM\"\nformat = \"n64-rom\"\nhidden = true\nrequired = true\nsize = 64\nsha256 = \"" + sha256_hex(rom) + "\"\n");
PSXRecompV4::mod_runtime_end_netplay(); test_match_caps = {};
check(PSXRecompV4::mod_runtime_initialize(prepared_root, "READER", 0, {}, &error) &&
PSXRecompV4::mod_runtime_commit_for_netplay(iso_path, &error), "direct launch prepares its selected resources");
const std::string prepared_fp = PSXRecompV4::mod_runtime_session_plan_fp();
PSXRecompV4::mod_runtime_end_netplay();
const std::string disabled = "format_version = 2\n[[feature]]\npackage_id = \"prepared.test\"\nid = \"active\"\nenabled = false\n";
write_text(prepared_root / "state.toml", disabled);
check(PSXRecompV4::mod_runtime_initialize(prepared_root, "READER", 0, {}, &error), "unprepared guest initialize");
test_match_caps.valid = 1; test_match_caps.mod_count = 1;
std::snprintf(test_match_caps.mod_plan_fp, sizeof(test_match_caps.mod_plan_fp), "%s", prepared_fp.c_str());
std::snprintf(test_match_caps.mods[0].id, sizeof(test_match_caps.mods[0].id), "prepared.test");
std::snprintf(test_match_caps.mods[0].ver, sizeof(test_match_caps.mods[0].ver), "1.0.0");
std::snprintf(test_match_caps.mods[0].feats, sizeof(test_match_caps.mods[0].feats), "active");
check(PSXRecompV4::mod_runtime_commit_for_netplay(iso_path, &error), "host enables and prepares guest feature before verification");
check(prepare_calls == 2 && PSXRecompV4::mod_runtime_plan_fingerprint_portable() == prepared_fp,
"prepared guest resolves identical verified bytes");
std::ifstream saved(prepared_root / "state.toml");
check(std::string(std::istreambuf_iterator<char>(saved), {}) == disabled, "prepared online launch preserves disabled offline choice");
PSXRecompV4::mod_runtime_end_netplay(); test_match_caps = {};
}
// The audio timeline skips INDEX00 pregaps and maps into the unchanged
// mounted BIN, including a second audio track sharing the same file.
const auto audio_root=root/"disc-audio";
std::vector<uint8_t> pcm(8*2352);
for(unsigned i=0;i<pcm.size();++i)pcm[i]=uint8_t(i/2352+17);
const auto pcm_path=audio_root/"audio.bin";write_bytes(pcm_path,pcm);
const auto audio_cue_path=audio_root/"disc.cue";
write_text(audio_cue_path,"FILE \""+raw_path.generic_string()+"\" BINARY\n"
" TRACK 01 MODE2/2352\n INDEX 01 00:00:00\n"
"FILE \"audio.bin\" BINARY\n"
" TRACK 02 AUDIO\n INDEX 00 00:00:00\n INDEX 01 00:00:02\n"
" TRACK 03 AUDIO\n INDEX 00 00:00:05\n INDEX 01 00:00:06\n");
write_text(audio_root/"packages/audio.test/1.0.0/manifest.toml",
"format_version = 8\nid = \"audio.test\"\nversion = \"1.0.0\"\nname = \"Audio\"\n"
"[[target]]\ngame_id = \"READER\"\n[[feature]]\nid = \"active\"\nname = \"Active\"\ndefault_enabled = true\n"
"[[plugin]]\nfeature = \"active\"\nid = \"audio.test.plugin\"\n");
PSXRecompV4::mod_runtime_end_netplay();
check(psx_mod_register_activation_plugin("audio.test.plugin",test_disc_audio_plugin),"register mounted audio fixture");
check(PSXRecompV4::mod_runtime_initialize(audio_root,"READER",0,{},&error) &&
PSXRecompV4::mod_runtime_commit(audio_cue_path,&error),"mount audio fixture");
mod_runtime_activate_plugins();mod_runtime_enable_disc_patches();
mounted=iso_open(audio_cue_path.string().c_str());
check(mounted && iso_cdda_sector_count(mounted)==155 &&
iso_read_cdda_sector(mounted,150,sector.data(),sector.size()) &&
std::all_of(sector.begin(),sector.begin()+2352,[](auto b){return b==19;}),"first mounted audio track excludes pregap");
check(iso_read_cdda_sector(mounted,153,sector.data(),sector.size()) &&
std::all_of(sector.begin(),sector.begin()+2352,[](auto b){return b==23;}),"shared-file second track maps its INDEX01");
check(iso_read_sector(mounted,22,sector.data(),sector.size()) && sector[1]==7,"audio remapping preserves data reads");
check(!iso_read_cdda_sector(mounted,155,sector.data(),sector.size()),"mounted audio leadout is exclusive");
iso_close(mounted);
/* Plugin activation (native asset preparation) runs before the emulated
* drive's disc patches are enabled. Host reads must already return the
* committed plan's effective bytes, or a prepared cache would silently
* serve original assets under a modded plan. The drive path stays gated. */
{
const auto patched_root = root / "host-reader-patched";
const auto patched_iso = patched_root / "original.iso";
/* A byte past the original 3000-byte extent; a mod that grows the file
* through its directory record exposes it. */
std::vector<uint8_t> grown_iso = iso;
grown_iso[22 * 2048 + 3500] = 0x77;
write_bytes(patched_iso, grown_iso);
write_text(patched_root / "packages/reader.patch/1.0.0/manifest.toml",
"format_version = 5\n"
"id = \"reader.patch\"\n"
"version = \"1.0.0\"\n"
"name = \"Reader Patch\"\n"
"[[target]]\n"
"game_id = \"READER\"\n"
"disc_sha256 = \"" + sha256_hex(grown_iso) + "\"\n"
"[[feature]]\n"
"id = \"asset\"\n"
"name = \"Asset\"\n"
"[[feature]]\n"
"id = \"grow\"\n"
"name = \"Grow\"\n"
"[[patch]]\n"
"feature = \"grow\"\n"
"target = \"disc_user\"\n"
"offset = " + std::to_string(21 * 2048 + 10) + "\n"
"expected = \"b80b0000\"\n"
"replace = \"a00f0000\"\n"
"[[feature]]\n"
"id = \"huge\"\n"
"name = \"Huge\"\n"
"[[patch]]\n"
"feature = \"huge\"\n"
"target = \"disc_user\"\n"
"offset = " + std::to_string(21 * 2048 + 10) + "\n"
"expected = \"b80b0000\"\n"
"replace = \"00f82204\"\n"
"[[patch]]\n"
"feature = \"asset\"\n"
"target = \"disc_user\"\n"
"offset = " + std::to_string(22 * 2048 + 5) + "\n"
"expected = \"23\"\n"
"replace = \"5a\"\n");
write_text(patched_root / "state.toml",
"format_version = 2\n"
"[[feature]]\n"
"package_id = \"reader.patch\"\n"
"id = \"asset\"\n"
"enabled = true\n"
"[[feature]]\n"
"package_id = \"reader.patch\"\n"
"id = \"grow\"\n"
"enabled = true\n");
check(PSXRecompV4::mod_runtime_initialize(patched_root, "READER", 0, {}, &error),
"patched reader initialize");
check(PSXRecompV4::mod_runtime_commit(patched_iso, &error), "patched reader commit");
check(psx_mod_read_disc_file("S0/LEVEL.NSF", nullptr, 0, &bytes) && bytes == 4000,
"host disc reads resolve paths through patched directory records");
std::vector<uint8_t> effective(4000);
check(psx_mod_read_disc_file("S0/LEVEL.NSF", effective.data(),
(uint32_t)effective.size(), &bytes) &&
bytes == 4000 && effective[5] == 0x5a && effective[3500] == 0x77 &&
std::equal(effective.begin() + 6, effective.begin() + 3000,
iso.begin() + 22 * 2048 + 6),
"host disc reads apply the committed plan before the drive is enabled");
check(!psx_mod_read_disc_file("S0", nullptr, 0, &bytes),
"effective lookup still rejects directories");
std::array<uint8_t, 2048> drive_sector{};
std::copy_n(iso.begin() + 22 * 2048, 2048, drive_sector.begin());
mod_runtime_patch_disc_sector(22, 0, drive_sector.data(), (uint32_t)drive_sector.size());
check(drive_sector[5] == 0x23,
"the emulated drive path stays original until disc patches are enabled");
mod_runtime_enable_disc_patches();
mod_runtime_patch_disc_sector(22, 0, drive_sector.data(), (uint32_t)drive_sector.size());
check(drive_sector[5] == 0x5a, "enabled drive path applies the same plan");
/* Grown archives may exceed 64 MiB (an extended MMX6 ROCK_X6.DAT is
* 69,400,576 bytes); the bound is CD capacity, not a fixed cap. */
write_text(patched_root / "state.toml",
"format_version = 2\n"
"[[feature]]\n"
"package_id = \"reader.patch\"\n"
"id = \"huge\"\n"
"enabled = true\n");
check(PSXRecompV4::mod_runtime_initialize(patched_root, "READER", 0, {}, &error) &&
PSXRecompV4::mod_runtime_commit(patched_iso, &error), "huge plan commit");
check(psx_mod_read_disc_file("S0/LEVEL.NSF", nullptr, 0, &bytes) && bytes == 69400576u,
"files grown past 64 MiB keep their effective size");
}
{
const fs::path hd_mods = root / "hd-mods";
write_text(hd_mods / "bundled" / "runtime.hd" / "1.0.0" / "manifest.toml",
"format_version = 5\nid = \"runtime.hd\"\nversion = \"1.0.0\"\n"
"name = \"HD\"\nresolver = \"declarative\"\nsave_compatibility = \"shared\"\n"
"[[target]]\ngame_id = \"SCUS-94236\"\n"
"[[feature]]\nid = \"textures\"\nname = \"HD\"\ndefault_enabled = false\n"
"[[resource]]\nfeature = \"textures\"\nid = \"pack\"\nlabel = \"Pack\"\n"
"format = \"directory\"\nrequired = true\n"
"[[option]]\nfeature = \"textures\"\nid = \"replacements\"\n"
"label = \"Load\"\ntype = \"boolean\"\ndefault = \"true\"\n"
"[[option]]\nfeature = \"textures\"\nid = \"dump\"\n"
"label = \"Dump\"\ntype = \"boolean\"\ndefault = \"false\"\n"
"[[plugin]]\nfeature = \"textures\"\nid = \"psx.hd-textures\"\n");
write_text(hd_mods / "state.toml",
"format_version = 2\n[[feature]]\npackage_id = \"runtime.hd\"\n"
"id = \"textures\"\nenabled = true\n[feature.values]\ndump = \"true\"\n");
check(psx_mod_register_activation_plugin("psx.hd-textures", test_hd_activation),
"HD activation plugin registers");
check(PSXRecompV4::mod_runtime_initialize(hd_mods, "SCUS-94236", 0, {}, &error),
"HD mods initialize");
const fs::path expected = fs::absolute(hd_mods / "texture-packs" / "SCUS-94236");
check(fs::is_directory(expected / "dumps") && fs::is_directory(expected / "replacements"),
"default HD directories exist before first launch, even without dumping");
check(PSXRecompV4::mod_runtime_commit(stock_path, &error),
"default pack resource commits without manual folder selection");
const int shutdown_before = hd_shutdown_calls;
mod_runtime_activate_plugins();
check(hd_shutdown_calls == shutdown_before + 1 && hd_configure_calls == 1 &&
hd_root == expected.string() && hd_replacements_value == 1 && hd_dump_value == 1,
"HD activation clears previous session and reads owning committed options");
char value[16] = "old";
check(!psx_mod_current_option_value("dump", value, sizeof(value)) && value[0] == '\0',
"owning option context does not escape the trusted callback");
check(!psx_mod_set_hd_texture_pack("pack", 1, 1),
"pack configuration requires the trusted resource callback context");
hd_replacements_value = 0;
check(psx_mod_set_hd_texture_dump(0) && hd_dump_value == 0,
"live dump control forwards the requested switch");
check(!gpu_hd_textures_active() && psx_mod_set_hd_texture_dump(1) && hd_dump_value == 1,
"dump-only session can turn capture back on after turning it off");
check(psx_mod_reload_hd_texture_pack() && hd_reload_calls == 1,
"pack reload forwards to renderer");
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), "HD plan clears");
mod_runtime_activate_plugins();
check(hd_shutdown_calls == shutdown_before + 2 && hd_configure_calls == 1,
"disabled HD feature leaves no previous session pack active");
check(!psx_mod_set_hd_texture_dump(1), "live dump control refuses an unconfigured session");
const fs::path custom = hd_mods / fs::u8path(u8"chosen pack # % \u65e5\u672c");
fs::create_directories(custom);
write_text(hd_mods / "state.toml",
"format_version = 2\n[[feature]]\npackage_id = \"runtime.hd\"\n"
"id = \"textures\"\nenabled = true\n[feature.resources]\npack = \"" +
custom.generic_u8string() + "\"\n");
check(PSXRecompV4::mod_runtime_initialize(hd_mods, "SCUS-94236", 0, {}, &error) &&
PSXRecompV4::mod_runtime_commit(stock_path, &error),
"chosen Unicode pack path resolves and persists through commit");
mod_runtime_activate_plugins();
check(hd_root == custom.generic_u8string(), "renderer receives the chosen folder as UTF-8");
check(PSXRecompV4::mod_runtime_initialize(hd_mods, "SCUS-94236", 0, {}, &error) &&
PSXRecompV4::mod_runtime_commit(stock_path, &error),
"committed Unicode pack path reloads without corruption");
mod_runtime_activate_plugins();
check(hd_root == custom.generic_u8string(), "Unicode pack root survives the complete session cycle");
}
/* A compiled game hook survives mod-plan clearing, but runs only online. */
check(psx_game_register_netplay_function_entry(
0x80004000u, test_game_netplay_entry) == 1,
"trusted game netplay entry must register");
check(psx_game_register_netplay_function_entry(
0x80004000u, test_game_netplay_entry) == 1,
"duplicate game netplay registration must be idempotent");
check(psx_game_register_netplay_function_filter(
0x80005000u, test_game_netplay_filter) == 1,
"trusted game netplay filter must register");
CPUState game_cpu{};
psx_mod_function_entry(&game_cpu, 0x80004000u);
game_cpu.gpr[31] = 0x80006000u;
check(!psx_mod_function_entry(&game_cpu, 0x80005000u),
"game netplay filter must not consume offline function");
check(game_netplay_entry_hits == 0,
"game netplay hook must not run offline");
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), error.c_str());
test_netplay_active = 1;
psx_mod_function_entry(&game_cpu, 0xA0004000u);
check(game_netplay_entry_hits == 1 && g_psx_mod_function_entry_hooks == 2,
"game hook must survive netplay mod clear and match code aliases");
check(psx_mod_function_entry(&game_cpu, 0xA0005000u) == 1 &&
game_cpu.pc == 0x80006000u && game_cpu.gpr[2] == 0x1234u &&
game_netplay_filter_hits == 1,
"game netplay filter must consume call and resume at return PC");
psx_mod_function_entry(&game_cpu, 0x80004004u);
check(game_netplay_entry_hits == 1,
"game hook must ignore other addresses");
test_netplay_active = 0;
psx_mod_function_entry(&game_cpu, 0x80004000u);
check(game_netplay_entry_hits == 1,
"game hook must disarm when netplay stops");
check(!psx_mod_function_entry(&game_cpu, 0x80005000u) &&
game_netplay_filter_hits == 1,
"game filter must disarm when netplay stops");
// Reliable identity detects byte replacement even when size/mtime are restored.
{
const auto identity_path = root / "identity.bin";
write_bytes(identity_path, {1, 2, 3, 4});
const auto original_time = fs::last_write_time(identity_path);
std::string before, after;
const bool reliable = PSXRecompV4::host_file_identity(identity_path, before);
write_bytes(identity_path, {4, 3, 2, 1});
fs::last_write_time(identity_path, original_time);
check(!reliable || (PSXRecompV4::host_file_identity(identity_path, after) && before != after),
"file identity rejects same-size restored-mtime replacement");
check(!PSXRecompV4::host_file_identity(root, after), "directories are never file cache identities");
check(!PSXRecompV4::host_file_identity(root / "absent.bin", after), "missing identity is a cache miss");
}
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
{
const auto cached_root = root / "audited-cache";
const auto output_path = root / "audited-output.z64";
const auto input_path = root / "audited-input.bin";
const auto disc = root / "audited-disc.iso";
fs::copy_file(iso_path, disc, fs::copy_options::overwrite_existing);
write_bytes(input_path, {1,2,3,4});
std::string input_receipt, output_receipt;
int prepare_calls = 0;
bool throw_probe = false, mutate_on_resolve = false, throw_resolver = false;
const std::string manifest =
"format_version = 9\nid = \"cache.test\"\nversion = \"1.0.0\"\nname = \"Cache\"\nprepare = \"test.cache.prepare\"\nresolver = \"builtin:test.cache.resolve\"\n"
"[[target]]\ngame_id = \"READER\"\n"
"[[feature]]\nid = \"active\"\nname = \"Active\"\ndefault_enabled = true\n"
"[[resource]]\nfeature = \"active\"\nid = \"source\"\nlabel = \"Source\"\ninput_only = true\nshared_source = \"cache.original\"\nrequired = true\n"
"[[resource]]\nfeature = \"active\"\nid = \"rom\"\nlabel = \"ROM\"\nformat = \"n64-rom\"\nhidden = true\nrequired = true\nsize = 64\nsha256 = \"" + sha256_hex(rom) + "\"\n"
"[[feature]]\nid = \"second\"\nname = \"Second\"\ndefault_enabled = false\n"
"[[resource]]\nfeature = \"second\"\nid = \"rom\"\nlabel = \"ROM\"\nformat = \"n64-rom\"\nhidden = true\nrequired = true\nsize = 64\nsha256 = \"" + sha256_hex(rom) + "\"\n";
check(PSXRecompV4::mod_register_builtin_resolver("test.cache.resolve",
[&](const auto&, const auto&, const auto&, auto&, auto&) {
if (throw_resolver) throw std::runtime_error("resolver fixture exception");
if (mutate_on_resolve) write_bytes(input_path, {7,7,7,7});
return true;
}), "register resolver for mutation and unwind regression");
const auto manifest_path = cached_root / "packages/cache.test/1.0.0/manifest.toml";
write_text(manifest_path, manifest);
check(PSXRecompV4::mod_register_media_preparer("test.cache.prepare",
[&](const PSXRecompV4::ModPrepareContext&, std::map<std::string, fs::path>& outputs, std::string&) {
++prepare_calls;
write_bytes(output_path, rom);
PSXRecompV4::host_file_identity(input_path, input_receipt);
PSXRecompV4::host_file_identity(output_path, output_receipt);
outputs["rom"] = output_path;
return true;
},
[&](const PSXRecompV4::ModPrepareContext& context, std::map<std::string, fs::path>& outputs, std::string&) {
if (throw_probe) throw std::runtime_error("probe fixture exception");
std::string input, output;
if (context.feature_id == "second" ||
!PSXRecompV4::host_file_identity(input_path, input) || input != input_receipt ||
!PSXRecompV4::host_file_identity(output_path, output) || output != output_receipt) return false;
outputs["rom"] = output_path; return true;
}), "register audited preparation and cheap identity probe");
PSXRecompV4::ModPackageManager source_selection(cached_root);
check(source_selection.scan(&error) &&
source_selection.set_feature_resource_path("cache.test", "active", "source", input_path, &error) &&
source_selection.save_state(&error), "persist shared input selection for audited fixture");
PSXRecompV4::mod_runtime_end_netplay(); test_match_caps = {};
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error), "audited initialize");
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc) && prepare_calls == 0,
"cold cached-only launch does not prepare missing receipt");
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) && prepare_calls == 1,
"cold launcher creates verified preparation");
std::string input_stamp, output_stamp, disc_stamp, manifest_stamp;
const bool qualified_stamps = PSXRecompV4::host_file_identity(input_path,input_stamp) &&
PSXRecompV4::host_file_identity(output_path,output_stamp) &&
PSXRecompV4::host_file_identity(disc,disc_stamp) &&
PSXRecompV4::host_file_identity(manifest_path,manifest_stamp);
if (qualified_stamps) {
const auto reused = startup_counter("startup.plan_reuse");
const auto hashed = startup_counter("startup.disc_hash");
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) &&
PSXRecompV4::mod_runtime_commit(disc, &error) && prepare_calls == 1 &&
startup_counter("startup.plan_reuse") == reused + 2 && startup_counter("startup.disc_hash") == hashed,
"duplicate provider and main commit reuse a prepared plan without converter or hash");
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error) &&
PSXRecompV4::mod_runtime_try_prepare_cached(disc) && prepare_calls == 1 &&
startup_counter("startup.disc_hash") == hashed,
"warm initialize preloads receipts with no preparer or raw hash");
mod_runtime_activate_plugins();
check(PSXRecompV4::mod_runtime_commit(disc, &error) && prepare_calls == 2,
"activation invalidates the preboot ticket");
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error) &&
PSXRecompV4::mod_runtime_try_prepare_cached(disc), "restore warm plan for source invalidation");
const auto input_time = fs::last_write_time(input_path);
write_bytes(input_path, {4,3,2,1}); fs::last_write_time(input_path, input_time);
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc), "external shared input replacement invalidates title receipt");
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) && prepare_calls == 3,
"changed source goes through full preparation");
auto corrupt_rom = rom; corrupt_rom[32] ^= 1; write_bytes(output_path, corrupt_rom);
// Even if the probe claims the file is current, the first warm resolve
// must verify output bytes against the declared SHA.
PSXRecompV4::host_file_identity(output_path, output_receipt);
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error) &&
!PSXRecompV4::mod_runtime_try_prepare_cached(disc) && prepare_calls == 3,
"warm output corruption fails immutable-byte verification");
fs::remove(output_path);
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc) && prepare_calls == 3,
"missing output fails without invoking converter");
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) && prepare_calls == 4,
"full preparation repairs missing output");
} else {
const auto hashed = startup_counter("startup.disc_hash");
const auto reused = startup_counter("startup.plan_reuse");
const auto before = prepare_calls;
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc,&error) &&
PSXRecompV4::mod_runtime_commit(disc,&error) && prepare_calls==before+2 &&
startup_counter("startup.disc_hash")==hashed+2 &&
startup_counter("startup.plan_reuse")==reused,
"unqualified identities retain full preparation and hashing without tickets");
check(PSXRecompV4::mod_runtime_initialize(cached_root,"READER",0,{},&error) &&
!PSXRecompV4::mod_runtime_try_prepare_cached(disc) && prepare_calls==before+2,
"unqualified warm startup never invokes the converter or claims readiness");
const auto input_time=fs::last_write_time(input_path);
write_bytes(input_path,{4,3,2,1});fs::last_write_time(input_path,input_time);
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc,&error) && prepare_calls==before+3,
"restored-mtime source still goes through full preparation");
auto corrupt=rom;corrupt[32]^=1;write_bytes(output_path,corrupt);
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc) && prepare_calls==before+3,
"unqualified corrupt output never obtains cached readiness");
fs::remove(output_path);
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc,&error) && prepare_calls==before+4,
"unqualified full preparation repairs missing content");
}
throw_probe = true;
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc), "throwing cached probe fails safely");
throw_probe = false;
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error), "cached flag restored after failure");
// Probe results are transactional across multiple enabled features.
PSXRecompV4::ModPackageManager transactional(cached_root);
check(transactional.scan(&error) && transactional.load_state(&error) &&
transactional.set_feature_enabled("cache.test", "second", true, &error) &&
transactional.set_feature_resource_path("cache.test", "active", "rom", rom_path, &error), "enable second probe fixture");
const auto original_output = transactional.feature_resource_path("cache.test", "active", "rom");
check(!transactional.prepare_resources("READER", disc, root / "cache", &error, true) &&
transactional.feature_resource_path("cache.test", "active", "rom") == original_output,
"later cache miss discards earlier pending output selections");
// A throw inside resolve must restore cached-only/provider modes.
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error), "invalidate ticket for resolver exception fixture");
throw_resolver = true;
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc),
"best-effort preload contains resolver exceptions before launcher UI");
throw_resolver = false;
fs::remove(output_path);
const auto before_unwind = prepare_calls;
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) && prepare_calls == before_unwind + 1,
"cached-only mode restored after resolve exception");
// A dependency changed during resolution must never publish readiness.
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error), "invalidate ticket for resolver mutation fixture");
const auto before_mutation = prepare_calls;
mutate_on_resolve = true;
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error), "full resolution can finish with concurrent input mutation");
mutate_on_resolve = false;
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) && prepare_calls == before_mutation + 2,
"unstable resolve never publishes a reusable ticket");
if (qualified_stamps) {
// Altering a syntactically valid digest must fail its receipt checksum.
std::filesystem::path cache_dir;
#if defined(_WIN32)
if (const char* local = std::getenv("LOCALAPPDATA")) cache_dir = local;
#else
if (const char* cache = std::getenv("XDG_CACHE_HOME")) cache_dir = cache;
else if (const char* home_dir = std::getenv("HOME")) cache_dir = fs::path(home_dir) / ".cache";
#endif
if (cache_dir.empty()) cache_dir = fs::temp_directory_path();
cache_dir /= "psxrecomp/imports/disc-digests-v2";
bool corrupted_receipt = false;
for (const auto& entry : fs::directory_iterator(cache_dir)) {
std::ifstream saved(entry.path(), std::ios::binary);
std::string receipt(std::istreambuf_iterator<char>(saved), {});
if (receipt.find(disc.generic_string()) == std::string::npos || receipt.size() < 130) continue;
const size_t digest_offset = receipt.size() - 130;
receipt[digest_offset] = receipt[digest_offset] == '0' ? '1' : '0';
write_text(entry.path(), receipt); corrupted_receipt = true;
}
check(corrupted_receipt && PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error) &&
!PSXRecompV4::mod_runtime_try_prepare_cached(disc), "corrupt well-formed digest receipt misses warm startup");
const auto before_corrupt_hash = startup_counter("startup.disc_hash");
check(PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) &&
startup_counter("startup.disc_hash") == before_corrupt_hash + 1,
"full launch repairs corrupted digest with authoritative source hash");
}
// Catalog changes cannot certify stale in-memory parsed manifests.
write_text(manifest_path, manifest + "\n# external catalog mutation\n");
check(!PSXRecompV4::mod_runtime_prepare_for_launcher(disc, &error) &&
error.find("catalog changed") != std::string::npos,
"changed manifest requires rescan instead of resealing stale package");
check(PSXRecompV4::mod_runtime_initialize(cached_root, "READER", 0, {}, &error), "explicit initialize rescans changed catalog");
const int before_netplay = prepare_calls;
const auto net_hash = startup_counter("startup.disc_hash");
check(PSXRecompV4::mod_runtime_commit_for_direct_netplay(disc, &error) &&
prepare_calls == before_netplay + 1 && startup_counter("startup.disc_hash") > net_hash,
"netplay ignores preboot ticket and hashes the source fully");
check(!PSXRecompV4::mod_runtime_try_prepare_cached(disc), "active netplay never preloads an offline ticket");
PSXRecompV4::mod_runtime_end_netplay();
// Legacy preparers have no cheap receipt and must never claim warm readiness.
const auto legacy_root = root / "legacy-cache";
int legacy_calls = 0;
check(PSXRecompV4::mod_register_media_preparer("test.legacy.prepare",
[&](const PSXRecompV4::ModPrepareContext&, std::map<std::string, fs::path>& outputs, std::string&) {
++legacy_calls; outputs["rom"] = output_path; return true;
}), "register legacy preparer without probe");
std::string legacy_manifest = manifest;
legacy_manifest.replace(legacy_manifest.find("test.cache.prepare"), std::string("test.cache.prepare").size(), "test.legacy.prepare");
write_text(legacy_root / "packages/cache.test/1.0.0/manifest.toml", legacy_manifest);
check(PSXRecompV4::mod_runtime_initialize(legacy_root, "READER", 0, {}, &error) &&
!PSXRecompV4::mod_runtime_try_prepare_cached(disc) && legacy_calls == 0,
"legacy preparer always misses cached-only startup");
}
#else
{
const auto defaults = root / "default-hashing";
fs::create_directories(defaults);
check(PSXRecompV4::mod_runtime_initialize(defaults, "READER", 0, {}, &error), "default hashing fixture initialize");
const auto hash_count = startup_counter("startup.disc_hash");
check(PSXRecompV4::mod_runtime_commit(iso_path, &error) &&
PSXRecompV4::mod_runtime_commit(iso_path, &error) &&
startup_counter("startup.disc_hash") == hash_count + 1,
"default runtime preserves hash-only-on-selected-path-change behavior");
check(!PSXRecompV4::mod_runtime_try_prepare_cached(iso_path), "default runtime does not opt into cached startup");
}
#endif
/* The executable-owned profile forces its trusted plugin, including hooks,
* without applying/saving any player's offline patches or selections. */
test_match_caps = {};
const bool missing_profile = argc > 1 && std::string(argv[1]) == "--missing-profile";
const bool missing_renderer = argc > 1 && std::string(argv[1]) == "--missing-renderer";
const bool option_renderer_missing = argc > 1 && std::string(argv[1]) == "--option-renderer-missing";
const bool profile_options = option_renderer_missing ||
(argc > 1 && std::string(argv[1]) == "--profile-options");
static const PSXModNetplayProfile profile = {
missing_profile ? "runtime.missing-simulation" : "runtime.test-vblank",
"runtime-coop-delay-v1", 0, 0, 1, 3u,
profile_options ? "runtime.option-renderer" : "runtime.missing-renderer", nullptr, 0,
profile_options ? "runtime.test" : nullptr,
profile_options ? "vblank-plugin" : nullptr,
profile_options ? "cameras" : nullptr,
profile_options ? "split" : nullptr
};
if (profile_options && !option_renderer_missing)
check(psx_mod_register_activation_plugin("runtime.option-renderer", option_renderer_activate),
"profile option renderer registration");
check(psx_mod_register_netplay_profile(&profile), "trusted profile registration");
check(PSXRecompV4::mod_runtime_initialize(root, "SLUS-RUNTIME", 0x80002000, {}, &error),
"profile reinitialize offline catalog");
if (missing_profile || missing_renderer || option_renderer_missing) {
if (missing_renderer) check(psx_mod_netplay_set_aspect(1), "select profile renderer");
if (option_renderer_missing) {
test_match_caps.valid = 1;
test_match_caps.mod_count = 1;
std::strcpy(test_match_caps.mods[0].id, "runtime.test");
std::strcpy(test_match_caps.mods[0].ver, "1.0.0");
std::strcpy(test_match_caps.mods[0].feats, "vblank-plugin=cameras~split");
std::string fingerprint;
check(PSXRecompV4::mod_runtime_netplay_settings_fingerprint(test_match_caps, fingerprint, &error),
"missing renderer offer still has a valid settings identity");
std::strcpy(test_match_caps.mod_plan_fp, fingerprint.c_str());
}
check(!PSXRecompV4::mod_runtime_commit_netplay(iso_path, &error) &&
!psx_mod_netplay_is_active() && !error.empty(),
"missing trusted plugin refuses profile activation with a reason");
check(psx_mod_netplay_savestates_supported(), "failed profile cannot latch snapshot policy");
const auto before_failed_activation = activation_calls;
mod_runtime_activate_plugins();
check(activation_calls == before_failed_activation, "failed profile cannot activate stale offline plugin");
fs::remove_all(root, ec);
return failures ? 1 : 0;
}
const auto selection_path = root / "state.toml";
auto read_selection = [&]() {
std::ifstream file(selection_path, std::ios::binary);
return std::string(std::istreambuf_iterator<char>(file), {});
};
std::string offline_selection = read_selection();
check(PSXRecompV4::mod_runtime_commit_for_direct_netplay(iso_path, &error), error.c_str());
check(psx_mod_netplay_is_active() && !psx_mod_netplay_savestates_supported(),
"profile activates host-state snapshot policy");
const int activations_before = activation_calls;
mod_runtime_activate_plugins();
check(activation_calls == activations_before + 1, "profile forces native plugin activation");
ram[0x1000] = 0x5a;
mod_runtime_on_dispatch(0x80002000);
check(ram[0x1000] == 0x5a, "profile excludes offline executable writes");
check(psx_mod_read_disc_file("S0/LEVEL.NSF", result.data(), 3000, &bytes) && bytes == 3000,
"profile retains original-disc reads");
check(read_selection() == offline_selection, "profile does not save temporary selections");
check(PSXRecompV4::mod_runtime_commit_netplay_view(iso_path, &error), "profile rematch commit");
mod_runtime_activate_plugins();
check(activation_calls == activations_before + 2, "profile rematch activates exactly once");
CPUState profile_cpu{};
test_netplay_active = 1;
filter_handles = true;
check(psx_mod_function_entry(&profile_cpu, 0x80003008) && profile_cpu.gpr[2] == 42u,
"profile retains modern trusted function filters");
test_netplay_active = 0;
test_match_caps.valid = 1;
test_match_caps.mod_count = 1;
check(!PSXRecompV4::mod_runtime_commit_for_netplay(iso_path, &error) &&
!psx_mod_netplay_is_active(), "profile rejects imported package plans");
test_match_caps = {};
check(PSXRecompV4::mod_runtime_commit_netplay(iso_path, &error), "profile restores after rejected plan");
if (profile_options) {
char value[32], unified_identity[65], split_identity[65];
check(psx_mod_option_value("runtime.test", "vblank-plugin", "cameras", value, sizeof(value)) &&
std::string(value) == "unified", "profile includes default camera option");
check(psx_mod_netplay_content_identity("", unified_identity), "default profile content identity");
test_match_caps.valid = 1;
test_match_caps.mod_count = 1;
std::strcpy(test_match_caps.mods[0].id, "runtime.test");
std::strcpy(test_match_caps.mods[0].ver, "1.0.0");
std::strcpy(test_match_caps.mods[0].feats, "vblank-plugin=cameras~split");
std::string fingerprint;
check(PSXRecompV4::mod_runtime_netplay_settings_fingerprint(test_match_caps, fingerprint, &error) &&
fingerprint == "24a3aeaf6034d1a59df3ac27334227343237550d4938b7b00c6ae77dfec21367",
"published settings fingerprint matches the canonical codec fixture");
char preview_identity[65];
check(psx_mod_netplay_content_identity("", preview_identity) &&
std::string(preview_identity) == unified_identity && read_selection() == offline_selection,
"offer preview does not mutate the active profile or preferences");
check(!PSXRecompV4::mod_runtime_commit_netplay(iso_path, &error), "missing settings fingerprint rejected");
std::strcpy(test_match_caps.mod_plan_fp, fingerprint.c_str());
test_match_caps.mod_plan_fp[0] = '0';
check(!PSXRecompV4::mod_runtime_commit_netplay(iso_path, &error), "tampered settings fingerprint rejected");
std::strcpy(test_match_caps.mod_plan_fp, fingerprint.c_str());
check(PSXRecompV4::mod_runtime_commit_netplay(iso_path, &error), error.c_str());
mod_runtime_activate_plugins();
check(psx_mod_option_value("runtime.test", "vblank-plugin", "cameras", value, sizeof(value)) &&
std::string(value) == "split", "profile adopts host split camera option");
check(option_renderer_activations == 1, "split activates the declared renderer at native aspect");
check(psx_mod_netplay_content_identity("", split_identity) &&
std::string(split_identity) != unified_identity, "different camera policies cannot handshake");
check(read_selection() == offline_selection, "host options do not persist over offline choices");
std::strcpy(test_match_caps.mods[0].feats, "vblank-plugin=cameras~unified");
check(!PSXRecompV4::mod_runtime_commit_netplay(iso_path, &error), "changed options with stale fingerprint rejected");
check(PSXRecompV4::mod_runtime_netplay_settings_fingerprint(test_match_caps, fingerprint, &error),
"unified offer fingerprint");
std::strcpy(test_match_caps.mod_plan_fp, fingerprint.c_str());
check(PSXRecompV4::mod_runtime_commit_netplay(iso_path, &error), error.c_str());
mod_runtime_activate_plugins();
check(option_renderer_activations == 1, "unified does not activate the option renderer");
const char* rejected[] = {"dynamic-main", "vblank-plugin,dynamic-main",
"vblank-plugin=cameras~invalid", "vblank-plugin=unknown~split"};
for (const char* token : rejected) {
std::strcpy(test_match_caps.mods[0].feats, token);
check(!PSXRecompV4::mod_runtime_commit_netplay(iso_path, &error) &&
!psx_mod_netplay_is_active(), "profile rejects undeclared host policy");
}
std::strcpy(test_match_caps.mods[0].feats, "vblank-plugin=cameras~split");
test_match_caps.mod_count = 2;
check(!PSXRecompV4::mod_runtime_commit_netplay(iso_path, &error), "profile rejects extra packages");
test_match_caps.mod_count = 1;
std::strcpy(test_match_caps.mods[0].ver, "different-version");
check(!PSXRecompV4::mod_runtime_commit_netplay(iso_path, &error), "profile rejects different package versions");
std::strcpy(test_match_caps.mods[0].ver, "1.0.0");
std::strcpy(test_match_caps.mods[0].id, "different-package");
check(!PSXRecompV4::mod_runtime_commit_netplay(iso_path, &error), "profile rejects unrelated settings packages");
test_match_caps = {};
check(PSXRecompV4::mod_runtime_commit_netplay(iso_path, &error), "profile recovers after rejected options");
check(read_selection() == offline_selection, "all settings commits preserve offline state");
PSXRecompV4::mod_runtime_end_netplay();
PSXRecompV4::ModPackageManager preferences(root);
check(preferences.scan(&error) && preferences.load_state(&error) &&
preferences.set_feature_option("runtime.test", "vblank-plugin", "cameras", "split", &error) &&
preferences.save_state(&error), "explicit local split preference");
offline_selection = read_selection();
check(PSXRecompV4::mod_runtime_initialize(root, "SLUS-RUNTIME", 0x80002000, {}, &error),
"profile reloads the explicit local preference");
check(PSXRecompV4::mod_runtime_commit_for_direct_netplay(iso_path, &error), error.c_str());
check(psx_mod_option_value("runtime.test", "vblank-plugin", "cameras", value, sizeof(value)) &&
std::string(value) == "split", "direct sessions use the local agreed preference");
check(psx_mod_netplay_content_identity("", split_identity) &&
std::string(split_identity) != unified_identity, "direct option identity covers camera policy");
test_match_caps.valid = 1;
test_match_caps.mod_count = 1;
std::strcpy(test_match_caps.mods[0].id, "runtime.test");
std::strcpy(test_match_caps.mods[0].ver, "1.0.0");
std::strcpy(test_match_caps.mods[0].feats, "vblank-plugin");
check(PSXRecompV4::mod_runtime_netplay_settings_fingerprint(test_match_caps, fingerprint, &error),
"default host offer fingerprint");
std::strcpy(test_match_caps.mod_plan_fp, fingerprint.c_str());
check(PSXRecompV4::mod_runtime_commit_netplay(iso_path, &error), error.c_str());
check(psx_mod_option_value("runtime.test", "vblank-plugin", "cameras", value, sizeof(value)) &&
std::string(value) == "unified", "omitted host options use defaults rather than guest preferences");
check(read_selection() == offline_selection, "host defaults preserve the guest's split preference");
}
PSXRecompV4::mod_runtime_end_netplay();
check(!psx_mod_netplay_is_active() && psx_mod_netplay_savestates_supported() &&
PSXRecompV4::mod_runtime_fingerprint().empty(), "profile end clears hooks and snapshot policy");
check(read_selection() == offline_selection, "profile end preserves offline selection file");
{
const auto a = root / "portable-default-a";
const auto b = root / "portable-default-b";
const auto manifest = a / "bundled/portable.hd/1.0.0/manifest.toml";
write_text(manifest,
"format_version = 5\nid = \"portable.hd\"\nversion = \"1.0.0\"\nname = \"Portable HD fixture\"\nresolver = \"declarative\"\nsave_compatibility = \"shared\"\n"
"[[target]]\ngame_id = \"SLUS-PORTABLE\"\n"
"[[feature]]\nid = \"hd\"\nname = \"HD\"\ndefault_enabled = false\n"
"[[resource]]\nfeature = \"hd\"\nid = \"pack\"\nlabel = \"Pack\"\nformat = \"directory\"\nrequired = true\n"
"[[option]]\nfeature = \"hd\"\nid = \"replacements\"\nlabel = \"Load\"\ntype = \"boolean\"\ndefault = \"true\"\n"
"[[option]]\nfeature = \"hd\"\nid = \"dump\"\nlabel = \"Dump\"\ntype = \"boolean\"\ndefault = \"false\"\n"
"[[plugin]]\nfeature = \"hd\"\nid = \"psx.hd-textures\"\n");
check(PSXRecompV4::mod_runtime_initialize(a, "SLUS-PORTABLE", 0x80002000, {}, &error), error.c_str());
check(fs::is_directory(a / "texture-packs/SLUS-PORTABLE/replacements"), "disabled default was actually initialized");
check(PSXRecompV4::mod_runtime_commit({}, &error), error.c_str());
fs::copy(a, b, fs::copy_options::recursive, ec);
check(!ec, "copy default-disabled fixture to relocated root");
fs::rename(a, root / "unavailable-default-a", ec);
check(!ec && !fs::exists(a), "old install root is unavailable");
check(PSXRecompV4::mod_runtime_initialize(b, "SLUS-PORTABLE", 0x80002000, {}, &error), error.c_str());
#if defined(RECOMP_LAUNCHER)
const auto* portable_provider = PSXRecompV4::mod_runtime_launcher_provider();
RecompLauncherCModResource portable_resource{};
check(portable_provider && portable_provider->feature_resource_get(portable_provider->ctx,
"portable.hd", "hd", 0, &portable_resource) &&
fs::path(portable_resource.path) == fs::absolute(b / "texture-packs/SLUS-PORTABLE"),
"live launcher getter exposes exact relocated B default while disabled");
#endif
check(PSXRecompV4::mod_runtime_commit({}, &error), error.c_str());
PSXRecompV4::ModPackageManager readback(b);
check(readback.scan(&error) && readback.load_state(&error), error.c_str());
check(readback.selected_package("portable.hd") != nullptr, "portable fixture package remains selected");
const auto retained = readback.feature_resource_path("portable.hd", "hd", "pack");
check(retained.empty() || retained.lexically_relative(a).empty() ||
*retained.lexically_relative(a).begin() == "..",
"disabled generated default must not retain unavailable install root");
check(!fs::exists(a), "relocation does not recreate unavailable old root");
check(readback.set_feature_enabled("portable.hd", "hd", true, &error) && readback.save_state(&error), error.c_str());
check(PSXRecompV4::mod_runtime_initialize(b, "SLUS-PORTABLE", 0x80002000, {}, &error), error.c_str());
const auto before_cwd = fs::current_path();
fs::current_path(root);
check(PSXRecompV4::mod_runtime_commit({}, &error), error.c_str());
mod_runtime_activate_plugins();
check(fs::path(hd_root) == fs::absolute(b / "texture-packs/SLUS-PORTABLE"),
"enabled relocated plugin receives B default despite changed cwd");
fs::current_path(before_cwd);
PSXRecompV4::ModPackageManager generated(b);
check(generated.scan(&error) && generated.load_state(&error), error.c_str());
check(generated.set_generated_feature_resource_default("portable.hd", "hd", "pack",
"texture-packs/SLUS-PORTABLE", &error), error.c_str());
const auto expected = fs::absolute(b / "texture-packs/SLUS-PORTABLE");
check(generated.feature_resource_path("portable.hd", "hd", "pack") == expected,
"manager getter returns exact relocated default");
const auto resolved = generated.resolve("SLUS-PORTABLE");
check(resolved.ok && resolved.resources.size() == 1 && resolved.resources[0].path == expected,
"committed resolver and getter agree on B");
for (const auto& invalid : {fs::path("../escape"), fs::path("./escape"), expected})
check(!generated.set_generated_feature_resource_default("portable.hd", "hd", "pack", invalid, &error),
"generated defaults reject rooted or traversal paths");
const auto external = root / "explicit-external-pack";
fs::create_directories(external);
check(generated.set_feature_resource_path("portable.hd", "hd", "pack", external, &error) &&
generated.save_state(&error), error.c_str());
fs::rename(external, root / "unavailable-external-pack", ec);
check(!ec, "explicit external fixture made unavailable");
PSXRecompV4::ModPackageManager legacy(b);
check(legacy.scan(&error) && legacy.load_state(&error) &&
legacy.set_generated_feature_resource_default("portable.hd", "hd", "pack",
"texture-packs/SLUS-PORTABLE", &error), error.c_str());
check(legacy.feature_resource_path("portable.hd", "hd", "pack") == external,
"missing legacy absolute user selection remains unchanged");
const auto missing_plan = legacy.resolve("SLUS-PORTABLE");
check(missing_plan.resources.size() == 1 && missing_plan.resources[0].path == external,
"resolver preserves missing selection for existing downstream validation");
}
fs::remove_all(root, ec);
if (failures) return 1;
std::cout << "mod runtime tests passed\n";
return 0;
}
/* render_pass.c: no sandboxed local view runs in this test. */
extern "C" int psx_mod_local_view_scope(void) { return 0; }