Several hand-ported scripts carry pokered dialogue as inline English literals instead of reading game.data.text, because the real ROM label was never reachable from data/generated/text.lua: ViridianCityYoungster2OkThenText/CaterpieAndWeedleDescriptionText, TMNotebookText, the SS Anne kitchen cook's three dish lines, and the Viridian fisher's pre-gift line (data/scripts/story5.lua's gift() already read t[label] here, just had a stale comment and a missing fallback). Traced the actual cause carefully -- there are two independent, differently-behaved label scanners in this codebase: - tools/extract/text.py's parse_text_file() requires a label to start with "_" to be collected. This is a real bug (confirmed against a real pret/pokered checkout), but this function has no callers anywhere in the tree and no __main__ entry point -- it looks like dead code left over from an earlier version of the pipeline. - The function that actually produces the shipped label list is text_metadata() in tools/make_rom_manifest.py, which feeds manifest["text"]["labels"], which build_rom_data.py's extract_text() iterates to decode each label straight from the ROM. text_metadata() already uses the permissive regex (no "_" requirement) since commit0f581e2f. So the actual blocker is that the committed tools/rom_manifest.json was stale relative to text_metadata()'s current code, not a source bug. Verified by rebuilding pret/pokered from source with RGBDS (reproducible -- the resulting pokered.gbc/pokeblue.gbc hash to the same canonical SHA-1s gen1recomp already pins, so no cartridge dump was involved anywhere here) and running the real, unmodified make_rom_manifest.py against it: 2595 labels against the committed manifest's 2585, a clean superset containing everything these scripts need. SilphCo2FSilphWorkerFPleaseTakeThisText is the one exception already in the manifest -- confirmed by commit0f581e2f("so many bugs i cannot even breathe") that it was hand-patched in exactly this same targeted way, for issue #393. Fix: - tools/extract/text.py: relaxed parse_text_file()'s regex to match text_metadata()'s, for consistency (no effect on what ships, since nothing calls this function, but no reason to leave a legacy copy of the same scanner out of sync). - Four scripts read the real label first (t[label] or fallback, the established pattern): celadon_eevee.lua, ss_anne_kitchen.lua, viridian_city.lua, story5.lua (comment/fallback only, lookup was already correct). - tools/rom_manifest.json and tools/rom_manifest_blue.json: regenerated for real -- both files are the direct, unedited output of running make_rom_manifest.py/make_blue_manifest.py against a real pret/pokered checkout at POKERED_REVISION, not hand-assembled or reverse-engineered to match. Only safe because of the two fixes below, which exist specifically so a real run doesn't regress anything the previously-committed files had. Diffing a real make_rom_manifest.py run against that previous file (1143 lines out of 45253) found exactly what a naive "just regenerate against whatever pokered HEAD is handy" would have silently broken: - pret/pokered commit 079d1cc92fc3b0ec82bc1418c2b4045bfca84620 (PR #596, 2026-08-06) renamed _SilphCo10FGiovanniILostAgainText/_SilphCo10FPorygonText to _SilphCo11F... (they live in text/SilphCo11F.asm, Giovanni's floor). data/scripts/victories.lua:183 still hardcodes the old name, and extracting under pokered's new name would silently blank Giovanni's "I lost again!?" rematch line. Fix is the pin below: POKERED_REVISION is pinned to the last commit before this rename, so today's generator output matches victories.lua natively, no engine code touched, no generator-side workaround either. Advancing the pin past this commit is a real, welcome future upgrade -- it just needs victories.lua's labels (and anything else's) fixed up in the same change. - trainerPartyOverrides.OPP_CHIEF (Giovanni's Celadon gym team) wasn't produced by any code under tools/ at all. Traced why: pret/pokered's data/trainers/parties.asm has "ChiefData: ; none" -- the Celadon Chief's battle is unused/cut content in the original game, and RomExtractor.lua's own comment confirms gen1recomp reimplements it as a real fight using a hand-authored party for exactly that reason -- no pokered commit, old or new, will ever produce this data. Added a TRAINER_PARTY_OVERRIDES constant to make_rom_manifest.py so this survives every future regeneration automatically; verified it reproduces the committed value byte-for-byte and flows through to Blue/Yellow for free via their existing derive-from-Red path. - trainerHeaders.MtMoonB2F's Super Nerd slot is pre-existing fabricated data, not pokered drift: his event name, EVENT_BEAT_MT_MOON_3_SUPER_NERD, has never existed in pokered at any point in its history (the real name, EVENT_BEAT_MT_MOON_EXIT_SUPER_NERD, has been stable since 2015), nor in gen1recomp's own event_flags.lua; trainerDefeated() checks defeatedTrainers[npc.id] first, the same pattern already used for the Fighting Dojo's Karate Master, so this entry is very likely already inert. field.seafoam also differs from a fresh regeneration (two showObject boulder-toggle IDs in the B3F puzzle, a live gameplay system nobody has verified either value against), and field.tradeArt is new content a fresh extraction produces that was never shipped. None of those three are this PR's problem to fix, but a real generator run has to do something with them regardless -- so make_rom_manifest.py gets a new apply_known_nonreproducible_overrides(), called right after text_metadata()/field_metadata(), that pins MtMoonB2F and seafoam back to what was already shipped and drops tradeArt, each with a comment explaining why and what the real fix looks like (MtMoonB2F needs a Data:seedMtMoonB2FSuperNerd()-style engine seed, not manifest data). Verified this override function was complete and correct -- diffed a real run against the previously-committed file first, empty -- before trusting it to write tools/rom_manifest.json/_blue.json directly; both are now literally that generator's output, not hand-assembled. tools/rom_manifest_yellow.json isn't touched by this PR at all: it already had all ten labels, and make_yellow_manifest.py has no matching override yet for its own field.oldManBattle outlier, so regenerating it for real isn't safe the same way yet. - tools/make_rom_manifest.py: added a POKERED_REVISION pin -- cf621a76d4941c93c078eb38e0880fe8db48ef40, the last pret/pokered commit before the Silph Co rename above, chosen deliberately rather than current HEAD -- and a check_pokered_revision() guard main() calls before generating: fails loudly if --pokered isn't at that commit instead of silently absorbing whatever upstream renames or restructures since, with an explicit --allow-revision-mismatch escape hatch for a deliberate pin bump. That's the intended way this pin moves forward: diff a fresh run against the committed manifest, fix up whatever engine code depends on by exact name, and bump POKERED_REVISION in the same change -- a conscious, reviewable decision instead of a silent contributor default. Wired the same guard into make_blue_manifest.py and make_yellow_manifest.py for their own --pokered/--pokeyellow checkouts; make_yellow_manifest.py gets its own POKEYELLOW_REVISION (e6ba56989b0f2694f393e6924820be11dcc1fbb8, verified here). The pin and its guard live entirely in the generator source, not also embedded as a field in the shipped manifests -- that would be redundant with the .py constant next to it in the same commit, for no protection the guard doesn't already give. - tools/make_rom_manifest.py, make_blue_manifest.py, make_yellow_manifest.py: switched json.dump(..., ensure_ascii= False, ...) to ensure_ascii=True to match how the committed manifests were actually encoded (escaped \uXXXX rather than literal UTF-8). Purely cosmetic -- json.load parses both identically -- but needed so that a fresh make_rom_manifest.py run at POKERED_REVISION now produces tools/rom_manifest.json byte-for-byte (plain diff empty), not just content-equal. One small, pre-existing cosmetic mismatch remains and wasn't chased: a single nested dict, field.cardKeyDoors.doors, has its SILPH_CO_10F/11F keys in natural floor order in the committed file instead of the sort_keys=True lexicographic order everything else in the file uses -- same content either way. - tests/rom_manifest_generator_test.py: new ROM-free unit tests (same style as the existing tests/build_rom_data_cli_test.py, wired into scripts/test.sh as a T0 tier) for check_pokered_revision() and apply_known_nonreproducible_overrides() -- matching/mismatched/ bypassed/unresolvable-checkout revision cases, and that the MtMoonB2F/seafoam/tradeArt overrides land correctly (including alongside a populated map entry, and without erroring when tradeArt is already absent). Doesn't replace the manual real-ROM verification above, which needs an actual pokered/RGBDS toolchain -- but a future typo or logic slip in either function now fails immediately instead of only surfacing next time someone happens to redo that manual check. - Left data/scripts/flavor/silph_co_9f.lua's nurse dialogue (labels also added to both manifests here) untouched code-wise: static command table, not a function, needs its face_player/heal_party/fade state machine restructured to use t[label] safely, and show_text's un-resolved-label fallback prints the label name literally rather than English -- not safe without interactive testing. Checked Yellow's equivalent case (Melanie's House) since it looked like the same shape: it isn't actually broken. tools/make_yellow_manifest.py's YELLOW_EXTRA_TEXT_LABELS already force-includes those eight labels, and a real built dialogue_yellow.lua already has correct French translations for them. tools/rom_manifest_yellow.json also already carries all ten labels this PR adds to Red/Blue. No label changes needed there. Tested: patched parse_text_file() against a real pret/pokered checkout (+11 labels, 0 removed, all clearly dialogue-shaped); rebuilt pokered.gbc/pokeblue.gbc with RGBDS at current pret/pokered HEAD (confirmed hashes to gen1recomp's own canonical SHA-1s) and diffed a fresh make_rom_manifest.py run there against the committed manifest to map out what a full regen at HEAD would need; re-checked out the same pokered checkout at cf621a76 (POKERED_REVISION), rebuilt both ROMs again (same canonical hashes); with apply_known_nonreproducible_overrides() and the ensure_ascii fix in place, ran both generators to a scratch path first and diffed against the then-committed manifests -- empty -- before running them again writing tools/rom_manifest.json/_blue.json directly, so both files are now the generator's literal, unedited output (git diff on that final write: 18 lines moved in rom_manifest.json, exactly the pre-existing SILPH_CO_10F/11F ordering quirk; zero lines changed in rom_manifest_blue.json); rebuilt pokeyellow.gbc the same way, confirmed its canonical hash, and confirmed make_yellow_manifest.py's symbols/ text/trainerHeaders/trainerPartyOverrides also come out byte-for- byte identical to the committed tools/rom_manifest_yellow.json; ran the real build_rom_data.py --only text against all three rebuilt ROMs and confirmed every added label decodes from real ROM bytes matching the English fallback literals exactly; verified check_pokered_revision() actually raises on a deliberate mismatch before relying on it to gate the runs above; python3 tests/rom_manifest_generator_test.py: 9/9 pass; luajit tests/run_engine.lua: 250/250 suites pass.
Gen1Recomp
A native LÖVE2D recreation of Poke Red, Blue and Yellow. The engine and map behavior are hand-written Lua; game data and graphics are decoded from a ROM supplied by the player.
And before you say, "that's not a recomp", you're wrong. Recomp is an acronym. Reverse Engineering Causes Obsessive Mental Problems
Click Here for the AI Use Disclosure!
Caution
We are NOT affiliated with the website
gen1recomp[.]comThat website is not run by this project, was not authorized by us, and we have no idea who operates it. It is impersonating this project; do not download anything from it, and treat anything it hosts or claims as untrustworthy. Even if the site currently links back to this repository, the people behind it can change its content at any time, so nothing on it should ever be trusted. This GitHub repository and the Discord linked below are the only official sources for this project. Also, as I assumed would eventually happen, the idiot that made that website now pumped it full of adware. Please stay away from that website.
SUPPORT / ANNOUNCEMENTS / MODS: Discord
Watch the latest update video
This project does not include a ROM, emulate the Game Boy, transpile assembly, or download a disassembly. A canonical US Poke Red, Blue, Yellow, or Gold ROM is the only game content input.
The ROM is verified, used during import, and then released from memory. It is
not copied into the cache. Later launches load the private generated cache and
do not ask for the ROM again. Red, Blue, Yellow, and Gold can all be imported
side by side. Gold is Gen 2 Phase 1 (import + launcher; see
docs/gold-phase1.md): the Gen 2 engine is still under construction.
Quick Start
Open the desktop app. On first boot, choose your legally obtained .gb /
.gbc file or drop it onto the window. Import takes a few seconds and the
game starts automatically.
Only the canonical US Red, Blue, Yellow (1 MiB), and Gold (2 MiB) ROMs are accepted. The importer verifies SHA-1 before creating any game data:
- Red:
ea9bcae617fdf159b045185467ae58b2e4a48b9a - Blue:
d7037c83e1ae5b39bde3c30787637ba1d4c48ce2 - Yellow:
cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1 - Gold:
d8b8a3600a465308c9953dfa04f0081c05bdcb94
The packaged app contains neither a ROM nor pre-extracted game data. Music, sound effects, and cries are synthesized while the game runs from compact audio channel programs copied out of the verified ROM.
A note on Windows Defender warnings
Windows Defender sometimes flags the Windows build with a generic
machine-learning detection such as Trojan:Win32/Wacatac!ml (#621). This is
a known false positive: the exe is the official LÖVE runtime with the game
archive appended (the standard way LÖVE games ship), and Defender's
heuristics distrust unsigned executables with appended data. Every release
publishes SHA-256 checksums (sha256sums.txt) so you can verify your
download, and you can confirm a flagged file yourself on
VirusTotal, where these builds come back clean
on every engine except Defender's heuristic. False positives are reported to
Microsoft as they come up.
Controls
| Action | Keyboard | Controller |
|---|---|---|
| Move | Arrow keys / WASD | D-pad / left stick |
| A | Z / Enter / Space | A |
| B | X / Backspace | B |
| Start | Escape | Start |
| Select | Tab / Shift | Back / Select |
Rebind any of these in-game under OPTIONS → CONTROLS. Controllers are supported out of the box.
Hotkeys
| Key | What it does |
|---|---|
- / = |
Zoom out / in (overworld; also mouse wheel) |
1 |
Cycle GAME SPEED up (controller: R2 faster, L2 slower) |
2 |
Cycle COLORS |
3 |
Cycle TILT (free-roam overworld) |
4 |
Cycle ZOOM through every level (free-roam overworld) |
5 |
Cycle GBC FX |
F1 |
Save |
F2 |
Load |
F10 |
Open / close the mod manager |
COLORS, TILT, ZOOM, GBC FX, GAME SPEED, and VOID FILL are also in the
Options menu and persist in options.lua.
Low-end devices
OPTIONS → PERFORMANCE scales the port's optional extras for weaker hardware: HIGH (everything on), BALANCED (no 3D tilt or GBC FX), LOW (also no survey zoom, FPS capped), or AUTO — the default, which picks a tier from your device (ARM handhelds → LOW, phones → BALANCED, normal desktops → HIGH, unchanged). It only scales presentation; the fixed-step game logic is identical on every tier, and a lower tier hides your tilt/zoom/GBC-FX preferences without forgetting them. Details in docs/new-features.md.
Rulesets
OPTIONS → RULESET picks which set of Gen 1 battle behaviors to run.
Both rulesets share the same damage formulas; they differ only in whether
the original's quirks are kept. The setting persists in options.lua, and
mods can register their own.
gen1_faithful is the default and reproduces the original cartridge,
famous bugs included:
| Rule | Behavior |
|---|---|
oneIn256Miss |
A 100%-accurate move still misses on a roll of 255 |
critUsesBaseSpeed |
Crit rate reads base speed, not the current stat |
critIgnoresStages |
Crit rate ignores stat stages |
focusEnergyBug |
FOCUS ENERGY quarters the crit rate instead of x4 |
enemyUnlimitedPP |
Enemies never spend PP, so they never Struggle |
hyperBeamSkipRechargeOnKO |
HYPER BEAM skips its recharge when the target faints |
randMin / randMax |
Damage random factor 217-255 |
modern_clean keeps the formulas but removes the notorious quirks:
| Rule | Behavior |
|---|---|
oneIn256Miss |
Off: a 100%-accurate move always hits |
critUsesBaseSpeed |
Unchanged: crit rate still reads base speed |
critIgnoresStages |
Off: stat stages count toward the crit rate |
focusEnergyBug |
Off: FOCUS ENERGY raises the crit rate as intended |
enemyUnlimitedPP |
Off: enemies deplete PP and Struggle when empty |
hyperBeamSkipRechargeOnKO |
Off: HYPER BEAM always recharges, like Gen 2+ |
randMin / randMax |
Damage random factor 217-255, same as faithful |
Running From Source
Requires LÖVE 11.x. Place a Red, Blue, or Yellow ROM in the project folder and
double-click Play-Mac.command or Play-Windows.bat, or run:
scripts/setup.sh --rom "/path/to/Poke Red.gb" # or Blue.gb / Yellow.gbc
scripts/run.sh
then love . for later launches. Windows PowerShell scripts, the optional
developer data build, test suites, and cache management are covered in
Developer Setup.
Portable Mode
By default the game keeps your save, options, and the private ROM-derived
data cache in your OS's normal per-user app data folder. To keep everything
next to the game instead (handy for a USB stick or portable drive you carry
between computers), drop an empty file named portable.txt next to the app
(next to gen1recomp.app/.exe, or next to main.lua/conf.lua when
running from source), then launch the game. Portable mode is desktop-only
(Windows, Linux, macOS); it has no effect on Android or iOS, where the app
runs from a read-only package.
With portable.txt present:
save.lua,save.lua.bak, andoptions.luaare read from and written to that same folder instead of the OS save directory.- A ROM import writes the generated
data/generatedandassets/generatedcache straight into that folder too (nothing is left in the OS save directory), so a later launch reuses it without asking for the ROM again even on a different computer, as long as the same folder comes along. - Deleting
portable.txtswitches back to the normal OS save directory; nothing already written to either location is touched automatically, so copy files over yourself if you want to carry existing progress across the switch.
Launch Options
By default the app opens the launcher so you can pick a game. Launch options skip it and start one game directly, which is what you want for a one-click entry: a desktop shortcut per game, a Steam entry, or a handheld frontend.
| Option | Effect |
|---|---|
--game=red |
boot Red, skipping the launcher (blue and yellow too, or just r / b / y) |
--slot=2 |
load that save slot; takes a slot number or a slot id |
--launcher |
open the launcher anyway, so you can edit a shortcut you already made |
Linux on arm64 (Raspberry Pi)
Alongside the x86_64 gen1recomp-*-linux.zip, every release ships
gen1recomp-*-linux-arm64.AppImage for 64-bit ARM desktop Linux — Raspberry
Pi 4/5, Armbian and other SBC distros, and arm64 VMs on Apple Silicon:
chmod +x gen1recomp-*-linux-arm64.AppImage
./gen1recomp-*-linux-arm64.AppImage
LÖVE publishes no aarch64 binary of any kind, so this artifact compiles the engine — and SDL2, OpenAL and the codecs — from source inside a Debian bullseye arm64 container. It needs only glibc 2.29+, libstdc++, freetype and zlib on the host; OpenGL, X11, Wayland, KMSDRM, ALSA and PulseAudio are all dlopened, so the same image runs on a full desktop, a Wayland-only session or a KMSDRM handheld with no X server. Build instructions and the reasoning are in docs/linux-arm64-build.md.
iOS
Every release ships gen1recomp++-*-ios.ipa. Sideload it with AltStore
(Windows or Mac) — see docs/ios-sideload.md. To
build and install from source on a Mac instead, see
docs/ios-install.md.
Xbox Dev Mode
Every release ships gen1recomp-*-xbox-uwp.zip for Xbox One and Xbox Series
consoles in Developer Mode. It cannot be installed in retail mode.
Extract the archive, then use Xbox Device Portal to install the .msix and
the x64 package under Dependencies.
External setup
- Put your legally obtained Red, Blue, or Yellow ROMs on an external drive. Mod ZIPs can go on the same drive.
- Connect the drive to the Xbox and open Gen1Recomp.
- Select Import ROM or Import Mod, then choose the file with the Xbox file picker.
- Repeat the ROM import for each version you want to use.
Internal setup
- Create a folder named
baseromson your PC and place your legally obtained Red, Blue, or Yellow ROMs inside it. - ZIP the folder, keeping
baseromsat the top level of the archive. - Launch Gen1Recomp once, then close it.
- Open Xbox Device Portal and upload the ZIP to
Gen1Recomp/LocalState/pokemon-love2d/. - Choose Yes when Device Portal asks whether to extract the archive.
- Open Gen1Recomp. The launcher checks baseroms once at startup. When it finds a compatible ROM, that game’s tab shows ROM FOUND and an Import detected ROM button.
ROMs, generated game data, saves, and mods remain in LocalState and are not included in the app.
Source builds and package details are covered in the Xbox UWP build notes.
Handhelds
A PortMaster-style port for the Anbernic RG34XXSP on Stock OS 64-bit MOD
ships with every release as gen1recomp-*-rg34xxsp-stockos64-mod.zip.
Install steps, controls, and troubleshooting live in
docs/anbernic-rg34xxsp.md.
Nintendo Switch
Releases ship an SD-ready gen1recomp-*-switch.zip. Runtime target is pinned
love-nx 11.5-nx1. Requires a
console that can run Switch homebrew.
- Players: docs/switch-install.md. Download the zip, extract at the microSD root (install or update), title-override launch, import your own legal ROM, Joy-Con controls and shortcuts.
- Builders: docs/switch-build.md.
--fetch/--loose/--fused, toolchain, Docker fallback, and CI vs release (path-gated ubuntu selftest, fused PR artifact on the main repo, release hard-fail). - File transfer (MTP / SD / FTP): docs/switch-transfer.md.
Modding
The game ships a native mod platform: content registries, events and hooks, per-mod saves and options, and an in-game manager. The full modding book — getting started, a twelve-rung tutorial ladder, a cookbook, and the generated reference — lives on the project wiki.
Shipped example mods, one per kind of author, live in [mods/](mods/).
Maps can be edited in our own build of Tiled, bryanthaboi/tiled_gen1recomp, and exported back out as a mod; see docs/tiled-map-editing.md.
Bugs
Found a bug? A warp dropping you somewhere it shouldn't, a battle doing math that looks wrong, text in the wrong box, anything that does not match the original game. Open a bug report. Attach a screenshot if you can. It saves a lot of back and forth, and if you can't get one, the form asks you to describe what you saw instead.
More
- Link play — START > LINK connects two copies directly over UDP.
- Save editor — edit party, boxes, items, events, and Pokédex flags outside the game.
docs/architecture.md— runtime details;docs/behavior-porting-notes.md— formula provenance;docs/link-security.md— what link play defends against, and what it doesn't.
Special Thanks
This project would not be possible without pret > the pret band of decompiling maniacs > and their pokered disassembly.






