diff --git a/soh/include/functions.h b/soh/include/functions.h index 854562e8f..42aab2813 100644 --- a/soh/include/functions.h +++ b/soh/include/functions.h @@ -2416,7 +2416,7 @@ void GameOver_Update(PlayState* play); void func_80110990(PlayState* play); void func_801109B0(PlayState* play); void Message_Init(PlayState* play); -void func_80112098(PlayState* play); +void Regs_InitData(PlayState* play); void Title_Init(GameState* thisx); void Title_PrintBuildInfo(Gfx** gfxp); @@ -2433,6 +2433,11 @@ void Heaps_Free(void); CollisionHeader* BgCheck_GetCollisionHeader(CollisionContext* colCtx, s32 bgId); +// Exposing these methods to leverage them from the file select screen to render messages +void Message_OpenText(PlayState* play, u16 textId); +void Message_Decode(PlayState* play); +void Message_DrawText(PlayState* play, Gfx** gfxP); + #ifdef __cplusplus #undef this }; diff --git a/soh/include/variables.h b/soh/include/variables.h index 13e4dce95..a2ac65faf 100644 --- a/soh/include/variables.h +++ b/soh/include/variables.h @@ -46,6 +46,9 @@ extern "C" extern OSViMode osViModeFpalLan1; extern u32 __additional_scanline; extern u8 gBuildVersion[]; + extern s16 gBuildVersionMajor; + extern s16 gBuildVersionMinor; + extern s16 gBuildVersionPatch; extern u8 gBuildTeam[]; extern u8 gBuildDate[]; extern u8 gBuildMakeOption[]; diff --git a/soh/include/z64save.h b/soh/include/z64save.h index e3c18dd34..d44a703df 100644 --- a/soh/include/z64save.h +++ b/soh/include/z64save.h @@ -54,6 +54,10 @@ typedef struct { } Inventory; // size = 0x5E typedef struct { + /* */ char buildVersion[50]; + /* */ s16 buildVersionMajor; + /* */ s16 buildVersionMinor; + /* */ s16 buildVersionPatch; /* */ u8 heartPieces; /* */ u8 heartContainers; /* */ u8 dungeonKeys[19]; diff --git a/soh/soh/Enhancements/custom-message/CustomMessageTypes.h b/soh/soh/Enhancements/custom-message/CustomMessageTypes.h index 21f47000d..2e5ecce26 100644 --- a/soh/soh/Enhancements/custom-message/CustomMessageTypes.h +++ b/soh/soh/Enhancements/custom-message/CustomMessageTypes.h @@ -41,6 +41,7 @@ typedef enum { TEXT_WARP_RANDOM_REPLACED_TEXT = 0x9200, TEXT_LAKE_HYLIA_WATER_SWITCH_SIGN = 0x346, // 0x3yy for cuttable sign range TEXT_LAKE_HYLIA_WATER_SWITCH_NAVI = 0x1B3, // 0x1yy for Navi msg range + TEXT_RANDO_SAVE_VERSION_WARNING = 0x9300, } TextIDs; #ifdef __cplusplus diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index dcd862fb6..5528bfba1 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -1692,6 +1692,9 @@ extern "C" int CustomMessage_RetrieveIfExists(PlayState* play) { if (textId == TEXT_MARKET_GUARD_NIGHT && CVarGetInteger("gMarketSneak", 0) && play->sceneNum == SCENE_ENTRA_N) { messageEntry = CustomMessageManager::Instance->RetrieveMessage(customMessageTableID, TEXT_MARKET_GUARD_NIGHT); } + if (textId == TEXT_RANDO_SAVE_VERSION_WARNING) { + messageEntry = CustomMessageManager::Instance->RetrieveMessage(customMessageTableID, TEXT_RANDO_SAVE_VERSION_WARNING); + } if (messageEntry.textBoxType != -1) { font->charTexBuf[0] = (messageEntry.textBoxType << 4) | messageEntry.textBoxPos; switch (gSaveContext.language) { diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp index d1328ab05..270bca60c 100644 --- a/soh/soh/SaveManager.cpp +++ b/soh/soh/SaveManager.cpp @@ -5,6 +5,7 @@ #include "z64.h" #include "functions.h" #include "macros.h" +#include #include #include @@ -52,6 +53,11 @@ SaveManager::SaveManager() { info.randoSave = 0; info.requiresMasterQuest = 0; info.requiresOriginal = 0; + + info.buildVersionMajor = 0; + info.buildVersionMinor = 0; + info.buildVersionPatch = 0; + memset(&info.buildVersion, 0, sizeof(info.buildVersion)); } } @@ -389,6 +395,12 @@ void SaveManager::InitMeta(int fileNum) { // If the file is not marked as Master Quest, it could still theoretically be a rando save with all 12 MQ dungeons, in which case // we don't actually require a vanilla OTR. fileMetaInfo[fileNum].requiresOriginal = !gSaveContext.isMasterQuest && (!gSaveContext.n64ddFlag || gSaveContext.mqDungeonCount < 12); + + fileMetaInfo[fileNum].buildVersionMajor = gSaveContext.sohStats.buildVersionMajor; + fileMetaInfo[fileNum].buildVersionMinor = gSaveContext.sohStats.buildVersionMinor; + fileMetaInfo[fileNum].buildVersionPatch = gSaveContext.sohStats.buildVersionPatch; + strncpy(fileMetaInfo[fileNum].buildVersion, gSaveContext.sohStats.buildVersion, sizeof(fileMetaInfo[fileNum].buildVersion) - 1); + fileMetaInfo[fileNum].buildVersion[sizeof(fileMetaInfo[fileNum].buildVersion) - 1] = 0; } void SaveManager::InitFile(bool isDebug) { @@ -559,6 +571,13 @@ void SaveManager::InitFileNormal() { gSaveContext.infTable[29] = 1; gSaveContext.sceneFlags[5].swch = 0x40000000; gSaveContext.pendingSale = ITEM_NONE; + + strncpy(gSaveContext.sohStats.buildVersion, (const char*) gBuildVersion, sizeof(gSaveContext.sohStats.buildVersion) - 1); + gSaveContext.sohStats.buildVersion[sizeof(gSaveContext.sohStats.buildVersion) - 1] = 0; + gSaveContext.sohStats.buildVersionMajor = gBuildVersionMajor; + gSaveContext.sohStats.buildVersionMinor = gBuildVersionMinor; + gSaveContext.sohStats.buildVersionPatch = gBuildVersionPatch; + //RANDOTODO (ADD ITEMLOCATIONS TO GSAVECONTEXT) } @@ -1248,6 +1267,14 @@ void SaveManager::LoadBaseVersion3() { SaveManager::Instance->LoadData("gsTokens", gSaveContext.inventory.gsTokens); }); SaveManager::Instance->LoadStruct("sohStats", []() { + std::string buildVersion; + SaveManager::Instance->LoadData("buildVersion", buildVersion); + strncpy(gSaveContext.sohStats.buildVersion, buildVersion.c_str(), ARRAY_COUNT(gSaveContext.sohStats.buildVersion) - 1); + gSaveContext.sohStats.buildVersion[ARRAY_COUNT(gSaveContext.sohStats.buildVersion) - 1] = 0; + SaveManager::Instance->LoadData("buildVersionMajor", gSaveContext.sohStats.buildVersionMajor); + SaveManager::Instance->LoadData("buildVersionMinor", gSaveContext.sohStats.buildVersionMinor); + SaveManager::Instance->LoadData("buildVersionPatch", gSaveContext.sohStats.buildVersionPatch); + SaveManager::Instance->LoadData("heartPieces", gSaveContext.sohStats.heartPieces); SaveManager::Instance->LoadData("heartContainers", gSaveContext.sohStats.heartContainers); SaveManager::Instance->LoadArray("dungeonKeys", ARRAY_COUNT(gSaveContext.sohStats.dungeonKeys), [](size_t i) { @@ -1441,6 +1468,11 @@ void SaveManager::SaveBase() { SaveManager::Instance->SaveData("gsTokens", gSaveContext.inventory.gsTokens); }); SaveManager::Instance->SaveStruct("sohStats", []() { + SaveManager::Instance->SaveData("buildVersion", gSaveContext.sohStats.buildVersion); + SaveManager::Instance->SaveData("buildVersionMajor", gSaveContext.sohStats.buildVersionMajor); + SaveManager::Instance->SaveData("buildVersionMinor", gSaveContext.sohStats.buildVersionMinor); + SaveManager::Instance->SaveData("buildVersionPatch", gSaveContext.sohStats.buildVersionPatch); + SaveManager::Instance->SaveData("heartPieces", gSaveContext.sohStats.heartPieces); SaveManager::Instance->SaveData("heartContainers", gSaveContext.sohStats.heartContainers); SaveManager::Instance->SaveArray("dungeonKeys", ARRAY_COUNT(gSaveContext.sohStats.dungeonKeys), [](size_t i) { @@ -1681,6 +1713,11 @@ void SaveManager::CopyZeldaFile(int from, int to) { fileMetaInfo[to].randoSave = fileMetaInfo[from].randoSave; fileMetaInfo[to].requiresMasterQuest = fileMetaInfo[from].requiresMasterQuest; fileMetaInfo[to].requiresOriginal = fileMetaInfo[from].requiresOriginal; + fileMetaInfo[to].buildVersionMajor = fileMetaInfo[from].buildVersionMajor; + fileMetaInfo[to].buildVersionMinor = fileMetaInfo[from].buildVersionMinor; + fileMetaInfo[to].buildVersionPatch = fileMetaInfo[from].buildVersionPatch; + strncpy(fileMetaInfo[to].buildVersion, fileMetaInfo[from].buildVersion, sizeof(fileMetaInfo[to].buildVersion) - 1); + fileMetaInfo[to].buildVersion[sizeof(fileMetaInfo[to].buildVersion) - 1] = 0; } void SaveManager::DeleteZeldaFile(int fileNum) { diff --git a/soh/soh/SaveManager.h b/soh/soh/SaveManager.h index 32d4f2fbd..a43e80910 100644 --- a/soh/soh/SaveManager.h +++ b/soh/soh/SaveManager.h @@ -14,6 +14,10 @@ typedef struct { u32 requiresOriginal; u8 seedHash[5]; u8 randoSave; + char buildVersion[50]; + s16 buildVersionMajor; + s16 buildVersionMinor; + s16 buildVersionPatch; } SaveFileMetaInfo; #ifdef __cplusplus diff --git a/soh/soh/z_message_OTR.cpp b/soh/soh/z_message_OTR.cpp index 829cefbe8..bb3437216 100644 --- a/soh/soh/z_message_OTR.cpp +++ b/soh/soh/z_message_OTR.cpp @@ -153,4 +153,13 @@ extern "C" void OTRMessage_Init() "Tu as l'air de t'ennuyer. Tu veux&aller faire un tour?\x1B&%gOui&Non%w", } ); + CustomMessageManager::Instance->CreateMessage( + customMessageTableID, TEXT_RANDO_SAVE_VERSION_WARNING, + { + TEXTBOX_TYPE_NONE_BOTTOM, TEXTBOX_POS_BOTTOM, + "This save was created on&a different version of SoH.&&Things may be broken.", + "Dieser Spielstand wurde auf einer&anderen Version von SoH erstellt.&&Es könnten Fehler auftreten.", + "Cette sauvegarde a été créée sur&une version différente de SoH.&Certaines fonctionnalités&peuvent être corrompues.", + } + ); } diff --git a/soh/src/boot/build.c.in b/soh/src/boot/build.c.in index 6bf50e39f..f4c2899cf 100644 --- a/soh/src/boot/build.c.in +++ b/soh/src/boot/build.c.in @@ -1,4 +1,7 @@ const char gBuildVersion[] = "@PROJECT_BUILD_NAME@ (@CMAKE_PROJECT_VERSION_MAJOR@.@CMAKE_PROJECT_VERSION_MINOR@.@CMAKE_PROJECT_VERSION_PATCH@)"; +const int gBuildVersionMajor = @CMAKE_PROJECT_VERSION_MAJOR@; +const int gBuildVersionMinor = @CMAKE_PROJECT_VERSION_MINOR@; +const int gBuildVersionPatch = @CMAKE_PROJECT_VERSION_PATCH@; const char gBuildTeam[] = "@PROJECT_TEAM@"; const char gBuildDate[] = __DATE__ " " __TIME__; const char gBuildMakeOption[] = ""; diff --git a/soh/src/code/z_construct.c b/soh/src/code/z_construct.c index 1b77d38b3..44b0ee5e7 100644 --- a/soh/src/code/z_construct.c +++ b/soh/src/code/z_construct.c @@ -163,7 +163,7 @@ void Message_Init(PlayState* play) { YREG(31) = 0; } -void func_80111070(void) { +void Regs_InitDataImpl(void) { YREG(8) = 10; YREG(14) = 0; YREG(15) = 0; @@ -572,6 +572,6 @@ void func_80111070(void) { VREG(92) = -63; } -void func_80112098(PlayState* play) { - func_80111070(); +void Regs_InitData(PlayState* play) { + Regs_InitDataImpl(); } diff --git a/soh/src/code/z_play.c b/soh/src/code/z_play.c index 71e5984e0..981f5b975 100644 --- a/soh/src/code/z_play.c +++ b/soh/src/code/z_play.c @@ -463,7 +463,7 @@ void Play_Init(GameState* thisx) { play->cameraPtrs[MAIN_CAM]->uid = 0; play->activeCamera = MAIN_CAM; func_8005AC48(&play->mainCamera, 0xFF); - func_80112098(play); + Regs_InitData(play); Message_Init(play); GameOver_Init(play); SoundSource_InitAll(play); diff --git a/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c b/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c index de08e780c..613f16261 100644 --- a/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c +++ b/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c @@ -11,6 +11,8 @@ #include "objects/gameplay_keep/gameplay_keep.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Enhancements/custom-message/CustomMessageTypes.h" + #define NORMAL_QUEST 0 #define MASTER_QUEST 1 #define RANDOMIZER_QUEST 2 @@ -387,30 +389,42 @@ void DrawSeedHashSprites(FileChooseContext* this) { gDPPipeSync(POLY_OPA_DISP++); gDPSetCombineMode(POLY_OPA_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM); - if (this->windowRot == 0 || (this->configMode == CM_QUEST_MENU && this->questType[this->buttonIndex] == RANDOMIZER_QUEST)) { - if (this->selectMode == SM_CONFIRM_FILE) { - gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 0xFF, 0xFF, 0xFF, this->fileInfoAlpha[this->buttonIndex]); + // Draw icons on the main menu, when a rando file is selected, and when quest selection is set to rando + if ((this->configMode == CM_MAIN_MENU && + (this->selectMode != SM_CONFIRM_FILE || Save_GetSaveMetaInfo(this->selectedFileIndex)->randoSave == 1)) || + (this->configMode == CM_QUEST_MENU && this->questType[this->buttonIndex] == RANDOMIZER_QUEST)) { + + if (this->fileInfoAlpha[this->selectedFileIndex] > 0) { + // Use file info alpha to match fading + gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 0xFF, 0xFF, 0xFF, this->fileInfoAlpha[this->selectedFileIndex]); u16 xStart = 64; - // Draw Seed Icons + // Draw Seed Icons for specific file for (unsigned int i = 0; i < 5; i++) { if (Save_GetSaveMetaInfo(this->selectedFileIndex)->randoSave == 1) { SpriteLoad(this, GetSeedTexture(Save_GetSaveMetaInfo(this->selectedFileIndex)->seedHash[i])); SpriteDraw(this, GetSeedTexture(Save_GetSaveMetaInfo(this->selectedFileIndex)->seedHash[i]), - xStart + (18 * i), 136, 16, 16); + xStart + (18 * i), 136, 16, 16); } } } - gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 0xFF, 0xFF, 0xFF, this->fileButtonAlpha[this->buttonIndex]); + // Fade top seed icons based on main menu fade and if save supports rando + u8 alpha = MAX(this->optionButtonAlpha, Save_GetSaveMetaInfo(this->selectedFileIndex)->randoSave == 1 ? 0xFF : 0); + if (alpha >= 200) { + alpha = 0xFF; + } + gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 0xFF, 0xFF, 0xFF, alpha); + + // Draw Seed Icons for spoiler log if (strnlen(CVarGetString("gSpoilerLog", ""), 1) != 0 && fileSelectSpoilerFileLoaded) { u16 xStart = 64; for (unsigned int i = 0; i < 5; i++) { SpriteLoad(this, GetSeedTexture(gSaveContext.seedIcons[i])); SpriteDraw(this, GetSeedTexture(gSaveContext.seedIcons[i]), xStart + (40 * i), 10, 24, 24); } - } + } } gDPPipeSync(POLY_OPA_DISP++); @@ -1340,6 +1354,47 @@ void FileChoose_DrawFileInfo(GameState* thisx, s16 fileIndex, s16 isActive) { } } } + + // Use file info alpha to match fading + u8 textAlpha = this->fileInfoAlpha[fileIndex]; + if (textAlpha >= 200) { + textAlpha = 255; + } + + // Draw rando seed warning when build version doesn't match for Major or Minor number + if (Save_GetSaveMetaInfo(fileIndex)->randoSave == 1 && + this->menuMode == FS_MENU_MODE_SELECT && + (gBuildVersionMajor != Save_GetSaveMetaInfo(fileIndex)->buildVersionMajor || + gBuildVersionMinor != Save_GetSaveMetaInfo(fileIndex)->buildVersionMinor)) { + + // Stub out a dummy play state to be able to use the dialog system (MessageCtx) + PlayState dummyPlay; + PlayState* dummyPlayPtr = &dummyPlay; + + // Set the MessageCtx and GameState onto the dummy play state + dummyPlayPtr->msgCtx = this->msgCtx; + dummyPlayPtr->state = this->state; + + // Load the custom text ID without doing a textbox + Message_OpenText(dummyPlayPtr, TEXT_RANDO_SAVE_VERSION_WARNING); + // Force the context into message print mode + dummyPlayPtr->msgCtx.msgMode = MSGMODE_TEXT_NEXT_MSG; + Message_Decode(dummyPlayPtr); + + // Set the draw pos to end of text to render it all at once + dummyPlayPtr->msgCtx.textDrawPos = dummyPlayPtr->msgCtx.decodedTextLen; + dummyPlayPtr->msgCtx.textColorAlpha = textAlpha; + + // Set position and spacing values + R_TEXT_LINE_SPACING = 10; + R_TEXT_INIT_XPOS = 128; + R_TEXT_INIT_YPOS = 154; + + Gfx* gfx = Graph_GfxPlusOne(POLY_OPA_DISP); + Message_DrawText(dummyPlayPtr, &gfx); + + POLY_OPA_DISP = gfx; + } } CLOSE_DISPS(this->state.gfxCtx); @@ -2658,6 +2713,9 @@ void FileChoose_Init(GameState* thisx) { DmaMgr_SendRequest1(this->parameterSegment, (u32)_parameter_staticSegmentRomStart, size, __FILE__, __LINE__); + // Load some registers used by the dialog system + Regs_InitData(NULL); // Passing in NULL as we dont have a playstate, and it isn't used in the func + Matrix_Init(&this->state); View_Init(&this->view, this->state.gfxCtx); this->state.main = FileChoose_Main;