A 3DS game code injection and patching tool. Compiles custom ARM code, injects it into a game's code.bin, applies hooks, and updates exheader.bin accordingly.
This is a pure-Python CLI rewrite of the original C++/Qt application. No GUI, no Qt dependency — just Python 3.10+ and the ARM devkitPro toolchain.
All files here were generated entirely by an LLM. The only manually applied patch was changing text.size = text.physicalRegionSize << 12 to the new code size, as the algined variant crashed on Luma + 3DS for msr-remote-connecotr
You can find the original project at https://github.com/RicBent/Magikoopa
- Python 3.10 or newer
- devkitPro with the following components installed:
- devkitARM — provides
arm-none-eabi-gcc,arm-none-eabi-as,make, etc. - libctru — Nintendo 3DS C standard library (both Makefiles link against
-lctru) - 3DS portlibs — needed by the newcode Makefile (
armv6kand3dsportlibs)
- devkitARM — provides
- The following environment variables must be set (devkitPro's installer usually does this):
DEVKITPRO— path to the devkitPro installation (e.g./opt/devkitpro)DEVKITARM— path to devkitARM (e.g./opt/devkitpro/devkitARM)PORTLIBS_PATH— path to portlibs root (e.g./opt/devkitpro/portlibs)
- A patch project directory set up from
PatchTemplate/(see below) - Install dependencies from the
requirements.txt
On Windows, devkitPro is installed via devkitProUpdater and sets the environment variables automatically. On Linux/macOS, use the dkp-pacman package manager and install 3ds-dev which pulls in devkitARM, libctru, and the portlibs in one step.
See devkitPro Getting Started
python magikoopa.py insert [working_dir]
python magikoopa.py clean [working_dir]
working_dir is the path to your patch project. It defaults to the current directory if omitted.
| Command | Description |
|---|---|
insert |
Compile custom code and inject it into the game binary |
clean |
Clean all build artifacts (newcode + loader) |
# Inject from the current directory
python magikoopa.py insert
# Inject from a specific project path
python magikoopa.py insert /path/to/my-nsmb2-hack
# Clean build artifacts
python magikoopa.py clean /path/to/my-nsmb2-hackThe working directory must contain:
my-hack/
├── Makefile # Builds your custom code → newcode.bin, newcode.sym
├── code.bin # Original game code binary (unmodified)
├── exheader.bin # Original game exheader (unmodified)
├── source/ # Your custom C/C++/ASM source files
│ └── *.hks # Hook definition files (optional here)
├── hooks/ # Additional hook definition files (optional)
│ └── *.hks
└── loader/
├── Makefile # Builds the loader bootstrap → loader.bin, loader.sym
└── source/
├── loader.c # Loader entry point (uses SVC to set RWX on newcode)
├── svc.s # ARM SVC stubs
└── hooks.hks # Loader hook definitions
Use PatchTemplate/ from the repository root as your starting point.
This is important to understand:
On the very first insert, Magikoopa checks whether bak/code.bin and bak/exheader.bin exist. Since they don't, it copies the current code.bin and exheader.bin into bak/ as the originals. These backups are never overwritten again.
my-hack/
├── code.bin ← gets patched (overwritten with injected version)
├── exheader.bin ← gets patched (overwritten with modified version)
└── bak/
├── code.bin ← created now — the untouched original
└── exheader.bin← created now — the untouched original
At the start of each insert, Magikoopa restores code.bin and exheader.bin from bak/ before doing anything. This means:
- The tool always patches from the original, unmodified game files, not from the last run's output.
- Running
inserttwice in a row produces the same result both times. - The
bak/copies are the source of truth. Never delete them unless you want to re-establish new originals from whatevercode.bin/exheader.binare currently in the project root.
Memory layout (loader offset, new code offset) is also computed from bak/exheader.bin, not the patched one.
If you want to start fresh with a different base code.bin (e.g. a different game version):
- Delete
bak/code.binandbak/exheader.bin. - Place the new originals as
code.binandexheader.binin the project root. - Run
insert— the new files will be backed up and used as the new originals.
Hook files use a simple YAML-like format. Each entry starts with a name followed by a colon (no indentation), and its properties are indented below it. Lines starting with # are comments.
# Replace a game function with a branch to custom code (via symbol name)
MyBranchHook:
type: branch
link: true # required: true = BL, false = B
addr: 0x00430988
func: MyCustomFunction
# Branch to a raw address instead of a symbol
MyBranchHookRaw:
type: branch
link: false
addr: 0x00430988
dest: 0x00431000
# Non-destructive hook: saves registers, calls your function, restores them
MySoftHook:
type: softbranch
opcode: post # pre | post | ignore (default: ignore — skips original instruction)
addr: 0x00431000
func: MyHookFunction
# Overwrite bytes at an address with raw hex data
MyPatch:
type: patch
addr: 0x00432000
data: 0xE3A00001 # MOV r0, #1
# Write a symbol's address as a 32-bit value (for patching function pointers)
MySymPatch:
type: symbol
addr: 0x00433000
sym: MyCallbackFunction| Type | Aliases | Description |
|---|---|---|
branch |
— | Overwrites one ARM instruction with B or BL to your function. Requires link: true or link: false. Use func: for a symbol name or dest: for a raw address. |
softbranch |
soft_branch |
Saves all registers, calls your function, restores them, optionally runs the original instruction before (pre) or after (post), then returns. Default opcode is ignore (original instruction is skipped). Use func: for a symbol name or dest: for a raw address. |
patch |
— | Writes raw hex bytes (data:) or copies data from a symbol address (src: + len:) |
symbol |
symptr, sym_ptr |
Writes a symbol's 32-bit address to a location (for patching function pointers) |
Hook files are loaded from source/ and hooks/ in the project root (for newcode hooks) and from loader/source/ and loader/hooks/ (for loader hooks).
Create a file named <project-folder-name>.mkproj.user in the project root (INI format) to have the patched files copied to another location after each successful insert:
[CopyPaths]
Code=C:\path\to\game\romfs\code.bin
Exheader=C:\path\to\game\exheader.binBoth keys are optional. If a key is absent or empty, no copy is performed for that file.