From 9397b10f172c5c2c4762c72482fe11e09a0546fb Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:28:59 +0100 Subject: [PATCH 01/22] iAdd split ocarina songs mode with part-based progression. This introduces song part items, logic/state tracking, and UI/runtime integration so two parts unlock each full song while preserving stable randomizer behavior in Anywhere mode. --- soh/soh/Enhancements/debugconsole.cpp | 22 ++ .../randomizer/3drando/item_pool.cpp | 95 ++++- .../randomizer/Messages/ItemMessages.cpp | 57 ++- soh/soh/Enhancements/randomizer/Traps.cpp | 6 + .../Enhancements/randomizer/hook_handlers.cpp | 4 + soh/soh/Enhancements/randomizer/item.cpp | 5 + soh/soh/Enhancements/randomizer/item_list.cpp | 25 ++ soh/soh/Enhancements/randomizer/logic.cpp | 52 ++- .../randomizer/option_descriptions.cpp | 10 + .../Enhancements/randomizer/randomizer.cpp | 9 + .../randomizerEnums/RandomizerGet.h | 24 ++ .../randomizerEnums/RandomizerInf.h | 24 ++ .../randomizerEnums/RandomizerSettingKey.h | 1 + .../randomizer/randomizer_item_tracker.cpp | 207 ++++++++++- soh/soh/Enhancements/randomizer/settings.cpp | 18 + .../Enhancements/randomizer/split_songs.cpp | 327 ++++++++++++++++++ soh/soh/Enhancements/randomizer/split_songs.h | 82 +++++ 17 files changed, 949 insertions(+), 19 deletions(-) create mode 100644 soh/soh/Enhancements/randomizer/split_songs.cpp create mode 100644 soh/soh/Enhancements/randomizer/split_songs.h diff --git a/soh/soh/Enhancements/debugconsole.cpp b/soh/soh/Enhancements/debugconsole.cpp index 7fecad83ac2..b6c8eaa4800 100644 --- a/soh/soh/Enhancements/debugconsole.cpp +++ b/soh/soh/Enhancements/debugconsole.cpp @@ -12,6 +12,8 @@ #include "soh/Enhancements/cosmetics/CosmeticsEditor.h" #include "soh/Enhancements/audio/AudioEditor.h" #include "soh/Enhancements/randomizer/logic.h" +#include "soh/Enhancements/randomizer/static_data.h" +#include "soh/Enhancements/randomizer/split_songs.h" #define Path _Path #define PATH_HACK @@ -403,6 +405,20 @@ static bool GiveItemHandler(std::shared_ptr Console, const std::v return 0; } +static bool RandoGiveAllSplitSongPartsHandler(std::shared_ptr Console, + const std::vector args, std::string* output) { + if (gPlayState == nullptr) { + ERROR_MESSAGE("No active game; load into gameplay first."); + return 1; + } + if (!IS_RANDO) { + ERROR_MESSAGE("Only works in Randomizer (load a rando save)."); + return 1; + } + Rando::SplitSongs::DebugGiveAllSongParts(gPlayState); + return 0; +} + static bool EntranceHandler(std::shared_ptr Console, const std::vector& args, std::string* output) { if (args.size() < 2) { @@ -1605,6 +1621,12 @@ void DebugConsole_Init(void) { { "giveItemID", Ship::ArgumentType::NUMBER }, } }); + CMD_REGISTER("rando_give_all_split_song_parts", + { RandoGiveAllSplitSongPartsHandler, + "Randomizer: grants all 12 ocarina songs via both split parts (tests Randomizer_Item_Give + " + "completion). Rando save only.", + {} }); + CMD_REGISTER("item", { ItemHandler, "Sets item ID in arg 1 into slot arg 2. No boundary checks. Use with caution.", { diff --git a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp index 8a962d1a7ab..68cc39434de 100644 --- a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp @@ -8,6 +8,7 @@ #include "random.hpp" #include "spoiler_log.hpp" #include "soh/Enhancements/randomizer/Traps.h" +#include "soh/Enhancements/randomizer/split_songs.h" #include "z64item.h" #include @@ -155,6 +156,15 @@ void GenerateItemPool() { lesserPool.clear(); int reservedSlots = 0; + if (ctx->GetOption(RSK_SHUFFLE_SONGS).Is(RO_SONG_SHUFFLE_ANYWHERE)) { + const bool split = static_cast(ctx->GetOption(RSK_SPLIT_OCARINA_SONGS)); + std::vector songTrapModels; + Rando::SplitSongs::AppendSongIceTrapModels(songTrapModels, split); + for (RandomizerGet rg : songTrapModels) { + ctx->possibleIceTrapModels.insert(rg); + } + } + // clang-format off AddItemToPool(RG_BOOMERANG, 2, 1, 1, 1); AddItemToPool(RG_LENS_OF_TRUTH, 2, 1, 1, 1); @@ -251,41 +261,102 @@ void GenerateItemPool() { // add extra songs only if song shuffle is anywhere if (ctx->GetOption(RSK_SHUFFLE_SONGS).IsNot(RO_SONG_SHUFFLE_OFF)) { bool songAnywhere = ctx->GetOption(RSK_SHUFFLE_SONGS).Is(RO_SONG_SHUFFLE_ANYWHERE); + const bool split = ctx->GetOption(RSK_SPLIT_OCARINA_SONGS) && songAnywhere; if (!ctx->GetOption(RSK_STARTING_ZELDAS_LULLABY).Get()) { - AddItemToPool(RG_ZELDAS_LULLABY, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + if (!split) { + AddItemToPool(RG_ZELDAS_LULLABY, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } else { + AddItemToPool(RG_ZELDAS_LULLABY_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_ZELDAS_LULLABY_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } } if (!ctx->GetOption(RSK_STARTING_EPONAS_SONG).Get()) { - AddItemToPool(RG_EPONAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + if (!split) { + AddItemToPool(RG_EPONAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } else { + AddItemToPool(RG_EPONAS_SONG_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_EPONAS_SONG_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } } if (!ctx->GetOption(RSK_STARTING_SARIAS_SONG).Get()) { - AddItemToPool(RG_SARIAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + if (!split) { + AddItemToPool(RG_SARIAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } else { + AddItemToPool(RG_SARIAS_SONG_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_SARIAS_SONG_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } } if (!ctx->GetOption(RSK_STARTING_SUNS_SONG).Get()) { - AddItemToPool(RG_SUNS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + if (!split) { + AddItemToPool(RG_SUNS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } else { + AddItemToPool(RG_SUNS_SONG_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_SUNS_SONG_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } } if (!ctx->GetOption(RSK_STARTING_SONG_OF_TIME).Get()) { - AddItemToPool(RG_SONG_OF_TIME, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + if (!split) { + AddItemToPool(RG_SONG_OF_TIME, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } else { + AddItemToPool(RG_SONG_OF_TIME_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_SONG_OF_TIME_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } } if (!ctx->GetOption(RSK_STARTING_SONG_OF_STORMS).Get()) { - AddItemToPool(RG_SONG_OF_STORMS, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + if (!split) { + AddItemToPool(RG_SONG_OF_STORMS, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } else { + AddItemToPool(RG_SONG_OF_STORMS_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_SONG_OF_STORMS_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } } if (!ctx->GetOption(RSK_STARTING_MINUET_OF_FOREST).Get()) { - AddItemToPool(RG_MINUET_OF_FOREST, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + if (!split) { + AddItemToPool(RG_MINUET_OF_FOREST, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } else { + AddItemToPool(RG_MINUET_OF_FOREST_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_MINUET_OF_FOREST_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } } if (!ctx->GetOption(RSK_STARTING_BOLERO_OF_FIRE).Get()) { - AddItemToPool(RG_BOLERO_OF_FIRE, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + if (!split) { + AddItemToPool(RG_BOLERO_OF_FIRE, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } else { + AddItemToPool(RG_BOLERO_OF_FIRE_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_BOLERO_OF_FIRE_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } } if (!ctx->GetOption(RSK_STARTING_SERENADE_OF_WATER).Get()) { - AddItemToPool(RG_SERENADE_OF_WATER, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + if (!split) { + AddItemToPool(RG_SERENADE_OF_WATER, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } else { + AddItemToPool(RG_SERENADE_OF_WATER_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_SERENADE_OF_WATER_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } } if (!ctx->GetOption(RSK_STARTING_REQUIEM_OF_SPIRIT).Get()) { - AddItemToPool(RG_REQUIEM_OF_SPIRIT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + if (!split) { + AddItemToPool(RG_REQUIEM_OF_SPIRIT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } else { + AddItemToPool(RG_REQUIEM_OF_SPIRIT_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_REQUIEM_OF_SPIRIT_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } } if (!ctx->GetOption(RSK_STARTING_NOCTURNE_OF_SHADOW).Get()) { - AddItemToPool(RG_NOCTURNE_OF_SHADOW, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + if (!split) { + AddItemToPool(RG_NOCTURNE_OF_SHADOW, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } else { + AddItemToPool(RG_NOCTURNE_OF_SHADOW_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_NOCTURNE_OF_SHADOW_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } } if (!ctx->GetOption(RSK_STARTING_PRELUDE_OF_LIGHT).Get()) { - AddItemToPool(RG_PRELUDE_OF_LIGHT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + if (!split) { + AddItemToPool(RG_PRELUDE_OF_LIGHT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } else { + AddItemToPool(RG_PRELUDE_OF_LIGHT_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PRELUDE_OF_LIGHT_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + } } } else { ctx->PlaceItemInLocation(RC_SHEIK_IN_FOREST, RG_MINUET_OF_FOREST, false, true); diff --git a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp index cd1ae016927..85b9a9ca76e 100644 --- a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp @@ -5,6 +5,8 @@ * etc. */ #include +#include "soh/Enhancements/randomizer/split_songs.h" +#include "soh/Enhancements/randomizer/static_data.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" #include "soh/Enhancements/custom-message/CustomMessageTypes.h" @@ -19,9 +21,45 @@ extern "C" { #include #include #include "z64item.h" +#include "z64player.h" extern PlayState* gPlayState; } +namespace { + +ItemID VanillaItemIdForFullSong(RandomizerGet fullSongRg) { + switch (fullSongRg) { + case RG_ZELDAS_LULLABY: + return ITEM_SONG_LULLABY; + case RG_EPONAS_SONG: + return ITEM_SONG_EPONA; + case RG_SARIAS_SONG: + return ITEM_SONG_SARIA; + case RG_SUNS_SONG: + return ITEM_SONG_SUN; + case RG_SONG_OF_TIME: + return ITEM_SONG_TIME; + case RG_SONG_OF_STORMS: + return ITEM_SONG_STORMS; + case RG_MINUET_OF_FOREST: + return ITEM_SONG_MINUET; + case RG_BOLERO_OF_FIRE: + return ITEM_SONG_BOLERO; + case RG_SERENADE_OF_WATER: + return ITEM_SONG_SERENADE; + case RG_REQUIEM_OF_SPIRIT: + return ITEM_SONG_REQUIEM; + case RG_NOCTURNE_OF_SHADOW: + return ITEM_SONG_NOCTURNE; + case RG_PRELUDE_OF_LIGHT: + return ITEM_SONG_PRELUDE; + default: + return ITEM_NONE; + } +} + +} // namespace + void BuildTriforcePieceMessage(CustomMessage& msg) { uint8_t current = gSaveContext.ship.quest.data.randomizer.triforcePiecesCollected + 1; uint8_t required = OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_TRIFORCE_HUNT_PIECES_REQUIRED) + 1; @@ -72,11 +110,28 @@ void BuildCustomItemMessage(Player* player, CustomMessage& msg) { msg = CustomMessage("You found [[article]][[color]][[name]]%w!", "Du erhältst [[article]][[color]][[name]]%w gefunden!", "Vous avez trouvé [[article]][[color]][[name]]%w!", TEXTBOX_TYPE_BLUE); - if (player->getItemEntry.objectId != OBJECT_INVALID) { + if (player->getItemEntry.modIndex == MOD_RANDOMIZER) { + rgid = player->getItemEntry.getItemId; + } else if (player->getItemEntry.objectId != OBJECT_INVALID) { rgid = player->getItemEntry.getItemId; } else { rgid = player->getItemId; } + if (player->getItemEntry.modIndex == MOD_RANDOMIZER && rgid < 0) { + rgid = (s16)-rgid; + } + if (IS_RANDO && Rando::SplitSongs::IsSongPart(static_cast(rgid))) { + const RandomizerGet partRg = static_cast(rgid); + const auto& songPartItem = Rando::StaticData::RetrieveItem(partRg); + const auto* splitDef = Rando::SplitSongs::GetSongDefFromPart(partRg); + const ItemID iconItemId = + splitDef != nullptr ? VanillaItemIdForFullSong(splitDef->fullSong) : ITEM_NONE; + const auto& nm = songPartItem.GetName(); + CustomMessage getItemText(nm.GetEnglish(), nm.GetGerman(), nm.GetFrench(), TEXTBOX_TYPE_BLUE, TEXTBOX_POS_BOTTOM); + getItemText.Format(iconItemId); + msg = getItemText; + return; + } CustomMessage name = CustomMessage(Rando::StaticData::RetrieveItem(static_cast(rgid)).GetName(), TEXTBOX_TYPE_BLUE); CustomMessage article = CustomMessage( diff --git a/soh/soh/Enhancements/randomizer/Traps.cpp b/soh/soh/Enhancements/randomizer/Traps.cpp index 050913a4a00..6f0a448a40f 100644 --- a/soh/soh/Enhancements/randomizer/Traps.cpp +++ b/soh/soh/Enhancements/randomizer/Traps.cpp @@ -2,6 +2,7 @@ #include "soh/Enhancements/randomizer/SeedContext.h" #include "soh/Enhancements/randomizer/randomizerTypes.h" #include "soh/Enhancements/randomizer/static_data.h" +#include "soh/Enhancements/randomizer/split_songs.h" #include "soh/Enhancements/randomizer/3drando/random.hpp" #include @@ -1425,6 +1426,11 @@ Text Rando::Traps::GetTrapName(uint16_t id) { initTrickNames = true; } + RandomizerGet rg = static_cast(id); + if (const Rando::SplitSongDef* splitDef = Rando::SplitSongs::GetSongDefFromPart(rg)) { + id = static_cast(splitDef->fullSong); + } + if (trickNameTable[id].empty()) { assert(false); return Text{ "not an Ice Trap" }; diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp index 73b2cda6de5..ae8db673c22 100644 --- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp +++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp @@ -4,6 +4,7 @@ #include "soh/Enhancements/enhancementTypes.h" #include "soh/Enhancements/custom-message/CustomMessageTypes.h" #include "soh/Enhancements/randomizer/randomizerTypes.h" +#include "soh/Enhancements/randomizer/split_songs.h" #include "soh/Enhancements/randomizer/dungeon.h" #include "soh/Enhancements/randomizer/static_data.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" @@ -2544,6 +2545,8 @@ std::unordered_map swimSpecialRespawnInfo = { f32 triforcePieceScale; void RandomizerOnPlayerUpdateHandler() { + Rando::SplitSongs::ProcessPendingFullSongGrants(); + if ((GET_PLAYER(gPlayState)->stateFlags1 & PLAYER_STATE1_IN_WATER) && !Flags_GetRandomizerInf(RAND_INF_CAN_SWIM) && CUR_EQUIP_VALUE(EQUIP_TYPE_BOOTS) != EQUIP_VALUE_BOOTS_IRON) { // if you void out in water temple without swim you get instantly kicked out to prevent softlocks @@ -2688,6 +2691,7 @@ static void RandomizerRegisterHooks() { randomizerQueuedChecks = std::queue(); randomizerQueuedCheck = RC_UNKNOWN_CHECK; randomizerQueuedItemEntry = GET_ITEM_NONE; + Rando::SplitSongs::ClearPendingFullSongGrants(); GameInteractor::Instance->UnregisterGameHook(onFlagSetHook); GameInteractor::Instance->UnregisterGameHook(onSceneFlagSetHook); diff --git a/soh/soh/Enhancements/randomizer/item.cpp b/soh/soh/Enhancements/randomizer/item.cpp index e784612f7cd..fbbb96bbcaa 100644 --- a/soh/soh/Enhancements/randomizer/item.cpp +++ b/soh/soh/Enhancements/randomizer/item.cpp @@ -2,6 +2,7 @@ #include "item_location.h" #include "SeedContext.h" +#include "split_songs.h" #include "logic.h" #include "3drando/item_pool.hpp" #include "z64item.h" @@ -51,6 +52,8 @@ void Item::ApplyEffect() const { auto logic = ctx->GetLogic(); if (!logic->CalculatingAvailableChecks) { logic->ApplyItemEffect(StaticData::RetrieveItem(randomizerGet), true); + } else if (SplitSongs::IsSongPart(randomizerGet)) { + SplitSongs::ApplyPartEffectToLogicScratch(logic.get(), randomizerGet, true); } logic->Set(logicVal, true); } @@ -60,6 +63,8 @@ void Item::UndoEffect() const { auto logic = ctx->GetLogic(); if (!logic->CalculatingAvailableChecks) { logic->ApplyItemEffect(StaticData::RetrieveItem(randomizerGet), false); + } else if (SplitSongs::IsSongPart(randomizerGet)) { + SplitSongs::ApplyPartEffectToLogicScratch(logic.get(), randomizerGet, false); } logic->Set(logicVal, false); } diff --git a/soh/soh/Enhancements/randomizer/item_list.cpp b/soh/soh/Enhancements/randomizer/item_list.cpp index 892cc8ac04c..4f9695594d0 100644 --- a/soh/soh/Enhancements/randomizer/item_list.cpp +++ b/soh/soh/Enhancements/randomizer/item_list.cpp @@ -87,6 +87,31 @@ void Rando::StaticData::InitItemTable() { itemTable[RG_BOTTLE_WITH_POE] = Item(RG_BOTTLE_WITH_POE, Text{ "Bottle with Poe", "Bouteille avec un Esprit", "Flasche mit einem Geist" }, ITEMTYPE_ITEM, 0x94, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_POE, RG_BOTTLE_WITH_POE, OBJECT_GI_GHOST, GID_POE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "eine ", "une "}); itemTable[RG_RUTOS_LETTER] = Item(RG_RUTOS_LETTER, Text{ "Bottle with Ruto's Letter", "Bouteille avec la Lettre de Ruto", "Flasche mit Rutos Brief" }, ITEMTYPE_ITEM, GI_LETTER_RUTO, true, LOGIC_RUTOS_LETTER, RHT_RUTOS_LETTER, ITEM_LETTER_RUTO, OBJECT_GI_BOTTLE_LETTER, GID_LETTER_RUTO, 0x99, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"a ", "einen ", "un "}); itemTable[RG_BOTTLE_WITH_BIG_POE] = Item(RG_BOTTLE_WITH_BIG_POE, Text{ "Bottle with Big Poe", "Bouteille avec une Âme", "Flasche mit Seele" }, ITEMTYPE_ITEM, 0x93, true, LOGIC_BOTTLE_WITH_BIG_POE, RHT_BOTTLE_WITH_BIG_POE, RG_BOTTLE_WITH_BIG_POE, OBJECT_GI_GHOST, GID_BIG_POE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "einen ", "un "}); + // Split Song Parts + itemTable[RG_ZELDAS_LULLABY_PART1] = Item(RG_ZELDAS_LULLABY_PART1, Text{ "Zelda's Lullaby (Part 1)", "Berceuse de Zelda (Partie 1)", "Zeldas Wiegenlied (Teil 1)" }, ITEMTYPE_SONG, 0xE1, true, LOGIC_NONE, RHT_ZELDAS_LULLABY, RG_ZELDAS_LULLABY_PART1, OBJECT_GI_MELODY, GID_SONG_ZELDA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_ZELDAS_LULLABY_PART2] = Item(RG_ZELDAS_LULLABY_PART2, Text{ "Zelda's Lullaby (Part 2)", "Berceuse de Zelda (Partie 2)", "Zeldas Wiegenlied (Teil 2)" }, ITEMTYPE_SONG, 0xE2, true, LOGIC_NONE, RHT_ZELDAS_LULLABY, RG_ZELDAS_LULLABY_PART2, OBJECT_GI_MELODY, GID_SONG_ZELDA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_EPONAS_SONG_PART1] = Item(RG_EPONAS_SONG_PART1, Text{ "Epona's Song (Part 1)", "Chant d'Epona (Partie 1)", "Eponas Lied (Teil 1)" }, ITEMTYPE_SONG, 0xE3, true, LOGIC_NONE, RHT_EPONAS_SONG, RG_EPONAS_SONG_PART1, OBJECT_GI_MELODY, GID_SONG_EPONA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_EPONAS_SONG_PART2] = Item(RG_EPONAS_SONG_PART2, Text{ "Epona's Song (Part 2)", "Chant d'Epona (Partie 2)", "Eponas Lied (Teil 2)" }, ITEMTYPE_SONG, 0xE4, true, LOGIC_NONE, RHT_EPONAS_SONG, RG_EPONAS_SONG_PART2, OBJECT_GI_MELODY, GID_SONG_EPONA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_SARIAS_SONG_PART1] = Item(RG_SARIAS_SONG_PART1, Text{ "Saria's Song (Part 1)", "Chant de Saria (Partie 1)", "Salias Lied (Teil 1)" }, ITEMTYPE_SONG, 0xE5, true, LOGIC_NONE, RHT_SARIAS_SONG, RG_SARIAS_SONG_PART1, OBJECT_GI_MELODY, GID_SONG_SARIA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_SARIAS_SONG_PART2] = Item(RG_SARIAS_SONG_PART2, Text{ "Saria's Song (Part 2)", "Chant de Saria (Partie 2)", "Salias Lied (Teil 2)" }, ITEMTYPE_SONG, 0xE6, true, LOGIC_NONE, RHT_SARIAS_SONG, RG_SARIAS_SONG_PART2, OBJECT_GI_MELODY, GID_SONG_SARIA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_SUNS_SONG_PART1] = Item(RG_SUNS_SONG_PART1, Text{ "Sun's Song (Part 1)", "Chant du Soleil (Partie 1)", "Hymne der Sonne (Teil 1)" }, ITEMTYPE_SONG, 0xE7, true, LOGIC_NONE, RHT_SUNS_SONG, RG_SUNS_SONG_PART1, OBJECT_GI_MELODY, GID_SONG_SUN, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_SUNS_SONG_PART2] = Item(RG_SUNS_SONG_PART2, Text{ "Sun's Song (Part 2)", "Chant du Soleil (Partie 2)", "Hymne der Sonne (Teil 2)" }, ITEMTYPE_SONG, 0xE8, true, LOGIC_NONE, RHT_SUNS_SONG, RG_SUNS_SONG_PART2, OBJECT_GI_MELODY, GID_SONG_SUN, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_SONG_OF_TIME_PART1] = Item(RG_SONG_OF_TIME_PART1, Text{ "Song of Time (Part 1)", "Chant du Temps (Partie 1)", "Hymne der Zeit (Teil 1)" }, ITEMTYPE_SONG, 0xE9, true, LOGIC_NONE, RHT_SONG_OF_TIME, RG_SONG_OF_TIME_PART1, OBJECT_GI_MELODY, GID_SONG_TIME, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_SONG_OF_TIME_PART2] = Item(RG_SONG_OF_TIME_PART2, Text{ "Song of Time (Part 2)", "Chant du Temps (Partie 2)", "Hymne der Zeit (Teil 2)" }, ITEMTYPE_SONG, 0xEA, true, LOGIC_NONE, RHT_SONG_OF_TIME, RG_SONG_OF_TIME_PART2, OBJECT_GI_MELODY, GID_SONG_TIME, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_SONG_OF_STORMS_PART1] = Item(RG_SONG_OF_STORMS_PART1, Text{ "Song of Storms (Part 1)", "Chant des Tempêtes (Partie 1)", "Hymne des Sturms (Teil 1)" }, ITEMTYPE_SONG, 0xEB, true, LOGIC_NONE, RHT_SONG_OF_STORMS, RG_SONG_OF_STORMS_PART1, OBJECT_GI_MELODY, GID_SONG_STORM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_SONG_OF_STORMS_PART2] = Item(RG_SONG_OF_STORMS_PART2, Text{ "Song of Storms (Part 2)", "Chant des Tempêtes (Partie 2)", "Hymne des Sturms (Teil 2)" }, ITEMTYPE_SONG, 0xEC, true, LOGIC_NONE, RHT_SONG_OF_STORMS, RG_SONG_OF_STORMS_PART2, OBJECT_GI_MELODY, GID_SONG_STORM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_MINUET_OF_FOREST_PART1] = Item(RG_MINUET_OF_FOREST_PART1, Text{ "Minuet of Forest (Part 1)", "Menuet des Bois (Partie 1)", "Menuett des Waldes (Teil 1)" }, ITEMTYPE_SONG, 0xED, true, LOGIC_NONE, RHT_MINUET_OF_FOREST, RG_MINUET_OF_FOREST_PART1, OBJECT_GI_MELODY, GID_SONG_MINUET, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_MINUET_OF_FOREST_PART2] = Item(RG_MINUET_OF_FOREST_PART2, Text{ "Minuet of Forest (Part 2)", "Menuet des Bois (Partie 2)", "Menuett des Waldes (Teil 2)" }, ITEMTYPE_SONG, 0xEE, true, LOGIC_NONE, RHT_MINUET_OF_FOREST, RG_MINUET_OF_FOREST_PART2, OBJECT_GI_MELODY, GID_SONG_MINUET, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_BOLERO_OF_FIRE_PART1] = Item(RG_BOLERO_OF_FIRE_PART1, Text{ "Bolero of Fire (Part 1)", "Boléro du Feu (Partie 1)", "Bolero des Feuers (Teil 1)" }, ITEMTYPE_SONG, 0xEF, true, LOGIC_NONE, RHT_BOLERO_OF_FIRE, RG_BOLERO_OF_FIRE_PART1, OBJECT_GI_MELODY, GID_SONG_BOLERO, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_BOLERO_OF_FIRE_PART2] = Item(RG_BOLERO_OF_FIRE_PART2, Text{ "Bolero of Fire (Part 2)", "Boléro du Feu (Partie 2)", "Bolero des Feuers (Teil 2)" }, ITEMTYPE_SONG, 0xF0, true, LOGIC_NONE, RHT_BOLERO_OF_FIRE, RG_BOLERO_OF_FIRE_PART2, OBJECT_GI_MELODY, GID_SONG_BOLERO, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_SERENADE_OF_WATER_PART1] = Item(RG_SERENADE_OF_WATER_PART1, Text{ "Serenade of Water (Part 1)", "Sérénade de l'Eau (Partie 1)", "Serenade des Wassers (Teil 1)" }, ITEMTYPE_SONG, 0xF1, true, LOGIC_NONE, RHT_SERENADE_OF_WATER, RG_SERENADE_OF_WATER_PART1, OBJECT_GI_MELODY, GID_SONG_SERENADE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_SERENADE_OF_WATER_PART2] = Item(RG_SERENADE_OF_WATER_PART2, Text{ "Serenade of Water (Part 2)", "Sérénade de l'Eau (Partie 2)", "Serenade des Wassers (Teil 2)" }, ITEMTYPE_SONG, 0xF2, true, LOGIC_NONE, RHT_SERENADE_OF_WATER, RG_SERENADE_OF_WATER_PART2, OBJECT_GI_MELODY, GID_SONG_SERENADE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_REQUIEM_OF_SPIRIT_PART1] = Item(RG_REQUIEM_OF_SPIRIT_PART1, Text{ "Requiem of Spirit (Part 1)", "Requiem des Esprits (Partie 1)", "Requiem der Geister (Teil 1)" }, ITEMTYPE_SONG, 0xF3, true, LOGIC_NONE, RHT_REQUIEM_OF_SPIRIT, RG_REQUIEM_OF_SPIRIT_PART1, OBJECT_GI_MELODY, GID_SONG_REQUIEM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_REQUIEM_OF_SPIRIT_PART2] = Item(RG_REQUIEM_OF_SPIRIT_PART2, Text{ "Requiem of Spirit (Part 2)", "Requiem des Esprits (Partie 2)", "Requiem der Geister (Teil 2)" }, ITEMTYPE_SONG, 0xF4, true, LOGIC_NONE, RHT_REQUIEM_OF_SPIRIT, RG_REQUIEM_OF_SPIRIT_PART2, OBJECT_GI_MELODY, GID_SONG_REQUIEM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_NOCTURNE_OF_SHADOW_PART1] = Item(RG_NOCTURNE_OF_SHADOW_PART1, Text{ "Nocturne of Shadow (Part 1)", "Nocturne de l'Ombre (Partie 1)", "Nocturne des Schattens (Teil 1)" }, ITEMTYPE_SONG, 0xF5, true, LOGIC_NONE, RHT_NOCTURNE_OF_SHADOW, RG_NOCTURNE_OF_SHADOW_PART1, OBJECT_GI_MELODY, GID_SONG_NOCTURNE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_NOCTURNE_OF_SHADOW_PART2] = Item(RG_NOCTURNE_OF_SHADOW_PART2, Text{ "Nocturne of Shadow (Part 2)", "Nocturne de l'Ombre (Partie 2)", "Nocturne des Schattens (Teil 2)" }, ITEMTYPE_SONG, 0xF6, true, LOGIC_NONE, RHT_NOCTURNE_OF_SHADOW, RG_NOCTURNE_OF_SHADOW_PART2, OBJECT_GI_MELODY, GID_SONG_NOCTURNE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_PRELUDE_OF_LIGHT_PART1] = Item(RG_PRELUDE_OF_LIGHT_PART1, Text{ "Prelude of Light (Part 1)", "Prélude de la Lumière (Partie 1)", "Kantate des Lichts (Teil 1)" }, ITEMTYPE_SONG, 0xF7, true, LOGIC_NONE, RHT_PRELUDE_OF_LIGHT, RG_PRELUDE_OF_LIGHT_PART1, OBJECT_GI_MELODY, GID_SONG_PRELUDE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); + itemTable[RG_PRELUDE_OF_LIGHT_PART2] = Item(RG_PRELUDE_OF_LIGHT_PART2, Text{ "Prelude of Light (Part 2)", "Prélude de la Lumière (Partie 2)", "Kantate des Lichts (Teil 2)" }, ITEMTYPE_SONG, 0xF8, true, LOGIC_NONE, RHT_PRELUDE_OF_LIGHT, RG_PRELUDE_OF_LIGHT_PART2, OBJECT_GI_MELODY, GID_SONG_PRELUDE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); // Songs itemTable[RG_ZELDAS_LULLABY] = Item(RG_ZELDAS_LULLABY, Text{ "Zelda's Lullaby", "Berceuse de Zelda", "Zeldas Wiegenlied" }, ITEMTYPE_SONG, 0xC1, true, LOGIC_ZELDAS_LULLABY, RHT_ZELDAS_LULLABY, ITEM_SONG_LULLABY, OBJECT_GI_MELODY, GID_SONG_ZELDA, 0xD4, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE); itemTable[RG_EPONAS_SONG] = Item(RG_EPONAS_SONG, Text{ "Epona's Song", "Chant d'Epona", "Eponas Lied" }, ITEMTYPE_SONG, 0xC2, true, LOGIC_EPONAS_SONG, RHT_EPONAS_SONG, ITEM_SONG_EPONA, OBJECT_GI_MELODY, GID_SONG_EPONA, 0xD2, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE); diff --git a/soh/soh/Enhancements/randomizer/logic.cpp b/soh/soh/Enhancements/randomizer/logic.cpp index 427a6c4c2e5..12da475ac4b 100644 --- a/soh/soh/Enhancements/randomizer/logic.cpp +++ b/soh/soh/Enhancements/randomizer/logic.cpp @@ -7,6 +7,7 @@ #include "soh/OTRGlobals.h" #include "dungeon.h" #include "SeedContext.h" +#include "split_songs.h" #include "macros.h" #include "variables.h" #include @@ -92,7 +93,7 @@ bool Logic::HasItem(RandomizerGet itemName) { return CurrentUpgrade(UPG_BOMB_BAG); case RG_MAGIC_SINGLE: return GetSaveContext()->magicLevel >= 1 || GetSaveContext()->isMagicAcquired; - // Songs + // Songs (split + Anywhere: logical ownership is two parts on the logic scratch before quest is granted) case RG_ZELDAS_LULLABY: case RG_EPONAS_SONG: case RG_SARIAS_SONG: @@ -104,7 +105,24 @@ bool Logic::HasItem(RandomizerGet itemName) { case RG_SERENADE_OF_WATER: case RG_REQUIEM_OF_SPIRIT: case RG_NOCTURNE_OF_SHADOW: - case RG_PRELUDE_OF_LIGHT: + case RG_PRELUDE_OF_LIGHT: { + const bool splitAnywhere = ctx->GetOption(RSK_SPLIT_OCARINA_SONGS) && + ctx->GetOption(RSK_SHUFFLE_SONGS).Is(RO_SONG_SHUFFLE_ANYWHERE); + const auto qiIt = RandoGetToQuestItem.find(itemName); + if (qiIt == RandoGetToQuestItem.end()) { + SPDLOG_ERROR("HasItem: song RandomizerGet {} missing from RandoGetToQuestItem", + static_cast(itemName)); + assert(false); + return false; + } + if (splitAnywhere) { + const SplitSongDef* sdef = SplitSongs::GetSongDefFromFullSong(itemName); + if (sdef != nullptr && SplitSongs::HasBothParts(sdef->id)) { + return true; + } + } + return CheckQuestItem(qiIt->second); + } // Dungeon Rewards case RG_KOKIRI_EMERALD: case RG_GORON_RUBY: @@ -293,6 +311,22 @@ bool Logic::HasItem(RandomizerGet itemName) { default: break; } + if (SplitSongs::IsSongPart(itemName)) { + const SplitSongDef* def = SplitSongs::GetSongDefFromPart(itemName); + if (def == nullptr) { + return false; + } + if (HasItem(def->fullSong)) { + return true; + } + if (itemName == def->part1) { + return SplitSongs::HasPart1(def->id); + } + if (itemName == def->part2) { + return SplitSongs::HasPart2(def->id); + } + return false; + } SPDLOG_ERROR("HasItem reached `return false;`. Missing case for RandomizerGet of {}", static_cast(itemName)); assert(false); @@ -2079,9 +2113,17 @@ void Logic::ApplyItemEffect(Item& item, bool state) { } } break; case ITEMTYPE_DUNGEONREWARD: - case ITEMTYPE_SONG: - SetQuestItem(RandoGetToQuestItem.find(item.GetRandomizerGet())->second, state); - break; + case ITEMTYPE_SONG: { + RandomizerGet rg = item.GetRandomizerGet(); + if (SplitSongs::IsSongPart(rg)) { + SplitSongs::ApplyPartEffectToLogicScratch(this, rg, state); + break; + } + auto qi = RandoGetToQuestItem.find(rg); + if (qi != RandoGetToQuestItem.end()) { + SetQuestItem(qi->second, state); + } + } break; case ITEMTYPE_MAP: SetDungeonItem(DUNGEON_MAP, RandoGetToDungeonScene.find(item.GetRandomizerGet())->second, state); break; diff --git a/soh/soh/Enhancements/randomizer/option_descriptions.cpp b/soh/soh/Enhancements/randomizer/option_descriptions.cpp index 049ea8d3ee4..4392b3b0cc1 100644 --- a/soh/soh/Enhancements/randomizer/option_descriptions.cpp +++ b/soh/soh/Enhancements/randomizer/option_descriptions.cpp @@ -217,6 +217,16 @@ void Settings::CreateOptionDescriptions() { " - Gerudo Training Ground's Ice Arrows location\n" "\n" "Anywhere - Songs can appear at any location."; + mOptionDescriptions[RSK_SPLIT_OCARINA_SONGS] = + "Randomizer option next to Shuffle Songs.\n" + "\n" + "Each of the 12 ocarina songs becomes two checks (Part 1 and Part 2). Collecting both grants the full song.\n" + "\n" + "Requires Shuffle Songs: \"Anywhere\" — Song Locations / Dungeon Rewards only have 12 song slots, so split " + "parts are not generated there.\n" + "\n" + "When enabled with Anywhere, the item pool gains 12 extra song-part items; junk fill is reduced so the seed " + "still generates."; mOptionDescriptions[RSK_SHUFFLE_TOKENS] = "Shuffles Golden Skulltula Tokens into the item pool. This means " "Golden Skulltulas can contain other items as well.\n" "\n" diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index aa3a05dff3d..ff70ca0863c 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -21,6 +21,7 @@ #include "soh/OTRGlobals.h" #include #include "static_data.h" +#include "split_songs.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "trial.h" #include "settings.h" @@ -296,6 +297,9 @@ ItemObtainability Randomizer::GetItemObtainabilityFromRandomizerCheck(Randomizer } ItemObtainability Randomizer::GetItemObtainabilityFromRandomizerGet(RandomizerGet randoGet) { + if (Rando::SplitSongs::IsSongPart(randoGet)) { + return Rando::SplitSongs::GetPartObtainability(randoGet); + } if (randomizerGetToRandInf.find(randoGet) != randomizerGetToRandInf.end()) { return Flags_GetRandomizerInf(randomizerGetToRandInf.find(randoGet)->second) ? CANT_OBTAIN_ALREADY_HAVE : CAN_OBTAIN; @@ -3651,6 +3655,11 @@ extern "C" u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry) { // Gameplay stats: Update the time the item was obtained Randomizer_GameplayStats_SetTimestamp(item); + if (Rando::SplitSongs::IsSongPart(item)) { + Rando::SplitSongs::OnItemReceived(item); + return Return_Item_Entry(giEntry, RG_NONE); + } + // if it's an item that just sets a randomizerInf, set it if (randomizerGetToRandInf.find(item) != randomizerGetToRandInf.end()) { Flags_SetRandomizerInf(randomizerGetToRandInf.find(item)->second); diff --git a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerGet.h b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerGet.h index 3b7806385f6..8e9d7fff9b9 100644 --- a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerGet.h +++ b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerGet.h @@ -319,6 +319,30 @@ RANDO_ENUM_ITEM(RG_FISHING_HOLE_KEY) // Custom Items RANDO_ENUM_ITEM(RG_ROCS_FEATHER) +RANDO_ENUM_ITEM(RG_ZELDAS_LULLABY_PART1) +RANDO_ENUM_ITEM(RG_ZELDAS_LULLABY_PART2) +RANDO_ENUM_ITEM(RG_EPONAS_SONG_PART1) +RANDO_ENUM_ITEM(RG_EPONAS_SONG_PART2) +RANDO_ENUM_ITEM(RG_SARIAS_SONG_PART1) +RANDO_ENUM_ITEM(RG_SARIAS_SONG_PART2) +RANDO_ENUM_ITEM(RG_SUNS_SONG_PART1) +RANDO_ENUM_ITEM(RG_SUNS_SONG_PART2) +RANDO_ENUM_ITEM(RG_SONG_OF_TIME_PART1) +RANDO_ENUM_ITEM(RG_SONG_OF_TIME_PART2) +RANDO_ENUM_ITEM(RG_SONG_OF_STORMS_PART1) +RANDO_ENUM_ITEM(RG_SONG_OF_STORMS_PART2) +RANDO_ENUM_ITEM(RG_MINUET_OF_FOREST_PART1) +RANDO_ENUM_ITEM(RG_MINUET_OF_FOREST_PART2) +RANDO_ENUM_ITEM(RG_BOLERO_OF_FIRE_PART1) +RANDO_ENUM_ITEM(RG_BOLERO_OF_FIRE_PART2) +RANDO_ENUM_ITEM(RG_SERENADE_OF_WATER_PART1) +RANDO_ENUM_ITEM(RG_SERENADE_OF_WATER_PART2) +RANDO_ENUM_ITEM(RG_REQUIEM_OF_SPIRIT_PART1) +RANDO_ENUM_ITEM(RG_REQUIEM_OF_SPIRIT_PART2) +RANDO_ENUM_ITEM(RG_NOCTURNE_OF_SHADOW_PART1) +RANDO_ENUM_ITEM(RG_NOCTURNE_OF_SHADOW_PART2) +RANDO_ENUM_ITEM(RG_PRELUDE_OF_LIGHT_PART1) +RANDO_ENUM_ITEM(RG_PRELUDE_OF_LIGHT_PART2) // Logic Only RANDO_ENUM_ITEM(RG_STICKS) diff --git a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerInf.h b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerInf.h index d679bb22347..4bde3994ea2 100644 --- a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerInf.h +++ b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerInf.h @@ -2128,6 +2128,30 @@ RANDO_ENUM_ITEM(RAND_INF_OBTAINED_RUTOS_LETTER) RANDO_ENUM_ITEM(RAND_INF_OBTAINED_NAYRUS_LOVE) RANDO_ENUM_ITEM(RAND_INF_OBTAINED_ROCS_FEATHER) RANDO_ENUM_ITEM(RAND_INF_TALON_SENT_MALON_HOME) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_ZL_PART1) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_ZL_PART2) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_EPONA_PART1) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_EPONA_PART2) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_SARIA_PART1) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_SARIA_PART2) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_SUN_PART1) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_SUN_PART2) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_TIME_PART1) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_TIME_PART2) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_STORMS_PART1) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_STORMS_PART2) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_MINUET_PART1) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_MINUET_PART2) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_BOLERO_PART1) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_BOLERO_PART2) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_SERENADE_PART1) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_SERENADE_PART2) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_REQUIEM_PART1) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_REQUIEM_PART2) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_NOCTURNE_PART1) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_NOCTURNE_PART2) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_PRELUDE_PART1) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_PRELUDE_PART2) RANDO_ENUM_ITEM(RAND_INF_MAX) diff --git a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerSettingKey.h b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerSettingKey.h index 40ebb218de1..85085008d21 100644 --- a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerSettingKey.h +++ b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerSettingKey.h @@ -244,6 +244,7 @@ RANDO_ENUM_ITEM(RSK_SHUFFLE_SONG_FAIRIES) RANDO_ENUM_ITEM(RSK_LOCK_OVERWORLD_DOORS) RANDO_ENUM_ITEM(RSK_SHUFFLE_GRASS) RANDO_ENUM_ITEM(RSK_ROCS_FEATHER) +RANDO_ENUM_ITEM(RSK_SPLIT_OCARINA_SONGS) RANDO_ENUM_ITEM(RSK_MAX) RANDO_ENUM_END(RandomizerSettingKey) diff --git a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp index c2d6cf3be1d..1554ede7eb1 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp @@ -1,14 +1,22 @@ #include +#include #include #include +#include #include #include #include #include "randomizer_check_tracker.h" +#include "randomizer_check_objects.h" #include "randomizer_item_tracker.h" #include "randomizerTypes.h" +#include "logic.h" +#include "SeedContext.h" +#include "split_songs.h" +#include "static_data.h" +#include "soh/SohGui/ImGuiUtils.h" #include "soh/cvar_prefixes.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "soh/OTRGlobals.h" @@ -33,6 +41,7 @@ void DrawDungeonItem(ItemTrackerItem item); void DrawBottle(ItemTrackerItem item); void DrawQuest(ItemTrackerItem item); void DrawSong(ItemTrackerItem item); +void DrawSplitSongProgress(ItemTrackerItem item); int itemTrackerSectionId; @@ -58,6 +67,7 @@ static WidgetInfo jabberNutsTracking; static WidgetInfo ocarinaButtonTracking; static WidgetInfo overworldKeysTracking; static WidgetInfo fishingPoleTracking; +static WidgetInfo songPartsTracking; static WidgetInfo personalNotesWiget; static WidgetInfo hookshotIdentWidget; @@ -448,6 +458,93 @@ typedef enum { SECTION_DISPLAY_MINIMAL_SEPARATE, } ItemTrackerMinimalDisplayType; +// One icon per logical song; order matches Rando::SplitSongs::GetSongDef (split_songs.cpp kSplitSongs). +static std::vector songPartItemsTemplate; +std::vector songPartItems; + +static void EnsureSongPartItemsTemplate() { + if (songPartItemsTemplate.size() == static_cast(Rando::SplitSongId::SPLIT_SONG_MAX)) { + return; + } + songPartItemsTemplate.clear(); + for (int i = 0; i < static_cast(Rando::SplitSongId::SPLIT_SONG_MAX); i++) { + const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDef(static_cast(i)); + if (def == nullptr) { + continue; + } + const auto qiIt = Rando::Logic::RandoGetToQuestItem.find(static_cast(def->fullSong)); + if (qiIt == Rando::Logic::RandoGetToQuestItem.end()) { + continue; + } + const QuestItem questSong = static_cast(qiIt->second); + const auto sit = songMapping.find(questSong); + if (sit == songMapping.end()) { + continue; + } + const std::string& texName = sit->second.name; + const std::string& texFaded = sit->second.nameFaded; + songPartItemsTemplate.push_back( + { static_cast(def->fullSong), texName, texFaded, 0, DrawSplitSongProgress }); + } +} + +static bool ItemTrackerSongPartsSeedActive() { + if (GameInteractor::IsSaveLoaded() && IS_RANDO) { + const uint8_t shuffle = OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_SHUFFLE_SONGS); + return OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_SPLIT_OCARINA_SONGS) != 0 && + shuffle != RO_SONG_SHUFFLE_OFF && shuffle == RO_SONG_SHUFFLE_ANYWHERE; + } + const int shuffleSongs = CVarGetInteger(CVAR_RANDOMIZER_SETTING("ShuffleSongs"), RO_SONG_SHUFFLE_SONG_LOCATIONS); + return CVarGetInteger(CVAR_RANDOMIZER_SETTING("SplitOcarinaSongs"), 0) != 0 && + shuffleSongs != RO_SONG_SHUFFLE_OFF && shuffleSongs == RO_SONG_SHUFFLE_ANYWHERE; +} + +static int ItemTrackerEffectiveSongPartsDisplay() { + if (!ItemTrackerSongPartsSeedActive()) { + return SECTION_DISPLAY_HIDDEN; + } + return CVarGetInteger(CVAR_TRACKER_ITEM("DisplayType.SongParts"), SECTION_DISPLAY_HIDDEN); +} + +static std::unordered_map BuildSongPartSpoilerAreas() { + std::unordered_map partRgToArea; + if (!GameInteractor::IsSaveLoaded() || !IS_RANDO) { + return partRgToArea; + } + const auto ctx = Rando::Context::GetInstance(); + if (!ctx || (!ctx->IsSeedGenerated() && !ctx->IsSpoilerLoaded())) { + return partRgToArea; + } + for (RandomizerCheck rc : ctx->allLocations) { + auto* loc = ctx->GetItemLocation(rc); + if (loc == nullptr) { + continue; + } + const RandomizerGet rg = loc->GetPlacedRandomizerGet(); + if (!Rando::SplitSongs::IsSongPart(rg)) { + continue; + } + if (partRgToArea.find(rg) != partRgToArea.end()) { + continue; + } + auto* staticLoc = Rando::StaticData::GetLocation(rc); + if (staticLoc != nullptr) { + partRgToArea[rg] = staticLoc->GetArea(); + } + } + std::unordered_map fullSongToArea; + for (const auto& kv : partRgToArea) { + const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromPart(kv.first); + if (def == nullptr) { + continue; + } + if (fullSongToArea.find(def->fullSong) == fullSongToArea.end()) { + fullSongToArea[def->fullSong] = kv.second; + } + } + return fullSongToArea; +} + struct ItemTrackerNumbers { int currentCapacity; int maxCapacity; @@ -1360,6 +1457,66 @@ void DrawSong(ItemTrackerItem item) { Tooltip(SohUtils::GetQuestItemName(item.id).c_str()); } +void DrawSplitSongProgress(ItemTrackerItem item) { + const RandomizerGet fullSongRg = static_cast(item.id); + const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromFullSong(fullSongRg); + int partsCollected = 0; + if (def != nullptr) { + const bool p1 = Rando::SplitSongs::HasPart1(def->id); + const bool p2 = Rando::SplitSongs::HasPart2(def->id); + if (Rando::SplitSongs::HasFullSong(def->id)) { + partsCollected = 2; + } else { + partsCollected = (p1 ? 1 : 0) + (p2 ? 1 : 0); + } + } + + const bool hasAnyPart = partsCollected > 0; + float iconSize = static_cast(CVarGetInteger(CVAR_TRACKER_ITEM("IconSize"), 36)); + ImGui::BeginGroup(); + ImVec2 p = ImGui::GetCursorScreenPos(); + ImGui::SetCursorScreenPos(ImVec2(p.x + 6, p.y)); + ImGui::Image(Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName( + hasAnyPart && IsValidSaveFile() ? item.name : item.nameFaded), + ImVec2(iconSize / 1.5f, iconSize), ImVec2(0, 0), ImVec2(1, 1)); + + const RandomizerCheckArea area = static_cast(item.data); + ImU32 labelColor = IM_COL32(255, 255, 255, 255); + if (area != RCAREA_INVALID && GameInteractor::IsSaveLoaded() && IS_RANDO && + CheckTracker::IsAreaSpoiled(area)) { + labelColor = IM_COL32(255, 255, 160, 255); + } + + char progressLabel[8]; + const bool showProgress = def != nullptr && partsCollected > 0; + if (showProgress) { + std::snprintf(progressLabel, sizeof(progressLabel), "%d/2", partsCollected); + } + const ImVec2 iconMin = ImGui::GetItemRectMin(); + const ImVec2 iconMax = ImGui::GetItemRectMax(); + if (showProgress) { + const ImVec2 textSize = ImGui::CalcTextSize(progressLabel); + const ImVec2 textPos(iconMin.x + ((iconMax.x - iconMin.x) - textSize.x) * 0.5f, iconMax.y - textSize.y - 2.0f); + ImDrawList* dl = ImGui::GetWindowDrawList(); + dl->AddText(ImVec2(textPos.x + 1.0f, textPos.y + 1.0f), IM_COL32(0, 0, 0, 220), progressLabel); + dl->AddText(textPos, labelColor, progressLabel); + } + + ImGui::EndGroup(); + + std::string tip; + if (def != nullptr) { + tip = Rando::StaticData::RetrieveItem(fullSongRg).GetName().GetEnglish(); + if (area != RCAREA_INVALID && IS_RANDO) { + tip += "\n"; + tip += RandomizerCheckObjects::GetRCAreaName(area); + } + } + if (!tip.empty()) { + Tooltip(tip.c_str()); + } +} + void DrawNotes(bool resizeable = false) { ImGui::BeginGroup(); float iconSize = static_cast(CVarGetInteger(CVAR_TRACKER_ITEM("IconSize"), 36)); @@ -1582,6 +1739,21 @@ void UpdateVectors() { } } + songPartItems.clear(); + if (ItemTrackerSongPartsSeedActive()) { + EnsureSongPartItemsTemplate(); + const auto partAreas = BuildSongPartSpoilerAreas(); + songPartItems = songPartItemsTemplate; + for (auto& it : songPartItems) { + const RandomizerGet rg = static_cast(it.id); + const auto found = partAreas.find(rg); + it.data = found != partAreas.end() ? static_cast(found->second) + : static_cast(RCAREA_INVALID); + } + } + + const int songPartsDisplay = ItemTrackerEffectiveSongPartsDisplay(); + mainWindowItems.clear(); if (CVarGetInteger(CVAR_TRACKER_ITEM("DisplayType.Inventory"), SECTION_DISPLAY_MAIN_WINDOW) == SECTION_DISPLAY_MAIN_WINDOW) { @@ -1610,7 +1782,18 @@ void UpdateVectors() { mainWindowItems.push_back(ITEM_TRACKER_ITEM(ITEM_NONE, 0, DrawItem)); mainWindowItems.push_back(ITEM_TRACKER_ITEM(ITEM_NONE, 0, DrawItem)); } - mainWindowItems.insert(mainWindowItems.end(), songItems.begin(), songItems.end()); + const bool splitSongPartsReplaceSongsOnMain = ItemTrackerSongPartsSeedActive() && + songPartsDisplay == SECTION_DISPLAY_MAIN_WINDOW && + !songPartItems.empty(); + if (!splitSongPartsReplaceSongsOnMain) { + mainWindowItems.insert(mainWindowItems.end(), songItems.begin(), songItems.end()); + } + } + if (songPartsDisplay == SECTION_DISPLAY_MAIN_WINDOW && !songPartItems.empty()) { + while (mainWindowItems.size() % 6) { + mainWindowItems.push_back(ITEM_TRACKER_ITEM(ITEM_NONE, 0, DrawItem)); + } + mainWindowItems.insert(mainWindowItems.end(), songPartItems.begin(), songPartItems.end()); } if (CVarGetInteger(CVAR_TRACKER_ITEM("DisplayType.DungeonItems"), SECTION_DISPLAY_HIDDEN) == SECTION_DISPLAY_MAIN_WINDOW) { @@ -1810,6 +1993,8 @@ void ItemTrackerWindow::Draw() { void ItemTrackerWindow::DrawElement() { UpdateVectors(); + const int songPartsDisplayDraw = ItemTrackerEffectiveSongPartsDisplay(); + int iconSize = CVarGetInteger(CVAR_TRACKER_ITEM("IconSize"), 36); int iconSpacing = CVarGetInteger(CVAR_TRACKER_ITEM("IconSpacing"), 12); int comboButton1Mask = buttonMap[CVarGetInteger(CVAR_TRACKER_ITEM("ComboButton1"), TRACKER_COMBO_BUTTON_L)]; @@ -1836,6 +2021,7 @@ void ItemTrackerWindow::DrawElement() { SECTION_DISPLAY_MAIN_WINDOW) || (CVarGetInteger(CVAR_TRACKER_ITEM("DisplayType.Songs"), SECTION_DISPLAY_MAIN_WINDOW) == SECTION_DISPLAY_MAIN_WINDOW) || + (songPartsDisplayDraw == SECTION_DISPLAY_MAIN_WINDOW) || (CVarGetInteger(CVAR_TRACKER_ITEM("DisplayType.DungeonItems"), SECTION_DISPLAY_HIDDEN) == SECTION_DISPLAY_MAIN_WINDOW) || (CVarGetInteger(CVAR_TRACKER_ITEM("DisplayType.Greg"), SECTION_DISPLAY_EXTENDED_HIDDEN) == @@ -1900,6 +2086,12 @@ void ItemTrackerWindow::DrawElement() { EndFloatingWindows(); } + if (songPartsDisplayDraw == SECTION_DISPLAY_SEPARATE && !songPartItems.empty()) { + BeginFloatingWindows("Song Parts Tracker"); + DrawItemsInRows(songPartItems, 6); + EndFloatingWindows(); + } + if (CVarGetInteger(CVAR_TRACKER_ITEM("DisplayType.DungeonItems"), SECTION_DISPLAY_HIDDEN) == SECTION_DISPLAY_SEPARATE) { BeginFloatingWindows("Dungeon Items Tracker"); @@ -2171,6 +2363,7 @@ void ItemTrackerSettingsWindow::DrawElement() { SohGui::mSohMenu->MenuDrawItem(ocarinaButtonTracking, 250, THEME_COLOR); SohGui::mSohMenu->MenuDrawItem(overworldKeysTracking, 250, THEME_COLOR); SohGui::mSohMenu->MenuDrawItem(fishingPoleTracking, 250, THEME_COLOR); + SohGui::mSohMenu->MenuDrawItem(songPartsTracking, 250, THEME_COLOR); if (CVarCombobox("Total Checks", CVAR_TRACKER_ITEM("TotalChecks.DisplayType"), minimalDisplayTypes, ComboboxOptions() @@ -2367,6 +2560,18 @@ void RegisterItemTrackerWidgets() { SohGui::mSohMenu->AddSearchWidget( { fishingPoleTracking, "Randomizer", "Item Tracker", "General Settings", "icon" }); + songPartsTracking = { .name = "Song parts (split)", .type = WidgetType::WIDGET_CVAR_COMBOBOX }; + songPartsTracking.CVar(CVAR_TRACKER_ITEM("DisplayType.SongParts")) + .Options(ComboboxOptions() + .DefaultIndex(SECTION_DISPLAY_HIDDEN) + .ComponentAlignment(ComponentAlignments::Right) + .LabelPosition(LabelPositions::Far) + .Color(THEME_COLOR) + .ComboMap(displayTypes)) + .Callback([](WidgetInfo& info) { shouldUpdateVectors = true; }); + SohGui::mSohMenu->AddSearchWidget( + { songPartsTracking, "Randomizer", "Item Tracker", "General Settings", "split ocarina" }); + personalNotesWiget = { .name = "Personal notes", .type = WidgetType::WIDGET_CVAR_COMBOBOX }; static const char* notesDisabledTooltip = "Disabled because tracker is set to floating and display combo is enabled."; diff --git a/soh/soh/Enhancements/randomizer/settings.cpp b/soh/soh/Enhancements/randomizer/settings.cpp index bfa0d2ad125..07f4b08259a 100644 --- a/soh/soh/Enhancements/randomizer/settings.cpp +++ b/soh/soh/Enhancements/randomizer/settings.cpp @@ -603,6 +603,22 @@ void Settings::CreateOptions() { }); OPT_U8(RSK_LINKS_POCKET_REWARD, "Link's Pocket Reward Type", {"Dungeon Reward", "Stone", "Medallion"}, OptionCategory::Setting, CVAR_RANDOMIZER_SETTING("LinksPocketReward"), "", WIDGET_CVAR_COMBOBOX, RO_LINKS_POCKET_REWARD); OPT_U8(RSK_SHUFFLE_SONGS, "Shuffle Songs", {"Off", "Song Locations", "Dungeon Rewards", "Anywhere"}, OptionCategory::Setting, CVAR_RANDOMIZER_SETTING("ShuffleSongs"), mOptionDescriptions[RSK_SHUFFLE_SONGS], WIDGET_CVAR_COMBOBOX, RO_SONG_SHUFFLE_SONG_LOCATIONS); + OPT_CALLBACK(RSK_SHUFFLE_SONGS, { + const int shuffleSongs = + CVarGetInteger(CVAR_RANDOMIZER_SETTING("ShuffleSongs"), RO_SONG_SHUFFLE_SONG_LOCATIONS); + if (shuffleSongs == RO_SONG_SHUFFLE_ANYWHERE) { + mOptions[RSK_SPLIT_OCARINA_SONGS].Enable(); + } else if (shuffleSongs != RO_SONG_SHUFFLE_OFF) { + mOptions[RSK_SPLIT_OCARINA_SONGS].Disable( + "Split Ocarina Songs is only supported when Shuffle Songs is \"Anywhere\" (other modes keep 12 song " + "checks, not 24 parts)."); + } else { + mOptions[RSK_SPLIT_OCARINA_SONGS].Disable( + "Turn on Shuffle Songs to use Split Ocarina Songs (then set Shuffle Songs to \"Anywhere\")."); + } + }); + OPT_BOOL(RSK_SPLIT_OCARINA_SONGS, "Split Ocarina Songs", CVAR_RANDOMIZER_SETTING("SplitOcarinaSongs"), + mOptionDescriptions[RSK_SPLIT_OCARINA_SONGS]); OPT_U8(RSK_SHOPSANITY, "Shop Shuffle", {"Off", "Specific Count", "Random"}, OptionCategory::Setting, CVAR_RANDOMIZER_SETTING("Shopsanity"), mOptionDescriptions[RSK_SHOPSANITY], WIDGET_CVAR_COMBOBOX, RO_SHOPSANITY_OFF); OPT_CALLBACK(RSK_SHOPSANITY, { // Hide shopsanity prices if shopsanity is off or zero @@ -2432,6 +2448,7 @@ void Settings::CreateOptions() { OptionGroup::SubGroup("Shuffle Items", { &mOptions[RSK_SHUFFLE_SONGS], + &mOptions[RSK_SPLIT_OCARINA_SONGS], &mOptions[RSK_SHUFFLE_TOKENS], &mOptions[RSK_SHUFFLE_KOKIRI_SWORD], &mOptions[RSK_SHUFFLE_MASTER_SWORD], @@ -2694,6 +2711,7 @@ void Settings::CreateOptions() { &mOptions[RSK_SHUFFLE_DUNGEON_REWARDS], &mOptions[RSK_LINKS_POCKET], &mOptions[RSK_SHUFFLE_SONGS], + &mOptions[RSK_SPLIT_OCARINA_SONGS], &mOptions[RSK_SHOPSANITY], &mOptions[RSK_SHOPSANITY_COUNT], &mOptions[RSK_SHOPSANITY_PRICES], diff --git a/soh/soh/Enhancements/randomizer/split_songs.cpp b/soh/soh/Enhancements/randomizer/split_songs.cpp new file mode 100644 index 00000000000..a99ba3dd0fc --- /dev/null +++ b/soh/soh/Enhancements/randomizer/split_songs.cpp @@ -0,0 +1,327 @@ +// split_songs.cpp — Split Ocarina Songs (randomizer) +// Copyright (c) 2026 Raccooncloud. All rights reserved. Created by Raccooncloud. + +#include "split_songs.h" + +#include "SeedContext.h" +#include "logic.h" +#include "static_data.h" + +#include "functions.h" +#include "global.h" +#include "macros.h" +#include "z64player.h" +#include "variables.h" + +#include + +namespace Rando { + +static constexpr uint32_t kSplitSongPart1Flags[SPLIT_SONG_MAX] = { + RAND_INF_SPLIT_ZL_PART1, + RAND_INF_SPLIT_EPONA_PART1, + RAND_INF_SPLIT_SARIA_PART1, + RAND_INF_SPLIT_SUN_PART1, + RAND_INF_SPLIT_TIME_PART1, + RAND_INF_SPLIT_STORMS_PART1, + RAND_INF_SPLIT_MINUET_PART1, + RAND_INF_SPLIT_BOLERO_PART1, + RAND_INF_SPLIT_SERENADE_PART1, + RAND_INF_SPLIT_REQUIEM_PART1, + RAND_INF_SPLIT_NOCTURNE_PART1, + RAND_INF_SPLIT_PRELUDE_PART1, +}; + +static constexpr uint32_t kSplitSongPart2Flags[SPLIT_SONG_MAX] = { + RAND_INF_SPLIT_ZL_PART2, + RAND_INF_SPLIT_EPONA_PART2, + RAND_INF_SPLIT_SARIA_PART2, + RAND_INF_SPLIT_SUN_PART2, + RAND_INF_SPLIT_TIME_PART2, + RAND_INF_SPLIT_STORMS_PART2, + RAND_INF_SPLIT_MINUET_PART2, + RAND_INF_SPLIT_BOLERO_PART2, + RAND_INF_SPLIT_SERENADE_PART2, + RAND_INF_SPLIT_REQUIEM_PART2, + RAND_INF_SPLIT_NOCTURNE_PART2, + RAND_INF_SPLIT_PRELUDE_PART2, +}; + +static constexpr SplitSongDef kSplitSongs[SPLIT_SONG_MAX] = { + { SPLIT_SONG_ZELDAS_LULLABY, RG_ZELDAS_LULLABY_PART1, RG_ZELDAS_LULLABY_PART2, RG_ZELDAS_LULLABY }, + { SPLIT_SONG_EPONAS_SONG, RG_EPONAS_SONG_PART1, RG_EPONAS_SONG_PART2, RG_EPONAS_SONG }, + { SPLIT_SONG_SARIAS_SONG, RG_SARIAS_SONG_PART1, RG_SARIAS_SONG_PART2, RG_SARIAS_SONG }, + { SPLIT_SONG_SUNS_SONG, RG_SUNS_SONG_PART1, RG_SUNS_SONG_PART2, RG_SUNS_SONG }, + { SPLIT_SONG_SONG_OF_TIME, RG_SONG_OF_TIME_PART1, RG_SONG_OF_TIME_PART2, RG_SONG_OF_TIME }, + { SPLIT_SONG_SONG_OF_STORMS, RG_SONG_OF_STORMS_PART1, RG_SONG_OF_STORMS_PART2, RG_SONG_OF_STORMS }, + { SPLIT_SONG_MINUET_OF_FOREST, RG_MINUET_OF_FOREST_PART1, RG_MINUET_OF_FOREST_PART2, RG_MINUET_OF_FOREST }, + { SPLIT_SONG_BOLERO_OF_FIRE, RG_BOLERO_OF_FIRE_PART1, RG_BOLERO_OF_FIRE_PART2, RG_BOLERO_OF_FIRE }, + { SPLIT_SONG_SERENADE_OF_WATER, RG_SERENADE_OF_WATER_PART1, RG_SERENADE_OF_WATER_PART2, RG_SERENADE_OF_WATER }, + { SPLIT_SONG_REQUIEM_OF_SPIRIT, RG_REQUIEM_OF_SPIRIT_PART1, RG_REQUIEM_OF_SPIRIT_PART2, RG_REQUIEM_OF_SPIRIT }, + { SPLIT_SONG_NOCTURNE_OF_SHADOW, RG_NOCTURNE_OF_SHADOW_PART1, RG_NOCTURNE_OF_SHADOW_PART2, RG_NOCTURNE_OF_SHADOW }, + { SPLIT_SONG_PRELUDE_OF_LIGHT, RG_PRELUDE_OF_LIGHT_PART1, RG_PRELUDE_OF_LIGHT_PART2, RG_PRELUDE_OF_LIGHT }, +}; + +static bool IsValidSplitSongId(SplitSongId id) { + return id >= 0 && id < SPLIT_SONG_MAX; +} + +static std::vector sPendingFullSongGrants; +static bool sSongGrantQueued[SPLIT_SONG_MAX] = {}; + +/** Granting the quest song updates save while get-item / blocking CS is active; defer to next safe player update. */ +static bool ShouldDeferFullSongQuestGrant() { + if (gPlayState == nullptr) { + return false; + } + Player* player = GET_PLAYER(gPlayState); + if (player == nullptr) { + return false; + } + return Player_InBlockingCsMode(gPlayState, player) || (player->stateFlags1 & PLAYER_STATE1_IN_ITEM_CS) || + (player->stateFlags1 & PLAYER_STATE1_GETTING_ITEM) || (player->stateFlags1 & PLAYER_STATE1_CARRYING_ACTOR); +} + +bool SplitSongs::IsSongPart(RandomizerGet rg) { + return GetSongDefFromPart(rg) != nullptr; +} + +SplitSongId SplitSongs::GetSongIdFromPart(RandomizerGet rg) { + const SplitSongDef* def = GetSongDefFromPart(rg); + return def != nullptr ? def->id : SPLIT_SONG_MAX; +} + +const SplitSongDef* SplitSongs::GetSongDef(SplitSongId id) { + if (!IsValidSplitSongId(id)) { + return nullptr; + } + return &kSplitSongs[id]; +} + +const SplitSongDef* SplitSongs::GetSongDefFromPart(RandomizerGet rg) { + for (const auto& def : kSplitSongs) { + if (def.part1 == rg || def.part2 == rg) { + return &def; + } + } + return nullptr; +} + +const SplitSongDef* SplitSongs::GetSongDefFromFullSong(RandomizerGet fullSongRg) { + for (const auto& def : kSplitSongs) { + if (def.fullSong == fullSongRg) { + return &def; + } + } + return nullptr; +} + +static bool UsingLogicSimulationBuffer(Logic* logic) { + return logic != nullptr && logic->mSaveContext != nullptr && logic->mSaveContext != &gSaveContext; +} + +bool SplitSongs::HasPart1(SplitSongId id) { + if (!IsValidSplitSongId(id)) { + return false; + } + const RandomizerInf flag = static_cast(kSplitSongPart1Flags[id]); + auto* logic = Context::GetInstance()->GetLogic().get(); + if (UsingLogicSimulationBuffer(logic)) { + return logic->CheckRandoInf(flag); + } + return Flags_GetRandomizerInf(flag) != 0; +} + +bool SplitSongs::HasPart2(SplitSongId id) { + if (!IsValidSplitSongId(id)) { + return false; + } + const RandomizerInf flag = static_cast(kSplitSongPart2Flags[id]); + auto* logic = Context::GetInstance()->GetLogic().get(); + if (UsingLogicSimulationBuffer(logic)) { + return logic->CheckRandoInf(flag); + } + return Flags_GetRandomizerInf(flag) != 0; +} + +bool SplitSongs::HasBothParts(SplitSongId id) { + return HasPart1(id) && HasPart2(id); +} + +void SplitSongs::SetPart1(SplitSongId id, bool state) { + if (!IsValidSplitSongId(id)) { + return; + } + RandomizerInf flag = static_cast(kSplitSongPart1Flags[id]); + if (state) { + Flags_SetRandomizerInf(flag); + } else { + Flags_UnsetRandomizerInf(flag); + } +} + +void SplitSongs::SetPart2(SplitSongId id, bool state) { + if (!IsValidSplitSongId(id)) { + return; + } + RandomizerInf flag = static_cast(kSplitSongPart2Flags[id]); + if (state) { + Flags_SetRandomizerInf(flag); + } else { + Flags_UnsetRandomizerInf(flag); + } +} + +bool SplitSongs::HasFullSong(SplitSongId id) { + const SplitSongDef* def = GetSongDef(id); + if (def == nullptr) { + return false; + } + const auto qiIt = Logic::RandoGetToQuestItem.find(static_cast(def->fullSong)); + if (qiIt == Logic::RandoGetToQuestItem.end()) { + return false; + } + auto* logic = Context::GetInstance()->GetLogic().get(); + if (UsingLogicSimulationBuffer(logic)) { + return logic->CheckQuestItem(qiIt->second); + } + return CHECK_QUEST_ITEM(qiIt->second); +} + +void SplitSongs::GrantFullSong(SplitSongId id) { + const SplitSongDef* def = GetSongDef(id); + if (def == nullptr) { + return; + } + + auto logic = Context::GetInstance()->GetLogic(); + logic->SetSaveContext(&gSaveContext); + const auto qiIt = Logic::RandoGetToQuestItem.find(static_cast(def->fullSong)); + if (qiIt != Logic::RandoGetToQuestItem.end() && CHECK_QUEST_ITEM(qiIt->second)) { + return; + } + + auto& fullSongItem = StaticData::RetrieveItem(def->fullSong); + logic->ApplyItemEffect(fullSongItem, true); +} + +void SplitSongs::TryCompleteSong(SplitSongId id) { + if (!HasBothParts(id) || HasFullSong(id)) { + return; + } + if (gPlayState == nullptr) { + GrantFullSong(id); + return; + } + if (!IsValidSplitSongId(id) || sSongGrantQueued[id]) { + return; + } + sSongGrantQueued[id] = true; + sPendingFullSongGrants.push_back(id); +} + +void SplitSongs::ProcessPendingFullSongGrants() { + if (sPendingFullSongGrants.empty() || gPlayState == nullptr) { + return; + } + Player* player = GET_PLAYER(gPlayState); + if (player == nullptr || ShouldDeferFullSongQuestGrant()) { + return; + } + + std::vector batch = std::move(sPendingFullSongGrants); + sPendingFullSongGrants.clear(); + + for (SplitSongId id : batch) { + if (IsValidSplitSongId(id)) { + sSongGrantQueued[id] = false; + } + } + for (SplitSongId id : batch) { + if (!IsValidSplitSongId(id)) { + continue; + } + if (HasBothParts(id) && !HasFullSong(id)) { + GrantFullSong(id); + } + } +} + +void SplitSongs::ClearPendingFullSongGrants() { + sPendingFullSongGrants.clear(); + for (size_t i = 0; i < SPLIT_SONG_MAX; i++) { + sSongGrantQueued[i] = false; + } +} + +void SplitSongs::OnItemReceived(RandomizerGet rg) { + const SplitSongDef* def = GetSongDefFromPart(rg); + if (def == nullptr) { + return; + } + + if (rg == def->part1) { + SetPart1(def->id, true); + } else if (rg == def->part2) { + SetPart2(def->id, true); + } + + TryCompleteSong(def->id); +} + +ItemObtainability SplitSongs::GetPartObtainability(RandomizerGet partRg) { + const SplitSongDef* def = GetSongDefFromPart(partRg); + if (def == nullptr) { + return CAN_OBTAIN; + } + const auto qiIt = Logic::RandoGetToQuestItem.find(static_cast(def->fullSong)); + if (qiIt != Logic::RandoGetToQuestItem.end() && CHECK_QUEST_ITEM(qiIt->second)) { + return CANT_OBTAIN_ALREADY_HAVE; + } + if (partRg == def->part1 && HasPart1(def->id)) { + return CANT_OBTAIN_ALREADY_HAVE; + } + if (partRg == def->part2 && HasPart2(def->id)) { + return CANT_OBTAIN_ALREADY_HAVE; + } + return CAN_OBTAIN; +} + +void SplitSongs::AppendShuffledSongPoolItems(std::vector& pool, bool split) { + for (const auto& def : kSplitSongs) { + if (split) { + pool.push_back(def.part1); + pool.push_back(def.part2); + } else { + pool.push_back(def.fullSong); + } + } +} + +void SplitSongs::AppendSongIceTrapModels(std::vector& models, bool split) { + AppendShuffledSongPoolItems(models, split); +} + +void SplitSongs::DebugGiveAllSongParts(PlayState* play) { + if (play == nullptr) { + return; + } + for (const auto& def : kSplitSongs) { + Randomizer_Item_Give(play, StaticData::RetrieveItem(def.part1).GetGIEntry_Copy()); + Randomizer_Item_Give(play, StaticData::RetrieveItem(def.part2).GetGIEntry_Copy()); + } +} + +void SplitSongs::ApplyPartEffectToLogicScratch(Logic* logic, RandomizerGet rg, bool state) { + if (!UsingLogicSimulationBuffer(logic)) { + return; + } + const SplitSongDef* def = GetSongDefFromPart(rg); + if (def == nullptr) { + return; + } + const uint32_t flag = (rg == def->part1) ? kSplitSongPart1Flags[def->id] : kSplitSongPart2Flags[def->id]; + logic->SetRandoInf(flag, state); +} + +} // namespace Rando diff --git a/soh/soh/Enhancements/randomizer/split_songs.h b/soh/soh/Enhancements/randomizer/split_songs.h new file mode 100644 index 00000000000..28cc7bff158 --- /dev/null +++ b/soh/soh/Enhancements/randomizer/split_songs.h @@ -0,0 +1,82 @@ +// split_songs.h — Split Ocarina Songs (randomizer) +// Copyright (c) 2026 Raccooncloud. All rights reserved. Created by Raccooncloud. + +#pragma once + +#include "randomizerTypes.h" +#include + +struct PlayState; + +namespace Rando { + +class Logic; + +enum SplitSongId { + SPLIT_SONG_ZELDAS_LULLABY = 0, + SPLIT_SONG_EPONAS_SONG, + SPLIT_SONG_SARIAS_SONG, + SPLIT_SONG_SUNS_SONG, + SPLIT_SONG_SONG_OF_TIME, + SPLIT_SONG_SONG_OF_STORMS, + SPLIT_SONG_MINUET_OF_FOREST, + SPLIT_SONG_BOLERO_OF_FIRE, + SPLIT_SONG_SERENADE_OF_WATER, + SPLIT_SONG_REQUIEM_OF_SPIRIT, + SPLIT_SONG_NOCTURNE_OF_SHADOW, + SPLIT_SONG_PRELUDE_OF_LIGHT, + SPLIT_SONG_MAX +}; + +struct SplitSongDef { + SplitSongId id; + RandomizerGet part1; + RandomizerGet part2; + RandomizerGet fullSong; +}; + +class SplitSongs { + public: + static bool IsSongPart(RandomizerGet rg); + static SplitSongId GetSongIdFromPart(RandomizerGet rg); + static const SplitSongDef* GetSongDef(SplitSongId id); + static const SplitSongDef* GetSongDefFromPart(RandomizerGet rg); + /** Full-song RandomizerGet (RG_ZELDAS_LULLABY, …), not part items. */ + static const SplitSongDef* GetSongDefFromFullSong(RandomizerGet fullSongRg); + + static bool HasPart1(SplitSongId id); + static bool HasPart2(SplitSongId id); + static bool HasBothParts(SplitSongId id); + + static void SetPart1(SplitSongId id, bool state); + static void SetPart2(SplitSongId id, bool state); + + static bool HasFullSong(SplitSongId id); + static void GrantFullSong(SplitSongId id); + static void TryCompleteSong(SplitSongId id); + + /** After get-item / cutscene-safe: applies deferred quest song when both parts were obtained mid get-item. */ + static void ProcessPendingFullSongGrants(); + /** Reset deferred grants (e.g. save load). */ + static void ClearPendingFullSongGrants(); + + static void OnItemReceived(RandomizerGet rg); + + // Item pool / ice-trap models: 12 full songs vs 24 part items (same 12 logical songs). + static void AppendShuffledSongPoolItems(std::vector& pool, bool split); + static void AppendSongIceTrapModels(std::vector& models, bool split); + + /** In-world obtainability for RG_*_PART1/PART2 (duplicate parts / song already complete). */ + static ItemObtainability GetPartObtainability(RandomizerGet partRg); + + /** Dev/testing: grant every song via Part 1 + Part 2 through the same path as chests/NPCs (Randomizer_Item_Give). */ + static void DebugGiveAllSongParts(PlayState* play); + + /** + * During 3drando fill, Logic uses a scratch SaveContext. Song-part flags must live there (not gSaveContext). + * No-op when Logic is tied to the real gSaveContext (in-game). + */ + static void ApplyPartEffectToLogicScratch(Logic* logic, RandomizerGet rg, bool state); +}; + +} // namespace Rando From 5feddec3c570b9c8ccf7616486f02748eade619b Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:44:52 +0100 Subject: [PATCH 02/22] iUpdate split songs file headers with creator attribution. Replaces the previous copyright wording in split_songs.h and split_songs.cpp with a simple Created by RaccoonCloud header line. --- soh/soh/Enhancements/randomizer/split_songs.cpp | 2 +- soh/soh/Enhancements/randomizer/split_songs.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/split_songs.cpp b/soh/soh/Enhancements/randomizer/split_songs.cpp index a99ba3dd0fc..403c0918933 100644 --- a/soh/soh/Enhancements/randomizer/split_songs.cpp +++ b/soh/soh/Enhancements/randomizer/split_songs.cpp @@ -1,5 +1,5 @@ // split_songs.cpp — Split Ocarina Songs (randomizer) -// Copyright (c) 2026 Raccooncloud. All rights reserved. Created by Raccooncloud. +// Created by RaccoonCloud. #include "split_songs.h" diff --git a/soh/soh/Enhancements/randomizer/split_songs.h b/soh/soh/Enhancements/randomizer/split_songs.h index 28cc7bff158..02125a8554d 100644 --- a/soh/soh/Enhancements/randomizer/split_songs.h +++ b/soh/soh/Enhancements/randomizer/split_songs.h @@ -1,5 +1,5 @@ // split_songs.h — Split Ocarina Songs (randomizer) -// Copyright (c) 2026 Raccooncloud. All rights reserved. Created by Raccooncloud. +// Created by RaccoonCloud. #pragma once From 541777495657efbe2d6420896eff608db9e5ba03 Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Fri, 24 Apr 2026 17:34:14 +0100 Subject: [PATCH 03/22] Apply clang-format to split songs changes --- soh/soh/Enhancements/debugconsole.cpp | 8 ++--- .../randomizer/Messages/ItemMessages.cpp | 6 ++-- .../randomizer/randomizer_item_tracker.cpp | 7 ++-- .../Enhancements/randomizer/split_songs.cpp | 32 +++++-------------- soh/soh/Enhancements/randomizer/split_songs.h | 3 +- 5 files changed, 20 insertions(+), 36 deletions(-) diff --git a/soh/soh/Enhancements/debugconsole.cpp b/soh/soh/Enhancements/debugconsole.cpp index b6c8eaa4800..f6252337afb 100644 --- a/soh/soh/Enhancements/debugconsole.cpp +++ b/soh/soh/Enhancements/debugconsole.cpp @@ -1622,10 +1622,10 @@ void DebugConsole_Init(void) { } }); CMD_REGISTER("rando_give_all_split_song_parts", - { RandoGiveAllSplitSongPartsHandler, - "Randomizer: grants all 12 ocarina songs via both split parts (tests Randomizer_Item_Give + " - "completion). Rando save only.", - {} }); + { RandoGiveAllSplitSongPartsHandler, + "Randomizer: grants all 12 ocarina songs via both split parts (tests Randomizer_Item_Give + " + "completion). Rando save only.", + {} }); CMD_REGISTER("item", { ItemHandler, "Sets item ID in arg 1 into slot arg 2. No boundary checks. Use with caution.", diff --git a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp index 85b9a9ca76e..eaa38a2d30f 100644 --- a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp @@ -124,10 +124,10 @@ void BuildCustomItemMessage(Player* player, CustomMessage& msg) { const RandomizerGet partRg = static_cast(rgid); const auto& songPartItem = Rando::StaticData::RetrieveItem(partRg); const auto* splitDef = Rando::SplitSongs::GetSongDefFromPart(partRg); - const ItemID iconItemId = - splitDef != nullptr ? VanillaItemIdForFullSong(splitDef->fullSong) : ITEM_NONE; + const ItemID iconItemId = splitDef != nullptr ? VanillaItemIdForFullSong(splitDef->fullSong) : ITEM_NONE; const auto& nm = songPartItem.GetName(); - CustomMessage getItemText(nm.GetEnglish(), nm.GetGerman(), nm.GetFrench(), TEXTBOX_TYPE_BLUE, TEXTBOX_POS_BOTTOM); + CustomMessage getItemText(nm.GetEnglish(), nm.GetGerman(), nm.GetFrench(), TEXTBOX_TYPE_BLUE, + TEXTBOX_POS_BOTTOM); getItemText.Format(iconItemId); msg = getItemText; return; diff --git a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp index 1b21170324b..80b33b2e8dd 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp @@ -1495,8 +1495,7 @@ void DrawSplitSongProgress(ItemTrackerItem item) { const RandomizerCheckArea area = static_cast(item.data); ImU32 labelColor = IM_COL32(255, 255, 255, 255); - if (area != RCAREA_INVALID && GameInteractor::IsSaveLoaded() && IS_RANDO && - CheckTracker::IsAreaSpoiled(area)) { + if (area != RCAREA_INVALID && GameInteractor::IsSaveLoaded() && IS_RANDO && CheckTracker::IsAreaSpoiled(area)) { labelColor = IM_COL32(255, 255, 160, 255); } @@ -1760,8 +1759,8 @@ void UpdateVectors() { for (auto& it : songPartItems) { const RandomizerGet rg = static_cast(it.id); const auto found = partAreas.find(rg); - it.data = found != partAreas.end() ? static_cast(found->second) - : static_cast(RCAREA_INVALID); + it.data = + found != partAreas.end() ? static_cast(found->second) : static_cast(RCAREA_INVALID); } } diff --git a/soh/soh/Enhancements/randomizer/split_songs.cpp b/soh/soh/Enhancements/randomizer/split_songs.cpp index 403c0918933..bf083b2a30b 100644 --- a/soh/soh/Enhancements/randomizer/split_songs.cpp +++ b/soh/soh/Enhancements/randomizer/split_songs.cpp @@ -18,33 +18,17 @@ namespace Rando { static constexpr uint32_t kSplitSongPart1Flags[SPLIT_SONG_MAX] = { - RAND_INF_SPLIT_ZL_PART1, - RAND_INF_SPLIT_EPONA_PART1, - RAND_INF_SPLIT_SARIA_PART1, - RAND_INF_SPLIT_SUN_PART1, - RAND_INF_SPLIT_TIME_PART1, - RAND_INF_SPLIT_STORMS_PART1, - RAND_INF_SPLIT_MINUET_PART1, - RAND_INF_SPLIT_BOLERO_PART1, - RAND_INF_SPLIT_SERENADE_PART1, - RAND_INF_SPLIT_REQUIEM_PART1, - RAND_INF_SPLIT_NOCTURNE_PART1, - RAND_INF_SPLIT_PRELUDE_PART1, + RAND_INF_SPLIT_ZL_PART1, RAND_INF_SPLIT_EPONA_PART1, RAND_INF_SPLIT_SARIA_PART1, + RAND_INF_SPLIT_SUN_PART1, RAND_INF_SPLIT_TIME_PART1, RAND_INF_SPLIT_STORMS_PART1, + RAND_INF_SPLIT_MINUET_PART1, RAND_INF_SPLIT_BOLERO_PART1, RAND_INF_SPLIT_SERENADE_PART1, + RAND_INF_SPLIT_REQUIEM_PART1, RAND_INF_SPLIT_NOCTURNE_PART1, RAND_INF_SPLIT_PRELUDE_PART1, }; static constexpr uint32_t kSplitSongPart2Flags[SPLIT_SONG_MAX] = { - RAND_INF_SPLIT_ZL_PART2, - RAND_INF_SPLIT_EPONA_PART2, - RAND_INF_SPLIT_SARIA_PART2, - RAND_INF_SPLIT_SUN_PART2, - RAND_INF_SPLIT_TIME_PART2, - RAND_INF_SPLIT_STORMS_PART2, - RAND_INF_SPLIT_MINUET_PART2, - RAND_INF_SPLIT_BOLERO_PART2, - RAND_INF_SPLIT_SERENADE_PART2, - RAND_INF_SPLIT_REQUIEM_PART2, - RAND_INF_SPLIT_NOCTURNE_PART2, - RAND_INF_SPLIT_PRELUDE_PART2, + RAND_INF_SPLIT_ZL_PART2, RAND_INF_SPLIT_EPONA_PART2, RAND_INF_SPLIT_SARIA_PART2, + RAND_INF_SPLIT_SUN_PART2, RAND_INF_SPLIT_TIME_PART2, RAND_INF_SPLIT_STORMS_PART2, + RAND_INF_SPLIT_MINUET_PART2, RAND_INF_SPLIT_BOLERO_PART2, RAND_INF_SPLIT_SERENADE_PART2, + RAND_INF_SPLIT_REQUIEM_PART2, RAND_INF_SPLIT_NOCTURNE_PART2, RAND_INF_SPLIT_PRELUDE_PART2, }; static constexpr SplitSongDef kSplitSongs[SPLIT_SONG_MAX] = { diff --git a/soh/soh/Enhancements/randomizer/split_songs.h b/soh/soh/Enhancements/randomizer/split_songs.h index 02125a8554d..f617fa98621 100644 --- a/soh/soh/Enhancements/randomizer/split_songs.h +++ b/soh/soh/Enhancements/randomizer/split_songs.h @@ -69,7 +69,8 @@ class SplitSongs { /** In-world obtainability for RG_*_PART1/PART2 (duplicate parts / song already complete). */ static ItemObtainability GetPartObtainability(RandomizerGet partRg); - /** Dev/testing: grant every song via Part 1 + Part 2 through the same path as chests/NPCs (Randomizer_Item_Give). */ + /** Dev/testing: grant every song via Part 1 + Part 2 through the same path as chests/NPCs (Randomizer_Item_Give). + */ static void DebugGiveAllSongParts(PlayState* play); /** From 3d0a0f6de66d2d2e62173f87bfcfda253dfcbd37 Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:23:50 +0100 Subject: [PATCH 04/22] =?UTF-8?q?=EF=BB=BF=20Made=20split=20songs=20to=20u?= =?UTF-8?q?se=20progressive=20item=20flow=20similar=20to=20Bomb=20bags=20a?= =?UTF-8?q?nd=20Strengh=20Upgardes=20etc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces discrete song part pool entries with progressive song pickups while preserving part-flag progression, song completion behavior, and logic/obtainability handling for split song CURRENTLY ONLY 2 PARTS!! --- .../randomizer/3drando/item_pool.cpp | 48 ++++---- soh/soh/Enhancements/randomizer/item.cpp | 18 +++ soh/soh/Enhancements/randomizer/item_list.cpp | 12 ++ soh/soh/Enhancements/randomizer/logic.cpp | 9 ++ .../Enhancements/randomizer/randomizer.cpp | 7 ++ .../randomizerEnums/RandomizerGet.h | 12 ++ .../Enhancements/randomizer/split_songs.cpp | 112 +++++++++++++++--- soh/soh/Enhancements/randomizer/split_songs.h | 9 ++ 8 files changed, 189 insertions(+), 38 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp index 426d6bfa8b4..f5c669f1f77 100644 --- a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp @@ -267,96 +267,96 @@ void GenerateItemPool() { if (!split) { AddItemToPool(RG_ZELDAS_LULLABY, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } else { - AddItemToPool(RG_ZELDAS_LULLABY_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_ZELDAS_LULLABY_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_ZELDAS_LULLABY, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_ZELDAS_LULLABY, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } } if (!ctx->GetOption(RSK_STARTING_EPONAS_SONG).Get()) { if (!split) { AddItemToPool(RG_EPONAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } else { - AddItemToPool(RG_EPONAS_SONG_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_EPONAS_SONG_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_EPONAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_EPONAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } } if (!ctx->GetOption(RSK_STARTING_SARIAS_SONG).Get()) { if (!split) { AddItemToPool(RG_SARIAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } else { - AddItemToPool(RG_SARIAS_SONG_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_SARIAS_SONG_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_SARIAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_SARIAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } } if (!ctx->GetOption(RSK_STARTING_SUNS_SONG).Get()) { if (!split) { AddItemToPool(RG_SUNS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } else { - AddItemToPool(RG_SUNS_SONG_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_SUNS_SONG_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_SUNS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_SUNS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } } if (!ctx->GetOption(RSK_STARTING_SONG_OF_TIME).Get()) { if (!split) { AddItemToPool(RG_SONG_OF_TIME, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } else { - AddItemToPool(RG_SONG_OF_TIME_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_SONG_OF_TIME_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_SONG_OF_TIME, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_SONG_OF_TIME, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } } if (!ctx->GetOption(RSK_STARTING_SONG_OF_STORMS).Get()) { if (!split) { AddItemToPool(RG_SONG_OF_STORMS, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } else { - AddItemToPool(RG_SONG_OF_STORMS_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_SONG_OF_STORMS_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_SONG_OF_STORMS, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_SONG_OF_STORMS, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } } if (!ctx->GetOption(RSK_STARTING_MINUET_OF_FOREST).Get()) { if (!split) { AddItemToPool(RG_MINUET_OF_FOREST, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } else { - AddItemToPool(RG_MINUET_OF_FOREST_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_MINUET_OF_FOREST_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_MINUET_OF_FOREST, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_MINUET_OF_FOREST, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } } if (!ctx->GetOption(RSK_STARTING_BOLERO_OF_FIRE).Get()) { if (!split) { AddItemToPool(RG_BOLERO_OF_FIRE, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } else { - AddItemToPool(RG_BOLERO_OF_FIRE_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_BOLERO_OF_FIRE_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_BOLERO_OF_FIRE, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_BOLERO_OF_FIRE, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } } if (!ctx->GetOption(RSK_STARTING_SERENADE_OF_WATER).Get()) { if (!split) { AddItemToPool(RG_SERENADE_OF_WATER, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } else { - AddItemToPool(RG_SERENADE_OF_WATER_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_SERENADE_OF_WATER_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_SERENADE_OF_WATER, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_SERENADE_OF_WATER, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } } if (!ctx->GetOption(RSK_STARTING_REQUIEM_OF_SPIRIT).Get()) { if (!split) { AddItemToPool(RG_REQUIEM_OF_SPIRIT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } else { - AddItemToPool(RG_REQUIEM_OF_SPIRIT_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_REQUIEM_OF_SPIRIT_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } } if (!ctx->GetOption(RSK_STARTING_NOCTURNE_OF_SHADOW).Get()) { if (!split) { AddItemToPool(RG_NOCTURNE_OF_SHADOW, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } else { - AddItemToPool(RG_NOCTURNE_OF_SHADOW_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_NOCTURNE_OF_SHADOW_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } } if (!ctx->GetOption(RSK_STARTING_PRELUDE_OF_LIGHT).Get()) { if (!split) { AddItemToPool(RG_PRELUDE_OF_LIGHT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } else { - AddItemToPool(RG_PRELUDE_OF_LIGHT_PART1, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_PRELUDE_OF_LIGHT_PART2, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_PRELUDE_OF_LIGHT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(RG_PROGRESSIVE_PRELUDE_OF_LIGHT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } } } else { diff --git a/soh/soh/Enhancements/randomizer/item.cpp b/soh/soh/Enhancements/randomizer/item.cpp index fbbb96bbcaa..fc3a7ad5028 100644 --- a/soh/soh/Enhancements/randomizer/item.cpp +++ b/soh/soh/Enhancements/randomizer/item.cpp @@ -52,6 +52,8 @@ void Item::ApplyEffect() const { auto logic = ctx->GetLogic(); if (!logic->CalculatingAvailableChecks) { logic->ApplyItemEffect(StaticData::RetrieveItem(randomizerGet), true); + } else if (SplitSongs::IsProgressiveSong(randomizerGet)) { + SplitSongs::ApplyProgressiveEffectToLogicScratch(logic.get(), randomizerGet, true); } else if (SplitSongs::IsSongPart(randomizerGet)) { SplitSongs::ApplyPartEffectToLogicScratch(logic.get(), randomizerGet, true); } @@ -63,6 +65,8 @@ void Item::UndoEffect() const { auto logic = ctx->GetLogic(); if (!logic->CalculatingAvailableChecks) { logic->ApplyItemEffect(StaticData::RetrieveItem(randomizerGet), false); + } else if (SplitSongs::IsProgressiveSong(randomizerGet)) { + SplitSongs::ApplyProgressiveEffectToLogicScratch(logic.get(), randomizerGet, false); } else if (SplitSongs::IsSongPart(randomizerGet)) { SplitSongs::ApplyPartEffectToLogicScratch(logic.get(), randomizerGet, false); } @@ -372,6 +376,20 @@ std::shared_ptr Item::GetGIEntry() const { // NOLINT(*-no-recursio case RG_PROGRESSIVE_GORONSWORD: // todo progressive? actual = RG_BIGGORON_SWORD; break; + case RG_PROGRESSIVE_ZELDAS_LULLABY: + case RG_PROGRESSIVE_EPONAS_SONG: + case RG_PROGRESSIVE_SARIAS_SONG: + case RG_PROGRESSIVE_SUNS_SONG: + case RG_PROGRESSIVE_SONG_OF_TIME: + case RG_PROGRESSIVE_SONG_OF_STORMS: + case RG_PROGRESSIVE_MINUET_OF_FOREST: + case RG_PROGRESSIVE_BOLERO_OF_FIRE: + case RG_PROGRESSIVE_SERENADE_OF_WATER: + case RG_PROGRESSIVE_REQUIEM_OF_SPIRIT: + case RG_PROGRESSIVE_NOCTURNE_OF_SHADOW: + case RG_PROGRESSIVE_PRELUDE_OF_LIGHT: + actual = SplitSongs::ResolveProgressiveSongStage(randomizerGet); + break; case RG_PROGRESSIVE_BOMBCHU_BAG: if (OTRGlobals::Instance->gRandoContext->GetOption(RSK_BOMBCHU_BAG).Is(RO_BOMBCHU_BAG_SINGLE)) { if (logic->CurrentInventory(ITEM_BOMBCHU) != ITEM_NONE) { diff --git a/soh/soh/Enhancements/randomizer/item_list.cpp b/soh/soh/Enhancements/randomizer/item_list.cpp index 31550becb8d..d4068c3ebb9 100644 --- a/soh/soh/Enhancements/randomizer/item_list.cpp +++ b/soh/soh/Enhancements/randomizer/item_list.cpp @@ -73,6 +73,18 @@ void Rando::StaticData::InitItemTable() { itemTable[RG_PROGRESSIVE_MAGIC_METER] = Item(RG_PROGRESSIVE_MAGIC_METER, Text{ "Progressive Magic Meter", "Jauge de Magie (prog.)", "Progressives Magisches Maß" }, ITEMTYPE_ITEM, 0x8A, true, LOGIC_PROGRESSIVE_MAGIC, RHT_PROGRESSIVE_MAGIC_METER, ITEM_CATEGORY_MAJOR, {"a ", "une ", "ein "}, "%g", true).CustomIcon(gQuestIconMagicJarBigTex, ICON_SIZE_24); itemTable[RG_PROGRESSIVE_OCARINA] = Item(RG_PROGRESSIVE_OCARINA, Text{ "Progressive Ocarina", "Ocarina (prog.)", "Progressive Okarina" }, ITEMTYPE_ITEM, 0x8B, true, LOGIC_PROGRESSIVE_OCARINA, RHT_PROGRESSIVE_OCARINA, ITEM_CATEGORY_MAJOR, {"a ", "un ", "eine "}, "%g", true); itemTable[RG_PROGRESSIVE_GORONSWORD] = Item(RG_PROGRESSIVE_GORONSWORD, Text{ "Progressive Goron Sword", "Épée Goron (prog.)", "Progressives Goronen-Schwert" }, ITEMTYPE_ITEM, 0xD4, true, LOGIC_PROGRESSIVE_GIANT_KNIFE, RHT_PROGRESSIVE_GORONSWORD, ITEM_CATEGORY_MAJOR, {"a ", "une ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_ZELDAS_LULLABY] = Item(RG_PROGRESSIVE_ZELDAS_LULLABY, Text{ "Progressive Zelda's Lullaby", "Berceuse de Zelda (prog.)", "Progressives Zeldas Wiegenlied" }, ITEMTYPE_SONG, 0xE1, true, LOGIC_NONE, RHT_ZELDAS_LULLABY, ITEM_CATEGORY_MAJOR, {"a ", "une ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_EPONAS_SONG] = Item(RG_PROGRESSIVE_EPONAS_SONG, Text{ "Progressive Epona's Song", "Chant d'Epona (prog.)", "Progressives Eponas Lied" }, ITEMTYPE_SONG, 0xE2, true, LOGIC_NONE, RHT_EPONAS_SONG, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SARIAS_SONG] = Item(RG_PROGRESSIVE_SARIAS_SONG, Text{ "Progressive Saria's Song", "Chant de Saria (prog.)", "Progressives Salias Lied" }, ITEMTYPE_SONG, 0xE3, true, LOGIC_NONE, RHT_SARIAS_SONG, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SUNS_SONG] = Item(RG_PROGRESSIVE_SUNS_SONG, Text{ "Progressive Sun's Song", "Chant du Soleil (prog.)", "Progressives Hymne der Sonne" }, ITEMTYPE_SONG, 0xE4, true, LOGIC_NONE, RHT_SUNS_SONG, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SONG_OF_TIME] = Item(RG_PROGRESSIVE_SONG_OF_TIME, Text{ "Progressive Song of Time", "Chant du Temps (prog.)", "Progressives Hymne der Zeit" }, ITEMTYPE_SONG, 0xE5, true, LOGIC_NONE, RHT_SONG_OF_TIME, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SONG_OF_STORMS] = Item(RG_PROGRESSIVE_SONG_OF_STORMS, Text{ "Progressive Song of Storms", "Chant des Tempêtes (prog.)", "Progressives Hymne des Sturms" }, ITEMTYPE_SONG, 0xE6, true, LOGIC_NONE, RHT_SONG_OF_STORMS, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_MINUET_OF_FOREST] = Item(RG_PROGRESSIVE_MINUET_OF_FOREST, Text{ "Progressive Minuet of Forest", "Menuet des Bois (prog.)", "Progressives Menuett des Waldes" }, ITEMTYPE_SONG, 0xE7, true, LOGIC_NONE, RHT_MINUET_OF_FOREST, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_BOLERO_OF_FIRE] = Item(RG_PROGRESSIVE_BOLERO_OF_FIRE, Text{ "Progressive Bolero of Fire", "Boléro du Feu (prog.)", "Progressives Bolero des Feuers" }, ITEMTYPE_SONG, 0xE8, true, LOGIC_NONE, RHT_BOLERO_OF_FIRE, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SERENADE_OF_WATER] = Item(RG_PROGRESSIVE_SERENADE_OF_WATER, Text{ "Progressive Serenade of Water", "Sérénade de l'Eau (prog.)", "Progressive Serenade des Wassers" }, ITEMTYPE_SONG, 0xE9, true, LOGIC_NONE, RHT_SERENADE_OF_WATER, ITEM_CATEGORY_MAJOR, {"a ", "une ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_REQUIEM_OF_SPIRIT] = Item(RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, Text{ "Progressive Requiem of Spirit", "Requiem des Esprits (prog.)", "Progressives Requiem der Geister" }, ITEMTYPE_SONG, 0xEA, true, LOGIC_NONE, RHT_REQUIEM_OF_SPIRIT, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_NOCTURNE_OF_SHADOW] = Item(RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, Text{ "Progressive Nocturne of Shadow", "Nocturne de l'Ombre (prog.)", "Progressive Nocturne des Schattens" }, ITEMTYPE_SONG, 0xEB, true, LOGIC_NONE, RHT_NOCTURNE_OF_SHADOW, ITEM_CATEGORY_MAJOR, {"a ", "une ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_PRELUDE_OF_LIGHT] = Item(RG_PROGRESSIVE_PRELUDE_OF_LIGHT, Text{ "Progressive Prelude of Light", "Prélude de la Lumière (prog.)", "Progressive Kantate des Lichts" }, ITEMTYPE_SONG, 0xEC, true, LOGIC_NONE, RHT_PRELUDE_OF_LIGHT, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); // Bottles itemTable[RG_EMPTY_BOTTLE] = Item(RG_EMPTY_BOTTLE, Text{ "Empty Bottle", "Bouteille Vide", "Leere Flasche" }, ITEMTYPE_ITEM, GI_BOTTLE, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_MILK, ITEM_BOTTLE, OBJECT_GI_BOTTLE, GID_BOTTLE, 0x42, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"an ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_MILK] = Item(RG_BOTTLE_WITH_MILK, Text{ "Bottle with Milk", "Bouteille avec du Lait", "Flasche mit Milch" }, ITEMTYPE_ITEM, GI_MILK_BOTTLE, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_MILK, ITEM_MILK_BOTTLE, OBJECT_GI_MILK, GID_MILK, 0x98, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"a ", "une ", "eine "}); diff --git a/soh/soh/Enhancements/randomizer/logic.cpp b/soh/soh/Enhancements/randomizer/logic.cpp index e43e4ce0c4b..e56bfcc89e7 100644 --- a/soh/soh/Enhancements/randomizer/logic.cpp +++ b/soh/soh/Enhancements/randomizer/logic.cpp @@ -20,6 +20,11 @@ namespace Rando { bool Logic::HasItem(RandomizerGet itemName) { + if (SplitSongs::IsProgressiveSong(itemName)) { + const SplitSongDef* def = SplitSongs::GetSongDefFromProgressive(itemName); + return def != nullptr && SplitSongs::HasBothParts(def->id); + } + switch (itemName) { case RG_FAIRY_OCARINA: return CheckInventory(ITEM_OCARINA_FAIRY, false); @@ -2134,6 +2139,10 @@ void Logic::ApplyItemEffect(Item& item, bool state) { case ITEMTYPE_DUNGEONREWARD: case ITEMTYPE_SONG: { RandomizerGet rg = item.GetRandomizerGet(); + if (SplitSongs::IsProgressiveSong(rg)) { + SplitSongs::ApplyProgressiveEffectToLogicScratch(this, rg, state); + break; + } if (SplitSongs::IsSongPart(rg)) { SplitSongs::ApplyPartEffectToLogicScratch(this, rg, state); break; diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index c3c458ed6f5..d2c840730e4 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -296,6 +296,9 @@ ItemObtainability Randomizer::GetItemObtainabilityFromRandomizerCheck(Randomizer } ItemObtainability Randomizer::GetItemObtainabilityFromRandomizerGet(RandomizerGet randoGet) { + if (Rando::SplitSongs::IsProgressiveSong(randoGet)) { + return Rando::SplitSongs::GetProgressiveSongObtainability(randoGet); + } if (Rando::SplitSongs::IsSongPart(randoGet)) { return Rando::SplitSongs::GetPartObtainability(randoGet); } @@ -4642,6 +4645,10 @@ extern "C" u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry) { Rando::SplitSongs::OnItemReceived(item); return Return_Item_Entry(giEntry, RG_NONE); } + if (Rando::SplitSongs::IsProgressiveSong(item)) { + Rando::SplitSongs::OnProgressiveSongReceived(item); + return Return_Item_Entry(giEntry, RG_NONE); + } // if it's an item that just sets a randomizerInf, set it if (randomizerGetToRandInf.find(item) != randomizerGetToRandInf.end()) { diff --git a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerGet.h b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerGet.h index 8e9d7fff9b9..65013dbb5f0 100644 --- a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerGet.h +++ b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerGet.h @@ -66,6 +66,18 @@ RANDO_ENUM_ITEM(RG_MAGIC_SINGLE) // Added for refactor of GetItemEntries RANDO_ENUM_ITEM(RG_MAGIC_DOUBLE) // Added for refactor of GetItemEntries RANDO_ENUM_ITEM(RG_PROGRESSIVE_OCARINA) RANDO_ENUM_ITEM(RG_PROGRESSIVE_GORONSWORD) +RANDO_ENUM_ITEM(RG_PROGRESSIVE_ZELDAS_LULLABY) +RANDO_ENUM_ITEM(RG_PROGRESSIVE_EPONAS_SONG) +RANDO_ENUM_ITEM(RG_PROGRESSIVE_SARIAS_SONG) +RANDO_ENUM_ITEM(RG_PROGRESSIVE_SUNS_SONG) +RANDO_ENUM_ITEM(RG_PROGRESSIVE_SONG_OF_TIME) +RANDO_ENUM_ITEM(RG_PROGRESSIVE_SONG_OF_STORMS) +RANDO_ENUM_ITEM(RG_PROGRESSIVE_MINUET_OF_FOREST) +RANDO_ENUM_ITEM(RG_PROGRESSIVE_BOLERO_OF_FIRE) +RANDO_ENUM_ITEM(RG_PROGRESSIVE_SERENADE_OF_WATER) +RANDO_ENUM_ITEM(RG_PROGRESSIVE_REQUIEM_OF_SPIRIT) +RANDO_ENUM_ITEM(RG_PROGRESSIVE_NOCTURNE_OF_SHADOW) +RANDO_ENUM_ITEM(RG_PROGRESSIVE_PRELUDE_OF_LIGHT) RANDO_ENUM_ITEM(RG_EMPTY_BOTTLE) RANDO_ENUM_ITEM(RG_BOTTLE_WITH_MILK) RANDO_ENUM_ITEM(RG_BOTTLE_WITH_RED_POTION) diff --git a/soh/soh/Enhancements/randomizer/split_songs.cpp b/soh/soh/Enhancements/randomizer/split_songs.cpp index bf083b2a30b..d20de1c1b64 100644 --- a/soh/soh/Enhancements/randomizer/split_songs.cpp +++ b/soh/soh/Enhancements/randomizer/split_songs.cpp @@ -32,18 +32,27 @@ static constexpr uint32_t kSplitSongPart2Flags[SPLIT_SONG_MAX] = { }; static constexpr SplitSongDef kSplitSongs[SPLIT_SONG_MAX] = { - { SPLIT_SONG_ZELDAS_LULLABY, RG_ZELDAS_LULLABY_PART1, RG_ZELDAS_LULLABY_PART2, RG_ZELDAS_LULLABY }, - { SPLIT_SONG_EPONAS_SONG, RG_EPONAS_SONG_PART1, RG_EPONAS_SONG_PART2, RG_EPONAS_SONG }, - { SPLIT_SONG_SARIAS_SONG, RG_SARIAS_SONG_PART1, RG_SARIAS_SONG_PART2, RG_SARIAS_SONG }, - { SPLIT_SONG_SUNS_SONG, RG_SUNS_SONG_PART1, RG_SUNS_SONG_PART2, RG_SUNS_SONG }, - { SPLIT_SONG_SONG_OF_TIME, RG_SONG_OF_TIME_PART1, RG_SONG_OF_TIME_PART2, RG_SONG_OF_TIME }, - { SPLIT_SONG_SONG_OF_STORMS, RG_SONG_OF_STORMS_PART1, RG_SONG_OF_STORMS_PART2, RG_SONG_OF_STORMS }, - { SPLIT_SONG_MINUET_OF_FOREST, RG_MINUET_OF_FOREST_PART1, RG_MINUET_OF_FOREST_PART2, RG_MINUET_OF_FOREST }, - { SPLIT_SONG_BOLERO_OF_FIRE, RG_BOLERO_OF_FIRE_PART1, RG_BOLERO_OF_FIRE_PART2, RG_BOLERO_OF_FIRE }, - { SPLIT_SONG_SERENADE_OF_WATER, RG_SERENADE_OF_WATER_PART1, RG_SERENADE_OF_WATER_PART2, RG_SERENADE_OF_WATER }, - { SPLIT_SONG_REQUIEM_OF_SPIRIT, RG_REQUIEM_OF_SPIRIT_PART1, RG_REQUIEM_OF_SPIRIT_PART2, RG_REQUIEM_OF_SPIRIT }, - { SPLIT_SONG_NOCTURNE_OF_SHADOW, RG_NOCTURNE_OF_SHADOW_PART1, RG_NOCTURNE_OF_SHADOW_PART2, RG_NOCTURNE_OF_SHADOW }, - { SPLIT_SONG_PRELUDE_OF_LIGHT, RG_PRELUDE_OF_LIGHT_PART1, RG_PRELUDE_OF_LIGHT_PART2, RG_PRELUDE_OF_LIGHT }, + { SPLIT_SONG_ZELDAS_LULLABY, RG_PROGRESSIVE_ZELDAS_LULLABY, RG_ZELDAS_LULLABY_PART1, RG_ZELDAS_LULLABY_PART2, + RG_ZELDAS_LULLABY }, + { SPLIT_SONG_EPONAS_SONG, RG_PROGRESSIVE_EPONAS_SONG, RG_EPONAS_SONG_PART1, RG_EPONAS_SONG_PART2, RG_EPONAS_SONG }, + { SPLIT_SONG_SARIAS_SONG, RG_PROGRESSIVE_SARIAS_SONG, RG_SARIAS_SONG_PART1, RG_SARIAS_SONG_PART2, RG_SARIAS_SONG }, + { SPLIT_SONG_SUNS_SONG, RG_PROGRESSIVE_SUNS_SONG, RG_SUNS_SONG_PART1, RG_SUNS_SONG_PART2, RG_SUNS_SONG }, + { SPLIT_SONG_SONG_OF_TIME, RG_PROGRESSIVE_SONG_OF_TIME, RG_SONG_OF_TIME_PART1, RG_SONG_OF_TIME_PART2, + RG_SONG_OF_TIME }, + { SPLIT_SONG_SONG_OF_STORMS, RG_PROGRESSIVE_SONG_OF_STORMS, RG_SONG_OF_STORMS_PART1, RG_SONG_OF_STORMS_PART2, + RG_SONG_OF_STORMS }, + { SPLIT_SONG_MINUET_OF_FOREST, RG_PROGRESSIVE_MINUET_OF_FOREST, RG_MINUET_OF_FOREST_PART1, + RG_MINUET_OF_FOREST_PART2, RG_MINUET_OF_FOREST }, + { SPLIT_SONG_BOLERO_OF_FIRE, RG_PROGRESSIVE_BOLERO_OF_FIRE, RG_BOLERO_OF_FIRE_PART1, RG_BOLERO_OF_FIRE_PART2, + RG_BOLERO_OF_FIRE }, + { SPLIT_SONG_SERENADE_OF_WATER, RG_PROGRESSIVE_SERENADE_OF_WATER, RG_SERENADE_OF_WATER_PART1, + RG_SERENADE_OF_WATER_PART2, RG_SERENADE_OF_WATER }, + { SPLIT_SONG_REQUIEM_OF_SPIRIT, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, RG_REQUIEM_OF_SPIRIT_PART1, + RG_REQUIEM_OF_SPIRIT_PART2, RG_REQUIEM_OF_SPIRIT }, + { SPLIT_SONG_NOCTURNE_OF_SHADOW, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, RG_NOCTURNE_OF_SHADOW_PART1, + RG_NOCTURNE_OF_SHADOW_PART2, RG_NOCTURNE_OF_SHADOW }, + { SPLIT_SONG_PRELUDE_OF_LIGHT, RG_PROGRESSIVE_PRELUDE_OF_LIGHT, RG_PRELUDE_OF_LIGHT_PART1, + RG_PRELUDE_OF_LIGHT_PART2, RG_PRELUDE_OF_LIGHT }, }; static bool IsValidSplitSongId(SplitSongId id) { @@ -91,6 +100,15 @@ const SplitSongDef* SplitSongs::GetSongDefFromPart(RandomizerGet rg) { return nullptr; } +const SplitSongDef* SplitSongs::GetSongDefFromProgressive(RandomizerGet rg) { + for (const auto& def : kSplitSongs) { + if (def.progressive == rg) { + return &def; + } + } + return nullptr; +} + const SplitSongDef* SplitSongs::GetSongDefFromFullSong(RandomizerGet fullSongRg) { for (const auto& def : kSplitSongs) { if (def.fullSong == fullSongRg) { @@ -100,6 +118,10 @@ const SplitSongDef* SplitSongs::GetSongDefFromFullSong(RandomizerGet fullSongRg) return nullptr; } +bool SplitSongs::IsProgressiveSong(RandomizerGet rg) { + return GetSongDefFromProgressive(rg) != nullptr; +} + static bool UsingLogicSimulationBuffer(Logic* logic) { return logic != nullptr && logic->mSaveContext != nullptr && logic->mSaveContext != &gSaveContext; } @@ -253,6 +275,19 @@ void SplitSongs::OnItemReceived(RandomizerGet rg) { TryCompleteSong(def->id); } +void SplitSongs::OnProgressiveSongReceived(RandomizerGet rg) { + const SplitSongDef* def = GetSongDefFromProgressive(rg); + if (def == nullptr || HasFullSong(def->id)) { + return; + } + if (!HasPart1(def->id)) { + SetPart1(def->id, true); + } else if (!HasPart2(def->id)) { + SetPart2(def->id, true); + } + TryCompleteSong(def->id); +} + ItemObtainability SplitSongs::GetPartObtainability(RandomizerGet partRg) { const SplitSongDef* def = GetSongDefFromPart(partRg); if (def == nullptr) { @@ -271,11 +306,19 @@ ItemObtainability SplitSongs::GetPartObtainability(RandomizerGet partRg) { return CAN_OBTAIN; } +ItemObtainability SplitSongs::GetProgressiveSongObtainability(RandomizerGet progressiveRg) { + const SplitSongDef* def = GetSongDefFromProgressive(progressiveRg); + if (def == nullptr) { + return CAN_OBTAIN; + } + return HasBothParts(def->id) || HasFullSong(def->id) ? CANT_OBTAIN_ALREADY_HAVE : CAN_OBTAIN; +} + void SplitSongs::AppendShuffledSongPoolItems(std::vector& pool, bool split) { for (const auto& def : kSplitSongs) { if (split) { - pool.push_back(def.part1); - pool.push_back(def.part2); + pool.push_back(def.progressive); + pool.push_back(def.progressive); } else { pool.push_back(def.fullSong); } @@ -308,4 +351,45 @@ void SplitSongs::ApplyPartEffectToLogicScratch(Logic* logic, RandomizerGet rg, b logic->SetRandoInf(flag, state); } +void SplitSongs::ApplyProgressiveEffectToLogicScratch(Logic* logic, RandomizerGet rg, bool state) { + if (!UsingLogicSimulationBuffer(logic)) { + return; + } + const SplitSongDef* def = GetSongDefFromProgressive(rg); + if (def == nullptr) { + return; + } + + const uint32_t part1Flag = kSplitSongPart1Flags[def->id]; + const uint32_t part2Flag = kSplitSongPart2Flags[def->id]; + const bool hasPart1 = logic->CheckRandoInf(static_cast(part1Flag)); + const bool hasPart2 = logic->CheckRandoInf(static_cast(part2Flag)); + + if (state) { + if (!hasPart1) { + logic->SetRandoInf(part1Flag, true); + } else if (!hasPart2) { + logic->SetRandoInf(part2Flag, true); + } + } else if (hasPart2) { + logic->SetRandoInf(part2Flag, false); + } else if (hasPart1) { + logic->SetRandoInf(part1Flag, false); + } +} + +RandomizerGet SplitSongs::ResolveProgressiveSongStage(RandomizerGet rg) { + const SplitSongDef* def = GetSongDefFromProgressive(rg); + if (def == nullptr) { + return RG_NONE; + } + if (!HasPart1(def->id)) { + return def->part1; + } + if (!HasPart2(def->id)) { + return def->part2; + } + return def->fullSong; +} + } // namespace Rando diff --git a/soh/soh/Enhancements/randomizer/split_songs.h b/soh/soh/Enhancements/randomizer/split_songs.h index f617fa98621..cfc7a90a383 100644 --- a/soh/soh/Enhancements/randomizer/split_songs.h +++ b/soh/soh/Enhancements/randomizer/split_songs.h @@ -30,6 +30,7 @@ enum SplitSongId { struct SplitSongDef { SplitSongId id; + RandomizerGet progressive; RandomizerGet part1; RandomizerGet part2; RandomizerGet fullSong; @@ -41,12 +42,14 @@ class SplitSongs { static SplitSongId GetSongIdFromPart(RandomizerGet rg); static const SplitSongDef* GetSongDef(SplitSongId id); static const SplitSongDef* GetSongDefFromPart(RandomizerGet rg); + static const SplitSongDef* GetSongDefFromProgressive(RandomizerGet rg); /** Full-song RandomizerGet (RG_ZELDAS_LULLABY, …), not part items. */ static const SplitSongDef* GetSongDefFromFullSong(RandomizerGet fullSongRg); static bool HasPart1(SplitSongId id); static bool HasPart2(SplitSongId id); static bool HasBothParts(SplitSongId id); + static bool IsProgressiveSong(RandomizerGet rg); static void SetPart1(SplitSongId id, bool state); static void SetPart2(SplitSongId id, bool state); @@ -61,6 +64,7 @@ class SplitSongs { static void ClearPendingFullSongGrants(); static void OnItemReceived(RandomizerGet rg); + static void OnProgressiveSongReceived(RandomizerGet rg); // Item pool / ice-trap models: 12 full songs vs 24 part items (same 12 logical songs). static void AppendShuffledSongPoolItems(std::vector& pool, bool split); @@ -68,6 +72,7 @@ class SplitSongs { /** In-world obtainability for RG_*_PART1/PART2 (duplicate parts / song already complete). */ static ItemObtainability GetPartObtainability(RandomizerGet partRg); + static ItemObtainability GetProgressiveSongObtainability(RandomizerGet progressiveRg); /** Dev/testing: grant every song via Part 1 + Part 2 through the same path as chests/NPCs (Randomizer_Item_Give). */ @@ -78,6 +83,10 @@ class SplitSongs { * No-op when Logic is tied to the real gSaveContext (in-game). */ static void ApplyPartEffectToLogicScratch(Logic* logic, RandomizerGet rg, bool state); + static void ApplyProgressiveEffectToLogicScratch(Logic* logic, RandomizerGet rg, bool state); + + /** Returns the concrete part item represented by this progressive pickup at current state. */ + static RandomizerGet ResolveProgressiveSongStage(RandomizerGet rg); }; } // namespace Rando From 03860519b3ef5dbd01ab557632f61c297a0a9c99 Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:47:50 +0100 Subject: [PATCH 05/22] =?UTF-8?q?=EF=BB=BFUpdate=20split=20songs=20option?= =?UTF-8?q?=20text=20for=20progressive=20behavior.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reworded the option in UI in rando menu to explain split songs now use two progressive pickups per song and keeps the Anywhere requirement messaging. --- soh/soh/Enhancements/randomizer/option_descriptions.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/option_descriptions.cpp b/soh/soh/Enhancements/randomizer/option_descriptions.cpp index 311cf5ad0b0..0daa222edae 100644 --- a/soh/soh/Enhancements/randomizer/option_descriptions.cpp +++ b/soh/soh/Enhancements/randomizer/option_descriptions.cpp @@ -220,13 +220,14 @@ void Settings::CreateOptionDescriptions() { mOptionDescriptions[RSK_SPLIT_OCARINA_SONGS] = "Randomizer option next to Shuffle Songs.\n" "\n" - "Each of the 12 ocarina songs becomes two checks (Part 1 and Part 2). Collecting both grants the full song.\n" + "Each of the 12 ocarina songs is split into a 2-step progressive song item. Collecting both progressive " + "steps grants the full song.\n" "\n" "Requires Shuffle Songs: \"Anywhere\" — Song Locations / Dungeon Rewards only have 12 song slots, so split " "parts are not generated there.\n" "\n" - "When enabled with Anywhere, the item pool gains 12 extra song-part items; junk fill is reduced so the seed " - "still generates."; + "When enabled with Anywhere, the item pool places two progressive song pickups per song (24 total song " + "progressive pickups instead of 12 full songs)."; mOptionDescriptions[RSK_SHUFFLE_TOKENS] = "Shuffles Golden Skulltula Tokens into the item pool. This means " "Golden Skulltulas can contain other items as well.\n" "\n" From 6d49e4d71163983da4c6f3590a342646496b0b40 Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Sun, 10 May 2026 19:02:39 +0100 Subject: [PATCH 06/22] Randomizer: pickup textbox icons and progressive split songs Updated get-item textbox icon handling so OTR paths load and draw correctly on pickup (including custom icons). Song icons use the correct texture format, sizing, and per-song colors. Songs are fully progressive in randomizer logic; seed/shuffle paths allow more than two song pieces in the pool so duplicate parts and edge cases do not softlock. Also: sync player get-item entry for message context, resolve gift row when entry and id disagree, custom icons for blue potion and key rings, and ice trap naming for split song models. --- .../randomizer/Messages/ItemMessages.cpp | 384 +++++++++++++++--- .../Enhancements/randomizer/SeedContext.cpp | 13 + .../Enhancements/randomizer/ShuffleSongs.cpp | 24 +- soh/soh/Enhancements/randomizer/Traps.cpp | 2 + soh/soh/Enhancements/randomizer/item.cpp | 6 + soh/soh/Enhancements/randomizer/item_list.cpp | 22 +- .../Enhancements/randomizer/randomizer.cpp | 11 + soh/soh/SohGui/SohMenuDevTools.cpp | 6 + soh/src/code/z_message_PAL.c | 241 +++++++++-- .../actors/ovl_player_actor/z_player.c | 4 + 10 files changed, 578 insertions(+), 135 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp index eaa38a2d30f..da3260b2d04 100644 --- a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp @@ -4,30 +4,88 @@ * Vanilla/MQ hints when collecting Maps, Ice Trap messages, * etc. */ +#include #include #include "soh/Enhancements/randomizer/split_songs.h" #include "soh/Enhancements/randomizer/static_data.h" -#include "soh/Enhancements/game-interactor/GameInteractor.h" -#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" #include "soh/Enhancements/custom-message/CustomMessageTypes.h" #include "soh/Enhancements/randomizer/Traps.h" #include "soh/Enhancements/randomizer/item.h" #include "soh/ShipInit.hpp" #include +#include #include +#include extern "C" { #include #include #include "z64item.h" #include "z64player.h" +#include "z64save.h" extern PlayState* gPlayState; +void Message_LoadItemIcon(PlayState* play, u16 itemId, s16 y); +GetItemEntry ItemTable_Retrieve(int16_t getItemID); +GetItemEntry ItemTable_RetrieveEntry(s16 modIndex, s16 getItemID); } -namespace { +/** + * Match `Player`'s get-item display (`z_player` func_8084DFF4): if `getItemId` does not match `getItemEntry`, + * the table row keyed by `getItemId` wins. `Message_OpenText` runs before the entry is synced onto the player, + * so reading only `player->getItemEntry` here can embed the wrong textbox icon versus the real gift. + */ +static GetItemEntry ResolveGiftGetItemEntry(const Player* player) { + if (player == nullptr) { + return GET_ITEM_NONE; + } + if (player->getItemEntry.objectId == OBJECT_INVALID || + player->getItemId != player->getItemEntry.getItemId) { + if (IS_RANDO && player->getItemId > RG_NONE && player->getItemId < RG_MAX) { + return ItemTable_RetrieveEntry(MOD_RANDOMIZER, player->getItemId); + } + return ItemTable_Retrieve(player->getItemId); + } + return player->getItemEntry; +} + +static bool LoadCustomItemIcon(bool displayAsEnglish); +static bool RefreshCustomItemIconForTextbox(bool displayAsEnglish, bool incrementDecodeState); +static void ApplyCustomIconPathToTextbox(const char* customIcon, CustomIconSize iconSize, bool displayAsEnglish, + bool incrementDecodeState); +static void DrawTextboxItemIconFromSegment(Gfx** p); +static RandomizerGet RandomizerGetFromPlayerItemEntry(const Player* player); + +/** Cache the custom icon chosen during decode; draw can reuse if player context drifts before render. */ +static const char* g_LastDecodedCustomIconPath = nullptr; +static CustomIconSize g_LastDecodedCustomIconSize = ICON_SIZE_32; + +/** Called from z_message_PAL MESSAGE_ITEM_ICON decode — ITEM_CUSTOM uses OTR path then vanilla fallback. */ +extern "C" void Randomizer_Message_DecodeLoadItemIcon(PlayState* play, u16 iconToLoad, s32 displayAsEnglishAsInt) { + if (play == nullptr || iconToLoad < ITEM_CUSTOM) { + return; + } + const bool displayAsEnglish = displayAsEnglishAsInt != 0; + if (gSaveContext.ship.quest.id == QUEST_RANDOMIZER) { + if (LoadCustomItemIcon(displayAsEnglish)) { + return; + } + } + Message_LoadItemIcon(play, iconToLoad, (s16)(R_TEXTBOX_Y + 10)); +} + +extern "C" void Randomizer_Message_RefreshCustomItemIconAtDraw(s32 displayAsEnglishAsInt) { + const bool displayAsEnglish = displayAsEnglishAsInt != 0; + (void)RefreshCustomItemIconForTextbox(displayAsEnglish, false); +} -ItemID VanillaItemIdForFullSong(RandomizerGet fullSongRg) { +extern "C" void Randomizer_Message_AppendSegmentIconGfx(Gfx** gfxp) { + if (gfxp != nullptr) { + DrawTextboxItemIconFromSegment(gfxp); + } +} + +static ItemID VanillaItemIdForFullSong(RandomizerGet fullSongRg) { switch (fullSongRg) { case RG_ZELDAS_LULLABY: return ITEM_SONG_LULLABY; @@ -58,7 +116,150 @@ ItemID VanillaItemIdForFullSong(RandomizerGet fullSongRg) { } } -} // namespace +/** Same rules as `BuildCustomItemMessage` for resolving `RandomizerGet` from the current get-item context. */ +static RandomizerGet RandomizerGetFromPlayerItemEntry(const Player* player) { + if (player == nullptr) { + return RG_NONE; + } + const GetItemEntry gift = ResolveGiftGetItemEntry(player); + int16_t rgid; + if (gift.modIndex == MOD_RANDOMIZER) { + rgid = gift.getItemId; + } else if (gift.objectId != OBJECT_INVALID) { + rgid = gift.getItemId; + } else { + rgid = player->getItemId; + } + if (rgid < 0) { + rgid = static_cast(-rgid); + } + return static_cast(rgid); +} + +/** Fallback: vanilla pause `gItemIcons` quest song row if tracker mapping is unavailable. */ +static ItemID VanillaSongIconForSplitOrProgressive(RandomizerGet rg) { + if (Rando::SplitSongs::IsSongPart(rg)) { + const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromPart(rg); + return def != nullptr ? VanillaItemIdForFullSong(def->fullSong) : ITEM_NONE; + } + if (Rando::SplitSongs::IsProgressiveSong(rg)) { + RandomizerGet stage = Rando::SplitSongs::ResolveProgressiveSongStage(rg); + if (stage == RG_NONE) { + const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromProgressive(rg); + if (def != nullptr) { + stage = def->part1; + } + } + const Rando::SplitSongDef* def = stage != RG_NONE ? Rando::SplitSongs::GetSongDefFromPart(stage) : nullptr; + return def != nullptr ? VanillaItemIdForFullSong(def->fullSong) : ITEM_NONE; + } + return ITEM_NONE; +} + +static ItemID DirectSongIconForRandomizerGet(RandomizerGet rg) { + switch (rg) { + case RG_ZELDAS_LULLABY: + case RG_PROGRESSIVE_ZELDAS_LULLABY: + case RG_ZELDAS_LULLABY_PART1: + case RG_ZELDAS_LULLABY_PART2: + return ITEM_SONG_LULLABY; + case RG_EPONAS_SONG: + case RG_PROGRESSIVE_EPONAS_SONG: + case RG_EPONAS_SONG_PART1: + case RG_EPONAS_SONG_PART2: + return ITEM_SONG_EPONA; + case RG_SARIAS_SONG: + case RG_PROGRESSIVE_SARIAS_SONG: + case RG_SARIAS_SONG_PART1: + case RG_SARIAS_SONG_PART2: + return ITEM_SONG_SARIA; + case RG_SUNS_SONG: + case RG_PROGRESSIVE_SUNS_SONG: + case RG_SUNS_SONG_PART1: + case RG_SUNS_SONG_PART2: + return ITEM_SONG_SUN; + case RG_SONG_OF_TIME: + case RG_PROGRESSIVE_SONG_OF_TIME: + case RG_SONG_OF_TIME_PART1: + case RG_SONG_OF_TIME_PART2: + return ITEM_SONG_TIME; + case RG_SONG_OF_STORMS: + case RG_PROGRESSIVE_SONG_OF_STORMS: + case RG_SONG_OF_STORMS_PART1: + case RG_SONG_OF_STORMS_PART2: + return ITEM_SONG_STORMS; + case RG_MINUET_OF_FOREST: + case RG_PROGRESSIVE_MINUET_OF_FOREST: + case RG_MINUET_OF_FOREST_PART1: + case RG_MINUET_OF_FOREST_PART2: + return ITEM_SONG_MINUET; + case RG_BOLERO_OF_FIRE: + case RG_PROGRESSIVE_BOLERO_OF_FIRE: + case RG_BOLERO_OF_FIRE_PART1: + case RG_BOLERO_OF_FIRE_PART2: + return ITEM_SONG_BOLERO; + case RG_SERENADE_OF_WATER: + case RG_PROGRESSIVE_SERENADE_OF_WATER: + case RG_SERENADE_OF_WATER_PART1: + case RG_SERENADE_OF_WATER_PART2: + return ITEM_SONG_SERENADE; + case RG_REQUIEM_OF_SPIRIT: + case RG_PROGRESSIVE_REQUIEM_OF_SPIRIT: + case RG_REQUIEM_OF_SPIRIT_PART1: + case RG_REQUIEM_OF_SPIRIT_PART2: + return ITEM_SONG_REQUIEM; + case RG_NOCTURNE_OF_SHADOW: + case RG_PROGRESSIVE_NOCTURNE_OF_SHADOW: + case RG_NOCTURNE_OF_SHADOW_PART1: + case RG_NOCTURNE_OF_SHADOW_PART2: + return ITEM_SONG_NOCTURNE; + case RG_PRELUDE_OF_LIGHT: + case RG_PROGRESSIVE_PRELUDE_OF_LIGHT: + case RG_PRELUDE_OF_LIGHT_PART1: + case RG_PRELUDE_OF_LIGHT_PART2: + return ITEM_SONG_PRELUDE; + default: + return ITEM_NONE; + } +} + +static ItemID ResolveSongMessageIcon(RandomizerGet rg, RandomizerGet resolvedStage = RG_NONE) { + ItemID icon = DirectSongIconForRandomizerGet(rg); + if (icon != ITEM_NONE) { + return icon; + } + if (resolvedStage != RG_NONE) { + icon = DirectSongIconForRandomizerGet(resolvedStage); + if (icon != ITEM_NONE) { + return icon; + } + } + icon = VanillaSongIconForSplitOrProgressive(rg); + if (icon != ITEM_NONE) { + return icon; + } + if (resolvedStage != RG_NONE) { + const auto* stageDef = Rando::SplitSongs::GetSongDefFromPart(resolvedStage); + if (stageDef != nullptr) { + icon = VanillaItemIdForFullSong(stageDef->fullSong); + if (icon != ITEM_NONE) { + return icon; + } + } + } + // Never return ITEM_CUSTOM for split/progressive song messages; avoid static/noise tile path. + return ITEM_SONG_LULLABY; +} + +extern "C" u16 Message_GetRandoTextboxItemIconOverride(PlayState* play, u16 itemId) { + (void)play; + return itemId; +} + +extern "C" u16 Randomizer_ResolveSongIconAtDrawTime(PlayState* play, u16 itemId) { + (void)play; + return itemId; +} void BuildTriforcePieceMessage(CustomMessage& msg) { uint8_t current = gSaveContext.ship.quest.data.randomizer.triforcePiecesCollected + 1; @@ -110,81 +311,140 @@ void BuildCustomItemMessage(Player* player, CustomMessage& msg) { msg = CustomMessage("You found [[article]][[color]][[name]]%w!", "Du erhältst [[article]][[color]][[name]]%w gefunden!", "Vous avez trouvé [[article]][[color]][[name]]%w!", TEXTBOX_TYPE_BLUE); - if (player->getItemEntry.modIndex == MOD_RANDOMIZER) { - rgid = player->getItemEntry.getItemId; - } else if (player->getItemEntry.objectId != OBJECT_INVALID) { - rgid = player->getItemEntry.getItemId; + const GetItemEntry gift = ResolveGiftGetItemEntry(player); + if (gift.modIndex == MOD_RANDOMIZER) { + rgid = gift.getItemId; + } else if (gift.objectId != OBJECT_INVALID) { + rgid = gift.getItemId; } else { rgid = player->getItemId; } - if (player->getItemEntry.modIndex == MOD_RANDOMIZER && rgid < 0) { + if (gift.modIndex == MOD_RANDOMIZER && rgid < 0) { rgid = (s16)-rgid; } - if (IS_RANDO && Rando::SplitSongs::IsSongPart(static_cast(rgid))) { - const RandomizerGet partRg = static_cast(rgid); - const auto& songPartItem = Rando::StaticData::RetrieveItem(partRg); - const auto* splitDef = Rando::SplitSongs::GetSongDefFromPart(partRg); - const ItemID iconItemId = splitDef != nullptr ? VanillaItemIdForFullSong(splitDef->fullSong) : ITEM_NONE; + const RandomizerGet rgEnum = static_cast(rgid); + // Song icons: ResolveSongMessageIcon maps RG_* → vanilla ITEM_SONG_*; Format() writes MESSAGE_ITEM_ICON bytes only. + // Decode loads icon strictly from that buffer (same contract as non-song items using GetGIEntry()->itemId). + if (IS_RANDO && Rando::SplitSongs::IsProgressiveSong(rgEnum)) { + RandomizerGet stage = Rando::SplitSongs::ResolveProgressiveSongStage(rgEnum); + if (stage == RG_NONE) { + const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromProgressive(rgEnum); + if (def != nullptr) { + stage = def->part1; + } + } + const ItemID iconFallback = ResolveSongMessageIcon(rgEnum, stage); + if (stage != RG_NONE) { + const auto& nm = Rando::StaticData::RetrieveItem(stage).GetName(); + CustomMessage getItemText(nm.GetEnglish(), nm.GetGerman(), nm.GetFrench(), TEXTBOX_TYPE_BLUE, + TEXTBOX_POS_BOTTOM); + getItemText.Format(iconFallback); + msg = getItemText; + return; + } + // Stage could not be resolved to a part yet; still show progressive name with a valid vanilla song icon. + if (iconFallback != ITEM_NONE) { + const auto& nm = Rando::StaticData::RetrieveItem(rgEnum).GetName(); + CustomMessage getItemText(nm.GetEnglish(), nm.GetGerman(), nm.GetFrench(), TEXTBOX_TYPE_BLUE, + TEXTBOX_POS_BOTTOM); + getItemText.Format(iconFallback); + msg = getItemText; + return; + } + } + if (IS_RANDO && Rando::SplitSongs::IsSongPart(rgEnum)) { + const auto& songPartItem = Rando::StaticData::RetrieveItem(rgEnum); + const ItemID iconFallback = ResolveSongMessageIcon(rgEnum, rgEnum); const auto& nm = songPartItem.GetName(); CustomMessage getItemText(nm.GetEnglish(), nm.GetGerman(), nm.GetFrench(), TEXTBOX_TYPE_BLUE, TEXTBOX_POS_BOTTOM); - getItemText.Format(iconItemId); + getItemText.Format(iconFallback); msg = getItemText; return; } - CustomMessage name = - CustomMessage(Rando::StaticData::RetrieveItem(static_cast(rgid)).GetName(), TEXTBOX_TYPE_BLUE); - CustomMessage article = CustomMessage( - Rando::StaticData::RetrieveItem(static_cast(rgid)).GetArticle(), TEXTBOX_TYPE_BLUE); + auto& itemForMsg = Rando::StaticData::RetrieveItem(rgEnum); + CustomMessage name = CustomMessage(itemForMsg.GetName(), TEXTBOX_TYPE_BLUE); + CustomMessage article = CustomMessage(itemForMsg.GetArticle(), TEXTBOX_TYPE_BLUE); msg.Replace("[[article]]", article); - msg.Replace("[[color]]", Rando::StaticData::RetrieveItem(static_cast(rgid)).GetColor()); + msg.Replace("[[color]]", itemForMsg.GetColor()); msg.Replace("[[name]]", name); - if (Rando::StaticData::RetrieveItem(static_cast(rgid)).HasCustomIcon()) { + if (itemForMsg.HasCustomIcon()) { msg.AutoFormat(ITEM_CUSTOM); } else { - msg.AutoFormat(); + // Vanilla pause-menu item id: embed a real icon byte so load/draw use gItemIcons (not stale segment data). + const uint16_t pauseIcon = itemForMsg.GetGIEntry()->itemId; + if (pauseIcon < ITEM_CUSTOM && pauseIcon != ITEM_NONE) { + msg.AutoFormat(static_cast(pauseIcon)); + } else { + msg.AutoFormat(); + } } } -void LoadCustomItemIcon(bool displayAsEnglish) { +/** @return true if an OTR icon path was copied into the textbox segment (skip vanilla ITEM_CUSTOM load). */ +static bool LoadCustomItemIcon(bool displayAsEnglish) { + return RefreshCustomItemIconForTextbox(displayAsEnglish, true); +} + +static bool RefreshCustomItemIconForTextbox(bool displayAsEnglish, bool incrementDecodeState) { Player* player = GET_PLAYER(gPlayState); const char* customIcon = nullptr; CustomIconSize iconSize = ICON_SIZE_32; - if (player->getItemEntry.objectId != OBJECT_INVALID) { - RandomizerGet rgid = static_cast(player->getItemEntry.getItemId); + RandomizerGet rgid = RG_NONE; + const GetItemEntry gift = ResolveGiftGetItemEntry(player); + if (gift.modIndex == MOD_RANDOMIZER) { + rgid = RandomizerGetFromPlayerItemEntry(player); + if (rgid != RG_NONE) { + customIcon = Rando::StaticData::RetrieveItem(rgid).GetCustomIcon(); + iconSize = Rando::StaticData::RetrieveItem(rgid).GetCustomIconSize(); + } + } else if (gift.objectId != OBJECT_INVALID) { + rgid = static_cast(gift.getItemId); customIcon = Rando::StaticData::RetrieveItem(rgid).GetCustomIcon(); iconSize = Rando::StaticData::RetrieveItem(rgid).GetCustomIconSize(); } if (customIcon != nullptr) { - static int16_t sIconItem32XOffsets[] = { 74, 74, 74, 54 }; - static int16_t sIconItem24XOffsets[] = { 72, 72, 72, 50 }; - MessageContext* msgCtx = &gPlayState->msgCtx; - uint8_t language = displayAsEnglish ? LANGUAGE_ENG : gSaveContext.language; - if (iconSize == ICON_SIZE_32) { - R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem32XOffsets[language]; - R_TEXTBOX_ICON_YPOS = (R_TEXTBOX_Y + 10) + 6; - R_TEXTBOX_ICON_SIZE = 32; - } else { - R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem24XOffsets[language]; - R_TEXTBOX_ICON_YPOS = (R_TEXTBOX_Y + 10) + 10; - R_TEXTBOX_ICON_SIZE = 24; - } - strcpy((char*)((uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE), customIcon); + g_LastDecodedCustomIconPath = customIcon; + g_LastDecodedCustomIconSize = iconSize; + ApplyCustomIconPathToTextbox(customIcon, iconSize, displayAsEnglish, incrementDecodeState); + return true; + } + if (!incrementDecodeState && g_LastDecodedCustomIconPath != nullptr) { + ApplyCustomIconPathToTextbox(g_LastDecodedCustomIconPath, g_LastDecodedCustomIconSize, displayAsEnglish, false); + return true; + } + if (incrementDecodeState) { + g_LastDecodedCustomIconPath = nullptr; + } + return false; +} + +static void ApplyCustomIconPathToTextbox(const char* customIcon, CustomIconSize iconSize, bool displayAsEnglish, + bool incrementDecodeState) { + static int16_t sIconItem32XOffsets[] = { 74, 74, 74, 54 }; + static int16_t sIconItem24XOffsets[] = { 72, 72, 72, 50 }; + MessageContext* msgCtx = &gPlayState->msgCtx; + uint8_t language = displayAsEnglish ? LANGUAGE_ENG : gSaveContext.language; + if (iconSize == ICON_SIZE_32) { + R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem32XOffsets[language]; + R_TEXTBOX_ICON_YPOS = (R_TEXTBOX_Y + 10) + 6; + R_TEXTBOX_ICON_SIZE = 32; + } else { + R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem24XOffsets[language]; + R_TEXTBOX_ICON_YPOS = (R_TEXTBOX_Y + 10) + 10; + R_TEXTBOX_ICON_SIZE = 24; + } + strcpy((char*)((uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE), customIcon); + if (incrementDecodeState) { msgCtx->msgBufPos++; msgCtx->choiceNum = 1; } } -void DrawCustomItemIcon(Gfx** p) { +static void DrawTextboxItemIconFromSegment(Gfx** p) { Gfx* gfx = *p; MessageContext* msgCtx = &gPlayState->msgCtx; - Player* player = GET_PLAYER(gPlayState); - CustomIconSize iconSize = ICON_SIZE_32; - if (player->getItemEntry.objectId != OBJECT_INVALID) { - RandomizerGet rgid = static_cast(player->getItemEntry.getItemId); - iconSize = Rando::StaticData::RetrieveItem(rgid).GetCustomIconSize(); - } - if (iconSize == ICON_SIZE_24) { + if (R_TEXTBOX_ICON_SIZE == 24) { gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, G_IM_SIZ_32b, 24, 24, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); @@ -199,10 +459,11 @@ void DrawCustomItemIcon(Gfx** p) { void BuildItemMessage(u16* textId, bool* loadFromMessageTable) { Player* player = GET_PLAYER(gPlayState); CustomMessage msg; + const GetItemEntry giftEntry = ResolveGiftGetItemEntry(player); - if (player->getItemEntry.getItemId == RG_ICE_TRAP) { - Rando::Traps::BuildIceTrapMessage(msg, player->getItemEntry); - } else if (player->getItemEntry.getItemId == RG_TRIFORCE_PIECE) { + if (giftEntry.modIndex == MOD_RANDOMIZER && giftEntry.getItemId == RG_ICE_TRAP) { + Rando::Traps::BuildIceTrapMessage(msg, giftEntry); + } else if (giftEntry.modIndex == MOD_RANDOMIZER && giftEntry.getItemId == RG_TRIFORCE_PIECE) { BuildTriforcePieceMessage(msg); } else { BuildCustomItemMessage(player, msg); @@ -300,6 +561,10 @@ void BuildSmallKeyMessage(uint16_t* textId, bool* loadFromMessageTable) { } void RegisterItemMessages() { + if (CVarGetInteger(CVAR_DEVELOPER_TOOLS("LogTextboxItemIcons"), 0) != 0) { + SPDLOG_INFO("[TextboxItemIcon] Logging is ON (menu: Dev Tools → General → Log textbox item icons). Watch for " + "decode + DRAW lines when the textbox parses an icon control code."); + } COND_ID_HOOK(OnOpenText, TEXT_RANDOMIZER_CUSTOM_ITEM, IS_RANDO, BuildItemMessage); COND_ID_HOOK(OnOpenText, TEXT_ITEM_DUNGEON_MAP, DUNGEON_ITEMS_CAN_BE_OUTSIDE_DUNGEON(RSK_SHUFFLE_MAPANDCOMPASS), BuildMapMessage); @@ -315,19 +580,4 @@ void RegisterItemMessages() { BuildSmallKeyMessage); } -static RegisterShipInitFunc initFunc(RegisterItemMessages, { "IS_RANDO" }); - -void RegisterCustomIconHooks() { - COND_VB_SHOULD(VB_LOAD_ITEM_ICON, IS_RANDO, { - if (*should == false) { - LoadCustomItemIcon(static_cast(va_arg(args, int))); - } - }); - COND_VB_SHOULD(VB_DRAW_ITEM_ICON, IS_RANDO, { - if (*should == false) { - DrawCustomItemIcon(va_arg(args, Gfx**)); - } - }); -} - -static RegisterShipInitFunc customIconInitFunc(RegisterCustomIconHooks, { "IS_RANDO" }); \ No newline at end of file +static RegisterShipInitFunc initFunc(RegisterItemMessages, { "IS_RANDO" }); \ No newline at end of file diff --git a/soh/soh/Enhancements/randomizer/SeedContext.cpp b/soh/soh/Enhancements/randomizer/SeedContext.cpp index 0cd00b768e1..0049df11ddb 100644 --- a/soh/soh/Enhancements/randomizer/SeedContext.cpp +++ b/soh/soh/Enhancements/randomizer/SeedContext.cpp @@ -8,6 +8,7 @@ #include "settings.h" #include "rando_hash.h" #include "fishsanity.h" +#include "split_songs.h" #include "macros.h" #include "3drando/hints.hpp" #include "soh/util.h" @@ -401,6 +402,18 @@ GetItemEntry Context::GetFinalGIEntry(const RandomizerCheck rc, const bool check return ItemTableManager::Instance->RetrieveItemEntry(MOD_NONE, GI_RUPEE_BLUE); } GetItemEntry giEntry = itemLoc->GetPlacedItem().GetGIEntry_Copy(); + if (SplitSongs::IsProgressiveSong(itemLoc->GetPlacedRandomizerGet())) { + RandomizerGet stage = SplitSongs::ResolveProgressiveSongStage(itemLoc->GetPlacedRandomizerGet()); + if (stage == RG_NONE) { + const SplitSongDef* def = SplitSongs::GetSongDefFromProgressive(itemLoc->GetPlacedRandomizerGet()); + if (def != nullptr) { + stage = def->part1; + } + } + if (stage != RG_NONE) { + giEntry = StaticData::RetrieveItem(stage).GetGIEntry_Copy(); + } + } if (overrides.contains(rc)) { const auto fakeGiEntry = StaticData::RetrieveItem(overrides[rc].LooksLike()).GetGIEntry(); giEntry.gid = fakeGiEntry->gid; diff --git a/soh/soh/Enhancements/randomizer/ShuffleSongs.cpp b/soh/soh/Enhancements/randomizer/ShuffleSongs.cpp index 8bece13a1b5..8ac43a1f6c6 100644 --- a/soh/soh/Enhancements/randomizer/ShuffleSongs.cpp +++ b/soh/soh/Enhancements/randomizer/ShuffleSongs.cpp @@ -8,18 +8,18 @@ void Rando::StaticData::RegisterSongLocations() { return; registered = true; // clang-format off - locationTable[RC_SHEIK_IN_FOREST] = Location::Base(RC_SHEIK_IN_FOREST, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_SACRED_FOREST_MEADOW, 0x00, "Sheik in Forest", "Sheik in Forest", RHT_SHEIK_IN_FOREST, RG_MINUET_OF_FOREST, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_MINUET_OF_FOREST), true); - locationTable[RC_SHEIK_IN_CRATER] = Location::Base(RC_SHEIK_IN_CRATER, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_DEATH_MOUNTAIN_CRATER, 0x00, "Sheik in Crater", "Sheik in Crater", RHT_SHEIK_IN_CRATER, RG_BOLERO_OF_FIRE, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_BOLERO_OF_FIRE), true); - locationTable[RC_SHEIK_IN_ICE_CAVERN] = Location::Base(RC_SHEIK_IN_ICE_CAVERN, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_ICE_CAVERN, 0x00, "Sheik in Ice Cavern", "Sheik in Ice Cavern", RHT_SHEIK_IN_ICE_CAVERN, RG_SERENADE_OF_WATER, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_SERENADE_OF_WATER), true); - locationTable[RC_SHEIK_AT_COLOSSUS] = Location::Base(RC_SHEIK_AT_COLOSSUS, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_DESERT_COLOSSUS, 0x00, "Sheik at Colossus", "Sheik at Colossus", RHT_SHEIK_AT_COLOSSUS, RG_REQUIEM_OF_SPIRIT, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_REQUIEM_OF_SPIRIT), true); - locationTable[RC_SHEIK_IN_KAKARIKO] = Location::Base(RC_SHEIK_IN_KAKARIKO, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_KAKARIKO_VILLAGE, 0x00, "Sheik in Kakariko", "Sheik in Kakariko", RHT_SHEIK_IN_KAKARIKO, RG_NOCTURNE_OF_SHADOW, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_BONGO_BONGO_ESCAPED_FROM_WELL), true); - locationTable[RC_SHEIK_AT_TEMPLE] = Location::Base(RC_SHEIK_AT_TEMPLE, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_TEMPLE_OF_TIME, 0x00, "Sheik at Temple", "Sheik at Temple", RHT_SHEIK_AT_TEMPLE, RG_PRELUDE_OF_LIGHT, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_PRELUDE_OF_LIGHT), true); - locationTable[RC_SONG_FROM_IMPA] = Location::Base(RC_SONG_FROM_IMPA, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, RCAREA_HYRULE_CASTLE, ACTOR_ID_MAX, SCENE_ID_MAX, 0x00, "Song from Impa", "Song from Impa", RHT_SONG_FROM_IMPA, RG_ZELDAS_LULLABY, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_ZELDAS_LULLABY), true); - locationTable[RC_SONG_FROM_MALON] = Location::Base(RC_SONG_FROM_MALON, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_LON_LON_RANCH, 0x00, "Song from Malon", "Song from Malon", RHT_SONG_FROM_MALON, RG_EPONAS_SONG, SpoilerCollectionCheck::RandomizerInf(RAND_INF_LEARNED_EPONA_SONG), true); - locationTable[RC_SONG_FROM_SARIA] = Location::Base(RC_SONG_FROM_SARIA, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_SACRED_FOREST_MEADOW, 0x00, "Song from Saria", "Song from Saria", RHT_SONG_FROM_SARIA, RG_SARIAS_SONG, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_SARIAS_SONG), true); - locationTable[RC_SONG_FROM_ROYAL_FAMILYS_TOMB] = Location::Base(RC_SONG_FROM_ROYAL_FAMILYS_TOMB, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_ROYAL_FAMILYS_TOMB, 0x00, "Song from Royal Family's Tomb", "Song from Royal Family's Tomb", RHT_SONG_FROM_ROYAL_FAMILYS_TOMB, RG_SUNS_SONG, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_SUNS_SONG), true); - locationTable[RC_SONG_FROM_OCARINA_OF_TIME] = Location::Base(RC_SONG_FROM_OCARINA_OF_TIME, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_HYRULE_FIELD, 0x00, "Song from Ocarina of Time", "Song from Ocarina of Time", RHT_SONG_FROM_OCARINA_OF_TIME, RG_SONG_OF_TIME, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_SONG_OF_TIME), true); - locationTable[RC_SONG_FROM_WINDMILL] = Location::Base(RC_SONG_FROM_WINDMILL, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, RCAREA_KAKARIKO_VILLAGE, ACTOR_ID_MAX, SCENE_WINDMILL_AND_DAMPES_GRAVE, 0x00, "Song from Windmill", "Song from Windmill", RHT_SONG_FROM_WINDMILL, RG_SONG_OF_STORMS, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_SONG_OF_STORMS), true); + locationTable[RC_SHEIK_IN_FOREST] = Location::Base(RC_SHEIK_IN_FOREST, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_SACRED_FOREST_MEADOW, 0x00, "Sheik in Forest", "Sheik in Forest", RHT_SHEIK_IN_FOREST, RG_PROGRESSIVE_MINUET_OF_FOREST, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_MINUET_OF_FOREST), true); + locationTable[RC_SHEIK_IN_CRATER] = Location::Base(RC_SHEIK_IN_CRATER, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_DEATH_MOUNTAIN_CRATER, 0x00, "Sheik in Crater", "Sheik in Crater", RHT_SHEIK_IN_CRATER, RG_PROGRESSIVE_BOLERO_OF_FIRE, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_BOLERO_OF_FIRE), true); + locationTable[RC_SHEIK_IN_ICE_CAVERN] = Location::Base(RC_SHEIK_IN_ICE_CAVERN, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_ICE_CAVERN, 0x00, "Sheik in Ice Cavern", "Sheik in Ice Cavern", RHT_SHEIK_IN_ICE_CAVERN, RG_PROGRESSIVE_SERENADE_OF_WATER, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_SERENADE_OF_WATER), true); + locationTable[RC_SHEIK_AT_COLOSSUS] = Location::Base(RC_SHEIK_AT_COLOSSUS, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_DESERT_COLOSSUS, 0x00, "Sheik at Colossus", "Sheik at Colossus", RHT_SHEIK_AT_COLOSSUS, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_REQUIEM_OF_SPIRIT), true); + locationTable[RC_SHEIK_IN_KAKARIKO] = Location::Base(RC_SHEIK_IN_KAKARIKO, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_KAKARIKO_VILLAGE, 0x00, "Sheik in Kakariko", "Sheik in Kakariko", RHT_SHEIK_IN_KAKARIKO, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_BONGO_BONGO_ESCAPED_FROM_WELL), true); + locationTable[RC_SHEIK_AT_TEMPLE] = Location::Base(RC_SHEIK_AT_TEMPLE, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_TEMPLE_OF_TIME, 0x00, "Sheik at Temple", "Sheik at Temple", RHT_SHEIK_AT_TEMPLE, RG_PROGRESSIVE_PRELUDE_OF_LIGHT, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_PRELUDE_OF_LIGHT), true); + locationTable[RC_SONG_FROM_IMPA] = Location::Base(RC_SONG_FROM_IMPA, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, RCAREA_HYRULE_CASTLE, ACTOR_ID_MAX, SCENE_ID_MAX, 0x00, "Song from Impa", "Song from Impa", RHT_SONG_FROM_IMPA, RG_PROGRESSIVE_ZELDAS_LULLABY, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_ZELDAS_LULLABY), true); + locationTable[RC_SONG_FROM_MALON] = Location::Base(RC_SONG_FROM_MALON, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_LON_LON_RANCH, 0x00, "Song from Malon", "Song from Malon", RHT_SONG_FROM_MALON, RG_PROGRESSIVE_EPONAS_SONG, SpoilerCollectionCheck::RandomizerInf(RAND_INF_LEARNED_EPONA_SONG), true); + locationTable[RC_SONG_FROM_SARIA] = Location::Base(RC_SONG_FROM_SARIA, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_SACRED_FOREST_MEADOW, 0x00, "Song from Saria", "Song from Saria", RHT_SONG_FROM_SARIA, RG_PROGRESSIVE_SARIAS_SONG, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_SARIAS_SONG), true); + locationTable[RC_SONG_FROM_ROYAL_FAMILYS_TOMB] = Location::Base(RC_SONG_FROM_ROYAL_FAMILYS_TOMB, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_ROYAL_FAMILYS_TOMB, 0x00, "Song from Royal Family's Tomb", "Song from Royal Family's Tomb", RHT_SONG_FROM_ROYAL_FAMILYS_TOMB, RG_PROGRESSIVE_SUNS_SONG, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_SUNS_SONG), true); + locationTable[RC_SONG_FROM_OCARINA_OF_TIME] = Location::Base(RC_SONG_FROM_OCARINA_OF_TIME, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, ACTOR_ID_MAX, SCENE_HYRULE_FIELD, 0x00, "Song from Ocarina of Time", "Song from Ocarina of Time", RHT_SONG_FROM_OCARINA_OF_TIME, RG_PROGRESSIVE_SONG_OF_TIME, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_SONG_OF_TIME), true); + locationTable[RC_SONG_FROM_WINDMILL] = Location::Base(RC_SONG_FROM_WINDMILL, RCQUEST_BOTH, RCTYPE_SONG_LOCATION, RCAREA_KAKARIKO_VILLAGE, ACTOR_ID_MAX, SCENE_WINDMILL_AND_DAMPES_GRAVE, 0x00, "Song from Windmill", "Song from Windmill", RHT_SONG_FROM_WINDMILL, RG_PROGRESSIVE_SONG_OF_STORMS, SpoilerCollectionCheck::EventChkInf(EVENTCHKINF_LEARNED_SONG_OF_STORMS), true); // clang-format-on } diff --git a/soh/soh/Enhancements/randomizer/Traps.cpp b/soh/soh/Enhancements/randomizer/Traps.cpp index aefae55970c..4b383ec075d 100644 --- a/soh/soh/Enhancements/randomizer/Traps.cpp +++ b/soh/soh/Enhancements/randomizer/Traps.cpp @@ -1430,6 +1430,8 @@ Text Rando::Traps::GetTrapName(uint16_t id) { RandomizerGet rg = static_cast(id); if (const Rando::SplitSongDef* splitDef = Rando::SplitSongs::GetSongDefFromPart(rg)) { id = static_cast(splitDef->fullSong); + } else if (const Rando::SplitSongDef* progDef = Rando::SplitSongs::GetSongDefFromProgressive(rg)) { + id = static_cast(progDef->fullSong); } if (trickNameTable[id].empty()) { diff --git a/soh/soh/Enhancements/randomizer/item.cpp b/soh/soh/Enhancements/randomizer/item.cpp index fc3a7ad5028..f6dbf340a1e 100644 --- a/soh/soh/Enhancements/randomizer/item.cpp +++ b/soh/soh/Enhancements/randomizer/item.cpp @@ -389,6 +389,12 @@ std::shared_ptr Item::GetGIEntry() const { // NOLINT(*-no-recursio case RG_PROGRESSIVE_NOCTURNE_OF_SHADOW: case RG_PROGRESSIVE_PRELUDE_OF_LIGHT: actual = SplitSongs::ResolveProgressiveSongStage(randomizerGet); + if (actual == RG_NONE) { + const SplitSongDef* def = SplitSongs::GetSongDefFromProgressive(randomizerGet); + if (def != nullptr) { + actual = def->part1; + } + } break; case RG_PROGRESSIVE_BOMBCHU_BAG: if (OTRGlobals::Instance->gRandoContext->GetOption(RSK_BOMBCHU_BAG).Is(RO_BOMBCHU_BAG_SINGLE)) { diff --git a/soh/soh/Enhancements/randomizer/item_list.cpp b/soh/soh/Enhancements/randomizer/item_list.cpp index d4068c3ebb9..d06dddef122 100644 --- a/soh/soh/Enhancements/randomizer/item_list.cpp +++ b/soh/soh/Enhancements/randomizer/item_list.cpp @@ -90,7 +90,7 @@ void Rando::StaticData::InitItemTable() { itemTable[RG_BOTTLE_WITH_MILK] = Item(RG_BOTTLE_WITH_MILK, Text{ "Bottle with Milk", "Bouteille avec du Lait", "Flasche mit Milch" }, ITEMTYPE_ITEM, GI_MILK_BOTTLE, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_MILK, ITEM_MILK_BOTTLE, OBJECT_GI_MILK, GID_MILK, 0x98, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_RED_POTION] = Item(RG_BOTTLE_WITH_RED_POTION, Text{ "Bottle with Red Potion", "Bouteille avec une Potion Rouge", "Flasche mit rotem Elixier" }, ITEMTYPE_ITEM, 0x8C, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_RED_POTION, RG_BOTTLE_WITH_RED_POTION, OBJECT_GI_LIQUID, GID_POTION_RED, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_GREEN_POTION] = Item(RG_BOTTLE_WITH_GREEN_POTION, Text{ "Bottle with Green Potion", "Bouteille avec une Potion Verte", "Flasche mit grünem Elixier" }, ITEMTYPE_ITEM, 0x8D, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_GREEN_POTION, RG_BOTTLE_WITH_GREEN_POTION, OBJECT_GI_LIQUID, GID_POTION_GREEN, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); - itemTable[RG_BOTTLE_WITH_BLUE_POTION] = Item(RG_BOTTLE_WITH_BLUE_POTION, Text{ "Bottle with Blue Potion", "Bouteille avec une Potion Bleue", "Flasche mit blauem Elixier" }, ITEMTYPE_ITEM, 0x8E, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_BLUE_POTION, RG_BOTTLE_WITH_BLUE_POTION, OBJECT_GI_LIQUID, GID_POTION_BLUE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); + itemTable[RG_BOTTLE_WITH_BLUE_POTION] = Item(RG_BOTTLE_WITH_BLUE_POTION, Text{ "Bottle with Blue Potion", "Bouteille avec une Potion Bleue", "Flasche mit blauem Elixier" }, ITEMTYPE_ITEM, 0x8E, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_BLUE_POTION, RG_BOTTLE_WITH_BLUE_POTION, OBJECT_GI_LIQUID, GID_POTION_BLUE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}).CustomIcon(gItemIconBottlePotionBlueTex); itemTable[RG_BOTTLE_WITH_FAIRY] = Item(RG_BOTTLE_WITH_FAIRY, Text{ "Bottle with Fairy", "Bouteille avec une Fée", "Flasche mit Fee" }, ITEMTYPE_ITEM, 0x8F, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_FAIRY, RG_BOTTLE_WITH_FAIRY, OBJECT_GI_BOTTLE, GID_BOTTLE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_FISH] = Item(RG_BOTTLE_WITH_FISH, Text{ "Bottle with Fish", "Bouteille avec un Poisson", "Flasche mit Fisch" }, ITEMTYPE_ITEM, 0x90, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_FISH, RG_BOTTLE_WITH_FISH, OBJECT_GI_FISH, GID_FISH, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_BLUE_FIRE] = Item(RG_BOTTLE_WITH_BLUE_FIRE, Text{ "Bottle with Blue Fire", "Bouteille avec une Flamme Bleue", "Flasche mit blauem Feuer" }, ITEMTYPE_ITEM, 0x91, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_BLUE_FIRE, RG_BOTTLE_WITH_BLUE_FIRE, OBJECT_GI_FIRE, GID_BLUE_FIRE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); @@ -258,25 +258,25 @@ void Rando::StaticData::InitItemTable() { itemTable[RG_FISHING_HOLE_KEY] = Item(RG_FISHING_HOLE_KEY, Text{ "Fishing Hole Key", "Clé de l'Étang", "Schlüssel für den Fischweiher" }, ITEMTYPE_ITEM, GI_DOOR_KEY, true, LOGIC_FISHING_HOLE_KEY, RHT_OVERWORLD_KEY, RG_FISHING_HOLE_KEY, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "la ", "den "}).CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_FISHING_HOLE_KEY].SetCustomDrawFunc(Randomizer_DrawOverworldKey); // Key Rings - itemTable[RG_FOREST_TEMPLE_KEY_RING] = Item(RG_FOREST_TEMPLE_KEY_RING, Text{ "Forest Temple Key Ring", "Trousseau du Temple de la Forêt", "Schlüsselbund für den Waldtempel" }, ITEMTYPE_SMALLKEY, 0xD5, true, LOGIC_FOREST_TEMPLE_KEYS, RHT_FOREST_TEMPLE_KEY_RING, RG_FOREST_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%g"); + itemTable[RG_FOREST_TEMPLE_KEY_RING] = Item(RG_FOREST_TEMPLE_KEY_RING, Text{ "Forest Temple Key Ring", "Trousseau du Temple de la Forêt", "Schlüsselbund für den Waldtempel" }, ITEMTYPE_SMALLKEY, 0xD5, true, LOGIC_FOREST_TEMPLE_KEYS, RHT_FOREST_TEMPLE_KEY_RING, RG_FOREST_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%g").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_FOREST_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_FIRE_TEMPLE_KEY_RING] = Item(RG_FIRE_TEMPLE_KEY_RING, Text{ "Fire Temple Key Ring", "Trousseau du Temple du Feu", "Schlüsselbund für den Feuertempel" }, ITEMTYPE_SMALLKEY, 0xD6, true, LOGIC_FIRE_TEMPLE_KEYS, RHT_FIRE_TEMPLE_KEY_RING, RG_FIRE_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%r"); + itemTable[RG_FIRE_TEMPLE_KEY_RING] = Item(RG_FIRE_TEMPLE_KEY_RING, Text{ "Fire Temple Key Ring", "Trousseau du Temple du Feu", "Schlüsselbund für den Feuertempel" }, ITEMTYPE_SMALLKEY, 0xD6, true, LOGIC_FIRE_TEMPLE_KEYS, RHT_FIRE_TEMPLE_KEY_RING, RG_FIRE_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%r").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_FIRE_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_WATER_TEMPLE_KEY_RING] = Item(RG_WATER_TEMPLE_KEY_RING, Text{ "Water Temple Key Ring", "Trousseau du Temple de l'Eau", "Schlüsselbund für den Wassertempel" }, ITEMTYPE_SMALLKEY, 0xD7, true, LOGIC_WATER_TEMPLE_KEYS, RHT_WATER_TEMPLE_KEY_RING, RG_WATER_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%b"); + itemTable[RG_WATER_TEMPLE_KEY_RING] = Item(RG_WATER_TEMPLE_KEY_RING, Text{ "Water Temple Key Ring", "Trousseau du Temple de l'Eau", "Schlüsselbund für den Wassertempel" }, ITEMTYPE_SMALLKEY, 0xD7, true, LOGIC_WATER_TEMPLE_KEYS, RHT_WATER_TEMPLE_KEY_RING, RG_WATER_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%b").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_WATER_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_SPIRIT_TEMPLE_KEY_RING] = Item(RG_SPIRIT_TEMPLE_KEY_RING, Text{ "Spirit Temple Key Ring", "Trousseau du Temple de l'Esprit", "Schlüsselbund für den Geistertempel" }, ITEMTYPE_SMALLKEY, 0xD8, true, LOGIC_SPIRIT_TEMPLE_KEYS, RHT_SPIRIT_TEMPLE_KEY_RING, RG_SPIRIT_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y"); + itemTable[RG_SPIRIT_TEMPLE_KEY_RING] = Item(RG_SPIRIT_TEMPLE_KEY_RING, Text{ "Spirit Temple Key Ring", "Trousseau du Temple de l'Esprit", "Schlüsselbund für den Geistertempel" }, ITEMTYPE_SMALLKEY, 0xD8, true, LOGIC_SPIRIT_TEMPLE_KEYS, RHT_SPIRIT_TEMPLE_KEY_RING, RG_SPIRIT_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_SPIRIT_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_SHADOW_TEMPLE_KEY_RING] = Item(RG_SHADOW_TEMPLE_KEY_RING, Text{ "Shadow Temple Key Ring", "Trousseau du Temple de l'Ombre", "Schlüsselbund für den Schattentempel" }, ITEMTYPE_SMALLKEY, 0xD9, true, LOGIC_SHADOW_TEMPLE_KEYS, RHT_SHADOW_TEMPLE_KEY_RING, RG_SHADOW_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%p"); + itemTable[RG_SHADOW_TEMPLE_KEY_RING] = Item(RG_SHADOW_TEMPLE_KEY_RING, Text{ "Shadow Temple Key Ring", "Trousseau du Temple de l'Ombre", "Schlüsselbund für den Schattentempel" }, ITEMTYPE_SMALLKEY, 0xD9, true, LOGIC_SHADOW_TEMPLE_KEYS, RHT_SHADOW_TEMPLE_KEY_RING, RG_SHADOW_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%p").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_SHADOW_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_BOTTOM_OF_THE_WELL_KEY_RING] = Item(RG_BOTTOM_OF_THE_WELL_KEY_RING, Text{ "Bottom of the Well Key Ring", "Trousseau du Puits", "Schlüsselbund für den Grund des Brunnens" }, ITEMTYPE_SMALLKEY, 0xDA, true, LOGIC_BOTTOM_OF_THE_WELL_KEYS, RHT_BOTTOM_OF_THE_WELL_KEY_RING, RG_BOTTOM_OF_THE_WELL_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%p"); + itemTable[RG_BOTTOM_OF_THE_WELL_KEY_RING] = Item(RG_BOTTOM_OF_THE_WELL_KEY_RING, Text{ "Bottom of the Well Key Ring", "Trousseau du Puits", "Schlüsselbund für den Grund des Brunnens" }, ITEMTYPE_SMALLKEY, 0xDA, true, LOGIC_BOTTOM_OF_THE_WELL_KEYS, RHT_BOTTOM_OF_THE_WELL_KEY_RING, RG_BOTTOM_OF_THE_WELL_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%p").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_BOTTOM_OF_THE_WELL_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_GERUDO_TRAINING_GROUND_KEY_RING] = Item(RG_GERUDO_TRAINING_GROUND_KEY_RING, Text{ "Training Ground Key Ring", "Trousseau du Gymnase Gerudo", "Schlüsselbund für das Gerudo-Trainingsgelände" }, ITEMTYPE_SMALLKEY, 0xDB, true, LOGIC_GERUDO_TRAINING_GROUND_KEYS, RHT_GERUDO_TRAINING_GROUND_KEY_RING, RG_GERUDO_TRAINING_GROUND_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y"); + itemTable[RG_GERUDO_TRAINING_GROUND_KEY_RING] = Item(RG_GERUDO_TRAINING_GROUND_KEY_RING, Text{ "Training Ground Key Ring", "Trousseau du Gymnase Gerudo", "Schlüsselbund für das Gerudo-Trainingsgelände" }, ITEMTYPE_SMALLKEY, 0xDB, true, LOGIC_GERUDO_TRAINING_GROUND_KEYS, RHT_GERUDO_TRAINING_GROUND_KEY_RING, RG_GERUDO_TRAINING_GROUND_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_GERUDO_TRAINING_GROUND_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_GERUDO_FORTRESS_KEY_RING] = Item(RG_GERUDO_FORTRESS_KEY_RING, Text{ "Gerudo Fortress Key Ring", "Trousseau du Repaire des Voleurs", "Schlüsselbund für die Gerudo-Festung" }, ITEMTYPE_FORTRESS_SMALLKEY, 0xDC, true, LOGIC_GERUDO_FORTRESS_KEYS, RHT_GERUDO_FORTRESS_KEY_RING, RG_GERUDO_FORTRESS_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y"); + itemTable[RG_GERUDO_FORTRESS_KEY_RING] = Item(RG_GERUDO_FORTRESS_KEY_RING, Text{ "Gerudo Fortress Key Ring", "Trousseau du Repaire des Voleurs", "Schlüsselbund für die Gerudo-Festung" }, ITEMTYPE_FORTRESS_SMALLKEY, 0xDC, true, LOGIC_GERUDO_FORTRESS_KEYS, RHT_GERUDO_FORTRESS_KEY_RING, RG_GERUDO_FORTRESS_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_GERUDO_FORTRESS_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_GANONS_CASTLE_KEY_RING] = Item(RG_GANONS_CASTLE_KEY_RING, Text{ "Ganon's Castle Key Ring", "Trousseau du Château de Ganon", "Schlüsselbund für Ganons Schloß" }, ITEMTYPE_SMALLKEY, 0xDD, true, LOGIC_GANONS_CASTLE_KEYS, RHT_GANONS_CASTLE_KEY_RING, RG_GANONS_CASTLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%r"); + itemTable[RG_GANONS_CASTLE_KEY_RING] = Item(RG_GANONS_CASTLE_KEY_RING, Text{ "Ganon's Castle Key Ring", "Trousseau du Château de Ganon", "Schlüsselbund für Ganons Schloß" }, ITEMTYPE_SMALLKEY, 0xDD, true, LOGIC_GANONS_CASTLE_KEYS, RHT_GANONS_CASTLE_KEY_RING, RG_GANONS_CASTLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%r").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_GANONS_CASTLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_TREASURE_GAME_KEY_RING] = Item(RG_TREASURE_GAME_KEY_RING, Text{ "Chest Game Key Ring", "Trousseau du jeu la Chasse-aux-Trésors", "Schlüsselbund für das Truhenspiel" }, ITEMTYPE_SMALLKEY, 0xDE, true, LOGIC_TREASURE_GAME_KEYS, RHT_TREASURE_GAME_KEY_RING, RG_TREASURE_GAME_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}); + itemTable[RG_TREASURE_GAME_KEY_RING] = Item(RG_TREASURE_GAME_KEY_RING, Text{ "Chest Game Key Ring", "Trousseau du jeu la Chasse-aux-Trésors", "Schlüsselbund für das Truhenspiel" }, ITEMTYPE_SMALLKEY, 0xDE, true, LOGIC_TREASURE_GAME_KEYS, RHT_TREASURE_GAME_KEY_RING, RG_TREASURE_GAME_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}).CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_TREASURE_GAME_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); // Dungeon Rewards itemTable[RG_KOKIRI_EMERALD] = Item(RG_KOKIRI_EMERALD, Text{ "Kokiri's Emerald", "Émeraude Kokiri", "Kokiri-Smaragd" }, ITEMTYPE_DUNGEONREWARD, 0xCB, true, LOGIC_KOKIRI_EMERALD, RHT_KOKIRI_EMERALD, ITEM_KOKIRI_EMERALD, OBJECT_GI_JEWEL, GID_KOKIRI_EMERALD, 0x80, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"the ", "l'", "den "}, "%g"); diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index d2c840730e4..2151c3085e1 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -4646,7 +4646,18 @@ extern "C" u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry) { return Return_Item_Entry(giEntry, RG_NONE); } if (Rando::SplitSongs::IsProgressiveSong(item)) { + // Resolve display before applying progression so first pickup shows Part 1 and second shows Part 2. + RandomizerGet displayItem = Rando::SplitSongs::ResolveProgressiveSongStage(item); + if (displayItem == RG_NONE) { + const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromProgressive(item); + if (def != nullptr) { + displayItem = def->part1; + } + } Rando::SplitSongs::OnProgressiveSongReceived(item); + if (displayItem != RG_NONE) { + return Return_Item_Entry(Rando::StaticData::RetrieveItem(displayItem).GetGIEntry_Copy(), RG_NONE); + } return Return_Item_Entry(giEntry, RG_NONE); } diff --git a/soh/soh/SohGui/SohMenuDevTools.cpp b/soh/soh/SohGui/SohMenuDevTools.cpp index c5b88ce8fbc..dfbb0754f23 100644 --- a/soh/soh/SohGui/SohMenuDevTools.cpp +++ b/soh/soh/SohGui/SohMenuDevTools.cpp @@ -78,6 +78,12 @@ void SohMenu::AddMenuDevTools() { AddWidget(path, "Resource logging", WIDGET_CVAR_CHECKBOX) .CVar(CVAR_DEVELOPER_TOOLS("ResourceLogging")) .Options(CheckboxOptions().Tooltip("Logs some resources as XML when they're loaded in binary format.")); + AddWidget(path, "Log textbox item icons", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_DEVELOPER_TOOLS("LogTextboxItemIcons")) + .Options(CheckboxOptions().Tooltip( + "Writes [TextboxItemIcon] decode and DRAW lines to the log when the message system loads or draws a " + "textbox item icon (MESSAGE_ITEM_ICON). Restart after enabling to see the startup confirmation when " + "randomizer hooks register; then open any get-item text that shows an icon.")); AddWidget(path, "Frame Advance", WIDGET_CHECKBOX) .Options(CheckboxOptions().Tooltip( diff --git a/soh/src/code/z_message_PAL.c b/soh/src/code/z_message_PAL.c index ce09fe27168..fc3f0e15350 100644 --- a/soh/src/code/z_message_PAL.c +++ b/soh/src/code/z_message_PAL.c @@ -7,6 +7,7 @@ #include "textures/parameter_static/parameter_static.h" #include "textures/message_static/message_static.h" #include "textures/message_texture_static/message_texture_static.h" +#include "textures/icon_item_static/icon_item_static.h" #include "soh/Enhancements/cosmetics/CosmeticsEditor.h" #include "soh/Enhancements/cosmetics/cosmeticsTypes.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" @@ -15,6 +16,10 @@ #include "soh/SaveManager.h" #include "soh/ResourceManagerHelpers.h" +extern void Randomizer_Message_DecodeLoadItemIcon(PlayState* play, u16 iconToLoad, s32 displayAsEnglishAsInt); +extern void Randomizer_Message_AppendSegmentIconGfx(Gfx** gfxp); +extern void Randomizer_Message_RefreshCustomItemIconAtDraw(s32 displayAsEnglishAsInt); + // #region SOH [NTSC] - Allows custom messages to work on japanese static bool sDisplayNextMessageAsEnglish = false; static u8 sLastLanguage = LANGUAGE_ENG; @@ -837,6 +842,119 @@ f32 sFontWidths[144] = { 14.0f, // ? }; +/** + * Copies the item's OTR icon path into the textbox segment and sets R_TEXTBOX_ICON_*. + * The segment slot is shared with other message data; re-applying at draw time avoids stale or + * clobbered bytes being interpreted as a texture (garbled icon) when decode and draw are far apart. + */ +static void Message_SetTextboxItemIconFromItemId(PlayState* play, u16 itemId, s16 y) { + static s16 sIconItem32XOffsets[] = { 74, 74, 74, 54 }; + static s16 sIconItem24XOffsets[] = { 72, 72, 72, 50 }; + MessageContext* msgCtx = &play->msgCtx; + InterfaceContext* interfaceCtx = &play->interfaceCtx; + u8 language = sDisplayNextMessageAsEnglish ? LANGUAGE_ENG : gSaveContext.language; + const void* iconEntry; + const char* texPath; + size_t pathLen; + u16 id = itemId; + bool isSongIcon; + + if (id == ITEM_DUNGEON_MAP) { + interfaceCtx->mapPalette[30] = 0xFF; + interfaceCtx->mapPalette[31] = 0xFF; + } + if (id >= ARRAY_COUNT(gItemIcons)) { + id = ITEM_STICK; + } + isSongIcon = (id >= ITEM_SONG_MINUET) && (id <= ITEM_SONG_STORMS); + iconEntry = gItemIcons[id]; + texPath = (const char*)iconEntry; + if (texPath == NULL || texPath[0] == '\0') { + texPath = (const char*)gItemIconDekuStickTex; + } + pathLen = strlen(texPath) + 1; + if ((id < ITEM_MEDALLION_FOREST) && !isSongIcon) { + R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem32XOffsets[language]; + R_TEXTBOX_ICON_YPOS = y + 6; + R_TEXTBOX_ICON_SIZE = 32; + memcpy((void*)((uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE), texPath, pathLen); + osSyncPrintf("アイテム32-0\n"); + } else { + R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem24XOffsets[language]; + R_TEXTBOX_ICON_YPOS = y + 10; + R_TEXTBOX_ICON_SIZE = 24; + memcpy((void*)((uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE), texPath, pathLen); + osSyncPrintf("アイテム24=%d (%d) {%d}\n", id, id - ITEM_KOKIRI_EMERALD, 84); + } +} + +static bool Message_GetSongIconTint(u16 itemId, u8* r, u8* g, u8* b) { + switch (itemId) { + case ITEM_SONG_LULLABY: + *r = 224; + *g = 107; + *b = 255; + return true; + case ITEM_SONG_EPONA: + *r = 255; + *g = 195; + *b = 60; + return true; + case ITEM_SONG_SARIA: + *r = 127; + *g = 255; + *b = 137; + return true; + case ITEM_SONG_SUN: + *r = 255; + *g = 255; + *b = 60; + return true; + case ITEM_SONG_TIME: + *r = 119; + *g = 236; + *b = 255; + return true; + case ITEM_SONG_STORMS: + *r = 165; + *g = 165; + *b = 165; + return true; + case ITEM_SONG_MINUET: + *r = 150; + *g = 255; + *b = 100; + return true; + case ITEM_SONG_BOLERO: + *r = 255; + *g = 80; + *b = 40; + return true; + case ITEM_SONG_SERENADE: + *r = 100; + *g = 150; + *b = 255; + return true; + case ITEM_SONG_REQUIEM: + *r = 255; + *g = 160; + *b = 0; + return true; + case ITEM_SONG_NOCTURNE: + *r = 255; + *g = 100; + *b = 255; + return true; + case ITEM_SONG_PRELUDE: + *r = 255; + *g = 240; + *b = 100; + return true; + default: + return false; + } +} + u16 Message_DrawItemIcon(PlayState* play, u16 itemId, Gfx** p, u16 i) { s32 pad; Gfx* gfx = *p; @@ -853,20 +971,52 @@ u16 Message_DrawItemIcon(PlayState* play, u16 itemId, Gfx** p, u16 i) { // Invalidate icon texture as it may have changed from the last time a text box had an icon gSPInvalidateTexCache(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE); - if (GameInteractor_Should(VB_DRAW_ITEM_ICON, itemId < ITEM_CUSTOM, &gfx)) { - if (itemId >= ITEM_MEDALLION_FOREST) { - gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, - G_IM_SIZ_32b, 24, 24, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, - G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); + s32 drawKind = -2; + s32 iconDrawWidth = R_TEXTBOX_ICON_SIZE; + s32 iconDrawHeight = R_TEXTBOX_ICON_SIZE; + + if (itemId < ITEM_CUSTOM) { + if (GameInteractor_Should(VB_DRAW_ITEM_ICON, true, &gfx)) { + u16 iconIdClamped = itemId; + u8 songR; + u8 songG; + u8 songB; + if (iconIdClamped >= ARRAY_COUNT(gItemIcons)) { + iconIdClamped = ITEM_STICK; + } + Message_SetTextboxItemIconFromItemId(play, itemId, (s16)(R_TEXTBOX_Y + 10)); + if (Message_GetSongIconTint(iconIdClamped, &songR, &songG, &songB)) { + gDPSetPrimColor(gfx++, 0, 0, songR, songG, songB, msgCtx->textColorAlpha); + } + if ((iconIdClamped >= ITEM_SONG_MINUET) && (iconIdClamped <= ITEM_SONG_STORMS)) { + gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_IA, + G_IM_SIZ_8b, 16, 24, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, + G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); + drawKind = 16; + iconDrawWidth = 16; + iconDrawHeight = 24; + } else if (iconIdClamped >= ITEM_MEDALLION_FOREST) { + gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, + G_IM_SIZ_32b, 24, 24, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, + G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); + drawKind = 24; + } else { + gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, + G_IM_SIZ_32b, 32, 32, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, + G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); + drawKind = 32; + } } else { - gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, - G_IM_SIZ_32b, 32, 32, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, - G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); + drawKind = -1; } + } else { + Randomizer_Message_RefreshCustomItemIconAtDraw(sDisplayNextMessageAsEnglish ? 1 : 0); + Randomizer_Message_AppendSegmentIconGfx(&gfx); + drawKind = 0; } gSPTextureRectangle(gfx++, (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS) << 2, R_TEXTBOX_ICON_YPOS << 2, - (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS + R_TEXTBOX_ICON_SIZE) << 2, - (R_TEXTBOX_ICON_YPOS + R_TEXTBOX_ICON_SIZE) << 2, G_TX_RENDERTILE, 0, 0, 1 << 10, 1 << 10); + (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS + iconDrawWidth) << 2, + (R_TEXTBOX_ICON_YPOS + iconDrawHeight) << 2, G_TX_RENDERTILE, 0, 0, 1 << 10, 1 << 10); gDPPipeSync(gfx++); gDPSetCombineLERP(gfx++, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0); @@ -1640,33 +1790,9 @@ void Message_DrawText(PlayState* play, Gfx** gfxP) { } void Message_LoadItemIcon(PlayState* play, u16 itemId, s16 y) { - static s16 sIconItem32XOffsets[] = { 74, 74, 74, 54 }; - static s16 sIconItem24XOffsets[] = { 72, 72, 72, 50 }; MessageContext* msgCtx = &play->msgCtx; - InterfaceContext* interfaceCtx = &play->interfaceCtx; - u8 language = sDisplayNextMessageAsEnglish ? LANGUAGE_ENG : gSaveContext.language; - if (itemId == ITEM_DUNGEON_MAP) { - interfaceCtx->mapPalette[30] = 0xFF; - interfaceCtx->mapPalette[31] = 0xFF; - } - if (itemId < ITEM_MEDALLION_FOREST) { - R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem32XOffsets[language]; - R_TEXTBOX_ICON_YPOS = y + 6; - R_TEXTBOX_ICON_SIZE = 32; - memcpy((uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, gItemIcons[itemId], - strlen(gItemIcons[itemId]) + 1); - // "Item 32-0" - osSyncPrintf("アイテム32-0\n"); - } else { - R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem24XOffsets[language]; - R_TEXTBOX_ICON_YPOS = y + 10; - R_TEXTBOX_ICON_SIZE = 24; - memcpy((uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, gItemIcons[itemId], - strlen(gItemIcons[itemId]) + 1); - // "Item 24" - osSyncPrintf("アイテム24=%d (%d) {%d}\n", itemId, itemId - ITEM_KOKIRI_EMERALD, 84); - } + Message_SetTextboxItemIconFromItemId(play, itemId, y); msgCtx->msgBufPos++; msgCtx->choiceNum = 1; } @@ -1924,6 +2050,7 @@ void Message_DecodeJPN(PlayState* play) { s16 digits[4]; u16 value; s16 decodedBufPos = 0; + s16 itemIconDecodedPos = -1; s16 i; s16 j; f32 timeInSeconds; @@ -2223,10 +2350,22 @@ void Message_DecodeJPN(PlayState* play) { } } } else if (curChar == MESSAGE_ITEM_ICON_JPN) { - msgCtx->msgBufDecodedWide[++decodedBufPos] = font->msgBufWide[msgCtx->msgBufPos + 1]; - if (GameInteractor_Should(VB_LOAD_ITEM_ICON, (uint8_t)font->msgBuf[msgCtx->msgBufPos + 1] < ITEM_CUSTOM, - sDisplayNextMessageAsEnglish)) { - Message_LoadItemIcon(play, font->msgBufWide[msgCtx->msgBufPos + 1], R_TEXTBOX_Y + 10); + u16 messageItemIcon = font->msgBufWide[msgCtx->msgBufPos + 1]; + u16 iconToLoad = messageItemIcon; + if (msgCtx->textId == 0xF8 && itemIconDecodedPos >= 0) { + msgCtx->msgBufDecodedWide[itemIconDecodedPos] = iconToLoad; + } else { + msgCtx->msgBufDecodedWide[++decodedBufPos] = iconToLoad; + itemIconDecodedPos = decodedBufPos; + } + if (iconToLoad < ITEM_CUSTOM) { + s32 loadPermitted = + GameInteractor_Should(VB_LOAD_ITEM_ICON, true, sDisplayNextMessageAsEnglish) ? 1 : 0; + if (loadPermitted) { + Message_LoadItemIcon(play, iconToLoad, R_TEXTBOX_Y + 10); + } + } else { + Randomizer_Message_DecodeLoadItemIcon(play, iconToLoad, sDisplayNextMessageAsEnglish ? 1 : 0); } } else if (curChar == MESSAGE_BACKGROUND_JPN) { msgCtx->textboxBackgroundIdx = font->msgBufWide[msgCtx->msgBufPos + 1] * 2; @@ -2278,6 +2417,7 @@ void Message_Decode(PlayState* play) { s32 charTexIdx = 0; s16 playerNameLen; s16 decodedBufPos = 0; + s16 itemIconDecodedPos = -1; s16 numLines = 0; s16 i; s16 digits[4]; @@ -2654,12 +2794,23 @@ void Message_Decode(PlayState* play) { } decodedBufPos--; } else if (temp_s2 == MESSAGE_ITEM_ICON) { - msgCtx->msgBufDecoded[++decodedBufPos] = font->msgBuf[msgCtx->msgBufPos + 1]; - osSyncPrintf("ITEM_NO=(%d) (%d)\n", msgCtx->msgBufDecoded[decodedBufPos], - font->msgBuf[msgCtx->msgBufPos + 1]); - if (GameInteractor_Should(VB_LOAD_ITEM_ICON, (uint8_t)font->msgBuf[msgCtx->msgBufPos + 1] < ITEM_CUSTOM, - sDisplayNextMessageAsEnglish)) { - Message_LoadItemIcon(play, font->msgBuf[msgCtx->msgBufPos + 1], R_TEXTBOX_Y + 10); + u16 messageItemIcon = (uint8_t)font->msgBuf[msgCtx->msgBufPos + 1]; + u16 iconToLoad = messageItemIcon; + if (msgCtx->textId == 0xF8 && itemIconDecodedPos >= 0) { + msgCtx->msgBufDecoded[itemIconDecodedPos] = iconToLoad; + } else { + msgCtx->msgBufDecoded[++decodedBufPos] = iconToLoad; + itemIconDecodedPos = decodedBufPos; + } + osSyncPrintf("ITEM_NO=(%d) (%d)\n", iconToLoad, iconToLoad); + if (iconToLoad < ITEM_CUSTOM) { + s32 loadPermitted = + GameInteractor_Should(VB_LOAD_ITEM_ICON, true, sDisplayNextMessageAsEnglish) ? 1 : 0; + if (loadPermitted) { + Message_LoadItemIcon(play, iconToLoad, R_TEXTBOX_Y + 10); + } + } else { + Randomizer_Message_DecodeLoadItemIcon(play, iconToLoad, sDisplayNextMessageAsEnglish ? 1 : 0); } } else if (temp_s2 == MESSAGE_BACKGROUND) { msgCtx->textboxBackgroundIdx = font->msgBuf[msgCtx->msgBufPos + 1] * 2; 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 7ffe8af7f43..79847595d99 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -14135,6 +14135,10 @@ s32 func_8084DFF4(PlayState* play, Player* this) { } else { giEntry = this->getItemEntry; } + // SOH [Randomizer]: Message_OpenText / custom text hooks read player->getItemEntry. Without syncing, + // that struct can lag behind the resolved giEntry used here (same mismatch path as ItemTable_Retrieve). + this->getItemEntry = giEntry; + this->getItemId = giEntry.getItemId; this->av1.actionVar1 = 1; equipItem = giEntry.itemId; equipNow = CVarGetInteger(CVAR_ENHANCEMENT("AskToEquip"), 0) && giEntry.modIndex == MOD_NONE && From 042e0352ee6764af91a0bbfbff03d9a8a12d6a4b Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Sun, 10 May 2026 19:09:08 +0100 Subject: [PATCH 07/22] Fix clang-format in ItemMessages.cpp --- soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp index da3260b2d04..0412c4bcf2f 100644 --- a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp @@ -39,8 +39,7 @@ static GetItemEntry ResolveGiftGetItemEntry(const Player* player) { if (player == nullptr) { return GET_ITEM_NONE; } - if (player->getItemEntry.objectId == OBJECT_INVALID || - player->getItemId != player->getItemEntry.getItemId) { + if (player->getItemEntry.objectId == OBJECT_INVALID || player->getItemId != player->getItemEntry.getItemId) { if (IS_RANDO && player->getItemId > RG_NONE && player->getItemId < RG_MAX) { return ItemTable_RetrieveEntry(MOD_RANDOMIZER, player->getItemId); } From 6a1053766f50240f9eee3f7a740a1b2909fba44e Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Sun, 17 May 2026 18:18:28 +0100 Subject: [PATCH 08/22] Randomizer: all song textbox icons work for progressive pickup All pickup textbox icons now work correctly for progressive songs and split song parts, with the correct note icon and per-song color for each song. --- .../randomizer/Messages/ItemMessages.cpp | 40 ++++++++++++++++++- soh/src/code/z_message_PAL.c | 2 + 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp index 0412c4bcf2f..17cc997b1f1 100644 --- a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp @@ -250,14 +250,46 @@ static ItemID ResolveSongMessageIcon(RandomizerGet rg, RandomizerGet resolvedSta return ITEM_SONG_LULLABY; } +static bool RandomizerGet_IsSongTextboxPickup(RandomizerGet rg) { + return Rando::SplitSongs::IsSongPart(rg) || Rando::SplitSongs::IsProgressiveSong(rg) || + DirectSongIconForRandomizerGet(rg) != ITEM_NONE; +} + extern "C" u16 Message_GetRandoTextboxItemIconOverride(PlayState* play, u16 itemId) { (void)play; return itemId; } +/** + * Text id 0xF8 (TEXT_RANDOMIZER_CUSTOM_ITEM) can overwrite an earlier decoded icon byte; some vanilla + * song rows also use numeric text ids that collide with ITEM_* when mis-encoded. Re-resolve from the + * player's get-item row so dungeon songs (e.g. Nocturne) always use the correct ITEM_SONG_* + tint. + */ extern "C" u16 Randomizer_ResolveSongIconAtDrawTime(PlayState* play, u16 itemId) { - (void)play; - return itemId; + if (!IS_RANDO || play == nullptr) { + return itemId; + } + Player* player = GET_PLAYER(play); + if (player == nullptr) { + return itemId; + } + const GetItemEntry gift = ResolveGiftGetItemEntry(player); + if (gift.modIndex != MOD_RANDOMIZER || gift.getItemId <= RG_NONE || gift.getItemId >= RG_MAX) { + return itemId; + } + const RandomizerGet rg = static_cast(gift.getItemId); + if (!RandomizerGet_IsSongTextboxPickup(rg)) { + return itemId; + } + RandomizerGet stage = RG_NONE; + if (Rando::SplitSongs::IsProgressiveSong(rg)) { + stage = Rando::SplitSongs::ResolveProgressiveSongStage(rg); + } + const ItemID resolved = ResolveSongMessageIcon(rg, stage); + if (resolved == ITEM_NONE) { + return itemId; + } + return static_cast(resolved); } void BuildTriforcePieceMessage(CustomMessage& msg) { @@ -369,6 +401,10 @@ void BuildCustomItemMessage(Player* player, CustomMessage& msg) { msg.Replace("[[name]]", name); if (itemForMsg.HasCustomIcon()) { msg.AutoFormat(ITEM_CUSTOM); + } else if (IS_RANDO && itemForMsg.GetItemType() == ITEMTYPE_SONG) { + // Always embed ITEM_SONG_* for songs. GetGIEntry()->itemId can disagree with quest text ids (e.g. Nocturne + // uses vanilla text 0x77) or with decoded-buffer overwrites for TEXT_RANDOMIZER_CUSTOM_ITEM (0xF8). + msg.AutoFormat(ResolveSongMessageIcon(rgEnum, RG_NONE)); } else { // Vanilla pause-menu item id: embed a real icon byte so load/draw use gItemIcons (not stale segment data). const uint16_t pauseIcon = itemForMsg.GetGIEntry()->itemId; diff --git a/soh/src/code/z_message_PAL.c b/soh/src/code/z_message_PAL.c index fc3f0e15350..a02f5502ea8 100644 --- a/soh/src/code/z_message_PAL.c +++ b/soh/src/code/z_message_PAL.c @@ -19,6 +19,7 @@ extern void Randomizer_Message_DecodeLoadItemIcon(PlayState* play, u16 iconToLoad, s32 displayAsEnglishAsInt); extern void Randomizer_Message_AppendSegmentIconGfx(Gfx** gfxp); extern void Randomizer_Message_RefreshCustomItemIconAtDraw(s32 displayAsEnglishAsInt); +extern u16 Randomizer_ResolveSongIconAtDrawTime(PlayState* play, u16 itemId); // #region SOH [NTSC] - Allows custom messages to work on japanese static bool sDisplayNextMessageAsEnglish = false; @@ -977,6 +978,7 @@ u16 Message_DrawItemIcon(PlayState* play, u16 itemId, Gfx** p, u16 i) { if (itemId < ITEM_CUSTOM) { if (GameInteractor_Should(VB_DRAW_ITEM_ICON, true, &gfx)) { + itemId = Randomizer_ResolveSongIconAtDrawTime(play, itemId); u16 iconIdClamped = itemId; u8 songR; u8 songG; From bc2027e8b54138f9f5a3adc1d135cd49fea79b54 Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:47:37 +0100 Subject: [PATCH 09/22] Simplify split songs to progressive-only RandInf tracking Drop Part 1/Part 2 as separate items. Each song uses progressive pool entries (still doubled for softlock) and one RandInf; first pickup sets the flag, second progressive grants the full song. --- .../randomizer/Messages/ItemMessages.cpp | 94 +-------- .../Enhancements/randomizer/SeedContext.cpp | 8 +- soh/soh/Enhancements/randomizer/Traps.cpp | 4 +- soh/soh/Enhancements/randomizer/item.cpp | 10 - soh/soh/Enhancements/randomizer/item_list.cpp | 25 --- soh/soh/Enhancements/randomizer/logic.cpp | 24 +-- .../Enhancements/randomizer/randomizer.cpp | 16 +- .../randomizerEnums/RandomizerGet.h | 24 --- .../randomizerEnums/RandomizerInf.h | 12 -- .../randomizer/randomizer_item_tracker.cpp | 10 +- .../Enhancements/randomizer/split_songs.cpp | 198 ++++-------------- soh/soh/Enhancements/randomizer/split_songs.h | 33 +-- 12 files changed, 68 insertions(+), 390 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp index 17cc997b1f1..8f8d1e813b2 100644 --- a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp @@ -135,87 +135,43 @@ static RandomizerGet RandomizerGetFromPlayerItemEntry(const Player* player) { return static_cast(rgid); } -/** Fallback: vanilla pause `gItemIcons` quest song row if tracker mapping is unavailable. */ -static ItemID VanillaSongIconForSplitOrProgressive(RandomizerGet rg) { - if (Rando::SplitSongs::IsSongPart(rg)) { - const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromPart(rg); - return def != nullptr ? VanillaItemIdForFullSong(def->fullSong) : ITEM_NONE; - } - if (Rando::SplitSongs::IsProgressiveSong(rg)) { - RandomizerGet stage = Rando::SplitSongs::ResolveProgressiveSongStage(rg); - if (stage == RG_NONE) { - const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromProgressive(rg); - if (def != nullptr) { - stage = def->part1; - } - } - const Rando::SplitSongDef* def = stage != RG_NONE ? Rando::SplitSongs::GetSongDefFromPart(stage) : nullptr; - return def != nullptr ? VanillaItemIdForFullSong(def->fullSong) : ITEM_NONE; - } - return ITEM_NONE; -} - static ItemID DirectSongIconForRandomizerGet(RandomizerGet rg) { switch (rg) { case RG_ZELDAS_LULLABY: case RG_PROGRESSIVE_ZELDAS_LULLABY: - case RG_ZELDAS_LULLABY_PART1: - case RG_ZELDAS_LULLABY_PART2: return ITEM_SONG_LULLABY; case RG_EPONAS_SONG: case RG_PROGRESSIVE_EPONAS_SONG: - case RG_EPONAS_SONG_PART1: - case RG_EPONAS_SONG_PART2: return ITEM_SONG_EPONA; case RG_SARIAS_SONG: case RG_PROGRESSIVE_SARIAS_SONG: - case RG_SARIAS_SONG_PART1: - case RG_SARIAS_SONG_PART2: return ITEM_SONG_SARIA; case RG_SUNS_SONG: case RG_PROGRESSIVE_SUNS_SONG: - case RG_SUNS_SONG_PART1: - case RG_SUNS_SONG_PART2: return ITEM_SONG_SUN; case RG_SONG_OF_TIME: case RG_PROGRESSIVE_SONG_OF_TIME: - case RG_SONG_OF_TIME_PART1: - case RG_SONG_OF_TIME_PART2: return ITEM_SONG_TIME; case RG_SONG_OF_STORMS: case RG_PROGRESSIVE_SONG_OF_STORMS: - case RG_SONG_OF_STORMS_PART1: - case RG_SONG_OF_STORMS_PART2: return ITEM_SONG_STORMS; case RG_MINUET_OF_FOREST: case RG_PROGRESSIVE_MINUET_OF_FOREST: - case RG_MINUET_OF_FOREST_PART1: - case RG_MINUET_OF_FOREST_PART2: return ITEM_SONG_MINUET; case RG_BOLERO_OF_FIRE: case RG_PROGRESSIVE_BOLERO_OF_FIRE: - case RG_BOLERO_OF_FIRE_PART1: - case RG_BOLERO_OF_FIRE_PART2: return ITEM_SONG_BOLERO; case RG_SERENADE_OF_WATER: case RG_PROGRESSIVE_SERENADE_OF_WATER: - case RG_SERENADE_OF_WATER_PART1: - case RG_SERENADE_OF_WATER_PART2: return ITEM_SONG_SERENADE; case RG_REQUIEM_OF_SPIRIT: case RG_PROGRESSIVE_REQUIEM_OF_SPIRIT: - case RG_REQUIEM_OF_SPIRIT_PART1: - case RG_REQUIEM_OF_SPIRIT_PART2: return ITEM_SONG_REQUIEM; case RG_NOCTURNE_OF_SHADOW: case RG_PROGRESSIVE_NOCTURNE_OF_SHADOW: - case RG_NOCTURNE_OF_SHADOW_PART1: - case RG_NOCTURNE_OF_SHADOW_PART2: return ITEM_SONG_NOCTURNE; case RG_PRELUDE_OF_LIGHT: case RG_PROGRESSIVE_PRELUDE_OF_LIGHT: - case RG_PRELUDE_OF_LIGHT_PART1: - case RG_PRELUDE_OF_LIGHT_PART2: return ITEM_SONG_PRELUDE; default: return ITEM_NONE; @@ -232,27 +188,22 @@ static ItemID ResolveSongMessageIcon(RandomizerGet rg, RandomizerGet resolvedSta if (icon != ITEM_NONE) { return icon; } - } - icon = VanillaSongIconForSplitOrProgressive(rg); - if (icon != ITEM_NONE) { - return icon; - } - if (resolvedStage != RG_NONE) { - const auto* stageDef = Rando::SplitSongs::GetSongDefFromPart(resolvedStage); - if (stageDef != nullptr) { - icon = VanillaItemIdForFullSong(stageDef->fullSong); + const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromFullSong(resolvedStage); + if (def != nullptr) { + icon = VanillaItemIdForFullSong(def->fullSong); if (icon != ITEM_NONE) { return icon; } } } - // Never return ITEM_CUSTOM for split/progressive song messages; avoid static/noise tile path. + if (const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromProgressive(rg)) { + return VanillaItemIdForFullSong(def->fullSong); + } return ITEM_SONG_LULLABY; } static bool RandomizerGet_IsSongTextboxPickup(RandomizerGet rg) { - return Rando::SplitSongs::IsSongPart(rg) || Rando::SplitSongs::IsProgressiveSong(rg) || - DirectSongIconForRandomizerGet(rg) != ITEM_NONE; + return Rando::SplitSongs::IsProgressiveSong(rg) || DirectSongIconForRandomizerGet(rg) != ITEM_NONE; } extern "C" u16 Message_GetRandoTextboxItemIconOverride(PlayState* play, u16 itemId) { @@ -357,36 +308,9 @@ void BuildCustomItemMessage(Player* player, CustomMessage& msg) { // Song icons: ResolveSongMessageIcon maps RG_* → vanilla ITEM_SONG_*; Format() writes MESSAGE_ITEM_ICON bytes only. // Decode loads icon strictly from that buffer (same contract as non-song items using GetGIEntry()->itemId). if (IS_RANDO && Rando::SplitSongs::IsProgressiveSong(rgEnum)) { - RandomizerGet stage = Rando::SplitSongs::ResolveProgressiveSongStage(rgEnum); - if (stage == RG_NONE) { - const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromProgressive(rgEnum); - if (def != nullptr) { - stage = def->part1; - } - } + const RandomizerGet stage = Rando::SplitSongs::ResolveProgressiveSongStage(rgEnum); const ItemID iconFallback = ResolveSongMessageIcon(rgEnum, stage); - if (stage != RG_NONE) { - const auto& nm = Rando::StaticData::RetrieveItem(stage).GetName(); - CustomMessage getItemText(nm.GetEnglish(), nm.GetGerman(), nm.GetFrench(), TEXTBOX_TYPE_BLUE, - TEXTBOX_POS_BOTTOM); - getItemText.Format(iconFallback); - msg = getItemText; - return; - } - // Stage could not be resolved to a part yet; still show progressive name with a valid vanilla song icon. - if (iconFallback != ITEM_NONE) { - const auto& nm = Rando::StaticData::RetrieveItem(rgEnum).GetName(); - CustomMessage getItemText(nm.GetEnglish(), nm.GetGerman(), nm.GetFrench(), TEXTBOX_TYPE_BLUE, - TEXTBOX_POS_BOTTOM); - getItemText.Format(iconFallback); - msg = getItemText; - return; - } - } - if (IS_RANDO && Rando::SplitSongs::IsSongPart(rgEnum)) { - const auto& songPartItem = Rando::StaticData::RetrieveItem(rgEnum); - const ItemID iconFallback = ResolveSongMessageIcon(rgEnum, rgEnum); - const auto& nm = songPartItem.GetName(); + const auto& nm = Rando::StaticData::RetrieveItem(stage != RG_NONE ? stage : rgEnum).GetName(); CustomMessage getItemText(nm.GetEnglish(), nm.GetGerman(), nm.GetFrench(), TEXTBOX_TYPE_BLUE, TEXTBOX_POS_BOTTOM); getItemText.Format(iconFallback); diff --git a/soh/soh/Enhancements/randomizer/SeedContext.cpp b/soh/soh/Enhancements/randomizer/SeedContext.cpp index 0049df11ddb..69c01389564 100644 --- a/soh/soh/Enhancements/randomizer/SeedContext.cpp +++ b/soh/soh/Enhancements/randomizer/SeedContext.cpp @@ -403,13 +403,7 @@ GetItemEntry Context::GetFinalGIEntry(const RandomizerCheck rc, const bool check } GetItemEntry giEntry = itemLoc->GetPlacedItem().GetGIEntry_Copy(); if (SplitSongs::IsProgressiveSong(itemLoc->GetPlacedRandomizerGet())) { - RandomizerGet stage = SplitSongs::ResolveProgressiveSongStage(itemLoc->GetPlacedRandomizerGet()); - if (stage == RG_NONE) { - const SplitSongDef* def = SplitSongs::GetSongDefFromProgressive(itemLoc->GetPlacedRandomizerGet()); - if (def != nullptr) { - stage = def->part1; - } - } + const RandomizerGet stage = SplitSongs::ResolveProgressiveSongStage(itemLoc->GetPlacedRandomizerGet()); if (stage != RG_NONE) { giEntry = StaticData::RetrieveItem(stage).GetGIEntry_Copy(); } diff --git a/soh/soh/Enhancements/randomizer/Traps.cpp b/soh/soh/Enhancements/randomizer/Traps.cpp index 4b383ec075d..5a6f952c3b2 100644 --- a/soh/soh/Enhancements/randomizer/Traps.cpp +++ b/soh/soh/Enhancements/randomizer/Traps.cpp @@ -1428,9 +1428,7 @@ Text Rando::Traps::GetTrapName(uint16_t id) { } RandomizerGet rg = static_cast(id); - if (const Rando::SplitSongDef* splitDef = Rando::SplitSongs::GetSongDefFromPart(rg)) { - id = static_cast(splitDef->fullSong); - } else if (const Rando::SplitSongDef* progDef = Rando::SplitSongs::GetSongDefFromProgressive(rg)) { + if (const Rando::SplitSongDef* progDef = Rando::SplitSongs::GetSongDefFromProgressive(rg)) { id = static_cast(progDef->fullSong); } diff --git a/soh/soh/Enhancements/randomizer/item.cpp b/soh/soh/Enhancements/randomizer/item.cpp index f6dbf340a1e..16f05c0cbb2 100644 --- a/soh/soh/Enhancements/randomizer/item.cpp +++ b/soh/soh/Enhancements/randomizer/item.cpp @@ -54,8 +54,6 @@ void Item::ApplyEffect() const { logic->ApplyItemEffect(StaticData::RetrieveItem(randomizerGet), true); } else if (SplitSongs::IsProgressiveSong(randomizerGet)) { SplitSongs::ApplyProgressiveEffectToLogicScratch(logic.get(), randomizerGet, true); - } else if (SplitSongs::IsSongPart(randomizerGet)) { - SplitSongs::ApplyPartEffectToLogicScratch(logic.get(), randomizerGet, true); } logic->Set(logicVal, true); } @@ -67,8 +65,6 @@ void Item::UndoEffect() const { logic->ApplyItemEffect(StaticData::RetrieveItem(randomizerGet), false); } else if (SplitSongs::IsProgressiveSong(randomizerGet)) { SplitSongs::ApplyProgressiveEffectToLogicScratch(logic.get(), randomizerGet, false); - } else if (SplitSongs::IsSongPart(randomizerGet)) { - SplitSongs::ApplyPartEffectToLogicScratch(logic.get(), randomizerGet, false); } logic->Set(logicVal, false); } @@ -389,12 +385,6 @@ std::shared_ptr Item::GetGIEntry() const { // NOLINT(*-no-recursio case RG_PROGRESSIVE_NOCTURNE_OF_SHADOW: case RG_PROGRESSIVE_PRELUDE_OF_LIGHT: actual = SplitSongs::ResolveProgressiveSongStage(randomizerGet); - if (actual == RG_NONE) { - const SplitSongDef* def = SplitSongs::GetSongDefFromProgressive(randomizerGet); - if (def != nullptr) { - actual = def->part1; - } - } break; case RG_PROGRESSIVE_BOMBCHU_BAG: if (OTRGlobals::Instance->gRandoContext->GetOption(RSK_BOMBCHU_BAG).Is(RO_BOMBCHU_BAG_SINGLE)) { diff --git a/soh/soh/Enhancements/randomizer/item_list.cpp b/soh/soh/Enhancements/randomizer/item_list.cpp index d06dddef122..0620aad56bb 100644 --- a/soh/soh/Enhancements/randomizer/item_list.cpp +++ b/soh/soh/Enhancements/randomizer/item_list.cpp @@ -98,31 +98,6 @@ void Rando::StaticData::InitItemTable() { itemTable[RG_BOTTLE_WITH_POE] = Item(RG_BOTTLE_WITH_POE, Text{ "Bottle with Poe", "Bouteille avec un Esprit", "Flasche mit einem Geist" }, ITEMTYPE_ITEM, 0x94, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_POE, RG_BOTTLE_WITH_POE, OBJECT_GI_GHOST, GID_POE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); itemTable[RG_RUTOS_LETTER] = Item(RG_RUTOS_LETTER, Text{ "Bottle with Ruto's Letter", "Bouteille avec la Lettre de Ruto", "Flasche mit Rutos Brief" }, ITEMTYPE_ITEM, GI_LETTER_RUTO, true, LOGIC_RUTOS_LETTER, RHT_RUTOS_LETTER, ITEM_LETTER_RUTO, OBJECT_GI_BOTTLE_LETTER, GID_LETTER_RUTO, 0x99, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"a ", "une ", "einen "}); itemTable[RG_BOTTLE_WITH_BIG_POE] = Item(RG_BOTTLE_WITH_BIG_POE, Text{ "Bottle with Big Poe", "Bouteille avec une Âme", "Flasche mit Seele" }, ITEMTYPE_ITEM, 0x93, true, LOGIC_BOTTLE_WITH_BIG_POE, RHT_BOTTLE_WITH_BIG_POE, RG_BOTTLE_WITH_BIG_POE, OBJECT_GI_GHOST, GID_BIG_POE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "einen "}); - // Split Song Parts - itemTable[RG_ZELDAS_LULLABY_PART1] = Item(RG_ZELDAS_LULLABY_PART1, Text{ "Zelda's Lullaby (Part 1)", "Berceuse de Zelda (Partie 1)", "Zeldas Wiegenlied (Teil 1)" }, ITEMTYPE_SONG, 0xE1, true, LOGIC_NONE, RHT_ZELDAS_LULLABY, RG_ZELDAS_LULLABY_PART1, OBJECT_GI_MELODY, GID_SONG_ZELDA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_ZELDAS_LULLABY_PART2] = Item(RG_ZELDAS_LULLABY_PART2, Text{ "Zelda's Lullaby (Part 2)", "Berceuse de Zelda (Partie 2)", "Zeldas Wiegenlied (Teil 2)" }, ITEMTYPE_SONG, 0xE2, true, LOGIC_NONE, RHT_ZELDAS_LULLABY, RG_ZELDAS_LULLABY_PART2, OBJECT_GI_MELODY, GID_SONG_ZELDA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_EPONAS_SONG_PART1] = Item(RG_EPONAS_SONG_PART1, Text{ "Epona's Song (Part 1)", "Chant d'Epona (Partie 1)", "Eponas Lied (Teil 1)" }, ITEMTYPE_SONG, 0xE3, true, LOGIC_NONE, RHT_EPONAS_SONG, RG_EPONAS_SONG_PART1, OBJECT_GI_MELODY, GID_SONG_EPONA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_EPONAS_SONG_PART2] = Item(RG_EPONAS_SONG_PART2, Text{ "Epona's Song (Part 2)", "Chant d'Epona (Partie 2)", "Eponas Lied (Teil 2)" }, ITEMTYPE_SONG, 0xE4, true, LOGIC_NONE, RHT_EPONAS_SONG, RG_EPONAS_SONG_PART2, OBJECT_GI_MELODY, GID_SONG_EPONA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_SARIAS_SONG_PART1] = Item(RG_SARIAS_SONG_PART1, Text{ "Saria's Song (Part 1)", "Chant de Saria (Partie 1)", "Salias Lied (Teil 1)" }, ITEMTYPE_SONG, 0xE5, true, LOGIC_NONE, RHT_SARIAS_SONG, RG_SARIAS_SONG_PART1, OBJECT_GI_MELODY, GID_SONG_SARIA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_SARIAS_SONG_PART2] = Item(RG_SARIAS_SONG_PART2, Text{ "Saria's Song (Part 2)", "Chant de Saria (Partie 2)", "Salias Lied (Teil 2)" }, ITEMTYPE_SONG, 0xE6, true, LOGIC_NONE, RHT_SARIAS_SONG, RG_SARIAS_SONG_PART2, OBJECT_GI_MELODY, GID_SONG_SARIA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_SUNS_SONG_PART1] = Item(RG_SUNS_SONG_PART1, Text{ "Sun's Song (Part 1)", "Chant du Soleil (Partie 1)", "Hymne der Sonne (Teil 1)" }, ITEMTYPE_SONG, 0xE7, true, LOGIC_NONE, RHT_SUNS_SONG, RG_SUNS_SONG_PART1, OBJECT_GI_MELODY, GID_SONG_SUN, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_SUNS_SONG_PART2] = Item(RG_SUNS_SONG_PART2, Text{ "Sun's Song (Part 2)", "Chant du Soleil (Partie 2)", "Hymne der Sonne (Teil 2)" }, ITEMTYPE_SONG, 0xE8, true, LOGIC_NONE, RHT_SUNS_SONG, RG_SUNS_SONG_PART2, OBJECT_GI_MELODY, GID_SONG_SUN, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_SONG_OF_TIME_PART1] = Item(RG_SONG_OF_TIME_PART1, Text{ "Song of Time (Part 1)", "Chant du Temps (Partie 1)", "Hymne der Zeit (Teil 1)" }, ITEMTYPE_SONG, 0xE9, true, LOGIC_NONE, RHT_SONG_OF_TIME, RG_SONG_OF_TIME_PART1, OBJECT_GI_MELODY, GID_SONG_TIME, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_SONG_OF_TIME_PART2] = Item(RG_SONG_OF_TIME_PART2, Text{ "Song of Time (Part 2)", "Chant du Temps (Partie 2)", "Hymne der Zeit (Teil 2)" }, ITEMTYPE_SONG, 0xEA, true, LOGIC_NONE, RHT_SONG_OF_TIME, RG_SONG_OF_TIME_PART2, OBJECT_GI_MELODY, GID_SONG_TIME, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_SONG_OF_STORMS_PART1] = Item(RG_SONG_OF_STORMS_PART1, Text{ "Song of Storms (Part 1)", "Chant des Tempêtes (Partie 1)", "Hymne des Sturms (Teil 1)" }, ITEMTYPE_SONG, 0xEB, true, LOGIC_NONE, RHT_SONG_OF_STORMS, RG_SONG_OF_STORMS_PART1, OBJECT_GI_MELODY, GID_SONG_STORM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_SONG_OF_STORMS_PART2] = Item(RG_SONG_OF_STORMS_PART2, Text{ "Song of Storms (Part 2)", "Chant des Tempêtes (Partie 2)", "Hymne des Sturms (Teil 2)" }, ITEMTYPE_SONG, 0xEC, true, LOGIC_NONE, RHT_SONG_OF_STORMS, RG_SONG_OF_STORMS_PART2, OBJECT_GI_MELODY, GID_SONG_STORM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_MINUET_OF_FOREST_PART1] = Item(RG_MINUET_OF_FOREST_PART1, Text{ "Minuet of Forest (Part 1)", "Menuet des Bois (Partie 1)", "Menuett des Waldes (Teil 1)" }, ITEMTYPE_SONG, 0xED, true, LOGIC_NONE, RHT_MINUET_OF_FOREST, RG_MINUET_OF_FOREST_PART1, OBJECT_GI_MELODY, GID_SONG_MINUET, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_MINUET_OF_FOREST_PART2] = Item(RG_MINUET_OF_FOREST_PART2, Text{ "Minuet of Forest (Part 2)", "Menuet des Bois (Partie 2)", "Menuett des Waldes (Teil 2)" }, ITEMTYPE_SONG, 0xEE, true, LOGIC_NONE, RHT_MINUET_OF_FOREST, RG_MINUET_OF_FOREST_PART2, OBJECT_GI_MELODY, GID_SONG_MINUET, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_BOLERO_OF_FIRE_PART1] = Item(RG_BOLERO_OF_FIRE_PART1, Text{ "Bolero of Fire (Part 1)", "Boléro du Feu (Partie 1)", "Bolero des Feuers (Teil 1)" }, ITEMTYPE_SONG, 0xEF, true, LOGIC_NONE, RHT_BOLERO_OF_FIRE, RG_BOLERO_OF_FIRE_PART1, OBJECT_GI_MELODY, GID_SONG_BOLERO, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_BOLERO_OF_FIRE_PART2] = Item(RG_BOLERO_OF_FIRE_PART2, Text{ "Bolero of Fire (Part 2)", "Boléro du Feu (Partie 2)", "Bolero des Feuers (Teil 2)" }, ITEMTYPE_SONG, 0xF0, true, LOGIC_NONE, RHT_BOLERO_OF_FIRE, RG_BOLERO_OF_FIRE_PART2, OBJECT_GI_MELODY, GID_SONG_BOLERO, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_SERENADE_OF_WATER_PART1] = Item(RG_SERENADE_OF_WATER_PART1, Text{ "Serenade of Water (Part 1)", "Sérénade de l'Eau (Partie 1)", "Serenade des Wassers (Teil 1)" }, ITEMTYPE_SONG, 0xF1, true, LOGIC_NONE, RHT_SERENADE_OF_WATER, RG_SERENADE_OF_WATER_PART1, OBJECT_GI_MELODY, GID_SONG_SERENADE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_SERENADE_OF_WATER_PART2] = Item(RG_SERENADE_OF_WATER_PART2, Text{ "Serenade of Water (Part 2)", "Sérénade de l'Eau (Partie 2)", "Serenade des Wassers (Teil 2)" }, ITEMTYPE_SONG, 0xF2, true, LOGIC_NONE, RHT_SERENADE_OF_WATER, RG_SERENADE_OF_WATER_PART2, OBJECT_GI_MELODY, GID_SONG_SERENADE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_REQUIEM_OF_SPIRIT_PART1] = Item(RG_REQUIEM_OF_SPIRIT_PART1, Text{ "Requiem of Spirit (Part 1)", "Requiem des Esprits (Partie 1)", "Requiem der Geister (Teil 1)" }, ITEMTYPE_SONG, 0xF3, true, LOGIC_NONE, RHT_REQUIEM_OF_SPIRIT, RG_REQUIEM_OF_SPIRIT_PART1, OBJECT_GI_MELODY, GID_SONG_REQUIEM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_REQUIEM_OF_SPIRIT_PART2] = Item(RG_REQUIEM_OF_SPIRIT_PART2, Text{ "Requiem of Spirit (Part 2)", "Requiem des Esprits (Partie 2)", "Requiem der Geister (Teil 2)" }, ITEMTYPE_SONG, 0xF4, true, LOGIC_NONE, RHT_REQUIEM_OF_SPIRIT, RG_REQUIEM_OF_SPIRIT_PART2, OBJECT_GI_MELODY, GID_SONG_REQUIEM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_NOCTURNE_OF_SHADOW_PART1] = Item(RG_NOCTURNE_OF_SHADOW_PART1, Text{ "Nocturne of Shadow (Part 1)", "Nocturne de l'Ombre (Partie 1)", "Nocturne des Schattens (Teil 1)" }, ITEMTYPE_SONG, 0xF5, true, LOGIC_NONE, RHT_NOCTURNE_OF_SHADOW, RG_NOCTURNE_OF_SHADOW_PART1, OBJECT_GI_MELODY, GID_SONG_NOCTURNE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_NOCTURNE_OF_SHADOW_PART2] = Item(RG_NOCTURNE_OF_SHADOW_PART2, Text{ "Nocturne of Shadow (Part 2)", "Nocturne de l'Ombre (Partie 2)", "Nocturne des Schattens (Teil 2)" }, ITEMTYPE_SONG, 0xF6, true, LOGIC_NONE, RHT_NOCTURNE_OF_SHADOW, RG_NOCTURNE_OF_SHADOW_PART2, OBJECT_GI_MELODY, GID_SONG_NOCTURNE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_PRELUDE_OF_LIGHT_PART1] = Item(RG_PRELUDE_OF_LIGHT_PART1, Text{ "Prelude of Light (Part 1)", "Prélude de la Lumière (Partie 1)", "Kantate des Lichts (Teil 1)" }, ITEMTYPE_SONG, 0xF7, true, LOGIC_NONE, RHT_PRELUDE_OF_LIGHT, RG_PRELUDE_OF_LIGHT_PART1, OBJECT_GI_MELODY, GID_SONG_PRELUDE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); - itemTable[RG_PRELUDE_OF_LIGHT_PART2] = Item(RG_PRELUDE_OF_LIGHT_PART2, Text{ "Prelude of Light (Part 2)", "Prélude de la Lumière (Partie 2)", "Kantate des Lichts (Teil 2)" }, ITEMTYPE_SONG, 0xF8, true, LOGIC_NONE, RHT_PRELUDE_OF_LIGHT, RG_PRELUDE_OF_LIGHT_PART2, OBJECT_GI_MELODY, GID_SONG_PRELUDE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER); // Songs itemTable[RG_ZELDAS_LULLABY] = Item(RG_ZELDAS_LULLABY, Text{ "Zelda's Lullaby", "Berceuse de Zelda", "Zeldas Wiegenlied" }, ITEMTYPE_SONG, 0xC1, true, LOGIC_ZELDAS_LULLABY, RHT_ZELDAS_LULLABY, ITEM_SONG_LULLABY, OBJECT_GI_MELODY, GID_SONG_ZELDA, 0xD4, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"", "la ", ""}); itemTable[RG_EPONAS_SONG] = Item(RG_EPONAS_SONG, Text{ "Epona's Song", "Chant d'Epona", "Eponas Lied" }, ITEMTYPE_SONG, 0xC2, true, LOGIC_EPONAS_SONG, RHT_EPONAS_SONG, ITEM_SONG_EPONA, OBJECT_GI_MELODY, GID_SONG_EPONA, 0xD2, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"", "le ", ""}); diff --git a/soh/soh/Enhancements/randomizer/logic.cpp b/soh/soh/Enhancements/randomizer/logic.cpp index e56bfcc89e7..c8458b2d10d 100644 --- a/soh/soh/Enhancements/randomizer/logic.cpp +++ b/soh/soh/Enhancements/randomizer/logic.cpp @@ -22,7 +22,7 @@ namespace Rando { bool Logic::HasItem(RandomizerGet itemName) { if (SplitSongs::IsProgressiveSong(itemName)) { const SplitSongDef* def = SplitSongs::GetSongDefFromProgressive(itemName); - return def != nullptr && SplitSongs::HasBothParts(def->id); + return def != nullptr && SplitSongs::HasFullSong(def->id); } switch (itemName) { @@ -122,7 +122,7 @@ bool Logic::HasItem(RandomizerGet itemName) { } if (splitAnywhere) { const SplitSongDef* sdef = SplitSongs::GetSongDefFromFullSong(itemName); - if (sdef != nullptr && SplitSongs::HasBothParts(sdef->id)) { + if (sdef != nullptr && SplitSongs::HasFullSong(sdef->id)) { return true; } } @@ -316,22 +316,6 @@ bool Logic::HasItem(RandomizerGet itemName) { default: break; } - if (SplitSongs::IsSongPart(itemName)) { - const SplitSongDef* def = SplitSongs::GetSongDefFromPart(itemName); - if (def == nullptr) { - return false; - } - if (HasItem(def->fullSong)) { - return true; - } - if (itemName == def->part1) { - return SplitSongs::HasPart1(def->id); - } - if (itemName == def->part2) { - return SplitSongs::HasPart2(def->id); - } - return false; - } SPDLOG_ERROR("HasItem reached `return false;`. Missing case for RandomizerGet of {}", static_cast(itemName)); assert(false); @@ -2143,10 +2127,6 @@ void Logic::ApplyItemEffect(Item& item, bool state) { SplitSongs::ApplyProgressiveEffectToLogicScratch(this, rg, state); break; } - if (SplitSongs::IsSongPart(rg)) { - SplitSongs::ApplyPartEffectToLogicScratch(this, rg, state); - break; - } auto qi = RandoGetToQuestItem.find(rg); if (qi != RandoGetToQuestItem.end()) { SetQuestItem(qi->second, state); diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index 2151c3085e1..1be92297555 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -299,9 +299,6 @@ ItemObtainability Randomizer::GetItemObtainabilityFromRandomizerGet(RandomizerGe if (Rando::SplitSongs::IsProgressiveSong(randoGet)) { return Rando::SplitSongs::GetProgressiveSongObtainability(randoGet); } - if (Rando::SplitSongs::IsSongPart(randoGet)) { - return Rando::SplitSongs::GetPartObtainability(randoGet); - } if (randomizerGetToRandInf.find(randoGet) != randomizerGetToRandInf.end()) { return Flags_GetRandomizerInf(randomizerGetToRandInf.find(randoGet)->second) ? CANT_OBTAIN_ALREADY_HAVE : CAN_OBTAIN; @@ -4641,19 +4638,8 @@ extern "C" u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry) { // Gameplay stats: Update the time the item was obtained Randomizer_GameplayStats_SetTimestamp(item); - if (Rando::SplitSongs::IsSongPart(item)) { - Rando::SplitSongs::OnItemReceived(item); - return Return_Item_Entry(giEntry, RG_NONE); - } if (Rando::SplitSongs::IsProgressiveSong(item)) { - // Resolve display before applying progression so first pickup shows Part 1 and second shows Part 2. - RandomizerGet displayItem = Rando::SplitSongs::ResolveProgressiveSongStage(item); - if (displayItem == RG_NONE) { - const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromProgressive(item); - if (def != nullptr) { - displayItem = def->part1; - } - } + const RandomizerGet displayItem = Rando::SplitSongs::ResolveProgressiveSongStage(item); Rando::SplitSongs::OnProgressiveSongReceived(item); if (displayItem != RG_NONE) { return Return_Item_Entry(Rando::StaticData::RetrieveItem(displayItem).GetGIEntry_Copy(), RG_NONE); diff --git a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerGet.h b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerGet.h index 65013dbb5f0..4ef7e3ca296 100644 --- a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerGet.h +++ b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerGet.h @@ -331,30 +331,6 @@ RANDO_ENUM_ITEM(RG_FISHING_HOLE_KEY) // Custom Items RANDO_ENUM_ITEM(RG_ROCS_FEATHER) -RANDO_ENUM_ITEM(RG_ZELDAS_LULLABY_PART1) -RANDO_ENUM_ITEM(RG_ZELDAS_LULLABY_PART2) -RANDO_ENUM_ITEM(RG_EPONAS_SONG_PART1) -RANDO_ENUM_ITEM(RG_EPONAS_SONG_PART2) -RANDO_ENUM_ITEM(RG_SARIAS_SONG_PART1) -RANDO_ENUM_ITEM(RG_SARIAS_SONG_PART2) -RANDO_ENUM_ITEM(RG_SUNS_SONG_PART1) -RANDO_ENUM_ITEM(RG_SUNS_SONG_PART2) -RANDO_ENUM_ITEM(RG_SONG_OF_TIME_PART1) -RANDO_ENUM_ITEM(RG_SONG_OF_TIME_PART2) -RANDO_ENUM_ITEM(RG_SONG_OF_STORMS_PART1) -RANDO_ENUM_ITEM(RG_SONG_OF_STORMS_PART2) -RANDO_ENUM_ITEM(RG_MINUET_OF_FOREST_PART1) -RANDO_ENUM_ITEM(RG_MINUET_OF_FOREST_PART2) -RANDO_ENUM_ITEM(RG_BOLERO_OF_FIRE_PART1) -RANDO_ENUM_ITEM(RG_BOLERO_OF_FIRE_PART2) -RANDO_ENUM_ITEM(RG_SERENADE_OF_WATER_PART1) -RANDO_ENUM_ITEM(RG_SERENADE_OF_WATER_PART2) -RANDO_ENUM_ITEM(RG_REQUIEM_OF_SPIRIT_PART1) -RANDO_ENUM_ITEM(RG_REQUIEM_OF_SPIRIT_PART2) -RANDO_ENUM_ITEM(RG_NOCTURNE_OF_SHADOW_PART1) -RANDO_ENUM_ITEM(RG_NOCTURNE_OF_SHADOW_PART2) -RANDO_ENUM_ITEM(RG_PRELUDE_OF_LIGHT_PART1) -RANDO_ENUM_ITEM(RG_PRELUDE_OF_LIGHT_PART2) // Logic Only RANDO_ENUM_ITEM(RG_STICKS) diff --git a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerInf.h b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerInf.h index 0732f41bfee..5b59a5bc711 100644 --- a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerInf.h +++ b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerInf.h @@ -2632,29 +2632,17 @@ RANDO_ENUM_ITEM(RAND_INF_OBTAINED_NAYRUS_LOVE) RANDO_ENUM_ITEM(RAND_INF_OBTAINED_ROCS_FEATHER) RANDO_ENUM_ITEM(RAND_INF_TALON_SENT_MALON_HOME) RANDO_ENUM_ITEM(RAND_INF_SPLIT_ZL_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_ZL_PART2) RANDO_ENUM_ITEM(RAND_INF_SPLIT_EPONA_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_EPONA_PART2) RANDO_ENUM_ITEM(RAND_INF_SPLIT_SARIA_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_SARIA_PART2) RANDO_ENUM_ITEM(RAND_INF_SPLIT_SUN_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_SUN_PART2) RANDO_ENUM_ITEM(RAND_INF_SPLIT_TIME_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_TIME_PART2) RANDO_ENUM_ITEM(RAND_INF_SPLIT_STORMS_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_STORMS_PART2) RANDO_ENUM_ITEM(RAND_INF_SPLIT_MINUET_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_MINUET_PART2) RANDO_ENUM_ITEM(RAND_INF_SPLIT_BOLERO_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_BOLERO_PART2) RANDO_ENUM_ITEM(RAND_INF_SPLIT_SERENADE_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_SERENADE_PART2) RANDO_ENUM_ITEM(RAND_INF_SPLIT_REQUIEM_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_REQUIEM_PART2) RANDO_ENUM_ITEM(RAND_INF_SPLIT_NOCTURNE_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_NOCTURNE_PART2) RANDO_ENUM_ITEM(RAND_INF_SPLIT_PRELUDE_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_PRELUDE_PART2) // Overworld Signs RANDO_ENUM_ITEM(RAND_INF_KF_DEKU_TREE_RECTANGLE_SIGN) diff --git a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp index 80b33b2e8dd..34280c922ed 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp @@ -521,7 +521,7 @@ static std::unordered_map BuildSongPartSpoil continue; } const RandomizerGet rg = loc->GetPlacedRandomizerGet(); - if (!Rando::SplitSongs::IsSongPart(rg)) { + if (!Rando::SplitSongs::IsProgressiveSong(rg)) { continue; } if (partRgToArea.find(rg) != partRgToArea.end()) { @@ -534,7 +534,7 @@ static std::unordered_map BuildSongPartSpoil } std::unordered_map fullSongToArea; for (const auto& kv : partRgToArea) { - const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromPart(kv.first); + const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromProgressive(kv.first); if (def == nullptr) { continue; } @@ -1475,12 +1475,10 @@ void DrawSplitSongProgress(ItemTrackerItem item) { const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromFullSong(fullSongRg); int partsCollected = 0; if (def != nullptr) { - const bool p1 = Rando::SplitSongs::HasPart1(def->id); - const bool p2 = Rando::SplitSongs::HasPart2(def->id); if (Rando::SplitSongs::HasFullSong(def->id)) { partsCollected = 2; - } else { - partsCollected = (p1 ? 1 : 0) + (p2 ? 1 : 0); + } else if (Rando::SplitSongs::HasSplitPart(def->id)) { + partsCollected = 1; } } diff --git a/soh/soh/Enhancements/randomizer/split_songs.cpp b/soh/soh/Enhancements/randomizer/split_songs.cpp index d20de1c1b64..77dbc8c6d06 100644 --- a/soh/soh/Enhancements/randomizer/split_songs.cpp +++ b/soh/soh/Enhancements/randomizer/split_songs.cpp @@ -17,42 +17,26 @@ namespace Rando { -static constexpr uint32_t kSplitSongPart1Flags[SPLIT_SONG_MAX] = { +static constexpr uint32_t kSplitSongPartFlags[SPLIT_SONG_MAX] = { RAND_INF_SPLIT_ZL_PART1, RAND_INF_SPLIT_EPONA_PART1, RAND_INF_SPLIT_SARIA_PART1, RAND_INF_SPLIT_SUN_PART1, RAND_INF_SPLIT_TIME_PART1, RAND_INF_SPLIT_STORMS_PART1, RAND_INF_SPLIT_MINUET_PART1, RAND_INF_SPLIT_BOLERO_PART1, RAND_INF_SPLIT_SERENADE_PART1, RAND_INF_SPLIT_REQUIEM_PART1, RAND_INF_SPLIT_NOCTURNE_PART1, RAND_INF_SPLIT_PRELUDE_PART1, }; -static constexpr uint32_t kSplitSongPart2Flags[SPLIT_SONG_MAX] = { - RAND_INF_SPLIT_ZL_PART2, RAND_INF_SPLIT_EPONA_PART2, RAND_INF_SPLIT_SARIA_PART2, - RAND_INF_SPLIT_SUN_PART2, RAND_INF_SPLIT_TIME_PART2, RAND_INF_SPLIT_STORMS_PART2, - RAND_INF_SPLIT_MINUET_PART2, RAND_INF_SPLIT_BOLERO_PART2, RAND_INF_SPLIT_SERENADE_PART2, - RAND_INF_SPLIT_REQUIEM_PART2, RAND_INF_SPLIT_NOCTURNE_PART2, RAND_INF_SPLIT_PRELUDE_PART2, -}; - static constexpr SplitSongDef kSplitSongs[SPLIT_SONG_MAX] = { - { SPLIT_SONG_ZELDAS_LULLABY, RG_PROGRESSIVE_ZELDAS_LULLABY, RG_ZELDAS_LULLABY_PART1, RG_ZELDAS_LULLABY_PART2, - RG_ZELDAS_LULLABY }, - { SPLIT_SONG_EPONAS_SONG, RG_PROGRESSIVE_EPONAS_SONG, RG_EPONAS_SONG_PART1, RG_EPONAS_SONG_PART2, RG_EPONAS_SONG }, - { SPLIT_SONG_SARIAS_SONG, RG_PROGRESSIVE_SARIAS_SONG, RG_SARIAS_SONG_PART1, RG_SARIAS_SONG_PART2, RG_SARIAS_SONG }, - { SPLIT_SONG_SUNS_SONG, RG_PROGRESSIVE_SUNS_SONG, RG_SUNS_SONG_PART1, RG_SUNS_SONG_PART2, RG_SUNS_SONG }, - { SPLIT_SONG_SONG_OF_TIME, RG_PROGRESSIVE_SONG_OF_TIME, RG_SONG_OF_TIME_PART1, RG_SONG_OF_TIME_PART2, - RG_SONG_OF_TIME }, - { SPLIT_SONG_SONG_OF_STORMS, RG_PROGRESSIVE_SONG_OF_STORMS, RG_SONG_OF_STORMS_PART1, RG_SONG_OF_STORMS_PART2, - RG_SONG_OF_STORMS }, - { SPLIT_SONG_MINUET_OF_FOREST, RG_PROGRESSIVE_MINUET_OF_FOREST, RG_MINUET_OF_FOREST_PART1, - RG_MINUET_OF_FOREST_PART2, RG_MINUET_OF_FOREST }, - { SPLIT_SONG_BOLERO_OF_FIRE, RG_PROGRESSIVE_BOLERO_OF_FIRE, RG_BOLERO_OF_FIRE_PART1, RG_BOLERO_OF_FIRE_PART2, - RG_BOLERO_OF_FIRE }, - { SPLIT_SONG_SERENADE_OF_WATER, RG_PROGRESSIVE_SERENADE_OF_WATER, RG_SERENADE_OF_WATER_PART1, - RG_SERENADE_OF_WATER_PART2, RG_SERENADE_OF_WATER }, - { SPLIT_SONG_REQUIEM_OF_SPIRIT, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, RG_REQUIEM_OF_SPIRIT_PART1, - RG_REQUIEM_OF_SPIRIT_PART2, RG_REQUIEM_OF_SPIRIT }, - { SPLIT_SONG_NOCTURNE_OF_SHADOW, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, RG_NOCTURNE_OF_SHADOW_PART1, - RG_NOCTURNE_OF_SHADOW_PART2, RG_NOCTURNE_OF_SHADOW }, - { SPLIT_SONG_PRELUDE_OF_LIGHT, RG_PROGRESSIVE_PRELUDE_OF_LIGHT, RG_PRELUDE_OF_LIGHT_PART1, - RG_PRELUDE_OF_LIGHT_PART2, RG_PRELUDE_OF_LIGHT }, + { SPLIT_SONG_ZELDAS_LULLABY, RG_PROGRESSIVE_ZELDAS_LULLABY, RG_ZELDAS_LULLABY }, + { SPLIT_SONG_EPONAS_SONG, RG_PROGRESSIVE_EPONAS_SONG, RG_EPONAS_SONG }, + { SPLIT_SONG_SARIAS_SONG, RG_PROGRESSIVE_SARIAS_SONG, RG_SARIAS_SONG }, + { SPLIT_SONG_SUNS_SONG, RG_PROGRESSIVE_SUNS_SONG, RG_SUNS_SONG }, + { SPLIT_SONG_SONG_OF_TIME, RG_PROGRESSIVE_SONG_OF_TIME, RG_SONG_OF_TIME }, + { SPLIT_SONG_SONG_OF_STORMS, RG_PROGRESSIVE_SONG_OF_STORMS, RG_SONG_OF_STORMS }, + { SPLIT_SONG_MINUET_OF_FOREST, RG_PROGRESSIVE_MINUET_OF_FOREST, RG_MINUET_OF_FOREST }, + { SPLIT_SONG_BOLERO_OF_FIRE, RG_PROGRESSIVE_BOLERO_OF_FIRE, RG_BOLERO_OF_FIRE }, + { SPLIT_SONG_SERENADE_OF_WATER, RG_PROGRESSIVE_SERENADE_OF_WATER, RG_SERENADE_OF_WATER }, + { SPLIT_SONG_REQUIEM_OF_SPIRIT, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, RG_REQUIEM_OF_SPIRIT }, + { SPLIT_SONG_NOCTURNE_OF_SHADOW, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, RG_NOCTURNE_OF_SHADOW }, + { SPLIT_SONG_PRELUDE_OF_LIGHT, RG_PROGRESSIVE_PRELUDE_OF_LIGHT, RG_PRELUDE_OF_LIGHT }, }; static bool IsValidSplitSongId(SplitSongId id) { @@ -62,7 +46,6 @@ static bool IsValidSplitSongId(SplitSongId id) { static std::vector sPendingFullSongGrants; static bool sSongGrantQueued[SPLIT_SONG_MAX] = {}; -/** Granting the quest song updates save while get-item / blocking CS is active; defer to next safe player update. */ static bool ShouldDeferFullSongQuestGrant() { if (gPlayState == nullptr) { return false; @@ -75,13 +58,8 @@ static bool ShouldDeferFullSongQuestGrant() { (player->stateFlags1 & PLAYER_STATE1_GETTING_ITEM) || (player->stateFlags1 & PLAYER_STATE1_CARRYING_ACTOR); } -bool SplitSongs::IsSongPart(RandomizerGet rg) { - return GetSongDefFromPart(rg) != nullptr; -} - -SplitSongId SplitSongs::GetSongIdFromPart(RandomizerGet rg) { - const SplitSongDef* def = GetSongDefFromPart(rg); - return def != nullptr ? def->id : SPLIT_SONG_MAX; +static bool UsingLogicSimulationBuffer(Logic* logic) { + return logic != nullptr && logic->mSaveContext != nullptr && logic->mSaveContext != &gSaveContext; } const SplitSongDef* SplitSongs::GetSongDef(SplitSongId id) { @@ -91,15 +69,6 @@ const SplitSongDef* SplitSongs::GetSongDef(SplitSongId id) { return &kSplitSongs[id]; } -const SplitSongDef* SplitSongs::GetSongDefFromPart(RandomizerGet rg) { - for (const auto& def : kSplitSongs) { - if (def.part1 == rg || def.part2 == rg) { - return &def; - } - } - return nullptr; -} - const SplitSongDef* SplitSongs::GetSongDefFromProgressive(RandomizerGet rg) { for (const auto& def : kSplitSongs) { if (def.progressive == rg) { @@ -122,27 +91,11 @@ bool SplitSongs::IsProgressiveSong(RandomizerGet rg) { return GetSongDefFromProgressive(rg) != nullptr; } -static bool UsingLogicSimulationBuffer(Logic* logic) { - return logic != nullptr && logic->mSaveContext != nullptr && logic->mSaveContext != &gSaveContext; -} - -bool SplitSongs::HasPart1(SplitSongId id) { - if (!IsValidSplitSongId(id)) { - return false; - } - const RandomizerInf flag = static_cast(kSplitSongPart1Flags[id]); - auto* logic = Context::GetInstance()->GetLogic().get(); - if (UsingLogicSimulationBuffer(logic)) { - return logic->CheckRandoInf(flag); - } - return Flags_GetRandomizerInf(flag) != 0; -} - -bool SplitSongs::HasPart2(SplitSongId id) { +bool SplitSongs::HasSplitPart(SplitSongId id) { if (!IsValidSplitSongId(id)) { return false; } - const RandomizerInf flag = static_cast(kSplitSongPart2Flags[id]); + const RandomizerInf flag = static_cast(kSplitSongPartFlags[id]); auto* logic = Context::GetInstance()->GetLogic().get(); if (UsingLogicSimulationBuffer(logic)) { return logic->CheckRandoInf(flag); @@ -150,27 +103,11 @@ bool SplitSongs::HasPart2(SplitSongId id) { return Flags_GetRandomizerInf(flag) != 0; } -bool SplitSongs::HasBothParts(SplitSongId id) { - return HasPart1(id) && HasPart2(id); -} - -void SplitSongs::SetPart1(SplitSongId id, bool state) { +void SplitSongs::SetSplitPart(SplitSongId id, bool state) { if (!IsValidSplitSongId(id)) { return; } - RandomizerInf flag = static_cast(kSplitSongPart1Flags[id]); - if (state) { - Flags_SetRandomizerInf(flag); - } else { - Flags_UnsetRandomizerInf(flag); - } -} - -void SplitSongs::SetPart2(SplitSongId id, bool state) { - if (!IsValidSplitSongId(id)) { - return; - } - RandomizerInf flag = static_cast(kSplitSongPart2Flags[id]); + RandomizerInf flag = static_cast(kSplitSongPartFlags[id]); if (state) { Flags_SetRandomizerInf(flag); } else { @@ -212,7 +149,7 @@ void SplitSongs::GrantFullSong(SplitSongId id) { } void SplitSongs::TryCompleteSong(SplitSongId id) { - if (!HasBothParts(id) || HasFullSong(id)) { + if (!HasSplitPart(id) || HasFullSong(id)) { return; } if (gPlayState == nullptr) { @@ -247,7 +184,7 @@ void SplitSongs::ProcessPendingFullSongGrants() { if (!IsValidSplitSongId(id)) { continue; } - if (HasBothParts(id) && !HasFullSong(id)) { + if (HasSplitPart(id) && !HasFullSong(id)) { GrantFullSong(id); } } @@ -260,58 +197,24 @@ void SplitSongs::ClearPendingFullSongGrants() { } } -void SplitSongs::OnItemReceived(RandomizerGet rg) { - const SplitSongDef* def = GetSongDefFromPart(rg); - if (def == nullptr) { - return; - } - - if (rg == def->part1) { - SetPart1(def->id, true); - } else if (rg == def->part2) { - SetPart2(def->id, true); - } - - TryCompleteSong(def->id); -} - void SplitSongs::OnProgressiveSongReceived(RandomizerGet rg) { const SplitSongDef* def = GetSongDefFromProgressive(rg); if (def == nullptr || HasFullSong(def->id)) { return; } - if (!HasPart1(def->id)) { - SetPart1(def->id, true); - } else if (!HasPart2(def->id)) { - SetPart2(def->id, true); + if (!HasSplitPart(def->id)) { + SetSplitPart(def->id, true); + return; } TryCompleteSong(def->id); } -ItemObtainability SplitSongs::GetPartObtainability(RandomizerGet partRg) { - const SplitSongDef* def = GetSongDefFromPart(partRg); - if (def == nullptr) { - return CAN_OBTAIN; - } - const auto qiIt = Logic::RandoGetToQuestItem.find(static_cast(def->fullSong)); - if (qiIt != Logic::RandoGetToQuestItem.end() && CHECK_QUEST_ITEM(qiIt->second)) { - return CANT_OBTAIN_ALREADY_HAVE; - } - if (partRg == def->part1 && HasPart1(def->id)) { - return CANT_OBTAIN_ALREADY_HAVE; - } - if (partRg == def->part2 && HasPart2(def->id)) { - return CANT_OBTAIN_ALREADY_HAVE; - } - return CAN_OBTAIN; -} - ItemObtainability SplitSongs::GetProgressiveSongObtainability(RandomizerGet progressiveRg) { const SplitSongDef* def = GetSongDefFromProgressive(progressiveRg); if (def == nullptr) { return CAN_OBTAIN; } - return HasBothParts(def->id) || HasFullSong(def->id) ? CANT_OBTAIN_ALREADY_HAVE : CAN_OBTAIN; + return HasFullSong(def->id) ? CANT_OBTAIN_ALREADY_HAVE : CAN_OBTAIN; } void SplitSongs::AppendShuffledSongPoolItems(std::vector& pool, bool split) { @@ -334,21 +237,13 @@ void SplitSongs::DebugGiveAllSongParts(PlayState* play) { return; } for (const auto& def : kSplitSongs) { - Randomizer_Item_Give(play, StaticData::RetrieveItem(def.part1).GetGIEntry_Copy()); - Randomizer_Item_Give(play, StaticData::RetrieveItem(def.part2).GetGIEntry_Copy()); - } -} - -void SplitSongs::ApplyPartEffectToLogicScratch(Logic* logic, RandomizerGet rg, bool state) { - if (!UsingLogicSimulationBuffer(logic)) { - return; - } - const SplitSongDef* def = GetSongDefFromPart(rg); - if (def == nullptr) { - return; + if (!HasSplitPart(def.id)) { + Randomizer_Item_Give(play, StaticData::RetrieveItem(def.progressive).GetGIEntry_Copy()); + } + if (!HasFullSong(def.id)) { + Randomizer_Item_Give(play, StaticData::RetrieveItem(def.progressive).GetGIEntry_Copy()); + } } - const uint32_t flag = (rg == def->part1) ? kSplitSongPart1Flags[def->id] : kSplitSongPart2Flags[def->id]; - logic->SetRandoInf(flag, state); } void SplitSongs::ApplyProgressiveEffectToLogicScratch(Logic* logic, RandomizerGet rg, bool state) { @@ -360,21 +255,16 @@ void SplitSongs::ApplyProgressiveEffectToLogicScratch(Logic* logic, RandomizerGe return; } - const uint32_t part1Flag = kSplitSongPart1Flags[def->id]; - const uint32_t part2Flag = kSplitSongPart2Flags[def->id]; - const bool hasPart1 = logic->CheckRandoInf(static_cast(part1Flag)); - const bool hasPart2 = logic->CheckRandoInf(static_cast(part2Flag)); - + const uint32_t partFlag = kSplitSongPartFlags[def->id]; if (state) { - if (!hasPart1) { - logic->SetRandoInf(part1Flag, true); - } else if (!hasPart2) { - logic->SetRandoInf(part2Flag, true); + if (!logic->CheckRandoInf(static_cast(partFlag))) { + logic->SetRandoInf(partFlag, true); + } else { + auto& fullSongItem = StaticData::RetrieveItem(def->fullSong); + logic->ApplyItemEffect(fullSongItem, true); } - } else if (hasPart2) { - logic->SetRandoInf(part2Flag, false); - } else if (hasPart1) { - logic->SetRandoInf(part1Flag, false); + } else if (logic->CheckRandoInf(static_cast(partFlag))) { + logic->SetRandoInf(partFlag, false); } } @@ -383,13 +273,13 @@ RandomizerGet SplitSongs::ResolveProgressiveSongStage(RandomizerGet rg) { if (def == nullptr) { return RG_NONE; } - if (!HasPart1(def->id)) { - return def->part1; + if (HasFullSong(def->id)) { + return def->fullSong; } - if (!HasPart2(def->id)) { - return def->part2; + if (HasSplitPart(def->id)) { + return def->fullSong; } - return def->fullSong; + return def->progressive; } } // namespace Rando diff --git a/soh/soh/Enhancements/randomizer/split_songs.h b/soh/soh/Enhancements/randomizer/split_songs.h index cfc7a90a383..18224b815d3 100644 --- a/soh/soh/Enhancements/randomizer/split_songs.h +++ b/soh/soh/Enhancements/randomizer/split_songs.h @@ -31,61 +31,40 @@ enum SplitSongId { struct SplitSongDef { SplitSongId id; RandomizerGet progressive; - RandomizerGet part1; - RandomizerGet part2; RandomizerGet fullSong; }; class SplitSongs { public: - static bool IsSongPart(RandomizerGet rg); - static SplitSongId GetSongIdFromPart(RandomizerGet rg); static const SplitSongDef* GetSongDef(SplitSongId id); - static const SplitSongDef* GetSongDefFromPart(RandomizerGet rg); static const SplitSongDef* GetSongDefFromProgressive(RandomizerGet rg); - /** Full-song RandomizerGet (RG_ZELDAS_LULLABY, …), not part items. */ static const SplitSongDef* GetSongDefFromFullSong(RandomizerGet fullSongRg); - static bool HasPart1(SplitSongId id); - static bool HasPart2(SplitSongId id); - static bool HasBothParts(SplitSongId id); - static bool IsProgressiveSong(RandomizerGet rg); - - static void SetPart1(SplitSongId id, bool state); - static void SetPart2(SplitSongId id, bool state); + /** RandInf set after the first progressive pickup for this song. */ + static bool HasSplitPart(SplitSongId id); + static void SetSplitPart(SplitSongId id, bool state); static bool HasFullSong(SplitSongId id); static void GrantFullSong(SplitSongId id); static void TryCompleteSong(SplitSongId id); - /** After get-item / cutscene-safe: applies deferred quest song when both parts were obtained mid get-item. */ + static bool IsProgressiveSong(RandomizerGet rg); + static void ProcessPendingFullSongGrants(); - /** Reset deferred grants (e.g. save load). */ static void ClearPendingFullSongGrants(); - static void OnItemReceived(RandomizerGet rg); static void OnProgressiveSongReceived(RandomizerGet rg); - // Item pool / ice-trap models: 12 full songs vs 24 part items (same 12 logical songs). static void AppendShuffledSongPoolItems(std::vector& pool, bool split); static void AppendSongIceTrapModels(std::vector& models, bool split); - /** In-world obtainability for RG_*_PART1/PART2 (duplicate parts / song already complete). */ - static ItemObtainability GetPartObtainability(RandomizerGet partRg); static ItemObtainability GetProgressiveSongObtainability(RandomizerGet progressiveRg); - /** Dev/testing: grant every song via Part 1 + Part 2 through the same path as chests/NPCs (Randomizer_Item_Give). - */ static void DebugGiveAllSongParts(PlayState* play); - /** - * During 3drando fill, Logic uses a scratch SaveContext. Song-part flags must live there (not gSaveContext). - * No-op when Logic is tied to the real gSaveContext (in-game). - */ - static void ApplyPartEffectToLogicScratch(Logic* logic, RandomizerGet rg, bool state); static void ApplyProgressiveEffectToLogicScratch(Logic* logic, RandomizerGet rg, bool state); - /** Returns the concrete part item represented by this progressive pickup at current state. */ + /** Item to show on pickup / GI resolve: progressive first, full song on second. */ static RandomizerGet ResolveProgressiveSongStage(RandomizerGet rg); }; From 72c849f0a7c655fc98374b3d4f6869ec911e2eb3 Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:51:04 +0100 Subject: [PATCH 10/22] Address split songs PR review feedback Use one AddItemToPool call per song (3,2,2,2) instead of doubling calls. Drop unrelated blue potion and key ring CustomIcon tweaks. Shorten option text now that Anywhere-only behavior is enforced in settings. --- .../randomizer/3drando/item_pool.cpp | 119 ++++-------------- soh/soh/Enhancements/randomizer/item_list.cpp | 22 ++-- .../randomizer/option_descriptions.cpp | 12 +- soh/soh/Enhancements/randomizer/settings.cpp | 2 +- .../Enhancements/randomizer/split_songs.cpp | 7 +- 5 files changed, 42 insertions(+), 120 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp index f5c669f1f77..e1e60c7adda 100644 --- a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp @@ -263,102 +263,37 @@ void GenerateItemPool() { if (ctx->GetOption(RSK_SHUFFLE_SONGS).IsNot(RO_SONG_SHUFFLE_OFF)) { bool songAnywhere = ctx->GetOption(RSK_SHUFFLE_SONGS).Is(RO_SONG_SHUFFLE_ANYWHERE); const bool split = ctx->GetOption(RSK_SPLIT_OCARINA_SONGS) && songAnywhere; - if (!ctx->GetOption(RSK_STARTING_ZELDAS_LULLABY).Get()) { - if (!split) { - AddItemToPool(RG_ZELDAS_LULLABY, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } else { - AddItemToPool(RG_PROGRESSIVE_ZELDAS_LULLABY, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_PROGRESSIVE_ZELDAS_LULLABY, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } - } - if (!ctx->GetOption(RSK_STARTING_EPONAS_SONG).Get()) { - if (!split) { - AddItemToPool(RG_EPONAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } else { - AddItemToPool(RG_PROGRESSIVE_EPONAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_PROGRESSIVE_EPONAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } - } - if (!ctx->GetOption(RSK_STARTING_SARIAS_SONG).Get()) { - if (!split) { - AddItemToPool(RG_SARIAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } else { - AddItemToPool(RG_PROGRESSIVE_SARIAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_PROGRESSIVE_SARIAS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } - } - if (!ctx->GetOption(RSK_STARTING_SUNS_SONG).Get()) { - if (!split) { - AddItemToPool(RG_SUNS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } else { - AddItemToPool(RG_PROGRESSIVE_SUNS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_PROGRESSIVE_SUNS_SONG, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } - } - if (!ctx->GetOption(RSK_STARTING_SONG_OF_TIME).Get()) { - if (!split) { - AddItemToPool(RG_SONG_OF_TIME, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } else { - AddItemToPool(RG_PROGRESSIVE_SONG_OF_TIME, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_PROGRESSIVE_SONG_OF_TIME, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } - } - if (!ctx->GetOption(RSK_STARTING_SONG_OF_STORMS).Get()) { - if (!split) { - AddItemToPool(RG_SONG_OF_STORMS, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } else { - AddItemToPool(RG_PROGRESSIVE_SONG_OF_STORMS, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_PROGRESSIVE_SONG_OF_STORMS, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + const int defaultPlentiful = songAnywhere ? 2 : 1; + auto addShuffledSong = [&](RandomizerGet fullSong, RandomizerGet progressive, bool hasStarting) { + if (hasStarting) { + return; } - } - if (!ctx->GetOption(RSK_STARTING_MINUET_OF_FOREST).Get()) { - if (!split) { - AddItemToPool(RG_MINUET_OF_FOREST, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } else { - AddItemToPool(RG_PROGRESSIVE_MINUET_OF_FOREST, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_PROGRESSIVE_MINUET_OF_FOREST, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } - } - if (!ctx->GetOption(RSK_STARTING_BOLERO_OF_FIRE).Get()) { - if (!split) { - AddItemToPool(RG_BOLERO_OF_FIRE, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } else { - AddItemToPool(RG_PROGRESSIVE_BOLERO_OF_FIRE, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_PROGRESSIVE_BOLERO_OF_FIRE, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } - } - if (!ctx->GetOption(RSK_STARTING_SERENADE_OF_WATER).Get()) { - if (!split) { - AddItemToPool(RG_SERENADE_OF_WATER, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } else { - AddItemToPool(RG_PROGRESSIVE_SERENADE_OF_WATER, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_PROGRESSIVE_SERENADE_OF_WATER, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } - } - if (!ctx->GetOption(RSK_STARTING_REQUIEM_OF_SPIRIT).Get()) { - if (!split) { - AddItemToPool(RG_REQUIEM_OF_SPIRIT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } else { - AddItemToPool(RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } - } - if (!ctx->GetOption(RSK_STARTING_NOCTURNE_OF_SHADOW).Get()) { if (!split) { - AddItemToPool(RG_NOCTURNE_OF_SHADOW, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(fullSong, defaultPlentiful, 1, 1, 1, songAnywhere); } else { - AddItemToPool(RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); + AddItemToPool(progressive, 3, 2, 2, 2, songAnywhere); } - } - if (!ctx->GetOption(RSK_STARTING_PRELUDE_OF_LIGHT).Get()) { - if (!split) { - AddItemToPool(RG_PRELUDE_OF_LIGHT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } else { - AddItemToPool(RG_PROGRESSIVE_PRELUDE_OF_LIGHT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - AddItemToPool(RG_PROGRESSIVE_PRELUDE_OF_LIGHT, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); - } - } + }; + addShuffledSong(RG_ZELDAS_LULLABY, RG_PROGRESSIVE_ZELDAS_LULLABY, + ctx->GetOption(RSK_STARTING_ZELDAS_LULLABY).Get()); + addShuffledSong(RG_EPONAS_SONG, RG_PROGRESSIVE_EPONAS_SONG, ctx->GetOption(RSK_STARTING_EPONAS_SONG).Get()); + addShuffledSong(RG_SARIAS_SONG, RG_PROGRESSIVE_SARIAS_SONG, ctx->GetOption(RSK_STARTING_SARIAS_SONG).Get()); + addShuffledSong(RG_SUNS_SONG, RG_PROGRESSIVE_SUNS_SONG, ctx->GetOption(RSK_STARTING_SUNS_SONG).Get()); + addShuffledSong(RG_SONG_OF_TIME, RG_PROGRESSIVE_SONG_OF_TIME, ctx->GetOption(RSK_STARTING_SONG_OF_TIME).Get()); + addShuffledSong(RG_SONG_OF_STORMS, RG_PROGRESSIVE_SONG_OF_STORMS, + ctx->GetOption(RSK_STARTING_SONG_OF_STORMS).Get()); + addShuffledSong(RG_MINUET_OF_FOREST, RG_PROGRESSIVE_MINUET_OF_FOREST, + ctx->GetOption(RSK_STARTING_MINUET_OF_FOREST).Get()); + addShuffledSong(RG_BOLERO_OF_FIRE, RG_PROGRESSIVE_BOLERO_OF_FIRE, + ctx->GetOption(RSK_STARTING_BOLERO_OF_FIRE).Get()); + addShuffledSong(RG_SERENADE_OF_WATER, RG_PROGRESSIVE_SERENADE_OF_WATER, + ctx->GetOption(RSK_STARTING_SERENADE_OF_WATER).Get()); + addShuffledSong(RG_REQUIEM_OF_SPIRIT, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, + ctx->GetOption(RSK_STARTING_REQUIEM_OF_SPIRIT).Get()); + addShuffledSong(RG_NOCTURNE_OF_SHADOW, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, + ctx->GetOption(RSK_STARTING_NOCTURNE_OF_SHADOW).Get()); + addShuffledSong(RG_PRELUDE_OF_LIGHT, RG_PROGRESSIVE_PRELUDE_OF_LIGHT, + ctx->GetOption(RSK_STARTING_PRELUDE_OF_LIGHT).Get()); } else { ctx->PlaceItemInLocation(RC_SHEIK_IN_FOREST, RG_MINUET_OF_FOREST, false, true); ctx->PlaceItemInLocation(RC_SHEIK_IN_CRATER, RG_BOLERO_OF_FIRE, false, true); diff --git a/soh/soh/Enhancements/randomizer/item_list.cpp b/soh/soh/Enhancements/randomizer/item_list.cpp index 0620aad56bb..28338fa86ae 100644 --- a/soh/soh/Enhancements/randomizer/item_list.cpp +++ b/soh/soh/Enhancements/randomizer/item_list.cpp @@ -90,7 +90,7 @@ void Rando::StaticData::InitItemTable() { itemTable[RG_BOTTLE_WITH_MILK] = Item(RG_BOTTLE_WITH_MILK, Text{ "Bottle with Milk", "Bouteille avec du Lait", "Flasche mit Milch" }, ITEMTYPE_ITEM, GI_MILK_BOTTLE, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_MILK, ITEM_MILK_BOTTLE, OBJECT_GI_MILK, GID_MILK, 0x98, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_RED_POTION] = Item(RG_BOTTLE_WITH_RED_POTION, Text{ "Bottle with Red Potion", "Bouteille avec une Potion Rouge", "Flasche mit rotem Elixier" }, ITEMTYPE_ITEM, 0x8C, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_RED_POTION, RG_BOTTLE_WITH_RED_POTION, OBJECT_GI_LIQUID, GID_POTION_RED, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_GREEN_POTION] = Item(RG_BOTTLE_WITH_GREEN_POTION, Text{ "Bottle with Green Potion", "Bouteille avec une Potion Verte", "Flasche mit grünem Elixier" }, ITEMTYPE_ITEM, 0x8D, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_GREEN_POTION, RG_BOTTLE_WITH_GREEN_POTION, OBJECT_GI_LIQUID, GID_POTION_GREEN, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); - itemTable[RG_BOTTLE_WITH_BLUE_POTION] = Item(RG_BOTTLE_WITH_BLUE_POTION, Text{ "Bottle with Blue Potion", "Bouteille avec une Potion Bleue", "Flasche mit blauem Elixier" }, ITEMTYPE_ITEM, 0x8E, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_BLUE_POTION, RG_BOTTLE_WITH_BLUE_POTION, OBJECT_GI_LIQUID, GID_POTION_BLUE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}).CustomIcon(gItemIconBottlePotionBlueTex); + itemTable[RG_BOTTLE_WITH_BLUE_POTION] = Item(RG_BOTTLE_WITH_BLUE_POTION, Text{ "Bottle with Blue Potion", "Bouteille avec une Potion Bleue", "Flasche mit blauem Elixier" }, ITEMTYPE_ITEM, 0x8E, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_BLUE_POTION, RG_BOTTLE_WITH_BLUE_POTION, OBJECT_GI_LIQUID, GID_POTION_BLUE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_FAIRY] = Item(RG_BOTTLE_WITH_FAIRY, Text{ "Bottle with Fairy", "Bouteille avec une Fée", "Flasche mit Fee" }, ITEMTYPE_ITEM, 0x8F, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_FAIRY, RG_BOTTLE_WITH_FAIRY, OBJECT_GI_BOTTLE, GID_BOTTLE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_FISH] = Item(RG_BOTTLE_WITH_FISH, Text{ "Bottle with Fish", "Bouteille avec un Poisson", "Flasche mit Fisch" }, ITEMTYPE_ITEM, 0x90, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_FISH, RG_BOTTLE_WITH_FISH, OBJECT_GI_FISH, GID_FISH, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_BLUE_FIRE] = Item(RG_BOTTLE_WITH_BLUE_FIRE, Text{ "Bottle with Blue Fire", "Bouteille avec une Flamme Bleue", "Flasche mit blauem Feuer" }, ITEMTYPE_ITEM, 0x91, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_BLUE_FIRE, RG_BOTTLE_WITH_BLUE_FIRE, OBJECT_GI_FIRE, GID_BLUE_FIRE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); @@ -233,25 +233,25 @@ void Rando::StaticData::InitItemTable() { itemTable[RG_FISHING_HOLE_KEY] = Item(RG_FISHING_HOLE_KEY, Text{ "Fishing Hole Key", "Clé de l'Étang", "Schlüssel für den Fischweiher" }, ITEMTYPE_ITEM, GI_DOOR_KEY, true, LOGIC_FISHING_HOLE_KEY, RHT_OVERWORLD_KEY, RG_FISHING_HOLE_KEY, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "la ", "den "}).CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_FISHING_HOLE_KEY].SetCustomDrawFunc(Randomizer_DrawOverworldKey); // Key Rings - itemTable[RG_FOREST_TEMPLE_KEY_RING] = Item(RG_FOREST_TEMPLE_KEY_RING, Text{ "Forest Temple Key Ring", "Trousseau du Temple de la Forêt", "Schlüsselbund für den Waldtempel" }, ITEMTYPE_SMALLKEY, 0xD5, true, LOGIC_FOREST_TEMPLE_KEYS, RHT_FOREST_TEMPLE_KEY_RING, RG_FOREST_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%g").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_FOREST_TEMPLE_KEY_RING] = Item(RG_FOREST_TEMPLE_KEY_RING, Text{ "Forest Temple Key Ring", "Trousseau du Temple de la Forêt", "Schlüsselbund für den Waldtempel" }, ITEMTYPE_SMALLKEY, 0xD5, true, LOGIC_FOREST_TEMPLE_KEYS, RHT_FOREST_TEMPLE_KEY_RING, RG_FOREST_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%g"); itemTable[RG_FOREST_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_FIRE_TEMPLE_KEY_RING] = Item(RG_FIRE_TEMPLE_KEY_RING, Text{ "Fire Temple Key Ring", "Trousseau du Temple du Feu", "Schlüsselbund für den Feuertempel" }, ITEMTYPE_SMALLKEY, 0xD6, true, LOGIC_FIRE_TEMPLE_KEYS, RHT_FIRE_TEMPLE_KEY_RING, RG_FIRE_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%r").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_FIRE_TEMPLE_KEY_RING] = Item(RG_FIRE_TEMPLE_KEY_RING, Text{ "Fire Temple Key Ring", "Trousseau du Temple du Feu", "Schlüsselbund für den Feuertempel" }, ITEMTYPE_SMALLKEY, 0xD6, true, LOGIC_FIRE_TEMPLE_KEYS, RHT_FIRE_TEMPLE_KEY_RING, RG_FIRE_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%r"); itemTable[RG_FIRE_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_WATER_TEMPLE_KEY_RING] = Item(RG_WATER_TEMPLE_KEY_RING, Text{ "Water Temple Key Ring", "Trousseau du Temple de l'Eau", "Schlüsselbund für den Wassertempel" }, ITEMTYPE_SMALLKEY, 0xD7, true, LOGIC_WATER_TEMPLE_KEYS, RHT_WATER_TEMPLE_KEY_RING, RG_WATER_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%b").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_WATER_TEMPLE_KEY_RING] = Item(RG_WATER_TEMPLE_KEY_RING, Text{ "Water Temple Key Ring", "Trousseau du Temple de l'Eau", "Schlüsselbund für den Wassertempel" }, ITEMTYPE_SMALLKEY, 0xD7, true, LOGIC_WATER_TEMPLE_KEYS, RHT_WATER_TEMPLE_KEY_RING, RG_WATER_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%b"); itemTable[RG_WATER_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_SPIRIT_TEMPLE_KEY_RING] = Item(RG_SPIRIT_TEMPLE_KEY_RING, Text{ "Spirit Temple Key Ring", "Trousseau du Temple de l'Esprit", "Schlüsselbund für den Geistertempel" }, ITEMTYPE_SMALLKEY, 0xD8, true, LOGIC_SPIRIT_TEMPLE_KEYS, RHT_SPIRIT_TEMPLE_KEY_RING, RG_SPIRIT_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_SPIRIT_TEMPLE_KEY_RING] = Item(RG_SPIRIT_TEMPLE_KEY_RING, Text{ "Spirit Temple Key Ring", "Trousseau du Temple de l'Esprit", "Schlüsselbund für den Geistertempel" }, ITEMTYPE_SMALLKEY, 0xD8, true, LOGIC_SPIRIT_TEMPLE_KEYS, RHT_SPIRIT_TEMPLE_KEY_RING, RG_SPIRIT_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y"); itemTable[RG_SPIRIT_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_SHADOW_TEMPLE_KEY_RING] = Item(RG_SHADOW_TEMPLE_KEY_RING, Text{ "Shadow Temple Key Ring", "Trousseau du Temple de l'Ombre", "Schlüsselbund für den Schattentempel" }, ITEMTYPE_SMALLKEY, 0xD9, true, LOGIC_SHADOW_TEMPLE_KEYS, RHT_SHADOW_TEMPLE_KEY_RING, RG_SHADOW_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%p").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_SHADOW_TEMPLE_KEY_RING] = Item(RG_SHADOW_TEMPLE_KEY_RING, Text{ "Shadow Temple Key Ring", "Trousseau du Temple de l'Ombre", "Schlüsselbund für den Schattentempel" }, ITEMTYPE_SMALLKEY, 0xD9, true, LOGIC_SHADOW_TEMPLE_KEYS, RHT_SHADOW_TEMPLE_KEY_RING, RG_SHADOW_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%p"); itemTable[RG_SHADOW_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_BOTTOM_OF_THE_WELL_KEY_RING] = Item(RG_BOTTOM_OF_THE_WELL_KEY_RING, Text{ "Bottom of the Well Key Ring", "Trousseau du Puits", "Schlüsselbund für den Grund des Brunnens" }, ITEMTYPE_SMALLKEY, 0xDA, true, LOGIC_BOTTOM_OF_THE_WELL_KEYS, RHT_BOTTOM_OF_THE_WELL_KEY_RING, RG_BOTTOM_OF_THE_WELL_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%p").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_BOTTOM_OF_THE_WELL_KEY_RING] = Item(RG_BOTTOM_OF_THE_WELL_KEY_RING, Text{ "Bottom of the Well Key Ring", "Trousseau du Puits", "Schlüsselbund für den Grund des Brunnens" }, ITEMTYPE_SMALLKEY, 0xDA, true, LOGIC_BOTTOM_OF_THE_WELL_KEYS, RHT_BOTTOM_OF_THE_WELL_KEY_RING, RG_BOTTOM_OF_THE_WELL_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%p"); itemTable[RG_BOTTOM_OF_THE_WELL_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_GERUDO_TRAINING_GROUND_KEY_RING] = Item(RG_GERUDO_TRAINING_GROUND_KEY_RING, Text{ "Training Ground Key Ring", "Trousseau du Gymnase Gerudo", "Schlüsselbund für das Gerudo-Trainingsgelände" }, ITEMTYPE_SMALLKEY, 0xDB, true, LOGIC_GERUDO_TRAINING_GROUND_KEYS, RHT_GERUDO_TRAINING_GROUND_KEY_RING, RG_GERUDO_TRAINING_GROUND_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_GERUDO_TRAINING_GROUND_KEY_RING] = Item(RG_GERUDO_TRAINING_GROUND_KEY_RING, Text{ "Training Ground Key Ring", "Trousseau du Gymnase Gerudo", "Schlüsselbund für das Gerudo-Trainingsgelände" }, ITEMTYPE_SMALLKEY, 0xDB, true, LOGIC_GERUDO_TRAINING_GROUND_KEYS, RHT_GERUDO_TRAINING_GROUND_KEY_RING, RG_GERUDO_TRAINING_GROUND_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y"); itemTable[RG_GERUDO_TRAINING_GROUND_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_GERUDO_FORTRESS_KEY_RING] = Item(RG_GERUDO_FORTRESS_KEY_RING, Text{ "Gerudo Fortress Key Ring", "Trousseau du Repaire des Voleurs", "Schlüsselbund für die Gerudo-Festung" }, ITEMTYPE_FORTRESS_SMALLKEY, 0xDC, true, LOGIC_GERUDO_FORTRESS_KEYS, RHT_GERUDO_FORTRESS_KEY_RING, RG_GERUDO_FORTRESS_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_GERUDO_FORTRESS_KEY_RING] = Item(RG_GERUDO_FORTRESS_KEY_RING, Text{ "Gerudo Fortress Key Ring", "Trousseau du Repaire des Voleurs", "Schlüsselbund für die Gerudo-Festung" }, ITEMTYPE_FORTRESS_SMALLKEY, 0xDC, true, LOGIC_GERUDO_FORTRESS_KEYS, RHT_GERUDO_FORTRESS_KEY_RING, RG_GERUDO_FORTRESS_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y"); itemTable[RG_GERUDO_FORTRESS_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_GANONS_CASTLE_KEY_RING] = Item(RG_GANONS_CASTLE_KEY_RING, Text{ "Ganon's Castle Key Ring", "Trousseau du Château de Ganon", "Schlüsselbund für Ganons Schloß" }, ITEMTYPE_SMALLKEY, 0xDD, true, LOGIC_GANONS_CASTLE_KEYS, RHT_GANONS_CASTLE_KEY_RING, RG_GANONS_CASTLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%r").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_GANONS_CASTLE_KEY_RING] = Item(RG_GANONS_CASTLE_KEY_RING, Text{ "Ganon's Castle Key Ring", "Trousseau du Château de Ganon", "Schlüsselbund für Ganons Schloß" }, ITEMTYPE_SMALLKEY, 0xDD, true, LOGIC_GANONS_CASTLE_KEYS, RHT_GANONS_CASTLE_KEY_RING, RG_GANONS_CASTLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%r"); itemTable[RG_GANONS_CASTLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_TREASURE_GAME_KEY_RING] = Item(RG_TREASURE_GAME_KEY_RING, Text{ "Chest Game Key Ring", "Trousseau du jeu la Chasse-aux-Trésors", "Schlüsselbund für das Truhenspiel" }, ITEMTYPE_SMALLKEY, 0xDE, true, LOGIC_TREASURE_GAME_KEYS, RHT_TREASURE_GAME_KEY_RING, RG_TREASURE_GAME_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}).CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_TREASURE_GAME_KEY_RING] = Item(RG_TREASURE_GAME_KEY_RING, Text{ "Chest Game Key Ring", "Trousseau du jeu la Chasse-aux-Trésors", "Schlüsselbund für das Truhenspiel" }, ITEMTYPE_SMALLKEY, 0xDE, true, LOGIC_TREASURE_GAME_KEYS, RHT_TREASURE_GAME_KEY_RING, RG_TREASURE_GAME_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}); itemTable[RG_TREASURE_GAME_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); // Dungeon Rewards itemTable[RG_KOKIRI_EMERALD] = Item(RG_KOKIRI_EMERALD, Text{ "Kokiri's Emerald", "Émeraude Kokiri", "Kokiri-Smaragd" }, ITEMTYPE_DUNGEONREWARD, 0xCB, true, LOGIC_KOKIRI_EMERALD, RHT_KOKIRI_EMERALD, ITEM_KOKIRI_EMERALD, OBJECT_GI_JEWEL, GID_KOKIRI_EMERALD, 0x80, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"the ", "l'", "den "}, "%g"); diff --git a/soh/soh/Enhancements/randomizer/option_descriptions.cpp b/soh/soh/Enhancements/randomizer/option_descriptions.cpp index 0daa222edae..16c2d0fa830 100644 --- a/soh/soh/Enhancements/randomizer/option_descriptions.cpp +++ b/soh/soh/Enhancements/randomizer/option_descriptions.cpp @@ -218,16 +218,8 @@ void Settings::CreateOptionDescriptions() { "\n" "Anywhere - Songs can appear at any location."; mOptionDescriptions[RSK_SPLIT_OCARINA_SONGS] = - "Randomizer option next to Shuffle Songs.\n" - "\n" - "Each of the 12 ocarina songs is split into a 2-step progressive song item. Collecting both progressive " - "steps grants the full song.\n" - "\n" - "Requires Shuffle Songs: \"Anywhere\" — Song Locations / Dungeon Rewards only have 12 song slots, so split " - "parts are not generated there.\n" - "\n" - "When enabled with Anywhere, the item pool places two progressive song pickups per song (24 total song " - "progressive pickups instead of 12 full songs)."; + "Each ocarina song becomes a progressive song item (like bomb bag upgrades). The first pickup marks " + "progress; the second grants the full song. The pool includes extra copies per song to reduce softlocks."; mOptionDescriptions[RSK_SHUFFLE_TOKENS] = "Shuffles Golden Skulltula Tokens into the item pool. This means " "Golden Skulltulas can contain other items as well.\n" "\n" diff --git a/soh/soh/Enhancements/randomizer/settings.cpp b/soh/soh/Enhancements/randomizer/settings.cpp index 51d967e25ac..5587391bb9f 100644 --- a/soh/soh/Enhancements/randomizer/settings.cpp +++ b/soh/soh/Enhancements/randomizer/settings.cpp @@ -615,7 +615,7 @@ void Settings::CreateOptions() { } else if (shuffleSongs != RO_SONG_SHUFFLE_OFF) { mOptions[RSK_SPLIT_OCARINA_SONGS].Disable( "Split Ocarina Songs is only supported when Shuffle Songs is \"Anywhere\" (other modes keep 12 song " - "checks, not 24 parts)."); + "checks)."); } else { mOptions[RSK_SPLIT_OCARINA_SONGS].Disable( "Turn on Shuffle Songs to use Split Ocarina Songs (then set Shuffle Songs to \"Anywhere\")."); diff --git a/soh/soh/Enhancements/randomizer/split_songs.cpp b/soh/soh/Enhancements/randomizer/split_songs.cpp index 77dbc8c6d06..c0a17cfd9a7 100644 --- a/soh/soh/Enhancements/randomizer/split_songs.cpp +++ b/soh/soh/Enhancements/randomizer/split_songs.cpp @@ -219,12 +219,7 @@ ItemObtainability SplitSongs::GetProgressiveSongObtainability(RandomizerGet prog void SplitSongs::AppendShuffledSongPoolItems(std::vector& pool, bool split) { for (const auto& def : kSplitSongs) { - if (split) { - pool.push_back(def.progressive); - pool.push_back(def.progressive); - } else { - pool.push_back(def.fullSong); - } + pool.push_back(split ? def.progressive : def.fullSong); } } From 11a4287ec15dcac5d46b40644637765be9dde66c Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Thu, 4 Jun 2026 17:29:23 +0100 Subject: [PATCH 11/22] Fix ItemMessages build after develop merge Restore GameInteractor hook includes removed during the develop merge. RegisterItemMessages still uses COND_ID_HOOK macros from GameInteractor_Hooks.h. --- soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp index 8cda168be13..ca188712f36 100644 --- a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp @@ -6,6 +6,8 @@ */ #include #include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" #include "soh/Enhancements/randomizer/split_songs.h" #include "soh/Enhancements/randomizer/static_data.h" #include "soh/Enhancements/custom-message/CustomMessageTypes.h" From f8464be10897d2b98ab61b910ee2df10aa097787 Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:16:51 +0100 Subject: [PATCH 12/22] Bump libultraship to match upstream develop after merge The develop merge updated SoH sources but left libultraship on an older commit missing Fast3dGui.h and Fast::WindowBackend, breaking all platforms. --- libultraship | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libultraship b/libultraship index fdcaf633677..f30fe0ed1e9 160000 --- a/libultraship +++ b/libultraship @@ -1 +1 @@ -Subproject commit fdcaf6336776d24a6408d016b0a52243f108f250 +Subproject commit f30fe0ed1e9a73ee6f47067a56fa22039c397baf From 10f462d8735c5e8d194fb8e1b1d6d8b0bda15aa1 Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:16:51 +0100 Subject: [PATCH 13/22] Fix split song tracker texture lookup for Fast3dGui API DrawSplitSongProgress must cast Ship::Gui to Fast::Fast3dGui before GetTextureByName, matching DrawSong and the rest of the item tracker. --- soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp index 6d7cbea95d9..239b2d85ea8 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp @@ -1490,8 +1490,8 @@ void DrawSplitSongProgress(ItemTrackerItem item) { ImGui::BeginGroup(); ImVec2 p = ImGui::GetCursorScreenPos(); ImGui::SetCursorScreenPos(ImVec2(p.x + 6, p.y)); - ImGui::Image(Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName( - hasAnyPart && IsValidSaveFile() ? item.name : item.nameFaded), + ImGui::Image(std::dynamic_pointer_cast(Ship::Context::GetInstance()->GetWindow()->GetGui()) + ->GetTextureByName(hasAnyPart && IsValidSaveFile() ? item.name : item.nameFaded), ImVec2(iconSize / 1.5f, iconSize), ImVec2(0, 0), ImVec2(1, 1)); const RandomizerCheckArea area = static_cast(item.data); From 2340e5ac554ef618ce8fde10ce6c9d3ad41934e8 Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:16:51 +0100 Subject: [PATCH 14/22] Fix seed generation crash when regenerating with split songs Clear the seed-generated flag before fill so the item tracker does not read spoiler locations while the generator thread resets context. Skip song-part spoiler lookups while RandoGenerating is set. Break infinite GetGIEntry recursion for first-stage progressive song items. --- soh/soh/Enhancements/randomizer/3drando/playthrough.cpp | 1 + soh/soh/Enhancements/randomizer/item.cpp | 9 ++++++++- .../Enhancements/randomizer/randomizer_item_tracker.cpp | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/randomizer/3drando/playthrough.cpp b/soh/soh/Enhancements/randomizer/3drando/playthrough.cpp index 4ee635323cc..2e14f2fe898 100644 --- a/soh/soh/Enhancements/randomizer/3drando/playthrough.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/playthrough.cpp @@ -21,6 +21,7 @@ int Playthrough_Init(uint32_t seed, std::set excludedLocations, Random_Init(seed); auto ctx = Rando::Context::GetInstance(); + ctx->SetSeedGenerated(false); ctx->overrides.clear(); ctx->ItemReset(); ctx->HintReset(); diff --git a/soh/soh/Enhancements/randomizer/item.cpp b/soh/soh/Enhancements/randomizer/item.cpp index fdf09b64987..381eb62e2bd 100644 --- a/soh/soh/Enhancements/randomizer/item.cpp +++ b/soh/soh/Enhancements/randomizer/item.cpp @@ -384,9 +384,16 @@ std::shared_ptr Item::GetGIEntry() const { // NOLINT(*-no-recursio case RG_PROGRESSIVE_SERENADE_OF_WATER: case RG_PROGRESSIVE_REQUIEM_OF_SPIRIT: case RG_PROGRESSIVE_NOCTURNE_OF_SHADOW: - case RG_PROGRESSIVE_PRELUDE_OF_LIGHT: + case RG_PROGRESSIVE_PRELUDE_OF_LIGHT: { actual = SplitSongs::ResolveProgressiveSongStage(randomizerGet); + if (actual == randomizerGet || actual == RG_NONE) { + const SplitSongDef* def = SplitSongs::GetSongDefFromProgressive(randomizerGet); + if (def != nullptr) { + return StaticData::RetrieveItem(def->fullSong).GetGIEntry(); + } + } break; + } case RG_PROGRESSIVE_BOMBCHU_BAG: if (OTRGlobals::Instance->gRandoContext->GetOption(RSK_BOMBCHU_BAG).Is(RO_BOMBCHU_BAG_SINGLE)) { if (logic->CurrentInventory(ITEM_BOMBCHU) != ITEM_NONE) { diff --git a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp index 239b2d85ea8..83086fdb2f5 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp @@ -511,6 +511,9 @@ static int ItemTrackerEffectiveSongPartsDisplay() { static std::unordered_map BuildSongPartSpoilerAreas() { std::unordered_map partRgToArea; + if (CVarGetInteger(CVAR_GENERAL("RandoGenerating"), 0) != 0) { + return partRgToArea; + } if (!GameInteractor::IsSaveLoaded() || !IS_RANDO) { return partRgToArea; } From 3e1aff77f45b507cf3cfc5859e088642284930a1 Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:16:51 +0100 Subject: [PATCH 15/22] Fix split song part-1 grant and restore key ring icons Progressive song pickups no longer resolve to full-song GI entries on the first piece; part tracking completes the song on the second pickup. Re-add key ring CustomIcon assignments dropped during the develop merge. --- .../Enhancements/randomizer/SeedContext.cpp | 7 --- soh/soh/Enhancements/randomizer/item.cpp | 9 +--- soh/soh/Enhancements/randomizer/item_list.cpp | 46 +++++++++---------- .../Enhancements/randomizer/randomizer.cpp | 4 -- 4 files changed, 24 insertions(+), 42 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/SeedContext.cpp b/soh/soh/Enhancements/randomizer/SeedContext.cpp index 7d1488ca270..71bbd84d2b8 100644 --- a/soh/soh/Enhancements/randomizer/SeedContext.cpp +++ b/soh/soh/Enhancements/randomizer/SeedContext.cpp @@ -8,7 +8,6 @@ #include "settings.h" #include "rando_hash.h" #include "fishsanity.h" -#include "split_songs.h" #include "macros.h" #include "3drando/hints.hpp" #include "soh/util.h" @@ -403,12 +402,6 @@ GetItemEntry Context::GetFinalGIEntry(const RandomizerCheck rc, const bool check return ItemTableManager::Instance->RetrieveItemEntry(MOD_NONE, GI_RUPEE_BLUE); } GetItemEntry giEntry = itemLoc->GetPlacedItem().GetGIEntry_Copy(); - if (SplitSongs::IsProgressiveSong(itemLoc->GetPlacedRandomizerGet())) { - const RandomizerGet stage = SplitSongs::ResolveProgressiveSongStage(itemLoc->GetPlacedRandomizerGet()); - if (stage != RG_NONE) { - giEntry = StaticData::RetrieveItem(stage).GetGIEntry_Copy(); - } - } if (overrides.contains(rc)) { const auto fakeGiEntry = StaticData::RetrieveItem(overrides[rc].LooksLike()).GetGIEntry(); giEntry.gid = fakeGiEntry->gid; diff --git a/soh/soh/Enhancements/randomizer/item.cpp b/soh/soh/Enhancements/randomizer/item.cpp index 381eb62e2bd..fdf09b64987 100644 --- a/soh/soh/Enhancements/randomizer/item.cpp +++ b/soh/soh/Enhancements/randomizer/item.cpp @@ -384,16 +384,9 @@ std::shared_ptr Item::GetGIEntry() const { // NOLINT(*-no-recursio case RG_PROGRESSIVE_SERENADE_OF_WATER: case RG_PROGRESSIVE_REQUIEM_OF_SPIRIT: case RG_PROGRESSIVE_NOCTURNE_OF_SHADOW: - case RG_PROGRESSIVE_PRELUDE_OF_LIGHT: { + case RG_PROGRESSIVE_PRELUDE_OF_LIGHT: actual = SplitSongs::ResolveProgressiveSongStage(randomizerGet); - if (actual == randomizerGet || actual == RG_NONE) { - const SplitSongDef* def = SplitSongs::GetSongDefFromProgressive(randomizerGet); - if (def != nullptr) { - return StaticData::RetrieveItem(def->fullSong).GetGIEntry(); - } - } break; - } case RG_PROGRESSIVE_BOMBCHU_BAG: if (OTRGlobals::Instance->gRandoContext->GetOption(RSK_BOMBCHU_BAG).Is(RO_BOMBCHU_BAG_SINGLE)) { if (logic->CurrentInventory(ITEM_BOMBCHU) != ITEM_NONE) { diff --git a/soh/soh/Enhancements/randomizer/item_list.cpp b/soh/soh/Enhancements/randomizer/item_list.cpp index 7d67fc99940..79c6df5095f 100644 --- a/soh/soh/Enhancements/randomizer/item_list.cpp +++ b/soh/soh/Enhancements/randomizer/item_list.cpp @@ -73,24 +73,24 @@ void Rando::StaticData::InitItemTable() { itemTable[RG_PROGRESSIVE_MAGIC_METER] = Item(RG_PROGRESSIVE_MAGIC_METER, Text{ "Progressive Magic Meter", "Jauge de Magie (prog.)", "Progressives Magisches Maß" }, ITEMTYPE_ITEM, 0x8A, true, LOGIC_PROGRESSIVE_MAGIC, RHT_PROGRESSIVE_MAGIC_METER, ITEM_CATEGORY_MAJOR, {"a ", "une ", "ein "}, "%g", true).CustomIcon(gQuestIconMagicJarBigTex, ICON_SIZE_24); itemTable[RG_PROGRESSIVE_OCARINA] = Item(RG_PROGRESSIVE_OCARINA, Text{ "Progressive Ocarina", "Ocarina (prog.)", "Progressive Okarina" }, ITEMTYPE_ITEM, 0x8B, true, LOGIC_PROGRESSIVE_OCARINA, RHT_PROGRESSIVE_OCARINA, ITEM_CATEGORY_MAJOR, {"a ", "un ", "eine "}, "%g", true); itemTable[RG_PROGRESSIVE_GORONSWORD] = Item(RG_PROGRESSIVE_GORONSWORD, Text{ "Progressive Goron Sword", "Épée Goron (prog.)", "Progressives Goronen-Schwert" }, ITEMTYPE_ITEM, 0xD4, true, LOGIC_PROGRESSIVE_GIANT_KNIFE, RHT_PROGRESSIVE_GORONSWORD, ITEM_CATEGORY_MAJOR, {"a ", "une ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_ZELDAS_LULLABY] = Item(RG_PROGRESSIVE_ZELDAS_LULLABY, Text{ "Progressive Zelda's Lullaby", "Berceuse de Zelda (prog.)", "Progressives Zeldas Wiegenlied" }, ITEMTYPE_SONG, 0xE1, true, LOGIC_NONE, RHT_ZELDAS_LULLABY, ITEM_CATEGORY_MAJOR, {"a ", "une ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_EPONAS_SONG] = Item(RG_PROGRESSIVE_EPONAS_SONG, Text{ "Progressive Epona's Song", "Chant d'Epona (prog.)", "Progressives Eponas Lied" }, ITEMTYPE_SONG, 0xE2, true, LOGIC_NONE, RHT_EPONAS_SONG, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_SARIAS_SONG] = Item(RG_PROGRESSIVE_SARIAS_SONG, Text{ "Progressive Saria's Song", "Chant de Saria (prog.)", "Progressives Salias Lied" }, ITEMTYPE_SONG, 0xE3, true, LOGIC_NONE, RHT_SARIAS_SONG, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_SUNS_SONG] = Item(RG_PROGRESSIVE_SUNS_SONG, Text{ "Progressive Sun's Song", "Chant du Soleil (prog.)", "Progressives Hymne der Sonne" }, ITEMTYPE_SONG, 0xE4, true, LOGIC_NONE, RHT_SUNS_SONG, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_SONG_OF_TIME] = Item(RG_PROGRESSIVE_SONG_OF_TIME, Text{ "Progressive Song of Time", "Chant du Temps (prog.)", "Progressives Hymne der Zeit" }, ITEMTYPE_SONG, 0xE5, true, LOGIC_NONE, RHT_SONG_OF_TIME, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_SONG_OF_STORMS] = Item(RG_PROGRESSIVE_SONG_OF_STORMS, Text{ "Progressive Song of Storms", "Chant des Tempêtes (prog.)", "Progressives Hymne des Sturms" }, ITEMTYPE_SONG, 0xE6, true, LOGIC_NONE, RHT_SONG_OF_STORMS, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_MINUET_OF_FOREST] = Item(RG_PROGRESSIVE_MINUET_OF_FOREST, Text{ "Progressive Minuet of Forest", "Menuet des Bois (prog.)", "Progressives Menuett des Waldes" }, ITEMTYPE_SONG, 0xE7, true, LOGIC_NONE, RHT_MINUET_OF_FOREST, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_BOLERO_OF_FIRE] = Item(RG_PROGRESSIVE_BOLERO_OF_FIRE, Text{ "Progressive Bolero of Fire", "Boléro du Feu (prog.)", "Progressives Bolero des Feuers" }, ITEMTYPE_SONG, 0xE8, true, LOGIC_NONE, RHT_BOLERO_OF_FIRE, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_SERENADE_OF_WATER] = Item(RG_PROGRESSIVE_SERENADE_OF_WATER, Text{ "Progressive Serenade of Water", "Sérénade de l'Eau (prog.)", "Progressive Serenade des Wassers" }, ITEMTYPE_SONG, 0xE9, true, LOGIC_NONE, RHT_SERENADE_OF_WATER, ITEM_CATEGORY_MAJOR, {"a ", "une ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_REQUIEM_OF_SPIRIT] = Item(RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, Text{ "Progressive Requiem of Spirit", "Requiem des Esprits (prog.)", "Progressives Requiem der Geister" }, ITEMTYPE_SONG, 0xEA, true, LOGIC_NONE, RHT_REQUIEM_OF_SPIRIT, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_NOCTURNE_OF_SHADOW] = Item(RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, Text{ "Progressive Nocturne of Shadow", "Nocturne de l'Ombre (prog.)", "Progressive Nocturne des Schattens" }, ITEMTYPE_SONG, 0xEB, true, LOGIC_NONE, RHT_NOCTURNE_OF_SHADOW, ITEM_CATEGORY_MAJOR, {"a ", "une ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_PRELUDE_OF_LIGHT] = Item(RG_PROGRESSIVE_PRELUDE_OF_LIGHT, Text{ "Progressive Prelude of Light", "Prélude de la Lumière (prog.)", "Progressive Kantate des Lichts" }, ITEMTYPE_SONG, 0xEC, true, LOGIC_NONE, RHT_PRELUDE_OF_LIGHT, ITEM_CATEGORY_MAJOR, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_ZELDAS_LULLABY] = Item(RG_PROGRESSIVE_ZELDAS_LULLABY, Text{ "Progressive Zelda's Lullaby", "Berceuse de Zelda (prog.)", "Progressives Zeldas Wiegenlied" }, ITEMTYPE_SONG, 0xE1, true, LOGIC_NONE, RHT_ZELDAS_LULLABY, RG_PROGRESSIVE_ZELDAS_LULLABY, OBJECT_GI_MELODY, GID_SONG_ZELDA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_EPONAS_SONG] = Item(RG_PROGRESSIVE_EPONAS_SONG, Text{ "Progressive Epona's Song", "Chant d'Epona (prog.)", "Progressives Eponas Lied" }, ITEMTYPE_SONG, 0xE2, true, LOGIC_NONE, RHT_EPONAS_SONG, RG_PROGRESSIVE_EPONAS_SONG, OBJECT_GI_MELODY, GID_SONG_EPONA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SARIAS_SONG] = Item(RG_PROGRESSIVE_SARIAS_SONG, Text{ "Progressive Saria's Song", "Chant de Saria (prog.)", "Progressives Salias Lied" }, ITEMTYPE_SONG, 0xE3, true, LOGIC_NONE, RHT_SARIAS_SONG, RG_PROGRESSIVE_SARIAS_SONG, OBJECT_GI_MELODY, GID_SONG_SARIA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SUNS_SONG] = Item(RG_PROGRESSIVE_SUNS_SONG, Text{ "Progressive Sun's Song", "Chant du Soleil (prog.)", "Progressives Hymne der Sonne" }, ITEMTYPE_SONG, 0xE4, true, LOGIC_NONE, RHT_SUNS_SONG, RG_PROGRESSIVE_SUNS_SONG, OBJECT_GI_MELODY, GID_SONG_SUN, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SONG_OF_TIME] = Item(RG_PROGRESSIVE_SONG_OF_TIME, Text{ "Progressive Song of Time", "Chant du Temps (prog.)", "Progressives Hymne der Zeit" }, ITEMTYPE_SONG, 0xE5, true, LOGIC_NONE, RHT_SONG_OF_TIME, RG_PROGRESSIVE_SONG_OF_TIME, OBJECT_GI_MELODY, GID_SONG_TIME, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SONG_OF_STORMS] = Item(RG_PROGRESSIVE_SONG_OF_STORMS, Text{ "Progressive Song of Storms", "Chant des Tempêtes (prog.)", "Progressives Hymne des Sturms" }, ITEMTYPE_SONG, 0xE6, true, LOGIC_NONE, RHT_SONG_OF_STORMS, RG_PROGRESSIVE_SONG_OF_STORMS, OBJECT_GI_MELODY, GID_SONG_STORM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_MINUET_OF_FOREST] = Item(RG_PROGRESSIVE_MINUET_OF_FOREST, Text{ "Progressive Minuet of Forest", "Menuet des Bois (prog.)", "Progressives Menuett des Waldes" }, ITEMTYPE_SONG, 0xE7, true, LOGIC_NONE, RHT_MINUET_OF_FOREST, RG_PROGRESSIVE_MINUET_OF_FOREST, OBJECT_GI_MELODY, GID_SONG_MINUET, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_BOLERO_OF_FIRE] = Item(RG_PROGRESSIVE_BOLERO_OF_FIRE, Text{ "Progressive Bolero of Fire", "Boléro du Feu (prog.)", "Progressives Bolero des Feuers" }, ITEMTYPE_SONG, 0xE8, true, LOGIC_NONE, RHT_BOLERO_OF_FIRE, RG_PROGRESSIVE_BOLERO_OF_FIRE, OBJECT_GI_MELODY, GID_SONG_BOLERO, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SERENADE_OF_WATER] = Item(RG_PROGRESSIVE_SERENADE_OF_WATER, Text{ "Progressive Serenade of Water", "Sérénade de l'Eau (prog.)", "Progressive Serenade des Wassers" }, ITEMTYPE_SONG, 0xE9, true, LOGIC_NONE, RHT_SERENADE_OF_WATER, RG_PROGRESSIVE_SERENADE_OF_WATER, OBJECT_GI_MELODY, GID_SONG_SERENADE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_REQUIEM_OF_SPIRIT] = Item(RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, Text{ "Progressive Requiem of Spirit", "Requiem des Esprits (prog.)", "Progressives Requiem der Geister" }, ITEMTYPE_SONG, 0xEA, true, LOGIC_NONE, RHT_REQUIEM_OF_SPIRIT, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, OBJECT_GI_MELODY, GID_SONG_REQUIEM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_NOCTURNE_OF_SHADOW] = Item(RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, Text{ "Progressive Nocturne of Shadow", "Nocturne de l'Ombre (prog.)", "Progressive Nocturne des Schattens" }, ITEMTYPE_SONG, 0xEB, true, LOGIC_NONE, RHT_NOCTURNE_OF_SHADOW, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, OBJECT_GI_MELODY, GID_SONG_NOCTURNE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_PRELUDE_OF_LIGHT] = Item(RG_PROGRESSIVE_PRELUDE_OF_LIGHT, Text{ "Progressive Prelude of Light", "Prélude de la Lumière (prog.)", "Progressive Kantate des Lichts" }, ITEMTYPE_SONG, 0xEC, true, LOGIC_NONE, RHT_PRELUDE_OF_LIGHT, RG_PROGRESSIVE_PRELUDE_OF_LIGHT, OBJECT_GI_MELODY, GID_SONG_PRELUDE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); // Bottles itemTable[RG_EMPTY_BOTTLE] = Item(RG_EMPTY_BOTTLE, Text{ "Empty Bottle", "Bouteille Vide", "Leere Flasche" }, ITEMTYPE_ITEM, GI_BOTTLE, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_MILK, ITEM_BOTTLE, OBJECT_GI_BOTTLE, GID_BOTTLE, 0x42, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"an ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_MILK] = Item(RG_BOTTLE_WITH_MILK, Text{ "Bottle with Milk", "Bouteille avec du Lait", "Flasche mit Milch" }, ITEMTYPE_ITEM, GI_MILK_BOTTLE, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_MILK, ITEM_MILK_BOTTLE, OBJECT_GI_MILK, GID_MILK, 0x98, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_RED_POTION] = Item(RG_BOTTLE_WITH_RED_POTION, Text{ "Bottle with Red Potion", "Bouteille avec une Potion Rouge", "Flasche mit rotem Elixier" }, ITEMTYPE_ITEM, 0x8C, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_RED_POTION, RG_BOTTLE_WITH_RED_POTION, OBJECT_GI_LIQUID, GID_POTION_RED, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_GREEN_POTION] = Item(RG_BOTTLE_WITH_GREEN_POTION, Text{ "Bottle with Green Potion", "Bouteille avec une Potion Verte", "Flasche mit grünem Elixier" }, ITEMTYPE_ITEM, 0x8D, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_GREEN_POTION, RG_BOTTLE_WITH_GREEN_POTION, OBJECT_GI_LIQUID, GID_POTION_GREEN, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); - itemTable[RG_BOTTLE_WITH_BLUE_POTION] = Item(RG_BOTTLE_WITH_BLUE_POTION, Text{ "Bottle with Blue Potion", "Bouteille avec une Potion Bleue", "Flasche mit blauem Elixier" }, ITEMTYPE_ITEM, 0x8E, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_BLUE_POTION, RG_BOTTLE_WITH_BLUE_POTION, OBJECT_GI_LIQUID, GID_POTION_BLUE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); + itemTable[RG_BOTTLE_WITH_BLUE_POTION] = Item(RG_BOTTLE_WITH_BLUE_POTION, Text{ "Bottle with Blue Potion", "Bouteille avec une Potion Bleue", "Flasche mit blauem Elixier" }, ITEMTYPE_ITEM, 0x8E, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_BLUE_POTION, RG_BOTTLE_WITH_BLUE_POTION, OBJECT_GI_LIQUID, GID_POTION_BLUE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}).CustomIcon(gItemIconBottlePotionBlueTex); itemTable[RG_BOTTLE_WITH_FAIRY] = Item(RG_BOTTLE_WITH_FAIRY, Text{ "Bottle with Fairy", "Bouteille avec une Fée", "Flasche mit Fee" }, ITEMTYPE_ITEM, 0x8F, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_FAIRY, RG_BOTTLE_WITH_FAIRY, OBJECT_GI_BOTTLE, GID_BOTTLE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_FISH] = Item(RG_BOTTLE_WITH_FISH, Text{ "Bottle with Fish", "Bouteille avec un Poisson", "Flasche mit Fisch" }, ITEMTYPE_ITEM, 0x90, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_FISH, RG_BOTTLE_WITH_FISH, OBJECT_GI_FISH, GID_FISH, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_BLUE_FIRE] = Item(RG_BOTTLE_WITH_BLUE_FIRE, Text{ "Bottle with Blue Fire", "Bouteille avec une Flamme Bleue", "Flasche mit blauem Feuer" }, ITEMTYPE_ITEM, 0x91, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_BLUE_FIRE, RG_BOTTLE_WITH_BLUE_FIRE, OBJECT_GI_FIRE, GID_BLUE_FIRE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); @@ -233,25 +233,25 @@ void Rando::StaticData::InitItemTable() { itemTable[RG_FISHING_HOLE_KEY] = Item(RG_FISHING_HOLE_KEY, Text{ "Fishing Hole Key", "Clé de l'Étang", "Schlüssel für den Fischweiher" }, ITEMTYPE_ITEM, GI_DOOR_KEY, true, LOGIC_FISHING_HOLE_KEY, RHT_OVERWORLD_KEY, RG_FISHING_HOLE_KEY, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "la ", "den "}).CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_FISHING_HOLE_KEY].SetCustomDrawFunc(Randomizer_DrawOverworldKey); // Key Rings - itemTable[RG_FOREST_TEMPLE_KEY_RING] = Item(RG_FOREST_TEMPLE_KEY_RING, Text{ "Forest Temple Key Ring", "Trousseau du Temple de la Forêt", "Schlüsselbund für den Waldtempel" }, ITEMTYPE_SMALLKEY, 0xD5, true, LOGIC_FOREST_TEMPLE_KEYS, RHT_FOREST_TEMPLE_KEY_RING, RG_FOREST_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%g"); + itemTable[RG_FOREST_TEMPLE_KEY_RING] = Item(RG_FOREST_TEMPLE_KEY_RING, Text{ "Forest Temple Key Ring", "Trousseau du Temple de la Forêt", "Schlüsselbund für den Waldtempel" }, ITEMTYPE_SMALLKEY, 0xD5, true, LOGIC_FOREST_TEMPLE_KEYS, RHT_FOREST_TEMPLE_KEY_RING, RG_FOREST_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%g").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_FOREST_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_FIRE_TEMPLE_KEY_RING] = Item(RG_FIRE_TEMPLE_KEY_RING, Text{ "Fire Temple Key Ring", "Trousseau du Temple du Feu", "Schlüsselbund für den Feuertempel" }, ITEMTYPE_SMALLKEY, 0xD6, true, LOGIC_FIRE_TEMPLE_KEYS, RHT_FIRE_TEMPLE_KEY_RING, RG_FIRE_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%r"); + itemTable[RG_FIRE_TEMPLE_KEY_RING] = Item(RG_FIRE_TEMPLE_KEY_RING, Text{ "Fire Temple Key Ring", "Trousseau du Temple du Feu", "Schlüsselbund für den Feuertempel" }, ITEMTYPE_SMALLKEY, 0xD6, true, LOGIC_FIRE_TEMPLE_KEYS, RHT_FIRE_TEMPLE_KEY_RING, RG_FIRE_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%r").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_FIRE_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_WATER_TEMPLE_KEY_RING] = Item(RG_WATER_TEMPLE_KEY_RING, Text{ "Water Temple Key Ring", "Trousseau du Temple de l'Eau", "Schlüsselbund für den Wassertempel" }, ITEMTYPE_SMALLKEY, 0xD7, true, LOGIC_WATER_TEMPLE_KEYS, RHT_WATER_TEMPLE_KEY_RING, RG_WATER_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%b"); + itemTable[RG_WATER_TEMPLE_KEY_RING] = Item(RG_WATER_TEMPLE_KEY_RING, Text{ "Water Temple Key Ring", "Trousseau du Temple de l'Eau", "Schlüsselbund für den Wassertempel" }, ITEMTYPE_SMALLKEY, 0xD7, true, LOGIC_WATER_TEMPLE_KEYS, RHT_WATER_TEMPLE_KEY_RING, RG_WATER_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%b").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_WATER_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_SPIRIT_TEMPLE_KEY_RING] = Item(RG_SPIRIT_TEMPLE_KEY_RING, Text{ "Spirit Temple Key Ring", "Trousseau du Temple de l'Esprit", "Schlüsselbund für den Geistertempel" }, ITEMTYPE_SMALLKEY, 0xD8, true, LOGIC_SPIRIT_TEMPLE_KEYS, RHT_SPIRIT_TEMPLE_KEY_RING, RG_SPIRIT_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y"); + itemTable[RG_SPIRIT_TEMPLE_KEY_RING] = Item(RG_SPIRIT_TEMPLE_KEY_RING, Text{ "Spirit Temple Key Ring", "Trousseau du Temple de l'Esprit", "Schlüsselbund für den Geistertempel" }, ITEMTYPE_SMALLKEY, 0xD8, true, LOGIC_SPIRIT_TEMPLE_KEYS, RHT_SPIRIT_TEMPLE_KEY_RING, RG_SPIRIT_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_SPIRIT_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_SHADOW_TEMPLE_KEY_RING] = Item(RG_SHADOW_TEMPLE_KEY_RING, Text{ "Shadow Temple Key Ring", "Trousseau du Temple de l'Ombre", "Schlüsselbund für den Schattentempel" }, ITEMTYPE_SMALLKEY, 0xD9, true, LOGIC_SHADOW_TEMPLE_KEYS, RHT_SHADOW_TEMPLE_KEY_RING, RG_SHADOW_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%p"); + itemTable[RG_SHADOW_TEMPLE_KEY_RING] = Item(RG_SHADOW_TEMPLE_KEY_RING, Text{ "Shadow Temple Key Ring", "Trousseau du Temple de l'Ombre", "Schlüsselbund für den Schattentempel" }, ITEMTYPE_SMALLKEY, 0xD9, true, LOGIC_SHADOW_TEMPLE_KEYS, RHT_SHADOW_TEMPLE_KEY_RING, RG_SHADOW_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%p").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_SHADOW_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_BOTTOM_OF_THE_WELL_KEY_RING] = Item(RG_BOTTOM_OF_THE_WELL_KEY_RING, Text{ "Bottom of the Well Key Ring", "Trousseau du Puits", "Schlüsselbund für den Grund des Brunnens" }, ITEMTYPE_SMALLKEY, 0xDA, true, LOGIC_BOTTOM_OF_THE_WELL_KEYS, RHT_BOTTOM_OF_THE_WELL_KEY_RING, RG_BOTTOM_OF_THE_WELL_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%p"); + itemTable[RG_BOTTOM_OF_THE_WELL_KEY_RING] = Item(RG_BOTTOM_OF_THE_WELL_KEY_RING, Text{ "Bottom of the Well Key Ring", "Trousseau du Puits", "Schlüsselbund für den Grund des Brunnens" }, ITEMTYPE_SMALLKEY, 0xDA, true, LOGIC_BOTTOM_OF_THE_WELL_KEYS, RHT_BOTTOM_OF_THE_WELL_KEY_RING, RG_BOTTOM_OF_THE_WELL_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%p").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_BOTTOM_OF_THE_WELL_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_GERUDO_TRAINING_GROUND_KEY_RING] = Item(RG_GERUDO_TRAINING_GROUND_KEY_RING, Text{ "Training Ground Key Ring", "Trousseau du Gymnase Gerudo", "Schlüsselbund für das Gerudo-Trainingsgelände" }, ITEMTYPE_SMALLKEY, 0xDB, true, LOGIC_GERUDO_TRAINING_GROUND_KEYS, RHT_GERUDO_TRAINING_GROUND_KEY_RING, RG_GERUDO_TRAINING_GROUND_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y"); + itemTable[RG_GERUDO_TRAINING_GROUND_KEY_RING] = Item(RG_GERUDO_TRAINING_GROUND_KEY_RING, Text{ "Training Ground Key Ring", "Trousseau du Gymnase Gerudo", "Schlüsselbund für das Gerudo-Trainingsgelände" }, ITEMTYPE_SMALLKEY, 0xDB, true, LOGIC_GERUDO_TRAINING_GROUND_KEYS, RHT_GERUDO_TRAINING_GROUND_KEY_RING, RG_GERUDO_TRAINING_GROUND_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_GERUDO_TRAINING_GROUND_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_GERUDO_FORTRESS_KEY_RING] = Item(RG_GERUDO_FORTRESS_KEY_RING, Text{ "Gerudo Fortress Key Ring", "Trousseau du Repaire des Voleurs", "Schlüsselbund für die Gerudo-Festung" }, ITEMTYPE_FORTRESS_SMALLKEY, 0xDC, true, LOGIC_GERUDO_FORTRESS_KEYS, RHT_GERUDO_FORTRESS_KEY_RING, RG_GERUDO_FORTRESS_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y"); + itemTable[RG_GERUDO_FORTRESS_KEY_RING] = Item(RG_GERUDO_FORTRESS_KEY_RING, Text{ "Gerudo Fortress Key Ring", "Trousseau du Repaire des Voleurs", "Schlüsselbund für die Gerudo-Festung" }, ITEMTYPE_FORTRESS_SMALLKEY, 0xDC, true, LOGIC_GERUDO_FORTRESS_KEYS, RHT_GERUDO_FORTRESS_KEY_RING, RG_GERUDO_FORTRESS_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_GERUDO_FORTRESS_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_GANONS_CASTLE_KEY_RING] = Item(RG_GANONS_CASTLE_KEY_RING, Text{ "Ganon's Castle Key Ring", "Trousseau du Château de Ganon", "Schlüsselbund für Ganons Schloß" }, ITEMTYPE_SMALLKEY, 0xDD, true, LOGIC_GANONS_CASTLE_KEYS, RHT_GANONS_CASTLE_KEY_RING, RG_GANONS_CASTLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%r"); + itemTable[RG_GANONS_CASTLE_KEY_RING] = Item(RG_GANONS_CASTLE_KEY_RING, Text{ "Ganon's Castle Key Ring", "Trousseau du Château de Ganon", "Schlüsselbund für Ganons Schloß" }, ITEMTYPE_SMALLKEY, 0xDD, true, LOGIC_GANONS_CASTLE_KEYS, RHT_GANONS_CASTLE_KEY_RING, RG_GANONS_CASTLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%r").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_GANONS_CASTLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_TREASURE_GAME_KEY_RING] = Item(RG_TREASURE_GAME_KEY_RING, Text{ "Chest Game Key Ring", "Trousseau du jeu la Chasse-aux-Trésors", "Schlüsselbund für das Truhenspiel" }, ITEMTYPE_SMALLKEY, 0xDE, true, LOGIC_TREASURE_GAME_KEYS, RHT_TREASURE_GAME_KEY_RING, RG_TREASURE_GAME_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}); + itemTable[RG_TREASURE_GAME_KEY_RING] = Item(RG_TREASURE_GAME_KEY_RING, Text{ "Chest Game Key Ring", "Trousseau du jeu la Chasse-aux-Trésors", "Schlüsselbund für das Truhenspiel" }, ITEMTYPE_SMALLKEY, 0xDE, true, LOGIC_TREASURE_GAME_KEYS, RHT_TREASURE_GAME_KEY_RING, RG_TREASURE_GAME_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}).CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_TREASURE_GAME_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); // Dungeon Rewards itemTable[RG_KOKIRI_EMERALD] = Item(RG_KOKIRI_EMERALD, Text{ "Kokiri's Emerald", "Émeraude Kokiri", "Kokiri-Smaragd" }, ITEMTYPE_DUNGEONREWARD, 0xCB, true, LOGIC_KOKIRI_EMERALD, RHT_KOKIRI_EMERALD, ITEM_KOKIRI_EMERALD, OBJECT_GI_JEWEL, GID_KOKIRI_EMERALD, 0x80, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"the ", "l'", "den "}, "%g"); diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index d7c7e13b234..8085ec37e88 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -1139,11 +1139,7 @@ extern "C" u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry) { Randomizer_GameplayStats_SetTimestamp(item); if (Rando::SplitSongs::IsProgressiveSong(item)) { - const RandomizerGet displayItem = Rando::SplitSongs::ResolveProgressiveSongStage(item); Rando::SplitSongs::OnProgressiveSongReceived(item); - if (displayItem != RG_NONE) { - return Return_Item_Entry(Rando::StaticData::RetrieveItem(displayItem).GetGIEntry_Copy(), RG_NONE); - } return Return_Item_Entry(giEntry, RG_NONE); } From 299803471a19e88de2b67acf25e72c2d4c1f3599 Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:24:07 +0100 Subject: [PATCH 16/22] Simplify split songs to RandInf + progressive-only pattern. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert z_message_PAL overhaul and unrelated icon/debug changes; keep minimal song pickup icons via vanilla ITEM_SONG_* in ItemMessages. Drop deferred grant queue — second pickup resolves to full song through GetGIEntry. --- soh/soh/Enhancements/debugconsole.cpp | 21 -- .../randomizer/3drando/item_pool.cpp | 21 +- .../randomizer/Messages/ItemMessages.cpp | 346 ++++-------------- soh/soh/Enhancements/randomizer/Traps.cpp | 1 - .../Enhancements/randomizer/hook_handlers.cpp | 4 - soh/soh/Enhancements/randomizer/item_list.cpp | 22 +- .../Enhancements/randomizer/split_songs.cpp | 123 +------ soh/soh/Enhancements/randomizer/split_songs.h | 19 - soh/soh/SohGui/SohMenuDevTools.cpp | 6 - soh/src/code/z_message_PAL.c | 243 +++--------- 10 files changed, 148 insertions(+), 658 deletions(-) diff --git a/soh/soh/Enhancements/debugconsole.cpp b/soh/soh/Enhancements/debugconsole.cpp index 9b60615d13d..1021543df22 100644 --- a/soh/soh/Enhancements/debugconsole.cpp +++ b/soh/soh/Enhancements/debugconsole.cpp @@ -13,7 +13,6 @@ #include "soh/Enhancements/audio/AudioEditor.h" #include "soh/Enhancements/randomizer/logic.h" #include "soh/Enhancements/randomizer/randomizer.h" -#include "soh/Enhancements/randomizer/split_songs.h" #define Path _Path #define PATH_HACK @@ -405,20 +404,6 @@ static bool GiveItemHandler(std::shared_ptr Console, const std::v return 0; } -static bool RandoGiveAllSplitSongPartsHandler(std::shared_ptr Console, - const std::vector args, std::string* output) { - if (gPlayState == nullptr) { - ERROR_MESSAGE("No active game; load into gameplay first."); - return 1; - } - if (!IS_RANDO) { - ERROR_MESSAGE("Only works in Randomizer (load a rando save)."); - return 1; - } - Rando::SplitSongs::DebugGiveAllSongParts(gPlayState); - return 0; -} - static bool EntranceHandler(std::shared_ptr Console, const std::vector& args, std::string* output) { if (args.size() < 2) { @@ -1621,12 +1606,6 @@ void DebugConsole_Init(void) { { "giveItemID", Ship::ArgumentType::NUMBER }, } }); - CMD_REGISTER("rando_give_all_split_song_parts", - { RandoGiveAllSplitSongPartsHandler, - "Randomizer: grants all 12 ocarina songs via both split parts (tests Randomizer_Item_Give + " - "completion). Rando save only.", - {} }); - CMD_REGISTER("item", { ItemHandler, "Sets item ID in arg 1 into slot arg 2. No boundary checks. Use with caution.", { diff --git a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp index 4cf6da2e876..71600cc1110 100644 --- a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp @@ -8,7 +8,6 @@ #include "random.hpp" #include "spoiler_log.hpp" #include "soh/Enhancements/randomizer/Traps.h" -#include "soh/Enhancements/randomizer/split_songs.h" #include "z64item.h" #include @@ -159,11 +158,21 @@ void GenerateItemPool() { if (ctx->GetOption(RSK_SHUFFLE_SONGS).Is(RO_SONG_SHUFFLE_ANYWHERE)) { const bool split = static_cast(ctx->GetOption(RSK_SPLIT_OCARINA_SONGS)); - std::vector songTrapModels; - Rando::SplitSongs::AppendSongIceTrapModels(songTrapModels, split); - for (RandomizerGet rg : songTrapModels) { - ctx->possibleIceTrapModels.insert(rg); - } + auto addSongIceTrapModel = [&](RandomizerGet fullSong, RandomizerGet progressive) { + ctx->possibleIceTrapModels.insert(split ? progressive : fullSong); + }; + addSongIceTrapModel(RG_ZELDAS_LULLABY, RG_PROGRESSIVE_ZELDAS_LULLABY); + addSongIceTrapModel(RG_EPONAS_SONG, RG_PROGRESSIVE_EPONAS_SONG); + addSongIceTrapModel(RG_SARIAS_SONG, RG_PROGRESSIVE_SARIAS_SONG); + addSongIceTrapModel(RG_SUNS_SONG, RG_PROGRESSIVE_SUNS_SONG); + addSongIceTrapModel(RG_SONG_OF_TIME, RG_PROGRESSIVE_SONG_OF_TIME); + addSongIceTrapModel(RG_SONG_OF_STORMS, RG_PROGRESSIVE_SONG_OF_STORMS); + addSongIceTrapModel(RG_MINUET_OF_FOREST, RG_PROGRESSIVE_MINUET_OF_FOREST); + addSongIceTrapModel(RG_BOLERO_OF_FIRE, RG_PROGRESSIVE_BOLERO_OF_FIRE); + addSongIceTrapModel(RG_SERENADE_OF_WATER, RG_PROGRESSIVE_SERENADE_OF_WATER); + addSongIceTrapModel(RG_REQUIEM_OF_SPIRIT, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT); + addSongIceTrapModel(RG_NOCTURNE_OF_SHADOW, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW); + addSongIceTrapModel(RG_PRELUDE_OF_LIGHT, RG_PROGRESSIVE_PRELUDE_OF_LIGHT); } // clang-format off diff --git a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp index ca188712f36..5283ecacfd6 100644 --- a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp @@ -4,141 +4,28 @@ * Vanilla/MQ hints when collecting Maps, Ice Trap messages, * etc. */ -#include #include #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" -#include "soh/Enhancements/randomizer/split_songs.h" -#include "soh/Enhancements/randomizer/static_data.h" #include "soh/Enhancements/custom-message/CustomMessageTypes.h" #include "soh/Enhancements/randomizer/Traps.h" #include "soh/Enhancements/randomizer/item.h" #include "soh/Enhancements/randomizer/randomizer.h" +#include "soh/Enhancements/randomizer/split_songs.h" +#include "soh/Enhancements/randomizer/static_data.h" #include "soh/ShipInit.hpp" #include -#include #include -#include extern "C" { #include #include #include "z64item.h" -#include "z64player.h" -#include "z64save.h" extern PlayState* gPlayState; -void Message_LoadItemIcon(PlayState* play, u16 itemId, s16 y); -GetItemEntry ItemTable_Retrieve(int16_t getItemID); -GetItemEntry ItemTable_RetrieveEntry(s16 modIndex, s16 getItemID); -} - -/** - * Match `Player`'s get-item display (`z_player` func_8084DFF4): if `getItemId` does not match `getItemEntry`, - * the table row keyed by `getItemId` wins. `Message_OpenText` runs before the entry is synced onto the player, - * so reading only `player->getItemEntry` here can embed the wrong textbox icon versus the real gift. - */ -static GetItemEntry ResolveGiftGetItemEntry(const Player* player) { - if (player == nullptr) { - return GET_ITEM_NONE; - } - if (player->getItemEntry.objectId == OBJECT_INVALID || player->getItemId != player->getItemEntry.getItemId) { - if (IS_RANDO && player->getItemId > RG_NONE && player->getItemId < RG_MAX) { - return ItemTable_RetrieveEntry(MOD_RANDOMIZER, player->getItemId); - } - return ItemTable_Retrieve(player->getItemId); - } - return player->getItemEntry; -} - -static bool LoadCustomItemIcon(bool displayAsEnglish); -static bool RefreshCustomItemIconForTextbox(bool displayAsEnglish, bool incrementDecodeState); -static void ApplyCustomIconPathToTextbox(const char* customIcon, CustomIconSize iconSize, bool displayAsEnglish, - bool incrementDecodeState); -static void DrawTextboxItemIconFromSegment(Gfx** p); -static RandomizerGet RandomizerGetFromPlayerItemEntry(const Player* player); - -/** Cache the custom icon chosen during decode; draw can reuse if player context drifts before render. */ -static const char* g_LastDecodedCustomIconPath = nullptr; -static CustomIconSize g_LastDecodedCustomIconSize = ICON_SIZE_32; - -/** Called from z_message_PAL MESSAGE_ITEM_ICON decode — ITEM_CUSTOM uses OTR path then vanilla fallback. */ -extern "C" void Randomizer_Message_DecodeLoadItemIcon(PlayState* play, u16 iconToLoad, s32 displayAsEnglishAsInt) { - if (play == nullptr || iconToLoad < ITEM_CUSTOM) { - return; - } - const bool displayAsEnglish = displayAsEnglishAsInt != 0; - if (gSaveContext.ship.quest.id == QUEST_RANDOMIZER) { - if (LoadCustomItemIcon(displayAsEnglish)) { - return; - } - } - Message_LoadItemIcon(play, iconToLoad, (s16)(R_TEXTBOX_Y + 10)); -} - -extern "C" void Randomizer_Message_RefreshCustomItemIconAtDraw(s32 displayAsEnglishAsInt) { - const bool displayAsEnglish = displayAsEnglishAsInt != 0; - (void)RefreshCustomItemIconForTextbox(displayAsEnglish, false); -} - -extern "C" void Randomizer_Message_AppendSegmentIconGfx(Gfx** gfxp) { - if (gfxp != nullptr) { - DrawTextboxItemIconFromSegment(gfxp); - } -} - -static ItemID VanillaItemIdForFullSong(RandomizerGet fullSongRg) { - switch (fullSongRg) { - case RG_ZELDAS_LULLABY: - return ITEM_SONG_LULLABY; - case RG_EPONAS_SONG: - return ITEM_SONG_EPONA; - case RG_SARIAS_SONG: - return ITEM_SONG_SARIA; - case RG_SUNS_SONG: - return ITEM_SONG_SUN; - case RG_SONG_OF_TIME: - return ITEM_SONG_TIME; - case RG_SONG_OF_STORMS: - return ITEM_SONG_STORMS; - case RG_MINUET_OF_FOREST: - return ITEM_SONG_MINUET; - case RG_BOLERO_OF_FIRE: - return ITEM_SONG_BOLERO; - case RG_SERENADE_OF_WATER: - return ITEM_SONG_SERENADE; - case RG_REQUIEM_OF_SPIRIT: - return ITEM_SONG_REQUIEM; - case RG_NOCTURNE_OF_SHADOW: - return ITEM_SONG_NOCTURNE; - case RG_PRELUDE_OF_LIGHT: - return ITEM_SONG_PRELUDE; - default: - return ITEM_NONE; - } -} - -/** Same rules as `BuildCustomItemMessage` for resolving `RandomizerGet` from the current get-item context. */ -static RandomizerGet RandomizerGetFromPlayerItemEntry(const Player* player) { - if (player == nullptr) { - return RG_NONE; - } - const GetItemEntry gift = ResolveGiftGetItemEntry(player); - int16_t rgid; - if (gift.modIndex == MOD_RANDOMIZER) { - rgid = gift.getItemId; - } else if (gift.objectId != OBJECT_INVALID) { - rgid = gift.getItemId; - } else { - rgid = player->getItemId; - } - if (rgid < 0) { - rgid = static_cast(-rgid); - } - return static_cast(rgid); } -static ItemID DirectSongIconForRandomizerGet(RandomizerGet rg) { +static ItemID SongIconForRandomizerGet(RandomizerGet rg) { switch (rg) { case RG_ZELDAS_LULLABY: case RG_PROGRESSIVE_ZELDAS_LULLABY: @@ -181,69 +68,14 @@ static ItemID DirectSongIconForRandomizerGet(RandomizerGet rg) { } } -static ItemID ResolveSongMessageIcon(RandomizerGet rg, RandomizerGet resolvedStage = RG_NONE) { - ItemID icon = DirectSongIconForRandomizerGet(rg); - if (icon != ITEM_NONE) { - return icon; +static RandomizerGet ResolveRandoGetFromPlayer(const Player* player) { + if (player->getItemEntry.objectId != OBJECT_INVALID && player->getItemEntry.modIndex == MOD_RANDOMIZER) { + return static_cast(player->getItemEntry.getItemId); } - if (resolvedStage != RG_NONE) { - icon = DirectSongIconForRandomizerGet(resolvedStage); - if (icon != ITEM_NONE) { - return icon; - } - const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromFullSong(resolvedStage); - if (def != nullptr) { - icon = VanillaItemIdForFullSong(def->fullSong); - if (icon != ITEM_NONE) { - return icon; - } - } - } - if (const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromProgressive(rg)) { - return VanillaItemIdForFullSong(def->fullSong); - } - return ITEM_SONG_LULLABY; -} - -static bool RandomizerGet_IsSongTextboxPickup(RandomizerGet rg) { - return Rando::SplitSongs::IsProgressiveSong(rg) || DirectSongIconForRandomizerGet(rg) != ITEM_NONE; -} - -extern "C" u16 Message_GetRandoTextboxItemIconOverride(PlayState* play, u16 itemId) { - (void)play; - return itemId; -} - -/** - * Text id 0xF8 (TEXT_RANDOMIZER_CUSTOM_ITEM) can overwrite an earlier decoded icon byte; some vanilla - * song rows also use numeric text ids that collide with ITEM_* when mis-encoded. Re-resolve from the - * player's get-item row so dungeon songs (e.g. Nocturne) always use the correct ITEM_SONG_* + tint. - */ -extern "C" u16 Randomizer_ResolveSongIconAtDrawTime(PlayState* play, u16 itemId) { - if (!IS_RANDO || play == nullptr) { - return itemId; - } - Player* player = GET_PLAYER(play); - if (player == nullptr) { - return itemId; - } - const GetItemEntry gift = ResolveGiftGetItemEntry(player); - if (gift.modIndex != MOD_RANDOMIZER || gift.getItemId <= RG_NONE || gift.getItemId >= RG_MAX) { - return itemId; - } - const RandomizerGet rg = static_cast(gift.getItemId); - if (!RandomizerGet_IsSongTextboxPickup(rg)) { - return itemId; + if (IS_RANDO && player->getItemId > RG_NONE && player->getItemId < RG_MAX) { + return static_cast(player->getItemId); } - RandomizerGet stage = RG_NONE; - if (Rando::SplitSongs::IsProgressiveSong(rg)) { - stage = Rando::SplitSongs::ResolveProgressiveSongStage(rg); - } - const ItemID resolved = ResolveSongMessageIcon(rg, stage); - if (resolved == ITEM_NONE) { - return itemId; - } - return static_cast(resolved); + return static_cast(player->getItemId); } void BuildTriforcePieceMessage(CustomMessage& msg) { @@ -292,121 +124,81 @@ void BuildTriforcePieceMessage(CustomMessage& msg) { } void BuildCustomItemMessage(Player* player, CustomMessage& msg) { - int16_t rgid; - msg = CustomMessage("You found [[article]][[color]][[name]]%w!", - "Du erhältst [[article]][[color]][[name]]%w gefunden!", - "Vous avez trouvé [[article]][[color]][[name]]%w!", TEXTBOX_TYPE_BLUE); - const GetItemEntry gift = ResolveGiftGetItemEntry(player); - if (gift.modIndex == MOD_RANDOMIZER) { - rgid = gift.getItemId; - } else if (gift.objectId != OBJECT_INVALID) { - rgid = gift.getItemId; - } else { - rgid = player->getItemId; - } - if (gift.modIndex == MOD_RANDOMIZER && rgid < 0) { - rgid = (s16)-rgid; - } - const RandomizerGet rgEnum = static_cast(rgid); - // Song icons: ResolveSongMessageIcon maps RG_* → vanilla ITEM_SONG_*; Format() writes MESSAGE_ITEM_ICON bytes only. - // Decode loads icon strictly from that buffer (same contract as non-song items using GetGIEntry()->itemId). + const RandomizerGet rgEnum = ResolveRandoGetFromPlayer(player); + if (IS_RANDO && Rando::SplitSongs::IsProgressiveSong(rgEnum)) { const RandomizerGet stage = Rando::SplitSongs::ResolveProgressiveSongStage(rgEnum); - const ItemID iconFallback = ResolveSongMessageIcon(rgEnum, stage); + const ItemID songIcon = SongIconForRandomizerGet(stage != RG_NONE ? stage : rgEnum); const auto& nm = Rando::StaticData::RetrieveItem(stage != RG_NONE ? stage : rgEnum).GetName(); CustomMessage getItemText(nm.GetEnglish(), nm.GetGerman(), nm.GetFrench(), TEXTBOX_TYPE_BLUE, TEXTBOX_POS_BOTTOM); - getItemText.Format(iconFallback); + getItemText.Format(songIcon); msg = getItemText; return; } - auto& itemForMsg = Rando::StaticData::RetrieveItem(rgEnum); - CustomMessage name = CustomMessage(itemForMsg.GetName(), TEXTBOX_TYPE_BLUE); - CustomMessage article = CustomMessage(itemForMsg.GetArticle(), TEXTBOX_TYPE_BLUE); + + int16_t rgid; + msg = CustomMessage("You found [[article]][[color]][[name]]%w!", + "Du erhältst [[article]][[color]][[name]]%w gefunden!", + "Vous avez trouvé [[article]][[color]][[name]]%w!", TEXTBOX_TYPE_BLUE); + if (player->getItemEntry.objectId != OBJECT_INVALID) { + rgid = player->getItemEntry.getItemId; + } else { + rgid = player->getItemId; + } + CustomMessage name = + CustomMessage(Rando::StaticData::RetrieveItem(static_cast(rgid)).GetName(), TEXTBOX_TYPE_BLUE); + CustomMessage article = CustomMessage( + Rando::StaticData::RetrieveItem(static_cast(rgid)).GetArticle(), TEXTBOX_TYPE_BLUE); msg.Replace("[[article]]", article); - msg.Replace("[[color]]", itemForMsg.GetColor()); + msg.Replace("[[color]]", Rando::StaticData::RetrieveItem(static_cast(rgid)).GetColor()); msg.Replace("[[name]]", name); - if (itemForMsg.HasCustomIcon()) { + if (Rando::StaticData::RetrieveItem(static_cast(rgid)).HasCustomIcon()) { msg.AutoFormat(ITEM_CUSTOM); - } else if (IS_RANDO && itemForMsg.GetItemType() == ITEMTYPE_SONG) { - // Always embed ITEM_SONG_* for songs. GetGIEntry()->itemId can disagree with quest text ids (e.g. Nocturne - // uses vanilla text 0x77) or with decoded-buffer overwrites for TEXT_RANDOMIZER_CUSTOM_ITEM (0xF8). - msg.AutoFormat(ResolveSongMessageIcon(rgEnum, RG_NONE)); } else { - // Vanilla pause-menu item id: embed a real icon byte so load/draw use gItemIcons (not stale segment data). - const uint16_t pauseIcon = itemForMsg.GetGIEntry()->itemId; - if (pauseIcon < ITEM_CUSTOM && pauseIcon != ITEM_NONE) { - msg.AutoFormat(static_cast(pauseIcon)); - } else { - msg.AutoFormat(); - } + msg.AutoFormat(); } } -/** @return true if an OTR icon path was copied into the textbox segment (skip vanilla ITEM_CUSTOM load). */ -static bool LoadCustomItemIcon(bool displayAsEnglish) { - return RefreshCustomItemIconForTextbox(displayAsEnglish, true); -} - -static bool RefreshCustomItemIconForTextbox(bool displayAsEnglish, bool incrementDecodeState) { +void LoadCustomItemIcon(bool displayAsEnglish) { Player* player = GET_PLAYER(gPlayState); const char* customIcon = nullptr; CustomIconSize iconSize = ICON_SIZE_32; - RandomizerGet rgid = RG_NONE; - const GetItemEntry gift = ResolveGiftGetItemEntry(player); - if (gift.modIndex == MOD_RANDOMIZER) { - rgid = RandomizerGetFromPlayerItemEntry(player); - if (rgid != RG_NONE) { - customIcon = Rando::StaticData::RetrieveItem(rgid).GetCustomIcon(); - iconSize = Rando::StaticData::RetrieveItem(rgid).GetCustomIconSize(); - } - } else if (gift.objectId != OBJECT_INVALID) { - rgid = static_cast(gift.getItemId); + if (player->getItemEntry.objectId != OBJECT_INVALID) { + RandomizerGet rgid = static_cast(player->getItemEntry.getItemId); customIcon = Rando::StaticData::RetrieveItem(rgid).GetCustomIcon(); iconSize = Rando::StaticData::RetrieveItem(rgid).GetCustomIconSize(); } if (customIcon != nullptr) { - g_LastDecodedCustomIconPath = customIcon; - g_LastDecodedCustomIconSize = iconSize; - ApplyCustomIconPathToTextbox(customIcon, iconSize, displayAsEnglish, incrementDecodeState); - return true; - } - if (!incrementDecodeState && g_LastDecodedCustomIconPath != nullptr) { - ApplyCustomIconPathToTextbox(g_LastDecodedCustomIconPath, g_LastDecodedCustomIconSize, displayAsEnglish, false); - return true; - } - if (incrementDecodeState) { - g_LastDecodedCustomIconPath = nullptr; - } - return false; -} - -static void ApplyCustomIconPathToTextbox(const char* customIcon, CustomIconSize iconSize, bool displayAsEnglish, - bool incrementDecodeState) { - static int16_t sIconItem32XOffsets[] = { 74, 74, 74, 54 }; - static int16_t sIconItem24XOffsets[] = { 72, 72, 72, 50 }; - MessageContext* msgCtx = &gPlayState->msgCtx; - uint8_t language = displayAsEnglish ? LANGUAGE_ENG : gSaveContext.language; - if (iconSize == ICON_SIZE_32) { - R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem32XOffsets[language]; - R_TEXTBOX_ICON_YPOS = (R_TEXTBOX_Y + 10) + 6; - R_TEXTBOX_ICON_SIZE = 32; - } else { - R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem24XOffsets[language]; - R_TEXTBOX_ICON_YPOS = (R_TEXTBOX_Y + 10) + 10; - R_TEXTBOX_ICON_SIZE = 24; - } - strcpy((char*)((uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE), customIcon); - if (incrementDecodeState) { + static int16_t sIconItem32XOffsets[] = { 74, 74, 74, 54 }; + static int16_t sIconItem24XOffsets[] = { 72, 72, 72, 50 }; + MessageContext* msgCtx = &gPlayState->msgCtx; + uint8_t language = displayAsEnglish ? LANGUAGE_ENG : gSaveContext.language; + if (iconSize == ICON_SIZE_32) { + R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem32XOffsets[language]; + R_TEXTBOX_ICON_YPOS = (R_TEXTBOX_Y + 10) + 6; + R_TEXTBOX_ICON_SIZE = 32; + } else { + R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem24XOffsets[language]; + R_TEXTBOX_ICON_YPOS = (R_TEXTBOX_Y + 10) + 10; + R_TEXTBOX_ICON_SIZE = 24; + } + strcpy((char*)((uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE), customIcon); msgCtx->msgBufPos++; msgCtx->choiceNum = 1; } } -static void DrawTextboxItemIconFromSegment(Gfx** p) { +void DrawCustomItemIcon(Gfx** p) { Gfx* gfx = *p; MessageContext* msgCtx = &gPlayState->msgCtx; - if (R_TEXTBOX_ICON_SIZE == 24) { + Player* player = GET_PLAYER(gPlayState); + CustomIconSize iconSize = ICON_SIZE_32; + if (player->getItemEntry.objectId != OBJECT_INVALID) { + RandomizerGet rgid = static_cast(player->getItemEntry.getItemId); + iconSize = Rando::StaticData::RetrieveItem(rgid).GetCustomIconSize(); + } + if (iconSize == ICON_SIZE_24) { gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, G_IM_SIZ_32b, 24, 24, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); @@ -421,11 +213,10 @@ static void DrawTextboxItemIconFromSegment(Gfx** p) { void BuildItemMessage(u16* textId, bool* loadFromMessageTable) { Player* player = GET_PLAYER(gPlayState); CustomMessage msg; - const GetItemEntry giftEntry = ResolveGiftGetItemEntry(player); - if (giftEntry.modIndex == MOD_RANDOMIZER && giftEntry.getItemId == RG_ICE_TRAP) { - Rando::Traps::BuildIceTrapMessage(msg, giftEntry); - } else if (giftEntry.modIndex == MOD_RANDOMIZER && giftEntry.getItemId == RG_TRIFORCE_PIECE) { + if (player->getItemEntry.getItemId == RG_ICE_TRAP) { + Rando::Traps::BuildIceTrapMessage(msg, player->getItemEntry); + } else if (player->getItemEntry.getItemId == RG_TRIFORCE_PIECE) { BuildTriforcePieceMessage(msg); } else { BuildCustomItemMessage(player, msg); @@ -523,10 +314,6 @@ void BuildSmallKeyMessage(uint16_t* textId, bool* loadFromMessageTable) { } void RegisterItemMessages() { - if (CVarGetInteger(CVAR_DEVELOPER_TOOLS("LogTextboxItemIcons"), 0) != 0) { - SPDLOG_INFO("[TextboxItemIcon] Logging is ON (menu: Dev Tools → General → Log textbox item icons). Watch for " - "decode + DRAW lines when the textbox parses an icon control code."); - } COND_ID_HOOK(OnOpenText, TEXT_RANDOMIZER_CUSTOM_ITEM, IS_RANDO, BuildItemMessage); COND_ID_HOOK(OnOpenText, TEXT_ITEM_DUNGEON_MAP, DUNGEON_ITEMS_CAN_BE_OUTSIDE_DUNGEON(RSK_SHUFFLE_MAPANDCOMPASS), BuildMapMessage); @@ -542,4 +329,19 @@ void RegisterItemMessages() { BuildSmallKeyMessage); } -static RegisterShipInitFunc initFunc(RegisterItemMessages, { "IS_RANDO" }); \ No newline at end of file +static RegisterShipInitFunc initFunc(RegisterItemMessages, { "IS_RANDO" }); + +void RegisterCustomIconHooks() { + COND_VB_SHOULD(VB_LOAD_ITEM_ICON, IS_RANDO, { + if (*should == false) { + LoadCustomItemIcon(static_cast(va_arg(args, int))); + } + }); + COND_VB_SHOULD(VB_DRAW_ITEM_ICON, IS_RANDO, { + if (*should == false) { + DrawCustomItemIcon(va_arg(args, Gfx**)); + } + }); +} + +static RegisterShipInitFunc customIconInitFunc(RegisterCustomIconHooks, { "IS_RANDO" }); diff --git a/soh/soh/Enhancements/randomizer/Traps.cpp b/soh/soh/Enhancements/randomizer/Traps.cpp index 5a6f952c3b2..474380d3521 100644 --- a/soh/soh/Enhancements/randomizer/Traps.cpp +++ b/soh/soh/Enhancements/randomizer/Traps.cpp @@ -3,7 +3,6 @@ #include "soh/Enhancements/randomizer/randomizerTypes.h" #include "soh/Enhancements/randomizer/static_data.h" #include "soh/Enhancements/randomizer/split_songs.h" -#include "soh/Enhancements/randomizer/3drando/random.hpp" #include "soh/ShipUtils.h" #include diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp index 673ed548ecc..9822f419774 100644 --- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp +++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp @@ -4,7 +4,6 @@ #include "soh/Enhancements/enhancementTypes.h" #include "soh/Enhancements/custom-message/CustomMessageTypes.h" #include "soh/Enhancements/randomizer/randomizerTypes.h" -#include "soh/Enhancements/randomizer/split_songs.h" #include "soh/Enhancements/randomizer/dungeon.h" #include "soh/Enhancements/randomizer/static_data.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" @@ -2639,8 +2638,6 @@ std::unordered_map swimSpecialRespawnInfo = { f32 triforcePieceScale; void RandomizerOnPlayerUpdateHandler() { - Rando::SplitSongs::ProcessPendingFullSongGrants(); - if ((GET_PLAYER(gPlayState)->stateFlags1 & PLAYER_STATE1_IN_WATER) && !Flags_GetRandomizerInf(RAND_INF_CAN_SWIM) && CUR_EQUIP_VALUE(EQUIP_TYPE_BOOTS) != EQUIP_VALUE_BOOTS_IRON) { // if you void out in water temple without swim you get instantly kicked out to prevent softlocks @@ -2785,7 +2782,6 @@ static void RandomizerRegisterHooks() { randomizerQueuedChecks = std::queue(); randomizerQueuedCheck = RC_UNKNOWN_CHECK; randomizerQueuedItemEntry = GET_ITEM_NONE; - Rando::SplitSongs::ClearPendingFullSongGrants(); GameInteractor::Instance->UnregisterGameHook(onFlagSetHook); GameInteractor::Instance->UnregisterGameHook(onSceneFlagSetHook); diff --git a/soh/soh/Enhancements/randomizer/item_list.cpp b/soh/soh/Enhancements/randomizer/item_list.cpp index 79c6df5095f..5bd1513f963 100644 --- a/soh/soh/Enhancements/randomizer/item_list.cpp +++ b/soh/soh/Enhancements/randomizer/item_list.cpp @@ -90,7 +90,7 @@ void Rando::StaticData::InitItemTable() { itemTable[RG_BOTTLE_WITH_MILK] = Item(RG_BOTTLE_WITH_MILK, Text{ "Bottle with Milk", "Bouteille avec du Lait", "Flasche mit Milch" }, ITEMTYPE_ITEM, GI_MILK_BOTTLE, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_MILK, ITEM_MILK_BOTTLE, OBJECT_GI_MILK, GID_MILK, 0x98, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_RED_POTION] = Item(RG_BOTTLE_WITH_RED_POTION, Text{ "Bottle with Red Potion", "Bouteille avec une Potion Rouge", "Flasche mit rotem Elixier" }, ITEMTYPE_ITEM, 0x8C, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_RED_POTION, RG_BOTTLE_WITH_RED_POTION, OBJECT_GI_LIQUID, GID_POTION_RED, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_GREEN_POTION] = Item(RG_BOTTLE_WITH_GREEN_POTION, Text{ "Bottle with Green Potion", "Bouteille avec une Potion Verte", "Flasche mit grünem Elixier" }, ITEMTYPE_ITEM, 0x8D, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_GREEN_POTION, RG_BOTTLE_WITH_GREEN_POTION, OBJECT_GI_LIQUID, GID_POTION_GREEN, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); - itemTable[RG_BOTTLE_WITH_BLUE_POTION] = Item(RG_BOTTLE_WITH_BLUE_POTION, Text{ "Bottle with Blue Potion", "Bouteille avec une Potion Bleue", "Flasche mit blauem Elixier" }, ITEMTYPE_ITEM, 0x8E, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_BLUE_POTION, RG_BOTTLE_WITH_BLUE_POTION, OBJECT_GI_LIQUID, GID_POTION_BLUE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}).CustomIcon(gItemIconBottlePotionBlueTex); + itemTable[RG_BOTTLE_WITH_BLUE_POTION] = Item(RG_BOTTLE_WITH_BLUE_POTION, Text{ "Bottle with Blue Potion", "Bouteille avec une Potion Bleue", "Flasche mit blauem Elixier" }, ITEMTYPE_ITEM, 0x8E, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_BLUE_POTION, RG_BOTTLE_WITH_BLUE_POTION, OBJECT_GI_LIQUID, GID_POTION_BLUE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_FAIRY] = Item(RG_BOTTLE_WITH_FAIRY, Text{ "Bottle with Fairy", "Bouteille avec une Fée", "Flasche mit Fee" }, ITEMTYPE_ITEM, 0x8F, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_FAIRY, RG_BOTTLE_WITH_FAIRY, OBJECT_GI_BOTTLE, GID_BOTTLE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_FISH] = Item(RG_BOTTLE_WITH_FISH, Text{ "Bottle with Fish", "Bouteille avec un Poisson", "Flasche mit Fisch" }, ITEMTYPE_ITEM, 0x90, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_FISH, RG_BOTTLE_WITH_FISH, OBJECT_GI_FISH, GID_FISH, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_BLUE_FIRE] = Item(RG_BOTTLE_WITH_BLUE_FIRE, Text{ "Bottle with Blue Fire", "Bouteille avec une Flamme Bleue", "Flasche mit blauem Feuer" }, ITEMTYPE_ITEM, 0x91, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_BLUE_FIRE, RG_BOTTLE_WITH_BLUE_FIRE, OBJECT_GI_FIRE, GID_BLUE_FIRE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "eine "}); @@ -233,25 +233,25 @@ void Rando::StaticData::InitItemTable() { itemTable[RG_FISHING_HOLE_KEY] = Item(RG_FISHING_HOLE_KEY, Text{ "Fishing Hole Key", "Clé de l'Étang", "Schlüssel für den Fischweiher" }, ITEMTYPE_ITEM, GI_DOOR_KEY, true, LOGIC_FISHING_HOLE_KEY, RHT_OVERWORLD_KEY, RG_FISHING_HOLE_KEY, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "la ", "den "}).CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); itemTable[RG_FISHING_HOLE_KEY].SetCustomDrawFunc(Randomizer_DrawOverworldKey); // Key Rings - itemTable[RG_FOREST_TEMPLE_KEY_RING] = Item(RG_FOREST_TEMPLE_KEY_RING, Text{ "Forest Temple Key Ring", "Trousseau du Temple de la Forêt", "Schlüsselbund für den Waldtempel" }, ITEMTYPE_SMALLKEY, 0xD5, true, LOGIC_FOREST_TEMPLE_KEYS, RHT_FOREST_TEMPLE_KEY_RING, RG_FOREST_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%g").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_FOREST_TEMPLE_KEY_RING] = Item(RG_FOREST_TEMPLE_KEY_RING, Text{ "Forest Temple Key Ring", "Trousseau du Temple de la Forêt", "Schlüsselbund für den Waldtempel" }, ITEMTYPE_SMALLKEY, 0xD5, true, LOGIC_FOREST_TEMPLE_KEYS, RHT_FOREST_TEMPLE_KEY_RING, RG_FOREST_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%g"); itemTable[RG_FOREST_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_FIRE_TEMPLE_KEY_RING] = Item(RG_FIRE_TEMPLE_KEY_RING, Text{ "Fire Temple Key Ring", "Trousseau du Temple du Feu", "Schlüsselbund für den Feuertempel" }, ITEMTYPE_SMALLKEY, 0xD6, true, LOGIC_FIRE_TEMPLE_KEYS, RHT_FIRE_TEMPLE_KEY_RING, RG_FIRE_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%r").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_FIRE_TEMPLE_KEY_RING] = Item(RG_FIRE_TEMPLE_KEY_RING, Text{ "Fire Temple Key Ring", "Trousseau du Temple du Feu", "Schlüsselbund für den Feuertempel" }, ITEMTYPE_SMALLKEY, 0xD6, true, LOGIC_FIRE_TEMPLE_KEYS, RHT_FIRE_TEMPLE_KEY_RING, RG_FIRE_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%r"); itemTable[RG_FIRE_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_WATER_TEMPLE_KEY_RING] = Item(RG_WATER_TEMPLE_KEY_RING, Text{ "Water Temple Key Ring", "Trousseau du Temple de l'Eau", "Schlüsselbund für den Wassertempel" }, ITEMTYPE_SMALLKEY, 0xD7, true, LOGIC_WATER_TEMPLE_KEYS, RHT_WATER_TEMPLE_KEY_RING, RG_WATER_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%b").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_WATER_TEMPLE_KEY_RING] = Item(RG_WATER_TEMPLE_KEY_RING, Text{ "Water Temple Key Ring", "Trousseau du Temple de l'Eau", "Schlüsselbund für den Wassertempel" }, ITEMTYPE_SMALLKEY, 0xD7, true, LOGIC_WATER_TEMPLE_KEYS, RHT_WATER_TEMPLE_KEY_RING, RG_WATER_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%b"); itemTable[RG_WATER_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_SPIRIT_TEMPLE_KEY_RING] = Item(RG_SPIRIT_TEMPLE_KEY_RING, Text{ "Spirit Temple Key Ring", "Trousseau du Temple de l'Esprit", "Schlüsselbund für den Geistertempel" }, ITEMTYPE_SMALLKEY, 0xD8, true, LOGIC_SPIRIT_TEMPLE_KEYS, RHT_SPIRIT_TEMPLE_KEY_RING, RG_SPIRIT_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_SPIRIT_TEMPLE_KEY_RING] = Item(RG_SPIRIT_TEMPLE_KEY_RING, Text{ "Spirit Temple Key Ring", "Trousseau du Temple de l'Esprit", "Schlüsselbund für den Geistertempel" }, ITEMTYPE_SMALLKEY, 0xD8, true, LOGIC_SPIRIT_TEMPLE_KEYS, RHT_SPIRIT_TEMPLE_KEY_RING, RG_SPIRIT_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y"); itemTable[RG_SPIRIT_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_SHADOW_TEMPLE_KEY_RING] = Item(RG_SHADOW_TEMPLE_KEY_RING, Text{ "Shadow Temple Key Ring", "Trousseau du Temple de l'Ombre", "Schlüsselbund für den Schattentempel" }, ITEMTYPE_SMALLKEY, 0xD9, true, LOGIC_SHADOW_TEMPLE_KEYS, RHT_SHADOW_TEMPLE_KEY_RING, RG_SHADOW_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%p").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_SHADOW_TEMPLE_KEY_RING] = Item(RG_SHADOW_TEMPLE_KEY_RING, Text{ "Shadow Temple Key Ring", "Trousseau du Temple de l'Ombre", "Schlüsselbund für den Schattentempel" }, ITEMTYPE_SMALLKEY, 0xD9, true, LOGIC_SHADOW_TEMPLE_KEYS, RHT_SHADOW_TEMPLE_KEY_RING, RG_SHADOW_TEMPLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%p"); itemTable[RG_SHADOW_TEMPLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_BOTTOM_OF_THE_WELL_KEY_RING] = Item(RG_BOTTOM_OF_THE_WELL_KEY_RING, Text{ "Bottom of the Well Key Ring", "Trousseau du Puits", "Schlüsselbund für den Grund des Brunnens" }, ITEMTYPE_SMALLKEY, 0xDA, true, LOGIC_BOTTOM_OF_THE_WELL_KEYS, RHT_BOTTOM_OF_THE_WELL_KEY_RING, RG_BOTTOM_OF_THE_WELL_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%p").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_BOTTOM_OF_THE_WELL_KEY_RING] = Item(RG_BOTTOM_OF_THE_WELL_KEY_RING, Text{ "Bottom of the Well Key Ring", "Trousseau du Puits", "Schlüsselbund für den Grund des Brunnens" }, ITEMTYPE_SMALLKEY, 0xDA, true, LOGIC_BOTTOM_OF_THE_WELL_KEYS, RHT_BOTTOM_OF_THE_WELL_KEY_RING, RG_BOTTOM_OF_THE_WELL_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%p"); itemTable[RG_BOTTOM_OF_THE_WELL_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_GERUDO_TRAINING_GROUND_KEY_RING] = Item(RG_GERUDO_TRAINING_GROUND_KEY_RING, Text{ "Training Ground Key Ring", "Trousseau du Gymnase Gerudo", "Schlüsselbund für das Gerudo-Trainingsgelände" }, ITEMTYPE_SMALLKEY, 0xDB, true, LOGIC_GERUDO_TRAINING_GROUND_KEYS, RHT_GERUDO_TRAINING_GROUND_KEY_RING, RG_GERUDO_TRAINING_GROUND_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_GERUDO_TRAINING_GROUND_KEY_RING] = Item(RG_GERUDO_TRAINING_GROUND_KEY_RING, Text{ "Training Ground Key Ring", "Trousseau du Gymnase Gerudo", "Schlüsselbund für das Gerudo-Trainingsgelände" }, ITEMTYPE_SMALLKEY, 0xDB, true, LOGIC_GERUDO_TRAINING_GROUND_KEYS, RHT_GERUDO_TRAINING_GROUND_KEY_RING, RG_GERUDO_TRAINING_GROUND_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y"); itemTable[RG_GERUDO_TRAINING_GROUND_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_GERUDO_FORTRESS_KEY_RING] = Item(RG_GERUDO_FORTRESS_KEY_RING, Text{ "Gerudo Fortress Key Ring", "Trousseau du Repaire des Voleurs", "Schlüsselbund für die Gerudo-Festung" }, ITEMTYPE_FORTRESS_SMALLKEY, 0xDC, true, LOGIC_GERUDO_FORTRESS_KEYS, RHT_GERUDO_FORTRESS_KEY_RING, RG_GERUDO_FORTRESS_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_GERUDO_FORTRESS_KEY_RING] = Item(RG_GERUDO_FORTRESS_KEY_RING, Text{ "Gerudo Fortress Key Ring", "Trousseau du Repaire des Voleurs", "Schlüsselbund für die Gerudo-Festung" }, ITEMTYPE_FORTRESS_SMALLKEY, 0xDC, true, LOGIC_GERUDO_FORTRESS_KEYS, RHT_GERUDO_FORTRESS_KEY_RING, RG_GERUDO_FORTRESS_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%y"); itemTable[RG_GERUDO_FORTRESS_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_GANONS_CASTLE_KEY_RING] = Item(RG_GANONS_CASTLE_KEY_RING, Text{ "Ganon's Castle Key Ring", "Trousseau du Château de Ganon", "Schlüsselbund für Ganons Schloß" }, ITEMTYPE_SMALLKEY, 0xDD, true, LOGIC_GANONS_CASTLE_KEYS, RHT_GANONS_CASTLE_KEY_RING, RG_GANONS_CASTLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%r").CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_GANONS_CASTLE_KEY_RING] = Item(RG_GANONS_CASTLE_KEY_RING, Text{ "Ganon's Castle Key Ring", "Trousseau du Château de Ganon", "Schlüsselbund für Ganons Schloß" }, ITEMTYPE_SMALLKEY, 0xDD, true, LOGIC_GANONS_CASTLE_KEYS, RHT_GANONS_CASTLE_KEY_RING, RG_GANONS_CASTLE_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}, "%r"); itemTable[RG_GANONS_CASTLE_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); - itemTable[RG_TREASURE_GAME_KEY_RING] = Item(RG_TREASURE_GAME_KEY_RING, Text{ "Chest Game Key Ring", "Trousseau du jeu la Chasse-aux-Trésors", "Schlüsselbund für das Truhenspiel" }, ITEMTYPE_SMALLKEY, 0xDE, true, LOGIC_TREASURE_GAME_KEYS, RHT_TREASURE_GAME_KEY_RING, RG_TREASURE_GAME_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}).CustomIcon(gQuestIconSmallKeyTex, ICON_SIZE_24); + itemTable[RG_TREASURE_GAME_KEY_RING] = Item(RG_TREASURE_GAME_KEY_RING, Text{ "Chest Game Key Ring", "Trousseau du jeu la Chasse-aux-Trésors", "Schlüsselbund für das Truhenspiel" }, ITEMTYPE_SMALLKEY, 0xDE, true, LOGIC_TREASURE_GAME_KEYS, RHT_TREASURE_GAME_KEY_RING, RG_TREASURE_GAME_KEY_RING, OBJECT_GI_KEY, GID_KEY_SMALL, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_SMALL_KEY,MOD_RANDOMIZER, {"the ", "le ", "den "}); itemTable[RG_TREASURE_GAME_KEY_RING].SetCustomDrawFunc(Randomizer_DrawKeyRing); // Dungeon Rewards itemTable[RG_KOKIRI_EMERALD] = Item(RG_KOKIRI_EMERALD, Text{ "Kokiri's Emerald", "Émeraude Kokiri", "Kokiri-Smaragd" }, ITEMTYPE_DUNGEONREWARD, 0xCB, true, LOGIC_KOKIRI_EMERALD, RHT_KOKIRI_EMERALD, ITEM_KOKIRI_EMERALD, OBJECT_GI_JEWEL, GID_KOKIRI_EMERALD, 0x80, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"the ", "l'", "den "}, "%g"); diff --git a/soh/soh/Enhancements/randomizer/split_songs.cpp b/soh/soh/Enhancements/randomizer/split_songs.cpp index c0a17cfd9a7..cf777d01ef8 100644 --- a/soh/soh/Enhancements/randomizer/split_songs.cpp +++ b/soh/soh/Enhancements/randomizer/split_songs.cpp @@ -7,13 +7,8 @@ #include "logic.h" #include "static_data.h" -#include "functions.h" #include "global.h" #include "macros.h" -#include "z64player.h" -#include "variables.h" - -#include namespace Rando { @@ -43,21 +38,6 @@ static bool IsValidSplitSongId(SplitSongId id) { return id >= 0 && id < SPLIT_SONG_MAX; } -static std::vector sPendingFullSongGrants; -static bool sSongGrantQueued[SPLIT_SONG_MAX] = {}; - -static bool ShouldDeferFullSongQuestGrant() { - if (gPlayState == nullptr) { - return false; - } - Player* player = GET_PLAYER(gPlayState); - if (player == nullptr) { - return false; - } - return Player_InBlockingCsMode(gPlayState, player) || (player->stateFlags1 & PLAYER_STATE1_IN_ITEM_CS) || - (player->stateFlags1 & PLAYER_STATE1_GETTING_ITEM) || (player->stateFlags1 & PLAYER_STATE1_CARRYING_ACTOR); -} - static bool UsingLogicSimulationBuffer(Logic* logic) { return logic != nullptr && logic->mSaveContext != nullptr && logic->mSaveContext != &gSaveContext; } @@ -131,82 +111,12 @@ bool SplitSongs::HasFullSong(SplitSongId id) { return CHECK_QUEST_ITEM(qiIt->second); } -void SplitSongs::GrantFullSong(SplitSongId id) { - const SplitSongDef* def = GetSongDef(id); - if (def == nullptr) { - return; - } - - auto logic = Context::GetInstance()->GetLogic(); - logic->SetSaveContext(&gSaveContext); - const auto qiIt = Logic::RandoGetToQuestItem.find(static_cast(def->fullSong)); - if (qiIt != Logic::RandoGetToQuestItem.end() && CHECK_QUEST_ITEM(qiIt->second)) { - return; - } - - auto& fullSongItem = StaticData::RetrieveItem(def->fullSong); - logic->ApplyItemEffect(fullSongItem, true); -} - -void SplitSongs::TryCompleteSong(SplitSongId id) { - if (!HasSplitPart(id) || HasFullSong(id)) { - return; - } - if (gPlayState == nullptr) { - GrantFullSong(id); - return; - } - if (!IsValidSplitSongId(id) || sSongGrantQueued[id]) { - return; - } - sSongGrantQueued[id] = true; - sPendingFullSongGrants.push_back(id); -} - -void SplitSongs::ProcessPendingFullSongGrants() { - if (sPendingFullSongGrants.empty() || gPlayState == nullptr) { - return; - } - Player* player = GET_PLAYER(gPlayState); - if (player == nullptr || ShouldDeferFullSongQuestGrant()) { - return; - } - - std::vector batch = std::move(sPendingFullSongGrants); - sPendingFullSongGrants.clear(); - - for (SplitSongId id : batch) { - if (IsValidSplitSongId(id)) { - sSongGrantQueued[id] = false; - } - } - for (SplitSongId id : batch) { - if (!IsValidSplitSongId(id)) { - continue; - } - if (HasSplitPart(id) && !HasFullSong(id)) { - GrantFullSong(id); - } - } -} - -void SplitSongs::ClearPendingFullSongGrants() { - sPendingFullSongGrants.clear(); - for (size_t i = 0; i < SPLIT_SONG_MAX; i++) { - sSongGrantQueued[i] = false; - } -} - void SplitSongs::OnProgressiveSongReceived(RandomizerGet rg) { const SplitSongDef* def = GetSongDefFromProgressive(rg); - if (def == nullptr || HasFullSong(def->id)) { + if (def == nullptr || HasFullSong(def->id) || HasSplitPart(def->id)) { return; } - if (!HasSplitPart(def->id)) { - SetSplitPart(def->id, true); - return; - } - TryCompleteSong(def->id); + SetSplitPart(def->id, true); } ItemObtainability SplitSongs::GetProgressiveSongObtainability(RandomizerGet progressiveRg) { @@ -217,30 +127,6 @@ ItemObtainability SplitSongs::GetProgressiveSongObtainability(RandomizerGet prog return HasFullSong(def->id) ? CANT_OBTAIN_ALREADY_HAVE : CAN_OBTAIN; } -void SplitSongs::AppendShuffledSongPoolItems(std::vector& pool, bool split) { - for (const auto& def : kSplitSongs) { - pool.push_back(split ? def.progressive : def.fullSong); - } -} - -void SplitSongs::AppendSongIceTrapModels(std::vector& models, bool split) { - AppendShuffledSongPoolItems(models, split); -} - -void SplitSongs::DebugGiveAllSongParts(PlayState* play) { - if (play == nullptr) { - return; - } - for (const auto& def : kSplitSongs) { - if (!HasSplitPart(def.id)) { - Randomizer_Item_Give(play, StaticData::RetrieveItem(def.progressive).GetGIEntry_Copy()); - } - if (!HasFullSong(def.id)) { - Randomizer_Item_Give(play, StaticData::RetrieveItem(def.progressive).GetGIEntry_Copy()); - } - } -} - void SplitSongs::ApplyProgressiveEffectToLogicScratch(Logic* logic, RandomizerGet rg, bool state) { if (!UsingLogicSimulationBuffer(logic)) { return; @@ -268,10 +154,7 @@ RandomizerGet SplitSongs::ResolveProgressiveSongStage(RandomizerGet rg) { if (def == nullptr) { return RG_NONE; } - if (HasFullSong(def->id)) { - return def->fullSong; - } - if (HasSplitPart(def->id)) { + if (HasFullSong(def->id) || HasSplitPart(def->id)) { return def->fullSong; } return def->progressive; diff --git a/soh/soh/Enhancements/randomizer/split_songs.h b/soh/soh/Enhancements/randomizer/split_songs.h index 18224b815d3..572ccc069a8 100644 --- a/soh/soh/Enhancements/randomizer/split_songs.h +++ b/soh/soh/Enhancements/randomizer/split_songs.h @@ -4,9 +4,6 @@ #pragma once #include "randomizerTypes.h" -#include - -struct PlayState; namespace Rando { @@ -40,31 +37,15 @@ class SplitSongs { static const SplitSongDef* GetSongDefFromProgressive(RandomizerGet rg); static const SplitSongDef* GetSongDefFromFullSong(RandomizerGet fullSongRg); - /** RandInf set after the first progressive pickup for this song. */ static bool HasSplitPart(SplitSongId id); static void SetSplitPart(SplitSongId id, bool state); - static bool HasFullSong(SplitSongId id); - static void GrantFullSong(SplitSongId id); - static void TryCompleteSong(SplitSongId id); static bool IsProgressiveSong(RandomizerGet rg); - - static void ProcessPendingFullSongGrants(); - static void ClearPendingFullSongGrants(); - static void OnProgressiveSongReceived(RandomizerGet rg); - static void AppendShuffledSongPoolItems(std::vector& pool, bool split); - static void AppendSongIceTrapModels(std::vector& models, bool split); - static ItemObtainability GetProgressiveSongObtainability(RandomizerGet progressiveRg); - - static void DebugGiveAllSongParts(PlayState* play); - static void ApplyProgressiveEffectToLogicScratch(Logic* logic, RandomizerGet rg, bool state); - - /** Item to show on pickup / GI resolve: progressive first, full song on second. */ static RandomizerGet ResolveProgressiveSongStage(RandomizerGet rg); }; diff --git a/soh/soh/SohGui/SohMenuDevTools.cpp b/soh/soh/SohGui/SohMenuDevTools.cpp index dfbb0754f23..c5b88ce8fbc 100644 --- a/soh/soh/SohGui/SohMenuDevTools.cpp +++ b/soh/soh/SohGui/SohMenuDevTools.cpp @@ -78,12 +78,6 @@ void SohMenu::AddMenuDevTools() { AddWidget(path, "Resource logging", WIDGET_CVAR_CHECKBOX) .CVar(CVAR_DEVELOPER_TOOLS("ResourceLogging")) .Options(CheckboxOptions().Tooltip("Logs some resources as XML when they're loaded in binary format.")); - AddWidget(path, "Log textbox item icons", WIDGET_CVAR_CHECKBOX) - .CVar(CVAR_DEVELOPER_TOOLS("LogTextboxItemIcons")) - .Options(CheckboxOptions().Tooltip( - "Writes [TextboxItemIcon] decode and DRAW lines to the log when the message system loads or draws a " - "textbox item icon (MESSAGE_ITEM_ICON). Restart after enabling to see the startup confirmation when " - "randomizer hooks register; then open any get-item text that shows an icon.")); AddWidget(path, "Frame Advance", WIDGET_CHECKBOX) .Options(CheckboxOptions().Tooltip( diff --git a/soh/src/code/z_message_PAL.c b/soh/src/code/z_message_PAL.c index a02f5502ea8..ce09fe27168 100644 --- a/soh/src/code/z_message_PAL.c +++ b/soh/src/code/z_message_PAL.c @@ -7,7 +7,6 @@ #include "textures/parameter_static/parameter_static.h" #include "textures/message_static/message_static.h" #include "textures/message_texture_static/message_texture_static.h" -#include "textures/icon_item_static/icon_item_static.h" #include "soh/Enhancements/cosmetics/CosmeticsEditor.h" #include "soh/Enhancements/cosmetics/cosmeticsTypes.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" @@ -16,11 +15,6 @@ #include "soh/SaveManager.h" #include "soh/ResourceManagerHelpers.h" -extern void Randomizer_Message_DecodeLoadItemIcon(PlayState* play, u16 iconToLoad, s32 displayAsEnglishAsInt); -extern void Randomizer_Message_AppendSegmentIconGfx(Gfx** gfxp); -extern void Randomizer_Message_RefreshCustomItemIconAtDraw(s32 displayAsEnglishAsInt); -extern u16 Randomizer_ResolveSongIconAtDrawTime(PlayState* play, u16 itemId); - // #region SOH [NTSC] - Allows custom messages to work on japanese static bool sDisplayNextMessageAsEnglish = false; static u8 sLastLanguage = LANGUAGE_ENG; @@ -843,119 +837,6 @@ f32 sFontWidths[144] = { 14.0f, // ? }; -/** - * Copies the item's OTR icon path into the textbox segment and sets R_TEXTBOX_ICON_*. - * The segment slot is shared with other message data; re-applying at draw time avoids stale or - * clobbered bytes being interpreted as a texture (garbled icon) when decode and draw are far apart. - */ -static void Message_SetTextboxItemIconFromItemId(PlayState* play, u16 itemId, s16 y) { - static s16 sIconItem32XOffsets[] = { 74, 74, 74, 54 }; - static s16 sIconItem24XOffsets[] = { 72, 72, 72, 50 }; - MessageContext* msgCtx = &play->msgCtx; - InterfaceContext* interfaceCtx = &play->interfaceCtx; - u8 language = sDisplayNextMessageAsEnglish ? LANGUAGE_ENG : gSaveContext.language; - const void* iconEntry; - const char* texPath; - size_t pathLen; - u16 id = itemId; - bool isSongIcon; - - if (id == ITEM_DUNGEON_MAP) { - interfaceCtx->mapPalette[30] = 0xFF; - interfaceCtx->mapPalette[31] = 0xFF; - } - if (id >= ARRAY_COUNT(gItemIcons)) { - id = ITEM_STICK; - } - isSongIcon = (id >= ITEM_SONG_MINUET) && (id <= ITEM_SONG_STORMS); - iconEntry = gItemIcons[id]; - texPath = (const char*)iconEntry; - if (texPath == NULL || texPath[0] == '\0') { - texPath = (const char*)gItemIconDekuStickTex; - } - pathLen = strlen(texPath) + 1; - if ((id < ITEM_MEDALLION_FOREST) && !isSongIcon) { - R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem32XOffsets[language]; - R_TEXTBOX_ICON_YPOS = y + 6; - R_TEXTBOX_ICON_SIZE = 32; - memcpy((void*)((uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE), texPath, pathLen); - osSyncPrintf("アイテム32-0\n"); - } else { - R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem24XOffsets[language]; - R_TEXTBOX_ICON_YPOS = y + 10; - R_TEXTBOX_ICON_SIZE = 24; - memcpy((void*)((uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE), texPath, pathLen); - osSyncPrintf("アイテム24=%d (%d) {%d}\n", id, id - ITEM_KOKIRI_EMERALD, 84); - } -} - -static bool Message_GetSongIconTint(u16 itemId, u8* r, u8* g, u8* b) { - switch (itemId) { - case ITEM_SONG_LULLABY: - *r = 224; - *g = 107; - *b = 255; - return true; - case ITEM_SONG_EPONA: - *r = 255; - *g = 195; - *b = 60; - return true; - case ITEM_SONG_SARIA: - *r = 127; - *g = 255; - *b = 137; - return true; - case ITEM_SONG_SUN: - *r = 255; - *g = 255; - *b = 60; - return true; - case ITEM_SONG_TIME: - *r = 119; - *g = 236; - *b = 255; - return true; - case ITEM_SONG_STORMS: - *r = 165; - *g = 165; - *b = 165; - return true; - case ITEM_SONG_MINUET: - *r = 150; - *g = 255; - *b = 100; - return true; - case ITEM_SONG_BOLERO: - *r = 255; - *g = 80; - *b = 40; - return true; - case ITEM_SONG_SERENADE: - *r = 100; - *g = 150; - *b = 255; - return true; - case ITEM_SONG_REQUIEM: - *r = 255; - *g = 160; - *b = 0; - return true; - case ITEM_SONG_NOCTURNE: - *r = 255; - *g = 100; - *b = 255; - return true; - case ITEM_SONG_PRELUDE: - *r = 255; - *g = 240; - *b = 100; - return true; - default: - return false; - } -} - u16 Message_DrawItemIcon(PlayState* play, u16 itemId, Gfx** p, u16 i) { s32 pad; Gfx* gfx = *p; @@ -972,53 +853,20 @@ u16 Message_DrawItemIcon(PlayState* play, u16 itemId, Gfx** p, u16 i) { // Invalidate icon texture as it may have changed from the last time a text box had an icon gSPInvalidateTexCache(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE); - s32 drawKind = -2; - s32 iconDrawWidth = R_TEXTBOX_ICON_SIZE; - s32 iconDrawHeight = R_TEXTBOX_ICON_SIZE; - - if (itemId < ITEM_CUSTOM) { - if (GameInteractor_Should(VB_DRAW_ITEM_ICON, true, &gfx)) { - itemId = Randomizer_ResolveSongIconAtDrawTime(play, itemId); - u16 iconIdClamped = itemId; - u8 songR; - u8 songG; - u8 songB; - if (iconIdClamped >= ARRAY_COUNT(gItemIcons)) { - iconIdClamped = ITEM_STICK; - } - Message_SetTextboxItemIconFromItemId(play, itemId, (s16)(R_TEXTBOX_Y + 10)); - if (Message_GetSongIconTint(iconIdClamped, &songR, &songG, &songB)) { - gDPSetPrimColor(gfx++, 0, 0, songR, songG, songB, msgCtx->textColorAlpha); - } - if ((iconIdClamped >= ITEM_SONG_MINUET) && (iconIdClamped <= ITEM_SONG_STORMS)) { - gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_IA, - G_IM_SIZ_8b, 16, 24, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, - G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); - drawKind = 16; - iconDrawWidth = 16; - iconDrawHeight = 24; - } else if (iconIdClamped >= ITEM_MEDALLION_FOREST) { - gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, - G_IM_SIZ_32b, 24, 24, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, - G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); - drawKind = 24; - } else { - gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, - G_IM_SIZ_32b, 32, 32, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, - G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); - drawKind = 32; - } + if (GameInteractor_Should(VB_DRAW_ITEM_ICON, itemId < ITEM_CUSTOM, &gfx)) { + if (itemId >= ITEM_MEDALLION_FOREST) { + gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, + G_IM_SIZ_32b, 24, 24, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, + G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); } else { - drawKind = -1; + gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, + G_IM_SIZ_32b, 32, 32, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, + G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); } - } else { - Randomizer_Message_RefreshCustomItemIconAtDraw(sDisplayNextMessageAsEnglish ? 1 : 0); - Randomizer_Message_AppendSegmentIconGfx(&gfx); - drawKind = 0; } gSPTextureRectangle(gfx++, (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS) << 2, R_TEXTBOX_ICON_YPOS << 2, - (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS + iconDrawWidth) << 2, - (R_TEXTBOX_ICON_YPOS + iconDrawHeight) << 2, G_TX_RENDERTILE, 0, 0, 1 << 10, 1 << 10); + (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS + R_TEXTBOX_ICON_SIZE) << 2, + (R_TEXTBOX_ICON_YPOS + R_TEXTBOX_ICON_SIZE) << 2, G_TX_RENDERTILE, 0, 0, 1 << 10, 1 << 10); gDPPipeSync(gfx++); gDPSetCombineLERP(gfx++, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0); @@ -1792,9 +1640,33 @@ void Message_DrawText(PlayState* play, Gfx** gfxP) { } void Message_LoadItemIcon(PlayState* play, u16 itemId, s16 y) { + static s16 sIconItem32XOffsets[] = { 74, 74, 74, 54 }; + static s16 sIconItem24XOffsets[] = { 72, 72, 72, 50 }; MessageContext* msgCtx = &play->msgCtx; + InterfaceContext* interfaceCtx = &play->interfaceCtx; + u8 language = sDisplayNextMessageAsEnglish ? LANGUAGE_ENG : gSaveContext.language; - Message_SetTextboxItemIconFromItemId(play, itemId, y); + if (itemId == ITEM_DUNGEON_MAP) { + interfaceCtx->mapPalette[30] = 0xFF; + interfaceCtx->mapPalette[31] = 0xFF; + } + if (itemId < ITEM_MEDALLION_FOREST) { + R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem32XOffsets[language]; + R_TEXTBOX_ICON_YPOS = y + 6; + R_TEXTBOX_ICON_SIZE = 32; + memcpy((uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, gItemIcons[itemId], + strlen(gItemIcons[itemId]) + 1); + // "Item 32-0" + osSyncPrintf("アイテム32-0\n"); + } else { + R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem24XOffsets[language]; + R_TEXTBOX_ICON_YPOS = y + 10; + R_TEXTBOX_ICON_SIZE = 24; + memcpy((uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, gItemIcons[itemId], + strlen(gItemIcons[itemId]) + 1); + // "Item 24" + osSyncPrintf("アイテム24=%d (%d) {%d}\n", itemId, itemId - ITEM_KOKIRI_EMERALD, 84); + } msgCtx->msgBufPos++; msgCtx->choiceNum = 1; } @@ -2052,7 +1924,6 @@ void Message_DecodeJPN(PlayState* play) { s16 digits[4]; u16 value; s16 decodedBufPos = 0; - s16 itemIconDecodedPos = -1; s16 i; s16 j; f32 timeInSeconds; @@ -2352,22 +2223,10 @@ void Message_DecodeJPN(PlayState* play) { } } } else if (curChar == MESSAGE_ITEM_ICON_JPN) { - u16 messageItemIcon = font->msgBufWide[msgCtx->msgBufPos + 1]; - u16 iconToLoad = messageItemIcon; - if (msgCtx->textId == 0xF8 && itemIconDecodedPos >= 0) { - msgCtx->msgBufDecodedWide[itemIconDecodedPos] = iconToLoad; - } else { - msgCtx->msgBufDecodedWide[++decodedBufPos] = iconToLoad; - itemIconDecodedPos = decodedBufPos; - } - if (iconToLoad < ITEM_CUSTOM) { - s32 loadPermitted = - GameInteractor_Should(VB_LOAD_ITEM_ICON, true, sDisplayNextMessageAsEnglish) ? 1 : 0; - if (loadPermitted) { - Message_LoadItemIcon(play, iconToLoad, R_TEXTBOX_Y + 10); - } - } else { - Randomizer_Message_DecodeLoadItemIcon(play, iconToLoad, sDisplayNextMessageAsEnglish ? 1 : 0); + msgCtx->msgBufDecodedWide[++decodedBufPos] = font->msgBufWide[msgCtx->msgBufPos + 1]; + if (GameInteractor_Should(VB_LOAD_ITEM_ICON, (uint8_t)font->msgBuf[msgCtx->msgBufPos + 1] < ITEM_CUSTOM, + sDisplayNextMessageAsEnglish)) { + Message_LoadItemIcon(play, font->msgBufWide[msgCtx->msgBufPos + 1], R_TEXTBOX_Y + 10); } } else if (curChar == MESSAGE_BACKGROUND_JPN) { msgCtx->textboxBackgroundIdx = font->msgBufWide[msgCtx->msgBufPos + 1] * 2; @@ -2419,7 +2278,6 @@ void Message_Decode(PlayState* play) { s32 charTexIdx = 0; s16 playerNameLen; s16 decodedBufPos = 0; - s16 itemIconDecodedPos = -1; s16 numLines = 0; s16 i; s16 digits[4]; @@ -2796,23 +2654,12 @@ void Message_Decode(PlayState* play) { } decodedBufPos--; } else if (temp_s2 == MESSAGE_ITEM_ICON) { - u16 messageItemIcon = (uint8_t)font->msgBuf[msgCtx->msgBufPos + 1]; - u16 iconToLoad = messageItemIcon; - if (msgCtx->textId == 0xF8 && itemIconDecodedPos >= 0) { - msgCtx->msgBufDecoded[itemIconDecodedPos] = iconToLoad; - } else { - msgCtx->msgBufDecoded[++decodedBufPos] = iconToLoad; - itemIconDecodedPos = decodedBufPos; - } - osSyncPrintf("ITEM_NO=(%d) (%d)\n", iconToLoad, iconToLoad); - if (iconToLoad < ITEM_CUSTOM) { - s32 loadPermitted = - GameInteractor_Should(VB_LOAD_ITEM_ICON, true, sDisplayNextMessageAsEnglish) ? 1 : 0; - if (loadPermitted) { - Message_LoadItemIcon(play, iconToLoad, R_TEXTBOX_Y + 10); - } - } else { - Randomizer_Message_DecodeLoadItemIcon(play, iconToLoad, sDisplayNextMessageAsEnglish ? 1 : 0); + msgCtx->msgBufDecoded[++decodedBufPos] = font->msgBuf[msgCtx->msgBufPos + 1]; + osSyncPrintf("ITEM_NO=(%d) (%d)\n", msgCtx->msgBufDecoded[decodedBufPos], + font->msgBuf[msgCtx->msgBufPos + 1]); + if (GameInteractor_Should(VB_LOAD_ITEM_ICON, (uint8_t)font->msgBuf[msgCtx->msgBufPos + 1] < ITEM_CUSTOM, + sDisplayNextMessageAsEnglish)) { + Message_LoadItemIcon(play, font->msgBuf[msgCtx->msgBufPos + 1], R_TEXTBOX_Y + 10); } } else if (temp_s2 == MESSAGE_BACKGROUND) { msgCtx->textboxBackgroundIdx = font->msgBuf[msgCtx->msgBufPos + 1] * 2; From 600b8d392424e3069da5fb1f3b205732a4e6f2ae Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:33:51 +0100 Subject: [PATCH 17/22] Address split songs review: settings, pool, tracker, traps. FinalizeSettings clears split songs when shuffle is not Anywhere. Drop manual ice trap model list. Add progressive song trick names. Song tracker shows 1/2 on the normal songs row. getItemEntry sync lives in ItemMessages, not z_player. --- .../randomizer/3drando/item_pool.cpp | 21 +- .../randomizer/Messages/ItemMessages.cpp | 21 ++ soh/soh/Enhancements/randomizer/Traps.cpp | 68 +++++- soh/soh/Enhancements/randomizer/logic.cpp | 3 +- .../randomizer/option_descriptions.cpp | 2 +- .../randomizer/randomizer_item_tracker.cpp | 207 +++--------------- soh/soh/Enhancements/randomizer/settings.cpp | 4 + .../actors/ovl_player_actor/z_player.c | 4 - 8 files changed, 123 insertions(+), 207 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp index 71600cc1110..f78c9126c2b 100644 --- a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp @@ -156,25 +156,6 @@ void GenerateItemPool() { lesserPool.clear(); int reservedSlots = 0; - if (ctx->GetOption(RSK_SHUFFLE_SONGS).Is(RO_SONG_SHUFFLE_ANYWHERE)) { - const bool split = static_cast(ctx->GetOption(RSK_SPLIT_OCARINA_SONGS)); - auto addSongIceTrapModel = [&](RandomizerGet fullSong, RandomizerGet progressive) { - ctx->possibleIceTrapModels.insert(split ? progressive : fullSong); - }; - addSongIceTrapModel(RG_ZELDAS_LULLABY, RG_PROGRESSIVE_ZELDAS_LULLABY); - addSongIceTrapModel(RG_EPONAS_SONG, RG_PROGRESSIVE_EPONAS_SONG); - addSongIceTrapModel(RG_SARIAS_SONG, RG_PROGRESSIVE_SARIAS_SONG); - addSongIceTrapModel(RG_SUNS_SONG, RG_PROGRESSIVE_SUNS_SONG); - addSongIceTrapModel(RG_SONG_OF_TIME, RG_PROGRESSIVE_SONG_OF_TIME); - addSongIceTrapModel(RG_SONG_OF_STORMS, RG_PROGRESSIVE_SONG_OF_STORMS); - addSongIceTrapModel(RG_MINUET_OF_FOREST, RG_PROGRESSIVE_MINUET_OF_FOREST); - addSongIceTrapModel(RG_BOLERO_OF_FIRE, RG_PROGRESSIVE_BOLERO_OF_FIRE); - addSongIceTrapModel(RG_SERENADE_OF_WATER, RG_PROGRESSIVE_SERENADE_OF_WATER); - addSongIceTrapModel(RG_REQUIEM_OF_SPIRIT, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT); - addSongIceTrapModel(RG_NOCTURNE_OF_SHADOW, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW); - addSongIceTrapModel(RG_PRELUDE_OF_LIGHT, RG_PROGRESSIVE_PRELUDE_OF_LIGHT); - } - // clang-format off AddItemToPool(RG_BOOMERANG, 2, 1, 1, 1); AddItemToPool(RG_LENS_OF_TRUTH, 2, 1, 1, 1); @@ -271,7 +252,7 @@ void GenerateItemPool() { // add extra songs only if song shuffle is anywhere if (ctx->GetOption(RSK_SHUFFLE_SONGS).IsNot(RO_SONG_SHUFFLE_OFF)) { bool songAnywhere = ctx->GetOption(RSK_SHUFFLE_SONGS).Is(RO_SONG_SHUFFLE_ANYWHERE); - const bool split = ctx->GetOption(RSK_SPLIT_OCARINA_SONGS) && songAnywhere; + const bool split = ctx->GetOption(RSK_SPLIT_OCARINA_SONGS).Get(); const int defaultPlentiful = songAnywhere ? 2 : 1; auto addShuffledSong = [&](RandomizerGet fullSong, RandomizerGet progressive, bool hasStarting) { if (hasStarting) { diff --git a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp index 5283ecacfd6..7832d16ec4d 100644 --- a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp @@ -22,7 +22,27 @@ extern "C" { #include #include #include "z64item.h" +#include "z64player.h" extern PlayState* gPlayState; +GetItemEntry ItemTable_Retrieve(int16_t getItemID); +GetItemEntry ItemTable_RetrieveEntry(s16 modIndex, s16 getItemID); +} + +static void SyncPlayerGetItemEntryForMessage(Player* player) { + if (player == nullptr) { + return; + } + if (player->getItemEntry.objectId != OBJECT_INVALID && player->getItemId == player->getItemEntry.getItemId) { + return; + } + GetItemEntry giEntry; + if (IS_RANDO && player->getItemId > RG_NONE && player->getItemId < RG_MAX) { + giEntry = ItemTable_RetrieveEntry(MOD_RANDOMIZER, player->getItemId); + } else { + giEntry = ItemTable_Retrieve(player->getItemId); + } + player->getItemEntry = giEntry; + player->getItemId = giEntry.getItemId; } static ItemID SongIconForRandomizerGet(RandomizerGet rg) { @@ -212,6 +232,7 @@ void DrawCustomItemIcon(Gfx** p) { void BuildItemMessage(u16* textId, bool* loadFromMessageTable) { Player* player = GET_PLAYER(gPlayState); + SyncPlayerGetItemEntryForMessage(player); CustomMessage msg; if (player->getItemEntry.getItemId == RG_ICE_TRAP) { diff --git a/soh/soh/Enhancements/randomizer/Traps.cpp b/soh/soh/Enhancements/randomizer/Traps.cpp index 474380d3521..40b31e32ecc 100644 --- a/soh/soh/Enhancements/randomizer/Traps.cpp +++ b/soh/soh/Enhancements/randomizer/Traps.cpp @@ -2,7 +2,6 @@ #include "soh/Enhancements/randomizer/SeedContext.h" #include "soh/Enhancements/randomizer/randomizerTypes.h" #include "soh/Enhancements/randomizer/static_data.h" -#include "soh/Enhancements/randomizer/split_songs.h" #include "soh/ShipUtils.h" #include @@ -358,6 +357,68 @@ static void InitTrickNames() { Text{ "Progressive Flute", "Flûte (prog.)", "Flöte (prog.)" }, // "Flauta progresiva" Text{ "Progressive Recorder", "Harmonica (prog.)", "Rekorder (prog.)" }, // "Armónica progresiva" }; + trickNameTable[RG_PROGRESSIVE_ZELDAS_LULLABY] = { + Text{ "Progressive Ballad of the Goddess", "Berceuse (prog.)", "Progressives Wiegenlied" }, + Text{ "Progressive Song of Healing", "Chant de l'apaisement (prog.)", "Progressives Heilungslied" }, + Text{ "Progressive Song of the Hero", "Chant du héros (prog.)", "Progressives Heldenlied" }, + }; + trickNameTable[RG_PROGRESSIVE_EPONAS_SONG] = { + Text{ "Progressive Song of Birds", "Chant d'Epona (prog.)", "Progressives Eponas Lied" }, + Text{ "Progressive Song of Soaring", "Chant de l'envol (prog.)", "Progressives Lied der Schwingen" }, + Text{ "Progressive Song of Horse", "Chant du cheval (prog.)", "Progressives Pferdelied" }, + }; + trickNameTable[RG_PROGRESSIVE_SARIAS_SONG] = { + Text{ "Progressive Mido's Song", "Chant de Saria (prog.)", "Progressives Salias Lied" }, + Text{ "Progressive Kass' Theme", "Chant de Kass (prog.)", "Progressives Kashiwas Thema" }, + Text{ "Progressive Tune of Echoes", "Chant des Échos (prog.)", "Progressive Melodie des Echos" }, + }; + trickNameTable[RG_PROGRESSIVE_SUNS_SONG] = { + Text{ "Progressive Song of Passing", "Chant du Soleil (prog.)", "Progressiver Hymne der Sonne" }, + Text{ "Progressive Command Melody", "Mambo (prog.)", "Progressive Tag- und Nachtmusik" }, + Text{ "Progressive Moon's Song", "Chant de la lune (prog.)", "Progressives Mondlied" }, + }; + trickNameTable[RG_PROGRESSIVE_SONG_OF_TIME] = { + Text{ "Progressive Song of Double Time", "Chant du Temps (prog.)", "Progressiver Hymne der Zeit" }, + Text{ "Progressive Inverted Song of Time", "Chant du temps inversé (prog.)", "Progressive Ballade des Kronos" }, + Text{ "Progressive Tune of Ages", "Chant du Temps (prog.)", "Progressive Melodie der Zeit" }, + }; + trickNameTable[RG_PROGRESSIVE_SONG_OF_STORMS] = { + Text{ "Progressive Ballad of Gales", "Chant des Tempêtes (prog.)", "Progressiver Hymne des Sturms" }, + Text{ "Progressive Frog's Song of Soul", "Rap des grenouilles (prog.)", "Progressiver Krötenrap" }, + Text{ "Progressive Wind's Requiem", "Mélodie du vent (prog.)", "Progressives Lied des Windes" }, + }; + trickNameTable[RG_PROGRESSIVE_MINUET_OF_FOREST] = { + Text{ "Progressive Saria's Karaoke", "Menuet des Bois (prog.)", "Progressives Menuett des Waldes" }, + Text{ "Progressive Sonata of Awakening", "Sonate de l'éveil (prog.)", "Progressive Sonate des Erwachens" }, + Text{ "Progressive Wind God's Aria", "Hymne du dieu du vent (prog.)", "Progressive Hymne des Zephirgottes" }, + }; + trickNameTable[RG_PROGRESSIVE_BOLERO_OF_FIRE] = { + Text{ "Progressive Darunia's Tango", "Boléro du Feu (prog.)", "Progressives Bolero des Feuers" }, + Text{ "Progressive Tune of Currents", "Chants des Flux (prog.)", "Progressives Lied des Zeitstroms" }, + Text{ "Progressive Goron Lullaby", "Berceuse des Gorons (prog.)", "Progressives Goronisches Schlummerlied" }, + }; + trickNameTable[RG_PROGRESSIVE_SERENADE_OF_WATER] = { + Text{ "Progressive Ruto's Blues", "Sérénade de l'Eau (prog.)", "Progressive Serenade des Wassers" }, + Text{ "Progressive New Wave Bossa Nova", "Bossa-nova des flots (prog.)", "Progressive Bossa Nova der Kaskaden" }, + Text{ "Progressive Manbo's Mambo", "Mambo de Manbo (prog.)", "Progressives Manbos Mambo" }, + }; + trickNameTable[RG_PROGRESSIVE_REQUIEM_OF_SPIRIT] = { + Text{ "Progressive Nabooru's Reggae", "Requiem des Esprits (prog.)", "Progressives Requiem der Geister" }, + Text{ "Progressive Elegy of Emptiness", "Hymne du vide (prog.)", "Progressive Elegie des leeren Herzens" }, + Text{ "Progressive Earth God's Lyric", "Hymne du dieu de la terre (prog.)", + "Progressive Hymne des Terragottes" }, + }; + trickNameTable[RG_PROGRESSIVE_NOCTURNE_OF_SHADOW] = { + Text{ "Progressive Impa's Death Metal", "Nocturne de l'Ombre (prog.)", "Progressive Nocturne des Schattens" }, + Text{ "Progressive Oath to Order", "Ode de l'appel (prog.)", "Progressiver Gesang des Himmels" }, + Text{ "Progressive Song of Discovery", "Chant des secrets (prog.)", "Progressives Schatzsucherlied" }, + }; + trickNameTable[RG_PROGRESSIVE_PRELUDE_OF_LIGHT] = { + Text{ "Progressive Rauru's Sing-Along", "Prélude de la Lumière (prog.)", "Progressive Kantate des Lichts" }, + Text{ "Progressive Ballad of the Wind Fish", "Ballade sur Poisson-Rêve (prog.)", + "Progressive Ballade vom Windfisch" }, + Text{ "Progressive Song of Light", "Chant de la lumière (prog.)", "Progressives Lied des Lichts" }, + }; trickNameTable[RG_PROGRESSIVE_GORONSWORD] = { Text{ "Progressive Titan Blade", "Lame des Titans (prog.)", "Titanenklinge (prog.)" }, // "Hoja del Titán progresiva" @@ -1426,11 +1487,6 @@ Text Rando::Traps::GetTrapName(uint16_t id) { initTrickNames = true; } - RandomizerGet rg = static_cast(id); - if (const Rando::SplitSongDef* progDef = Rando::SplitSongs::GetSongDefFromProgressive(rg)) { - id = static_cast(progDef->fullSong); - } - if (trickNameTable[id].empty()) { assert(false); return Text{ "not an Ice Trap" }; diff --git a/soh/soh/Enhancements/randomizer/logic.cpp b/soh/soh/Enhancements/randomizer/logic.cpp index 56f65fbaa61..7a60f338213 100644 --- a/soh/soh/Enhancements/randomizer/logic.cpp +++ b/soh/soh/Enhancements/randomizer/logic.cpp @@ -111,8 +111,7 @@ bool Logic::HasItem(RandomizerGet itemName) { case RG_REQUIEM_OF_SPIRIT: case RG_NOCTURNE_OF_SHADOW: case RG_PRELUDE_OF_LIGHT: { - const bool splitAnywhere = ctx->GetOption(RSK_SPLIT_OCARINA_SONGS) && - ctx->GetOption(RSK_SHUFFLE_SONGS).Is(RO_SONG_SHUFFLE_ANYWHERE); + const bool splitAnywhere = ctx->GetOption(RSK_SPLIT_OCARINA_SONGS).Get(); const auto qiIt = RandoGetToQuestItem.find(itemName); if (qiIt == RandoGetToQuestItem.end()) { SPDLOG_ERROR("HasItem: song RandomizerGet {} missing from RandoGetToQuestItem", diff --git a/soh/soh/Enhancements/randomizer/option_descriptions.cpp b/soh/soh/Enhancements/randomizer/option_descriptions.cpp index 16c2d0fa830..89301407112 100644 --- a/soh/soh/Enhancements/randomizer/option_descriptions.cpp +++ b/soh/soh/Enhancements/randomizer/option_descriptions.cpp @@ -219,7 +219,7 @@ void Settings::CreateOptionDescriptions() { "Anywhere - Songs can appear at any location."; mOptionDescriptions[RSK_SPLIT_OCARINA_SONGS] = "Each ocarina song becomes a progressive song item (like bomb bag upgrades). The first pickup marks " - "progress; the second grants the full song. The pool includes extra copies per song to reduce softlocks."; + "progress; the second grants the full song. Requires Shuffle Songs: Anywhere."; mOptionDescriptions[RSK_SHUFFLE_TOKENS] = "Shuffles Golden Skulltula Tokens into the item pool. This means " "Golden Skulltulas can contain other items as well.\n" "\n" diff --git a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp index 83086fdb2f5..beebb867673 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp @@ -44,7 +44,6 @@ void DrawDungeonItem(ItemTrackerItem item); void DrawBottle(ItemTrackerItem item); void DrawQuest(ItemTrackerItem item); void DrawSong(ItemTrackerItem item); -void DrawSplitSongProgress(ItemTrackerItem item); int itemTrackerSectionId; @@ -70,7 +69,6 @@ static WidgetInfo jabberNutsTracking; static WidgetInfo ocarinaButtonTracking; static WidgetInfo overworldKeysTracking; static WidgetInfo fishingPoleTracking; -static WidgetInfo songPartsTracking; static WidgetInfo personalNotesWiget; static WidgetInfo hookshotIdentWidget; @@ -461,94 +459,42 @@ typedef enum { SECTION_DISPLAY_MINIMAL_SEPARATE, } ItemTrackerMinimalDisplayType; -// One icon per logical song; order matches Rando::SplitSongs::GetSongDef (split_songs.cpp kSplitSongs). -static std::vector songPartItemsTemplate; -std::vector songPartItems; - -static void EnsureSongPartItemsTemplate() { - if (songPartItemsTemplate.size() == static_cast(Rando::SplitSongId::SPLIT_SONG_MAX)) { - return; +static bool ItemTrackerSplitSongsActive() { + if (GameInteractor::IsSaveLoaded() && IS_RANDO) { + return OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_SPLIT_OCARINA_SONGS) != 0; } - songPartItemsTemplate.clear(); + return CVarGetInteger(CVAR_RANDOMIZER_SETTING("SplitOcarinaSongs"), 0) != 0; +} + +static const Rando::SplitSongDef* SplitSongDefForQuestBit(uint32_t questBit) { for (int i = 0; i < static_cast(Rando::SplitSongId::SPLIT_SONG_MAX); i++) { const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDef(static_cast(i)); if (def == nullptr) { continue; } const auto qiIt = Rando::Logic::RandoGetToQuestItem.find(static_cast(def->fullSong)); - if (qiIt == Rando::Logic::RandoGetToQuestItem.end()) { - continue; - } - const QuestItem questSong = static_cast(qiIt->second); - const auto sit = songMapping.find(questSong); - if (sit == songMapping.end()) { - continue; + if (qiIt != Rando::Logic::RandoGetToQuestItem.end() && static_cast(qiIt->second) == questBit) { + return def; } - const std::string& texName = sit->second.name; - const std::string& texFaded = sit->second.nameFaded; - songPartItemsTemplate.push_back( - { static_cast(def->fullSong), texName, texFaded, 0, DrawSplitSongProgress }); } + return nullptr; } -static bool ItemTrackerSongPartsSeedActive() { - if (GameInteractor::IsSaveLoaded() && IS_RANDO) { - const uint8_t shuffle = OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_SHUFFLE_SONGS); - return OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_SPLIT_OCARINA_SONGS) != 0 && - shuffle != RO_SONG_SHUFFLE_OFF && shuffle == RO_SONG_SHUFFLE_ANYWHERE; +static int SplitSongPartsCollected(uint32_t questBit) { + if (!ItemTrackerSplitSongsActive()) { + return -1; } - const int shuffleSongs = CVarGetInteger(CVAR_RANDOMIZER_SETTING("ShuffleSongs"), RO_SONG_SHUFFLE_SONG_LOCATIONS); - return CVarGetInteger(CVAR_RANDOMIZER_SETTING("SplitOcarinaSongs"), 0) != 0 && - shuffleSongs != RO_SONG_SHUFFLE_OFF && shuffleSongs == RO_SONG_SHUFFLE_ANYWHERE; -} - -static int ItemTrackerEffectiveSongPartsDisplay() { - if (!ItemTrackerSongPartsSeedActive()) { - return SECTION_DISPLAY_HIDDEN; - } - return CVarGetInteger(CVAR_TRACKER_ITEM("DisplayType.SongParts"), SECTION_DISPLAY_HIDDEN); -} - -static std::unordered_map BuildSongPartSpoilerAreas() { - std::unordered_map partRgToArea; - if (CVarGetInteger(CVAR_GENERAL("RandoGenerating"), 0) != 0) { - return partRgToArea; - } - if (!GameInteractor::IsSaveLoaded() || !IS_RANDO) { - return partRgToArea; + const Rando::SplitSongDef* def = SplitSongDefForQuestBit(questBit); + if (def == nullptr) { + return -1; } - const auto ctx = Rando::Context::GetInstance(); - if (!ctx || (!ctx->IsSeedGenerated() && !ctx->IsSpoilerLoaded())) { - return partRgToArea; - } - for (RandomizerCheck rc : ctx->allLocations) { - auto* loc = ctx->GetItemLocation(rc); - if (loc == nullptr) { - continue; - } - const RandomizerGet rg = loc->GetPlacedRandomizerGet(); - if (!Rando::SplitSongs::IsProgressiveSong(rg)) { - continue; - } - if (partRgToArea.find(rg) != partRgToArea.end()) { - continue; - } - auto* staticLoc = Rando::StaticData::GetLocation(rc); - if (staticLoc != nullptr) { - partRgToArea[rg] = staticLoc->GetArea(); - } + if (Rando::SplitSongs::HasFullSong(def->id)) { + return 2; } - std::unordered_map fullSongToArea; - for (const auto& kv : partRgToArea) { - const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromProgressive(kv.first); - if (def == nullptr) { - continue; - } - if (fullSongToArea.find(def->fullSong) == fullSongToArea.end()) { - fullSongToArea[def->fullSong] = kv.second; - } + if (Rando::SplitSongs::HasSplitPart(def->id)) { + return 1; } - return fullSongToArea; + return 0; } struct ItemTrackerNumbers { @@ -1466,71 +1412,32 @@ void DrawDungeonItem(ItemTrackerItem item) { } void DrawSong(ItemTrackerItem item) { - float iconSize = static_cast(CVarGetInteger(CVAR_TRACKER_ITEM("IconSize"), 36)); - ImVec2 p = ImGui::GetCursorScreenPos(); - bool hasSong = HasSong(item); - ImGui::SetCursorScreenPos(ImVec2(p.x + 6, p.y)); - ImGui::Image(std::dynamic_pointer_cast(Ship::Context::GetInstance()->GetWindow()->GetGui()) - ->GetTextureByName(hasSong && IsValidSaveFile() ? item.name : item.nameFaded), - ImVec2(iconSize / 1.5f, iconSize), ImVec2(0, 0), ImVec2(1, 1)); - Tooltip(SohUtils::GetQuestItemName(item.id).c_str()); -} + const int partsCollected = SplitSongPartsCollected(item.id); + const bool hasSong = HasSong(item) || partsCollected == 2; + const bool hasProgress = partsCollected == 1; + const bool showBright = hasSong || hasProgress; -void DrawSplitSongProgress(ItemTrackerItem item) { - const RandomizerGet fullSongRg = static_cast(item.id); - const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDefFromFullSong(fullSongRg); - int partsCollected = 0; - if (def != nullptr) { - if (Rando::SplitSongs::HasFullSong(def->id)) { - partsCollected = 2; - } else if (Rando::SplitSongs::HasSplitPart(def->id)) { - partsCollected = 1; - } - } - - const bool hasAnyPart = partsCollected > 0; float iconSize = static_cast(CVarGetInteger(CVAR_TRACKER_ITEM("IconSize"), 36)); ImGui::BeginGroup(); ImVec2 p = ImGui::GetCursorScreenPos(); ImGui::SetCursorScreenPos(ImVec2(p.x + 6, p.y)); ImGui::Image(std::dynamic_pointer_cast(Ship::Context::GetInstance()->GetWindow()->GetGui()) - ->GetTextureByName(hasAnyPart && IsValidSaveFile() ? item.name : item.nameFaded), + ->GetTextureByName(showBright && IsValidSaveFile() ? item.name : item.nameFaded), ImVec2(iconSize / 1.5f, iconSize), ImVec2(0, 0), ImVec2(1, 1)); - const RandomizerCheckArea area = static_cast(item.data); - ImU32 labelColor = IM_COL32(255, 255, 255, 255); - if (area != RCAREA_INVALID && GameInteractor::IsSaveLoaded() && IS_RANDO && CheckTracker::IsAreaSpoiled(area)) { - labelColor = IM_COL32(255, 255, 160, 255); - } - - char progressLabel[8]; - const bool showProgress = def != nullptr && partsCollected > 0; - if (showProgress) { - std::snprintf(progressLabel, sizeof(progressLabel), "%d/2", partsCollected); - } - const ImVec2 iconMin = ImGui::GetItemRectMin(); - const ImVec2 iconMax = ImGui::GetItemRectMax(); - if (showProgress) { + if (hasProgress) { + const char* progressLabel = "1/2"; + const ImVec2 iconMin = ImGui::GetItemRectMin(); + const ImVec2 iconMax = ImGui::GetItemRectMax(); const ImVec2 textSize = ImGui::CalcTextSize(progressLabel); const ImVec2 textPos(iconMin.x + ((iconMax.x - iconMin.x) - textSize.x) * 0.5f, iconMax.y - textSize.y - 2.0f); ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddText(ImVec2(textPos.x + 1.0f, textPos.y + 1.0f), IM_COL32(0, 0, 0, 220), progressLabel); - dl->AddText(textPos, labelColor, progressLabel); + dl->AddText(textPos, IM_COL32(255, 255, 255, 255), progressLabel); } ImGui::EndGroup(); - - std::string tip; - if (def != nullptr) { - tip = Rando::StaticData::RetrieveItem(fullSongRg).GetName().GetEnglish(); - if (area != RCAREA_INVALID && IS_RANDO) { - tip += "\n"; - tip += RandomizerCheckObjects::GetRCAreaName(area); - } - } - if (!tip.empty()) { - Tooltip(tip.c_str()); - } + Tooltip(SohUtils::GetQuestItemName(item.id).c_str()); } void DrawNotes(bool resizeable = false) { @@ -1755,21 +1662,6 @@ void UpdateVectors() { } } - songPartItems.clear(); - if (ItemTrackerSongPartsSeedActive()) { - EnsureSongPartItemsTemplate(); - const auto partAreas = BuildSongPartSpoilerAreas(); - songPartItems = songPartItemsTemplate; - for (auto& it : songPartItems) { - const RandomizerGet rg = static_cast(it.id); - const auto found = partAreas.find(rg); - it.data = - found != partAreas.end() ? static_cast(found->second) : static_cast(RCAREA_INVALID); - } - } - - const int songPartsDisplay = ItemTrackerEffectiveSongPartsDisplay(); - mainWindowItems.clear(); if (CVarGetInteger(CVAR_TRACKER_ITEM("DisplayType.Inventory"), SECTION_DISPLAY_MAIN_WINDOW) == SECTION_DISPLAY_MAIN_WINDOW) { @@ -1798,18 +1690,7 @@ void UpdateVectors() { mainWindowItems.push_back(ITEM_TRACKER_ITEM(ITEM_NONE, 0, DrawItem)); mainWindowItems.push_back(ITEM_TRACKER_ITEM(ITEM_NONE, 0, DrawItem)); } - const bool splitSongPartsReplaceSongsOnMain = ItemTrackerSongPartsSeedActive() && - songPartsDisplay == SECTION_DISPLAY_MAIN_WINDOW && - !songPartItems.empty(); - if (!splitSongPartsReplaceSongsOnMain) { - mainWindowItems.insert(mainWindowItems.end(), songItems.begin(), songItems.end()); - } - } - if (songPartsDisplay == SECTION_DISPLAY_MAIN_WINDOW && !songPartItems.empty()) { - while (mainWindowItems.size() % 6) { - mainWindowItems.push_back(ITEM_TRACKER_ITEM(ITEM_NONE, 0, DrawItem)); - } - mainWindowItems.insert(mainWindowItems.end(), songPartItems.begin(), songPartItems.end()); + mainWindowItems.insert(mainWindowItems.end(), songItems.begin(), songItems.end()); } if (CVarGetInteger(CVAR_TRACKER_ITEM("DisplayType.DungeonItems"), SECTION_DISPLAY_HIDDEN) == SECTION_DISPLAY_MAIN_WINDOW) { @@ -2009,8 +1890,6 @@ void ItemTrackerWindow::Draw() { void ItemTrackerWindow::DrawElement() { UpdateVectors(); - const int songPartsDisplayDraw = ItemTrackerEffectiveSongPartsDisplay(); - int iconSize = CVarGetInteger(CVAR_TRACKER_ITEM("IconSize"), 36); int iconSpacing = CVarGetInteger(CVAR_TRACKER_ITEM("IconSpacing"), 12); int comboButton1Mask = buttonMap[CVarGetInteger(CVAR_TRACKER_ITEM("ComboButton1"), TRACKER_COMBO_BUTTON_L)]; @@ -2037,7 +1916,6 @@ void ItemTrackerWindow::DrawElement() { SECTION_DISPLAY_MAIN_WINDOW) || (CVarGetInteger(CVAR_TRACKER_ITEM("DisplayType.Songs"), SECTION_DISPLAY_MAIN_WINDOW) == SECTION_DISPLAY_MAIN_WINDOW) || - (songPartsDisplayDraw == SECTION_DISPLAY_MAIN_WINDOW) || (CVarGetInteger(CVAR_TRACKER_ITEM("DisplayType.DungeonItems"), SECTION_DISPLAY_HIDDEN) == SECTION_DISPLAY_MAIN_WINDOW) || (CVarGetInteger(CVAR_TRACKER_ITEM("DisplayType.Greg"), SECTION_DISPLAY_EXTENDED_HIDDEN) == @@ -2102,12 +1980,6 @@ void ItemTrackerWindow::DrawElement() { EndFloatingWindows(); } - if (songPartsDisplayDraw == SECTION_DISPLAY_SEPARATE && !songPartItems.empty()) { - BeginFloatingWindows("Song Parts Tracker"); - DrawItemsInRows(songPartItems, 6); - EndFloatingWindows(); - } - if (CVarGetInteger(CVAR_TRACKER_ITEM("DisplayType.DungeonItems"), SECTION_DISPLAY_HIDDEN) == SECTION_DISPLAY_SEPARATE) { BeginFloatingWindows("Dungeon Items Tracker"); @@ -2379,7 +2251,6 @@ void ItemTrackerSettingsWindow::DrawElement() { SohGui::mSohMenu->MenuDrawItem(ocarinaButtonTracking, 250, THEME_COLOR); SohGui::mSohMenu->MenuDrawItem(overworldKeysTracking, 250, THEME_COLOR); SohGui::mSohMenu->MenuDrawItem(fishingPoleTracking, 250, THEME_COLOR); - SohGui::mSohMenu->MenuDrawItem(songPartsTracking, 250, THEME_COLOR); if (CVarCombobox("Total Checks", CVAR_TRACKER_ITEM("TotalChecks.DisplayType"), minimalDisplayTypes, ComboboxOptions() @@ -2576,18 +2447,6 @@ void RegisterItemTrackerWidgets() { SohGui::mSohMenu->AddSearchWidget( { fishingPoleTracking, "Randomizer", "Item Tracker", "General Settings", "icon" }); - songPartsTracking = { .name = "Song parts (split)", .type = WidgetType::WIDGET_CVAR_COMBOBOX }; - songPartsTracking.CVar(CVAR_TRACKER_ITEM("DisplayType.SongParts")) - .Options(ComboboxOptions() - .DefaultIndex(SECTION_DISPLAY_HIDDEN) - .ComponentAlignment(ComponentAlignments::Right) - .LabelPosition(LabelPositions::Far) - .Color(THEME_COLOR) - .ComboMap(displayTypes)) - .Callback([](WidgetInfo& info) { shouldUpdateVectors = true; }); - SohGui::mSohMenu->AddSearchWidget( - { songPartsTracking, "Randomizer", "Item Tracker", "General Settings", "split ocarina" }); - personalNotesWiget = { .name = "Personal notes", .type = WidgetType::WIDGET_CVAR_COMBOBOX }; static const char* notesDisabledTooltip = "Disabled because tracker is set to floating and display combo is enabled."; diff --git a/soh/soh/Enhancements/randomizer/settings.cpp b/soh/soh/Enhancements/randomizer/settings.cpp index 5587391bb9f..ca8a12361f6 100644 --- a/soh/soh/Enhancements/randomizer/settings.cpp +++ b/soh/soh/Enhancements/randomizer/settings.cpp @@ -2599,6 +2599,10 @@ void Context::FinalizeSettings(const std::set& excludedLocation mOptions[RSK_STARTING_NUTS].Set(false); } + if (mOptions[RSK_SHUFFLE_SONGS].IsNot(RO_SONG_SHUFFLE_ANYWHERE)) { + mOptions[RSK_SPLIT_OCARINA_SONGS].Set(0); + } + // RANDOTODO implement chest shuffle with keysanity // ShuffleChestMinigame.Set(cvarSettings[RSK_SHUFFLE_CHEST_MINIGAME]); mOptions[RSK_SHUFFLE_CHEST_MINIGAME].Set(RO_CHEST_GAME_OFF); 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 79847595d99..7ffe8af7f43 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -14135,10 +14135,6 @@ s32 func_8084DFF4(PlayState* play, Player* this) { } else { giEntry = this->getItemEntry; } - // SOH [Randomizer]: Message_OpenText / custom text hooks read player->getItemEntry. Without syncing, - // that struct can lag behind the resolved giEntry used here (same mismatch path as ItemTable_Retrieve). - this->getItemEntry = giEntry; - this->getItemId = giEntry.getItemId; this->av1.actionVar1 = 1; equipItem = giEntry.itemId; equipNow = CVarGetInteger(CVAR_ENHANCEMENT("AskToEquip"), 0) && giEntry.modIndex == MOD_NONE && From 39427cc778f8857fa1f80f307ee19c08619fb695 Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:42:21 +0100 Subject: [PATCH 18/22] Fix seed gen crash on progressive songs and clean up split songs menu text. Stop GetGIEntry recursing when the resolved stage is still the progressive item. Reword the option tooltip like Bombchu Bag, without bracketed asides. --- soh/soh/Enhancements/randomizer/item.cpp | 3 +++ soh/soh/Enhancements/randomizer/option_descriptions.cpp | 5 +++-- soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp | 3 +++ soh/soh/Enhancements/randomizer/settings.cpp | 6 +++--- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/item.cpp b/soh/soh/Enhancements/randomizer/item.cpp index fdf09b64987..e800be2755f 100644 --- a/soh/soh/Enhancements/randomizer/item.cpp +++ b/soh/soh/Enhancements/randomizer/item.cpp @@ -415,6 +415,9 @@ std::shared_ptr Item::GetGIEntry() const { // NOLINT(*-no-recursio if (giEntry != nullptr && actual == RG_NONE) { return giEntry; } + if (actual == randomizerGet && giEntry != nullptr) { + return giEntry; + } return StaticData::RetrieveItem(actual).GetGIEntry(); } diff --git a/soh/soh/Enhancements/randomizer/option_descriptions.cpp b/soh/soh/Enhancements/randomizer/option_descriptions.cpp index 89301407112..c45488a3c85 100644 --- a/soh/soh/Enhancements/randomizer/option_descriptions.cpp +++ b/soh/soh/Enhancements/randomizer/option_descriptions.cpp @@ -218,8 +218,9 @@ void Settings::CreateOptionDescriptions() { "\n" "Anywhere - Songs can appear at any location."; mOptionDescriptions[RSK_SPLIT_OCARINA_SONGS] = - "Each ocarina song becomes a progressive song item (like bomb bag upgrades). The first pickup marks " - "progress; the second grants the full song. Requires Shuffle Songs: Anywhere."; + "Each ocarina song is shuffled as a progressive item, similar to Bombchu Bag upgrades. The first pickup " + "marks progress for that song. The second pickup grants the full song.\n\n" + "Requires Shuffle Songs set to Anywhere."; mOptionDescriptions[RSK_SHUFFLE_TOKENS] = "Shuffles Golden Skulltula Tokens into the item pool. This means " "Golden Skulltulas can contain other items as well.\n" "\n" diff --git a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp index beebb867673..7673fbcae65 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp @@ -481,6 +481,9 @@ static const Rando::SplitSongDef* SplitSongDefForQuestBit(uint32_t questBit) { } static int SplitSongPartsCollected(uint32_t questBit) { + if (CVarGetInteger(CVAR_GENERAL("RandoGenerating"), 0) != 0) { + return -1; + } if (!ItemTrackerSplitSongsActive()) { return -1; } diff --git a/soh/soh/Enhancements/randomizer/settings.cpp b/soh/soh/Enhancements/randomizer/settings.cpp index ca8a12361f6..4a12ea0a3d1 100644 --- a/soh/soh/Enhancements/randomizer/settings.cpp +++ b/soh/soh/Enhancements/randomizer/settings.cpp @@ -614,11 +614,11 @@ void Settings::CreateOptions() { mOptions[RSK_SPLIT_OCARINA_SONGS].Enable(); } else if (shuffleSongs != RO_SONG_SHUFFLE_OFF) { mOptions[RSK_SPLIT_OCARINA_SONGS].Disable( - "Split Ocarina Songs is only supported when Shuffle Songs is \"Anywhere\" (other modes keep 12 song " - "checks)."); + "Split Ocarina Songs only works when Shuffle Songs is set to Anywhere. Other shuffle modes keep the " + "usual 12 song checks."); } else { mOptions[RSK_SPLIT_OCARINA_SONGS].Disable( - "Turn on Shuffle Songs to use Split Ocarina Songs (then set Shuffle Songs to \"Anywhere\")."); + "Turn on Shuffle Songs and set it to Anywhere to use Split Ocarina Songs."); } }); OPT_BOOL(RSK_SPLIT_OCARINA_SONGS, "Split Ocarina Songs", CVAR_RANDOMIZER_SETTING("SplitOcarinaSongs"), From b78aac9e18a569c25290a2dd8a5e2dbf972f5a6f Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Mon, 15 Jun 2026 19:50:21 +0100 Subject: [PATCH 19/22] Fix split song pickup icons and stabilize seed/save generation Hook song textbox icons through ItemMessages and the existing custom icon VB hooks, with correct IA8 16x24 gSongNoteTex drawing and warp song colours from the pause quest screen. Small z_message guard so the note is not stretched into a 24x24 quad. Sanitize UTF-8 in spoiler log, hints, and save JSON to fix regen and new-game crashes. Mark seed ready after spoiler write. Second progressive pickup grants the full song through normal item give. --- .../randomizer/3drando/playthrough.cpp | 3 + .../randomizer/3drando/spoiler_log.cpp | 166 +++++++------ .../randomizer/Messages/ItemMessages.cpp | 235 +++++++++++------- soh/soh/Enhancements/randomizer/Traps.cpp | 86 ++++--- soh/soh/Enhancements/randomizer/hint.cpp | 43 ++-- soh/soh/Enhancements/randomizer/item.cpp | 33 ++- soh/soh/Enhancements/randomizer/item_list.cpp | 24 +- soh/soh/Enhancements/randomizer/logic.cpp | 2 +- .../randomizer/option_descriptions.cpp | 4 +- .../Enhancements/randomizer/randomizer.cpp | 4 + .../Enhancements/randomizer/split_songs.cpp | 28 ++- soh/soh/Enhancements/randomizer/split_songs.h | 1 + soh/soh/SaveManager.cpp | 141 ++++++----- soh/soh/SaveManager.h | 2 + soh/soh/util.cpp | 53 ++++ soh/soh/util.h | 3 + soh/src/code/z_message_PAL.c | 18 +- 17 files changed, 538 insertions(+), 308 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/3drando/playthrough.cpp b/soh/soh/Enhancements/randomizer/3drando/playthrough.cpp index 2e14f2fe898..e18bb70880f 100644 --- a/soh/soh/Enhancements/randomizer/3drando/playthrough.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/playthrough.cpp @@ -82,6 +82,9 @@ int Playthrough_Init(uint32_t seed, std::set excludedLocations, SPDLOG_INFO("Writing Spoiler Log Done"); } + // Seed is only considered ready after spoiler JSON is fully written. + ctx->SetSeedGenerated(true); + ctx->playthroughLocations.clear(); ctx->playthroughBeatable = false; diff --git a/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp b/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp index 5e1b84ec22a..c1fe622c6fe 100644 --- a/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp @@ -24,8 +24,10 @@ #include #include +#include #include +#include using json = nlohmann::ordered_json; using namespace Rando; @@ -40,6 +42,10 @@ namespace { std::string placementtxt; } // namespace +static std::string SafeJsonString(std::string value) { + return SohUtils::SanitizeUtf8(std::move(value)); +} + void GenerateHash() { auto ctx = Rando::Context::GetInstance(); std::string hash = ctx->GetHash(); @@ -73,13 +79,16 @@ static void WriteLocation(std::string sphere, const RandomizerCheck locationKey, switch (gSaveContext.language) { case LANGUAGE_ENG: default: - jsonData["playthrough"][sphere][location->GetName()] = itemLocation->GetPlacedItemName().GetEnglish(); + jsonData["playthrough"][sphere][location->GetName()] = + SafeJsonString(itemLocation->GetPlacedItemName().GetEnglish()); break; case LANGUAGE_GER: - jsonData["playthrough"][sphere][location->GetName()] = itemLocation->GetPlacedItemName().GetGerman(); + jsonData["playthrough"][sphere][location->GetName()] = + SafeJsonString(itemLocation->GetPlacedItemName().GetGerman()); break; case LANGUAGE_FRA: - jsonData["playthrough"][sphere][location->GetName()] = itemLocation->GetPlacedItemName().GetFrench(); + jsonData["playthrough"][sphere][location->GetName()] = + SafeJsonString(itemLocation->GetPlacedItemName().GetFrench()); break; } } @@ -130,7 +139,7 @@ static void WriteShuffledEntrance(std::string sphereString, Entrance* entrance) case LANGUAGE_GER: case LANGUAGE_FRA: default: - jsonData["entrancesMap"][sphereString][name] = text; + jsonData["entrancesMap"][sphereString][name] = SafeJsonString(text); break; } } @@ -141,7 +150,8 @@ static void WriteSettings() { std::array options = Rando::Settings::GetInstance()->GetAllOptions(); for (const Rando::Option& option : options) { if (option.GetName() != "") { - jsonData["settings"][option.GetName()] = option.GetOptionText(ctx->GetOption(option.GetKey()).Get()); + jsonData["settings"][option.GetName()] = + SafeJsonString(option.GetOptionText(ctx->GetOption(option.GetKey()).Get())); } } } @@ -162,7 +172,7 @@ static void WriteExcludedLocations() { continue; } - jsonData["excludedLocations"].push_back(RemoveLineBreaks(location->GetName())); + jsonData["excludedLocations"].push_back(SafeJsonString(RemoveLineBreaks(location->GetName()))); } } } @@ -174,7 +184,8 @@ static void WriteStartingInventory() { for (const Rando::OptionGroup* subGroup : optionGroup.GetSubGroups()) { if (subGroup->GetContainsType() == Rando::OptionGroupType::DEFAULT) { for (Rando::Option* option : subGroup->GetOptions()) { - jsonData["settings"][option->GetName()] = option->GetOptionText(ctx->GetOption(option->GetKey()).Get()); + jsonData["settings"][option->GetName()] = + SafeJsonString(option->GetOptionText(ctx->GetOption(option->GetKey()).Get())); } } } @@ -188,7 +199,7 @@ static void WriteEnabledTricks() { if (ctx->GetTrickOption(static_cast(setting->GetKey())).IsNot(RO_GENERIC_ON)) { continue; } - jsonData["enabledTricks"].push_back(RemoveLineBreaks(setting->GetName()).c_str()); + jsonData["enabledTricks"].push_back(SafeJsonString(RemoveLineBreaks(setting->GetName()))); } } @@ -200,7 +211,7 @@ static void WriteMasterQuestDungeons() { if (dungeon->IsVanilla()) { continue; } - jsonData["masterQuestDungeons"].push_back(dungeon->GetName()); + jsonData["masterQuestDungeons"].push_back(SafeJsonString(dungeon->GetName())); } } @@ -210,7 +221,7 @@ static void WriteChosenOptions() { for (const auto& trial : ctx->GetTrials()->GetTrialList()) { if (trial->IsRequired()) { std::string trialName = trial->GetName().GetForCurrentLanguage(MF_CLEAN); - jsonData["requiredTrials"].push_back(RemoveLineBreaks(trialName)); + jsonData["requiredTrials"].push_back(SafeJsonString(RemoveLineBreaks(trialName))); } } if (ctx->GetOption(RSK_SELECTED_STARTING_AGE).Is(RO_AGE_ADULT)) { @@ -276,13 +287,13 @@ static void WriteAllLocations() { if (!location->HasCustomPrice() && location->GetPlacedRandomizerGet() != RG_ICE_TRAP) { jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()] = - placedItemName; + SafeJsonString(placedItemName); continue; } // We're dealing with a complex item, build out the json object for it jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()]["item"] = - placedItemName; + SafeJsonString(placedItemName); if (location->HasCustomPrice()) { jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()]["price"] = @@ -297,30 +308,33 @@ static void WriteAllLocations() { case 0: default: jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()] - ["model"] = Rando::StaticData::RetrieveItem( - ctx->overrides[location->GetRandomizerCheck()].LooksLike()) - .GetName() - .GetEnglish(); + ["model"] = SafeJsonString(Rando::StaticData::RetrieveItem( + ctx->overrides[location->GetRandomizerCheck()].LooksLike()) + .GetName() + .GetEnglish()); jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()] - ["trickName"] = ctx->overrides[location->GetRandomizerCheck()].GetTrickName().GetEnglish(); + ["trickName"] = SafeJsonString( + ctx->overrides[location->GetRandomizerCheck()].GetTrickName().GetEnglish()); break; case 1: jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()] - ["model"] = Rando::StaticData::RetrieveItem( - ctx->overrides[location->GetRandomizerCheck()].LooksLike()) - .GetName() - .GetGerman(); + ["model"] = SafeJsonString(Rando::StaticData::RetrieveItem( + ctx->overrides[location->GetRandomizerCheck()].LooksLike()) + .GetName() + .GetGerman()); jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()] - ["trickName"] = ctx->overrides[location->GetRandomizerCheck()].GetTrickName().GetGerman(); + ["trickName"] = SafeJsonString( + ctx->overrides[location->GetRandomizerCheck()].GetTrickName().GetGerman()); break; case 2: jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()] - ["model"] = Rando::StaticData::RetrieveItem( - ctx->overrides[location->GetRandomizerCheck()].LooksLike()) - .GetName() - .GetFrench(); + ["model"] = SafeJsonString(Rando::StaticData::RetrieveItem( + ctx->overrides[location->GetRandomizerCheck()].LooksLike()) + .GetName() + .GetFrench()); jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()] - ["trickName"] = ctx->overrides[location->GetRandomizerCheck()].GetTrickName().GetFrench(); + ["trickName"] = SafeJsonString( + ctx->overrides[location->GetRandomizerCheck()].GetTrickName().GetFrench()); break; } } @@ -330,59 +344,65 @@ static void WriteAllLocations() { void SpoilerLog_Write() { auto ctx = Rando::Context::GetInstance(); - jsonData.clear(); - - jsonData["version"] = (char*)gBuildVersion; - jsonData["fileType"] = FILE_TYPE_SPOILER; - jsonData["git_branch"] = (char*)gGitBranch; - jsonData["git_commit"] = (char*)gGitCommitHash; - jsonData["seed"] = ctx->GetSeedString(); - jsonData["finalSeed"] = ctx->GetSeed(); - - // Write Hash - int index = 0; - for (uint8_t seed_value : ctx->hashIconIndexes) { - jsonData["file_hash"][index] = seed_value; - index++; - } - - WriteSettings(); - WriteExcludedLocations(); - WriteStartingInventory(); - WriteEnabledTricks(); - WriteMasterQuestDungeons(); - WriteChosenOptions(); - WritePlaythrough(); + try { + jsonData.clear(); + hintedLocations.clear(); + + jsonData["version"] = (char*)gBuildVersion; + jsonData["fileType"] = FILE_TYPE_SPOILER; + jsonData["git_branch"] = (char*)gGitBranch; + jsonData["git_commit"] = (char*)gGitCommitHash; + jsonData["seed"] = SafeJsonString(ctx->GetSeedString()); + jsonData["finalSeed"] = ctx->GetSeed(); + + // Write Hash + int index = 0; + for (uint8_t seed_value : ctx->hashIconIndexes) { + jsonData["file_hash"][index] = seed_value; + index++; + } - ctx->playthroughLocations.clear(); - ctx->playthroughBeatable = false; + WriteSettings(); + WriteExcludedLocations(); + WriteStartingInventory(); + WriteEnabledTricks(); + WriteMasterQuestDungeons(); + WriteChosenOptions(); + WritePlaythrough(); - ctx->WriteHintJson(jsonData); - WriteShuffledEntrances(); - WriteAllLocations(); + ctx->playthroughLocations.clear(); + ctx->playthroughBeatable = false; - if (!std::filesystem::exists(Ship::Context::GetPathRelativeToAppDirectory("Randomizer"))) { - std::filesystem::create_directory(Ship::Context::GetPathRelativeToAppDirectory("Randomizer")); - } + ctx->WriteHintJson(jsonData); + WriteShuffledEntrances(); + WriteAllLocations(); - std::string jsonString = jsonData.dump(4); - std::ostringstream fileNameStream; - for (uint8_t i = 0; i < ctx->hashIconIndexes.size(); i++) { - if (i) { - fileNameStream << '-'; + if (!std::filesystem::exists(Ship::Context::GetPathRelativeToAppDirectory("Randomizer"))) { + std::filesystem::create_directory(Ship::Context::GetPathRelativeToAppDirectory("Randomizer")); } - if (ctx->hashIconIndexes[i] < 10) { - fileNameStream << '0'; + + std::string jsonString = jsonData.dump(4); + std::ostringstream fileNameStream; + for (uint8_t i = 0; i < ctx->hashIconIndexes.size(); i++) { + if (i) { + fileNameStream << '-'; + } + if (ctx->hashIconIndexes[i] < 10) { + fileNameStream << '0'; + } + fileNameStream << std::to_string(ctx->hashIconIndexes[i]); } - fileNameStream << std::to_string(ctx->hashIconIndexes[i]); + std::string fileName = fileNameStream.str(); + std::ofstream jsonFile(Ship::Context::GetPathRelativeToAppDirectory( + (std::string("Randomizer/") + fileName + std::string(".json")).c_str())); + jsonFile << std::setw(4) << jsonString << std::endl; + jsonFile.close(); + + CVarSetString(CVAR_GENERAL("SpoilerLog"), + (std::string("./Randomizer/") + fileName + std::string(".json")).c_str()); + } catch (const std::exception& e) { + SPDLOG_ERROR("SpoilerLog_Write failed: {}", e.what()); } - std::string fileName = fileNameStream.str(); - std::ofstream jsonFile(Ship::Context::GetPathRelativeToAppDirectory( - (std::string("Randomizer/") + fileName + std::string(".json")).c_str())); - jsonFile << std::setw(4) << jsonString << std::endl; - jsonFile.close(); - - CVarSetString(CVAR_GENERAL("SpoilerLog"), (std::string("./Randomizer/") + fileName + std::string(".json")).c_str()); } void PlacementLog_Msg(std::string_view msg) { diff --git a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp index 7832d16ec4d..2e207e8436e 100644 --- a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp @@ -16,86 +16,131 @@ #include "soh/ShipInit.hpp" #include -#include +#include extern "C" { #include #include #include "z64item.h" #include "z64player.h" +#include "textures/icon_item_static/icon_item_static.h" extern PlayState* gPlayState; GetItemEntry ItemTable_Retrieve(int16_t getItemID); GetItemEntry ItemTable_RetrieveEntry(s16 modIndex, s16 getItemID); } -static void SyncPlayerGetItemEntryForMessage(Player* player) { - if (player == nullptr) { - return; - } - if (player->getItemEntry.objectId != OBJECT_INVALID && player->getItemId == player->getItemEntry.getItemId) { - return; +// Progressive split songs use TEXT_RANDOMIZER_CUSTOM_ITEM with gSongNoteTex (16x24 IA8). +static bool sTextboxSongNoteIcon = false; +static u8 sTextboxSongNotePrimR = 255; +static u8 sTextboxSongNotePrimG = 255; +static u8 sTextboxSongNotePrimB = 255; + +extern "C" bool Rando_TextboxSongNoteIconActive(void) { + return sTextboxSongNoteIcon; +} + +static bool IsSongNoteTexPath(const char* path) { + return path != nullptr && strstr(path, "gSongNoteTex") != nullptr; +} + +static const Rando::SplitSongDef* GetActiveSplitSongDef(Player* player) { + if (!IS_RANDO || player->getItemEntry.modIndex != MOD_RANDOMIZER) { + return nullptr; } - GetItemEntry giEntry; - if (IS_RANDO && player->getItemId > RG_NONE && player->getItemId < RG_MAX) { - giEntry = ItemTable_RetrieveEntry(MOD_RANDOMIZER, player->getItemId); - } else { - giEntry = ItemTable_Retrieve(player->getItemId); + + const RandomizerGet rgEnum = static_cast(player->getItemEntry.getItemId); + if (Rando::SplitSongs::IsProgressiveSong(rgEnum)) { + return Rando::SplitSongs::GetSongDefFromProgressive(rgEnum); } - player->getItemEntry = giEntry; - player->getItemId = giEntry.getItemId; + return Rando::SplitSongs::GetSongDefFromFullSong(rgEnum); } -static ItemID SongIconForRandomizerGet(RandomizerGet rg) { - switch (rg) { - case RG_ZELDAS_LULLABY: - case RG_PROGRESSIVE_ZELDAS_LULLABY: - return ITEM_SONG_LULLABY; - case RG_EPONAS_SONG: - case RG_PROGRESSIVE_EPONAS_SONG: - return ITEM_SONG_EPONA; - case RG_SARIAS_SONG: - case RG_PROGRESSIVE_SARIAS_SONG: - return ITEM_SONG_SARIA; - case RG_SUNS_SONG: - case RG_PROGRESSIVE_SUNS_SONG: - return ITEM_SONG_SUN; - case RG_SONG_OF_TIME: - case RG_PROGRESSIVE_SONG_OF_TIME: - return ITEM_SONG_TIME; - case RG_SONG_OF_STORMS: - case RG_PROGRESSIVE_SONG_OF_STORMS: - return ITEM_SONG_STORMS; - case RG_MINUET_OF_FOREST: - case RG_PROGRESSIVE_MINUET_OF_FOREST: - return ITEM_SONG_MINUET; - case RG_BOLERO_OF_FIRE: - case RG_PROGRESSIVE_BOLERO_OF_FIRE: - return ITEM_SONG_BOLERO; - case RG_SERENADE_OF_WATER: - case RG_PROGRESSIVE_SERENADE_OF_WATER: - return ITEM_SONG_SERENADE; - case RG_REQUIEM_OF_SPIRIT: - case RG_PROGRESSIVE_REQUIEM_OF_SPIRIT: - return ITEM_SONG_REQUIEM; - case RG_NOCTURNE_OF_SHADOW: - case RG_PROGRESSIVE_NOCTURNE_OF_SHADOW: - return ITEM_SONG_NOCTURNE; - case RG_PRELUDE_OF_LIGHT: - case RG_PROGRESSIVE_PRELUDE_OF_LIGHT: - return ITEM_SONG_PRELUDE; +static int GetKaleidoSongNoteColorIndex(Rando::SplitSongId id) { + switch (id) { + case Rando::SPLIT_SONG_MINUET_OF_FOREST: + return 0; + case Rando::SPLIT_SONG_BOLERO_OF_FIRE: + return 1; + case Rando::SPLIT_SONG_SERENADE_OF_WATER: + return 2; + case Rando::SPLIT_SONG_REQUIEM_OF_SPIRIT: + return 3; + case Rando::SPLIT_SONG_NOCTURNE_OF_SHADOW: + return 4; + case Rando::SPLIT_SONG_PRELUDE_OF_LIGHT: + return 5; + case Rando::SPLIT_SONG_ZELDAS_LULLABY: + return 6; + case Rando::SPLIT_SONG_EPONAS_SONG: + return 7; + case Rando::SPLIT_SONG_SARIAS_SONG: + return 8; + case Rando::SPLIT_SONG_SUNS_SONG: + return 9; + case Rando::SPLIT_SONG_SONG_OF_TIME: + return 10; + case Rando::SPLIT_SONG_SONG_OF_STORMS: + return 11; default: - return ITEM_NONE; + return -1; } } -static RandomizerGet ResolveRandoGetFromPlayer(const Player* player) { - if (player->getItemEntry.objectId != OBJECT_INVALID && player->getItemEntry.modIndex == MOD_RANDOMIZER) { - return static_cast(player->getItemEntry.getItemId); +// Quest-status song note tints (z_kaleido_collect.c D_8082A164/17C/194), indexed by questItem - QUEST_SONG_MINUET. +static void SetTextboxSongNotePrimColor(const Rando::SplitSongDef* def) { + static constexpr s16 kR[] = { 150, 255, 100, 255, 255, 255, 255, 255, 255, 255, 255, 255 }; + static constexpr s16 kG[] = { 255, 80, 150, 160, 100, 240, 255, 255, 255, 255, 255, 255 }; + static constexpr s16 kB[] = { 100, 40, 255, 0, 255, 100, 255, 255, 255, 255, 255, 255 }; + + const int idx = GetKaleidoSongNoteColorIndex(def->id); + if (idx < 0) { + sTextboxSongNotePrimR = sTextboxSongNotePrimG = sTextboxSongNotePrimB = 255; + return; } - if (IS_RANDO && player->getItemId > RG_NONE && player->getItemId < RG_MAX) { - return static_cast(player->getItemId); + + sTextboxSongNotePrimR = static_cast(kR[idx]); + sTextboxSongNotePrimG = static_cast(kG[idx]); + sTextboxSongNotePrimB = static_cast(kB[idx]); +} + +static const char* GetSplitSongTextboxIconPath(Player* player) { + const Rando::SplitSongDef* def = GetActiveSplitSongDef(player); + if (def == nullptr) { + return nullptr; } - return static_cast(player->getItemId); + + const ItemID songItemId = static_cast(Rando::StaticData::RetrieveItem(def->fullSong).GetItemID()); + return static_cast(gItemIcons[songItemId]); +} + +static bool TryBuildSongItemMessage(Player* player, CustomMessage& msg) { + if (!IS_RANDO || player->getItemEntry.modIndex != MOD_RANDOMIZER) { + return false; + } + + const RandomizerGet rgEnum = static_cast(player->getItemEntry.getItemId); + const Rando::SplitSongDef* def = nullptr; + if (Rando::SplitSongs::IsProgressiveSong(rgEnum)) { + def = Rando::SplitSongs::GetSongDefFromProgressive(rgEnum); + } else { + def = Rando::SplitSongs::GetSongDefFromFullSong(rgEnum); + } + if (def == nullptr) { + return false; + } + + RandomizerGet nameRg = def->fullSong; + if (Rando::SplitSongs::IsProgressiveSong(rgEnum) && !Rando::SplitSongs::HasFullSong(def->id) && + !Rando::SplitSongs::HasSplitPart(def->id)) { + nameRg = def->progressive; + } + + const auto& nm = Rando::StaticData::RetrieveItem(nameRg).GetName(); + CustomMessage getItemText(nm.GetEnglish(), nm.GetGerman(), nm.GetFrench(), TEXTBOX_TYPE_BLUE, TEXTBOX_POS_BOTTOM); + // ITEM_CUSTOM routes through LoadCustomItemIcon; song note uses IA8 16x24 (not vanilla ITEM_SONG_* 32x32 RGBA). + getItemText.AutoFormat(ITEM_CUSTOM); + msg = getItemText; + return true; } void BuildTriforcePieceMessage(CustomMessage& msg) { @@ -144,16 +189,7 @@ void BuildTriforcePieceMessage(CustomMessage& msg) { } void BuildCustomItemMessage(Player* player, CustomMessage& msg) { - const RandomizerGet rgEnum = ResolveRandoGetFromPlayer(player); - - if (IS_RANDO && Rando::SplitSongs::IsProgressiveSong(rgEnum)) { - const RandomizerGet stage = Rando::SplitSongs::ResolveProgressiveSongStage(rgEnum); - const ItemID songIcon = SongIconForRandomizerGet(stage != RG_NONE ? stage : rgEnum); - const auto& nm = Rando::StaticData::RetrieveItem(stage != RG_NONE ? stage : rgEnum).GetName(); - CustomMessage getItemText(nm.GetEnglish(), nm.GetGerman(), nm.GetFrench(), TEXTBOX_TYPE_BLUE, - TEXTBOX_POS_BOTTOM); - getItemText.Format(songIcon); - msg = getItemText; + if (TryBuildSongItemMessage(player, msg)) { return; } @@ -184,17 +220,37 @@ void LoadCustomItemIcon(bool displayAsEnglish) { Player* player = GET_PLAYER(gPlayState); const char* customIcon = nullptr; CustomIconSize iconSize = ICON_SIZE_32; + sTextboxSongNoteIcon = false; + if (player->getItemEntry.objectId != OBJECT_INVALID) { RandomizerGet rgid = static_cast(player->getItemEntry.getItemId); customIcon = Rando::StaticData::RetrieveItem(rgid).GetCustomIcon(); iconSize = Rando::StaticData::RetrieveItem(rgid).GetCustomIconSize(); } + if (customIcon == nullptr) { + customIcon = GetSplitSongTextboxIconPath(player); + if (customIcon != nullptr) { + iconSize = ICON_SIZE_24; + } + } + if (customIcon != nullptr) { static int16_t sIconItem32XOffsets[] = { 74, 74, 74, 54 }; static int16_t sIconItem24XOffsets[] = { 72, 72, 72, 50 }; MessageContext* msgCtx = &gPlayState->msgCtx; uint8_t language = displayAsEnglish ? LANGUAGE_ENG : gSaveContext.language; - if (iconSize == ICON_SIZE_32) { + if (IsSongNoteTexPath(customIcon)) { + sTextboxSongNoteIcon = true; + const Rando::SplitSongDef* def = GetActiveSplitSongDef(player); + if (def != nullptr) { + SetTextboxSongNotePrimColor(def); + } else { + sTextboxSongNotePrimR = sTextboxSongNotePrimG = sTextboxSongNotePrimB = 255; + } + R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem24XOffsets[language]; + R_TEXTBOX_ICON_YPOS = (R_TEXTBOX_Y + 10) + 10; + R_TEXTBOX_ICON_SIZE = 24; + } else if (iconSize == ICON_SIZE_32) { R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem32XOffsets[language]; R_TEXTBOX_ICON_YPOS = (R_TEXTBOX_Y + 10) + 6; R_TEXTBOX_ICON_SIZE = 32; @@ -212,27 +268,40 @@ void LoadCustomItemIcon(bool displayAsEnglish) { void DrawCustomItemIcon(Gfx** p) { Gfx* gfx = *p; MessageContext* msgCtx = &gPlayState->msgCtx; - Player* player = GET_PLAYER(gPlayState); - CustomIconSize iconSize = ICON_SIZE_32; - if (player->getItemEntry.objectId != OBJECT_INVALID) { - RandomizerGet rgid = static_cast(player->getItemEntry.getItemId); - iconSize = Rando::StaticData::RetrieveItem(rgid).GetCustomIconSize(); - } - if (iconSize == ICON_SIZE_24) { - gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, - G_IM_SIZ_32b, 24, 24, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, + if (sTextboxSongNoteIcon) { + gDPSetPrimColor(gfx++, 0, 0, sTextboxSongNotePrimR, sTextboxSongNotePrimG, sTextboxSongNotePrimB, + msgCtx->textColorAlpha); + gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_IA, + G_IM_SIZ_8b, 16, 24, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); + gSPTextureRectangle(gfx++, (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS) << 2, R_TEXTBOX_ICON_YPOS << 2, + (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS + 16) << 2, (R_TEXTBOX_ICON_YPOS + 24) << 2, + G_TX_RENDERTILE, 0, 0, 1 << 10, 1 << 10); + gDPPipeSync(gfx++); + gDPSetCombineLERP(gfx++, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, + 0); } else { - gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, - G_IM_SIZ_32b, 32, 32, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, - G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); + Player* player = GET_PLAYER(gPlayState); + CustomIconSize iconSize = ICON_SIZE_32; + if (player->getItemEntry.objectId != OBJECT_INVALID) { + RandomizerGet rgid = static_cast(player->getItemEntry.getItemId); + iconSize = Rando::StaticData::RetrieveItem(rgid).GetCustomIconSize(); + } + if (iconSize == ICON_SIZE_24) { + gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, + G_IM_SIZ_32b, 24, 24, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, + G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); + } else { + gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, + G_IM_SIZ_32b, 32, 32, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, + G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); + } } *p = gfx; } void BuildItemMessage(u16* textId, bool* loadFromMessageTable) { Player* player = GET_PLAYER(gPlayState); - SyncPlayerGetItemEntryForMessage(player); CustomMessage msg; if (player->getItemEntry.getItemId == RG_ICE_TRAP) { diff --git a/soh/soh/Enhancements/randomizer/Traps.cpp b/soh/soh/Enhancements/randomizer/Traps.cpp index 40b31e32ecc..b405352cc81 100644 --- a/soh/soh/Enhancements/randomizer/Traps.cpp +++ b/soh/soh/Enhancements/randomizer/Traps.cpp @@ -4,6 +4,7 @@ #include "soh/Enhancements/randomizer/static_data.h" #include "soh/ShipUtils.h" +#include #include static std::array, RG_MAX> trickNameTable; // Table of trick names for ice traps @@ -61,17 +62,17 @@ static void InitTrickNames() { Text{ "Blue Mail", "Habits Bleus", "Blaues Gewand" }, // "Ropas azules" }; trickNameTable[RG_IRON_BOOTS] = { - Text{ "Iron Hoofs", "Patins de Plomb", "Eisenhufe€" }, // "Botas férreas" - Text{ "Snow Boots", "Bottes de Neige", "Schneestiefel€" }, // "Botas de nieve" - Text{ "Red Boots", "Bottes rouges", "Rote Stiefel€" }, // "Botas rojas" + Text{ "Iron Hoofs", "Patins de Plomb", "Eisenhufe" }, // "Botas férreas" + Text{ "Snow Boots", "Bottes de Neige", "Schneestiefel" }, // "Botas de nieve" + Text{ "Red Boots", "Bottes rouges", "Rote Stiefel" }, // "Botas rojas" Text{ "Zora Greaves", "Bottes Zora", "Zora-Beinschutz" }, // "Zora Greaves" - Text{ "Boots of Power", "Bottes de Puissance", "Stärkestiefel€" }, // "Botas de plomo" + Text{ "Boots of Power", "Bottes de Puissance", "Stärkestiefel" }, // "Botas de plomo" }; trickNameTable[RG_HOVER_BOOTS] = { - Text{ "Hover Hoofs", "Patins des airs", "Gleithufe€" }, // "Botas flotadoras" - Text{ "Golden Boots", "Bottes dorées", "Goldstiefel€" }, // "Botas de Oro" - Text{ "Pegasus Boots", "Bottes pégase", "Pegasus-Stiefel€" }, // "Botas de Pegaso" - Text{ "Boots of Speed", "Bottes de vitesse", "Tempostiefel€" }, // "Botas del desierto" + Text{ "Hover Hoofs", "Patins des airs", "Gleithufe" }, // "Botas flotadoras" + Text{ "Golden Boots", "Bottes dorées", "Goldstiefel" }, // "Botas de Oro" + Text{ "Pegasus Boots", "Bottes pégase", "Pegasus-Stiefel" }, // "Botas de Pegaso" + Text{ "Boots of Speed", "Bottes de vitesse", "Tempostiefel" }, // "Botas del desierto" }; trickNameTable[RG_WEIRD_EGG] = { Text{ "Poached Egg", "Oeuf à la coque", "Spiegelei" }, // "Huevo pasado" @@ -132,19 +133,19 @@ static void InitTrickNames() { }; trickNameTable[RG_FIRE_ARROWS] = { Text{ "Fire Rod", "Baguette de feu", "Feuerstab" }, // "Cetro de fuego" - Text{ "Bomb Arrow", "Flèche-Bombe", "Bomben-Pfeile€" }, // "Flecha bomba" + Text{ "Bomb Arrow", "Flèche-Bombe", "Bomben-Pfeile" }, // "Flecha bomba" Text{ "Red Candle", "Bougie Rouge", "Rote Kerze" }, // "Vela roja" }; trickNameTable[RG_ICE_ARROWS] = { Text{ "Ice Rod", "Baguette des Glaces", "Eisstab" }, // "Cetro de Hielo" - Text{ "Ancient Arrow", "Flèche Archéonique", "Antike Pfeile€" }, // "Flecha ancestral" - Text{ "Ice Trap Arrow", "Flèche de Piège de Glace", "Eisfallen-Pfeile€" }, // "Cetro de hielo" + Text{ "Ancient Arrow", "Flèche Archéonique", "Antike Pfeile" }, // "Flecha ancestral" + Text{ "Ice Trap Arrow", "Flèche de Piège de Glace", "Eisfallen-Pfeile" }, // "Cetro de hielo" }; trickNameTable[RG_LIGHT_ARROWS] = { - Text{ "Wind Arrow", "Flèche de Vent", "Wind-Pfeile€" }, // "Flecha del Viento" + Text{ "Wind Arrow", "Flèche de Vent", "Wind-Pfeile" }, // "Flecha del Viento" Text{ "Wand of Gamelon", "Baguette de Gamelon", "Zauberstab von Gamelon" }, // "Varita de Gamelón" - Text{ "Shock Arrow", "Flèches Électriques", "Elektro-Pfeile€" }, // "Flecha eléctrica" - Text{ "Silver Arrow", "Flèches d'Argent", "Silber-Pfeile€" }, // "Flecha de plata" + Text{ "Shock Arrow", "Flèches Électriques", "Elektro-Pfeile" }, // "Flecha eléctrica" + Text{ "Silver Arrow", "Flèches d'Argent", "Silber-Pfeile" }, // "Flecha de plata" }; trickNameTable[RG_GERUDO_MEMBERSHIP_CARD] = { Text{ "Desert Title Deed", "Abonnement Gerudo", "Wüsten-Urkunde" }, // "Escritura del desierto" @@ -157,12 +158,12 @@ static void InitTrickNames() { trickNameTable[RG_MAGIC_BEAN_PACK] = { Text{ "Funky Bean Pack", "Paquet de Fèves Magiques", "Wunderbohnen-Packung" }, // "Lote de frijoles mágicos" - Text{ "Grapple Berries", "Baies de grappin", "Grapple-Beeren€" }, // "Bayas de garfio" + Text{ "Grapple Berries", "Baies de grappin", "Grapple-Beeren" }, // "Bayas de garfio" Text{ "Crenel Bean Pack", "Paquet de Haricots Gonggle", "Gongolerbsen-Packung" }, // "Lote de alubias mágicas" Text{ "Mystical Seed Pack", "Pack de graines mystiques", "Saatbeutel" }, // "Paquete de semillas místicas" }; trickNameTable[RG_DOUBLE_DEFENSE] = { - Text{ "Diamond Hearts", "Coeurs de Diamant", "Diamantherzen€" }, // "Contenedor de diamante" + Text{ "Diamond Hearts", "Coeurs de Diamant", "Diamantherzen" }, // "Contenedor de diamante" Text{ "Double Damage", "Double Souffrance", "Doppelte Angriffskraft" }, // "Doble daño receptivo" Text{ "Quadruple Defence", "Quadruple Défence", "Vierfache Verteidigung" }, // "Defensa cuádruple" }; @@ -274,7 +275,7 @@ static void InitTrickNames() { Text{ "Progressive Powder Kegs", "Baril de Poudre (prog.)", "Pulverfass (prog.)" }, // "Barril de polvo progresivo" Text{ "Progressive Remote Bombs", "Bombes à distance (prog.)", - "Fernzünderbomben (prog.)€" }, // "Bombas remotas progresivas" + "Fernzünderbomben (prog.)" }, // "Bombas remotas progresivas" }; trickNameTable[RG_PROGRESSIVE_BOW] = { Text{ "Progressive Arrow Capacity", "Capacité de flèches (prog.)", @@ -309,7 +310,7 @@ static void InitTrickNames() { }; trickNameTable[RG_PROGRESSIVE_SCALE] = { Text{ "Progressive Flippers", "Palmes de Zora (prog.)", - "Schwimmflossen (prog.)€" }, // "Aletas de zora progresiva" + "Schwimmflossen (prog.)" }, // "Aletas de zora progresiva" Text{ "Progressive Dragon's Scale", "Écaille du dragon d'eau (prog.)", "Drachen-Schuppe (prog.)" }, // "Escama dragón acuático progresiva" Text{ "Progressive Diving Ability", "Plongée (prog.)", "Tauchfähigkeit (prog.)" }, // "Buceo progresivo" @@ -324,7 +325,7 @@ static void InitTrickNames() { "Putput-Kapazität (prog.)" }, // "Capacidad progresiva de pera" Text{ "Progressive Nut Bag", "Sac de noix (prog.)", "Nußbeutel (prog.)" }, // "Bolsa de nueces progresiva" Text{ "Progressive Husk Capacity", "Capacité de noisettes (prog.)", - "Schalen-Kapazität (prog.)€" }, // "Mayor capacidad de castañas" + "Schalen-Kapazität (prog.)" }, // "Mayor capacidad de castañas" }; trickNameTable[RG_PROGRESSIVE_STICK_UPGRADE] = { Text{ "Progressive Stick Bag", "Sac de bâtons (prog.)", @@ -337,11 +338,11 @@ static void InitTrickNames() { "Stock-Kapazität (prog.)" }, // "Mayor capacidad de cetros deku" }; trickNameTable[RG_PROGRESSIVE_BOMBCHU_BAG] = { - Text{ "Progressive Bomblings", "Bombinsectes (prog.)", "Bombenmäuse (prog.)€" }, // "Bombinsectos progresivos" + Text{ "Progressive Bomblings", "Bombinsectes (prog.)", "Bombenmäuse (prog.)" }, // "Bombinsectos progresivos" Text{ "Progressive Sentrobe Bombs", "Bombe de Sphérodrone (prog.)", - "Rokopterbomben (prog.)€" }, // "Bomba de helicobot progresivo" + "Rokopterbomben (prog.)" }, // "Bomba de helicobot progresivo" Text{ "Progressive Bomb-ombs", "Bombe Soldat (prog.)", "Bob-omb (prog.)" }, // "Soldado bomba progresivo" - Text{ "Progressive Missiles", "Missiles (prog.)", "Missiles (prog.)€" }, // "Misiles progresivos" + Text{ "Progressive Missiles", "Missiles (prog.)", "Missiles (prog.)" }, // "Misiles progresivos" }; trickNameTable[RG_PROGRESSIVE_MAGIC_METER] = { Text{ "Progressive Stamina Meter", "Jauge d'endurance (prog.)", @@ -680,31 +681,31 @@ static void InitTrickNames() { }; trickNameTable[RG_BLUE_RUPEE] = { Text{ "Blupee", "Bleubi", "Fünfer" }, // "Azupia" - Text{ "Five Rubies", "Cinq Rubys", "fünf Rubies€" }, // "Cinco rubíes" - Text{ "Five Rupees", "Cinq rubis", "fünf Rubine€" }, // "Bolívar hyliano" + Text{ "Five Rubies", "Cinq Rubys", "fünf Rubies" }, // "Cinco rubíes" + Text{ "Five Rupees", "Cinq rubis", "fünf Rubine" }, // "Bolívar hyliano" Text{ "Rupee (5)", "Rubis (5)", "Rubin (5)" }, // "Peso hyliano" Text{ "Rupoor (5)", "Roupir (5)", "Rubinfalle (5)" }, // "Rupobre (5)" }; trickNameTable[RG_RED_RUPEE] = { Text{ "Big 20", "Grand 20", "Zwanni" }, // "Los 20 grandes" - Text{ "Twenty Rubies", "vingt rubis", "zwanzig Rubies€" }, // "Veinte rubíes" - Text{ "Twenty Rupees", "Vingt rubis", "zwanzig Rubine€" }, // "Colon hyliano" + Text{ "Twenty Rubies", "vingt rubis", "zwanzig Rubies" }, // "Veinte rubíes" + Text{ "Twenty Rupees", "Vingt rubis", "zwanzig Rubine" }, // "Colon hyliano" Text{ "Rupee (20)", "Rubis (20)", "Rubin (20)" }, // "Peso hyliano" Text{ "Rupoor (20)", "Roupir (20)", "Rubinfalle (20)" }, // "Rupobre (20)" }; trickNameTable[RG_PURPLE_RUPEE] = { Text{ "Purpee", "Pourbi", "Fuffi" }, // "morupiua" - Text{ "Fifty Rubies", "Cinquante rubis", "fünfzig Rubies€" }, // "Cincuenta rubíes" - Text{ "Fifty Rupees", "Cinquante rubis", "fünfzig Rubine€" }, // "Balboa hyliano" + Text{ "Fifty Rubies", "Cinquante rubis", "fünfzig Rubies" }, // "Cincuenta rubíes" + Text{ "Fifty Rupees", "Cinquante rubis", "fünfzig Rubine" }, // "Balboa hyliano" Text{ "Rupee (50)", "Rubis (50)", "Rubin (50)" }, // "Peso hyliano" Text{ "Rupoor (50)", "Roupir (50)", "Rubinfalle (50)" }, // "Rupobre (50)" }; trickNameTable[RG_HUGE_RUPEE] = { - Text{ "Hugo", "Or Rubi", "zwei Hunnis€" }, // "Oro Rubi" - Text{ "Two Hundred Rubies", "Deux cents rubis", "zweihundert Rubies€" }, // "Doscientos rubíes" + Text{ "Hugo", "Or Rubi", "zwei Hunnis" }, // "Oro Rubi" + Text{ "Two Hundred Rubies", "Deux cents rubis", "zweihundert Rubies" }, // "Doscientos rubíes" Text{ "Diamond", "Diamant", "Diamant" }, // "Diamante" Text{ "Huge Ruby", "Énorme rubis", "großer Ruby" }, // "Rubi gigante" - Text{ "Two Hundred Rupees", "Deux cent rubis", "zweihundert Rubine€" }, // "Euro hyliano" + Text{ "Two Hundred Rupees", "Deux cent rubis", "zweihundert Rubine" }, // "Euro hyliano" Text{ "Rupee (200)", "Rubis (200)", "Rubin (200)" }, // "Dólar hyliano" }; trickNameTable[RG_PIECE_OF_HEART] = { @@ -784,7 +785,7 @@ static void InitTrickNames() { }; trickNameTable[RG_KING_DODONGO_SOUL] = { Text{ "Lizard Soul", "Âme d'un Lézard", "Reptilienseele" }, - Text{ "Regal Remains", "Restes Délicieux", "royale Überreste€" }, + Text{ "Regal Remains", "Restes Délicieux", "royale Überreste" }, Text{ "Dodongo's Core", "Coeur de Dodongo", "Dodongos Kern" }, }; trickNameTable[RG_BARINADE_SOUL] = { @@ -1346,19 +1347,19 @@ static void InitTrickNames() { semillas grande" }; trickNameTable[GI_STRENGTH_1] = { - Text{"Goron's Gauntlet", "Gantelet Goron", "Goronen-Handschuhe€" }, // "Brazalete amarillo" + Text{"Goron's Gauntlet", "Gantelet Goron", "Goronen-Handschuhe" }, // "Brazalete amarillo" Text{"Power Bracelet", "Bracelet de force", "Kraftarmband" }, // "Brazalete de fuerza" Text{"Magic Bracelet", "Bracelet de Lavio", "Magiearmband" }, // "Brazalete de Ravio" }; trickNameTable[GI_STRENGTH_2] = { Text{"Silver Bracelets", "Bracelets d'argent", "Silberarmband" }, // "Guantes Moguma" - Text{"Power Gloves", "Gant de puissance", "Silberhandschuhe€" }, // "Guante del Poder" - Text{"Magic Gauntlets", "Gantelet magique", "Magiehandschuhe€" }, // "Guante mágico" + Text{"Power Gloves", "Gant de puissance", "Silberhandschuhe" }, // "Guante del Poder" + Text{"Magic Gauntlets", "Gantelet magique", "Magiehandschuhe" }, // "Guante mágico" }; trickNameTable[GI_STRENGTH_3] = { Text{"Golden Bracelets", "Bracelets d'or", "Goldarmband" }, // "Guantelete de Thanos" - Text{"Titan's Mitts", "Moufle de titan", "Goldhandschuhe€" }, // "Guantes de Titán" - Text{"Magnetic Gloves", "Magnéto-gants", "Magnethandschuhe€" }, // "Guantes de fuego" + Text{"Titan's Mitts", "Moufle de titan", "Goldhandschuhe" }, // "Guantes de Titán" + Text{"Magnetic Gloves", "Magnéto-gants", "Magnethandschuhe" }, // "Guantes de fuego" }; trickNameTable[GI_SCALE_1] = { Text{"Silver Pearl", "Perle d'argent", "Silberne Perle" }, // "Perla de Plata progresiva" @@ -1463,7 +1464,7 @@ static void InitTrickNames() { }; trickNameTable[GI_MASK_ZORA] = { Text{"Zola Mask", "Masque Zola", "Zola-Maske" }, // "Máscara Zola" - Text{"Mask of Zora", "Masque des Zoras", "Zora-Schuppen€" }, // "Máscara de los Zora" + Text{"Mask of Zora", "Masque des Zoras", "Zora-Schuppen" }, // "Máscara de los Zora" Text{"Ruto Mask", "Masque de Ruto", "Rutos Maske" }, // "Máscara de Mikau" }; trickNameTable[GI_MASK_GERUDO] = { @@ -1487,9 +1488,12 @@ Text Rando::Traps::GetTrapName(uint16_t id) { initTrickNames = true; } - if (trickNameTable[id].empty()) { - assert(false); - return Text{ "not an Ice Trap" }; + if (id >= RG_MAX || trickNameTable[id].empty()) { + SPDLOG_ERROR("GetTrapName: missing trick name for item id {}", id); + if (id < RG_MAX) { + return StaticData::RetrieveItem(static_cast(id)).GetName(); + } + return Text{ "Ice Trap" }; } // Randomly get the easy, medium, or hard name for the given item id diff --git a/soh/soh/Enhancements/randomizer/hint.cpp b/soh/soh/Enhancements/randomizer/hint.cpp index a5954e9e029..d9c4ea508f5 100644 --- a/soh/soh/Enhancements/randomizer/hint.cpp +++ b/soh/soh/Enhancements/randomizer/hint.cpp @@ -2,6 +2,7 @@ #include "map" #include "string" #include "SeedContext.h" +#include "soh/util.h" #include #include "static_data.h" #include "3drando/random.hpp" @@ -375,31 +376,34 @@ oJson Hint::toJSON() { auto ctx = Rando::Context::GetInstance(); nlohmann::ordered_json log = {}; if (enabled) { - log["type"] = StaticData::hintTypeNames[hintType].GetForCurrentLanguage(MF_CLEAN); + log["type"] = SohUtils::SanitizeUtf8(StaticData::hintTypeNames[hintType].GetForCurrentLanguage(MF_CLEAN)); std::vector hintMessages = GetAllMessageStrings(MF_CLEAN); if (hintMessages.size() == 1) { - log["message"] = hintMessages[0]; + log["message"] = SohUtils::SanitizeUtf8(hintMessages[0]); } else if (hintMessages.size() > 1) { - log["messages"] = hintMessages; + std::vector sanitizedMessages; + sanitizedMessages.reserve(hintMessages.size()); + for (const std::string& message : hintMessages) { + sanitizedMessages.push_back(SohUtils::SanitizeUtf8(message)); + } + log["messages"] = sanitizedMessages; } if (distribution != "") { - log["distribution"] = distribution; + log["distribution"] = SohUtils::SanitizeUtf8(distribution); } if (hintType != HINT_TYPE_FOOLISH) { if (!(StaticData::staticHintInfoMap.contains(ownKey) && StaticData::staticHintInfoMap[ownKey].targetChecks.size() > 0)) { if (locations.size() == 1) { - log["location"] = StaticData::GetLocation(locations[0]) - ->GetName(); // RANDOTODO change to CustomMessage when VB is done; + log["location"] = SohUtils::SanitizeUtf8(StaticData::GetLocation(locations[0])->GetName()); } else if (locations.size() > 1) { // If we have defaults, no need to write more std::vector locStrings = {}; for (size_t c = 0; c < locations.size(); c++) { - locStrings.push_back(StaticData::GetLocation(locations[c]) - ->GetName()); // RANDOTODO change to CustomMessage when VB is done + locStrings.push_back(SohUtils::SanitizeUtf8(StaticData::GetLocation(locations[c])->GetName())); } log["locations"] = locStrings; } @@ -408,15 +412,11 @@ oJson Hint::toJSON() { if (!(StaticData::staticHintInfoMap.contains(ownKey) && StaticData::staticHintInfoMap[ownKey].targetItems.size() > 0)) { if (items.size() == 1) { - log["item"] = StaticData::GetItemTable()[items[0]] - .GetName() - .GetEnglish(); // RANDOTODO change to CustomMessage; + log["item"] = SohUtils::SanitizeUtf8(StaticData::GetItemTable()[items[0]].GetName().GetEnglish()); } else if (items.size() > 1) { std::vector itemStrings = {}; for (size_t c = 0; c < items.size(); c++) { - itemStrings.push_back(StaticData::GetItemTable()[items[c]] - .GetName() - .GetEnglish()); // RANDOTODO change to CustomMessage + itemStrings.push_back(SohUtils::SanitizeUtf8(StaticData::GetItemTable()[items[c]].GetName().GetEnglish())); } log["items"] = itemStrings; } @@ -433,16 +433,16 @@ oJson Hint::toJSON() { } } if (areas.size() == 1) { - log["area"] = - StaticData::hintTextTable[StaticData::areaNames[areas[0]]].GetClear().GetForCurrentLanguage(MF_CLEAN); + log["area"] = SohUtils::SanitizeUtf8( + StaticData::hintTextTable[StaticData::areaNames[areas[0]]].GetClear().GetForCurrentLanguage(MF_CLEAN)); } else if (areas.size() > 0 && !(StaticData::staticHintInfoMap.contains(ownKey) && StaticData::staticHintInfoMap[ownKey].targetChecks.size() > 0)) { // If we got locations from defaults, areas are derived from them and don't need logging std::vector areaStrings = {}; for (size_t c = 0; c < areas.size(); c++) { - areaStrings.push_back( + areaStrings.push_back(SohUtils::SanitizeUtf8( StaticData::hintTextTable[StaticData::areaNames[areas[c]]].GetClear().GetForCurrentLanguage( - MF_CLEAN)); + MF_CLEAN))); } log["areas"] = areaStrings; } @@ -458,11 +458,12 @@ oJson Hint::toJSON() { } if (trials.size() == 1) { - log["trial"] = ctx->GetTrial(trials[0])->GetName().GetForCurrentLanguage(MF_CLEAN); + log["trial"] = SohUtils::SanitizeUtf8(ctx->GetTrial(trials[0])->GetName().GetForCurrentLanguage(MF_CLEAN)); } else if (trials.size() > 0) { std::vector trialStrings = {}; for (size_t c = 0; c < trials.size(); c++) { - trialStrings.push_back(ctx->GetTrial(trials[c])->GetName().GetForCurrentLanguage(MF_CLEAN)); + trialStrings.push_back( + SohUtils::SanitizeUtf8(ctx->GetTrial(trials[c])->GetName().GetForCurrentLanguage(MF_CLEAN))); } log["trials"] = trialStrings; } @@ -505,7 +506,7 @@ void Hint::logHint(oJson& jsonData) { if (enabled && (!(staticHint && (hintType == HINT_TYPE_ITEM) && ctx->GetOption(RSK_HINT_CLARITY).Is(RO_HINT_CLARITY_CLEAR)))) { // skip if not enabled or if a static hint with no possible variance - jsonData[logMap][Rando::StaticData::hintNames[ownKey].GetForCurrentLanguage(MF_CLEAN)] = toJSON(); + jsonData[logMap][SohUtils::SanitizeUtf8(Rando::StaticData::hintNames[ownKey].GetForCurrentLanguage(MF_CLEAN))] = toJSON(); } } diff --git a/soh/soh/Enhancements/randomizer/item.cpp b/soh/soh/Enhancements/randomizer/item.cpp index e800be2755f..ce60cd73322 100644 --- a/soh/soh/Enhancements/randomizer/item.cpp +++ b/soh/soh/Enhancements/randomizer/item.cpp @@ -48,13 +48,17 @@ Item::Item(const RandomizerGet randomizerGet_, Text name_, const ItemType type_, article(std::move(article_)), color(std::move(color_)), progressive(progressive_), price(price_) { } +static bool UsingLogicSimulationBuffer(const std::shared_ptr& logic) { + return logic != nullptr && logic->mSaveContext != nullptr && logic->mSaveContext != &gSaveContext; +} + void Item::ApplyEffect() const { auto ctx = Rando::Context::GetInstance(); auto logic = ctx->GetLogic(); - if (!logic->CalculatingAvailableChecks) { - logic->ApplyItemEffect(StaticData::RetrieveItem(randomizerGet), true); - } else if (SplitSongs::IsProgressiveSong(randomizerGet)) { + if (SplitSongs::IsProgressiveSong(randomizerGet) && UsingLogicSimulationBuffer(logic)) { SplitSongs::ApplyProgressiveEffectToLogicScratch(logic.get(), randomizerGet, true); + } else if (!logic->CalculatingAvailableChecks) { + logic->ApplyItemEffect(StaticData::RetrieveItem(randomizerGet), true); } logic->Set(logicVal, true); } @@ -62,10 +66,10 @@ void Item::ApplyEffect() const { void Item::UndoEffect() const { auto ctx = Rando::Context::GetInstance(); auto logic = ctx->GetLogic(); - if (!logic->CalculatingAvailableChecks) { - logic->ApplyItemEffect(StaticData::RetrieveItem(randomizerGet), false); - } else if (SplitSongs::IsProgressiveSong(randomizerGet)) { + if (SplitSongs::IsProgressiveSong(randomizerGet) && UsingLogicSimulationBuffer(logic)) { SplitSongs::ApplyProgressiveEffectToLogicScratch(logic.get(), randomizerGet, false); + } else if (!logic->CalculatingAvailableChecks) { + logic->ApplyItemEffect(StaticData::RetrieveItem(randomizerGet), false); } logic->Set(logicVal, false); } @@ -107,7 +111,8 @@ uint16_t Item::GetPrice() const { } std::shared_ptr Item::GetGIEntry() const { // NOLINT(*-no-recursion) - if (giEntry != nullptr && giEntry->itemId != RG_PROGRESSIVE_BOMBCHU_BAG) { + if (giEntry != nullptr && giEntry->itemId != RG_PROGRESSIVE_BOMBCHU_BAG && + !SplitSongs::IsProgressiveSong(randomizerGet)) { return giEntry; } std::shared_ptr ctx = Rando::Context::GetInstance(); @@ -385,7 +390,7 @@ std::shared_ptr Item::GetGIEntry() const { // NOLINT(*-no-recursio case RG_PROGRESSIVE_REQUIEM_OF_SPIRIT: case RG_PROGRESSIVE_NOCTURNE_OF_SHADOW: case RG_PROGRESSIVE_PRELUDE_OF_LIGHT: - actual = SplitSongs::ResolveProgressiveSongStage(randomizerGet); + actual = SplitSongs::ResolveProgressiveSongStage(logic.get(), randomizerGet); break; case RG_PROGRESSIVE_BOMBCHU_BAG: if (OTRGlobals::Instance->gRandoContext->GetOption(RSK_BOMBCHU_BAG).Is(RO_BOMBCHU_BAG_SINGLE)) { @@ -418,11 +423,21 @@ std::shared_ptr Item::GetGIEntry() const { // NOLINT(*-no-recursio if (actual == randomizerGet && giEntry != nullptr) { return giEntry; } + if (actual == RG_NONE) { + if (giEntry != nullptr) { + return giEntry; + } + return StaticData::RetrieveItem(RG_NONE).GetGIEntry(); + } return StaticData::RetrieveItem(actual).GetGIEntry(); } GetItemEntry Item::GetGIEntry_Copy() const { - return *GetGIEntry(); + const auto entry = GetGIEntry(); + if (entry != nullptr) { + return *entry; + } + return *StaticData::RetrieveItem(RG_NONE).GetGIEntry(); } void Item::SetPrice(const uint16_t price_) { diff --git a/soh/soh/Enhancements/randomizer/item_list.cpp b/soh/soh/Enhancements/randomizer/item_list.cpp index 5bd1513f963..5ab3f9bbdc5 100644 --- a/soh/soh/Enhancements/randomizer/item_list.cpp +++ b/soh/soh/Enhancements/randomizer/item_list.cpp @@ -73,18 +73,18 @@ void Rando::StaticData::InitItemTable() { itemTable[RG_PROGRESSIVE_MAGIC_METER] = Item(RG_PROGRESSIVE_MAGIC_METER, Text{ "Progressive Magic Meter", "Jauge de Magie (prog.)", "Progressives Magisches Maß" }, ITEMTYPE_ITEM, 0x8A, true, LOGIC_PROGRESSIVE_MAGIC, RHT_PROGRESSIVE_MAGIC_METER, ITEM_CATEGORY_MAJOR, {"a ", "une ", "ein "}, "%g", true).CustomIcon(gQuestIconMagicJarBigTex, ICON_SIZE_24); itemTable[RG_PROGRESSIVE_OCARINA] = Item(RG_PROGRESSIVE_OCARINA, Text{ "Progressive Ocarina", "Ocarina (prog.)", "Progressive Okarina" }, ITEMTYPE_ITEM, 0x8B, true, LOGIC_PROGRESSIVE_OCARINA, RHT_PROGRESSIVE_OCARINA, ITEM_CATEGORY_MAJOR, {"a ", "un ", "eine "}, "%g", true); itemTable[RG_PROGRESSIVE_GORONSWORD] = Item(RG_PROGRESSIVE_GORONSWORD, Text{ "Progressive Goron Sword", "Épée Goron (prog.)", "Progressives Goronen-Schwert" }, ITEMTYPE_ITEM, 0xD4, true, LOGIC_PROGRESSIVE_GIANT_KNIFE, RHT_PROGRESSIVE_GORONSWORD, ITEM_CATEGORY_MAJOR, {"a ", "une ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_ZELDAS_LULLABY] = Item(RG_PROGRESSIVE_ZELDAS_LULLABY, Text{ "Progressive Zelda's Lullaby", "Berceuse de Zelda (prog.)", "Progressives Zeldas Wiegenlied" }, ITEMTYPE_SONG, 0xE1, true, LOGIC_NONE, RHT_ZELDAS_LULLABY, RG_PROGRESSIVE_ZELDAS_LULLABY, OBJECT_GI_MELODY, GID_SONG_ZELDA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_EPONAS_SONG] = Item(RG_PROGRESSIVE_EPONAS_SONG, Text{ "Progressive Epona's Song", "Chant d'Epona (prog.)", "Progressives Eponas Lied" }, ITEMTYPE_SONG, 0xE2, true, LOGIC_NONE, RHT_EPONAS_SONG, RG_PROGRESSIVE_EPONAS_SONG, OBJECT_GI_MELODY, GID_SONG_EPONA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_SARIAS_SONG] = Item(RG_PROGRESSIVE_SARIAS_SONG, Text{ "Progressive Saria's Song", "Chant de Saria (prog.)", "Progressives Salias Lied" }, ITEMTYPE_SONG, 0xE3, true, LOGIC_NONE, RHT_SARIAS_SONG, RG_PROGRESSIVE_SARIAS_SONG, OBJECT_GI_MELODY, GID_SONG_SARIA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_SUNS_SONG] = Item(RG_PROGRESSIVE_SUNS_SONG, Text{ "Progressive Sun's Song", "Chant du Soleil (prog.)", "Progressives Hymne der Sonne" }, ITEMTYPE_SONG, 0xE4, true, LOGIC_NONE, RHT_SUNS_SONG, RG_PROGRESSIVE_SUNS_SONG, OBJECT_GI_MELODY, GID_SONG_SUN, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_SONG_OF_TIME] = Item(RG_PROGRESSIVE_SONG_OF_TIME, Text{ "Progressive Song of Time", "Chant du Temps (prog.)", "Progressives Hymne der Zeit" }, ITEMTYPE_SONG, 0xE5, true, LOGIC_NONE, RHT_SONG_OF_TIME, RG_PROGRESSIVE_SONG_OF_TIME, OBJECT_GI_MELODY, GID_SONG_TIME, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_SONG_OF_STORMS] = Item(RG_PROGRESSIVE_SONG_OF_STORMS, Text{ "Progressive Song of Storms", "Chant des Tempêtes (prog.)", "Progressives Hymne des Sturms" }, ITEMTYPE_SONG, 0xE6, true, LOGIC_NONE, RHT_SONG_OF_STORMS, RG_PROGRESSIVE_SONG_OF_STORMS, OBJECT_GI_MELODY, GID_SONG_STORM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_MINUET_OF_FOREST] = Item(RG_PROGRESSIVE_MINUET_OF_FOREST, Text{ "Progressive Minuet of Forest", "Menuet des Bois (prog.)", "Progressives Menuett des Waldes" }, ITEMTYPE_SONG, 0xE7, true, LOGIC_NONE, RHT_MINUET_OF_FOREST, RG_PROGRESSIVE_MINUET_OF_FOREST, OBJECT_GI_MELODY, GID_SONG_MINUET, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_BOLERO_OF_FIRE] = Item(RG_PROGRESSIVE_BOLERO_OF_FIRE, Text{ "Progressive Bolero of Fire", "Boléro du Feu (prog.)", "Progressives Bolero des Feuers" }, ITEMTYPE_SONG, 0xE8, true, LOGIC_NONE, RHT_BOLERO_OF_FIRE, RG_PROGRESSIVE_BOLERO_OF_FIRE, OBJECT_GI_MELODY, GID_SONG_BOLERO, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_SERENADE_OF_WATER] = Item(RG_PROGRESSIVE_SERENADE_OF_WATER, Text{ "Progressive Serenade of Water", "Sérénade de l'Eau (prog.)", "Progressive Serenade des Wassers" }, ITEMTYPE_SONG, 0xE9, true, LOGIC_NONE, RHT_SERENADE_OF_WATER, RG_PROGRESSIVE_SERENADE_OF_WATER, OBJECT_GI_MELODY, GID_SONG_SERENADE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_REQUIEM_OF_SPIRIT] = Item(RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, Text{ "Progressive Requiem of Spirit", "Requiem des Esprits (prog.)", "Progressives Requiem der Geister" }, ITEMTYPE_SONG, 0xEA, true, LOGIC_NONE, RHT_REQUIEM_OF_SPIRIT, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, OBJECT_GI_MELODY, GID_SONG_REQUIEM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_NOCTURNE_OF_SHADOW] = Item(RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, Text{ "Progressive Nocturne of Shadow", "Nocturne de l'Ombre (prog.)", "Progressive Nocturne des Schattens" }, ITEMTYPE_SONG, 0xEB, true, LOGIC_NONE, RHT_NOCTURNE_OF_SHADOW, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, OBJECT_GI_MELODY, GID_SONG_NOCTURNE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_PRELUDE_OF_LIGHT] = Item(RG_PROGRESSIVE_PRELUDE_OF_LIGHT, Text{ "Progressive Prelude of Light", "Prélude de la Lumière (prog.)", "Progressive Kantate des Lichts" }, ITEMTYPE_SONG, 0xEC, true, LOGIC_NONE, RHT_PRELUDE_OF_LIGHT, RG_PROGRESSIVE_PRELUDE_OF_LIGHT, OBJECT_GI_MELODY, GID_SONG_PRELUDE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_ZELDAS_LULLABY] = Item(RG_PROGRESSIVE_ZELDAS_LULLABY, Text{ "Progressive Zelda's Lullaby", "Berceuse de Zelda (prog.)", "Progressives Zeldas Wiegenlied" }, ITEMTYPE_SONG, 0xE1, true, LOGIC_NONE, RHT_ZELDAS_LULLABY, RG_PROGRESSIVE_ZELDAS_LULLABY, OBJECT_GI_MELODY, GID_SONG_ZELDA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); + itemTable[RG_PROGRESSIVE_EPONAS_SONG] = Item(RG_PROGRESSIVE_EPONAS_SONG, Text{ "Progressive Epona's Song", "Chant d'Epona (prog.)", "Progressives Eponas Lied" }, ITEMTYPE_SONG, 0xE2, true, LOGIC_NONE, RHT_EPONAS_SONG, RG_PROGRESSIVE_EPONAS_SONG, OBJECT_GI_MELODY, GID_SONG_EPONA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); + itemTable[RG_PROGRESSIVE_SARIAS_SONG] = Item(RG_PROGRESSIVE_SARIAS_SONG, Text{ "Progressive Saria's Song", "Chant de Saria (prog.)", "Progressives Salias Lied" }, ITEMTYPE_SONG, 0xE3, true, LOGIC_NONE, RHT_SARIAS_SONG, RG_PROGRESSIVE_SARIAS_SONG, OBJECT_GI_MELODY, GID_SONG_SARIA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); + itemTable[RG_PROGRESSIVE_SUNS_SONG] = Item(RG_PROGRESSIVE_SUNS_SONG, Text{ "Progressive Sun's Song", "Chant du Soleil (prog.)", "Progressives Hymne der Sonne" }, ITEMTYPE_SONG, 0xE4, true, LOGIC_NONE, RHT_SUNS_SONG, RG_PROGRESSIVE_SUNS_SONG, OBJECT_GI_MELODY, GID_SONG_SUN, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); + itemTable[RG_PROGRESSIVE_SONG_OF_TIME] = Item(RG_PROGRESSIVE_SONG_OF_TIME, Text{ "Progressive Song of Time", "Chant du Temps (prog.)", "Progressives Hymne der Zeit" }, ITEMTYPE_SONG, 0xE5, true, LOGIC_NONE, RHT_SONG_OF_TIME, RG_PROGRESSIVE_SONG_OF_TIME, OBJECT_GI_MELODY, GID_SONG_TIME, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); + itemTable[RG_PROGRESSIVE_SONG_OF_STORMS] = Item(RG_PROGRESSIVE_SONG_OF_STORMS, Text{ "Progressive Song of Storms", "Chant des Tempêtes (prog.)", "Progressives Hymne des Sturms" }, ITEMTYPE_SONG, 0xE6, true, LOGIC_NONE, RHT_SONG_OF_STORMS, RG_PROGRESSIVE_SONG_OF_STORMS, OBJECT_GI_MELODY, GID_SONG_STORM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); + itemTable[RG_PROGRESSIVE_MINUET_OF_FOREST] = Item(RG_PROGRESSIVE_MINUET_OF_FOREST, Text{ "Progressive Minuet of Forest", "Menuet des Bois (prog.)", "Progressives Menuett des Waldes" }, ITEMTYPE_SONG, 0xE7, true, LOGIC_NONE, RHT_MINUET_OF_FOREST, RG_PROGRESSIVE_MINUET_OF_FOREST, OBJECT_GI_MELODY, GID_SONG_MINUET, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); + itemTable[RG_PROGRESSIVE_BOLERO_OF_FIRE] = Item(RG_PROGRESSIVE_BOLERO_OF_FIRE, Text{ "Progressive Bolero of Fire", "Boléro du Feu (prog.)", "Progressives Bolero des Feuers" }, ITEMTYPE_SONG, 0xE8, true, LOGIC_NONE, RHT_BOLERO_OF_FIRE, RG_PROGRESSIVE_BOLERO_OF_FIRE, OBJECT_GI_MELODY, GID_SONG_BOLERO, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); + itemTable[RG_PROGRESSIVE_SERENADE_OF_WATER] = Item(RG_PROGRESSIVE_SERENADE_OF_WATER, Text{ "Progressive Serenade of Water", "Sérénade de l'Eau (prog.)", "Progressive Serenade des Wassers" }, ITEMTYPE_SONG, 0xE9, true, LOGIC_NONE, RHT_SERENADE_OF_WATER, RG_PROGRESSIVE_SERENADE_OF_WATER, OBJECT_GI_MELODY, GID_SONG_SERENADE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); + itemTable[RG_PROGRESSIVE_REQUIEM_OF_SPIRIT] = Item(RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, Text{ "Progressive Requiem of Spirit", "Requiem des Esprits (prog.)", "Progressives Requiem der Geister" }, ITEMTYPE_SONG, 0xEA, true, LOGIC_NONE, RHT_REQUIEM_OF_SPIRIT, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, OBJECT_GI_MELODY, GID_SONG_REQUIEM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); + itemTable[RG_PROGRESSIVE_NOCTURNE_OF_SHADOW] = Item(RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, Text{ "Progressive Nocturne of Shadow", "Nocturne de l'Ombre (prog.)", "Progressive Nocturne des Schattens" }, ITEMTYPE_SONG, 0xEB, true, LOGIC_NONE, RHT_NOCTURNE_OF_SHADOW, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, OBJECT_GI_MELODY, GID_SONG_NOCTURNE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); + itemTable[RG_PROGRESSIVE_PRELUDE_OF_LIGHT] = Item(RG_PROGRESSIVE_PRELUDE_OF_LIGHT, Text{ "Progressive Prelude of Light", "Prélude de la Lumière (prog.)", "Progressive Kantate des Lichts" }, ITEMTYPE_SONG, 0xEC, true, LOGIC_NONE, RHT_PRELUDE_OF_LIGHT, RG_PROGRESSIVE_PRELUDE_OF_LIGHT, OBJECT_GI_MELODY, GID_SONG_PRELUDE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); // Bottles itemTable[RG_EMPTY_BOTTLE] = Item(RG_EMPTY_BOTTLE, Text{ "Empty Bottle", "Bouteille Vide", "Leere Flasche" }, ITEMTYPE_ITEM, GI_BOTTLE, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_MILK, ITEM_BOTTLE, OBJECT_GI_BOTTLE, GID_BOTTLE, 0x42, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"an ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_MILK] = Item(RG_BOTTLE_WITH_MILK, Text{ "Bottle with Milk", "Bouteille avec du Lait", "Flasche mit Milch" }, ITEMTYPE_ITEM, GI_MILK_BOTTLE, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_MILK, ITEM_MILK_BOTTLE, OBJECT_GI_MILK, GID_MILK, 0x98, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"a ", "une ", "eine "}); diff --git a/soh/soh/Enhancements/randomizer/logic.cpp b/soh/soh/Enhancements/randomizer/logic.cpp index 7a60f338213..ad1fb90bc87 100644 --- a/soh/soh/Enhancements/randomizer/logic.cpp +++ b/soh/soh/Enhancements/randomizer/logic.cpp @@ -2122,7 +2122,7 @@ void Logic::ApplyItemEffect(Item& item, bool state) { case ITEMTYPE_DUNGEONREWARD: case ITEMTYPE_SONG: { RandomizerGet rg = item.GetRandomizerGet(); - if (SplitSongs::IsProgressiveSong(rg)) { + if (SplitSongs::IsProgressiveSong(rg) && mSaveContext != nullptr && mSaveContext != &gSaveContext) { SplitSongs::ApplyProgressiveEffectToLogicScratch(this, rg, state); break; } diff --git a/soh/soh/Enhancements/randomizer/option_descriptions.cpp b/soh/soh/Enhancements/randomizer/option_descriptions.cpp index c45488a3c85..139e8ba99b4 100644 --- a/soh/soh/Enhancements/randomizer/option_descriptions.cpp +++ b/soh/soh/Enhancements/randomizer/option_descriptions.cpp @@ -218,8 +218,8 @@ void Settings::CreateOptionDescriptions() { "\n" "Anywhere - Songs can appear at any location."; mOptionDescriptions[RSK_SPLIT_OCARINA_SONGS] = - "Each ocarina song is shuffled as a progressive item, similar to Bombchu Bag upgrades. The first pickup " - "marks progress for that song. The second pickup grants the full song.\n\n" + "Each ocarina song is shuffled as a progressive item, (2 Pieces). The first pickup marks progress for that " + "song. The second pickup grants the full song.\n\n" "Requires Shuffle Songs set to Anywhere."; mOptionDescriptions[RSK_SHUFFLE_TOKENS] = "Shuffles Golden Skulltula Tokens into the item pool. This means " "Golden Skulltulas can contain other items as well.\n" diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index 8085ec37e88..a00b7af26cf 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -1139,6 +1139,10 @@ extern "C" u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry) { Randomizer_GameplayStats_SetTimestamp(item); if (Rando::SplitSongs::IsProgressiveSong(item)) { + const RandomizerGet resolved = Rando::SplitSongs::ResolveProgressiveSongStage(item); + if (resolved != item && resolved != RG_NONE) { + return Randomizer_Item_Give(play, Rando::StaticData::RetrieveItem(resolved).GetGIEntry_Copy()); + } Rando::SplitSongs::OnProgressiveSongReceived(item); return Return_Item_Entry(giEntry, RG_NONE); } diff --git a/soh/soh/Enhancements/randomizer/split_songs.cpp b/soh/soh/Enhancements/randomizer/split_songs.cpp index cf777d01ef8..ae19c9fb369 100644 --- a/soh/soh/Enhancements/randomizer/split_songs.cpp +++ b/soh/soh/Enhancements/randomizer/split_songs.cpp @@ -137,27 +137,49 @@ void SplitSongs::ApplyProgressiveEffectToLogicScratch(Logic* logic, RandomizerGe } const uint32_t partFlag = kSplitSongPartFlags[def->id]; + const auto qiIt = Logic::RandoGetToQuestItem.find(static_cast(def->fullSong)); + if (qiIt == Logic::RandoGetToQuestItem.end()) { + return; + } + if (state) { if (!logic->CheckRandoInf(static_cast(partFlag))) { logic->SetRandoInf(partFlag, true); } else { - auto& fullSongItem = StaticData::RetrieveItem(def->fullSong); - logic->ApplyItemEffect(fullSongItem, true); + logic->SetQuestItem(qiIt->second, true); } + } else if (logic->CheckQuestItem(qiIt->second)) { + logic->SetQuestItem(qiIt->second, false); } else if (logic->CheckRandoInf(static_cast(partFlag))) { logic->SetRandoInf(partFlag, false); } } -RandomizerGet SplitSongs::ResolveProgressiveSongStage(RandomizerGet rg) { +RandomizerGet SplitSongs::ResolveProgressiveSongStage(Logic* logic, RandomizerGet rg) { const SplitSongDef* def = GetSongDefFromProgressive(rg); if (def == nullptr) { return RG_NONE; } + + if (logic != nullptr && UsingLogicSimulationBuffer(logic)) { + const auto qiIt = Logic::RandoGetToQuestItem.find(static_cast(def->fullSong)); + if (qiIt != Logic::RandoGetToQuestItem.end() && logic->CheckQuestItem(qiIt->second)) { + return def->fullSong; + } + if (logic->CheckRandoInf(static_cast(kSplitSongPartFlags[def->id]))) { + return def->fullSong; + } + return def->progressive; + } + if (HasFullSong(def->id) || HasSplitPart(def->id)) { return def->fullSong; } return def->progressive; } +RandomizerGet SplitSongs::ResolveProgressiveSongStage(RandomizerGet rg) { + return ResolveProgressiveSongStage(Context::GetInstance()->GetLogic().get(), rg); +} + } // namespace Rando diff --git a/soh/soh/Enhancements/randomizer/split_songs.h b/soh/soh/Enhancements/randomizer/split_songs.h index 572ccc069a8..eb00aebb57a 100644 --- a/soh/soh/Enhancements/randomizer/split_songs.h +++ b/soh/soh/Enhancements/randomizer/split_songs.h @@ -47,6 +47,7 @@ class SplitSongs { static ItemObtainability GetProgressiveSongObtainability(RandomizerGet progressiveRg); static void ApplyProgressiveEffectToLogicScratch(Logic* logic, RandomizerGet rg, bool state); static RandomizerGet ResolveProgressiveSongStage(RandomizerGet rg); + static RandomizerGet ResolveProgressiveSongStage(Logic* logic, RandomizerGet rg); }; } // namespace Rando diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp index 0772b93ebd6..25167e034cf 100644 --- a/soh/soh/SaveManager.cpp +++ b/soh/soh/SaveManager.cpp @@ -30,6 +30,17 @@ extern "C" SaveContext gSaveContext; using namespace std::string_literals; +template <> +void SaveManager::SaveData(const std::string& name, const std::string& data) { + const std::string sanitized = SohUtils::SanitizeUtf8(data); + if (name == "") { + assert((*currentJsonContext).is_array()); + (*currentJsonContext).push_back(sanitized); + } else { + (*currentJsonContext)[name.c_str()] = sanitized; + } +} + void SaveManager::WriteSaveFile(const std::filesystem::path& savePath, const uintptr_t addr, void* dramAddr, const size_t size) { std::ofstream saveFile = std::ofstream(savePath, std::fstream::in | std::fstream::out | std::fstream::binary); @@ -1131,80 +1142,90 @@ int copy_file(const char* src, const char* dst) { void SaveManager::SaveFileThreaded(int fileNum, SaveContext* saveContext, int sectionID) { saveMtx.lock(); SPDLOG_INFO("Save File - fileNum: {}", fileNum); - // Needed for first time save, hasn't changed in forever anyway - saveBlock["version"] = 1; - if (IS_RANDO) { - saveBlock["fileType"] = FILE_TYPE_SAVE_RANDO; - } else { - saveBlock["fileType"] = FILE_TYPE_SAVE_VANILLA; - } - if (sectionID == SECTION_ID_BASE) { - for (auto& sectionHandlerPair : sectionSaveHandlers) { - auto& saveFuncInfo = sectionHandlerPair.second; - // Don't call SaveFuncs for sections that aren't tied to game save - if (!saveFuncInfo.saveWithBase || (saveFuncInfo.name == "randomizer" && !IS_RANDO)) { - continue; - } - nlohmann::json& sectionBlock = saveBlock["sections"][saveFuncInfo.name]; - sectionBlock["version"] = sectionHandlerPair.second.version; - // If any save file is loaded for medatata, or a spoiler log is loaded (not sure which at this point), there - // is still data in the "randomizer" section This clears the randomizer data block if and only if the - // section being called is "randomizer" and the current save file is not a randomizer save file. - currentJsonContext = §ionBlock["data"]; - sectionHandlerPair.second.func(saveContext, sectionID, true); + try { + // Needed for first time save, hasn't changed in forever anyway + saveBlock["version"] = 1; + if (IS_RANDO) { + saveBlock["fileType"] = FILE_TYPE_SAVE_RANDO; + } else { + saveBlock["fileType"] = FILE_TYPE_SAVE_VANILLA; } - } else { - SaveFuncInfo svi = sectionSaveHandlers.find(sectionID)->second; - auto& sectionName = svi.name; - auto sectionVersion = svi.version; - // If section has a parentSection, it is a subsection. Load parentSection version and set sectionBlock to parent - // string - if (svi.parentSection != -1 && svi.parentSection < sectionIndex) { - auto parentSvi = sectionSaveHandlers.find(svi.parentSection)->second; - sectionName = parentSvi.name; - sectionVersion = parentSvi.version; + if (sectionID == SECTION_ID_BASE) { + for (auto& sectionHandlerPair : sectionSaveHandlers) { + auto& saveFuncInfo = sectionHandlerPair.second; + // Don't call SaveFuncs for sections that aren't tied to game save + if (!saveFuncInfo.saveWithBase || (saveFuncInfo.name == "randomizer" && !IS_RANDO)) { + continue; + } + nlohmann::json& sectionBlock = saveBlock["sections"][saveFuncInfo.name]; + sectionBlock["version"] = sectionHandlerPair.second.version; + // If any save file is loaded for medatata, or a spoiler log is loaded (not sure which at this point), there + // is still data in the "randomizer" section This clears the randomizer data block if and only if the + // section being called is "randomizer" and the current save file is not a randomizer save file. + + currentJsonContext = §ionBlock["data"]; + sectionHandlerPair.second.func(saveContext, sectionID, true); + } + } else { + SaveFuncInfo svi = sectionSaveHandlers.find(sectionID)->second; + auto& sectionName = svi.name; + auto sectionVersion = svi.version; + // If section has a parentSection, it is a subsection. Load parentSection version and set sectionBlock to parent + // string + if (svi.parentSection != -1 && svi.parentSection < sectionIndex) { + auto parentSvi = sectionSaveHandlers.find(svi.parentSection)->second; + sectionName = parentSvi.name; + sectionVersion = parentSvi.version; + } + nlohmann::json& sectionBlock = saveBlock["sections"][sectionName]; + sectionBlock["version"] = sectionVersion; + currentJsonContext = §ionBlock["data"]; + svi.func(saveContext, sectionID, false); } - nlohmann::json& sectionBlock = saveBlock["sections"][sectionName]; - sectionBlock["version"] = sectionVersion; - currentJsonContext = §ionBlock["data"]; - svi.func(saveContext, sectionID, false); - } - std::filesystem::path fileName = GetFileName(fileNum); - std::filesystem::path tempFile = GetFileTempName(fileNum); + std::filesystem::path fileName = GetFileName(fileNum); + std::filesystem::path tempFile = GetFileTempName(fileNum); - if (std::filesystem::exists(tempFile)) { - std::filesystem::remove(tempFile); - } + if (std::filesystem::exists(tempFile)) { + std::filesystem::remove(tempFile); + } #if defined(__SWITCH__) || defined(__WIIU__) - FILE* w = fopen(tempFile.c_str(), "w"); - std::string json_string = saveBlock.dump(1); - fwrite(json_string.c_str(), sizeof(char), json_string.length(), w); - fclose(w); + FILE* w = fopen(tempFile.c_str(), "w"); + std::string json_string = saveBlock.dump(1); + fwrite(json_string.c_str(), sizeof(char), json_string.length(), w); + fclose(w); #else - std::ofstream output(tempFile); - output << std::setw(1) << saveBlock << std::endl; - output.close(); + std::ofstream output(tempFile); + output << std::setw(1) << saveBlock << std::endl; + output.close(); #endif #if defined(__SWITCH__) || defined(__WIIU__) - if (std::filesystem::exists(fileName)) { - std::filesystem::remove(fileName); - } - copy_file(tempFile.c_str(), fileName.c_str()); - if (std::filesystem::exists(tempFile)) { - std::filesystem::remove(tempFile); - } + if (std::filesystem::exists(fileName)) { + std::filesystem::remove(fileName); + } + copy_file(tempFile.c_str(), fileName.c_str()); + if (std::filesystem::exists(tempFile)) { + std::filesystem::remove(tempFile); + } #else - std::filesystem::rename(tempFile, fileName); + std::filesystem::rename(tempFile, fileName); #endif - delete saveContext; - InitMeta(fileNum); - GameInteractor::Instance->ExecuteHooks(fileNum, sectionID); - SPDLOG_INFO("Save File Finish - fileNum: {}", fileNum); + delete saveContext; + saveContext = nullptr; + InitMeta(fileNum); + GameInteractor::Instance->ExecuteHooks(fileNum, sectionID); + SPDLOG_INFO("Save File Finish - fileNum: {}", fileNum); + } catch (const std::exception& e) { + SPDLOG_ERROR("SaveFileThreaded failed for fileNum {}: {}", fileNum, e.what()); + if (saveContext != nullptr) { + delete saveContext; + } + } + saveMtx.unlock(); } diff --git a/soh/soh/SaveManager.h b/soh/soh/SaveManager.h index 80f58b915e5..6e3cda0dbc3 100644 --- a/soh/soh/SaveManager.h +++ b/soh/soh/SaveManager.h @@ -119,6 +119,8 @@ class SaveManager { } } + template <> void SaveData(const std::string& name, const std::string& data); + // In the SaveArrayFunc func, the name must be "" to save to the array. using SaveArrayFunc = std::function; void SaveArray(const std::string& name, const size_t size, SaveArrayFunc func); diff --git a/soh/soh/util.cpp b/soh/soh/util.cpp index 7efa4b7d160..fd9409b26f0 100644 --- a/soh/soh/util.cpp +++ b/soh/soh/util.cpp @@ -765,6 +765,59 @@ std::string SohUtils::Sanitize(std::string stringValue) { return stringValue; } +std::string SohUtils::SanitizeUtf8(std::string value) { + auto isUtf8Continuation = [](unsigned char byte) { return (byte & 0xC0) == 0x80; }; + + auto utf8SequenceLength = [](unsigned char lead) -> size_t { + if (lead < 0x80) { + return 1; + } + if ((lead & 0xE0) == 0xC0) { + return 2; + } + if ((lead & 0xF0) == 0xE0) { + return 3; + } + if ((lead & 0xF8) == 0xF0) { + return 4; + } + return 0; + }; + + std::string sanitized; + sanitized.reserve(value.size()); + + for (size_t i = 0; i < value.size();) { + const unsigned char lead = static_cast(value[i]); + const size_t sequenceLength = utf8SequenceLength(lead); + + if (sequenceLength == 0 || i + sequenceLength > value.size()) { + sanitized.push_back('?'); + i++; + continue; + } + + bool valid = true; + for (size_t j = 1; j < sequenceLength; j++) { + if (!isUtf8Continuation(static_cast(value[i + j]))) { + valid = false; + break; + } + } + + if (!valid) { + sanitized.push_back('?'); + i++; + continue; + } + + sanitized.append(value, i, sequenceLength); + i += sequenceLength; + } + + return sanitized; +} + size_t SohUtils::CopyStringToCharBuffer(char* buffer, const std::string& source, const size_t maxBufferSize) { if (!source.empty() && maxBufferSize > 0) { memset(buffer, 0, maxBufferSize); diff --git a/soh/soh/util.h b/soh/soh/util.h index 2058a344290..bd8c46fd4a7 100644 --- a/soh/soh/util.h +++ b/soh/soh/util.h @@ -20,6 +20,9 @@ void CopyStringToCharArray(char* destination, std::string source, size_t size); std::string Sanitize(std::string stringValue); +// Strips invalid UTF-8 sequences so JSON serializers accept the string. +std::string SanitizeUtf8(std::string value); + // Copies a string into a char buffer up to maxBufferSize characters. This does NOT insert a null terminator // on the end, as this is used for in-game messages which are not null-terminated. size_t CopyStringToCharBuffer(char* buffer, const std::string& source, size_t maxBufferSize); diff --git a/soh/src/code/z_message_PAL.c b/soh/src/code/z_message_PAL.c index ce09fe27168..3323c32fcea 100644 --- a/soh/src/code/z_message_PAL.c +++ b/soh/src/code/z_message_PAL.c @@ -837,6 +837,16 @@ f32 sFontWidths[144] = { 14.0f, // ? }; +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" + +#ifdef __cplusplus +extern "C" { +#endif +bool Rando_TextboxSongNoteIconActive(void); +#ifdef __cplusplus +} +#endif + u16 Message_DrawItemIcon(PlayState* play, u16 itemId, Gfx** p, u16 i) { s32 pad; Gfx* gfx = *p; @@ -864,9 +874,11 @@ u16 Message_DrawItemIcon(PlayState* play, u16 itemId, Gfx** p, u16 i) { G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); } } - gSPTextureRectangle(gfx++, (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS) << 2, R_TEXTBOX_ICON_YPOS << 2, - (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS + R_TEXTBOX_ICON_SIZE) << 2, - (R_TEXTBOX_ICON_YPOS + R_TEXTBOX_ICON_SIZE) << 2, G_TX_RENDERTILE, 0, 0, 1 << 10, 1 << 10); + if (!Rando_TextboxSongNoteIconActive()) { + gSPTextureRectangle(gfx++, (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS) << 2, R_TEXTBOX_ICON_YPOS << 2, + (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS + R_TEXTBOX_ICON_SIZE) << 2, + (R_TEXTBOX_ICON_YPOS + R_TEXTBOX_ICON_SIZE) << 2, G_TX_RENDERTILE, 0, 0, 1 << 10, 1 << 10); + } gDPPipeSync(gfx++); gDPSetCombineLERP(gfx++, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0); From 79b68824f7a51ac1a7d6a32326d9929480467c6f Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:50:02 +0100 Subject: [PATCH 20/22] Fix Linux CI: use SaveData string overload for UTF-8 sanitization. GCC rejects in-class template specializations that MSVC allows. Same behavior as before - strings are still passed through SanitizeUtf8 on save. --- soh/soh/SaveManager.cpp | 3 +-- soh/soh/SaveManager.h | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp index 25167e034cf..604f203a201 100644 --- a/soh/soh/SaveManager.cpp +++ b/soh/soh/SaveManager.cpp @@ -30,8 +30,7 @@ extern "C" SaveContext gSaveContext; using namespace std::string_literals; -template <> -void SaveManager::SaveData(const std::string& name, const std::string& data) { +void SaveManager::SaveData(const std::string& name, const std::string& data) { const std::string sanitized = SohUtils::SanitizeUtf8(data); if (name == "") { assert((*currentJsonContext).is_array()); diff --git a/soh/soh/SaveManager.h b/soh/soh/SaveManager.h index 6e3cda0dbc3..0e3bebacd74 100644 --- a/soh/soh/SaveManager.h +++ b/soh/soh/SaveManager.h @@ -119,7 +119,7 @@ class SaveManager { } } - template <> void SaveData(const std::string& name, const std::string& data); + void SaveData(const std::string& name, const std::string& data); // In the SaveArrayFunc func, the name must be "" to save to the array. using SaveArrayFunc = std::function; From 7d146a63c6067c7dab609c880eb6d31941df55dc Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:27:27 +0100 Subject: [PATCH 21/22] Split PR scope per review: drop icon and UTF-8 side fixes. Progressive songs now go through normal ApplyItemEffect (child wallet style) instead of the logic scratch workaround. --- .../randomizer/3drando/spoiler_log.cpp | 166 +++++++--------- .../randomizer/Messages/ItemMessages.cpp | 188 ++---------------- soh/soh/Enhancements/randomizer/hint.cpp | 43 ++-- soh/soh/Enhancements/randomizer/item.cpp | 12 +- soh/soh/Enhancements/randomizer/item_list.cpp | 24 +-- soh/soh/Enhancements/randomizer/logic.cpp | 18 +- .../Enhancements/randomizer/split_songs.cpp | 29 +-- soh/soh/Enhancements/randomizer/split_songs.h | 2 +- soh/soh/SaveManager.cpp | 140 ++++++------- soh/soh/SaveManager.h | 2 - soh/soh/util.cpp | 53 ----- soh/soh/util.h | 3 - soh/src/code/z_message_PAL.c | 18 +- 13 files changed, 207 insertions(+), 491 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp b/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp index c1fe622c6fe..5e1b84ec22a 100644 --- a/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp @@ -24,10 +24,8 @@ #include #include -#include #include -#include using json = nlohmann::ordered_json; using namespace Rando; @@ -42,10 +40,6 @@ namespace { std::string placementtxt; } // namespace -static std::string SafeJsonString(std::string value) { - return SohUtils::SanitizeUtf8(std::move(value)); -} - void GenerateHash() { auto ctx = Rando::Context::GetInstance(); std::string hash = ctx->GetHash(); @@ -79,16 +73,13 @@ static void WriteLocation(std::string sphere, const RandomizerCheck locationKey, switch (gSaveContext.language) { case LANGUAGE_ENG: default: - jsonData["playthrough"][sphere][location->GetName()] = - SafeJsonString(itemLocation->GetPlacedItemName().GetEnglish()); + jsonData["playthrough"][sphere][location->GetName()] = itemLocation->GetPlacedItemName().GetEnglish(); break; case LANGUAGE_GER: - jsonData["playthrough"][sphere][location->GetName()] = - SafeJsonString(itemLocation->GetPlacedItemName().GetGerman()); + jsonData["playthrough"][sphere][location->GetName()] = itemLocation->GetPlacedItemName().GetGerman(); break; case LANGUAGE_FRA: - jsonData["playthrough"][sphere][location->GetName()] = - SafeJsonString(itemLocation->GetPlacedItemName().GetFrench()); + jsonData["playthrough"][sphere][location->GetName()] = itemLocation->GetPlacedItemName().GetFrench(); break; } } @@ -139,7 +130,7 @@ static void WriteShuffledEntrance(std::string sphereString, Entrance* entrance) case LANGUAGE_GER: case LANGUAGE_FRA: default: - jsonData["entrancesMap"][sphereString][name] = SafeJsonString(text); + jsonData["entrancesMap"][sphereString][name] = text; break; } } @@ -150,8 +141,7 @@ static void WriteSettings() { std::array options = Rando::Settings::GetInstance()->GetAllOptions(); for (const Rando::Option& option : options) { if (option.GetName() != "") { - jsonData["settings"][option.GetName()] = - SafeJsonString(option.GetOptionText(ctx->GetOption(option.GetKey()).Get())); + jsonData["settings"][option.GetName()] = option.GetOptionText(ctx->GetOption(option.GetKey()).Get()); } } } @@ -172,7 +162,7 @@ static void WriteExcludedLocations() { continue; } - jsonData["excludedLocations"].push_back(SafeJsonString(RemoveLineBreaks(location->GetName()))); + jsonData["excludedLocations"].push_back(RemoveLineBreaks(location->GetName())); } } } @@ -184,8 +174,7 @@ static void WriteStartingInventory() { for (const Rando::OptionGroup* subGroup : optionGroup.GetSubGroups()) { if (subGroup->GetContainsType() == Rando::OptionGroupType::DEFAULT) { for (Rando::Option* option : subGroup->GetOptions()) { - jsonData["settings"][option->GetName()] = - SafeJsonString(option->GetOptionText(ctx->GetOption(option->GetKey()).Get())); + jsonData["settings"][option->GetName()] = option->GetOptionText(ctx->GetOption(option->GetKey()).Get()); } } } @@ -199,7 +188,7 @@ static void WriteEnabledTricks() { if (ctx->GetTrickOption(static_cast(setting->GetKey())).IsNot(RO_GENERIC_ON)) { continue; } - jsonData["enabledTricks"].push_back(SafeJsonString(RemoveLineBreaks(setting->GetName()))); + jsonData["enabledTricks"].push_back(RemoveLineBreaks(setting->GetName()).c_str()); } } @@ -211,7 +200,7 @@ static void WriteMasterQuestDungeons() { if (dungeon->IsVanilla()) { continue; } - jsonData["masterQuestDungeons"].push_back(SafeJsonString(dungeon->GetName())); + jsonData["masterQuestDungeons"].push_back(dungeon->GetName()); } } @@ -221,7 +210,7 @@ static void WriteChosenOptions() { for (const auto& trial : ctx->GetTrials()->GetTrialList()) { if (trial->IsRequired()) { std::string trialName = trial->GetName().GetForCurrentLanguage(MF_CLEAN); - jsonData["requiredTrials"].push_back(SafeJsonString(RemoveLineBreaks(trialName))); + jsonData["requiredTrials"].push_back(RemoveLineBreaks(trialName)); } } if (ctx->GetOption(RSK_SELECTED_STARTING_AGE).Is(RO_AGE_ADULT)) { @@ -287,13 +276,13 @@ static void WriteAllLocations() { if (!location->HasCustomPrice() && location->GetPlacedRandomizerGet() != RG_ICE_TRAP) { jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()] = - SafeJsonString(placedItemName); + placedItemName; continue; } // We're dealing with a complex item, build out the json object for it jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()]["item"] = - SafeJsonString(placedItemName); + placedItemName; if (location->HasCustomPrice()) { jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()]["price"] = @@ -308,33 +297,30 @@ static void WriteAllLocations() { case 0: default: jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()] - ["model"] = SafeJsonString(Rando::StaticData::RetrieveItem( - ctx->overrides[location->GetRandomizerCheck()].LooksLike()) - .GetName() - .GetEnglish()); + ["model"] = Rando::StaticData::RetrieveItem( + ctx->overrides[location->GetRandomizerCheck()].LooksLike()) + .GetName() + .GetEnglish(); jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()] - ["trickName"] = SafeJsonString( - ctx->overrides[location->GetRandomizerCheck()].GetTrickName().GetEnglish()); + ["trickName"] = ctx->overrides[location->GetRandomizerCheck()].GetTrickName().GetEnglish(); break; case 1: jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()] - ["model"] = SafeJsonString(Rando::StaticData::RetrieveItem( - ctx->overrides[location->GetRandomizerCheck()].LooksLike()) - .GetName() - .GetGerman()); + ["model"] = Rando::StaticData::RetrieveItem( + ctx->overrides[location->GetRandomizerCheck()].LooksLike()) + .GetName() + .GetGerman(); jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()] - ["trickName"] = SafeJsonString( - ctx->overrides[location->GetRandomizerCheck()].GetTrickName().GetGerman()); + ["trickName"] = ctx->overrides[location->GetRandomizerCheck()].GetTrickName().GetGerman(); break; case 2: jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()] - ["model"] = SafeJsonString(Rando::StaticData::RetrieveItem( - ctx->overrides[location->GetRandomizerCheck()].LooksLike()) - .GetName() - .GetFrench()); + ["model"] = Rando::StaticData::RetrieveItem( + ctx->overrides[location->GetRandomizerCheck()].LooksLike()) + .GetName() + .GetFrench(); jsonData["locations"][Rando::StaticData::GetLocation(location->GetRandomizerCheck())->GetName()] - ["trickName"] = SafeJsonString( - ctx->overrides[location->GetRandomizerCheck()].GetTrickName().GetFrench()); + ["trickName"] = ctx->overrides[location->GetRandomizerCheck()].GetTrickName().GetFrench(); break; } } @@ -344,65 +330,59 @@ static void WriteAllLocations() { void SpoilerLog_Write() { auto ctx = Rando::Context::GetInstance(); - try { - jsonData.clear(); - hintedLocations.clear(); - - jsonData["version"] = (char*)gBuildVersion; - jsonData["fileType"] = FILE_TYPE_SPOILER; - jsonData["git_branch"] = (char*)gGitBranch; - jsonData["git_commit"] = (char*)gGitCommitHash; - jsonData["seed"] = SafeJsonString(ctx->GetSeedString()); - jsonData["finalSeed"] = ctx->GetSeed(); - - // Write Hash - int index = 0; - for (uint8_t seed_value : ctx->hashIconIndexes) { - jsonData["file_hash"][index] = seed_value; - index++; - } + jsonData.clear(); - WriteSettings(); - WriteExcludedLocations(); - WriteStartingInventory(); - WriteEnabledTricks(); - WriteMasterQuestDungeons(); - WriteChosenOptions(); - WritePlaythrough(); + jsonData["version"] = (char*)gBuildVersion; + jsonData["fileType"] = FILE_TYPE_SPOILER; + jsonData["git_branch"] = (char*)gGitBranch; + jsonData["git_commit"] = (char*)gGitCommitHash; + jsonData["seed"] = ctx->GetSeedString(); + jsonData["finalSeed"] = ctx->GetSeed(); - ctx->playthroughLocations.clear(); - ctx->playthroughBeatable = false; + // Write Hash + int index = 0; + for (uint8_t seed_value : ctx->hashIconIndexes) { + jsonData["file_hash"][index] = seed_value; + index++; + } - ctx->WriteHintJson(jsonData); - WriteShuffledEntrances(); - WriteAllLocations(); + WriteSettings(); + WriteExcludedLocations(); + WriteStartingInventory(); + WriteEnabledTricks(); + WriteMasterQuestDungeons(); + WriteChosenOptions(); + WritePlaythrough(); - if (!std::filesystem::exists(Ship::Context::GetPathRelativeToAppDirectory("Randomizer"))) { - std::filesystem::create_directory(Ship::Context::GetPathRelativeToAppDirectory("Randomizer")); - } + ctx->playthroughLocations.clear(); + ctx->playthroughBeatable = false; - std::string jsonString = jsonData.dump(4); - std::ostringstream fileNameStream; - for (uint8_t i = 0; i < ctx->hashIconIndexes.size(); i++) { - if (i) { - fileNameStream << '-'; - } - if (ctx->hashIconIndexes[i] < 10) { - fileNameStream << '0'; - } - fileNameStream << std::to_string(ctx->hashIconIndexes[i]); + ctx->WriteHintJson(jsonData); + WriteShuffledEntrances(); + WriteAllLocations(); + + if (!std::filesystem::exists(Ship::Context::GetPathRelativeToAppDirectory("Randomizer"))) { + std::filesystem::create_directory(Ship::Context::GetPathRelativeToAppDirectory("Randomizer")); + } + + std::string jsonString = jsonData.dump(4); + std::ostringstream fileNameStream; + for (uint8_t i = 0; i < ctx->hashIconIndexes.size(); i++) { + if (i) { + fileNameStream << '-'; + } + if (ctx->hashIconIndexes[i] < 10) { + fileNameStream << '0'; } - std::string fileName = fileNameStream.str(); - std::ofstream jsonFile(Ship::Context::GetPathRelativeToAppDirectory( - (std::string("Randomizer/") + fileName + std::string(".json")).c_str())); - jsonFile << std::setw(4) << jsonString << std::endl; - jsonFile.close(); - - CVarSetString(CVAR_GENERAL("SpoilerLog"), - (std::string("./Randomizer/") + fileName + std::string(".json")).c_str()); - } catch (const std::exception& e) { - SPDLOG_ERROR("SpoilerLog_Write failed: {}", e.what()); + fileNameStream << std::to_string(ctx->hashIconIndexes[i]); } + std::string fileName = fileNameStream.str(); + std::ofstream jsonFile(Ship::Context::GetPathRelativeToAppDirectory( + (std::string("Randomizer/") + fileName + std::string(".json")).c_str())); + jsonFile << std::setw(4) << jsonString << std::endl; + jsonFile.close(); + + CVarSetString(CVAR_GENERAL("SpoilerLog"), (std::string("./Randomizer/") + fileName + std::string(".json")).c_str()); } void PlacementLog_Msg(std::string_view msg) { diff --git a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp index 2e207e8436e..d52dba31cbc 100644 --- a/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/ItemMessages.cpp @@ -11,136 +11,16 @@ #include "soh/Enhancements/randomizer/Traps.h" #include "soh/Enhancements/randomizer/item.h" #include "soh/Enhancements/randomizer/randomizer.h" -#include "soh/Enhancements/randomizer/split_songs.h" -#include "soh/Enhancements/randomizer/static_data.h" #include "soh/ShipInit.hpp" #include -#include +#include extern "C" { #include #include #include "z64item.h" -#include "z64player.h" -#include "textures/icon_item_static/icon_item_static.h" extern PlayState* gPlayState; -GetItemEntry ItemTable_Retrieve(int16_t getItemID); -GetItemEntry ItemTable_RetrieveEntry(s16 modIndex, s16 getItemID); -} - -// Progressive split songs use TEXT_RANDOMIZER_CUSTOM_ITEM with gSongNoteTex (16x24 IA8). -static bool sTextboxSongNoteIcon = false; -static u8 sTextboxSongNotePrimR = 255; -static u8 sTextboxSongNotePrimG = 255; -static u8 sTextboxSongNotePrimB = 255; - -extern "C" bool Rando_TextboxSongNoteIconActive(void) { - return sTextboxSongNoteIcon; -} - -static bool IsSongNoteTexPath(const char* path) { - return path != nullptr && strstr(path, "gSongNoteTex") != nullptr; -} - -static const Rando::SplitSongDef* GetActiveSplitSongDef(Player* player) { - if (!IS_RANDO || player->getItemEntry.modIndex != MOD_RANDOMIZER) { - return nullptr; - } - - const RandomizerGet rgEnum = static_cast(player->getItemEntry.getItemId); - if (Rando::SplitSongs::IsProgressiveSong(rgEnum)) { - return Rando::SplitSongs::GetSongDefFromProgressive(rgEnum); - } - return Rando::SplitSongs::GetSongDefFromFullSong(rgEnum); -} - -static int GetKaleidoSongNoteColorIndex(Rando::SplitSongId id) { - switch (id) { - case Rando::SPLIT_SONG_MINUET_OF_FOREST: - return 0; - case Rando::SPLIT_SONG_BOLERO_OF_FIRE: - return 1; - case Rando::SPLIT_SONG_SERENADE_OF_WATER: - return 2; - case Rando::SPLIT_SONG_REQUIEM_OF_SPIRIT: - return 3; - case Rando::SPLIT_SONG_NOCTURNE_OF_SHADOW: - return 4; - case Rando::SPLIT_SONG_PRELUDE_OF_LIGHT: - return 5; - case Rando::SPLIT_SONG_ZELDAS_LULLABY: - return 6; - case Rando::SPLIT_SONG_EPONAS_SONG: - return 7; - case Rando::SPLIT_SONG_SARIAS_SONG: - return 8; - case Rando::SPLIT_SONG_SUNS_SONG: - return 9; - case Rando::SPLIT_SONG_SONG_OF_TIME: - return 10; - case Rando::SPLIT_SONG_SONG_OF_STORMS: - return 11; - default: - return -1; - } -} - -// Quest-status song note tints (z_kaleido_collect.c D_8082A164/17C/194), indexed by questItem - QUEST_SONG_MINUET. -static void SetTextboxSongNotePrimColor(const Rando::SplitSongDef* def) { - static constexpr s16 kR[] = { 150, 255, 100, 255, 255, 255, 255, 255, 255, 255, 255, 255 }; - static constexpr s16 kG[] = { 255, 80, 150, 160, 100, 240, 255, 255, 255, 255, 255, 255 }; - static constexpr s16 kB[] = { 100, 40, 255, 0, 255, 100, 255, 255, 255, 255, 255, 255 }; - - const int idx = GetKaleidoSongNoteColorIndex(def->id); - if (idx < 0) { - sTextboxSongNotePrimR = sTextboxSongNotePrimG = sTextboxSongNotePrimB = 255; - return; - } - - sTextboxSongNotePrimR = static_cast(kR[idx]); - sTextboxSongNotePrimG = static_cast(kG[idx]); - sTextboxSongNotePrimB = static_cast(kB[idx]); -} - -static const char* GetSplitSongTextboxIconPath(Player* player) { - const Rando::SplitSongDef* def = GetActiveSplitSongDef(player); - if (def == nullptr) { - return nullptr; - } - - const ItemID songItemId = static_cast(Rando::StaticData::RetrieveItem(def->fullSong).GetItemID()); - return static_cast(gItemIcons[songItemId]); -} - -static bool TryBuildSongItemMessage(Player* player, CustomMessage& msg) { - if (!IS_RANDO || player->getItemEntry.modIndex != MOD_RANDOMIZER) { - return false; - } - - const RandomizerGet rgEnum = static_cast(player->getItemEntry.getItemId); - const Rando::SplitSongDef* def = nullptr; - if (Rando::SplitSongs::IsProgressiveSong(rgEnum)) { - def = Rando::SplitSongs::GetSongDefFromProgressive(rgEnum); - } else { - def = Rando::SplitSongs::GetSongDefFromFullSong(rgEnum); - } - if (def == nullptr) { - return false; - } - - RandomizerGet nameRg = def->fullSong; - if (Rando::SplitSongs::IsProgressiveSong(rgEnum) && !Rando::SplitSongs::HasFullSong(def->id) && - !Rando::SplitSongs::HasSplitPart(def->id)) { - nameRg = def->progressive; - } - - const auto& nm = Rando::StaticData::RetrieveItem(nameRg).GetName(); - CustomMessage getItemText(nm.GetEnglish(), nm.GetGerman(), nm.GetFrench(), TEXTBOX_TYPE_BLUE, TEXTBOX_POS_BOTTOM); - // ITEM_CUSTOM routes through LoadCustomItemIcon; song note uses IA8 16x24 (not vanilla ITEM_SONG_* 32x32 RGBA). - getItemText.AutoFormat(ITEM_CUSTOM); - msg = getItemText; - return true; } void BuildTriforcePieceMessage(CustomMessage& msg) { @@ -189,10 +69,6 @@ void BuildTriforcePieceMessage(CustomMessage& msg) { } void BuildCustomItemMessage(Player* player, CustomMessage& msg) { - if (TryBuildSongItemMessage(player, msg)) { - return; - } - int16_t rgid; msg = CustomMessage("You found [[article]][[color]][[name]]%w!", "Du erhältst [[article]][[color]][[name]]%w gefunden!", @@ -220,37 +96,17 @@ void LoadCustomItemIcon(bool displayAsEnglish) { Player* player = GET_PLAYER(gPlayState); const char* customIcon = nullptr; CustomIconSize iconSize = ICON_SIZE_32; - sTextboxSongNoteIcon = false; - if (player->getItemEntry.objectId != OBJECT_INVALID) { RandomizerGet rgid = static_cast(player->getItemEntry.getItemId); customIcon = Rando::StaticData::RetrieveItem(rgid).GetCustomIcon(); iconSize = Rando::StaticData::RetrieveItem(rgid).GetCustomIconSize(); } - if (customIcon == nullptr) { - customIcon = GetSplitSongTextboxIconPath(player); - if (customIcon != nullptr) { - iconSize = ICON_SIZE_24; - } - } - if (customIcon != nullptr) { static int16_t sIconItem32XOffsets[] = { 74, 74, 74, 54 }; static int16_t sIconItem24XOffsets[] = { 72, 72, 72, 50 }; MessageContext* msgCtx = &gPlayState->msgCtx; uint8_t language = displayAsEnglish ? LANGUAGE_ENG : gSaveContext.language; - if (IsSongNoteTexPath(customIcon)) { - sTextboxSongNoteIcon = true; - const Rando::SplitSongDef* def = GetActiveSplitSongDef(player); - if (def != nullptr) { - SetTextboxSongNotePrimColor(def); - } else { - sTextboxSongNotePrimR = sTextboxSongNotePrimG = sTextboxSongNotePrimB = 255; - } - R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem24XOffsets[language]; - R_TEXTBOX_ICON_YPOS = (R_TEXTBOX_Y + 10) + 10; - R_TEXTBOX_ICON_SIZE = 24; - } else if (iconSize == ICON_SIZE_32) { + if (iconSize == ICON_SIZE_32) { R_TEXTBOX_ICON_XPOS = R_TEXT_INIT_XPOS - sIconItem32XOffsets[language]; R_TEXTBOX_ICON_YPOS = (R_TEXTBOX_Y + 10) + 6; R_TEXTBOX_ICON_SIZE = 32; @@ -268,34 +124,20 @@ void LoadCustomItemIcon(bool displayAsEnglish) { void DrawCustomItemIcon(Gfx** p) { Gfx* gfx = *p; MessageContext* msgCtx = &gPlayState->msgCtx; - if (sTextboxSongNoteIcon) { - gDPSetPrimColor(gfx++, 0, 0, sTextboxSongNotePrimR, sTextboxSongNotePrimG, sTextboxSongNotePrimB, - msgCtx->textColorAlpha); - gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_IA, - G_IM_SIZ_8b, 16, 24, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, + Player* player = GET_PLAYER(gPlayState); + CustomIconSize iconSize = ICON_SIZE_32; + if (player->getItemEntry.objectId != OBJECT_INVALID) { + RandomizerGet rgid = static_cast(player->getItemEntry.getItemId); + iconSize = Rando::StaticData::RetrieveItem(rgid).GetCustomIconSize(); + } + if (iconSize == ICON_SIZE_24) { + gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, + G_IM_SIZ_32b, 24, 24, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); - gSPTextureRectangle(gfx++, (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS) << 2, R_TEXTBOX_ICON_YPOS << 2, - (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS + 16) << 2, (R_TEXTBOX_ICON_YPOS + 24) << 2, - G_TX_RENDERTILE, 0, 0, 1 << 10, 1 << 10); - gDPPipeSync(gfx++); - gDPSetCombineLERP(gfx++, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, - 0); } else { - Player* player = GET_PLAYER(gPlayState); - CustomIconSize iconSize = ICON_SIZE_32; - if (player->getItemEntry.objectId != OBJECT_INVALID) { - RandomizerGet rgid = static_cast(player->getItemEntry.getItemId); - iconSize = Rando::StaticData::RetrieveItem(rgid).GetCustomIconSize(); - } - if (iconSize == ICON_SIZE_24) { - gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, - G_IM_SIZ_32b, 24, 24, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, - G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); - } else { - gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, - G_IM_SIZ_32b, 32, 32, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, - G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); - } + gDPLoadTextureBlock(gfx++, (uintptr_t)msgCtx->textboxSegment + MESSAGE_STATIC_TEX_SIZE, G_IM_FMT_RGBA, + G_IM_SIZ_32b, 32, 32, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, + G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); } *p = gfx; } @@ -434,4 +276,4 @@ void RegisterCustomIconHooks() { }); } -static RegisterShipInitFunc customIconInitFunc(RegisterCustomIconHooks, { "IS_RANDO" }); +static RegisterShipInitFunc customIconInitFunc(RegisterCustomIconHooks, { "IS_RANDO" }); \ No newline at end of file diff --git a/soh/soh/Enhancements/randomizer/hint.cpp b/soh/soh/Enhancements/randomizer/hint.cpp index 8ac6e6d60e8..4f692373f79 100644 --- a/soh/soh/Enhancements/randomizer/hint.cpp +++ b/soh/soh/Enhancements/randomizer/hint.cpp @@ -2,7 +2,6 @@ #include "map" #include "string" #include "SeedContext.h" -#include "soh/util.h" #include #include "static_data.h" #include "3drando/random.hpp" @@ -376,34 +375,31 @@ oJson Hint::toJSON() { auto ctx = Rando::Context::GetInstance(); nlohmann::ordered_json log = {}; if (enabled) { - log["type"] = SohUtils::SanitizeUtf8(StaticData::hintTypeNames[hintType].GetForCurrentLanguage(MF_CLEAN)); + log["type"] = StaticData::hintTypeNames[hintType].GetForCurrentLanguage(MF_CLEAN); std::vector hintMessages = GetAllMessageStrings(MF_CLEAN); if (hintMessages.size() == 1) { - log["message"] = SohUtils::SanitizeUtf8(hintMessages[0]); + log["message"] = hintMessages[0]; } else if (hintMessages.size() > 1) { - std::vector sanitizedMessages; - sanitizedMessages.reserve(hintMessages.size()); - for (const std::string& message : hintMessages) { - sanitizedMessages.push_back(SohUtils::SanitizeUtf8(message)); - } - log["messages"] = sanitizedMessages; + log["messages"] = hintMessages; } if (distribution != "") { - log["distribution"] = SohUtils::SanitizeUtf8(distribution); + log["distribution"] = distribution; } if (hintType != HINT_TYPE_FOOLISH) { if (!(StaticData::staticHintInfoMap.contains(ownKey) && StaticData::staticHintInfoMap[ownKey].targetChecks.size() > 0)) { if (locations.size() == 1) { - log["location"] = SohUtils::SanitizeUtf8(StaticData::GetLocation(locations[0])->GetName()); + log["location"] = StaticData::GetLocation(locations[0]) + ->GetName(); // RANDOTODO change to CustomMessage when VB is done; } else if (locations.size() > 1) { // If we have defaults, no need to write more std::vector locStrings = {}; for (size_t c = 0; c < locations.size(); c++) { - locStrings.push_back(SohUtils::SanitizeUtf8(StaticData::GetLocation(locations[c])->GetName())); + locStrings.push_back(StaticData::GetLocation(locations[c]) + ->GetName()); // RANDOTODO change to CustomMessage when VB is done } log["locations"] = locStrings; } @@ -412,11 +408,15 @@ oJson Hint::toJSON() { if (!(StaticData::staticHintInfoMap.contains(ownKey) && StaticData::staticHintInfoMap[ownKey].targetItems.size() > 0)) { if (items.size() == 1) { - log["item"] = SohUtils::SanitizeUtf8(StaticData::GetItemTable()[items[0]].GetName().GetEnglish()); + log["item"] = StaticData::GetItemTable()[items[0]] + .GetName() + .GetEnglish(); // RANDOTODO change to CustomMessage; } else if (items.size() > 1) { std::vector itemStrings = {}; for (size_t c = 0; c < items.size(); c++) { - itemStrings.push_back(SohUtils::SanitizeUtf8(StaticData::GetItemTable()[items[c]].GetName().GetEnglish())); + itemStrings.push_back(StaticData::GetItemTable()[items[c]] + .GetName() + .GetEnglish()); // RANDOTODO change to CustomMessage } log["items"] = itemStrings; } @@ -433,16 +433,16 @@ oJson Hint::toJSON() { } } if (areas.size() == 1) { - log["area"] = SohUtils::SanitizeUtf8( - StaticData::hintTextTable[StaticData::areaNames[areas[0]]].GetClear().GetForCurrentLanguage(MF_CLEAN)); + log["area"] = + StaticData::hintTextTable[StaticData::areaNames[areas[0]]].GetClear().GetForCurrentLanguage(MF_CLEAN); } else if (areas.size() > 0 && !(StaticData::staticHintInfoMap.contains(ownKey) && StaticData::staticHintInfoMap[ownKey].targetChecks.size() > 0)) { // If we got locations from defaults, areas are derived from them and don't need logging std::vector areaStrings = {}; for (size_t c = 0; c < areas.size(); c++) { - areaStrings.push_back(SohUtils::SanitizeUtf8( + areaStrings.push_back( StaticData::hintTextTable[StaticData::areaNames[areas[c]]].GetClear().GetForCurrentLanguage( - MF_CLEAN))); + MF_CLEAN)); } log["areas"] = areaStrings; } @@ -458,12 +458,11 @@ oJson Hint::toJSON() { } if (trials.size() == 1) { - log["trial"] = SohUtils::SanitizeUtf8(ctx->GetTrial(trials[0])->GetName().GetForCurrentLanguage(MF_CLEAN)); + log["trial"] = ctx->GetTrial(trials[0])->GetName().GetForCurrentLanguage(MF_CLEAN); } else if (trials.size() > 0) { std::vector trialStrings = {}; for (size_t c = 0; c < trials.size(); c++) { - trialStrings.push_back( - SohUtils::SanitizeUtf8(ctx->GetTrial(trials[c])->GetName().GetForCurrentLanguage(MF_CLEAN))); + trialStrings.push_back(ctx->GetTrial(trials[c])->GetName().GetForCurrentLanguage(MF_CLEAN)); } log["trials"] = trialStrings; } @@ -506,7 +505,7 @@ void Hint::logHint(oJson& jsonData) { if (enabled && (!(staticHint && (hintType == HINT_TYPE_ITEM) && ctx->GetOption(RSK_HINT_CLARITY).Is(RO_HINT_CLARITY_CLEAR)))) { // skip if not enabled or if a static hint with no possible variance - jsonData[logMap][SohUtils::SanitizeUtf8(Rando::StaticData::hintNames[ownKey].GetForCurrentLanguage(MF_CLEAN))] = toJSON(); + jsonData[logMap][Rando::StaticData::hintNames[ownKey].GetForCurrentLanguage(MF_CLEAN)] = toJSON(); } } diff --git a/soh/soh/Enhancements/randomizer/item.cpp b/soh/soh/Enhancements/randomizer/item.cpp index 431aa3fa863..409c50424f2 100644 --- a/soh/soh/Enhancements/randomizer/item.cpp +++ b/soh/soh/Enhancements/randomizer/item.cpp @@ -48,16 +48,10 @@ Item::Item(const RandomizerGet randomizerGet_, Text name_, const ItemType type_, article(std::move(article_)), color(std::move(color_)), progressive(progressive_), price(price_) { } -static bool UsingLogicSimulationBuffer(const std::shared_ptr& logic) { - return logic != nullptr && logic->mSaveContext != nullptr && logic->mSaveContext != &gSaveContext; -} - void Item::ApplyEffect() const { auto ctx = Rando::Context::GetInstance(); auto logic = ctx->GetLogic(); - if (SplitSongs::IsProgressiveSong(randomizerGet) && UsingLogicSimulationBuffer(logic)) { - SplitSongs::ApplyProgressiveEffectToLogicScratch(logic.get(), randomizerGet, true); - } else if (!logic->CalculatingAvailableChecks) { + if (!logic->CalculatingAvailableChecks) { logic->ApplyItemEffect(StaticData::RetrieveItem(randomizerGet), true); } logic->Set(logicVal, true); @@ -66,9 +60,7 @@ void Item::ApplyEffect() const { void Item::UndoEffect() const { auto ctx = Rando::Context::GetInstance(); auto logic = ctx->GetLogic(); - if (SplitSongs::IsProgressiveSong(randomizerGet) && UsingLogicSimulationBuffer(logic)) { - SplitSongs::ApplyProgressiveEffectToLogicScratch(logic.get(), randomizerGet, false); - } else if (!logic->CalculatingAvailableChecks) { + if (!logic->CalculatingAvailableChecks) { logic->ApplyItemEffect(StaticData::RetrieveItem(randomizerGet), false); } logic->Set(logicVal, false); diff --git a/soh/soh/Enhancements/randomizer/item_list.cpp b/soh/soh/Enhancements/randomizer/item_list.cpp index 5ab3f9bbdc5..5bd1513f963 100644 --- a/soh/soh/Enhancements/randomizer/item_list.cpp +++ b/soh/soh/Enhancements/randomizer/item_list.cpp @@ -73,18 +73,18 @@ void Rando::StaticData::InitItemTable() { itemTable[RG_PROGRESSIVE_MAGIC_METER] = Item(RG_PROGRESSIVE_MAGIC_METER, Text{ "Progressive Magic Meter", "Jauge de Magie (prog.)", "Progressives Magisches Maß" }, ITEMTYPE_ITEM, 0x8A, true, LOGIC_PROGRESSIVE_MAGIC, RHT_PROGRESSIVE_MAGIC_METER, ITEM_CATEGORY_MAJOR, {"a ", "une ", "ein "}, "%g", true).CustomIcon(gQuestIconMagicJarBigTex, ICON_SIZE_24); itemTable[RG_PROGRESSIVE_OCARINA] = Item(RG_PROGRESSIVE_OCARINA, Text{ "Progressive Ocarina", "Ocarina (prog.)", "Progressive Okarina" }, ITEMTYPE_ITEM, 0x8B, true, LOGIC_PROGRESSIVE_OCARINA, RHT_PROGRESSIVE_OCARINA, ITEM_CATEGORY_MAJOR, {"a ", "un ", "eine "}, "%g", true); itemTable[RG_PROGRESSIVE_GORONSWORD] = Item(RG_PROGRESSIVE_GORONSWORD, Text{ "Progressive Goron Sword", "Épée Goron (prog.)", "Progressives Goronen-Schwert" }, ITEMTYPE_ITEM, 0xD4, true, LOGIC_PROGRESSIVE_GIANT_KNIFE, RHT_PROGRESSIVE_GORONSWORD, ITEM_CATEGORY_MAJOR, {"a ", "une ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_ZELDAS_LULLABY] = Item(RG_PROGRESSIVE_ZELDAS_LULLABY, Text{ "Progressive Zelda's Lullaby", "Berceuse de Zelda (prog.)", "Progressives Zeldas Wiegenlied" }, ITEMTYPE_SONG, 0xE1, true, LOGIC_NONE, RHT_ZELDAS_LULLABY, RG_PROGRESSIVE_ZELDAS_LULLABY, OBJECT_GI_MELODY, GID_SONG_ZELDA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); - itemTable[RG_PROGRESSIVE_EPONAS_SONG] = Item(RG_PROGRESSIVE_EPONAS_SONG, Text{ "Progressive Epona's Song", "Chant d'Epona (prog.)", "Progressives Eponas Lied" }, ITEMTYPE_SONG, 0xE2, true, LOGIC_NONE, RHT_EPONAS_SONG, RG_PROGRESSIVE_EPONAS_SONG, OBJECT_GI_MELODY, GID_SONG_EPONA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); - itemTable[RG_PROGRESSIVE_SARIAS_SONG] = Item(RG_PROGRESSIVE_SARIAS_SONG, Text{ "Progressive Saria's Song", "Chant de Saria (prog.)", "Progressives Salias Lied" }, ITEMTYPE_SONG, 0xE3, true, LOGIC_NONE, RHT_SARIAS_SONG, RG_PROGRESSIVE_SARIAS_SONG, OBJECT_GI_MELODY, GID_SONG_SARIA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); - itemTable[RG_PROGRESSIVE_SUNS_SONG] = Item(RG_PROGRESSIVE_SUNS_SONG, Text{ "Progressive Sun's Song", "Chant du Soleil (prog.)", "Progressives Hymne der Sonne" }, ITEMTYPE_SONG, 0xE4, true, LOGIC_NONE, RHT_SUNS_SONG, RG_PROGRESSIVE_SUNS_SONG, OBJECT_GI_MELODY, GID_SONG_SUN, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); - itemTable[RG_PROGRESSIVE_SONG_OF_TIME] = Item(RG_PROGRESSIVE_SONG_OF_TIME, Text{ "Progressive Song of Time", "Chant du Temps (prog.)", "Progressives Hymne der Zeit" }, ITEMTYPE_SONG, 0xE5, true, LOGIC_NONE, RHT_SONG_OF_TIME, RG_PROGRESSIVE_SONG_OF_TIME, OBJECT_GI_MELODY, GID_SONG_TIME, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); - itemTable[RG_PROGRESSIVE_SONG_OF_STORMS] = Item(RG_PROGRESSIVE_SONG_OF_STORMS, Text{ "Progressive Song of Storms", "Chant des Tempêtes (prog.)", "Progressives Hymne des Sturms" }, ITEMTYPE_SONG, 0xE6, true, LOGIC_NONE, RHT_SONG_OF_STORMS, RG_PROGRESSIVE_SONG_OF_STORMS, OBJECT_GI_MELODY, GID_SONG_STORM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); - itemTable[RG_PROGRESSIVE_MINUET_OF_FOREST] = Item(RG_PROGRESSIVE_MINUET_OF_FOREST, Text{ "Progressive Minuet of Forest", "Menuet des Bois (prog.)", "Progressives Menuett des Waldes" }, ITEMTYPE_SONG, 0xE7, true, LOGIC_NONE, RHT_MINUET_OF_FOREST, RG_PROGRESSIVE_MINUET_OF_FOREST, OBJECT_GI_MELODY, GID_SONG_MINUET, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); - itemTable[RG_PROGRESSIVE_BOLERO_OF_FIRE] = Item(RG_PROGRESSIVE_BOLERO_OF_FIRE, Text{ "Progressive Bolero of Fire", "Boléro du Feu (prog.)", "Progressives Bolero des Feuers" }, ITEMTYPE_SONG, 0xE8, true, LOGIC_NONE, RHT_BOLERO_OF_FIRE, RG_PROGRESSIVE_BOLERO_OF_FIRE, OBJECT_GI_MELODY, GID_SONG_BOLERO, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); - itemTable[RG_PROGRESSIVE_SERENADE_OF_WATER] = Item(RG_PROGRESSIVE_SERENADE_OF_WATER, Text{ "Progressive Serenade of Water", "Sérénade de l'Eau (prog.)", "Progressive Serenade des Wassers" }, ITEMTYPE_SONG, 0xE9, true, LOGIC_NONE, RHT_SERENADE_OF_WATER, RG_PROGRESSIVE_SERENADE_OF_WATER, OBJECT_GI_MELODY, GID_SONG_SERENADE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); - itemTable[RG_PROGRESSIVE_REQUIEM_OF_SPIRIT] = Item(RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, Text{ "Progressive Requiem of Spirit", "Requiem des Esprits (prog.)", "Progressives Requiem der Geister" }, ITEMTYPE_SONG, 0xEA, true, LOGIC_NONE, RHT_REQUIEM_OF_SPIRIT, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, OBJECT_GI_MELODY, GID_SONG_REQUIEM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); - itemTable[RG_PROGRESSIVE_NOCTURNE_OF_SHADOW] = Item(RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, Text{ "Progressive Nocturne of Shadow", "Nocturne de l'Ombre (prog.)", "Progressive Nocturne des Schattens" }, ITEMTYPE_SONG, 0xEB, true, LOGIC_NONE, RHT_NOCTURNE_OF_SHADOW, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, OBJECT_GI_MELODY, GID_SONG_NOCTURNE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); - itemTable[RG_PROGRESSIVE_PRELUDE_OF_LIGHT] = Item(RG_PROGRESSIVE_PRELUDE_OF_LIGHT, Text{ "Progressive Prelude of Light", "Prélude de la Lumière (prog.)", "Progressive Kantate des Lichts" }, ITEMTYPE_SONG, 0xEC, true, LOGIC_NONE, RHT_PRELUDE_OF_LIGHT, RG_PROGRESSIVE_PRELUDE_OF_LIGHT, OBJECT_GI_MELODY, GID_SONG_PRELUDE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true).CustomIcon(gSongNoteTex, ICON_SIZE_24); + itemTable[RG_PROGRESSIVE_ZELDAS_LULLABY] = Item(RG_PROGRESSIVE_ZELDAS_LULLABY, Text{ "Progressive Zelda's Lullaby", "Berceuse de Zelda (prog.)", "Progressives Zeldas Wiegenlied" }, ITEMTYPE_SONG, 0xE1, true, LOGIC_NONE, RHT_ZELDAS_LULLABY, RG_PROGRESSIVE_ZELDAS_LULLABY, OBJECT_GI_MELODY, GID_SONG_ZELDA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_EPONAS_SONG] = Item(RG_PROGRESSIVE_EPONAS_SONG, Text{ "Progressive Epona's Song", "Chant d'Epona (prog.)", "Progressives Eponas Lied" }, ITEMTYPE_SONG, 0xE2, true, LOGIC_NONE, RHT_EPONAS_SONG, RG_PROGRESSIVE_EPONAS_SONG, OBJECT_GI_MELODY, GID_SONG_EPONA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SARIAS_SONG] = Item(RG_PROGRESSIVE_SARIAS_SONG, Text{ "Progressive Saria's Song", "Chant de Saria (prog.)", "Progressives Salias Lied" }, ITEMTYPE_SONG, 0xE3, true, LOGIC_NONE, RHT_SARIAS_SONG, RG_PROGRESSIVE_SARIAS_SONG, OBJECT_GI_MELODY, GID_SONG_SARIA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SUNS_SONG] = Item(RG_PROGRESSIVE_SUNS_SONG, Text{ "Progressive Sun's Song", "Chant du Soleil (prog.)", "Progressives Hymne der Sonne" }, ITEMTYPE_SONG, 0xE4, true, LOGIC_NONE, RHT_SUNS_SONG, RG_PROGRESSIVE_SUNS_SONG, OBJECT_GI_MELODY, GID_SONG_SUN, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SONG_OF_TIME] = Item(RG_PROGRESSIVE_SONG_OF_TIME, Text{ "Progressive Song of Time", "Chant du Temps (prog.)", "Progressives Hymne der Zeit" }, ITEMTYPE_SONG, 0xE5, true, LOGIC_NONE, RHT_SONG_OF_TIME, RG_PROGRESSIVE_SONG_OF_TIME, OBJECT_GI_MELODY, GID_SONG_TIME, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SONG_OF_STORMS] = Item(RG_PROGRESSIVE_SONG_OF_STORMS, Text{ "Progressive Song of Storms", "Chant des Tempêtes (prog.)", "Progressives Hymne des Sturms" }, ITEMTYPE_SONG, 0xE6, true, LOGIC_NONE, RHT_SONG_OF_STORMS, RG_PROGRESSIVE_SONG_OF_STORMS, OBJECT_GI_MELODY, GID_SONG_STORM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_MINUET_OF_FOREST] = Item(RG_PROGRESSIVE_MINUET_OF_FOREST, Text{ "Progressive Minuet of Forest", "Menuet des Bois (prog.)", "Progressives Menuett des Waldes" }, ITEMTYPE_SONG, 0xE7, true, LOGIC_NONE, RHT_MINUET_OF_FOREST, RG_PROGRESSIVE_MINUET_OF_FOREST, OBJECT_GI_MELODY, GID_SONG_MINUET, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_BOLERO_OF_FIRE] = Item(RG_PROGRESSIVE_BOLERO_OF_FIRE, Text{ "Progressive Bolero of Fire", "Boléro du Feu (prog.)", "Progressives Bolero des Feuers" }, ITEMTYPE_SONG, 0xE8, true, LOGIC_NONE, RHT_BOLERO_OF_FIRE, RG_PROGRESSIVE_BOLERO_OF_FIRE, OBJECT_GI_MELODY, GID_SONG_BOLERO, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SERENADE_OF_WATER] = Item(RG_PROGRESSIVE_SERENADE_OF_WATER, Text{ "Progressive Serenade of Water", "Sérénade de l'Eau (prog.)", "Progressive Serenade des Wassers" }, ITEMTYPE_SONG, 0xE9, true, LOGIC_NONE, RHT_SERENADE_OF_WATER, RG_PROGRESSIVE_SERENADE_OF_WATER, OBJECT_GI_MELODY, GID_SONG_SERENADE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_REQUIEM_OF_SPIRIT] = Item(RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, Text{ "Progressive Requiem of Spirit", "Requiem des Esprits (prog.)", "Progressives Requiem der Geister" }, ITEMTYPE_SONG, 0xEA, true, LOGIC_NONE, RHT_REQUIEM_OF_SPIRIT, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, OBJECT_GI_MELODY, GID_SONG_REQUIEM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_NOCTURNE_OF_SHADOW] = Item(RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, Text{ "Progressive Nocturne of Shadow", "Nocturne de l'Ombre (prog.)", "Progressive Nocturne des Schattens" }, ITEMTYPE_SONG, 0xEB, true, LOGIC_NONE, RHT_NOCTURNE_OF_SHADOW, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, OBJECT_GI_MELODY, GID_SONG_NOCTURNE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_PRELUDE_OF_LIGHT] = Item(RG_PROGRESSIVE_PRELUDE_OF_LIGHT, Text{ "Progressive Prelude of Light", "Prélude de la Lumière (prog.)", "Progressive Kantate des Lichts" }, ITEMTYPE_SONG, 0xEC, true, LOGIC_NONE, RHT_PRELUDE_OF_LIGHT, RG_PROGRESSIVE_PRELUDE_OF_LIGHT, OBJECT_GI_MELODY, GID_SONG_PRELUDE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); // Bottles itemTable[RG_EMPTY_BOTTLE] = Item(RG_EMPTY_BOTTLE, Text{ "Empty Bottle", "Bouteille Vide", "Leere Flasche" }, ITEMTYPE_ITEM, GI_BOTTLE, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_MILK, ITEM_BOTTLE, OBJECT_GI_BOTTLE, GID_BOTTLE, 0x42, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"an ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_MILK] = Item(RG_BOTTLE_WITH_MILK, Text{ "Bottle with Milk", "Bouteille avec du Lait", "Flasche mit Milch" }, ITEMTYPE_ITEM, GI_MILK_BOTTLE, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_MILK, ITEM_MILK_BOTTLE, OBJECT_GI_MILK, GID_MILK, 0x98, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"a ", "une ", "eine "}); diff --git a/soh/soh/Enhancements/randomizer/logic.cpp b/soh/soh/Enhancements/randomizer/logic.cpp index 7ee701ab52f..d0ef8bb5278 100644 --- a/soh/soh/Enhancements/randomizer/logic.cpp +++ b/soh/soh/Enhancements/randomizer/logic.cpp @@ -2376,8 +2376,22 @@ void Logic::ApplyItemEffect(Item& item, bool state) { case ITEMTYPE_DUNGEONREWARD: case ITEMTYPE_SONG: { RandomizerGet rg = item.GetRandomizerGet(); - if (SplitSongs::IsProgressiveSong(rg) && mSaveContext != nullptr && mSaveContext != &gSaveContext) { - SplitSongs::ApplyProgressiveEffectToLogicScratch(this, rg, state); + if (SplitSongs::IsProgressiveSong(rg)) { + const SplitSongDef* def = SplitSongs::GetSongDefFromProgressive(rg); + if (def != nullptr) { + const RandomizerInf partFlag = SplitSongs::GetPartFlag(def->id); + auto qi = RandoGetToQuestItem.find(static_cast(def->fullSong)); + if (!CheckRandoInf(partFlag) && state) { + SetRandoInf(partFlag, true); + } else if (qi != RandoGetToQuestItem.end() && CheckRandoInf(partFlag) && + !CheckQuestItem(qi->second) && state) { + SetQuestItem(qi->second, true); + } else if (qi != RandoGetToQuestItem.end() && CheckQuestItem(qi->second) && !state) { + SetQuestItem(qi->second, false); + } else if (CheckRandoInf(partFlag) && !state) { + SetRandoInf(partFlag, false); + } + } break; } auto qi = RandoGetToQuestItem.find(rg); diff --git a/soh/soh/Enhancements/randomizer/split_songs.cpp b/soh/soh/Enhancements/randomizer/split_songs.cpp index ae19c9fb369..7eb2881e304 100644 --- a/soh/soh/Enhancements/randomizer/split_songs.cpp +++ b/soh/soh/Enhancements/randomizer/split_songs.cpp @@ -127,32 +127,11 @@ ItemObtainability SplitSongs::GetProgressiveSongObtainability(RandomizerGet prog return HasFullSong(def->id) ? CANT_OBTAIN_ALREADY_HAVE : CAN_OBTAIN; } -void SplitSongs::ApplyProgressiveEffectToLogicScratch(Logic* logic, RandomizerGet rg, bool state) { - if (!UsingLogicSimulationBuffer(logic)) { - return; - } - const SplitSongDef* def = GetSongDefFromProgressive(rg); - if (def == nullptr) { - return; - } - - const uint32_t partFlag = kSplitSongPartFlags[def->id]; - const auto qiIt = Logic::RandoGetToQuestItem.find(static_cast(def->fullSong)); - if (qiIt == Logic::RandoGetToQuestItem.end()) { - return; - } - - if (state) { - if (!logic->CheckRandoInf(static_cast(partFlag))) { - logic->SetRandoInf(partFlag, true); - } else { - logic->SetQuestItem(qiIt->second, true); - } - } else if (logic->CheckQuestItem(qiIt->second)) { - logic->SetQuestItem(qiIt->second, false); - } else if (logic->CheckRandoInf(static_cast(partFlag))) { - logic->SetRandoInf(partFlag, false); +RandomizerInf SplitSongs::GetPartFlag(SplitSongId id) { + if (!IsValidSplitSongId(id)) { + return RAND_INF_MAX; } + return static_cast(kSplitSongPartFlags[id]); } RandomizerGet SplitSongs::ResolveProgressiveSongStage(Logic* logic, RandomizerGet rg) { diff --git a/soh/soh/Enhancements/randomizer/split_songs.h b/soh/soh/Enhancements/randomizer/split_songs.h index eb00aebb57a..87766cac0bc 100644 --- a/soh/soh/Enhancements/randomizer/split_songs.h +++ b/soh/soh/Enhancements/randomizer/split_songs.h @@ -45,7 +45,7 @@ class SplitSongs { static void OnProgressiveSongReceived(RandomizerGet rg); static ItemObtainability GetProgressiveSongObtainability(RandomizerGet progressiveRg); - static void ApplyProgressiveEffectToLogicScratch(Logic* logic, RandomizerGet rg, bool state); + static RandomizerInf GetPartFlag(SplitSongId id); static RandomizerGet ResolveProgressiveSongStage(RandomizerGet rg); static RandomizerGet ResolveProgressiveSongStage(Logic* logic, RandomizerGet rg); }; diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp index 0bded3a11c4..67e250a58c3 100644 --- a/soh/soh/SaveManager.cpp +++ b/soh/soh/SaveManager.cpp @@ -30,16 +30,6 @@ extern "C" SaveContext gSaveContext; using namespace std::string_literals; -void SaveManager::SaveData(const std::string& name, const std::string& data) { - const std::string sanitized = SohUtils::SanitizeUtf8(data); - if (name == "") { - assert((*currentJsonContext).is_array()); - (*currentJsonContext).push_back(sanitized); - } else { - (*currentJsonContext)[name.c_str()] = sanitized; - } -} - void SaveManager::WriteSaveFile(const std::filesystem::path& savePath, const uintptr_t addr, void* dramAddr, const size_t size) { std::ofstream saveFile = std::ofstream(savePath, std::fstream::in | std::fstream::out | std::fstream::binary); @@ -1160,90 +1150,80 @@ int copy_file(const char* src, const char* dst) { void SaveManager::SaveFileThreaded(int fileNum, SaveContext* saveContext, int sectionID) { saveMtx.lock(); SPDLOG_INFO("Save File - fileNum: {}", fileNum); - - try { - // Needed for first time save, hasn't changed in forever anyway - saveBlock["version"] = 1; - if (IS_RANDO) { - saveBlock["fileType"] = FILE_TYPE_SAVE_RANDO; - } else { - saveBlock["fileType"] = FILE_TYPE_SAVE_VANILLA; - } - if (sectionID == SECTION_ID_BASE) { - for (auto& sectionHandlerPair : sectionSaveHandlers) { - auto& saveFuncInfo = sectionHandlerPair.second; - // Don't call SaveFuncs for sections that aren't tied to game save - if (!saveFuncInfo.saveWithBase || (saveFuncInfo.name == "randomizer" && !IS_RANDO)) { - continue; - } - nlohmann::json& sectionBlock = saveBlock["sections"][saveFuncInfo.name]; - sectionBlock["version"] = sectionHandlerPair.second.version; - // If any save file is loaded for medatata, or a spoiler log is loaded (not sure which at this point), there - // is still data in the "randomizer" section This clears the randomizer data block if and only if the - // section being called is "randomizer" and the current save file is not a randomizer save file. - - currentJsonContext = §ionBlock["data"]; - sectionHandlerPair.second.func(saveContext, sectionID, true); - } - } else { - SaveFuncInfo svi = sectionSaveHandlers.find(sectionID)->second; - auto& sectionName = svi.name; - auto sectionVersion = svi.version; - // If section has a parentSection, it is a subsection. Load parentSection version and set sectionBlock to parent - // string - if (svi.parentSection != -1 && svi.parentSection < sectionIndex) { - auto parentSvi = sectionSaveHandlers.find(svi.parentSection)->second; - sectionName = parentSvi.name; - sectionVersion = parentSvi.version; + // Needed for first time save, hasn't changed in forever anyway + saveBlock["version"] = 1; + if (IS_RANDO) { + saveBlock["fileType"] = FILE_TYPE_SAVE_RANDO; + } else { + saveBlock["fileType"] = FILE_TYPE_SAVE_VANILLA; + } + if (sectionID == SECTION_ID_BASE) { + for (auto& sectionHandlerPair : sectionSaveHandlers) { + auto& saveFuncInfo = sectionHandlerPair.second; + // Don't call SaveFuncs for sections that aren't tied to game save + if (!saveFuncInfo.saveWithBase || (saveFuncInfo.name == "randomizer" && !IS_RANDO)) { + continue; } - nlohmann::json& sectionBlock = saveBlock["sections"][sectionName]; - sectionBlock["version"] = sectionVersion; + nlohmann::json& sectionBlock = saveBlock["sections"][saveFuncInfo.name]; + sectionBlock["version"] = sectionHandlerPair.second.version; + // If any save file is loaded for medatata, or a spoiler log is loaded (not sure which at this point), there + // is still data in the "randomizer" section This clears the randomizer data block if and only if the + // section being called is "randomizer" and the current save file is not a randomizer save file. + currentJsonContext = §ionBlock["data"]; - svi.func(saveContext, sectionID, false); + sectionHandlerPair.second.func(saveContext, sectionID, true); + } + } else { + SaveFuncInfo svi = sectionSaveHandlers.find(sectionID)->second; + auto& sectionName = svi.name; + auto sectionVersion = svi.version; + // If section has a parentSection, it is a subsection. Load parentSection version and set sectionBlock to parent + // string + if (svi.parentSection != -1 && svi.parentSection < sectionIndex) { + auto parentSvi = sectionSaveHandlers.find(svi.parentSection)->second; + sectionName = parentSvi.name; + sectionVersion = parentSvi.version; } + nlohmann::json& sectionBlock = saveBlock["sections"][sectionName]; + sectionBlock["version"] = sectionVersion; + currentJsonContext = §ionBlock["data"]; + svi.func(saveContext, sectionID, false); + } - std::filesystem::path fileName = GetFileName(fileNum); - std::filesystem::path tempFile = GetFileTempName(fileNum); + std::filesystem::path fileName = GetFileName(fileNum); + std::filesystem::path tempFile = GetFileTempName(fileNum); - if (std::filesystem::exists(tempFile)) { - std::filesystem::remove(tempFile); - } + if (std::filesystem::exists(tempFile)) { + std::filesystem::remove(tempFile); + } #if defined(__SWITCH__) || defined(__WIIU__) - FILE* w = fopen(tempFile.c_str(), "w"); - std::string json_string = saveBlock.dump(1); - fwrite(json_string.c_str(), sizeof(char), json_string.length(), w); - fclose(w); + FILE* w = fopen(tempFile.c_str(), "w"); + std::string json_string = saveBlock.dump(1); + fwrite(json_string.c_str(), sizeof(char), json_string.length(), w); + fclose(w); #else - std::ofstream output(tempFile); - output << std::setw(1) << saveBlock << std::endl; - output.close(); + std::ofstream output(tempFile); + output << std::setw(1) << saveBlock << std::endl; + output.close(); #endif #if defined(__SWITCH__) || defined(__WIIU__) - if (std::filesystem::exists(fileName)) { - std::filesystem::remove(fileName); - } - copy_file(tempFile.c_str(), fileName.c_str()); - if (std::filesystem::exists(tempFile)) { - std::filesystem::remove(tempFile); - } + if (std::filesystem::exists(fileName)) { + std::filesystem::remove(fileName); + } + copy_file(tempFile.c_str(), fileName.c_str()); + if (std::filesystem::exists(tempFile)) { + std::filesystem::remove(tempFile); + } #else - std::filesystem::rename(tempFile, fileName); + std::filesystem::rename(tempFile, fileName); #endif - delete saveContext; - saveContext = nullptr; - InitMeta(fileNum); - GameInteractor::Instance->ExecuteHooks(fileNum, sectionID); - SPDLOG_INFO("Save File Finish - fileNum: {}", fileNum); - } catch (const std::exception& e) { - SPDLOG_ERROR("SaveFileThreaded failed for fileNum {}: {}", fileNum, e.what()); - if (saveContext != nullptr) { - delete saveContext; - } - } - + delete saveContext; + InitMeta(fileNum); + GameInteractor::Instance->ExecuteHooks(fileNum, sectionID); + SPDLOG_INFO("Save File Finish - fileNum: {}", fileNum); saveMtx.unlock(); } diff --git a/soh/soh/SaveManager.h b/soh/soh/SaveManager.h index 0e3bebacd74..80f58b915e5 100644 --- a/soh/soh/SaveManager.h +++ b/soh/soh/SaveManager.h @@ -119,8 +119,6 @@ class SaveManager { } } - void SaveData(const std::string& name, const std::string& data); - // In the SaveArrayFunc func, the name must be "" to save to the array. using SaveArrayFunc = std::function; void SaveArray(const std::string& name, const size_t size, SaveArrayFunc func); diff --git a/soh/soh/util.cpp b/soh/soh/util.cpp index fd9409b26f0..7efa4b7d160 100644 --- a/soh/soh/util.cpp +++ b/soh/soh/util.cpp @@ -765,59 +765,6 @@ std::string SohUtils::Sanitize(std::string stringValue) { return stringValue; } -std::string SohUtils::SanitizeUtf8(std::string value) { - auto isUtf8Continuation = [](unsigned char byte) { return (byte & 0xC0) == 0x80; }; - - auto utf8SequenceLength = [](unsigned char lead) -> size_t { - if (lead < 0x80) { - return 1; - } - if ((lead & 0xE0) == 0xC0) { - return 2; - } - if ((lead & 0xF0) == 0xE0) { - return 3; - } - if ((lead & 0xF8) == 0xF0) { - return 4; - } - return 0; - }; - - std::string sanitized; - sanitized.reserve(value.size()); - - for (size_t i = 0; i < value.size();) { - const unsigned char lead = static_cast(value[i]); - const size_t sequenceLength = utf8SequenceLength(lead); - - if (sequenceLength == 0 || i + sequenceLength > value.size()) { - sanitized.push_back('?'); - i++; - continue; - } - - bool valid = true; - for (size_t j = 1; j < sequenceLength; j++) { - if (!isUtf8Continuation(static_cast(value[i + j]))) { - valid = false; - break; - } - } - - if (!valid) { - sanitized.push_back('?'); - i++; - continue; - } - - sanitized.append(value, i, sequenceLength); - i += sequenceLength; - } - - return sanitized; -} - size_t SohUtils::CopyStringToCharBuffer(char* buffer, const std::string& source, const size_t maxBufferSize) { if (!source.empty() && maxBufferSize > 0) { memset(buffer, 0, maxBufferSize); diff --git a/soh/soh/util.h b/soh/soh/util.h index bd8c46fd4a7..2058a344290 100644 --- a/soh/soh/util.h +++ b/soh/soh/util.h @@ -20,9 +20,6 @@ void CopyStringToCharArray(char* destination, std::string source, size_t size); std::string Sanitize(std::string stringValue); -// Strips invalid UTF-8 sequences so JSON serializers accept the string. -std::string SanitizeUtf8(std::string value); - // Copies a string into a char buffer up to maxBufferSize characters. This does NOT insert a null terminator // on the end, as this is used for in-game messages which are not null-terminated. size_t CopyStringToCharBuffer(char* buffer, const std::string& source, size_t maxBufferSize); diff --git a/soh/src/code/z_message_PAL.c b/soh/src/code/z_message_PAL.c index 007aa66eb37..57e442676a5 100644 --- a/soh/src/code/z_message_PAL.c +++ b/soh/src/code/z_message_PAL.c @@ -840,16 +840,6 @@ f32 sFontWidths[144] = { 14.0f, // ? }; -#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" - -#ifdef __cplusplus -extern "C" { -#endif -bool Rando_TextboxSongNoteIconActive(void); -#ifdef __cplusplus -} -#endif - u16 Message_DrawItemIcon(PlayState* play, u16 itemId, Gfx** p, u16 i) { s32 pad; Gfx* gfx = *p; @@ -877,11 +867,9 @@ u16 Message_DrawItemIcon(PlayState* play, u16 itemId, Gfx** p, u16 i) { G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); } } - if (!Rando_TextboxSongNoteIconActive()) { - gSPTextureRectangle(gfx++, (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS) << 2, R_TEXTBOX_ICON_YPOS << 2, - (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS + R_TEXTBOX_ICON_SIZE) << 2, - (R_TEXTBOX_ICON_YPOS + R_TEXTBOX_ICON_SIZE) << 2, G_TX_RENDERTILE, 0, 0, 1 << 10, 1 << 10); - } + gSPTextureRectangle(gfx++, (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS) << 2, R_TEXTBOX_ICON_YPOS << 2, + (msgCtx->textPosX + R_TEXTBOX_ICON_XPOS + R_TEXTBOX_ICON_SIZE) << 2, + (R_TEXTBOX_ICON_YPOS + R_TEXTBOX_ICON_SIZE) << 2, G_TX_RENDERTILE, 0, 0, 1 << 10, 1 << 10); gDPPipeSync(gfx++); gDPSetCombineLERP(gfx++, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0); From 2edee8d0a732bdda6ad1a887c3c4f41fd9de3e4b Mon Sep 17 00:00:00 2001 From: RaccoonCloud <127690874+RaccoonCloud@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:37:48 +0100 Subject: [PATCH 22/22] Refactor split songs to PART items and static songData maps per review. --- .../randomizer/3drando/item_pool.cpp | 44 ++--- soh/soh/Enhancements/randomizer/Traps.cpp | 90 +++++----- soh/soh/Enhancements/randomizer/item.cpp | 30 ++-- soh/soh/Enhancements/randomizer/item_list.cpp | 36 ++-- soh/soh/Enhancements/randomizer/logic.cpp | 99 +++++------ .../Enhancements/randomizer/randomizer.cpp | 37 ++-- .../randomizerEnums/RandomizerGet.h | 12 ++ .../randomizerEnums/RandomizerInf.h | 24 +-- .../Enhancements/randomizer/randomizerTypes.h | 7 + .../randomizer/randomizer_item_tracker.cpp | 54 ++---- .../Enhancements/randomizer/split_songs.cpp | 164 ------------------ soh/soh/Enhancements/randomizer/split_songs.h | 53 ------ .../Enhancements/randomizer/static_data.cpp | 42 +++++ soh/soh/Enhancements/randomizer/static_data.h | 2 + 14 files changed, 258 insertions(+), 436 deletions(-) delete mode 100644 soh/soh/Enhancements/randomizer/split_songs.cpp delete mode 100644 soh/soh/Enhancements/randomizer/split_songs.h diff --git a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp index f78c9126c2b..ba2e7ade9fe 100644 --- a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp @@ -8,6 +8,7 @@ #include "random.hpp" #include "spoiler_log.hpp" #include "soh/Enhancements/randomizer/Traps.h" +#include "../static_data.h" #include "z64item.h" #include @@ -252,38 +253,29 @@ void GenerateItemPool() { // add extra songs only if song shuffle is anywhere if (ctx->GetOption(RSK_SHUFFLE_SONGS).IsNot(RO_SONG_SHUFFLE_OFF)) { bool songAnywhere = ctx->GetOption(RSK_SHUFFLE_SONGS).Is(RO_SONG_SHUFFLE_ANYWHERE); - const bool split = ctx->GetOption(RSK_SPLIT_OCARINA_SONGS).Get(); - const int defaultPlentiful = songAnywhere ? 2 : 1; - auto addShuffledSong = [&](RandomizerGet fullSong, RandomizerGet progressive, bool hasStarting) { + auto addShuffledSong = [&](RandomizerGet progSong, bool hasStarting) { if (hasStarting) { return; } - if (!split) { - AddItemToPool(fullSong, defaultPlentiful, 1, 1, 1, songAnywhere); + SongData song = Rando::StaticData::songData.at(progSong); + if (!ctx->GetOption(RSK_SPLIT_OCARINA_SONGS).Get()) { + AddItemToPool(song.realSong, songAnywhere ? 2 : 1, 1, 1, 1, songAnywhere); } else { - AddItemToPool(progressive, 3, 2, 2, 2, songAnywhere); + AddItemToPool(progSong, 3, 2, 2, 2, songAnywhere); } }; - addShuffledSong(RG_ZELDAS_LULLABY, RG_PROGRESSIVE_ZELDAS_LULLABY, - ctx->GetOption(RSK_STARTING_ZELDAS_LULLABY).Get()); - addShuffledSong(RG_EPONAS_SONG, RG_PROGRESSIVE_EPONAS_SONG, ctx->GetOption(RSK_STARTING_EPONAS_SONG).Get()); - addShuffledSong(RG_SARIAS_SONG, RG_PROGRESSIVE_SARIAS_SONG, ctx->GetOption(RSK_STARTING_SARIAS_SONG).Get()); - addShuffledSong(RG_SUNS_SONG, RG_PROGRESSIVE_SUNS_SONG, ctx->GetOption(RSK_STARTING_SUNS_SONG).Get()); - addShuffledSong(RG_SONG_OF_TIME, RG_PROGRESSIVE_SONG_OF_TIME, ctx->GetOption(RSK_STARTING_SONG_OF_TIME).Get()); - addShuffledSong(RG_SONG_OF_STORMS, RG_PROGRESSIVE_SONG_OF_STORMS, - ctx->GetOption(RSK_STARTING_SONG_OF_STORMS).Get()); - addShuffledSong(RG_MINUET_OF_FOREST, RG_PROGRESSIVE_MINUET_OF_FOREST, - ctx->GetOption(RSK_STARTING_MINUET_OF_FOREST).Get()); - addShuffledSong(RG_BOLERO_OF_FIRE, RG_PROGRESSIVE_BOLERO_OF_FIRE, - ctx->GetOption(RSK_STARTING_BOLERO_OF_FIRE).Get()); - addShuffledSong(RG_SERENADE_OF_WATER, RG_PROGRESSIVE_SERENADE_OF_WATER, - ctx->GetOption(RSK_STARTING_SERENADE_OF_WATER).Get()); - addShuffledSong(RG_REQUIEM_OF_SPIRIT, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, - ctx->GetOption(RSK_STARTING_REQUIEM_OF_SPIRIT).Get()); - addShuffledSong(RG_NOCTURNE_OF_SHADOW, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, - ctx->GetOption(RSK_STARTING_NOCTURNE_OF_SHADOW).Get()); - addShuffledSong(RG_PRELUDE_OF_LIGHT, RG_PROGRESSIVE_PRELUDE_OF_LIGHT, - ctx->GetOption(RSK_STARTING_PRELUDE_OF_LIGHT).Get()); + addShuffledSong(RG_PROGRESSIVE_ZELDAS_LULLABY, ctx->GetOption(RSK_STARTING_ZELDAS_LULLABY).Get()); + addShuffledSong(RG_PROGRESSIVE_EPONAS_SONG, ctx->GetOption(RSK_STARTING_EPONAS_SONG).Get()); + addShuffledSong(RG_PROGRESSIVE_SARIAS_SONG, ctx->GetOption(RSK_STARTING_SARIAS_SONG).Get()); + addShuffledSong(RG_PROGRESSIVE_SUNS_SONG, ctx->GetOption(RSK_STARTING_SUNS_SONG).Get()); + addShuffledSong(RG_PROGRESSIVE_SONG_OF_TIME, ctx->GetOption(RSK_STARTING_SONG_OF_TIME).Get()); + addShuffledSong(RG_PROGRESSIVE_SONG_OF_STORMS, ctx->GetOption(RSK_STARTING_SONG_OF_STORMS).Get()); + addShuffledSong(RG_PROGRESSIVE_MINUET_OF_FOREST, ctx->GetOption(RSK_STARTING_MINUET_OF_FOREST).Get()); + addShuffledSong(RG_PROGRESSIVE_BOLERO_OF_FIRE, ctx->GetOption(RSK_STARTING_BOLERO_OF_FIRE).Get()); + addShuffledSong(RG_PROGRESSIVE_SERENADE_OF_WATER, ctx->GetOption(RSK_STARTING_SERENADE_OF_WATER).Get()); + addShuffledSong(RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, ctx->GetOption(RSK_STARTING_REQUIEM_OF_SPIRIT).Get()); + addShuffledSong(RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, ctx->GetOption(RSK_STARTING_NOCTURNE_OF_SHADOW).Get()); + addShuffledSong(RG_PROGRESSIVE_PRELUDE_OF_LIGHT, ctx->GetOption(RSK_STARTING_PRELUDE_OF_LIGHT).Get()); } else { ctx->PlaceItemInLocation(RC_SHEIK_IN_FOREST, RG_MINUET_OF_FOREST, false, true); ctx->PlaceItemInLocation(RC_SHEIK_IN_CRATER, RG_BOLERO_OF_FIRE, false, true); diff --git a/soh/soh/Enhancements/randomizer/Traps.cpp b/soh/soh/Enhancements/randomizer/Traps.cpp index 61391176d1b..009f1635376 100644 --- a/soh/soh/Enhancements/randomizer/Traps.cpp +++ b/soh/soh/Enhancements/randomizer/Traps.cpp @@ -64,17 +64,17 @@ static void InitTrickNames() { Text{ "Blue Mail", "Habits Bleus", "Blaues Gewand" }, // "Ropas azules" }; trickNameTable[RG_IRON_BOOTS] = { - Text{ "Iron Hoofs", "Patins de Plomb", "Eisenhufe" }, // "Botas férreas" - Text{ "Snow Boots", "Bottes de Neige", "Schneestiefel" }, // "Botas de nieve" - Text{ "Red Boots", "Bottes rouges", "Rote Stiefel" }, // "Botas rojas" + Text{ "Iron Hoofs", "Patins de Plomb", "Eisenhufe€" }, // "Botas férreas" + Text{ "Snow Boots", "Bottes de Neige", "Schneestiefel€" }, // "Botas de nieve" + Text{ "Red Boots", "Bottes rouges", "Rote Stiefel€" }, // "Botas rojas" Text{ "Zora Greaves", "Bottes Zora", "Zora-Beinschutz" }, // "Zora Greaves" - Text{ "Boots of Power", "Bottes de Puissance", "Stärkestiefel" }, // "Botas de plomo" + Text{ "Boots of Power", "Bottes de Puissance", "Stärkestiefel€" }, // "Botas de plomo" }; trickNameTable[RG_HOVER_BOOTS] = { - Text{ "Hover Hoofs", "Patins des airs", "Gleithufe" }, // "Botas flotadoras" - Text{ "Golden Boots", "Bottes dorées", "Goldstiefel" }, // "Botas de Oro" - Text{ "Pegasus Boots", "Bottes pégase", "Pegasus-Stiefel" }, // "Botas de Pegaso" - Text{ "Boots of Speed", "Bottes de vitesse", "Tempostiefel" }, // "Botas del desierto" + Text{ "Hover Hoofs", "Patins des airs", "Gleithufe€" }, // "Botas flotadoras" + Text{ "Golden Boots", "Bottes dorées", "Goldstiefel€" }, // "Botas de Oro" + Text{ "Pegasus Boots", "Bottes pégase", "Pegasus-Stiefel€" }, // "Botas de Pegaso" + Text{ "Boots of Speed", "Bottes de vitesse", "Tempostiefel€" }, // "Botas del desierto" }; trickNameTable[RG_WEIRD_EGG] = { Text{ "Poached Egg", "Oeuf à la coque", "Spiegelei" }, // "Huevo pasado" @@ -135,19 +135,19 @@ static void InitTrickNames() { }; trickNameTable[RG_FIRE_ARROWS] = { Text{ "Fire Rod", "Baguette de feu", "Feuerstab" }, // "Cetro de fuego" - Text{ "Bomb Arrow", "Flèche-Bombe", "Bomben-Pfeile" }, // "Flecha bomba" + Text{ "Bomb Arrow", "Flèche-Bombe", "Bomben-Pfeile€" }, // "Flecha bomba" Text{ "Red Candle", "Bougie Rouge", "Rote Kerze" }, // "Vela roja" }; trickNameTable[RG_ICE_ARROWS] = { Text{ "Ice Rod", "Baguette des Glaces", "Eisstab" }, // "Cetro de Hielo" - Text{ "Ancient Arrow", "Flèche Archéonique", "Antike Pfeile" }, // "Flecha ancestral" - Text{ "Ice Trap Arrow", "Flèche de Piège de Glace", "Eisfallen-Pfeile" }, // "Cetro de hielo" + Text{ "Ancient Arrow", "Flèche Archéonique", "Antike Pfeile€" }, // "Flecha ancestral" + Text{ "Ice Trap Arrow", "Flèche de Piège de Glace", "Eisfallen-Pfeile€" }, // "Cetro de hielo" }; trickNameTable[RG_LIGHT_ARROWS] = { - Text{ "Wind Arrow", "Flèche de Vent", "Wind-Pfeile" }, // "Flecha del Viento" + Text{ "Wind Arrow", "Flèche de Vent", "Wind-Pfeile€" }, // "Flecha del Viento" Text{ "Wand of Gamelon", "Baguette de Gamelon", "Zauberstab von Gamelon" }, // "Varita de Gamelón" - Text{ "Shock Arrow", "Flèches Électriques", "Elektro-Pfeile" }, // "Flecha eléctrica" - Text{ "Silver Arrow", "Flèches d'Argent", "Silber-Pfeile" }, // "Flecha de plata" + Text{ "Shock Arrow", "Flèches Électriques", "Elektro-Pfeile€" }, // "Flecha eléctrica" + Text{ "Silver Arrow", "Flèches d'Argent", "Silber-Pfeile€" }, // "Flecha de plata" }; trickNameTable[RG_GERUDO_MEMBERSHIP_CARD] = { Text{ "Desert Title Deed", "Abonnement Gerudo", "Wüsten-Urkunde" }, // "Escritura del desierto" @@ -160,12 +160,12 @@ static void InitTrickNames() { trickNameTable[RG_MAGIC_BEAN_PACK] = { Text{ "Funky Bean Pack", "Paquet de Fèves Magiques", "Wunderbohnen-Packung" }, // "Lote de frijoles mágicos" - Text{ "Grapple Berries", "Baies de grappin", "Grapple-Beeren" }, // "Bayas de garfio" + Text{ "Grapple Berries", "Baies de grappin", "Grapple-Beeren€" }, // "Bayas de garfio" Text{ "Crenel Bean Pack", "Paquet de Haricots Gonggle", "Gongolerbsen-Packung" }, // "Lote de alubias mágicas" Text{ "Mystical Seed Pack", "Pack de graines mystiques", "Saatbeutel" }, // "Paquete de semillas místicas" }; trickNameTable[RG_DOUBLE_DEFENSE] = { - Text{ "Diamond Hearts", "Coeurs de Diamant", "Diamantherzen" }, // "Contenedor de diamante" + Text{ "Diamond Hearts", "Coeurs de Diamant", "Diamantherzen€" }, // "Contenedor de diamante" Text{ "Double Damage", "Double Souffrance", "Doppelte Angriffskraft" }, // "Doble daño receptivo" Text{ "Quadruple Defence", "Quadruple Défence", "Vierfache Verteidigung" }, // "Defensa cuádruple" }; @@ -277,7 +277,7 @@ static void InitTrickNames() { Text{ "Progressive Powder Kegs", "Baril de Poudre (prog.)", "Pulverfass (prog.)" }, // "Barril de polvo progresivo" Text{ "Progressive Remote Bombs", "Bombes à distance (prog.)", - "Fernzünderbomben (prog.)" }, // "Bombas remotas progresivas" + "Fernzünderbomben (prog.)€" }, // "Bombas remotas progresivas" }; trickNameTable[RG_PROGRESSIVE_BOW] = { Text{ "Progressive Arrow Capacity", "Capacité de flèches (prog.)", @@ -312,7 +312,7 @@ static void InitTrickNames() { }; trickNameTable[RG_PROGRESSIVE_SCALE] = { Text{ "Progressive Flippers", "Palmes de Zora (prog.)", - "Schwimmflossen (prog.)" }, // "Aletas de zora progresiva" + "Schwimmflossen (prog.)€" }, // "Aletas de zora progresiva" Text{ "Progressive Dragon's Scale", "Écaille du dragon d'eau (prog.)", "Drachen-Schuppe (prog.)" }, // "Escama dragón acuático progresiva" Text{ "Progressive Diving Ability", "Plongée (prog.)", "Tauchfähigkeit (prog.)" }, // "Buceo progresivo" @@ -327,7 +327,7 @@ static void InitTrickNames() { "Putput-Kapazität (prog.)" }, // "Capacidad progresiva de pera" Text{ "Progressive Nut Bag", "Sac de noix (prog.)", "Nußbeutel (prog.)" }, // "Bolsa de nueces progresiva" Text{ "Progressive Husk Capacity", "Capacité de noisettes (prog.)", - "Schalen-Kapazität (prog.)" }, // "Mayor capacidad de castañas" + "Schalen-Kapazität (prog.)€" }, // "Mayor capacidad de castañas" }; trickNameTable[RG_PROGRESSIVE_STICK_UPGRADE] = { Text{ "Progressive Stick Bag", "Sac de bâtons (prog.)", @@ -340,11 +340,11 @@ static void InitTrickNames() { "Stock-Kapazität (prog.)" }, // "Mayor capacidad de cetros deku" }; trickNameTable[RG_PROGRESSIVE_BOMBCHU_BAG] = { - Text{ "Progressive Bomblings", "Bombinsectes (prog.)", "Bombenmäuse (prog.)" }, // "Bombinsectos progresivos" + Text{ "Progressive Bomblings", "Bombinsectes (prog.)", "Bombenmäuse (prog.)€" }, // "Bombinsectos progresivos" Text{ "Progressive Sentrobe Bombs", "Bombe de Sphérodrone (prog.)", - "Rokopterbomben (prog.)" }, // "Bomba de helicobot progresivo" + "Rokopterbomben (prog.)€" }, // "Bomba de helicobot progresivo" Text{ "Progressive Bomb-ombs", "Bombe Soldat (prog.)", "Bob-omb (prog.)" }, // "Soldado bomba progresivo" - Text{ "Progressive Missiles", "Missiles (prog.)", "Missiles (prog.)" }, // "Misiles progresivos" + Text{ "Progressive Missiles", "Missiles (prog.)", "Missiles (prog.)€" }, // "Misiles progresivos" }; trickNameTable[RG_PROGRESSIVE_MAGIC_METER] = { Text{ "Progressive Stamina Meter", "Jauge d'endurance (prog.)", @@ -402,7 +402,8 @@ static void InitTrickNames() { }; trickNameTable[RG_PROGRESSIVE_SERENADE_OF_WATER] = { Text{ "Progressive Ruto's Blues", "Sérénade de l'Eau (prog.)", "Progressive Serenade des Wassers" }, - Text{ "Progressive New Wave Bossa Nova", "Bossa-nova des flots (prog.)", "Progressive Bossa Nova der Kaskaden" }, + Text{ "Progressive New Wave Bossa Nova", "Bossa-nova des flots (prog.)", + "Progressive Bossa Nova der Kaskaden" }, Text{ "Progressive Manbo's Mambo", "Mambo de Manbo (prog.)", "Progressives Manbos Mambo" }, }; trickNameTable[RG_PROGRESSIVE_REQUIEM_OF_SPIRIT] = { @@ -683,31 +684,31 @@ static void InitTrickNames() { }; trickNameTable[RG_BLUE_RUPEE] = { Text{ "Blupee", "Bleubi", "Fünfer" }, // "Azupia" - Text{ "Five Rubies", "Cinq Rubys", "fünf Rubies" }, // "Cinco rubíes" - Text{ "Five Rupees", "Cinq rubis", "fünf Rubine" }, // "Bolívar hyliano" + Text{ "Five Rubies", "Cinq Rubys", "fünf Rubies€" }, // "Cinco rubíes" + Text{ "Five Rupees", "Cinq rubis", "fünf Rubine€" }, // "Bolívar hyliano" Text{ "Rupee (5)", "Rubis (5)", "Rubin (5)" }, // "Peso hyliano" Text{ "Rupoor (5)", "Roupir (5)", "Rubinfalle (5)" }, // "Rupobre (5)" }; trickNameTable[RG_RED_RUPEE] = { Text{ "Big 20", "Grand 20", "Zwanni" }, // "Los 20 grandes" - Text{ "Twenty Rubies", "vingt rubis", "zwanzig Rubies" }, // "Veinte rubíes" - Text{ "Twenty Rupees", "Vingt rubis", "zwanzig Rubine" }, // "Colon hyliano" + Text{ "Twenty Rubies", "vingt rubis", "zwanzig Rubies€" }, // "Veinte rubíes" + Text{ "Twenty Rupees", "Vingt rubis", "zwanzig Rubine€" }, // "Colon hyliano" Text{ "Rupee (20)", "Rubis (20)", "Rubin (20)" }, // "Peso hyliano" Text{ "Rupoor (20)", "Roupir (20)", "Rubinfalle (20)" }, // "Rupobre (20)" }; trickNameTable[RG_PURPLE_RUPEE] = { Text{ "Purpee", "Pourbi", "Fuffi" }, // "morupiua" - Text{ "Fifty Rubies", "Cinquante rubis", "fünfzig Rubies" }, // "Cincuenta rubíes" - Text{ "Fifty Rupees", "Cinquante rubis", "fünfzig Rubine" }, // "Balboa hyliano" + Text{ "Fifty Rubies", "Cinquante rubis", "fünfzig Rubies€" }, // "Cincuenta rubíes" + Text{ "Fifty Rupees", "Cinquante rubis", "fünfzig Rubine€" }, // "Balboa hyliano" Text{ "Rupee (50)", "Rubis (50)", "Rubin (50)" }, // "Peso hyliano" Text{ "Rupoor (50)", "Roupir (50)", "Rubinfalle (50)" }, // "Rupobre (50)" }; trickNameTable[RG_HUGE_RUPEE] = { - Text{ "Hugo", "Or Rubi", "zwei Hunnis" }, // "Oro Rubi" - Text{ "Two Hundred Rubies", "Deux cents rubis", "zweihundert Rubies" }, // "Doscientos rubíes" + Text{ "Hugo", "Or Rubi", "zwei Hunnis€" }, // "Oro Rubi" + Text{ "Two Hundred Rubies", "Deux cents rubis", "zweihundert Rubies€" }, // "Doscientos rubíes" Text{ "Diamond", "Diamant", "Diamant" }, // "Diamante" Text{ "Huge Ruby", "Énorme rubis", "großer Ruby" }, // "Rubi gigante" - Text{ "Two Hundred Rupees", "Deux cent rubis", "zweihundert Rubine" }, // "Euro hyliano" + Text{ "Two Hundred Rupees", "Deux cent rubis", "zweihundert Rubine€" }, // "Euro hyliano" Text{ "Rupee (200)", "Rubis (200)", "Rubin (200)" }, // "Dólar hyliano" }; trickNameTable[RG_PIECE_OF_HEART] = { @@ -787,7 +788,7 @@ static void InitTrickNames() { }; trickNameTable[RG_KING_DODONGO_SOUL] = { Text{ "Lizard Soul", "Âme d'un Lézard", "Reptilienseele" }, - Text{ "Regal Remains", "Restes Délicieux", "royale Überreste" }, + Text{ "Regal Remains", "Restes Délicieux", "royale Überreste€" }, Text{ "Dodongo's Core", "Coeur de Dodongo", "Dodongos Kern" }, }; trickNameTable[RG_BARINADE_SOUL] = { @@ -1119,7 +1120,7 @@ static void InitTrickNames() { trickNameTable[RG_SPEAK_HYLIAN] = { // TODO_TRANSLATE Text{ "Human Jingle Nut" }, - Text{ "Sheikah Jabber nut" }, + Text{ "Sheikah Jabber Nut" }, Text{ "Lorulean Blabber Nut" }, }; trickNameTable[RG_SPEAK_KOKIRI] = { @@ -1349,19 +1350,19 @@ static void InitTrickNames() { semillas grande" }; trickNameTable[GI_STRENGTH_1] = { - Text{"Goron's Gauntlet", "Gantelet Goron", "Goronen-Handschuhe" }, // "Brazalete amarillo" + Text{"Goron's Gauntlet", "Gantelet Goron", "Goronen-Handschuhe€" }, // "Brazalete amarillo" Text{"Power Bracelet", "Bracelet de force", "Kraftarmband" }, // "Brazalete de fuerza" Text{"Magic Bracelet", "Bracelet de Lavio", "Magiearmband" }, // "Brazalete de Ravio" }; trickNameTable[GI_STRENGTH_2] = { Text{"Silver Bracelets", "Bracelets d'argent", "Silberarmband" }, // "Guantes Moguma" - Text{"Power Gloves", "Gant de puissance", "Silberhandschuhe" }, // "Guante del Poder" - Text{"Magic Gauntlets", "Gantelet magique", "Magiehandschuhe" }, // "Guante mágico" + Text{"Power Gloves", "Gant de puissance", "Silberhandschuhe€" }, // "Guante del Poder" + Text{"Magic Gauntlets", "Gantelet magique", "Magiehandschuhe€" }, // "Guante mágico" }; trickNameTable[GI_STRENGTH_3] = { Text{"Golden Bracelets", "Bracelets d'or", "Goldarmband" }, // "Guantelete de Thanos" - Text{"Titan's Mitts", "Moufle de titan", "Goldhandschuhe" }, // "Guantes de Titán" - Text{"Magnetic Gloves", "Magnéto-gants", "Magnethandschuhe" }, // "Guantes de fuego" + Text{"Titan's Mitts", "Moufle de titan", "Goldhandschuhe€" }, // "Guantes de Titán" + Text{"Magnetic Gloves", "Magnéto-gants", "Magnethandschuhe€" }, // "Guantes de fuego" }; trickNameTable[GI_SCALE_1] = { Text{"Silver Pearl", "Perle d'argent", "Silberne Perle" }, // "Perla de Plata progresiva" @@ -1466,7 +1467,7 @@ static void InitTrickNames() { }; trickNameTable[GI_MASK_ZORA] = { Text{"Zola Mask", "Masque Zola", "Zola-Maske" }, // "Máscara Zola" - Text{"Mask of Zora", "Masque des Zoras", "Zora-Schuppen" }, // "Máscara de los Zora" + Text{"Mask of Zora", "Masque des Zoras", "Zora-Schuppen€" }, // "Máscara de los Zora" Text{"Ruto Mask", "Masque de Ruto", "Rutos Maske" }, // "Máscara de Mikau" }; trickNameTable[GI_MASK_GERUDO] = { @@ -1490,12 +1491,9 @@ Text Rando::Traps::GetTrapName(uint16_t id, uint64_t* state) { initTrickNames = true; } - if (id >= RG_MAX || trickNameTable[id].empty()) { - SPDLOG_ERROR("GetTrapName: missing trick name for item id {}", id); - if (id < RG_MAX) { - return StaticData::RetrieveItem(static_cast(id)).GetName(); - } - return Text{ "Ice Trap" }; + if (trickNameTable[id].empty()) { + assert(false); + return Text{ "not an Ice Trap" }; } // Randomly get the easy, medium, or hard name for the given item id diff --git a/soh/soh/Enhancements/randomizer/item.cpp b/soh/soh/Enhancements/randomizer/item.cpp index 409c50424f2..31fec479b22 100644 --- a/soh/soh/Enhancements/randomizer/item.cpp +++ b/soh/soh/Enhancements/randomizer/item.cpp @@ -2,7 +2,7 @@ #include "item_location.h" #include "SeedContext.h" -#include "split_songs.h" +#include "static_data.h" #include "logic.h" #include "3drando/item_pool.hpp" #include "z64item.h" @@ -109,8 +109,7 @@ uint16_t Item::GetPrice() const { } std::shared_ptr Item::GetGIEntry() const { // NOLINT(*-no-recursion) - if (giEntry != nullptr && giEntry->itemId != RG_PROGRESSIVE_BOMBCHU_BAG && - !SplitSongs::IsProgressiveSong(randomizerGet)) { + if (giEntry != nullptr && giEntry->itemId != RG_PROGRESSIVE_BOMBCHU_BAG) { return giEntry; } std::shared_ptr ctx = Rando::Context::GetInstance(); @@ -387,9 +386,15 @@ std::shared_ptr Item::GetGIEntry() const { // NOLINT(*-no-recursio case RG_PROGRESSIVE_SERENADE_OF_WATER: case RG_PROGRESSIVE_REQUIEM_OF_SPIRIT: case RG_PROGRESSIVE_NOCTURNE_OF_SHADOW: - case RG_PROGRESSIVE_PRELUDE_OF_LIGHT: - actual = SplitSongs::ResolveProgressiveSongStage(logic.get(), randomizerGet); + case RG_PROGRESSIVE_PRELUDE_OF_LIGHT: { + const SongData* song = &StaticData::songData.at(randomizerGet); + if (logic->CheckRandoInf(song->randInf)) { + actual = song->realSong; + } else { + actual = song->part; + } break; + } case RG_PROGRESSIVE_BOMBCHU_BAG: if (OTRGlobals::Instance->gRandoContext->GetOption(RSK_BOMBCHU_BAG).Is(RO_BOMBCHU_BAG_SINGLE)) { if (logic->CurrentInventory(ITEM_BOMBCHU) != ITEM_NONE) { @@ -418,24 +423,11 @@ std::shared_ptr Item::GetGIEntry() const { // NOLINT(*-no-recursio if (giEntry != nullptr && actual == RG_NONE) { return giEntry; } - if (actual == randomizerGet && giEntry != nullptr) { - return giEntry; - } - if (actual == RG_NONE) { - if (giEntry != nullptr) { - return giEntry; - } - return StaticData::RetrieveItem(RG_NONE).GetGIEntry(); - } return StaticData::RetrieveItem(actual).GetGIEntry(); } GetItemEntry Item::GetGIEntry_Copy() const { - const auto entry = GetGIEntry(); - if (entry != nullptr) { - return *entry; - } - return *StaticData::RetrieveItem(RG_NONE).GetGIEntry(); + return *GetGIEntry(); } void Item::SetPrice(const uint16_t price_) { diff --git a/soh/soh/Enhancements/randomizer/item_list.cpp b/soh/soh/Enhancements/randomizer/item_list.cpp index 5bd1513f963..3a67b740824 100644 --- a/soh/soh/Enhancements/randomizer/item_list.cpp +++ b/soh/soh/Enhancements/randomizer/item_list.cpp @@ -73,18 +73,18 @@ void Rando::StaticData::InitItemTable() { itemTable[RG_PROGRESSIVE_MAGIC_METER] = Item(RG_PROGRESSIVE_MAGIC_METER, Text{ "Progressive Magic Meter", "Jauge de Magie (prog.)", "Progressives Magisches Maß" }, ITEMTYPE_ITEM, 0x8A, true, LOGIC_PROGRESSIVE_MAGIC, RHT_PROGRESSIVE_MAGIC_METER, ITEM_CATEGORY_MAJOR, {"a ", "une ", "ein "}, "%g", true).CustomIcon(gQuestIconMagicJarBigTex, ICON_SIZE_24); itemTable[RG_PROGRESSIVE_OCARINA] = Item(RG_PROGRESSIVE_OCARINA, Text{ "Progressive Ocarina", "Ocarina (prog.)", "Progressive Okarina" }, ITEMTYPE_ITEM, 0x8B, true, LOGIC_PROGRESSIVE_OCARINA, RHT_PROGRESSIVE_OCARINA, ITEM_CATEGORY_MAJOR, {"a ", "un ", "eine "}, "%g", true); itemTable[RG_PROGRESSIVE_GORONSWORD] = Item(RG_PROGRESSIVE_GORONSWORD, Text{ "Progressive Goron Sword", "Épée Goron (prog.)", "Progressives Goronen-Schwert" }, ITEMTYPE_ITEM, 0xD4, true, LOGIC_PROGRESSIVE_GIANT_KNIFE, RHT_PROGRESSIVE_GORONSWORD, ITEM_CATEGORY_MAJOR, {"a ", "une ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_ZELDAS_LULLABY] = Item(RG_PROGRESSIVE_ZELDAS_LULLABY, Text{ "Progressive Zelda's Lullaby", "Berceuse de Zelda (prog.)", "Progressives Zeldas Wiegenlied" }, ITEMTYPE_SONG, 0xE1, true, LOGIC_NONE, RHT_ZELDAS_LULLABY, RG_PROGRESSIVE_ZELDAS_LULLABY, OBJECT_GI_MELODY, GID_SONG_ZELDA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_EPONAS_SONG] = Item(RG_PROGRESSIVE_EPONAS_SONG, Text{ "Progressive Epona's Song", "Chant d'Epona (prog.)", "Progressives Eponas Lied" }, ITEMTYPE_SONG, 0xE2, true, LOGIC_NONE, RHT_EPONAS_SONG, RG_PROGRESSIVE_EPONAS_SONG, OBJECT_GI_MELODY, GID_SONG_EPONA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_SARIAS_SONG] = Item(RG_PROGRESSIVE_SARIAS_SONG, Text{ "Progressive Saria's Song", "Chant de Saria (prog.)", "Progressives Salias Lied" }, ITEMTYPE_SONG, 0xE3, true, LOGIC_NONE, RHT_SARIAS_SONG, RG_PROGRESSIVE_SARIAS_SONG, OBJECT_GI_MELODY, GID_SONG_SARIA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_SUNS_SONG] = Item(RG_PROGRESSIVE_SUNS_SONG, Text{ "Progressive Sun's Song", "Chant du Soleil (prog.)", "Progressives Hymne der Sonne" }, ITEMTYPE_SONG, 0xE4, true, LOGIC_NONE, RHT_SUNS_SONG, RG_PROGRESSIVE_SUNS_SONG, OBJECT_GI_MELODY, GID_SONG_SUN, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_SONG_OF_TIME] = Item(RG_PROGRESSIVE_SONG_OF_TIME, Text{ "Progressive Song of Time", "Chant du Temps (prog.)", "Progressives Hymne der Zeit" }, ITEMTYPE_SONG, 0xE5, true, LOGIC_NONE, RHT_SONG_OF_TIME, RG_PROGRESSIVE_SONG_OF_TIME, OBJECT_GI_MELODY, GID_SONG_TIME, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_SONG_OF_STORMS] = Item(RG_PROGRESSIVE_SONG_OF_STORMS, Text{ "Progressive Song of Storms", "Chant des Tempêtes (prog.)", "Progressives Hymne des Sturms" }, ITEMTYPE_SONG, 0xE6, true, LOGIC_NONE, RHT_SONG_OF_STORMS, RG_PROGRESSIVE_SONG_OF_STORMS, OBJECT_GI_MELODY, GID_SONG_STORM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_MINUET_OF_FOREST] = Item(RG_PROGRESSIVE_MINUET_OF_FOREST, Text{ "Progressive Minuet of Forest", "Menuet des Bois (prog.)", "Progressives Menuett des Waldes" }, ITEMTYPE_SONG, 0xE7, true, LOGIC_NONE, RHT_MINUET_OF_FOREST, RG_PROGRESSIVE_MINUET_OF_FOREST, OBJECT_GI_MELODY, GID_SONG_MINUET, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_BOLERO_OF_FIRE] = Item(RG_PROGRESSIVE_BOLERO_OF_FIRE, Text{ "Progressive Bolero of Fire", "Boléro du Feu (prog.)", "Progressives Bolero des Feuers" }, ITEMTYPE_SONG, 0xE8, true, LOGIC_NONE, RHT_BOLERO_OF_FIRE, RG_PROGRESSIVE_BOLERO_OF_FIRE, OBJECT_GI_MELODY, GID_SONG_BOLERO, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_SERENADE_OF_WATER] = Item(RG_PROGRESSIVE_SERENADE_OF_WATER, Text{ "Progressive Serenade of Water", "Sérénade de l'Eau (prog.)", "Progressive Serenade des Wassers" }, ITEMTYPE_SONG, 0xE9, true, LOGIC_NONE, RHT_SERENADE_OF_WATER, RG_PROGRESSIVE_SERENADE_OF_WATER, OBJECT_GI_MELODY, GID_SONG_SERENADE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_REQUIEM_OF_SPIRIT] = Item(RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, Text{ "Progressive Requiem of Spirit", "Requiem des Esprits (prog.)", "Progressives Requiem der Geister" }, ITEMTYPE_SONG, 0xEA, true, LOGIC_NONE, RHT_REQUIEM_OF_SPIRIT, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, OBJECT_GI_MELODY, GID_SONG_REQUIEM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_NOCTURNE_OF_SHADOW] = Item(RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, Text{ "Progressive Nocturne of Shadow", "Nocturne de l'Ombre (prog.)", "Progressive Nocturne des Schattens" }, ITEMTYPE_SONG, 0xEB, true, LOGIC_NONE, RHT_NOCTURNE_OF_SHADOW, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, OBJECT_GI_MELODY, GID_SONG_NOCTURNE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true); - itemTable[RG_PROGRESSIVE_PRELUDE_OF_LIGHT] = Item(RG_PROGRESSIVE_PRELUDE_OF_LIGHT, Text{ "Progressive Prelude of Light", "Prélude de la Lumière (prog.)", "Progressive Kantate des Lichts" }, ITEMTYPE_SONG, 0xEC, true, LOGIC_NONE, RHT_PRELUDE_OF_LIGHT, RG_PROGRESSIVE_PRELUDE_OF_LIGHT, OBJECT_GI_MELODY, GID_SONG_PRELUDE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_ZELDAS_LULLABY] = Item(RG_PROGRESSIVE_ZELDAS_LULLABY, Text{ "Progressive Zelda's Lullaby", "Berceuse de Zelda (prog.)", "Progressives Zeldas Wiegenlied" }, ITEMTYPE_ITEM, 0xE1, true, LOGIC_NONE, RHT_ZELDAS_LULLABY, RG_PROGRESSIVE_ZELDAS_LULLABY, OBJECT_GI_MELODY, GID_SONG_ZELDA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_EPONAS_SONG] = Item(RG_PROGRESSIVE_EPONAS_SONG, Text{ "Progressive Epona's Song", "Chant d'Epona (prog.)", "Progressives Eponas Lied" }, ITEMTYPE_ITEM, 0xE2, true, LOGIC_NONE, RHT_EPONAS_SONG, RG_PROGRESSIVE_EPONAS_SONG, OBJECT_GI_MELODY, GID_SONG_EPONA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SARIAS_SONG] = Item(RG_PROGRESSIVE_SARIAS_SONG, Text{ "Progressive Saria's Song", "Chant de Saria (prog.)", "Progressives Salias Lied" }, ITEMTYPE_ITEM, 0xE3, true, LOGIC_NONE, RHT_SARIAS_SONG, RG_PROGRESSIVE_SARIAS_SONG, OBJECT_GI_MELODY, GID_SONG_SARIA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SUNS_SONG] = Item(RG_PROGRESSIVE_SUNS_SONG, Text{ "Progressive Sun's Song", "Chant du Soleil (prog.)", "Progressives Hymne der Sonne" }, ITEMTYPE_ITEM, 0xE4, true, LOGIC_NONE, RHT_SUNS_SONG, RG_PROGRESSIVE_SUNS_SONG, OBJECT_GI_MELODY, GID_SONG_SUN, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SONG_OF_TIME] = Item(RG_PROGRESSIVE_SONG_OF_TIME, Text{ "Progressive Song of Time", "Chant du Temps (prog.)", "Progressives Hymne der Zeit" }, ITEMTYPE_ITEM, 0xE5, true, LOGIC_NONE, RHT_SONG_OF_TIME, RG_PROGRESSIVE_SONG_OF_TIME, OBJECT_GI_MELODY, GID_SONG_TIME, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SONG_OF_STORMS] = Item(RG_PROGRESSIVE_SONG_OF_STORMS, Text{ "Progressive Song of Storms", "Chant des Tempêtes (prog.)", "Progressives Hymne des Sturms" }, ITEMTYPE_ITEM, 0xE6, true, LOGIC_NONE, RHT_SONG_OF_STORMS, RG_PROGRESSIVE_SONG_OF_STORMS, OBJECT_GI_MELODY, GID_SONG_STORM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_MINUET_OF_FOREST] = Item(RG_PROGRESSIVE_MINUET_OF_FOREST, Text{ "Progressive Minuet of Forest", "Menuet des Bois (prog.)", "Progressives Menuett des Waldes" }, ITEMTYPE_ITEM, 0xE7, true, LOGIC_NONE, RHT_MINUET_OF_FOREST, RG_PROGRESSIVE_MINUET_OF_FOREST, OBJECT_GI_MELODY, GID_SONG_MINUET, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_BOLERO_OF_FIRE] = Item(RG_PROGRESSIVE_BOLERO_OF_FIRE, Text{ "Progressive Bolero of Fire", "Boléro du Feu (prog.)", "Progressives Bolero des Feuers" }, ITEMTYPE_ITEM, 0xE8, true, LOGIC_NONE, RHT_BOLERO_OF_FIRE, RG_PROGRESSIVE_BOLERO_OF_FIRE, OBJECT_GI_MELODY, GID_SONG_BOLERO, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_SERENADE_OF_WATER] = Item(RG_PROGRESSIVE_SERENADE_OF_WATER, Text{ "Progressive Serenade of Water", "Sérénade de l'Eau (prog.)", "Progressive Serenade des Wassers" }, ITEMTYPE_ITEM, 0xE9, true, LOGIC_NONE, RHT_SERENADE_OF_WATER, RG_PROGRESSIVE_SERENADE_OF_WATER, OBJECT_GI_MELODY, GID_SONG_SERENADE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_REQUIEM_OF_SPIRIT] = Item(RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, Text{ "Progressive Requiem of Spirit", "Requiem des Esprits (prog.)", "Progressives Requiem der Geister" }, ITEMTYPE_ITEM, 0xEA, true, LOGIC_NONE, RHT_REQUIEM_OF_SPIRIT, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, OBJECT_GI_MELODY, GID_SONG_REQUIEM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_NOCTURNE_OF_SHADOW] = Item(RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, Text{ "Progressive Nocturne of Shadow", "Nocturne de l'Ombre (prog.)", "Progressive Nocturne des Schattens" }, ITEMTYPE_ITEM, 0xEB, true, LOGIC_NONE, RHT_NOCTURNE_OF_SHADOW, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, OBJECT_GI_MELODY, GID_SONG_NOCTURNE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "une ", "ein "}, "%g", true); + itemTable[RG_PROGRESSIVE_PRELUDE_OF_LIGHT] = Item(RG_PROGRESSIVE_PRELUDE_OF_LIGHT, Text{ "Progressive Prelude of Light", "Prélude de la Lumière (prog.)", "Progressive Kantate des Lichts" }, ITEMTYPE_ITEM, 0xEC, true, LOGIC_NONE, RHT_PRELUDE_OF_LIGHT, RG_PROGRESSIVE_PRELUDE_OF_LIGHT, OBJECT_GI_MELODY, GID_SONG_PRELUDE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}, "%g", true); // Bottles itemTable[RG_EMPTY_BOTTLE] = Item(RG_EMPTY_BOTTLE, Text{ "Empty Bottle", "Bouteille Vide", "Leere Flasche" }, ITEMTYPE_ITEM, GI_BOTTLE, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_MILK, ITEM_BOTTLE, OBJECT_GI_BOTTLE, GID_BOTTLE, 0x42, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"an ", "une ", "eine "}); itemTable[RG_BOTTLE_WITH_MILK] = Item(RG_BOTTLE_WITH_MILK, Text{ "Bottle with Milk", "Bouteille avec du Lait", "Flasche mit Milch" }, ITEMTYPE_ITEM, GI_MILK_BOTTLE, true, LOGIC_BOTTLES, RHT_BOTTLE_WITH_MILK, ITEM_MILK_BOTTLE, OBJECT_GI_MILK, GID_MILK, 0x98, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_NONE, {"a ", "une ", "eine "}); @@ -465,6 +465,18 @@ void Rando::StaticData::InitItemTable() { itemTable[RG_DEKU_STICK_CAPACITY_30] = Item(RG_DEKU_STICK_CAPACITY_30, Text{ "Deku Stick Capacity (30)", "Capacité de Bâtons Mojo (30)", "Deku-Stab-Kapazität (30)" }, ITEMTYPE_ITEM, GI_STICK_UPGRADE_30, true, LOGIC_PROGRESSIVE_STICK_BAG, RHT_DEKU_STICK_CAPACITY_30, ITEM_STICK_UPGRADE_30, OBJECT_GI_STICK, GID_STICK, 0x91, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_LESSER, MOD_NONE).CustomIcon(gItemIconDekuStickTex); itemTable[RG_MAGIC_SINGLE] = Item(RG_MAGIC_SINGLE, Text{ "Magic Meter", "Jauge de Magie", "Magisches Maß" }, ITEMTYPE_ITEM, 0x8A, true, LOGIC_PROGRESSIVE_MAGIC, RHT_MAGIC_SINGLE, RG_MAGIC_SINGLE, OBJECT_GI_MAGICPOT, GID_MAGIC_SMALL, 0xE4, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"the ", "la ", "das "}).CustomIcon(gQuestIconMagicJarSmallTex, ICON_SIZE_24); itemTable[RG_MAGIC_DOUBLE] = Item(RG_MAGIC_DOUBLE, Text{ "Enhanced Magic Meter", "Jauge de Magie améliorée", "Verbessertes Magisches Maß" }, ITEMTYPE_ITEM, 0x8A, true, LOGIC_PROGRESSIVE_MAGIC, RHT_MAGIC_DOUBLE, RG_MAGIC_DOUBLE, OBJECT_GI_MAGICPOT, GID_MAGIC_LARGE, 0xE8, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_LESSER, MOD_RANDOMIZER, {"the ", "la ", "das "}).CustomIcon(gQuestIconMagicJarBigTex, ICON_SIZE_24); + itemTable[RG_PART_OF_ZELDAS_LULLABY] = Item(RG_PART_OF_ZELDAS_LULLABY, Text{ "Part of Zelda's Lullaby", TODO_TRANSLATE, TODO_TRANSLATE }, ITEMTYPE_ITEM, 0xC1, true, LOGIC_ZELDAS_LULLABY, RHT_ZELDAS_LULLABY, RG_PART_OF_ZELDAS_LULLABY, OBJECT_GI_MELODY, GID_SONG_ZELDA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"", "", ""}); + itemTable[RG_PART_OF_EPONAS_SONG] = Item(RG_PART_OF_EPONAS_SONG, Text{ "Part of Epona's Song", TODO_TRANSLATE, TODO_TRANSLATE }, ITEMTYPE_ITEM, 0xC2, true, LOGIC_EPONAS_SONG, RHT_EPONAS_SONG, RG_PART_OF_EPONAS_SONG, OBJECT_GI_MELODY, GID_SONG_EPONA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"", "", ""}); + itemTable[RG_PART_OF_SARIAS_SONG] = Item(RG_PART_OF_SARIAS_SONG, Text{ "Part of Saria's Song", TODO_TRANSLATE, TODO_TRANSLATE }, ITEMTYPE_ITEM, 0xC3, true, LOGIC_SARIAS_SONG, RHT_SARIAS_SONG, RG_PART_OF_SARIAS_SONG, OBJECT_GI_MELODY, GID_SONG_SARIA, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"", "", ""}); + itemTable[RG_PART_OF_SUNS_SONG] = Item(RG_PART_OF_SUNS_SONG, Text{ "Part of Sun's Song", TODO_TRANSLATE, TODO_TRANSLATE }, ITEMTYPE_ITEM, 0xC4, true, LOGIC_SUNS_SONG, RHT_SUNS_SONG, RG_PART_OF_SUNS_SONG, OBJECT_GI_MELODY, GID_SONG_SUN, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"", "", ""}); + itemTable[RG_PART_OF_SONG_OF_TIME] = Item(RG_PART_OF_SONG_OF_TIME, Text{ "Part of Song of Time", TODO_TRANSLATE, TODO_TRANSLATE }, ITEMTYPE_ITEM, 0xC5, true, LOGIC_SONG_OF_TIME, RHT_SONG_OF_TIME, RG_PART_OF_SONG_OF_TIME, OBJECT_GI_MELODY, GID_SONG_TIME, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"", "", ""}); + itemTable[RG_PART_OF_SONG_OF_STORMS] = Item(RG_PART_OF_SONG_OF_STORMS, Text{ "Part of Song of Storms", TODO_TRANSLATE, TODO_TRANSLATE }, ITEMTYPE_ITEM, 0xC6, true, LOGIC_SONG_OF_STORMS, RHT_SONG_OF_STORMS, RG_PART_OF_SONG_OF_STORMS, OBJECT_GI_MELODY, GID_SONG_STORM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"", "", ""}); + itemTable[RG_PART_OF_MINUET_OF_FOREST] = Item(RG_PART_OF_MINUET_OF_FOREST, Text{ "Part of Minuet of Forest", TODO_TRANSLATE, TODO_TRANSLATE }, ITEMTYPE_ITEM, 0xBB, true, LOGIC_MINUET_OF_FOREST, RHT_MINUET_OF_FOREST, RG_PART_OF_MINUET_OF_FOREST, OBJECT_GI_MELODY, GID_SONG_MINUET, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"", "", ""}); + itemTable[RG_PART_OF_BOLERO_OF_FIRE] = Item(RG_PART_OF_BOLERO_OF_FIRE, Text{ "Part of Bolero of Fire", TODO_TRANSLATE, TODO_TRANSLATE }, ITEMTYPE_ITEM, 0xBC, true, LOGIC_BOLERO_OF_FIRE, RHT_BOLERO_OF_FIRE, RG_PART_OF_BOLERO_OF_FIRE, OBJECT_GI_MELODY, GID_SONG_BOLERO, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"", "", ""}); + itemTable[RG_PART_OF_SERENADE_OF_WATER] = Item(RG_PART_OF_SERENADE_OF_WATER, Text{ "Part of Serenade of Water", TODO_TRANSLATE, TODO_TRANSLATE }, ITEMTYPE_ITEM, 0xBD, true, LOGIC_SERENADE_OF_WATER, RHT_SERENADE_OF_WATER, RG_PART_OF_SERENADE_OF_WATER, OBJECT_GI_MELODY, GID_SONG_SERENADE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"", "", ""}); + itemTable[RG_PART_OF_REQUIEM_OF_SPIRIT] = Item(RG_PART_OF_REQUIEM_OF_SPIRIT, Text{ "Part of Requiem of Spirit", TODO_TRANSLATE, TODO_TRANSLATE }, ITEMTYPE_ITEM, 0xBE, true, LOGIC_REQUIEM_OF_SPIRIT, RHT_REQUIEM_OF_SPIRIT, RG_PART_OF_REQUIEM_OF_SPIRIT, OBJECT_GI_MELODY, GID_SONG_REQUIEM, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"", "", ""}); + itemTable[RG_PART_OF_NOCTURNE_OF_SHADOW] = Item(RG_PART_OF_NOCTURNE_OF_SHADOW, Text{ "Part of Nocturne of Shadow", TODO_TRANSLATE, TODO_TRANSLATE }, ITEMTYPE_ITEM, 0xBF, true, LOGIC_NOCTURNE_OF_SHADOW, RHT_NOCTURNE_OF_SHADOW, RG_PART_OF_NOCTURNE_OF_SHADOW, OBJECT_GI_MELODY, GID_SONG_NOCTURNE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"", "", ""}); + itemTable[RG_PART_OF_PRELUDE_OF_LIGHT] = Item(RG_PART_OF_PRELUDE_OF_LIGHT, Text{ "Part of Prelude of Light", TODO_TRANSLATE, TODO_TRANSLATE }, ITEMTYPE_ITEM, 0xC0, true, LOGIC_PRELUDE_OF_LIGHT, RHT_PRELUDE_OF_LIGHT, RG_PART_OF_PRELUDE_OF_LIGHT, OBJECT_GI_MELODY, GID_SONG_PRELUDE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"", "", ""}); itemTable[RG_TRIFORCE_PIECE] = Item(RG_TRIFORCE_PIECE, Text{ "Triforce Piece", "Morceau de Triforce", "Triforce-Fragment" }, ITEMTYPE_ITEM, 0xDF, true, LOGIC_TRIFORCE_PIECES, RHT_TRIFORCE_PIECE, RG_TRIFORCE_PIECE, OBJECT_GI_BOMB_2, GID_TRIFORCE_PIECE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "un ", "ein "}).CustomIcon(gTriforcePieceTex); itemTable[RG_ROCS_FEATHER] = Item(RG_ROCS_FEATHER, Text{ "Roc's Feather", "Plume de Roc", "Grefenfeider" }, ITEMTYPE_ITEM, 0xE0, true, LOGIC_ROCS_FEATHER, RHT_ROCS_FEATHER, RG_ROCS_FEATHER, OBJECT_GI_BOMB_2, GID_STONE_OF_AGONY, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, {"a ", "la ", "ein "}).CustomIcon(gRocsFeatherTex); itemTable[RG_ROCS_FEATHER].SetCustomDrawFunc(Randomizer_DrawRocsFeather); diff --git a/soh/soh/Enhancements/randomizer/logic.cpp b/soh/soh/Enhancements/randomizer/logic.cpp index d0ef8bb5278..35a17b9f345 100644 --- a/soh/soh/Enhancements/randomizer/logic.cpp +++ b/soh/soh/Enhancements/randomizer/logic.cpp @@ -7,7 +7,7 @@ #include "soh/OTRGlobals.h" #include "dungeon.h" #include "SeedContext.h" -#include "split_songs.h" +#include "static_data.h" #include "macros.h" #include "variables.h" #include @@ -21,11 +21,6 @@ namespace Rando { bool Logic::HasItem(RandomizerGet itemName) { - if (SplitSongs::IsProgressiveSong(itemName)) { - const SplitSongDef* def = SplitSongs::GetSongDefFromProgressive(itemName); - return def != nullptr && SplitSongs::HasFullSong(def->id); - } - switch (itemName) { case RG_FAIRY_OCARINA: return CheckInventory(ITEM_OCARINA_FAIRY, false); @@ -99,7 +94,7 @@ bool Logic::HasItem(RandomizerGet itemName) { return CurrentUpgrade(UPG_BOMB_BAG); case RG_MAGIC_SINGLE: return GetSaveContext()->magicLevel >= 1 || GetSaveContext()->isMagicAcquired; - // Songs (split + Anywhere: logical ownership is two parts on the logic scratch before quest is granted) + // Songs case RG_ZELDAS_LULLABY: case RG_EPONAS_SONG: case RG_SARIAS_SONG: @@ -111,23 +106,7 @@ bool Logic::HasItem(RandomizerGet itemName) { case RG_SERENADE_OF_WATER: case RG_REQUIEM_OF_SPIRIT: case RG_NOCTURNE_OF_SHADOW: - case RG_PRELUDE_OF_LIGHT: { - const bool splitAnywhere = ctx->GetOption(RSK_SPLIT_OCARINA_SONGS).Get(); - const auto qiIt = RandoGetToQuestItem.find(itemName); - if (qiIt == RandoGetToQuestItem.end()) { - SPDLOG_ERROR("HasItem: song RandomizerGet {} missing from RandoGetToQuestItem", - static_cast(itemName)); - assert(false); - return false; - } - if (splitAnywhere) { - const SplitSongDef* sdef = SplitSongs::GetSongDefFromFullSong(itemName); - if (sdef != nullptr && SplitSongs::HasFullSong(sdef->id)) { - return true; - } - } - return CheckQuestItem(qiIt->second); - } + case RG_PRELUDE_OF_LIGHT: // Dungeon Rewards case RG_KOKIRI_EMERALD: case RG_GORON_RUBY: @@ -1873,6 +1852,18 @@ std::map Logic::RandoGetToRandInf = { { RG_BACK_TOWER_KEY, RAND_INF_BACK_TOWER_KEY_OBTAINED }, { RG_HYLIA_LAB_KEY, RAND_INF_HYLIA_LAB_KEY_OBTAINED }, { RG_FISHING_HOLE_KEY, RAND_INF_FISHING_HOLE_KEY_OBTAINED }, + { RG_PART_OF_ZELDAS_LULLABY, RAND_INF_SPLIT_ZL_PART }, + { RG_PART_OF_EPONAS_SONG, RAND_INF_SPLIT_EPONA_PART }, + { RG_PART_OF_SARIAS_SONG, RAND_INF_SPLIT_SARIA_PART }, + { RG_PART_OF_SUNS_SONG, RAND_INF_SPLIT_SUN_PART }, + { RG_PART_OF_SONG_OF_TIME, RAND_INF_SPLIT_TIME_PART }, + { RG_PART_OF_SONG_OF_STORMS, RAND_INF_SPLIT_STORMS_PART }, + { RG_PART_OF_MINUET_OF_FOREST, RAND_INF_SPLIT_MINUET_PART }, + { RG_PART_OF_BOLERO_OF_FIRE, RAND_INF_SPLIT_BOLERO_PART }, + { RG_PART_OF_SERENADE_OF_WATER, RAND_INF_SPLIT_SERENADE_PART }, + { RG_PART_OF_REQUIEM_OF_SPIRIT, RAND_INF_SPLIT_REQUIEM_PART }, + { RG_PART_OF_NOCTURNE_OF_SHADOW, RAND_INF_SPLIT_NOCTURNE_PART }, + { RG_PART_OF_PRELUDE_OF_LIGHT, RAND_INF_SPLIT_PRELUDE_PART }, }; std::map Logic::RandoGetToDungeonScene = { @@ -2207,6 +2198,26 @@ void Logic::ApplyItemEffect(Item& item, bool state) { } SetInventory(ITEM_OCARINA_FAIRY, OcarinaLookup[i]); } break; + case RG_PROGRESSIVE_ZELDAS_LULLABY: + case RG_PROGRESSIVE_EPONAS_SONG: + case RG_PROGRESSIVE_SARIAS_SONG: + case RG_PROGRESSIVE_SONG_OF_TIME: + case RG_PROGRESSIVE_SUNS_SONG: + case RG_PROGRESSIVE_SONG_OF_STORMS: + case RG_PROGRESSIVE_MINUET_OF_FOREST: + case RG_PROGRESSIVE_BOLERO_OF_FIRE: + case RG_PROGRESSIVE_SERENADE_OF_WATER: + case RG_PROGRESSIVE_REQUIEM_OF_SPIRIT: + case RG_PROGRESSIVE_NOCTURNE_OF_SHADOW: + case RG_PROGRESSIVE_PRELUDE_OF_LIGHT: { + const SongData* song = &StaticData::songData.at(randoGet); + if (CheckRandoInf(song->randInf)) { + SetQuestItem(song->quest, state); + } else { + SetRandoInf(song->randInf, state); + } + break; + } case RG_HEART_CONTAINER: mSaveContext->healthCapacity += (!state ? -16 : 16); break; @@ -2324,6 +2335,18 @@ void Logic::ApplyItemEffect(Item& item, bool state) { case RG_BACK_TOWER_KEY: case RG_HYLIA_LAB_KEY: case RG_FISHING_HOLE_KEY: + case RG_PART_OF_ZELDAS_LULLABY: + case RG_PART_OF_EPONAS_SONG: + case RG_PART_OF_SARIAS_SONG: + case RG_PART_OF_SUNS_SONG: + case RG_PART_OF_SONG_OF_TIME: + case RG_PART_OF_SONG_OF_STORMS: + case RG_PART_OF_MINUET_OF_FOREST: + case RG_PART_OF_BOLERO_OF_FIRE: + case RG_PART_OF_SERENADE_OF_WATER: + case RG_PART_OF_REQUIEM_OF_SPIRIT: + case RG_PART_OF_NOCTURNE_OF_SHADOW: + case RG_PART_OF_PRELUDE_OF_LIGHT: SetRandoInf(RandoGetToRandInf.at(randoGet), state); break; case RG_TRIFORCE_PIECE: @@ -2374,31 +2397,9 @@ void Logic::ApplyItemEffect(Item& item, bool state) { } } break; case ITEMTYPE_DUNGEONREWARD: - case ITEMTYPE_SONG: { - RandomizerGet rg = item.GetRandomizerGet(); - if (SplitSongs::IsProgressiveSong(rg)) { - const SplitSongDef* def = SplitSongs::GetSongDefFromProgressive(rg); - if (def != nullptr) { - const RandomizerInf partFlag = SplitSongs::GetPartFlag(def->id); - auto qi = RandoGetToQuestItem.find(static_cast(def->fullSong)); - if (!CheckRandoInf(partFlag) && state) { - SetRandoInf(partFlag, true); - } else if (qi != RandoGetToQuestItem.end() && CheckRandoInf(partFlag) && - !CheckQuestItem(qi->second) && state) { - SetQuestItem(qi->second, true); - } else if (qi != RandoGetToQuestItem.end() && CheckQuestItem(qi->second) && !state) { - SetQuestItem(qi->second, false); - } else if (CheckRandoInf(partFlag) && !state) { - SetRandoInf(partFlag, false); - } - } - break; - } - auto qi = RandoGetToQuestItem.find(rg); - if (qi != RandoGetToQuestItem.end()) { - SetQuestItem(qi->second, state); - } - } break; + case ITEMTYPE_SONG: + SetQuestItem(RandoGetToQuestItem.find(item.GetRandomizerGet())->second, state); + break; case ITEMTYPE_MAP: SetDungeonItem(DUNGEON_MAP, RandoGetToDungeonScene.find(item.GetRandomizerGet())->second, state); break; diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index d6406764084..d372f3af9f3 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -21,7 +21,6 @@ #include "soh/OTRGlobals.h" #include #include "static_data.h" -#include "split_songs.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "trial.h" #include "settings.h" @@ -158,6 +157,18 @@ std::unordered_map randomizerGetToRandInf = { { RG_BONGO_BONGO_SOUL, RAND_INF_BONGO_BONGO_SOUL }, { RG_TWINROVA_SOUL, RAND_INF_TWINROVA_SOUL }, { RG_GANON_SOUL, RAND_INF_GANON_SOUL }, + { RG_PART_OF_ZELDAS_LULLABY, RAND_INF_SPLIT_ZL_PART }, + { RG_PART_OF_EPONAS_SONG, RAND_INF_SPLIT_EPONA_PART }, + { RG_PART_OF_SARIAS_SONG, RAND_INF_SPLIT_SARIA_PART }, + { RG_PART_OF_SUNS_SONG, RAND_INF_SPLIT_SUN_PART }, + { RG_PART_OF_SONG_OF_TIME, RAND_INF_SPLIT_TIME_PART }, + { RG_PART_OF_SONG_OF_STORMS, RAND_INF_SPLIT_STORMS_PART }, + { RG_PART_OF_MINUET_OF_FOREST, RAND_INF_SPLIT_MINUET_PART }, + { RG_PART_OF_BOLERO_OF_FIRE, RAND_INF_SPLIT_BOLERO_PART }, + { RG_PART_OF_SERENADE_OF_WATER, RAND_INF_SPLIT_SERENADE_PART }, + { RG_PART_OF_REQUIEM_OF_SPIRIT, RAND_INF_SPLIT_REQUIEM_PART }, + { RG_PART_OF_NOCTURNE_OF_SHADOW, RAND_INF_SPLIT_NOCTURNE_PART }, + { RG_PART_OF_PRELUDE_OF_LIGHT, RAND_INF_SPLIT_PRELUDE_PART }, }; #ifdef _MSC_VER @@ -297,9 +308,6 @@ ItemObtainability Randomizer::GetItemObtainabilityFromRandomizerCheck(Randomizer } ItemObtainability Randomizer::GetItemObtainabilityFromRandomizerGet(RandomizerGet randoGet) { - if (Rando::SplitSongs::IsProgressiveSong(randoGet)) { - return Rando::SplitSongs::GetProgressiveSongObtainability(randoGet); - } if (randomizerGetToRandInf.find(randoGet) != randomizerGetToRandInf.end()) { return Flags_GetRandomizerInf(randomizerGetToRandInf.find(randoGet)->second) ? CANT_OBTAIN_ALREADY_HAVE : CAN_OBTAIN; @@ -566,28 +574,40 @@ ItemObtainability Randomizer::GetItemObtainabilityFromRandomizerGet(RandomizerGe return !Flags_GetRandomizerInf(RAND_INF_FISHING_POLE_FOUND) ? CAN_OBTAIN : CANT_OBTAIN_ALREADY_HAVE; // Songs + case RG_PROGRESSIVE_ZELDAS_LULLABY: case RG_ZELDAS_LULLABY: return !CHECK_QUEST_ITEM(QUEST_SONG_LULLABY) ? CAN_OBTAIN : CANT_OBTAIN_ALREADY_HAVE; + case RG_PROGRESSIVE_EPONAS_SONG: case RG_EPONAS_SONG: return !CHECK_QUEST_ITEM(QUEST_SONG_EPONA) ? CAN_OBTAIN : CANT_OBTAIN_ALREADY_HAVE; + case RG_PROGRESSIVE_SARIAS_SONG: case RG_SARIAS_SONG: return !CHECK_QUEST_ITEM(QUEST_SONG_SARIA) ? CAN_OBTAIN : CANT_OBTAIN_ALREADY_HAVE; + case RG_PROGRESSIVE_SUNS_SONG: case RG_SUNS_SONG: return !CHECK_QUEST_ITEM(QUEST_SONG_SUN) ? CAN_OBTAIN : CANT_OBTAIN_ALREADY_HAVE; + case RG_PROGRESSIVE_SONG_OF_TIME: case RG_SONG_OF_TIME: return !CHECK_QUEST_ITEM(QUEST_SONG_TIME) ? CAN_OBTAIN : CANT_OBTAIN_ALREADY_HAVE; + case RG_PROGRESSIVE_SONG_OF_STORMS: case RG_SONG_OF_STORMS: return !CHECK_QUEST_ITEM(QUEST_SONG_STORMS) ? CAN_OBTAIN : CANT_OBTAIN_ALREADY_HAVE; + case RG_PROGRESSIVE_MINUET_OF_FOREST: case RG_MINUET_OF_FOREST: return !CHECK_QUEST_ITEM(QUEST_SONG_MINUET) ? CAN_OBTAIN : CANT_OBTAIN_ALREADY_HAVE; + case RG_PROGRESSIVE_BOLERO_OF_FIRE: case RG_BOLERO_OF_FIRE: return !CHECK_QUEST_ITEM(QUEST_SONG_BOLERO) ? CAN_OBTAIN : CANT_OBTAIN_ALREADY_HAVE; + case RG_PROGRESSIVE_SERENADE_OF_WATER: case RG_SERENADE_OF_WATER: return !CHECK_QUEST_ITEM(QUEST_SONG_SERENADE) ? CAN_OBTAIN : CANT_OBTAIN_ALREADY_HAVE; + case RG_PROGRESSIVE_REQUIEM_OF_SPIRIT: case RG_REQUIEM_OF_SPIRIT: return !CHECK_QUEST_ITEM(QUEST_SONG_REQUIEM) ? CAN_OBTAIN : CANT_OBTAIN_ALREADY_HAVE; + case RG_PROGRESSIVE_NOCTURNE_OF_SHADOW: case RG_NOCTURNE_OF_SHADOW: return !CHECK_QUEST_ITEM(QUEST_SONG_NOCTURNE) ? CAN_OBTAIN : CANT_OBTAIN_ALREADY_HAVE; + case RG_PROGRESSIVE_PRELUDE_OF_LIGHT: case RG_PRELUDE_OF_LIGHT: return !CHECK_QUEST_ITEM(QUEST_SONG_PRELUDE) ? CAN_OBTAIN : CANT_OBTAIN_ALREADY_HAVE; @@ -1115,15 +1135,6 @@ extern "C" u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry) { // Gameplay stats: Update the time the item was obtained Randomizer_GameplayStats_SetTimestamp(item); - if (Rando::SplitSongs::IsProgressiveSong(item)) { - const RandomizerGet resolved = Rando::SplitSongs::ResolveProgressiveSongStage(item); - if (resolved != item && resolved != RG_NONE) { - return Randomizer_Item_Give(play, Rando::StaticData::RetrieveItem(resolved).GetGIEntry_Copy()); - } - Rando::SplitSongs::OnProgressiveSongReceived(item); - return Return_Item_Entry(giEntry, RG_NONE); - } - // if it's an item that just sets a randomizerInf, set it if (randomizerGetToRandInf.find(item) != randomizerGetToRandInf.end()) { Flags_SetRandomizerInf(randomizerGetToRandInf.find(item)->second); diff --git a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerGet.h b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerGet.h index 4ef7e3ca296..99ec9441c53 100644 --- a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerGet.h +++ b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerGet.h @@ -78,6 +78,18 @@ RANDO_ENUM_ITEM(RG_PROGRESSIVE_SERENADE_OF_WATER) RANDO_ENUM_ITEM(RG_PROGRESSIVE_REQUIEM_OF_SPIRIT) RANDO_ENUM_ITEM(RG_PROGRESSIVE_NOCTURNE_OF_SHADOW) RANDO_ENUM_ITEM(RG_PROGRESSIVE_PRELUDE_OF_LIGHT) +RANDO_ENUM_ITEM(RG_PART_OF_ZELDAS_LULLABY) +RANDO_ENUM_ITEM(RG_PART_OF_EPONAS_SONG) +RANDO_ENUM_ITEM(RG_PART_OF_SARIAS_SONG) +RANDO_ENUM_ITEM(RG_PART_OF_SUNS_SONG) +RANDO_ENUM_ITEM(RG_PART_OF_SONG_OF_TIME) +RANDO_ENUM_ITEM(RG_PART_OF_SONG_OF_STORMS) +RANDO_ENUM_ITEM(RG_PART_OF_MINUET_OF_FOREST) +RANDO_ENUM_ITEM(RG_PART_OF_BOLERO_OF_FIRE) +RANDO_ENUM_ITEM(RG_PART_OF_SERENADE_OF_WATER) +RANDO_ENUM_ITEM(RG_PART_OF_REQUIEM_OF_SPIRIT) +RANDO_ENUM_ITEM(RG_PART_OF_NOCTURNE_OF_SHADOW) +RANDO_ENUM_ITEM(RG_PART_OF_PRELUDE_OF_LIGHT) RANDO_ENUM_ITEM(RG_EMPTY_BOTTLE) RANDO_ENUM_ITEM(RG_BOTTLE_WITH_MILK) RANDO_ENUM_ITEM(RG_BOTTLE_WITH_RED_POTION) diff --git a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerInf.h b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerInf.h index f32c7c660c0..b01bdeec044 100644 --- a/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerInf.h +++ b/soh/soh/Enhancements/randomizer/randomizerEnums/RandomizerInf.h @@ -2642,18 +2642,18 @@ RANDO_ENUM_ITEM(RAND_INF_OBTAINED_RUTOS_LETTER) RANDO_ENUM_ITEM(RAND_INF_OBTAINED_NAYRUS_LOVE) RANDO_ENUM_ITEM(RAND_INF_OBTAINED_ROCS_FEATHER) RANDO_ENUM_ITEM(RAND_INF_TALON_SENT_MALON_HOME) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_ZL_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_EPONA_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_SARIA_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_SUN_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_TIME_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_STORMS_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_MINUET_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_BOLERO_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_SERENADE_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_REQUIEM_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_NOCTURNE_PART1) -RANDO_ENUM_ITEM(RAND_INF_SPLIT_PRELUDE_PART1) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_ZL_PART) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_EPONA_PART) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_SARIA_PART) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_SUN_PART) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_TIME_PART) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_STORMS_PART) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_MINUET_PART) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_BOLERO_PART) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_SERENADE_PART) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_REQUIEM_PART) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_NOCTURNE_PART) +RANDO_ENUM_ITEM(RAND_INF_SPLIT_PRELUDE_PART) // Overworld Signs RANDO_ENUM_ITEM(RAND_INF_KF_DEKU_TREE_RECTANGLE_SIGN) diff --git a/soh/soh/Enhancements/randomizer/randomizerTypes.h b/soh/soh/Enhancements/randomizer/randomizerTypes.h index 92bba438bcb..efc18ec886f 100644 --- a/soh/soh/Enhancements/randomizer/randomizerTypes.h +++ b/soh/soh/Enhancements/randomizer/randomizerTypes.h @@ -44,6 +44,13 @@ typedef struct ShopItemIdentity { int32_t itemPrice; } ShopItemIdentity; +struct SongData { + RandomizerGet realSong; + RandomizerGet part; + RandomizerInf randInf; + QuestItem quest; +}; + #define ENTRANCE_GROTTO_LOAD_START 0x0700 #define ENTRANCE_GROTTO_EXIT_START 0x0800 diff --git a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp index b609a10d45b..6d97a9d4613 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp @@ -14,7 +14,7 @@ #include "randomizerTypes.h" #include "logic.h" #include "SeedContext.h" -#include "split_songs.h" +#include "static_data.h" #include "static_data.h" #include "soh/SohGui/ImGuiUtils.h" #include "soh/cvar_prefixes.h" @@ -466,40 +466,6 @@ static bool ItemTrackerSplitSongsActive() { return CVarGetInteger(CVAR_RANDOMIZER_SETTING("SplitOcarinaSongs"), 0) != 0; } -static const Rando::SplitSongDef* SplitSongDefForQuestBit(uint32_t questBit) { - for (int i = 0; i < static_cast(Rando::SplitSongId::SPLIT_SONG_MAX); i++) { - const Rando::SplitSongDef* def = Rando::SplitSongs::GetSongDef(static_cast(i)); - if (def == nullptr) { - continue; - } - const auto qiIt = Rando::Logic::RandoGetToQuestItem.find(static_cast(def->fullSong)); - if (qiIt != Rando::Logic::RandoGetToQuestItem.end() && static_cast(qiIt->second) == questBit) { - return def; - } - } - return nullptr; -} - -static int SplitSongPartsCollected(uint32_t questBit) { - if (CVarGetInteger(CVAR_GENERAL("RandoGenerating"), 0) != 0) { - return -1; - } - if (!ItemTrackerSplitSongsActive()) { - return -1; - } - const Rando::SplitSongDef* def = SplitSongDefForQuestBit(questBit); - if (def == nullptr) { - return -1; - } - if (Rando::SplitSongs::HasFullSong(def->id)) { - return 2; - } - if (Rando::SplitSongs::HasSplitPart(def->id)) { - return 1; - } - return 0; -} - struct ItemTrackerNumbers { int currentCapacity; int maxCapacity; @@ -1416,21 +1382,25 @@ void DrawDungeonItem(ItemTrackerItem item) { } void DrawSong(ItemTrackerItem item) { - const int partsCollected = SplitSongPartsCollected(item.id); - const bool hasSong = HasSong(item) || partsCollected == 2; - const bool hasProgress = partsCollected == 1; - const bool showBright = hasSong || hasProgress; + const SongData* song = + &Rando::StaticData::songData[Rando::StaticData::songQuestToProg.at(static_cast(item.id))]; + const bool hasSong = HasSong(item); float iconSize = static_cast(CVarGetInteger(CVAR_TRACKER_ITEM("IconSize"), 36)); ImGui::BeginGroup(); ImVec2 p = ImGui::GetCursorScreenPos(); ImGui::SetCursorScreenPos(ImVec2(p.x + 6, p.y)); ImGui::Image(std::dynamic_pointer_cast(Ship::Context::GetRawInstance()->GetWindow()->GetGui()) - ->GetTextureByName(showBright && IsValidSaveFile() ? item.name : item.nameFaded), + ->GetTextureByName(hasSong && IsValidSaveFile() ? item.name : item.nameFaded), ImVec2(iconSize / 1.5f, iconSize), ImVec2(0, 0), ImVec2(1, 1)); - if (hasProgress) { - const char* progressLabel = "1/2"; + if (ItemTrackerSplitSongsActive()) { + const char* progressLabel = "0/2"; + if (hasSong) { + progressLabel = "2/2"; + } else if (Flags_GetRandomizerInf(song->randInf)) { + progressLabel = "1/2"; + } const ImVec2 iconMin = ImGui::GetItemRectMin(); const ImVec2 iconMax = ImGui::GetItemRectMax(); const ImVec2 textSize = ImGui::CalcTextSize(progressLabel); diff --git a/soh/soh/Enhancements/randomizer/split_songs.cpp b/soh/soh/Enhancements/randomizer/split_songs.cpp deleted file mode 100644 index 7eb2881e304..00000000000 --- a/soh/soh/Enhancements/randomizer/split_songs.cpp +++ /dev/null @@ -1,164 +0,0 @@ -// split_songs.cpp — Split Ocarina Songs (randomizer) -// Created by RaccoonCloud. - -#include "split_songs.h" - -#include "SeedContext.h" -#include "logic.h" -#include "static_data.h" - -#include "global.h" -#include "macros.h" - -namespace Rando { - -static constexpr uint32_t kSplitSongPartFlags[SPLIT_SONG_MAX] = { - RAND_INF_SPLIT_ZL_PART1, RAND_INF_SPLIT_EPONA_PART1, RAND_INF_SPLIT_SARIA_PART1, - RAND_INF_SPLIT_SUN_PART1, RAND_INF_SPLIT_TIME_PART1, RAND_INF_SPLIT_STORMS_PART1, - RAND_INF_SPLIT_MINUET_PART1, RAND_INF_SPLIT_BOLERO_PART1, RAND_INF_SPLIT_SERENADE_PART1, - RAND_INF_SPLIT_REQUIEM_PART1, RAND_INF_SPLIT_NOCTURNE_PART1, RAND_INF_SPLIT_PRELUDE_PART1, -}; - -static constexpr SplitSongDef kSplitSongs[SPLIT_SONG_MAX] = { - { SPLIT_SONG_ZELDAS_LULLABY, RG_PROGRESSIVE_ZELDAS_LULLABY, RG_ZELDAS_LULLABY }, - { SPLIT_SONG_EPONAS_SONG, RG_PROGRESSIVE_EPONAS_SONG, RG_EPONAS_SONG }, - { SPLIT_SONG_SARIAS_SONG, RG_PROGRESSIVE_SARIAS_SONG, RG_SARIAS_SONG }, - { SPLIT_SONG_SUNS_SONG, RG_PROGRESSIVE_SUNS_SONG, RG_SUNS_SONG }, - { SPLIT_SONG_SONG_OF_TIME, RG_PROGRESSIVE_SONG_OF_TIME, RG_SONG_OF_TIME }, - { SPLIT_SONG_SONG_OF_STORMS, RG_PROGRESSIVE_SONG_OF_STORMS, RG_SONG_OF_STORMS }, - { SPLIT_SONG_MINUET_OF_FOREST, RG_PROGRESSIVE_MINUET_OF_FOREST, RG_MINUET_OF_FOREST }, - { SPLIT_SONG_BOLERO_OF_FIRE, RG_PROGRESSIVE_BOLERO_OF_FIRE, RG_BOLERO_OF_FIRE }, - { SPLIT_SONG_SERENADE_OF_WATER, RG_PROGRESSIVE_SERENADE_OF_WATER, RG_SERENADE_OF_WATER }, - { SPLIT_SONG_REQUIEM_OF_SPIRIT, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, RG_REQUIEM_OF_SPIRIT }, - { SPLIT_SONG_NOCTURNE_OF_SHADOW, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, RG_NOCTURNE_OF_SHADOW }, - { SPLIT_SONG_PRELUDE_OF_LIGHT, RG_PROGRESSIVE_PRELUDE_OF_LIGHT, RG_PRELUDE_OF_LIGHT }, -}; - -static bool IsValidSplitSongId(SplitSongId id) { - return id >= 0 && id < SPLIT_SONG_MAX; -} - -static bool UsingLogicSimulationBuffer(Logic* logic) { - return logic != nullptr && logic->mSaveContext != nullptr && logic->mSaveContext != &gSaveContext; -} - -const SplitSongDef* SplitSongs::GetSongDef(SplitSongId id) { - if (!IsValidSplitSongId(id)) { - return nullptr; - } - return &kSplitSongs[id]; -} - -const SplitSongDef* SplitSongs::GetSongDefFromProgressive(RandomizerGet rg) { - for (const auto& def : kSplitSongs) { - if (def.progressive == rg) { - return &def; - } - } - return nullptr; -} - -const SplitSongDef* SplitSongs::GetSongDefFromFullSong(RandomizerGet fullSongRg) { - for (const auto& def : kSplitSongs) { - if (def.fullSong == fullSongRg) { - return &def; - } - } - return nullptr; -} - -bool SplitSongs::IsProgressiveSong(RandomizerGet rg) { - return GetSongDefFromProgressive(rg) != nullptr; -} - -bool SplitSongs::HasSplitPart(SplitSongId id) { - if (!IsValidSplitSongId(id)) { - return false; - } - const RandomizerInf flag = static_cast(kSplitSongPartFlags[id]); - auto* logic = Context::GetInstance()->GetLogic().get(); - if (UsingLogicSimulationBuffer(logic)) { - return logic->CheckRandoInf(flag); - } - return Flags_GetRandomizerInf(flag) != 0; -} - -void SplitSongs::SetSplitPart(SplitSongId id, bool state) { - if (!IsValidSplitSongId(id)) { - return; - } - RandomizerInf flag = static_cast(kSplitSongPartFlags[id]); - if (state) { - Flags_SetRandomizerInf(flag); - } else { - Flags_UnsetRandomizerInf(flag); - } -} - -bool SplitSongs::HasFullSong(SplitSongId id) { - const SplitSongDef* def = GetSongDef(id); - if (def == nullptr) { - return false; - } - const auto qiIt = Logic::RandoGetToQuestItem.find(static_cast(def->fullSong)); - if (qiIt == Logic::RandoGetToQuestItem.end()) { - return false; - } - auto* logic = Context::GetInstance()->GetLogic().get(); - if (UsingLogicSimulationBuffer(logic)) { - return logic->CheckQuestItem(qiIt->second); - } - return CHECK_QUEST_ITEM(qiIt->second); -} - -void SplitSongs::OnProgressiveSongReceived(RandomizerGet rg) { - const SplitSongDef* def = GetSongDefFromProgressive(rg); - if (def == nullptr || HasFullSong(def->id) || HasSplitPart(def->id)) { - return; - } - SetSplitPart(def->id, true); -} - -ItemObtainability SplitSongs::GetProgressiveSongObtainability(RandomizerGet progressiveRg) { - const SplitSongDef* def = GetSongDefFromProgressive(progressiveRg); - if (def == nullptr) { - return CAN_OBTAIN; - } - return HasFullSong(def->id) ? CANT_OBTAIN_ALREADY_HAVE : CAN_OBTAIN; -} - -RandomizerInf SplitSongs::GetPartFlag(SplitSongId id) { - if (!IsValidSplitSongId(id)) { - return RAND_INF_MAX; - } - return static_cast(kSplitSongPartFlags[id]); -} - -RandomizerGet SplitSongs::ResolveProgressiveSongStage(Logic* logic, RandomizerGet rg) { - const SplitSongDef* def = GetSongDefFromProgressive(rg); - if (def == nullptr) { - return RG_NONE; - } - - if (logic != nullptr && UsingLogicSimulationBuffer(logic)) { - const auto qiIt = Logic::RandoGetToQuestItem.find(static_cast(def->fullSong)); - if (qiIt != Logic::RandoGetToQuestItem.end() && logic->CheckQuestItem(qiIt->second)) { - return def->fullSong; - } - if (logic->CheckRandoInf(static_cast(kSplitSongPartFlags[def->id]))) { - return def->fullSong; - } - return def->progressive; - } - - if (HasFullSong(def->id) || HasSplitPart(def->id)) { - return def->fullSong; - } - return def->progressive; -} - -RandomizerGet SplitSongs::ResolveProgressiveSongStage(RandomizerGet rg) { - return ResolveProgressiveSongStage(Context::GetInstance()->GetLogic().get(), rg); -} - -} // namespace Rando diff --git a/soh/soh/Enhancements/randomizer/split_songs.h b/soh/soh/Enhancements/randomizer/split_songs.h deleted file mode 100644 index 87766cac0bc..00000000000 --- a/soh/soh/Enhancements/randomizer/split_songs.h +++ /dev/null @@ -1,53 +0,0 @@ -// split_songs.h — Split Ocarina Songs (randomizer) -// Created by RaccoonCloud. - -#pragma once - -#include "randomizerTypes.h" - -namespace Rando { - -class Logic; - -enum SplitSongId { - SPLIT_SONG_ZELDAS_LULLABY = 0, - SPLIT_SONG_EPONAS_SONG, - SPLIT_SONG_SARIAS_SONG, - SPLIT_SONG_SUNS_SONG, - SPLIT_SONG_SONG_OF_TIME, - SPLIT_SONG_SONG_OF_STORMS, - SPLIT_SONG_MINUET_OF_FOREST, - SPLIT_SONG_BOLERO_OF_FIRE, - SPLIT_SONG_SERENADE_OF_WATER, - SPLIT_SONG_REQUIEM_OF_SPIRIT, - SPLIT_SONG_NOCTURNE_OF_SHADOW, - SPLIT_SONG_PRELUDE_OF_LIGHT, - SPLIT_SONG_MAX -}; - -struct SplitSongDef { - SplitSongId id; - RandomizerGet progressive; - RandomizerGet fullSong; -}; - -class SplitSongs { - public: - static const SplitSongDef* GetSongDef(SplitSongId id); - static const SplitSongDef* GetSongDefFromProgressive(RandomizerGet rg); - static const SplitSongDef* GetSongDefFromFullSong(RandomizerGet fullSongRg); - - static bool HasSplitPart(SplitSongId id); - static void SetSplitPart(SplitSongId id, bool state); - static bool HasFullSong(SplitSongId id); - - static bool IsProgressiveSong(RandomizerGet rg); - static void OnProgressiveSongReceived(RandomizerGet rg); - - static ItemObtainability GetProgressiveSongObtainability(RandomizerGet progressiveRg); - static RandomizerInf GetPartFlag(SplitSongId id); - static RandomizerGet ResolveProgressiveSongStage(RandomizerGet rg); - static RandomizerGet ResolveProgressiveSongStage(Logic* logic, RandomizerGet rg); -}; - -} // namespace Rando diff --git a/soh/soh/Enhancements/randomizer/static_data.cpp b/soh/soh/Enhancements/randomizer/static_data.cpp index 390febf5e80..8b47b6a571b 100644 --- a/soh/soh/Enhancements/randomizer/static_data.cpp +++ b/soh/soh/Enhancements/randomizer/static_data.cpp @@ -537,4 +537,46 @@ std::vector StaticData::overworldKeys = { RG_HYLIA_LAB_KEY, RG_FISHING_HOLE_KEY, }; + +std::unordered_map StaticData::songData = { + { RG_PROGRESSIVE_ZELDAS_LULLABY, + { RG_ZELDAS_LULLABY, RG_PART_OF_ZELDAS_LULLABY, RAND_INF_SPLIT_ZL_PART, QUEST_SONG_LULLABY } }, + { RG_PROGRESSIVE_EPONAS_SONG, + { RG_EPONAS_SONG, RG_PART_OF_EPONAS_SONG, RAND_INF_SPLIT_EPONA_PART, QUEST_SONG_EPONA } }, + { RG_PROGRESSIVE_SARIAS_SONG, + { RG_SARIAS_SONG, RG_PART_OF_SARIAS_SONG, RAND_INF_SPLIT_SARIA_PART, QUEST_SONG_SARIA } }, + { RG_PROGRESSIVE_SUNS_SONG, { RG_SUNS_SONG, RG_PART_OF_SUNS_SONG, RAND_INF_SPLIT_SUN_PART, QUEST_SONG_SUN } }, + { RG_PROGRESSIVE_SONG_OF_TIME, + { RG_SONG_OF_TIME, RG_PART_OF_SONG_OF_TIME, RAND_INF_SPLIT_TIME_PART, QUEST_SONG_TIME } }, + { RG_PROGRESSIVE_SONG_OF_STORMS, + { RG_SONG_OF_STORMS, RG_PART_OF_SONG_OF_STORMS, RAND_INF_SPLIT_STORMS_PART, QUEST_SONG_STORMS } }, + { RG_PROGRESSIVE_MINUET_OF_FOREST, + { RG_MINUET_OF_FOREST, RG_PART_OF_MINUET_OF_FOREST, RAND_INF_SPLIT_MINUET_PART, QUEST_SONG_MINUET } }, + { RG_PROGRESSIVE_BOLERO_OF_FIRE, + { RG_BOLERO_OF_FIRE, RG_PART_OF_BOLERO_OF_FIRE, RAND_INF_SPLIT_BOLERO_PART, QUEST_SONG_BOLERO } }, + { RG_PROGRESSIVE_SERENADE_OF_WATER, + { RG_SERENADE_OF_WATER, RG_PART_OF_SERENADE_OF_WATER, RAND_INF_SPLIT_SERENADE_PART, QUEST_SONG_SERENADE } }, + { RG_PROGRESSIVE_REQUIEM_OF_SPIRIT, + { RG_REQUIEM_OF_SPIRIT, RG_PART_OF_REQUIEM_OF_SPIRIT, RAND_INF_SPLIT_REQUIEM_PART, QUEST_SONG_REQUIEM } }, + { RG_PROGRESSIVE_NOCTURNE_OF_SHADOW, + { RG_NOCTURNE_OF_SHADOW, RG_PART_OF_NOCTURNE_OF_SHADOW, RAND_INF_SPLIT_NOCTURNE_PART, QUEST_SONG_NOCTURNE } }, + { RG_PROGRESSIVE_PRELUDE_OF_LIGHT, + { RG_PRELUDE_OF_LIGHT, RG_PART_OF_PRELUDE_OF_LIGHT, RAND_INF_SPLIT_PRELUDE_PART, QUEST_SONG_PRELUDE } }, +}; + +std::unordered_map StaticData::songQuestToProg = { + { QUEST_SONG_LULLABY, RG_PROGRESSIVE_ZELDAS_LULLABY }, + { QUEST_SONG_EPONA, RG_PROGRESSIVE_EPONAS_SONG }, + { QUEST_SONG_SARIA, RG_PROGRESSIVE_SARIAS_SONG }, + { QUEST_SONG_SUN, RG_PROGRESSIVE_SUNS_SONG }, + { QUEST_SONG_TIME, RG_PROGRESSIVE_SONG_OF_TIME }, + { QUEST_SONG_STORMS, RG_PROGRESSIVE_SONG_OF_STORMS }, + { QUEST_SONG_MINUET, RG_PROGRESSIVE_MINUET_OF_FOREST }, + { QUEST_SONG_BOLERO, RG_PROGRESSIVE_BOLERO_OF_FIRE }, + { QUEST_SONG_SERENADE, RG_PROGRESSIVE_SERENADE_OF_WATER }, + { QUEST_SONG_REQUIEM, RG_PROGRESSIVE_REQUIEM_OF_SPIRIT }, + { QUEST_SONG_NOCTURNE, RG_PROGRESSIVE_NOCTURNE_OF_SHADOW }, + { QUEST_SONG_PRELUDE, RG_PROGRESSIVE_PRELUDE_OF_LIGHT }, +}; + } // namespace Rando diff --git a/soh/soh/Enhancements/randomizer/static_data.h b/soh/soh/Enhancements/randomizer/static_data.h index eefe2681944..1cf7ffd7051 100644 --- a/soh/soh/Enhancements/randomizer/static_data.h +++ b/soh/soh/Enhancements/randomizer/static_data.h @@ -101,6 +101,8 @@ class StaticData { static std::set restrictTrade; static std::set allowMasks; static std::set allowBottleMaskTrade; + static std::unordered_map songData; + static std::unordered_map songQuestToProg; StaticData(); ~StaticData();