Making an NPC

From ZeldaHacking Wiki
Jump to navigation Jump to search

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.