So... you want to make a block that freezes Mario only when he's small? Or perhaps make a block that kills him if he has zero coins because you've played through SMW and well, there are no more gimmicks? Well... with ASM, you can easily create these type of blocks - ASM basically lets you do custom programming and add hacks to your rom. It's going to be fun to add custom stuff to your hack, isn't it!
This is a tutorial which I've been working on for a while.. it is meant to explain 65c816 ASM (used for creating / modding SNES games, such as Mario World) in an easy-to-understand way, and I've also gone in depth with details so that each and every bit of it becomes clear. Also, if you have any suggestions/questions, then feel free to post them here. This tutorial is meant to explain ASM as easy as possible, so with your help we can make people understand ASM easily and in a simple way. Also, I just want to state that you should be very patient and calm if you want to learn ASM. You can't just skim through this tutorial and become a know-it-all. ASM comes from experience too, so it's a good idea to practice the basics before moving on to the intermediate and advanced stuff. Also, I suggest you read a hex and binary tutorial first in case you are not familiar with it yet. If not, you'll be having a bit of trouble.
Well, are you ready? Let's get started!
Lesson 1: Loading and Storing
The first thing you should know is opcodes. A opcode is like an action given by the programmer which indicates what the code should do, like of it as an 'action command'.
First, we're going to learn about the Accumulator. The Accumulator is the place where things get stored and loaded. Think of it as a bank - you deposit and collect money from there (okay, that example might suck but it's the best I can think of yet..)
The first two opcodes we are going to learn first are the 2 important ones, LDA and STA. LDA stands for 'LoaD to Accumulator'. It's used to load a hex-value, decimal value, or RAM Address into the "Accumulator" - The place where code gets stored and loaded as stated before. So, how do we load a decimal-value? Simple, you just type "LDA" followed by '#' and then the value you want to load. E.g.:
LDA #20 LDA #70
It's also important to note that the Accumulator won't recognize odd-digit values. So, if you wanted to load the value '5', we would have to put a 05 instead:
LDA #05 ; load 5 into 'A' (Accumulator).
See? That was pretty simple? We're learnt how to load a decimal value. But, how would we load a hex-value? It's pretty much the same, except that we place a dollar sign '
$' after the hash '#'. It's also a good idea to use the windows calculator to convert decimal to hex and vice-versa. E.g, 20 in hex is 32 in decimal. So, if we wanted to load 20 (32 decimal), we would type:
LDA #$20 ; load the value '32' into the accumulator. ; Note that we are not actually loading the value 20, ; because the game takes the value as a hex value.
If we wanted to load 50 (in hex) into the accumulator, what would you type? Guess that one yourself... I hope it's clear by now. If it is, let's proceed to our next opcode: STA.
STA takes a value which was loaded, and stores it into where-ever you specify. So if we're loading a value, we must store it somewhere. Makes sense, right? Let's suppose we want to make a code that makes Mario big. We'll need to load a value that controls Big Mario and then store it to the current power-up state. How would we do this?
Well, look at the RAM Map. The address
$7E0019 controls Mario's power-up states. There are 4 possible values of
$7E0019 (well, there's technically 00-FF, but you'll just get random garbage if you use 04 and above.):
00 - Small Mario 01 - Big Mario 02 - Caped Mario 03 - Fiery Mario
Since we want to make Mario big, we'd have to load the value 1. You remember how to do this right?
Now... we need to store it to the RAM Address
$19. (We can use just
$19 because the "7E" is assumed almost always and the "00" is always assumed when you use only two digits for a RAM address.) A RAM Address is where you store your values, in this case,
$19 is a RAM Address that handles Mario's power-ups. Also, if you are wondering about the difference between a RAM Address and the Accumulator, the RAM Addresses are data stored in the ROM (game) for use, while the accumulator is just a place to store values. Anyway, let's continue. To make Mario big, we need to store the value
#$01 into the RAM Address
$19. How would we do that?
LDA #$01 ; load the value 1, which is Big Mario according to current power-up state. STA $19 ; store it to the RAM Address $19, which is the power-up state.
See how we stored the value 01 into
$19? To store a value to a RAM Address (
$19 in this case), we simply type STA followed by the address to store to. Since we aren't storing a value (which is obviously, stupid and senseless), we simply need to type
$address. This code works by storing the value 01 (Big Mario's state) into the RAM Address
$19, which is the power-up state. But wait.. there's more than that!
You can't just load and store like that. You have to "end" your code, otherwise the code will never stop being executed and thus, the game will freeze. We need to end our code, otherwise the game will crash. So, we need to learn another opcode for that, RTS - Return from Subroutine.
RTS is simply used to end most code. That's why we type it right after our code:
LDA #$01 ; get Big Mario's value (of $19) STA $19 ; into current power-up state. RTS ; and return (end our code here).
Now, let's do another example; a code that makes Mario small instead of big.
LDA #$00 ; load the value 00, which is small Mario (remember, we can't do LDA #$0). STA $19 ; store it to the power-up state.
And now, here's another simple command: STZ - Set To Zero. What this does is take a RAM Address, and makes it zero no matter what. This is also what we're doing here; we're loading the value '
#$00', and storing it to
$19. STZ also does the same, except that you don't need to load a value and simply type in
STZ $address to make it 0. So, to simplify the above code... we would do:
STZ $19 ; Set RAM Address $19 to 00 ; in this case, make the current power-up state 00, ; which is small Mario's state.
#$ = Hex value
# = Decimal value
#% = Binary value (We will learn about this in Part 2)
$ = RAM Address.
I guess that should be clear by now. So far, we've learnt that LDA can load a hex-value and decimal value (
# respectively), and STA can store it to a RAM Address. We can also load a RAM Address (such as
LDA $19), but we'll learn more about that in the next lesson. We've also learnt that the opcode 'RTS' is used to end a code. Also, I'm stating this once more: You cannot store a value (e.g.
STA #$20). You are supposed to store to a RAM Address and not a value. Storing to a value wouldn't make sense anyway, would it?. STZ is used to make a RAM Address become 0, no matter what. To use it, type
STZ $address, where address is the RAM Address (in our example, we used
$19, the power-up state) you want to become zero. So far, we've learnt 4 opcodes:
Yay, isn't that great? Let's move on to lesson 2 now, which may be slightly harder than loading and storing. (oh.. and btw, there are a ton of opcodes in 65c816 ASM, so don't be so happy yet).
Lesson 2: Technical Data
Like I said earlier, you can only load even-digit values. Loading a one-digit or 3-digit (e.g.
LDA #$5 or
STA $DBF) will NOT work. Instead, they should be
LDA #$05 and
Now, there are 3 ways you can load/store RAM Addresses (for this sample, I will use
Direct - LDA $xx Absolute - LDA $xxxx Direct - LDA $xxxxxx
As you can see:
You can load a 2 digit value (
LDA $xx), which is called directing addressing ; This is a 8-bit address.
You can load a 4 digit value (
LDA $xxxx), which is called absolute addressing ; This is a 16-bit address.
You can load a 6 digit value (
LDA $xxxxxx), which is called long addressing ; This is a 24-bit address.
You can do the exact same with
LDA #$xx and
STA $xx. Basically, we're loading the same address, but in different modes.
BIG NOTE: You can't STZ a 24 bit address (
STZ $xxxxxx), because the assembler (a program that converts our code to machine code, like [[xkas]]) will read it as another opcode! You will have to do
LDA #$00; STA $xxxxxx if you want to make a 24-bit address become 0!
These ways of loading and storing are called addressing modes. It's rather useful to know this, later on.
One other important thing to know is a bank. A bank is something like this
$02:xxxx. The first two digits are the bank number, in this case it's 02. So, if you look at the ROM Map, you can see that there's addresses starting with
$01:0001 are NOT the same RAM Addresses, because they're in a different bank (0 and 1). So, if the current bank is 02, and suppose we load
$01, the instructions at
$020001 will get executed. How do we change the bank, though? We'll learn that one in Part 2 of this tutorial, as it is not really required now.
Last thing in this lesson is where addresses are stored. Look at these addresses:
$0000 - $1FFF -> RAM Data. $2000 - $7FFF -> Hardware registers. $8000 - $FFFF -> ROM Data.
Normally you start addresses from
$0000, and end them at
$FFFF. From there onwards, the same thing repeats but the next bank starts. For instance:
$010000 -> Bank 1. $01FFFF -> End of bank 1. $020000 -> Bank 2. $02FFFF -> End of bank 2.
And so on. However, what exactly is this ROM/RAM Data and hardware registers? Well.. here's an explanation of them one by one:
RAM Data is basically all of the stuff you load and store. For instance, in the RAM Map, there are a bunch of RAM Addresses you can write and store to such as
$0DBF or whatever. Think of it as this - you can modify these addresses to your liking and can make custom codes from these addresses. RAM Data always in a bank from
$0000 and ends at
$1FFF. After that, there is no more RAM in a bank. However, there are still some RAM Addresses in a bank after
$1FFF. How do you load those?
Well, in those cases you have to specify the bank number as well. For instance, to load
$7E:C100, this would be wrong:
ROM Data is the area where all of the coding of the ROM goes. In Super Mario World, all of the data that controls sprites/level data etc. is stored in the ROM Data.
In a bank, ROM data starts from
$8000 and ends at
$FFFF. It does not begin earlier than that. You also cannot write/store to ROM, unlike RAM. That's why it's called Read Only Memory. You never load ROM Data, for example, this fails:
LDA $8008 ; FAIL! You can't load from ROM!
Hardware [[Registers]] are basically the technical components of the SNES. For example, all of your data is stored in a place called Video RAM (VRAM). You don't ever modify these components/data unless you know what you're doing. This stuff is slightly advanced, so we will not go through it in this tutorial. All you need to know is that there is no need to modify RAM or ROM data located in the range
$2000-$7FFF unless you have a good idea of how the SNES works. Besides, it is not really required to change data of here to create awesome ASM stuff.
Lesson 3: Branching and Conditions
Now, what if we wanted to make Mario become Fiery, only when he's small? I.e., only execute codes when certain criteria are met. That's where branching comes handy. We'll need to learn a few more opcodes for this.
First is CMP. - CoMPare. You can guess what this does - it compares a certain value with a RAM Address which you load into 'A'. You compare a value just as you do with loading -
CMP #$value. You know that we load a RAM Address by typing
LDA $address. In this case, we need to load RAM Address
$19 once again.
LDA $19 ; load power-up state CMP #$00 ; is Mario small?
But wait..! What do we have to do after comparing? We need to use other opcodes (commands) that will go to another piece code or "branch to another code" if the condition is met. Branching means to go to another piece of code when a criteria is met. We can branch if the criteria is equal, not equal, less, greater than etc. For now, we'll use an opcode that will branch if the value in 'A' is equal to the value we compared with. This command is called BEQ - Branch if Equal. We have to use it RIGHT after the CMP command. But, how do you specify where the code goes? We use symbols. A symbol can text written after the BEQ opcode, to indicate where to branch to. For example, we may write BEQ MakeFiery. The symbol can be any name, such as BEQ lolrofl, or BEQ superlong64code, as long as it does NOT have a space in between it, and that the the symbol we branch to has the EXACT same name (yes, capitals do matter).
LDA $19 ; load current power-up state. ; If Mario's big, we're loading #$01, if he's small we're loading 00, etc. CMP #$00 ; Now, we're comparing it to value #$00, which is Small Mario's state. BEQ MakeFiery ; BEQ branches if equal, and it will now branch to the code "MakeFiery". ; However, to identify this, we'll need to add a MakeFiery label in our code. RTS ; Return. If the current power-up state ($19) is NOT equal to zero, ; this code will be called instead. So if we're not small Mario, ; we're simply ending the code right here. MakeFiery: ; Otherwise, it Mario is small ($19 IS 00), then this code will be called. ; We're indicating this code by the use of the 'MakeFiery' symbol. ; This is how to are supposed to use symbols. LDA #$03 ; load value 03 of $19, which is Fire Mario's state. STA $19 ; and store it to the power-up address, making Mario fiery. RTS ; finally, return.
As you can see there, I first loaded the RAM Address
$19, the power-up states. Then I compared it to value 0, which is small Mario. If the condition was true (i.e,. Mario was small..), then it would branch to a code called "MakeFiery". Otherwise, it would end the code right after that.
Notice, that when typing the symbol, we must put a colon to indicate the symbol. It's necessary to make the assembler recognize the symbols. (In case you don't know, an assembler is a tool used to convert these codes into machine language).
So... this might have been a bit difficult to understand, but eventually you'll get it. Remember, we type a symbol name right after the BEQ, and then use that symbol to write the new code when branching.
There are several other branching commands too, other than BEQ.
BNE - This will branch when the value in A is NOT equal to what is being compared. (Branch if NOT equal). It's the opposite of BEQ.
BCC - This will branch if the value in A is less than what's being compared. E.g., if we're comparing
#$02, this will branch if the value in
$19 is LESS than 02, meaning 00 or 01.)
BCS - The opposite of BCC. This will branch when the value in 'A' is greater than what's being compared. E.g., if we were comparing
#$01, this will branch if the value in
$19 is GREATER than 01, meaning it will branch if it's 02 or 03 (caped or fire Mario).
BRA - Branch Always. Heh, this one branches ALL the time, regardless of values and such. This also means that it's senseless to compare and load... because it's branching all the time, meaning it's useless to load a value and compare it. So we don't need to use a CMP or LDA command before it.
There are more commands, but these are the ones which you'll need for now and are the most important ones.
Well, we've learnt how to compare and branch here. We compare a value by typing
CMP #$value, and use a branching opcode (
BCS etc.) right after it (on the next line). The branching command is followed by a symbol to indicate where the code will branch to (e.g., Nextlabel). Right after the BEQ symbol line, we type the code to be executed if the condition is not met.
Then, we type the "Nextlabel" (or whatever symbol name you wrote) symbol, followed by a colon: (e.g. Nextlabel:) to indicate the next code when the branching condition is met. Here's another sample code to make it a bit clear:
LDA $19 ; load power-up state CMP #$01 ; compare to it '01', which is Big Mario... BNE NotBig ; If Mario isn't big, we'll branch. We've used the BNE command here. RTS ; otherwise, if he is big, we'll return and end our code here. NotBig: ; If Mario isn't big.. we'll call this code instead. LDA #$03 ; Otherwise, if he isn't big... load value 03... STA $19 ; and store it to the power-up state. ; Since value 03 is Fiery Mario, we're making Mario fiery.
See how it works? Branching should be clear by now. If it isn't, I suggest you read through the lesson once more and try to understand it... it shouldn't be too difficult.
Lesson 4: Math Commands
What if you wanted to added a certain RAM Address... by one? or decrease it by one? We'll need to use some math commands here - INC and DEC. You can easily tell that INC will increase a RAM Address by one and DEC will decrease a RAM Address by one.
How do we use these though? It's pretty straightforward:
INC $0DBF ; increase the address $0DBF by one, which is the coin counter. ; It'll increase by one. RTS ; return and end the code here. DEC $0DBF ; decrease the coin counter by one. RTS ; return and end the code here.
INC and DEC do not work for long (
See? You just write down the respective opcode, followed by the address to decrease/increase. It's also important to state here that unless you write some code to execute these operations every second or so, this code will be executed every frame. That means, for every frame, the coins will increase by one. It isn't a good idea to do so. So you should always make the code INC/DEC every "XX' frames or when using a branching command.
tl;dr, INC and DEC by default increase a RAM Address by one every frame.
BIG NOTE: INC and DEC, much like STZ, won't work with a 24-bit address! (E.g.
INC/DEC $xxxxxx), because the assembler will read it as another opcode! You'll have to do
LDA $xxxxxx; INC A; STA $xxxxxx instead.
However, it's also important to say that if a RAM Address is being decreased when it is
#$00, it becomes
#$FF. And vice versa; If a RAM Address is being increased when it's
#$FF, it'll go back to
#$00. Always remember this. Here's a short example:
LDA $0DBF ; imagine that $0DBF has the value of 00 right now. DEC A STA $0DBF ; you might think it'll become -1, but really it goes back to FF. ; When it's FF, it'll go backwards again!
But.. sometimes, we want to increase RAM Addresses by a certain amount; not just by one. We'll need to learn two more opcodes for this - CLC and ADC.
Let's say we want to increase the coin counter by 32. This is what we'll have to do:
LDA $0DBF ; first load the coin counter.. CLC ; we always put this before the ADC.. it goes by itself. ADC #$20 ; amount to add. Since we want to increase it by 32, ; we'll need to load 32 (20 in hex = 32). STA $0DBF ; store the new result. It's important to do this; ; if you don't store the new amount to the counter, ; you can say that the code will do nothing at all. ; It's important to store it, so remember that. RTS ; return and end our code.
So.. see how this works? First we clear the "carry" flag, by typing CLC (CLear Carry.) Then RIGHT after the CLC, we type
ADC #$amount. After that, we store the new amount back to the RAM Address. It's very very important to do this, I repeat... nothing gets stored. (We use CLC because the ADC command will add number you choose + (1 if the carry flag is set.) so we always clear the carry so we don't end up adding one more than we want.)
What if we want to subtract by a certain amount (let's say, the same, 32). Well just have to use
SBC #$xx instead of
LDA $0DBF ; load coin counter SEC ; this goes by itself.. SBC #$20 ; subtract 32 (20 in hex = 32 decimal).. STA $0DBF ; and store the new amount to the coin counter. RTS ; return and end the code here.
For multiplication, we use the ASL command. We use it like:
LDA $0DBF ; load the coin counter ASL A ; multiply the amount by 2 STA $0DBF ; and store the new amount. RTS ; return and end the code here.
LSR actually work is that they don't multiply/divide by two - they affect the binary (the 0s and 1s in the value when converted from hex) by one which basically does the same thing. If you convert your hex value to binary using a calculator (use the Windows one.), and the same value with an
LSR applied, you'll notice that there'll be an extra one/zero. This is how
LSR actually work).
Pretty easy, isn't it? All you have to do is load the RAM Address, type
ASL A (multiply A), and store the new amount. For dividing, it's exactly the same except that we use
LSR instead of
LDA $0DBF ; load the coin counter LSR A ; divide the amount in the counter by 2. STA $0DBF ; and store the new amount. RTS ; return and end the code here.
Well, those commands should be very easy to understand.
INC will increase the value in a RAM Address by one, while
DEC will do the same, except decrease it, not increase it.
You can add and subtract by more than one by using the
SBC commands. You load a RAM Address, type
SEC (for adding and subtracting respectively), followed by a
ADC #$xx or
SBC #$xx (
ADC for adding, and
SEC for subtracting). Lastly, you store the new amount back to the RAM Address (
STA $Address), and end your code.
ASL with multiply the value in a RAM Address by two, while
LSR does the opposite. You use it like this:
LDA $RAM ASL A (or LSR A) STA $RAM.
Pretty simple, isn't it? If all is clear, you can proceed to Lesson 4, where we will learn about jumping to subroutines.
Lesson 5: Jumping
Now, say for example... you want to write the same code multiple times, but you think it's a big waste of time writing about 50 lines of code again and again. Now, this is where jumping comes handy. There are 4 jumping opcodes:
JSR: Jump to Subroutine. JSL: Jump to Subroutine Long JMP: Jump (Like BRA). JML: Jump Long.
JSR does is jump (or let's say, branch) to a certain code (which we call a subroutine) and once an
RTS (return) opcode is hit, the code after the
JSR gets executed. Similar to branching commands (
BNE etc.), you can use a symbol after the
JSR. However, you can also write down an address after it too, as we can jump to any location (SNES Address) of the ROM. If you look at the ROM Map, you can find a bunch of SNES Addresses. You can
JSR to them, too incase you want to execute a code from there. Here's a short example of
LDA #$02 ; load the value "02" JSR Code ; here, we are branching to the "Code" subroutine. RTS ; after the subroutine, return. Code: STA $19 ; store #$02 to the accumulator. Note that we loaded it in the beginning. RTS ; return.
So what this code is going to do is load the value 2, and then branch to the code subroutine - a routine (piece of code) inside a routine. When it touches that RTS after the STA $19, it'll follow the code after itself - the RTS in yellow will be executed next.
Small summary of what JSR does: A JSR will jump, or branch to a certain symbol in the code, and will execute the code after it once an RTS is hit. Like branching commands, you use a symbol after it. You can JSR to an ROM address too.
Find it a bit difficult? Here's another example:
JSR Sub ; go to the symbol 'Sub'. ; Once an RTS in the code below 'Sub' is hit, ; the instructions right after this line will be executed. LDA #$01 ; Now, once the code below 'Sub' has been executed, we're STA $19 ; I'm sure by now you'll know what this code does. RTL Sub: LDA #$08 ; load value 08.. STA $00 ; store it to RAM Address $00. RTS
Well... this is pretty simple to understand. First we are JSR'ing (yeah I made that word up..) to a routine called Sub. Once the
RTS there is hit, the instructions after the
JSR opcode will be executed. (Oh, and I hope this example was okay.)
Let's use an example for
JSR $address now. Also, note that this is actually ROM Address, not RAM. So, we'll look at the ROM Map. JSR'ing to a RAM Address wouldn't make sense would it, as all routines are stored in the ROM, not RAM.
Suppose, we want a piece of code that executes the cape ground smash effect. Rather than typing the entire code for that, we can just JSR to that address (i.e. JSR ROM address). Here's how we would do that:
116C1 $02:94C1 52 bytes Subroutine Cape Mario smashes ground subroutine.
This piece of information tells us that the cape smash effect is located at the address
$0294C1 (Ignore the colon, that's for just organizing). So, we simply need to JSR to that.
JSR $0294C1 ; execute the code located at ROM Address $0294C1 RTS ; and then return.
I think that becomes clear now. You can JSR to a ROM Address, and also to a label in your rom.
Then, we have an opcode called
JSL which works in the EXACT same way as
JSR, except that we use an
RTL to end the code rather than an
RTS. Using an
RTS with a
JSL would crash the game.
Here's something cool about a
JSL compared to
JSR - a
JSR has a small "branching length". This means that it can branch to a code no more than 128 bytes away. For instance:
JSR Code ...128 bytes of code here. Code: RTS
The Code label is too far away (128 bytes distance), so the
JSR wouldn't branch that far and would report an error. For these type of cases, it's best to use a
JSR has a long branching length of 8000 bytes... a lot, isn't it? That's like, 500 lines of code in between, compared to about 30/40 or so with
JSL works in the same way as
JSR; you can
JSL to an address or subroutine, but then the subroutine MUST end with an
RTS. The opposite goes for
JSR in case you still don't get it. Here's the sample code I used earlier for the
JSR, but this time with a
JSL Sub ; We're JSLing to the subroutine called Sub. LDA #$01 ; Now, once the code below 'Sub' has been executed, we're performing this code. STA $19 ; I'm sure by now you'll know what this code does. RTS ; RTS. Note that this isn't the subroutine, so it can be an RTS. Sub: LDA #$08 ; load value 08.. STA $00 ; store it to RAM Address $00. RTL ; and Return. See how I can use an RTS to end the code, but not here?
When you JSL to something, you end with
But then, what's the point of
JSR? Well, there are already some subroutines (for instance, the cape subroutine one) with already end with an
RTS. You know that
JSL'ing to them will cause the game to crash.. so we must we
JSR in those cases instead.
Now... moving on to
JML jumps to a certain address in the ROM, much like
JSL $Address. You cannot
JML to a symbol (E.g.,
JML Code) as it is used to jump to an address.
JML $00F606 ; jump to the code at ROM Address 00F606.
JML will ignore everything that's written after it. So...
JML $00F606 ; jump to this code of the ROM (guess what this routine handles) STZ $0DBF ; This gets ignored. LDA #$02 ; So does this. STA $19 ; This one too. RTL ; And the RTL also gets ignored.
JML will jump to a certain address of the ROM, and ignore everything after it. A
JMP is very similar to a
About the length of a
JML - both of them have a range of 8000 bytes, so you shouldn't worry if you have a lot of code between the
JML (addr/label) and the label itself.
JSR can jump to a symbol in a piece of code, and executes the instructions after it once an
RTS is hit. A
JSR can also jump to a ROM Address:
JSR $0094C5 ; jump to this subroutine RTS ; and return, of course.
JSL is very similar, except for one thing: You have to have an
RTL to end the subroutine, not
JMP jump to a location of the ROM, like
JSL $Address and
JSR $Address. However, the difference is that instructions after the
JML $Address and
JMP $Address are ignored. So...
JML $00F5B7 ; jump to this code. LDA #$04 ; IGNORED. STA $00 ; IGNORED.
But, there's also a difference between
JML can jump to any ROM address in any bank, where as
JMP as a bank limit. You can only jump to an address in the current bank with a
JMP. Some rules of JMP:
JMPignores everything after it.
JMPjumps to an address and has a limit of branching as far as up to x8000 bytes. This means that you can have 7FFF bytes of code in between the
JMP[label] and [label], and the code will actually branch that far.
JSR does not follow these rules.
Well, that's it for Part 1! If something isn't clear, don't hesitate to post it! We can always help!
[[ASM Tutorial Part 2]]: be sure to read that if you want to build up your ASM knowledge!