From 9f63aaae9974440db6f67b9e11b50587a3009bd4 Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Sun, 21 Aug 2022 16:14:41 -0500 Subject: [PATCH] Initial work towards scrubsanity, got affordable prices mostly functional Write and read prices from spoiler, move IdentifyScrub up to Randomizer In a semi working state Fix bug when going from c to cpp Kill scrubs before they spawn if already purchased Woopsie from rebase Update soh/soh/OTRGlobals.cpp --- soh/include/z64save.h | 1 + .../Enhancements/randomizer/3drando/fill.cpp | 13 + .../randomizer/3drando/item_location.cpp | 50 +++ .../randomizer/3drando/item_location.hpp | 17 +- .../randomizer/3drando/settings.cpp | 1 + .../randomizer/3drando/spoiler_log.cpp | 15 +- .../Enhancements/randomizer/randomizer.cpp | 321 +++++++++++++++++- soh/soh/Enhancements/randomizer/randomizer.h | 2 + .../Enhancements/randomizer/randomizerTypes.h | 10 + soh/soh/OTRGlobals.cpp | 22 +- soh/soh/OTRGlobals.h | 1 + soh/soh/SaveManager.cpp | 12 + soh/src/code/z_sram.c | 6 + soh/src/overlays/actors/ovl_En_Dns/z_en_dns.c | 72 ++-- soh/src/overlays/actors/ovl_En_Dns/z_en_dns.h | 1 + .../actors/ovl_En_Shopnuts/z_en_shopnuts.c | 9 + 16 files changed, 492 insertions(+), 61 deletions(-) diff --git a/soh/include/z64save.h b/soh/include/z64save.h index 9da18f72e..d219fd17f 100644 --- a/soh/include/z64save.h +++ b/soh/include/z64save.h @@ -185,6 +185,7 @@ typedef struct { u8 dungeonsDone[8]; u8 trialsDone[6]; u8 cowsMilked[10]; + u8 scrubsPurchased[35]; u8 temporaryWeapon; u16 adultTradeItems; } SaveContext; // size = 0x1428 diff --git a/soh/soh/Enhancements/randomizer/3drando/fill.cpp b/soh/soh/Enhancements/randomizer/3drando/fill.cpp index 14bf594c2..b78e3f630 100644 --- a/soh/soh/Enhancements/randomizer/3drando/fill.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/fill.cpp @@ -1039,6 +1039,19 @@ int Fill() { //Fast fill for the rest of the pool std::vector remainingPool = FilterAndEraseFromPool(ItemPool, [](const auto i) { return true; }); FastFill(remainingPool, GetAllEmptyLocations(), false); + + // Add prices for scrubsanity + if (Scrubsanity.Is(SCRUBSANITY_AFFORDABLE)) { + for (size_t i = 0; i < ScrubLocations.size(); i++) { + Location(ScrubLocations[i])->SetScrubsanityPrice(10); + } + } else if (Scrubsanity.Is(SCRUBSANITY_RANDOM_PRICES)) { + for (size_t i = 0; i < ScrubLocations.size(); i++) { + int randomPrice = GetRandomScrubPrice(); + Location(ScrubLocations[i])->SetScrubsanityPrice(randomPrice); + } + } + GeneratePlaythrough(); //Successful placement, produced beatable result if(playthroughBeatable && !placementFailure) { diff --git a/soh/soh/Enhancements/randomizer/3drando/item_location.cpp b/soh/soh/Enhancements/randomizer/3drando/item_location.cpp index 08d191740..0a4405c78 100644 --- a/soh/soh/Enhancements/randomizer/3drando/item_location.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/item_location.cpp @@ -1011,6 +1011,56 @@ std::vector> ShopLocationLists = { GC_ShopLocations, }; +//List of scrubs, used for pricing the scrubs +std::vector ScrubLocations = { + LW_DEKU_SCRUB_NEAR_DEKU_THEATER_RIGHT, + LW_DEKU_SCRUB_NEAR_DEKU_THEATER_LEFT, + LW_DEKU_SCRUB_NEAR_BRIDGE, + LW_DEKU_SCRUB_GROTTO_REAR, + LW_DEKU_SCRUB_GROTTO_FRONT, + SFM_DEKU_SCRUB_GROTTO_REAR, + SFM_DEKU_SCRUB_GROTTO_FRONT, + HF_DEKU_SCRUB_GROTTO, + LH_DEKU_SCRUB_GROTTO_LEFT, + LH_DEKU_SCRUB_GROTTO_RIGHT, + LH_DEKU_SCRUB_GROTTO_CENTER, + GV_DEKU_SCRUB_GROTTO_REAR, + GV_DEKU_SCRUB_GROTTO_FRONT, + COLOSSUS_DEKU_SCRUB_GROTTO_REAR, + COLOSSUS_DEKU_SCRUB_GROTTO_FRONT, + GC_DEKU_SCRUB_GROTTO_LEFT, + GC_DEKU_SCRUB_GROTTO_RIGHT, + GC_DEKU_SCRUB_GROTTO_CENTER, + DMC_DEKU_SCRUB, + DMC_DEKU_SCRUB_GROTTO_LEFT, + DMC_DEKU_SCRUB_GROTTO_RIGHT, + DMC_DEKU_SCRUB_GROTTO_CENTER, + ZR_DEKU_SCRUB_GROTTO_REAR, + ZR_DEKU_SCRUB_GROTTO_FRONT, + LLR_DEKU_SCRUB_GROTTO_LEFT, + LLR_DEKU_SCRUB_GROTTO_RIGHT, + LLR_DEKU_SCRUB_GROTTO_CENTER, + DEKU_TREE_MQ_DEKU_SCRUB, + DODONGOS_CAVERN_DEKU_SCRUB_NEAR_BOMB_BAG_LEFT, + DODONGOS_CAVERN_DEKU_SCRUB_SIDE_ROOM_NEAR_DODONGOS, + DODONGOS_CAVERN_DEKU_SCRUB_NEAR_BOMB_BAG_RIGHT, + DODONGOS_CAVERN_DEKU_SCRUB_LOBBY, + DODONGOS_CAVERN_MQ_DEKU_SCRUB_LOBBY_REAR, + DODONGOS_CAVERN_MQ_DEKU_SCRUB_LOBBY_FRONT, + DODONGOS_CAVERN_MQ_DEKU_SCRUB_STAIRCASE, + DODONGOS_CAVERN_MQ_DEKU_SCRUB_SIDE_ROOM_NEAR_LOWER_LIZALFOS, + JABU_JABUS_BELLY_DEKU_SCRUB, + GANONS_CASTLE_DEKU_SCRUB_CENTER_LEFT, + GANONS_CASTLE_DEKU_SCRUB_CENTER_RIGHT, + GANONS_CASTLE_DEKU_SCRUB_RIGHT, + GANONS_CASTLE_DEKU_SCRUB_LEFT, + GANONS_CASTLE_MQ_DEKU_SCRUB_RIGHT, + GANONS_CASTLE_MQ_DEKU_SCRUB_CENTER_LEFT, + GANONS_CASTLE_MQ_DEKU_SCRUB_CENTER, + GANONS_CASTLE_MQ_DEKU_SCRUB_CENTER_RIGHT, + GANONS_CASTLE_MQ_DEKU_SCRUB_LEFT, +}; + //List of gossip stone locations for hints std::vector gossipStoneLocations = { DMC_GOSSIP_STONE, diff --git a/soh/soh/Enhancements/randomizer/3drando/item_location.hpp b/soh/soh/Enhancements/randomizer/3drando/item_location.hpp index 823cd88ef..efe1a6257 100644 --- a/soh/soh/Enhancements/randomizer/3drando/item_location.hpp +++ b/soh/soh/Enhancements/randomizer/3drando/item_location.hpp @@ -257,8 +257,8 @@ public: } void SetPrice(uint16_t price_) { - //don't override price if the price was set for shopsanity - if (hasShopsanityPrice) { + //don't override price if the price was set for shopsanity/scrubsanity + if (hasShopsanityPrice || hasScrubsanityPrice) { return; } price = price_; @@ -269,10 +269,19 @@ public: hasShopsanityPrice = true; } + void SetScrubsanityPrice(uint16_t price_) { + price = price_; + hasScrubsanityPrice = true; + } + bool HasShopsanityPrice() const { return hasShopsanityPrice; } + bool HasScrubsanityPrice() const { + return hasScrubsanityPrice; + } + bool IsExcluded() const { return excludedOption.Value(); } @@ -426,6 +435,7 @@ public: isHintable = false; price = 0; hasShopsanityPrice = false; + hasScrubsanityPrice = false; hidden = false; } @@ -451,6 +461,7 @@ private: bool isHintable = false; uint32_t parentRegion = NONE; bool hasShopsanityPrice = false; + bool hasScrubsanityPrice = false; bool hidden = false; }; @@ -467,6 +478,8 @@ ItemLocation* Location(uint32_t locKey); extern std::vector> ShopLocationLists; +extern std::vector ScrubLocations; + extern std::vector gossipStoneLocations; extern std::vector dungeonRewardLocations; diff --git a/soh/soh/Enhancements/randomizer/3drando/settings.cpp b/soh/soh/Enhancements/randomizer/3drando/settings.cpp index 51eaff18b..c7e06b812 100644 --- a/soh/soh/Enhancements/randomizer/3drando/settings.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/settings.cpp @@ -2532,6 +2532,7 @@ namespace Settings { ShuffleRewards.SetSelectedIndex(cvarSettings[RSK_SHUFFLE_DUNGEON_REWARDS]); ShuffleSongs.SetSelectedIndex(cvarSettings[RSK_SHUFFLE_SONGS]); Tokensanity.SetSelectedIndex(cvarSettings[RSK_SHUFFLE_TOKENS]); + Scrubsanity.SetSelectedIndex(cvarSettings[RSK_SHUFFLE_SCRUBS]); ShuffleCows.SetSelectedIndex(cvarSettings[RSK_SHUFFLE_COWS]); ShuffleKokiriSword.SetSelectedIndex(cvarSettings[RSK_SHUFFLE_KOKIRI_SWORD]); ShuffleOcarinas.SetSelectedIndex(cvarSettings[RSK_SHUFFLE_OCARINA]); diff --git a/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp b/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp index 22d1f196d..562671fd1 100644 --- a/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp @@ -675,15 +675,14 @@ static void WriteHints(int language) { static void WriteAllLocations(int language) { for (const uint32_t key : allLocations) { ItemLocation* location = Location(key); + std::string placedItemName = language == 2 ? location->GetPlacedItemName().french : location->GetPlacedItemName().english; - switch (language) { - case 0: - default: - jsonData["locations"][location->GetName()] = location->GetPlacedItemName().english; - break; - case 2: - jsonData["locations"][location->GetName()] = location->GetPlacedItemName().french; - break; + // Eventually check for other things here like fake name + if (location->HasScrubsanityPrice() || location->HasShopsanityPrice()) { + jsonData["locations"][location->GetName()]["item"] = placedItemName; + jsonData["locations"][location->GetName()]["price"] = location->GetPrice();; + } else { + jsonData["locations"][location->GetName()] = placedItemName; } } } diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index 60c198ed4..299d418c8 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -81,6 +81,7 @@ Sprite* Randomizer::GetSeedTexture(uint8_t index) { Randomizer::~Randomizer() { this->randoSettings.clear(); this->itemLocations.clear(); + this->scrubPrices.clear(); } std::unordered_map getItemIdToItemId = { @@ -523,6 +524,7 @@ std::unordered_map SpoilerfileSettingNameToEn { "Open Settings:Random Ganon's Trials", RSK_RANDOM_TRIALS }, { "Open Settings:Trial Count", RSK_TRIAL_COUNT }, { "Shuffle Settings:Shuffle Gerudo Card", RSK_SHUFFLE_GERUDO_MEMBERSHIP_CARD }, + { "Shuffle Settings:Scrub Shuffle", RSK_SHUFFLE_SCRUBS }, { "Shuffle Settings:Shuffle Cows", RSK_SHUFFLE_COWS }, { "Shuffle Settings:Tokensanity", RSK_SHUFFLE_TOKENS }, { "Shuffle Settings:Shuffle Adult Trade", RSK_SHUFFLE_ADULT_TRADE }, @@ -749,6 +751,17 @@ void Randomizer::ParseRandomizerSettingsFile(const char* spoilerFileName) { numericValueString = it.value(); gSaveContext.randoSettings[index].value = std::stoi(numericValueString); break; + case RSK_SHUFFLE_SCRUBS: + if(it.value() == "Off") { + gSaveContext.randoSettings[index].value = 0; + } else if(it.value() == "Affordable") { + gSaveContext.randoSettings[index].value = 1; + } else if(it.value() == "Expensive") { + gSaveContext.randoSettings[index].value = 2; + } else if(it.value() == "Random Prices") { + gSaveContext.randoSettings[index].value = 3; + } + break; case RSK_SHUFFLE_GERUDO_MEMBERSHIP_CARD: case RSK_SHUFFLE_COWS: case RSK_SHUFFLE_ADULT_TRADE: @@ -1079,9 +1092,11 @@ void Randomizer::ParseItemLocationsFile(const char* spoilerFileName, bool silent for (auto itemit = itemJson.begin(); itemit != itemJson.end(); ++itemit) { // todo handle prices if (itemit.key() == "item") { - gSaveContext.itemLocations[index].check = SpoilerfileCheckNameToEnum[it.key()]; gSaveContext.itemLocations[index].get = SpoilerfileGetNameToEnum[itemit.value()]; + } else if (itemit.key() == "price") { + scrubPrices[gSaveContext.itemLocations[index].check] = itemit.value(); + // TODO: Handle shop prices } } } else { @@ -1565,6 +1580,281 @@ std::string Randomizer::GetGanonHintText() const { return ganonHintText; } +ScrubIdentity Randomizer::IdentifyScrub(s32 sceneNum, s32 actorParams, s32 respawnData) { + struct ScrubIdentity scrubIdentity; + + scrubIdentity.scrubId = -1; + scrubIdentity.randomizerCheck = RC_UNKNOWN_CHECK; + scrubIdentity.getItemId = GI_NONE; + scrubIdentity.itemPrice = -1; + scrubIdentity.isShuffled = GetRandoSettingValue(RSK_SHUFFLE_SCRUBS) > 0; + + switch (actorParams) { + case 0x00: + scrubIdentity.getItemId = GI_NUTS_5_2; + break; + case 0x01: + scrubIdentity.getItemId = GI_STICKS_1; + break; + case 0x02: + scrubIdentity.getItemId = GI_HEART_PIECE; + break; + case 0x03: + scrubIdentity.getItemId = GI_SEEDS_30; + break; + case 0x04: + scrubIdentity.getItemId = GI_SHIELD_DEKU; + break; + case 0x05: + scrubIdentity.getItemId = GI_BOMBS_5; + break; + case 0x06: + scrubIdentity.getItemId = GI_ARROWS_LARGE; + break; + case 0x07: + scrubIdentity.getItemId = GI_POTION_RED; + break; + case 0x08: + scrubIdentity.getItemId = GI_POTION_GREEN; + break; + case 0x09: + scrubIdentity.getItemId = GI_STICK_UPGRADE_20; + break; + case 0x0A: + scrubIdentity.getItemId = GI_NUT_UPGRADE_30; + break; + } + + // TODO: Handle MQ scrubs + switch (sceneNum) { + case SCENE_DDAN: // Dodongo's Cavern + switch (actorParams) { + case 0x00: + scrubIdentity.scrubId = 0x00; + scrubIdentity.randomizerCheck = RC_DODONGOS_CAVERN_DEKU_SCRUB_NEAR_BOMB_BAG_LEFT; + break; + case 0x01: + scrubIdentity.scrubId = 0x01; + scrubIdentity.randomizerCheck = RC_DODONGOS_CAVERN_DEKU_SCRUB_SIDE_ROOM_NEAR_DODONGOS; + break; + case 0x03: + case 0x06: + scrubIdentity.scrubId = 0x02; + scrubIdentity.randomizerCheck = RC_DODONGOS_CAVERN_DEKU_SCRUB_NEAR_BOMB_BAG_RIGHT; + break; + case 0x04: + scrubIdentity.scrubId = 0x03; + scrubIdentity.randomizerCheck = RC_DODONGOS_CAVERN_DEKU_SCRUB_LOBBY; + break; + } + break; + case SCENE_BDAN: // Jabu Jabu's Belly + switch (actorParams) { + case 0x00: + scrubIdentity.scrubId = 0x04; + scrubIdentity.randomizerCheck = RC_JABU_JABUS_BELLY_DEKU_SCRUB; + break; + } + break; + case SCENE_GANONTIKA: // Ganon's Castle + switch (actorParams) { + case 0x05: + scrubIdentity.scrubId = 0x05; + scrubIdentity.randomizerCheck = RC_GANONS_CASTLE_DEKU_SCRUB_CENTER_LEFT; + break; + case 0x03: + case 0x06: + scrubIdentity.scrubId = 0x06; + scrubIdentity.randomizerCheck = RC_GANONS_CASTLE_DEKU_SCRUB_CENTER_RIGHT; + break; + case 0x07: + scrubIdentity.scrubId = 0x07; + scrubIdentity.randomizerCheck = RC_GANONS_CASTLE_DEKU_SCRUB_RIGHT; + break; + case 0x08: + scrubIdentity.scrubId = 0x08; + scrubIdentity.randomizerCheck = RC_GANONS_CASTLE_DEKU_SCRUB_LEFT; + break; + } + break; + case SCENE_KAKUSIANA: // Grotto + // Ugly, but the best way we can identify which grotto we are in, same method 3DRando uses, but we'll need to account for entrance rando + switch (respawnData) { + case 0xE6: // Hyrule Field Scrub Grotto + switch (actorParams) { + case 0x02: + scrubIdentity.scrubId = 0x09; + scrubIdentity.randomizerCheck = RC_HF_DEKU_SCRUB_GROTTO; + scrubIdentity.isShuffled = true; + break; + } + break; + case 0xEB: // ZR Scrub Grotto + switch (actorParams) { + case 0x07: + scrubIdentity.scrubId = 0x0A; + scrubIdentity.randomizerCheck = RC_ZR_DEKU_SCRUB_GROTTO_REAR; + break; + case 0x08: + scrubIdentity.scrubId = 0x0B; + scrubIdentity.randomizerCheck = RC_ZR_DEKU_SCRUB_GROTTO_FRONT; + break; + } + break; + case 0xEE: // Sacred Forest Meadow Scrub Grotto + switch (actorParams) { + case 0x07: + scrubIdentity.scrubId = 0x0C; + scrubIdentity.randomizerCheck = RC_SFM_DEKU_SCRUB_GROTTO_REAR; + break; + case 0x08: + scrubIdentity.scrubId = 0x0D; + scrubIdentity.randomizerCheck = RC_SFM_DEKU_SCRUB_GROTTO_FRONT; + break; + } + break; + case 0xEF: // Lake Hylia Scrub Grotto + switch (actorParams) { + case 0x00: + scrubIdentity.scrubId = 0x0E; + scrubIdentity.randomizerCheck = RC_LH_DEKU_SCRUB_GROTTO_LEFT; + break; + case 0x05: + scrubIdentity.scrubId = 0x0F; + scrubIdentity.randomizerCheck = RC_LH_DEKU_SCRUB_GROTTO_RIGHT; + break; + case 0x03: + case 0x06: + scrubIdentity.scrubId = 0x10; + scrubIdentity.randomizerCheck = RC_LH_DEKU_SCRUB_GROTTO_CENTER; + break; + } + break; + case 0xF0: // Gerudo Valley Scrub Grotto + switch (actorParams) { + case 0x07: + scrubIdentity.scrubId = 0x11; + scrubIdentity.randomizerCheck = RC_GV_DEKU_SCRUB_GROTTO_REAR; + break; + case 0x08: + scrubIdentity.scrubId = 0x12; + scrubIdentity.randomizerCheck = RC_GV_DEKU_SCRUB_GROTTO_FRONT; + break; + } + break; + case 0xF5: // Lost Woods Scrub Grotto + switch (actorParams) { + case 0x03: + case 0x06: + scrubIdentity.scrubId = 0x13; + scrubIdentity.randomizerCheck = RC_LW_DEKU_SCRUB_GROTTO_REAR; + break; + case 0x0A: + scrubIdentity.scrubId = 0x14; + scrubIdentity.randomizerCheck = RC_LW_DEKU_SCRUB_GROTTO_FRONT; + scrubIdentity.isShuffled = true; + break; + } + break; + case 0xF9: // Death Mountain Crater Scrub Grotto + switch (actorParams) { + case 0x00: + scrubIdentity.scrubId = 0x15; + scrubIdentity.randomizerCheck = RC_DMC_DEKU_SCRUB_GROTTO_LEFT; + break; + case 0x05: + scrubIdentity.scrubId = 0x16; + scrubIdentity.randomizerCheck = RC_DMC_DEKU_SCRUB_GROTTO_RIGHT; + break; + case 0x03: + case 0x06: + scrubIdentity.scrubId = 0x17; + scrubIdentity.randomizerCheck = RC_DMC_DEKU_SCRUB_GROTTO_CENTER; + break; + } + break; + case 0xFB: // Gerudo City Scrub Grotto + switch (actorParams) { + case 0x00: + scrubIdentity.scrubId = 0x18; + scrubIdentity.randomizerCheck = RC_GC_DEKU_SCRUB_GROTTO_LEFT; + break; + case 0x05: + scrubIdentity.scrubId = 0x19; + scrubIdentity.randomizerCheck = RC_GC_DEKU_SCRUB_GROTTO_RIGHT; + break; + case 0x03: + case 0x06: + scrubIdentity.scrubId = 0x1A; + scrubIdentity.randomizerCheck = RC_GC_DEKU_SCRUB_GROTTO_CENTER; + break; + } + break; + case 0xFC: // Lon Lon Ranch Scrub Grotto + switch (actorParams) { + case 0x00: + scrubIdentity.scrubId = 0x1B; + scrubIdentity.randomizerCheck = RC_LLR_DEKU_SCRUB_GROTTO_LEFT; + break; + case 0x05: + scrubIdentity.scrubId = 0x1C; + scrubIdentity.randomizerCheck = RC_LLR_DEKU_SCRUB_GROTTO_RIGHT; + break; + case 0x03: + case 0x06: + scrubIdentity.scrubId = 0x1D; + scrubIdentity.randomizerCheck = RC_LLR_DEKU_SCRUB_GROTTO_CENTER; + break; + } + break; + case 0xFD: // Desert Colossus Scrub Grotto + switch (actorParams) { + case 0x07: + scrubIdentity.scrubId = 0x1E; + scrubIdentity.randomizerCheck = RC_COLOSSUS_DEKU_SCRUB_GROTTO_REAR; + break; + case 0x08: + scrubIdentity.scrubId = 0x1F; + scrubIdentity.randomizerCheck = RC_COLOSSUS_DEKU_SCRUB_GROTTO_FRONT; + break; + } + break; + } + break; + case SCENE_SPOT10: // Lost woods + switch (actorParams) { + case 0x00: + scrubIdentity.scrubId = 0x20; + scrubIdentity.randomizerCheck = RC_LW_DEKU_SCRUB_NEAR_DEKU_THEATER_RIGHT; + break; + case 0x01: + scrubIdentity.scrubId = 0x21; + scrubIdentity.randomizerCheck = RC_LW_DEKU_SCRUB_NEAR_DEKU_THEATER_LEFT; + break; + case 0x09: + scrubIdentity.scrubId = 0x22; + scrubIdentity.randomizerCheck = RC_LW_DEKU_SCRUB_NEAR_BRIDGE; + scrubIdentity.isShuffled = true; + break; + } + break; + case SCENE_SPOT17: // Death Mountain Crater + switch (actorParams) { + case 0x05: + scrubIdentity.scrubId = 0x23; + scrubIdentity.randomizerCheck = RC_DMC_DEKU_SCRUB; + break; + } + break; + } + + if (scrubPrices.find(scrubIdentity.randomizerCheck) != scrubPrices.end()) { + scrubIdentity.itemPrice = scrubPrices[scrubIdentity.randomizerCheck]; + } + + return scrubIdentity; +} + u8 Randomizer::GetRandoSettingValue(RandomizerSettingKey randoSettingKey) { return this->randoSettings[randoSettingKey]; } @@ -2158,10 +2448,6 @@ RandomizerCheck Randomizer::GetCheckFromActor(s16 sceneNum, s16 actorId, s16 act break; case 62: switch (actorParams) { - case 2: - return RC_HF_DEKU_SCRUB_GROTTO; - case 10: - return RC_LW_DEKU_SCRUB_GROTTO_FRONT; case 22988: return RC_KF_STORMS_GROTTO_CHEST; case -22988: @@ -2426,8 +2712,6 @@ RandomizerCheck Randomizer::GetCheckFromActor(s16 sceneNum, s16 actorId, s16 act break; case 91: switch (actorParams) { - case 9: - return RC_LW_DEKU_SCRUB_NEAR_BRIDGE; case 14365: return RC_LW_GOSSIP_STONE; case 27905: @@ -2592,6 +2876,7 @@ void GenerateRandomizerImgui() { cvarSettings[RSK_SHUFFLE_DUNGEON_REWARDS] = CVar_GetS32("gRandomizeShuffleDungeonReward", 0); cvarSettings[RSK_SHUFFLE_SONGS] = CVar_GetS32("gRandomizeShuffleSongs", 0); cvarSettings[RSK_SHUFFLE_TOKENS] = CVar_GetS32("gRandomizeShuffleTokens", 0); + cvarSettings[RSK_SHUFFLE_SCRUBS] = CVar_GetS32("gRandomizeShuffleScrubs", 0); cvarSettings[RSK_SHUFFLE_COWS] = CVar_GetS32("gRandomizeShuffleCows", 0); cvarSettings[RSK_SHUFFLE_ADULT_TRADE] = CVar_GetS32("gRandomizeShuffleAdultTrade", 0); cvarSettings[RSK_SKIP_CHILD_ZELDA] = CVar_GetS32("gRandomizeSkipChildZelda", 0); @@ -3108,11 +3393,20 @@ void DrawRandoEditor(bool& open) { "expected to be collected after getting Sun's Song."); PaddedSeparator(); - // Shuffle Cows - SohImGui::EnhancementCheckbox(Settings::ShuffleCows.GetName().c_str(), "gRandomizeShuffleCows"); - InsertHelpHoverText( - "Cows give a randomized item from the pool upon performing Epona's Song in front of them."); - PaddedSeparator(); + // Shuffle Scrubs + ImGui::Text(Settings::Scrubsanity.GetName().c_str()); + InsertHelpHoverText( + "Off - Scrubs will not be shuffled.\n" + "\n" + "Affordable - Scrubs will be shuffled and their item will cost 10 rupees.\n" + ); + SohImGui::EnhancementCombobox("gRandomizeShuffleScrubs", randoShuffleScrubs, 4, 0); + PaddedSeparator(); + + // Shuffle Cows + SohImGui::EnhancementCheckbox(Settings::ShuffleCows.GetName().c_str(), "gRandomizeShuffleCows"); + InsertHelpHoverText("Cows give a randomized item from the pool upon performing Epona's Song in front of them."); + PaddedSeparator(); // Shuffle Adult Trade Quest SohImGui::EnhancementCheckbox(Settings::ShuffleAdultTradeQuest.GetName().c_str(), @@ -3699,8 +3993,7 @@ void CreateGetItemMessages(std::vector messageEntries) { void CreateScrubMessages() { CustomMessageManager* customMessageManager = CustomMessageManager::Instance; customMessageManager->AddCustomMessageTable(Randomizer::scrubMessageTableID); - const std::vector prices = { 10, 40 }; - for (u8 price : prices) { + for (u32 price = 0; price <= 95; price += 5) { customMessageManager->CreateMessage(Randomizer::scrubMessageTableID, price, { TEXTBOX_TYPE_BLACK, TEXTBOX_POS_BOTTOM, "\x12\x38\x82\All right! You win! In return for&sparing me, I will sell you a&%gmysterious item%w!&%r" + diff --git a/soh/soh/Enhancements/randomizer/randomizer.h b/soh/soh/Enhancements/randomizer/randomizer.h index 15394fbcb..98c2efa5f 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.h +++ b/soh/soh/Enhancements/randomizer/randomizer.h @@ -16,6 +16,7 @@ class Randomizer { std::string ganonHintText; std::string ganonText; std::unordered_map randoSettings; + std::unordered_map scrubPrices; s16 GetItemFromGet(RandomizerGet randoGet, GetItemID ogItemId); s16 GetItemFromActor(s16 actorId, s16 actorParams, s16 sceneNum, GetItemID ogItemId); void ParseRandomizerSettingsFile(const char* spoilerFileName); @@ -45,6 +46,7 @@ class Randomizer { std::string GetAdultAltarText() const; std::string GetGanonText() const; std::string GetGanonHintText() const; + ScrubIdentity IdentifyScrub(s32 sceneNum, s32 actorParams, s32 respawnData); s16 GetRandomizedItemIdFromKnownCheck(RandomizerCheck randomizerCheck, GetItemID ogId); s16 GetRandomizedItemId(GetItemID ogId, s16 actorId, s16 actorParams, s16 sceneNum); static void CreateCustomMessages(); diff --git a/soh/soh/Enhancements/randomizer/randomizerTypes.h b/soh/soh/Enhancements/randomizer/randomizerTypes.h index 21635278c..79d0db060 100644 --- a/soh/soh/Enhancements/randomizer/randomizerTypes.h +++ b/soh/soh/Enhancements/randomizer/randomizerTypes.h @@ -1,6 +1,7 @@ #pragma once #include +#include "z64item.h" // This should probably go in a less rando-specific location // but the best location will probably be in the modding engine @@ -987,6 +988,7 @@ typedef enum { RSK_SHUFFLE_DUNGEON_REWARDS, RSK_SHUFFLE_SONGS, RSK_SHUFFLE_TOKENS, + RSK_SHUFFLE_SCRUBS, RSK_SHUFFLE_COWS, RSK_SHUFFLE_WEIRD_EGG, RSK_SHUFFLE_GERUDO_MEMBERSHIP_CARD, @@ -1015,3 +1017,11 @@ typedef enum { RSK_SKULLS_SUNS_SONG, RSK_SHUFFLE_ADULT_TRADE } RandomizerSettingKey; + +typedef struct ScrubIdentity { + int32_t scrubId; + RandomizerCheck randomizerCheck; + GetItemID getItemId; + int32_t itemPrice; + bool isShuffled; +} ScrubIdentity; \ No newline at end of file diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 805e866da..af727a812 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -1568,18 +1568,12 @@ extern "C" RandomizerCheck Randomizer_GetCheckFromActor(s16 sceneNum, s16 actorI return OTRGlobals::Instance->gRandomizer->GetCheckFromActor(sceneNum, actorId, actorParams); } -extern "C" CustomMessageEntry Randomizer_GetScrubMessage(u16 scrubTextId) { - int price = 0; - switch (scrubTextId) { - case TEXT_SCRUB_POH: - price = 10; - break; - case TEXT_SCRUB_STICK_UPGRADE: - case TEXT_SCRUB_NUT_UPGRADE: - price = 40; - break; - } - return CustomMessageManager::Instance->RetrieveMessage(Randomizer::scrubMessageTableID, price); +extern "C" ScrubIdentity Randomizer_IdentifyScrub(s32 sceneNum, s32 actorParams, s32 respawnData) { + return OTRGlobals::Instance->gRandomizer->IdentifyScrub(sceneNum, actorParams, respawnData); +} + +extern "C" CustomMessageEntry Randomizer_GetScrubMessage(s16 itemPrice) { + return CustomMessageManager::Instance->RetrieveMessage(Randomizer::scrubMessageTableID, itemPrice); } extern "C" CustomMessageEntry Randomizer_GetAltarMessage() { @@ -1703,8 +1697,8 @@ extern "C" int CustomMessage_RetrieveIfExists(GlobalContext* globalCtx) { } else { messageEntry = Randomizer_GetGanonHintText(); } - } else if (textId == TEXT_SCRUB_POH || textId == TEXT_SCRUB_STICK_UPGRADE || textId == TEXT_SCRUB_NUT_UPGRADE) { - messageEntry = Randomizer_GetScrubMessage(textId); + } else if (textId >= 0x9000 && textId <= 0x905F) { + messageEntry = Randomizer_GetScrubMessage((textId & ((1 << 8) - 1))); } } if (textId == TEXT_GS_NO_FREEZE || textId == TEXT_GS_FREEZE) { diff --git a/soh/soh/OTRGlobals.h b/soh/soh/OTRGlobals.h index 069707cd9..75f375fa7 100644 --- a/soh/soh/OTRGlobals.h +++ b/soh/soh/OTRGlobals.h @@ -96,6 +96,7 @@ Sprite* GetSeedTexture(uint8_t index); void Randomizer_LoadSettings(const char* spoilerFileName); u8 Randomizer_GetSettingValue(RandomizerSettingKey randoSettingKey); RandomizerCheck Randomizer_GetCheckFromActor(s16 actorId, s16 actorParams, s16 sceneNum); +ScrubIdentity Randomizer_IdentifyScrub(s32 sceneNum, s32 actorParams, s32 respawnData); void Randomizer_LoadHintLocations(const char* spoilerFileName); void Randomizer_LoadItemLocations(const char* spoilerFileName, bool silent); GetItemEntry Randomizer_GetRandomizedItem(GetItemID ogId, s16 actorId, s16 actorParams, s16 sceneNum); diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp index 9f93a777d..4621c5e93 100644 --- a/soh/soh/SaveManager.cpp +++ b/soh/soh/SaveManager.cpp @@ -768,6 +768,10 @@ void SaveManager::LoadBaseVersion1() { SaveManager::Instance->LoadArray("cowsMilked", ARRAY_COUNT(gSaveContext.cowsMilked), [](size_t i) { SaveManager::Instance->LoadData("", gSaveContext.cowsMilked[i]); }); + + SaveManager::Instance->LoadArray("scrubsPurchased", ARRAY_COUNT(gSaveContext.scrubsPurchased), [](size_t i) { + SaveManager::Instance->LoadData("", gSaveContext.scrubsPurchased[i]); + }); } void SaveManager::LoadBaseVersion2() { @@ -932,6 +936,10 @@ void SaveManager::LoadBaseVersion2() { SaveManager::Instance->LoadArray("cowsMilked", ARRAY_COUNT(gSaveContext.cowsMilked), [](size_t i) { SaveManager::Instance->LoadData("", gSaveContext.cowsMilked[i]); }); + + SaveManager::Instance->LoadArray("scrubsPurchased", ARRAY_COUNT(gSaveContext.scrubsPurchased), [](size_t i) { + SaveManager::Instance->LoadData("", gSaveContext.scrubsPurchased[i]); + }); } void SaveManager::SaveBase() { @@ -1092,6 +1100,10 @@ void SaveManager::SaveBase() { SaveManager::Instance->SaveArray("cowsMilked", ARRAY_COUNT(gSaveContext.cowsMilked), [](size_t i) { SaveManager::Instance->SaveData("", gSaveContext.cowsMilked[i]); }); + + SaveManager::Instance->SaveArray("scrubsPurchased", ARRAY_COUNT(gSaveContext.scrubsPurchased), [](size_t i) { + SaveManager::Instance->SaveData("", gSaveContext.scrubsPurchased[i]); + }); } void SaveManager::SaveArray(const std::string& name, const size_t size, SaveArrayFunc func) { diff --git a/soh/src/code/z_sram.c b/soh/src/code/z_sram.c index 8d23e42eb..4b362f070 100644 --- a/soh/src/code/z_sram.c +++ b/soh/src/code/z_sram.c @@ -7,6 +7,7 @@ #define NUM_DUNGEONS 8 #define NUM_TRIALS 6 #define NUM_COWS 10 +#define NUM_SCRUBS 35 /** * Initialize new save. @@ -717,6 +718,11 @@ void Sram_InitSave(FileChooseContext* fileChooseCtx) { gSaveContext.cowsMilked[i] = 0; } + // Sets all scrubs to not purchased when generating a rando save. + for (u8 i = 0; i < NUM_SCRUBS; i++) { + gSaveContext.scrubsPurchased[i] = 0; + } + // Set Cutscene flags to skip them gSaveContext.eventChkInf[0xC] |= 0x10; // returned to tot with medallions gSaveContext.eventChkInf[0xC] |= 0x20; //sheik at tot pedestal diff --git a/soh/src/overlays/actors/ovl_En_Dns/z_en_dns.c b/soh/src/overlays/actors/ovl_En_Dns/z_en_dns.c index d3a924356..878e8eeed 100644 --- a/soh/src/overlays/actors/ovl_En_Dns/z_en_dns.c +++ b/soh/src/overlays/actors/ovl_En_Dns/z_en_dns.c @@ -15,6 +15,7 @@ void EnDns_Destroy(Actor* thisx, GlobalContext* globalCtx); void EnDns_Update(Actor* thisx, GlobalContext* globalCtx); void EnDns_Draw(Actor* thisx, GlobalContext* globalCtx); +u32 EnDns_RandomizerPurchaseableCheck(EnDns* this); u32 func_809EF5A4(EnDns* this); u32 func_809EF658(EnDns* this); u32 func_809EF70C(EnDns* this); @@ -24,6 +25,7 @@ u32 func_809EF854(EnDns* this); u32 func_809EF8F4(EnDns* this); u32 func_809EF9A4(EnDns* this); +void EnDns_RandomizerPurchase(EnDns* this); void func_809EF9F8(EnDns* this); void func_809EFA28(EnDns* this); void func_809EFA58(EnDns* this); @@ -155,7 +157,6 @@ void EnDns_Init(Actor* thisx, GlobalContext* globalCtx) { Collider_InitCylinder(globalCtx, &this->collider); Collider_SetCylinderType1(globalCtx, &this->collider, &this->actor, &sCylinderInit); ActorShape_Init(&this->actor.shape, 0.0f, ActorShadow_DrawCircle, 35.0f); - this->actor.textId = D_809F040C[this->actor.params]; Actor_SetScale(&this->actor, 0.01f); this->actor.colChkInfo.mass = MASS_IMMOVABLE; this->maintainCollider = 1; @@ -164,7 +165,24 @@ void EnDns_Init(Actor* thisx, GlobalContext* globalCtx) { this->actor.speedXZ = 0.0f; this->actor.velocity.y = 0.0f; this->actor.gravity = -1.0f; + this->actor.textId = D_809F040C[this->actor.params]; this->dnsItemEntry = sItemEntries[this->actor.params]; + if (gSaveContext.n64ddFlag) { + s16 respawnData = gSaveContext.respawn[RESPAWN_MODE_RETURN].data & ((1 << 8) - 1); + this->scrubIdentity = Randomizer_IdentifyScrub(globalCtx->sceneNum, this->actor.params, respawnData); + + if (Randomizer_GetSettingValue(RSK_SHUFFLE_SCRUBS) == 1 || Randomizer_GetSettingValue(RSK_SHUFFLE_SCRUBS) == 3 && this->scrubIdentity.itemPrice != -1) { + this->dnsItemEntry->itemPrice = this->scrubIdentity.itemPrice; + } + + if (this->scrubIdentity.isShuffled) { + this->dnsItemEntry->getItemId = this->scrubIdentity.getItemId; + this->dnsItemEntry->purchaseableCheck = EnDns_RandomizerPurchaseableCheck; + this->dnsItemEntry->setRupeesAndFlags = EnDns_RandomizerPurchase; + this->dnsItemEntry->itemAmount = 1; + this->actor.textId = 0x9000 + this->dnsItemEntry->itemPrice; + } + } this->actionFunc = EnDns_SetupWait; } @@ -185,6 +203,13 @@ void EnDns_ChangeAnim(EnDns* this, u8 index) { /* Item give checking functions */ +u32 EnDns_RandomizerPurchaseableCheck(EnDns* this) { + if (gSaveContext.rupees < this->dnsItemEntry->itemPrice || gSaveContext.scrubsPurchased[this->scrubIdentity.scrubId] == 1) { + return 0; + } + return 4; +} + u32 func_809EF5A4(EnDns* this) { if ((CUR_CAPACITY(UPG_NUTS) != 0) && (AMMO(ITEM_NUT) >= CUR_CAPACITY(UPG_NUTS))) { return 1; @@ -281,6 +306,10 @@ u32 func_809EF9A4(EnDns* this) { } /* Paying and flagging functions */ +void EnDns_RandomizerPurchase(EnDns* this) { + Rupees_ChangeBy(-this->dnsItemEntry->itemPrice); + gSaveContext.scrubsPurchased[this->scrubIdentity.scrubId] = 1; +} void func_809EF9F8(EnDns* this) { Rupees_ChangeBy(-this->dnsItemEntry->itemPrice); @@ -369,31 +398,25 @@ void EnDns_Talk(EnDns* this, GlobalContext* globalCtx) { } void func_809EFDD0(EnDns* this, GlobalContext* globalCtx) { - if (this->actor.params == 0x9) { - if (gSaveContext.n64ddFlag) { - GetItemEntry getItemEntry = Randomizer_GetRandomizedItem(GI_STICK_UPGRADE_30, this->actor.id, this->actor.params, globalCtx->sceneNum); - GiveItemEntryFromActor(&this->actor, globalCtx, getItemEntry, 130.0f, 100.0f); - } else if (CUR_UPG_VALUE(UPG_STICKS) < 2) { - func_8002F434(&this->actor, globalCtx, GI_STICK_UPGRADE_20, 130.0f, 100.0f); + if (!gSaveContext.n64ddFlag || !this->scrubIdentity.isShuffled) { + if (this->actor.params == 0x9) { + if (CUR_UPG_VALUE(UPG_STICKS) < 2) { + func_8002F434(&this->actor, globalCtx, GI_STICK_UPGRADE_20, 130.0f, 100.0f); + } else { + func_8002F434(&this->actor, globalCtx, GI_STICK_UPGRADE_30, 130.0f, 100.0f); + } + } else if (this->actor.params == 0xA) { + if (CUR_UPG_VALUE(UPG_NUTS) < 2) { + func_8002F434(&this->actor, globalCtx, GI_NUT_UPGRADE_30, 130.0f, 100.0f); + } else { + func_8002F434(&this->actor, globalCtx, GI_NUT_UPGRADE_40, 130.0f, 100.0f); + } } else { - func_8002F434(&this->actor, globalCtx, GI_STICK_UPGRADE_30, 130.0f, 100.0f); - } - } else if (this->actor.params == 0xA) { - if (gSaveContext.n64ddFlag) { - GetItemEntry getItemEntry = Randomizer_GetRandomizedItem(GI_NUT_UPGRADE_40, this->actor.id, this->actor.params, globalCtx->sceneNum); - GiveItemEntryFromActor(&this->actor, globalCtx, getItemEntry, 130.0f, 100.0f); - } else if (CUR_UPG_VALUE(UPG_NUTS) < 2) { - func_8002F434(&this->actor, globalCtx, GI_NUT_UPGRADE_30, 130.0f, 100.0f); - } else { - func_8002F434(&this->actor, globalCtx, GI_NUT_UPGRADE_40, 130.0f, 100.0f); + func_8002F434(&this->actor, globalCtx, this->dnsItemEntry->getItemId, 130.0f, 100.0f); } } else { - if (!gSaveContext.n64ddFlag) { - func_8002F434(&this->actor, globalCtx, this->dnsItemEntry->getItemId, 130.0f, 100.0f); - } else { - GetItemEntry getItemEntry = Randomizer_GetRandomizedItem(this->dnsItemEntry->getItemId, this->actor.id, this->actor.params, globalCtx->sceneNum); - GiveItemEntryFromActor(&this->actor, globalCtx, getItemEntry, 130.0f, 100.0f); - } + GetItemEntry itemEntry = Randomizer_GetItemFromKnownCheck(this->scrubIdentity.randomizerCheck, this->scrubIdentity.getItemId); + GiveItemEntryFromActor(&this->actor, globalCtx, itemEntry, 130.0f, 100.0f); } } @@ -489,6 +512,9 @@ void EnDns_Update(Actor* thisx, GlobalContext* globalCtx) { this->dustTimer++; this->actor.textId = D_809F040C[this->actor.params]; + if (gSaveContext.n64ddFlag && this->scrubIdentity.isShuffled) { + this->actor.textId = 0x9000 + this->dnsItemEntry->itemPrice; + } Actor_SetFocus(&this->actor, 60.0f); Actor_SetScale(&this->actor, 0.01f); SkelAnime_Update(&this->skelAnime); diff --git a/soh/src/overlays/actors/ovl_En_Dns/z_en_dns.h b/soh/src/overlays/actors/ovl_En_Dns/z_en_dns.h index 90bd3d095..4dab2bf39 100644 --- a/soh/src/overlays/actors/ovl_En_Dns/z_en_dns.h +++ b/soh/src/overlays/actors/ovl_En_Dns/z_en_dns.h @@ -32,6 +32,7 @@ typedef struct EnDns { /* 0x02BD */ u8 dropCollectible; /* 0x02C0 */ DnsItemEntry* dnsItemEntry; /* 0x02C4 */ f32 yInitPos; + /* */ ScrubIdentity scrubIdentity; } EnDns; // size = 0x02C8 #endif diff --git a/soh/src/overlays/actors/ovl_En_Shopnuts/z_en_shopnuts.c b/soh/src/overlays/actors/ovl_En_Shopnuts/z_en_shopnuts.c index 2b1f6a12e..6655b3aea 100644 --- a/soh/src/overlays/actors/ovl_En_Shopnuts/z_en_shopnuts.c +++ b/soh/src/overlays/actors/ovl_En_Shopnuts/z_en_shopnuts.c @@ -69,6 +69,15 @@ void EnShopnuts_Init(Actor* thisx, GlobalContext* globalCtx) { CollisionCheck_SetInfo(&this->actor.colChkInfo, NULL, &sColChkInfoInit); Collider_UpdateCylinder(&this->actor, &this->collider); + if (gSaveContext.n64ddFlag) { + s16 respawnData = gSaveContext.respawn[RESPAWN_MODE_RETURN].data & ((1 << 8) - 1); + ScrubIdentity scrubIdentity = Randomizer_IdentifyScrub(globalCtx->sceneNum, this->actor.params, respawnData); + + if (scrubIdentity.isShuffled && gSaveContext.scrubsPurchased[scrubIdentity.scrubId] == 1) { + Actor_Kill(&this->actor); + } + } + if (((this->actor.params == 0x0002) && (gSaveContext.itemGetInf[0] & 0x800)) || ((this->actor.params == 0x0009) && (gSaveContext.infTable[25] & 4)) || ((this->actor.params == 0x000A) && (gSaveContext.infTable[25] & 8))) {