name: pokemon-rom-randomizer description: >- Build deterministic, modded Pokémon ROMs headlessly with Universal Pokémon Randomizer ZX (Gen 1-4, GBA + NDS) and deliver them to Delta on iOS. Use for randomized wilds/trainers/gyms/legendaries, custom starters, hand-placed early-game teams, looking up what a species became in an existing build, and Gen 3 ROM surgery the randomizer has no knob for: soundtrack replacement, dialogue edits (e.g. gym text matching randomized types), and map-script patches such as an intro skip. The user supplies the base ROM; never download one.
pokemon-rom-randomizer
Produce reproducible, modded Pokémon ROMs (custom starters, limited/expanded species pools, randomized wilds/trainers/legendaries, gym type-theming, QoL tweaks) fully headless, and get them onto iOS via Delta. Works GB/GBC → GBA → NDS (Gen 1–4 verified).
When to use this
The user wants a modded/randomized Pokémon game — "randomize my Emerald", "Gen 2 starters + Gen 1-2 only", "randomize the gyms", "all gens 1-4 in Platinum", etc.; or an authored early-game team ("let me catch Bagon/Larvitar/… by the first gym", "build this team early"); or wants to look up what a species became in an existing build.
Prerequisites
- Universal Pokémon Randomizer ZX jar (the Ajarmar fork, supports Gen 1–7):
~/Applications/PokeRandoZX/PokeRandoZX.jar. If absent, it's a downloadable release zip. - A JDK. GOTCHA:
/usr/bin/javais often a non-functional stub even when Homebrew's openjdk IS installed. Probe/opt/homebrew/opt/openjdk/bin/java -version; if missing,brew install openjdk. Use that direct path, not/usr/bin/java. - The base ROM comes from the user (legally owned). Never download one. Verify it before building: 16 MB GBA / ~64–128 MB NDS; check internal title + game code at the header.
- Hard constraint: the base ROM's generation caps the species pool. The randomizer only reshuffles species already in the cartridge — it cannot ADD later-gen Pokémon. So Gen 4 mons need a Gen 4 base (DPPt/HGSS); you cannot put them in a Gen 3 ROM via this tool.
Method
Do NOT hand-craft the .rnqs settings file (versioned binary with a checksum — fragile).
Instead a 3-file Java pipeline drives the randomizer's own code (assets in this skill dir):
BuildSettings.template.java— builds aSettingsobject with the desired options and.write()s a valid.rnqs. Copy per-build, edit the options.SeededRandomize.java— loads the ROM (generation-agnostic), applies the.rnqswith a fixed seed, writes the ROM + a-llog. Deterministic: same inputs → byte-identical output.EncounterDiff.java— diffs vanilla vs modded encounter tables to answer "what did species X become / where does it appear" (also proves global 1-to-1 consistency).
Set JAR, JAVA and JAVAC once, e.g. JAR=~/Applications/PokeRandoZX/PokeRandoZX.jar,
JAVA=/opt/homebrew/opt/openjdk/bin/java, JAVAC=/opt/homebrew/opt/openjdk/bin/javac, then:
"$JAVAC" -cp "$JAR" -d . BuildSettings.java SeededRandomize.java EncounterDiff.java # compile
"$JAVA" -cp "$JAR:." BuildSettings out.rnqs # emit settings
"$JAVA" -Xmx6144M -cp "$JAR:." SeededRandomize out.rnqs "in.gba" "out.gba" 19960227 # build (last arg = seed)
NDS builds take longer and the output is smaller than input — UPR trims NDS padding; that's normal, not corruption.
Gotchas (the expensive-to-learn parts)
- A fresh
new Settings()NPEs inwrite()unless you setsetSelectedEXPCurve(...)andsetRomName(...). The GUI sets these implicitly; a headless builder must set them. setXxxMod(boolean...)is radio-button/ordinal order — pass onetrueat the index of the enum value you want, in declaration order. Confirm each enum's order rather than guessing:javap -cp "$JAR" 'com.dabomstew.pkrandom.Settings$WildPokemonMod'.customStartersvalues are national-dex-number + 1 (the internal list has a leading placeholder). Kanto{2,5,8}, Johto{153,156,159}, Hoenn{253,256,259}, Sinnoh{388,391,394}. ALWAYS verify starters by name in the-llog — if they're off by one, the offset convention shifted for that gen/version.- Misc tweaks = bitwise-OR of
MiscTweak.<NAME>.getValue()passed tosetCurrentMiscTweaks. Bits unsupported by the base generation are auto-masked at apply time, so a superset is safe. - Loading is generation-agnostic by trying every factory (
Gen1RomHandler.Factory…Gen7RomHandler.Factory) and using the first whoseisLoadable(path)is true — the shippedSeededRandomize.java/EncounterDiff.javaalready do this, so the same code handles GBA + NDS. - The CLI random-seeds.
PokeRandoZX.jar cli -s … -i … -o … -lpicks a new seed each run (not reproducible). For a fixed, reproducible world use the driver'sSettings.read+Randomizer.randomize(outFile, log, seed)(the 3-arg overload). TheRandomizerconstructor needs aResourceBundle—ResourceBundle.getBundle("com.dabomstew.pkrandom.newgui.Bundle"). - Static
RANDOM_MATCHINGkeeps legendaries-as-legendaries (and regular statics/gifts regular).UNCHANGEDkeeps them exactly vanilla — the way to preserve a game's own legendaries while randomizing everything else. - Changing only the custom starter still changes the output hash (starter + rival data) but leaves the wild/trainer/legendary roll intact under a fixed seed — verify with EncounterDiff before assuming a re-roll happened.
- Emerald's RTC makes generated savestates clock-dependent. An identical framebuffer does not prove byte-identical state; when savestate bytes are an identity anchor, capture and pin one state (or explicitly fix the emulated RTC) instead of regenerating it at boot.
Recipe reference
- 1-to-1 mapping:
AREA_MAPPING= per-route consistent (more total variety);GLOBAL_MAPPING= one replacement per species game-wide (learnable, coherent). Pair withSIMILAR_STRENGTH. - Randomize gym types:
TrainersMod.TYPE_THEMED_ELITE4_GYMS— gyms + Elite Four get random single-type themes, other trainers stay random. (Champion is typically left random.) - Trade evos on an emulator: always
setChangeImpossibleEvolutions(true)— Delta has no link trading, so Machamp/Alakazam/Golem/Gengar/Steelix/etc. are otherwise unobtainable. - Good QoL default: FASTEST_TEXT + RUNNING_SHOES_INDOORS + NATIONAL_DEX_AT_START + FASTER_HP_AND_EXP_BARS (National Dex at start matters for a full multi-gen wild pool).
- Keep base stats / types / movesets / abilities vanilla by default so
SIMILAR_STRENGTHstays meaningful and mons stay recognizable; the template lists these as opt-in. - A memorable literal seed keeps builds reproducible;
Math.random()/Datearen't needed.
Verify — never trust the settings blindly
Read the -l log and confirm against intent every build:
- starters appear by the intended names (catches the dex±1 offset);
- gym/E4 leaders are each mono-type (type-theming took);
- the
--Static Pokemon--section shows legendary→legendary matching (or "Unchanged"); - run
EncounterDiff <vanilla> <modded> <SPECIES>— for global 1-to-1 the replacement must be identical across every area.
Delivery (Delta on iOS)
Delta (App Store) emulates GBA and DS, no BIOS needed. AirDrop the .gba/.nds; in Delta
tap + → import. Saves: the in-game .sav is portable (Export/Import Save File in Delta,
or its Google-Drive/Dropbox sync) and loads across any build of the same base game; save
states are ROM-specific and fragile — tell users to rely on in-game saves. A save does NOT
cross between different games (e.g. Emerald .sav ≠ Sapphire — different game code + format).
Fastest rebuild handoff: give the new build the same filename and overwrite the old file in
Files → On My iPhone → Delta — the game entry, its save, and cloud sync all stay attached, no
export/import needed. (Name-keyed desktop emulators like mGBA get the same effect for free.)
Hand-placing specific species (wishlist team)
The randomizer has no "put species X on route Y" knob, but the ROM API does the job directly:
getEncounters/setEncounters, setStarters, and setStaticPokemon/canChangeStaticPokemon,
then saveRomFile(path, 0). WishlistInject.java uses this to overwrite the earliest grass
tables so a chosen team is catchable by the first gym — run it as a post-step on an
already-built ROM (base build → inject → final). Match areas by displayName substring (e.g.
"ROUTE 101", "PETALBURG WOODS"), filter to "GRASS" tables, keep each slot's existing level
and just swap .pokemon (via getPokemon().get(nationalDex)). Note: Gen 3 Ruby/Sapphire have no
early gift-NPC to repurpose, so deliver a "gift" mon as a 100%-fill first-route encounter instead;
enable NATIONAL_DEX_AT_START so out-of-regional-dex species behave. Verify by loading the
saved ROM back and re-reading the tables — confirm the edits survived saveRomFile.
Evolution stones early: field items are a dead end for authored placement — the list
(getRegularFieldItems, ~210 entries in Gen 3) has no location labels, so you can't guarantee a
ground item is "early." Instead give the stone as a guaranteed wild held item on the mon that
needs it: getPokemon().get(dex).guaranteedHeldItem = <itemId> (and zero common/rareHeldItem).
Item IDs are generation-specific (Gen 3: fireStone=95, thunderstone=96, waterStone=97 — read
com.dabomstew.pkrandom.constants.Gen<N>Items). saveRomFile persists species held items; verify
by reloading and reading the field back.
Gen 3 surgery UPR has no knob for — soundtrack replacement (m4a/MP2K, not
MP3), dialogue edits (uncompressed single-byte pret charmap), and map-script
patches such as an Emerald intro skip: recipes in
references/gen3-rom-surgery.md. The rules that hold across all three: anchor
every site by its full vanilla bytes (exactly one match, fail closed), never
by absolute offset; in-place text is equal-or-shorter plus 0xFF and zero fill,
never longer, because strings sit back-to-back among script bytecode; repoint
the single header pointer last so the patch is atomic; and prove the edit by
reloading the saved ROM with the handler — a valid header is not playback.
Assets
assets/BuildSettings.template.java— copy per build; every knob documented inline.assets/SeededRandomize.java— deterministic, generation-agnostic build driver.assets/EncounterDiff.java— encounter-mapping lookup / global-1-to-1 verifier.assets/WishlistInject.java— hand-place a fixed team into early routes (edit theplanmap).
Persist a per-build folder with the filled BuildSettings.java, the .rnqs, the .log, and a
rebuild.command wrapper so the exact ROM is reproducible later.
