creative/pokemon-rom-randomizer/SKILL.md

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/java is 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):

  1. BuildSettings.template.java — builds a Settings object with the desired options and .write()s a valid .rnqs. Copy per-build, edit the options.
  2. SeededRandomize.java — loads the ROM (generation-agnostic), applies the .rnqs with a fixed seed, writes the ROM + a -l log. Deterministic: same inputs → byte-identical output.
  3. 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 in write() unless you set setSelectedEXPCurve(...) and setRomName(...). The GUI sets these implicitly; a headless builder must set them.
  • setXxxMod(boolean...) is radio-button/ordinal order — pass one true at 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'.
  • customStarters values 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 -l log — if they're off by one, the offset convention shifted for that gen/version.
  • Misc tweaks = bitwise-OR of MiscTweak.<NAME>.getValue() passed to setCurrentMiscTweaks. 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.FactoryGen7RomHandler.Factory) and using the first whose isLoadable(path) is true — the shipped SeededRandomize.java/EncounterDiff.java already do this, so the same code handles GBA + NDS.
  • The CLI random-seeds. PokeRandoZX.jar cli -s … -i … -o … -l picks a new seed each run (not reproducible). For a fixed, reproducible world use the driver's Settings.read + Randomizer.randomize(outFile, log, seed) (the 3-arg overload). The Randomizer constructor needs a ResourceBundleResourceBundle.getBundle("com.dabomstew.pkrandom.newgui.Bundle").
  • Static RANDOM_MATCHING keeps legendaries-as-legendaries (and regular statics/gifts regular). UNCHANGED keeps 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 with SIMILAR_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_STRENGTH stays meaningful and mons stay recognizable; the template lists these as opt-in.
  • A memorable literal seed keeps builds reproducible; Math.random()/Date aren'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 the plan map).

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.