save_file.c (21008B)
1 #include <ultra64.h> 2 3 #include "sm64.h" 4 #include "game_init.h" 5 #include "main.h" 6 #include "engine/math_util.h" 7 #include "area.h" 8 #include "level_update.h" 9 #include "save_file.h" 10 #include "sound_init.h" 11 #include "level_table.h" 12 #include "course_table.h" 13 #include "rumble_init.h" 14 15 #define MENU_DATA_MAGIC 0x4849 16 #define SAVE_FILE_MAGIC 0x4441 17 18 STATIC_ASSERT(sizeof(struct SaveBuffer) == EEPROM_SIZE, "eeprom buffer size must match"); 19 20 extern struct SaveBuffer gSaveBuffer; 21 22 struct WarpCheckpoint gWarpCheckpoint; 23 24 s8 gMainMenuDataModified; 25 s8 gSaveFileModified; 26 27 u8 gLastCompletedCourseNum = COURSE_NONE; 28 u8 gLastCompletedStarNum = 0; 29 s8 sUnusedGotGlobalCoinHiScore = FALSE; 30 u8 gGotFileCoinHiScore = FALSE; 31 u8 gCurrCourseStarFlags = 0; 32 33 u8 gSpecialTripleJump = FALSE; 34 35 #define STUB_LEVEL(_0, _1, courseenum, _3, _4, _5, _6, _7, _8) courseenum, 36 #define DEFINE_LEVEL(_0, _1, courseenum, _3, _4, _5, _6, _7, _8, _9, _10) courseenum, 37 38 s8 gLevelToCourseNumTable[] = { 39 #include "levels/level_defines.h" 40 }; 41 #undef STUB_LEVEL 42 #undef DEFINE_LEVEL 43 44 STATIC_ASSERT(ARRAY_COUNT(gLevelToCourseNumTable) == LEVEL_COUNT - 1, 45 "change this array if you are adding levels"); 46 47 // This was probably used to set progress to 100% for debugging, but 48 // it was removed from the release ROM. 49 static void stub_save_file_1(void) { 50 UNUSED u8 filler[4]; 51 } 52 53 /** 54 * Read from EEPROM to a given address. 55 * The EEPROM address is computed using the offset of the destination address from gSaveBuffer. 56 * Try at most 4 times, and return 0 on success. On failure, return the status returned from 57 * osEepromLongRead. It also returns 0 if EEPROM isn't loaded correctly in the system. 58 */ 59 static s32 read_eeprom_data(void *buffer, s32 size) { 60 s32 status = 0; 61 62 if (gEepromProbe != 0) { 63 s32 triesLeft = 4; 64 u32 offset = (u32)((u8 *) buffer - (u8 *) &gSaveBuffer) / 8; 65 66 do { 67 #if ENABLE_RUMBLE 68 block_until_rumble_pak_free(); 69 #endif 70 triesLeft--; 71 status = osEepromLongRead(&gSIEventMesgQueue, offset, buffer, size); 72 #if ENABLE_RUMBLE 73 release_rumble_pak_control(); 74 #endif 75 } while (triesLeft > 0 && status != 0); 76 } 77 78 return status; 79 } 80 81 /** 82 * Write data to EEPROM. 83 * The EEPROM address is computed using the offset of the source address from gSaveBuffer. 84 * Try at most 4 times, and return 0 on success. On failure, return the status returned from 85 * osEepromLongWrite. Unlike read_eeprom_data, return 1 if EEPROM isn't loaded. 86 */ 87 static s32 write_eeprom_data(void *buffer, s32 size) { 88 s32 status = 1; 89 90 if (gEepromProbe != 0) { 91 s32 triesLeft = 4; 92 u32 offset = (u32)((u8 *) buffer - (u8 *) &gSaveBuffer) >> 3; 93 94 do { 95 #if ENABLE_RUMBLE 96 block_until_rumble_pak_free(); 97 #endif 98 triesLeft--; 99 status = osEepromLongWrite(&gSIEventMesgQueue, offset, buffer, size); 100 #if ENABLE_RUMBLE 101 release_rumble_pak_control(); 102 #endif 103 } while (triesLeft > 0 && status != 0); 104 } 105 106 return status; 107 } 108 109 /** 110 * Sum the bytes in data to data + size - 2. The last two bytes are ignored 111 * because that is where the checksum is stored. 112 */ 113 static u16 calc_checksum(u8 *data, s32 size) { 114 u16 chksum = 0; 115 116 while (size-- > 2) { 117 chksum += *data++; 118 } 119 return chksum; 120 } 121 122 /** 123 * Verify the signature at the end of the block to check if the data is valid. 124 */ 125 static s32 verify_save_block_signature(void *buffer, s32 size, u16 magic) { 126 struct SaveBlockSignature *sig = (struct SaveBlockSignature *) ((size - 4) + (u8 *) buffer); 127 128 if (sig->magic != magic) { 129 return FALSE; 130 } 131 if (sig->chksum != calc_checksum(buffer, size)) { 132 return FALSE; 133 } 134 return TRUE; 135 } 136 137 /** 138 * Write a signature at the end of the block to make sure the data is valid 139 */ 140 static void add_save_block_signature(void *buffer, s32 size, u16 magic) { 141 struct SaveBlockSignature *sig = (struct SaveBlockSignature *) ((size - 4) + (u8 *) buffer); 142 143 sig->magic = magic; 144 sig->chksum = calc_checksum(buffer, size); 145 } 146 147 /** 148 * Copy main menu data from one backup slot to the other slot. 149 */ 150 static void restore_main_menu_data(s32 srcSlot) { 151 s32 destSlot = srcSlot ^ 1; 152 153 // Compute checksum on source data 154 add_save_block_signature(&gSaveBuffer.menuData[srcSlot], sizeof(gSaveBuffer.menuData[srcSlot]), MENU_DATA_MAGIC); 155 156 // Copy source data to destination 157 bcopy(&gSaveBuffer.menuData[srcSlot], &gSaveBuffer.menuData[destSlot], sizeof(gSaveBuffer.menuData[destSlot])); 158 159 // Write destination data to EEPROM 160 write_eeprom_data(&gSaveBuffer.menuData[destSlot], sizeof(gSaveBuffer.menuData[destSlot])); 161 } 162 163 static void save_main_menu_data(void) { 164 if (gMainMenuDataModified) { 165 // Compute checksum 166 add_save_block_signature(&gSaveBuffer.menuData[0], sizeof(gSaveBuffer.menuData[0]), MENU_DATA_MAGIC); 167 168 // Back up data 169 bcopy(&gSaveBuffer.menuData[0], &gSaveBuffer.menuData[1], sizeof(gSaveBuffer.menuData[1])); 170 171 // Write to EEPROM 172 write_eeprom_data(gSaveBuffer.menuData, sizeof(gSaveBuffer.menuData)); 173 174 gMainMenuDataModified = FALSE; 175 } 176 } 177 178 static void wipe_main_menu_data(void) { 179 bzero(&gSaveBuffer.menuData[0], sizeof(gSaveBuffer.menuData[0])); 180 181 // Set score ages for all courses to 3, 2, 1, and 0, respectively. 182 gSaveBuffer.menuData[0].coinScoreAges[0] = 0x3FFFFFFF; 183 gSaveBuffer.menuData[0].coinScoreAges[1] = 0x2AAAAAAA; 184 gSaveBuffer.menuData[0].coinScoreAges[2] = 0x15555555; 185 186 gMainMenuDataModified = TRUE; 187 save_main_menu_data(); 188 } 189 190 static s32 get_coin_score_age(s32 fileIndex, s32 courseIndex) { 191 return (gSaveBuffer.menuData[0].coinScoreAges[fileIndex] >> (2 * courseIndex)) & 0x3; 192 } 193 194 static void set_coin_score_age(s32 fileIndex, s32 courseIndex, s32 age) { 195 s32 mask = 0x3 << (2 * courseIndex); 196 197 gSaveBuffer.menuData[0].coinScoreAges[fileIndex] &= ~mask; 198 gSaveBuffer.menuData[0].coinScoreAges[fileIndex] |= age << (2 * courseIndex); 199 } 200 201 /** 202 * Mark a coin score for a save file as the newest out of all save files. 203 */ 204 static void touch_coin_score_age(s32 fileIndex, s32 courseIndex) { 205 s32 i; 206 u32 age; 207 u32 currentAge = get_coin_score_age(fileIndex, courseIndex); 208 209 if (currentAge != 0) { 210 for (i = 0; i < NUM_SAVE_FILES; i++) { 211 age = get_coin_score_age(i, courseIndex); 212 if (age < currentAge) { 213 set_coin_score_age(i, courseIndex, age + 1); 214 } 215 } 216 217 set_coin_score_age(fileIndex, courseIndex, 0); 218 gMainMenuDataModified = TRUE; 219 } 220 } 221 222 /** 223 * Mark all coin scores for a save file as new. 224 */ 225 static void touch_high_score_ages(s32 fileIndex) { 226 s32 i; 227 228 for (i = COURSE_NUM_TO_INDEX(COURSE_MIN); i <= COURSE_NUM_TO_INDEX(COURSE_STAGES_MAX); i++) { 229 touch_coin_score_age(fileIndex, i); 230 } 231 } 232 233 /** 234 * Copy save file data from one backup slot to the other slot. 235 */ 236 static void restore_save_file_data(s32 fileIndex, s32 srcSlot) { 237 s32 destSlot = srcSlot ^ 1; 238 239 // Compute checksum on source data 240 add_save_block_signature(&gSaveBuffer.files[fileIndex][srcSlot], 241 sizeof(gSaveBuffer.files[fileIndex][srcSlot]), SAVE_FILE_MAGIC); 242 243 // Copy source data to destination slot 244 bcopy(&gSaveBuffer.files[fileIndex][srcSlot], &gSaveBuffer.files[fileIndex][destSlot], 245 sizeof(gSaveBuffer.files[fileIndex][destSlot])); 246 247 // Write destination data to EEPROM 248 write_eeprom_data(&gSaveBuffer.files[fileIndex][destSlot], 249 sizeof(gSaveBuffer.files[fileIndex][destSlot])); 250 } 251 252 void save_file_do_save(s32 fileIndex) { 253 if (gSaveFileModified) { 254 // Compute checksum 255 add_save_block_signature(&gSaveBuffer.files[fileIndex][0], 256 sizeof(gSaveBuffer.files[fileIndex][0]), SAVE_FILE_MAGIC); 257 258 // Copy to backup slot 259 bcopy(&gSaveBuffer.files[fileIndex][0], &gSaveBuffer.files[fileIndex][1], 260 sizeof(gSaveBuffer.files[fileIndex][1])); 261 262 // Write to EEPROM 263 write_eeprom_data(gSaveBuffer.files[fileIndex], sizeof(gSaveBuffer.files[fileIndex])); 264 265 gSaveFileModified = FALSE; 266 } 267 268 save_main_menu_data(); 269 } 270 271 void save_file_erase(s32 fileIndex) { 272 touch_high_score_ages(fileIndex); 273 bzero(&gSaveBuffer.files[fileIndex][0], sizeof(gSaveBuffer.files[fileIndex][0])); 274 275 gSaveFileModified = TRUE; 276 save_file_do_save(fileIndex); 277 } 278 279 //! Needs to be s32 to match on -O2, despite no return value. 280 BAD_RETURN(s32) save_file_copy(s32 srcFileIndex, s32 destFileIndex) { 281 UNUSED u8 filler[4]; 282 283 touch_high_score_ages(destFileIndex); 284 bcopy(&gSaveBuffer.files[srcFileIndex][0], &gSaveBuffer.files[destFileIndex][0], 285 sizeof(gSaveBuffer.files[destFileIndex][0])); 286 287 gSaveFileModified = TRUE; 288 save_file_do_save(destFileIndex); 289 } 290 291 void save_file_load_all(void) { 292 s32 file; 293 s32 validSlots; 294 295 gMainMenuDataModified = FALSE; 296 gSaveFileModified = FALSE; 297 298 bzero(&gSaveBuffer, sizeof(gSaveBuffer)); 299 read_eeprom_data(&gSaveBuffer, sizeof(gSaveBuffer)); 300 301 // Verify the main menu data and create a backup copy if only one of the slots is valid. 302 validSlots = verify_save_block_signature(&gSaveBuffer.menuData[0], sizeof(gSaveBuffer.menuData[0]), MENU_DATA_MAGIC); 303 validSlots |= verify_save_block_signature(&gSaveBuffer.menuData[1], sizeof(gSaveBuffer.menuData[1]),MENU_DATA_MAGIC) << 1; 304 switch (validSlots) { 305 case 0: // Neither copy is correct 306 wipe_main_menu_data(); 307 break; 308 case 1: // Slot 0 is correct and slot 1 is incorrect 309 restore_main_menu_data(0); 310 break; 311 case 2: // Slot 1 is correct and slot 0 is incorrect 312 restore_main_menu_data(1); 313 break; 314 } 315 316 for (file = 0; file < NUM_SAVE_FILES; file++) { 317 // Verify the save file and create a backup copy if only one of the slots is valid. 318 validSlots = verify_save_block_signature(&gSaveBuffer.files[file][0], sizeof(gSaveBuffer.files[file][0]), SAVE_FILE_MAGIC); 319 validSlots |= verify_save_block_signature(&gSaveBuffer.files[file][1], sizeof(gSaveBuffer.files[file][1]), SAVE_FILE_MAGIC) << 1; 320 switch (validSlots) { 321 case 0: // Neither copy is correct 322 save_file_erase(file); 323 break; 324 case 1: // Slot 0 is correct and slot 1 is incorrect 325 restore_save_file_data(file, 0); 326 break; 327 case 2: // Slot 1 is correct and slot 0 is incorrect 328 restore_save_file_data(file, 1); 329 break; 330 } 331 } 332 333 stub_save_file_1(); 334 } 335 336 /** 337 * Reload the current save file from its backup copy, which is effectively a 338 * a cached copy of what has been written to EEPROM. 339 * This is used after getting a game over. 340 */ 341 void save_file_reload(void) { 342 // Copy save file data from backup 343 bcopy(&gSaveBuffer.files[gCurrSaveFileNum - 1][1], &gSaveBuffer.files[gCurrSaveFileNum - 1][0], 344 sizeof(gSaveBuffer.files[gCurrSaveFileNum - 1][0])); 345 346 // Copy main menu data from backup 347 bcopy(&gSaveBuffer.menuData[1], &gSaveBuffer.menuData[0], sizeof(gSaveBuffer.menuData[0])); 348 349 gMainMenuDataModified = FALSE; 350 gSaveFileModified = FALSE; 351 } 352 353 /** 354 * Update the current save file after collecting a star or a key. 355 * If coin score is greater than the current high score, update it. 356 */ 357 void save_file_collect_star_or_key(s16 coinScore, s16 starIndex) { 358 s32 fileIndex = gCurrSaveFileNum - 1; 359 s32 courseIndex = COURSE_NUM_TO_INDEX(gCurrCourseNum); 360 361 s32 starFlag = 1 << starIndex; 362 UNUSED s32 flags = save_file_get_flags(); 363 364 gLastCompletedCourseNum = courseIndex + 1; 365 gLastCompletedStarNum = starIndex + 1; 366 sUnusedGotGlobalCoinHiScore = FALSE; 367 gGotFileCoinHiScore = FALSE; 368 369 if (courseIndex >= COURSE_NUM_TO_INDEX(COURSE_MIN) 370 && courseIndex <= COURSE_NUM_TO_INDEX(COURSE_STAGES_MAX)) { 371 //! Compares the coin score as a 16 bit value, but only writes the 8 bit 372 // truncation. This can allow a high score to decrease. 373 374 if (coinScore > ((u16) save_file_get_max_coin_score(courseIndex) & 0xFFFF)) { 375 sUnusedGotGlobalCoinHiScore = TRUE; 376 } 377 378 if (coinScore > save_file_get_course_coin_score(fileIndex, courseIndex)) { 379 gSaveBuffer.files[fileIndex][0].courseCoinScores[courseIndex] = coinScore; 380 touch_coin_score_age(fileIndex, courseIndex); 381 382 gGotFileCoinHiScore = TRUE; 383 gSaveFileModified = TRUE; 384 } 385 } 386 387 switch (gCurrLevelNum) { 388 case LEVEL_BOWSER_1: 389 if (!(save_file_get_flags() & (SAVE_FLAG_HAVE_KEY_1 | SAVE_FLAG_UNLOCKED_BASEMENT_DOOR))) { 390 save_file_set_flags(SAVE_FLAG_HAVE_KEY_1); 391 } 392 break; 393 394 case LEVEL_BOWSER_2: 395 if (!(save_file_get_flags() & (SAVE_FLAG_HAVE_KEY_2 | SAVE_FLAG_UNLOCKED_UPSTAIRS_DOOR))) { 396 save_file_set_flags(SAVE_FLAG_HAVE_KEY_2); 397 } 398 break; 399 400 case LEVEL_BOWSER_3: 401 break; 402 403 default: 404 if (!(save_file_get_star_flags(fileIndex, courseIndex) & starFlag)) { 405 save_file_set_star_flags(fileIndex, courseIndex, starFlag); 406 } 407 break; 408 } 409 } 410 411 s32 save_file_exists(s32 fileIndex) { 412 return (gSaveBuffer.files[fileIndex][0].flags & SAVE_FLAG_FILE_EXISTS) != 0; 413 } 414 415 /** 416 * Get the maximum coin score across all files for a course. The lower 16 bits 417 * of the returned value are the score, and the upper 16 bits are the file number 418 * of the save file with this score. 419 */ 420 u32 save_file_get_max_coin_score(s32 courseIndex) { 421 s32 fileIndex; 422 s32 maxCoinScore = -1; 423 s32 maxScoreAge = -1; 424 s32 maxScoreFileNum = 0; 425 426 for (fileIndex = 0; fileIndex < NUM_SAVE_FILES; fileIndex++) { 427 if (save_file_get_star_flags(fileIndex, courseIndex) != 0) { 428 s32 coinScore = save_file_get_course_coin_score(fileIndex, courseIndex); 429 s32 scoreAge = get_coin_score_age(fileIndex, courseIndex); 430 431 if (coinScore > maxCoinScore || (coinScore == maxCoinScore && scoreAge > maxScoreAge)) { 432 maxCoinScore = coinScore; 433 maxScoreAge = scoreAge; 434 maxScoreFileNum = fileIndex + 1; 435 } 436 } 437 } 438 return (maxScoreFileNum << 16) + max(maxCoinScore, 0); 439 } 440 441 s32 save_file_get_course_star_count(s32 fileIndex, s32 courseIndex) { 442 s32 i; 443 s32 count = 0; 444 u8 flag = 1; 445 u8 starFlags = save_file_get_star_flags(fileIndex, courseIndex); 446 447 for (i = 0; i < 7; i++, flag <<= 1) { 448 if (starFlags & flag) { 449 count++; 450 } 451 } 452 return count; 453 } 454 455 s32 save_file_get_total_star_count(s32 fileIndex, s32 minCourse, s32 maxCourse) { 456 s32 count = 0; 457 458 // Get standard course star count. 459 for (; minCourse <= maxCourse; minCourse++) { 460 count += save_file_get_course_star_count(fileIndex, minCourse); 461 } 462 463 // Add castle secret star count. 464 return save_file_get_course_star_count(fileIndex, COURSE_NUM_TO_INDEX(COURSE_NONE)) + count; 465 } 466 467 void save_file_set_flags(u32 flags) { 468 gSaveBuffer.files[gCurrSaveFileNum - 1][0].flags |= (flags | SAVE_FLAG_FILE_EXISTS); 469 gSaveFileModified = TRUE; 470 } 471 472 void save_file_clear_flags(u32 flags) { 473 gSaveBuffer.files[gCurrSaveFileNum - 1][0].flags &= ~flags; 474 gSaveBuffer.files[gCurrSaveFileNum - 1][0].flags |= SAVE_FLAG_FILE_EXISTS; 475 gSaveFileModified = TRUE; 476 } 477 478 u32 save_file_get_flags(void) { 479 if (gCurrCreditsEntry != NULL || gCurrDemoInput != NULL) { 480 return 0; 481 } 482 return gSaveBuffer.files[gCurrSaveFileNum - 1][0].flags; 483 } 484 485 /** 486 * Return the bitset of obtained stars in the specified course. 487 * If course is COURSE_NONE, return the bitset of obtained castle secret stars. 488 */ 489 u32 save_file_get_star_flags(s32 fileIndex, s32 courseIndex) { 490 u32 starFlags; 491 492 if (courseIndex == COURSE_NUM_TO_INDEX(COURSE_NONE)) { 493 starFlags = SAVE_FLAG_TO_STAR_FLAG(gSaveBuffer.files[fileIndex][0].flags); 494 } else { 495 starFlags = gSaveBuffer.files[fileIndex][0].courseStars[courseIndex] & 0x7F; 496 } 497 498 return starFlags; 499 } 500 501 /** 502 * Add to the bitset of obtained stars in the specified course. 503 * If course is COURSE_NONE, add to the bitset of obtained castle secret stars. 504 */ 505 void save_file_set_star_flags(s32 fileIndex, s32 courseIndex, u32 starFlags) { 506 if (courseIndex == COURSE_NUM_TO_INDEX(COURSE_NONE)) { 507 gSaveBuffer.files[fileIndex][0].flags |= STAR_FLAG_TO_SAVE_FLAG(starFlags); 508 } else { 509 gSaveBuffer.files[fileIndex][0].courseStars[courseIndex] |= starFlags; 510 } 511 512 gSaveBuffer.files[fileIndex][0].flags |= SAVE_FLAG_FILE_EXISTS; 513 gSaveFileModified = TRUE; 514 } 515 516 s32 save_file_get_course_coin_score(s32 fileIndex, s32 courseIndex) { 517 return gSaveBuffer.files[fileIndex][0].courseCoinScores[courseIndex]; 518 } 519 520 /** 521 * Return TRUE if the cannon is unlocked in the current course. 522 */ 523 s32 save_file_is_cannon_unlocked(void) { 524 return (gSaveBuffer.files[gCurrSaveFileNum - 1][0].courseStars[gCurrCourseNum] & (1 << 7)) != 0; 525 } 526 527 /** 528 * Sets the cannon status to unlocked in the current course. 529 */ 530 void save_file_set_cannon_unlocked(void) { 531 gSaveBuffer.files[gCurrSaveFileNum - 1][0].courseStars[gCurrCourseNum] |= (1 << 7); 532 gSaveBuffer.files[gCurrSaveFileNum - 1][0].flags |= SAVE_FLAG_FILE_EXISTS; 533 gSaveFileModified = TRUE; 534 } 535 536 void save_file_set_cap_pos(s16 x, s16 y, s16 z) { 537 struct SaveFile *saveFile = &gSaveBuffer.files[gCurrSaveFileNum - 1][0]; 538 539 saveFile->capLevel = gCurrLevelNum; 540 saveFile->capArea = gCurrAreaIndex; 541 vec3s_set(saveFile->capPos, x, y, z); 542 save_file_set_flags(SAVE_FLAG_CAP_ON_GROUND); 543 } 544 545 s32 save_file_get_cap_pos(Vec3s capPos) { 546 struct SaveFile *saveFile = &gSaveBuffer.files[gCurrSaveFileNum - 1][0]; 547 s32 flags = save_file_get_flags(); 548 549 if (saveFile->capLevel == gCurrLevelNum && saveFile->capArea == gCurrAreaIndex 550 && (flags & SAVE_FLAG_CAP_ON_GROUND)) { 551 vec3s_copy(capPos, saveFile->capPos); 552 return TRUE; 553 } 554 return FALSE; 555 } 556 557 void save_file_set_sound_mode(u16 mode) { 558 set_sound_mode(mode); 559 gSaveBuffer.menuData[0].soundMode = mode; 560 561 gMainMenuDataModified = TRUE; 562 save_main_menu_data(); 563 } 564 565 u16 save_file_get_sound_mode(void) { 566 return gSaveBuffer.menuData[0].soundMode; 567 } 568 569 void save_file_move_cap_to_default_location(void) { 570 if (save_file_get_flags() & SAVE_FLAG_CAP_ON_GROUND) { 571 switch (gSaveBuffer.files[gCurrSaveFileNum - 1][0].capLevel) { 572 case LEVEL_SSL: 573 save_file_set_flags(SAVE_FLAG_CAP_ON_KLEPTO); 574 break; 575 case LEVEL_SL: 576 save_file_set_flags(SAVE_FLAG_CAP_ON_MR_BLIZZARD); 577 break; 578 case LEVEL_TTM: 579 save_file_set_flags(SAVE_FLAG_CAP_ON_UKIKI); 580 break; 581 } 582 save_file_clear_flags(SAVE_FLAG_CAP_ON_GROUND); 583 } 584 } 585 586 #ifdef VERSION_EU 587 void eu_set_language(u16 language) { 588 gSaveBuffer.menuData[0].language = language; 589 gMainMenuDataModified = TRUE; 590 save_main_menu_data(); 591 } 592 593 u16 eu_get_language(void) { 594 return gSaveBuffer.menuData[0].language; 595 } 596 #endif 597 598 void disable_warp_checkpoint(void) { 599 // check_warp_checkpoint() checks to see if gWarpCheckpoint.courseNum != COURSE_NONE 600 gWarpCheckpoint.courseNum = COURSE_NONE; 601 } 602 603 /** 604 * Checks the upper bit of the WarpNode->destLevel byte to see if the 605 * game should set a warp checkpoint. 606 */ 607 void check_if_should_set_warp_checkpoint(struct WarpNode *warpNode) { 608 if (warpNode->destLevel & 0x80) { 609 // Overwrite the warp checkpoint variables. 610 gWarpCheckpoint.actNum = gCurrActNum; 611 gWarpCheckpoint.courseNum = gCurrCourseNum; 612 gWarpCheckpoint.levelID = warpNode->destLevel & 0x7F; 613 gWarpCheckpoint.areaNum = warpNode->destArea; 614 gWarpCheckpoint.warpNode = warpNode->destNode; 615 } 616 } 617 618 /** 619 * Checks to see if a checkpoint is properly active or not. This will 620 * also update the level, area, and destination node of the input WarpNode. 621 * returns TRUE if input WarpNode was updated, and FALSE if not. 622 */ 623 s32 check_warp_checkpoint(struct WarpNode *warpNode) { 624 s16 warpCheckpointActive = FALSE; 625 s16 currCourseNum = gLevelToCourseNumTable[(warpNode->destLevel & 0x7F) - 1]; 626 627 // gSavedCourseNum is only used in this function. 628 if (gWarpCheckpoint.courseNum != COURSE_NONE && gSavedCourseNum == currCourseNum 629 && gWarpCheckpoint.actNum == gCurrActNum) { 630 warpNode->destLevel = gWarpCheckpoint.levelID; 631 warpNode->destArea = gWarpCheckpoint.areaNum; 632 warpNode->destNode = gWarpCheckpoint.warpNode; 633 warpCheckpointActive = TRUE; 634 } else { 635 // Disable the warp checkpoint just in case the other 2 conditions failed? 636 gWarpCheckpoint.courseNum = COURSE_NONE; 637 } 638 639 return warpCheckpointActive; 640 }