Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 37 additions & 4 deletions MGSM2Fix.ini
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,16 @@ UnlockVRMovies = false
; EXTREME, and Integral's additional clear-game menu). Separate from
; UnlockBriefing below. Does not erase any progress already in a save.
UnlockTitleBonuses = false

; MGS1 only: Enables the in-game developer menu (stage select).
; MGS1 only: Opens the game's own developer menu in place of the title screen.
; `true` opens the top menu, which in the retail data offers only TITLE / DEMO
; ALL / SOUND TEST. The four stage lists are separate menus it never links to,
; so name one to open it directly: select1 (D00A..S04B), select2 (S05A..S09A),
; select3 (S10A..S14E), select4 (S15A..S20A). Every list has a RETURN entry
; back to the top menu, and TITLE there loads the title screen as normal. A
; gameplay stage name (s11a) drops straight into that stage with no menu; event
; stages that need prior story state may hang there. No debug load ever changes
; the disc: disc 2 is set only by the `change` stage's CD check, which only the
; real story swap runs - to reach disc 2, play across the break from a save.
StageSelect = false

; MGS1 only (Integral): Selects English text in the game's own option screen.
Expand All @@ -125,8 +133,33 @@ StageSelect = false
; the event is logged. Has no effect on any other version.
EnglishText = true



; MGS1 only (Integral, USA): Shows every item in the title screen's BRIEFING
; menu. The game hides items until the story unlocks them (a fresh boot shows
; 1 / 3 / 6 of the sixteen); this holds all sixteen unlock flags set while the
; title screens and the briefing are up. It never writes during a running game,
; but note the flags live in the same memory a NEW GAME starts from, so a game
; begun with this on carries all sixteen as already unlocked in its save.
UnlockBriefing = false

; MGS1 only: TEST AID. Comma-separated item ids to hand Snake once per gameplay
; stage, MGS1's own numbering - 12 is the Camera (0 Cigs, 1 Scope, 2-4 Boxes,
; 5 NVG, 6 Thermal, 7 Gas Mask, 8 Body Armor, 9 Ketchup, 10 Stealth, 11 Bandana,
; 13 Ration, 14 Cold Medicine, 15 Diazepam, 16 PAL Key, 17 Card, 18 Timer Bomb,
; 19 Mine Detector, 20 Disk, 21 Rope, 22 Handkerchief, 23 Suppressor). Use
; 0,1,2,...,23 for the lot. Only items Snake does not have are touched - the
; game stores an absent item as -1 - so a real inventory is never reduced, and
; an item the game disabled on purpose is left disabled. The inventory is part
; of the save, so a save made afterwards keeps them. Empty for normal play.
GiveItems =

; MGS1 only: TEST AID. The same for weapons, in MGS1's own WP_ numbering, which
; is NOT the order the menu shows: 0 SOCOM, 1 FA-MAS, 2 Grenade, 3 Nikita,
; 4 Stinger, 5 Claymore, 6 C4, 7 Stun Grenade, 8 Chaff Grenade, 9 PSG1. Use
; 0,1,2,3,4,5,6,7,8,9 for the lot. A weapon Snake is not carrying is granted
; with the full magazine the game already records for it, and with none if it
; records no capacity - no ammo count is invented. Weapons already carried are
; left untouched, ammo and all. Empty for normal play.
GiveWeapons =

[Update Notifications]
; If set to true, MGSM2Fix will notify you when launching the game if a new MGSM2Fix update is available for download.
Expand Down
127 changes: 123 additions & 4 deletions src/games/mgs1.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,28 @@ void MGS1::SQOnMemoryDefine()
MGS1_LanguagePTR, MGS1_LanguageMask);
}

// GCL's variable buffer (libgcl/variable.c var_buf) is a static, not a memory
// define, and its address moves between builds - the Master Collection's USA
// executable has it 8 bytes below the retail disc's. What is stable is
// variable.c's own layout: var_buf[1024 shorts], sv_linkvarbuf[96],
// sv_var_buf[1024], stage_name[16], linkvarbuf[96], so var_buf sits 0x10D0
// below linkvarbuf and the "scene_name" define is stage_name, 0x10 below
// linkvarbuf. Read from RAM on Integral (0xB3CC8) and the collection's USA
// (0xB6440), the relation holds in both. The VR disks have no briefing menu.
MGS1_VarBufPTR = 0;
MGS1_UnlockWrites = 0;
MGS1_LastStageName[0] = 0;

