Jump to content

Recommended Posts

Posted (edited)

TLDR; Scroll all the way down to the bottom of this post to see the GitHub where I provide a simple solution to how you can accomplish this. 

See next message for Permanent Megas & Hoopa-Unbound persistence through save restarts. THIS message applies directly to USUM, but the Permanent Mega persistence and Hoopa-Unbound/Primal Reversion/etc applies to BOTH USUM AND ORAS!

Hello everyone! 

This post is meant to document ALL the progress I made in improving/editing the Gen 7 USUM. As of 6/2/26, you will learn how to remove the nerfs to Prankster, Gale Wings, Parental Bond, and Soul Dew in Pokemon Ultra Sun and Ultra Moon after reading this post (and if I discover anything more, I'll just reply to this thread or edit the post if need be).

So full disclosure - I've spent many years trying to figure out how to crack the Gen 7 code, and with our current age of technology it was made possible. I was able to reverse engineer a lot of the USUM battle engine with heavy use of an AI assistant for some of the grunt work, including decompiling, scanning the save-state RAM, and cross-checking. I drove the project, supplied the save states, and verified every step in-game myself. What I've outlined here is reproducible, so you don't have to take my word for any of it. This post is for research purposes.

Chapter 1: How to remove Dark's immunity to Prankster

So my goal was to Gen 6 Prankster behavior in Ultra Sun / Ultra Moon - status moves from a Prankster user can once again affect Dark-types (no more "It doesn't affect…"). Tested and working.

---

The fix (if you just want it)

In Battle.cro (from the RomFS), change one byte:
 

Offset: 0x24B14 
Before: D1 FF FF 0A 
After: D1 FF FF EA 

(Only the last byte changes: 0x24B17, 0A -> EA.) That's it. This flips a conditional branch to an unconditional one so the engine stops failing Prankster-boosted status moves against Dark-types. It does not touch any other immunity (powder/Grass, trapping/Ghost, type-chart immunities all still work).

Installing it - two options:

  1.  LayeredFS (easy, no repacking): put the patched Battle.cro at %APPDATA%\Azahar\load\mods\00040000001B5100\romfs\Battle.cro, fully quit and relaunch Azahar.
  2. Repack the CIA: splice the patched Battle.cro back in and rebuild the RomFS IVFC hash tree + the NCCH romfs hash (signatures can stay broken; Azahar/Citra accept that).
  3. Big gotcha: test on a fresh battle after a clean boot, NOT by loading a save state. A save state is a snapshot of RAM that still contains the old unpatched code, so it'll always show the old behavior no matter how you patch the files.

---

How it was found (short version)

Gen 7 added: a Prankster-boosted status move that hits a Dark-type fails. I wanted that gone on the actual game, not just on a Showdown server.

The hard part: the check isn't a simple if (ability == Prankster). USUM's battle engine (Battle.cro) is a Showdown-style event-dispatch system, and its handler tables are filled in by the loader at runtime meaning they're blank in the file on disk. So static analysis in Ghidra just saw zeros where the logic should be. That blocked progress for a long time (and produced several "patches" that did nothing - including one that accidentally targeted the text formatter, because `158`/Prankster also shows up as a text token).

What cracked it: a Citra/Azahar save state. A .cst is a zstd-compressed snapshot of console RAM - decompress it and you get ~302 MB with all the runtime relocations already applied. From a save state of Prankster Shuckle vs. Dark Tyranitar, I could:

- read the real, populated dispatch tables for the first time;
- find the actual battlers in memory and diff their type fields - that pinned the type cache at battler+0x1E4/5/6, with Dark = 0x10 (Tyranitar showed 05 10 12 = Rock/Dark/none; Shuckle showed 06 05 12 = Bug/Rock/none);
- locate the engine's hasType() function and find the single place in the whole binary that calls hasType(target, Dark) - that's FUN_05024868, the per-target immunity filter.

Its Dark branch is basically the Showdown rule:

target is Dark  AND  move was Prankster-boosted  AND  it's an opponent
   → "It doesn't affect…" + the move fails on that target

Patching the first branch of that check to always "keep" the target removes the immunity. Confirmed in a fresh battle: Thunder Wave / Will-O-Wisp / etc. from a Prankster user now land on Dark-types.

---

Notes / credits

- This is one piece of a broader "restore Gen 6 abilities" project (Gale Wings, Parental Bond, Soul Dew are next).
- Prankster's +1 priority is separate (it lives in `code.bin`); this patch only removes the Dark immunity, leaving the priority boost intact — i.e. true Gen 6 Prankster.
- Method that made it possible, in one line: when a binary's tables are loader-relocated, stop fighting the static image and read a save state instead.

If you're interested in learning more here's a thread I made on hackmons.com that details more information.

---

Chapter 2: How to remove Gale Wing's HP restriction

What this does: restores Gen 6 Gale Wings in Ultra Sun / Ultra Moon - its +1 priority on
Flying-type moves applies at any HP, not just at full HP. Tested and working in Azahar.

---

The fix (if you just want it)

In Battle.cro (from the RomFS), change one instruction (4 bytes):

Offset: 0xDA514
Before: 09 00 00 0A  (`BEQ`) 
After: 00 F0 20 E3  (`NOP`) 

Install it exactly like the Prankster patch (LayeredFS drop, or repack the CIA + rebuild the
IVFC hash tree). Same save-state gotcha: test on a fresh battle after a clean boot, not a
loaded save state.

The two patches are independent and can both live in the same Battle.cro.

---

How it was found (short version)

Showdown's Gen-7 Gale Wings is literally:
if (move.type === 'Flying' && pokemon.hp === pokemon.maxhp) return priority + 1;
The job was to find that "hp === maxhp" gate in the ROM and delete it.

Same wall as before - the logic is event-dispatched, not a hardcoded if (ability == GaleWings),
so the on-disk binary is unhelpful. This time the breakthrough came from two save states +
a live hardware watchpoint:

1. Save-state static pass narrowed it down: there's no cmp #0xB1 (Gale Wings' ability ID)
   anywhere in the battle module, confirming it's dispatched. The priority logic turned out to
   live in the core executable (code.bin), reachable via a mapping I derived from the
   save state (blob = vaddr − 0x100000 + 0x1266A).
