Making an NPC
This page gives an outline of how one might make a custom NPC using ZOSE. Patch (interaction $9400
) will be used as a template.
Tools used: BGB, ZOSE, and the disassembly as a reference.
This process may be easier with a pure disassembly approach, but for now, it's assumed that the disassembly can't be used directly for this purpose (since most hacks are using the mostly-incompatible ZOLE).
Background information
NPCs are interaction objects. They generally have two components: their assembly code, and their script. As a rough guideline, things that should be updated every frame are put in their assembly code, and sequences of events that occur over many frames are put into scripts.
The NPC's sprite and animations are tied to the high byte of their ID. So, interactions $9400-$94ff
must all use the same graphics (patch's sprites). This means you need to be careful when selecting an NPC to modify.
You should preferably know:
- Everything here
- How to write basic scripts in ZOSE
- A bit of assembly knowledge
Modifying assembly code
The recommended method is to use the BGB emulator with a symbol file. This will allow you to use the Ctrl-G hotkey to jump to any known label, without needing to know its address. For instance, typing "interactionCode94" in that window would take you directly to Patch's code.
The code itself can be modified in the debugger by right-clicking and selecting "modify code/data", then typing the opcodes (separated by semicolons).
The procedure
Start by creating a basic script in ZOSE. It can be expanded later. Something like the following will do:
WriteLocation X
SetInteraction72 Y
InitCollisions
WriteLocation X+1 // Just for clarity
CheckAButton
ShowText <textindex>
Jump2byte X+1
This is the most basic kind of NPC; it will show text when you press A in front of it, and loop forever. (Note: the lack of labels in ZOSE requires you to calculate the address for the loop by hand. In this case, the "InitCollisions" opcode only takes up one byte.)
The "SetInteraction72" line is not necessary, since you will not be using the script with interaction 72 (we're using patch, interaction 94, instead). Still, you can use interaction 72 to test the script before replacing Patch. (It will behave like an invisible NPC.)
Modifying the NPC's assembly code
The vast majority of NPCs have assembly code that you won't want. Patch, in particular, has a great deal of logic referring to whether you've obtained the tuni nut, etc. We'll want to get rid of all of that.
We will use the disassembly for reference. Open main.s and search for interactionCode94
. This will take you to the code that runs for the interaction on each frame. You will see something like this:
interactionCode94:
ld e,$42 ; $787e
ld a,(de) ; $7880
ld e,$44 ; $7881
rst_jumpTable ; $7883
.dw $7894
.dw $793c
.dw $7a98
.dw $7b4c
.dw $7bf0
.dw $7bf0
.dw $7c40
.dw $7c40
In the context of an object's code, the 'd' register represents the high byte of the object's RAM block. Brush up on the memory structure for interactions. In doing so, you'll see that the first 2 lines:
ld e,$42
ld a,(de)
get the interaction's "subid" byte. Our interaction's full ID is $9400
; so the "id" byte is $94, and the "subid" byte is $00. It then uses this byte in a jump table, meaning it will jump to the first entry in the jump table, address $7894. You can ignore all of the other entries since they pertain to different subids.
At that address, you'll then come across another jump table:
ld e,$44 ; $7894
ld a,(de) ; $7896
rst_jumpTable ; $7897
.dw $789e
.dw $7913
.dw $7928
$44 is the object's "state" byte. Patch has 3 states, but we will only need 2: state 0 for "uninitialized", and state 1 for "initialized".
State 0
Patch's state 0 is quite large, but we only need a few function calls. Replace the code for state 0 (starting at 0a:789e
) with the following:
call interactionInitGraphics
call interactionIncState
ld hl, <scriptAddress>
jp interactionSetScript
"<scriptAddress>" must be in bank C, which means you can't point it directly to your actual script. Look through the npc's code to find an unused script. Patch has a few; we'll use script787e
in this example.
We'll use this as a bootstrap for the actual script. Add the following lines to the top of your script in ZOSE:
WriteLocation 3387e // 0c:787e converted to an absolute address
LoadScript X // Where X is the address of your script, as above
State 1
Patch's state 1, at 0a:7913
, is quite close to what we need compared to state 0. The end result should look like this:
ld c,$20
call objectUpdateSpeedZ_paramC
call interactionRunScript
jp npcAnimate_staticDirection
Calling objectUpdateSpeedZ_paramC
will allow the setZSpeed
script command to work properly. npcAnimate_staticDirection
, as you probably suspected, animates the npc without modifying its direction.
Note that the the main difference to Patch's state 1 is the last line, which was originally jp nc,npcAnimate_followLink
. Not only was it a conditional jump, but it called a different variant of npcAnimate
, which constantly updates the object's direction to face Link when he's close enough. This is problematic when the MoveNPCUp/Down/Left/Right
opcodes are used, so the static variant is used instead.
The final result
The script
// The script's "bootstrap" (somewhere in bank C)
WriteLocation 3387e // 0c:787e converted to an absolute address
LoadScript X // Where X is the address of the script
// The script itself
WriteLocation X
InitCollisions
WriteLocation X+1 // Just for clarity
CheckAButton
ShowText <textindex>
Jump2byte X+1
The code
The following is what the NPC's code should look like on a general level (with labels being used everywhere, unlike in the example with patch).
interactionCodeXX:
ld e,Interaction.subid
ld a,(de)
rst_jumpTable
.dw @subid0
.dw ...
...
@subid0:
ld e,Interaction.state
ld a,(de)
rst_jumpTable
.dw @state0
.dw @state1
@state0:
call interactionInitGraphics
call interactionIncState
ld hl, <scriptAddress>
jp interactionSetScript
@state1:
ld c,$20
call objectUpdateSpeedZ_paramC
call interactionRunScript
jp npcAnimate_staticDirection
Caveats
Jump opcodes before ZOSE v0.7
As of ZOSE v0.7.00, jump opcodes can be used in any context. Before, however, 3-byte jumps only worked with interaction 72, and 2-byte jumps only worked with everything but interaction 72. If you used to use an older version of ZOSE, you may need to click "Re-apply ASM patches" to fix this.
2-byte jumps
The built-in jumping opcodes (jump2byte, jumpifmemoryeq, etc) have the following limitations:
- The destination must be in bank $0C, OR
- The destination must be within 256 bytes of the start of the script. It cannot jump anywhere before the start of the script.
- The "start of the script" is the point at which the last "LoadScript" or "Jump3Byte" opcode was invoked.