After poking around I’ve figured out what my problem was. This is also a reminder to myself and other programmers to take a break from hard problems from time to time; it really does helps clear the head. I’m posting what I’ve figured out here, and I’ve edited the title to both better reflect what my problem actually was and so that it can be searched for a little easier in case anyone else is looking for this kind of stuff.
One of the things overworlds do is define a trainer’s behavior. Each overworld in a map’s event file has a script field with a script number in it. This script number will either be relatively small (usually < 10), or >= 3000. If the number is >= 3000, a trainer battle is called. In this case, there’s no scripted behavior and instead the game stops player’s movement, pulls up a text box, and starts the battle. The trainer battle it calls is the script number minus 2999 (0xBB7), so to call trainer battle 10, the script number for that trainer’s overworld should be 10 + 2999 = 3009. Gardenia’s gym is an exception and calls scripts since her trainers need to move the clock and fountains after you win their battle. Otherwise, when the script number is small, the overworld calls that script number within that map’s script file*.
Gym trainers don’t actually check the player’s badges to determine whether they can fight the trainer. They can check badges if scripted for it, but they don’t. Instead, gym trainers get marked as already battled in the function called by the gym leader once the player wins. It uses the SetValue “instruction” (in SDSME; in PPRE it’s the SetTrainerID instruction), so SetValue 0xF4 and SetValue 0xF5 mark trainers 244 and 245 as battled, which are the two trainers in Oreburgh’s gym. Removing these instructions let me battle them after Roark. I was swapping these instructions over to the other gym leaders’ scripts, so battling the leaders out-of-order but changing the badge I received to be the one received for that city led me to assume that gym trainers checked badges.
Also, maybe I could have been a little bit more specific with what I was doing and had already accomplished in my first post. However, I didn’t want to bog it down with jargon and details that people might not understand.
*Specific technical stuff here, it probably isn’t too useful to most ROM hackers. In both SDSME and PPRE, a script file’s functions are split into “scripts” and “functions.” From what I can tell the only difference between these two is whether or not their offset is mapped in the script file’s header. The header has some number of consecutive 32-bit integers and is ended by the 16-bit delimiter 0xFD13 (big-endian; it’s 0x13FD in the actual file). Each of those 32-bit integers is the offset - 4 of the start of some function in the file. If a function’s offset is referenced in the header, it’s a script. Otherwise if it isn’t referenced in the header, it’s a function. A function can only be called by an overworld if it is a script, so in turn it has to have its offset defined in the script file’s header. I should also note that the header and script numbers are 1-based instead of 0-based. So the first 32-bit integer in the header (the first 32 bits of the file), at offset 0, is referenced by script number 1. Also minor gripe: naming the functions in script files “scripts” and “functions” in PPRE and SDSME is confusing and misleading. The script files containing something called scripts is confusing. “Scripts” are not really any different from functions, so it would have been better to call them “overworld functions” or “marked functions”, or something.