The Problem: Every Plugin Reinvents the Same Hard Parts
After building a Pokémon plugin and then a Zelda one for the 3DS, a pattern was obvious: the genuinely game-specific work — the cheat codes, the art, the Title ID — is maybe 10% of a plugin. The other 90% is the same every time. Draw a menu over a running game. Get the system font on screen. Pause the game cleanly. Read the touch screen. Save settings to the SD card. Every plugin re-solves those, and most tie themselves to one plugin framework, which breaks when Luma3DS or the game updates.
Older .plg and .3gx plugins die for exactly this reason: they depend on a particular framework build matching a particular game, and when that assumption slips, the plugin no longer loads. I wanted the opposite — an engine that draws everything itself, uses no game hooks, and therefore doesn't care which game or which Luma version it's running under.
So I pulled all the reusable parts out into CTRComposer: a blank, buildable template that is the engine, with no game bolted on. Clone it, add a cheat table, some art and a Title ID, and you have a working plugin. Like everything on this site, it was built in collaboration with Claude — and this article is the write-up of how each piece actually works.
What's in the Engine
CTRComposer is a C plugin that renders its own UI directly to the framebuffer and runs inside the game's process — so writing memory is just a pointer, and it draws everything with no dependency on any framework build. What ships in the box:
{A} or {D-Pad} in any label and the real console icon renders — localizes cleanly, works in both font sizes.T("English") localization system that loads languages from SD text files.The template ships with zero game art and zero game addresses — the example cheats are inert placeholders that write nothing until you enable them. It also ships with one neutral monochrome theme on purpose, so a fresh plugin isn't born looking like Zelda.
No Game Hooks — That's the Whole Point
The design decision everything else follows from: CTRComposer uses no game hooks. It doesn't patch the game's functions or install wrapper addresses. It draws its own UI and reads input from hardware registers directly, so there are no per-game hook addresses to line up and nothing that a game or Luma update can invalidate.
Because it lives in the game's address space, applying a cheat is a pointer write and pausing is a system call — no cooperation from the game required. That portability is the reason the same engine runs unchanged across titles and Luma versions. Write it once; each new game only brings its own cheats, art and Title ID.
Drawing an Overlay Over a Running Game
There's no graphics library here — you talk to the LCD registers directly. The framebuffer is column-major, Y-flipped, with bytes in BGR order, and the physical mirror is slow (~18 ms for a full screen). Two techniques make it usable: draw everything into a normal cached RAM buffer first (which also enables real alpha-blending), then do a single blit; and double-buffer, drawing into the hidden buffer and flipping, so there's no flicker.
// Framebuffer write: column-major + Y-flip, bytes in BGR.
u32 off = x*stride + (H-1-y)*bpp;
volatile u8 *px = (u8*)((fb + off) | (1u<<31)); // uncached physical mirror
px[0]=b; px[1]=g; px[2]=r;
The visual key is the system font — the same anti-aliased shared font the console uses, fetched by IPC to the APT service without ever calling aptInit(). Small art (icons, keys) is embedded as RGBA4444 arrays and alpha-blended into the compose buffer, with a nearest-neighbour scaled blit so one small source tile can fill a large key.
r>>4) then decoding (*17) rounds most colors upward — the art reads lighter than the source. Pack with round-to-nearest (clamp((r+8)/17, 0, 15)), the true inverse of the decode, to remove the bias.
Applying Cheats — the Only Game-Specific Part
Because the plugin shares the game's address space, a data cheat is a direct write, and a "base + offset" cheat follows a pointer. Code cheats — patching the game's own instructions — need the memory made writable-executable once, and you must always save the original bytes so the cheat can be turned off. Only the addresses change between games; the mechanism is universal.
// Data cheat — direct, or via a base pointer:
*(volatile u16*)<addr> = <value>;
u32 base = *(volatile u32*)<playerPtr>;
if (base) *(volatile u32*)(base + <offset>) = <value>;
// Code cheat — patch instructions in R-X memory (the "how" is universal):
svcControlProcess(CUR_PROCESS_HANDLE, PROCESSOP_SET_MMU_TO_RWX, 0, 0); // once
// ALWAYS save the original instructions so the cheat can be turned off!
*(volatile u32*)<codeAddr> = <newInstruction>;
svcFlushEntireDataCache(); svcInvalidateEntireInstructionCache();
Addresses come from an existing source (a .plg, an Action Replay code bank, a community plugin) or from the engine's own Cheat Search — and they're always region- and version-specific, so you re-anchor when you change region. A neat trick that comes up a lot: many effects that look like they need a function call are really just memory writes the game reads back on its own — set the fields a routine consumes, flip its "do it now" flag, and the game does the work next frame. That's how warps and respawns work with no hooks.
Save-Diffing: Mapping Packed Save Fields
Cheat Search finds a value by its magnitude — perfect for a rupee counter, useless for a packed bitfield where a dozen unrelated facts share one byte (which songs you know, which switches a dungeon flipped). Triggering each event live and diffing RAM is slow and ambiguous when a byte gains several bits at once. Progressive save files solve it cleanly, and many games have community "start-to-finish" save collections — a documented series of small steps.
Anchor a save file to RAM by finding a field whose bytes you already know — that single match pins the whole file-offset ↔ RAM-address mapping, because a save is usually a raw serialization of the same in-memory struct, so bit order is identical in both. Then diff consecutive saves: the bits that flip on are exactly what that step gained, and the collection's written notes give each bit a name. A short Python script does all three in one pass, and the map self-validates against bits already confirmed on hardware. This is the technique behind the completion tracker in the Ocarina of Time plugin.
Live Themes and Localization, the Same Trick Twice
Switching a theme without touching hundreds of draw calls uses one idea: the color macros are indirection into runtime arrays. ApplyTheme(i) copies one row of a theme table into those arrays, and every call site that already used the macro just picks up the new color on the next frame. An auto-contrast check reads the current background's luminance and chooses dark or light text, so every theme — even ones added later — stays readable with no per-theme tweaking.
Localization is the same indirection, applied to text instead of color. Every string is written once in English and wrapped in T("English source"), which looks the English up in a runtime table and returns a translation — or the English unchanged if there's no entry, so a partial translation degrades gracefully instead of showing blanks.
const char *T(const char *en) { // table is empty until a language file loads
for (int i = 0; i < g_langCount; ++i)
if (strcmp(g_enKey[i], en) == 0) return g_trVal[i];
return en; // no entry -> English fallback
}
The table is filled by parsing a plain lang/<Name>.txt file from the SD card — one English=Translation per line. English needs no file; it is the key. Switching language is just re-parsing, so it's instant and needs no restart.
The Gotchas That Cost Real Time
A self-rendered plugin runs below most of the comforts of libctru, and a handful of traps each cost a debugging session. These are the ones worth knowing before you start:
hidInit() internally calls irrstInit(), and ir:rst conflicts with the running game — it freezes the game and locks the HOME button. Read buttons from the hardware register directly, and do a manual hid:USER init for the touch screen that never touches ir:rst.
ir:rst, which games without Circle Pad Pro support don't have in their exheader — and a .3gx can't patch the exheader. Assume ZL/ZR are off-limits from a plugin; use L/R combos instead.
svcCreateThread has no libctru ThreadVars, so malloc/printf/fs will crash until you seed the TLS by hand. The devkitPro toolchain breaks on paths with spaces — build from a space-free path through the msys2 shell. And use 3gxtool 1.3: it writes the 3GX$0002 container Luma expects; the older 3GX$0001 is rejected.
There's also the overlay-flicker problem: the game erases your overlay every frame (~33 ms). Re-stamping the visible buffer every 4 ms drops the gap to about 12%, giving a stable overlay — and only the visible buffer, since the hidden one gets rewritten by the game before the flip.
Built with Claude
Same as every project here: I'm not a programmer. What this one needed was a lot of low-level 3DS knowledge that lives in ten-year-old forum posts and scattered decomp notes — hardware register maps, kernel syscalls, the exact IPC command to get the shared font. I described what the engine had to do; Claude wrote and rewrote the C against those constraints; and every piece got proven on real hardware before it counted as done.
The payoff of doing it as a reusable engine is that the next plugin isn't a from-scratch fight with the framebuffer — it's a cheat table and some art. That's the difference between shipping one plugin and being able to ship a few.
The Result
CTRComposer is a free, open-source (MIT) blank template for building 3DS overlay/cheat plugins. It builds to CTRComposer-BlankTemplate.3gx — the whole engine with no game attached — and works out of the box under any Title ID, reading its own install path at runtime. It's also a way to revive older .plg/.3gx plugins that no longer load on current Luma builds, by giving them a self-rendered foundation that doesn't depend on a framework matching the game. The Ocarina of Time 3D plugin is the reference build made with it.
Source, the full engineering guide, and the template: github.com/samaBR85/CTRComposer — MIT, free.
Credits
CTRComposer owes a great deal to CTRPluginFramework and the earlier community .plg plugins — those projects made 3DS plugins possible and are the direct inspiration here.
- Luma3DS (LumaTeam) — the plugin loader
- PabloMK7 and Nanquitas — the
.3gxformat and 3gxtool - PabloMK7/CTRPluginFramework-BlankTemplate — the repository structure model
- The Linux console font — the small 6×10 bitmap font; button glyphs are original, generated by a script
Game names and game content belong to their publishers. Plugins you build from this template are yours — the MIT license only covers the template itself.