Remove the artificial save delay - pret/pokecrystal GitHub Wiki
This tutorial describes how to remove the artificial delays introduced when saving the game.
Saving the game is a reasonably quick process. A full write to SRAM takes less than a quarter of a second. However, for reasons unknown, the game makes it seem like a five-second process, displaying at least two confirmation windows, and forcing the player to wait while saving. Regardless of whether this annoyance was an intentional design choice or an artifact of its time, there is no actual need for the delays, and thus they can easily be removed for a much smoother saving process.
Note that most of this tutorial takes place in a single file, engine/menus/save.asm. If nothing else is indicated, assume that the listed functions are found in that file.
- Understanding the saving process
- Removing the slow text printing function
- Renaming functions
- Removing delays in save function
- Removing the extra prompt
- Removing the delays on Bill's PC
- Removing the remaining delays
- Adding a message without delays
Before making any changes to the saving process, it is instructive to look at how it works. The game gets saved on five occasions:
- When the player manually initiates a save, via the Start menu;
- When the player uses the "move Pokémon without mail" feature;
- When the player switches PC boxes;
- When the player reaches the Hall of Fame and becomes Champion;
- When using link features and/or the Battle Tower.
Each of these five situations calls slightly different code, but the underlying saving functions are mostly the same. The most common case, of course, is the case of manually initiated saves. Such a save triggers the following sequence:
- Display a prompt asking if they want to save.
- If a previous savefile exists, display another confirmation prompt (with separate prompts for updating the current game's savefile vs. overwriting a previous game's savefile with a completely new one).
- Block all player inputs after confirming.
- Override the text speed, setting it to "Medium", and display
SAVING… DON'T TURN OFF THE POWER
. - Wait 16 frames.
- Actually save the game. This is the important step.
- Wait 32 frames.
- Override the text speed, setting it to "Medium", and display
<PLAYER> saved the game!
. - Play a sound confirming that saving was successful.
- Wait 30 frames.
- Close the menus and let the player continue playing.
The key takeaway from this list is that saving only occurs during step 6. That means that while the game is waiting and displaying the SAVING…
message, no actual saving is happening. Therefore, all of these delays can be removed without interfering at all with the saving feature. The following steps show how to achieve this goal.
Note that, with the exception of the immediately following section, all sections are optional. If you want to keep some of the delays, skip the corresponding sections.
Much of the slowness in the process comes from the function that prints the SAVING… DON'T TURN OFF THE POWER
text. This function is adequately called SavingDontTurnOffThePower
, and handles steps 3 through 5 in the section above. Since this function serves no useful purpose, it can safely be deleted, along with the two calls to it. Delete the entire function, and remove the two calls as follows:
ChangeBoxSaveGame:
; ...
call PauseGameLogic
- call SavingDontTurnOffThePower
call SaveBox
; ...
; ...
_SavingDontTurnOffThePower:
- call SavingDontTurnOffThePower
SavedTheGame:
; ...
The deleted function also references a text element, which in turn makes a far jump to the actual text in engine/menus/save.asm. Both of these elements may be deleted:
-SavingDontTurnOffThePowerText:
- text_far _SavingDontTurnOffThePowerText
- text_end
And in data/text/common_3.asm:
-_SavingDontTurnOffThePowerText::
- text "SAVING… DON'T TURN"
- line "OFF THE POWER."
- done
While most of the delay will be gone with this single step, the rest of the steps in this tutorial will fully remove the delays and polish the experience for the user.
After the deletion from the previous section, _SavingDontTurnOffThePower
becomes a simple dummy function that falls through into SavedTheGame
; the former can thus be removed, and all calls replaced with calls to the latter. Similarly, SaveGameData
is nothing but a wrapper for _SaveGameData
; the wrapper can be deleted and the underscore removed.
Note that, while this step isn't necessary, it will greatly improve the readability of the code involved and simplify its debugging in case anything goes wrong. While the rest of the tutorial doesn't concern itself with cleaning up existing code, this step is highly recommended.
SaveMenu:
; ...
call PauseGameLogic
- call _SavingDontTurnOffThePower
+ call SavedTheGame
call ResumeGameLogic
; ...
; ...
Link_SaveGame:
; ...
call PauseGameLogic
- call _SavingDontTurnOffThePower
+ call SavedTheGame
call ResumeGameLogic
; ...
; ...
StartMoveMonWOMail_SaveGame:
; ...
call PauseGameLogic
- call _SavingDontTurnOffThePower
+ call SavedTheGame
call ResumeGameLogic
and a
ret
; ...
-SaveGameData:
- call _SaveGameData
- ret
; ...
-_SavingDontTurnOffThePower:
SavedTheGame:
- call _SaveGameData
+ call SaveGameData
; ...
-_SaveGameData:
+SaveGameData:
ld a, TRUE
; ...
This renaming also requires editing a single line in mobile/mobile_5f.asm:
IncCrashCheckPointer_SaveGameData:
- inc_crash_check_pointer_farcall _SaveGameData
+ inc_crash_check_pointer_farcall SaveGameData
With the text delays removed, every piece of code that used to print the slow SAVING… DON'T TURN OFF THE POWER
message now calls SavedTheGame
instead; other parts of the code call it directly. This function is responsible for calling SaveGameData
(which is where the actual saving happens) and then informing the player that saving succeeded.
Of course, the informing part is done with plenty of delays (namely, steps 7, 8 and 10 in the first section), which we can remove:
SavedTheGame:
call SaveGameData
- ; wait 32 frames
- ld c, 32
- call DelayFrames
; copy the original text speed setting to the stack
ld a, [wOptions]
push af
- ; set text speed to medium
- ld a, TEXT_DELAY_MED
+ ; set text speed to fast
+ ld a, TEXT_DELAY_FAST
ld [wOptions], a
; <PLAYER> saved the game!
ld hl, Text_PlayerSavedTheGame
call PrintText
; restore the original text speed setting
pop af
ld [wOptions], a
ld de, SFX_SAVE
call WaitPlaySFX
- call WaitSFX
+ jp WaitSFX
- ; wait 30 frames
- ld c, 30
- call DelayFrames
- ret
When the player chooses to save the game, the game only saves immediately if no save data exists. If a (valid) savefile already exists, the game will ask the player to confirm if they want to overwrite that savefile.
Of course, this prompt is useful when saving on top of a different game's savefile (which can happen if the player hits New Game while a savefile exists), as it prevents the player from overwriting their previous saved game accidentally. However, saving on top of a previous savefile for the same game is virtually always intentional, as players will update their savefiles as they play through the game. This extra prompt is just inconvenient in this case.
Fortunately, since the game already contains separate texts for each situation, removing the extra prompt when saving on top of a previous savefile for the same game only requires a simple change:
AskOverwriteSaveFile:
ld a, [wSaveFileExists]
and a
jr z, .erase
call CompareLoadedAndSavedPlayerID
- jr z, .yoursavefile
+ ret z ; pretend the player answered "Yes", but without asking
ld hl, AnotherSaveFileText
call SaveTheGame_yesorno
jr nz, .refused
- jr .erase
-.yoursavefile
- ld hl, AlreadyASaveFileText
- call SaveTheGame_yesorno
- jr nz, .refused
- jr .ok
-
.erase
call ErasePreviousSave
-
-.ok
and a
ret
.refused
scf
ret
; ...
-AlreadyASaveFileText:
- text_far _AlreadyASaveFileText
- text_end
The corresponding text from data/text/common_3.asm can be removed as well:
-_AlreadyASaveFileText::
- text "There is already a"
- line "save file. Is it"
- cont "OK to overwrite?"
- done
Bill's PC requires a save when changing boxes, and also when using the "move Pokémon without mail" feature (which saves once initially and once per swap). Changing boxes just uses the regular save code (the same we just edited), so that's taken care of. However, swapping Pokémon through the "move Pokémon without mail" feature uses separate code, with smaller delays, simply because the longer delays from the regular function would be intolerable here. Despite these delays being smaller, they have no reason to exist, and thus can safely be removed.
First of all, the function in engine/menus/save.asm, the file we've been editing so far:
MoveMonWOMail_InsertMon_SaveGame:
; ...
call LoadBox
call ResumeGameLogic
ld de, SFX_SAVE
+ jp PlaySFX
- call PlaySFX
- ld c, 24
- call DelayFrames
- ret
And secondly, the actual function in Bill's PC's code, in engine/pokemon/bills_pc.asm:
MovePKMNWithoutMail_InsertMon:
; ...
ld de, .Saving_LeaveOn
call PlaceString
- ld c, 20
- call DelayFrames
pop af
pop bc
pop de
pop hl
; ...
At this point, only two sources of delay remain, corresponding to the two most uncommon cases out of the five listed in the first section. The first one of them is the "quick save" function, used by the link features and the Battle Tower when they require the player to save in the middle of their text prompts. The delay here is very small, hence the "quick" moniker, but it can still be removed.
TryQuickSave:
; ...
ld [wScriptVar], a
- ld c, 30
- call DelayFrames
pop af
ld [wChosenCableClubRoom], a
ret
Finally, the one delay that remains is the one that appears when the player becomes Champion, before the credits roll. This delay is very large, as it replaces all the smaller delays in other functions; it's nearly two seconds long. Fortunately, it can also be removed.
In engine/events/halloffame.asm:
HallOfFame_FadeOutMusic:
; ...
farcall InitDisplayForHallOfFame
- ld c, 100
- jp DelayFrames
+ ret
At this point, with all sources of delay removed, saving only takes the amount of time that is actually necessary to write data to SRAM. Sadly, since the code isn't as optimized as it could be and the GB is just slow, this takes a visible amount of time; typically around a quarter of a second, but enough to make it seem like the game temporarily froze.
A simple solution to this problem is to add again a message like the one we removed. Despite the game wasn't saving while SAVING… DON'T TURN OFF THE POWER
was being printed, the game did save at some point in time after that (in the middle of the delays) while the message was still visible, so the player knew that the game was doing something.
If we are to reinstate this message, we should be careful not to introduce back the delays we spent the whole tutorial removing. Therefore, any message that we introduce must meet the following conditions:
- The message must only be printed while the game is actually saving.
- Printing the message must not take any significant amount of time.
- As soon as saving is done, the message must be removed.
This can be achieved by printing the message instantaneously and keeping it short (which eliminates the delays) right before the game begins to save (which ensures that the message is shown while the game actually saves), and letting the regular <PLAYER> saved the game!
message overwrite it when saving is done (which removes it when no longer saving).
Making a final edit to SavedTheGame
achieves this goal:
SavedTheGame:
+ ld hl, wOptions
+ set NO_TEXT_SCROLL, [hl]
+ push hl
+ ld hl, .saving_text
+ call PrintText
+ pop hl
+ res NO_TEXT_SCROLL, [hl]
call SaveGameData
...
jp WaitSFX
+.saving_text
+ text "SAVING…"
+ done