Tutor Expansion Explained - haven1433/HexManiacAdvance GitHub Wiki
This document explains in detail how Tutor Expansion works in HexManiac. It attempts to explain all the important details of the original implementation, and then explain what changes are made.
This section talks about the parts of the game associated with the original implementation, to provide proper context so that the changes make sense.
- FireRed Special 397:
12781C
- LeafGreen Special 397:
1277F4
- Emerald Special 477:
1B892C
This special is really the 'entrance' into tutors.
- It has one argument: which tutor move are we trying to teach?
- It shows you your pokemon list so you can pick which pokemon should learn the move.
- It calls into many helper routines.
One of those helper routines in FireRed / LeafGreen starts at 11F430
/ 11F408
. It also needs to be updated.
- FireRed:
459B60
- LeafGreen:
459580
- Emerald:
61500C
The default tutor move table has two bytes per move. Since there are 15 moves in the move table (30 for Emerald), GameFreak just lays out the moveIDs, one after the other.
(FireRed and LeafGreen have 3 additional tutors, for the elemental hyper beams, but those are handled differently.)
Here's the only code that uses the Tutor Move Table in FireRed, for reference. Comments/Labels are added for clarity.
GetTutorMove: @ r0 = tutorID. At the end, r0 = moveID.
push lr, {}
lsl r0, r0, #24
lsr r1, r0, #24
cmp r1, #16
beq <tutor16>
cmp r1, #16
bgt <tutorMoreThan16>
cmp r1, #15
beq <tutor15>
b <normalTutor>
tutorMoreThan16:
cmp r1, #17
beq <tutor17>
b <normalTutor>
tutor15:
mov r0, #169 @ r0 = move 338: Frenzy Plant
lsl r0, r0, #1
b <done>
tutor16:
ldr r0, [pc, <blastBurnID>] @ r0 = move 307: Blast Burn
b <done>
blastBurnID:
.word 00000133
tutor17:
mov r0, #154 @ r0 = move 308: Hydro Cannon
lsl r0, r0, #1
b <done>
normalTutor:
ldr r0, [pc, <tutorMoveTable>]
lsl r1, r1, #1
add r1, r1, r0
ldrh r0, [r1, #0] @ r0 = (uint16)ROM[data.pokemon.moves.tutors + tutorID * 2]
done:
pop {r1}
bx r1
tutorMoveTable:
.word <data.pokemon.moves.tutors>
The most important piece of logic is the section labeled normalTutor
. Note that it
just plucks the appropriate pair of bytes from the table and returns it.
Emerald's code is simpler, because it doesn't have to deal with the 3 special cases.
- FireRed:
459B7E
- LeafGreen:
45959E
- Emerald:
615048
The default compatibility table has two bytes per pokemon (4 for Emerald). Since there are 15 moves in the move table (30 for Emerald), GameFreak uses 1 bit for each move for each pokemon to decide whether that pokemon can use that tutor.
(FireRed and LeafGreen have 3 additional tutors, for the elemental hyper beams, but those are handled differently.)
Here's the only code that uses the Tutor Compatibility Table in FireRed, for reference. Comments/Labels are added for clarity.
CanPokemonLearnTutorMove: @ r0=pokemonID, r1=tutorMoveID
push lr, {}
lsl r0, r0, #16
lsr r0, r0, #16
lsl r1, r1, #24
lsr r2, r1, #24
cmp r2, #16
beq <tutor16>
cmp r2, #16
bgt <tutorMoreThan16>
cmp r2, #15
beq <tutor15>
b <normalTutor>
tutorMoreThan16:
cmp r2, #17
beq <tutor17>
b <normalTutor>
tutor15:
cmp r0, #3 @ check Venasaur
beq <canLearn>
b <cannotLearn>
tutor16:
cmp r0, #6 @ check Charizard
beq <canLearn>
b <cannotLearn>
tutor17:
cmp r0, #9 @ check Blastoise
beq <canLearn>
b <cannotLearn>
normalTutor:
ldr r1, [pc, <tutorTable>]
lsl r0, r0, #1
add r0, r0, r1
ldrh r0, [r0, #0] @ r0 = (uint16)ROM[data.pokemon.moves.tutorcompatibility + pokemonID * 2]
asr r0, r2 @ r0 >>= tutorMoveID
mov r1, #1
and r0, r1
cmp r0, #0 @ if r0 != 0: canLearn
bne <canLearn>
cannotLearn:
mov r0, #0
b <done>
tutorTable:
.word <data.pokemon.moves.tutorcompatibility>
canLearn:
mov r0, #1
done:
pop {r1}
bx r1
The most important piece of logic is the section labeled normalTutor
. Note that it
just plucks the appropriate bit after loading from the table and return true if the bit is set.
Emerald's code is simpler, because it doesn't have to deal with the 3 special cases.
The next section will discuss the idea behind what changes are being made to the original code. Then all the exact changes are explained in detail.
- Edit Special 397 to make it act the same for all tutorIDs.
This one is pretty simple. Replace a boundry check to not act different after the first 15 moves. No changes are needed here for Emerald, since it doesn't have the special-case tutors.
- Edit
GetTutorMove
to remove special cases.
This one is also pretty easy. All the code we need is there, we can just make it shorter.
- Edit
CanPokemonLearnTutorMove
to remove the special cases and work for widths.
This is the fun change. Right now, the compatibility for a pokemon is loaded all at once. But since the game can only load up to 4 bytes into a single register, that approach limits us to only 32 moves. We can do some hacky if conditions to up the limit to 64 or even 128, but with an ounce of math we can get the same result with less code.
The tutorID is 8 bits long. The tutor compatibility is packed 8 moves to a byte. So we can use the upper 5 bits of the tutorID to select which byte we need, and use the bottom 3 bits to select which bit we need.
Psuedocode:
GetTutorMove(tutorID):
moveID = ROM[data.pokemon.moves.tutors + tutorID*2]
CanPokemonLearnTutorMove(pokemonID, tutorID):
bytesPerPokemon = ceiling(tutormoves_count / 8)
tutorByte = tutorID >> 3
tutorBit = tutorID & 7
isCompatible = ROM[data.pokemon.moves.tutorcompatibility + pokemonID*bytesPerPokemon + tutorByte] >> tutorBit
For FireRed and LeafGreen, there are two places where the code checks if this is a special case, and then branches off. Since we're removing the special cases, we just replace this branch command with a nop
.
- FireRed: replace the
bhi
commands at127826
and11F458
withnop
- LeafGreen: replace the
bhi
commands at1277FE
and11F430
withnop
- Emerald: no change needed
- FireRed:
120BA8
- LeafGreen:
120B80
- Emerald:
1B2360
GetTutorMove: @ r0 = tutorID. At the end, r0 = moveID.
lsl r0, r0, #1
ldr r1, [pc, <tutormovestable>]
ldrh r0, [r0, r1]
bx lr
tutormovestable:
.word <data.pokemon.moves.tutors>
When you take out the special cases, this routine is actually really simple. The one line of pseudocode translates rather neatly into thumb code.
The new routine is much shorter than the original, so there's no need to move it.
- FireRed:
120BE8
- LeafGreen:
120BC0
- Emerald:
1B2370
CanPokemonLearnTutorMove: @ r0=pokemonID, r1=tutorMoveID
ldr r2, [pc, <tutormoves_count>]
add r2, #7
lsr r2, r2, #3 @ r2 = bytesPerPokemon
mul r0, r2
lsr r2, r1, #3 @ r2 = tutorByte
add r0, r0, r2
mov r2, #7
and r1, r2 @ r1 = tutorBit
ldr r2, [pc, <tutorcompatibilitytable>]
ldrb r0, [r2, r0] @ r0 = ROM[data.pokemon.moves.tutorcompatibility + pokemonID*bytesPerPokemon + tutorByte]
lsr r0, r1 @ r0 isCompatible
mov r2, #1
and r0, r2
bx lr
tutorcompatibilitytable:
.word <data.pokemon.moves.tutorcompatibility>
tutormoves_count:
.word ::data.pokemon.moves.tutors
We removed the special cases and the input trimming. We also removed the push/pop logic since we don't need branch-link. And finally, we told HexManiac to keep the last word equal to data.pokemon.moves.tutors, so the routine will continue working as the user adds more tutors and expands the width.
The new routine is much shorter than the original, so there's no need to move it.
Emerald Special 478 also uses the tutor moves list along with 2 lists of moves within the tutor list to allow for NPCs to offer lists of tutor moves they can teach. The special starts at 13AEB4
and goes for 148 bytes. You could potentially update the tutor move limiters in this routine (search for cmp r2, #29
to find them both), but this isn't necessary since the moves they're looking for will be found within the first 30 moves, unless you change the original move tutors. But if you changed the original move tutors, you'd probably want to replace or rewrite these NPCs anyway.