Users browsing this thread: 1 Guest(s)
Condensing Spell List in Battle

#1
Posts: 175
Threads: 11
Thanks Received: 10
Thanks Given: 8
Joined: May 2013
Reputation: 13
Status
Well-Fed
So one of the things that's always bothered me about FFVI is having to scroll through half of the magic menu to get to someone's spells if someone else knows a lot of magic. BUT NO MORE. The holy grail of FFVI patches is here!

This assembly hack will sort through a character's known spells in battle and 'shuffle' the list up, spell by spell, until all of the known spells are in one contiguous list while blanking out old entries so there are no repeated spells. It's surprisingly simple, once I wrapped my head around how the game stores spell lists in-battle (and thank you, seibaby, for letting me ramble at you in Discord, because if I hadn't I don't think I'd have had the breakthrough that allowed me to code this). As of version 1.1, it also shuffles Lores in the same way, and I've fixed an error where I completely misunderstood the way MP deduction worked when a spell was cast, so it was pulling the wrong data.

VOILA.


Code:
hirom
;header

!freespace_C2_0 = $C2A65A
!freespace_C2_1 = $C2FAB0

org $C2256D                
JSR condenseSpellLists ; this was originally a JSR to modify available commands; we'll be JMPing to that
                            ; at the end of the modified code so that RTS comes back to the right spot


                            
org $C24F11
REP #$10
LDA $3A7B                ; (get attack #) -- the new code will do this again in a bit, but
CPX #$0008                ; we need it in A now or we'll break monster casting
BCS fka_4F47            ; (branch if it's a monster attacker.  they don't have
                        ; menus containing MP data, nor relics that can
                        ; alter MP costs.)
                        ; This was originally in the middle of the code, but
                        ; there shouldn't be any issues doing this check first,
                        ; and it makes the new code MUCH smoother.
JMP calculateMPDeduction

org $C24F47
fka_4F47:

                            
org !freespace_C2_0
condenseSpellLists:
PHX                ; This is our character ID coming in
PHP
REP #$10
LDY #$0004        ; this is the index of the first Spell slot in the character's spell list
STY $F0        ;
LDX #$004A        ; this is our main loop index; we're checking 53 spells and 23 lores, since we
                ; don't need to check the last slot of either list
    .checkLoreLoop        ; don't need to actually check the last slot of either list
    LDA ($F2),Y
    CMP #$FF
    BNE .checkNextLore

        .findNextLore
        INY #4
        CPY #$00DC    ; if we've hit the first Lore slot, there are no more spells to copy back
        BEQ .noMoreSpells    ; so jump out, reset $F0 to start our lores
        CPY #$013C    ; this is after the last Lore slot, so if we've gone that far, there are no more spells to copy back
        BEQ .noMoreLores
        LDA ($F2),Y
        CMP #$FF
        BEQ .findNextLore

    PHX            ; set aside the starting byte for the spell we found
    PHY            ; we'll be pushing and pulling within the loop, but we need to know
    LDX #$0003    ; where it started so we can blank out the slot we copied from
    
        .copyNextLore
        LDA ($F2),Y    ; Yes, we just did this, but we need to do it within this loop, too
        PHY            ; this stores our Y location, i.e. the next slot with a spell learned
        LDY $F0        ; and loads our index for slot to write to
        STA ($F2),Y
        PLY            ; back to our 'write from' location
        INY            ; and gets the next byte
        REP #$20
        INC $F0        ; while getting our next write-to byte, too. on the last loop,
                    ; this will get set to the first byte of the next spell slot (which
        SEP #$20
        DEX            
        BPL .copyNextLore    ; if we haven't done four bytes, loop back and grab the next

    PLY            ; this is the first byte of the slot we copied from
    LDA #$FF
    STA ($F2),Y    ; this blanks out the spell we copied from
    LDA #$00
    STA ($F4),Y    ; and zeroes out the MP cost
        
    PLX                ; this gets our index for the loop -- how many spell slots we've checked
    BRA .weCopiedALore
    .checkNextLore
    REP #$20
    INC $F0            ; if we DIDN'T copy a spell, we need to increment our 'current slot' index
    INC $F0            ; but if we did, the loop already has it pointing to the next slot
    INC $F0
    INC $F0
    SEP #$20
    .weCopiedALore
    LDY $F0            ; and then copy it over to Y for our next loop through
    CPY #$00D8        ; if this is the last spell slot
    BEQ .checkNextLore    ;loop back up and INC again so we skip over it and point at our first Lore slot
    
    DEX
    BPL .checkLoreLoop
    .noMoreLores
    
PLP    
PLX
JMP $532C

.noMoreSpells
LDX #$0016            ; reset our index to cover just the Lores
STY $F0                ; and reset our working index for the first Lore slot
BRA .checkLoreLoop



org !freespace_C2_1
calculateMPDeduction:

REP #$10            ; (Set 16-bit X and Y)
LDA $3A7A            ; (get command #)
CMP #$19
BEQ .calculateSummon    ; (branch if it's Summon)
CMP #$0C
BEQ .calculateLore    ; (branch if it's Lore)
CMP #$02
BEQ .calculateMagic    ; (branch if it's Magic)
CMP #$17
BNE fka_4F53        ; (branch if it's not X-Magic)

.calculateMagic
LDA $3A7B            ; (get attack #)
STA $F0                        ; save our spell ID in scratch memory
REP #$20
LDA #$0004                    ; four bytes per index

.loreEntersHere
CLC
ADC $302C,X                 ; get the start of our character's magic list (index #0 is esper)
STA $F2                        ; this points out our first Magic slot
INC #3
STA $F4                        ; and this points at our first MP cost slot
SEP #$20
PHY
LDY #$0000

    .findSpell
    LDA ($F2),Y
    CMP $F0
    BEQ .getMPCost
    CMP #$FF
    BEQ fka_4F53
    INY #4
    BRA .findSpell
    
.getMPCost
LDA ($F4),Y
PLY
BRA fka_4F45

.calculateLore
LDA $3A7B                    ; (get attack #)
SEC
SBC #$8B                    ; turn our raw spell ID into a 0-23 Lore ID
STA $F0
REP #$20
LDA #$00DC                    ; this is our first Lore slot in the character's spell list
BRA .loreEntersHere

.calculateSummon            ; rather than looking for the spell ID, I'm operating under
REP #$20                    ; the assumption that if someone is using Summon, it's already
LDA $302C,X                    ; checked for the equipped Esper, so I'm just loading the MP cost
TAX                            ; from the first entry in the character's list
SEP #$20
LDA $0003,X
BRA fka_4F45


fka_4F45: JMP $4F54            ; (clean up stack and exit)
fka_4F53: JMP $4F53


Anyone questions about what the code is doing, please let me know!



Attached Files
.zip  Condensed Spell List v1-1.zip (68.52 KB, 18 downloads)


Current Project: FF6: Tensei | Discord ID: TristanGrayse
  Find
Quote  
[-] The following 6 users say Thank You to GrayShadows for this post:
  • Morendo (05-30-2020), PowerPanda (09-17-2017), Rjenyawd (09-18-2017), Robo Jesus (12-24-2017), SSJ Rick (09-18-2017), Warrax (09-14-2017)

#2
Posts: 3,966
Threads: 279
Thanks Received: 233
Thanks Given: 56
Joined: Oct 2011
Reputation: 65
Status
Tissue-aware
This is a neat idea! Good job on this one!
  Find
Quote  

#3
Posts: 175
Threads: 11
Thanks Received: 10
Thanks Given: 8
Joined: May 2013
Reputation: 13
Status
Well-Fed
Thanks! I've been banging my head against this basically since I started work on Tensei, so it's nice to have the breakthrough today. I hope it's of use to some other people, too. Laugh


Current Project: FF6: Tensei | Discord ID: TristanGrayse
  Find
Quote  

#4
Posts: 208
Threads: 3
Thanks Received: 0
Thanks Given: 8
Joined: May 2013
Reputation: 0
Status
None
Great patch! Novalia Spirit did a similar patch years ago called Magic Menu Sorting fix but it wasn't 100% effective. Yours on the other end...everything sorts just like it's supposed to, good job!
  Find
Quote  

#5
Posts: 200
Threads: 1
Thanks Received: 10
Thanks Given: 0
Joined: Oct 2015
Reputation: 18
Status
None
GrayShadows: does MP cost deduction work right?  your new function doesn't seem to modify the $3084 structure for the swaps, and Function C2/4F08 reads from this structure.  but truthfully, i dunno HOW you'd do this, given there is a single $3084 structure used for everybody in the party.  seems like you'd need to have 4 separate versions of that list now.  or replace C2/4F30 with a slow traversal of your modified spell list to track the spell down.

Warrax: iirc, Novalia Spirit's patch successfully implemented its more modest goal: remove rows of spells that are unknown by all party members.  this is what FF6j did, but FF3us failed to match, because it didn't adjust for the change in row size.  iow, novalia's = bugfix, current thread = tweak.
Quote  

#6
Posts: 175
Threads: 11
Thanks Received: 10
Thanks Given: 8
Joined: May 2013
Reputation: 13
Status
Well-Fed
Per request, I've written additional code that applies the same functionality to the Lore list. It's been edited into a separate code box in the first post!

ETA: Hmmm, assassin. It's not going to work right, I can tell you that without even testing it. I'm going to have to sit down and take a look at what I can do about that...


Current Project: FF6: Tensei | Discord ID: TristanGrayse
  Find
Quote  

#7
Posts: 208
Threads: 3
Thanks Received: 0
Thanks Given: 8
Joined: May 2013
Reputation: 0
Status
None
(09-14-2017, 08:42 PM)assassin Wrote: Warrax: iirc, Novalia Spirit's patch successfully implemented its more modest goal: remove rows of spells that are unknown by all party members.  this is what FF6j did, but FF3us failed to match, because it didn't adjust for the change in row size.  iow, novalia's = bugfix, current thread = tweak.

Ah I see, that's why it felt somewhat incomplete.

Edit:
(09-14-2017, 08:42 PM)assassin Wrote: does MP cost deduction work right?
Nope! Some spells does, some don't. Looks like this tweak need more work!
  Find
Quote  

#8
Posts: 175
Threads: 11
Thanks Received: 10
Thanks Given: 8
Joined: May 2013
Reputation: 13
Status
Well-Fed
Yeah, I can see where the issue is coming in -- I just need to sit down and wrap my head around how to handle it. I think assassin's suggestion of running through the character's list until you find the spell in question is the best way.


Current Project: FF6: Tensei | Discord ID: TristanGrayse
  Find
Quote  

#9
Posts: 200
Threads: 1
Thanks Received: 10
Thanks Given: 0
Joined: Oct 2015
Reputation: 18
Status
None
i think the list condensing code can be sped up some.

now, it's only run once per party member at battle start, so speed shouldn't matter too much.  but 53^2 iterations is a bit high.

rather than having two, nested loops, you can have one main loop using two indices:
- current place in full list
- next spot to write in work-in-progress shortened list

you'd increment the first one normally while looping (54 passes).  whenever you find a non-null entry in the full list, copy from that position to the slot at the "short list" index (unless the indices are equal), and increment the latter index.

when you're done looping, fill the list from the short index through the end with FFs and 00s.

so that's 108 iterations overall max for Magic.
Quote  

#10
Posts: 175
Threads: 11
Thanks Received: 10
Thanks Given: 8
Joined: May 2013
Reputation: 13
Status
Well-Fed
Hmm. So I tackled the optimisation first, assassin, based on your suggestion about using two indices -- I think I did basically the opposite of what you suggested, but it's definitely working. 

Code:
org !freespace_C2_0

CondenseSpellLists:
PHX                ; This is our character ID coming in
LDY #$04        ; the #0 index in our list is the esper; the character's magic list starts at index 4
STY $F0            ; earlier, $F0 stores number of spells a characters knows, but it shoooould be free to use here, let's test it and see if something breaks
                ; now it's going to be our "last open spot" index for our loop
LDX #$34        ; this is our main loop index; we're checking 53 slots for a total of 54 spells
    .checkSpellLoop
    LDA ($F2),Y
    CMP #$FF
    BNE .checkNextSpell
    PHX
    LDX #$03
    
        .findNextSpell
        INY #4
        CPY #$DC    ; this is the first Lore slot, so if we've gone that far, there are no more spells to copy back
        BEQ .noMoreSpells
        LDA ($F2),Y
        CMP #$FF
        BEQ .findNextSpell

        PHY            ; set aside the starting byte for the spell we found
                    ; we'll be pushing and pulling within the loop, but we need to know
                    ; where it started so we can blank out the slot we copied from
        .copyNextSpell
        LDA ($F2),Y    ; Yes, we just did this, but we need to do it within this loop, too
        PHY            ; this stores our Y location, i.e. the next slot with a spell learned
        LDY $F0        ; and loads our index for slot to write to
        STA ($F2),Y
        PLY            ; back to our 'write from' location
        INY            ; and gets the next byte
        INC $F0        ; while getting our next write-to byte, too. on the last loop,
                    ; this will get set to the first byte of the next spell slot (which
        DEX            
        BPL .copyNextSpell    ; if we haven't done four bytes, loop back and grab the next

        PLY            ; this is the first byte of the slot we copied from
        LDA #$FF
        STA ($F2),Y    ; this blanks out the spell we copied from
        LDA #$00
        STA ($F4),Y    ; and zeroes out the MP cost
        
    PLX                ; this gets our index for the loop -- how many spell slots we've checked
    BRA .weCopiedASpell
    .checkNextSpell
    INC $F0            ; if we DIDN'T copy a spell, we need to increment our 'current slot' index
    INC $F0            ; but if we did, the loop already has it pointing to the next slot
    INC $F0
    INC $F0
    .weCopiedASpell
    LDY $F0            ; and then copy it over to Y for our next loop through
    
    DEX
    BPL .checkSpellLoop
    
    BRA .earlyExit
    .noMoreSpells
    PLX                ; if we exited early because there were no more spells to check, we need
                    ; to make sure we settle up our stack -- exit early bypasses a PLX
    .earlyExit
    
PLX
JMP $532C


Any suggestions on optimising it further? If this looks good, I'll swap it over for Lore sorting, too.

I'm sitting down with C2/4F08 now to figure out what I can do with that.

AHAHAHAHA I DID IT. Optimised the sorting per before (and sorting Lore again, as well), and MP deduction is working properly for Lore, Magic, and Espers, including with MP-cost adjusting relics.

Code:
hirom
;header

!freespace_C2_0 = $C2A65A
!freespace_C2_1 = $C2FAB0

org $C2256D                
JSR condenseSpellLists ; this was originally a JSR to modify available commands; we'll be JMPing to that
                            ; at the end of the modified code so that RTS comes back to the right spot


                            
org $C24F11
REP #$10
LDA $3A7B                ; (get attack #) -- the new code will do this again in a bit, but
CPX #$0008                ; we need it in A now or we'll break monster casting
BCS fka_4F47            ; (branch if it's a monster attacker.  they don't have
                        ; menus containing MP data, nor relics that can
                        ; alter MP costs.)
                        ; This was originally in the middle of the code, but
                        ; there shouldn't be any issues doing this check first,
                        ; and it makes the new code MUCH smoother.
JMP calculateMPDeduction

org $C24F47
fka_4F47:

                            
org !freespace_C2_0

condenseSpellLists:
PHX                ; This is our character ID coming in
LDY #$04        ; the #0 index in our list is the esper; the character's magic list starts at index 4
STY $F0            ; earlier, $F0 stores number of spells a characters knows, but it shoooould be free to use here, let's test it and see if something breaks
                ; now it's going to be our "last open spot" index for our loop
LDX #$34        ; this is our main loop index; we're checking 53 slots for a total of 54 spells
    .checkSpellLoop
    LDA ($F2),Y
    CMP #$FF
    BNE .checkNextSpell
    PHX
    LDX #$03
    
        .findNextSpell
        INY #4
        CPY #$DC    ; this is the first Lore slot, so if we've gone that far, there are no more spells to copy back
        BEQ .noMoreSpells
        LDA ($F2),Y
        CMP #$FF
        BEQ .findNextSpell

        PHY            ; set aside the starting byte for the spell we found
                    ; we'll be pushing and pulling within the loop, but we need to know
                    ; where it started so we can blank out the slot we copied from
        .copyNextSpell
        LDA ($F2),Y    ; Yes, we just did this, but we need to do it within this loop, too
        PHY            ; this stores our Y location, i.e. the next slot with a spell learned
        LDY $F0        ; and loads our index for slot to write to
        STA ($F2),Y
        PLY            ; back to our 'write from' location
        INY            ; and gets the next byte
        INC $F0        ; while getting our next write-to byte, too. on the last loop,
                    ; this will get set to the first byte of the next spell slot (which
        DEX            
        BPL .copyNextSpell    ; if we haven't done four bytes, loop back and grab the next

        PLY            ; this is the first byte of the slot we copied from
        LDA #$FF
        STA ($F2),Y    ; this blanks out the spell we copied from
        LDA #$00
        STA ($F4),Y    ; and zeroes out the MP cost
        
    PLX                ; this gets our index for the loop -- how many spell slots we've checked
    BRA .weCopiedASpell
    .checkNextSpell
    INC $F0            ; if we DIDN'T copy a spell, we need to increment our 'current slot' index
    INC $F0            ; but if we did, the loop already has it pointing to the next slot
    INC $F0
    INC $F0
    .weCopiedASpell
    LDY $F0            ; and then copy it over to Y for our next loop through
    
    DEX
    BPL .checkSpellLoop
    
    BRA .earlyExit
    .noMoreSpells
    PLX                ; if we exited early because there were no more spells to check, we need
                    ; to make sure we settle up our stack -- exit early bypasses a PLX
    .earlyExit
    
    
PHP
REP #$10
LDY #$00DC        ; this is the index of the first Lore slot in the character's spell list
STY $F0        ;
LDX #$0016        ; this is our main loop index; we're checking 53 slots for a total of 54 spells
    .checkLoreLoop
    LDA ($F2),Y
    CMP #$FF
    BNE .checkNextLore
    PHX
    LDX #$0003
    
        .findNextLore
        INY #4
        CPY #$013C    ; this is after the last Lore slot, so if we've gone that far, there are no more spells to copy back
        BEQ .noMoreLores
        LDA ($F2),Y
        CMP #$FF
        BEQ .findNextLore

        PHY            ; set aside the starting byte for the spell we found
                    ; we'll be pushing and pulling within the loop, but we need to know
                    ; where it started so we can blank out the slot we copied from
        .copyNextLore
        LDA ($F2),Y    ; Yes, we just did this, but we need to do it within this loop, too
        PHY            ; this stores our Y location, i.e. the next slot with a spell learned
        LDY $F0        ; and loads our index for slot to write to
        STA ($F2),Y
        PLY            ; back to our 'write from' location
        INY            ; and gets the next byte
        REP #$20
        INC $F0        ; while getting our next write-to byte, too. on the last loop,
                    ; this will get set to the first byte of the next spell slot (which
        SEP #$20
        DEX            
        BPL .copyNextLore    ; if we haven't done four bytes, loop back and grab the next

        PLY            ; this is the first byte of the slot we copied from
        LDA #$FF
        STA ($F2),Y    ; this blanks out the spell we copied from
        LDA #$00
        STA ($F4),Y    ; and zeroes out the MP cost
        
    PLX                ; this gets our index for the loop -- how many spell slots we've checked
    BRA .weCopiedALore
    .checkNextLore
    REP #$20
    INC $F0            ; if we DIDN'T copy a spell, we need to increment our 'current slot' index
    INC $F0            ; but if we did, the loop already has it pointing to the next slot
    INC $F0
    INC $F0
    SEP #$20
    .weCopiedALore
    LDY $F0            ; and then copy it over to Y for our next loop through
    
    DEX
    BPL .checkLoreLoop
    
    BRA .earlyExitLore
    .noMoreLores
    PLX                ; if we exited early because there were no more spells to check, we need
                    ; to make sure we settle up our stack -- exit early bypasses a PLX
    .earlyExitLore
    
PLP    
PLX
JMP $532C


org !freespace_C2_1
calculateMPDeduction:

REP #$10            ; (Set 16-bit X and Y)
LDA $3A7A            ; (get command #)
CMP #$19
BEQ .calculateSummon    ; (branch if it's Summon)
CMP #$0C
BEQ .calculateLore    ; (branch if it's Lore)
CMP #$02
BEQ .calculateMagic    ; (branch if it's Magic)
CMP #$17
BNE fka_4F53        ; (branch if it's not X-Magic)

.calculateMagic
LDA $3A7B            ; (get attack #)
STA $F0                        ; save our spell ID in scratch memory
REP #$20
LDA #$0004                    ; four bytes per index

.loreEntersHere
CLC
ADC $302C,X                 ; get the start of our character's magic list (index #0 is esper)
STA $F2                        ; this points out our first Magic slot
INC #3
STA $F4                        ; and this points at our first MP cost slot
SEP #$20
PHY
LDY #$0000

    .findSpell
    LDA ($F2),Y
    CMP $F0
    BEQ .getMPCost
    CMP #$FF
    BEQ fka_4F53
    INY #4
    BRA .findSpell
    
.getMPCost
LDA ($F4),Y
PLY
BRA fka_4F45

.calculateLore
LDA $3A7B                    ; (get attack #)
SEC
SBC #$8B                    ; turn our raw spell ID into a 0-23 Lore ID
STA $F0
REP #$20
LDA #$00DC                    ; this is our first Lore slot in the character's spell list
BRA .loreEntersHere

.calculateSummon            ; rather than looking for the spell ID, I'm operating under
REP #$20                    ; the assumption that if someone is using Summon, it's already
LDA $302C,X                    ; checked for the equipped Esper, so I'm just loading the MP cost
TAX                            ; from the first entry in the character's list
SEP #$20
LDA $0003,X
BRA fka_4F45


fka_4F45: JMP $4F54            ; (clean up stack and exit)
fka_4F53: JMP $4F53

If you're curious about jumping to 16-bit accumulator and back during sorting the Lores, it's because otherwise $F0 increments in 8-bit, which breaks the game, oops. I'm happy to take suggestions on improving that section.


Current Project: FF6: Tensei | Discord ID: TristanGrayse
  Find
Quote  



Forum Jump:

Users browsing this thread: 1 Guest(s)


Theme by Madsiur2017Custom Graphics by JamesWhite