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
- Create data tables for what to initialize
- Process the data tables to initialize everything
- 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 adw
inInitialEvents
- Every
setflag
command becomes adw
inInitialEngineFlags
- Every
variablesprite
command becomes aninitvarsprite
inInitialVariableSprites
(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 -1
s 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