# Baking Dolphin Gecko Codes into a GameCube ISO for Real Hardware (Nintendont)

A repeatable method for taking known Dolphin Gecko codes (widescreen, filtering,
etc.) and turning them into a **static `main.dol` patch** that runs on a real
Wii/GameCube via Nintendont, no cheat engine involved.

This exists because some Gecko codes work in Dolphin but crash the cheat engine
(kenobi) on Nintendont, and because some published codes rely on
Dolphin-only behaviour that crashes real hardware. Baking the logic into the
executable sidesteps both problems.

Worked example throughout: *The Legend of Zelda: Four Swords Adventures*, PAL
(game ID **G4SP01**).

---

## 0. Tools you need

- **Dolphin** (for converting disc images and extracting `main.dol`, and as a
  fast local test loop).
- **Python 3** (standard library only).
- The injector script (`inject_widescreen.py`) included alongside this guide.
- A hex-aware mindset. You will be reading a binary header and doing some
  address math, but the scripts handle the arithmetic.

---

## 1. Get a clean, untouched ISO

1. Dump your own physical disc if you can. If you own the game but never dumped
   it, a clean image is the practical substitute. For FSA PAL the RVZ lives at
   <https://vimm.net/vault/38510>.
2. **Convert to a plain ISO.** Open the RVZ in Dolphin, right-click the game ->
   **Convert File** -> Format **ISO**, compression None.

   - **Never patch an `.nkit.iso`, and don't rely on it as your source.** A
     GameCube NKit image boots fine unmodified, so it's tempting, but it's
     scrubbed and restructured, and editing bytes inside it does not work (this
     is exactly what cost us hours). Start from a clean RVZ as in step 1.

Keep this clean ISO. Every patch run should start from an unmodified copy.

---

## 2. Get the codes for YOUR region

Gecko codes are **region-specific**: the RAM addresses differ between NA, EU,
and JP because the game binaries differ. Match the code block to your disc's
game ID:

