Tutor Expansion Explained - haven1433/HexManiacAdvance GitHub Wiki

From Scratch: Tutor Expansion

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.

Prerequisits

This section talks about the parts of the game associated with the original implementation, to provide proper context so that the changes make sense.

Tutor Special

  • 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.

Tutor Moves Table

  • 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[tutormoves + tutorID * 2]
done:
    pop   {r1}
    bx    r1
tutorMoveTable:
    .word <tutormoves>

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.

Tutor Compatibility Table

  • 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[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 <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.

Changes

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.

Overview

  • 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[tutormoves + tutorID*2]

CanPokemonLearnTutorMove(pokemonID, tutorID):
    bytesPerPokemon = ceiling(tutormoves_count / 8)
    tutorByte = tutorID >> 3
    tutorBit  = tutorID & 7
    isCompatible = ROM[tutorcompatibility + pokemonID*bytesPerPokemon + tutorByte] >> tutorBit

Editing Special 397

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 at 127826 and 11F458 with nop
  • LeafGreen: replace the bhi commands at 1277FE and 11F430 with nop
  • Emerald: no change needed

Editing GetTutorMove

  • 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 <tutormoves>

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.

Editing CanPokemonLearnTutorMove

  • 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[tutorcompatibility + pokemonID*bytesPerPokemon + tutorByte]
    lsr   r0, r1          @ r0 isCompatible
    mov   r2, #1
    and   r0, r2
    bx    lr
tutorcompatibilitytable:
    .word <tutorcompatibility>
tutormoves_count:
    .word ::tutormoves

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 tutormoves, 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.

Other things we could edit

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.

⚠️ **GitHub.com Fallback** ⚠️