In this section of the website, we'll analyze the code of the original Chinese release of the game.
Hopefully, one day, we'll have a fully reverse engineered version of the code. I will be labelling the different memory addresses and sub-routines of the game,
so we can have a full understanding of how it all works.
I'm doing this mainly because I've tried playing through the game without cheats myself, but the obscurity of the game's mechanics and lack of documentation
made me want to dwelve deeper into the game's inner workings, and, considering I do have some experience with the 6502 and other machines, similar to
the NES, I figured I'd give this a go.
This is where the game keeps track of the current health of every playable character.
Each one is 16-bits long, meaning you can have a theoretical maximum of 65535 HP, although
the game only displays the upper 4 digits of the characters' health, both in battle and in the menus.
The lower bytes of the characters' health are stored in addresses $604A to $6050 (which, I've decided to label as PlayerCurrentHPLowerByte), while the upper bytes are stored at addresses $6051 to $6057. Those I've named as PlayerCurrentHPHigherByte.
Here's a handy table that covers each playable character and the memory locations of their HP:
Character | Lower byte | Upper byte |
---|---|---|
Cloud | $604A | $6051 |
Barret | $604B | $6052 |
Tifa | $604C | $6053 |
Aeris | $604D | $6054 |
Red XIII | $604E | $6055 |
Cait Sith | $604F | $6056 |
Cid | $6050 | $6057 |
--------SubstractHealthFromPlayer--------
SubstractHealthFromPlayer:
D3EE AE 1E 6F LDX SelectedPartyMember
D3F1 BD 3C 60 LDA WhichPartyMember,X
D3F4 AA TAX
D3F5 BD 4A 60 LDA PlayerCurrentHPLowerByte,X
D3F8 38 SEC
D3F9 ED 0C 06 SBC CurrentDamageToBeDealtLowerByte
D3FC 9D 4A 60 STA PlayerCurrentHPLowerByte ,X
D3FF BD 51 60 LDA PlayerCurrentHPHigherByte ,X
D402 ED 0D 06 SBC CurrentDamageToBeDealtHigherByte
D405 9D 51 60 STA PlayerCurrentHPHigherByte ,X
D408 90 0A BCC SetPlayerHPToZero ;If the player's HP has fallen below zero...
D40A BD 4A 60 LDA PlayerCurrentHPLowerByte ,X
D40D 1D 51 60 ORA PlayerCurrentHPHigherByte ,X ;If the player's health is exactly equal to zero...
D410 F0 0A BEQ $D41C ;Don't do anything else and return...
D412 38 SEC ;Set the carry flag so we're sure the player DIDN'T die...
D413 60 RTS
----------------
SetPlayerHPToZero:
D414 A9 00 LDA #$00
D416 9D 4A 60 STA PlayerCurrentHPLowerByte ,X
D419 9D 51 60 STA PlayerCurrentHPHigherByte ,X
D41C 18 CLC ;Clear the carry flag so we know the player has died
D41D 60 RTS
----------------
This sub-routine is called at the end of each enemy's attack during combat, to actually substract the corresponding damage to the target player.
In order to figure out which player character to attack, we first load the value located at memory address $6F1E (which I've labeled as SelectedPartyMember),
which holds the target position the enemy has selected, into the X Register.
Now, we will load the value located at WhichPartyMember ($603C), using the X Register as the offset, into the Accumulator.
This value basically tells us the offset of the party member current health value that we'll want to load (remember that those are stored as 2 tables of 16-bit numbers representing
the current health of every party member in the game).
As we transfer that to the X Register to afterwards use as an offset, we load the lower byte of the targeted player character's health into the
Accumulator, set the Carry flag, and then substract from this value the lower byte of the CurrentDamageToBeDealt ($060C), and store it back into the corresponding player health
location.
We will now do the same for the higher bytes.
Afterwards, we will branch to SetPlayerHPToZero ($D414) if the Carry flag is set to zero, which is only possible if we've lost all our health and overflowed it.
If this was the case, over at SetPlayerHPToZero we would set both bytes of our health to zero, and then exit out of the sub-routine.
If it wasn't, now we'd ORA both of the player's health bytes, and if the result of this operation is zero, that means we died, and we will branch to address $D41C, where
the Carry bit will be cleared, which will let the parent sub-routine know that we died, and exit the sub-routine.
If our health was not zero, then we would simply set the Carry bit (NOTE: I'm pretty sure this is unnecessary, as the only way to reach this part of the code is for the Carry bit to already be set)
and exit the sub-routine.