Sector bugs, UI polish, and closing in on beta
Sector bugs, UI polish, and closing in on beta
It’s been a minute since the last update, and a lot has happened. Last time I left off on a high note – the radio expansion was working, disc 1 story calls were translated, and the tooling was finally feeling mature.
It was my hope I was about to reveal some great news… and now after squashing a major bug, I’m ready to let people know it’s there:
I present to you… the MGS1 Translation Toolkit! https://github.com/DoktorDeSparkle/mgs1-translation-toolkit

Since the beginning of my project, I’ve known there were many other folks who want to translate this wonderful game into different languages. Now that the command line scripts are nearing completion, I had accelerated a side-project of making them visually editable in a fully-fledged GUI application.
I want to talk more about it, but it’s still very much in beta. And there are a lot of improvements yet to be made. But the logic mostly runs on the scripts I already had in place. More features will continue to come, but in the meantime, please test it out and provide feedback, either via discord or issues. I will be making sure both ways to edit are doable.
Part of the reason i pivoted over was that one or two use cases are easier to do in the GUI. I had planned on testing that we can split subtitles in RADIO into two separate dialogue timings. I think it works, but in testing I discovered one of the worst bugs I could’ve hit, and it much delayed launching the GUI to you fine folks. So without further ado…
The USA sector bug – or, how one byte can ruin your whole week
So here’s some context. After the long bytes expansion landed for Integral, I started circling back to the original retail versions – USA and Japanese. These are the versions most people actually played, and the ones I ultimately want to ship first. The recompiler was already handling USA/JPN STAGE.DIR offset patching (different from Integral’s block-based offsets), and the round-trip tests were passing. Everything looked good on paper.
Then I tried actually playing a modified USA build and it crashed. Hard.
Not on boot, not on the menu – on the first codec call. 140.85, Campbell, the very first thing you hear in the game. The codec window opens, you see maybe one frame of text, and then the game locks up or throws garbage on screen.
It was so weird. I thought there must be some kind of weird issue with the GUI, as I’d never seen this occur on the japanese version. And weirder yet, the garbled graphics disappeared leaving Snake’s face and no Campbell. No dialogue or subtitles appeared, and the music would stop dead.
The full video will be up on youtube soon. It was certainly bizzare to behold. And i had no idea why after all the logic of RADIO.DAT was figured out, we’re hitting this issue.
Here’s the full video:
Down the rabbit hole
I’ll let Claude walk through the technical details here, because honestly this one required some serious disassembly work that I could not have done alone.
The crash comes down to an 8-byte reference in the GCX scripts inside STAGE.DIR. Every radio call has a pointer that tells the game where to find it in RADIO.DAT. That pointer looks like this:
[01] [freq_hi] [freq_lo] [0a] [byte4] [byte5] [byte6] [byte7]
Bytes 5-7 are a 3-byte big-endian byte offset into RADIO.DAT. That part, my recompiler was updating correctly. But byte4? That’s a sector count. It tells the game how many 2048-byte sectors to read from the CD. The game computes:
int sectorCount = (byte4 + 1); // byte4 + 1
int size = sectorCount * 2048; // total bytes to load
int startSector = byteOffset / 2048; // which sector to start at
int withinSector = byteOffset % 2048; // offset within that sector
That last line is the killer. Unlike Integral, which sector-aligns every call so withinSector is always 0, the USA and JPN versions pack calls tightly. A call can start anywhere within a sector. The game allocates a buffer of (byte4 + 1) * 2048 bytes, reads that many bytes from disc, and then sets its internal pointer to buffer + withinSector.
My recompiler was updating the byte offset (bytes 5-7) when calls moved around in the rebuilt RADIO.DAT. But it was not touching byte4. So when a call shifts to a new position with a different within-sector alignment, the original sector count might not cover the full call anymore.
What the crash actually looked like
We confirmed this with a GDB trace against SLUS_005.94. Here’s what was happening:
- Call data starts at
withinSector = 2032(0x7F0) – 16 bytes before the end of the sector byte4 = 0x00– game reads 1 sector (2048 bytes)- Usable bytes after the within-sector offset:
2048 - 2032 = 16 - Actual call size: 843 bytes
So the game loads 16 bytes of valid call data, then hits stale buffer contents. The GCL interpreter processes the first valid command, loops back, reads byte 0xAE from leftover memory, and crashes into the “illegal code” path.
Sixteen bytes. That’s all it got before everything went sideways.
The scope of the problem
I had Claude scan all 515 Campbell (140.85) references in STAGE.DIR. 506 had their byte offsets modified by the recompiler. Byte4 was preserved in every single one. This wasn’t a one-off – it was systematic.
Confirming across versions
One thing that gave me some peace of mind: we confirmed through disassembly that USA and JPN are instruction-for-instruction identical in how they decode this pointer. Same opcodes, same register allocation, same logic. Only the absolute addresses differ. So whatever fix we apply works for both versions.
Integral, predictably, is the odd one out. It uses a completely different decoding path – andi 0xFFFF to extract a 16-bit sector number (not a byte offset), no addiu +1 on the sector count, and no within-sector adjustment. This tracks with what we already knew: Integral sector-aligns all calls, so it never needs within-sector math.
The fix
The cleanest option is to sector-align USA calls the same way Integral does. Pad every call to 0x800 boundaries in the rebuilt RADIO.DAT, then withinSector is always 0, and we just need:
sectors_needed = math.ceil(call_total_size / 2048)
stageBytes[key + 4] = sectors_needed - 1 # update byte4
stageBytes[key + 5: key + 8] = struct.pack('>L', new_offset)[1:4] # update offset
The tradeoff is RADIO.DAT inflates by up to ~1MB from the padding. Need to verify it fits on disc, but given we’re already rebuilding ISOs with mkpsxiso, I don’t think this will be a problem.
The alternative – recomputing byte4 without padding – also works but requires carrying call_total_size through to STAGE.DIR patch time, which adds complexity. I’m leaning towards the padding approach for now because it’s the same strategy that’s already proven on Integral, and frankly I’m tired of debugging sector math.
GUI progress
In the last post I mentioned having Claude run forward with the GUI editor. I’ve been coming back to it in between the heavier reverse engineering work, and it’s genuinely starting to feel like a real application.
For those who missed it: the GUI is a PySide6 (Qt) desktop app with four editor modes – Radio, Demo, VOX, and ZMovie – each tailored to its respective game file format. The idea is that someone who doesn’t want to touch the command line can open a .mtp project file, browse codec calls or cutscene dialogue, edit translations, preview how they’ll look, and export a build.
Recent work has been focused on:
-
Code review tooling: I wrote (well, Claude and I wrote) a pair of code review scripts –
code_review.pyfor general Python review andcode_review_mgs.pywith project-specific context injected. These use the Claude API to review code in chunks, split by AST boundaries, and output categorized markdown reports. It’s been genuinely useful for catching things I’d otherwise miss in a codebase this size. The MGS wrapper knows about the four editor modes, the key globals inmainwindow.py, and known issues like the module identity bug and the full-width vertical bar line breaks. -
VOX offset adjuster integration: The new
voxOffsetAdjuster.py(149 lines) mirrors whatdemoOffsetAdjuster.pydoes for DEMO.DAT. When VOX text injection causes blocks to grow, downstream STAGE.DIR offsets go stale. The adjuster recalculates them. This is now wired into the build pipeline in the right order, which means VOX injection actually works end-to-end for the first time. -
Build test script:
runJpnBuildTest.shrecompiles all game data and launches directly in DuckStation. Has skip flags for each asset type so you can iterate on just what you’re working on. Small thing, but it’s cut my test cycle time significantly.
There’s still a long feature request list – font changer, mkpsxiso integration, ZMOVIE playback with subtitle preview, a mass auto-format button for Radio entries – but the core editing workflows are solid. I’m holding off on a public release until I can do a proper pass on the UX, but it’s getting close.
Translation sprint and where things stand
The big milestone from last month still stands: disc 1 story calls are fully translated. Every codec call Snake can place through the end of disc 1 is done. The torture sequence, the Meryl and Wolf sections, all of it.
VOX entries for in-game dialogue are indexed and coming along. The ZMOVIE bug (off-by-one in the two-block case for zmovie-02) is fixed. The kanji cleanup from PR #7 caught phantom font entries that would have caused mid-playthrough rendering issues.
The big remaining items:
- USA sector bug fix – implementing and testing the padding approach described above
- Port long bytes to Japanese binary – 3 code sites to patch manually, similar to what we did for Integral but with different addresses
- Disc 2 translation – demos barely started, radio not started
- Full non-story radio translation – this is the dense part. Hundreds of calls.
- Retail build – still won’t boot, separate investigation needed
The tooling is mature. The pipeline is solid. The sector bug was the last real showstopper for USA builds, and now that we understand exactly what’s happening, the fix is straightforward. I said in the last post that a public beta of disc 1 might be within reach in a month or two. That timeline has slipped a bit with the sector bug detour, but not by much.
More soon.
-J-Rush