The codes for this example come from the Dolphin wiki page for the game:
<https://wiki.dolphin-emu.org/index.php/The_Legend_of_Zelda:_Four_Swords_Adventures>
(for other games, swap in that game's wiki page).

| Region | Game ID | Use the wiki's... |
|--------|---------|-------------------|
| NA     | G4SE01  | NA block          |
| EU/PAL | G4SP01  | EU block          |
| JP     | G4SJ01  | JP block          |

**Read the warnings, and don't assume the wiki code is the right one.** The
Dolphin wiki's FSA widescreen codes are explicitly flagged: "dependent on unique
Dolphin behavior and instantly crash the game on real hardware." The wiki's EU
widescreen code is an `F6`/`D2` search-and-patch block that gets the 1.3333
aspect constant by writing it to memory (`stw r14, 0x7000(r2)`) and reading it
back; that store lands on live memory and dies on hardware. So for widescreen we
did **not** use a wiki code at all - the working PAL code came from **gc-forever**
(Ralf), `C2047C58 ...` (PAL thread:
<https://gc-forever.com/forums/viewtopic.php?t=2136>), which reuses an existing
1.3333 constant already in the rtoc instead of storing one. The filter code, by
contrast, IS the wiki's EU code and works as-is.

Takeaway for other games: when a wiki code is flagged as hardware-unsafe, find a
hardware-tested equivalent (gc-forever threads are the usual source) rather than
assuming the wiki code will bake in cleanly. And if you test a code live in
Dolphin to confirm its logic, be clear with yourself about which code that was -
a Dolphin test proving the logic works does not mean the wiki's version is the
one that's hardware-safe.

---

## 3. Understand the Gecko code types you'll translate

You only need to handle a couple of types to bake most codes in:

- **`04aaaaaa vvvvvvvv` (32-bit write):** write value `vvvvvvvv` at RAM address
  `8000aaaa`... i.e. `0x80000000 | 0xaaaaaa`. This is a direct instruction/data
  replacement. Trivial: overwrite those 4 bytes.

- **`C2aaaaaa 000000nn` + ASM (insert assembly):** at RAM address `0x80aaaaaa`,
  the code handler replaces the instruction with a branch to a buffer, runs your
  `nn` lines (2 instructions per line) of ASM, then branches back to
  `address + 4`. **The original instruction is NOT preserved automatically** -
  the author must include it in the ASM (it's usually the last meaningful
  instruction before the `60000000`/`00000000` padding). To bake a C2 in, you
  recreate that branch-to-cave-and-back yourself (see step 5).

- **Dolphin-only types (`F6`, `D2`, `E0`, etc.):** pattern-search and
  conditional patch types. These are often the ones flagged as crashing on
  hardware. Prefer a plain `C2`/`04` equivalent.

---

## 4. Extract `main.dol`

In Dolphin: right-click the game -> **Properties** -> **Filesystem** tab ->
right-click the top **"Disc"** node -> **Extract System Data...** -> choose a
folder. You get `boot.bin`, `bi2.bin`, `apploader.img`, `fst.bin`, and
**`main.dol`**. The DOL is the file you patch.

---

## 5. Translate the codes into a `main.dol` patch

### 5a. Parse the DOL header

A DOL header is 0x100 bytes:

| Offset | Contents |
|--------|----------|
| 0x00 | 7 text-section file offsets (4 bytes each) |
| 0x1C | 11 data-section file offsets |
| 0x48 | 7 text-section RAM addresses |
| 0x64 | 11 data-section RAM addresses |
| 0x90 | 7 text-section sizes |
| 0xAC | 11 data-section sizes |
| 0xD8 | BSS RAM address |
| 0xDC | BSS size |
| 0xE0 | Entry point |

To convert a RAM address to a file offset, find the section whose
`[addr, addr+size)` range contains it, then
`file_offset = section_file_offset + (ram - section_ram_addr)`.

### 5b. Apply `04` writes

Map the address to a file offset, write the 4-byte value. Done. (Always read the
existing bytes first and confirm they're what the code expects to replace.)

### 5c. Apply `C2` codes with a code cave

This is the part that bit us repeatedly, so follow it carefully.

1. **Decode the payload.** Identify the real instructions vs. the trailing
   `60000000`/`00000000` padding. Confirm whether the last real instruction is
   the game's **original** instruction at the hook address (read that address in
   the DOL and compare). If it is, it must run inside your cave so behaviour is
   preserved.

2. **Find a code cave** - free space to hold `[your ASM] + [branch back]`. This
   is the single most important decision:

   - **The cave must be in RESIDENT memory** - memory that stays put the whole
     time the game runs.
   - **Do NOT use the small first text section** (the bootstrap/init section,
     often a few KB at the lowest text address). It can be reused after init and
     your cave gets clobbered -> black screen.
   - **Good locations:** trailing padding of the **main text section**, or a
     **sandwiched zero gap** (alignment padding between two real data structures)
     in a data section. On GameCube all of MEM1 is executable, so code runs fine
     from a data section.
   - **Avoid** the head of a giant zero block (might be a runtime buffer) and the
     small-data sections near the r2/r13 bases (sdata/sbss are written at
     runtime).
   - **Validation:** a bad cave fails in Dolphin too, not just on hardware, so
     Dolphin is a valid test for cave residency. (This is different from a
     bad-logic crash, which can be hardware-only.)

3. **Write the hook and cave:**
   - At the hook address: `b <cave>` (unconditional branch).
   - At the cave: your real ASM instructions, then `b <hook+4>` to return.
   - Branch encoding: `0x48000000 | ((target - source) & 0x03FFFFFC)`. Range is
     +/-32 MB, never an issue inside one DOL.

### 5d. Keep the DOL the same size

Only edit bytes in place. Don't append or grow the file. A same-size DOL can be
injected into the ISO without rebuilding anything (step 6). If you ever need more
room than existing padding allows, prefer multiple small caves over resizing.

---

## 6. Inject the patched `main.dol` into the ISO

The injector reads the DOL offset from `boot.bin` (ISO offset 0x420),
sanity-checks the bytes at the hook before writing, makes a `.bak`, then
overwrites the DOL in place (same size = clean swap). Save it as
`inject_widescreen.py`:

```python
#!/usr/bin/env python3
# Injects the patched main.dol into a GameCube ISO in place (same size, safe).
# Usage: python3 inject_widescreen.py <game.iso> [patched_main.dol]
import struct, sys, shutil, os

def main():
    if len(sys.argv) < 2:
        print("Usage: python3 inject_widescreen.py <game.iso> [patched_main.dol]")
        sys.exit(1)
    iso_path = sys.argv[1]
    dol_path = sys.argv[2] if len(sys.argv) > 2 else "main.dol"

    patched = open(dol_path, "rb").read()
    with open(iso_path, "r+b") as iso:
        iso.seek(0x420)                          # boot.bin: main.dol offset (generic)
        dol_off = struct.unpack(">I", iso.read(4))[0]
        print(f"DOL offset in ISO: 0x{dol_off:X}")

        # --- The next three constants are GAME-SPECIFIC (FSA PAL / G4SP01) ---
        # 0x288B8     = file offset of the widescreen hook inside the DOL
        # ED494024    = original instruction expected there (unpatched)
        # 4842E858    = the branch we write there (i.e. "already patched")
        # For another game, change these to that game's hook offset/bytes.
        iso.seek(dol_off + 0x288B8)
        cur = iso.read(4)
        print(f"Current bytes at hook: {cur.hex().upper()}")

        if cur == bytes.fromhex("4842E858"):
            print("ISO is already patched. Nothing to do.")
            return
        if cur != bytes.fromhex("ED494024"):
            print("ERROR: expected ED494024 here. Wrong ISO/region or wrong file. Aborting.")
            return

        backup = iso_path + ".bak"
        if not os.path.exists(backup):
            shutil.copy(iso_path, backup)
            print(f"Backup saved: {backup}")

        iso.seek(dol_off)
        iso.write(patched)
    print(f"Injected {len(patched)} bytes at 0x{dol_off:X}. Done.")

if __name__ == "__main__":
    main()
```

Run it with the patched `main.dol` in the same folder:

```
python3 inject_widescreen.py "Game.iso"
```

**Adapting the script for other games:** the three constants flagged in the
comments are FSA-specific. The hook file offset (`0x288B8`), the expected
original bytes (`ED494024`), and the already-patched marker (`4842E858`) all
change per game. Update them, or strip the sanity check entirely, when patching a
different title. Everything else (DOL offset lookup, backup, in-place write) is
generic.

---

## 7. Test

1. **Dolphin first** (fast loop). Load the patched plain ISO. This validates the
   code logic AND whether the cave survives. If it works here, the cave is good.
2. **Real hardware** (Nintendont). Copy the patched plain ISO to your SD/USB.
   Set **Cheats OFF**, no `.gct`, leave **Force Widescreen OFF** and video on
   **Auto**. Boot.

If Dolphin is clean but hardware black-screens, suspect the code's *logic* being
hardware-incompatible (e.g. a Dolphin-only memory trick), not the cave.

---

## 8. Lessons learned (the short version)

- Start from a clean **RVZ** and convert it to a **plain ISO** before patching.
  Don't use NKit as a source and never edit an NKit image.
- The cave **must be resident**; never the init/bootstrap text section.
- A bad cave fails in **Dolphin too**, so use Dolphin to validate cave choice.
- Watch for **"crashes on real hardware"** warnings on published codes; the
  store-to-memory aspect-ratio trick is the classic offender. Use a
  hardware-tested code instead.
- **Region matters.** G4SE / G4SP / G4SJ have different addresses; never mix.
- Nintendont's cheat engine can choke on `C2` codes Dolphin runs fine. Baking
  into the DOL avoids the cheat engine entirely.
- Keep the DOL the **same size** so injection is a clean in-place overwrite.

---

## Appendix: exact FSA PAL (G4SP01) patch used

**16:9 Widescreen** (gc-forever / Ralf, NOT a Dolphin wiki code; hardware-safe
because it reuses the rtoc 1.3333 constant rather than storing one).

Source: gc-forever PAL thread <https://gc-forever.com/forums/viewtopic.php?t=2136>,
specifically Ralf's post with the fixed code:
<https://gc-forever.com/forums/viewtopic.php?p=35292#p35292>.

- Hook at RAM `0x80047C58` (original instruction `ED494024` = `fdivs f10,f9,f8`).
- The original gc-forever code loads the constant into `f15`
  (`C1E282D4 ED0F0232 / ED494024`). The baked version below instead uses `f10`
  as scratch, to avoid clobbering the non-volatile `f15`. Same result; the `f15`
  version also tested fine in Dolphin, so if you compare the disabled Dolphin
  code (`C1E282D4`) to the patched DOL (`C14282D4`) they are equivalent, not
  byte-identical.
- Cave (resident, in main text-section trailing padding):
  ```
  lfs   f10, -0x7D2C(r2)   ; load 1.3333 (already present in rtoc)
  fmuls f8,  f10, f8       ; scale the divisor by 1.3333
  fdivs f10, f9,  f8       ; the original instruction
  b     0x80047C5C         ; return
  ```

**Disable Linear Filtering** (Vague Rant; matches the Dolphin wiki EU block):

- `C2` hook at RAM `0x8005E4B4` (original instruction `881E001F`), cave reproduces
  the 14-instruction payload (sets up args and calls the filter routine at
  `0x8005E4DC`, then runs the original instruction) and branches back to
  `0x8005E4B8`.
- Two `04` writes: `0x800933E0` and `0x800934DC`, each `88BF0033`
  (`lbz r5,0x33(r31)`) -> `38A00000` (`li r5,0`).

Both caves were placed in resident memory (main text padding for widescreen, a
sandwiched data-section alignment gap for filtering), verified in Dolphin, then
confirmed on real hardware via Nintendont.