2. Live watchpoint (a small Python GDB-stub script) on the Gale Wings Pokémon's HP, run in a
   fresh battle at full HP vs. below full HP, caught every function that reads HP during the
   turn. Cross-referencing those against a static scan found a clean IsFullHP(mon) helper
   (curHP == maxHP -> bool) at 0x7663BC.
3. Of the 13 registered event-handlers that call IsFullHP, exactly one - the handler for
   the priority event 0x11 at 0x7B74E4 - also checks move type == Flying and adds 1 to
   priority. That's Gale Wings, byte-for-byte matching Showdown:

bl IsFullHP            ; curHP == maxHP ?
cmp r0, #0 ; beq skip  ; <-- the Gen-7 nerf (this branch becomes NOP)
... GetMoveType == 2 (Flying) ? ...
GetPriority -> +1 -> SetPriority

NOP the one branch and the +1 applies regardless of HP, while the Flying-type check stays intact.

Here's a detailed breakdown of Gale Wings in the Battle Engine for more information. I will be sharing the expanded details of each of these modifications in these separate links so as not to "clog" up these forums lol

---

Chapter 3: How to make Parental Bond's second hit do 50% damage instead of 25%

What this does: restores Gen 6 Parental Bond in Ultra Sun / Ultra Moon - the second hit deals half damage instead of the Gen 7 quarter. Tested and working in Azahar.

---

The fix (if you just want it)

In `attle.cro (from the RomFS), change one byte:

Offset: 0x24EAC
Before: 01 0B A0 13 (`movne r0,#0x400` = 0.25×)
After: 02 0B A0 13 (`movne r0,#0x800` = 0.5×) 

Same save-state gotcha: test in a fresh battle after a clean boot, not a loaded save state. All of these patches are independent and coexist in one Battle.cro.

---

How it was found (short version)

Showdown's Gen-7 Parental Bond applies chainModify(0.25) to the second hit; Gen 6 was 0.5. So the task was to find the per-hit damage multiplier and double the nerfed value.

The trick was not to chase the ability ID (185 / 0xB9) - event-dispatched engines don't keep tidy "if (ability == ParentalBond)" branches, and my first attempt keyed off the wrong ability byte and did nothing in-game. Instead I chased the damage math:

- The engine uses Q12 fixed-point multipliers (0x1000 = 1.0x, 0x800 = 0.5x, 0x400 = 0.25x).
- In the move-execution function (@vaddr 0x701D68), on the live 2nd-hit call chain, the 2nd+ strike of a reduced multi-strike move loads 0x400 (the Gen 7 quarter) and feeds it to the damage builder (0x75BCE4); the first hit uses 0x1000.
- That 0x400 is an instruction immediate at @vaddr 0x701EAC. Changing the encoded value 0x01 -> 0x02 makes it 0x800 (0.5x).

The first hit (1.0x path) and ordinary multi-hit moves (Bullet Seed, etc., which take the 1.0x path) are untouched - only the reduced second strike changes.

Live-confirmed: with a Parental Bond user vs a fixed target, hit 1 = 50, hit 2 = 24 (ratio ~ 0.48, i.e. half), where Gen 7 had it at a quarter. Numbers read off-screen and cross-checked against the Smogon calc.

Here's a link to further documentation on how this was made possible.

---

Chapter 4: How to buff Soul Dew back to Gen 3-6 mechanics

This is the big one, and it's not a byte flip - Gen 7 deleted the stat logic and replaced it with a +20% Psychic/Dragon move-power effect. I disassembled the Gen-6 handler out of ORAS's DllBattle.cro, rewrote it for USUM's engine, and injected it as a new handler into a code cave in Battle.cro, then repointed Soul Dew's existing handler at it.

