Assembly Language Tutorial

Any information you would like to share or ask in regards to the hacking of Legend of Legaia
Post Reply
User avatar
meth962
Level 23
Posts: 266
Joined: Tue Apr 03, 2012 8:37 am
Location: USA
Contact:

Assembly Language Tutorial

Post by meth962 » Thu Mar 05, 2015 10:34 am

So I was bored this morning and decided why not start a little tutorial on how to do the assembly or game hacking. I know some have asked for this so maybe it's about time! This will just be what I have learned by myself so it might not be the greatest!

Chapter 1 - Tools
First we're going to need a couple things to be able to apply what you learn. Otherwise you can skip this part.

PSX - this is the emulator of choice in debugging. It allows you to browse the memory, assembly code (game code), dump video memory, and most importantly....set breakpoints on memory read/write!

ArtMoney - this is a free program for scanning/searching memory of programs and is optional. You may need something to search the memory to find what you're looking for and I use this. It's a shame PSX doesn't come with one!

Calculator - need something for converting hex and adding values and shifting bytes. Windows calc works just fine.

That's about it. Oh, also I keep a notepad or notepad++ window open for notes. You'll need that when you're trying to make sense out of machine code! It's not like regular code which is actually legible :(

PSX Tour
I'll describe some of the basics on how to use this emulator for debugging purposes. That way you can get use to the feel of it when you actually try your own debugging.

Start the emulator and run Legaia first of all.

Debug Window
Then to open the debugger, go to Debug > r3000. This opens your debug window which you will work out of, slide it to the side of the game. You'll still want to have the game visible at times.
I can't remember which windows are open by default, but I would recommend going to 'window' and opening Assembler, registers, memory, and breakpoints. You can organize things how you want, but you'll want the assembler and register windows to be visible together when you use them. So you can make the two windows split the space, and then memory you can have full screen whenever you want to see it and switch. For me, I have a narrow assembler window, then a narrow registry window, then the bulk of the space is memory. I like to see all three at once. Breakpoints you don't need to constantly see so put that anywhere.

Memory
Everything starts with memory, and all the answers will show through it as well, so you need to get comfortable here. To full understand this part, go ahead and load a game (so long as you're not on the title screen).

To get to a specific address while in the memory window, press Ctrl+G and a window will pop up.
Enter "0x84140" into the input box and hit enter.

This will take you to the 84140 address of the playstation. This happens to be the start section of the save file in memory. All your important game variables are stored starting here and continuing for the next 8000+ bytes. In fact, what you see below will be an exact copy of what your save file looks like on your memory card (if you open it up in hex of course). Pretty neat huh?

So a couple things to note, you MUST provide a hex number for your memory address by putting "0x" in front of any hex number. Like my example for 84140.
Also, you can follow gameshark notations as well. If some code said 80084140, you can actually "go to" that 0x80084140 and the emulator is smart enough to take you to 84140. This is interesting because there is no address at 80084140. It would be a loooooong way away but the emulator interpets the address correctly.

Guess what? In the memory window you can just edit away all you want. Let's go edit your items. In game, open the items in menu so you can see your list of items, then switch to the debugger.

In the memory window, Ctrl+G (go to) the address 0x85958. Now you will see a bunch of hex numbers which represent your inventory. The first number is the "id" of which item is in that slot, and the second is the quantity of that slot. Go ahead and change the number at 85958 or the next one to anything you want. Don't worry, you can't break it, pick something random! As you do this, watch the game window which will be continually running the game. As you change the memory you will see the item or quantity change immediately in the game. This is due to how games work in general. When the playstation is running the game, even in the menu, it is constantly redrawing the screen. You might think "nothing is changing on the screen though", but it doesn't matter, it has to repaint every frame. To do that, it must read the memory for your inventory, to find out what is where and how much, and draw the name on the screen. It does this 60 times per second. Because of this, you might see some memory values changing constantly. You won't see them change 60 times a second though because the emulator program is only refreshing the memory for you every half second or so. And to prove that and bring up the next bit.....

Breakpoints
This is the cornerstone method of how you find out the answers. You need to find where the game code is for something in the game and the way you find it is breakpointing a certain memory address. Now that we know where the inventory is, let's breakpoint when the game reads or writes to it.

Go to your breakpoint window and right-click inside of it to add a new breakpoint for memory.
Enter 0x85958 in the address. Tip: you can see the address value of any 'cell' you click in the memory window. It will show in the title of the memory window.
Leave the size at 0x00001. You can write normal decimals like "1" isntead of 0x000001 here so don't worry, it just will replace your values with the ugly hex ones.
Now make sure "Read" is checked and "Write" is unchecked. We don't really care what is writing to it at this point.
Click OK.

You will immediately see the debugger and game pause. That means you've successfully hit your breakpoint. The bottom status in the window will say "Break on 0x85958 read". Well lookie that, something's reading that inventory slot! Take a look at the assembler window. The playstation just executed code to read that memory, so the next line will be ready to execute, but paused. You'll see this notated by a ">" arrow to the left of the code address. Dont' worry too much about this confusing window, I'll cover how to read it all later. For now I just want you to see what it looks like and how to get there.

You can make the playstation execute one line at a time by the F7 key (or go Debug > Step). Without knowing too much on what it's doing, you'll see the execute line (notated by the arrow) move from line to line, and you will likely see the registers change values too. This will all come into play later. To let the playstation to continue as normal, hit the F9 key.

Wait, it paused again. See, it's still reading your inventory for the next frame, or possibly reading it again for some other purpose. Go back to your breakpoint window and delete your breakpoint, or change it to "Write" and not "Read". I like to do that if I don't expect the game to write the address, and that way I can "save" it essentially and toggle it later. Once you do that, the game will still be paused, so hit F9 anywhere in the debug window to let the game to continue. Now it should be running constantly. Proven by lovely Legaia music.

Assembly Window
Not going to cover much here at the time because you still need to know both hex and assembler, but I will point out that you can use the same Go to (Ctrl+G) here like you did with the memory. This will be good for when you take notes and write down the address of where some code is. You can just jump back to it to read it. Try it if you want.

That concludes the tools tutorial! Next I'll cover how to find your memory addresses with Art Money. But if you already have that handled, skip to Chapter 3 for assembly!

-- Thu Mar 05, 2015 11:06 am --

Chapter 2 - Find that memory!
So after you're comfortable with the debugger, the first step in finding answers is to know where to start looking. This is like the mob interrogating known associates. You need to find the value in memory that is being affected so you can breakpoint it. Then again, if you already have that memory address thanks to a gameshark code or someone else, you can skip this part.

Unfortunately PSX cannot do that for you so will I use ArtMoney to demonstrate. It does not matter what tool you/I use because the methodology is going to be the same.

1. We scan the memory for an unknown value or search for a specific value.
2. We rescan those results to see if they change/do not change/increase/decrease.
3. Repeat 2 until the results are small enough to look at.

This is your basic and classic gameshark method. Find that value, write a code for it. Since we're on an emulator though, we'll have the advantage of save states to help.

If you download ArtMoney, you should have the newest version and it will come built in with emulator offsets. This is important because ArtMoney scans the emulator's memory which gives different memory addresses than the playstation would. The playstation only had 2MB of RAM. Your computer is going to have a lot more. When your emulator starts up, it uses a bunch of memory itself, and then needs to set aside 2MB of RAM for the playstation to emulate it's hardware. In doing so, the memory address isn't going to make sense. However, most emulators are smart and set a specific address to store the playstation's memory which makes it easy for tools like ArtMoney to scan and "translate" that address to what the actual Playstation address would be.

Select your process in ArtMoney to be the pSX emulator. Click search.
In Address Range, select "Emulator", then "Sony Playstation", then your version of the emulator. Now ArtMoney will give you the correct playstation addresses. It is also very helpful because it will limit the searches to only the areas that the emulator uses for the playstation. This is BIG since that means only scanning 2MB of memory opposed to hundreds of MB that the emulator would use.

You can select your type of value you are searching for. If it's a number, the default should be just fine. It searches for the common number types. The only thing you would need to switch for would be a text string or such.

So basic knowledge here. You will find a myriad of addresses when you search for something in a game. 2MB of RAM might not sound like much to you, but that's over 2 billion bytes. So the more unique your number is (and large), the less results you are likely to find. For instance, if you search for 10 or less for your quantity of some item in your inventory, you're going to find a LOT. Sometimes you can't avoid that, but if you search for your money (G) near the end game, you should have very unique and large number. You might even just find one result!

So the methodology you use is pretty similar, it's just how you start. If it's a number you can see, you can search for exact value. Then filter based on if it changes or doesn't change. Sometimes your number doesn't even need to change. Remember that the playstation does a lot of work in only a couple frames. Some of the numbers that you find can be changing so quick, that if you hit filter again without any changes, the results will narrow more and more.

There's also unknown values. Like the fishing minigame. If you want to find that tension bar, you have no idea what the numerical value that represents it. That's when you do an "unknown value" search. It essentially scans all memory and waits for changes. You can specify those changes as "changed" (unknown amount), "unchanged", "increased" (unknown value), "decreased" (unknown value), "increased by X" (known vlaue), "decreased by X" (known value).

You basically keep playing this game until you narrow down your results as much as you can. Also be warned that sometimes one value can be represented in multiple locations in memory. For instance, you might think Vahn's HP should only be in one location, but later find out that the game has a temp HP, max HP, current HP, in-menu HP, and a on-field HP which displays if you stand still on a map. Not to worry though, these are usually all linked together and fed from one. You find the right one and change it, they all will propagate. You'll notice if you find the wrong one, the game will immediately overwrite what you did.

So remember Search first, filter, filter, filter, filter.
You can pause the game to help from changing values.
Also, remember you can force change the values to help filter too! Go spend some money, or take HP damage, anything to change the value you're looking for because exact value filters will narrow it down even quicker.

Once you have a narrow list of memory addresses, you can take a look at them and watch them or change them to figure out if they're right. When you get one you think is right, you have to get the playstation's memory address from ArtMoney. In your list, double-click in the Address field and you will see the address change to what looks like a gameshark code.

Before: 0094DFD8
After: 80001FB8

The gameshark-looking one is the one you want. You can now go to that address in the PSX emulator. Remember you can use the 0x80 or omit the 80 and they both will work.

Once you're sure you have the right address, you can start debugging assembly code. So next section will be all about assembly.

-- Thu Mar 05, 2015 12:35 pm --

Chapter 3 - Assembly
This is it, what you came here for. If you have the sanity to make it through this, you are one more step towards becoming a robot yourself. You will speak machine code! Well it's actually just electricity following paths around a circuit but let's not get into that!

The gravity of the situation is it doesn't matter what language code is written in. Applications, browsers, operating systems - all written in different language but they all have to be broken down to the most simplistic instructions for a computer processor to "understand". Now different chips have different instructions, and the playstation's cpu was designed to do a smaller but very specific set of instructions while your computer's CPU is meant to do much more. The idea is generally the same though.

To grasp the simplicity of what we do in assembly, you need to know how the playstation cpu behaves. All it does is read a huge instruction list into memory (the game code) and follows them. All the instructions tell it to do is to play with numbers and store them in memory, or in registers. Think of registers as a few small pieces of internal memory for the CPU to use. It has to do a bunch of math (poor thing) and to do that, it will take a few numbers out of memory and store them in one of the 32 registers it has available. Think of this scenario:

Grab Vahn's HP from memory, store it in Register 2.
Grab Level up HP from memory, store it in Register 3.
Add Register 2 and 3 together, storing the result in Register 2. (so basically adding it to itself)
Store Register 2 back in memory. - Done-

Real simple steps, but also takes more steps than you would to just add a number to vahn's hp.
Now these steps are coded by some person, and are translated into assembly. Their code would have been more legible and written in C. The assembly code is more raw and looks hard coded.

So where does it know where these addresses are in memory? Like how does it know where Vahn's HP is?
You might be shocked, but they will be hardcoded. The code will say pull it from memory 0x8480C. That number is just in the code because it knows that is where Vahn's HP is always going to be stored. However, due to code compiling, you will see a couple tricks. It is more common for you to see a command like this:

Grab Vahn's HP from memory address 0x84800 plus 0xC. This gives you the address 8480C added together.
They do this because they will store the address where all of Vahn's stats start, say 0x84800, and then they add the offset of the stat they want. If they know HP is offset by 12 (which is C in hex) then they add it to the value. With that in mind, let's start learning the syntax!

Let's look at one made up example of assembly code as it would appear in the emulator.

0x0019eab: lb r2,0x0010(r3) : 9382000B

It looks crazy, but don't worry, we're going to trim it down by 2/3rds. Cut out the right side completely, in fact, resize your assembly window so it hides this. This is even more illegible machine code (in hex). The line is interpreted for you in the middle, so you don't need to see the right side.
The left side you can just ignore. It is the address location of the code line. You'll only need to takes notes on that when you want to remember where some code is.
That leaves us the middle! Get use to see these lines as it will be what you focus on.

lb r2,0x0010(r3)

So the first part is always a couple letters that denote what instruction to do. Here we see "lb" which is "load byte". These means load a byte from memory into a register. Now the order is generally the same but some instructions are a little different. We'll see that later when we go through the instruction list.

r2, - this is which register to load the byte into. Remember there are 32 registers named r0 through r31.
0x0010(r3) - this designates where to pull the data from in memory. Split this into two more parts.
The (r3) is telling the playstation to pull from the number stored in r3. This can just be hardcoded, but usually not. r3 at the time is holding a memory address of 0x84140.
The 0x0010 before it is saying take the address in r3 plus this amount and pull that byte out. This does not mean it changes r3's value.

So in the end we have: load byte from r3's address plus 0x0010 and put it into the r2 register.

Once you think you understand that example, it's time to look at the rest!

Instructions for the playstation RISC processor
noop - no op - means no operation and so the playstation doesn't do anything but move on to the next line of code.

Memory - moving memory values to and from registers
lb - load byte - get a byte from memory and put it into a register.
sb - store byte - store a byte from a register into memory.
lw - Load word - get data from memory to register
sw - Store word - write data from register to memory
lui - Load upper immediate - load a constant into the upper two bytes of the register
You will see this like lui r2,0x8008 which means load the constant number 0x8008 into the upper two bytes of register 2. So instead of r2 equaling 0x00 00 80 08 it will be 0x80 08 00 00.
mfhi - move from hi - this deals with the arithmetic division or multiplication. A "hi" register holds the remainder or overflow of multiplication and division. This instructions moves that value to a register.
mflo - move from lo - same as above but the "lo" register holds the actual division or multiplication result.

Arithmetic
add - add two numbers and store into a register
sub - subtract two numbers and store into a register
addi - add immidiate - means add a constant number to another. So addi r2, r3, 0x100 means add register 3 with the hard coded 0x100 into the r2 register.
addu - add unsigned - this is a regular add but with no negative numbers.
subi - subtract immediate - subtract a constant.
subu - subtract unsigned - subtract two non-negative numbers
addui - add immediate unsigned - add a constant and no negative numbers.
mult - multiply two numbers and store the result into the "lo" register. The overflow amount goes into the "hi" register.
multu - multiply unsigned - same as above but only with positive numbers.
div - divide - divide two numbers and store the result in "lo" register, and the remainder in "hi" register.
divu - divide unsigned - same as above but with only positive numbers

Logical - these are bit operations (covered later)
and - logical and
or - logical or
andi - and immediate - same as and but with a constant number
ori - or immediate - same as or but with a constant number
xor - exclusive or on two numbers
xori - exclusive or immediate on two numbers
sll - shift left logical - shift one number X amount of times to the left. This can be viewed by multiply by 2 to the power of X.
srl - shift right logical - shift one number X amount of times to the right. This can be viewed by divide by 2 to the power of X.
sllv - shift left logical variable - same as above but gets the shift value from a register instead of a constant number.
srlv - shift right logical variable - same as above but gets the shift value from a register instead of a constant number.
sra - shift right arithmetic - same as above but does not use the sign bit.

Conditionals - this is where all the IF blocks of normal code appear. More on branching later.
beq - branch on equal - this checks if two values are the same, and then jumps to another code line if it is true.
bne - branch not equal - this jumps to another line of code if two values are NOT equal.
bgez - branch greater or equal to zero - jump if value is greater or equal to zero.
bgezal - branch greater or equal to zero and link - same as above but saves the return address in r31.
bgtz - branch greater than zero - jump if value is greater than zero.
blez - branch less than or equal to zero - jump if values is less than zero.
bltz - branch less than zero - jump if value is less than zero. (negative number)
bltzal -branch less than zero and link - same as above but stores return address in r31.
slt - set less than - set a value to 1 if true, or 0 if false (like a boolean). Compare if one number is less than another.
slti - set less than immediate - same as above but with a constant number to compare against.
sltu - set less than unsigned - same as above except with only positive numbers
sltiu - set less than immediate unsigned - same as above except with a constant and no negative numbers.

Jumps - jump for joy! Or to another code line.
j - jump - just means go to provided line of code.
jr - jump register - jump to address in register.
jal - jump and link - jump like normal, but store the current address in Register 32 (also known as r31). this saves where the code "came from" when it jumped.

So the list won't explain everything to you, and you might be using a cheat sheet for a while. But the more you read those three little letters and say them out loud "bgtz...break greater than zero", the quicker you will just start to remember them.

In the next section, I will cover bitwise operations so that Logical instructions make sense and Branching.

-- Thu Mar 05, 2015 1:41 pm --

Chapter 4 - Logic? What does it all mean?
So even with the syntax, there are many questions left. What is shifting, how do conditional jumps work, what is the difference between a byte/word, and what the heck is logical and/or's? Now it's time to look deeper into what a number is in a computer, and what byte really means.

Byte or Word- these are both numbers but with different minimum and maximum ranges. This is how you describe them aka their "size" in memory.
Byte - single byte: -127 to 127 (00 to FF)
Half-word - 2 bytes: -32,767 to 32,767 (0000 to FFFF)
Word - 4 bytes: -2,147,483,647 to +2,147,483,647 (00000000 to FFFFFFFF)

Now the word "word" is actually stupid at this point due to ambiguity. If you go back in history of computers, silly engineers never though you would need more than 8 bits, then 16 bits, etc. So the term "word" changed from being 2 bytes to 4 bytes at some point. It's better to not use the term "word" but this is engrained into the assembly language interpretation :P

Time to read the signs! What is a sign? A signed number simply means a byte, half-word or word can be a negative number if it is signed. See our example above with the ranges in the negative? If those numbers were unsigned, they could have a larger range but not be able to be less than zero.

Unsigned byte: 0 to 255 (00 to FF)
Unsigned half: 0 to 65,535 (0000 to FFFF)
Unsigned word: 0 to 4,294,967,295

You'll probably recognize these numbers now. And as you can see it makes a huge difference. Our 4 byte number goes from a 2.147 billion ceiling to a 4.294 billion maximum. It's sometimes important to realize these maximums because when you try to surpass them, you get what is called an "overflow". In the circuits, there's nowhere to go so you end up overflowing a number back to the start. In a signed byte (-127 to 127) if you try adding past 127 you will overflow to -127 or in an unsigned byte (0 to 255) if you add past 255 you will overflow back to 0. This is why you see glitches in games sometimes and witness negative numbers where there shouldn't be.

This brings me to the multiplication and division instructions. Since these can easily hit these maximums, these instructions are handled differently. They use a separate two registers to store the results of multiplication and division. These are the "hi" and "lo" registers. In multiplying, the "lo" stores the result. However, if the result is over the maximum 4 bytes, the overflow value is stored in the "hi" register. Division is a little different. Again "lo" stores the result, but "hi" will contain the remainder. You will see the remainder used many times for a "random number" method.

Logical And/Or/XOR
These are comparisons to bits, which make up a byte. What you need to know in gaming is that these are generally used for flags. For instance, flags for treasures opened. If one bit can be a flag for what accessory you're wearing, you can fit 8 bits (flags) into one byte. Let's draw it in binary.

0000 0001 = Mei's pendant
0000 0010 = Speed Chain
0000 0100 = Power Ring
0000 1000 = Magic Ring
0001 0000 = Golden Book
0010 0000 = Bronze Book

These are made up but you can see the '1' bit represents which accessory. Say you had the first three accessories on. Your single byte would look like this.
0000 0111

So how do you compare if the Speed Chain is on? It doesn't make sense to try and do it by numbers, so you have to compare bits. This is where the logical AND comes into play.
0000 0111 - your accessories
AND
0000 0010 - the bit for the Speed chain

The logical and results in any bit that is both '1' in the two numbers. In this case, it doesn't matter what else you have on, it will determine if you are wearing the speed chain. The result is: 0000 0010
Now the game knows you're wearing the speed chain.

OR is like you would think, instead of AND, it checks if either of the two values have a '1' and returns the result.

0000 0111
OR
0000 0010
=
0000 0111

You will see assembly code use this to add large numbers together like so...
8008 0000
OR
0000 43CB
= 8008 43CB

XOR is a little twist more. It results if either are a '1' but not when they BOTH are '1'. In other words, only if ONE of them is on. So our example would be...
0000 0111
XOR
0000 0010
=
0000 01101

Shifting
Shifting is an inexpensive way to multiply or divide by an exponential amount. Shifting left can be written as "<<" and right as ">>". It's easier to just look at the bits or the hex.

0001 shift left by 1 will equal 0010. Pretty easy to see it shifted, but the numbers are exponential. It's basically multiply by 2 to the power of the shift amount.

shift left by 1 = n x 2
shift left by 2 = n x 4
shift left by 3 = n x 8
shift left by 4 = n x 16
shift left by 5 = n x 32
etc.

You can also see it easily in hex if you prefer that.
000F shift left by 1 = 00F0

You'll see this used many times to make a ridiculous looking formula where it really is calculating something simple. For instance if you saw this...

(r3 << 5) - r3

this would be your notes after writing down the multiple assembly lines to do this. It's saying shift r3 to the left 5 times, and then subtract r3. Since you know that << 5 is the same as multiply by 32, you refactor to r3 * 32 - r3. So now you may ask, "so all they're doing is r3 times 31?" and you would be correct. That will be the hard part if you have to break down assembly code into a formula, and then refactor it until it makes sense. You'll see this done a lot for things like "take 31/32 of a value" which is about 3.125%.

Speaking of that, take notes if you find a section that you know is going to be your focus point. Start by just jotting down what address you started at, then just shorthand what the instructions are doing. I write down notes like this:

r2 = r2 + r3
r3 = r2 << 2
r2 = r2 + r3

And then try to get a formula out of it. It is a pain, and does take a while :)

Jumping
When you look at the conditional jumping (bne, bgtz, etc), you will a lot of times see how it sets some condition first with a sltz (set less than zero) or some other. Basically setting one register to a '1' or a '0' and then breaking if it is one way or the other. You will also see how r0 (register one) is always zero and used in many break-if situations to compare something to zero.

You will also run into a "j 0x56896" or such line (I forget off the top of my head). It is a jump to a pretty low code line and what it is used for is RNG. It basically multiplies a large number to a saved memory location and perform a logical and to get a random number. Usually it's returned in r2. Go ahead an skip over the line with F8 in PSX debugger.

Your typical "if statements" are a little more hard to see coming in assembly. For instance, trying to loop through all the characters in your party may look like this.

lb r2, 0x0000(r3) - load byte into r2 from r3's address. This happens to be an 'id' for character. 1 = Vahn, 2 = Noa, 3 = Gala, 4 = Terra
slti r1, r5, 0x003 - set r1 to 1 if r5 is less than 3. r5 is a counter and increased later. It tells us how many characters we processed. This sets if we processed less than 3 characters.
bez r1,0x1e9ff8 - break if r1 is equal to zero. This will only happen when r5 is not less than 3 like the above line set it to.
.. do a bunch of stuff with the character ..
addi r5, r5, 0x0001 - add 1 to r5, starts at 0 and is used as a counter for how many characters we processed. We want to only process 3 party members.
addi r3, r3, 0x001 - add a 1 to r3. This is used to increase what address r2 reads in the first line above. In this case, the three character IDs are 3 sequential bytes so we only increase by 1.
j 0x1e9c58 - this is our loop, it brings up back to the top line and keeps looping. Notice that we need r5 to increase up to 3 before it will break out via the break if equal to zero line.

As you can see, following this assembly code is much more difficult than a standard coding loop.

for(int i = 0; i < 3; i++)
{
// do character stuff
}

But someone probably wrote that very for loop. The compiler turned it into the assembler code above. In doing such, you will see some weird code that doesn't make sense which was lost in translation or because of code that wasn't cleaned up in C.

In the next section, I plan on providing examples of code I found in Legaia and explain how they work.
まさかつあがつ

User avatar
Redhollowlives999
Level Awesome.
Posts: 12139
Joined: Sun Jun 05, 2011 9:39 pm
Location: Yes
Contact:

Re: Assembly Language Tutorial

Post by Redhollowlives999 » Sun Mar 08, 2015 11:23 am

WOW that is a lot. .____.



but the challenge, she calls fer me
it really do be like that tho

User avatar
meth962
Level 23
Posts: 266
Joined: Tue Apr 03, 2012 8:37 am
Location: USA
Contact:

Re: Assembly Language Tutorial

Post by meth962 » Thu Mar 19, 2015 1:04 pm

Random Number Generator
It's time to see the basic RNG code! You can think of it as a time sensitive RNG like how most RNG seeds work, but the reality of it is it's simpler than that. There is a number in memory that keeps getting calculated and added to each time the "RNG" is called. So not only can it be called per frame, but multiple times as well.

1. lw r3, 0x9010
2. lui r1, 0x41c6
3. ori r1, 0x4e6d
4. mult r3,r1
5. mflo r3
6. addi r3,0x3039
7. srl r2, r3, 0x10
8. andi r2,0x7FFF

That's the gist of it, now to break it down. Line 1 first grabs whatever number is stored in the current rng field in register 3, which is always address 0x9010. On bootup, the playstation has this hardcoded to the value 0x24040001.

Line 2 and 3 simply put the hardcoded value 0x41c64e6d into register 1. For clarity, it does this by first loading the upper bytes and then performing an "OR". Think of it like this.

0x41c60000
+
0x00004e6d
=
0x41c64e6d

Lines 4 and 5 do the multiplication of these two large numbers. This should always overflow, and then we take the value from the 'lo' register. This essentially means take the last 4 bytes of a long number. Let's do the example of 0x24040001 x 0x41c64e6d. Type this in windows calculator in hex and you should receive:
0x940EA20CF7A4E6D, break this up in 4 bytes each to get 0x940EA20 CF7A4E6D
So the lo register is the right side of that number, so we load 0xCF7A4E6D into register 3.

Line 6 we just add the hardcoded value 0x3039 to our number. In decimal, that number is 12345. :) Interesting RNG huh? Now r3 equals CF7A7EA6.

