diff --git a/soh/assets/custom/presets/Rando Seed Settings - Hell Mode.json b/soh/assets/custom/presets/Rando Seed Settings - Hell Mode.json index 5e0d7e5863..f5baa80047 100644 --- a/soh/assets/custom/presets/Rando Seed Settings - Hell Mode.json +++ b/soh/assets/custom/presets/Rando Seed Settings - Hell Mode.json @@ -4,7 +4,7 @@ "gRandoSettings": { "BigPoeTargetCount": 1, "BlueFireArrows": 1, - "BombchuBag": 1, + "BombchuBag": 2, "BossKeysanity": 5, "ClosedForest": 2, "CuccosToReturn": 1, @@ -21,11 +21,13 @@ "LacsRewardCount": 10, "LacsRewardOptions": 1, "LockOverworldDoors": 1, + "MedallionLockedTrials": 1, "MixBosses": 1, "MixDungeons": 1, "MixGrottos": 1, "MixInteriors": 1, "MixOverworld": 1, + "MixThievesHideout": 1, "MixedEntrances": 1, "RainbowBridge": 7, "ScrubsPrices": 2, @@ -35,12 +37,16 @@ "Shuffle100GSReward": 1, "ShuffleAdultTrade": 1, "ShuffleBeanFairies": 1, + "ShuffleBeanSouls": 1, "ShuffleBeehives": 1, "ShuffleBossEntrances": 2, "ShuffleBossSouls": 2, + "ShuffleBushes": 1, "ShuffleChildWallet": 1, + "ShuffleClimb": 1, "ShuffleCows": 1, "ShuffleCrates": 3, + "ShuffleCrawl": 1, "ShuffleDekuNutBag": 1, "ShuffleDekuStickBag": 1, "ShuffleDungeonsEntrances": 2, @@ -50,7 +56,9 @@ "ShuffleFreestanding": 3, "ShuffleFrogSongRupees": 1, "ShuffleGanonBossKey": 9, + "ShuffleGanonTowerEntrance": 1, "ShuffleGerudoToken": 1, + "ShuffleGrab": 1, "ShuffleGrass": 3, "ShuffleGrottosEntrances": 1, "ShuffleInteriorsEntrances": 2, @@ -59,18 +67,21 @@ "ShuffleMerchants": 3, "ShuffleOcarinaButtons": 1, "ShuffleOcarinas": 1, + "ShuffleOpenChest": 1, "ShuffleOverworldEntrances": 1, "ShuffleOverworldSpawns": 1, "ShuffleOwlDrops": 1, "ShufflePots": 3, "ShuffleScrubs": 2, "ShuffleSongs": 2, + "ShuffleSpeak": 1, "ShuffleStoneFairies": 1, "ShuffleSwim": 1, + "ShuffleThievesHideoutEntrances": 1, + "ShuffleTrees": 1, "ShuffleTokens": 3, "ShuffleWarpSongs": 1, "ShuffleWeirdEgg": 1, - "SkipEponaRace": 1, "StartingAge": 2, "StartingHearts": 0, "StartingMapsCompasses": 5, diff --git a/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp b/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp index 6938b7cd7e..1ec49e0afe 100644 --- a/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp +++ b/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp @@ -19,6 +19,7 @@ extern "C" { #include "src/overlays/actors/ovl_En_Blkobj/z_en_blkobj.h" #include "src/overlays/actors/ovl_En_Encount1/z_en_encount1.h" #include "src/overlays/actors/ovl_En_GeldB/z_en_geldb.h" +#include "src/overlays/actors/ovl_En_Peehat/z_en_peehat.h" #include "src/overlays/actors/ovl_En_Rr/z_en_rr.h" #include "src/overlays/actors/ovl_En_Vali/z_en_vali.h" @@ -37,8 +38,8 @@ extern std::shared_ptr mSohMenu; typedef struct EnemyEntry { const char* cvar; const char* name; - int16_t id; - int16_t params; + s16 id; + s16 params; } EnemyEntry; // clang-format off @@ -60,7 +61,7 @@ static EnemyEntry randomizedEnemySpawnTable[] = { { CVAR_ENHANCEMENT("RandomizedEnemyList.Dinolfos"), "Dinolfos", ACTOR_EN_ZF, -2 }, // Dinolfos { CVAR_ENHANCEMENT("RandomizedEnemyList.Dodongo"), "Dodongo", ACTOR_EN_DODONGO, -1 }, // Dodongo { CVAR_ENHANCEMENT("RandomizedEnemyList.FireKeese"), "Fire Keese", ACTOR_EN_FIREFLY, 1 }, // Fire Keese - // { CVAR_ENHANCEMENT("RandomizedEnemyList.FlareDancer"), "Flare Dancer", ACTOR_EN_FD, 0 }, // Flare Dancer (possible cause of crashes because of spawning flame actors on sloped ground) + // { CVAR_ENHANCEMENT("RandomizedEnemyList.FlareDancer"), "Flare Dancer", ACTOR_EN_FD, 0 }, // Flare Dancer (possible cause of crashes because of spawning flame actors on sloped ground or overloading) { CVAR_ENHANCEMENT("RandomizedEnemyList.FloorTile"), "Floor Tile", ACTOR_EN_YUKABYUN, 0 }, // Flying Floor Tile { CVAR_ENHANCEMENT("RandomizedEnemyList.Floormaster"), "Floormaster", ACTOR_EN_FLOORMAS, 0 }, // Floormaster { CVAR_ENHANCEMENT("RandomizedEnemyList.FlyingPeahat"), "Flying Peahat", ACTOR_EN_PEEHAT, -1 }, // Flying Peahat (big grounded, doesn't spawn larva) @@ -80,16 +81,16 @@ static EnemyEntry randomizedEnemySpawnTable[] = { { CVAR_ENHANCEMENT("RandomizedEnemyList.InvisStalfos"), "Invisible Stalfos", ACTOR_EN_TEST, 0 }, // Stalfos (invisible) { CVAR_ENHANCEMENT("RandomizedEnemyList.Keese"), "Keese", ACTOR_EN_FIREFLY, 2 }, // Regular Keese { CVAR_ENHANCEMENT("RandomizedEnemyList.LargeBaba"), "Large Deku Baba", ACTOR_EN_DEKUBABA, 1 }, // Deku Baba (large) - // { CVAR_ENHANCEMENT("RandomizedEnemyList.Leever"), "Leever", ACTOR_EN_REEBA, 0 }, // Leever Doesn't work (reliant on surface, without a spawner it kills itself too quickly) + // { CVAR_ENHANCEMENT("RandomizedEnemyList.Leever"), "Leever", ACTOR_EN_REEBA, 0 }, // Leever Doesn't work (reliant on surface, without a spawner it kills itself too quickly) { CVAR_ENHANCEMENT("RandomizedEnemyList.LikeLike"), "Like-Like", ACTOR_EN_RR, 0 }, // Like-Like { CVAR_ENHANCEMENT("RandomizedEnemyList.Lizalfos"), "Lizalfos", ACTOR_EN_ZF, -1 }, // Lizalfos { CVAR_ENHANCEMENT("RandomizedEnemyList.MadScrub"), "Mad Scrub", ACTOR_EN_DEKUNUTS, 768 }, // Mad Scrub (triple attack) (projectiles don't work) { CVAR_ENHANCEMENT("RandomizedEnemyList.NormalWolfos"), "Wolfos (Normal)", ACTOR_EN_WF, 0 }, // Wolfos (normal) - // { CVAR_ENHANCEMENT("RandomizedEnemyList.Octorok"), "Octorok", ACTOR_EN_OKUTA, 0 }, // Octorok Doesn't work (actor directly uses water box collision to handle hiding/popping up) + // { CVAR_ENHANCEMENT("RandomizedEnemyList.Octorok"), "Octorok", ACTOR_EN_OKUTA, 0 }, // Octorok Doesn't work (actor directly uses water box collision to handle hiding/popping up) { CVAR_ENHANCEMENT("RandomizedEnemyList.PeahatLarva"), "Peahat Larva", ACTOR_EN_PEEHAT, 1 }, // Flying Peahat Larva - // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe"), "Poe", ACTOR_EN_POH, 0 }, // Poe Doesn't work (Seems to rely on other objects?) - // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe"), "Poe", ACTOR_EN_POH, 2 }, // Poe (composer Sharp) Doesn't work (Seems to rely on other objects?) - // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe"), "Poe", ACTOR_EN_POH, 3 }, // Poe (composer Flat) Doesn't work (Seems to rely on other objects?) + // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe"), "Poe", ACTOR_EN_POH, 0 }, // Poe Doesn't work (Seems to rely on other objects?) + // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe.Sharp"), "Poe (Sharp)", ACTOR_EN_POH, 2 }, // Poe (composer Sharp) Doesn't work (Seems to rely on other objects?) + // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe.Flat"), "Poe (Flat)", ACTOR_EN_POH, 3 }, // Poe (composer Flat) Doesn't work (Seems to rely on other objects?) { CVAR_ENHANCEMENT("RandomizedEnemyList.Redead"), "Redead", ACTOR_EN_RD, 1 }, // Redead (standing) { CVAR_ENHANCEMENT("RandomizedEnemyList.RedTektite"), "Red Tektite", ACTOR_EN_TITE, -1 }, // Tektite (red) { CVAR_ENHANCEMENT("RandomizedEnemyList.Shabom"), "Shabom", ACTOR_EN_BUBBLE, 0 }, // Shabom (bubble) @@ -159,125 +160,205 @@ static int enemiesToRandomize[] = { // ACTOR_EN_REEBA, // Leever (reliant on spawner (z_en_encount1.c)) }; -bool IsEnemyAllowedToSpawn(int16_t sceneNum, int8_t roomNum, EnemyEntry enemy) { - uint32_t isMQ = ResourceMgr_IsSceneMasterQuest(sceneNum); - - // Freezard - Child Link can only kill this with Deku Stick jumpslash or other equipment like bombs. - // Beamos - Needs bombs. - // Anubis - Needs fire. - // Shell Blade & Spike - Child Link can't kill these with sword or Deku Stick. - // Flare dancer, Arwing & Dark Link - Both go out of bounds way too easily, softlocking the player. - // Wallmaster - Not easily visible, often makes players think they're softlocked and that there's no enemies left. - // Club Moblin - Many issues with them falling or placing out of bounds. Maybe fixable in the future? - bool enemiesToExcludeClearRooms = - enemy.id == ACTOR_EN_FZ || enemy.id == ACTOR_EN_VM || enemy.id == ACTOR_EN_SB || enemy.id == ACTOR_EN_NY || - enemy.id == ACTOR_EN_CLEAR_TAG || enemy.id == ACTOR_EN_WALLMAS || enemy.id == ACTOR_EN_TORCH2 || - (enemy.id == ACTOR_EN_MB && enemy.params == 0) || enemy.id == ACTOR_EN_FD || enemy.id == ACTOR_EN_ANUBICE_TAG; - - // Bari - Spawns 3 more enemies, potentially extremely difficult in timed rooms. - bool enemiesToExcludeTimedRooms = enemiesToExcludeClearRooms || enemy.id == ACTOR_EN_VALI; - - switch (sceneNum) { - // Deku Tree - case SCENE_DEKU_TREE: - return (!(!isMQ && enemiesToExcludeClearRooms && (roomNum == 1 || roomNum == 9)) && - !(isMQ && enemiesToExcludeClearRooms && - (roomNum == 4 || roomNum == 6 || roomNum == 9 || roomNum == 10))); - // Dodongo's Cavern - case SCENE_DODONGOS_CAVERN: - return (!(!isMQ && enemiesToExcludeClearRooms && roomNum == 15) && - !(isMQ && enemiesToExcludeClearRooms && (roomNum == 5 || roomNum == 13 || roomNum == 14))); - // Jabu Jabu - case SCENE_JABU_JABU: - return (!(!isMQ && enemiesToExcludeClearRooms && (roomNum == 8 || roomNum == 9)) && - !(!isMQ && enemiesToExcludeTimedRooms && roomNum == 12) && - !(isMQ && enemiesToExcludeClearRooms && (roomNum == 11 || roomNum == 14))); - // Forest Temple - case SCENE_FOREST_TEMPLE: - return (!(!isMQ && enemiesToExcludeClearRooms && - (roomNum == 6 || roomNum == 10 || roomNum == 18 || roomNum == 21)) && - !(isMQ && enemiesToExcludeClearRooms && - (roomNum == 5 || roomNum == 6 || roomNum == 18 || roomNum == 21))); - // Fire Temple - case SCENE_FIRE_TEMPLE: - return (!(!isMQ && enemiesToExcludeClearRooms && roomNum == 15) && - !(isMQ && enemiesToExcludeClearRooms && (roomNum == 15 || roomNum == 17 || roomNum == 18))); - // Water Temple - case SCENE_WATER_TEMPLE: - return (!(!isMQ && enemiesToExcludeClearRooms && (roomNum == 13 || roomNum == 18 || roomNum == 19)) && - !(isMQ && enemiesToExcludeClearRooms && (roomNum == 13 || roomNum == 18))); - // Spirit Temple - case SCENE_SPIRIT_TEMPLE: - return (!(!isMQ && enemiesToExcludeClearRooms && - (roomNum == 1 || roomNum == 10 || roomNum == 17 || roomNum == 20)) && - !(isMQ && enemiesToExcludeClearRooms && - (roomNum == 1 || roomNum == 2 || roomNum == 4 || roomNum == 10 || roomNum == 15 || - roomNum == 19 || roomNum == 20))); - // Shadow Temple - case SCENE_SHADOW_TEMPLE: - return ( - !(!isMQ && enemiesToExcludeClearRooms && - (roomNum == 1 || roomNum == 7 || roomNum == 11 || roomNum == 14 || roomNum == 16 || roomNum == 17 || - roomNum == 19 || roomNum == 20)) && - !(isMQ && enemiesToExcludeClearRooms && - (roomNum == 1 || roomNum == 6 || roomNum == 7 || roomNum == 11 || roomNum == 14 || roomNum == 20))); - // Ganon's Castle Trials - case SCENE_INSIDE_GANONS_CASTLE: - return (!(!isMQ && enemiesToExcludeClearRooms && (roomNum == 2 || roomNum == 5 || roomNum == 9)) && - !(isMQ && enemiesToExcludeClearRooms && - (roomNum == 0 || roomNum == 2 || roomNum == 5 || roomNum == 9))); - // Ice Caverns - case SCENE_ICE_CAVERN: - return (!(!isMQ && enemiesToExcludeClearRooms && (roomNum == 1 || roomNum == 7)) && - !(isMQ && enemiesToExcludeClearRooms && (roomNum == 3 || roomNum == 7))); - // Bottom of the Well - // Exclude Dark Link from room with holes in the floor because it can pull you in a like-like making the player - // fall down. - case SCENE_BOTTOM_OF_THE_WELL: - return (!(!isMQ && enemy.id == ACTOR_EN_TORCH2 && roomNum == 3)); - // Don't allow Dark Link in areas with lava void out zones as it voids out the player as well. - // Gerudo Training Ground. - case SCENE_GERUDO_TRAINING_GROUND: - return (!(enemy.id == ACTOR_EN_TORCH2 && roomNum == 6) && - !(!isMQ && enemiesToExcludeTimedRooms && (roomNum == 1 || roomNum == 7)) && - !(!isMQ && enemiesToExcludeClearRooms && (roomNum == 3 || roomNum == 5 || roomNum == 10)) && - !(isMQ && enemiesToExcludeTimedRooms && - (roomNum == 1 || roomNum == 3 || roomNum == 5 || roomNum == 7)) && - !(isMQ && enemiesToExcludeClearRooms && roomNum == 10)); - // Don't allow certain enemies in Ganon's Tower because they would spawn up on the ceiling, - // becoming impossible to kill. - // Ganon's Tower. - case SCENE_GANONS_TOWER: - return (!(enemiesToExcludeClearRooms || enemy.id == ACTOR_EN_VALI || - (enemy.id == ACTOR_EN_ZF && enemy.params == -1))); - // Ganon's Tower Escape. - case SCENE_GANONS_TOWER_COLLAPSE_INTERIOR: - return (!((enemiesToExcludeTimedRooms || (enemy.id == ACTOR_EN_ZF && enemy.params == -1)) && roomNum == 1)); - // Don't allow big Stalchildren, big Peahats and the large Bari (jellyfish) during the Gohma fight because they - // can clip into Gohma and it crashes the game. Likely because Gohma on the ceiling can't handle collision with - // other enemies. - case SCENE_DEKU_TREE_BOSS: - return (!enemiesToExcludeTimedRooms && !(enemy.id == ACTOR_EN_SKB && enemy.params == 20) && - !(enemy.id == ACTOR_EN_PEEHAT && enemy.params == -1)); - // Grottos. - case SCENE_GROTTOS: - return (!(enemiesToExcludeClearRooms && (roomNum == 2 || roomNum == 7))); - // Royal Grave. - case SCENE_ROYAL_FAMILYS_TOMB: - return (!(enemiesToExcludeClearRooms && roomNum == 0)); - // Don't allow Dark Link in areas with lava void out zones as it voids out the player as well. - // Death Mountain Crater. - case SCENE_DEATH_MOUNTAIN_CRATER: - return (enemy.id != ACTOR_EN_TORCH2); +static bool IsExcludedFromClearRooms(s16 enemyId, s16 enemyParams) { + switch (enemyId) { + // Freezard - Child Link can only kill this with Deku Stick jumpslash or other equipment like bombs + case ACTOR_EN_FZ: + // Beamos - Needs bombs + case ACTOR_EN_VM: + // Shell Blade - It's annoying to kill these as Child Link with sword or Deku Stick + case ACTOR_EN_SB: + // Spike - Child Link can't kill these with sword or Deku Stick + case ACTOR_EN_NY: + // Arwing - Goes out of bounds way too easily, softlocking the player + case ACTOR_EN_CLEAR_TAG: + // Wallmaster - Not easily visible, often makes players think they're softlocked and that there's no enemies + // left + case ACTOR_EN_WALLMAS: + // Dark Link - Goes out of bounds way too easily, softlocking the player + case ACTOR_EN_TORCH2: + // Flare dancer - Goes out of bounds way too easily, softlocking the player + case ACTOR_EN_FD: + // Anubis - Needs fire + case ACTOR_EN_ANUBICE_TAG: + return true; + case ACTOR_EN_MB: + return enemyParams == 0; default: - return 1; + return false; } } +static bool IsExcludedFromTimedRooms(s16 enemyId, s16 enemyParams) { + switch (enemyId) { + // Bari - Spawns 3 more enemies, potentially extremely difficult in timed rooms + case ACTOR_EN_VALI: + return true; + default: + return IsExcludedFromClearRooms(enemyId, enemyParams); + } +} + +static bool IsClearRoom(bool mq, s16 sceneNum, s8 roomNum) { + switch (sceneNum) { + case SCENE_DEKU_TREE: + if (mq) { + return roomNum == 4 || roomNum == 6 || roomNum == 9 || roomNum == 10; + } else { + return roomNum == 1 || roomNum == 9; + } + case SCENE_DODONGOS_CAVERN: + if (mq) { + return roomNum == 5 || roomNum == 6 || roomNum == 13 || roomNum == 14; + } else { + return roomNum == 15; + } + case SCENE_JABU_JABU: + if (mq) { + return roomNum == 11 || roomNum == 13 || roomNum == 14; + } else { + return roomNum == 8 || roomNum == 9; + } + case SCENE_FOREST_TEMPLE: + if (mq) { + return roomNum == 5 || roomNum == 6 || roomNum == 18 || roomNum == 21; + } else { + return roomNum == 6 || roomNum == 10 || roomNum == 18 || roomNum == 21; + } + case SCENE_FIRE_TEMPLE: + if (mq) { + return roomNum == 15 || roomNum == 17 || roomNum == 18; + } else { + return roomNum == 15; + } + case SCENE_WATER_TEMPLE: + if (mq) { + return roomNum == 13 || roomNum == 18; + } else { + return roomNum == 13 || roomNum == 18 || roomNum == 19; + } + case SCENE_SPIRIT_TEMPLE: + if (mq) { + return roomNum == 1 || roomNum == 2 || roomNum == 4 || roomNum == 10 || roomNum == 15 || + roomNum == 19 || roomNum == 20; + } else { + return roomNum == 1 || roomNum == 10 || roomNum == 17 || roomNum == 20 || roomNum == 27; + } + case SCENE_SHADOW_TEMPLE: + if (mq) { + return roomNum == 1 || roomNum == 6 || roomNum == 7 || roomNum == 11 || roomNum == 14 || roomNum == 20; + } else { + return roomNum == 1 || roomNum == 7 || roomNum == 11 || roomNum == 14 || roomNum == 16 || + roomNum == 17 || roomNum == 19 || roomNum == 20; + } + case SCENE_INSIDE_GANONS_CASTLE: + if (mq) { + return roomNum == 0 || roomNum == 2 || roomNum == 5 || roomNum == 9; + } else { + return roomNum == 2 || roomNum == 5 || roomNum == 9; + } + case SCENE_ICE_CAVERN: + if (mq) { + return roomNum == 3 || roomNum == 7; + } else { + return roomNum == 1 || roomNum == 7; + } + case SCENE_GERUDO_TRAINING_GROUND: + if (mq) { + return roomNum == 10; + } else { + return roomNum == 3 || roomNum == 5 || roomNum == 10; + } + case SCENE_GANONS_TOWER: + return true; + case SCENE_GROTTOS: + return roomNum == 2 || roomNum == 7; + case SCENE_ROYAL_FAMILYS_TOMB: + return roomNum == 0; + default: + return false; + } +} + +static bool IsTimedRoom(bool mq, s16 sceneNum, s8 roomNum) { + switch (sceneNum) { + case SCENE_JABU_JABU: + return !mq && roomNum == 12; + case SCENE_GERUDO_TRAINING_GROUND: + if (mq) { + return roomNum == 1 || roomNum == 3 || roomNum == 5 || roomNum == 7; + } else { + return roomNum == 1 || roomNum == 7; + } + case SCENE_GANONS_TOWER_COLLAPSE_INTERIOR: + return roomNum == 1; + default: + return false; + } +} + +static bool IsEnemyAllowedToSpawn(s16 sceneNum, s8 roomNum, EnemyEntry enemy, s16 posY, bool fromBari) { + bool mq = ResourceMgr_IsSceneMasterQuest(sceneNum); + + if (IsExcludedFromClearRooms(enemy.id, enemy.params) && IsClearRoom(mq, sceneNum, roomNum)) { + return false; + } + + if (IsExcludedFromTimedRooms(enemy.id, enemy.params) && IsTimedRoom(mq, sceneNum, roomNum)) { + return false; + } + + // Don't allow Lizalfos or Baris in Ganon's Tower because they would spawn up on the ceiling, becoming impossible to + // kill. + if (sceneNum == SCENE_GANONS_TOWER && + (enemy.id == ACTOR_EN_VALI || (enemy.id == ACTOR_EN_ZF && enemy.params == -1))) { + return false; + } + + // Don't allow Lizalfos in the first room of the interior of the castle collapse + if (sceneNum == SCENE_GANONS_TOWER_COLLAPSE_INTERIOR && roomNum == 1 && enemy.id == ACTOR_EN_ZF && + enemy.params == -1) { + return false; + } + + // Don't allow big Stalchildren, big Peahats and Baris (big jellyfish) during the Gohma fight because they can clip + // into Gohma and it crashes the game. Likely because Gohma on the ceiling can't handle collision with other + // enemies. + if (sceneNum == SCENE_DEKU_TREE_BOSS && + ((enemy.id == ACTOR_EN_SKB && enemy.params == 20) || (enemy.id == ACTOR_EN_PEEHAT && enemy.params == -1) || + (enemy.id == ACTOR_EN_VALI))) { + return false; + } + + // Don't allow the following enemies in the first spawn of the first room in MQ Fire Temple loop as when spawned and + // they get stuck in the room above + // - Lizalfos/Dinolfos, Bari: they drop in + // - Skulltulla: they appear above + // - Flying Peehat: they rise above the ceiling + if (mq && sceneNum == SCENE_FIRE_TEMPLE && roomNum == 15 && posY == 64 && + (enemy.id == ACTOR_EN_ZF || enemy.id == ACTOR_EN_VALI || enemy.id == ACTOR_EN_ST || + enemy.id == ACTOR_EN_PEEHAT)) { + return false; + } + + // Don't allow Stalfos in the child spirit clear room as they jump out of bounds frequently + if (sceneNum == SCENE_SPIRIT_TEMPLE && roomNum == 1 && enemy.id == ACTOR_EN_TEST) { + return false; + } + + // Don't allow baris to spawn another bari + if (fromBari && enemy.id == ACTOR_EN_VALI) { + return false; + } + + return true; +} + static std::vector selectedEnemyList; -void GetSelectedEnemies() { +static void UpdateSelectedEnemies() { selectedEnemyList.clear(); + for (int i = 0; i < ARRAY_COUNT(randomizedEnemySpawnTable); i++) { if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemyList.All"), 0)) { selectedEnemyList.push_back(randomizedEnemySpawnTable[i]); @@ -285,24 +366,29 @@ void GetSelectedEnemies() { selectedEnemyList.push_back(randomizedEnemySpawnTable[i]); } } + if (selectedEnemyList.size() == 0) { selectedEnemyList.push_back(randomizedEnemySpawnTable[0]); } } -EnemyEntry GetRandomizedEnemyEntry(uint32_t seed, PlayState* play) { +static EnemyEntry GetRandomizedEnemyEntry(u32 seed, PlayState* play, s16 posY, bool fromBari) { std::vector filteredEnemyList = {}; + if (selectedEnemyList.size() == 0) { - GetSelectedEnemies(); + UpdateSelectedEnemies(); } + for (EnemyEntry enemy : selectedEnemyList) { - if (IsEnemyAllowedToSpawn(play->sceneNum, play->roomCtx.curRoom.num, enemy)) { + if (IsEnemyAllowedToSpawn(play->sceneNum, play->roomCtx.curRoom.num, enemy, posY, fromBari)) { filteredEnemyList.push_back(enemy); } } + if (filteredEnemyList.size() == 0) { filteredEnemyList = selectedEnemyList; } + if (CVAR_ENEMY_RANDOMIZER_VALUE == ENEMY_RANDOMIZER_RANDOM_SEEDED) { uint64_t randomState = 0; @@ -316,78 +402,83 @@ EnemyEntry GetRandomizedEnemyEntry(uint32_t seed, PlayState* play) { return ShipUtils::RandomElement(filteredEnemyList, false); } -bool IsEnemyFoundToRandomize(int16_t sceneNum, int8_t roomNum, int16_t actorId, int16_t params, float posX) { - - uint32_t isMQ = ResourceMgr_IsSceneMasterQuest(sceneNum); +static bool IsEnemyFoundToRandomize(s16 sceneNum, s8 roomNum, s16 actorId, s16 params, f32 posX) { + u32 isMQ = ResourceMgr_IsSceneMasterQuest(sceneNum); for (int i = 0; i < ARRAY_COUNT(enemiesToRandomize); i++) { - if (actorId == enemiesToRandomize[i]) { - switch (actorId) { - // Only randomize the main component of Electric Tailparasans, not the tail segments they spawn. - case ACTOR_EN_TP: - return (params == -1); - // Only randomize the initial Deku Scrub actor (single and triple attack), not the flower they spawn. - case ACTOR_EN_DEKUNUTS: - return (params == -256 || params == 768); - // Don't randomize the OoB wallmaster in the Silver Rupee room because it's only there to - // not trigger unlocking the door after killing the other wallmaster in authentic gameplay. - case ACTOR_EN_WALLMAS: - return (!(!isMQ && sceneNum == SCENE_GERUDO_TRAINING_GROUND && roomNum == 2 && posX == -2345)); - // Only randomize initial Floormaster actor (it can split and does some spawning on init). - case ACTOR_EN_FLOORMAS: - return (params == 0 || params == -32768); - // Only randomize the initial eggs, not the enemies that spawn from them. - case ACTOR_EN_GOMA: - return (params >= 0 && params <= 9); - // Only randomize Skullwalltulas, not Golden Skulltulas. - case ACTOR_EN_SW: - return (params == 0); - // Don't randomize Nabooru because it'll break the cutscene and the door. - // Don't randomize Iron Knuckle in MQ Spirit Trial because it's needed to - // break the thrones in the room to access a button. - case ACTOR_EN_IK: - return (params != 1280 && !(isMQ && sceneNum == SCENE_INSIDE_GANONS_CASTLE && roomNum == 17)); - // Only randomize the initial spawn of the huge jellyfish. It spawns another copy when hit with a sword. - case ACTOR_EN_VALI: - return (params == -1); - // Don't randomize Lizalfos in Dodongo's Cavern because the gates won't work correctly otherwise. - case ACTOR_EN_ZF: - return (params != 1280 && params != 1281 && params != 1536 && params != 1537); - // Don't randomize the Wolfos in SFM because it's needed to open the gate. - case ACTOR_EN_WF: - return (params != 7936); - // Don't randomize the Stalfos in Forest Temple because other enemies fall through the hole and don't - // trigger the platform. Don't randomize the Stalfos spawning on the boat in Shadow Temple, as - // randomizing them places the new enemies down in the river. - case ACTOR_EN_TEST: - return (params != 1 && !(sceneNum == SCENE_SHADOW_TEMPLE && roomNum == 21)); - // Only randomize the enemy variant of Armos Statue. - // Leave one Armos unrandomized in the Spirit Temple room where an armos is needed to push down a - // button. - case ACTOR_EN_AM: - return ((params == -1 || params == 255) && !(sceneNum == SCENE_SPIRIT_TEMPLE && posX == 2141)); - // Don't randomize Shell Blades and Spikes in the underwater portion in Water Temple as it's impossible - // to kill most other enemies underwater with just hookshot and they're required to be killed for a - // grate to open. - case ACTOR_EN_SB: - case ACTOR_EN_NY: - return (!(!isMQ && sceneNum == SCENE_WATER_TEMPLE && roomNum == 2)); - case ACTOR_EN_SKJ: - return !(sceneNum == SCENE_LOST_WOODS && LINK_IS_CHILD); - default: - return 1; - } + if (actorId != enemiesToRandomize[i]) { + continue; + } + + switch (actorId) { + // Only randomize the main component of Electric Tailparasans, not the tail segments they spawn. + case ACTOR_EN_TP: + return params == -1; + // Only randomize the initial Deku Scrub actor (single and triple attack), not the flower they spawn. + case ACTOR_EN_DEKUNUTS: + return params == -256 || params == 768; + // Don't randomize the OoB wallmaster in the Silver Rupee room because it's only there to + // not trigger unlocking the door after killing the other wallmaster in authentic gameplay. + case ACTOR_EN_WALLMAS: + return !(!isMQ && sceneNum == SCENE_GERUDO_TRAINING_GROUND && roomNum == 2 && posX == -2345); + // Only randomize initial Floormaster actor (it can split and does some spawning on init). + case ACTOR_EN_FLOORMAS: + return params == 0 || params == -32768; + // Only randomize the initial eggs, not the enemies that spawn from them. + case ACTOR_EN_GOMA: + return params >= 0 && params <= 9; + // Only randomize Skullwalltulas, not Golden Skulltulas. + case ACTOR_EN_SW: + return params == 0; + // Don't randomize Nabooru because it'll break the cutscene and the door. + // Don't randomize Iron Knuckle in MQ Spirit Trial because it's needed to + // break the thrones in the room to access a button. + case ACTOR_EN_IK: + return params != 1280 && !(isMQ && sceneNum == SCENE_INSIDE_GANONS_CASTLE && roomNum == 17); + // Only randomize the initial spawn of the huge jellyfish. It spawns another copy when hit with a sword. + case ACTOR_EN_VALI: + return params == -1; + // Don't randomize Lizalfos in Dodongo's Cavern because the gates won't work correctly otherwise. + case ACTOR_EN_ZF: + return params != 1280 && params != 1281 && params != 1536 && params != 1537; + // Don't randomize the right baby dodongo on the first tunnel in Dodongo's Cavern as in vanilla you use them + // isntead of bombs to blow up a wall + case ACTOR_EN_DODOJR: + return !(sceneNum == SCENE_DODONGOS_CAVERN && roomNum == 1 && posX == 1972); + // Don't randomize the Wolfos in SFM because it's needed to open the gate. + case ACTOR_EN_WF: + return params != 7936; + // Don't randomize the Stalfos in Forest Temple because other enemies fall through the hole and don't + // trigger the platform. Don't randomize the Stalfos spawning on the boat in Shadow Temple, as + // randomizing them places the new enemies down in the river. + case ACTOR_EN_TEST: + return params != 1 && !(sceneNum == SCENE_SHADOW_TEMPLE && roomNum == 21); + // Only randomize the enemy variant of Armos Statue. + // Leave one Armos unrandomized in the Spirit Temple room where an armos is needed to push down a + // button. + case ACTOR_EN_AM: + return (params == -1 || params == 255) && !(sceneNum == SCENE_SPIRIT_TEMPLE && posX == 2141); + // Don't randomize Shell Blades and Spikes in the underwater portion in Water Temple as it's impossible + // to kill most other enemies underwater with just hookshot and they're required to be killed for a + // grate to open. + case ACTOR_EN_SB: + case ACTOR_EN_NY: + return !(!isMQ && sceneNum == SCENE_WATER_TEMPLE && roomNum == 2); + // Don't randomize Skull Kids in Lost Woods as child as they're not enemies + case ACTOR_EN_SKJ: + return !(sceneNum == SCENE_LOST_WOODS && LINK_IS_CHILD); + default: + return true; } } // If no enemy is found, don't randomize the actor. - return 0; + return false; } -uint8_t GetRandomizedEnemy(PlayState* play, int16_t* actorId, s16* posX, s16* posY, s16* posZ, int16_t* rotX, - int16_t* rotY, int16_t* rotZ, int16_t* params) { - - uint32_t isMQ = ResourceMgr_IsSceneMasterQuest(play->sceneNum); +static u8 GetRandomizedEnemy(PlayState* play, s16* actorId, s16* posX, s16* posY, s16* posZ, s16* rotX, s16* rotY, + s16* rotZ, s16* params, s16 offset = 0, bool fromBari = false) { + u32 isMQ = ResourceMgr_IsSceneMasterQuest(play->sceneNum); // Hack to remove enemies that wrongfully spawn because of bypassing object dependency with enemy randomizer on. // This should probably be handled on OTR generation in the future when object dependency is fully removed. @@ -415,7 +506,6 @@ uint8_t GetRandomizedEnemy(PlayState* play, int16_t* actorId, s16* posX, s16* po } if (IsEnemyFoundToRandomize(play->sceneNum, play->roomCtx.curRoom.num, *actorId, *params, *posX)) { - // When replacing Iron Knuckles in Spirit Temple, move them away from the throne because // some enemies can get stuck on the throne. if (*actorId == ACTOR_EN_IK && play->sceneNum == SCENE_SPIRIT_TEMPLE) { @@ -446,17 +536,27 @@ uint8_t GetRandomizedEnemy(PlayState* play, int16_t* actorId, s16* posX, s16* po pos.x = *posX; pos.y = *posY + 50; pos.z = *posZ; - raycastResult = BgCheck_AnyRaycastFloor1(&play->colCtx, &poly, &pos); - // If ground is found below actor, move actor to that height. - if (raycastResult > BGCHECK_Y_MIN) { - *posY = raycastResult; + // the forest temple second twisted hallway spawns after the enemies so we need to "find the floor" manually + if (play->sceneNum == SCENE_FOREST_TEMPLE && play->roomCtx.curRoom.num == 20 && *posZ > -3000) { + // when hallway is twisted (play->actorCtx.flags.tempSwch & 1), one spawn has the floor at 1235.165 & + // the other at 1239.094 but that changes based on the player position + // when not twisted, the whole floor is at 1228 + + *posY = 1228.0; + } else { + raycastResult = BgCheck_AnyRaycastFloor1(&play->colCtx, &poly, &pos); + + // If ground is found below actor, move actor to that height. + if (raycastResult > BGCHECK_Y_MIN) { + *posY = raycastResult; + } } // Get randomized enemy ID and parameter. - uint32_t seed = - play->sceneNum + *actorId + (int)*posX + (int)*posY + (int)*posZ + *rotX + *rotY + *rotZ + *params; - EnemyEntry randomEnemy = GetRandomizedEnemyEntry(seed, play); + u32 seed = + play->sceneNum + *actorId + (int)*posX + (int)*posY + (int)*posZ + *rotX + *rotY + *rotZ + *params + offset; + EnemyEntry randomEnemy = GetRandomizedEnemyEntry(seed, play, *posY, fromBari); *actorId = randomEnemy.id; *params = randomEnemy.params; @@ -531,6 +631,25 @@ void CustomStalfosPairFightDestroy(Actor* thisx, PlayState* play) { ObjectExtension::GetInstance().Remove(thisx); } +struct CustomPeehatLarvaData { + EnPeehat* peehat = nullptr; + ActorFunc originalDestroy = nullptr; +}; + +static ObjectExtension::Register CustomPeehatLarvaDataRegister; + +void CustomPeehatLarvaDestroy(Actor* thisx, PlayState* play) { + assert(ObjectExtension::GetInstance().Has(thisx)); + + CustomPeehatLarvaData* customPeehatLarvaData = ObjectExtension::GetInstance().Get(thisx); + + customPeehatLarvaData->peehat->unk_2FA -= 1; + + customPeehatLarvaData->originalDestroy(thisx, play); + + ObjectExtension::GetInstance().Remove(thisx); +} + void RegisterEnemyRandomizer() { COND_ID_HOOK(OnActorInit, ACTOR_EN_MB, ENEMY_RANDOMIZER_ENABLED, FixClubMoblinScale); @@ -759,14 +878,18 @@ void RegisterEnemyRandomizer() { s16 rotZ = 0; s16 params = 0; - for (s32 i = 0; i < 3; i++) { - // Offset small jellyfish with Enemy Randomizer, otherwise it gets - // stuck in a loop spawning more big jellyfish with seeded spawns. - if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0)) { - rotY += ShipUtils::Random(0, 50); - } + s16 homePosX = vali->actor.home.pos.x; + s16 homePosY = vali->actor.home.pos.y; + s16 homePosZ = vali->actor.home.pos.z; - if (!GetRandomizedEnemy(play, &actorId, &posX, &posY, &posZ, &rotX, &rotY, &rotZ, ¶ms)) { + s16 homeRotX = vali->actor.home.rot.x; + s16 homeRotY = vali->actor.home.rot.y; + s16 homeRotZ = vali->actor.home.rot.z; + + for (s32 i = 0; i < 3; i++) { + // use the home pos & rot to make it consistent + if (!GetRandomizedEnemy(play, &actorId, &homePosX, &homePosY, &homePosZ, &homeRotX, &homeRotY, &homeRotZ, + ¶ms, i * 1000, true)) { assert(false); } @@ -922,9 +1045,43 @@ void RegisterEnemyRandomizer() { *should = false; }); + + COND_VB_SHOULD(VB_PEEHAT_SPAWN_LARVAS, ENEMY_RANDOMIZER_ENABLED, { + EnPeehat* peehat = va_arg(args, EnPeehat*); + PlayState* play = va_arg(args, PlayState*); + + s16 actorId = ACTOR_EN_PEEHAT; + s16 homePosX = peehat->actor.home.pos.x; + s16 homePosY = peehat->actor.home.pos.y + 50.0f; + s16 homePosZ = peehat->actor.home.pos.z; + s16 rotX = 0; + s16 rotY = 0; + s16 rotZ = 0; + s16 params = PEAHAT_TYPE_LARVA; + + // 3 is MAX_LARVA + for (s32 i = 3 - peehat->unk_2FA; i > 0; i--) { + if (!GetRandomizedEnemy(play, &actorId, &homePosX, &homePosY, &homePosZ, &rotX, &rotY, &rotZ, ¶ms, + i * 1000)) { + assert(false); + } + + Actor* enemy = + Actor_Spawn(&play->actorCtx, play, actorId, homePosX, homePosY, homePosZ, rotX, rotY, rotZ, params); + + if (enemy == NULL) { + assert(false); + } else { + peehat->unk_2FA++; + ObjectExtension::GetInstance().Set( + enemy, CustomPeehatLarvaData{ .peehat = peehat, .originalDestroy = enemy->destroy }); + enemy->destroy = CustomPeehatLarvaDestroy; + } + } + }); } -static const std::map enemyRandomizerModes = { +static const std::map enemyRandomizerModes = { { ENEMY_RANDOMIZER_OFF, "Disabled" }, { ENEMY_RANDOMIZER_RANDOM, "Random" }, { ENEMY_RANDOMIZER_RANDOM_SEEDED, "Random (Seeded)" }, @@ -935,7 +1092,7 @@ void RegisterEnemyRandomizerWidgets() { SohGui::mSohMenu->AddWidget(path, "Enemy Randomizer", WIDGET_CVAR_COMBOBOX) .CVar(CVAR_ENHANCEMENT("RandomizedEnemies")) - .Callback([](WidgetInfo& info) { GetSelectedEnemies(); }) + .Callback([](WidgetInfo& info) { UpdateSelectedEnemies(); }) .Options( UIWidgets::ComboboxOptions() .DefaultIndex(ENEMY_RANDOMIZER_OFF) @@ -966,7 +1123,7 @@ void RegisterEnemyRandomizerWidgets() { SohGui::mSohMenu->AddWidget(path, "Select all Enemies", WIDGET_CVAR_CHECKBOX) .CVar(CVAR_ENHANCEMENT("RandomizedEnemyList.All")) .PreFunc([](WidgetInfo& info) { info.isHidden = !CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0); }) - .Callback([](WidgetInfo& info) { GetSelectedEnemies(); }); + .Callback([](WidgetInfo& info) { UpdateSelectedEnemies(); }); SohGui::mSohMenu->AddWidget(path, "Enemy List", WIDGET_SEPARATOR).PreFunc([](WidgetInfo& info) { info.isHidden = !CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0); @@ -981,7 +1138,7 @@ void RegisterEnemyRandomizerWidgets() { info.options->disabled = CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemyList.All"), 0); info.options->disabledTooltip = "These options are disabled because \"Select All Enemies\" is enabled."; }) - .Callback([](WidgetInfo& info) { GetSelectedEnemies(); }); + .Callback([](WidgetInfo& info) { UpdateSelectedEnemies(); }); } } diff --git a/soh/soh/Enhancements/Fishing.cpp b/soh/soh/Enhancements/Fishing.cpp new file mode 100644 index 0000000000..109533f984 --- /dev/null +++ b/soh/soh/Enhancements/Fishing.cpp @@ -0,0 +1,21 @@ +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include + +f32 Fishing_GetMinimumRequiredScore(); +} + +void BuildFishingMessage(uint16_t* textId, bool* loadFromMessageTable) { + if (gSaveContext.minigameScore == 0) { + gSaveContext.minigameScore = Fishing_GetMinimumRequiredScore(); + } +} + +void RegisterFishingMessages() { + COND_ID_HOOK(OnOpenText, 0x40AE, CVarGetInteger(CVAR_ENHANCEMENT("CustomizeFishing"), 0), BuildFishingMessage); + COND_ID_HOOK(OnOpenText, 0x4080, CVarGetInteger(CVAR_ENHANCEMENT("CustomizeFishing"), 0), BuildFishingMessage); +} + +static RegisterShipInitFunc initFunc(RegisterFishingMessages, { CVAR_ENHANCEMENT("CustomizeFishing") }); diff --git a/soh/soh/Enhancements/Items/AdditionalReticles.cpp b/soh/soh/Enhancements/Items/AdditionalReticles.cpp index 2cb3b975af..9b8b5cfcaf 100644 --- a/soh/soh/Enhancements/Items/AdditionalReticles.cpp +++ b/soh/soh/Enhancements/Items/AdditionalReticles.cpp @@ -36,7 +36,7 @@ void RegisterAdditionalReticles() { bool shouldRegister = CVAR_BOW_RETICLE_VALUE || CVAR_BOOMERANG_RETICLE_VALUE; COND_VB_SHOULD(VB_DRAW_ADDITIONAL_RETICLES, shouldRegister, { - Player* player = GET_PLAYER(gPlayState); + Player* player = va_arg(args, Player*); Actor* heldActor = player->heldActor; if (CVAR_BOW_RETICLE_VALUE && ((player->heldItemAction >= PLAYER_IA_BOW && player->heldItemAction <= PLAYER_IA_BOW_LIGHT) || diff --git a/soh/soh/Enhancements/Items/ArrowCycle.cpp b/soh/soh/Enhancements/Items/ArrowCycle.cpp index e70d5dee04..a60f562f12 100644 --- a/soh/soh/Enhancements/Items/ArrowCycle.cpp +++ b/soh/soh/Enhancements/Items/ArrowCycle.cpp @@ -43,11 +43,11 @@ static bool HasArrowType(PlayerItemAction itemAction) { case PLAYER_IA_BOW: return true; case PLAYER_IA_BOW_FIRE: - return INV_CONTENT(ITEM_ARROW_FIRE) == ITEM_ARROW_FIRE; + return INV_CONTENT(ITEM_ARROW_FIRE) == ITEM_ARROW_FIRE && gSaveContext.magic >= sMagicArrowCosts[0]; case PLAYER_IA_BOW_ICE: - return INV_CONTENT(ITEM_ARROW_ICE) == ITEM_ARROW_ICE; + return INV_CONTENT(ITEM_ARROW_ICE) == ITEM_ARROW_ICE && gSaveContext.magic >= sMagicArrowCosts[1]; case PLAYER_IA_BOW_LIGHT: - return INV_CONTENT(ITEM_ARROW_LIGHT) == ITEM_ARROW_LIGHT; + return INV_CONTENT(ITEM_ARROW_LIGHT) == ITEM_ARROW_LIGHT && gSaveContext.magic >= sMagicArrowCosts[2]; default: return false; } diff --git a/soh/soh/Enhancements/Items/InjectItemCounts.cpp b/soh/soh/Enhancements/Items/InjectItemCounts.cpp index e7e4bdcd00..caa2cf9d0e 100644 --- a/soh/soh/Enhancements/Items/InjectItemCounts.cpp +++ b/soh/soh/Enhancements/Items/InjectItemCounts.cpp @@ -7,11 +7,11 @@ extern "C" { void BuildSkulltulaMessage(uint16_t* textId, bool* loadFromMessageTable) { CustomMessage msg = - CustomMessage("You got a %rGold Skulltula Token%w!&You've collected %r[[gsCount]]%w tokens&in total!", - "Ein %rGoldenes Skulltula-Symbol%w!&Du hast nun insgesamt %r[[gsCount]]&%wGoldene " - "Skulltula-Symbole&gesammelt!", - "Vous obtenez un %rSymbole de&Skulltula d'or%w! Vous avez&collecté %r[[gsCount]]%w symboles en " - "tout!", + CustomMessage("You got a %rGold Skulltula Token%w!&You've collected %r[[d]]%w |token|tokens|&in total!", + "Ein %rGoldenes Skulltula-Symbol%w!&Du hast nun insgesamt %r[[d]]&%w|Goldenes " + "Skulltula-Symbol|Goldene Skulltula-Symbole|&gesammelt!", + "Vous obtenez un %rSymbole de&Skulltula d'or%w! Vous avez&collecté %r[[d]]%w |symbole|symboles| " + "en tout!", TEXTBOX_TYPE_BLUE); // The freeze text cannot be manually dismissed and must be auto-dismissed. // This is fine and even wanted when skull tokens are not shuffled, but when @@ -26,7 +26,7 @@ void BuildSkulltulaMessage(uint16_t* textId, bool* loadFromMessageTable) { msg = msg + "\x0E\x3C"; } int16_t gsCount = gSaveContext.inventory.gsTokens; - msg.Replace("[[gsCount]]", std::to_string(gsCount)); + msg.InsertNumber(gsCount); msg.AutoFormat(ITEM_SKULL_TOKEN); msg.LoadIntoFont(); *loadFromMessageTable = false; diff --git a/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipBlueWarp.cpp b/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipBlueWarp.cpp index 0ade592d64..1bbbc3cd01 100644 --- a/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipBlueWarp.cpp +++ b/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipBlueWarp.cpp @@ -86,8 +86,7 @@ void RegisterShouldPlayBlueWarp() { * should also account for the difference between your first and following visits to the blue warp. */ REGISTER_VB_SHOULD(VB_PLAY_TRANSITION_CS, { - // Do nothing when in a boss rush - if (IS_BOSS_RUSH) { + if (IS_BOSS_RUSH || gSaveContext.gameMode == GAMEMODE_END_CREDITS) { return; } diff --git a/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp b/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp index e272b13c73..34e1716975 100644 --- a/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp +++ b/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp @@ -24,7 +24,6 @@ extern "C" { #include "src/overlays/actors/ovl_En_Jj/z_en_jj.h" #include "src/overlays/actors/ovl_En_Daiku/z_en_daiku.h" #include "src/overlays/actors/ovl_Bg_Spot02_Objects/z_bg_spot02_objects.h" -#include "src/overlays/actors/ovl_Bg_Spot06_Objects/z_bg_spot06_objects.h" #include "src/overlays/actors/ovl_Bg_Spot03_Taki/z_bg_spot03_taki.h" #include "src/overlays/actors/ovl_Bg_Hidan_Kousi/z_bg_hidan_kousi.h" #include "src/overlays/actors/ovl_Bg_Dy_Yoseizo/z_bg_dy_yoseizo.h" @@ -1241,41 +1240,41 @@ void TimeSaverOnFlagSetHandler(int16_t flagType, int16_t flag) { case FLAG_EVENT_CHECK_INF: switch (flag) { case EVENTCHKINF_SPOKE_TO_SARIA_ON_BRIDGE: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_FAIRY_OCARINA).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_FAIRY_OCARINA); break; case EVENTCHKINF_OBTAINED_KOKIRI_EMERALD_DEKU_TREE_DEAD: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_KOKIRI_EMERALD).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_KOKIRI_EMERALD); break; case EVENTCHKINF_USED_DODONGOS_CAVERN_BLUE_WARP: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_GORON_RUBY).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_GORON_RUBY); break; case EVENTCHKINF_USED_JABU_JABUS_BELLY_BLUE_WARP: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_ZORA_SAPPHIRE).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_ZORA_SAPPHIRE); break; case EVENTCHKINF_USED_FOREST_TEMPLE_BLUE_WARP: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_FOREST_MEDALLION).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_FOREST_MEDALLION); break; case EVENTCHKINF_USED_FIRE_TEMPLE_BLUE_WARP: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_FIRE_MEDALLION).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_FIRE_MEDALLION); break; case EVENTCHKINF_USED_WATER_TEMPLE_BLUE_WARP: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_WATER_MEDALLION).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_WATER_MEDALLION); break; case EVENTCHKINF_RETURNED_TO_TEMPLE_OF_TIME_WITH_ALL_MEDALLIONS: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_LIGHT_ARROWS).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_LIGHT_ARROWS); break; case EVENTCHKINF_TIME_TRAVELED_TO_ADULT: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_LIGHT_MEDALLION).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_LIGHT_MEDALLION); break; } break; case FLAG_RANDOMIZER_INF: switch (flag) { case RAND_INF_DUNGEONS_DONE_SHADOW_TEMPLE: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_SHADOW_MEDALLION).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_SHADOW_MEDALLION); break; case RAND_INF_DUNGEONS_DONE_SPIRIT_TEMPLE: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_SPIRIT_MEDALLION).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_SPIRIT_MEDALLION); break; } break; @@ -1287,22 +1286,22 @@ void TimeSaverOnFlagSetHandler(int16_t flagType, int16_t flag) { case FLAG_RANDOMIZER_INF: switch (flag) { case RAND_INF_ZF_GREAT_FAIRY_REWARD: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_FARORES_WIND).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_FARORES_WIND); break; case RAND_INF_HC_GREAT_FAIRY_REWARD: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_DINS_FIRE).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_DINS_FIRE); break; case RAND_INF_COLOSSUS_GREAT_FAIRY_REWARD: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_NAYRUS_LOVE).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_NAYRUS_LOVE); break; case RAND_INF_DMT_GREAT_FAIRY_REWARD: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_MAGIC_SINGLE).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_MAGIC_SINGLE); break; case RAND_INF_DMC_GREAT_FAIRY_REWARD: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_MAGIC_DOUBLE).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_MAGIC_DOUBLE); break; case RAND_INF_OGC_GREAT_FAIRY_REWARD: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_DOUBLE_DEFENSE).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_DOUBLE_DEFENSE); break; } break; @@ -1328,47 +1327,44 @@ void TimeSaverOnFlagSetHandler(int16_t flagType, int16_t flag) { case FLAG_EVENT_CHECK_INF: switch (flag) { case EVENTCHKINF_LEARNED_ZELDAS_LULLABY: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_ZELDAS_LULLABY).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_ZELDAS_LULLABY); break; case EVENTCHKINF_LEARNED_MINUET_OF_FOREST: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_MINUET_OF_FOREST).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_MINUET_OF_FOREST); break; case EVENTCHKINF_LEARNED_BOLERO_OF_FIRE: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_BOLERO_OF_FIRE).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_BOLERO_OF_FIRE); break; case EVENTCHKINF_LEARNED_SERENADE_OF_WATER: - vanillaQueuedItemEntry = - Rando::StaticData::RetrieveItem(RG_SERENADE_OF_WATER).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_SERENADE_OF_WATER); break; case EVENTCHKINF_LEARNED_REQUIEM_OF_SPIRIT: - vanillaQueuedItemEntry = - Rando::StaticData::RetrieveItem(RG_REQUIEM_OF_SPIRIT).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_REQUIEM_OF_SPIRIT); break; case EVENTCHKINF_BONGO_BONGO_ESCAPED_FROM_WELL: - vanillaQueuedItemEntry = - Rando::StaticData::RetrieveItem(RG_NOCTURNE_OF_SHADOW).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_NOCTURNE_OF_SHADOW); break; case EVENTCHKINF_LEARNED_PRELUDE_OF_LIGHT: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_PRELUDE_OF_LIGHT).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_PRELUDE_OF_LIGHT); break; case EVENTCHKINF_LEARNED_SARIAS_SONG: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_SARIAS_SONG).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_SARIAS_SONG); break; case EVENTCHKINF_LEARNED_SONG_OF_TIME: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_SONG_OF_TIME).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_SONG_OF_TIME); break; case EVENTCHKINF_LEARNED_SONG_OF_STORMS: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_SONG_OF_STORMS).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_SONG_OF_STORMS); break; case EVENTCHKINF_LEARNED_SUNS_SONG: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_SUNS_SONG).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_SUNS_SONG); break; } break; case FLAG_RANDOMIZER_INF: switch (flag) { case RAND_INF_LEARNED_EPONA_SONG: - vanillaQueuedItemEntry = Rando::StaticData::RetrieveItem(RG_EPONAS_SONG).GetGIEntry_Copy(); + TimeSaverQueueItem(RG_EPONAS_SONG); break; } break; @@ -1415,12 +1411,10 @@ static void TimeSaverRegisterHooks() { TimeSaverOnSceneInitHandler); COND_HOOK(OnVanillaBehavior, true, TimeSaverOnVanillaBehaviorHandler); COND_HOOK(OnActorInit, true, TimeSaverOnActorInitHandler); + COND_HOOK(OnSceneInit, true, [](int16_t sceneNum) { successChimeCooldown = 0; }); // item queue for use outside rando, rando has its own queue - COND_HOOK(OnLoadGame, !IS_RANDO, [](int32_t fileNum) { - vanillaQueuedItemEntry = GET_ITEM_NONE; - successChimeCooldown = 0; - }); + COND_HOOK(OnLoadGame, !IS_RANDO, [](int32_t fileNum) { vanillaQueuedItemEntry = GET_ITEM_NONE; }); COND_HOOK(OnItemReceive, !IS_RANDO, TimeSaverOnItemReceiveHandler); COND_HOOK(OnPlayerUpdate, !IS_RANDO, TimeSaverOnPlayerUpdateHandler); COND_HOOK(OnFlagSet, diff --git a/soh/soh/Enhancements/audio/AudioEditor.cpp b/soh/soh/Enhancements/audio/AudioEditor.cpp index 8038efdce7..ce3bc7c245 100644 --- a/soh/soh/Enhancements/audio/AudioEditor.cpp +++ b/soh/soh/Enhancements/audio/AudioEditor.cpp @@ -55,6 +55,7 @@ extern std::shared_ptr mSohMenu; #define SEQ_COUNT_INSTRUMENT 6 #define SEQ_COUNT_SFX 57 #define SEQ_COUNT_VOICE 108 +#define SEQ_COUNT_ENDING 5 size_t AuthenticCountBySequenceType(SeqType type) { switch (type) { @@ -76,6 +77,8 @@ size_t AuthenticCountBySequenceType(SeqType type) { return SEQ_COUNT_INSTRUMENT; case SEQ_VOICE: return SEQ_COUNT_VOICE; + case SEQ_ENDING: + return SEQ_COUNT_ENDING; default: return 0; } @@ -808,7 +811,8 @@ void AudioEditor::DrawElement() { } std::vector allTypes = { - SEQ_BGM_WORLD, SEQ_BGM_EVENT, SEQ_BGM_BATTLE, SEQ_OCARINA, SEQ_FANFARE, SEQ_INSTRUMENT, SEQ_SFX, SEQ_VOICE, + SEQ_BGM_WORLD, SEQ_BGM_EVENT, SEQ_BGM_BATTLE, SEQ_OCARINA, SEQ_FANFARE, + SEQ_INSTRUMENT, SEQ_SFX, SEQ_VOICE, SEQ_ENDING, }; void AudioEditor_RandomizeAll() { diff --git a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp index 424ffd1bf2..09c4069d87 100644 --- a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp +++ b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp @@ -2708,6 +2708,7 @@ void RegisterCosmeticHooks() { [](s16 sceneNum) { CosmeticsEditor_AutoRandomizeAll(); }); COND_HOOK(OnGameFrameUpdate, true, CosmeticsUpdateTick); + COND_HOOK(OnAssetAltChange, true, []() { ApplyOrResetCustomGfxPatches(true); }); } void RegisterCosmeticWidgets() { diff --git a/soh/soh/Enhancements/customequipment.cpp b/soh/soh/Enhancements/customequipment.cpp index 3f5f758b19..a33e34d851 100644 --- a/soh/soh/Enhancements/customequipment.cpp +++ b/soh/soh/Enhancements/customequipment.cpp @@ -123,6 +123,8 @@ static bool IsDummyPlayer(const Player* player) { return player != nullptr && player->actor.update == DummyPlayer_Update; } +static bool sPrevAltAssetsEnabled = false; + void PatchOrUnpatch(const char* resource, const char* gfx, const char* dlist1, const char* dlist2, const char* dlist3, const char* alternateDL) { if (resource == NULL || gfx == NULL || dlist1 == NULL || dlist2 == NULL) { @@ -130,6 +132,7 @@ void PatchOrUnpatch(const char* resource, const char* gfx, const char* dlist1, c } const bool altAssetsRuntime = ResourceMgr_IsAltAssetsEnabled(); + const bool altAssetsChanged = (altAssetsRuntime != sPrevAltAssetsEnabled); if (!altAssetsRuntime) { // Alt assets are off; ensure any prior patches using these names are reverted. @@ -138,11 +141,16 @@ void PatchOrUnpatch(const char* resource, const char* gfx, const char* dlist1, c if (dlist3 != NULL) { ResourceMgr_UnpatchGfxByName(resource, dlist3); } - // Drop any cached version of the resource so it reloads clean (unpatched) next use. - ResourceMgr_UnloadResource(resource); + if (altAssetsChanged) { + ResourceMgr_UnloadResource(resource); + } return; } + if (altAssetsChanged) { + ResourceMgr_UnloadResource(resource); + } + if (!ResourceGetIsCustomByName(gfx)) { return; } @@ -512,6 +520,8 @@ void UpdatePatchCustomEquipmentDlists() { } ApplyCommonEquipmentPatches(); + + sPrevAltAssetsEnabled = ResourceMgr_IsAltAssetsEnabled(); } static void PatchCustomEquipment() { diff --git a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h index f96ff1ca5f..64b47de7d1 100644 --- a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h +++ b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h @@ -573,10 +573,11 @@ typedef enum { // #### `result` // ```c - // true + // (this->heldItemAction == PLAYER_IA_HOOKSHOT) || + // (this->heldItemAction == PLAYER_IA_LONGSHOT) // ``` // #### `args` - // - None + // - '*Player' VB_DRAW_ADDITIONAL_RETICLES, // #### `result` @@ -2991,7 +2992,16 @@ typedef enum { // ``` // #### `args` // - `*int32_t (camId)` - VB_SHOULD_LOAD_BG_IMAGE + VB_SHOULD_LOAD_BG_IMAGE, + + // #### `result` + // ```c + // true + // ``` + // #### `args` + // - `*EnPeehat` + // - `*PlayState` + VB_PEEHAT_SPAWN_LARVAS, } GIVanillaBehavior; #endif diff --git a/soh/soh/Enhancements/randomizer/3drando/hint_list/hint_list_exclude_dungeon.cpp b/soh/soh/Enhancements/randomizer/3drando/hint_list/hint_list_exclude_dungeon.cpp index 2413003cd2..ce68ff495c 100644 --- a/soh/soh/Enhancements/randomizer/3drando/hint_list/hint_list_exclude_dungeon.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/hint_list/hint_list_exclude_dungeon.cpp @@ -1350,6 +1350,14 @@ void StaticData::HintTable_Init_Exclude_Dungeon() { /*german*/ "Man erzählt sich, daß sich bewacht von einem #Ring der Flammen#, im Geistertempel #[[1]]# |befände|befänden|.", /*french*/ "Selon moi, protégé par un #cercle de flammes# dans le Temple de l'Esprit se trouve #[[1]]#.", {QM_RED, QM_GREEN})); + hintTextTable[RHT_SPIRIT_TEMPLE_BOULDER_ROOM_SUN_FAIRY] = HintText(CustomMessage("They say that #calling the sun past rolling boulders in Spirit Temple# reveals #[[1]]#.", + /*german*/ TODO_TRANSLATE, + /*french*/ TODO_TRANSLATE, {QM_RED, QM_GREEN})); + + hintTextTable[RHT_SPIRIT_TEMPLE_ARMOS_ROOM_SUN_FAIRY] = HintText(CustomMessage("They say that #calling the sun in the spotlight by statues# reveals #[[1]]#.", + /*german*/ TODO_TRANSLATE, + /*french*/ TODO_TRANSLATE, {QM_RED, QM_GREEN})); + hintTextTable[RHT_CRATE_SPIRIT_TEMPLE] = HintText(CustomMessage("They say that a #crate in Spirit Temple# contains #[[1]]#.", /*german*/ "Man erzählt sich, daß eine #Kiste im Geistertempel# #[[1]]# enthielte.", /*french*/ "Selon moi, une #caisse dans le Temple de l'Esprit# contient #[[1]]#.", {QM_RED, QM_GREEN})); diff --git a/soh/soh/Enhancements/randomizer/3drando/hint_list/hint_list_exclude_overworld.cpp b/soh/soh/Enhancements/randomizer/3drando/hint_list/hint_list_exclude_overworld.cpp index ee861f3c2b..fa53cea590 100644 --- a/soh/soh/Enhancements/randomizer/3drando/hint_list/hint_list_exclude_overworld.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/hint_list/hint_list_exclude_overworld.cpp @@ -2249,7 +2249,7 @@ void StaticData::HintTable_Init_Exclude_Overworld() { /*french*/ "Selon moi, un #arbre au Ranch Lon Lon# cache #[[1]]#.", { QM_RED, QM_GREEN })); hintTextTable[RHT_BUSH_HYRULE_FIELD] = - HintText(CustomMessage("They say that a #bush in Hyrle Field# contains #[[1]]#.", + HintText(CustomMessage("They say that a #bush in Hyrule Field# contains #[[1]]#.", /*german*/ TODO_TRANSLATE, /*french*/ "Selon moi, un #buisson dans la Plaine d'Hyrule# cache #[[1]]#.", { QM_RED, QM_GREEN })); hintTextTable[RHT_BUSH_ZORAS_FOUNTAIN] = diff --git a/soh/soh/Enhancements/randomizer/3drando/starting_inventory.cpp b/soh/soh/Enhancements/randomizer/3drando/starting_inventory.cpp index e7f875bf1c..ded9523a1e 100644 --- a/soh/soh/Enhancements/randomizer/3drando/starting_inventory.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/starting_inventory.cpp @@ -126,6 +126,7 @@ void GenerateStartingInventory() { AddItemToInventory(RG_NOCTURNE_OF_SHADOW, ctx->GetOption(RSK_STARTING_NOCTURNE_OF_SHADOW) ? 1 : 0); AddItemToInventory(RG_PRELUDE_OF_LIGHT, ctx->GetOption(RSK_STARTING_PRELUDE_OF_LIGHT) ? 1 : 0); AddItemToInventory(RG_KOKIRI_SWORD, ctx->GetOption(RSK_STARTING_KOKIRI_SWORD) ? 1 : 0); + AddItemToInventory(RG_MAGIC_BEAN_PACK, ctx->GetOption(RSK_STARTING_BEANS) ? 1 : 0); // if (ProgressiveGoronSword) { // AddItemToInventory(RG_PROGRESSIVE_GORONSWORD, StartingBiggoronSword.Value()); // } else { diff --git a/soh/soh/Enhancements/randomizer/Messages/GossipStoneHints.cpp b/soh/soh/Enhancements/randomizer/Messages/GossipStoneHints.cpp index a3e47344d9..f7e2847bf0 100644 --- a/soh/soh/Enhancements/randomizer/Messages/GossipStoneHints.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/GossipStoneHints.cpp @@ -14,7 +14,7 @@ extern PlayState* gPlayState; void BuildHintStoneMessage(uint16_t* textId, bool* loadFromMessageTable) { if ((RAND_GET_OPTION(RSK_GOSSIP_STONE_HINTS).Is(RO_GOSSIP_STONES_NEED_TRUTH) && - Player_GetMask(gPlayState) == PLAYER_MASK_TRUTH) || + Player_GetMask(gPlayState) != PLAYER_MASK_TRUTH) || (RAND_GET_OPTION(RSK_GOSSIP_STONE_HINTS).Is(RO_GOSSIP_STONES_NEED_STONE) && CHECK_QUEST_ITEM(QUEST_STONE_OF_AGONY) == 0)) { return; diff --git a/soh/soh/Enhancements/randomizer/Messages/MerchantMessages.cpp b/soh/soh/Enhancements/randomizer/Messages/MerchantMessages.cpp index 91d4895bc6..c0651d48ff 100644 --- a/soh/soh/Enhancements/randomizer/Messages/MerchantMessages.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/MerchantMessages.cpp @@ -111,6 +111,8 @@ void BuildCarpetGuyMessage(uint16_t* textId, bool* loadFromMessageTable) { BuildMerchantMessage(msg, RC_WASTELAND_BOMBCHU_SALESMAN, !RAND_GET_OPTION(RSK_MERCHANT_TEXT_HINT) || CVarGetInteger(CVAR_RANDOMIZER_ENHANCEMENT("MysteriousShuffle"), 0)); + } else { + return; } msg.AutoFormat(); msg.LoadIntoFont(); diff --git a/soh/soh/Enhancements/randomizer/Messages/StaticHints.cpp b/soh/soh/Enhancements/randomizer/Messages/StaticHints.cpp index c462477f15..1ae35bdd9a 100644 --- a/soh/soh/Enhancements/randomizer/Messages/StaticHints.cpp +++ b/soh/soh/Enhancements/randomizer/Messages/StaticHints.cpp @@ -141,7 +141,7 @@ void BuildSkulltulaPeopleMessage(uint16_t* textId, bool* loadFromMessageTable) { "et j'aurai quelque chose à te donner! [[color]]([[1]])%w"); msg.InsertNumber(count); msg.Replace("[[color]]", item.GetColor()); - msg.InsertNames({ item.GetHint().GetHintMessage().GetForCurrentLanguage() }); + msg.InsertNames({ item.GetHint().GetHintMessage() }); msg.AutoFormat(); msg.LoadIntoFont(); *loadFromMessageTable = false; @@ -159,7 +159,7 @@ void Build100SkullsHintMessage(uint16_t* textId, bool* loadFromMessageTable) { Rando::Item& item = Rando::StaticData::RetrieveItem(RAND_GET_ITEM_LOC(RC_KAK_100_GOLD_SKULLTULA_REWARD)->GetPlacedRandomizerGet()); msg.Replace("[[color]]", item.GetColor()); - msg.InsertNames({ item.GetHint().GetHintMessage().GetForCurrentLanguage() }); + msg.InsertNames({ item.GetHint().GetHintMessage() }); msg.AutoFormat(); msg.LoadIntoFont(); *loadFromMessageTable = false; @@ -179,78 +179,43 @@ void BuildGregHintMessage(uint16_t* textId, bool* loadFromMessageTable) { } } -void BuildMysteriousWarpMessage() { - CustomMessage msg = CustomMessage( - "Warp to&%ra mysterious place?%w&" + CustomMessage::TWO_WAY_CHOICE() + "%gOK&No%w", - "Zu&%reinem mysteriösen Ort%w?&" + CustomMessage::TWO_WAY_CHOICE() + "%gOK&No%w", - "Se téléporter vers&%run endroit mystérieux%w?&" + CustomMessage::TWO_WAY_CHOICE() + "%rOK!&Non%w"); - msg.LoadIntoFont(); +static void BuildWarpMessage(RandomizerHint rh, bool* loadFromMessageTable) { + if (!RAND_GET_OPTION(RSK_WARP_SONG_HINTS)) { + CustomMessage msg = CustomMessage( + "Warp to&%ra mysterious place?%w&" + CustomMessage::TWO_WAY_CHOICE() + "%gOK&No%w", + "Zu&%reinem mysteriösen Ort%w?&" + CustomMessage::TWO_WAY_CHOICE() + "%gOK&No%w", + "Se téléporter vers&%run endroit mystérieux%w?&" + CustomMessage::TWO_WAY_CHOICE() + "%rOK!&Non%w"); + msg.AutoFormat(); + msg.LoadIntoFont(); + } else { + CustomMessage msg = RAND_GET_HINT(rh)->GetHintMessage(MF_AUTO_FORMAT); + msg.LoadIntoFont(); + } + *loadFromMessageTable = false; } void BuildMinuetWarpMessage(uint16_t* textId, bool* loadFromMessageTable) { - if (!RAND_GET_OPTION(RSK_WARP_SONG_HINTS)) { - BuildMysteriousWarpMessage(); - *loadFromMessageTable = false; - return; - } - CustomMessage msg = RAND_GET_HINT(RH_MINUET_WARP_LOC)->GetHintMessage(MF_AUTO_FORMAT); - msg.LoadIntoFont(); - *loadFromMessageTable = false; + BuildWarpMessage(RH_MINUET_WARP_LOC, loadFromMessageTable); } void BuildBoleroWarpMessage(uint16_t* textId, bool* loadFromMessageTable) { - if (!RAND_GET_OPTION(RSK_WARP_SONG_HINTS)) { - BuildMysteriousWarpMessage(); - *loadFromMessageTable = false; - return; - } - CustomMessage msg = RAND_GET_HINT(RH_BOLERO_WARP_LOC)->GetHintMessage(MF_AUTO_FORMAT); - msg.LoadIntoFont(); - *loadFromMessageTable = false; + BuildWarpMessage(RH_BOLERO_WARP_LOC, loadFromMessageTable); } void BuildSerenadeWarpMessage(uint16_t* textId, bool* loadFromMessageTable) { - if (!RAND_GET_OPTION(RSK_WARP_SONG_HINTS)) { - BuildMysteriousWarpMessage(); - *loadFromMessageTable = false; - return; - } - CustomMessage msg = RAND_GET_HINT(RH_SERENADE_WARP_LOC)->GetHintMessage(MF_AUTO_FORMAT); - msg.LoadIntoFont(); - *loadFromMessageTable = false; + BuildWarpMessage(RH_SERENADE_WARP_LOC, loadFromMessageTable); } void BuildRequiemWarpMessage(uint16_t* textId, bool* loadFromMessageTable) { - if (!RAND_GET_OPTION(RSK_WARP_SONG_HINTS)) { - BuildMysteriousWarpMessage(); - *loadFromMessageTable = false; - return; - } - CustomMessage msg = RAND_GET_HINT(RH_REQUIEM_WARP_LOC)->GetHintMessage(MF_AUTO_FORMAT); - msg.LoadIntoFont(); - *loadFromMessageTable = false; + BuildWarpMessage(RH_REQUIEM_WARP_LOC, loadFromMessageTable); } void BuildNocturneWarpMessage(uint16_t* textId, bool* loadFromMessageTable) { - if (!RAND_GET_OPTION(RSK_WARP_SONG_HINTS)) { - BuildMysteriousWarpMessage(); - *loadFromMessageTable = false; - return; - } - CustomMessage msg = RAND_GET_HINT(RH_NOCTURNE_WARP_LOC)->GetHintMessage(MF_AUTO_FORMAT); - msg.LoadIntoFont(); - *loadFromMessageTable = false; + BuildWarpMessage(RH_NOCTURNE_WARP_LOC, loadFromMessageTable); } void BuildPreludeWarpMessage(uint16_t* textId, bool* loadFromMessageTable) { - if (!RAND_GET_OPTION(RSK_WARP_SONG_HINTS)) { - BuildMysteriousWarpMessage(); - *loadFromMessageTable = false; - return; - } - CustomMessage msg = RAND_GET_HINT(RH_PRELUDE_WARP_LOC)->GetHintMessage(MF_AUTO_FORMAT); - msg.LoadIntoFont(); - *loadFromMessageTable = false; + BuildWarpMessage(RH_PRELUDE_WARP_LOC, loadFromMessageTable); } void BuildFrogsHintMessage(uint16_t* textId, bool* loadFromMessageTable) { @@ -420,15 +385,17 @@ void RegisterStaticHints() { COND_ID_HOOK(OnOpenText, TEXT_CHEST_GAME_REAL_GAMBLER, RAND_GET_OPTION(RSK_GREG_HINT), BuildGregHintMessage); COND_ID_HOOK(OnOpenText, TEXT_CHEST_GAME_THANKS_A_LOT, RAND_GET_OPTION(RSK_GREG_HINT), BuildGregHintMessage); // Warp - COND_ID_HOOK(OnOpenText, TEXT_WARP_MINUET_OF_FOREST, RAND_GET_OPTION(RSK_WARP_SONG_HINTS), BuildMinuetWarpMessage); - COND_ID_HOOK(OnOpenText, TEXT_WARP_BOLERO_OF_FIRE, RAND_GET_OPTION(RSK_WARP_SONG_HINTS), BuildBoleroWarpMessage); - COND_ID_HOOK(OnOpenText, TEXT_WARP_SERENADE_OF_WATER, RAND_GET_OPTION(RSK_WARP_SONG_HINTS), + COND_ID_HOOK(OnOpenText, TEXT_WARP_MINUET_OF_FOREST, RAND_GET_OPTION(RSK_SHUFFLE_WARP_SONGS), + BuildMinuetWarpMessage); + COND_ID_HOOK(OnOpenText, TEXT_WARP_BOLERO_OF_FIRE, RAND_GET_OPTION(RSK_SHUFFLE_WARP_SONGS), BuildBoleroWarpMessage); + COND_ID_HOOK(OnOpenText, TEXT_WARP_SERENADE_OF_WATER, RAND_GET_OPTION(RSK_SHUFFLE_WARP_SONGS), BuildSerenadeWarpMessage); - COND_ID_HOOK(OnOpenText, TEXT_WARP_REQUIEM_OF_SPIRIT, RAND_GET_OPTION(RSK_WARP_SONG_HINTS), + COND_ID_HOOK(OnOpenText, TEXT_WARP_REQUIEM_OF_SPIRIT, RAND_GET_OPTION(RSK_SHUFFLE_WARP_SONGS), BuildRequiemWarpMessage); - COND_ID_HOOK(OnOpenText, TEXT_WARP_NOCTURNE_OF_SHADOW, RAND_GET_OPTION(RSK_WARP_SONG_HINTS), + COND_ID_HOOK(OnOpenText, TEXT_WARP_NOCTURNE_OF_SHADOW, RAND_GET_OPTION(RSK_SHUFFLE_WARP_SONGS), BuildNocturneWarpMessage); - COND_ID_HOOK(OnOpenText, TEXT_WARP_PRELUDE_OF_LIGHT, RAND_GET_OPTION(RSK_WARP_SONG_HINTS), BuildPreludeWarpMessage); + COND_ID_HOOK(OnOpenText, TEXT_WARP_PRELUDE_OF_LIGHT, RAND_GET_OPTION(RSK_SHUFFLE_WARP_SONGS), + BuildPreludeWarpMessage); // Frogs COND_ID_HOOK(OnOpenText, TEXT_FROGS_UNDERWATER, RAND_GET_OPTION(RSK_FROGS_HINT), BuildFrogsHintMessage); // Loach diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp index c2e2025861..865d149c2f 100644 --- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp +++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp @@ -2686,7 +2686,16 @@ void RandomizerOnPlayerUpdateHandler() { gSaveContext.respawn[RESPAWN_MODE_DOWN].yaw = respawn->second.yaw; } - Play_TriggerVoidOut(gPlayState); + if (gPlayState->sceneNum == SCENE_GROTTOS) { + // RESPAWN_MODE_DOWN isn't refreshed on grotto entry, reload grotto instead + gPlayState->nextEntranceIndex = gSaveContext.entranceIndex; + gPlayState->transitionTrigger = TRANS_TRIGGER_START; + gPlayState->transitionType = TRANS_TYPE_FADE_BLACK; + gSaveContext.nextTransitionType = TRANS_TYPE_FADE_BLACK; + gSaveContext.respawnFlag = 0; + } else { + Play_TriggerVoidOut(gPlayState); + } } } @@ -2699,8 +2708,14 @@ void RandomizerOnPlayerUpdateHandler() { } if (!GameInteractor::IsGameplayPaused() && RAND_GET_OPTION(RSK_TRIFORCE_HUNT).IsNot(RO_TRIFORCE_HUNT_OFF)) { - // Warp to credits - if (GameInteractor::State::TriforceHuntCreditsWarpActive) { + // Warp to credits once item queue has drained to avoid losing queued items + if (GameInteractor::State::TriforceHuntCreditsWarpActive && randomizerQueuedChecks.empty() && + randomizerQueuedCheck == RC_UNKNOWN_CHECK) { + gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_TRIFORCE_COMPLETED] = + static_cast(GAMEPLAYSTAT_TOTAL_TIME); + gSaveContext.ship.stats.gameComplete = 1; + Play_PerformSave(gPlayState); + Notification::Emit({ .message = "Game autosaved" }); gPlayState->nextEntranceIndex = ENTR_CHAMBER_OF_THE_SAGES_0; gSaveContext.nextCutsceneIndex = 0xFFF2; gPlayState->transitionTrigger = TRANS_TRIGGER_START; diff --git a/soh/soh/Enhancements/randomizer/item.h b/soh/soh/Enhancements/randomizer/item.h index 0404c46a67..a0c7b68e4f 100644 --- a/soh/soh/Enhancements/randomizer/item.h +++ b/soh/soh/Enhancements/randomizer/item.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include "soh/Enhancements/custom-message/text.h" diff --git a/soh/soh/Enhancements/randomizer/location.cpp b/soh/soh/Enhancements/randomizer/location.cpp index 2a805825c4..a530051a81 100644 --- a/soh/soh/Enhancements/randomizer/location.cpp +++ b/soh/soh/Enhancements/randomizer/location.cpp @@ -64,7 +64,7 @@ bool Rando::Location::IsOverworld() const { } bool Rando::Location::IsShop() const { - return scene >= SCENE_BAZAAR && scene <= SCENE_BOMBCHU_SHOP; + return (scene >= SCENE_BAZAAR && scene <= SCENE_BOMBCHU_SHOP) || scene == SCENE_TEST01; } bool Rando::Location::IsVanillaCompletion() const { diff --git a/soh/soh/Enhancements/randomizer/location_access/dungeons/spirit_temple.cpp b/soh/soh/Enhancements/randomizer/location_access/dungeons/spirit_temple.cpp index 392224cda5..55461f1417 100644 --- a/soh/soh/Enhancements/randomizer/location_access/dungeons/spirit_temple.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/dungeons/spirit_temple.cpp @@ -136,7 +136,6 @@ void RegionTable_Init_SpiritTemple() { LOCATION(RC_SPIRIT_TEMPLE_CHILD_CLIMB_EAST_CHEST, SpiritShared(RR_SPIRIT_TEMPLE_SUN_ON_FLOOR_2F, []{return logic->CanHitSwitch(ED_BOMB_THROW) && logic->HasItem(RG_OPEN_CHEST);})), LOCATION(RC_SPIRIT_TEMPLE_GS_SUN_ON_FLOOR_ROOM, SpiritShared(RR_SPIRIT_TEMPLE_SUN_ON_FLOOR_2F, []{return logic->CanKillEnemy(RE_GOLD_SKULLTULA, logic->TakeDamage() ? ED_SHORT_JUMPSLASH : ED_BOMB_THROW);}, false, RR_SPIRIT_TEMPLE_SUN_ON_FLOOR_1F, []{return logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA, ED_BOOMERANG);})), - LOCATION(RC_SPIRIT_TEMPLE_BOULDER_ROOM_SUN_FAIRY, logic->CanUse(RG_SUNS_SONG) && (logic->CanUse(RG_FAIRY_BOW) || logic->CanUse(RG_HOOKSHOT) || logic->CanUse(RG_FAIRY_SLINGSHOT) || logic->CanUse(RG_BOOMERANG) || logic->CanUse(RG_BOMBCHU_5) || (logic->CanUse(RG_BOMB_BAG) && logic->IsAdult && ctx->GetTrickOption(RT_SPIRIT_LOWER_ADULT_SWITCH))) && (logic->CanUse(RG_HOVER_BOOTS) || logic->CanJumpslash())), }, { //Exits ENTRANCE(RR_SPIRIT_TEMPLE_SUN_ON_FLOOR_1F, true), diff --git a/soh/soh/Enhancements/randomizer/location_access/overworld/death_mountain_trail.cpp b/soh/soh/Enhancements/randomizer/location_access/overworld/death_mountain_trail.cpp index e6d0061808..22ac7b4939 100644 --- a/soh/soh/Enhancements/randomizer/location_access/overworld/death_mountain_trail.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/overworld/death_mountain_trail.cpp @@ -102,7 +102,7 @@ void RegionTable_Init_DeathMountainTrail() { //Exits ENTRANCE(RR_DEATH_MOUNTAIN_ROCKFALL, true), ENTRANCE(RR_DMC_UPPER_ENTRY, true), - ENTRANCE(RR_DMT_OWL_FLIGHT, logic->IsChild && (logic->HasItem(RG_SPEAK_DEKU) || logic->HasItem(RG_SPEAK_GERUDO) || logic->HasItem(RG_SPEAK_GORON) || logic->HasItem(RG_SPEAK_HYLIAN) || logic->HasItem(RG_SPEAK_ZORA))), + ENTRANCE(RR_DMT_OWL_FLIGHT, logic->IsChild && (logic->HasItem(RG_SPEAK_DEKU) || logic->HasItem(RG_SPEAK_GERUDO) || logic->HasItem(RG_SPEAK_GORON) || logic->HasItem(RG_SPEAK_KOKIRI) || logic->HasItem(RG_SPEAK_HYLIAN) || logic->HasItem(RG_SPEAK_ZORA))), ENTRANCE(RR_DMT_GREAT_FAIRY_FOUNTAIN, AnyAgeTime([]{return logic->BlastOrSmash();})), }); diff --git a/soh/soh/Enhancements/randomizer/location_access/overworld/lake_hylia.cpp b/soh/soh/Enhancements/randomizer/location_access/overworld/lake_hylia.cpp index 4a8d80619b..1abe5fc54b 100644 --- a/soh/soh/Enhancements/randomizer/location_access/overworld/lake_hylia.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/overworld/lake_hylia.cpp @@ -87,11 +87,11 @@ void RegionTable_Init_LakeHylia() { //Exits ENTRANCE(RR_HF_TO_LAKE_HYLIA, true), ENTRANCE(RR_LH_FROM_SHORTCUT, true), - ENTRANCE(RR_LH_OWL_FLIGHT, logic->IsChild && (logic->HasItem(RG_SPEAK_DEKU) || logic->HasItem(RG_SPEAK_GERUDO) || logic->HasItem(RG_SPEAK_GORON) || logic->HasItem(RG_SPEAK_HYLIAN) || logic->HasItem(RG_SPEAK_ZORA))), + ENTRANCE(RR_LH_OWL_FLIGHT, logic->IsChild && (logic->HasItem(RG_SPEAK_DEKU) || logic->HasItem(RG_SPEAK_GERUDO) || logic->HasItem(RG_SPEAK_GORON) || logic->HasItem(RG_SPEAK_KOKIRI) || logic->HasItem(RG_SPEAK_HYLIAN) || logic->HasItem(RG_SPEAK_ZORA))), ENTRANCE(RR_LH_FISHING_ISLAND, ((logic->IsChild || logic->Get(LOGIC_WATER_TEMPLE_CLEAR)) && logic->HasItem(RG_BRONZE_SCALE)) || (logic->IsAdult && (logic->ReachScarecrow() || CanPlantBean(RR_LAKE_HYLIA, RG_LAKE_HYLIA_BEAN_SOUL)))), ENTRANCE(RR_LH_LAB, logic->CanOpenOverworldDoor(RG_HYLIA_LAB_KEY)), ENTRANCE(RR_LH_FROM_WATER_TEMPLE, true), - ENTRANCE(RR_LH_GROTTO, logic->HasItem(RG_POWER_BRACELET) && (logic->IsAdult || logic->HasItem(RG_SPEAK_DEKU) || logic->HasItem(RG_SPEAK_GERUDO) || logic->HasItem(RG_SPEAK_GORON) || logic->HasItem(RG_SPEAK_HYLIAN) || logic->HasItem(RG_SPEAK_ZORA))), + ENTRANCE(RR_LH_GROTTO, logic->HasItem(RG_POWER_BRACELET) && (logic->IsAdult || logic->HasItem(RG_SPEAK_DEKU) || logic->HasItem(RG_SPEAK_GERUDO) || logic->HasItem(RG_SPEAK_GORON) || logic->HasItem(RG_SPEAK_KOKIRI) || logic->HasItem(RG_SPEAK_HYLIAN) || logic->HasItem(RG_SPEAK_ZORA))), }); areaTable[RR_LH_FROM_SHORTCUT] = Region("LH From Shortcut", SCENE_LAKE_HYLIA, TIME_DOESNT_PASS, {RA_LAKE_HYLIA}, {}, {}, { diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index a701249c59..ea1af77527 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -1072,49 +1072,28 @@ static std::unordered_map randomizerGetToS // Gameplay stat tracking: Update time the item was acquired // (special cases for rando items) void Randomizer_GameplayStats_SetTimestamp(uint16_t item) { - u32 time = static_cast(GAMEPLAYSTAT_TOTAL_TIME); - // Have items in Link's pocket shown as being obtained at 0.1 seconds if (time == 0) { time = 1; } - // Use ITEM_KEY_BOSS to timestamp Ganon's boss key - if (item == RG_GANONS_CASTLE_BOSS_KEY) { - gSaveContext.ship.stats.itemTimestamp[ITEM_KEY_BOSS] = time; - return; - } - - if (randomizerGetToStatsTimeStamp.contains((RandomizerGet)item)) { - gSaveContext.ship.stats.itemTimestamp[randomizerGetToStatsTimeStamp[(RandomizerGet)item]] = time; - return; - } - - // Count any bottled item as a bottle - if (item >= RG_EMPTY_BOTTLE && item <= RG_BOTTLE_WITH_BIG_POE) { - if (gSaveContext.ship.stats.itemTimestamp[ITEM_BOTTLE] == 0) { + if (gSaveContext.ship.stats.itemTimestamp[item] == 0) { + if (item == RG_GANONS_CASTLE_BOSS_KEY) { + gSaveContext.ship.stats.itemTimestamp[ITEM_KEY_BOSS] = time; + } else if (item == RG_MASTER_SWORD) { + gSaveContext.ship.stats.itemTimestamp[ITEM_SWORD_MASTER] = time; + } else if (randomizerGetToStatsTimeStamp.contains((RandomizerGet)item)) { + gSaveContext.ship.stats.itemTimestamp[randomizerGetToStatsTimeStamp[(RandomizerGet)item]] = time; + } else if (item >= RG_EMPTY_BOTTLE && item <= RG_BOTTLE_WITH_BIG_POE) { gSaveContext.ship.stats.itemTimestamp[ITEM_BOTTLE] = time; - } - return; - } - - // Count any bombchu pack as bombchus - if ((item >= RG_BOMBCHU_5 && item <= RG_BOMBCHU_20) || item == RG_PROGRESSIVE_BOMBCHU_BAG) { - if (gSaveContext.ship.stats.itemTimestamp[ITEM_BOMBCHU] = 0) { + } else if ((item >= RG_BOMBCHU_5 && item <= RG_BOMBCHU_20) || item == RG_PROGRESSIVE_BOMBCHU_BAG) { gSaveContext.ship.stats.itemTimestamp[ITEM_BOMBCHU] = time; + } else if (item == RG_MAGIC_SINGLE) { + gSaveContext.ship.stats.itemTimestamp[ITEM_SINGLE_MAGIC] = time; + } else if (item == RG_DOUBLE_DEFENSE) { + gSaveContext.ship.stats.itemTimestamp[ITEM_DOUBLE_DEFENSE] = time; } - return; - } - - if (item == RG_MAGIC_SINGLE) { - gSaveContext.ship.stats.itemTimestamp[ITEM_SINGLE_MAGIC] = time; - return; - } - - if (item == RG_DOUBLE_DEFENSE) { - gSaveContext.ship.stats.itemTimestamp[ITEM_DOUBLE_DEFENSE] = time; - return; } } @@ -1385,13 +1364,7 @@ extern "C" u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry) { if (OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_TRIFORCE_HUNT) == RO_TRIFORCE_HUNT_WIN) { - gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_TRIFORCE_COMPLETED] = - static_cast(GAMEPLAYSTAT_TOTAL_TIME); - gSaveContext.ship.stats.gameComplete = 1; - Play_PerformSave(play); - Notification::Emit({ - .message = "Game autosaved", - }); + // Save and warp are deferred until item queue drains GameInteractor_SetTriforceHuntCreditsWarpActive(true); } } diff --git a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp index a0238ed6bf..039337a551 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp @@ -1677,7 +1677,8 @@ bool IsCheckShuffled(RandomizerCheck rc) { (loc->GetRCType() != RCTYPE_SCRUB || showScrubs || (showMajorScrubs && (rc == RC_LW_DEKU_SCRUB_NEAR_BRIDGE || // The 3 scrubs that are always randomized rc == RC_HF_DEKU_SCRUB_GROTTO || rc == RC_LW_DEKU_SCRUB_GROTTO_FRONT))) && - (loc->GetRCType() != RCTYPE_MERCHANT || showMerchants) && + ((loc->GetRCType() != RCTYPE_MERCHANT || showMerchants) || + (rc == RC_ZR_MAGIC_BEAN_SALESMAN && showBeans)) && (loc->GetRCType() != RCTYPE_BEGGAR || showBeggar) && (loc->GetRCType() != RCTYPE_SONG_LOCATION || showSongs) && (loc->GetRCType() != RCTYPE_BEEHIVE || showBeehives) && @@ -1728,8 +1729,7 @@ bool IsCheckShuffled(RandomizerCheck rc) { rc == RC_DMT_TRADE_CLAIM_CHECK // even when shuffle adult trade is off ) && (rc != RC_KF_KOKIRI_SWORD_CHEST || showKokiriSword) && (rc != RC_TOT_MASTER_SWORD || showMasterSword) && - (rc != RC_LH_HYRULE_LOACH || showHyruleLoach) && (rc != RC_ZR_MAGIC_BEAN_SALESMAN || showBeans) && - (rc != RC_HC_MALON_EGG || showWeirdEgg) && + (rc != RC_LH_HYRULE_LOACH || showHyruleLoach) && (rc != RC_HC_MALON_EGG || showWeirdEgg) && (loc->GetRCType() != RCTYPE_FROG_SONG || showFrogSongRupees) && ((loc->GetRCType() != RCTYPE_MAP && loc->GetRCType() != RCTYPE_COMPASS) || showStartingMapsCompasses) && (loc->GetRCType() != RCTYPE_FOUNTAIN_FAIRY || showFountainFairies) && diff --git a/soh/soh/Enhancements/randomizer/randomizer_entrance.c b/soh/soh/Enhancements/randomizer/randomizer_entrance.c index bc21cea29e..6daf6f4768 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_entrance.c +++ b/soh/soh/Enhancements/randomizer/randomizer_entrance.c @@ -699,17 +699,18 @@ void Entrance_OverrideSpawnScene(s32 sceneNum, s32 spawn) { modifiedLinkActorEntry.rot = gPlayState->linkActorEntry->rot; modifiedLinkActorEntry.params = gPlayState->linkActorEntry->params; - if (Randomizer_GetSettingValue(RSK_SHUFFLE_DUNGEON_ENTRANCES) == RO_DUNGEON_ENTRANCE_SHUFFLE_ON_PLUS_GANON) { - // Move Ganon's Castle exit spawn to be on the small ledge near the castle and not over the void - // to prevent Link from falling if the bridge isn't spawned - if (sceneNum == SCENE_OUTSIDE_GANONS_CASTLE && spawn == 1) { - modifiedLinkActorEntry.pos.x = 0xFEA8; - modifiedLinkActorEntry.pos.y = 0x065C; - modifiedLinkActorEntry.pos.z = 0x0290; - modifiedLinkActorEntry.rot.y = 0x0700; - modifiedLinkActorEntry.params = 0x0DFF; // stationary spawn - gPlayState->linkActorEntry = &modifiedLinkActorEntry; - } + // Move Ganon's Castle exit spawn to be on the small ledge near the castle and not over the void + // to prevent Link from falling if the bridge isn't spawned + if (sceneNum == SCENE_OUTSIDE_GANONS_CASTLE && spawn == 1 && + (Randomizer_GetSettingValue(RSK_SHUFFLE_DUNGEON_ENTRANCES) == RO_DUNGEON_ENTRANCE_SHUFFLE_ON_PLUS_GANON || + Randomizer_GetSettingValue(RSK_SHUFFLE_BOSS_ENTRANCES) != RO_BOSS_ROOM_ENTRANCE_SHUFFLE_OFF || + Randomizer_GetSettingValue(RSK_SHUFFLE_GANONS_TOWER_ENTRANCE))) { + modifiedLinkActorEntry.pos.x = 0xFEA8; + modifiedLinkActorEntry.pos.y = 0x065C; + modifiedLinkActorEntry.pos.z = 0x0290; + modifiedLinkActorEntry.rot.y = 0x0700; + modifiedLinkActorEntry.params = 0x0DFF; // stationary spawn + gPlayState->linkActorEntry = &modifiedLinkActorEntry; } if (Randomizer_GetSettingValue(RSK_SHUFFLE_BOSS_ENTRANCES) != RO_BOSS_ROOM_ENTRANCE_SHUFFLE_OFF) { diff --git a/soh/soh/Network/Anchor/JsonConversions.hpp b/soh/soh/Network/Anchor/JsonConversions.hpp index 69ad131360..9a3e802884 100644 --- a/soh/soh/Network/Anchor/JsonConversions.hpp +++ b/soh/soh/Network/Anchor/JsonConversions.hpp @@ -192,6 +192,9 @@ inline void from_json(const json& j, SaveContext& saveContext) { j.at("swordHealth").get_to(saveContext.swordHealth); std::vector sceneFlagsArray; j.at("sceneFlags").get_to(sceneFlagsArray); + if (sceneFlagsArray.size() < 124 * 4) { + sceneFlagsArray.resize(124 * 4, 0); + } for (int i = 0; i < 124; i++) { saveContext.sceneFlags[i].chest = sceneFlagsArray[i * 4]; saveContext.sceneFlags[i].swch = sceneFlagsArray[i * 4 + 1]; diff --git a/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp b/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp index ef023a8b49..86551783ad 100644 --- a/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp +++ b/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp @@ -3,6 +3,7 @@ #include #include "soh/Enhancements/game-interactor/GameInteractor.h" #include "soh/OTRGlobals.h" +#include "soh/Enhancements/randomizer/randomizerEnums/RandomizerCheck.h" #include "soh/Enhancements/randomizer/randomizer.h" static bool isResultOfHandling = false; @@ -40,6 +41,10 @@ void Anchor::HandlePacket_SetCheckStatus(nlohmann::json payload) { auto randoContext = Rando::Context::GetInstance(); RandomizerCheck rc = payload.at("rc").get(); + if (rc < 0 || rc >= RC_MAX) { + SPDLOG_ERROR("[Anchor] SET_CHECK_STATUS: rc {} out of range", (int)rc); + return; + } RandomizerCheckStatus status = payload.at("status").get(); bool skipped = payload.at("skipped").get(); diff --git a/soh/soh/Network/Anchor/Packets/SetFlag.cpp b/soh/soh/Network/Anchor/Packets/SetFlag.cpp index 1cabfcea79..c4501a0814 100644 --- a/soh/soh/Network/Anchor/Packets/SetFlag.cpp +++ b/soh/soh/Network/Anchor/Packets/SetFlag.cpp @@ -41,6 +41,13 @@ void Anchor::HandlePacket_SetFlag(nlohmann::json payload) { s16 flagType = payload.at("flagType").get(); s16 flag = payload.at("flag").get(); + // sceneNum == SCENE_ID_MAX is a sentinel meaning "global flag" (handled below); only larger + // values would index gSaveContext.sceneFlags out of bounds. + if (sceneNum < 0 || sceneNum > SCENE_ID_MAX) { + SPDLOG_ERROR("[Anchor] SET_FLAG: sceneNum {} out of range", sceneNum); + return; + } + if (sceneNum == SCENE_ID_MAX) { auto effect = new GameInteractionEffect::SetFlag(); effect->parameters[0] = flagType; diff --git a/soh/soh/Network/Anchor/Packets/TeleportTo.cpp b/soh/soh/Network/Anchor/Packets/TeleportTo.cpp index c23ce73043..34cf074262 100644 --- a/soh/soh/Network/Anchor/Packets/TeleportTo.cpp +++ b/soh/soh/Network/Anchor/Packets/TeleportTo.cpp @@ -39,6 +39,12 @@ void Anchor::HandlePacket_TeleportTo(nlohmann::json payload) { s32 entranceIndex = payload.at("entranceIndex").get(); s8 roomIndex = payload.at("roomIndex").get(); + + if (entranceIndex < 0 || roomIndex < 0) { + SPDLOG_ERROR("[Anchor] TELEPORT_TO: invalid entranceIndex {} or roomIndex {}", entranceIndex, (int)roomIndex); + return; + } + PosRot posRot = payload.at("posRot").get(); gPlayState->nextEntranceIndex = entranceIndex; diff --git a/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp b/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp index 6943c1a6b4..a0609ca7f0 100644 --- a/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp +++ b/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp @@ -41,6 +41,13 @@ void Anchor::HandlePacket_UnsetFlag(nlohmann::json payload) { s16 flagType = payload.at("flagType").get(); s16 flag = payload.at("flag").get(); + // sceneNum == SCENE_ID_MAX is a sentinel meaning "global flag" (handled below); only larger + // values would index gSaveContext.sceneFlags out of bounds. + if (sceneNum < 0 || sceneNum > SCENE_ID_MAX) { + SPDLOG_ERROR("[Anchor] UNSET_FLAG: sceneNum {} out of range", sceneNum); + return; + } + if (sceneNum == SCENE_ID_MAX) { auto effect = new GameInteractionEffect::UnsetFlag(); effect->parameters[0] = flagType; diff --git a/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp b/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp index a994b564c1..59f3b80c97 100644 --- a/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp +++ b/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp @@ -1,8 +1,11 @@ #include "soh/Network/Anchor/Anchor.h" #include #include -#include "soh/Enhancements/game-interactor/GameInteractor.h" -#include "soh/OTRGlobals.h" +#include + +extern "C" { +#include "include/macros.h" +} /** * UPDATE_DUNGEON_ITEMS @@ -33,6 +36,12 @@ void Anchor::HandlePacket_UpdateDungeonItems(nlohmann::json payload) { } u16 mapIndex = payload.at("mapIndex").get(); + // dungeonKeys is shorter than dungeonItems (19 vs 20), so bound by the smaller of the two. + if (mapIndex >= ARRAY_COUNT(gSaveContext.inventory.dungeonItems) || + mapIndex >= ARRAY_COUNT(gSaveContext.inventory.dungeonKeys)) { + SPDLOG_ERROR("[Anchor] UPDATE_DUNGEON_ITEMS: mapIndex {} out of range", mapIndex); + return; + } gSaveContext.inventory.dungeonItems[mapIndex] = payload.at("dungeonItems").get(); gSaveContext.inventory.dungeonKeys[mapIndex] = payload.at("dungeonKeys").get(); } diff --git a/soh/soh/Network/CrowdControl/CrowdControl.cpp b/soh/soh/Network/CrowdControl/CrowdControl.cpp index 12a5a643f8..a0621a4eac 100644 --- a/soh/soh/Network/CrowdControl/CrowdControl.cpp +++ b/soh/soh/Network/CrowdControl/CrowdControl.cpp @@ -29,19 +29,19 @@ void CrowdControl::OnDisconnected() { } void CrowdControl::OnIncomingJson(nlohmann::json payload) { - Effect* incomingEffect = ParseMessage(payload); + std::unique_ptr incomingEffect = ParseMessage(payload); if (!incomingEffect) { return; } // If effect is not a timed effect, execute and return result. if (!incomingEffect->timeRemaining) { - EffectResult result = CrowdControl::ExecuteEffect(incomingEffect); + EffectResult result = CrowdControl::ExecuteEffect(incomingEffect.get()); EmitMessage(incomingEffect->id, incomingEffect->timeRemaining, result); } else { // If another timed effect is already active that conflicts with the incoming effect. bool isConflictingEffectActive = false; - for (Effect* effect : activeEffects) { + for (const auto& effect : activeEffects) { if (effect != incomingEffect && effect->category == incomingEffect->category && effect->id < incomingEffect->id) { isConflictingEffectActive = true; @@ -52,14 +52,14 @@ void CrowdControl::OnIncomingJson(nlohmann::json payload) { if (!isConflictingEffectActive) { // Check if effect can be applied, if it can't, let CC know. - EffectResult result = CrowdControl::CanApplyEffect(incomingEffect); + EffectResult result = CrowdControl::CanApplyEffect(incomingEffect.get()); if (result == EffectResult::Retry || result == EffectResult::Failure) { EmitMessage(incomingEffect->id, incomingEffect->timeRemaining, result); return; } activeEffectsMutex.lock(); - activeEffects.push_back(incomingEffect); + activeEffects.push_back(std::move(incomingEffect)); activeEffectsMutex.unlock(); } } @@ -74,17 +74,15 @@ void CrowdControl::ProcessActiveEffects() { auto it = activeEffects.begin(); while (it != activeEffects.end()) { - Effect* effect = *it; + Effect* effect = it->get(); EffectResult result = CrowdControl::ExecuteEffect(effect); if (result == EffectResult::Success) { // If time remaining has reached 0, we have finished the effect. if (effect->timeRemaining <= 0) { - it = activeEffects.erase(std::remove(activeEffects.begin(), activeEffects.end(), effect), - activeEffects.end()); GameInteractor::RemoveEffect( *dynamic_cast(effect->giEffect.get())); - delete effect; + it = activeEffects.erase(it); } else { // If we have a success after previously being paused, tell CC to resume timer. if (effect->isPaused) { @@ -176,7 +174,7 @@ CrowdControl::EffectResult CrowdControl::TranslateGiEnum(GameInteractionEffectQu return result; } -CrowdControl::Effect* CrowdControl::ParseMessage(nlohmann::json dataReceived) { +std::unique_ptr CrowdControl::ParseMessage(nlohmann::json dataReceived) { if (!dataReceived.contains("id") || !dataReceived.contains("type")) { SPDLOG_ERROR("[CrowdControl] Invalid payload received:\n{}", dataReceived.dump()); return nullptr; @@ -184,13 +182,16 @@ CrowdControl::Effect* CrowdControl::ParseMessage(nlohmann::json dataReceived) { SPDLOG_INFO("[CrowdControl] Received payload:\n{}", dataReceived.dump()); - if (!dataReceived.contains("code")) { + // "parameters" is intentionally not required: most effects (spawn enemies, teleports, status + // effects, etc.) carry no parameters. Its absence is handled safely below, and any type error + // is caught by the guard in Network::HandleRemoteJson. + if (!dataReceived.contains("code") || !dataReceived.contains("viewer")) { // This seems to happen when the CC session ends - SPDLOG_ERROR("[CrowdControl] Payload does not contain code, ignoring."); + SPDLOG_ERROR("[CrowdControl] Payload does not contain code or viewer, ignoring."); return nullptr; } - Effect* effect = new Effect(); + auto effect = std::make_unique(); effect->lastExecutionResult = EffectResult::Initiate; effect->id = dataReceived["id"]; effect->viewerName = dataReceived["viewer"]; @@ -202,9 +203,15 @@ CrowdControl::Effect* CrowdControl::ParseMessage(nlohmann::json dataReceived) { receivedParameter = dataReceived["parameters"][0]; } + auto it = effectStringToEnum.find(effectName); + if (it == effectStringToEnum.end()) { + SPDLOG_ERROR("[CrowdControl] Unknown effect code: {}", effectName); + return nullptr; + } + // Assign GameInteractionEffect + values to CC effect. // Categories are mostly used for checking for conflicting timed effects. - switch (effectStringToEnum[effectName]) { + switch (it->second) { // Spawn Enemies and Objects case kEffectSpawnCuccoStorm: diff --git a/soh/soh/Network/CrowdControl/CrowdControl.h b/soh/soh/Network/CrowdControl/CrowdControl.h index 5cc0883d42..8effe1ebed 100644 --- a/soh/soh/Network/CrowdControl/CrowdControl.h +++ b/soh/soh/Network/CrowdControl/CrowdControl.h @@ -64,14 +64,14 @@ class CrowdControl : public Network { std::thread ccThreadProcess; - std::vector activeEffects; + std::vector> activeEffects; std::mutex activeEffectsMutex; void HandleRemoteData(nlohmann::json payload); void ProcessActiveEffects(); void EmitMessage(uint32_t eventId, long timeRemaining, EffectResult status); - Effect* ParseMessage(nlohmann::json payload); + std::unique_ptr ParseMessage(nlohmann::json payload); EffectResult ExecuteEffect(Effect* effect); EffectResult CanApplyEffect(Effect* effect); EffectResult TranslateGiEnum(GameInteractionEffectQueryResult giResult); diff --git a/soh/soh/Network/Network.cpp b/soh/soh/Network/Network.cpp index 6eae1f3f14..1145705079 100644 --- a/soh/soh/Network/Network.cpp +++ b/soh/soh/Network/Network.cpp @@ -157,5 +157,9 @@ void Network::HandleRemoteJson(std::string payload) { return; } - OnIncomingJson(jsonPayload); + try { + OnIncomingJson(jsonPayload); + } catch (const std::exception& e) { + SPDLOG_ERROR("[Network] Exception handling incoming JSON: {}", e.what()); + } catch (...) { SPDLOG_ERROR("[Network] Unknown exception handling incoming JSON"); } } diff --git a/soh/soh/Network/Sail/Sail.cpp b/soh/soh/Network/Sail/Sail.cpp index 4fb4535034..43de5115cf 100644 --- a/soh/soh/Network/Sail/Sail.cpp +++ b/soh/soh/Network/Sail/Sail.cpp @@ -2,8 +2,6 @@ #include #include #include -#include "soh/OTRGlobals.h" -#include "soh/util.h" #include "soh/ShipUtils.h" template bool IsType(const SrcType* src) { @@ -99,12 +97,12 @@ void Sail::OnIncomingJson(nlohmann::json payload) { return; } - GameInteractionEffectBase* giEffect = EffectFromJson(payload["effect"]); + auto giEffect = EffectFromJson(payload["effect"]); if (giEffect) { GameInteractionEffectQueryResult result; if (effectType == "remove") { - if (IsType(giEffect)) { - result = dynamic_cast(giEffect)->Remove(); + if (IsType(giEffect.get())) { + result = dynamic_cast(giEffect.get())->Remove(); } else { result = GameInteractionEffectQueryResult::NotPossible; } @@ -134,7 +132,7 @@ void Sail::OnIncomingJson(nlohmann::json payload) { } catch (...) { SPDLOG_ERROR("[Sail] Unknown exception handling remote JSON"); } } -GameInteractionEffectBase* Sail::EffectFromJson(nlohmann::json payload) { +std::unique_ptr Sail::EffectFromJson(nlohmann::json payload) { if (!payload.contains("name")) { return nullptr; } @@ -142,7 +140,7 @@ GameInteractionEffectBase* Sail::EffectFromJson(nlohmann::json payload) { std::string name = payload["name"].get(); if (name == "SetSceneFlag") { - auto effect = new GameInteractionEffect::SetSceneFlag(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); @@ -150,7 +148,7 @@ GameInteractionEffectBase* Sail::EffectFromJson(nlohmann::json payload) { } return effect; } else if (name == "UnsetSceneFlag") { - auto effect = new GameInteractionEffect::UnsetSceneFlag(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); @@ -158,171 +156,171 @@ GameInteractionEffectBase* Sail::EffectFromJson(nlohmann::json payload) { } return effect; } else if (name == "SetFlag") { - auto effect = new GameInteractionEffect::SetFlag(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); } return effect; } else if (name == "UnsetFlag") { - auto effect = new GameInteractionEffect::UnsetFlag(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); } return effect; } else if (name == "ModifyHeartContainers") { - auto effect = new GameInteractionEffect::ModifyHeartContainers(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "FillMagic") { - return new GameInteractionEffect::FillMagic(); + return std::make_unique(); } else if (name == "EmptyMagic") { - return new GameInteractionEffect::EmptyMagic(); + return std::make_unique(); } else if (name == "ModifyRupees") { - auto effect = new GameInteractionEffect::ModifyRupees(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "NoUI") { - return new GameInteractionEffect::NoUI(); + return std::make_unique(); } else if (name == "ModifyGravity") { - auto effect = new GameInteractionEffect::ModifyGravity(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "ModifyHealth") { - auto effect = new GameInteractionEffect::ModifyHealth(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "SetPlayerHealth") { - auto effect = new GameInteractionEffect::SetPlayerHealth(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "FreezePlayer") { - return new GameInteractionEffect::FreezePlayer(); + return std::make_unique(); } else if (name == "BurnPlayer") { - return new GameInteractionEffect::BurnPlayer(); + return std::make_unique(); } else if (name == "ElectrocutePlayer") { - return new GameInteractionEffect::ElectrocutePlayer(); + return std::make_unique(); } else if (name == "KnockbackPlayer") { - auto effect = new GameInteractionEffect::KnockbackPlayer(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "ModifyLinkSize") { - auto effect = new GameInteractionEffect::ModifyLinkSize(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "InvisibleLink") { - return new GameInteractionEffect::InvisibleLink(); + return std::make_unique(); } else if (name == "PacifistMode") { - return new GameInteractionEffect::PacifistMode(); + return std::make_unique(); } else if (name == "DisableZTargeting") { - return new GameInteractionEffect::DisableZTargeting(); + return std::make_unique(); } else if (name == "WeatherRainstorm") { - return new GameInteractionEffect::WeatherRainstorm(); + return std::make_unique(); } else if (name == "ReverseControls") { - return new GameInteractionEffect::ReverseControls(); + return std::make_unique(); } else if (name == "ForceEquipBoots") { - auto effect = new GameInteractionEffect::ForceEquipBoots(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "ModifyMovementSpeedMultiplier") { - auto effect = new GameInteractionEffect::ModifyMovementSpeedMultiplier(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "OneHitKO") { - return new GameInteractionEffect::OneHitKO(); + return std::make_unique(); } else if (name == "ModifyDefenseModifier") { - auto effect = new GameInteractionEffect::ModifyDefenseModifier(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "GiveOrTakeShield") { - auto effect = new GameInteractionEffect::GiveOrTakeShield(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "TeleportPlayer") { - auto effect = new GameInteractionEffect::TeleportPlayer(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "ClearAssignedButtons") { - auto effect = new GameInteractionEffect::ClearAssignedButtons(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "SetTimeOfDay") { - auto effect = new GameInteractionEffect::SetTimeOfDay(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "SetCollisionViewer") { - return new GameInteractionEffect::SetCollisionViewer(); + return std::make_unique(); } else if (name == "RandomizeCosmetics") { - return new GameInteractionEffect::RandomizeCosmetics(); + return std::make_unique(); } else if (name == "PressButton") { - auto effect = new GameInteractionEffect::PressButton(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "PressRandomButton") { - auto effect = new GameInteractionEffect::PressRandomButton(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); } return effect; } else if (name == "AddOrTakeAmmo") { - auto effect = new GameInteractionEffect::AddOrTakeAmmo(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); } return effect; } else if (name == "RandomBombFuseTimer") { - return new GameInteractionEffect::RandomBombFuseTimer(); + return std::make_unique(); } else if (name == "DisableLedgeGrabs") { - return new GameInteractionEffect::DisableLedgeGrabs(); + return std::make_unique(); } else if (name == "RandomWind") { - return new GameInteractionEffect::RandomWind(); + return std::make_unique(); } else if (name == "RandomBonks") { - return new GameInteractionEffect::RandomBonks(); + return std::make_unique(); } else if (name == "PlayerInvincibility") { - return new GameInteractionEffect::PlayerInvincibility(); + return std::make_unique(); } else if (name == "SlipperyFloor") { - return new GameInteractionEffect::SlipperyFloor(); + return std::make_unique(); } else if (name == "SpawnEnemyWithOffset") { - auto effect = new GameInteractionEffect::SpawnEnemyWithOffset(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); } return effect; } else if (name == "SpawnActor") { - auto effect = new GameInteractionEffect::SpawnActor(); + auto effect = std::make_unique(); if (payload.contains("parameters")) { effect->parameters[0] = payload["parameters"][0].get(); effect->parameters[1] = payload["parameters"][1].get(); diff --git a/soh/soh/Network/Sail/Sail.h b/soh/soh/Network/Sail/Sail.h index fa2f0ff581..418606e66c 100644 --- a/soh/soh/Network/Sail/Sail.h +++ b/soh/soh/Network/Sail/Sail.h @@ -2,12 +2,14 @@ #define NETWORK_SAIL_H #ifdef __cplusplus +#include + #include "soh/Network/Network.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" class Sail : public Network { private: - GameInteractionEffectBase* EffectFromJson(nlohmann::json payload); + std::unique_ptr EffectFromJson(nlohmann::json payload); void RegisterHooks(); public: diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 657bf457d2..b52beaead3 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -2454,6 +2454,7 @@ bool SoH_HandleConfigDrop(char* filePath) { ->ClearBindings(); Rando::Settings::GetInstance()->UpdateAllOptions(); + SohGui::MarkRandomizerMenusDirty(); gui->SaveConsoleVariablesNextFrame(); ShipInit::Init("*"); diff --git a/soh/soh/SohGui/SohMenu.h b/soh/soh/SohGui/SohMenu.h index 5b0b07827d..c50c436740 100644 --- a/soh/soh/SohGui/SohMenu.h +++ b/soh/soh/SohGui/SohMenu.h @@ -28,6 +28,7 @@ static std::map languages = { }; void UpdateMenuTricks(); void UpdateMenuLocations(); +void MarkRandomizerMenusDirty(); class SohMenu : public Ship::Menu { public: diff --git a/soh/soh/SohGui/SohMenuRandomizer.cpp b/soh/soh/SohGui/SohMenuRandomizer.cpp index d6b7a99d44..a51a4fbca4 100644 --- a/soh/soh/SohGui/SohMenuRandomizer.cpp +++ b/soh/soh/SohGui/SohMenuRandomizer.cpp @@ -183,6 +183,11 @@ void DrawLocationsMenu(WidgetInfo& info) { ImGui::EndDisabled(); } +void MarkRandomizerMenusDirty() { + locationsDirty = true; + tricksDirty = true; +} + void UpdateMenuLocations() { RandomizerCheckObjects::UpdateImGuiVisibility(); // todo: this efficiently when we build out cvar array support diff --git a/soh/src/code/z_actor.c b/soh/src/code/z_actor.c index ef249ae9e8..7369727e02 100644 --- a/soh/src/code/z_actor.c +++ b/soh/src/code/z_actor.c @@ -2032,8 +2032,10 @@ s32 GiveItemEntryWithoutActor(PlayState* play, GetItemEntry getItemEntry) { PLAYER_STATE1_CLIMBING_LEDGE | PLAYER_STATE1_JUMPING | PLAYER_STATE1_FREEFALL | PLAYER_STATE1_FIRST_PERSON | PLAYER_STATE1_CLIMBING_LADDER)) && Player_GetExplosiveHeld(player) < 0) { - if (((player->heldActor != NULL) && ((getItemEntry.getItemId > GI_NONE) && (getItemEntry.getItemId < GI_MAX)) || - (IS_RANDO && (getItemEntry.getItemId > RG_NONE) && (getItemEntry.getItemId < RG_MAX))) || + if (((player->heldActor != NULL && (getItemEntry.modIndex == MOD_NONE && getItemEntry.getItemId > GI_NONE && + getItemEntry.getItemId < GI_MAX)) || + (getItemEntry.modIndex == MOD_RANDOMIZER && getItemEntry.getItemId > RG_NONE && + getItemEntry.getItemId < RG_MAX)) || (!(player->stateFlags1 & (PLAYER_STATE1_CARRYING_ACTOR | PLAYER_STATE1_IN_CUTSCENE)))) { if ((getItemEntry.getItemId != GI_NONE)) { player->getItemEntry = getItemEntry; @@ -2073,8 +2075,10 @@ s32 GiveItemEntryFromActor(Actor* actor, PlayState* play, GetItemEntry getItemEn PLAYER_STATE1_CLIMBING_LADDER)) && Player_GetExplosiveHeld(player) < 0) { if ((((player->heldActor != NULL) || (actor == player->talkActor)) && - ((!IS_RANDO && ((getItemEntry.getItemId > GI_NONE) && (getItemEntry.getItemId < GI_MAX))) || - (IS_RANDO && ((getItemEntry.getItemId > RG_NONE) && (getItemEntry.getItemId < RG_MAX))))) || + ((getItemEntry.getItemId == MOD_NONE && + ((getItemEntry.getItemId > GI_NONE) && (getItemEntry.getItemId < GI_MAX))) || + (getItemEntry.getItemId == MOD_RANDOMIZER && + ((getItemEntry.getItemId > RG_NONE) && (getItemEntry.getItemId < RG_MAX))))) || (!(player->stateFlags1 & (PLAYER_STATE1_CARRYING_ACTOR | PLAYER_STATE1_IN_CUTSCENE)))) { if ((actor->xzDistToPlayer < xzRange) && (fabsf(actor->yDistToPlayer) < yRange)) { s16 yawDiff = actor->yawTowardsPlayer - player->actor.shape.rot.y; @@ -2118,8 +2122,7 @@ s32 Actor_OfferGetItem(Actor* actor, PlayState* play, s32 getItemId, f32 xzRange PLAYER_STATE1_CLIMBING_LADDER)) && Player_GetExplosiveHeld(player) < 0) { if ((((player->heldActor != NULL) || (actor == player->talkActor)) && - ((!IS_RANDO && ((getItemId > GI_NONE) && (getItemId < GI_MAX))) || - (IS_RANDO && ((getItemId > RG_NONE) && (getItemId < RG_MAX))))) || + ((getItemId > GI_NONE) && (getItemId < GI_MAX))) || (!(player->stateFlags1 & (PLAYER_STATE1_CARRYING_ACTOR | PLAYER_STATE1_IN_CUTSCENE)))) { if ((actor->xzDistToPlayer < xzRange) && (fabsf(actor->yDistToPlayer) < yRange)) { s16 yawDiff = actor->yawTowardsPlayer - player->actor.shape.rot.y; diff --git a/soh/src/code/z_player_lib.c b/soh/src/code/z_player_lib.c index 227c802e90..f2e2b7321d 100644 --- a/soh/src/code/z_player_lib.c +++ b/soh/src/code/z_player_lib.c @@ -3,7 +3,6 @@ #include "objects/gameplay_field_keep/gameplay_field_keep.h" #include "objects/object_link_boy/object_link_boy.h" #include "objects/object_link_child/object_link_child.h" -#include "objects/object_triforce_spot/object_triforce_spot.h" #include "overlays/actors/ovl_Demo_Effect/z_demo_effect.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" @@ -1317,12 +1316,14 @@ s32 Player_OverrideLimbDrawGameplayCommon(PlayState* play, s32 limbIndex, Gfx** if (limbIndex == PLAYER_LIMB_HEAD) { if (CVarGetInteger(CVAR_COSMETIC("Link.HeadScale.Changed"), 0)) { f32 scale = CVarGetFloat(CVAR_COSMETIC("Link.HeadScale.Value"), 1.0f); - Matrix_Scale(scale, scale, scale, MTXMODE_APPLY); - if (scale > 1.2f) { - Matrix_Translate(-((LINK_IS_ADULT ? 320.0f : 200.0f) * scale), 0.0f, 0.0f, MTXMODE_APPLY); - } else if (scale < 1.0f) { - Matrix_Translate((LINK_IS_ADULT ? 3600.0f : 2900.0f) * ABS(scale - 1.0f), 0.0f, 0.0f, - MTXMODE_APPLY); + if (scale != 1.0f) { + Matrix_Scale(scale, scale, scale, MTXMODE_APPLY); + if (scale > 1.2f) { + Matrix_Translate(-((LINK_IS_ADULT ? 320.0f : 200.0f) * scale), 0.0f, 0.0f, MTXMODE_APPLY); + } else if (scale < 1.0f) { + Matrix_Translate((LINK_IS_ADULT ? 3600.0f : 2900.0f) * ABS(scale - 1.0f), 0.0f, 0.0f, + MTXMODE_APPLY); + } } } rot->x += this->headLimbRot.z; @@ -1331,8 +1332,10 @@ s32 Player_OverrideLimbDrawGameplayCommon(PlayState* play, s32 limbIndex, Gfx** } else if (limbIndex == PLAYER_LIMB_L_HAND) { if (CVarGetInteger(CVAR_COSMETIC("Link.SwordScale.Changed"), 0)) { f32 scale = CVarGetFloat(CVAR_COSMETIC("Link.SwordScale.Value"), 1.0f); - Matrix_Scale(scale, scale, scale, MTXMODE_APPLY); - Matrix_Translate(-((LINK_IS_ADULT ? 320.0f : 200.0f) * scale), 0.0f, 0.0f, MTXMODE_APPLY); + if (scale != 1.0f) { + Matrix_Scale(scale, scale, scale, MTXMODE_APPLY); + Matrix_Translate(-((LINK_IS_ADULT ? 320.0f : 200.0f) * (scale - 1.0f)), 0.0f, 0.0f, MTXMODE_APPLY); + } } } else if (limbIndex == PLAYER_LIMB_UPPER) { if (this->upperLimbYawSecondary != 0) { @@ -1913,8 +1916,10 @@ void Player_PostLimbDrawGameplay(PlayState* play, s32 limbIndex, Gfx** dList, Ve } if (this->actor.scale.y >= 0.0f) { - if (GameInteractor_Should(VB_DRAW_ADDITIONAL_RETICLES, (this->heldItemAction == PLAYER_IA_HOOKSHOT) || - (this->heldItemAction == PLAYER_IA_LONGSHOT))) { + if (GameInteractor_Should(VB_DRAW_ADDITIONAL_RETICLES, + (this->heldItemAction == PLAYER_IA_HOOKSHOT) || + (this->heldItemAction == PLAYER_IA_LONGSHOT), + this)) { Matrix_MultVec3f(&D_80126184, &this->unk_3C8); if (heldActor != NULL) { diff --git a/soh/src/overlays/actors/ovl_En_Ko/z_en_ko.c b/soh/src/overlays/actors/ovl_En_Ko/z_en_ko.c index 68b7765097..cfd312c714 100644 --- a/soh/src/overlays/actors/ovl_En_Ko/z_en_ko.c +++ b/soh/src/overlays/actors/ovl_En_Ko/z_en_ko.c @@ -1183,7 +1183,7 @@ void func_80A99048(EnKo* this, PlayState* play) { if (ENKO_TYPE == ENKO_TYPE_CHILD_5) { this->collider.base.ocFlags1 |= 0x40; } - if (GameInteractor_Should(VB_KOKIRI_GET_FOREST_QUEST_STATE2, false, this)) { + if (GameInteractor_Should(VB_KOKIRI_GET_FOREST_QUEST_STATE2, true, this)) { this->forestQuestState = EnKo_GetForestQuestState2(this); } Animation_ChangeByInfo(&this->skelAnime, sAnimationInfo, sOsAnimeLookup[ENKO_TYPE][this->forestQuestState]); diff --git a/soh/src/overlays/actors/ovl_En_Peehat/z_en_peehat.c b/soh/src/overlays/actors/ovl_En_Peehat/z_en_peehat.c index 0957398887..d48b611ea5 100644 --- a/soh/src/overlays/actors/ovl_En_Peehat/z_en_peehat.c +++ b/soh/src/overlays/actors/ovl_En_Peehat/z_en_peehat.c @@ -298,17 +298,19 @@ void EnPeehat_HitWhenGrounded(EnPeehat* this, PlayState* play) { s32 i; this->colCylinder.base.acFlags &= ~AC_HIT; - for (i = MAX_LARVA - this->unk_2FA; i > 0; i--) { - Actor* larva = - Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_EN_PEEHAT, - Rand_CenteredFloat(25.0f) + this->actor.world.pos.x, - Rand_CenteredFloat(25.0f) + (this->actor.world.pos.y + 50.0f), - Rand_CenteredFloat(25.0f) + this->actor.world.pos.z, 0, 0, 0, PEAHAT_TYPE_LARVA); + if (GameInteractor_Should(VB_PEEHAT_SPAWN_LARVAS, true, this, play)) { + for (i = MAX_LARVA - this->unk_2FA; i > 0; i--) { + Actor* larva = + Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_EN_PEEHAT, + Rand_CenteredFloat(25.0f) + this->actor.world.pos.x, + Rand_CenteredFloat(25.0f) + (this->actor.world.pos.y + 50.0f), + Rand_CenteredFloat(25.0f) + this->actor.world.pos.z, 0, 0, 0, PEAHAT_TYPE_LARVA); - if (larva != NULL) { - larva->velocity.y = 6.0f; - larva->shape.rot.y = larva->world.rot.y = Rand_CenteredFloat(0xFFFF); - this->unk_2FA++; + if (larva != NULL) { + larva->velocity.y = 6.0f; + larva->shape.rot.y = larva->world.rot.y = Rand_CenteredFloat(0xFFFF); + this->unk_2FA++; + } } } this->unk_2D4 = 8; diff --git a/soh/src/overlays/actors/ovl_Fishing/z_fishing.c b/soh/src/overlays/actors/ovl_Fishing/z_fishing.c index 77e16a147c..41aee59654 100644 --- a/soh/src/overlays/actors/ovl_Fishing/z_fishing.c +++ b/soh/src/overlays/actors/ovl_Fishing/z_fishing.c @@ -432,6 +432,8 @@ static FishingEffect sFishingEffects[FISHING_EFFECT_COUNT]; static Vec3f sStreamSoundProjectedPos; static s16 sFishOnHandParams; +f32 Fishing_GetMinimumRequiredScore(); + u8 AllHyruleLoaches() { return CVarGetInteger(CVAR_ENHANCEMENT("CustomizeFishing"), 0) && CVarGetInteger(CVAR_ENHANCEMENT("AllHyruleLoaches"), 0); @@ -904,12 +906,16 @@ void Fishing_Init(Actor* thisx, PlayState* play2) { if (sLinkAge == LINK_AGE_CHILD) { if ((HIGH_SCORE(HS_FISHING) & HS_FISH_LENGTH_CHILD) != 0) { sFishingRecordLength = HIGH_SCORE(HS_FISHING) & HS_FISH_LENGTH_CHILD; + } else if (CVarGetInteger(CVAR_ENHANCEMENT("CustomizeFishing"), 0)) { + sFishingRecordLength = Fishing_GetMinimumRequiredScore(); } else { sFishingRecordLength = 40.0f; // 6 lbs } } else { if ((HIGH_SCORE(HS_FISHING) & HS_FISH_LENGTH_ADULT) != 0) { sFishingRecordLength = (HIGH_SCORE(HS_FISHING) & HS_FISH_LENGTH_ADULT) >> 0x18; + } else if (CVarGetInteger(CVAR_ENHANCEMENT("CustomizeFishing"), 0)) { + sFishingRecordLength = Fishing_GetMinimumRequiredScore(); } else { sFishingRecordLength = 45.0f; // 7 lbs } 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 c6b092ea64..bb18a2da13 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -7340,8 +7340,9 @@ s32 Player_ActionHandler_2(Player* this, PlayState* play) { // getting bombchus need to show the cutscene) and whenever the player doesn't have the item yet. In // rando, we're overruling this because we need to keep showing the cutscene because those items can be // randomized and thus it's important to keep showing the cutscene. - uint8_t showItemCutscene = play->sceneNum == SCENE_BOMBCHU_BOWLING_ALLEY || - Item_CheckObtainability(giEntry.itemId) == ITEM_NONE || IS_RANDO; + uint8_t showItemCutscene = play->sceneNum == SCENE_BOMBCHU_BOWLING_ALLEY || IS_RANDO || + giEntry.modIndex == MOD_RANDOMIZER || + Item_CheckObtainability(giEntry.itemId) == ITEM_NONE; // Only skip cutscenes for drops when they're items/consumables from bushes/rocks/enemies. uint8_t isDropToSkip = @@ -7360,8 +7361,8 @@ s32 Player_ActionHandler_2(Player* this, PlayState* play) { // the player already has because those items could be a randomized item coming from scrubs, // freestanding PoH's and keys. So we need to once again overrule this specifically for items coming // from bushes/rocks/enemies when the player has already picked that item up. - uint8_t skipItemCutsceneRando = - IS_RANDO && Item_CheckObtainability(giEntry.itemId) != ITEM_NONE && isDropToSkip; + uint8_t skipItemCutsceneRando = IS_RANDO && giEntry.modIndex == MOD_NONE && + Item_CheckObtainability(giEntry.itemId) != ITEM_NONE && isDropToSkip; // Show cutscene when picking up a item. if (showItemCutscene && !skipItemCutscene && !skipItemCutsceneRando) {