Improve the event initialization system - pret/pokecrystal GitHub Wiki

When you start a new game, the memory is zeroed out. But some things need to be initialized to nonzero values.

This is done with a callback script in maps/PlayersHouse2F.asm, the map you first start in: if the EVENT_INITIALIZED_EVENTS event is not set yet, it does jumpstd InitializeEventsScript. That command actually calls the InitializeEventsScript script, which is defined in engine/events/std_scripts.asm. It sets a lot of events, a couple of engine flags, initializes variable sprites, and finally sets the EVENT_INITIALIZED_EVENTS event so it won't repeat every time you enter PlayersHouse2F.

There are a few problems with this approach. One, it's tied to a particular map: if you change which map the player starts in, you have to remember to move the InitializeEventsScript callback. Two, it's hard to find: every time you need to add another initially-set event, you have to scroll halfway through an engine file to the right script. Three, it wastes ROM space: every single setevent, setflag, or variablesprite line spends one byte to identify the command.

This tutorial (using code originally by ISSOtm) will solve all of those problems. It's recommended for if you want your project to be a base for other hacks, since they'll all need their own initializations.

Contents

  1. Create data tables for what to initialize
  2. Process the data tables to initialize everything
  3. Remove the scripts that initialized everything

1. Create data tables for what to initialize

Create data/events/init_events.asm:

+InitialEvents:
+	dw EVENT_EARLS_ACADEMY_EARL
+	...
+	dw EVENT_INDIGO_PLATEAU_POKECENTER_RIVAL
+	dw EVENT_INITIALIZED_EVENTS
+	dw -1 ; end
+
+InitialEngineFlags:
+	dw ENGINE_ROCKET_SIGNAL_ON_CH20
+	dw ENGINE_ROCKETS_IN_MAHOGANY
+	dw -1 ; end
+
+InitialVariableSprites:
+MACRO initvarsprite
+; variable sprite, appearance sprite
+	db \1 - SPRITE_VARS, \2
+ENDM
+	initvarsprite SPRITE_WEIRD_TREE, SPRITE_SUDOWOODO
+	initvarsprite SPRITE_OLIVINE_RIVAL, SPRITE_RIVAL
+	initvarsprite SPRITE_AZALEA_ROCKET, SPRITE_ROCKET
+	initvarsprite SPRITE_FUCHSIA_GYM_1, SPRITE_JANINE
+	initvarsprite SPRITE_FUCHSIA_GYM_2, SPRITE_JANINE
+	initvarsprite SPRITE_FUCHSIA_GYM_3, SPRITE_JANINE
+	initvarsprite SPRITE_FUCHSIA_GYM_4, SPRITE_JANINE
+	initvarsprite SPRITE_COPYCAT, SPRITE_LASS
+	initvarsprite SPRITE_JANINE_IMPERSONATOR, SPRITE_LASS
+	db -1 ; end

This file defines three tables: InitialEvents, InitialEngineFlags, and InitialVariableSprites. They're all based on the commands from InitializeEventsScript:

  • Every setevent command becomes a dw in InitialEvents
  • Every setflag command becomes a dw in InitialEngineFlags
  • Every variablesprite command becomes an initvarsprite in InitialVariableSprites

(The initvarsprite macro is so you don't have to subtract SPRITE_VARS in every single line; it's based on the definition of variablesprite in macros/scripts/events.asm.)

There are 136 entries in these three tables, so that saves 136 bytes because they don't all start with script commands. We spend five bytes on the -1s to mark the end of each table (dw -1 is two bytes), so if the code to process these tables takes fewer than 131 bytes, we'll have saved space overall.

2. Process the data tables to initialize everything

Create engine/events/init_events.asm:

