mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Qualify native language sessions and derived file lifecycle
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
# Native text and donor assets
|
||||
|
||||
`runtime/include/native_text.h` provides the reusable text side of a native
|
||||
translation mod. It complements the existing draw-hook translation support:
|
||||
adapters can encode plain Unicode back into original guest text and font data,
|
||||
then let the guest renderer consume it.
|
||||
|
||||
Catalog schema 1 keeps a stable `id`, readable `original`, editable
|
||||
`translation`, and lowercase `source_sha256` for each entry. The digest identifies
|
||||
the exact original native record. An empty translation means native fallback.
|
||||
The adapter validates source digests against its title's records before making
|
||||
changes. Duplicate IDs, malformed Unicode and missing digests are errors.
|
||||
|
||||
`NativeGlyphMap` maps Unicode scalars to native glyph indices.
|
||||
`native_text_encode` wraps words and emits adapter-supplied line, page and final
|
||||
control bits. Unsupported characters and words exceeding the native width fail;
|
||||
they never silently become spaces. A title owns its dimensions, style bits,
|
||||
source record inventory, buffer capacity and alignment requirements.
|
||||
|
||||
`native_font_compose` copies a cell in the original indexed 4-bpp font to an
|
||||
unused cell and composes an acute accent, tilde, diaeresis or inverted mark.
|
||||
Native palette indices, base shape and baseline are retained. The adapter can
|
||||
omit top rows of the copied shape to remove i's dot before composing í. Original
|
||||
cells remain unchanged, and occupied destinations are refused. Other native
|
||||
font layouts need their own explicit adapter; there is no host-font fallback.
|
||||
|
||||
For asset-backed games, an activation callback can use
|
||||
`psx_mod_replace_disc_file` to mount a derived file without modifying the owner
|
||||
disc. The runtime copies its bytes into an immutable Mode-2 Form-1 extent and
|
||||
updates both endian halves of its ISO directory record. Host file readers and
|
||||
guest CD reads resolve the same final file. Multiple replacements of the same
|
||||
file layer in activation order. Private native LBA/size tables remain the
|
||||
title adapter's responsibility. Use only verified committed resources and
|
||||
trusted adapter code to derive these bytes; paths, clocks and host fonts must
|
||||
not affect their content.
|
||||
|
||||
The `psx-mode2` verified resource format retains 2336 bytes per raw Mode-2
|
||||
sector, including XA subheaders and audio/video payloads. Its identity differs
|
||||
from the existing cooked `psx-disc` format. Cooked ISO images cannot satisfy this
|
||||
contract. `ModMode2Disc` performs bounded ISO9660 lookup and ordinary file reads
|
||||
against this immutable snapshot; raw streams mount through
|
||||
`psx_mod_append_disc_extent` and the original CD decoder.
|
||||
|
||||
Register a session-reset callback for title-owned preparation caches. Activation
|
||||
reconstructs them from the committed plan; reset retires stale data even when
|
||||
the next session disables the feature. Save-state identity includes verified
|
||||
catalog/donor content through the ordinary mod plan. Adapters still need title
|
||||
playback, visual and audio qualification, plus any relevant co-op or netplay
|
||||
qualification.
|
||||
|
||||
Mega Man X4 is the first asset adapter. Its source keeps archive and message
|
||||
format knowledge outside the framework. Focused coverage is in
|
||||
`native_text_test`, `mod_media_test` and `mod_runtime_test`, including malformed
|
||||
directory records, cross-sector reads, layered replacement, native directory
|
||||
parity, immutable sources, session cleanup and edited catalog checksums.
|
||||
@@ -12,58 +12,19 @@
|
||||
#include <sys/stat.h>
|
||||
#endif
|
||||
namespace PSXRecompV4 {
|
||||
// A failed identity is never a cache hit. Change time protects restored mtimes.
|
||||
// A failed identity is never a cache hit; callers must verify content instead.
|
||||
inline bool host_file_identity(const std::filesystem::path& path, std::string& identity) {
|
||||
identity.clear();
|
||||
std::ostringstream out;
|
||||
#if defined(_WIN32)
|
||||
HANDLE file = CreateFileW(path.c_str(), FILE_READ_ATTRIBUTES,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
|
||||
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE) return false;
|
||||
struct HandleScope { HANDLE value; ~HandleScope() { CloseHandle(value); } } handle{file};
|
||||
WCHAR filesystem_name[32]{};
|
||||
// NTFS/ReFS expose change time and native file identity. For a remote
|
||||
// handle also require SMB3+, rather than assuming every NAS is unsupported.
|
||||
FILE_REMOTE_PROTOCOL_INFO remote{};
|
||||
const bool is_remote = GetFileInformationByHandleEx(file, FileRemoteProtocolInfo,
|
||||
&remote, sizeof remote) != FALSE;
|
||||
const DWORD protocol_error = is_remote ? ERROR_SUCCESS : GetLastError();
|
||||
bool local_handle = false;
|
||||
if (!is_remote) {
|
||||
// A failed protocol query proves nothing about locality. Resolve the
|
||||
// handle itself: mapped drives become UNC paths here and cannot take
|
||||
// the local fallback when a remote provider omits protocol metadata.
|
||||
WCHAR final_path[32768]{};
|
||||
const DWORD length = GetFinalPathNameByHandleW(file, final_path, 32768, FILE_NAME_NORMALIZED);
|
||||
if (length >= 7 && length < 32768 && final_path[0] == L'\\' &&
|
||||
final_path[1] == L'\\' && final_path[2] == L'?' && final_path[3] == L'\\' &&
|
||||
final_path[5] == L':' && final_path[6] == L'\\') {
|
||||
WCHAR drive_root[] = {final_path[4], L':', L'\\', L'\0'};
|
||||
const UINT drive_type = GetDriveTypeW(drive_root);
|
||||
local_handle = drive_type == DRIVE_FIXED || drive_type == DRIVE_REMOVABLE;
|
||||
}
|
||||
}
|
||||
const bool reliable_protocol = is_remote
|
||||
? remote.Protocol == 0x00020000 && remote.ProtocolMajorVersion >= 3 // WNNC_NET_LANMAN
|
||||
: local_handle && (protocol_error == ERROR_INVALID_PARAMETER || protocol_error == ERROR_NOT_SUPPORTED);
|
||||
const bool reliable_fs = reliable_protocol &&
|
||||
GetVolumeInformationByHandleW(file, nullptr, 0, nullptr, nullptr, nullptr,
|
||||
filesystem_name, 32) &&
|
||||
(wcscmp(filesystem_name, L"NTFS") == 0 || wcscmp(filesystem_name, L"ReFS") == 0);
|
||||
BY_HANDLE_FILE_INFORMATION info{};
|
||||
FILE_BASIC_INFO basic{};
|
||||
const bool ok = reliable_fs && GetFileInformationByHandle(file, &info) &&
|
||||
GetFileInformationByHandleEx(file, FileBasicInfo, &basic, sizeof basic) &&
|
||||
!(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) &&
|
||||
(info.nFileIndexHigh != 0 || info.nFileIndexLow != 0) &&
|
||||
info.dwVolumeSerialNumber != 0 && basic.ChangeTime.QuadPart != 0 &&
|
||||
basic.LastWriteTime.QuadPart != 0;
|
||||
if (!ok) return false;
|
||||
out << "win1:" << info.dwVolumeSerialNumber << ':' << info.nFileIndexHigh << ':'
|
||||
<< info.nFileIndexLow << ':' << info.nFileSizeHigh << ':' << info.nFileSizeLow
|
||||
<< ':' << basic.LastWriteTime.QuadPart << ':' << basic.ChangeTime.QuadPart;
|
||||
// NTFS change/write timestamps can collide across immediate rewrites,
|
||||
// including same-size writes with a restored mtime. A file USN alone also
|
||||
// cannot qualify persistent receipts: it may be zero or reset with its
|
||||
// journal, and the query is unavailable over SMB3. Until a journal-bound
|
||||
// change identity is qualified, retain the portable full-verification path.
|
||||
(void)path;
|
||||
return false;
|
||||
#else
|
||||
std::ostringstream out;
|
||||
struct stat info{};
|
||||
if (stat(path.c_str(), &info) || !S_ISREG(info.st_mode) || info.st_ino == 0) return false;
|
||||
out << "posix1:" << info.st_dev << ':' << info.st_ino << ':' << info.st_size;
|
||||
@@ -73,9 +34,9 @@ inline bool host_file_identity(const std::filesystem::path& path, std::string& i
|
||||
#else
|
||||
out << ':' << info.st_mtim.tv_sec << ':' << info.st_mtim.tv_nsec
|
||||
<< ':' << info.st_ctim.tv_sec << ':' << info.st_ctim.tv_nsec;
|
||||
#endif
|
||||
#endif
|
||||
identity = out.str();
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,6 +581,8 @@ private:
|
||||
bool mod_register_builtin_resolver(const std::string& id, ModBuiltinResolver resolver);
|
||||
void mod_clear_builtin_resolvers_for_tests();
|
||||
bool mod_register_activation_plugin(const std::string& id, void (*callback)(void));
|
||||
bool mod_register_session_reset_plugin(const std::string& id, void (*callback)(void));
|
||||
void mod_reset_plugin_sessions();
|
||||
bool mod_register_vblank_plugin(const std::string& id, void (*callback)(void));
|
||||
bool mod_register_savestate_plugin(const std::string& id, void (*callback)(void));
|
||||
void mod_invoke_savestate_plugin(const std::string& id);
|
||||
|
||||
@@ -25,6 +25,10 @@ typedef int (*PSXModFunctionFilterCallback)(struct CPUState* cpu,
|
||||
*/
|
||||
int psx_mod_register_activation_plugin(const char* id,
|
||||
PSXModActivationCallback callback);
|
||||
/* Host-only cleanup before a new plan, including when it omits this plugin.
|
||||
* Runs with emulation stopped and no current resource context. */
|
||||
int psx_mod_register_session_reset_plugin(const char* id,
|
||||
PSXModActivationCallback callback);
|
||||
int psx_mod_register_vblank_plugin(const char* id,
|
||||
PSXModVBlankCallback callback);
|
||||
/* Runs after successful guest-state restore, before the restored PC resumes.
|
||||
|
||||
@@ -43,6 +43,9 @@ struct NativeFontAtlas {
|
||||
std::vector<uint8_t> packed;
|
||||
};
|
||||
enum class NativeGlyphMark { Acute, Tilde, Diaeresis, Inverted };
|
||||
// omit_top_rows is an adapter-provided crop of the copied glyph, for example
|
||||
// to remove i's dot before composing í. The source atlas cell is preserved.
|
||||
bool native_font_compose(NativeFontAtlas& atlas, uint16_t base, uint16_t destination,
|
||||
NativeGlyphMark mark, std::string& error);
|
||||
NativeGlyphMark mark, std::string& error,
|
||||
uint32_t omit_top_rows=0);
|
||||
} // namespace PSXRecompV4
|
||||
|
||||
@@ -43,6 +43,7 @@ std::map<std::string, MediaPreparer>& media_preparers() {
|
||||
struct RegisteredPlugin {
|
||||
void (*savestate)(void) = nullptr;
|
||||
PSXModActivationCallback activation = nullptr;
|
||||
PSXModActivationCallback session_reset = nullptr;
|
||||
PSXModVBlankCallback vblank = nullptr;
|
||||
/* Generated-function entry hooks, keyed by guest address. One id may
|
||||
* observe several functions. */
|
||||
@@ -1491,6 +1492,18 @@ bool mod_register_activation_plugin(const std::string& id, void (*callback)(void
|
||||
return true;
|
||||
}
|
||||
|
||||
bool mod_register_session_reset_plugin(const std::string& id, void (*callback)(void)) {
|
||||
if (!valid_id(id) || !callback) return false;
|
||||
RegisteredPlugin& plugin = registered_plugins()[id];
|
||||
if (plugin.session_reset) return false;
|
||||
plugin.session_reset = callback;
|
||||
return true;
|
||||
}
|
||||
void mod_reset_plugin_sessions() {
|
||||
for (const auto& entry : registered_plugins())
|
||||
if (entry.second.session_reset) entry.second.session_reset();
|
||||
}
|
||||
|
||||
bool mod_register_vblank_plugin(const std::string& id, void (*callback)(void)) {
|
||||
if (!valid_id(id) || !callback) return false;
|
||||
RegisteredPlugin& plugin = registered_plugins()[id];
|
||||
@@ -4199,6 +4212,11 @@ extern "C" int psx_mod_register_activation_plugin(
|
||||
PSXRecompV4::mod_register_activation_plugin(id, callback) ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_register_session_reset_plugin(
|
||||
const char* id, PSXModActivationCallback callback) {
|
||||
return id && PSXRecompV4::mod_register_session_reset_plugin(id, callback) ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_register_savestate_plugin(
|
||||
const char* id, PSXModActivationCallback callback) {
|
||||
return id &&
|
||||
|
||||
@@ -1578,6 +1578,7 @@ bool mod_runtime_initialize(const std::filesystem::path& root,
|
||||
}
|
||||
|
||||
bool mod_runtime_clear_for_netplay(std::string* error) {
|
||||
mod_reset_plugin_sessions();
|
||||
psx_mod_netplay_set_active(0);
|
||||
(void)psx_mod_netplay_set_options_identity("");
|
||||
state().ticket_ready = false;
|
||||
@@ -2112,8 +2113,10 @@ bool mod_runtime_commit(const std::filesystem::path& disc_path, std::string* err
|
||||
if (!s.provider_preparing) s.ticket_ready = false;
|
||||
clear_function_entry_hooks();
|
||||
s.disc_extents.clear();
|
||||
s.derived_disc_patches.clear();
|
||||
s.derived_disc_storage.clear(); s.audio_tracks.clear(); s.extent_start = 0;
|
||||
s.derived_disc_patches.clear();
|
||||
s.derived_disc_storage.clear();
|
||||
s.audio_tracks.clear();
|
||||
s.extent_start = 0;
|
||||
s.main_applied = false;
|
||||
s.error.clear();
|
||||
psx_mod_counter_add("startup.plan_reuse", 1);
|
||||
@@ -2873,6 +2876,7 @@ bool mod_runtime_read_disc_file_sectors(const std::string& path, uint32_t max_by
|
||||
|
||||
extern "C" void mod_runtime_activate_plugins(void) {
|
||||
using namespace PSXRecompV4;
|
||||
mod_reset_plugin_sessions();
|
||||
HostLaunchTimingScope timing(HOST_LAUNCH_PLUGIN_ACTIVATION);
|
||||
RuntimeMods& s = state();
|
||||
s.ticket_ready = false;
|
||||
|
||||
@@ -109,14 +109,14 @@ bool native_text_encode(const std::string& text,const NativeGlyphMap& glyphs,
|
||||
}
|
||||
|
||||
bool native_font_compose(NativeFontAtlas& a,uint16_t base,uint16_t dest,
|
||||
NativeGlyphMark mark,std::string& error) {
|
||||
NativeGlyphMark mark,std::string& error,uint32_t omit_top_rows) {
|
||||
error.clear();
|
||||
if(!a.width || !a.height || !a.cell_width || !a.cell_height ||
|
||||
a.width%a.cell_width || a.height%a.cell_height || a.width%2 ||
|
||||
uint64_t(a.width)*a.height!=uint64_t(a.packed.size())*2 ||
|
||||
a.cell_width>64 || a.cell_height>64) {error="Invalid native font atlas";return false;}
|
||||
const uint32_t cols=a.width/a.cell_width,cells=cols*(a.height/a.cell_height);
|
||||
if(base>=cells || dest>=cells || base==dest) {error="Glyph cell outside native atlas";return false;}
|
||||
if(base>=cells || dest>=cells || base==dest || omit_top_rows>=a.cell_height) {error="Glyph cell outside native atlas";return false;}
|
||||
auto get=[&](unsigned id,unsigned x,unsigned y) {
|
||||
const size_t p=((id/cols)*a.cell_height+y)*a.width+(id%cols)*a.cell_width+x;
|
||||
return uint8_t((a.packed[p/2]>>((p&1)*4))&15);
|
||||
@@ -129,6 +129,14 @@ bool native_font_compose(NativeFontAtlas& a,uint16_t base,uint16_t dest,
|
||||
const auto v=get(base,x,y);cell[y*a.cell_width+x]=v;
|
||||
if(v) {++colors[v];top=std::min(top,y);bottom=std::max(bottom,y);}
|
||||
}
|
||||
// Adapter-owned metrics can omit the dot of i before adding an accent.
|
||||
// Only the copied destination shape changes; source pixels stay intact.
|
||||
if(omit_top_rows) {
|
||||
std::fill_n(cell.begin(),omit_top_rows*a.cell_width,0);
|
||||
top=a.cell_height;bottom=0;
|
||||
for(unsigned y=omit_top_rows;y<a.cell_height;++y)for(unsigned x=0;x<a.cell_width;++x)
|
||||
if(cell[y*a.cell_width+x]) {top=std::min(top,y);bottom=std::max(bottom,y);}
|
||||
}
|
||||
if(top==a.cell_height) {error="Source font glyph is blank";return false;}
|
||||
const auto original=cell;
|
||||
if(mark==NativeGlyphMark::Inverted) std::reverse(cell.begin(),cell.end());
|
||||
|
||||
@@ -128,6 +128,30 @@ int main(int argc, char** argv) {
|
||||
write(disc, iso);
|
||||
check(load_mod_media(disc, "psx-disc", iso.size(), hash(iso), bytes, &error) && *bytes == iso,
|
||||
"mixed-mode volume size may extend beyond data track");
|
||||
// Bounded immutable ISO9660 lookup, including a file crossing sectors.
|
||||
std::vector<uint8_t> mode2_files(24*2336);
|
||||
auto dual=[&](size_t at,uint32_t value) {
|
||||
for(unsigned i=0;i<4;++i) {mode2_files[at+i]=uint8_t(value>>(i*8));mode2_files[at+7-i]=uint8_t(value>>(i*8));}
|
||||
};
|
||||
const size_t volume=16*2336+8,dir=20*2336+8;
|
||||
mode2_files[volume]=1;std::copy_n("CD001",5,mode2_files.begin()+volume+1);mode2_files[volume+6]=1;
|
||||
dual(volume+158,20);dual(volume+166,2048);
|
||||
mode2_files[dir]=42;dual(dir+2,21);dual(dir+10,3000);mode2_files[dir+32]=8;
|
||||
std::copy_n("TEXT.BIN",8,mode2_files.begin()+dir+33);
|
||||
for(unsigned i=0;i<3000;++i)mode2_files[(21+i/2048)*2336+8+i%2048]=uint8_t(i*7);
|
||||
ModMode2Disc native(mode2_files.data(),mode2_files.size());
|
||||
uint32_t found_lba=0,found_size=0;std::vector<uint8_t> found;
|
||||
check(native.find("text.bin",found_lba,found_size) && found_lba==21 && found_size==3000 &&
|
||||
native.read("TEXT.BIN",found) && found[2049]==uint8_t(2049*7),"Mode-2 ISO file lookup and cross-sector read");
|
||||
check(!native.find("../TEXT.BIN",found_lba,found_size),"Mode-2 lookup rejects parent traversal");
|
||||
mode2_files[dir]=1;
|
||||
check(!native.find("TEXT.BIN",found_lba,found_size),"short directory record rejected before name-length access");
|
||||
mode2_files[dir]=42;mode2_files[dir+9]^=1;
|
||||
check(!native.find("TEXT.BIN",found_lba,found_size),"directory dual-endian fields must agree");
|
||||
mode2_files[dir+9]^=1;dual(dir+2,24);
|
||||
check(!native.find("TEXT.BIN",found_lba,found_size),"donor file extent cannot escape snapshot");
|
||||
dual(dir+2,21);mode2_files[21*2336+2]=0x20;
|
||||
check(!native.read("TEXT.BIN",found) && found.empty(),"ordinary file reads cannot consume Form-2 XA payload");
|
||||
for (const char* file : {"rom.z64", "disc.iso", "disc.bin", "disc.cue", "audio.bin"})
|
||||
fs::remove(root / file);
|
||||
fs::remove(root);
|
||||
|
||||
@@ -301,6 +301,22 @@ static void test_media_plugin(void) {
|
||||
|
||||
}
|
||||
|
||||
static int derived_reset_calls;
|
||||
static uint32_t derived_first, derived_second;
|
||||
static void test_derived_reset(void) { ++derived_reset_calls; }
|
||||
static void test_derived_plugin(void) {
|
||||
uint32_t size=0;
|
||||
if(!psx_mod_read_disc_file("S0/LEVEL.NSF",nullptr,0,&size) || size!=3000) ++failures;
|
||||
const uint8_t* catalog=nullptr;uint64_t length=0;
|
||||
if(!psx_mod_current_resource_bytes("catalog",&catalog,&length) || length!=3 || catalog[0]!='e') ++failures;
|
||||
std::vector<uint8_t> first(5000,0x36),second(7000,0x79);
|
||||
if(!psx_mod_replace_disc_file("S0/LEVEL.NSF",first.data(),uint32_t(first.size()),&derived_first)) ++failures;
|
||||
if(!psx_mod_replace_disc_file("S0/LEVEL.NSF",second.data(),uint32_t(second.size()),&derived_second)) ++failures;
|
||||
uint32_t invalid=99;
|
||||
if(psx_mod_replace_disc_file("S0",first.data(),uint32_t(first.size()),&invalid) || invalid) ++failures;
|
||||
if(psx_mod_replace_disc_file("S0/MISSING",first.data(),uint32_t(first.size()),&invalid) || invalid) ++failures;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
void* iso_open(const char*);
|
||||
void iso_close(void*);
|
||||
@@ -1199,6 +1215,63 @@ int main(int argc, char** argv) {
|
||||
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");
|
||||
/* Derived native files layer without changing owner media. The host and
|
||||
* emulated drive must agree on the final ISO directory and file bytes. */
|
||||
{
|
||||
const auto derived_root=root/"derived-native-file";
|
||||
const std::vector<uint8_t> bundled_catalog{'e','s','!'};
|
||||
write_bytes(derived_root/"packages/derived.test/1.0.0/catalog/es.json",bundled_catalog);
|
||||
write_text(derived_root/"packages/derived.test/1.0.0/manifest.toml",
|
||||
"format_version = 9\nid = \"derived.test\"\nversion = \"1.0.0\"\nname = \"Derived\"\n"
|
||||
"[[target]]\ngame_id = \"READER\"\n[[feature]]\nid = \"active\"\nname = \"Active\"\ndefault_enabled = true\n"
|
||||
"[[resource]]\nfeature = \"active\"\nid = \"catalog\"\nlabel = \"Catalog\"\nformat = \"file\"\nrequired = true\n"
|
||||
"default = \"catalog/es.json\"\nsize = 3\nsha256 = \""+sha256_hex(bundled_catalog)+"\"\n"
|
||||
"[[plugin]]\nfeature = \"active\"\nid = \"derived.test.plugin\"\n");
|
||||
check(psx_mod_register_activation_plugin("derived.test.plugin",test_derived_plugin),"register derived file preparation");
|
||||
check(psx_mod_register_session_reset_plugin("derived.test.plugin",test_derived_reset),"register session retirement");
|
||||
check(PSXRecompV4::mod_runtime_initialize(derived_root,"READER",0,{},&error) &&
|
||||
PSXRecompV4::mod_runtime_commit(raw_path,&error),"verified bundled catalog commits");
|
||||
const auto resets=derived_reset_calls;
|
||||
mod_runtime_activate_plugins();
|
||||
check(derived_reset_calls==resets+1,"retire previous plugin session before preparing assets");
|
||||
check(derived_first==24 && derived_second==27,"layered native files append stable extents");
|
||||
std::vector<uint8_t> derived(7000);
|
||||
check(psx_mod_read_disc_file("S0/LEVEL.NSF",derived.data(),uint32_t(derived.size()),&bytes) &&
|
||||
bytes==7000 && std::all_of(derived.begin(),derived.end(),[](auto b){return b==0x79;}),"host sees final derived file before drive activation");
|
||||
uint32_t refused=99;
|
||||
check(!psx_mod_replace_disc_file("S0/LEVEL.NSF",derived.data(),7000,&refused) && !refused,"replacement is activation only");
|
||||
mod_runtime_enable_disc_patches();
|
||||
void* derived_mount=iso_open(raw_path.string().c_str());
|
||||
std::array<uint8_t,2352> derived_sector{};
|
||||
check(derived_mount && iso_sector_count(derived_mount)==31,"drive includes both derived layers");
|
||||
check(iso_read_sector(derived_mount,21,derived_sector.data(),derived_sector.size()) &&
|
||||
derived_sector[2]==27 && derived_sector[9]==27 && derived_sector[10]==0x58 && derived_sector[11]==0x1b &&
|
||||
derived_sector[16]==0x1b && derived_sector[17]==0x58,"guest ISO record has final little and big endian extent and size");
|
||||
check(iso_read_raw_sector(derived_mount,27,derived_sector.data(),derived_sector.size()) &&
|
||||
derived_sector[15]==2 && derived_sector[18]==8 && derived_sector[22]==8 && derived_sector[24]==0x79,
|
||||
"guest reads native Mode-2 Form-1 derived data");
|
||||
check(iso_read_sector(derived_mount,30,derived_sector.data(),derived_sector.size()) &&
|
||||
derived_sector[855]==0x79 && derived_sector[856]==0,"derived final sector is padded");
|
||||
mod_runtime_activate_plugins();
|
||||
check(derived_first==24 && derived_second==27 && iso_sector_count(derived_mount)==31,"reactivation retires old derived state");
|
||||
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error),"clear derived native files");
|
||||
check(iso_sector_count(derived_mount)==24 && psx_mod_read_disc_file("S0/LEVEL.NSF",nullptr,0,&bytes) && bytes==3000,
|
||||
"clearing restores original host and guest extents");
|
||||
iso_close(derived_mount);PSXRecompV4::mod_runtime_end_netplay();
|
||||
std::ifstream unchanged(raw_path,std::ios::binary);
|
||||
check(std::vector<uint8_t>(std::istreambuf_iterator<char>(unchanged),{})==raw_iso,"owner disc remains byte identical");
|
||||
// scan() migrates legacy packages/ into installed/. Mutate the active
|
||||
// payload, rather than recreating the retired migration source.
|
||||
write_bytes(derived_root/"installed/derived.test/1.0.0/catalog/es.json",{'b','a','d'});
|
||||
write_text(derived_root/"state.toml","format_version = 2\n[[feature]]\npackage_id = \"derived.test\"\nid = \"active\"\nenabled = true\n");
|
||||
PSXRecompV4::ModPackageManager selected_derived;selected_derived.set_root(derived_root);
|
||||
check(selected_derived.scan(&error) && selected_derived.load_state(&error) &&
|
||||
selected_derived.feature_enabled("derived.test","active"),"tampered catalog feature remains selected for verification");
|
||||
const auto tampered=selected_derived.resolve("READER",{},{});
|
||||
check(!tampered.ok,"tampered bundled catalog fails resolution");
|
||||
check(PSXRecompV4::mod_runtime_initialize(derived_root,"READER",0,{},&error),"reload selected bundled catalog");
|
||||
check(!PSXRecompV4::mod_runtime_commit(raw_path,&error),"modified bundled catalog rejected by SHA-256");
|
||||
}
|
||||
/* 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";
|
||||
|
||||
@@ -39,6 +39,11 @@ int main() {
|
||||
check(atlas.packed[4*16+10]==0x33 && atlas.packed[5*16+10]==0x23,"base glyph palette and outline copied exactly");
|
||||
const auto composed=atlas.packed;
|
||||
check(!native_font_compose(atlas,0,1,NativeGlyphMark::Tilde,error) && atlas.packed==composed,"occupied glyph cannot be overwritten");
|
||||
NativeFontAtlas dotted;dotted.width=32;dotted.height=16;dotted.packed.resize(256);
|
||||
dotted.packed[2*16+2]=0x33;dotted.packed[7*16+2]=0x23;
|
||||
check(native_font_compose(dotted,0,1,NativeGlyphMark::Acute,error,4),"native i dot can be omitted before accenting");
|
||||
check(dotted.packed[2*16+2]==0x33 && dotted.packed[2*16+10]==0 && dotted.packed[7*16+10]==0x23,
|
||||
"dot removal preserves source and destination stem");
|
||||
std::cout<<(failures?"FAIL":"PASS")<<": native translation catalog, Unicode, layout and font composition\n";
|
||||
return failures?1:0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user