switch (SQGlobals<Squirk::Standard>::GetTitle()) {
case 99: // INTEGRAL (its VR-DISK version shares the title id)
if (SQSystemData<Squirk::Standard>::SettingETC::GetVersion() != "INTEGRAL") break;
[[fallthrough]];
case 980: case 981: case 982: case 983: case 984: case 985: case 986: // MGS1 JP/US/UK/DE/FR/IT/ES
if (MGS1_GlobalsPTR != 0) MGS1_VarBufPTR = MGS1_GlobalsPTR + 0x10 - 0x10D0;
break;
}
if (MGS1_VarBufPTR != 0) {
spdlog::info("[MGS 1] GCL var_buf is 0x{:x}.", MGS1_VarBufPTR);
}
}

void MGS1::SQOnUpdateGadgets()
Expand All @@ -115,10 +135,16 @@ void MGS1::SQOnUpdateGadgets()
memcpy(MGS1_LastStageName, MGS1_StageName, sizeof(MGS1_StageName));
}


if (M2Config::bGameStageSelect) {
if (strcmp(MGS1_LoaderName, "title") == 0 && strcmp(MGS1_StageName, "select") != 0) {
strcpy(MGS1_LoaderName, "select");
// The retail developer menu ("select") offers only TITLE / DEMO ALL /
// SOUND TEST; the four stage lists are their own menus - select1
// (D00A..S04B), select2 (S05A..S09A), select3 (S11A..S14E, i.e.
// disc 2), select4 (S15A..S20A) - which it never links to, so the
// setting may name one of them to open it directly.
const char *menu = M2Config::sGameStageSelect.empty() ? "select" : M2Config::sGameStageSelect.c_str();
if (strcmp(MGS1_LoaderName, "title") == 0 && strncmp(MGS1_StageName, "select", 6) != 0) {
strncpy(MGS1_LoaderName, menu, sizeof(MGS1_LoaderName) - 1);
MGS1_LoaderName[sizeof(MGS1_LoaderName) - 1] = 0;
SQEmuTask<Squirk::Standard>::RamCopy(MGS1_LoaderPTR, MGS1_LoaderName, sizeof(MGS1_LoaderName));
spdlog::info("[MGS 1] Set mgs_loader_stage to \"{}\".", MGS1_LoaderName);
}
Expand Down Expand Up @@ -189,6 +215,99 @@ void MGS1::SQOnUpdateGadgets()
}
}

// The briefing menu shows an item only while its GCL `$f:` flag is set.
// Those flags live in var_buf, which GCL_InitVar zeroes on boot and the
// stage scripts set as the story advances, so a fresh boot shows 1 / 3 / 6
// of the sixteen. Hold all of them set while the title screens and the
// briefing itself are up - and never once a stage is running, because
// var_buf is then the live game's flag memory.
if (M2Config::bGameUnlockBriefing && MGS1_VarBufPTR != 0
&& (!strcmp(MGS1_StageName, "title") || !strcmp(MGS1_StageName, "brf"))) {
bool changed = false;
for (unsigned i = 0; i < sizeof(MGS1_BriefingFlagsMask); i++) {
uintptr_t addr = MGS1_VarBufPTR + MGS1_BriefingFlagsOffset + i;
SQInteger flags = SQEmuTask<Squirk::Standard>::GetRamValue(CHAR_BIT, addr) & 0xFF;
if ((flags & MGS1_BriefingFlagsMask[i]) != MGS1_BriefingFlagsMask[i]) {
SQEmuTask<Squirk::Standard>::SetRamValue(CHAR_BIT, addr,
flags | MGS1_BriefingFlagsMask[i]);
changed = true;
}
}
// Log the first few writes: more than one means something cleared the
// flags again between the title and the briefing.
if (changed && ++MGS1_UnlockWrites <= 8) {
spdlog::info("[MGS 1] Set the sixteen briefing flags (var_buf+0x{:x}, write {}, scene \"{}\").",
MGS1_BriefingFlagsOffset, MGS1_UnlockWrites, MGS1_StageName);
}
}
}