Line 7 is a shift right of 16. Really this is making our number a 2 byte number. So 0xCF7A7EA6 becomes 0xCF7A

Line 8 performs an AND step with 0x7FFF which makes our value 0x7F7A. All this is doing is making sure our number is less than 0x7FFF which is 32,767. The significance of this is simply that it is grabbing the absolute value (non-negative number). In two byte numbers, 0x7FFF is 32,767 and if you add one, it becomes 0x8000 which will be -32,768 if a number is designated as "signed' (meaning it can be negative).

And there we have it. The playstation saves the larger value out to 0x9010 and it returns our smaller 0x7F7A to whatever method called for an RNG. How you use that two byte number to RNG will be covered next.

So let's say we want to get a random number between 0 and 200. But the RNG returned to us is 0x7f7a which is 32,634. We can easily do that by performing a modulus of that RNG / 200. So basically, take the remainder of RNG/200. That is written as RNG % 200. If you did that you would have 34 left. That is now your RNG of 0 to 200. Let's look at a game example where the game adds a random number of half your speed to determine turn order.

1. lhu r4,0x0164(r5)
2. srl r3,r4,0x01
3. addiu r3,r3,0x01
4. div r2,r3
5. mfhi r3
6. addu r4,r4,r3
7. addiu r4,r4,0x01

