A native Godot 4 reimplementation of the Game Boy Pokémon games. Gold, Silver and Crystal are playable end to end; Red, Blue and Yellow are read and cached, and their world is being built. It is written from scratch in GDScript, not an emulator, static recompilation or disassembly. A user-supplied cartridge dump is SHA-1 verified, decoded once into a cache, then released. No game data ships here: bring your own ROM.
Latest release ships
one file per platform. sha256sums.txt covers every one of them.
| You have | File |
|---|---|
| Windows | pokerecomp-<version>-windows-x86_64.exe |
| Windows on ARM | pokerecomp-<version>-windows-arm64.exe |
| macOS | pokerecomp-<version>-macos.zip |
| Linux | pokerecomp-<version>-linux-x86_64 |
| Linux on a Pi, an SBC or an ARM handheld | pokerecomp-<version>-linux-arm64 |
| Android, handhelds included | pokerecomp-<version>-android.apk |
| iPhone or iPad | pokerecomp-<version>-ios.ipa |
| Nintendo Switch running homebrew | pokerecomp-<version>-switch.zip |
Nothing is signed with a paid certificate, so each platform asks once:
-
Windows: More info, then Run anyway.
-
macOS: right-click the app, Open, then Open again.
-
Linux:
chmod +xthe file, then run it. -
Android: allow installing from this source.
-
iOS: the
.ipais deliberately unsigned. Install it with AltStore or SideStore, which sign it on your own machine with your own Apple ID. A free Apple ID works and needs re-signing weekly. Add this source to either one and every release arrives as an update:https://raw.githubusercontent.com/Decryptu/pokerecomp/main/.github/altstore/source.json -
Switch: extract the zip at the root of your microSD and launch
pokerecompfrom the homebrew menu. It needs a console that already runs homebrew; nothing here installs one.
The About page tells you when a newer release exists. It does not install it: download the new file and replace the old one. Saves live elsewhere and survive.
You need Godot 4.8 or newer. Enable the hooks once per clone. The commit one refuses cartridge data; the push one runs the checks CI runs that need no engine.
git config core.hooksPath .githooksPut dumps in roms/, then verify them. See roms/README.md.
godot --headless --path . -s res://tools/verify_rom.gdMatching uses SHA-1, never filenames. Unknown hashes are refused because an uncharacterised bank layout could produce corrupt assets. The three Generation 1 cartridges import their species, move, type, item and trainer tables, every map and tileset, and every map text, and a wild fight, a trainer battle and a standing wild Pokemon on one of their maps are all fought on the battle screen. An NPC's in-game trade is offered, refused and taken. The START menu is the cartridge's own list and opens its one-pocket bag; the launcher seats one and does not offer Play until its map scripts are interpreted.
| Game | SHA-1 |
|---|---|
| Red (USA/Europe) | ea9bcae617fdf159b045185467ae58b2e4a48b9a |
| Blue (USA/Europe) | d7037c83e1ae5b39bde3c30787637ba1d4c48ce2 |
| Yellow (USA/Europe) | cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1 |
| Gold (USA/Europe) | d8b8a3600a465308c9953dfa04f0081c05bdcb94 |
| Silver (USA/Europe) | 49b163f7e57702bc939d642a18f591de55d92dae |
| Crystal (USA/Europe Rev 1) | f2f52230b536214ef7c9924f483392993e226cfb |
godot --headless --path . -s res://tools/import_rom.gdA few seconds per game. The cache is keyed by game and hash and lives in Godot's
user://, never in the project or an export. --verify checks without writing.
A cache is never migrated. An update that changes its format discards the old one. The shelf marks that cartridge "Update needed", and pressing it says why and opens the picker: import the same dump again. Saves live under their own root and are not touched.
The same sheet, behind the three dots above the shelf, also swaps a cartridge's
picture for one of your own: any PNG, WebP or JPEG, scaled to fit the shell
whatever shape it is, with a way back to the shipped art. It is kept under
user:// beside the saves, so an update does not ask for it again.
| Data | Contents |
|---|---|
| Species | Names, base stats, types, held items, egg groups, TM/HM flags |
| Learnsets, evolutions | All 251 species' level-up moves in cartridge order; every evolution and its method |
| Egg moves | Every species' inherited moves: 478 across 106 species on Gold and Silver, 480 across 105 on Crystal |
| Moves, TM/HM | Power, type, accuracy, PP, effect and chance; the 57 or 60 TM, HM and tutor rows; the happiness table teaching one moves |
| Items, types | 255 items with prices, effects, pockets and healing metadata; 28 type names |
| Type chart | Every matchup and the two Foresight-cancelled entries |
| Trainers, NPC trades | Class names, pics, palettes, AI flags, DVs and parties; trade records with DVs and OT data |
| Sprites, palettes | Front/back for 251 species and 26 Unown forms; normal and shiny 15-bit colours |
| Font, borders, HUD | 128 glyphs, eight text-box frames, HP/EXP bars and panels |
| Splash, title, intro | Each cartridge's opening art, tilemaps and palette runs, including Crystal's 35-entry intro section |
| Region map | Three graphics sheets, both region tilemaps, the per-tile palette map and 96 landmarks |
| Prof Oak's PC, credits | The 19 OakRatings rows and their texts; CreditsScript's whole command stream |
| Overworld | Maps, tilesets, collisions, events, scripts, movement, palettes, animation and object sprites |
| Wild encounters | Grass, water and swarm tables, 13 fishing groups, the roaming graph, rates, slots and repel checks |
| World services | Menus, marts, fruit trees, phone contacts, special calls, bounded scripts and text, music, SFX and cries |
| Battle animations | 278 scripts, 188 objects, 185 framesets, 216 OAM sets, 39 graphics sheets and the sine table |
Sprites stay colour indices and receive a palette at draw time, so shiny rendering needs no duplicate images.
godot --headless --path . --quit-after 30The launcher is a shelf of six cartridges. An unimported bay is drawn in the cartridge's own outline: drop a dump on it, or click to browse. Mods, settings and about are in the dock underneath. Light and dark, and the same layout works on a phone. The clock and the charge across the top are the machine's own; a device whose charge cannot be read shows none rather than a full cell.
Play opens the save screen: validated slots, naming, export and import, .sav
import, party inspection, and a save editor that cannot produce a save the game
will not load. A new game asks which mode it is played in, then opens on the
cartridge's own splash, GameFreak animation, intro movie and title screen, then
the gender question and Oak's speech. Continue enters the overworld. See
docs/SAVES.md.
Three modes, built in. No mod to install, nothing to remember to do yourself: the game keeps the rules. The mode is chosen when a save is created and is fixed for that run, because a challenge that can be switched off after a death is not one. Every save made before this update is a Vanilla run.
| Mode | What changes |
|---|---|
| Vanilla | Nothing. The cartridge's own game. |
| Hard | Every trainer scores with all ten of the game's own AI layers instead of the handful its class carries, switches out often, and brings a party 15% higher with perfect DVs and full stat experience. One global rule each, so the teams are still the ones the cartridge wrote. |
| Nuzlocke | The challenge's rules, enforced by the game. |
The game enforces the Nuzlocke rules itself:
- One catch per area: the first wild Pokemon you meet on a route, in a cave or in a town is the only one you may throw a ball at there. It is spent whether you catch it, beat it or run, and the ball menu then says so. The area is the met location the Pokemon's own summary shows, so a whole cave is one encounter. Roamers and the Bug Catching Contest belong to no area and spend nothing.
- A faint is death: a Pokemon that faints is released on the way out of the battle and never comes back. It is written to disk the moment it happens, so quitting and reopening the slot cannot undo it.
- Every Pokemon is nicknamed: the YES/NO is skipped and the keyboard opens outright, for a catch, a gift and a hatched egg alike.
- A wipe ends the run: losing your last Pokemon heals nothing and halves no money. The slot stays on the shelf marked "Run over" with what it caught and what it lost, and it cannot be continued.
The start menu wires every source entry:
| Entry | What is there |
|---|---|
| Pokedex | The three source orderings, type search and the <MON>'S NEST area map |
| Pokemon | The party, its submenu, all eight field moves, and ITEM to give or take |
| Pack | Each item's own submenu: USE, GIVE, TOSS and SEL, which binds an item to SELECT |
| Pokegear | Clock, map, phone and radio on the hardware tile grid. A tuned station keeps playing after it closes, which is how the Poke Flute channel wakes Vermilion's Snorlax |
| Player, Save, Exit | The trainer card, the map/inventory/event/clock snapshot, and the way out |
| Options | The cartridge's seven-row OPTION screen over the same values the launcher's settings edit |
Facing something and pressing A is the other way to every field move: a cut tree, a whirlpool, a waterfall, a headbutt tree and open water each offer their move in the cartridge's own order and words. Fruit trees bear once a day, Poke Balls and hidden items are picked up by facing them, and the Players House PC opens the item PC while a Pokemon Center's opens BILL'S PC. Walking into a new area raises Crystal's map name sign for sixty frames, which Gold and Silver never had.
Icons come from Lucide. See docs/THIRD_PARTY.md.
A handheld with two displays -- the AYN Thor and its kind -- puts five of those entries on the lower one: the Pokedex, the party, the pack, the Pokegear's map and the trainer card. Under them is a menu box in the frame the player chose, with a tab for each, drawn with the cartridge's own art.
It is a view. The only thing on it that takes a touch is the tab row; no page on it reads input at all, and none of them is a copy, so what is on the lower screen is the same screen the START menu opens. A tab is there exactly when that menu's own gate would have offered its row, so the team page appears with the starter and the Pokegear page with the phone call.
Outside a game the panel shows a launcher page of its own: an empty cartridge bay, the project's name and a line saying nothing is running, on the same field and in the same light or dark appearance the shelf above it wears. The pages belong to a world, and there is no world behind the launcher.
Settings > Second screen switches it off, or opens the same panel in a desktop window on a machine with no such hardware.
The games are played with the eight buttons the hardware had. A key, a controller and the on-screen buttons all produce the same eight, so nothing in the game knows which one you used.
| Button | Keyboard | Controller |
|---|---|---|
| Up, Down, Left, Right | Arrows, WASD | D-pad, left stick |
| A | Z, Space |
Bottom face button |
| B | X, Escape |
Right face button |
| START | Enter | Start |
| SELECT | Backspace, Tab | Back |
A, B, START and SELECT together is the console's own reset, from anywhere: the opening, a menu, a battle or the map. It returns to the save screen without writing anything, so what you get back is your last SAVE, and the slot counts how many times you have done it. The count is on the save page under Mode, which is what a shiny hunt is measured in. The first reset asks before it happens.
Keys bind by physical position, so WASD stays under the same four fingers on a layout that spells them differently; settings shows each binding as the key actually printed on it. Everything can be rebound, with as many keys and controller buttons as you like, and a mod's own controls rebind the same way.
Any controller Godot recognises works without setup. On a touchscreen the games draw a d-pad, A, B, START and SELECT, which appear while you touch the screen and step aside on the next key press; settings can pin them on, turn them off and arrange them separately for upright and sideways. Three quick taps brings them back. The screen fills whatever window or device it is given, in either orientation.
A window is not the Game Boy's 10:9, and the black bars around a framed screen are room the overworld can draw into. Settings > Application > Screen:
| Screen | What it draws |
|---|---|
| Fill (default) | The map covers the whole window at any shape, and the maps connected to this one are drawn around it |
| Framed | The 160x144 screen at a whole scale, centred, with black bars, as the hardware had |
Everything laid out on the screen (text boxes, menus, the start menu, the cursor) stays inside the 160x144 rectangle in the middle of it, where the cartridge put it. Only the surround grows.
While walking, + and - zoom, 0 returns to the fitting scale, and the mouse
wheel does the same. They count screen pixels per Game Boy pixel, so a mod
drawing the world in 3D keeps them for its own camera.
Zoom out far enough and a region is on screen at once: the connection graph places the maps around this one and the border block fills what no map covers. Those maps are a picture. Their people stand where their map puts them and nothing else runs: no scripts, no walking, no wild encounters, no collision. Only the map you are on is live, exactly as on the cartridge.
Settings > Application carries four more that reach the engine:
| Setting | What it does |
|---|---|
| Game speed | Normal, double or half. Everything counted in hardware frames runs at that multiple: walking, animations, text, battle |
| Window | Windowed, fullscreen or borderless |
| Scrolling | Hardware or smooth. See Scrolling |
| Frame rate | Display, or a cap of 30, 60, 120 or 144 |
Sound sits outside game speed. The driver is fed by the audio output's own demand, not by a game frame, so music, effects and cries keep the cartridge's tempo and pitch at every setting.
Leave frame rate on Display unless you are saving battery. It is the only setting whose frames each reach the panel once. A cap below the panel's own rate is a sleep, not a refresh, so the same picture is shown for one refresh, then three, then two, and the overworld appears to move 0, 1 or 2 pixels however even the game is underneath.
The overworld moves two pixels once every two frames, as the hardware did and as a Game Boy's own screen smeared over. Smooth draws the frame in between one pixel on, so the map moves a pixel a frame, lands on the cartridge's own pixel at every pass boundary, and takes the same sixteen frames to cross a cell. Nothing in the game is timed differently either way.
Under both, the pump counts the host's own frames once it has seen enough of them at one length to be sure of it, rather than measuring time: a hardware frame is 16.742 ms and a host one is the panel's, so a count taken off measured time slips a whole frame every 3.7 seconds against the frames the player is shown. A panel whose refresh divides no hardware frame, 144 Hz among them, is measured instead.
Development shortcuts are debug-build only, along with the map and cell readout.
That readout carries fps (host frames drawn), hw (hardware frames the pump
spent, 59.7 a second while measuring time and the panel's own divided rate while
counting its frames) and worst (the longest single frame of the last second,
where a stutter shows and an average hides it).
| Scene | Keys |
|---|---|
game/render/pic_viewer.tscn |
left/right species, S shiny, B front/back, T trainer classes |
game/render/text_viewer.tscn |
Space advances, F cycles borders, C shows every glyph |
game/battle/battle_screen.tscn |
T turn, A advances, Y switch, R run, [/] matchup, G/H damage; in wild battles B opens the ball selector |
game/world/world_screen.tscn |
F fishes with an owned rod, 1/2/3 pick a rod, P opens the phone, V cycles views, F5 writes a snapshot |
Zoom is not one of these: +, - and 0 are player controls and work in a
release export.
A release export offers the eight buttons and nothing else. The method behind
each shortcut stays public, which is how tools/preview_*.gd drives them.
Headless, and all against a real imported cache.
godot --headless --path . -s res://tools/validate.gd -- alltools/validate.gd is the check suite: one topic per subject under
tools/checks/, each run against every cartridge of its generation. Name topics
or a group, or all; with no argument it lists them.
| Group | Topics |
|---|---|
field_moves |
Cut, Surf, Whirlpool, Strength, Headbutt, Rock Smash and the faced-tile prompt chain |
terrain |
Ledge hops, side walls, every map's drawn blocks, the story's map ids |
johto |
Radio Tower, the Rising Badge, command queues, item balls, Route 27, the Magnet Train and long scripted scenes |
kanto |
Each city, its gym and the way in, from Vermilion to Mt. Silver |
art |
Both intro movies, the credits, the region map, all 278 battle animations, the map name sign |
tables |
TM/HM, naming, world scripts, the opening lane |
trainers |
The Route 30 trainer on each profile |
gen1 |
Red, Blue and Yellow's species, move, type, item and trainer tables, the key-item and usable-item tables behind the bag, every picture their sprite codec decodes, all 226 or 227 maps with their tilesets, the text box, shop inventory and wild encounter a map reads, every trainer and standing wild an object stands on, and all 202 or 203 battle animations |
The rest are previews and dumps, each driving a real screen or table:
| Tool | Does |
|---|---|
dump_tables.gd <game> <table> |
Prints a decoded table: species, moves, items, types, matchups, trainers, learnsets, egg_moves, evolutions, growth or all |
preview_pics.gd <game> <png> [kind] |
Contact sheet of front, back, trainers, player_back, font or frames, in either generation |
preview_*.gd |
One per screen: the intro, title, credits, Hall of Fame, region map, party, marts, mail, fishing, battle switch and animations, overworld sprites and collision |
preview_world_story.gd |
Map entry callbacks, event-flag visibility, facing interactions and the whole story route, in either generation |
replay_world.gd [game ...] [frames] |
Records (frame, button) from a real run and replays it into a fresh world; the same seed and log must reach the same snapshot, party and battle outcome byte for byte, at 30 fps and at 144. One route fights: a wild battle is spent from the world's own pump and steered through its own funnel |
render_audio.gd <game> <kind> <id> <frames> <prefix> |
One record or a whole table through the driver and APU: a WAV plus a per-frame register trace to diff |
screenshot.gd <scene> <png> [frames] [method] |
Any scene to PNG. Opens a window, so it is not headless |
# the full walked route: Johto, the Hall of Fame, every Kanto gym, and Red
godot --headless --path . -s res://tools/preview_world_story.gd -- crystal 24 7 2 2 1 none home story
# Red, Blue or Yellow from the bedroom to Lt. Surge
godot --headless --path . -s res://tools/preview_world_story.gd -- red 0 38 3 6 1 none home storyGUT is in addons/gut; configuration is in
.gutconfig.json. Tests use synthetic files and a known SHA-1 vector, never a
real cartridge, so they run anywhere.
godot --headless -s res://addons/gut/gut_cmdln.gd -gexitThat is the unit tier, and the default: more than 3,300 tests in about a minute. The scene integration tier drives real screens and is slower, so it is asked for explicitly, which is how CI runs both:
godot --headless -s res://addons/gut/gut_cmdln.gd -gdir=res://tests -ginclude_subdirs -gexitExit code 0 means all tests passed. Run one script with -gselect=<name>.
| Path | Contents |
|---|---|
game/ |
Feature folders with colocated scenes and scripts |
autoload/ |
Project singletons |
assets/ |
Authored or freely licensed assets; assets/brand/ has its own README |
addons/ |
Third-party plugins, and this project's own Android editor plugin |
tests/ |
unit/ is the fast tier, integration/ drives real screens |
tools/ |
Headless developer scripts; tools/checks/ are validate topics |
roms/ |
User cartridges, excluded from Git and Godot imports |
mods/examples/ |
Development-only example mods, excluded from exports |
docs/ |
Contributor notes |
Every platform uses GL Compatibility. export_presets.cfg covers seven presets
for Windows, macOS, Linux, Android and iOS, x86_64 and arm64 for Windows and
Linux, and writes into builds/. Install the matching export templates first,
then:
godot --headless --path . --export-release "Linux" builds/linux/pokerecomp.x86_64The Switch has no preset, because a stock editor knows no such platform and
would drop one from the file. .github/workflows/release.yml exports the pack
and wraps it with devkitPro's elf2nro instead, around a template built from a
fork of the engine pin that adds platform/nx; docs/CONTRIBUTING.md has the
whole lane.
Tests, tools and GUT are excluded, and roms/ and the user:// cache are not
reachable from an export. No signing identity is committed: the iOS preset's
team id is deliberately empty, because a published .ipa is unsigned so that a
sideloader can sign it with the player's own Apple ID. .github/workflows/release.yml
builds every target from a v* tag and refuses to publish an .ipa that carries
a signature.
A published build carries the engine, and stock export templates ship every
renderer and every importable format whether or not this project can reach
them: they are around nine tenths of a download.
tools/build_export_templates.sh builds the same engine without the parts
nothing here can execute, and .github/workflows/export-templates.yml builds
one for every published target. release.yml calls it and overlays the result,
so a name it does not carry keeps the stock template. What may go is bounded by
the mod API rather than by the game: a mod is interpreted GDScript with the
whole engine in front of it, so 3D, audio formats and image formats all stay in
even though the game itself draws in 2D.
The engine is the slow half of a release, so that workflow also runs on its own, and on a pull request that moves the flag set. iOS rebuilds the release device slice alone, spliced into the stock archive: that template is an Xcode project around several xcframeworks and the export reaches for nothing else.
Android builds through gradle, because the second display is reached by a
platform plugin and a plugin needs one. Install the Android build template from
the editor, or pass --install-android-build-template alongside the export, and
build the plugin with tools/build_android_plugin.sh first.
iOS forbids JIT and runtime native code, so mods must be interpreted GDScript, not compiled extensions. The project is therefore GDScript-first. See docs/MODS.md.
The launcher's About page has a Report a bug button. Either route works:
Say which cartridge, where you were and what you did. A screenshot settles most of it.
The same sheet has Save a report file, which writes one .zip to your
downloads folder. It holds the build, your machine, your settings, your installed
mods and the last few session logs, and nothing else: no save data, no other file
from your computer. Copy the details is the same thing without the logs, for
a chat message. Attach the file if the game crashed or looked wrong.
The launcher tells you at the next launch when a session did not shut down
cleanly. Logs live under logs/ beside your saves:
%APPDATA%\Godot\app_userdata\pokerecomp on Windows,
~/Library/Application Support/Godot/app_userdata/pokerecomp on macOS,
~/.local/share/godot/app_userdata/pokerecomp on Linux. Old ones are deleted as
new ones arrive.
Read docs/CONTRIBUTING.md. No cartridge-derived data may
enter the repository: no ROM, .sav, extracted sprites, text, maps or audio.
.gitignore, the pre-commit hook and tests enforce this; do not weaken them.
For reproducible comparisons with the upstream disassemblies, see
docs/REFERENCES.md.
MIT covers the engine source here, not the games or supplied dumps, which remain the property of their respective owners.