// [Game] GiveItems: a test aid. GM_Items is linkvarbuf[37..60], shorts
// (include/linkvar.h "0x4a Items"), and linkvarbuf sits 0x10 above the
// scene_name define - the same relation UnlockBriefing already relies on.
// Granted once per gameplay stage, only while one is running (names are
// sNNx / dNNx, read from the mirrored MGS1_LastStageName; the menus, title
// and the developer select are left alone). linkvarbuf is saved with the
// game, so a save made afterwards keeps the item.
//
// AN ITEM YOU DO NOT HAVE IS -1, NOT 0. game/g_define.h has it in one line,
// `IT_None = -1`, and game/item.c tests `GM_Items[IT_Ketchup] == -1`. This
// first granted nothing for exactly that reason: it looked for a count of
// zero, every absent item read 0xFFFF, and the log dutifully reported
// "count 65535 -> 65535" twenty-four times while the inventory stayed
// empty. Owned-but-disabled is a separate state - disable_equipment() ORs
// IT_TYPE_DISABLED (0x8000) into the entry - and 0x8001 is deliberately
// left alone here, so a sequence that took an item away on purpose is not
// quietly undone.
//
// Nothing writes GM_ItemsMax any more. It was being set to 1 wherever it
// read 0, which helped nothing: for the three consumables the max the game
// actually consults is GM_Items[id + 11] (item.c add_item), not
// GM_ItemsMax[id], and for everything else add_item just assigns. Writing a
// slot whose meaning is not established is not a safe default.
if ((!M2Config::vGameGiveItems.empty() || !M2Config::vGameGiveWeapons.empty()) && MGS1_GlobalsPTR != 0
&& strlen(MGS1_LastStageName) == 4 && (MGS1_LastStageName[0] == 's' || MGS1_LastStageName[0] == 'd')
&& isdigit((unsigned char)MGS1_LastStageName[1]) && isdigit((unsigned char)MGS1_LastStageName[2])
&& strcmp(MGS1_GaveItemsIn, MGS1_LastStageName) != 0) {
strcpy(MGS1_GaveItemsIn, MGS1_LastStageName);
uintptr_t items = MGS1_GlobalsPTR + 0x10 + 0x4A; // linkvarbuf + GM_Items
for (int id : M2Config::vGameGiveItems) {
SQInteger have = SQEmuTask<Squirk::Standard>::GetRamValue(16, items + id * 2) & 0xFFFF;
bool absent = (have == 0xFFFF || have == 0);
if (absent) SQEmuTask<Squirk::Standard>::SetRamValue(16, items + id * 2, 1);
spdlog::info("[MGS 1] GiveItems: item {} in stage \"{}\" - {} ({} -> {}).",
id, MGS1_LastStageName, absent ? "granted" : "already held, left alone",
have, absent ? 1 : have);
}

// [Game] GiveWeapons. A weapon is a different shape from an item: two
// arrays, GM_Weapons at linkvarbuf[17..26] holding the CURRENT AMMO and
// GM_WeaponsMax at [27..36] holding the capacity, ten of each. The
// menu's own ownership test is `GM_Weapons[i] >= 0` (menu/weapon.c), so
// -1 is "not carried" here exactly as it is for items.
//
// What to put in it is taken from the game rather than invented. Its
// own add_weapon (game/item.c) lifts a negative entry to 0 and then
// fills toward GM_WeaponsMax, so that is what happens here: a weapon
// Snake does not have is given its recorded full magazine, and where
// the capacity is not set it is granted empty rather than having a
// number made up for it. Nothing writes GM_WeaponsMax - no capacity
// table exists in the decompiled source to write a truthful one from.
uintptr_t weapons = MGS1_GlobalsPTR + 0x10 + 0x22; // linkvarbuf + GM_Weapons
uintptr_t wmax = MGS1_GlobalsPTR + 0x10 + 0x36; // linkvarbuf + GM_WeaponsMax
for (int id : M2Config::vGameGiveWeapons) {
short ammo = (short)(SQEmuTask<Squirk::Standard>::GetRamValue(16, weapons + id * 2) & 0xFFFF);
short cap = (short)(SQEmuTask<Squirk::Standard>::GetRamValue(16, wmax + id * 2) & 0xFFFF);
if (ammo >= 0) {
spdlog::info("[MGS 1] GiveWeapons: weapon {} in stage \"{}\" - already carried,"
" left alone (ammo {} of {}).", id, MGS1_LastStageName, ammo, cap);
continue;
}
short give = cap > 0 ? cap : 0;
SQEmuTask<Squirk::Standard>::SetRamValue(16, weapons + id * 2, give);
spdlog::info("[MGS 1] GiveWeapons: weapon {} in stage \"{}\" - granted with {} of {}"
" ammo ({} -> {}).", id, MGS1_LastStageName, give, cap, ammo, give);
}
}

