Setting up (windows)
Summary
Install devkitPro to its default path of c:\devkitPro
| Wiki
Download lyn.exe by @StanH and place it into c:\devkitPro\
| FEU Thread
Install notepad++ or sublime for editing text and setup EA highlighting & asm highlighting.
Install grepWin
Install GithubDesktop and create an account
Fork the SkillSystemBuildfile and make your copy public.
It is assumed you already have the following:
FEBuilderGBA | no$gba (debug)
Other reading
Summary
https://feuniverse.us/t/ok-fine-lets-learn-how-to-make-skills-skill-creation-tutorial/7774
https://feuniverse.us/t/gbafe-assembly-for-dummies-by-dummies/3563
Opcode Glossary
Summary
Refer back to this as needed, or read @Tequila’s overview here.
Load | Store | LoadSigned | |
---|---|---|---|
Byte | ldrb | strb | ldsb |
Short | ldrh | strh | ldsh |
Word | ldr | str | |
Stack | pop | push |
Branching | |||
---|---|---|---|
cmp | compare | ||
b | always | ||
beq | equal | bne | not equal |
blt | less than | ble | less or equal |
bgt | greater than | bge | greater or equal |
Calling a… | |||
---|---|---|---|
Buildfile func: | bl | ||
Vanilla func: | blh | or | .short 0xf800 |
Returning | |||
---|---|---|---|
bx |
Math | |||
---|---|---|---|
add | sub | mul | swi 6 (divide) |
Logical Shifts | |||
---|---|---|---|
Left | lsl | ||
Right | lsr | asr |
Logic | |||
---|---|---|---|
orr | use bits from either | ||
and | keep bits in common | ||
bic | remove bits in common | ||
neg | x = 0-x | ||
mvn | !(x) |
And mov, of course. There are other opcodes that exist, but these are some common ones to get started with.
Your first ASM
Summary
All of that is scary, can’t I just use FEBuilder?
Sure, let’s start there.
In the disassembler we can look at asm (originally generated by C code) and edit it. The wait command (along with many others) has UsabilityAlways:
mov r0, #0x1
at the end of a function is typically return true;
while mov r0, #0
at the end is return false;
. (Menu options actually return 1 for display, 2 for greyed out and 3 to not show up.)
We can easily make this inline change of return 2;
so that Wait now shows up as greyed out.
However, we cannot add lines of code with inline edits. For example, if we wanted to grey it out for Seth but nobody else, we’d need more lines of code to do that. (To expand code, we’d need to create a new function and poin to it, or hook the code.)
Registers & Mov
Summary
The scratch registers are r0, r1, r2, and r3 which we can use for math.
We often store variables like x = 5;
in r4, r5, r6, and r7.
The other registers don’t really matter for now, though you may notice lr
/ r14
being pushed at the start of functuons.
mov
sets the register to a value.
Eg.
Set r0 to whatever is in r1 by doing mov r0, r1
.
Set r0 to some direct value up to 255 by doing mov r0, #12
.
For example, the Vulnerary Heal Amount patch simply edits the value in the mov r1, #10
command at 0x2FEC6
.
If I directly edit this line to #33 and reload the patch, it’ll also show me 33.
Most patches with a single value are just a mov
command you’re editing. (If you go edit it in the disassembler, then it counts as having done asm! )
Stat Getters
Summary
Located in SkillSystem\EngineHacks\Necessary\StatGetters
\statname.event with specific skill effects in SkillSystem\EngineHacks\SkillSystem\Skills\StatModifierSkills
, these simple routines get the stats of units with any penalties or bonuses included. For example, rescuing a unit halves your Spd/Skl.
I could add a routine called I_AM_SUPER_STRONG
to Power.event
to change a unit’s Strength bonus.
.thumb
.macro blh to, reg=r3
ldr \reg, =\to
mov lr, \reg
.short 0xf800
.endm
.type I_AM_SUPER_STRONG, %function
.global I_AM_SUPER_STRONG
I_AM_SUPER_STRONG:
push {r4-r7, lr}
@ stat getters are given the current stat value in r0
@ stat getters are given the relevant unit in r1
@ < my code >
mov r0, #99
@ < my code >
pop {r4-r7}
pop {r1}
bx r1
.ltorg
You don’t need to understand push
, pop
, .thumb
, .ltorg
, etc. for now. All you need to do is copy exactly what I wrote, only editing the @ < my code > section and the I_AM_SUPER_STRONG
name.
I paste my code into Guts.asm, drag it onto AssembleLyn.bat (or sometimes AssembleArm.bat) and run makehack.cmd.
Now I check ingame to see that everybody’s Str is indeed boosted to 99.
Great! It works. Now let’s make it conditional. I’ve decided I want a superboss. Thought you could survive him? Get rekt, kid.
If we look at the Teq Doq or StanH’s FE-Clib we can see the Unit Struct, also seen in the FEBuilder Debugger’s “etc” tab.
Since we’re given the unit struct in r1 for all stat getter functions, we can load any information from here as desired. We have ldr that loads a WORD (4 bytes), ldrh loads a Halfword / SHORT (2 bytes), and ldrb loads a byte.
C
int I_AM_SUPER_STRONG(int value, struct Unit* unit) {
if (unit->level == 20) { return 99; }
return value;
}
asm
@ < my code >
@ r0 = value
@ r1 = UnitStruct
.equ LevelOffset, 0x8
ldrb r2, [r1, #LevelOffset] @ Get the level of the unit
mov r3, #20 @ level we're comparing it to
cmp r2, r3 @ compare
bne Skip @ Branch (skip) if Not Equal
mov r0, #99 @
Skip:
@ < my code >
We also need cmp
, which compares two values and we need branching (which all start with the letter b
). If the level byte we load is not #20, then skip to the end. This would make it so level 20s have 99 Str. Let’s instead make it only apply to O’Neill and make it (Str*2+5).
int I_AM_SUPER_STRONG(int value, struct Unit* unit) {
if (unit->pCharacterData->number == 0x68) { return ((value*2)+5); }
return value;
}
pCharacterData is the data in the Character Editor within FEBuilder, so we ldr
that.
Then we’ll ldrb
the 4th byte (ID) to see if it is 0x68.
@ < my code >
@ r0 = value
@ r1 = UnitStruct
.equ pCharacterData, 0x0
.equ unitID, 0x4
ldr r2, [r1, #pCharacterData] @ Get the address of the CharEditorEntry
ldrb r2, [r2, #unitID] @ Get the unit id from the CharEditorEntry
mov r3, #0x68 @ unit ID we're powering up
cmp r2, r3 @ compare
bne Skip @ Branch (skip) if Not Equal
mov r1, #2 @ amount we will multiply by
mul r0, r1 @ multiply r0 by r1
add r0, #5 @ add #5 to r0
Skip:
@ < my code >
Finally we’ll mul
the value by #2, then add
#5. Now O’Neill has his Str doubled + 5, but nobody else does.
Using unit id #0x68 directly in the code is called hardcoding. It’s typically better practice to use a definition and label in your .event file, but we’ll talk more about that later.
How do I
addreplace a function like this via FEBuilder? I’m allergic to buildfiles.
It’s easier in a buildfile/custom build. Not my fault…
Summary
- Generate a .sym file by using Ctrl+F5 to run No$GBA in FEBuilder.
- Find
pPowerModifiers
and open that address in the hex editor.
prAddUnitPower
looks like this for me and is the first function inpPowerModifiers
:D0 F9 B7 08
is the same as8B7F9D0
that we see as the address in the disassembler but it’s written in Little Endian (basically backwards). - Choose one of these functions to replace before you get to
WORD 0
.
- Insert your ASM at some address, and overwrite one of these addresses with your asm’s address.
But really, you should probably do this via a buildfile to test. You may find it easier to do this via a CustomBuild.
Heal Loop
Summary
The HpRestoration calc loop, found in SkillSystem\EngineHacks\Necessary\CalcLoops\HPRestorationCalcLoop
with specific skills in SkillSystem_FE8\EngineHacks\SkillSystem\Skills\HPRestorationSkills
restores a percentage of health at the start of your turn.
Let’s add a function called GamesharkCode_Heal
.
And copy paste AssemblyLyn from somewhere (eg. Amaterasu
’s folder).
We’ll start with the same code template as before, only changing its name:
int GamesharkCode_Heal(struct Unit* unit, int value) {
return 99;
}
.thumb
.macro blh to, reg=r3
ldr \reg, =\to
mov lr, \reg
.short 0xf800
.endm
.type GamesharkCode_Heal, %function
.global GamesharkCode_Heal
GamesharkCode_Heal:
push {r4-r7, lr}
@ HpRestoration Loop is given the relevant unit in r0
@ HpRestoration Loop is given the current % to heal by in r1
@ < my code >
mov r0, #99
@ < my code >
@ HpRestoration Loop returns the % of Hp to heal in r0
pop {r4-r7}
pop {r1}
bx r1
.ltorg
Please note that this time r0 gives us the unit and r1 gives us the % to heal by.
When I test it ingame, I can see that all units heal by 99% of their max hp each turn.
Let’s Organize!
Typically at the start of a function you’ll save any parameters to your registers 4-7 as variables.
@ < my code >
mov r4, r0 @ unit struct is now in r4
mov r5, r1 @ % to heal is now in r5
This way, you can call other functions.
mov r0, r4
bl IsUnitPlayer @ given r0 = unit struct, return true if they are a player. False if not.
The other functions can be vanilla ones, from the same file (static), or global within the buildfile. Let’s make IsUnitPlayer
a static function. At the bottom of our file (after .ltorg
), we’ll write more code with our template but omit the .type name, %function
and the .global name
parts:
int IsUnitPlayer(struct Unit* unit) {
return (unit->index < 0x40);
}
IsUnitPlayer:
push {r4-r7, lr}
@ given r0 = unit struct, return true if they are a player. False if not.
@ < my code >
.equ Divide, 0x80D18FC @ Vanilla function to divide r0 by r1
.equ DeploymentByte, 0xB @ Found from the TeqDoq
@ (0x01-0x3F for player, 0x40-0x7F for NPC, 0x80-0xBF for enemy)
mov r4, r0 @ unit
ldrb r0, [r4, #DeploymentByte] @ Load the allegiance/deployment byte
mov r1, #0x40 @ NPCs start here
cmp r0, r1 @ compare
bge ReturnFalse @ Branch if Greater or Equal than
mov r0, #1 @ true that they are a player
b Exit_IsUnitPlayer @ Branch (jump) to this label
ReturnFalse:
mov r0, #0
Exit_IsUnitPlayer:
@ < my code >
pop {r4-r7}
pop {r1}
bx r1
.ltorg
We ldrb
to check their allegiance, cmp
it with #0x40, and we bge
(Branch if Greater or Equal) to ReturnFalse
. I also omitted .thumb
and the .macro
part, as we only need to have those once in our file. Even if you don’t fully understand the function we called, the point stands that we can organize our code more easily by making generic functions that can be reused, or by using vanilla functions.
Let’s finish our original function, now.
int GamesharkCode_Heal(struct Unit* unit, int value) {
if (IsUnitPlayer(unit)) { return (value*75/100); }
return value;
}
@ < my code >
.equ Divide, 0x80D18FC @ Vanilla function to divide r0 by r1
.equ DeploymentByte, 0xB @ Found from the TeqDoq
@ (0x01-0x3F for player, 0x40-0x7F for NPC, 0x80-0xBF for enemy)
mov r4, r0 @ unit struct is now in r4
mov r5, r1 @ % to heal is now in r5
mov r0, r4
bl IsUnitPlayer @ given r0 = unit struct, return true if they are a player. False if not.
cmp r0, #0 @ did IsUnitPlayer return false?
beq Exit @ if it returned false, branch to Exit
mov r0, r5 @ % to heal by
mov r1, #4 @ amount to divide by
blh Divide, r3 @ Vanilla function to divide r0 by r1
@swi 6 @ this would also work here instead of the vanilla divide function
sub r5, r0 @ r5 = r5-r0
Exit:
mov r0, r5 @ % to heal is returned in r0
@ < my code >
Previously we used mul
and add
, so this time I divided and used sub
(subtract). This makes it so player units now heal 100% - (100% / 4): 75% of what they used to heal. So a Fort would heal you 15% instead of 20% each turn.
What the heck? Why doesn’t division get its own opcode?
Division is complicated, so it has its own vanilla code for it. When we call a vanilla function, we use blh instead of bl. This is because bl can only reach code within approx. 1 mb (max rom size is 32 mb). However, blh uses up a register - by default, r3. (Functions that take 4 or more parameters use r3, so for them you’d need to do blh FunctionName, r4
.)
(7/48 hp is restored instead of 9/48: it works.)
What is
swi 6
?
The GBA itself can also perform division, but this is done by performing a SoftWare Interrupt. Division is slow compared with other commands, so it is often avoided with a little trickery. I’m not sure whether swi 6
or the vanilla function is faster, but logical shifts are much faster when possible. This is where we move digits. For example in decimal, 72 would become 720 moving left one place, or 72 would become 7 moving right one place. But because the computer works in binary / hexadecimal, shifting left is the same as multiplying it by 2, and right, dividing by 2.
mov r0, #16
lsr r0, #1 @ (logical shift right)
@ 16 / (2^1) = 8
@ r0 = 8
lsl r0, #1 @ (logical shift left)
@ 8 * (2^1) = 16
@ r0 = 16
lsr r0, #2 @ r0 = 4
lsl r0, #2 @ r0 = 16
In this case, we could use lsr
to put r5 divided by 4 into r0.
Eg.
mov r0, r5
lsr r0, #2 @ divide by 4
Finally, since we never actually used r6 and r7, we don’t need to push/pop those. Eg.
push {r4-r5, lr}
@ ...
pop {r4-r5}
pop {r1}
bx r1
.ltorg