News:

11 March 2016 - Forum Rules

Main Menu

16-bit Math Problems (on an 8-bit system)

Started by Jiggers, May 12, 2018, 04:10:48 PM

Previous topic - Next topic

Jiggers

I have been having trouble with high and low bytes of 16-bit numbers. Once in a while I almost understand things, but then it flies out the window the next.

I think, possibly, the part that I don't understand is the FLOW of what's needed. When do I start with the low byte and add the carry into the high byte? When do I start with the high byte?

Here's my current problem:

I want to make a healing spell that transfers HP from one character to another - damaging the healer, healing the patient.

The original code, which I understand perfectly, is:
LDA hp_recovery     ; healing potency
    CLC
    ADC ch_curhp, X             ; add healing potency to low byte of HP
    STA ch_curhp, X
    LDA ch_curhp+1, X           ; add 0 to high byte of HP (to catch carry from low byte)
    ADC #0
    STA ch_curhp+1, X


Originally I was just going to have "hp_recovery" subtract from the healer's HP after. The issue is, what if the patient is almost healthy? The healer's potency would damage them far more than they healed the patient. I need to cap the damage done to the healer if they overheal the patient.

So first I thought, maybe...
LDA ch_maxhp+1, X   
SEC
SBC ch_curhp+1, X     
STA MMC5_tmp+1
LDA ch_maxhp, X
SEC
SBC ch_curhp, X
STA MMC5_tmp

And that gets the amount of healing needed, stored in MMC5_tmp and +1. Then I could compare it to "hp_recovery" and do something to cut off the overheal amount before healing even begins...

But where to begin? High byte or low byte? "hp_recovery" is only one byte, so would I need to use #0 in the second comparison again, so it only compares the carry?

________

Edit: Slugged away at it a little more. Maybe made progress?

MenuRecoverHP_Magic:   
    LDA ch_maxhp, X
    SEC
    SBC ch_curhp, X
    STY MMC5_tmp
    LDA ch_maxhp+1, X   
    SEC
    SBC ch_curhp+1, X     
    STA MMC5_tmp+1
    BEQ :+              ; if high bytes are 0, then Z is set. Z is clear if there is high byte HP to heal
       CPY hp_recovery  ; compare the amount of lost low byte HP to the amount to be healed
                        ; C is clear if there is overheal. C and Z is set if its exactly equal.
       BCS @NoOverheal
            STY hp_recovery  ; cap hp_recovery at what will fill up the low byte.
            JMP @NoOverheal  ; and now there's no more overheal, so jump to healing!
   
    : ; jump here if there is healing to be done in the high bytes...
    ;; now I'm not sure what to do
    ;; Heal the low bytes and then do this all over again with the carry, somehow?
   
   
    @NoOverheal:
    LDA hp_recovery             ; healing potency
    CLC
    ADC ch_curhp, X             ; add healing potency to low byte of HP
    STA ch_curhp, X
    LDA ch_curhp+1, X           ; add 0 to high byte of HP (to catch carry from low byte)
    ADC #0
    STA ch_curhp+1, X
    CMP ch_maxhp+1, X           ; then compare against max HP to make sure we didn't go over
    BEQ _MenuRecoverHP_CheckLow ; if high byte of cur = high byte of max, we need to check the low byte
    BCS _MenuRecoverHP_OverMax  ; if high byte of cur > high byte of max... we went over


I know exactly what I'm doing. I just don't know what effect it's going to have.

I wrote some NES music! Its a legal ROM file. - I got a Ko-Fi page too.

Squall_FF8

QuoteBut where to begin? High byte or low byte? "hp_recovery" is only one byte, so would I need to use #0 in the second comparison again, so it only compares the carry?
When you do 16 bits math with 8 bit arithmetic, the order is:
- Prepare the Carry Flag
- low byte operation
- high byte operation
* when one of the 16 bit operand is < 256, assume 0 as high byte. Example $20(8bit) = $0020(16bit)
* you set the Carry Flag only once - at the beginning. It is call "Carry" for a reason - each operation carry the extra bit of each operation.
* It is similar if you need more bits - 24, 32,...  :)

BTW you have a mistake in your second code: - remove the 2nd SEC
Welcome to the FF5 Den: https://discord.gg/AUqDF85

