1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
|
#include <libultraship/bridge/consolevariablebridge.h>
#include "BenPort.h"
#include "2s2h/GameInteractor/GameInteractor.h"
#include "2s2h/ShipInit.hpp"
#include "2s2h/CustomMessage/CustomMessage.h"
extern "C" {
#include <variables.h>
#include <functions.h>
}
#define CVAR_REMEMBER_SAVE_LOCATION_NAME "gEnhancements.Saving.RememberSaveLocation"
#define CVAR_REMEMBER_SAVE_LOCATION CVarGetInteger(CVAR_REMEMBER_SAVE_LOCATION_NAME, 0)
static int lastEntrance = -1;
static int entranceToSave = -1;
static HOOK_ID skipEntranceCutsceneHookId = 0;
static HOOK_ID gameplayStartHookId = 0;
// Used for saving through Autosaves and Pause Menu saves.
extern "C" void SavingEnhancements_PersistSaveEntranceInfo() {
if (CVAR_REMEMBER_SAVE_LOCATION) {
// Maintain respawn information, used for grottos
for (int i = 0; i < RESPAWN_MODE_MAX; i++) {
gSaveContext.save.shipSaveInfo.respawn[i] = gSaveContext.respawn[i];
}
// Daytelop on new game, with Time Shuffle, makes it possible for entranceToSave to be -1. Given that the player
// must be at this entrance in that scenario, just use it as a fallback.
gSaveContext.save.shipSaveInfo.pauseSaveEntrance =
entranceToSave < 0 ? ENTRANCE(SOUTH_CLOCK_TOWN, 0) : entranceToSave;
} else {
switch (gPlayState->sceneId) {
// Woodfall Temple + Odolwa
case SCENE_MITURIN:
case SCENE_MITURIN_BS:
gSaveContext.save.shipSaveInfo.pauseSaveEntrance = ENTRANCE(WOODFALL_TEMPLE, 0);
break;
// Snowhead Temple + Goht
case SCENE_HAKUGIN:
case SCENE_HAKUGIN_BS:
gSaveContext.save.shipSaveInfo.pauseSaveEntrance = ENTRANCE(SNOWHEAD_TEMPLE, 0);
break;
// Great Bay Temple + Gyorg
case SCENE_SEA:
case SCENE_SEA_BS:
gSaveContext.save.shipSaveInfo.pauseSaveEntrance = ENTRANCE(GREAT_BAY_TEMPLE, 0);
break;
// Stone Tower Temple
case SCENE_INISIE_N:
gSaveContext.save.shipSaveInfo.pauseSaveEntrance = ENTRANCE(STONE_TOWER_TEMPLE, 0);
break;
// Stone Tower Temple (inverted) + Twinmold
case SCENE_INISIE_R:
case SCENE_INISIE_BS:
gSaveContext.save.shipSaveInfo.pauseSaveEntrance = ENTRANCE(STONE_TOWER_TEMPLE_INVERTED, 0);
break;
default:
gSaveContext.save.shipSaveInfo.pauseSaveEntrance = ENTRANCE(SOUTH_CLOCK_TOWN, 0);
break;
}
}
}
extern "C" void SavingEnhancements_ClearSaveEntranceInfo() {
gSaveContext.save.shipSaveInfo.pauseSaveEntrance = -1;
memset(&gSaveContext.save.shipSaveInfo.respawn, 0, sizeof(gSaveContext.save.shipSaveInfo.respawn));
}
extern "C" bool SavingEnhancements_CanSave() {
// Game State
if (gPlayState == NULL || GET_PLAYER(gPlayState) == NULL) {
return false;
}
// Owl saving available
if (!gSaveContext.flashSaveAvailable || gSaveContext.fileNum == 255) {
return false;
}
// Not in a blocking cutscene
if (Player_InBlockingCsMode(gPlayState, GET_PLAYER(gPlayState))) {
return false;
}
// Not in the middle of dialog
if (gPlayState->msgCtx.msgMode != 0) {
return false;
}
// Hasn't gotten to clock town yet
if (gPlayState->sceneId == SCENE_SPOT00 || gPlayState->sceneId == SCENE_LOST_WOODS ||
gPlayState->sceneId == SCENE_OPENINGDAN) {
return false;
}
// Can't save once you've gone to the moon
if (gPlayState->sceneId == SCENE_SOUGEN || gPlayState->sceneId == SCENE_LAST_LINK ||
gPlayState->sceneId == SCENE_LAST_DEKU || gPlayState->sceneId == SCENE_LAST_GORON ||
gPlayState->sceneId == SCENE_LAST_ZORA || gPlayState->sceneId == SCENE_LAST_BS) {
return false;
}
// Not in minigames that set temporary flags
if (CHECK_WEEKEVENTREG(WEEKEVENTREG_08_01) || CHECK_WEEKEVENTREG(WEEKEVENTREG_82_08) ||
CHECK_WEEKEVENTREG(WEEKEVENTREG_90_20) || CHECK_WEEKEVENTREG(WEEKEVENTREG_KICKOUT_WAIT) ||
CHECK_EVENTINF(EVENTINF_34) || CHECK_EVENTINF(EVENTINF_41)) {
return false;
}
return true;
}
extern "C" void SavingEnhancements_AdvancePlaytime() {
if (gSaveContext.save.shipSaveInfo.fileCompletedAt == 0) {
uint64_t timestamp = GetUnixTimestamp();
gSaveContext.save.shipSaveInfo.filePlaytime += timestamp - gSaveContext.shipSaveContext.lastTimeLog;
gSaveContext.shipSaveContext.lastTimeLog = timestamp;
}
}
void DeleteOwlSave() {
// Persist this in case the user is 0th daying
bool currentOwlSaveState = gSaveContext.save.isOwlSave;
// Typically called when loading into an owl save
func_80147314(&gPlayState->sramCtx, gSaveContext.fileNum);
// Set it to not be an owl save so after reloading the save file it doesn't try to load at the owl's position in
// clock town
gSaveContext.save.isOwlSave = currentOwlSaveState;
}
/*
* This respawn data is used for multiple things. Beyond the obvious usage for handling player respawns, this structure
* also maintains state information when entering shared grottos. This code executes from OnSaveLoad, which runs after
* save data is populated. This must run after that, otherwise the RESPAWN_MODE_DOWN entrance would get set to
* ENTR_LOAD_OPENING, which in turn would lead to a crash if the save is within a grotto and the player dies before
* leaving.
*/
void LoadRespawnData(s16 fileNum) {
for (int i = 0; i < RESPAWN_MODE_MAX; i++) {
gSaveContext.respawn[i] = gSaveContext.save.shipSaveInfo.respawn[i];
}
}
/*
* Upon loading a save, skip any cutscenes that would play if the save is from a cutscene entrance (e.g. owl warps, Link
* bowing at Mikau's grave, etc.). An OnPassPlayerInputs hook is used to detect when gameplay actually starts (any
* entrance cutscenes are done), at which point the cutscene skip hook is unregistered. This handles any potential cases
* where multiple cutscenes play in succession.
*/
static void UnregisterEntranceCutsceneSkip() {
if (skipEntranceCutsceneHookId) {
GameInteractor::Instance->UnregisterGameHookForID<GameInteractor::ShouldVanillaBehavior>(
skipEntranceCutsceneHookId);
skipEntranceCutsceneHookId = 0;
}
if (gameplayStartHookId) {
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnPassPlayerInputs>(gameplayStartHookId);
gameplayStartHookId = 0;
}
}
void SkipEntranceCutsceneOnLoad(s16 fileNum) {
// Clean up any existing hooks first
UnregisterEntranceCutsceneSkip();
// Register hook to skip entrance cutscenes - may skip multiple if they chain
skipEntranceCutsceneHookId = REGISTER_VB_SHOULD(VB_START_CUTSCENE, {
// Only skip normal cutscenes
if (gSaveContext.gameMode == GAMEMODE_NORMAL && gPlayState != nullptr && gPlayState->sceneId != SCENE_SPOT00) {
*should = false;
}
});
// Register hook to detect when gameplay starts (all cutscenes done)
// OnPassPlayerInputs only fires during normal gameplay, not during cutscenes
gameplayStartHookId =
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnPassPlayerInputs>([](Input* input) {
// Gameplay has started; any entrance cutscenes are done
// Now unregister both hooks so normal cutscenes can play
UnregisterEntranceCutsceneSkip();
});
}
static RegisterShipInitFunc registerSavingEnhancements(
[]() {
// Prevent deletion of owl saves based on cvar or if this was a pause/auto save
COND_VB_SHOULD(VB_DELETE_OWL_SAVE, true, {
if (CVarGetInteger("gEnhancements.Saving.PersistentOwlSaves", 0) ||
gSaveContext.save.shipSaveInfo.pauseSaveEntrance != -1) {
*should = false;
}
});
COND_HOOK(OnSaveLoad, true, [](s16 fileNum) {
if (gSaveContext.save.shipSaveInfo.fileCreatedAt == 0) {
gSaveContext.save.shipSaveInfo.fileCreatedAt = GetUnixTimestamp();
}
gSaveContext.shipSaveContext.lastTimeLog = GetUnixTimestamp();
lastEntrance = entranceToSave = gSaveContext.save.shipSaveInfo.pauseSaveEntrance;
});
// Owl statue prompt
COND_ID_HOOK(OnOpenText, 0xC01, true,
[](u16* textId, bool* loadFromMessageTable) { SavingEnhancements_AdvancePlaytime(); });
// Finished the game, mark fileCompletedAt accordingly
COND_HOOK(OnGameCompletion, true, []() {
if (gSaveContext.save.shipSaveInfo.fileCompletedAt == 0) {
SavingEnhancements_AdvancePlaytime();
gSaveContext.save.shipSaveInfo.fileCompletedAt = GetUnixTimestamp();
}
});
// When resetting the time cycle or letting the moon crash, an owl save would normally not even
// be present because they are deleted when loading. However, with persistent owl saves or
// pause/auto saves, we need to explicitly delete the owl save here, to let the player load
// into the new cycle or lose their progress on moon crash.
COND_HOOK(BeforeEndOfCycleSave, true, []() {
SavingEnhancements_AdvancePlaytime();
DeleteOwlSave();
});
COND_HOOK(BeforeMoonCrash, true, []() { DeleteOwlSave(); });
// Vanilla has an arbitrary 2 second delay when saving, we can't remove it entirely because
// it's used to pull off certain 0th Day glitches (specifically Any Item as Any Form and Goron Missile),
// because they required you to change forms before the SaveContext is restored in that window. We can
// determine if they are trying to pull this trick off if they are playing song of time and isOwlSave is
// set (which normally shouldn't be). Otherwise remove the delay.
COND_VB_SHOULD(VB_SAVE_DELAY, true, {
if (gPlayState == NULL || gPlayState->msgCtx.msgMode != MSGMODE_NEW_CYCLE_1 ||
!gSaveContext.save.isOwlSave) {
*should = true;
}
});
// In vanilla, isOwlSave determines if the owl save sram path is taken (which restores the SaveContext N seconds
// later) We only want this path to be taken for actual owl saves and when playing Song of Time with isOwlSave
// set (0th Daying). This might seem redundant, but isOwlSave can be set and the player can autosave/pause save.
COND_VB_SHOULD(VB_SAVE_USE_OWL_SAVE_TIMING, true, {
*should = gSaveContext.save.isOwlSave && (gPlayState->msgCtx.msgMode == MSGMODE_NEW_CYCLE_1 ||
gPlayState->msgCtx.msgMode == MSGMODE_OWL_SAVE_1);
});
// These hooks modify the SceneTitleCard messages for day/night on the 0th and 4th days
COND_ID_HOOK(OnOpenText, 0x1BB4, true, [](u16* textId, bool* loadFromMessageTable) {
if (CURRENT_DAY < 1 || CURRENT_DAY > 3) {
auto entry = CustomMessage::LoadVanillaMessageTableEntry(*textId);
// Weird edge case where dawn of the Zeroth day pulls the night entry
if (CURRENT_DAY == 0 && gSaveContext.save.time == CLOCK_TIME(6, 0) - 1) {
CustomMessage::Replace(&entry.msg, "Night of the First", "Dawn of the Zeroth");
} else {
switch (CURRENT_DAY) {
case 0:
CustomMessage::Replace(&entry.msg, "First", "Zeroth");
break;
case 4:
CustomMessage::Replace(&entry.msg, "First", "Fourth");
break;
}
}
CustomMessage::LoadCustomMessageIntoFont(entry);
*loadFromMessageTable = false;
}
});
COND_ID_HOOK(OnOpenText, 0x1BB2, true, [](u16* textId, bool* loadFromMessageTable) {
if (CURRENT_DAY < 1 || CURRENT_DAY > 3) {
auto entry = CustomMessage::LoadVanillaMessageTableEntry(*textId);
switch (CURRENT_DAY) {
case 0:
CustomMessage::Replace(&entry.msg, "First", "Zeroth");
break;
case 4:
CustomMessage::Replace(&entry.msg, "First", "Fourth");
break;
}
CustomMessage::LoadCustomMessageIntoFont(entry);
*loadFromMessageTable = false;
}
});
},
{});
static RegisterShipInitFunc registerRememberSaveLocation(
[]() {
COND_VB_SHOULD(VB_PLAY_TRANSITION_CS, CVAR_REMEMBER_SAVE_LOCATION, {
/*
* Update the entrance to save, unless we're leaving a grotto. Grottos exit to entrance 0 of the destination
* scene and adjust the position manually. In effect, there is no real entrance to target for loading
* purposes, so we just load into the last grotto instead under those circumstances.
*/
if (lastEntrance != -1 && !(Entrance_GetSceneIdAbsolute(gSaveContext.save.entrance) != SCENE_KAKUSIANA &&
Entrance_GetSceneIdAbsolute(lastEntrance) == SCENE_KAKUSIANA)) {
entranceToSave = gSaveContext.save.entrance;
}
lastEntrance = gSaveContext.save.entrance;
});
COND_HOOK(OnSaveLoad, CVAR_REMEMBER_SAVE_LOCATION, SkipEntranceCutsceneOnLoad);
COND_HOOK(OnSaveLoad, CVAR_REMEMBER_SAVE_LOCATION, LoadRespawnData);
},
{ CVAR_REMEMBER_SAVE_LOCATION_NAME });
|