- Mechanism: event 0x47 (damage modifier), arg 0x35 (Q12 multiplier). Offense sets 0x1800 (1.5x), defense sets 0xAAB (0.667x incoming = +50% Sp. Def). Special-category gate GetEventArg(0x1e)==2; species 0x17C/0x17D (covers Mega formes too).
- Code cave: the alignment padding between .text and .rodata (file 0xFC974/vaddr 0x7D9974) - genuinely free at runtime and inside .text's executable page. (The obvious-looking interior "zero gaps" are relocated pointer tables - zero on disk, overwritten at load. That trap cost me three crashes; validate caves against the loaded RAM image, not the file.)
- Verified in-game: Psychic (resisted) dealt 12 to a Lv50 Latios - below the 15-damage floor any unboosted Slowbro could deal -> Sp. Def boost proven; Oblivion Wing (non-STAB) chunked a bulky Slowbro ~50%/hit -> Sp. Atk boost proven.

Here's the complete walkthrough and documentation of how Soul Dew's un-nerf was made possible.

---

Last Remarks

Because two of these change behavior, I also edit the matching ability/item descriptions in the message archive (a/0/3/2, English: ability descriptions = bank 102, item descriptions = bank 39) so the in-game text isn't misleading. Done with a small Gen-7 text encoder that rebuilds the archive so every other string stays byte-identical, then re-fixes IVFC.

- **Gale Wings** -> "Gives priority to the Pokémon's Flying-type moves." (drops "when HP is full")
- **Soul Dew** -> "…It raises its Sp. Atk and Sp. Def stats."
- **Prankster / Parental Bond** -> no text change (their descriptions never stated the nerfed numbers).

Here is the link to some patches I created to make life easier for people (EXPERIMENTAL): https://github.com/isleep2late/Un-Nerf-Compendium (I will be actively maintaining this repository.)

 

Let me know if you have any questions or need help with anything!


-IS2L

Edited by isleep2late
I finally figured out how to do it but I didn't want to make a second post that would clog the forums.
  • isleep2late changed the title to [SOLVED] Editing abilities & Permanent Megas/Hoopa-Unbound persistence through save restarts!
Posted

Chapter 5: Permanent Mega/Primals/Hoopa-Unbound Persistence through saves

Before I get started, I just want to say a LOT of exciting things are happening/being discovered. If you've been following the Battle Maison/Battle Tree restriction post (click this very long hyperlink to get there), I discovered how to remove the Item/Species Clause AND specificically for ORAS, how to bypass the 510 EV limit, a feat once thought to be impossible.

 

Now onto the real deal. I didn't want to clog the forums with literature so I'm consolidating all/most of my findings into this thread. I created a small code patch that stops Gen 6/7 from resetting battle-only / restricted formes when you load a save. Set a Mega (or Primal, Hoopa-Unbound, Necrozma Ultra-Burst, Zygarde-Complete, etc) on your party with PKHeX and it now STAYS through save + reload - on your team and in the PC.

 

If you'd like to cut to the chase, say no more. Here's a link to the GitHub containing the patch and how to apply it.

 

By design, X/Y, OR/AS, S/M and US/UM run "normalize formes" routines outside of battle that quietly call `ChangeFormNo(baseForme)` on your Pokémon - on save-load (Mega/Primal), on the field clock (Furfrou 5-day / Hoopa 3-day), at night (Shaymin-Sky), on PC deposit, in Pokémon refresh, and in the Day Care. That's why a PKHeX-set Mega snaps back to base the moment you boot.

This patch NOPs those revert calls and leaves the forme setters untouched (you can still Mega Evolve in battle, use the Prison Bottle, etc.). Net result: the forme you store is the forme you keep.

Fusions (Kyurem-B/W, Necrozma Dusk-Mane/Dawn-Wings) already persist and need nothing. In-battle stance formes (Aegislash, Wishiwashi, etc.) are handled by the battle module and aren't touched, so battles still behave normally.

How to use:

1. Decrypt-dump your game (Mega/Primal-persist works on a decrypted `.cia` or `.3ds`).
2. `python formepersist.py YourGame.cia` - the Mega/Primal fix is auto-located, so it should theoretically work on any of X/Y, OR/AS, S/M, US/UM, any region/version (However, this was mostly tested on Ultra Moon and ORAS, but the patch should absolutely work on Ultra Sun because the location on Ultra Sun is in a slightly different location than Ultra Moon and was found. XY/Sun/Moon are the wildcards.)
3. Add `--full` for the complete forme set on US/UM and OR/AS (verified address tables included).
4. The script re-fixes the ExeFS/.code hashes (and TMD for `.cia`), so the build still installs and boots in Citra/Azahar/Lime3DS.

Notes

- This edits a decrypted personal dump

- Nothing copyrighted is distributed.
- It's for casual single-player use.

Semi-relevant but not for this specific channel: Sword/Shield first pass (LayeredFS `.pchtxt` for Crowned Zacian/Zamazenta + Eternamax) is in testing - different mechanism (formes are derived from held item / story flag rather than reset), so it's a few NOPs in the `main` NSO rather than one. More on that once it's confirmed in-game.

Feedback welcome - if a specific forme still reverts on your version, tell me which game/version and I'll add it.

Here's an article for a deep dive on how this was discovered.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...