Basic ASM/Hacking Zelda3 Tutorial

Table of Contents

1) Tools/Files Needed
2) Basic ASM Explanation
3) Understanding Original Menu Code
4) Snes Tiles/Tilemap Format
5) Making Custom Menu from B to Z


Tools needed for Zelda3 hacking
Lunar Address - to convert PC Address/Snes address
Asar - 65816 ASM Assembler
YY-CHR - GFX Editor
ZCompress or Lunar Compress - To Decompress/Compress GFX
SnesGFX - to create tilemap/gfx easily
Bsnes+
HXD - Hex Editor
Disassembly + Infos Japanese Disassembly - can also be helpful for US version (some codes are at different locations)



Basic ASM Explanation
65816 References / Wiki
Here we will go throught the "basics" opcodes for the 65816, Load Values, Change RAM, Conditions, Jump/Goto nothing too fancy or complicated
I will use references to modern programming languages, but knowing a programming language is not really mandatory
On the snes there is a thing called (A) Accumulator and (X,Y) Index Registers to "load" and "store" values in ROM/RAM also the ZERO flag
They are also use to do comparisons for conditions, it might looks complicated at first but this is actually pretty easy here's some basic opcodes :

LDA - Is used to (LD) LOAD a value into (A), it can be used on a ROM Address, Ram Address, Constant value, this is probably the most used opcode
ASM :
LDA #$08 ;Load a Constant value of #$08 in the (A)ccumulator
Pseudo Modern Language :
var A = 08; //Load value 08 into variable A


STA - Is used to (ST) STORE (A) value into a RAM address
ASM :
STA $1000 ; Store A value into Address $1000
Pseudo Modern Language :
var Address1000 = A; //set Address1000 variable on A value;


CMP - (CoMPare) allow you to make a comparison between (A)ccumulator and an address or Constant
ASM :
LDA #$08 ; Load Value #$08 into (A)ccumulator
CMP #$04 ; Compare #$04 with (A)ccumulator value IF Values are matching ZERO Flag is setted, otherwise it is unsetted like this example
Pseudo Modern Language :
var A = 08; //Set A on value 08
if (A == 04)


BEQ - Branch if Equal, IF the ZERO flag is setted it will branch to the location next to it
ASM :
LDA #$08 ; Load Value #$08 into (A)ccumulator
CMP #$04 ; Compare #$04 with (A)ccumulator value IF Values are matching ZERO Flag is setted, otherwise it is unsetted like this example
BEQ .matching
... ; Not Matching
.matching
... ; Matching

Pseudo Modern Language :
var A = 08; //Set A on value 08
if (A == 04)
{
//matching
}
//not matching


BNE - Branch if Not Equal, IF the ZERO flag is UNset it will branch to the location next to it
ASM :
LDA #$08 ; Load Value #$08 into (A)ccumulator
CMP #$04 ; Compare #$04 with (A)ccumulator value IF Values are matching ZERO Flag is setted, otherwise it is unsetted like this example
BNE .notmatching
... ; Matching
.notmatching
... ; Not Matching

Pseudo Modern Language :
var A = 08; //Set A on value 08
if (A != 04)
{
//not matching
}
//matching


JSL - Jump Subroutine Long allow you to go to a different portion of code in a different bank with the possibility of returning with RTL

JML - JuMp Long allow you to go to a different portion of code without possiblity of returning


the menu code is located in the file equipment.asm open that file preferably with a code editor like VisualStudio Code, Notepad++ etc...
get a pluggin for 65816 syntax i am using Visual Studio Code personally with the syntax highlight "65816 Assembly" by Josh Neta

So lets start trying to understand the menu code this is really well commented so it should not be that hard to find codes
we can scroll down a bit to the Local: section at line 34
; *$6DD36-$6DD59 LOCAL Local: { ; Appears to be a simple debug frame counter (8-bit) for this submodule ; Of course, it loops back every 256 frames INC $0206 LDA $0200 JSL UseImplicitRegIndexedLocalJumpTable dw ClearTilemap ; $DD5A = $6DD5A* ;00 dw Init ; $DDAB = $6DDAB* ;01 dw BringMenuDown ; $DE59 = $6DE59* ;02 dw ChooseNextMode ; $DE6E = $6DE6E* ;03 dw NormalMenu ; $DF15 = $6DF15* ;04 dw UpdateHUD ; $DFA9 = $6DFA9* ;05 dw CloseMenu ; $DFBA = $6DFBA* ;06 dw GotoBottleMenu ; $DFFB = $6DFFB* ;07 dw InitBottleMenu ; $E002 = $6E002* ;08 dw ExpandBottleMenu ; $E08C = $6E08C* ;09 dw BottleMenu ; $E0DF = $6E0DF* ;0A dw EraseBottleMenu ; $E2FD = $6E2FD* ;0B dw RestoreNormalMenu ; $E346 = $6E346* ;0C }