if (M2Config::bAnalog.has_value() && M2Config::bAnalog.value()) {
Expand Down
83 changes: 83 additions & 0 deletions src/games/mgs1.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,79 @@ class MGS1 : public M2Game
MGS1_KetchupPatches = MGS1_BrightnessTextPatches();
}

// Every watch below is keyed to Integral - title 99, its version and its
// disk - and reports only while that image is the running one. These are
// raw disc-image offsets with nothing title-specific about them, so an
// unkeyed watch would log another release's patch under an Integral
// label. Registered here because Load() runs before the title is known;
// the key is checked when each collection patch is submitted.

// Report what the collection writes into Integral's `option` stage. It
// patches the stage's GCL script at one place - disc 1's is
// disc1_16F8E024_patch.bin, stage offset 152460, i.e. tag 6 (the script
// chunk) + 908 - and that patch is what redirects KEY CONFIG to the
// collection's own Control Settings panel. The English port relocates
// the stage, so the patch lands on sectors the game no longer reads and
// the interception is lost. Watching it means a change on their side is
// visible in the log rather than silent.
// span = the retail `option` stage, sectors 27136..27210, through
// (lba + fo / 2048) * 2352 + 24 + fo % 2048, with STAGE.DIR at LBA
// 136654 on disc 1 and 105178 on disc 2.
SQHook<Squirk::Standard>::SetPatchWatch(99, "INTEGRAL", 0,
0x16F634B8ull, 0x16F8E5C8ull, "Integral disc 1 option stage");
SQHook<Squirk::Standard>::SetPatchWatch(99, "INTEGRAL", 1,
0x128C92F8ull, 0x128F4408ull, "Integral disc 2 option stage");
// The MISSION LOG stage (`abst`, sectors 139..218 on both discs) was
// ported on 2026-09-05 (en_abst) and relocated into DUMMY3M, so the
// collection's patch below now lands on a stage the game no longer
// reads. The watch stays to show what it wanted to write: the one
// patch the collection offers there is disc1_132F2716_patch_PS5.bin,
// stage sector +51 - the disc-change abstract's block in the script
// chunk, which the port now fills with USA's eight strings. Whether a
// _PS5-suffixed patch is applied on Windows at all is still unseen.
SQHook<Squirk::Standard>::SetPatchWatch(99, "INTEGRAL", 0,
0x132D51C8ull, 0x133030C8ull, "Integral disc 1 abst stage");
SQHook<Squirk::Standard>::SetPatchWatch(99, "INTEGRAL", 1,
0x0EF3F538ull, 0x0EF6D438ull, "Integral disc 2 abst stage");
// The collection also patches the three other disc-swap text copies
// (`change`, `demosel`, `title` - named files disc1_18345E07,
// disc1_18412A95, disc1_18412BD8, disc1_1822B55D) two bytes before the
// records en_menu / en_menu2 write there, and six places in `camera`
// (en_camsave). Mapped 2026-09-05 from a filtered log; the watches show
// what they write and whether the port's records are overlapped.
SQHook<Squirk::Standard>::SetPatchWatch(99, "INTEGRAL", 0,
0x18341268ull, 0x18346518ull, "Integral disc 1 change stage");
SQHook<Squirk::Standard>::SetPatchWatch(99, "INTEGRAL", 0,
0x183F6078ull, 0x18416C28ull, "Integral disc 1 demosel stage");
SQHook<Squirk::Standard>::SetPatchWatch(99, "INTEGRAL", 0,
0x181E7788ull, 0x1825C9C8ull, "Integral disc 1 title stage");
SQHook<Squirk::Standard>::SetPatchWatch(99, "INTEGRAL", 0,
0x16F8E5C8ull, 0x16FA42E8ull, "Integral disc 1 camera stage");
SQHook<Squirk::Standard>::SetPatchWatch(99, "INTEGRAL", 1,
0x13CA70A8ull, 0x13CAC358ull, "Integral disc 2 change stage");
SQHook<Squirk::Standard>::SetPatchWatch(99, "INTEGRAL", 1,
0x13D5BEB8ull, 0x13D7CA68ull, "Integral disc 2 demosel stage");
SQHook<Squirk::Standard>::SetPatchWatch(99, "INTEGRAL", 1,
0x13B4D5C8ull, 0x13BC2808ull, "Integral disc 2 title stage");
SQHook<Squirk::Standard>::SetPatchWatch(99, "INTEGRAL", 1,
0x128F4408ull, 0x1290A128ull, "Integral disc 2 camera stage");
// Integral's VR-DISC (title 99, version VR-DISK; a separate image, so
// these offsets are small). The English port (tools/integral-english/
// vr_*.py, 2026-09-06) patches these stages IN PLACE; the option stage
// in particular keeps its sectors so the collection's KEY CONFIG
// interception keeps landing - these watches show where that patch is
// and whether any collection patch overlaps the port's records.
SQHook<Squirk::Standard>::SetPatchWatch(99, "VR-DISK", 0,
0x0005E7EB0ull, 0x000611D60ull, "Integral VR-DISK option stage");
SQHook<Squirk::Standard>::SetPatchWatch(99, "VR-DISK", 0,
0x000611D60ull, 0x000627A80ull, "Integral VR-DISK camera stage");
SQHook<Squirk::Standard>::SetPatchWatch(99, "VR-DISK", 0,
0x00315B1C0ull, 0x0031B5430ull, "Integral VR-DISK vrtitle stage");
SQHook<Squirk::Standard>::SetPatchWatch(99, "VR-DISK", 0,
0x004C26BA0ull, 0x004C6D5B0ull, "Integral VR-DISK movie stage");
SQHook<Squirk::Standard>::SetPatchWatch(99, "VR-DISK", 0,
0x000CFA6E0ull, 0x000D5D320ull, "Integral VR-DISK vrsave stage");