Jiggers

#2
Thanks for catching that! Also...

Quotewhen one of the 16 bit operand is < 256, assume 0 as high byte. Example $20(8bit) = $0020(16bit)
< is "less than", right? I have a huge problem with those signs. I have a little chart to look at to know which is which and the moment I look away I lose track of what it means. Part of why doing comparison math stuff is so difficult for me... Starting out last year, every BNE, BEQ, BCC, BCS, was a huge nightmare taking half an hour until I somehow got it clicking (sometimes it still doesn't.) Taking numbers out and using words ("branch if not equal") helped a lot in really understanding what I was doing with them. And yet, less than/more than signs are still my nemesis.

I think I have it right so far, based on the order you gave me... But I don't know what kind of operation to perform now.

I need to... fill the low byte of HP with the heal potency...? Orrr, subtract the low byte FROM the heal potency, then compare the leftover heal potency with the high byte, and cap heal potency if THAT goes over...? Edit: I really don't know. I guess this is a logic flow problem. The math will be EASY compared to figuring out what I'm supposed to do.

Oh, here's another thing I was wondering about, but its a 24-bit number:

SumBattleRewardEXP:
;; Y = EXP

    LDA #$00
    STA battlereward            ; reset reward to zero
    STA battlereward+1
    STA battlereward+2
   
    LDX #$09                    ; loop 9 times, once for each enemy slot
   
  @Loop:
      CLC
      LDA btl_enemystats, Y     ; sum low byte
      ADC battlereward
      STA battlereward
     
      LDA btl_enemystats+1, Y   ; high byte
      ADC battlereward+1
      STA battlereward+1
     
      LDA battlereward+2        ; carry into 3rd byte - This is used for GP but not for Exp
      ADC #$00
      STA battlereward+2
     
      TYA                       ; it assumes C is clear here, since sum cannot be over $FFFFFF
      ADC #$14                  ; add $14 to source index to move to next enemy ($14 bytes per enemy)
      TAY
     
      DEX
      BNE @Loop
     
      LDA ExpGainOption
      BEQ HalveReward          ; If ExpGain is 0 (Low), halve the reward
          CMP #2
          BEQ DoubleReward     ; If ExpGain is 2 (High), 1.5x the reward
                               ;; Otherwise, ExpGain is 1, normal, so do nothing.
    RTS
   
HalveReward:
    LSR battlereward+1
    ROR battlereward
    LDA #0
    STA battlereward+2
    RTS

DoubleReward:   
    ASL battlereward
    ROL battlereward+1
    LDA #0
    STA battlereward+2          ; not sure what to do with a third byte for this
RTS


Should it be
LSR battlereward+2
ROR battlereward+1
ROR battlereward   ?
I know exactly what I'm doing. I just don't know what effect it's going to have.

I wrote some NES music! Its a legal ROM file. - I got a Ko-Fi page too.

Squall_FF8

#3
QuoteShould it be ...
Here you have another multi-bite operation - shift (left or right)

1. Shift Right (divide by 2):
LSR Variable +0  ; you start with the leftmost (highest order) byte
ROR Variable +1
ROR Variable +2

2. Shift Left ( multiply by 2)
ASL Variable +2  ; you start with rightmost byte (lowest order)
ROL Variable +1
ROL Variable +0

* you don't need to prepare C. ASL & LSR use 0 on the place of empty bit
* you don't use 0 as padding. That was done when the Variable was given a value

BTW if you have done this alone, excellent work in setting a multi-byte value and multi-byte addition (3bytes or 24bits) :thumbsup:
Welcome to the FF5 Den: https://discord.gg/AUqDF85

Jiggers

#4
Yay, I got that! I only did the LSR/ROR thing and the ExpGainOption bit alone, though. The rest is Disch's disassembly for FF1. Originally I was going to go for a .75% / 1.50% thing, but... that's a headache for another day.

I've re-worked my healing code. I think I need someone to check it out before I test it. Either I'm over-thinking the whole thing, or its fine and I'm not convinced it'll work for some reason.  :-[ I feel like I'm missing something either super obvious or super difficult that's over my head.


Okay, I've figured out the logic flow.

    If HP is one byte:
    Load max HP, subtract current HP = damage
    Compare healing to damage...
   
    If Healing is MORE THAN Damage:
    Healing - Damage = Overheal Amount
    Damage - Overheal Amount = healer damage

    If Healing is LESS THAN Damage:
    Healing = Healer damage
   
    ~ ~ ~
   
    If HP is two bytes:
    Load max HP, subtract current HP = damage (2 bytes)
   
    * How to compare 1 byte of healing to 2 bytes of HP?
   
    If Healing is MORE THAN Damage:
    Healing - Damage = Overheal amount (2 bytes)
    Damage - Overheal amount = healer damage (2 bytes)
   
    If Healing is LESS THAN damage:
    Healing = Healer damage (2 bytes)

So what's the best way to compare a 1 byte number against a 2 byte number?

I don't think I can simply convert the healing to a 2 byte number. The high byte would always be 0, since the max healing potency is 173 ($AD). I'd always be comparing 0 to 0, 1, 2, or 3. I dunno, would that work?
I know exactly what I'm doing. I just don't know what effect it's going to have.

I wrote some NES music! Its a legal ROM file. - I got a Ko-Fi page too.

Disch

#5
Sorry... I'm jumping into this late and only read the most recent post:


It sounds like you are over-complicating this.

QuoteIf HP is one byte:

But HP isn't one byte, so this is irrelevant.  Making a special case and treating HP under 256 differently from other cases just adds more code and makes things much more complicated.  Try to treat everything as uniformly as possible.

If you have a damage/healing calculation routine that works with 2-byte values, it will ALSO work with "1-byte" values that are really 2-bytes with the high byte set to zero.  There is absolutely no reason to code two separate routines.

QuoteI don't think I can simply convert the healing to a 2 byte number. The high byte would always be 0

You can and you should.  And highbyte=0 is exactly how you should do it.

EDIT:

To elaborate... this is how you'd typically compare 2 two-byte values ("P" and "Q")  (high byte is +1):

LDA P+1         ; compare high bytes first
CMP Q+1
BCC Q_is_larger ; (no need to examine low bytes if these branches take, we already know which is larger)
BNE P_is_larger

LDA P           ; compare low bytes next
CMP Q
; at this point, C,Z tell you the result:
; Z=set if P=Q
; C=clear if P<Q
; C=set if P>=Q



If 'Q' is only one byte wide... you can start by doing the same thing, but use an immediate zero for the high byte:


LDA P+1         ; compare high bytes first
CMP #0          ;  high byte of Q is zero
  ; ...


And if you're clever, you should be able to see how to optimize that, since CMP #0 will never clear C, and won't change Z as set by LDA:


LDA P+1         ; high byte
BNE P_is_larger

LDA P           ; low byte (as above)
CMP Q
; ...


Also simpler logic for this would be:


amount_to_heal = strength_of_spell();
if(amount_to_heal >= caster_hp)
    amount_to_heal = caster_hp-1;      // don't kill yourself

temp = target_hp;                      // their pre-heal HP amount
target_hp += amount_to_heal;
if(target_hp > target_max_hp)
{
    target_hp = target_max_hp;
    amount_to_heal = target_hp - temp; // how much we actually healed
}

caster_hp -= amount_to_heal;

Jiggers

#6
No worries! Thanks for taking the time to help again. :D

Part of why I was over-thinking it... I originally wanted the empath class to be able to keep healing when their HP is 1; they'd simply stop taking damage at that point. I thought it would hinder the fun of the game if you had to stop and heal them before continuing to use magic (in the menu. In battle, that's the whole point!) Polled some friends, and they suggested to keep it "hard mode" for menu healing... So that simplified things a lot. Maybe.

So a few hours of fiddling with things later... This compiles without error!

MenuRecoverHP:
    LDY ch_ailments, X          ; get out of battle ailments for this character
    CPY #$01
    BEQ _MenuRecoverHP_Exit     ; if dead... skip to exit (can't recover HP when dead)
   
    CPY #$02
    BEQ _MenuRecoverHP_Exit     ; if stone... skip to exit

MenuRecoverHP_Abs:
    LDA hp_recovery
    STA tmp                     ; back up HP to recover by stuffing it in tmp
    BEQ _MenuRecoverHP_OverMax  ; JIGS - if hp_recovery is 0 (set by using a max potion), max HP
       
    JSR LongCall
    .WORD UseItemCoffee
    .BYTE $0F
    JMP MenuRecoverHPDoHeal
    ;; Coffee heals %25 of max HP

MenuRecoverHP_Magic:   
    LDA #1
    STA EmpathHealing
    LDY menu_caster
    LDA ch_curhp+1, Y
    BNE @CasterHPIsMore     ; High byte of caster health is 1, 2, or 3; therefore, the low byte
                            ; will be able to handle up to 255 of healing
   
    LDA ch_curhp, Y
    CMP hp_recovery
    ;BCC @HP_RecoveryIsMore ; no need to branch... gonna do it anyway
    BNE @CasterHPIsMore
   
    @HP_RecoveryIsMore:
    ;LDA ch_curhp+, Y        ; this would be 0, though... so pointless to do, right?
    ;STA hp_recovery+1       ; since this is currently always 0
    LDA ch_curhp, Y
    SEC
    SBC #1   
    STA hp_recovery         ; subtract 1, so healer doesn't die
    BNE MenuRecoverHPDoHeal
    JMP CureFamily_CantUse  ; cancel healing if its 0, don't waste MP!
   
    @CasterHPIsMore:
    LDA ch_curhp, X
    STA MMC5_tmp
    LDA ch_curhp+1, X
    STA MMC5_tmp+1

    MenuRecoverHPDoHeal:
    LDA ch_curhp, X
    CLC
    ADC hp_recovery             ; add healing potency to low byte of HP
    STA ch_curhp, X
    LDA ch_curhp+1, X           ; add 0 to high byte of HP (to catch carry from low byte)
    ADC #0
    STA ch_curhp+1, X
    CMP ch_maxhp+1, X           ; then compare against max HP to make sure we didn't go over
    BEQ _MenuRecoverHP_CheckLow ; if high byte of cur = high byte of max, we need to check the low byte
    BCS _MenuRecoverHP_OverMax  ; if high byte of cur > high byte of max... we went over
                                ; otherwise.. we're done...

  _MenuRecoverHP_Done:
    LDA EmpathHealing
    BEQ @Done
   
    LDA ch_curhp, Y
    SEC
    SBC hp_recovery
    STA ch_curhp, Y
    LDA ch_curhp+1, Y
    SBC hp_recovery+1
    STA ch_curhp+1, Y
 
   @Done:
    LDA #0
    STA EmpathHealing
    JSR PlayHealSFX
   
    LDA tmp                     ; restore the HP recovery ammount in A, before exiting
                                ; (for tent party healing)
  _MenuRecoverHP_Exit:
    RTS

  _MenuRecoverHP_CheckLow:
    LDA ch_curhp, X             ; check low byte of HP against low byte of max
    CMP ch_maxhp, X
    BCS _MenuRecoverHP_OverMax  ; if cur >= max, we're over the max
    BCC _MenuRecoverHP_Done     ;  otherwise we're not, so we're done (always branches)

  _MenuRecoverHP_OverMax:
    LDA ch_maxhp, X             ; if over max, just replace cur HP with maximum HP.
    STA ch_curhp, X
    LDA ch_maxhp+1, X
    STA ch_curhp+1, X
   
    SEC                         ; nonsense if using a potion, but never used in that case
    SBC MMC5_tmp+1
    STA hp_recovery+1
    LDA ch_curhp, X
    SBC MMC5_tmp
    STA hp_recovery
    JMP _MenuRecoverHP_Done     ; and then jump to done


I've yet to bugtest it though.

After figuring out how to edit .sav files, re-designing another menu screen, and re-organizing spell data for menu casting... The caster's HP loops back to max instead of capping at 1. Gonna see if I can figure this out.

Edit agaiiiin:

While its hard to say which of the silly things that was going on was causing trouble, I am 95% certain this is it. It gave me a scare by having the healer's HP go to double digits from 999 at random... but after half an hour of debugging and trying to heal the healer, then someone else, then the healer, then someone else... in different patterns... it basically stopped doing that and started working???

Lookit my proud little messy baby!

MenuRecoverHP_Magic:   
    LDA #1
    STA EmpathHealing
    TXA                     ; transfer patient character index to A
    CMP menu_caster         ; compare with character index of caster
    BEQ MenuRecoverHPDoHeal ; character is healing self; skip the nonsense
   
    LDY menu_caster
    LDA ch_curhp+1, Y
    BNE @CasterHPIsMore     ; High byte of caster health is 1, 2, or 3; therefore, the low byte
                            ; will be able to handle up to 255 of healing
   
    LDA ch_curhp, Y
    CMP hp_recovery
    BCC @HP_RecoveryIsMore ; NEED TO BRANCH
    BNE @CasterHPIsMore
   
    @HP_RecoveryIsMore:
    LDA ch_curhp+1, Y        ; this would be 0, though... so pointless to do, right?
    STA hp_recovery+1       ; since this is currently always 0
    LDA ch_curhp, Y
    SEC
    SBC #1   
    STA hp_recovery         ; subtract 1, so healer doesn't die
    BEQ EmpathCantHeal
   
    @CasterHPIsMore:
    LDA ch_curhp, X
    STA MMC5_tmp
    LDA ch_curhp+1, X
    STA MMC5_tmp+1

    MenuRecoverHPDoHeal:
    LDA ch_curhp, X
    CLC
    ADC hp_recovery             ; add healing potency to low byte of HP
    STA ch_curhp, X
    LDA ch_curhp+1, X           ; add 0 to high byte of HP (to catch carry from low byte)
    ADC #0
    STA ch_curhp+1, X
    CMP ch_maxhp+1, X           ; then compare against max HP to make sure we didn't go over
    BEQ _MenuRecoverHP_CheckLow ; if high byte of cur = high byte of max, we need to check the low byte
    BCS _MenuRecoverHP_OverMax  ; if high byte of cur > high byte of max... we went over
                                ; otherwise.. we're done...

  _MenuRecoverHP_Done:
    LDA EmpathHealing       ; if empath is not healing (healing with potion instead) skip this
    BEQ @Done

    TXA                     ; transfer patient character index to A
    CMP menu_caster         ; compare with character index of caster
    BEQ @DECMANA            ; if casting on self, skip this, but still decrease mana
   
    LDA ch_curhp, Y
    SEC
    SBC hp_recovery
    STA ch_curhp, Y
    LDA ch_curhp+1, Y
    SBC hp_recovery+1
    STA ch_curhp+1, Y
   
   @DECMANA:
    LDX mp_required         ; put mp required index in X
    DEC ch_magicdata, X     ; and subtract 1 MP from the proper level
 
   @Done:
    LDA #0
    STA EmpathHealing
    JSR PlayHealSFX
   
    LDA tmp                     ; restore the HP recovery ammount in A, before exiting
                                ; (for tent party healing)
  _MenuRecoverHP_Exit:
    RTS

  _MenuRecoverHP_CheckLow:
    LDA ch_curhp, X             ; check low byte of HP against low byte of max
    CMP ch_maxhp, X
    BCS _MenuRecoverHP_OverMax  ; if cur >= max, we're over the max
    BCC _MenuRecoverHP_Done     ;  otherwise we're not, so we're done (always branches)

  _MenuRecoverHP_OverMax:
    LDA ch_maxhp, X             ; if over max, just replace cur HP with maximum HP.
    STA ch_curhp, X
    LDA ch_maxhp+1, X
    STA ch_curhp+1, X
   
    SEC                         ; nonsense if using a potion, but never used in that case
    SBC MMC5_tmp+1
    STA hp_recovery+1
    LDA ch_curhp, X
    SBC MMC5_tmp
    STA hp_recovery
    JMP _MenuRecoverHP_Done     ; and then jump to done
   
EmpathCantHeal:
    LDA #33                 ; update this later
    JSR DrawItemDescBox        ; different description text ID
    JSR PlaySFX_Error
    JSR MenuWaitForBtn_SFX  ; Then just wait for the player to press a button.  Then exit by re-entering magic menu
    RTS
I know exactly what I'm doing. I just don't know what effect it's going to have.

I wrote some NES music! Its a legal ROM file. - I got a Ko-Fi page too.

Disch