From 78ffb41cd2d8fa3e68ee9fcb1fb496ddeede878e Mon Sep 17 00:00:00 2001 From: Malkierian Date: Mon, 13 Nov 2023 16:11:29 -0700 Subject: [PATCH 01/15] Moved the check for `!seqInfo.canBeUsedAsReplacement` in `InitializeShufflePool` to exclude them before modifying either shuffle pool. (#3370) --- soh/soh/Enhancements/audio/AudioCollection.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/audio/AudioCollection.cpp b/soh/soh/Enhancements/audio/AudioCollection.cpp index a5499bd4f..9da742f91 100644 --- a/soh/soh/Enhancements/audio/AudioCollection.cpp +++ b/soh/soh/Enhancements/audio/AudioCollection.cpp @@ -400,8 +400,9 @@ void AudioCollection::InitializeShufflePool() { if (shufflePoolInitialized) return; for (auto& [seqId, seqInfo] : sequenceMap) { + if (!seqInfo.canBeUsedAsReplacement) continue; const std::string cvarKey = "gAudioEditor.Excluded." + seqInfo.sfxKey; - if (CVarGetInteger(cvarKey.c_str(), 0) && !seqInfo.canBeUsedAsReplacement) { + if (CVarGetInteger(cvarKey.c_str(), 0)) { excludedSequences.insert(&seqInfo); } else { includedSequences.insert(&seqInfo); From cf88b3d2bf0b7e63506f823b0e1bac0d2743124b Mon Sep 17 00:00:00 2001 From: Ralphie Morell Date: Mon, 13 Nov 2023 23:45:15 -0500 Subject: [PATCH 02/15] Fix edge case of MS shuffle (#3364) --- soh/src/code/z_parameter.c | 1 + 1 file changed, 1 insertion(+) diff --git a/soh/src/code/z_parameter.c b/soh/src/code/z_parameter.c index ec25d2062..bbaa8d90b 100644 --- a/soh/src/code/z_parameter.c +++ b/soh/src/code/z_parameter.c @@ -1469,6 +1469,7 @@ void Inventory_SwapAgeEquipment(void) { gSaveContext.equips.buttonItems[0] = ITEM_SWORD_MASTER; } else { gSaveContext.equips.buttonItems[0] = ITEM_NONE; + Flags_SetInfTable(INFTABLE_SWORDLESS); } if (gSaveContext.inventory.items[SLOT_NUT] != ITEM_NONE) { From 60687aff0d862e473a1c610a40da46260045dca4 Mon Sep 17 00:00:00 2001 From: Malkierian Date: Mon, 13 Nov 2023 21:45:41 -0700 Subject: [PATCH 03/15] Move everything in `RandomizerCheckTracker::LoadFile()` except the block to load the "trackerData" section to a new `OnLoadGame` hook function to fix crashes on transferred saves. (#3368) --- .../randomizer/randomizer_check_tracker.cpp | 112 +++++++++--------- 1 file changed, 58 insertions(+), 54 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp index 5f7790dd0..6123c1a27 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp @@ -452,6 +452,63 @@ bool HasItemBeenCollected(RandomizerCheck rc) { return false; } +void CheckTrackerLoadGame(int32_t fileNum) { + LoadSettings(); + TrySetAreas(); + for (auto [rc, rcObj] : RandomizerCheckObjects::GetAllRCObjects()) { + RandomizerCheckTrackerData rcTrackerData = gSaveContext.checkTrackerData[rc]; + if (rc == RC_UNKNOWN_CHECK || rc == RC_MAX || rc == RC_LINKS_POCKET || + !RandomizerCheckObjects::GetAllRCObjects().contains(rc)) + continue; + + RandomizerCheckObject realRcObj; + if (rc == RC_GIFT_FROM_SAGES && !IS_RANDO) { + realRcObj = RCO_RAORU; + } else { + realRcObj = rcObj; + } + if (!IsVisibleInCheckTracker(realRcObj)) continue; + + checksByArea.find(realRcObj.rcArea)->second.push_back(realRcObj); + if (rcTrackerData.status == RCSHOW_SAVED || rcTrackerData.skipped) { + areaChecksGotten[realRcObj.rcArea]++; + } + + if (areaChecksGotten[realRcObj.rcArea] != 0 || RandomizerCheckObjects::AreaIsOverworld(realRcObj.rcArea)) { + areasSpoiled |= (1 << realRcObj.rcArea); + } + } + if (OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_LINKS_POCKET) != RO_LINKS_POCKET_NOTHING && IS_RANDO) { + s8 startingAge = OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_STARTING_AGE); + RandomizerCheckArea startingArea; + switch (startingAge) { + case RO_AGE_CHILD: + startingArea = RCAREA_KOKIRI_FOREST; + break; + case RO_AGE_ADULT: + startingArea = RCAREA_MARKET; + break; + default: + startingArea = RCAREA_KOKIRI_FOREST; + break; + } + RandomizerCheckObject linksPocket = { RC_LINKS_POCKET, RCVORMQ_BOTH, RCTYPE_LINKS_POCKET, startingArea, ACTOR_ID_MAX, SCENE_ID_MAX, 0x00, GI_NONE, false, "Link's Pocket", "Link's Pocket" }; + + checksByArea.find(startingArea)->second.push_back(linksPocket); + areaChecksGotten[startingArea]++; + } + + showVOrMQ = (OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_RANDOM_MQ_DUNGEONS) == RO_MQ_DUNGEONS_RANDOM_NUMBER || + (OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_RANDOM_MQ_DUNGEONS) == RO_MQ_DUNGEONS_SET_NUMBER && + OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_MQ_DUNGEON_COUNT) < 12)); + LinksPocket(); + SongFromImpa(); + GiftFromSages(); + initialized = true; + UpdateAllOrdering(); + UpdateInventoryChecks(); +} + void CheckTrackerDialogClosed() { if (messageCloseCheck) { messageCloseCheck = false; @@ -679,9 +736,6 @@ void SaveFile(SaveContext* saveContext, int sectionID, bool fullSave) { } void LoadFile() { - Teardown(); - LoadSettings(); - TrySetAreas(); SaveManager::Instance->LoadArray("checks", RC_MAX, [](size_t i) { SaveManager::Instance->LoadStruct("", [&]() { SaveManager::Instance->LoadData("status", gSaveContext.checkTrackerData[i].status); @@ -689,58 +743,7 @@ void LoadFile() { SaveManager::Instance->LoadData("price", gSaveContext.checkTrackerData[i].price); SaveManager::Instance->LoadData("hintItem", gSaveContext.checkTrackerData[i].hintItem); }); - RandomizerCheckTrackerData entry = gSaveContext.checkTrackerData[i]; - RandomizerCheck rc = static_cast(i); - if (rc == RC_UNKNOWN_CHECK || rc == RC_MAX || - !RandomizerCheckObjects::GetAllRCObjects().contains(rc)) - return; - - RandomizerCheckObject entry2; - if (rc == RC_GIFT_FROM_SAGES && !IS_RANDO) { - entry2 = RCO_RAORU; - } else { - entry2 = RandomizerCheckObjects::GetAllRCObjects().find(rc)->second; - } - if (!IsVisibleInCheckTracker(entry2)) return; - - checksByArea.find(entry2.rcArea)->second.push_back(entry2); - if (entry.status == RCSHOW_SAVED || entry.skipped) { - areaChecksGotten[entry2.rcArea]++; - } - - if (areaChecksGotten[entry2.rcArea] != 0 || RandomizerCheckObjects::AreaIsOverworld(entry2.rcArea)) { - areasSpoiled |= (1 << entry2.rcArea); - } }); - if (OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_LINKS_POCKET) != RO_LINKS_POCKET_NOTHING && IS_RANDO) { - s8 startingAge = OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_STARTING_AGE); - RandomizerCheckArea startingArea; - switch (startingAge) { - case RO_AGE_CHILD: - startingArea = RCAREA_KOKIRI_FOREST; - break; - case RO_AGE_ADULT: - startingArea = RCAREA_MARKET; - break; - default: - startingArea = RCAREA_KOKIRI_FOREST; - break; - } - RandomizerCheckObject linksPocket = { RC_LINKS_POCKET, RCVORMQ_BOTH, RCTYPE_LINKS_POCKET, startingArea, ACTOR_ID_MAX, SCENE_ID_MAX, 0x00, GI_NONE, false, "Link's Pocket", "Link's Pocket" }; - - checksByArea.find(startingArea)->second.push_back(linksPocket); - areaChecksGotten[startingArea]++; - } - - showVOrMQ = (OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_RANDOM_MQ_DUNGEONS) == RO_MQ_DUNGEONS_RANDOM_NUMBER || - (OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_RANDOM_MQ_DUNGEONS) == RO_MQ_DUNGEONS_SET_NUMBER && - OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_MQ_DUNGEON_COUNT) < 12)); - LinksPocket(); - SongFromImpa(); - GiftFromSages(); - initialized = true; - UpdateAllOrdering(); - UpdateInventoryChecks(); } void Teardown() { @@ -1533,6 +1536,7 @@ void CheckTrackerWindow::InitElement() { SaveManager::Instance->AddInitFunction(InitTrackerData); sectionId = SaveManager::Instance->AddSaveFunction("trackerData", 1, SaveFile, true, -1); SaveManager::Instance->AddLoadFunction("trackerData", 1, LoadFile); + GameInteractor::Instance->RegisterGameHook(CheckTrackerLoadGame); GameInteractor::Instance->RegisterGameHook([](uint32_t fileNum) { Teardown(); }); From 384403edb5cd7dd5e2d37e86ff048845b315be05 Mon Sep 17 00:00:00 2001 From: Malkierian Date: Mon, 13 Nov 2023 21:45:52 -0700 Subject: [PATCH 04/15] Rename all instances of Desert Wasteland to Haunted Wasteland. (#3372) --- soh/soh/ActorDB.cpp | 2 +- soh/soh/Enhancements/randomizer/randomizer_check_objects.cpp | 2 +- soh/soh/Enhancements/randomizer/randomizer_tricks.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/soh/soh/ActorDB.cpp b/soh/soh/ActorDB.cpp index 2cf4d8fe7..a66048b32 100644 --- a/soh/soh/ActorDB.cpp +++ b/soh/soh/ActorDB.cpp @@ -439,7 +439,7 @@ static std::unordered_map actorDescriptions = { { ACTOR_EN_DAIKU_KAKARIKO, "Carpenters (Kakariko)" }, { ACTOR_BG_BOWL_WALL, "Bombchu Bowling Alley Wall" }, { ACTOR_EN_WALL_TUBO, "Bombchu Bowling Alley Bullseyes" }, - { ACTOR_EN_PO_DESERT, "Poe Guide (Desert Wasteland)" }, + { ACTOR_EN_PO_DESERT, "Poe Guide (Haunted Wasteland)" }, { ACTOR_EN_CROW, "Guay" }, { ACTOR_DOOR_KILLER, "Fake Door" }, { ACTOR_BG_SPOT11_OASIS, "Oasis (Desert Colossus)" }, diff --git a/soh/soh/Enhancements/randomizer/randomizer_check_objects.cpp b/soh/soh/Enhancements/randomizer/randomizer_check_objects.cpp index 9a3669582..d7f99edfb 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_check_objects.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_check_objects.cpp @@ -783,7 +783,7 @@ std::map rcAreaNames = { { RCAREA_LAKE_HYLIA, "Lake Hylia"}, { RCAREA_GERUDO_VALLEY, "Gerudo Valley"}, { RCAREA_GERUDO_FORTRESS, "Gerudo Fortress"}, - { RCAREA_WASTELAND, "Desert Wasteland"}, + { RCAREA_WASTELAND, "Haunted Wasteland"}, { RCAREA_DESERT_COLOSSUS, "Desert Colossus"}, { RCAREA_MARKET, "Hyrule Market"}, { RCAREA_HYRULE_CASTLE, "Hyrule Castle"}, diff --git a/soh/soh/Enhancements/randomizer/randomizer_tricks.cpp b/soh/soh/Enhancements/randomizer/randomizer_tricks.cpp index 398077f6e..8a397f840 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_tricks.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_tricks.cpp @@ -219,7 +219,7 @@ std::unordered_map rtAreaNames = { { RTAREA_LAKE_HYLIA, "Lake Hylia"}, { RTAREA_GERUDO_VALLEY, "Gerudo Valley"}, { RTAREA_GERUDO_FORTRESS, "Gerudo Fortress"}, - { RTAREA_WASTELAND, "Desert Wasteland"}, + { RTAREA_WASTELAND, "Haunted Wasteland"}, { RTAREA_DESERT_COLOSSUS, "Desert Colossus"}, { RTAREA_MARKET, "Hyrule Market"}, { RTAREA_HYRULE_CASTLE, "Hyrule Castle"}, From fe9c0fa4f722e01a099addb13268a4dedb2d71c7 Mon Sep 17 00:00:00 2001 From: briaguya <70942617+briaguya-ai@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:10:56 -0800 Subject: [PATCH 05/15] bump lus (#3394) --- libultraship | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libultraship b/libultraship index c75ff3653..9509806ae 160000 --- a/libultraship +++ b/libultraship @@ -1 +1 @@ -Subproject commit c75ff3653f699cb1a8c017b10e4b3986259d8cf0 +Subproject commit 9509806ae3ca6e35882fb976de70c5bde471b8f5 From 304016ddd2ac45e5101e03faa66f0a88f431d213 Mon Sep 17 00:00:00 2001 From: Malkierian Date: Mon, 13 Nov 2023 22:12:08 -0700 Subject: [PATCH 06/15] [Feature Fix] Tunics stolen by like likes now removed from the item buttons (#3375) * Extends `Assignable Boots and Tunics` functionality to check for and remove Goron and Zora tunics from item buttons when like likes steal them. * Comment documentation. --- soh/src/code/code_80097A00.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/soh/src/code/code_80097A00.c b/soh/src/code/code_80097A00.c index 9e20d66aa..eb83738e3 100644 --- a/soh/src/code/code_80097A00.c +++ b/soh/src/code/code_80097A00.c @@ -204,6 +204,17 @@ u8 Inventory_DeleteEquipment(PlayState* play, s16 equipment) { if (equipment == EQUIP_TYPE_TUNIC) { gSaveContext.equips.equipment |= EQUIP_VALUE_TUNIC_KOKIRI << (EQUIP_TYPE_TUNIC * 4); + // non-vanilla: remove goron and zora tunics from item buttons if assignable tunics is on + if (CVarGetInteger("gAssignableTunicsAndBoots", 0) && equipValue != EQUIP_VALUE_TUNIC_KOKIRI) { + ItemID item = (equipValue == EQUIP_VALUE_TUNIC_GORON ? ITEM_TUNIC_GORON : ITEM_TUNIC_ZORA); + for (int i = 1; i < ARRAY_COUNT(gSaveContext.equips.buttonItems); i++) { + if (gSaveContext.equips.buttonItems[i] == item) { + gSaveContext.equips.buttonItems[i] = ITEM_NONE; + gSaveContext.equips.cButtonSlots[i - 1] = SLOT_NONE; + } + } + } + // end non-vanilla } if (equipment == EQUIP_TYPE_SWORD) { From 4e9040d761dfcda2b0788364f76cef2ae8fe70ba Mon Sep 17 00:00:00 2001 From: Malkierian Date: Tue, 14 Nov 2023 14:35:19 -0700 Subject: [PATCH 07/15] [Feature] Remove `performDelayedSave` functionality from Autosave (#3387) * Removes delayed save functionality, making autosave work everywhere except Ganon and Chamber of Sages scenes. * Change AutoSave comment to remove the scenarios we no longer block autosave in. * handle temp B on saving outside of kaleido --------- Co-authored-by: Adam Bird --- soh/soh/Enhancements/mods.cpp | 26 +++----------------------- soh/src/code/z_play.c | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/soh/soh/Enhancements/mods.cpp b/soh/soh/Enhancements/mods.cpp index 6d66ff055..0bbeff8ca 100644 --- a/soh/soh/Enhancements/mods.cpp +++ b/soh/soh/Enhancements/mods.cpp @@ -35,8 +35,6 @@ extern PlayState* gPlayState; extern void Overlay_DisplayText(float duration, const char* text); uint32_t ResourceMgr_IsSceneMasterQuest(s16 sceneNum); } -bool performDelayedSave = false; -bool performSave = false; // TODO: When there's more uses of something like this, create a new GI::RawAction? void ReloadSceneTogglingLinkAge() { @@ -258,14 +256,12 @@ void RegisterOcarinaTimeTravel() { void AutoSave(GetItemEntry itemEntry) { u8 item = itemEntry.itemId; + bool performSave = false; // Don't autosave immediately after buying items from shops to prevent getting them for free! // Don't autosave in the Chamber of Sages since resuming from that map breaks the game // Don't autosave during the Ganon fight when picking up the Master Sword - // Don't autosave in the fishing pond to prevent getting rod on B outside of the pond - // Don't autosave in the bombchu bowling alley to prevent having chus on B outside of the minigame - // Don't autosave in grottos since resuming from grottos breaks the game. if ((CVarGetInteger("gAutosave", AUTOSAVE_OFF) != AUTOSAVE_OFF) && (gPlayState != NULL) && (gSaveContext.pendingSale == ITEM_NONE) && - (gPlayState->gameplayFrames > 60 && gSaveContext.cutsceneIndex < 0xFFF0) && (gPlayState->sceneNum != SCENE_GANON_BOSS)) { + (gPlayState->gameplayFrames > 60 && gSaveContext.cutsceneIndex < 0xFFF0) && (gPlayState->sceneNum != SCENE_GANON_BOSS) && (gPlayState->sceneNum != SCENE_CHAMBER_OF_THE_SAGES)) { if (((CVarGetInteger("gAutosave", AUTOSAVE_OFF) == AUTOSAVE_LOCATION_AND_ALL_ITEMS) || (CVarGetInteger("gAutosave", AUTOSAVE_OFF) == AUTOSAVE_ALL_ITEMS)) && (item != ITEM_NONE)) { // Autosave for all items performSave = true; @@ -326,25 +322,9 @@ void AutoSave(GetItemEntry itemEntry) { CVarGetInteger("gAutosave", AUTOSAVE_OFF) == AUTOSAVE_LOCATION) { performSave = true; } - if (gPlayState->sceneNum == SCENE_FAIRYS_FOUNTAIN || gPlayState->sceneNum == SCENE_GROTTOS || - gPlayState->sceneNum == SCENE_CHAMBER_OF_THE_SAGES || gPlayState->sceneNum == SCENE_FISHING_POND || - gPlayState->sceneNum == SCENE_BOMBCHU_BOWLING_ALLEY) { - if (CVarGetInteger("gAutosave", AUTOSAVE_OFF) == AUTOSAVE_LOCATION_AND_MAJOR_ITEMS || - CVarGetInteger("gAutosave", AUTOSAVE_OFF) == AUTOSAVE_LOCATION_AND_ALL_ITEMS || - CVarGetInteger("gAutosave", AUTOSAVE_OFF) == AUTOSAVE_LOCATION) { - performSave = false; - return; - } - if (performSave) { - performSave = false; - performDelayedSave = true; - } - return; - } - if (performSave || performDelayedSave) { + if (performSave) { Play_PerformSave(gPlayState); performSave = false; - performDelayedSave = false; } } } diff --git a/soh/src/code/z_play.c b/soh/src/code/z_play.c index bf7016884..841de8f13 100644 --- a/soh/src/code/z_play.c +++ b/soh/src/code/z_play.c @@ -2329,8 +2329,28 @@ void Play_PerformSave(PlayState* play) { if (play != NULL && gSaveContext.fileNum != 0xFF) { Play_SaveSceneFlags(play); gSaveContext.savedSceneNum = play->sceneNum; + + // Track values from temp B + uint8_t prevB = gSaveContext.equips.buttonItems[0]; + uint8_t prevStatus = gSaveContext.buttonStatus[0]; + + // Replicate the B button restore from minigames/epona that kaleido does + if (gSaveContext.equips.buttonItems[0] == ITEM_SLINGSHOT || + gSaveContext.equips.buttonItems[0] == ITEM_BOW || + gSaveContext.equips.buttonItems[0] == ITEM_BOMBCHU || + gSaveContext.equips.buttonItems[0] == ITEM_FISHING_POLE || + (gSaveContext.equips.buttonItems[0] == ITEM_NONE && !Flags_GetInfTable(INFTABLE_SWORDLESS))) { + + gSaveContext.equips.buttonItems[0] = gSaveContext.buttonStatus[0]; + Interface_RandoRestoreSwordless(); + } + Save_SaveFile(); + // Restore temp B values back + gSaveContext.equips.buttonItems[0] = prevB; + gSaveContext.buttonStatus[0] = prevStatus; + uint8_t triforceHuntCompleted = IS_RANDO && gSaveContext.triforcePiecesCollected == Randomizer_GetSettingValue(RSK_TRIFORCE_HUNT_PIECES_REQUIRED) && From bf31f2b330b53a212437cdcda9d6e0e147cc8c63 Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Tue, 14 Nov 2023 15:36:05 -0600 Subject: [PATCH 08/15] Stop hardcoding skeleton type to flex (#3397) --- soh/soh/resource/importer/SkeletonFactory.cpp | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/soh/soh/resource/importer/SkeletonFactory.cpp b/soh/soh/resource/importer/SkeletonFactory.cpp index 559da0cea..2ce651dee 100644 --- a/soh/soh/resource/importer/SkeletonFactory.cpp +++ b/soh/soh/resource/importer/SkeletonFactory.cpp @@ -103,38 +103,42 @@ void SkeletonFactoryV0::ParseFileXML(tinyxml2::XMLElement* reader, std::shared_p { std::shared_ptr skel = std::static_pointer_cast(resource); - std::string skeletonType = reader->Attribute("Type"); - // std::string skeletonLimbType = reader->Attribute("LimbType"); - int numLimbs = reader->IntAttribute("LimbCount"); - int numDLs = reader->IntAttribute("DisplayListCount"); + skel->type = SkeletonType::Flex; // Default to Flex for legacy reasons + if (reader->FindAttribute("Type")) { + std::string skeletonType = reader->Attribute("Type"); - if (skeletonType == "Flex") { - skel->type = SkeletonType::Flex; - } else if (skeletonType == "Curve") { - skel->type = SkeletonType::Curve; - } else if (skeletonType == "Normal") { - skel->type = SkeletonType::Normal; + if (skeletonType == "Flex") { + skel->type = SkeletonType::Flex; + } else if (skeletonType == "Curve") { + skel->type = SkeletonType::Curve; + } else if (skeletonType == "Normal") { + skel->type = SkeletonType::Normal; + } } - skel->type = SkeletonType::Flex; - skel->limbType = LimbType::LOD; + skel->limbType = LimbType::LOD; // Default to LOD for legacy reasons + if (reader->FindAttribute("LimbType")) { + std::string skeletonLimbType = reader->Attribute("LimbType"); - // if (skeletonLimbType == "Standard") - // skel->limbType = LimbType::Standard; - // else if (skeletonLimbType == "LOD") - // skel->limbType = LimbType::LOD; - // else if (skeletonLimbType == "Curve") - // skel->limbType = LimbType::Curve; - // else if (skeletonLimbType == "Skin") - // skel->limbType = LimbType::Skin; - // else if (skeletonLimbType == "Legacy") - // Sskel->limbType = LimbType::Legacy; + if (skeletonLimbType == "Standard") { + skel->limbType = LimbType::Standard; + } else if (skeletonLimbType == "LOD") { + skel->limbType = LimbType::LOD; + } else if (skeletonLimbType == "Curve") { + skel->limbType = LimbType::Curve; + } else if (skeletonLimbType == "Skin") { + skel->limbType = LimbType::Skin; + } else if (skeletonLimbType == "Legacy") { + skel->limbType = LimbType::Legacy; + } + } + + + skel->limbCount = reader->IntAttribute("LimbCount"); + skel->dListCount = reader->IntAttribute("DisplayListCount"); auto child = reader->FirstChildElement(); - skel->limbCount = numLimbs; - skel->dListCount = numDLs; - while (child != nullptr) { std::string childName = child->Name(); From e66eb8756d572c49101eba4c016848e93967bfdb Mon Sep 17 00:00:00 2001 From: Adam Bird Date: Tue, 14 Nov 2023 16:37:03 -0500 Subject: [PATCH 09/15] Fix: Prevent patching custom models (#3367) * fix prevent patching custom models * prevent patching chests textures for custom chest models * add tooltip for cosmetic editor about custom models * chest texture handling for alt toggles --- .../Enhancements/cosmetics/CosmeticsEditor.cpp | 4 ++++ soh/soh/OTRGlobals.cpp | 15 +++++++++++++++ soh/soh/OTRGlobals.h | 1 + soh/soh/SohMenuBar.cpp | 2 ++ soh/src/overlays/actors/ovl_En_Box/z_en_box.c | 16 ++++++++++++++-- 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp index 8c2707453..9c56ac32b 100644 --- a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp +++ b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp @@ -1767,6 +1767,10 @@ void CosmeticsEditorWindow::DrawElement() { ImGui::SameLine(); UIWidgets::EnhancementCombobox("gCosmetics.DefaultColorScheme", colorSchemes, COLORSCHEME_N64); UIWidgets::EnhancementCheckbox("Advanced Mode", "gCosmetics.AdvancedMode"); + UIWidgets::InsertHelpHoverText( + "Some cosmetic options may not apply if you have any mods that provide custom models for the cosmetic option.\n\n" + "For example, if you have custom Link model, then the Link's Hair color option will most likely not apply." + ); if (CVarGetInteger("gCosmetics.AdvancedMode", 0)) { if (ImGui::Button("Lock All Advanced", ImVec2(ImGui::GetContentRegionAvail().x / 2, 30.0f))) { for (auto& [id, cosmeticOption] : cosmeticOptions) { diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 1199a7e99..ad57d1ca5 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -1569,6 +1569,11 @@ extern "C" Gfx* ResourceMgr_LoadGfxByName(const char* path) return (Gfx*)&res->Instructions[0]; } +extern "C" uint8_t ResourceMgr_FileIsCustomByName(const char* path) { + auto res = std::static_pointer_cast(GetResourceByNameHandlingMQ(path)); + return res->GetInitData()->IsCustom; +} + typedef struct { int index; Gfx instruction; @@ -1600,6 +1605,11 @@ extern "C" void ResourceMgr_PatchGfxByName(const char* path, const char* patchNa // index /= 2; // } + // Do not patch custom assets as they most likely do not have the same instructions as authentic assets + if (res->GetInitData()->IsCustom) { + return; + } + Gfx* gfx = (Gfx*)&res->Instructions[index]; if (!originalGfx.contains(path) || !originalGfx[path].contains(patchName)) { @@ -1616,6 +1626,11 @@ extern "C" void ResourceMgr_PatchGfxCopyCommandByName(const char* path, const ch auto res = std::static_pointer_cast( LUS::Context::GetInstance()->GetResourceManager()->LoadResource(path)); + // Do not patch custom assets as they most likely do not have the same instructions as authentic assets + if (res->GetInitData()->IsCustom) { + return; + } + Gfx* destinationGfx = (Gfx*)&res->Instructions[destinationIndex]; Gfx sourceGfx = res->Instructions[sourceIndex]; diff --git a/soh/soh/OTRGlobals.h b/soh/soh/OTRGlobals.h index 82659dbc4..e00cfecd5 100644 --- a/soh/soh/OTRGlobals.h +++ b/soh/soh/OTRGlobals.h @@ -101,6 +101,7 @@ AnimationHeaderCommon* ResourceMgr_LoadAnimByName(const char* path); char* ResourceMgr_GetNameByCRC(uint64_t crc, char* alloc); Gfx* ResourceMgr_LoadGfxByCRC(uint64_t crc); Gfx* ResourceMgr_LoadGfxByName(const char* path); +uint8_t ResourceMgr_FileIsCustomByName(const char* path); void ResourceMgr_PatchGfxByName(const char* path, const char* patchName, int index, Gfx instruction); void ResourceMgr_UnpatchGfxByName(const char* path, const char* patchName); char* ResourceMgr_LoadArrayByNameAsVec3s(const char* path); diff --git a/soh/soh/SohMenuBar.cpp b/soh/soh/SohMenuBar.cpp index a5fc12299..a71d2b8e3 100644 --- a/soh/soh/SohMenuBar.cpp +++ b/soh/soh/SohMenuBar.cpp @@ -532,6 +532,8 @@ void DrawEnhancementsMenu() { " - Small keys: Small silver chest\n" " - Boss keys: Vanilla size and texture\n" " - Skulltula Tokens: Small skulltula chest\n" + "\n" + "NOTE: Textures will not apply if you are using a mod pack with a custom chest model." ); if (CVarGetInteger("gChestSizeAndTextureMatchesContents", CSMC_DISABLED) != CSMC_DISABLED) { UIWidgets::PaddedEnhancementCheckbox("Chests of Agony", "gChestSizeDependsStoneOfAgony", true, false); diff --git a/soh/src/overlays/actors/ovl_En_Box/z_en_box.c b/soh/src/overlays/actors/ovl_En_Box/z_en_box.c index 1e2c4d6cd..9e8705c33 100644 --- a/soh/src/overlays/actors/ovl_En_Box/z_en_box.c +++ b/soh/src/overlays/actors/ovl_En_Box/z_en_box.c @@ -88,6 +88,7 @@ Gfx gKeyTreasureChestChestFrontDL[128] = {0}; Gfx gChristmasRedTreasureChestChestFrontDL[128] = {0}; Gfx gChristmasGreenTreasureChestChestFrontDL[128] = {0}; u8 hasCreatedRandoChestTextures = 0; +u8 hasCustomChestDLs = 0; u8 hasChristmasChestTexturesAvailable = 0; void EnBox_SetupAction(EnBox* this, EnBoxActionFunc actionFunc) { @@ -690,7 +691,7 @@ void EnBox_UpdateSizeAndTexture(EnBox* this, PlayState* play) { } // Change texture - if (!isVanilla && (csmc == CSMC_BOTH || csmc == CSMC_TEXTURE)) { + if (!isVanilla && hasCreatedRandoChestTextures && !hasCustomChestDLs && (csmc == CSMC_BOTH || csmc == CSMC_TEXTURE)) { switch (getItemCategory) { case ITEM_CATEGORY_MAJOR: this->boxBodyDL = gGoldTreasureChestChestFrontDL; @@ -725,7 +726,7 @@ void EnBox_UpdateSizeAndTexture(EnBox* this, PlayState* play) { } } - if (CVarGetInteger("gLetItSnow", 0) && hasChristmasChestTexturesAvailable) { + if (CVarGetInteger("gLetItSnow", 0) && hasChristmasChestTexturesAvailable && hasCreatedRandoChestTextures && !hasCustomChestDLs) { if (this->dyna.actor.scale.x == 0.01f) { this->boxBodyDL = gChristmasRedTreasureChestChestFrontDL; this->boxLidDL = gChristmasRedTreasureChestChestSideAndLidDL; @@ -767,7 +768,18 @@ void EnBox_UpdateSizeAndTexture(EnBox* this, PlayState* play) { } void EnBox_CreateExtraChestTextures() { + // Don't patch textures for custom chest models, as they do not import textures the exact same way as vanilla chests + // OTRTODO: Make it so model packs can provide a unique DL per chest type, instead of us copying the brown chest and attempting to patch + if (ResourceMgr_FileIsCustomByName(gTreasureChestChestFrontDL) || + ResourceMgr_FileIsCustomByName(gTreasureChestChestSideAndLidDL)) { + hasCustomChestDLs = 1; + return; + } + + hasCustomChestDLs = 0; + if (hasCreatedRandoChestTextures) return; + Gfx gTreasureChestChestTextures[] = { gsDPSetTextureImage(G_IM_FMT_RGBA, G_IM_SIZ_16b, 1, gSkullTreasureChestFrontTex), gsDPSetTextureImage(G_IM_FMT_RGBA, G_IM_SIZ_16b, 1, gSkullTreasureChestSideAndTopTex), From bd0672767af6b3f5e229b300b814e1d896731470 Mon Sep 17 00:00:00 2001 From: AltoXorg <56553686+Alto1772@users.noreply.github.com> Date: Wed, 15 Nov 2023 05:37:54 +0800 Subject: [PATCH 10/15] Use substr method to determine file extension (#3390) See https://github.com/HarbourMasters/OTRExporter/pull/12 --- OTRExporter | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OTRExporter b/OTRExporter index 0d8f5570a..04b85b95f 160000 --- a/OTRExporter +++ b/OTRExporter @@ -1 +1 @@ -Subproject commit 0d8f5570a8e57f302ec6633d65615ee21ab39454 +Subproject commit 04b85b95fab07a394b62dcd28a502a3040f08e0c From ba987c49e2e4bfaaccdcff740709489bf59b34d5 Mon Sep 17 00:00:00 2001 From: Malkierian Date: Tue, 14 Nov 2023 14:46:38 -0700 Subject: [PATCH 11/15] SaveManager cleanup (#3386) * Move threadpool initialization and `OnExitGame` registration from `SaveManager::Init` to SM's constructor. Comment on `Init` to mention it's not an initializer for `SaveManager`. Added check for `SaveManager::SaveSection` to prevent firing a save worker if the game is already exited from a reset. * Removed `IsSaveLoaded` check in favor of another `ThreadPoolWait()` at the start of `SaveManager::Init()`. --- soh/soh/SaveManager.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp index 14ab736dc..2d1f6e68b 100644 --- a/soh/soh/SaveManager.cpp +++ b/soh/soh/SaveManager.cpp @@ -65,6 +65,10 @@ SaveManager::SaveManager() { AddInitFunction(InitFileImpl); + GameInteractor::Instance->RegisterGameHook([this](uint32_t fileNum) { ThreadPoolWait(); }); + + smThreadPool = std::make_shared(1); + for (SaveFileMetaInfo& info : fileMetaInfo) { info.valid = false; info.deaths = 0; @@ -357,12 +361,14 @@ void SaveManager::SaveRandomizer(SaveContext* saveContext, int sectionID, bool f }); } +// Init() here is an extension of InitSram, and thus not truly an initializer for SaveManager itself. don't put any class initialization stuff here void SaveManager::Init() { + // Wait on saves that snuck through the Wait in OnExitGame + ThreadPoolWait(); const std::filesystem::path sSavePath(LUS::Context::GetPathRelativeToAppDirectory("Save")); const std::filesystem::path sGlobalPath = sSavePath / std::string("global.sav"); auto sOldSavePath = LUS::Context::GetPathRelativeToAppDirectory("oot_save.sav"); auto sOldBackupSavePath = LUS::Context::GetPathRelativeToAppDirectory("oot_save.bak"); - GameInteractor::Instance->RegisterGameHook([this](uint32_t fileNum) { ThreadPoolWait(); }); // If the save directory does not exist, create it if (!std::filesystem::exists(sSavePath)) { @@ -403,7 +409,6 @@ void SaveManager::Init() { } else { CreateDefaultGlobal(); } - smThreadPool = std::make_shared(1); // Load files to initialize metadata for (int fileNum = 0; fileNum < MaxFiles; fileNum++) { From fb45b66903b2c7449a4673a331c2117a94ce02cb Mon Sep 17 00:00:00 2001 From: Josh Bodner <30329717+jbodner09@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:08:45 -0800 Subject: [PATCH 12/15] Fix magic being zeroed out when using fast file select (#3389) * Move to frame counter init to a place that fast file select also touches * Undo removing old fix * Reset on gameover --- soh/soh/Enhancements/debugconsole.cpp | 1 + soh/src/code/z_play.c | 7 +++++++ .../overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c | 2 ++ 3 files changed, 10 insertions(+) diff --git a/soh/soh/Enhancements/debugconsole.cpp b/soh/soh/Enhancements/debugconsole.cpp index 3e61505b2..88a23718e 100644 --- a/soh/soh/Enhancements/debugconsole.cpp +++ b/soh/soh/Enhancements/debugconsole.cpp @@ -207,6 +207,7 @@ static bool ResetHandler(std::shared_ptr Console, std::vectorgameplayFrames = 0; SET_NEXT_GAMESTATE(&gPlayState->state, TitleSetup_Init, GameState); gPlayState->state.running = false; GameInteractor::Instance->ExecuteHooks(gSaveContext.fileNum); diff --git a/soh/src/code/z_play.c b/soh/src/code/z_play.c index 841de8f13..59a5d0619 100644 --- a/soh/src/code/z_play.c +++ b/soh/src/code/z_play.c @@ -33,6 +33,7 @@ u64 D_801614D0[0xA00]; #endif PlayState* gPlayState; +s16 firstInit = 0; s16 gEnPartnerId; @@ -490,6 +491,12 @@ void Play_Init(GameState* thisx) { } } + // Properly initialize the frame counter so it doesn't use garbage data + if (!firstInit) { + play->gameplayFrames = 0; + firstInit = 1; + } + // Invalid entrance, so immediately exit the game to opening title if (gSaveContext.entranceIndex == -1) { gSaveContext.entranceIndex = 0; diff --git a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c index 55a5075a6..35490f079 100644 --- a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c +++ b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c @@ -4290,6 +4290,8 @@ void KaleidoScope_Update(PlayState* play) if (IS_RANDO && Randomizer_GetSettingValue(RSK_SHUFFLE_ENTRANCES)) { Grotto_ForceGrottoReturn(); } + // Reset frame counter to prevent autosave on respawn + play->gameplayFrames = 0; gSaveContext.nextTransitionType = 2; gSaveContext.health = CVarGetInteger("gFullHealthSpawn", 0) ? gSaveContext.healthCapacity : 0x30; Audio_QueueSeqCmd(0xF << 28 | SEQ_PLAYER_BGM_MAIN << 24 | 0xA); From afe032ea215215273a38b810d5c7be0705aaa460 Mon Sep 17 00:00:00 2001 From: Malkierian Date: Tue, 14 Nov 2023 18:46:50 -0700 Subject: [PATCH 13/15] [Feature/fix] Save to temp file first (#3376) * Add temp file flow to `SaveManager::SaveFileThreaded`. Add "Save finish" info log message. * Fix WiiU/Switch --- soh/soh/SaveManager.cpp | 84 +++++++++++++++++++++++++++-------------- soh/soh/SaveManager.h | 1 + 2 files changed, 57 insertions(+), 28 deletions(-) diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp index 2d1f6e68b..e61cf9223 100644 --- a/soh/soh/SaveManager.cpp +++ b/soh/soh/SaveManager.cpp @@ -47,6 +47,11 @@ std::filesystem::path SaveManager::GetFileName(int fileNum) { return sSavePath / ("file" + std::to_string(fileNum + 1) + ".sav"); } +std::filesystem::path SaveManager::GetFileTempName(int fileNum) { + const std::filesystem::path sSavePath(LUS::Context::GetPathRelativeToAppDirectory("Save")); + return sSavePath / ("file" + std::to_string(fileNum + 1) + ".temp"); +} + SaveManager::SaveManager() { coreSectionIDsByName["base"] = SECTION_ID_BASE; coreSectionIDsByName["randomizer"] = SECTION_ID_RANDOMIZER; @@ -874,6 +879,32 @@ void SaveManager::InitFileMaxed() { gSaveContext.sceneFlags[5].swch = 0x40000000; } +#if defined(__WIIU__) || defined(__SWITCH__) +// std::filesystem::copy_file doesn't work properly with the Wii U's toolchain atm +int copy_file(const char* src, const char* dst) { + alignas(0x40) uint8_t buf[4096]; + FILE* r = fopen(src, "r"); + if (!r) { + return -1; + } + FILE* w = fopen(dst, "w"); + if (!w) { + return -2; + } + + size_t res; + while ((res = fread(buf, 1, sizeof(buf), r)) > 0) { + if (fwrite(buf, 1, res, w) != res) { + break; + } + } + + fclose(r); + fclose(w); + return res >= 0 ? 0 : res; +} +#endif + // Threaded SaveFile takes copy of gSaveContext for local unmodified storage void SaveManager::SaveFileThreaded(int fileNum, SaveContext* saveContext, int sectionID) { @@ -915,19 +946,42 @@ void SaveManager::SaveFileThreaded(int fileNum, SaveContext* saveContext, int se svi.func(saveContext, sectionID, false); } + std::filesystem::path fileName = GetFileName(fileNum); + std::filesystem::path tempFile = GetFileTempName(fileNum); + + if (std::filesystem::exists(tempFile)) { + std::filesystem::remove(tempFile); + } + #if defined(__SWITCH__) || defined(__WIIU__) - FILE* w = fopen(GetFileName(fileNum).c_str(), "w"); + FILE* w = fopen(tempFile.c_str(), "w"); std::string json_string = saveBlock.dump(4); fwrite(json_string.c_str(), sizeof(char), json_string.length(), w); fclose(w); #else - std::ofstream output(GetFileName(fileNum)); + std::ofstream output(tempFile); output << std::setw(4) << saveBlock << std::endl; + output.close(); #endif + if (std::filesystem::exists(fileName)) { + std::filesystem::remove(fileName); + } + +#if defined(__SWITCH__) || defined(__WIIU__) + copy_file(tempFile.c_str(), fileName.c_str()); +#else + std::filesystem::copy_file(tempFile, fileName); +#endif + + if (std::filesystem::exists(tempFile)) { + std::filesystem::remove(tempFile); + } + delete saveContext; InitMeta(fileNum); GameInteractor::Instance->ExecuteHooks(fileNum); + SPDLOG_INFO("Save File Finish - fileNum: {}", fileNum); } // SaveSection creates a copy of gSaveContext to prevent mid-save data modification, and passes its reference to SaveFileThreaded @@ -2110,32 +2164,6 @@ void SaveManager::LoadStruct(const std::string& name, LoadStructFunc func) { } } -#if defined(__WIIU__) || defined(__SWITCH__) -// std::filesystem::copy_file doesn't work properly with the Wii U's toolchain atm -int copy_file(const char* src, const char* dst) { - alignas(0x40) uint8_t buf[4096]; - FILE* r = fopen(src, "r"); - if (!r) { - return -1; - } - FILE* w = fopen(dst, "w"); - if (!w) { - return -2; - } - - size_t res; - while ((res = fread(buf, 1, sizeof(buf), r)) > 0) { - if (fwrite(buf, 1, res, w) != res) { - break; - } - } - - fclose(r); - fclose(w); - return res >= 0 ? 0 : res; -} -#endif - void SaveManager::CopyZeldaFile(int from, int to) { assert(std::filesystem::exists(GetFileName(from))); DeleteZeldaFile(to); diff --git a/soh/soh/SaveManager.h b/soh/soh/SaveManager.h index 892aec923..310ecda7d 100644 --- a/soh/soh/SaveManager.h +++ b/soh/soh/SaveManager.h @@ -142,6 +142,7 @@ class SaveManager { private: std::filesystem::path GetFileName(int fileNum); + std::filesystem::path GetFileTempName(int fileNum); nlohmann::json saveBlock; void ConvertFromUnversioned(); From 044d32a46f1ed5bd7287b78d5c52233b27c0d262 Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Tue, 14 Nov 2023 19:47:07 -0600 Subject: [PATCH 14/15] Add gFixEyesOpenWhileSleeping (#3365) --- soh/soh/SohMenuBar.cpp | 2 ++ soh/src/overlays/actors/ovl_player_actor/z_player.c | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/soh/soh/SohMenuBar.cpp b/soh/soh/SohMenuBar.cpp index a71d2b8e3..8db6c7703 100644 --- a/soh/soh/SohMenuBar.cpp +++ b/soh/soh/SohMenuBar.cpp @@ -1061,6 +1061,8 @@ void DrawEnhancementsMenu() { UIWidgets::Tooltip("Fixes the bushes to drop items correctly rather than spawning undefined items."); UIWidgets::PaddedEnhancementCheckbox("Fix falling from vine edges", "gFixVineFall", true, false); UIWidgets::Tooltip("Prevents immediately falling off climbable surfaces if climbing on the edges."); + UIWidgets::PaddedEnhancementCheckbox("Fix Link's eyes open while sleeping", "gFixEyesOpenWhileSleeping", true, false); + UIWidgets::Tooltip("Fixes Link's eyes being open in the opening cutscene when he is supposed to be sleeping."); ImGui::EndMenu(); } diff --git a/soh/src/overlays/actors/ovl_player_actor/z_player.c b/soh/src/overlays/actors/ovl_player_actor/z_player.c index 09e33f2e4..497935a69 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -15169,6 +15169,10 @@ void func_80852C50(PlayState* play, Player* this, CsCmdActorAction* arg2) { sp24 = D_808547C4[this->unk_446]; func_80852B4C(play, this, linkCsAction, &D_80854E50[ABS(sp24)]); + + if (CVarGetInteger("gFixEyesOpenWhileSleeping", 0) && (play->csCtx.linkAction->action == 28 || play->csCtx.linkAction->action == 29)) { + this->skelAnime.jointTable[22].x = 8; + } } void func_80852E14(Player* this, PlayState* play) { From 2dfbbc63e3358f4fec9c518f6cba978aa93d71db Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Tue, 14 Nov 2023 20:14:47 -0600 Subject: [PATCH 15/15] Version bump to MacReady Bravo (#3398) --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0633ab80a..fccae7f36 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,8 +5,8 @@ set(CMAKE_CXX_STANDARD 20 CACHE STRING "The C++ standard to use") set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version") -project(Ship VERSION 8.0.0 LANGUAGES C CXX) -set(PROJECT_BUILD_NAME "MacReady Alfa" CACHE STRING "") +project(Ship VERSION 8.0.1 LANGUAGES C CXX) +set(PROJECT_BUILD_NAME "MacReady Bravo" CACHE STRING "") set(PROJECT_TEAM "github.com/harbourmasters" CACHE STRING "") set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT soh)