diff --git a/soh/soh/Enhancements/debugger/debugSaveEditor.h b/soh/soh/Enhancements/debugger/debugSaveEditor.h index b99931a6e..04e01917d 100644 --- a/soh/soh/Enhancements/debugger/debugSaveEditor.h +++ b/soh/soh/Enhancements/debugger/debugSaveEditor.h @@ -1164,6 +1164,9 @@ const std::vector flagTables = { { RAND_INF_COLOSSUS_GREAT_FAIRY_REWARD, "RAND_INF_COLOSSUS_GREAT_FAIRY_REWARD" }, { RAND_INF_OGC_GREAT_FAIRY_REWARD, "RAND_INF_OGC_GREAT_FAIRY_REWARD" }, + { RAND_INF_ZELDAS_LETTER, "RAND_INF_ZELDAS_LETTER" }, + { RAND_INF_WEIRD_EGG, "RAND_INF_WEIRD_EGG" }, + { RAND_INF_KF_SOUTH_GRASS_WEST_RUPEE, "RAND_INF_KF_SOUTH_GRASS_WEST_RUPEE" }, { RAND_INF_KF_NORTH_GRASS_WEST_RUPEE, "RAND_INF_KF_NORTH_GRASS_WEST_RUPEE" }, { RAND_INF_KF_NORTH_GRASS_EAST_RUPEE, "RAND_INF_KF_NORTH_GRASS_EAST_RUPEE" }, diff --git a/soh/soh/Enhancements/debugger/performanceTimer.cpp b/soh/soh/Enhancements/debugger/performanceTimer.cpp index 69ffc6925..54e1cb983 100644 --- a/soh/soh/Enhancements/debugger/performanceTimer.cpp +++ b/soh/soh/Enhancements/debugger/performanceTimer.cpp @@ -12,6 +12,10 @@ std::chrono::duration GetPerformanceTimer(TimerID timer){ return totalTimes[timer]; } +void ResetPerformanceTimer(TimerID timer) { + totalTimes[timer] = {}; +} + void ResetPerformanceTimers(){ totalTimes = {}; } \ No newline at end of file diff --git a/soh/soh/Enhancements/debugger/performanceTimer.h b/soh/soh/Enhancements/debugger/performanceTimer.h index f4bc8f017..b94ab5f6d 100644 --- a/soh/soh/Enhancements/debugger/performanceTimer.h +++ b/soh/soh/Enhancements/debugger/performanceTimer.h @@ -25,12 +25,14 @@ typedef enum { PT_TOD_ACCESS, PT_ENTRANCE_LOGIC, PT_LOCATION_LOGIC, + PT_RECALCULATE_AVAILABLE_CHECKS, PT_MAX } TimerID; void StartPerformanceTimer(TimerID timer); void StopPerformanceTimer(TimerID timer); std::chrono::duration GetPerformanceTimer(TimerID timer); +void ResetPerformanceTimer(TimerID timer); void ResetPerformanceTimers(); static std::array, PT_MAX> totalTimes = {}; static std::array timeStarted = {}; diff --git a/soh/soh/Enhancements/randomizer/3drando/fill.cpp b/soh/soh/Enhancements/randomizer/3drando/fill.cpp index 56675d41d..356bc507b 100644 --- a/soh/soh/Enhancements/randomizer/3drando/fill.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/fill.cpp @@ -402,7 +402,13 @@ bool AddCheckToLogic(LocationAccess& locPair, GetAccessibleLocationsStruct& gals Rando::ItemLocation* location = ctx->GetItemLocation(loc); RandomizerGet locItem = location->GetPlacedRandomizerGet(); - if (!location->IsAddedToPool() && locPair.ConditionsMet(parentRegion)) { + if (!location->IsAddedToPool() && locPair.ConditionsMet(parentRegion, gals.calculatingAvailableChecks)) { + if (gals.calculatingAvailableChecks) { + gals.accessibleLocations.push_back(loc); + StopPerformanceTimer(PT_LOCATION_LOGIC); + return false; + } + location->AddToPool(); if (locItem == RG_NONE) { @@ -498,19 +504,23 @@ void ProcessRegion(Region* region, GetAccessibleLocationsStruct& gals, Randomize } // Return any of the targetLocations that are accessible in logic -std::vector ReachabilitySearch(const std::vector& targetLocations, RandomizerGet ignore /* = RG_NONE*/) { +std::vector ReachabilitySearch(const std::vector& targetLocations, RandomizerGet ignore /* = RG_NONE*/, bool calculatingAvailableChecks /* = false */) { auto ctx = Rando::Context::GetInstance(); GetAccessibleLocationsStruct gals(0); - ResetLogic(ctx, gals, true); + gals.calculatingAvailableChecks = calculatingAvailableChecks; + ResetLogic(ctx, gals, !calculatingAvailableChecks); do { gals.InitLoop(); for (size_t i = 0; i < gals.regionPool.size(); i++) { ProcessRegion(RegionTable(gals.regionPool[i]), gals, ignore); } } while (gals.logicUpdated); - erase_if(gals.accessibleLocations, [&targetLocations, ctx](RandomizerCheck loc){ + erase_if(gals.accessibleLocations, [&targetLocations, ctx, calculatingAvailableChecks](RandomizerCheck loc) { + if (ctx->GetItemLocation(loc)->GetPlacedRandomizerGet() != RG_NONE && !calculatingAvailableChecks) { + return false; + } for (RandomizerCheck allowedLocation : targetLocations) { - if (loc == allowedLocation || ctx->GetItemLocation(loc)->GetPlacedRandomizerGet() != RG_NONE) { + if (loc == allowedLocation) { return false; } } diff --git a/soh/soh/Enhancements/randomizer/3drando/fill.hpp b/soh/soh/Enhancements/randomizer/3drando/fill.hpp index 106af7aab..bb013cc13 100644 --- a/soh/soh/Enhancements/randomizer/3drando/fill.hpp +++ b/soh/soh/Enhancements/randomizer/3drando/fill.hpp @@ -34,6 +34,8 @@ struct GetAccessibleLocationsStruct { std::vector itemSphere; std::list entranceSphere; + bool calculatingAvailableChecks = false; + GetAccessibleLocationsStruct(int _maxGsCount){ regionPool = {RR_ROOT}; gsCount = 0; @@ -62,7 +64,7 @@ std::vector GetEmptyLocations(std::vector allo void ProcessRegion(Region* region, GetAccessibleLocationsStruct& gals, RandomizerGet ignore = RG_NONE, bool stopOnBeatable = false, bool addToPlaythrough = false); -std::vector ReachabilitySearch(const std::vector& allowedLocations, RandomizerGet ignore=RG_NONE); +std::vector ReachabilitySearch(const std::vector& allowedLocations, RandomizerGet ignore=RG_NONE, bool calculatingAvailableChecks=false); void GeneratePlaythrough(); diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp index c3500edc0..a3088aabc 100644 --- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp +++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp @@ -236,6 +236,14 @@ void RandomizerOnFlagSetHandler(int16_t flagType, int16_t flag) { Flags_UnsetRandomizerInf(RAND_INF_CHILD_TRADES_HAS_CHICKEN); } + if (flagType == FLAG_EVENT_CHECK_INF && flag == EVENTCHKINF_OBTAINED_ZELDAS_LETTER) { + Flags_SetRandomizerInf(RAND_INF_ZELDAS_LETTER); + } + + if (flagType == FLAG_EVENT_CHECK_INF && flag == EVENTCHKINF_OBTAINED_POCKET_EGG) { + Flags_SetRandomizerInf(RAND_INF_WEIRD_EGG); + } + RandomizerCheck rc = GetRandomizerCheckFromFlag(flagType, flag); if (rc == RC_UNKNOWN_CHECK) return; @@ -351,6 +359,7 @@ void RandomizerOnItemReceiveHandler(GetItemEntry receivedItemEntry) { loc->SetCheckStatus(RCSHOW_COLLECTED); CheckTracker::SpoilAreaFromCheck(randomizerQueuedCheck); CheckTracker::RecalculateAllAreaTotals(); + CheckTracker::RecalculateAvailableChecks(); SaveManager::Instance->SaveSection(gSaveContext.fileNum, SECTION_ID_TRACKER_DATA, true); randomizerQueuedCheck = RC_UNKNOWN_CHECK; randomizerQueuedItemEntry = GET_ITEM_NONE; diff --git a/soh/soh/Enhancements/randomizer/item_location.cpp b/soh/soh/Enhancements/randomizer/item_location.cpp index 00e0366ab..cfdfb4204 100644 --- a/soh/soh/Enhancements/randomizer/item_location.cpp +++ b/soh/soh/Enhancements/randomizer/item_location.cpp @@ -228,5 +228,14 @@ void ItemLocation::ResetVariables() { areas = {}; status = RCSHOW_UNCHECKED; isSkipped = false; + isAvailable = false; +} + +bool ItemLocation::IsAvailable() const { + return isAvailable; +} + +void ItemLocation::SetAvailable(bool isAvailable_) { + isAvailable = isAvailable_; } } \ No newline at end of file diff --git a/soh/soh/Enhancements/randomizer/item_location.h b/soh/soh/Enhancements/randomizer/item_location.h index 142ac1c0b..e60d358eb 100644 --- a/soh/soh/Enhancements/randomizer/item_location.h +++ b/soh/soh/Enhancements/randomizer/item_location.h @@ -56,6 +56,8 @@ class ItemLocation { bool IsFoolishCandidate() const; void SetBarrenCandidate(); void ResetVariables(); + bool IsAvailable() const; + void SetAvailable(bool isAvailable_); private: RandomizerCheck rc; @@ -76,5 +78,6 @@ class ItemLocation { bool barrenCandidate = false; RandomizerCheckStatus status = RCSHOW_UNCHECKED; bool isSkipped = false; + bool isAvailable = false; }; } // namespace Rando \ No newline at end of file diff --git a/soh/soh/Enhancements/randomizer/location_access.cpp b/soh/soh/Enhancements/randomizer/location_access.cpp index 33abc3ce8..e2781e9fb 100644 --- a/soh/soh/Enhancements/randomizer/location_access.cpp +++ b/soh/soh/Enhancements/randomizer/location_access.cpp @@ -11,6 +11,11 @@ #include +extern "C" { + extern SaveContext gSaveContext; + extern PlayState* gPlayState; +} + //generic grotto event list std::vector grottoEvents; @@ -27,7 +32,7 @@ bool LocationAccess::CheckConditionAtAgeTime(bool& age, bool& time) const { return GetConditionsMet(); } -bool LocationAccess::ConditionsMet(Region* parentRegion) const { +bool LocationAccess::ConditionsMet(Region* parentRegion, bool calculatingAvailableChecks) const { //WARNING enterance validation can run this after resetting the access for sphere 0 validation //When refactoring ToD access, either fix the above or do not assume that we //have any access at all just because this is being run @@ -41,8 +46,8 @@ bool LocationAccess::ConditionsMet(Region* parentRegion) const { ) { conditionsMet = true; } - - return conditionsMet && CanBuy(); + + return conditionsMet && (calculatingAvailableChecks || CanBuy()); // TODO: run CanBuy when price is known due to settings } bool LocationAccess::CanBuy() const { @@ -224,8 +229,73 @@ bool MQSpiritSharedBrokenWallRoom(const RandomizerRegion region, ConditionFn con return areaTable[region].MQSpiritShared(condition, true, anyAge); } +bool BeanPlanted(const RandomizerRegion region) { + // swchFlag found using the Actor Viewer to get the Obj_Bean parameters & 0x3F + // not tested with multiple OTRs, but can be automated similarly to GetDungeonSmallKeyDoors + SceneID sceneID; + uint8_t swchFlag; + switch (region) { + case RR_ZORAS_RIVER: + sceneID = SceneID::SCENE_ZORAS_RIVER; + swchFlag = 3; + break; + case RR_THE_GRAVEYARD: + sceneID = SceneID::SCENE_GRAVEYARD; + swchFlag = 3; + break; + case RR_KOKIRI_FOREST: + sceneID = SceneID::SCENE_KOKIRI_FOREST; + swchFlag = 9; + break; + case RR_THE_LOST_WOODS: + sceneID = SceneID::SCENE_LOST_WOODS; + swchFlag = 4; + break; + case RR_LW_BEYOND_MIDO: + sceneID = SceneID::SCENE_LOST_WOODS; + swchFlag = 18; + break; + case RR_DEATH_MOUNTAIN_TRAIL: + sceneID = SceneID::SCENE_DEATH_MOUNTAIN_TRAIL; + swchFlag = 6; + break; + case RR_LAKE_HYLIA: + sceneID = SceneID::SCENE_LAKE_HYLIA; + swchFlag = 1; + break; + case RR_GERUDO_VALLEY: + sceneID = SceneID::SCENE_GERUDO_VALLEY; + swchFlag = 3; + break; + case RR_DMC_CENTRAL_LOCAL: + sceneID = SceneID::SCENE_DEATH_MOUNTAIN_CRATER; + swchFlag = 3; + break; + case RR_DESERT_COLOSSUS: + sceneID = SceneID::SCENE_DESERT_COLOSSUS; + swchFlag = 24; + break; + default: + sceneID = SCENE_ID_MAX; + swchFlag = 0; + break; + } + + // Get the swch value for the scene + uint32_t swch; + if (gPlayState != nullptr && gPlayState->sceneNum == sceneID) { + swch = gPlayState->actorCtx.flags.swch; + } else if (sceneID != SCENE_ID_MAX) { + swch = gSaveContext.sceneFlags[sceneID].swch; + } else { + swch = 0; + } + + return swch >> swchFlag & 1; +} + bool CanPlantBean(const RandomizerRegion region) { - return areaTable[region].CanPlantBeanCheck(); + return areaTable[region].CanPlantBeanCheck() || BeanPlanted(region); } bool BothAges(const RandomizerRegion region) { diff --git a/soh/soh/Enhancements/randomizer/location_access.h b/soh/soh/Enhancements/randomizer/location_access.h index 986d08d6a..ad7061963 100644 --- a/soh/soh/Enhancements/randomizer/location_access.h +++ b/soh/soh/Enhancements/randomizer/location_access.h @@ -75,7 +75,7 @@ class LocationAccess { bool CheckConditionAtAgeTime(bool& age, bool& time) const; - bool ConditionsMet(Region* parentRegion) const; + bool ConditionsMet(Region* parentRegion, bool calculatingAvailableChecks) const; RandomizerCheck GetLocation() const { return location; diff --git a/soh/soh/Enhancements/randomizer/logic.cpp b/soh/soh/Enhancements/randomizer/logic.cpp index 836a817c4..43ff3ce56 100644 --- a/soh/soh/Enhancements/randomizer/logic.cpp +++ b/soh/soh/Enhancements/randomizer/logic.cpp @@ -13,6 +13,11 @@ #include "macros.h" #include "variables.h" #include +#include "StringHelper.h" +#include "soh/resource/type/Scene.h" +#include "soh/resource/type/scenecommand/SetTransitionActorList.h" +#include "src/overlays/actors/ovl_En_Door/z_en_door.h" +#include "src/overlays/actors/ovl_Door_Shutter/z_door_shutter.h" namespace Rando { @@ -87,7 +92,7 @@ namespace Rando { case RG_BOMB_BAG: return CurrentUpgrade(UPG_BOMB_BAG); case RG_MAGIC_SINGLE: - return GetSaveContext()->magicLevel >= 1; + return GetSaveContext()->magicLevel >= 1 || GetSaveContext()->isMagicAcquired; // Songs case RG_ZELDAS_LULLABY: case RG_EPONAS_SONG: @@ -217,6 +222,7 @@ namespace Rando { case RG_GOLDEN_SCALE: return CurrentUpgrade(UPG_SCALE) >= 2; case RG_POCKET_EGG: + return CheckRandoInf(RAND_INF_ADULT_TRADES_HAS_POCKET_EGG); case RG_COJIRO: case RG_ODD_MUSHROOM: case RG_ODD_POTION: @@ -226,7 +232,7 @@ namespace Rando { case RG_EYEBALL_FROG: case RG_EYEDROPS: case RG_CLAIM_CHECK: - return CheckRandoInf(itemName - RG_POCKET_EGG + RAND_INF_ADULT_TRADES_HAS_POCKET_EGG); + return CheckRandoInf(itemName - RG_COJIRO + RAND_INF_ADULT_TRADES_HAS_COJIRO); case RG_BOTTLE_WITH_BIG_POE: case RG_BOTTLE_WITH_BLUE_FIRE: case RG_BOTTLE_WITH_BLUE_POTION: @@ -1509,6 +1515,8 @@ namespace Rando { mSaveContext->isDoubleDefenseAcquired = state; break; case RG_POCKET_EGG: + SetRandoInf(RAND_INF_ADULT_TRADES_HAS_POCKET_EGG, state); + break; case RG_COJIRO: case RG_ODD_MUSHROOM: case RG_ODD_POTION: @@ -1518,7 +1526,7 @@ namespace Rando { case RG_EYEBALL_FROG: case RG_EYEDROPS: case RG_CLAIM_CHECK: - SetRandoInf(randoGet - RG_POCKET_EGG + RAND_INF_ADULT_TRADES_HAS_POCKET_EGG, state); + SetRandoInf(randoGet - RG_COJIRO + RAND_INF_ADULT_TRADES_HAS_COJIRO, state); break; case RG_PROGRESSIVE_HOOKSHOT: { @@ -2096,8 +2104,111 @@ namespace Rando { } } + std::unordered_map SceneToDungeon = { + { SceneID::SCENE_DEKU_TREE, DungeonKey::DEKU_TREE }, + { SceneID::SCENE_DODONGOS_CAVERN, DungeonKey::DODONGOS_CAVERN }, + { SceneID::SCENE_JABU_JABU, DungeonKey::JABU_JABUS_BELLY }, + { SceneID::SCENE_FOREST_TEMPLE, DungeonKey::FOREST_TEMPLE }, + { SceneID::SCENE_FIRE_TEMPLE, DungeonKey::FIRE_TEMPLE }, + { SceneID::SCENE_WATER_TEMPLE, DungeonKey::WATER_TEMPLE }, + { SceneID::SCENE_SPIRIT_TEMPLE, DungeonKey::SPIRIT_TEMPLE }, + { SceneID::SCENE_SHADOW_TEMPLE, DungeonKey::SHADOW_TEMPLE }, + { SceneID::SCENE_BOTTOM_OF_THE_WELL, DungeonKey::BOTTOM_OF_THE_WELL }, + { SceneID::SCENE_ICE_CAVERN, DungeonKey::ICE_CAVERN }, + { SceneID::SCENE_GERUDO_TRAINING_GROUND, DungeonKey::GERUDO_TRAINING_GROUND }, + { SceneID::SCENE_INSIDE_GANONS_CASTLE, DungeonKey::GANONS_CASTLE }, + }; + + // Get the swch bit positions for the dungeon + const std::vector& GetDungeonSmallKeyDoors(SceneID sceneId) { + static const std::vector emptyVector; + auto foundDungeon = SceneToDungeon.find(static_cast(sceneId)); + if (foundDungeon == SceneToDungeon.end()) { + return emptyVector; + } + + bool masterQuest = Rando::Context::GetInstance()->GetDungeon(foundDungeon->second)->IsMQ(); + + // Create a unique key for the dungeon and master quest + uint8_t key = sceneId | (masterQuest << 7); + + static std::unordered_map> dungeonSmallKeyDoors; + auto foundEntry = dungeonSmallKeyDoors.find(key); + if (foundEntry != dungeonSmallKeyDoors.end()) { + return foundEntry->second; + } + dungeonSmallKeyDoors[key] = {}; + + // Get the scene path + SceneTableEntry* sceneTableEntry = &gSceneTable[sceneId]; + std::string scenePath = StringHelper::Sprintf("scenes/%s/%s/%s", masterQuest ? "mq" : "nonmq", + sceneTableEntry->sceneFile.fileName, sceneTableEntry->sceneFile.fileName); + + // Load the scene + std::shared_ptr scene = std::dynamic_pointer_cast( + Ship::Context::GetInstance()->GetResourceManager()->LoadResource(scenePath)); + if (scene == nullptr) { + return emptyVector; + } + + // Find the SetTransitionActorList command + std::shared_ptr transitionActorListCommand = nullptr; + for (auto& command : scene->commands) { + if (command->cmdId == SOH::SceneCommandID::SetTransitionActorList) { + transitionActorListCommand = std::dynamic_pointer_cast(command); + break; + } + } + if (transitionActorListCommand == nullptr) { + return emptyVector; + } + + // Find the bit position for the small key doors + for (auto& transitionActor : transitionActorListCommand->transitionActorList) { + if (transitionActor.id == ACTOR_EN_DOOR) { + uint8_t doorType = (transitionActor.params >> 7) & 7; + if (doorType == DOOR_LOCKED) { + dungeonSmallKeyDoors[key].emplace_back(transitionActor.params & 0x3F); + } + } else if (transitionActor.id == ACTOR_DOOR_SHUTTER) { + uint8_t doorType = (transitionActor.params >> 7) & 15; + if (doorType == SHUTTER_BACK_LOCKED || doorType == SHUTTER_BOSS || doorType == SHUTTER_KEY_LOCKED) { + dungeonSmallKeyDoors[key].emplace_back(transitionActor.params & 0x3F); + } + } + } + + return dungeonSmallKeyDoors[key]; + } + + int8_t GetUsedSmallKeyCount(SceneID sceneId) { + const auto& smallKeyDoors = GetDungeonSmallKeyDoors(sceneId); + + // Get the swch value for the scene + uint32_t swch; + if (gPlayState != nullptr && gPlayState->sceneNum == sceneId) { + swch = gPlayState->actorCtx.flags.swch; + } else { + swch = gSaveContext.sceneFlags[sceneId].swch; + } + + // Count the number of small keys doors unlocked + int8_t unlockedSmallKeyDoors = 0; + for (auto& smallKeyDoor : smallKeyDoors) { + unlockedSmallKeyDoors += swch >> smallKeyDoor & 1; + } + + // RANDOTODO: Account for MQ Water trick that causes the basement lock to unlock when the player clears the stalfos pit. + return unlockedSmallKeyDoors; + } + uint8_t Logic::GetSmallKeyCount(uint32_t dungeonIndex) { - return mSaveContext->inventory.dungeonKeys[dungeonIndex]; + int8_t dungeonKeys = mSaveContext->inventory.dungeonKeys[dungeonIndex]; + if (dungeonKeys == -1) { + // never got keys, so can't have used keys + return 0; + } + return dungeonKeys + GetUsedSmallKeyCount(SceneID(dungeonIndex)); } void Logic::SetSmallKeyCount(uint32_t dungeonIndex, uint8_t count) { @@ -2172,7 +2283,8 @@ namespace Rando { IsKeysanity = ctx->GetOption(RSK_KEYSANITY).Is(RO_DUNGEON_ITEM_LOC_ANYWHERE) || ctx->GetOption(RSK_KEYSANITY).Is(RO_DUNGEON_ITEM_LOC_ANYWHERE) || ctx->GetOption(RSK_KEYSANITY).Is(RO_DUNGEON_ITEM_LOC_ANYWHERE); - AmmoCanDrop = /*AmmoDrops.IsNot(AMMODROPS_NONE) TODO: AmmoDrop setting*/ true; + + //AmmoCanDrop = /*AmmoDrops.IsNot(AMMODROPS_NONE)*/ false; TODO: AmmoDrop setting //Child item logic SkullMask = false; diff --git a/soh/soh/Enhancements/randomizer/logic.h b/soh/soh/Enhancements/randomizer/logic.h index 2f68017e2..5b0922d6e 100644 --- a/soh/soh/Enhancements/randomizer/logic.h +++ b/soh/soh/Enhancements/randomizer/logic.h @@ -90,7 +90,7 @@ class Logic { bool FairyPot = false; bool FreeFairies = false; bool FairyPond = false; - bool AmmoCanDrop = false; + bool AmmoCanDrop = true; uint8_t PieceOfHeart = 0; uint8_t HeartContainer = 0; diff --git a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp index 80aeead5c..270bdbb7f 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp @@ -9,7 +9,10 @@ #include "soh/SohGui/UIWidgets.hpp" #include "soh/SohGui/SohGui.hpp" #include "dungeon.h" +#include "entrance.h" #include "location_access.h" +#include "3drando/fill.hpp" +#include "soh/Enhancements/debugger/performanceTimer.h" #include #include @@ -84,7 +87,7 @@ bool fishsanityAgeSplit; bool initialized; bool doAreaScroll; bool previousShowHidden = false; -bool hideShopUnshuffledChecks = true; +bool hideShopUnshuffledChecks = false; bool alwaysShowGS = false; std::map startingShopItem = { { SCENE_KOKIRI_SHOP, RC_KF_SHOP_ITEM_1 }, @@ -132,8 +135,10 @@ bool areasFullyChecked[RCAREA_INVALID]; u32 areasSpoiled = 0; bool showVOrMQ; s8 areaChecksGotten[RCAREA_INVALID]; //| "Kokiri Forest (4/9)" +s8 areaChecksAvailable[RCAREA_INVALID]; s8 areaCheckTotals[RCAREA_INVALID]; uint16_t totalChecks = 0; +uint16_t totalChecksAvailable = 0; uint16_t totalChecksGotten = 0; bool optCollapseAll; // A bool that will collapse all checks once bool optExpandAll; // A bool that will expand all checks once @@ -166,6 +171,8 @@ bool hideCollected = false; bool showHidden = true; bool mystery = false; bool showLogicTooltip = false; +bool enableAvailableChecks = false; +bool onlyShowAvailable = false; SceneID DungeonSceneLookupByArea(RandomizerCheckArea area) { switch (area) { @@ -235,10 +242,12 @@ void TrySetAreas() { void CalculateTotals() { totalChecks = 0; + totalChecksAvailable = 0; totalChecksGotten = 0; for (uint8_t i = 0; i < RCAREA_INVALID; i++) { totalChecks += areaCheckTotals[i]; + totalChecksAvailable += areaChecksAvailable[i]; totalChecksGotten += areaChecksGotten[i]; } } @@ -251,17 +260,43 @@ uint16_t GetTotalChecksGotten() { return totalChecksGotten; } +bool IsCheckHidden(RandomizerCheck rc) { + Rando::ItemLocation* itemLocation = OTRGlobals::Instance->gRandoContext->GetItemLocation(rc); + RandomizerCheckStatus status = itemLocation->GetCheckStatus(); + bool available = itemLocation->IsAvailable(); + bool skipped = itemLocation->GetIsSkipped(); + bool obtained = itemLocation->HasObtained(); + bool seen = status == RCSHOW_SEEN || status == RCSHOW_IDENTIFIED; + bool scummed = status == RCSHOW_SCUMMED; + bool unchecked = status == RCSHOW_UNCHECKED; + + return !showHidden && ( + (skipped && hideSkipped) || + (seen && hideSeen) || + (scummed && hideScummed) || + (unchecked && hideUnchecked) + ); +} + void RecalculateAreaTotals(RandomizerCheckArea rcArea) { areaChecksGotten[rcArea] = 0; + areaChecksAvailable[rcArea] = 0; areaCheckTotals[rcArea] = 0; for (auto rc : checksByArea.at(rcArea)) { if (!IsVisibleInCheckTracker(rc)) { continue; } areaCheckTotals[rcArea]++; - if (OTRGlobals::Instance->gRandoContext->GetItemLocation(rc)->GetIsSkipped() || OTRGlobals::Instance->gRandoContext->GetItemLocation(rc)->HasObtained()) { + + Rando::ItemLocation* itemLoc = OTRGlobals::Instance->gRandoContext->GetItemLocation(rc); + + if (itemLoc->GetIsSkipped() || itemLoc->HasObtained()) { areaChecksGotten[rcArea]++; } + + if (itemLoc->IsAvailable() && !IsCheckHidden(rc)) { + areaChecksAvailable[rcArea]++; + } } CalculateTotals(); } @@ -308,6 +343,7 @@ void SetCheckCollected(RandomizerCheck rc) { if (IsVisibleInCheckTracker(rc)) { if (!OTRGlobals::Instance->gRandoContext->GetItemLocation(rc)->GetIsSkipped()) { areaChecksGotten[loc->GetArea()]++; + areaChecksAvailable[loc->GetArea()]--; } else { OTRGlobals::Instance->gRandoContext->GetItemLocation(rc)->SetIsSkipped(false); } @@ -424,10 +460,12 @@ void ClearAreaChecksAndTotals() { for (auto& [rcArea, vec] : checksByArea) { vec.clear(); areaChecksGotten[rcArea] = 0; + areaChecksAvailable[rcArea] = 0; areaCheckTotals[rcArea] = 0; } totalChecks = 0; totalChecksGotten = 0; + totalChecksAvailable = 0; } void SetShopSeen(uint32_t sceneNum, bool prices) { @@ -469,6 +507,9 @@ void CheckTrackerLoadGame(int32_t fileNum) { if (loc->GetCheckStatus() == RCSHOW_SAVED || loc->GetIsSkipped()) { areaChecksGotten[entry2->GetArea()]++; } + if (loc->IsAvailable()) { + areaChecksAvailable[entry2->GetArea()]++; + } } if (areaChecksGotten[entry2->GetArea()] != 0 || RandomizerCheckObjects::AreaIsOverworld(entry2->GetArea()) || @@ -524,6 +565,7 @@ void CheckTrackerLoadGame(int32_t fileNum) { UpdateAllOrdering(); UpdateInventoryChecks(); UpdateFilters(); + RecalculateAvailableChecks(); } void CheckTrackerShopSlotChange(uint8_t cursorSlot, int16_t basePrice) { @@ -539,6 +581,7 @@ void CheckTrackerShopSlotChange(uint8_t cursorSlot, int16_t basePrice) { if (status == RCSHOW_SEEN) { OTRGlobals::Instance->gRandoContext->GetItemLocation(slot)->SetCheckStatus(RCSHOW_IDENTIFIED); SaveManager::Instance->SaveSection(gSaveContext.fileNum, sectionId, true); + RecalculateAvailableChecks(); } } @@ -812,6 +855,9 @@ void SaveTrackerData(SaveContext* saveContext, int sectionID, bool fullSave) { void SaveFile(SaveContext* saveContext, int sectionID, bool fullSave) { SaveTrackerData(saveContext, sectionID, fullSave); + if (fullSave) { + RecalculateAvailableChecks(); + } } void LoadFile() { @@ -882,6 +928,8 @@ void CheckTrackerWindow::DrawElement() { showHidden = CVarGetInteger(CVAR_TRACKER_CHECK("ShowHidden"), 0); mystery = CVarGetInteger(CVAR_RANDOMIZER_ENHANCEMENT("MysteriousShuffle"), 0); showLogicTooltip = CVarGetInteger(CVAR_TRACKER_CHECK("ShowLogic"), 0); + enableAvailableChecks = CVarGetInteger(CVAR_TRACKER_CHECK("EnableAvailableChecks"), 0); + onlyShowAvailable = CVarGetInteger(CVAR_TRACKER_CHECK("OnlyShowAvailable"), 0); hideShopUnshuffledChecks = CVarGetInteger(CVAR_TRACKER_CHECK("HideUnshuffledShopChecks"), 0); alwaysShowGS = CVarGetInteger(CVAR_TRACKER_CHECK("AlwaysShowGSLocs"), 0); @@ -932,9 +980,21 @@ void CheckTrackerWindow::DrawElement() { ImGui::TableNextRow(0, headerHeight); ImGui::TableNextColumn(); - UIWidgets::CVarCheckbox( - "Show Hidden Items", CVAR_TRACKER_CHECK("ShowHidden"), UIWidgets::CheckboxOptions({{ .tooltip = "When active, items will show hidden checks by default when updated to this state." }}) - .Color(THEME_COLOR)); + if (UIWidgets::CVarCheckbox( + "Show Hidden Items", CVAR_TRACKER_CHECK("ShowHidden"), UIWidgets::CheckboxOptions({{.tooltip = "When active, items will show hidden checks by default when updated to this state." }}) + .Color(THEME_COLOR))) { + doAreaScroll = true; + showHidden = CVarGetInteger(CVAR_TRACKER_CHECK("ShowHidden"), 0); + RecalculateAllAreaTotals(); + } + if (enableAvailableChecks) { + if (UIWidgets::CVarCheckbox( + "Only Show Available Checks", CVAR_TRACKER_CHECK("OnlyShowAvailable"), UIWidgets::CheckboxOptions({{ .tooltip = "When active, unavailable checks will be hidden." }}) + .Color(THEME_COLOR))) { + doAreaScroll = true; + RecalculateAllAreaTotals(); + } + } UIWidgets::PaddedSeparator(); if (UIWidgets::Button("Expand All", UIWidgets::ButtonOptions().Color(THEME_COLOR).Size(UIWidgets::Sizes::Inline))) { optCollapseAll = false; @@ -960,7 +1020,13 @@ void CheckTrackerWindow::DrawElement() { ImGui::Separator(); - ImGui::Text("Total Checks: %d / %d", totalChecksGotten, totalChecks); + std::ostringstream totalChecksSS; + totalChecksSS << "Total Checks: "; + if (enableAvailableChecks) { + totalChecksSS << totalChecksAvailable << " Available / "; + } + totalChecksSS << totalChecksGotten << " Checked / " << totalChecks << " Total"; + ImGui::Text(totalChecksSS.str().c_str()); UIWidgets::PaddedSeparator(); @@ -1012,7 +1078,8 @@ void CheckTrackerWindow::DrawElement() { doAreaScroll = true; } if ((shouldHideFilteredAreas && filterAreasHidden[rcArea]) || - (!showHidden && ((hideComplete && thisAreaFullyChecked) || (hideIncomplete && !thisAreaFullyChecked))) + (!showHidden && ((hideComplete && thisAreaFullyChecked) || (hideIncomplete && !thisAreaFullyChecked))) || + (enableAvailableChecks && onlyShowAvailable && areaChecksAvailable[rcArea] == 0) ) { doDraw = false; } else { @@ -1051,14 +1118,27 @@ void CheckTrackerWindow::DrawElement() { isThisAreaSpoiled = IsAreaSpoiled(rcArea) || mqSpoilers; if (isThisAreaSpoiled) { - if (showVOrMQ && RandomizerCheckObjects::AreaIsDungeon(rcArea)) { - if (OTRGlobals::Instance->gRandoContext->GetDungeons()->GetDungeonFromScene(DungeonSceneLookupByArea(rcArea))->IsMQ()) - ImGui::Text("(%d/%d) - MQ", areaChecksGotten[rcArea], areaCheckTotals[rcArea]); - else - ImGui::Text("(%d/%d) - Vanilla", areaChecksGotten[rcArea], areaCheckTotals[rcArea]); - } else { - ImGui::Text("(%d/%d)", areaChecksGotten[rcArea], areaCheckTotals[rcArea]); + std::ostringstream areaTotalsSS; + std::ostringstream areaTotalsTooltipSS; + + areaTotalsSS << "("; + if (enableAvailableChecks) { + areaTotalsSS << static_cast(areaChecksAvailable[rcArea]) << " / "; + areaTotalsTooltipSS << "Available / "; } + areaTotalsSS << static_cast(areaChecksGotten[rcArea]) << " / " << static_cast(areaCheckTotals[rcArea]) << ")"; + areaTotalsTooltipSS << "Checked / Total"; + + if (showVOrMQ && RandomizerCheckObjects::AreaIsDungeon(rcArea)) { + if (OTRGlobals::Instance->gRandoContext->GetDungeons()->GetDungeonFromScene(DungeonSceneLookupByArea(rcArea))->IsMQ()) { + areaTotalsSS << " - MQ"; + } else { + areaTotalsSS << " - Vanilla"; + } + } + + ImGui::Text(areaTotalsSS.str().c_str()); + UIWidgets::Tooltip(areaTotalsTooltipSS.str().c_str()); } else { ImGui::Text("???"); } @@ -1561,6 +1641,12 @@ void DrawLocation(RandomizerCheck rc) { Rando::ItemLocation* itemLoc = OTRGlobals::Instance->gRandoContext->GetItemLocation(rc); RandomizerCheckStatus status = itemLoc->GetCheckStatus(); bool skipped = itemLoc->GetIsSkipped(); + bool available = itemLoc->IsAvailable(); + + if (enableAvailableChecks && onlyShowAvailable && !available) { + return; + } + if (status == RCSHOW_COLLECTED) { if (!showHidden && hideCollected) { return; @@ -1637,10 +1723,18 @@ void DrawLocation(RandomizerCheck rc) { OTRGlobals::Instance->gRandoContext->GetItemLocation(rc)->SetIsSkipped(false); areaChecksGotten[loc->GetArea()]--; totalChecksGotten--; + if (available) { + areaChecksAvailable[loc->GetArea()]++; + totalChecksAvailable++; + } } else { OTRGlobals::Instance->gRandoContext->GetItemLocation(rc)->SetIsSkipped(true); areaChecksGotten[loc->GetArea()]++; totalChecksGotten++; + if (available) { + areaChecksAvailable[loc->GetArea()]--; + totalChecksAvailable--; + } } UpdateOrdering(loc->GetArea()); UpdateInventoryChecks(); @@ -1654,7 +1748,19 @@ void DrawLocation(RandomizerCheck rc) { ImGui::SameLine(); //Draw - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(mainColor.r / 255.0f, mainColor.g / 255.0f, mainColor.b / 255.0f, mainColor.a / 255.0f)); + ImVec4 styleColor(mainColor.r / 255.0f, mainColor.g / 255.0f, mainColor.b / 255.0f, mainColor.a / 255.0f); + if (enableAvailableChecks) { + if (itemLoc->HasObtained()) { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0, 0, 0, 0)); + } else { + ImGui::PushStyleColor(ImGuiCol_Text, styleColor); + } + ImGui::Text("%s", available ? ICON_FA_UNLOCK : ICON_FA_LOCK); + ImGui::PopStyleColor(); + ImGui::SameLine(); + } + + ImGui::PushStyleColor(ImGuiCol_Text, styleColor); ImGui::Text("%s", txt.c_str()); ImGui::PopStyleColor(); @@ -1726,7 +1832,7 @@ void DrawLocation(RandomizerCheck rc) { if (conditionStr != "true") { UIWidgets::Tooltip(conditionStr.c_str()); } - return; + break; } } } @@ -1804,6 +1910,55 @@ void ImGuiDrawTwoColorPickerSection(const char* text, const char* cvarMainName, UIWidgets::PopStyleCombobox(); } +void RecalculateAvailableChecks() { + if (!enableAvailableChecks) { + return; + } + + ResetPerformanceTimer(PT_RECALCULATE_AVAILABLE_CHECKS); + StartPerformanceTimer(PT_RECALCULATE_AVAILABLE_CHECKS); + + std::vector targetLocations; + targetLocations.reserve(RR_MAX); + for (auto& location : Rando::StaticData::GetLocationTable()) { + RandomizerCheck rc = location.GetRandomizerCheck(); + Rando::ItemLocation* itemLocation = OTRGlobals::Instance->gRandoContext->GetItemLocation(rc); + itemLocation->SetAvailable(false); + if (!itemLocation->HasObtained()) { + targetLocations.emplace_back(rc); + } + } + + std::vector availableChecks = ReachabilitySearch(targetLocations, RG_NONE, true); + for (auto& rc : availableChecks) { + const auto& location = Rando::StaticData::GetLocation(rc); + const auto& itemLocation = OTRGlobals::Instance->gRandoContext->GetItemLocation(rc); + if (location->GetRCType() == RCTYPE_SHOP && itemLocation->GetCheckStatus() == RCSHOW_IDENTIFIED) { + if (CanBuyAnother(rc)) { + itemLocation->SetAvailable(true); + } + } else { + itemLocation->SetAvailable(true); + } + } + + totalChecksAvailable = 0; + for (auto& [rcArea, vec] : checksByArea) { + areaChecksAvailable[rcArea] = 0; + for (auto& rc : vec) { + Rando::ItemLocation* itemLocation = OTRGlobals::Instance->gRandoContext->GetItemLocation(rc); + if (itemLocation->IsAvailable() && IsVisibleInCheckTracker(rc) && !IsCheckHidden(rc)) { + areaChecksAvailable[rcArea]++; + } + } + totalChecksAvailable += areaChecksAvailable[rcArea]; + } + + StopPerformanceTimer(PT_RECALCULATE_AVAILABLE_CHECKS); + SPDLOG_INFO("Recalculate Available Checks Time: {}ms", GetPerformanceTimer(PT_RECALCULATE_AVAILABLE_CHECKS).count()); +} + + void CheckTrackerWindow::Draw() { if (!IsVisible()) { return; @@ -1858,7 +2013,7 @@ void CheckTrackerSettingsWindow::DrawElement() { .Tooltip("If enabled, Vanilla/MQ dungeons will show on the tracker immediately. Otherwise, Vanilla/MQ dungeon locations must be unlocked.").Color(THEME_COLOR)); if (UIWidgets::CVarCheckbox("Hide unshuffled shop item checks", CVAR_TRACKER_CHECK("HideUnshuffledShopChecks"), UIWidgets::CheckboxOptions().Tooltip("If enabled, will prevent the tracker from displaying slots with non-shop-item shuffles.").Color(THEME_COLOR))) { - hideShopUnshuffledChecks = !hideShopUnshuffledChecks; + hideShopUnshuffledChecks = CVarGetInteger(CVAR_TRACKER_CHECK("HideUnshuffledShopChecks"), 0); UpdateFilters(); } if (UIWidgets::CVarCheckbox("Always show gold skulltulas", CVAR_TRACKER_CHECK("AlwaysShowGSLocs"), @@ -1868,6 +2023,11 @@ void CheckTrackerSettingsWindow::DrawElement() { } UIWidgets::CVarCheckbox("Show Logic", CVAR_TRACKER_CHECK("ShowLogic"), UIWidgets::CheckboxOptions().Tooltip("If enabled, will show a check's logic when hovering over it.").Color(THEME_COLOR)); + if (UIWidgets::CVarCheckbox("Enable Available Checks", CVAR_TRACKER_CHECK("EnableAvailableChecks"), + UIWidgets::CheckboxOptions().Tooltip("If enabled, will show the checks that are available to be collected with your current progress.").Color(THEME_COLOR))) { + enableAvailableChecks = CVarGetInteger(CVAR_TRACKER_CHECK("EnableAvailableChecks"), 0); + RecalculateAvailableChecks(); + } // Filtering settings UIWidgets::PaddedSeparator(); diff --git a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.h b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.h index c5bcaa074..962da88e3 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.h +++ b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.h @@ -61,4 +61,5 @@ void UpdateAllOrdering(); void UpdateAllAreas(); void RecalculateAllAreaTotals(); void SpoilAreaFromCheck(RandomizerCheck rc); +void RecalculateAvailableChecks(); } // namespace CheckTracker diff --git a/soh/soh/Enhancements/randomizer/savefile.cpp b/soh/soh/Enhancements/randomizer/savefile.cpp index f43fecf9e..049b87f65 100644 --- a/soh/soh/Enhancements/randomizer/savefile.cpp +++ b/soh/soh/Enhancements/randomizer/savefile.cpp @@ -306,11 +306,13 @@ extern "C" void Randomizer_InitSaveFile() { // Malon/Talon back at ranch. Flags_SetEventChkInf(EVENTCHKINF_OBTAINED_POCKET_EGG); + Flags_SetRandomizerInf(RAND_INF_WEIRD_EGG); Flags_SetEventChkInf(EVENTCHKINF_TALON_WOKEN_IN_CASTLE); Flags_SetEventChkInf(EVENTCHKINF_TALON_RETURNED_FROM_CASTLE); // Set "Got Zelda's Letter" flag. Also ensures Saria is back at SFM. Flags_SetEventChkInf(EVENTCHKINF_OBTAINED_ZELDAS_LETTER); + Flags_SetRandomizerInf(RAND_INF_ZELDAS_LETTER); Flags_SetRandomizerInf(RAND_INF_CHILD_TRADES_HAS_LETTER_ZELDA); // Got item from Impa. @@ -321,7 +323,6 @@ extern "C" void Randomizer_InitSaveFile() { // Set this at the end to ensure we always start with the letter. // This is for the off chance, we got the Weird Egg from Impa (which should never happen). INV_CONTENT(ITEM_LETTER_ZELDA) = ITEM_LETTER_ZELDA; - Flags_SetRandomizerInf(RAND_INF_CHILD_TRADES_HAS_LETTER_ZELDA); } if (Randomizer_GetSettingValue(RSK_SHUFFLE_MASTER_SWORD) && startingAge == RO_AGE_ADULT) { diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp index 6bde466ea..06a986a20 100644 --- a/soh/soh/SaveManager.cpp +++ b/soh/soh/SaveManager.cpp @@ -459,10 +459,16 @@ void SaveManager::LoadRandomizerVersion3() { }); randoContext->GetTrials()->SkipAll(); - SaveManager::Instance->LoadArray("requiredTrials", randoContext->GetOption(RSK_TRIAL_COUNT).Get() + 1, [&](size_t i) { + SaveManager::Instance->LoadArray("requiredTrials", randoContext->GetOption(RSK_TRIAL_COUNT).Get(), [&](size_t i) { size_t trialId; SaveManager::Instance->LoadData("", trialId); randoContext->GetTrial(trialId)->SetAsRequired(); + }); + + SaveManager::Instance->LoadArray("trickOptions", RT_MAX, [&](size_t i) { + uint8_t value = 0; + SaveManager::Instance->LoadData("", value); + randoContext->GetTrickOption(RandomizerTrick(i)).Set(value); }); } @@ -596,6 +602,10 @@ void SaveManager::SaveRandomizer(SaveContext* saveContext, int sectionID, bool f SaveManager::Instance->SaveData("", i); } }); + + SaveManager::Instance->SaveArray("trickOptions", RT_MAX, [&](size_t i) { + SaveManager::Instance->SaveData("", randoContext->GetTrickOption(RandomizerTrick(i)).Get()); + }); } // Init() here is an extension of InitSram, and thus not truly an initializer for SaveManager itself. don't put any class initialization stuff here