Breaking it down, Line 1 is just loading someone's speed into register 4.
Line 2 performs a Shift Right of 1, which in reality is the same as dividing it by 2. It stores the speed/2 value in register 3. This is our max RNG value.
Line 3 simply adds a 1 to that half speed value. Why you ask? Because if you want to be able to get 200 in a 0-200 rng, you must make sure the remainder can be 200. If you divide by 200, you will never have a remainder of 200. So you a number divided by 201 :) This is why you see some programming languages confusingly give you two numbers for range of RNG and the last number NEVER gets returned. For instance....Random.Next(0,100); Most people would think that's a perfect 0-100% RNG but really it is 0-99%, never returning 100. Even better, you will see this in code a lot: Random.Next(0,1) for a 50/50 or a random bool. Well guess what? That will always return zero, whoops! >:)

Line 4 is our divide. Take our RNG number and divide it by our "maximum" (which is +1 remember). So we're dividing a random number by half of someone's speed.
Line 5 loads our remainder, which is our RNG value finished.
Line 6 adds our original speed with that RNG value of speed/2.
Line 7 simply adds a 1. I'm assuming they do this in case you get a RNG of zero, so at least you have +1 of your speed at the minimum.

Now that it is finished, we have someone's random turn speed in register 4. The game goes on from here to store that number below your battle stats and does the same for the rest of the party and monsters. Then it can determine who goes first.
まさかつあがつ

Post Reply