From 12b8499da8b94ea7f89c69f7d9b5995047541ffe Mon Sep 17 00:00:00 2001 From: TideGear Date: Sun, 13 Sep 2026 00:40:12 -0700 Subject: [PATCH] Correct USA brightness help with scoped disc patches --- MGSM2Fix.ini | 12 ++++ MGSM2Fix.vcxproj | 1 + MGSM2Fix.vcxproj.filters | 3 + src/games/mgs1.h | 122 +++++++++++++++++++++++++++++++++++++ src/m2fix/ketchup.cpp | 26 ++++++++ src/m2fix/ketchup.h | 13 ++++ src/m2fix/m2config.cpp | 17 ++++++ src/m2fix/m2config.h | 14 +++++ src/m2fix/m2game.h | 3 + src/m2fix/patch_range.h | 10 +++ src/modules/sqhook.cpp | 13 ++++ src/modules/sqhook.h | 7 +++ tests/test_patch_range.cpp | 11 ++++ 13 files changed, 252 insertions(+) create mode 100644 src/m2fix/patch_range.h create mode 100644 tests/test_patch_range.cpp diff --git a/MGSM2Fix.ini b/MGSM2Fix.ini index a903515..a327cac 100644 --- a/MGSM2Fix.ini +++ b/MGSM2Fix.ini @@ -54,6 +54,18 @@ RestoreGhosts = true RestoreMedicine = true +; MGS1 only: The collection replaces the game's Option -> SCREEN help texture +; with a four-line version, dropping the line that names the button used to go +; back - fair enough, since that button's name is not the same on every +; platform. What it gets wrong is re-centring the four lines it keeps in the +; same canvas: that draws them 12 pixels below where the game draws them, and +; leaves a flat black band over the grey ramp above them. +; fixed - the collection's four lines, put back where the game draws +; them, and no band +; original - the game's own six lines, in that same place +; collection - leave the collection's version exactly as it is +; Only MGS1 (USA) is covered so far; other versions need their own offsets. +BrightnessText = fixed ; MGS1 only: The collection rewrites the game's whole 16-bit configuration word ; every frame to impose its button-type setting, from a copy it read a moment diff --git a/MGSM2Fix.vcxproj b/MGSM2Fix.vcxproj index bbaed82..b6636b0 100644 --- a/MGSM2Fix.vcxproj +++ b/MGSM2Fix.vcxproj @@ -98,6 +98,7 @@ + diff --git a/MGSM2Fix.vcxproj.filters b/MGSM2Fix.vcxproj.filters index a248d1b..5ea5063 100644 --- a/MGSM2Fix.vcxproj.filters +++ b/MGSM2Fix.vcxproj.filters @@ -362,6 +362,9 @@ M2Fix + + M2Fix + M2Fix diff --git a/src/games/mgs1.h b/src/games/mgs1.h index 85513c6..cadac96 100644 --- a/src/games/mgs1.h +++ b/src/games/mgs1.h @@ -59,6 +59,20 @@ class MGS1 : public M2Game SQHook::SetPatchDataBlacklist(MGS1_DataBlacklist_Medicine); } + if (M2Config::eBrightnessText != M2BrightnessText::Collection) { + // Scope the collection replacement to USA and the selected disc. + for (unsigned disk = 0; disk < MGS1_RangeBlacklist_BrightnessText.size(); ++disk) { + const auto &range = MGS1_RangeBlacklist_BrightnessText[disk]; + SQHook::SetPatchRangeBlacklist(981, disk, range.first, range.second); + } + } + + // `fixed` also writes back the two lines the collection was right to + // drop, blanked - see MGS1_BrightnessTextData. + if (M2Config::eBrightnessText == M2BrightnessText::Fixed) { + MGS1_KetchupPatches = MGS1_BrightnessTextPatches(); + } + if (M2Config::bPatchesDisableFont) { for (auto & MGS1_TextureWhitelist_Font : MGS1_TextureWhitelist_Fonts) { SQHook::SetTextureWhitelist(MGS1_TextureWhitelist_Font); @@ -95,6 +109,10 @@ class MGS1 : public M2Game return &MGS1_Ketchup; } + virtual std::vector *SQKetchupPatches() override + { + return &MGS1_KetchupPatches; + } #ifndef _WIN64 virtual void GWRenderGeometry(int & gw_width, int & gw_height, int & fb_width, int & fb_height, int & img_width, int & img_height) override @@ -200,6 +218,110 @@ class MGS1 : public M2Game const std::string MGS1_FileBlacklist_Ghosts = "shinrei"; + // The collection replaces the Option -> SCREEN help texture (`sc_text` in + // the `option` stage's archive) with a four-line version on the same 232x70 + // canvas. Dropping "Press the O button to return to the option screen." is + // fair - that button's name is not the same on every platform - but it also + // re-centres the four lines it keeps, and that is a bug: the whole canvas is + // opaque (its quad is drawn with abe = 0), and the game's art paints canvas + // rows 0..3 one step off black, at (8,8,8), so they vanish into the grey + // ramp's own 8 band and hide the backdrop's top edge. Re-centring carries + // those four rows down with the text, leaving flat black over the 8 band. + // Measured on the collection's USA: 0.00 luminance inside the canvas + // against 8.89 beside it, four rows, with the text one full line pitch + // (12 game rows) below where the game draws it. + // + // These are the four pieces of that one archive entry on MGS1 (USA) disc 1, + // addressed by image offset, which is how the collection names them. + // Integral needs none: it has no `sc_text` of its own for them to replace. + const std::vector MGS1_FileBlacklist_BrightnessText = { + "disc1_165A34CC", "disc1_165A3BD8", "disc1_165A4508", "disc1_165A4E38", + }; + + // The same entry as a disc-image range, per disk: `option` is STAGE.DIR + // sector 27023 on both of MGS1 (USA)'s disks, and the entry's payload is + // 5852 bytes at file offset 55499300, so the span follows from each disk's + // STAGE.DIR LBA (132344 and 100801). Tight enough to be safe, checked two + // ways against every patch candidate in the log archive: of the 107 the + // collection offers this title, exactly four fall anywhere inside the + // 81-sector option stage and they are the four above; and across titles + // 99 (Integral) and 980 (the Japanese MGS1), whose disc images do span + // these offsets, nothing falls inside either window. Registered with USA title 981 and a disc index; checked against the + // active title/disc when each collection patch is submitted. + const std::vector> MGS1_RangeBlacklist_BrightnessText = { + { 0x165A34CCull, 0x165A4F38ull }, // disk 0 + { 0x11EE2B7Cull, 0x11EE45E8ull }, // disk 1 + }; + + // Rows 46..69 of that texture, re-encoded blank. Splicing these over the + // game's own payload leaves rows 0..45 - the four lines the collection also + // keeps, and the (8,8,8) filler - byte for byte as the game has them, and + // drops lines 5 and 6. The payload's declared size does not change: PCX + // here is four bit planes run-length encoded one row at a time, so rows are + // independent in the byte stream, this encoding is shorter than the one it + // replaces, and the decoder stops after 70 rows without reading the rest. + // + // Why not move the quad instead: the texture's own size supplies the UVs + // (SetPacketTexture reads them from DG_TEX), so the quad cannot crop, and + // shifting it up 11 rows would drag the backdrop's top edge onto the ramp's + // brighter 16 band - a worse notch than the one being fixed. + // Kept as a plain array in .rdata, not an initializer_list: the bytes are + // then greppable in the shipped binary, which is how this build verified + // that what ends up on disc is what the tools produced. + static inline const unsigned char MGS1_BrightnessTextData[426] = { + 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0x40, 0xDB, 0x00, 0x10, 0xDF, 0x00, + 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, 0xBF, 0xDB, 0xFF, 0xC1, 0xEF, 0x00, + 0xDC, 0x00, 0x10, 0xDF, 0x00, 0xDC, 0xFF, 0xC1, 0xEF, 0x00, 0xDC, 0xFF, + 0xC1, 0xEF, 0x00, 0xDC, 0x00, 0x10, 0xDF, 0x00, 0xDC, 0xFF, 0xC1, 0xEF, + 0x00, 0xDC, 0xFF, 0xC1, 0xEF, 0x00, + }; + + // Where those bytes go: row 46 of the payload, i.e. file offset 55503496, + // mapped through (lba + fo / 2048) * 2352 + 24 + fo % 2048. Both writes + // land inside one sector, so neither needs splitting. + static std::vector MGS1_BrightnessTextPatches() + { + std::vector data(std::begin(MGS1_BrightnessTextData), + std::end(MGS1_BrightnessTextData)); + return { + {981, "USA", 0, 0x165A4790ull, data, "sc_text rows 46-69 blanked"}, + {981, "USA", 1, 0x11EE3E40ull, data, "sc_text rows 46-69 blanked"}, + }; + } + + std::vector MGS1_KetchupPatches = {}; + const std::vector MGS1_DataBlacklist_Medicine = { 0, 152, 0, 72, 152, 72, 152, 152, 152 }; diff --git a/src/m2fix/ketchup.cpp b/src/m2fix/ketchup.cpp index a2b95c9..6673932 100644 --- a/src/m2fix/ketchup.cpp +++ b/src/m2fix/ketchup.cpp @@ -319,10 +319,36 @@ std::filesystem::path Ketchup::RootPath(Ketchup_TitleInfo &title, Ketchup_Ver return root; } +template +bool Ketchup::ProcessBuiltins(HSQUIRRELVM v, Ketchup_TitleInfo &title, Ketchup_VersionInfo &version, Ketchup_DiskInfo &disk) +{ + auto *patches = M2Fix::GameInstance().SQKetchupPatches(); + if (!patches) return true; + + for (auto &patch : *patches) { + if (patch.title != title.id) continue; + if (patch.version != version.name) continue; + if (patch.disk != disk.id) continue; + if (patch.data.empty()) continue; + + spdlog::info("[SQ] [Ketchup] built-in patch {} -> {} {} disk {}.", + patch.name, title.name, version.name, disk.id); + WriteSource = static_cast(WriteSources.size()); + WriteSources.push_back("built-in " + patch.name); + ApplyBlock(v, title, version, disk, patch.offset, + patch.data.data(), patch.data.size()); + } + + return true; +} template bool Ketchup::ProcessDisk(HSQUIRRELVM v, Ketchup_TitleInfo &title, Ketchup_VersionInfo &version, Ketchup_DiskInfo &disk) { + // Built-ins first, and unconditionally: they do not live in the mods folder, + // so an absent or empty one must not skip them. + ProcessBuiltins(v, title, version, disk); + std::filesystem::directory_entry root { RootPath(title, version, disk) }; spdlog::info("[SQ] [Ketchup] base path is {}.", root.path().string()); diff --git a/src/m2fix/ketchup.h b/src/m2fix/ketchup.h index 61dc610..ed8c1cf 100644 --- a/src/m2fix/ketchup.h +++ b/src/m2fix/ketchup.h @@ -23,6 +23,18 @@ typedef struct { +// A patch this fix carries itself, rather than reading from a PPF in the mods +// folder. Same destination as a PPF record - an offset into the disc image - +// but the game keys it, so one entry targets exactly one release and disk. +typedef struct { + unsigned int title; + std::string version; + unsigned int disk; + uint64_t offset; + std::vector data; + std::string name; +} Ketchup_DiskPatch; + // One write as it was applied, kept only until the folder has been processed // so collisions between two patches can be reported. See ReportOverlaps(). typedef struct { @@ -64,6 +76,7 @@ class Ketchup static bool ApplyPPF3(HSQUIRRELVM v, Ketchup_TitleInfo &title, Ketchup_VersionInfo &version, Ketchup_DiskInfo &disk, std::ifstream &data); static int MetaPPF_FileId(std::ifstream &data, int version); + static bool ProcessBuiltins(HSQUIRRELVM v, Ketchup_TitleInfo &title, Ketchup_VersionInfo &version, Ketchup_DiskInfo &disk); static bool ProcessDisk(HSQUIRRELVM v, Ketchup_TitleInfo &title, Ketchup_VersionInfo &version, Ketchup_DiskInfo &disk); static bool ProcessVersion(HSQUIRRELVM v, Ketchup_TitleInfo &title, Ketchup_VersionInfo &version); static bool ProcessTitle(HSQUIRRELVM v, Ketchup_TitleInfo &title); diff --git a/src/m2fix/m2config.cpp b/src/m2fix/m2config.cpp index bb4138c..74d5896 100644 --- a/src/m2fix/m2config.cpp +++ b/src/m2fix/m2config.cpp @@ -108,6 +108,20 @@ void M2Config::Load() inipp::get_value(ini.sections["Patches"], "EnableMosaic", bPatchesEnableMosaic); inipp::get_value(ini.sections["Patches"], "RestoreGhosts", bPatchesRestoreGhosts); inipp::get_value(ini.sections["Patches"], "RestoreMedicine", bPatchesRestoreMedicine); + { + // Tri-state, and it accepts a bool for anyone who assumes one: `true` + // is the game's own text, which is what the old RestoreBrightnessText + // meant, and `false` leaves the collection's version alone. + std::string mode; + if (inipp::get_value(ini.sections["Patches"], "BrightnessText", mode)) { + for (auto &c : mode) c = static_cast(std::tolower(static_cast(c))); + if (mode == "fixed") eBrightnessText = M2BrightnessText::Fixed; + else if (mode == "original" || mode == "true") eBrightnessText = M2BrightnessText::Original; + else if (mode == "collection" || mode == "false") eBrightnessText = M2BrightnessText::Collection; + else spdlog::warn("[Config] BrightnessText: '{}' is not one of fixed / original /" + " collection, keeping the default.", mode); + } + } inipp::get_value(ini.sections["Patches"], "PreserveConfiguration", bPatchesPreserveConfiguration); inipp::get_value(ini.sections["Game"], "StageSelect", bGameStageSelect); inipp::get_value(ini.sections["Game"], "EnglishText", bGameEnglishText); @@ -174,6 +188,9 @@ void M2Config::Load() spdlog::info("[Config] bPatchesEnableMosaic: {}", bPatchesEnableMosaic); spdlog::info("[Config] bPatchesRestoreGhosts: {}", bPatchesRestoreGhosts); spdlog::info("[Config] bPatchesRestoreMedicine: {}", bPatchesRestoreMedicine); + spdlog::info("[Config] eBrightnessText: {}", + eBrightnessText == M2BrightnessText::Fixed ? "fixed" : + eBrightnessText == M2BrightnessText::Original ? "original" : "collection"); spdlog::info("[Config] bPatchesPreserveConfiguration: {}", bPatchesPreserveConfiguration); spdlog::info("[Config] bGameStageSelect: {}", bGameStageSelect); spdlog::info("[Config] bGameEnglishText: {}", bGameEnglishText); diff --git a/src/m2fix/m2config.h b/src/m2fix/m2config.h index f18da01..14b0358 100644 --- a/src/m2fix/m2config.h +++ b/src/m2fix/m2config.h @@ -4,6 +4,15 @@ #include "m2fixbase.h" +// What to do about the collection's replacement for MGS1's Option -> SCREEN +// help texture. See [Patches] BrightnessText in the ini. +enum class M2BrightnessText +{ + Collection, // leave it alone + Fixed, // its four lines, at the position the game draws them + Original, // the game's own six lines, at that same position +}; + class M2Config : public M2FixBase { public: @@ -13,6 +22,7 @@ class M2Config : public M2FixBase bPatchesEnableMosaic = true; bPatchesRestoreGhosts = true; bPatchesRestoreMedicine = true; + eBrightnessText = M2BrightnessText::Fixed; bPatchesPreserveConfiguration = true; } @@ -67,6 +77,10 @@ class M2Config : public M2FixBase static inline bool bPatchesEnableMosaic; static inline bool bPatchesRestoreGhosts; static inline bool bPatchesRestoreMedicine; + // Given a value here, not just in the constructor: an enum's zero is + // Collection, so a read before the singleton is first constructed would + // otherwise see the wrong default. + static inline M2BrightnessText eBrightnessText = M2BrightnessText::Fixed; static inline bool bPatchesPreserveConfiguration; static inline bool bShouldCheckForUpdates; static inline bool bConsoleUpdateNotifications; diff --git a/src/m2fix/m2game.h b/src/m2fix/m2game.h index 141965c..5e7dd7f 100644 --- a/src/m2fix/m2game.h +++ b/src/m2fix/m2game.h @@ -23,6 +23,9 @@ class M2Game : public M2FixBase } virtual std::vector *SQKetchupHook() { return nullptr; } + // Patches the fix writes to the disc image itself, alongside the mods + // folder's PPFs. Keyed by title, version and disk; see Ketchup_DiskPatch. + virtual std::vector *SQKetchupPatches() { return nullptr; } virtual void SQOnMemoryDefine() {} virtual void SQOnUpdateGadgets() {} diff --git a/src/m2fix/patch_range.h b/src/m2fix/patch_range.h new file mode 100644 index 0000000..11f29a6 --- /dev/null +++ b/src/m2fix/patch_range.h @@ -0,0 +1,10 @@ +#pragma once +#include + +struct PatchRange { + unsigned title, disk; + uint64_t start, end; + bool matches(unsigned currentTitle, unsigned currentDisk, uint64_t offset) const { + return title == currentTitle && disk == currentDisk && offset >= start && offset < end; + } +}; diff --git a/src/modules/sqhook.cpp b/src/modules/sqhook.cpp index 2a6cc66..1ee8d18 100644 --- a/src/modules/sqhook.cpp +++ b/src/modules/sqhook.cpp @@ -281,6 +281,11 @@ void SQHook::SetNativeCallHook(const char *name, SQFUNCTION func) NativeTable.push_back({ name, func }); } +template +void SQHook::SetPatchRangeBlacklist(unsigned title, unsigned disk, uint64_t start, uint64_t end) +{ + RangeBlacklist.push_back({ title, disk, start, end }); +} template @@ -636,6 +641,14 @@ SQInteger SQHook::SQNative_entryCdRomPatch(HSQUIRRELVM v) return 1; } } + for (auto &range : RangeBlacklist) + { + if (!range.matches(SQGlobals::GetTitle(), SQGlobals::GetDisk(), offset)) continue; + spdlog::info("[SQ] [Patch] filtering CD-ROM patch {} at offset 0x{:x}," + " inside blacklisted range 0x{:x}..0x{:x}.", + file.empty() ? "" : file, offset, range.start, range.end); + return 1; + } return 0; } diff --git a/src/modules/sqhook.h b/src/modules/sqhook.h index 6f57cce..71e9b36 100644 --- a/src/modules/sqhook.h +++ b/src/modules/sqhook.h @@ -1,6 +1,7 @@ #pragma once #include "m2fixbase.h" +#include "../m2fix/patch_range.h" #include "sqhelper.h" @@ -57,6 +58,7 @@ class SQHook: public M2FixBase static void SetLoadScriptHook(const char *name, SQFUNCTION func); static void SetNativeCallHook(const char *name, SQFUNCTION func); static void SetPatchFileBlacklist(std::string file); + static void SetPatchRangeBlacklist(unsigned title, unsigned disk, uint64_t start, uint64_t end); static void SetPatchDataBlacklist(std::vector data); static void SetTextureWhitelist(unsigned int data); @@ -139,6 +141,11 @@ class SQHook: public M2FixBase private: static inline HSQREMOTEDBG DBG = nullptr; static inline std::vector FileBlacklist = {}; + // Half-open disc-image ranges. Filters by where a patch writes rather than + // what its file is called, which is what you want for an asset whose patch + // names differ per disk - the collection names each piece after its own + // image offset, so one range covers every piece of one archive entry. + static inline std::vector RangeBlacklist = {}; static inline std::vector> DataBlacklist = {}; static inline std::vector TextureWhitelist = {}; static inline unsigned int ThreadCount = 0; diff --git a/tests/test_patch_range.cpp b/tests/test_patch_range.cpp new file mode 100644 index 0000000..3f763f5 --- /dev/null +++ b/tests/test_patch_range.cpp @@ -0,0 +1,11 @@ +#include "../src/m2fix/patch_range.h" +#include +#include +int main() { + PatchRange range{981, 1, 100, 200}; + if (!range.matches(981, 1, 100) || !range.matches(981, 1, 199) || + range.matches(99, 1, 150) || range.matches(981, 0, 150) || + range.matches(981, 1, 200)) + throw std::runtime_error("Title/disc/range boundary mismatch"); + std::cout << "Patch range tests passed\n"; +}