+InitializeEvents:
+; initialize events
+	ld hl, InitialEvents
+.events_loop
+	ld a, [hli]
+	ld e, a
+	ld a, [hli]
+	ld d, a
+	and e
+	cp -1
+	jr z, .events_done
+	ld b, SET_FLAG
+	push hl
+	call EventFlagAction
+	pop hl
+	jr .events_loop
+
+.events_done
+; initialize engine flags
+	ld hl, InitialEngineFlags
+.flags_loop
+	ld a, [hli]
+	ld e, a
+	ld a, [hli]
+	ld d, a
+	and e
+	cp -1
+	jr z, .flags_done
+	ld b, SET_FLAG
+	push hl
+	farcall EngineFlagAction
+	pop hl
+	jr .flags_loop
+
+.flags_done
+; initialize variable sprites
+	ld hl, InitialVariableSprites
+.sprites_loop
+	ld a, [hli]
+	ld e, a
+	ld d, 0
+	cp -1
+	ret z
+	ld a, [hli]
+	push hl
+	ld hl, wVariableSprites
+	add hl, de
+	ld [hl], a
+	pop hl
+	jr .sprites_loop
+
+INCLUDE "data/events/init_events.asm"

This defines the InitializeEvents assembly function. It has three main loops, one for each table. They're all based on the definitions of the original script commands (setevent, setflag, or variablesprite) from engine/overworld/scripting.asm.

Then edit engine/menus/intro_menu.asm:

 InitializeWorld:
 	call ShrinkPlayer
 	farcall SpawnPlayer
 	farcall _InitializeStartDay
+	farcall InitializeEvents
 	ret

And edit main.asm:

 SECTION "Phone Scripts 2", ROMX

 INCLUDE "engine/events/std_scripts.asm"
+INCLUDE "engine/events/init_events.asm"
 ...

The code uses 73 bytes—67 to define InitializeEvents and 6 to farcall it—which means we've saved 58 bytes so far. But we're about to save more, since the old system of calling InitializeEventsScript needs to go.

3. Remove the scripts that initialized everything

Edit engine/events/std_scripts.asm:

 StdScripts::
 	...
-	add_stdscript InitializeEventsScript
 	...
-InitializeEventsScript:
-	setevent EVENT_EARLS_ACADEMY_EARL
-	...
-	setevent EVENT_INITIALIZED_EVENTS
-	endcallback

And edit maps/PlayersHouse2F.asm:

 .InitializeRoom:
 	special ToggleDecorationsVisibility
 	setevent EVENT_TEMPORARY_UNTIL_MAP_RELOAD_8
-	checkevent EVENT_INITIALIZED_EVENTS
-	iftrue .SkipInitialization
-	jumpstd InitializeEventsScript
-	endcallback
-
-.SkipInitialization:
 	endcallback

Now we have a system for initializing the game that's independent of the starting map, easy to find with the rest of the game data, and uses less space.

Let's see how much less. The tools/free_space.py script exists to measure that, based on the .map file produced when you make the ROM. So run it before this tutorial:

$ tools/free_space.py pokecrystal.map
Free space: 455223/2097152 (21.71%)

And again after the tutorial:

$ tools/free_space.py pokecrystal.map
Free space: 455295/2097152 (21.71%)

That's 72 more ROM bytes than before. It's not a whole lot, but every bit helps.

You can eke out a few more by applying the tricks from the assembly optimization tutorial to InitializeEvents: it was written more for clarity than to save space. For example, all three cp -1 can become inc a (the "Compare a to 255" technique) to save three bytes and run a little faster. And you can optimize the entire .sprites_loop using the "Add a to an address" technique:

 .sprites_loop
 	ld a, [hli]
-	ld e, a
-	ld d, 0
-	cp -1
+	inc a
	ret z
+	; subtract 1 to balance the previous 'inc'
+	add LOW(wVariableSprites) - 1
+	ld e, a
+	adc HIGH(wVariableSprites)
+	sub e
+	ld d, a
 	ld a, [hli]
-	push hl
-	ld hl, wVariableSprites
-	add hl, de
-	ld [hl], a
-	pop hl
+	ld [de], a
 	jr .sprites_loop