sm64coopdx/src/game/save_file.c
MysterD 4a2c218f11 Synchronized save files
Now when a client joins, it will request the entire 512 byte EEPROM from
the server and refuse to continue until the server replies with the
contents. Then the client will override all reads and writes to the
EEPROM/save file. Thus, a client will never overwrite their local save.

Fixes #21
2020-09-04 15:07:32 -07:00

773 lines
No EOL
24 KiB
C

#include <ultra64.h>
#include "sm64.h"
#include "game_init.h"
#include "main.h"
#include "engine/math_util.h"
#include "area.h"
#include "level_update.h"
#include "save_file.h"
#include "sound_init.h"
#include "level_table.h"
#include "course_table.h"
#include "thread6.h"
#include "macros.h"
#include "pc/ini.h"
#include "pc/network/network.h"
#define MENU_DATA_MAGIC 0x4849
#define SAVE_FILE_MAGIC 0x4441
STATIC_ASSERT(sizeof(struct SaveBuffer) == EEPROM_SIZE, "eeprom buffer size must match");
extern struct SaveBuffer gSaveBuffer;
struct WarpCheckpoint gWarpCheckpoint;
s8 gMainMenuDataModified;
s8 gSaveFileModified;
u8 gLastCompletedCourseNum = COURSE_NONE;
u8 gLastCompletedStarNum = 0;
s8 sUnusedGotGlobalCoinHiScore = 0;
u8 gGotFileCoinHiScore = 0;
u8 gCurrCourseStarFlags = 0;
u8 gSpecialTripleJump = 0;
#define STUB_LEVEL(_0, _1, courseenum, _3, _4, _5, _6, _7, _8) courseenum,
#define DEFINE_LEVEL(_0, _1, courseenum, _3, _4, _5, _6, _7, _8, _9, _10) courseenum,
s8 gLevelToCourseNumTable[] = {
#include "levels/level_defines.h"
};
#undef STUB_LEVEL
#undef DEFINE_LEVEL
STATIC_ASSERT(ARRAY_COUNT(gLevelToCourseNumTable) == LEVEL_COUNT - 1,
"change this array if you are adding levels");
#ifdef TEXTSAVES
#include "text_save.inc.h"
#endif
// This was probably used to set progress to 100% for debugging, but
// it was removed from the release ROM.
static void stub_save_file_1(void) {
UNUSED s32 pad;
}
/**
* Byteswap all multibyte fields in a SaveBlockSignature.
*/
static inline void bswap_signature(struct SaveBlockSignature *data) {
data->magic = BSWAP16(data->magic);
data->chksum = BSWAP16(data->chksum); // valid as long as the checksum is a literal sum
}
/**
* Byteswap all multibyte fields in a MainMenuSaveData.
*/
static inline void bswap_menudata(struct MainMenuSaveData *data) {
for (int i = 0; i < NUM_SAVE_FILES; ++i)
data->coinScoreAges[i] = BSWAP32(data->coinScoreAges[i]);
data->soundMode = BSWAP16(data->soundMode);
#ifdef VERSION_EU
data->language = BSWAP16(data->language);
#endif
bswap_signature(&data->signature);
}
/**
* Byteswap all multibyte fields in a SaveFile.
*/
static inline void bswap_savefile(struct SaveFile *data) {
data->capPos[0] = BSWAP16(data->capPos[0]);
data->capPos[1] = BSWAP16(data->capPos[1]);
data->capPos[2] = BSWAP16(data->capPos[2]);
data->flags = BSWAP32(data->flags);
bswap_signature(&data->signature);
}
/**
* Read from EEPROM to a given address.
* The EEPROM address is computed using the offset of the destination address from gSaveBuffer.
* Try at most 4 times, and return 0 on success. On failure, return the status returned from
* osEepromLongRead. It also returns 0 if EEPROM isn't loaded correctly in the system.
*/
static s32 read_eeprom_data(void *buffer, s32 size) {
s32 status = 0;
if (gEepromProbe != 0) {
s32 triesLeft = 4;
u32 offset = (u32)((u8 *) buffer - (u8 *) &gSaveBuffer) / 8;
do {
block_until_rumble_pak_free();
triesLeft--;
status = osEepromLongRead(&gSIEventMesgQueue, offset, buffer, size);
release_rumble_pak_control();
} while (triesLeft > 0 && status != 0);
}
return status;
}
/**
* Write data to EEPROM.
* The EEPROM address was originally computed using the offset of the source address from gSaveBuffer.
* Try at most 4 times, and return 0 on success. On failure, return the status returned from
* osEepromLongWrite. Unlike read_eeprom_data, return 1 if EEPROM isn't loaded.
*/
static s32 write_eeprom_data(void *buffer, s32 size, const uintptr_t baseofs) {
s32 status = 1;
if (gEepromProbe != 0) {
s32 triesLeft = 4;
u32 offset = (u32)baseofs >> 3;
do {
block_until_rumble_pak_free();
triesLeft--;
status = osEepromLongWrite(&gSIEventMesgQueue, offset, buffer, size);
release_rumble_pak_control();
} while (triesLeft > 0 && status != 0);
}
return status;
}
/**
* Wrappers that byteswap the data on LE platforms before writing it to 'EEPROM'
*/
static inline s32 write_eeprom_savefile(const u32 file, const u32 slot, const u32 num) {
// calculate the EEPROM address using the file number and slot
const uintptr_t ofs = (u8*)&gSaveBuffer.files[file][slot] - (u8*)&gSaveBuffer;
#if IS_BIG_ENDIAN
return write_eeprom_data(&gSaveBuffer.files[file][slot], num * sizeof(struct SaveFile), ofs);
#else
// byteswap the data and then write it
struct SaveFile sf[num];
bcopy(&gSaveBuffer.files[file][slot], sf, num * sizeof(sf[0]));
for (u32 i = 0; i < num; ++i) bswap_savefile(&sf[i]);
return write_eeprom_data(&sf, sizeof(sf), ofs);
#endif
}
static inline s32 write_eeprom_menudata(const u32 slot, const u32 num) {
// calculate the EEPROM address using the slot
const uintptr_t ofs = (u8*)&gSaveBuffer.menuData[slot] - (u8*)&gSaveBuffer;
#if IS_BIG_ENDIAN
return write_eeprom_data(&gSaveBuffer.menuData[slot], num * sizeof(struct MainMenuSaveData), ofs);
#else
// byteswap the data and then write it
struct MainMenuSaveData md[num];
bcopy(&gSaveBuffer.menuData[slot], md, num * sizeof(md[0]));
for (u32 i = 0; i < num; ++i) bswap_menudata(&md[i]);
return write_eeprom_data(&md, sizeof(md), ofs);
#endif
}
/**
* Sum the bytes in data to data + size - 2. The last two bytes are ignored
* because that is where the checksum is stored.
*/
static u16 calc_checksum(u8 *data, s32 size) {
u16 chksum = 0;
while (size-- > 2) {
chksum += *data++;
}
return chksum;
}
/**
* Verify the signature at the end of the block to check if the data is valid.
*/
static s32 verify_save_block_signature(void *buffer, s32 size, u16 magic) {
struct SaveBlockSignature *sig = (struct SaveBlockSignature *) ((size - 4) + (u8 *) buffer);
if (sig->magic != magic) {
return FALSE;
}
if (sig->chksum != calc_checksum(buffer, size)) {
return FALSE;
}
return TRUE;
}
/**
* Write a signature at the end of the block to make sure the data is valid
*/
static void add_save_block_signature(void *buffer, s32 size, u16 magic) {
struct SaveBlockSignature *sig = (struct SaveBlockSignature *) ((size - 4) + (u8 *) buffer);
sig->magic = magic;
sig->chksum = calc_checksum(buffer, size);
}
/**
* Copy main menu data from one backup slot to the other slot.
*/
static void restore_main_menu_data(s32 srcSlot) {
s32 destSlot = srcSlot ^ 1;
// Compute checksum on source data
add_save_block_signature(&gSaveBuffer.menuData[srcSlot], sizeof(gSaveBuffer.menuData[srcSlot]), MENU_DATA_MAGIC);
// Copy source data to destination
bcopy(&gSaveBuffer.menuData[srcSlot], &gSaveBuffer.menuData[destSlot], sizeof(gSaveBuffer.menuData[destSlot]));
// Write destination data to EEPROM
write_eeprom_menudata(destSlot, 1);
}
static void save_main_menu_data(void) {
if (gMainMenuDataModified) {
// Compute checksum
add_save_block_signature(&gSaveBuffer.menuData[0], sizeof(gSaveBuffer.menuData[0]), MENU_DATA_MAGIC);
// Back up data
bcopy(&gSaveBuffer.menuData[0], &gSaveBuffer.menuData[1], sizeof(gSaveBuffer.menuData[1]));
// Write to EEPROM
write_eeprom_menudata(0, 2);
gMainMenuDataModified = FALSE;
}
}
static void wipe_main_menu_data(void) {
bzero(&gSaveBuffer.menuData[0], sizeof(gSaveBuffer.menuData[0]));
// Set score ages for all courses to 3, 2, 1, and 0, respectively.
gSaveBuffer.menuData[0].coinScoreAges[0] = 0x3FFFFFFF;
gSaveBuffer.menuData[0].coinScoreAges[1] = 0x2AAAAAAA;
gSaveBuffer.menuData[0].coinScoreAges[2] = 0x15555555;
gMainMenuDataModified = TRUE;
save_main_menu_data();
}
static s32 get_coin_score_age(s32 fileIndex, s32 courseIndex) {
return (gSaveBuffer.menuData[0].coinScoreAges[fileIndex] >> (2 * courseIndex)) & 0x3;
}
static void set_coin_score_age(s32 fileIndex, s32 courseIndex, s32 age) {
s32 mask = 0x3 << (2 * courseIndex);
gSaveBuffer.menuData[0].coinScoreAges[fileIndex] &= ~mask;
gSaveBuffer.menuData[0].coinScoreAges[fileIndex] |= age << (2 * courseIndex);
}
/**
* Mark a coin score for a save file as the newest out of all save files.
*/
static void touch_coin_score_age(s32 fileIndex, s32 courseIndex) {
s32 i;
u32 age;
u32 currentAge = get_coin_score_age(fileIndex, courseIndex);
if (currentAge != 0) {
for (i = 0; i < NUM_SAVE_FILES; i++) {
age = get_coin_score_age(i, courseIndex);
if (age < currentAge) {
set_coin_score_age(i, courseIndex, age + 1);
}
}
set_coin_score_age(fileIndex, courseIndex, 0);
gMainMenuDataModified = TRUE;
}
}
/**
* Mark all coin scores for a save file as new.
*/
static void touch_high_score_ages(s32 fileIndex) {
s32 i;
for (i = 0; i < 15; i++) {
touch_coin_score_age(fileIndex, i);
}
}
/**
* Copy save file data from one backup slot to the other slot.
*/
static void restore_save_file_data(s32 fileIndex, s32 srcSlot) {
s32 destSlot = srcSlot ^ 1;
// Compute checksum on source data
add_save_block_signature(&gSaveBuffer.files[fileIndex][srcSlot],
sizeof(gSaveBuffer.files[fileIndex][srcSlot]), SAVE_FILE_MAGIC);
// Copy source data to destination slot
bcopy(&gSaveBuffer.files[fileIndex][srcSlot], &gSaveBuffer.files[fileIndex][destSlot],
sizeof(gSaveBuffer.files[fileIndex][destSlot]));
// Write destination data to EEPROM
write_eeprom_savefile(fileIndex, destSlot, 1);
}
/**
* Check if the 'EEPROM' save has different endianness (e.g. it's from an actual N64).
*/
static u8 save_file_need_bswap(const struct SaveBuffer *buf) {
// check all signatures just in case
for (int i = 0; i < 2; ++i) {
if (buf->menuData[i].signature.magic == BSWAP16(MENU_DATA_MAGIC))
return TRUE;
for (int j = 0; j < NUM_SAVE_FILES; ++j) {
if (buf->files[j][i].signature.magic == BSWAP16(SAVE_FILE_MAGIC))
return TRUE;
}
}
return FALSE;
}
/**
* Byteswap all multibyte fields in a SaveBuffer.
*/
static void save_file_bswap(struct SaveBuffer *buf) {
bswap_menudata(buf->menuData + 0);
bswap_menudata(buf->menuData + 1);
for (int i = 0; i < NUM_SAVE_FILES; ++i) {
bswap_savefile(buf->files[i] + 0);
bswap_savefile(buf->files[i] + 1);
}
}
void save_file_do_save(s32 fileIndex) {
if (fileIndex < 0 || fileIndex >= NUM_SAVE_FILES)
return;
#ifdef TEXTSAVES
if (gSaveFileModified && networkType != NT_CLIENT) {
// Write to text file
write_text_save(fileIndex);
gSaveFileModified = FALSE;
gMainMenuDataModified = FALSE;
return;
}
#endif
if (gSaveFileModified) {
// Compute checksum
add_save_block_signature(&gSaveBuffer.files[fileIndex][0],
sizeof(gSaveBuffer.files[fileIndex][0]), SAVE_FILE_MAGIC);
// Copy to backup slot
bcopy(&gSaveBuffer.files[fileIndex][0], &gSaveBuffer.files[fileIndex][1],
sizeof(gSaveBuffer.files[fileIndex][1]));
// Write to EEPROM
write_eeprom_savefile(fileIndex, 0, 2);
gSaveFileModified = FALSE;
}
save_main_menu_data();
}
void save_file_erase(s32 fileIndex) {
if (fileIndex < 0 || fileIndex >= NUM_SAVE_FILES)
return;
touch_high_score_ages(fileIndex);
bzero(&gSaveBuffer.files[fileIndex][0], sizeof(gSaveBuffer.files[fileIndex][0]));
gSaveFileModified = TRUE;
save_file_do_save(fileIndex);
}
//! Needs to be s32 to match on -O2, despite no return value.
BAD_RETURN(s32) save_file_copy(s32 srcFileIndex, s32 destFileIndex) {
if (srcFileIndex < 0 || srcFileIndex >= NUM_SAVE_FILES || destFileIndex < 0 || destFileIndex >= NUM_SAVE_FILES)
return;
touch_high_score_ages(destFileIndex);
bcopy(&gSaveBuffer.files[srcFileIndex][0], &gSaveBuffer.files[destFileIndex][0],
sizeof(gSaveBuffer.files[destFileIndex][0]));
gSaveFileModified = TRUE;
save_file_do_save(destFileIndex);
}
#ifdef TEXTSAVES
static void save_file_load_textsaves(void) {
for (file = 0; file < NUM_SAVE_FILES; file++) {
read_text_save(file);
}
gSaveFileModified = TRUE;
gMainMenuDataModified = TRUE;
stub_save_file_1();
}
#endif
void save_file_load_all(u8 reload) {
s32 file;
gMainMenuDataModified = FALSE;
gSaveFileModified = FALSE;
bzero(&gSaveBuffer, sizeof(gSaveBuffer));
#ifdef TEXTSAVES
if (!reload) {
save_file_load_textsaves();
return;
}
#endif
read_eeprom_data(&gSaveBuffer, sizeof(gSaveBuffer));
if (save_file_need_bswap(&gSaveBuffer))
save_file_bswap(&gSaveBuffer);
// Verify the main menu data and create a backup copy if only one of the slots is valid.
s32 validSlots;
validSlots = verify_save_block_signature(&gSaveBuffer.menuData[0], sizeof(gSaveBuffer.menuData[0]), MENU_DATA_MAGIC);
validSlots |= verify_save_block_signature(&gSaveBuffer.menuData[1], sizeof(gSaveBuffer.menuData[1]),MENU_DATA_MAGIC) << 1;
switch (validSlots) {
case 0: // Neither copy is correct
wipe_main_menu_data();
break;
case 1: // Slot 0 is correct and slot 1 is incorrect
restore_main_menu_data(0);
break;
case 2: // Slot 1 is correct and slot 0 is incorrect
restore_main_menu_data(1);
break;
}
for (file = 0; file < NUM_SAVE_FILES; file++) {
// Verify the save file and create a backup copy if only one of the slots is valid.
validSlots = verify_save_block_signature(&gSaveBuffer.files[file][0], sizeof(gSaveBuffer.files[file][0]), SAVE_FILE_MAGIC);
validSlots |= verify_save_block_signature(&gSaveBuffer.files[file][1], sizeof(gSaveBuffer.files[file][1]), SAVE_FILE_MAGIC) << 1;
switch (validSlots) {
case 0: // Neither copy is correct
save_file_erase(file);
break;
case 1: // Slot 0 is correct and slot 1 is incorrect
restore_save_file_data(file, 0);
break;
case 2: // Slot 1 is correct and slot 0 is incorrect
restore_save_file_data(file, 1);
break;
}
}
stub_save_file_1();
}
/**
* Reload the current save file from its backup copy, which is effectively a
* a cached copy of what has been written to EEPROM.
* This is used after getting a game over.
*/
void save_file_reload(void) {
// Copy save file data from backup
bcopy(&gSaveBuffer.files[gCurrSaveFileNum - 1][1], &gSaveBuffer.files[gCurrSaveFileNum - 1][0],
sizeof(gSaveBuffer.files[gCurrSaveFileNum - 1][0]));
// Copy main menu data from backup
bcopy(&gSaveBuffer.menuData[1], &gSaveBuffer.menuData[0], sizeof(gSaveBuffer.menuData[0]));
gMainMenuDataModified = FALSE;
gSaveFileModified = FALSE;
}
/**
* Update the current save file after collecting a star or a key.
* If coin score is greater than the current high score, update it.
*/
void save_file_collect_star_or_key(s16 coinScore, s16 starIndex) {
s32 fileIndex = gCurrSaveFileNum - 1;
s32 courseIndex = gCurrCourseNum - 1;
s32 starFlag = 1 << starIndex;
UNUSED s32 flags = save_file_get_flags();
gLastCompletedCourseNum = courseIndex + 1;
gLastCompletedStarNum = starIndex + 1;
sUnusedGotGlobalCoinHiScore = 0;
gGotFileCoinHiScore = 0;
if (courseIndex >= 0 && courseIndex < COURSE_STAGES_COUNT) {
//! Compares the coin score as a 16 bit value, but only writes the 8 bit
// truncation. This can allow a high score to decrease.
if (coinScore > ((u16) save_file_get_max_coin_score(courseIndex) & 0xFFFF)) {
sUnusedGotGlobalCoinHiScore = 1;
}
if (coinScore > save_file_get_course_coin_score(fileIndex, courseIndex)) {
gSaveBuffer.files[fileIndex][0].courseCoinScores[courseIndex] = coinScore;
touch_coin_score_age(fileIndex, courseIndex);
gGotFileCoinHiScore = 1;
gSaveFileModified = TRUE;
}
}
switch (gCurrLevelNum) {
case LEVEL_BOWSER_1:
if (!(save_file_get_flags() & (SAVE_FLAG_HAVE_KEY_1 | SAVE_FLAG_UNLOCKED_BASEMENT_DOOR))) {
save_file_set_flags(SAVE_FLAG_HAVE_KEY_1);
}
break;
case LEVEL_BOWSER_2:
if (!(save_file_get_flags() & (SAVE_FLAG_HAVE_KEY_2 | SAVE_FLAG_UNLOCKED_UPSTAIRS_DOOR))) {
save_file_set_flags(SAVE_FLAG_HAVE_KEY_2);
}
break;
case LEVEL_BOWSER_3:
break;
default:
if (!(save_file_get_star_flags(fileIndex, courseIndex) & starFlag)) {
save_file_set_star_flags(fileIndex, courseIndex, starFlag);
}
break;
}
}
s32 save_file_exists(s32 fileIndex) {
return (gSaveBuffer.files[fileIndex][0].flags & SAVE_FLAG_FILE_EXISTS) != 0;
}
/**
* Get the maximum coin score across all files for a course. The lower 16 bits
* of the returned value are the score, and the upper 16 bits are the file number
* of the save file with this score.
*/
u32 save_file_get_max_coin_score(s32 courseIndex) {
s32 fileIndex;
s32 maxCoinScore = -1;
s32 maxScoreAge = -1;
s32 maxScoreFileNum = 0;
for (fileIndex = 0; fileIndex < NUM_SAVE_FILES; fileIndex++) {
if (save_file_get_star_flags(fileIndex, courseIndex) != 0) {
s32 coinScore = save_file_get_course_coin_score(fileIndex, courseIndex);
s32 scoreAge = get_coin_score_age(fileIndex, courseIndex);
if (coinScore > maxCoinScore || (coinScore == maxCoinScore && scoreAge > maxScoreAge)) {
maxCoinScore = coinScore;
maxScoreAge = scoreAge;
maxScoreFileNum = fileIndex + 1;
}
}
}
return (maxScoreFileNum << 16) + max(maxCoinScore, 0);
}
s32 save_file_get_course_star_count(s32 fileIndex, s32 courseIndex) {
s32 i;
s32 count = 0;
u8 flag = 1;
u8 starFlags = save_file_get_star_flags(fileIndex, courseIndex);
for (i = 0; i < 7; i++, flag <<= 1) {
if (starFlags & flag) {
count++;
}
}
return count;
}
s32 save_file_get_total_star_count(s32 fileIndex, s32 minCourse, s32 maxCourse) {
s32 count = 0;
// Get standard course star count.
for (; minCourse <= maxCourse; minCourse++) {
count += save_file_get_course_star_count(fileIndex, minCourse);
}
// Add castle secret star count.
return save_file_get_course_star_count(fileIndex, -1) + count;
}
void save_file_set_flags(u32 flags) {
gSaveBuffer.files[gCurrSaveFileNum - 1][0].flags |= (flags | SAVE_FLAG_FILE_EXISTS);
gSaveFileModified = TRUE;
}
void save_file_clear_flags(u32 flags) {
gSaveBuffer.files[gCurrSaveFileNum - 1][0].flags &= ~flags;
gSaveBuffer.files[gCurrSaveFileNum - 1][0].flags |= SAVE_FLAG_FILE_EXISTS;
gSaveFileModified = TRUE;
}
u32 save_file_get_flags(void) {
if (gCurrCreditsEntry != 0 || gCurrDemoInput != NULL) {
return 0;
}
return gSaveBuffer.files[gCurrSaveFileNum - 1][0].flags;
}
/**
* Return the bitset of obtained stars in the specified course.
* If course is -1, return the bitset of obtained castle secret stars.
*/
u32 save_file_get_star_flags(s32 fileIndex, s32 courseIndex) {
u32 starFlags;
if (courseIndex == -1) {
starFlags = (gSaveBuffer.files[fileIndex][0].flags >> 24) & 0x7F;
} else {
starFlags = gSaveBuffer.files[fileIndex][0].courseStars[courseIndex] & 0x7F;
}
return starFlags;
}
/**
* Add to the bitset of obtained stars in the specified course.
* If course is -1, add to the bitset of obtained castle secret stars.
*/
void save_file_set_star_flags(s32 fileIndex, s32 courseIndex, u32 starFlags) {
if (courseIndex == -1) {
gSaveBuffer.files[fileIndex][0].flags |= starFlags << 24;
} else {
gSaveBuffer.files[fileIndex][0].courseStars[courseIndex] |= starFlags;
}
gSaveBuffer.files[fileIndex][0].flags |= SAVE_FLAG_FILE_EXISTS;
gSaveFileModified = TRUE;
}
s32 save_file_get_course_coin_score(s32 fileIndex, s32 courseIndex) {
return gSaveBuffer.files[fileIndex][0].courseCoinScores[courseIndex];
}
/**
* Return TRUE if the cannon is unlocked in the current course.
*/
s32 save_file_is_cannon_unlocked(void) {
return (gSaveBuffer.files[gCurrSaveFileNum - 1][0].courseStars[gCurrCourseNum] & 0x80) != 0;
}
/**
* Sets the cannon status to unlocked in the current course.
*/
void save_file_set_cannon_unlocked(void) {
gSaveBuffer.files[gCurrSaveFileNum - 1][0].courseStars[gCurrCourseNum] |= 0x80;
gSaveBuffer.files[gCurrSaveFileNum - 1][0].flags |= SAVE_FLAG_FILE_EXISTS;
gSaveFileModified = TRUE;
}
void save_file_set_cap_pos(s16 x, s16 y, s16 z) {
struct SaveFile *saveFile = &gSaveBuffer.files[gCurrSaveFileNum - 1][0];
saveFile->capLevel = gCurrLevelNum;
saveFile->capArea = gCurrAreaIndex;
vec3s_set(saveFile->capPos, x, y, z);
save_file_set_flags(SAVE_FLAG_CAP_ON_GROUND);
}
s32 save_file_get_cap_pos(Vec3s capPos) {
struct SaveFile *saveFile = &gSaveBuffer.files[gCurrSaveFileNum - 1][0];
s32 flags = save_file_get_flags();
if (saveFile->capLevel == gCurrLevelNum && saveFile->capArea == gCurrAreaIndex
&& (flags & SAVE_FLAG_CAP_ON_GROUND)) {
vec3s_copy(capPos, saveFile->capPos);
return TRUE;
}
return FALSE;
}
void save_file_set_sound_mode(u16 mode) {
set_sound_mode(mode);
gSaveBuffer.menuData[0].soundMode = mode;
gMainMenuDataModified = TRUE;
save_main_menu_data();
}
u16 save_file_get_sound_mode(void) {
return gSaveBuffer.menuData[0].soundMode;
}
void save_file_move_cap_to_default_location(void) {
if (save_file_get_flags() & SAVE_FLAG_CAP_ON_GROUND) {
switch (gSaveBuffer.files[gCurrSaveFileNum - 1][0].capLevel) {
case LEVEL_SSL:
save_file_set_flags(SAVE_FLAG_CAP_ON_KLEPTO);
break;
case LEVEL_SL:
save_file_set_flags(SAVE_FLAG_CAP_ON_MR_BLIZZARD);
break;
case LEVEL_TTM:
save_file_set_flags(SAVE_FLAG_CAP_ON_UKIKI);
break;
}
save_file_clear_flags(SAVE_FLAG_CAP_ON_GROUND);
}
}
#ifdef VERSION_EU
void eu_set_language(u16 language) {
gSaveBuffer.menuData[0].language = language;
gMainMenuDataModified = TRUE;
save_main_menu_data();
}
u16 eu_get_language(void) {
// check if the language is in range, in case we loaded a US save with garbage padding or something
if (gSaveBuffer.menuData[0].language >= LANGUAGE_MAX)
eu_set_language(LANGUAGE_ENGLISH); // reset it to english if not
return gSaveBuffer.menuData[0].language;
}
#endif
void disable_warp_checkpoint(void) {
// check_warp_checkpoint() checks to see if gWarpCheckpoint.courseNum != COURSE_NONE
gWarpCheckpoint.courseNum = COURSE_NONE;
}
/**
* Checks the upper bit of the WarpNode->destLevel byte to see if the
* game should set a warp checkpoint.
*/
void check_if_should_set_warp_checkpoint(struct WarpNode *warpNode) {
if (warpNode->destLevel & 0x80) {
// Overwrite the warp checkpoint variables.
gWarpCheckpoint.actNum = gCurrActNum;
gWarpCheckpoint.courseNum = gCurrCourseNum;
gWarpCheckpoint.levelID = warpNode->destLevel & 0x7F;
gWarpCheckpoint.areaNum = warpNode->destArea;
gWarpCheckpoint.warpNode = warpNode->destNode;
}
}
/**
* Checks to see if a checkpoint is properly active or not. This will
* also update the level, area, and destination node of the input WarpNode.
* returns TRUE if input WarpNode was updated, and FALSE if not.
*/
s32 check_warp_checkpoint(struct WarpNode *warpNode) {
s16 isWarpCheckpointActive = FALSE;
s16 currCourseNum = gLevelToCourseNumTable[(warpNode->destLevel & 0x7F) - 1];
// gSavedCourseNum is only used in this function.
if (gWarpCheckpoint.courseNum != COURSE_NONE && gSavedCourseNum == currCourseNum
&& gWarpCheckpoint.actNum == gCurrActNum) {
warpNode->destLevel = gWarpCheckpoint.levelID;
warpNode->destArea = gWarpCheckpoint.areaNum;
warpNode->destNode = gWarpCheckpoint.warpNode;
isWarpCheckpointActive = TRUE;
} else {
// Disable the warp checkpoint just in case the other 2 conditions failed?
gWarpCheckpoint.courseNum = COURSE_NONE;
}
return isWarpCheckpointActive;
}