This section is the Main function of the menu what is gonna happen at every step basically, so at first when you press start it will start on ClearTilemap
then Init and then BringMenuDown, once it's done scrolling ChooseNextMode then NormalMenu that is the Main loop for the menu code
where you can move cursor around and pick items lets check the routine BringMenuDown first so Ctrl+F in your code editor search for "BringMenuDown:"

; *$6DE59-$6DE6D JUMP LOCATION BringMenuDown: { REP #$20 LDA $EA : SUB.w #$0008 : STA $EA : CMP.w #$FF18 SEP #$20 BNE .notDoneScrolling INC $0200 .notDoneScrolling RTS }

that is a very small routine, you should have a file called Zelda_3_RAM.log in the disassembly folder you can open that, that will help us understanding what is going on here
so first of all we have a REP #$20 that is basically telling the snes change the processor to read/write 16-bits (word) values from A instead of 8-bits (byte)
then we have a LDA $EA so we are loading the value of the address $EA in A to know what is in $EA we will check in the Zelda_3_RAM.log file and search for $EA
$EA[0x02] - (NMI)
BG3 vertical Scroll Register (BG3VOFS / $2112)

so this is loading the scrolling value of BG3 (menu), in (A)ccumulator nothing more next we have
SUB that is not an existing opcode it is a shortened opcode to combine 2 opcodes and should not be used real code here would be SEC : SBC.w #$0008
SEC will set the Carry you can ignore that for the moment, and SBC will Subtract a value from (A)ccumulator, so basically so far what that code is doing is
Load BG3 Vertical Scrolling Value in A, Subtract #$0008 from it, next we have STA $EA that is storing back the scrolling value -8 in $EA because A got 08 removed from it
CMP.w #$FF18 here we are doing a CoMPare to see if scrolling value == #$FF18 which is -232 in decimal,SEP #$20 we set back the processor mode to 8bits
BNE .notDoneScrolling then we Branch if Not Equal -232 to a RTS which is a Return function that will end the menu code for this frame
however when A is reaching -232 the INC $0200 code will be ran before the RTS, which is INCrementing by one the value of an address
$0200 is the position of the jumptable above so next frame we will be in the next subroutine which is ChooseNextMode


so here a small exercice lets say we wanna change the scrolling speed of the menu i will show how to do it with a Hex Editor and with ASM
if you check in the disassembly before the routine you usually have JUMP LOCATION that is the position where that code is located in the ROM
; *$6DE59-$6DE6D JUMP LOCATION BringMenuDown:
6DE59 so open up your zelda3 rom in a hex editor and make sure your rom is not headered you can use a header removal tool if you are not sure
SNES ROM Utility , All addresses in the Dissembly are PC Address and for Non-Headered ROM
You can use the tool called Lunar Address to convert PC Address to Snes Address and vice-versa, make sure it has the box Include copier header UNCHECKED
and set it on lorom 00:8000 (the first one) we'll come back to this in a moment so in the hex editor
go to the Address 6DE59 by using CTRL+G in the hex editor, you should see something like this :
hexeditor1 what you are seeing is pretty much just the actual game code in HEX
C2 20 REP #$20 A5 EA LDA $EA 38 SEC E9 08 00 SBC #$0008 85 EA STA $EA C9 18 FF CMP #$FF18 E2 20 SEP #$20 D0 03 BNE .notDoneScrolling (03 means the branch is going 3 byte further) EE 00 02 INC $0200 .notDoneScrolling 60 RTS
so lets try changing the SBC #$0008 into something else from a hex editor that is very simple all you need to do is edit the byte 08 to something else like 01
becareful tho the code is comparing with a specific value what that means is if you are not decrementing the value by where -232 is not divisible it will infinitely scroll
change that 08 in 01, save file with the hex editor, load your rom into your favorite emulator then press start to open your menu!
if you did it right the menu should scroll down very slowly like that gif
menuspeed
see that's super simple !! now lets do it in ASM
create a new file with your code editor call it main.asm
lorom ; This line is important to tell ASAR our ROM is in lorom mapping so it will write data to the right location in the ROM org $0DDE59 ; This is where ASAR will know where to write new data in the ROM, The SNES Address is Different than PC Address explanation below BringMenuDown: { REP #$20 LDA $EA SEC SBC #$00E8 ;Remove directly the 232 value STA $EA CMP #$FF18 SEP #$20 BNE .notDoneScrolling INC $0200 .notDoneScrolling RTS }

First let explain the org function that's used by ASAR to know where to write code/data in the ROM it's followed by an address
normally we would want that code written at the position 6DE59 since that's where we go in a hex editor to see that code but on the snes
the addresses are not mapped the same basically to keep it simple in a PC BANK you have 65536 (0x10000) bytes, on snes only 32768 (0x8000) bytes
so you need to convert that PC Address into Snes Address and we can use the tool Lunar Address for that with copier header checkbox unchecked and LOROM checked
you enter the PC Address on left side and it give you the SNES Address on right side screen here :
lunaraddress
IMPORTANT make sure your ROMs extension are .sfc, ASAR will not recognize properly .smc
ok so now that we have our ASM file we need to build it into the rom so we need to run ASAR on it there are multiple easy way of doing it
i personally put Asar.exe, VanillaROM.sfc, main.asm files in a same folder and use a .bat file with instructions like this
copy "VanillaROM.sfc" "Patched.sfc" asar.exe main.asm "Patched.sfc" pause
save that file as build.bat then all you have to do to build new code into your ROM is run the build.bat file!
if you are getting any error make sure all your files are in the same folder before running the .bat file the build.bat file also need to be in the same folder
what that code will do is copy the VanillaROM.sfc file into a file named Patched.sfc then ASAR will patch the file Patched.sfc with main.asm code
if you did it right you should have a file named Patched.sfc if you test that rom the menu should open instantly because we are removing whole value #$00E8
menuspeed2
Congratulations you just wrote your first ASM script ! it's pretty much just a copy of the original code but still ! Alright lets explains some things about that code
first of all you can't edit the disassembly directly in case you would be wondering, it is not in a build state, you need to do what we just did above locate code we want to change
put a org $Address where you want to insert new code at, write down new code or copy it from the disassembly (it won't always work) because sometime addresses are labels e.g.
    
; *$6DE6E-$6DEAF JUMP LOCATION ChooseNextMode: { ; Makes a determination whether to go to the normal menu handling mode ; or the bottle submenu handling mode. ; there's also mode 0x05... which appears to be hidden at this point. LDX.b #$12 LDA $7EF340 .haveAnyEquippable ORA $7EF341, X : DEX : BPL .haveAnyEquippable CMP.b #$00 : BEQ .haveNone ; Tell NMI to update BG3 tilemap next from by writing to address $6800 (word) in vram LDA.b #$01 : STA $17 LDA.b #$22 : STA $0116 JSR DoWeHaveThisItem : BCS .weHaveIt JSR TryEquipNextItem .weHaveIt JSR DrawSelectedYButtonItem ; Move to the next step of the submodule LDA.b #$04 : STA $0200 LDA $0202 : CMP.b #$10 : BNE .notOnBottleMenu ; switch to the step of this submodule that handles when the ; bottle submenu is up LDA.b #$0A : STA $0200 .notOnBottleMenu RTS .haveNone ; BYSTudlr LDA $F4 : BEQ .noButtonPress LDA.b #$05 : STA $0200 RTS .noButtonPress RTS }

If we try to copy that code into our main file convert the address 6DE6E to SNES Address 0DDE6E put an org $0DDE6E above and run build.bat
 
main.asm:25: error: (E5060): Label 'DoWeHaveThisItem' wasn't found. [JSR DoWeHaveThisItem] main.asm:27: error: (E5060): Label 'TryEquipNextItem' wasn't found. [JSR TryEquipNextItem] main.asm:31: error: (E5060): Label 'DrawSelectedYButtonItem' wasn't found. [JSR DrawSelectedYButtonItem]

we will get these errors, because these labels are not defined in our file we can fix that by finding these function in the equipment.asm file
search for DoWeHaveThisItem:you'll find it at the line 322
    
; *$6DEB0-$6DEBC LOCAL DoWeHaveThisItem: { LDX $0202 ; Check to see if we have this item... LDA $7EF33F, X BNE .haveThisItem CLC RTS .haveThisItem SEC RTS }

We can reference that address in our code without having to copy the whole section by creating a label at that address so ASAR will know where to go
6DEB0 convert that to SNES Address 0DDEB0, above the other code after the lorom line you will add
org $0DDEB0 DoWeHaveThisItem:

and that's it that function is now defined ASAR will know it need to Jump SubRoutine to that address now if you run build.bat you should only see 2 errors
repeat for the 2 other functions build and it will work, from there you can change that code like you want BUT there is a but... since you are writing
in the original rom location you have limited space like for example you can't add something in the middle of that code otherwise you will overwrite the code next to it
so if you want to add new code you need to replace code ! i will start explaining that code then we'll see what we can do with it
    
; *$6DE6E-$6DEAF JUMP LOCATION ChooseNextMode: { LDX.b #$12 ;LDX Opcode is almost like LDA where it loads a value into A but instead it is loading a value into X in that case it will set X on value #$12 LDA $7EF340 ;Here we load the value of the address $7EF340 into A the values 7EF are bit specials they are not in the RAM.log file ;because they are part of the SRAM (Save RAM) Save RAM is the RAM kept alive by the battery in the cartridge so it's not getting wipe on reset ;it can be accessed normally by using address $70XXXX but Zelda3 is using a file system where it store the SRAM in $7EFXXX and when you press ;Save & quit button it move that memory into $70XXXX, and when you start your console back and you load your save file content of $70XXXX is transfered into $7EFXXX ;here's a website where you can have all the SRAM Address descriptions SRAM Wiki ;so we're looking for address $7EF340 we need to ommit $7EF on the wiki so $340 ;that code is loading the BOW value .haveAnyEquippable ORA $7EF341, X : DEX ;Here what is happening is a bit complicated it will add values of every items in the inventory into A so Bow+Boomerang+Hookshot+Powder, etc... ;it's a loop of 12 (the value of X loaded above) BPL .haveAnyEquippable ;Loop until X == 0 (last opcode used is DEX) if X reach -1 it will set the Negative flag so until X > 0 it loop to .haveAnyEquippable label CMP.b #$00 : BEQ .haveNone ;Here if all items combined still == 0 then this is because we do not have any items in the menu so we go to .haveNone ;Otherwise here we have at least one item LDA.b #$01 : STA $17 ;<- check what is that value in ram.log file, description is complicated but we know it's used to update something in gfx LDA.b #$22 : STA $0116 ;<- check what is that value in ram.log file, same as above used to do gfx stuff ;that is a subroutine checking if we actually have the item we have selected in the menu JSR DoWeHaveThisItem : BCS .weHaveIt ;if we have it then move on to .weHaveIt JSR TryEquipNextItem ;otherwise check all the next items and equip the first one next to it instead .weHaveIt JSR DrawSelectedYButtonItem ;that code is drawing the item we have selected on the Y button LDA.b #$04 : STA $0200 ; that is increasing the menu state to ;NormalMenu LDA $0202 : CMP.b #$10 ;but wait if we are selected on a bottle BNE .notOnBottleMenu LDA.b #$0A : STA $0200 ;do that code here instead if we are selected on a bottle so it will go to menu state ;BottleMenu .notOnBottleMenu RTS ;Then return so next frame we'll be either on normal menu or bottle menu .haveNone ;That code is reached if we open the menu and do not have any item... code won't go any further it will loop infinitely here every frames LDA $F4 : BEQ .noButtonPress ;search for $F4 in ROM.log file ;$F4[0x01] - Filtered Joypad 1 Register: [BYST | udlr]. ;so since there's no condition on that LDA it is checking if the value of $F4 != 0 so any button on that list will trigger next code LDA.b #$05 : STA $0200 ;this code is setting menu state to UpdateHUD and UpdateHUD will swap to CloseMenu right after ;so basically what that code is doing if you press any of these buttons (Up, Left, Down, Right, B, Y, Start, Select) menu will update hud and close RTS .noButtonPress RTS }

To keep it simple what we will try to do here is add another condition that says if we press (A, X, L, R) open the bottle menu even if we don't have any items
so you check in ram.log again, oh it's $F5 ! that will be easy to do you can just copy the code above and go to bottle menu instead of updating hud and closing menu
but that's wrong you can't just ADD code in it since if original code is 6DE6E-$6DEAF 65 bytes long, adding your new code in will make it goes to ~75 bytes or so
and you will delete code next to it in that case it will delete the portion of code next to it :
    
; *$6DEB0-$6DEBC LOCAL DoWeHaveThisItem: { LDX $0202 ;will get deleted ; Check to see if we have this item... LDA $7EF33F, X ;will get deleted BNE .haveThisItem ;will get deleted CLC ;will get deleted RTS ;will get deleted .haveThisItem SEC RTS }

so the game will try to use that function it will not contains the code it should and the game will probably crash this is where the hooks come in play so we can add more code
somewhere else in the rom and run it from that location in the rom finding a good location for a good is not always easy and sometime you just don't have space to put a JSL
JSL is how you make a hook that will jump to a subroutine in the expanded rom or where you want, then it will run code and come back into original code once it's done with RTL
it take 4 bytes of space to do a JSL so you need to overwrite something you can easily restore in your subroutine that's a bit hard to identify as a beginner
but in that example we could remove the whole controller condition since we can easily restore it, so we'll do that we can just comment it out so we know what we replaced
    
; *$6DE6E-$6DEAF JUMP LOCATION org $0DDEB0 DoWeHaveThisItem: org $0DDEE2 TryEquipNextItem: org $0DEB3A DrawSelectedYButtonItem: org $0DDE6E ChooseNextMode: { LDX.b #$12 LDA $7EF340 .haveAnyEquippable ORA $7EF341, X : DEX BPL .haveAnyEquippable CMP.b #$00 : BEQ .haveNone LDA.b #$01 : STA $17 LDA.b #$22 : STA $0116 JSR DoWeHaveThisItem : BCS .weHaveIt JSR TryEquipNextItem .weHaveIt JSR DrawSelectedYButtonItem LDA.b #$04 : STA $0200 LDA $0202 : CMP.b #$10 BNE .notOnBottleMenu LDA.b #$0A : STA $0200 ; go to bottle menu .notOnBottleMenu RTS .haveNone ;LDA $F4 : BEQ .noButtonPress ;LDA.b #$05 : STA $0200 ;So we comment out these 2 line above, and we need to count the numbers of bytes they use again it's not easy as beginner but you can use hex editor ;go at position 6DEAF with your hex editor (that's the end of this section) that is commented at the top of the routine, from there slowly move backward ;search for byte $F4, F4 is the value loaded by the LDA so we go one byte further (A5) that's the LDA, then we count the numbers of bytes from there to the RTS (60) ;A5 F4 F0 06 A9 05 8D 00 02 ;We have 9 bytes so what we will do is add our JSL JSL NewControllerCode ;We add our Jump here ;it takes 4 bytes so there's still 5 bytes we can just NOP them out to prevent problem with them ;Where the RTL will bring us back NOP #05 ; next 5 bytes will turn into byte (EA) which is NOP (No OPeration) they will do nothing code will read it and skip it RTS .noButtonPress RTS } ;Then below here we are adding our new code for the button press in the expanded region org $218000 ;Set writing position in expanded region location NewControllerCode: { LDA $F4 : BEQ .noButtonPress ;Here we can restore the original code LDA.b #$05 : STA $0200 ;Here we can restore the original code .noButtonPress ;so we have restored the code we have overwritten we can now add new code for others buttons (the order depends on what you need) ;you could restore the code after your new code or before or not at all depending on your need LDA $F6 : BEQ .noButtonPress2 ;Lets just copy the original code but put a F6 instead of F4 ;We could add whatever else we want here as an example we can write data into the address $012E which will play a sound effect LDA #$30 ;Sound we will play STA $012E ;Address we write it in to play the sound .noButtonPress2 RTL ;We are returning to the previous code we came from with the RTL }

and voila this is how you make a hook you could add much more code in that section without overwriting existing code because your code is now in expanded region
you could do more experimenting if you want with that code by checking the ram.log file and searching for addresses that could be interesting to change
or the SRAM wiki which is a bit more interesting for single write changes like for example you could write value to the address $7EF340 that would give you the bow