flowchart LR A["Correct source ROM"] --> C["Apply IPS"] B["IPS containing only the changes"] --> C C --> D["Corrected ROM"]
How the IPS Patch Was Created
A reproducible record of a 12-byte correction for local verification
This IPS patch was not produced by giving two ROMs to a general-purpose difference tool. A dedicated Node.js script explicitly defines the offsets and new bytes, then constructs the 30-byte IPS file directly.
The patch contains two changes totaling 12 bytes. After generation, the script reapplies the IPS to the source ROM and confirms that its result is byte-for-byte identical to the corrected ROM it created independently.
What is an IPS patch?
An IPS patch is a small file that records which locations in another file should be changed and what their new contents should be. It does not contain another complete copy of the game ROM.
One way to understand it is as an errata sheet for a long book:
Replace five characters beginning with the tenth character on page 24.
Replace seven characters near the end of page 37.
IPS uses the same idea. Each record contains a location in the file, the length to replace, and the new contents. Applying those records to the source ROM changes only the specified locations and creates a corrected ROM.
An IPS does not contain most of the source ROM. Its status as a difference file does not, by itself, guarantee that publishing or distributing it is lawful.
This repository does not track or distribute the ROM, a corrected ROM, or the generated IPS. Anyone repeating the verification should review applicable laws and terms and use only a source ROM they can lawfully use.
Why IPS was used
There were three main reasons for using IPS in this investigation:
- To avoid overwriting the source ROM. The corrected build is written to a separate file, leaving the source untouched.
- To express the correction compactly. Only the 12 changed bytes and format metadata are needed, rather than another 512 KiB ROM.
- To make the scope of the change verifiable. Inspecting the IPS records confirms that no unintended location was modified.
Which ROM locations were changed?
The stage-ending routine originally contained these five bytes:
241A: 16 05 ld d,$05
241C: CD 90 FF call $FF90The investigation showed that the second banked call must receive DE=1400. The required replacement takes seven bytes and does not fit in the original five-byte region.
The original location was therefore changed to call unused space at $37F9, where the required work is performed before returning. This report calls that short intermediate routine a trampoline.
Change 1: Replace the original five bytes with a trampoline call
These five bytes are written at ROM file offset $241A:
241A: CD F9 37 call $37F9
241D: 00 nop
241E: 00 nopcall $37F9 transfers control to the trampoline. The two remaining bytes are filled with nop, an instruction that performs no operation.
Change 2: Place the seven-byte trampoline in unused space
These seven bytes are written at ROM file offset $37F9:
37F9: 11 00 14 ld de,$1400
37FC: CD 90 FF call $FF90
37FF: C9 retThe trampoline sets DE to the required value $1400, runs the original destination at $FF90, and finally uses ret to return to its caller.
| Record | Write offset | Length | New bytes |
|---|---|---|---|
| 1 | $241A |
5 bytes | CD F9 37 00 00 |
| 2 | $37F9 |
7 bytes | 11 00 14 CD 90 FF C9 |
| Total | 2 locations | 12 bytes | — |
Both offsets are within fixed bank 0, $0000-$3FFF, so the CPU addresses and ROM file offsets are the same in this case.
Contents of the IPS file
The generated IPS is 30 bytes. In hexadecimal, the complete file is:
50 41 54 43 48
00 24 1A 00 05 CD F9 37 00 00
00 37 F9 00 07 11 00 14 CD 90 FF C9
45 4F 46Broken down by purpose, it has this structure:
| Part | Hexadecimal | Meaning |
|---|---|---|
| Header | 50 41 54 43 48 |
ASCII text PATCH |
| Record 1 offset | 00 24 1A |
$241A |
| Record 1 length | 00 05 |
5 bytes |
| Record 1 contents | CD F9 37 00 00 |
Instruction that calls the trampoline |
| Record 2 offset | 00 37 F9 |
$37F9 |
| Record 2 length | 00 07 |
7 bytes |
| Record 2 contents | 11 00 14 CD 90 FF C9 |
Call $FF90 with DE=1400 |
| Terminator | 45 4F 46 |
ASCII text EOF |
Offsets use three bytes, lengths use two bytes, and the most significant byte is stored first. For example, 00 24 1A means hexadecimal $241A, while 00 05 means five bytes.
The file is 30 rather than 12 bytes because the 12 data bytes also require PATCH, an offset and length for each of two records, and EOF.
5 bytes : PATCH
10 bytes : Record 1 (offset 3 + length 2 + data 5)
12 bytes : Record 2 (offset 3 + length 2 + data 7)
3 bytes : EOF
----------------
30 bytes : Total
This IPS does not use the format’s run-length encoding feature.
Why use a dedicated script?
General IPS tools commonly compare a source ROM and an already corrected ROM to discover their differences. build-stage-transition-patch.mjs takes a slightly different approach.
The script explicitly lists the offsets and new bytes:
const ipsRecords = [
{ offset: CALL_SITE, data: callPatch },
{ offset: TRAMPOLINE, data: trampolinePatch },
];This makes it difficult to include an accidental third change and produces the same output every time from the same input. The same script can also validate both input and output rather than only creating the patch.
Generation procedure
1. Prepare the required files
- Node.js
- The correction script,
build-stage-transition-patch.mjs - The correct source ROM,
pokemon_jade_pikachu.gb
The expected source ROM is:
| Item | Value |
|---|---|
| File size | 524,288 bytes (512 KiB) |
| SHA-256 | 0E90D7659339A1F52C733BDC2B502D033E1C4C4004F761B6C539B236FA7FDD45 |
CRC32 of header $0100-$014F |
8628A287 |
SHA-256 is an identifier calculated from the entire file. A difference of even one byte will ordinarily produce a different value. A ROM with the same filename but a different SHA-256 is not a valid input for this patch.
2. Run the script
From the repository root, run this command in PowerShell:
node tools/build-stage-transition-patch.mjs `
pokemon_jade_pikachu.gb `
build/pokemon_jade_pikachu_stage_transition_fixed_mgba.gb `
build/pokemon_jade_pikachu_stage_transition_fix_mgba.ipsThe three filenames, in order, mean:
- Source ROM input
- Corrected ROM output
- IPS patch output
The backtick at the end of a line continues a PowerShell command onto the next line. The command can also be entered on one line.
3. Check the success messages
A successful generation displays output similar to this:
PASS source SHA-256 0E90D765...
PASS preserved header CRC32 8628A287
PASS source bytes at $241A and $37F9
PASS IPS round-trip reproduces patched ROM
OUTPUT ROM build/pokemon_jade_pikachu_stage_transition_fixed_mgba.gb
OUTPUT SHA-256 7463D309...
OUTPUT IPS build/pokemon_jade_pikachu_stage_transition_fix_mgba.ips
PASS indicates that a check succeeded. If a condition does not match, the script prints FAIL, stops, and writes no artifacts.
Safety checks performed by the script
The script does more than replace bytes. It checks the following conditions in order:
- The source ROM is 512 KiB.
- Its SHA-256 matches the target of the analysis.
- Its header CRC32 is the
8628A287expected by mGBA. $241Acontains the five original bytes.$37F9-$37FFcontains the sevenFFbytes expected in the unused region.- The source ROM is copied in memory, and only the copy is modified.
- Both modified locations contain the expected bytes.
- The ROM header at
$0100-$014Fremains unchanged from the source. - An IPS is assembled from the two change records.
- That IPS is reapplied to the source ROM and compared byte-for-byte with the corrected ROM.
- The corrected ROM and IPS are written only after every check passes.
What does reapplying and comparing mean?
This is an especially important part of generation.
flowchart TD
A["Copy source ROM and directly change 12 bytes"] --> B["Corrected ROM contents"]
C["Generate IPS from the same two records"] --> D["Apply IPS to source ROM"]
D --> E["Contents reconstructed from IPS"]
B --> F{"Do all bytes match?"}
E --> F
F -->|Match| G["Write artifacts"]
F -->|Mismatch| H["Stop with FAIL"]
Creating the corrected ROM and applying the IPS are separate operations. If their results are identical, errors in encoding an IPS offset, length, or data can be detected. This is called round-trip verification.
Why the ROM header is not changed
After modifying a ROM, it would usually be tempting to update the global checksum stored in the ROM. In this case, however, mGBA selects the special NT-new mapper using the CRC32 of header bytes $0100-$014F. The global checksum at $014E-$014F lies within that range.
flowchart TD A["Change the global checksum"] --> B["Header CRC32 changes"] B --> C["mGBA no longer recognizes the registered ROM"] C --> D["Required NT-new mapper is not selected"]
The patch therefore preserves the source header exactly. The script also checks that not one byte of the header changed.
Cautions when using the IPS
The following procedure is intended for local reproduction of the investigation. It neither recommends nor authorizes distribution of the generated files.
The IPS format has no built-in field for recording the SHA-256 of the required source ROM. A general IPS tool may continue even when a different ROM is selected. It is therefore important to verify the source ROM’s SHA-256 before applying the patch.
PowerShell can calculate it with:
Get-FileHash -Algorithm SHA256 pokemon_jade_pikachu.gbConfirm that the displayed value matches exactly:
0E90D7659339A1F52C733BDC2B502D033E1C4C4004F761B6C539B236FA7FDD45
When using another IPS application, do not overwrite the source ROM; save the corrected build under a different filename. The corrected ROM should have this SHA-256:
7463D30924B2827CFB824F986A5571B079B7F4952E1912023C5705B91E39CFCA
The generated 30-byte IPS has this SHA-256:
850EC96666011C792D62C4F587D0CDBAC2DCA87A3294A46D2651486D6BEF2584
Verified and unverified items
| Item | Status | Details |
|---|---|---|
| IPS structure | Verified | 30-byte IPS with two records changing 12 bytes total |
| Source-ROM identification | Verified | Size, SHA-256, and header CRC32 checked |
| IPS round trip | Verified | Application result matches the corrected ROM byte-for-byte |
| Transition from Stage 1 to Stage 2 in mGBA | Verified | Next stage appears without display corruption |
| Stages after Stage 2 | Verified | Transitions to the next stage after Stages 1 through 4 and to the ending after Stage 5 |
| Reproduction with other IPS tools | Not verified | An additional tool can be checked against the same corrected-ROM SHA-256 |