if (M2Config::bPatchesDisableFont) {
for (auto & MGS1_TextureWhitelist_Font : MGS1_TextureWhitelist_Fonts) {
SQHook<Squirk::Standard>::SetTextureWhitelist(MGS1_TextureWhitelist_Font);
Expand Down Expand Up @@ -203,7 +276,17 @@ class MGS1 : public M2Game
unsigned MGS1_ConfigSeen = 0;
bool MGS1_ConfigSeenValid = false;
unsigned MGS1_ConfigPreserved = 0;
// libgcl's var_buf (variable.c) - the GCL `$f:`/`$w:` variable memory. Not a
// memory define; a static whose address is read off GCL_GetVar's constants
// in each executable. The briefing menu's sixteen items are gated by the
// flags at var_buf+0x4C..0x4E (bits 0x4C.1-7, 0x4D.0-7, 0x4E.0), which the
// brf stage script passes to the menu as its `-f` option.
uintptr_t MGS1_VarBufPTR = 0;
unsigned MGS1_UnlockWrites = 0;
char MGS1_GaveItemsIn[16] = {}; // stage the GiveItems grant last ran in
char MGS1_LastStageName[8] = { 0 };
constexpr static unsigned MGS1_BriefingFlagsOffset = 0x4C;
constexpr static unsigned char MGS1_BriefingFlagsMask[3] = { 0xFE, 0xFF, 0x01 };
static inline HSQOBJECT<Squirk::Standard> SQ_EmuTask_getHeight = {};

#ifndef _WIN64
Expand Down
Loading