diff --git a/.gitignore b/.gitignore index 2b7352d95..7feccf1fe 100644 --- a/.gitignore +++ b/.gitignore @@ -417,6 +417,7 @@ tags shipofharkinian.ini shipofharkinian.json imgui.ini +timesplitdata.json # Switch Stuff diff --git a/CMakeLists.txt b/CMakeLists.txt index 775170f46..40573a22a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,9 +6,12 @@ set(CMAKE_C_STANDARD 23 CACHE STRING "The C standard to use") set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version") -project(Ship VERSION 9.1.0 LANGUAGES C CXX) +project(Ship VERSION 9.1.1 LANGUAGES C CXX) include(CMake/soh-cvars.cmake) include(CMake/lus-cvars.cmake) +set(SPDLOG_LEVEL_TRACE 0) +set(SPDLOG_LEVEL_OFF 6) +set(SPDLOG_MIN_CUTOFF SPDLOG_LEVEL_TRACE CACHE STRING "cutoff at trace") option(SUPPRESS_WARNINGS "Suppress warnings in LUS and src (decomp)" ON) if(SUPPRESS_WARNINGS) diff --git a/libultraship b/libultraship index 5f4be9b6f..a8bdcab36 160000 --- a/libultraship +++ b/libultraship @@ -1 +1 @@ -Subproject commit 5f4be9b6f5f74917c303ab8b66a0b2f4ef91613d +Subproject commit a8bdcab363571038bb71f195f21ec3e9033a220d diff --git a/soh/CMakeLists.txt b/soh/CMakeLists.txt index aa979f6ed..102f3f583 100644 --- a/soh/CMakeLists.txt +++ b/soh/CMakeLists.txt @@ -431,13 +431,15 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") "ENABLE_DX11;" ">" "$<$:" - "NDEBUG" + "NDEBUG;" ">" "$<$:ENABLE_REMOTE_CONTROL>" "INCLUDE_GAME_PRINTF;" "F3DEX_GBI_2" "UNICODE;" "_UNICODE" + SPDLOG_ACTIVE_LEVEL=${SPDLOG_MIN_CUTOFF} + LOG_LEVEL_GAME_PRINTS=${SPDLOG_LEVEL_OFF} STORMLIB_NO_AUTO_LINK "_CRT_SECURE_NO_WARNINGS;" NOMINMAX @@ -448,7 +450,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") "NOINCLUDE_GAME_PRINTF;" "_DEBUG;" "_CRT_SECURE_NO_WARNINGS;" - "ENABLE_OPENGL" + "ENABLE_OPENGL;" ">" "$<$:" "NDEBUG;" @@ -457,7 +459,9 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") "F3DEX_GBI_2" "WIN32;" "UNICODE;" - "_UNICODE" + "_UNICODE;" + SPDLOG_ACTIVE_LEVEL=${SPDLOG_MIN_CUTOFF} + LOG_LEVEL_GAME_PRINTS=${SPDLOG_LEVEL_OFF} STORMLIB_NO_AUTO_LINK NOMINMAX ) @@ -465,33 +469,35 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") elseif (CMAKE_SYSTEM_NAME STREQUAL "CafeOS") target_compile_definitions(${PROJECT_NAME} PRIVATE "$<$:" - "_DEBUG" + "_DEBUG;" ">" "$<$:" - "NDEBUG" + "NDEBUG;" ">" - "F3DEX_GBI_2" - "SPDLOG_ACTIVE_LEVEL=3;" + "F3DEX_GBI_2;" "SPDLOG_NO_THREAD_ID;" "SPDLOG_NO_TLS;" "STBI_NO_THREAD_LOCALS;" + SPDLOG_ACTIVE_LEVEL=${SPDLOG_MIN_CUTOFF} + LOG_LEVEL_GAME_PRINTS=${SPDLOG_LEVEL_OFF} ) elseif ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU|Clang|AppleClang") target_compile_definitions(${PROJECT_NAME} PRIVATE "$<$:" - "_DEBUG" + "_DEBUG;" ">" "$<$:" - "NDEBUG" + "NDEBUG;" ">" - "F3DEX_GBI_2" - "$<$:ENABLE_REMOTE_CONTROL>" - "SPDLOG_ACTIVE_LEVEL=0;" + "F3DEX_GBI_2;" + "$<$:ENABLE_REMOTE_CONTROL>;" "_CONSOLE;" "_CRT_SECURE_NO_WARNINGS;" "ENABLE_OPENGL;" "UNICODE;" - "_UNICODE" + "_UNICODE;" + SPDLOG_ACTIVE_LEVEL=${SPDLOG_MIN_CUTOFF} + LOG_LEVEL_GAME_PRINTS=${SPDLOG_LEVEL_OFF} ) endif() ################################################################################ diff --git a/soh/assets/custom/presets/Enhancements - Curated Randomizer.json b/soh/assets/custom/presets/Enhancements - Curated Randomizer.json index 069d85c90..78d1e5d75 100644 --- a/soh/assets/custom/presets/Enhancements - Curated Randomizer.json +++ b/soh/assets/custom/presets/Enhancements - Curated Randomizer.json @@ -16,6 +16,7 @@ "ClimbSpeed": 3, "CrawlSpeed": 2, "CreditsFix": 1, + "CuccosToReturn": 1, "CustomizeFishing": 1, "CustomizeFrogsOcarinaGame": 1, "CustomizeOcarinaGame": 1, diff --git a/soh/include/functions.h b/soh/include/functions.h index 043326bca..4aab58ab7 100644 --- a/soh/include/functions.h +++ b/soh/include/functions.h @@ -13,8 +13,8 @@ extern "C" #include #include -#if defined(INCLUDE_GAME_PRINTF) && defined(_DEBUG) -#define osSyncPrintf(fmt, ...) lusprintf(__FILE__, __LINE__, 0, fmt, ##__VA_ARGS__) +#if (LOG_LEVEL_GAME_PRINTS >= SPDLOG_ACTIVE_LEVEL) && !(LOG_LEVEL_GAME_PRINTS >= 6) +#define osSyncPrintf(...) lusprintf(__FILE__, __LINE__, LOG_LEVEL_GAME_PRINTS , __VA_ARGS__) #else #define osSyncPrintf(fmt, ...) osSyncPrintfUnused(fmt, ##__VA_ARGS__) #endif diff --git a/soh/include/z64item.h b/soh/include/z64item.h index 214d82711..b1fa897fe 100644 --- a/soh/include/z64item.h +++ b/soh/include/z64item.h @@ -146,6 +146,9 @@ typedef enum { /* 0x1B */ SLOT_BOOTS_KOKIRI, /* 0x1C */ SLOT_BOOTS_IRON, /* 0x1D */ SLOT_BOOTS_HOVER, + /* 0x1E */ SLOT_SHIELD_DEKU, + /* 0x1F */ SLOT_SHIELD_HYLIAN, + /* 0x20 */ SLOT_SHIELD_MIRROR, /* 0xFF */ SLOT_NONE = 0xFF } InventorySlot; diff --git a/soh/soh/Enhancements/AssignableTunicsAndBoots.cpp b/soh/soh/Enhancements/AssignableTunicsAndBoots.cpp index 1ca5a7c19..535cd1400 100644 --- a/soh/soh/Enhancements/AssignableTunicsAndBoots.cpp +++ b/soh/soh/Enhancements/AssignableTunicsAndBoots.cpp @@ -14,7 +14,7 @@ extern PlayState* gPlayState; static u16 sItemButtons[] = { BTN_B, BTN_CLEFT, BTN_CDOWN, BTN_CRIGHT, BTN_DUP, BTN_DDOWN, BTN_DLEFT, BTN_DRIGHT }; -void UseTunicBoots(Player* player, PlayState* play, Input* input) { +static void UseTunicBoots(Player* player, PlayState* play, Input* input) { // Boots and tunics equip despite state if (player->stateFlags1 & (PLAYER_STATE1_INPUT_DISABLED | PLAYER_STATE1_IN_ITEM_CS | PLAYER_STATE1_IN_CUTSCENE | PLAYER_STATE1_TALKING | PLAYER_STATE1_DEAD) || @@ -30,7 +30,7 @@ void UseTunicBoots(Player* player, PlayState* play, Input* input) { } } - if (item >= ITEM_TUNIC_KOKIRI && item <= ITEM_BOOTS_HOVER) { + if (item >= ITEM_SHIELD_DEKU && item <= ITEM_BOOTS_HOVER) { if (item >= ITEM_BOOTS_KOKIRI) { u16 bootsValue = item - ITEM_BOOTS_KOKIRI + 1; if (CUR_EQUIP_VALUE(EQUIP_TYPE_BOOTS) == bootsValue) { @@ -41,7 +41,7 @@ void UseTunicBoots(Player* player, PlayState* play, Input* input) { Player_SetEquipmentData(play, player); func_808328EC(player, CUR_EQUIP_VALUE(EQUIP_TYPE_BOOTS) == EQUIP_VALUE_BOOTS_IRON ? NA_SE_PL_WALK_HEAVYBOOTS : NA_SE_PL_CHANGE_ARMS); - } else { + } else if (item >= ITEM_TUNIC_KOKIRI) { u16 tunicValue = item - ITEM_TUNIC_KOKIRI + 1; if (CUR_EQUIP_VALUE(EQUIP_TYPE_TUNIC) == tunicValue) { Inventory_ChangeEquipment(EQUIP_TYPE_TUNIC, EQUIP_VALUE_TUNIC_KOKIRI); @@ -50,45 +50,93 @@ void UseTunicBoots(Player* player, PlayState* play, Input* input) { } Player_SetEquipmentData(play, player); func_808328EC(player, NA_SE_PL_CHANGE_ARMS); + } else { + u16 shieldValue = item - ITEM_SHIELD_DEKU + 1; + if (CUR_EQUIP_VALUE(EQUIP_TYPE_SHIELD) != shieldValue) { + Inventory_ChangeEquipment(EQUIP_TYPE_SHIELD, shieldValue); + Player_SetEquipmentData(play, player); + func_808328EC(player, NA_SE_PL_CHANGE_ARMS); + } } } } -void ClearAssignedTunicsBoots(int32_t unused = 0) { +static void ClearAssignedTunicsBoots(int32_t unused = 0) { for (int32_t buttonIndex = 0; buttonIndex < 8; buttonIndex++) { int32_t item = gSaveContext.equips.buttonItems[buttonIndex]; - if (item >= ITEM_TUNIC_KOKIRI && item <= ITEM_BOOTS_HOVER) { + if (item >= ITEM_SHIELD_DEKU && item <= ITEM_BOOTS_HOVER) { gSaveContext.equips.buttonItems[buttonIndex] = ITEM_NONE; } } } +static void ClearDeletedAssignedEquipment(int16_t equipmentType, uint16_t equipValue) { + ItemID itemToRemove = ITEM_NONE; + + if (equipmentType == EQUIP_TYPE_TUNIC) { + switch (equipValue) { + case EQUIP_VALUE_TUNIC_KOKIRI: + break; + case EQUIP_VALUE_TUNIC_GORON: + itemToRemove = ITEM_TUNIC_GORON; + break; + case EQUIP_VALUE_TUNIC_ZORA: + itemToRemove = ITEM_TUNIC_ZORA; + break; + } + } else if (equipmentType == EQUIP_TYPE_SHIELD) { + switch (equipValue) { + case EQUIP_VALUE_SHIELD_DEKU: + itemToRemove = ITEM_SHIELD_DEKU; + break; + case EQUIP_VALUE_SHIELD_HYLIAN: + itemToRemove = ITEM_SHIELD_HYLIAN; + break; + case EQUIP_VALUE_SHIELD_MIRROR: + itemToRemove = ITEM_SHIELD_MIRROR; + break; + } + } + + if (itemToRemove == ITEM_NONE) { + return; + } + + for (int i = 1; i < ARRAY_COUNT(gSaveContext.equips.buttonItems); i++) { + if (gSaveContext.equips.buttonItems[i] == itemToRemove) { + gSaveContext.equips.buttonItems[i] = ITEM_NONE; + gSaveContext.equips.cButtonSlots[i - 1] = SLOT_NONE; + } + } +} + #define CVAR_TUNICBOOTS_NAME CVAR_ENHANCEMENT("AssignableTunicsAndBoots") #define CVAR_TUNICBOOTS_DEFAULT 0 #define CVAR_TUNICBOOTS_VALUE CVarGetInteger(CVAR_TUNICBOOTS_NAME, CVAR_TUNICBOOTS_DEFAULT) +#define CVAR_TUNICBOOTS_SET (CVAR_TUNICBOOTS_VALUE != CVAR_TUNICBOOTS_DEFAULT) -void RegisterAssignableTunicsBoots() { - // make sure we don't change our held/equipped item when changing tunics/boots - COND_VB_SHOULD(VB_CHANGE_HELD_ITEM_AND_USE_ITEM, CVAR_TUNICBOOTS_VALUE != CVAR_TUNICBOOTS_DEFAULT, { +static void RegisterAssignableTunicsBoots() { + // make sure we don't change our held/equipped item when changing shield/tunic/boots + COND_VB_SHOULD(VB_CHANGE_HELD_ITEM_AND_USE_ITEM, CVAR_TUNICBOOTS_SET, { int32_t item = va_arg(args, int32_t); - if (item >= ITEM_TUNIC_KOKIRI && item <= ITEM_BOOTS_HOVER) { + if (item >= ITEM_SHIELD_DEKU && item <= ITEM_BOOTS_HOVER) { *should = false; } }); // make sure we don't crash because tunics/boots don't have assoicated item actions - COND_VB_SHOULD(VB_ITEM_ACTION_BE_NONE, CVAR_TUNICBOOTS_VALUE != CVAR_TUNICBOOTS_DEFAULT, { + COND_VB_SHOULD(VB_ITEM_ACTION_BE_NONE, CVAR_TUNICBOOTS_SET, { int32_t item = va_arg(args, int32_t); - if (item >= ITEM_TUNIC_KOKIRI && item <= ITEM_BOOTS_HOVER) { + if (item >= ITEM_SHIELD_DEKU && item <= ITEM_BOOTS_HOVER) { *should = true; } }); - // don't throw items when the pressed button is a tunic or boots - COND_VB_SHOULD(VB_THROW_OR_PUT_DOWN_HELD_ITEM, CVAR_TUNICBOOTS_VALUE != CVAR_TUNICBOOTS_DEFAULT, { + // don't throw items when the pressed button is a shield, tunic or boots + COND_VB_SHOULD(VB_THROW_OR_PUT_DOWN_HELD_ITEM, CVAR_TUNICBOOTS_SET, { // if the vanilla condition doesn't want us to throw/put down the item, early return if (!*should) { return; @@ -104,13 +152,13 @@ void RegisterAssignableTunicsBoots() { } } - if (item >= ITEM_TUNIC_KOKIRI && item <= ITEM_BOOTS_HOVER) { + if (item >= ITEM_SHIELD_DEKU && item <= ITEM_BOOTS_HOVER) { *should = false; } }); // do something when the player presses a button to use the tunics/boots - COND_VB_SHOULD(VB_EXECUTE_PLAYER_ACTION_FUNC, CVAR_TUNICBOOTS_VALUE != CVAR_TUNICBOOTS_DEFAULT, { + COND_VB_SHOULD(VB_EXECUTE_PLAYER_ACTION_FUNC, CVAR_TUNICBOOTS_SET, { // if the vanilla condition doesn't want us to run the actionFunc, don't do any of this if (!*should) { return; @@ -133,13 +181,16 @@ void RegisterAssignableTunicsBoots() { UseTunicBoots(player, gPlayState, input); }); + // remove assigned equipment when it gets deleted + COND_HOOK(OnEquipmentDelete, CVAR_TUNICBOOTS_SET, ClearDeletedAssignedEquipment); + // clear out assigned tunics/boots when the enhancement is toggled off - if (GameInteractor::IsSaveLoaded(true) && CVAR_TUNICBOOTS_VALUE == CVAR_TUNICBOOTS_DEFAULT) { + if (GameInteractor::IsSaveLoaded(true) && !CVAR_TUNICBOOTS_SET) { ClearAssignedTunicsBoots(); } // clear out assigned tunics/boots when loading a save with enhancement turned off - COND_HOOK(OnLoadGame, CVAR_TUNICBOOTS_VALUE == CVAR_TUNICBOOTS_DEFAULT, ClearAssignedTunicsBoots); + COND_HOOK(OnLoadGame, !CVAR_TUNICBOOTS_SET, ClearAssignedTunicsBoots); } static RegisterShipInitFunc initFunc(RegisterAssignableTunicsBoots, { CVAR_TUNICBOOTS_NAME }); diff --git a/soh/soh/Enhancements/BonkDamage.cpp b/soh/soh/Enhancements/BonkDamage.cpp deleted file mode 100644 index c9e5e089a..000000000 --- a/soh/soh/Enhancements/BonkDamage.cpp +++ /dev/null @@ -1,51 +0,0 @@ -#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" -#include "soh/ShipInit.hpp" -#include "./enhancementTypes.h" - -extern "C" { -#include "functions.h" -#include "macros.h" -extern PlayState* gPlayState; -extern SaveContext gSaveContext; -} - -void RegisterBonkDamage() { - COND_HOOK(OnPlayerBonk, CVarGetInteger(CVAR_ENHANCEMENT("BonkDamageMult"), BONK_DAMAGE_NONE) != BONK_DAMAGE_NONE, - [] { - uint16_t bonkDamage = 0; - switch (CVarGetInteger(CVAR_ENHANCEMENT("BonkDamageMult"), BONK_DAMAGE_NONE)) { - case BONK_DAMAGE_NONE: - return; - case BONK_DAMAGE_OHKO: - gSaveContext.health = 0; - return; - case BONK_DAMAGE_QUARTER_HEART: - bonkDamage = 4; - break; - case BONK_DAMAGE_HALF_HEART: - bonkDamage = 8; - break; - case BONK_DAMAGE_1_HEART: - bonkDamage = 16; - break; - case BONK_DAMAGE_2_HEARTS: - bonkDamage = 32; - break; - case BONK_DAMAGE_4_HEARTS: - bonkDamage = 64; - break; - case BONK_DAMAGE_8_HEARTS: - bonkDamage = 128; - break; - default: - break; - } - - Health_ChangeBy(gPlayState, -bonkDamage); - // Set invincibility to make Link flash red as a visual damage indicator. - Player* player = GET_PLAYER(gPlayState); - player->invincibilityTimer = 28; - }); -} - -static RegisterShipInitFunc initFunc(RegisterBonkDamage, { CVAR_ENHANCEMENT("BonkDamageMult") }); diff --git a/soh/soh/Enhancements/CuccosToReturn.cpp b/soh/soh/Enhancements/CuccosToReturn.cpp deleted file mode 100644 index 2de929fd3..000000000 --- a/soh/soh/Enhancements/CuccosToReturn.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" -#include "soh/ShipInit.hpp" - -extern "C" { -extern PlayState* gPlayState; -#include "src/overlays/actors/ovl_En_Niw_Lady/z_en_niw_lady.h" -} - -void RegisterCuccosToReturn() { - COND_VB_SHOULD(VB_SET_CUCCO_COUNT, CVarGetInteger(CVAR_ENHANCEMENT("CuccosToReturn"), 7) != 7, { - EnNiwLady* enNiwLady = va_arg(args, EnNiwLady*); - // Override starting Cucco count using setting value - enNiwLady->cuccosInPen = 7 - CVarGetInteger(CVAR_ENHANCEMENT("CuccosToReturn"), 7); - *should = false; - }); -} - -static RegisterShipInitFunc initFunc(RegisterCuccosToReturn, { CVAR_ENHANCEMENT("CuccosToReturn") }); diff --git a/soh/soh/Enhancements/CustomSkeletons.cpp b/soh/soh/Enhancements/CustomSkeletons.cpp new file mode 100644 index 000000000..8d22e0e1c --- /dev/null +++ b/soh/soh/Enhancements/CustomSkeletons.cpp @@ -0,0 +1,37 @@ +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/resource/type/Skeleton.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "macros.h" +#include "variables.h" +extern PlayState* gPlayState; +} + +static void UpdateCustomSkeletonOnEquipTunic() { + if (!GameInteractor::IsSaveLoaded() || gPlayState == NULL) { + return; + } + + static int8_t previousTunic = -1; + int8_t equippedTunic = CUR_EQUIP_VALUE(EQUIP_TYPE_TUNIC); + if (equippedTunic != previousTunic) { + SOH::SkeletonPatcher::UpdateCustomSkeletons(); + previousTunic = equippedTunic; + } +} + +static void UpdateCustomSkeletonOnAssetAltChange() { + if (!GameInteractor::IsSaveLoaded() || gPlayState == NULL) { + return; + } + + SOH::SkeletonPatcher::UpdateCustomSkeletons(); +} + +static void RegisterCustomSkeletons() { + COND_HOOK(OnGameFrameUpdate, true, UpdateCustomSkeletonOnEquipTunic); + COND_HOOK(OnAssetAltChange, true, UpdateCustomSkeletonOnAssetAltChange); +} + +static RegisterShipInitFunc initFunc(RegisterCustomSkeletons); diff --git a/soh/soh/Enhancements/DampeFire.cpp b/soh/soh/Enhancements/DampeFire.cpp deleted file mode 100644 index 1378ecb04..000000000 --- a/soh/soh/Enhancements/DampeFire.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" -#include "soh/ShipInit.hpp" -#include "soh/Enhancements/enhancementTypes.h" - -extern "C" { -extern PlayState* gPlayState; -#include "src/overlays/actors/ovl_En_Po_Relay/z_en_po_relay.h" -} - -void RegisterDampeFire() { - COND_VB_SHOULD(VB_DAMPE_DROP_FLAME, CVarGetInteger(CVAR_ENHANCEMENT("DampeDropRate"), DAMPE_NORMAL) != DAMPE_NORMAL, - { - double chance; - int cooldown = 9; - switch (CVarGetInteger(CVAR_ENHANCEMENT("DampeDropRate"), DAMPE_NORMAL)) { - case DAMPE_NONE: - *should = false; - return; - default: - case DAMPE_NORMAL: - return; - case DAMPE_JALAPENO: - chance = 0.03; - break; - case DAMPE_CHIPOTLE: - chance = 0.1; - break; - case DAMPE_SCOTCH_BONNET: - chance = 0.2; - break; - case DAMPE_GHOST_PEPPER: - chance = 0.5; - cooldown = 4; - break; - case DAMPE_INFERNO: - *should = true; - return; - } - - EnPoRelay* actor = va_arg(args, EnPoRelay*); - if (actor->actionTimer > cooldown) { - actor->actionTimer = cooldown; - } - *should = actor->actionTimer == 0 && Rand_ZeroOne() < chance; - }); -} - -static RegisterShipInitFunc initFunc(RegisterDampeFire, { CVAR_ENHANCEMENT("DampeDropRate") }); diff --git a/soh/soh/Enhancements/Difficulty/BonkDamage.cpp b/soh/soh/Enhancements/Difficulty/BonkDamage.cpp new file mode 100644 index 000000000..04618c4a3 --- /dev/null +++ b/soh/soh/Enhancements/Difficulty/BonkDamage.cpp @@ -0,0 +1,55 @@ +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ShipInit.hpp" +#include "soh/Enhancements/enhancementTypes.h" + +extern "C" { +#include "functions.h" +#include "macros.h" +extern PlayState* gPlayState; +extern SaveContext gSaveContext; +} + +static constexpr BonkDamage CVAR_BONK_DAMAGE_DEFAULT = BONK_DAMAGE_NONE; +#define CVAR_BONK_DAMAGE_NAME CVAR_ENHANCEMENT("BonkDamageMult") +#define CVAR_BONK_DAMAGE_VALUE CVarGetInteger(CVAR_BONK_DAMAGE_NAME, CVAR_BONK_DAMAGE_DEFAULT) +#define CVAR_BONK_DAMAGE_SET (CVAR_BONK_DAMAGE_VALUE != CVAR_BONK_DAMAGE_DEFAULT) + +static void RegisterBonkDamage() { + COND_HOOK(OnPlayerBonk, CVAR_BONK_DAMAGE_SET, [] { + uint16_t bonkDamage = 0; + switch (CVAR_BONK_DAMAGE_VALUE) { + case BONK_DAMAGE_NONE: + return; + case BONK_DAMAGE_OHKO: + gSaveContext.health = 0; + return; + case BONK_DAMAGE_QUARTER_HEART: + bonkDamage = 4; + break; + case BONK_DAMAGE_HALF_HEART: + bonkDamage = 8; + break; + case BONK_DAMAGE_1_HEART: + bonkDamage = 16; + break; + case BONK_DAMAGE_2_HEARTS: + bonkDamage = 32; + break; + case BONK_DAMAGE_4_HEARTS: + bonkDamage = 64; + break; + case BONK_DAMAGE_8_HEARTS: + bonkDamage = 128; + break; + default: + break; + } + + Health_ChangeBy(gPlayState, -bonkDamage); + // Set invincibility to make Link flash red as a visual damage indicator. + Player* player = GET_PLAYER(gPlayState); + player->invincibilityTimer = 28; + }); +} + +static RegisterShipInitFunc initFunc(RegisterBonkDamage, { CVAR_BONK_DAMAGE_NAME }); diff --git a/soh/soh/Enhancements/Difficulty/CuccosToReturn.cpp b/soh/soh/Enhancements/Difficulty/CuccosToReturn.cpp new file mode 100644 index 000000000..f1d5a2349 --- /dev/null +++ b/soh/soh/Enhancements/Difficulty/CuccosToReturn.cpp @@ -0,0 +1,23 @@ +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ShipInit.hpp" + +extern "C" { +extern PlayState* gPlayState; +#include "src/overlays/actors/ovl_En_Niw_Lady/z_en_niw_lady.h" +} + +static constexpr int32_t CVAR_CUCCOS_TO_RETURN_DEFAULT = 7; +#define CVAR_CUCCOS_TO_RETURN_NAME CVAR_ENHANCEMENT("CuccosToReturn") +#define CVAR_CUCCOS_TO_RETURN_VALUE CVarGetInteger(CVAR_CUCCOS_TO_RETURN_NAME, CVAR_CUCCOS_TO_RETURN_DEFAULT) +#define CVAR_CUCCOS_TO_RETURN_SET (CVAR_CUCCOS_TO_RETURN_VALUE != CVAR_CUCCOS_TO_RETURN_DEFAULT) + +static void RegisterCuccosToReturn() { + COND_VB_SHOULD(VB_SET_CUCCO_COUNT, CVAR_CUCCOS_TO_RETURN_SET, { + EnNiwLady* enNiwLady = va_arg(args, EnNiwLady*); + // Override starting Cucco count using setting value + enNiwLady->cuccosInPen = 7 - CVAR_CUCCOS_TO_RETURN_VALUE; + *should = false; + }); +} + +static RegisterShipInitFunc initFunc(RegisterCuccosToReturn, { CVAR_CUCCOS_TO_RETURN_NAME }); diff --git a/soh/soh/Enhancements/Difficulty/DampeFire.cpp b/soh/soh/Enhancements/Difficulty/DampeFire.cpp new file mode 100644 index 000000000..13afc88f0 --- /dev/null +++ b/soh/soh/Enhancements/Difficulty/DampeFire.cpp @@ -0,0 +1,52 @@ +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ShipInit.hpp" +#include "soh/Enhancements/enhancementTypes.h" + +extern "C" { +extern PlayState* gPlayState; +#include "src/overlays/actors/ovl_En_Po_Relay/z_en_po_relay.h" +} + +static constexpr DampeDropRate CVAR_DAMPE_DROP_RATE_DEFAULT = DAMPE_NORMAL; +#define CVAR_DAMPE_DROP_RATE_NAME CVAR_ENHANCEMENT("DampeDropRate") +#define CVAR_DAMPE_DROP_RATE_VALUE CVarGetInteger(CVAR_DAMPE_DROP_RATE_NAME, CVAR_DAMPE_DROP_RATE_DEFAULT) +#define CVAR_DAMPE_DROP_RATE_SET (CVAR_DAMPE_DROP_RATE_VALUE != CVAR_DAMPE_DROP_RATE_DEFAULT) + +static void RegisterDampeFire() { + COND_VB_SHOULD(VB_DAMPE_DROP_FLAME, CVAR_DAMPE_DROP_RATE_SET, { + double chance; + int cooldown = 9; + switch (CVAR_DAMPE_DROP_RATE_VALUE) { + case DAMPE_NONE: + *should = false; + return; + default: + case DAMPE_NORMAL: + return; + case DAMPE_JALAPENO: + chance = 0.03; + break; + case DAMPE_CHIPOTLE: + chance = 0.1; + break; + case DAMPE_SCOTCH_BONNET: + chance = 0.2; + break; + case DAMPE_GHOST_PEPPER: + chance = 0.5; + cooldown = 4; + break; + case DAMPE_INFERNO: + *should = true; + return; + } + + EnPoRelay* actor = va_arg(args, EnPoRelay*); + if (actor->actionTimer > cooldown) { + actor->actionTimer = cooldown; + } + *should = actor->actionTimer == 0 && Rand_ZeroOne() < chance; + }); +} + +static RegisterShipInitFunc initFunc(RegisterDampeFire, { CVAR_DAMPE_DROP_RATE_NAME }); diff --git a/soh/soh/Enhancements/Difficulty/PermanentLosses.cpp b/soh/soh/Enhancements/Difficulty/PermanentLosses.cpp new file mode 100644 index 000000000..32b00057e --- /dev/null +++ b/soh/soh/Enhancements/Difficulty/PermanentLosses.cpp @@ -0,0 +1,79 @@ +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/Enhancements/mods.h" +#include "soh/OTRGlobals.h" +#include "soh/SaveManager.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "functions.h" +#include "macros.h" +#include "variables.h" +#include "z64save.h" +extern SaveContext gSaveContext; +extern PlayState* gPlayState; +} + +static constexpr int32_t CVAR_PERM_HEART_LOSS_DEFAULT = 0; +#define CVAR_PERM_HEART_LOSS_NAME CVAR_ENHANCEMENT("PermanentHeartLoss") +#define CVAR_PERM_HEART_LOSS_VALUE CVarGetInteger(CVAR_PERM_HEART_LOSS_NAME, CVAR_PERM_HEART_LOSS_DEFAULT) + +static constexpr int32_t CVAR_DELETE_FILE_DEFAULT = 0; +#define CVAR_DELETE_FILE_NAME CVAR_ENHANCEMENT("DeleteFileOnDeath") +#define CVAR_DELETE_FILE_VALUE CVarGetInteger(CVAR_DELETE_FILE_NAME, CVAR_DELETE_FILE_DEFAULT) + +static bool hasAffectedHealth = false; + +void UpdatePermanentHeartLossState() { + if (!GameInteractor::IsSaveLoaded() || !hasAffectedHealth || CVAR_PERM_HEART_LOSS_VALUE) + return; + + uint8_t heartContainers = gSaveContext.ship.stats.heartContainers; // each worth 16 health + uint8_t heartPieces = gSaveContext.ship.stats.heartPieces; // each worth 4 health, but only in groups of 4 + uint8_t startingHealth = + 16 * (IS_RANDO ? (OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_STARTING_HEARTS) + 1) : 3); + + uint8_t newCapacity = startingHealth + (heartContainers * 16) + ((heartPieces - (heartPieces % 4)) * 4); + gSaveContext.healthCapacity = MAX(newCapacity, gSaveContext.healthCapacity); + gSaveContext.health = MIN(gSaveContext.health, gSaveContext.healthCapacity); + hasAffectedHealth = false; +} + +static void UpdateHealthCapacity() { + if (!GameInteractor::IsSaveLoaded()) + return; + + if (gSaveContext.healthCapacity > 16 && gSaveContext.healthCapacity - gSaveContext.health >= 16) { + gSaveContext.healthCapacity -= 16; + gSaveContext.health = MIN(gSaveContext.health, gSaveContext.healthCapacity); + hasAffectedHealth = true; + } +} + +static void DeleteFileOnDeath() { + if (!GameInteractor::IsSaveLoaded() || gPlayState == NULL) + return; + + if (gPlayState->gameOverCtx.state == GAMEOVER_DEATH_MENU && gPlayState->pauseCtx.state == 9) { + SaveManager::Instance->DeleteZeldaFile(gSaveContext.fileNum); + hasAffectedHealth = false; + std::reinterpret_pointer_cast( + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console")) + ->Dispatch("reset"); + } +} + +static void RegisterPermanentHeartLoss() { + COND_HOOK(OnPlayerUpdate, CVAR_PERM_HEART_LOSS_VALUE, UpdateHealthCapacity); +} + +static void RegisterDeleteFileOnDeath() { + COND_HOOK(OnGameFrameUpdate, CVAR_DELETE_FILE_VALUE, DeleteFileOnDeath); +} + +static void RegisterResetAffectedHealthOnLoad() { + COND_HOOK(OnLoadGame, true, [](int16_t) { hasAffectedHealth = false; }); +} + +static RegisterShipInitFunc initFunc_PermanentHeartLoss(RegisterPermanentHeartLoss, { CVAR_PERM_HEART_LOSS_NAME }); +static RegisterShipInitFunc initFunc_DeleteFileOnDeath(RegisterDeleteFileOnDeath, { CVAR_DELETE_FILE_NAME }); +static RegisterShipInitFunc initFunc_ResetAffectedHealth(RegisterResetAffectedHealthOnLoad); diff --git a/soh/soh/Enhancements/SwitchTimerMultiplier.cpp b/soh/soh/Enhancements/Difficulty/SwitchTimerMultiplier.cpp similarity index 69% rename from soh/soh/Enhancements/SwitchTimerMultiplier.cpp rename to soh/soh/Enhancements/Difficulty/SwitchTimerMultiplier.cpp index 94982837d..4090c1f95 100644 --- a/soh/soh/Enhancements/SwitchTimerMultiplier.cpp +++ b/soh/soh/Enhancements/Difficulty/SwitchTimerMultiplier.cpp @@ -5,9 +5,13 @@ extern "C" { extern PlayState* gPlayState; } +static constexpr int32_t CVAR_SWITCH_TIMER_DEFAULT = 0; +#define CVAR_SWITCH_TIMER_NAME CVAR_ENHANCEMENT("SwitchTimerMultiplier") +#define CVAR_SWITCH_TIMER_VALUE CVarGetInteger(CVAR_SWITCH_TIMER_NAME, CVAR_SWITCH_TIMER_DEFAULT) + void RegisterSwitchTimerMultiplier() { - COND_VB_SHOULD(VB_SWITCH_TIMER_TICK, CVarGetInteger(CVAR_ENHANCEMENT("SwitchTimerMultiplier"), 0) != 0, { - int multiplier = CVarGetInteger(CVAR_ENHANCEMENT("SwitchTimerMultiplier"), 0); + COND_VB_SHOULD(VB_SWITCH_TIMER_TICK, CVAR_SWITCH_TIMER_VALUE != 0, { + int multiplier = CVAR_SWITCH_TIMER_VALUE; if (multiplier != 0) { Actor* actor = va_arg(args, Actor*); if (multiplier < -3 && actor->id == ACTOR_OBJ_SYOKUDAI) { @@ -26,4 +30,4 @@ void RegisterSwitchTimerMultiplier() { }); } -static RegisterShipInitFunc initFunc(RegisterSwitchTimerMultiplier, { CVAR_ENHANCEMENT("SwitchTimerMultiplier") }); +static RegisterShipInitFunc initFunc(RegisterSwitchTimerMultiplier, { CVAR_SWITCH_TIMER_NAME }); diff --git a/soh/soh/Enhancements/TreesDropSticks.cpp b/soh/soh/Enhancements/Difficulty/TreesDropSticks.cpp similarity index 62% rename from soh/soh/Enhancements/TreesDropSticks.cpp rename to soh/soh/Enhancements/Difficulty/TreesDropSticks.cpp index 7fdc4eb0a..9bf868977 100644 --- a/soh/soh/Enhancements/TreesDropSticks.cpp +++ b/soh/soh/Enhancements/Difficulty/TreesDropSticks.cpp @@ -7,8 +7,12 @@ extern "C" { extern PlayState* gPlayState; -void RegisterTreesDropSticks() { - COND_VB_SHOULD(VB_TREE_DROP_COLLECTIBLE, CVarGetInteger(CVAR_ENHANCEMENT("TreesDropSticks"), 0), { +static constexpr int32_t CVAR_TREES_DROP_STICKS_DEFAULT = 0; +#define CVAR_TREES_DROP_STICKS_NAME CVAR_ENHANCEMENT("TreesDropSticks") +#define CVAR_TREES_DROP_STICKS_VALUE CVarGetInteger(CVAR_TREES_DROP_STICKS_NAME, CVAR_TREES_DROP_STICKS_DEFAULT) + +static void RegisterTreesDropSticks() { + COND_VB_SHOULD(VB_TREE_DROP_COLLECTIBLE, CVAR_TREES_DROP_STICKS_VALUE, { if (INV_CONTENT(ITEM_STICK) != ITEM_NONE) { EnWood02* tree = va_arg(args, EnWood02*); Vec3f dropsSpawnPt = tree->actor.world.pos; @@ -21,11 +25,11 @@ void RegisterTreesDropSticks() { } }); - COND_VB_SHOULD(VB_PREVENT_ADULT_STICK, CVarGetInteger(CVAR_ENHANCEMENT("TreesDropSticks"), 0), { + COND_VB_SHOULD(VB_PREVENT_ADULT_STICK, CVAR_TREES_DROP_STICKS_VALUE, { if (INV_CONTENT(ITEM_STICK) != ITEM_NONE) { *should = false; } }); } -static RegisterShipInitFunc initFunc(RegisterTreesDropSticks, { CVAR_ENHANCEMENT("TreesDropSticks") }); +static RegisterShipInitFunc initFunc(RegisterTreesDropSticks, { CVAR_TREES_DROP_STICKS_NAME }); diff --git a/soh/soh/Enhancements/ExtraModes/MirroredWorld.cpp b/soh/soh/Enhancements/ExtraModes/MirroredWorld.cpp new file mode 100644 index 000000000..2f4f9e4d0 --- /dev/null +++ b/soh/soh/Enhancements/ExtraModes/MirroredWorld.cpp @@ -0,0 +1,80 @@ +#include "soh/Enhancements/cosmetics/authenticGfxPatches.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/Enhancements/randomizer/3drando/random.hpp" +#include "soh/Enhancements/randomizer/context.h" +#include "soh/Enhancements/enhancementTypes.h" +#include "soh/Enhancements/mods.h" +#include "soh/ResourceManagerHelpers.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "variables.h" +extern SaveContext gSaveContext; +} + +static constexpr MirroredWorldMode CVAR_MIRRORED_WORLD_DEFAULT = MIRRORED_WORLD_OFF; +#define CVAR_MIRRORED_WORLD_NAME CVAR_ENHANCEMENT("MirroredWorld") +#define CVAR_MIRRORED_WORLD_MODE_NAME CVAR_ENHANCEMENT("MirroredWorldMode") +#define CVAR_MIRRORED_WORLD_MODE_VALUE CVarGetInteger(CVAR_MIRRORED_WORLD_MODE_NAME, CVAR_MIRRORED_WORLD_DEFAULT) + +static bool prevMirroredWorld = false; + +static bool MirroredWorld_IsInDungeon(int32_t sceneNum) { + return (sceneNum >= SCENE_DEKU_TREE && sceneNum <= SCENE_INSIDE_GANONS_CASTLE_COLLAPSE && + sceneNum != SCENE_THIEVES_HIDEOUT) || + (sceneNum >= SCENE_DEKU_TREE_BOSS && sceneNum <= SCENE_GANONS_TOWER_COLLAPSE_EXTERIOR) || + (sceneNum == SCENE_GANON_BOSS); +} + +static void MirroredWorld_InitRandomSeed(int32_t sceneNum) { + uint32_t seed = + sceneNum + (IS_RANDO ? Rando::Context::GetInstance()->GetSeed() : gSaveContext.ship.stats.fileCreatedAt); + Random_Init(seed); +} + +static bool MirroredWorld_ShouldApply(int32_t sceneNum) { + switch (CVAR_MIRRORED_WORLD_MODE_VALUE) { + case MIRRORED_WORLD_ALWAYS: + return true; + case MIRRORED_WORLD_RANDOM_SEEDED: + MirroredWorld_InitRandomSeed(sceneNum); + case MIRRORED_WORLD_RANDOM: + return Random(0, 2) == 1; + case MIRRORED_WORLD_DUNGEONS_ALL: + return MirroredWorld_IsInDungeon(sceneNum); + case MIRRORED_WORLD_DUNGEONS_VANILLA: + return MirroredWorld_IsInDungeon(sceneNum) && !ResourceMgr_IsSceneMasterQuest(sceneNum); + case MIRRORED_WORLD_DUNGEONS_MQ: + return MirroredWorld_IsInDungeon(sceneNum) && ResourceMgr_IsSceneMasterQuest(sceneNum); + case MIRRORED_WORLD_DUNGEONS_RANDOM_SEEDED: + MirroredWorld_InitRandomSeed(sceneNum); + case MIRRORED_WORLD_DUNGEONS_RANDOM: + return MirroredWorld_IsInDungeon(sceneNum) && (Random(0, 2) == 1); + default: + return false; + } +} + +static void RegisterMirroredWorld() { + COND_HOOK(OnSceneInit, CVAR_MIRRORED_WORLD_MODE_VALUE, UpdateMirrorModeState); +} + +static RegisterShipInitFunc initFunc(RegisterMirroredWorld, { CVAR_MIRRORED_WORLD_MODE_NAME }); + +void UpdateMirrorModeState(int32_t sceneNum) { + bool nextMirroredWorld = MirroredWorld_ShouldApply(sceneNum); + + if (prevMirroredWorld == nextMirroredWorld) { + return; + } + prevMirroredWorld = nextMirroredWorld; + + if (nextMirroredWorld) { + CVarSetInteger(CVAR_MIRRORED_WORLD_NAME, 1); + } else { + CVarClear(CVAR_MIRRORED_WORLD_NAME); + RegisterMirroredWorld(); + } + + ApplyMirrorWorldGfxPatches(); +} diff --git a/soh/soh/Enhancements/GameplayStats/BossDefeatTimestamps.cpp b/soh/soh/Enhancements/GameplayStats/BossDefeatTimestamps.cpp new file mode 100644 index 000000000..4a3f240f8 --- /dev/null +++ b/soh/soh/Enhancements/GameplayStats/BossDefeatTimestamps.cpp @@ -0,0 +1,29 @@ +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ShipInit.hpp" +#include "soh/Network/Archipelago/Archipelago.h" + +extern "C" SaveContext gSaveContext; + +#define BOSS_DEFEAT_TIMESTAMP(actorID, timestamp) \ + COND_ID_HOOK(OnBossDefeat, actorID, true, \ + [](void* refActor) { gSaveContext.ship.stats.itemTimestamp[timestamp] = GAMEPLAYSTAT_TOTAL_TIME; }); + +static void RegisterBossDefeatTimestamps() { + BOSS_DEFEAT_TIMESTAMP(ACTOR_BOSS_GOMA, TIMESTAMP_DEFEAT_GOHMA); + BOSS_DEFEAT_TIMESTAMP(ACTOR_BOSS_DODONGO, TIMESTAMP_DEFEAT_KING_DODONGO); + BOSS_DEFEAT_TIMESTAMP(ACTOR_BOSS_VA, TIMESTAMP_DEFEAT_BARINADE); + BOSS_DEFEAT_TIMESTAMP(ACTOR_BOSS_GANONDROF, TIMESTAMP_DEFEAT_PHANTOM_GANON); + BOSS_DEFEAT_TIMESTAMP(ACTOR_BOSS_FD2, TIMESTAMP_DEFEAT_VOLVAGIA); + BOSS_DEFEAT_TIMESTAMP(ACTOR_BOSS_MO, TIMESTAMP_DEFEAT_MORPHA); + BOSS_DEFEAT_TIMESTAMP(ACTOR_BOSS_SST, TIMESTAMP_DEFEAT_BONGO_BONGO); + BOSS_DEFEAT_TIMESTAMP(ACTOR_BOSS_TW, TIMESTAMP_DEFEAT_TWINROVA); + BOSS_DEFEAT_TIMESTAMP(ACTOR_BOSS_GANON, TIMESTAMP_DEFEAT_GANONDORF); + BOSS_DEFEAT_TIMESTAMP(ACTOR_BOSS_GANON2, TIMESTAMP_DEFEAT_GANON); + + COND_ID_HOOK(OnBossDefeat, ACTOR_BOSS_GANON2, true, [](void* refActor) { + gSaveContext.ship.stats.gameComplete = true; + ArchipelagoClient::GetInstance().SendGameWon(); + }); +} + +static RegisterShipInitFunc initFunc(RegisterBossDefeatTimestamps); diff --git a/soh/soh/Enhancements/Graphics/Disable2DBackgrounds.cpp b/soh/soh/Enhancements/Graphics/Disable2DBackgrounds.cpp index 253e0abd2..9fbc3375d 100644 --- a/soh/soh/Enhancements/Graphics/Disable2DBackgrounds.cpp +++ b/soh/soh/Enhancements/Graphics/Disable2DBackgrounds.cpp @@ -13,7 +13,7 @@ extern PlayState* gPlayState; #define CVAR_NAME CVAR_ENHANCEMENT("3DSceneRender") #define CVAR_VALUE CVarGetInteger(CVAR_NAME, 0) -std::vector fogControlList = { +std::set fogControlList = { SCENE_MARKET_ENTRANCE_DAY, SCENE_MARKET_ENTRANCE_NIGHT, SCENE_MARKET_ENTRANCE_RUINS, @@ -46,7 +46,7 @@ std::vector fogControlList = { SCENE_GRAVEKEEPERS_HUT, }; -std::vector skyboxSceneControlList = { +std::set skyboxSceneControlList = { SCENE_MARKET_ENTRANCE_DAY, SCENE_MARKET_ENTRANCE_NIGHT, SCENE_MARKET_ENTRANCE_RUINS, @@ -62,7 +62,7 @@ std::vector skyboxSceneControlList = { SCENE_FOREST_TEMPLE, }; -std::vector skyboxIdControlList = { +std::set skyboxIdControlList = { SKYBOX_BAZAAR, SKYBOX_HOUSE_LINK, SKYBOX_MARKET_ADULT, @@ -88,74 +88,49 @@ std::vector skyboxIdControlList = { void Register3DPreRenderedScenes() { COND_HOOK(AfterSceneCommands, CVAR_VALUE, [](int16_t sceneNum) { - // Check if this scene is in the skyboxControlList - bool shouldControlSkybox = false; - for (const auto& scene : skyboxSceneControlList) { - if (sceneNum == scene) { - shouldControlSkybox = true; - break; - } + if (!skyboxSceneControlList.contains(static_cast(sceneNum))) { + return; } - if (shouldControlSkybox) { - // Add a skybox on scenes from skyboxSceneControlList - gPlayState->envCtx.skyboxDisabled = false; + // Add a skybox on scenes from skyboxSceneControlList + gPlayState->envCtx.skyboxDisabled = false; - // Replace skybox with normal sky - gPlayState->skyboxId = SKYBOX_NORMAL_SKY; - // Apply the always cloudy skybox as an adult for Temple of Time and the Market - if (sceneNum == SCENE_TEMPLE_OF_TIME_EXTERIOR_RUINS || sceneNum == SCENE_MARKET_RUINS || - sceneNum == SCENE_MARKET_ENTRANCE_RUINS) { - gWeatherMode = 3; - } + // Replace skybox with normal sky + gPlayState->skyboxId = SKYBOX_NORMAL_SKY; + // Apply the always cloudy skybox as an adult for Temple of Time and the Market + if (sceneNum == SCENE_TEMPLE_OF_TIME_EXTERIOR_RUINS || sceneNum == SCENE_MARKET_RUINS || + sceneNum == SCENE_MARKET_ENTRANCE_RUINS) { + gWeatherMode = 3; } }); COND_HOOK(OnPlayDrawBegin, CVAR_VALUE, []() { - if (!CVarGetInteger(CVAR_ENHANCEMENT("3DSceneRender"), 0)) { + if (!fogControlList.contains(static_cast(gPlayState->sceneNum))) { return; } - for (auto& scene : fogControlList) { - if (scene == gPlayState->sceneNum) { - if ((HREG(80) != 10) || (HREG(82) != 0)) { - // Furthest possible fog and zFar - gPlayState->view.zFar = 12800; - gPlayState->lightCtx.fogNear = 996; // Set to 1000 to complete disable fog entirely - gPlayState->lightCtx.fogFar = 12800; - // General gray fog color - gPlayState->lightCtx.fogColor[0] = 100; - gPlayState->lightCtx.fogColor[1] = 100; - gPlayState->lightCtx.fogColor[2] = 100; - } - break; - } - } - }); - REGISTER_VB_SHOULD(VB_DRAW_2D_BACKGROUND, { - if (CVAR_VALUE) { - *should = false; - return; + if ((HREG(80) != 10) || (HREG(82) != 0)) { + // Furthest possible fog and zFar + gPlayState->view.zFar = 12800; + gPlayState->lightCtx.fogNear = 996; // Set to 1000 to complete disable fog entirely + gPlayState->lightCtx.fogFar = 12800; + // General gray fog color + gPlayState->lightCtx.fogColor[0] = 100; + gPlayState->lightCtx.fogColor[1] = 100; + gPlayState->lightCtx.fogColor[2] = 100; } }); - REGISTER_VB_SHOULD(VB_LOAD_SKYBOX, { - if (!gPlayState) { + COND_VB_SHOULD(VB_DRAW_2D_BACKGROUND, CVAR_VALUE, { *should = false; }); + + COND_VB_SHOULD(VB_LOAD_SKYBOX, CVAR_VALUE, { + if (!gPlayState || !skyboxIdControlList.contains(static_cast(gPlayState->skyboxCtx.skyboxId))) { return; } - if (!CVAR_VALUE) { - return; - } - - for (auto& skybox : skyboxIdControlList) { - if (gPlayState->skyboxCtx.skyboxId == skybox) { - gPlayState->skyboxCtx.unk_140 = 0; - *should = false; - return; - } - } + gPlayState->skyboxCtx.unk_140 = 0; + *should = false; }); } -static RegisterShipInitFunc initFunc(Register3DPreRenderedScenes, { CVAR_NAME }); \ No newline at end of file +static RegisterShipInitFunc initFunc(Register3DPreRenderedScenes, { CVAR_NAME }); diff --git a/soh/soh/Enhancements/Presets/Presets.cpp b/soh/soh/Enhancements/Presets/Presets.cpp index 0641eb22c..876ce1449 100644 --- a/soh/soh/Enhancements/Presets/Presets.cpp +++ b/soh/soh/Enhancements/Presets/Presets.cpp @@ -61,7 +61,9 @@ void PresetCheckboxStyle(const ImVec4& color) { } static BlockInfo blockInfo[PRESET_SECTION_MAX] = { - { { CVAR_PREFIX_SETTING, CVAR_PREFIX_WINDOW }, ICON_FA_COG, { "Settings", "settings" } }, + { { CVAR_PREFIX_SETTING, CVAR_PREFIX_WINDOW, CVAR_PREFIX_GAMEPLAY_STATS }, + ICON_FA_COG, + { "Settings", "settings" } }, { { CVAR_PREFIX_ENHANCEMENT, CVAR_PREFIX_RANDOMIZER_ENHANCEMENT, CVAR_PREFIX_CHEAT }, ICON_FA_PLUS_CIRCLE, { "Enhancements", "enhancements" } }, @@ -96,12 +98,28 @@ void applyPreset(std::string presetName, std::vector includeSecti } } auto section = info.presetValues["blocks"][blockInfo[i].names[1]]; + std::string sectionStrategy = "overwrite"; + if (info.presetValues.contains("blockStrategy") && + info.presetValues["blockStrategy"].contains(blockInfo[i].names[1])) { + sectionStrategy = info.presetValues["blockStrategy"][blockInfo[i].names[1]]; + } + for (auto& item : section.items()) { if (section[item.key()].is_null()) { CVarClearBlock(item.key().c_str()); } else { + auto block = item.value(); + if (sectionStrategy == "merge") { + auto currentJson = Ship::Context::GetInstance()->GetConfig()->GetNestedJson(); + if (currentJson.contains("CVars") && currentJson["CVars"].contains(item.key())) { + block = currentJson["CVars"][item.key()]; + // Recursively merge the two json objects + block.update(item.value(), true); + } + } + Ship::Context::GetInstance()->GetConfig()->SetBlock(fmt::format("{}.{}", "CVars", item.key()), - item.value()); + block); Ship::Context::GetInstance()->GetConsoleVariables()->Load(); } } @@ -233,6 +251,7 @@ void SavePreset(std::string& presetName) { fs::create_directory(presetFolder); } presets[presetName].presetValues["presetName"] = presetName; + presets[presetName].presetValues["fileType"] = FILE_TYPE_PRESET; std::ofstream file( fmt::format("{}/{}.json", Ship::Context::GetInstance()->LocateFileAcrossAppDirs("presets"), presetName)); file << presets[presetName].presetValues.dump(4); diff --git a/soh/soh/Enhancements/Autosave.cpp b/soh/soh/Enhancements/QoL/Autosave.cpp similarity index 72% rename from soh/soh/Enhancements/Autosave.cpp rename to soh/soh/Enhancements/QoL/Autosave.cpp index faa5ee8dd..49ab2996d 100644 --- a/soh/soh/Enhancements/Autosave.cpp +++ b/soh/soh/Enhancements/QoL/Autosave.cpp @@ -7,6 +7,7 @@ extern "C" { extern PlayState* gPlayState; #include "functions.h" +#include "macros.h" #include "variables.h" } @@ -15,27 +16,31 @@ static uint64_t lastSaveTimestamp = GetUnixTimestamp(); #define CVAR_AUTOSAVE_NAME CVAR_ENHANCEMENT("Autosave") #define CVAR_AUTOSAVE_DEFAULT AUTOSAVE_OFF #define CVAR_AUTOSAVE_VALUE CVarGetInteger(CVAR_AUTOSAVE_NAME, CVAR_AUTOSAVE_DEFAULT) -#define THREE_MINUTES_IN_UNIX 3 * 60000 +static constexpr uint64_t THREE_MINUTES_IN_UNIX = 3 * 60000; typedef enum { AUTOSAVE_OFF, AUTOSAVE_ON, } AutosaveOptions; -bool Autosave_CanSave() { +static bool Autosave_CanSave() { // Don't save when in title screen or debug file + // Don't save a file that doesn't exist (e.g. it was deleted on death by user option) // Don't save the first 60 frames to not save the magic meter when it's still in the animation of filling it. // Don't save in Chamber of Sages and the Cutscene map because of remember save location and cutscene item gives. - if (!GameInteractor::IsSaveLoaded(false) || gPlayState->gameplayFrames < 60 || - gPlayState->sceneNum == SCENE_CHAMBER_OF_THE_SAGES || gPlayState->sceneNum == SCENE_CUTSCENE_MAP) { + // Don't save between obtaining Ocarina of Time and Song of Time because the latter would become unobtainable. + if (!SaveManager::Instance->SaveFile_Exist(gSaveContext.fileNum) || !GameInteractor::IsSaveLoaded(false) || + gPlayState->gameplayFrames < 60 || gPlayState->sceneNum == SCENE_CHAMBER_OF_THE_SAGES || + gPlayState->sceneNum == SCENE_CUTSCENE_MAP || + (!CHECK_QUEST_ITEM(QUEST_SONG_TIME) && (INV_CONTENT(ITEM_OCARINA_TIME) == ITEM_OCARINA_TIME))) { return false; } return true; } -void Autosave_PerformSave() { +static void Autosave_PerformSave() { Play_PerformSave(gPlayState); // Send notification @@ -44,7 +49,7 @@ void Autosave_PerformSave() { }); } -void Autosave_IntervalSave() { +static void Autosave_IntervalSave() { // Check if the interval has passed in minutes. uint64_t currentTimestamp = GetUnixTimestamp(); if ((currentTimestamp - lastSaveTimestamp) < THREE_MINUTES_IN_UNIX) { @@ -64,13 +69,13 @@ void Autosave_IntervalSave() { } } -void Autosave_SoftResetSave() { +static void Autosave_SoftResetSave() { if (Autosave_CanSave()) { Autosave_PerformSave(); } } -void RegisterAutosave() { +static void RegisterAutosave() { COND_HOOK(GameInteractor::OnGameFrameUpdate, CVAR_AUTOSAVE_VALUE, Autosave_IntervalSave); COND_HOOK(GameInteractor::OnExitGame, CVAR_AUTOSAVE_VALUE, [](int32_t fileNum) { Autosave_SoftResetSave(); }); } diff --git a/soh/soh/Enhancements/TimeSavers/DampeAllNight.cpp b/soh/soh/Enhancements/TimeSavers/DampeAllNight.cpp new file mode 100644 index 000000000..683e7fbca --- /dev/null +++ b/soh/soh/Enhancements/TimeSavers/DampeAllNight.cpp @@ -0,0 +1,37 @@ +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/OTRGlobals.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "src/overlays/actors/ovl_En_Door/z_en_door.h" +extern SaveContext gSaveContext; +extern PlayState* gPlayState; +} + +static constexpr int32_t CVAR_DAMPE_ALL_NIGHT_DEFAULT = 0; +#define CVAR_DAMPE_ALL_NIGHT_NAME CVAR_ENHANCEMENT("DampeAllNight") +#define CVAR_DAMPE_ALL_NIGHT_VALUE CVarGetInteger(CVAR_DAMPE_ALL_NIGHT_NAME, CVAR_DAMPE_ALL_NIGHT_DEFAULT) + +static constexpr s16 DAMPE_HUT_DOOR_OPEN = 447; +static constexpr s16 DAMPE_HUT_DOOR_CLOSED = 774; + +static bool DampeIsResting() { + return LINK_IS_ADULT || gPlayState->sceneNum != SCENE_GRAVEYARD; +} + +static void OpenDampeHutDoor(void* refActor) { + EnDoor* enDoor = static_cast(refActor); + s16* params = &enDoor->actor.params; + + if (*params == DAMPE_HUT_DOOR_CLOSED && !DampeIsResting()) { + *params = DAMPE_HUT_DOOR_OPEN; + EnDoor_SetupType(enDoor, gPlayState); + } +} + +static void RegisterDampeAllNight() { + COND_VB_SHOULD(VB_DAMPE_IN_GRAVEYARD_DESPAWN, CVAR_DAMPE_ALL_NIGHT_VALUE, { *should = DampeIsResting(); }); + COND_ID_HOOK(OnActorInit, ACTOR_EN_DOOR, CVAR_DAMPE_ALL_NIGHT_VALUE, OpenDampeHutDoor); +} + +static RegisterShipInitFunc initFunc(RegisterDampeAllNight, { CVAR_DAMPE_ALL_NIGHT_NAME }); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h index f2579db60..c70e81f09 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h @@ -13,6 +13,7 @@ DEFINE_HOOK(OnExitGame, (int32_t fileNum)); DEFINE_HOOK(OnGameStateMainStart, ()); DEFINE_HOOK(OnGameFrameUpdate, ()); DEFINE_HOOK(OnItemReceive, (GetItemEntry itemEntry)); +DEFINE_HOOK(OnEquipmentDelete, (int16_t equipmentType, uint16_t equipValue)); DEFINE_HOOK(OnSaleEnd, (GetItemEntry itemEntry)); DEFINE_HOOK(OnTransitionEnd, (int16_t sceneNum)); DEFINE_HOOK(OnSceneInit, (int16_t sceneNum)); @@ -25,11 +26,15 @@ DEFINE_HOOK(OnSceneSpawnActors, ()); DEFINE_HOOK(OnPlayerUpdate, ()); DEFINE_HOOK(OnPlayerDeath, ()); DEFINE_HOOK(OnSetDoAction, (uint16_t action)); +DEFINE_HOOK(OnPlayerSfx, (u16 sfxId)); DEFINE_HOOK(OnOcarinaSongAction, ()); DEFINE_HOOK(OnCuccoOrChickenHatch, ()); DEFINE_HOOK(OnShopSlotChange, (uint8_t cursorIndex, int16_t price)); +DEFINE_HOOK(OnDungeonKeyUsed, (uint16_t mapIndex)); +DEFINE_HOOK(ShouldActorInit, (void* actor, bool* result)); DEFINE_HOOK(OnActorInit, (void* actor)); DEFINE_HOOK(OnActorSpawn, (void* actor)); +DEFINE_HOOK(ShouldActorUpdate, (void* actor, bool* result)); DEFINE_HOOK(OnActorUpdate, (void* actor)); DEFINE_HOOK(OnActorKill, (void* actor)); DEFINE_HOOK(OnActorDestroy, (void* actor)); @@ -47,7 +52,7 @@ DEFINE_HOOK(OnPlayDestroy, ()); DEFINE_HOOK(OnPlayDrawBegin, ()); DEFINE_HOOK(OnPlayDrawEnd, ()); DEFINE_HOOK(OnVanillaBehavior, (GIVanillaBehavior flag, bool* result, va_list originalArgs)); -DEFINE_HOOK(OnSaveFile, (int32_t fileNum)); +DEFINE_HOOK(OnSaveFile, (int32_t fileNum, int32_t sectionID)); DEFINE_HOOK(OnLoadFile, (int32_t fileNum)); DEFINE_HOOK(OnDeleteFile, (int32_t fileNum)); @@ -83,3 +88,8 @@ DEFINE_HOOK(OnRandomizerExternalCheck, (uint32_t rc)); // Audio DEFINE_HOOK(OnSeqPlayerInit, (int32_t playerIdx, int32_t seqId)); + +// Rando +DEFINE_HOOK(OnRandoSetCheckStatus, (RandomizerCheck rc, RandomizerCheckStatus status)); +DEFINE_HOOK(OnRandoSetIsSkipped, (RandomizerCheck rc, bool isSkipped)); +DEFINE_HOOK(OnRandoEntranceDiscovered, (u16 entranceIndex, u8 isReversedEntrance)); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp index 0e9e0c58e..a6ae783c9 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp @@ -38,6 +38,11 @@ void GameInteractor_ExecuteOnItemReceiveHooks(GetItemEntry itemEntry) { GameInteractor::Instance->ExecuteHooksForFilter(itemEntry); } +void GameInteractor_ExecuteOnEquipmentDelete(int16_t equipmentType, uint16_t equipValue) { + GameInteractor::Instance->ExecuteHooks(equipmentType, equipValue); + GameInteractor::Instance->ExecuteHooksForFilter(equipmentType, equipValue); +} + void GameInteractor_ExecuteOnSaleEndHooks(GetItemEntry itemEntry) { GameInteractor::Instance->ExecuteHooks(itemEntry); GameInteractor::Instance->ExecuteHooksForFilter(itemEntry); @@ -97,6 +102,10 @@ void GameInteractor_ExecuteOnSetDoAction(uint16_t action) { GameInteractor::Instance->ExecuteHooks(action); } +void GameInteractor_ExecuteOnPlayerSfx(u16 sfxId) { + GameInteractor::Instance->ExecuteHooks(sfxId); +} + void GameInteractor_ExecuteOnOcarinaSongAction() { GameInteractor::Instance->ExecuteHooks(); } @@ -109,6 +118,19 @@ void GameInteractor_ExecuteOnShopSlotChangeHooks(uint8_t cursorIndex, int16_t pr GameInteractor::Instance->ExecuteHooks(cursorIndex, price); } +void GameInteractor_ExecuteOnDungeonKeyUsedHooks(uint16_t mapIndex) { + GameInteractor::Instance->ExecuteHooks(mapIndex); +} + +bool GameInteractor_ShouldActorInit(void* actor) { + bool result = true; + GameInteractor::Instance->ExecuteHooks(actor, &result); + GameInteractor::Instance->ExecuteHooksForID(((Actor*)actor)->id, actor, &result); + GameInteractor::Instance->ExecuteHooksForPtr((uintptr_t)actor, actor, &result); + GameInteractor::Instance->ExecuteHooksForFilter(actor, &result); + return result; +} + void GameInteractor_ExecuteOnActorInit(void* actor) { GameInteractor::Instance->ExecuteHooks(actor); GameInteractor::Instance->ExecuteHooksForID(((Actor*)actor)->id, actor); @@ -123,6 +145,15 @@ void GameInteractor_ExecuteOnActorSpawn(void* actor) { GameInteractor::Instance->ExecuteHooksForFilter(actor); } +bool GameInteractor_ShouldActorUpdate(void* actor) { + bool result = true; + GameInteractor::Instance->ExecuteHooks(actor, &result); + GameInteractor::Instance->ExecuteHooksForID(((Actor*)actor)->id, actor, &result); + GameInteractor::Instance->ExecuteHooksForPtr((uintptr_t)actor, actor, &result); + GameInteractor::Instance->ExecuteHooksForFilter(actor, &result); + return result; +} + void GameInteractor_ExecuteOnActorUpdate(void* actor) { GameInteractor::Instance->ExecuteHooks(actor); GameInteractor::Instance->ExecuteHooksForID(((Actor*)actor)->id, actor); @@ -225,8 +256,8 @@ bool GameInteractor_Should(GIVanillaBehavior flag, u32 result, ...) { // MARK: - Save Files -void GameInteractor_ExecuteOnSaveFile(int32_t fileNum) { - GameInteractor::Instance->ExecuteHooks(fileNum); +void GameInteractor_ExecuteOnSaveFile(int32_t fileNum, int32_t sectionID) { + GameInteractor::Instance->ExecuteHooks(fileNum, sectionID); } void GameInteractor_ExecuteOnLoadFile(int32_t fileNum) { @@ -354,3 +385,9 @@ void GameInteractor_ExecuteOnRandomizerExternalCheck(uint32_t rc) { void GameInteractor_ExecuteOnSeqPlayerInit(int32_t playerIdx, int32_t seqId) { GameInteractor::Instance->ExecuteHooks(playerIdx, seqId); } + +// MARK: - Rando +void GameInteractor_ExecuteOnRandoEntranceDiscovered(u16 entranceIndex, u8 isReversedEntrance) { + GameInteractor::Instance->ExecuteHooks(entranceIndex, + isReversedEntrance); +} diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h index 8700d89ab..8ec76b12f 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h @@ -16,6 +16,7 @@ void GameInteractor_ExecuteOnExitGame(int32_t fileNum); void GameInteractor_ExecuteOnGameStateMainStart(); void GameInteractor_ExecuteOnGameFrameUpdate(); void GameInteractor_ExecuteOnItemReceiveHooks(GetItemEntry itemEntry); +void GameInteractor_ExecuteOnEquipmentDelete(int16_t equipmentType, uint16_t equipValue); void GameInteractor_ExecuteOnSaleEndHooks(GetItemEntry itemEntry); void GameInteractor_ExecuteOnTransitionEndHooks(int16_t sceneNum); void GameInteractor_ExecuteOnSceneInit(int16_t sceneNum); @@ -28,10 +29,13 @@ void GameInteractor_ExecuteOnSceneSpawnActors(); void GameInteractor_ExecuteOnPlayerUpdate(); void GameInteractor_ExecuteOnPlayerDeath(); void GameInteractor_ExecuteOnSetDoAction(uint16_t action); +void GameInteractor_ExecuteOnPlayerSfx(u16 sfxId); void GameInteractor_ExecuteOnOcarinaSongAction(); void GameInteractor_ExecuteOnCuccoOrChickenHatch(); +bool GameInteractor_ShouldActorInit(void* actor); void GameInteractor_ExecuteOnActorInit(void* actor); void GameInteractor_ExecuteOnActorSpawn(void* actor); +bool GameInteractor_ShouldActorUpdate(void* actor); void GameInteractor_ExecuteOnActorUpdate(void* actor); void GameInteractor_ExecuteOnActorKill(void* actor); void GameInteractor_ExecuteOnActorDestroy(void* actor); @@ -46,13 +50,14 @@ void GameInteractor_ExecuteOnPlayerFirstPersonControl(Player* player); void GameInteractor_ExecuteOnPlayerShieldControl(float_t* sp50, float_t* sp54); void GameInteractor_ExecuteOnPlayerProcessStick(); void GameInteractor_ExecuteOnShopSlotChangeHooks(uint8_t cursorIndex, int16_t price); +void GameInteractor_ExecuteOnDungeonKeyUsedHooks(uint16_t mapIndex); void GameInteractor_ExecuteOnPlayDestroy(); void GameInteractor_ExecuteOnPlayDrawBegin(); void GameInteractor_ExecuteOnPlayDrawEnd(); bool GameInteractor_Should(GIVanillaBehavior flag, uint32_t result, ...); // MARK: - Save Files -void GameInteractor_ExecuteOnSaveFile(int32_t fileNum); +void GameInteractor_ExecuteOnSaveFile(int32_t fileNum, int32_t sectionID); void GameInteractor_ExecuteOnLoadFile(int32_t fileNum); void GameInteractor_ExecuteOnDeleteFile(int32_t fileNum); @@ -98,6 +103,9 @@ void GameInteractor_ExecuteOnRandomizerExternalCheck(uint32_t rc); // Mark: - Audio void GameInteractor_ExecuteOnSeqPlayerInit(int32_t playerIdx, int32_t seqId); +// MARK: - Rando +void GameInteractor_ExecuteOnRandoEntranceDiscovered(u16 entranceIndex, u8 isReversedEntrance); + #ifdef __cplusplus } #endif diff --git a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h index 08d663c7e..fe437907a 100644 --- a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h +++ b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h @@ -2318,6 +2318,23 @@ typedef enum { // - `*Player` VB_SET_STATIC_FLOOR_TYPE, + // #### `result` + // ```c + // (this->collider.base.acFlags & AC_HIT) && !Player_InCsMode(play) && + // (player->meleeWeaponAnimation == 22 || player->meleeWeaponAnimation == 23) + // ``` + // #### `args` + // - `*BgHidanDalm` + VB_HAMMER_TOTEM_BREAK, + + // #### `result` + // ```c + // Actor_GetCollidedExplosive(play, &this->collider.base) != NULL + // ``` + // #### `args` + // - `*BgHidanKowarerukabe` + VB_FIRE_TEMPLE_BOMBABLE_WALL_BREAK, + } GIVanillaBehavior; #endif diff --git a/soh/soh/Enhancements/mod_menu.cpp b/soh/soh/Enhancements/mod_menu.cpp index 9e3fda114..e60eee7e9 100644 --- a/soh/soh/Enhancements/mod_menu.cpp +++ b/soh/soh/Enhancements/mod_menu.cpp @@ -55,6 +55,7 @@ void SetEnabledModsCVarValue() { } CVarSetString(CVAR_ENABLED_MODS_NAME, s.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); } void AfterModChange() { @@ -128,8 +129,9 @@ void UpdateModFiles(bool init = false, bool reset = false) { disabledModFiles.clear(); unsupportedFiles.clear(); filePaths.clear(); - std::string modsPath = Ship::Context::LocateFileAcrossAppDirs("mods", appShortName); bool changed = false; + std::string modsPath = Ship::Context::LocateFileAcrossAppDirs("mods", appShortName); + std::map tempMods; if (modsPath.length() > 0 && std::filesystem::exists(modsPath)) { std::vector enabledFiles; if (std::filesystem::is_directory(modsPath)) { @@ -147,11 +149,17 @@ void UpdateModFiles(bool init = false, bool reset = false) { bool enabled = std::find(enabledModFiles.begin(), enabledModFiles.end(), filename) != enabledModFiles.end(); if (!enabled) { - enabledModFiles.push_back(filename); - changed = true; + tempMods.emplace(p.path().lexically_normal().generic_string(), filename); } filePaths.emplace(filename, p.path()); } + if (tempMods.size() > 0) { + changed = true; + for (auto [path, name] : tempMods) { + enabledModFiles.push_back(name); + } + tempMods.clear(); + } if (init) { std::vector enabledTemp(enabledModFiles); for (std::string mod : enabledTemp) { @@ -159,13 +167,14 @@ void UpdateModFiles(bool init = false, bool reset = false) { GetArchiveManager()->AddArchive(filePaths.at(mod).generic_string()); } else { enabledModFiles.erase(std::find(enabledModFiles.begin(), enabledModFiles.end(), mod)); + changed = true; } } } } - } - if (changed) { - SetEnabledModsCVarValue(); + if (changed) { + SetEnabledModsCVarValue(); + } } } diff --git a/soh/soh/Enhancements/mods.cpp b/soh/soh/Enhancements/mods.cpp index dc1961530..93b4ce79e 100644 --- a/soh/soh/Enhancements/mods.cpp +++ b/soh/soh/Enhancements/mods.cpp @@ -2,14 +2,9 @@ #include #include "game-interactor/GameInteractor.h" #include "tts/tts.h" -#include "soh/OTRGlobals.h" -#include "soh/SaveManager.h" #include "soh/ResourceManagerHelpers.h" -#include "soh/resource/type/Skeleton.h" #include "soh/Enhancements/boss-rush/BossRush.h" #include "soh/Enhancements/enhancementTypes.h" -#include "soh/Enhancements/randomizer/3drando/random.hpp" -#include "soh/Enhancements/cosmetics/authenticGfxPatches.h" #include #include "soh/Enhancements/timesaver_hook_handlers.h" #include "soh/Enhancements/randomizer/hook_handlers.h" @@ -27,7 +22,6 @@ #include "src/overlays/actors/ovl_En_Tp/z_en_tp.h" #include "src/overlays/actors/ovl_En_Firefly/z_en_firefly.h" #include "src/overlays/actors/ovl_En_Xc/z_en_xc.h" -#include "src/overlays/actors/ovl_Fishing/z_fishing.h" #include "src/overlays/actors/ovl_Door_Shutter/z_door_shutter.h" #include "src/overlays/actors/ovl_Door_Gerudo/z_door_gerudo.h" #include "src/overlays/actors/ovl_En_Elf/z_en_elf.h" @@ -35,7 +29,6 @@ #include "objects/object_link_child/object_link_child.h" #include "soh_assets.h" #include "kaleido.h" -#include "soh/Network/Archipelago/Archipelago.h" extern "C" { #include @@ -144,58 +137,6 @@ void RegisterOcarinaTimeTravel() { }); } -static bool hasAffectedHealth = false; -void UpdatePermanentHeartLossState() { - if (!GameInteractor::IsSaveLoaded()) - return; - - if (!CVarGetInteger(CVAR_ENHANCEMENT("PermanentHeartLoss"), 0) && hasAffectedHealth) { - uint8_t heartContainers = gSaveContext.ship.stats.heartContainers; // each worth 16 health - uint8_t heartPieces = gSaveContext.ship.stats.heartPieces; // each worth 4 health, but only in groups of 4 - uint8_t startingHealth = - 16 * (IS_RANDO ? (OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_STARTING_HEARTS) + 1) : 3); - - uint8_t newCapacity = startingHealth + (heartContainers * 16) + ((heartPieces - (heartPieces % 4)) * 4); - gSaveContext.healthCapacity = MAX(newCapacity, gSaveContext.healthCapacity); - gSaveContext.health = MIN(gSaveContext.health, gSaveContext.healthCapacity); - hasAffectedHealth = false; - } -} - -void RegisterPermanentHeartLoss() { - GameInteractor::Instance->RegisterGameHook([](int16_t fileNum) { - hasAffectedHealth = false; - UpdatePermanentHeartLossState(); - }); - - GameInteractor::Instance->RegisterGameHook([]() { - if (!CVarGetInteger(CVAR_ENHANCEMENT("PermanentHeartLoss"), 0) || !GameInteractor::IsSaveLoaded()) - return; - - if (gSaveContext.healthCapacity > 16 && gSaveContext.healthCapacity - gSaveContext.health >= 16) { - gSaveContext.healthCapacity -= 16; - gSaveContext.health = MIN(gSaveContext.health, gSaveContext.healthCapacity); - hasAffectedHealth = true; - } - }); -}; - -void RegisterDeleteFileOnDeath() { - GameInteractor::Instance->RegisterGameHook([]() { - if (!CVarGetInteger(CVAR_ENHANCEMENT("DeleteFileOnDeath"), 0) || !GameInteractor::IsSaveLoaded() || - gPlayState == NULL) - return; - - if (gPlayState->gameOverCtx.state == GAMEOVER_DEATH_MENU && gPlayState->pauseCtx.state == 9) { - SaveManager::Instance->DeleteZeldaFile(gSaveContext.fileNum); - hasAffectedHealth = false; - std::reinterpret_pointer_cast( - Ship::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console")) - ->Dispatch("reset"); - } - }); -} - bool IsHyperBossesActive() { return CVarGetInteger(CVAR_ENHANCEMENT("HyperBosses"), 0) || (IS_BOSS_RUSH && @@ -287,51 +228,6 @@ void UpdateHyperEnemiesState() { } } -void UpdateMirrorModeState(int32_t sceneNum) { - static bool prevMirroredWorld = false; - bool nextMirroredWorld = false; - - int16_t mirroredMode = CVarGetInteger(CVAR_ENHANCEMENT("MirroredWorldMode"), MIRRORED_WORLD_OFF); - int16_t inDungeon = (sceneNum >= SCENE_DEKU_TREE && sceneNum <= SCENE_INSIDE_GANONS_CASTLE_COLLAPSE && - sceneNum != SCENE_THIEVES_HIDEOUT) || - (sceneNum >= SCENE_DEKU_TREE_BOSS && sceneNum <= SCENE_GANONS_TOWER_COLLAPSE_EXTERIOR) || - (sceneNum == SCENE_GANON_BOSS); - - if (mirroredMode == MIRRORED_WORLD_RANDOM_SEEDED || mirroredMode == MIRRORED_WORLD_DUNGEONS_RANDOM_SEEDED) { - uint32_t seed = - sceneNum + (IS_RANDO ? Rando::Context::GetInstance()->GetSeed() : gSaveContext.ship.stats.fileCreatedAt); - Random_Init(seed); - } - - bool randomMirror = Random(0, 2) == 1; - - if (mirroredMode == MIRRORED_WORLD_ALWAYS || - ((mirroredMode == MIRRORED_WORLD_RANDOM || mirroredMode == MIRRORED_WORLD_RANDOM_SEEDED) && randomMirror) || - // Dungeon modes - (inDungeon && - (mirroredMode == MIRRORED_WORLD_DUNGEONS_ALL || - (mirroredMode == MIRRORED_WORLD_DUNGEONS_VANILLA && !ResourceMgr_IsSceneMasterQuest(sceneNum)) || - (mirroredMode == MIRRORED_WORLD_DUNGEONS_MQ && ResourceMgr_IsSceneMasterQuest(sceneNum)) || - ((mirroredMode == MIRRORED_WORLD_DUNGEONS_RANDOM || mirroredMode == MIRRORED_WORLD_DUNGEONS_RANDOM_SEEDED) && - randomMirror)))) { - nextMirroredWorld = true; - CVarSetInteger(CVAR_ENHANCEMENT("MirroredWorld"), 1); - } else { - nextMirroredWorld = false; - CVarClear(CVAR_ENHANCEMENT("MirroredWorld")); - } - - if (prevMirroredWorld != nextMirroredWorld) { - prevMirroredWorld = nextMirroredWorld; - ApplyMirrorWorldGfxPatches(); - } -} - -void RegisterMirrorModeHandler() { - GameInteractor::Instance->RegisterGameHook( - [](int32_t sceneNum) { UpdateMirrorModeState(sceneNum); }); -} - void UpdatePatchHand() { if ((CVarGetInteger(CVAR_ENHANCEMENT("EquipmentAlwaysVisible"), 0)) && LINK_IS_CHILD) { ResourceMgr_PatchGfxByName(gLinkAdultLeftHandHoldingHammerNearDL, "childHammer1", 92, @@ -590,46 +486,6 @@ void RegisterEnemyDefeatCounts() { }); } -void RegisterBossDefeatTimestamps() { - GameInteractor::Instance->RegisterGameHook([](void* refActor) { - Actor* actor = static_cast(refActor); - switch (actor->id) { - case ACTOR_BOSS_DODONGO: - gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_KING_DODONGO] = GAMEPLAYSTAT_TOTAL_TIME; - break; - case ACTOR_BOSS_FD2: - gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_VOLVAGIA] = GAMEPLAYSTAT_TOTAL_TIME; - break; - case ACTOR_BOSS_GANON: - gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_GANONDORF] = GAMEPLAYSTAT_TOTAL_TIME; - break; - case ACTOR_BOSS_GANON2: - gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_GANON] = GAMEPLAYSTAT_TOTAL_TIME; - gSaveContext.ship.stats.gameComplete = true; - ArchipelagoClient::GetInstance().SendGameWon(); - break; - case ACTOR_BOSS_GANONDROF: - gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_PHANTOM_GANON] = GAMEPLAYSTAT_TOTAL_TIME; - break; - case ACTOR_BOSS_GOMA: - gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_GOHMA] = GAMEPLAYSTAT_TOTAL_TIME; - break; - case ACTOR_BOSS_MO: - gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_MORPHA] = GAMEPLAYSTAT_TOTAL_TIME; - break; - case ACTOR_BOSS_SST: - gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_BONGO_BONGO] = GAMEPLAYSTAT_TOTAL_TIME; - break; - case ACTOR_BOSS_TW: - gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_TWINROVA] = GAMEPLAYSTAT_TOTAL_TIME; - break; - case ACTOR_BOSS_VA: - gSaveContext.ship.stats.itemTimestamp[TIMESTAMP_DEFEAT_BARINADE] = GAMEPLAYSTAT_TOTAL_TIME; - break; - } - }); -} - void UpdateHurtContainerModeState(bool newState) { static bool hurtEnabled = false; if (hurtEnabled == newState) { @@ -706,44 +562,16 @@ void RegisterRandomizedEnemySizes() { }); } -void RegisterCustomSkeletons() { - static int8_t previousTunic = -1; - - GameInteractor::Instance->RegisterGameHook([]() { - if (!GameInteractor::IsSaveLoaded() || gPlayState == NULL) { - return; - } - - if (CUR_EQUIP_VALUE(EQUIP_TYPE_TUNIC) != previousTunic) { - SOH::SkeletonPatcher::UpdateCustomSkeletons(); - } - previousTunic = CUR_EQUIP_VALUE(EQUIP_TYPE_TUNIC); - }); - - GameInteractor::Instance->RegisterGameHook([]() { - if (!GameInteractor::IsSaveLoaded() || gPlayState == NULL) { - return; - } - - SOH::SkeletonPatcher::UpdateCustomSkeletons(); - }); -} - void InitMods() { RandomizerRegisterHooks(); TimeSaverRegisterHooks(); RegisterTTS(); RegisterOcarinaTimeTravel(); - RegisterPermanentHeartLoss(); - RegisterDeleteFileOnDeath(); RegisterHyperBosses(); UpdateHyperEnemiesState(); - RegisterMirrorModeHandler(); RegisterEnemyDefeatCounts(); - RegisterBossDefeatTimestamps(); RegisterRandomizedEnemySizes(); RegisterPatchHandHandler(); RegisterHurtContainerModeHandler(); RandoKaleido_RegisterHooks(); - RegisterCustomSkeletons(); } diff --git a/soh/soh/Enhancements/randomizer/3drando/fill.cpp b/soh/soh/Enhancements/randomizer/3drando/fill.cpp index 3204ace65..99deb9b70 100644 --- a/soh/soh/Enhancements/randomizer/3drando/fill.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/fill.cpp @@ -1018,7 +1018,7 @@ static void FillExcludedLocations() { FilterFromPool(ctx->allLocations, [ctx](const auto loc) { return ctx->GetItemLocation(loc)->IsExcluded(); }); for (RandomizerCheck loc : excludedLocations) { - PlaceJunkInExcludedLocation(loc); + ctx->PlaceItemInLocation(loc, GetJunkItem()); } } diff --git a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp index e8cab933f..ea6123b24 100644 --- a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp @@ -350,19 +350,6 @@ static void ReplaceMaxItem(const RandomizerGet itemToReplace, int max) { } } -void PlaceJunkInExcludedLocation(const RandomizerCheck il) { - // place a non-advancement item in this location - auto ctx = Rando::Context::GetInstance(); - for (size_t i = 0; i < ItemPool.size(); i++) { - if (Rando::StaticData::RetrieveItem(ItemPool[i]).GetCategory() == ITEM_CATEGORY_JUNK) { - ctx->PlaceItemInLocation(il, ItemPool[i]); - ItemPool.erase(ItemPool.begin() + i); - return; - } - } - SPDLOG_ERROR("ERROR: No Junk to Place!!!"); -} - static void PlaceVanillaMapsAndCompasses() { auto ctx = Rando::Context::GetInstance(); for (auto dungeon : ctx->GetDungeons()->GetDungeonList()) { diff --git a/soh/soh/Enhancements/randomizer/3drando/item_pool.hpp b/soh/soh/Enhancements/randomizer/3drando/item_pool.hpp index 095b417b4..a437099c8 100644 --- a/soh/soh/Enhancements/randomizer/3drando/item_pool.hpp +++ b/soh/soh/Enhancements/randomizer/3drando/item_pool.hpp @@ -9,7 +9,6 @@ class ItemLocation; void AddItemToPool(std::vector& pool, const RandomizerGet item, size_t count = 1); RandomizerGet GetJunkItem(); -void PlaceJunkInExcludedLocation(const RandomizerCheck il); void GenerateItemPool(); extern std::vector ItemPool; diff --git a/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp b/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp index b3e71e8f0..85225e2b1 100644 --- a/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/spoiler_log.cpp @@ -343,6 +343,7 @@ const char* SpoilerLog_Write() { jsonData.clear(); jsonData["version"] = (char*)gBuildVersion; + jsonData["fileType"] = FILE_TYPE_SPOILER; jsonData["git_branch"] = (char*)gGitBranch; jsonData["git_commit"] = (char*)gGitCommitHash; jsonData["seed"] = ctx->GetSeedString(); diff --git a/soh/soh/Enhancements/randomizer/context.cpp b/soh/soh/Enhancements/randomizer/context.cpp index 2327086a0..9519d2ef7 100644 --- a/soh/soh/Enhancements/randomizer/context.cpp +++ b/soh/soh/Enhancements/randomizer/context.cpp @@ -188,7 +188,6 @@ void Context::GenerateLocationPool() { location.GetRandomizerCheck() == RC_LW_DEKU_SCRUB_NEAR_BRIDGE || location.GetRandomizerCheck() == RC_HF_DEKU_SCRUB_GROTTO)) || (location.GetRCType() == RCTYPE_ADULT_TRADE && mOptions[RSK_SHUFFLE_ADULT_TRADE].Is(RO_GENERIC_OFF)) || - (location.GetRCType() == RCTYPE_SONG_LOCATION && mOptions[RSK_SHUFFLE_SONGS].Is(RO_SONG_SHUFFLE_OFF)) || (location.GetRCType() == RCTYPE_COW && mOptions[RSK_SHUFFLE_COWS].Is(RO_GENERIC_OFF)) || (location.GetRandomizerCheck() == RC_LH_HYRULE_LOACH && mOptions[RSK_FISHSANITY].IsNot(RO_FISHSANITY_HYRULE_LOACH)) || diff --git a/soh/soh/Enhancements/randomizer/entrance.cpp b/soh/soh/Enhancements/randomizer/entrance.cpp index d96eb8045..6ae7e993b 100644 --- a/soh/soh/Enhancements/randomizer/entrance.cpp +++ b/soh/soh/Enhancements/randomizer/entrance.cpp @@ -269,8 +269,8 @@ void SetAllEntrancesData() { { EntranceType::Dungeon, RR_SPIRIT_TEMPLE_ENTRYWAY, RR_DESERT_COLOSSUS_OUTSIDE_TEMPLE, ENTR_DESERT_COLOSSUS_OUTSIDE_TEMPLE } }, { { EntranceType::Dungeon, RR_GRAVEYARD_WARP_PAD_REGION, RR_SHADOW_TEMPLE_ENTRYWAY, ENTR_SHADOW_TEMPLE_ENTRANCE }, { EntranceType::Dungeon, RR_SHADOW_TEMPLE_ENTRYWAY, RR_GRAVEYARD_WARP_PAD_REGION, ENTR_GRAVEYARD_OUTSIDE_TEMPLE } }, - { { EntranceType::Dungeon, RR_KAK_WELL, RR_BOTTOM_OF_THE_WELL_ENTRYWAY, ENTR_BOTTOM_OF_THE_WELL_ENTRANCE }, - { EntranceType::Dungeon, RR_BOTTOM_OF_THE_WELL_ENTRYWAY, RR_KAK_WELL, ENTR_KAKARIKO_VILLAGE_OUTSIDE_BOTTOM_OF_THE_WELL } }, + { { EntranceType::Dungeon, RR_KAK_WELL, RR_BOTW_ENTRYWAY, ENTR_BOTTOM_OF_THE_WELL_ENTRANCE }, + { EntranceType::Dungeon, RR_BOTW_ENTRYWAY, RR_KAK_WELL, ENTR_KAKARIKO_VILLAGE_OUTSIDE_BOTTOM_OF_THE_WELL } }, { { EntranceType::Dungeon, RR_ZF_LEDGE, RR_ICE_CAVERN_ENTRYWAY, ENTR_ICE_CAVERN_ENTRANCE }, { EntranceType::Dungeon, RR_ICE_CAVERN_ENTRYWAY, RR_ZF_LEDGE, ENTR_ZORAS_FOUNTAIN_OUTSIDE_ICE_CAVERN } }, { { EntranceType::Dungeon, RR_GF_TO_GTG, RR_GERUDO_TRAINING_GROUND_ENTRYWAY, ENTR_GERUDO_TRAINING_GROUND_ENTRANCE }, @@ -401,10 +401,10 @@ void SetAllEntrancesData() { { EntranceType::GrottoGrave, RR_LH_GROTTO, RR_LAKE_HYLIA, ENTRANCE_GROTTO_EXIT(GROTTO_LH_OFFSET) } }, { { EntranceType::GrottoGrave, RR_ZORAS_RIVER, RR_ZR_STORMS_GROTTO, ENTRANCE_GROTTO_LOAD(GROTTO_ZR_STORMS_OFFSET) }, { EntranceType::GrottoGrave, RR_ZR_STORMS_GROTTO, RR_ZORAS_RIVER, ENTRANCE_GROTTO_EXIT(GROTTO_ZR_STORMS_OFFSET) } }, - { { EntranceType::GrottoGrave, RR_ZORAS_RIVER, RR_ZR_FAIRY_GROTTO, ENTRANCE_GROTTO_LOAD(GROTTO_ZR_FAIRY_OFFSET) }, - { EntranceType::GrottoGrave, RR_ZR_FAIRY_GROTTO, RR_ZORAS_RIVER, ENTRANCE_GROTTO_EXIT(GROTTO_ZR_FAIRY_OFFSET) } }, - { { EntranceType::GrottoGrave, RR_ZORAS_RIVER, RR_ZR_OPEN_GROTTO, ENTRANCE_GROTTO_LOAD(GROTTO_ZR_OPEN_OFFSET) }, - { EntranceType::GrottoGrave, RR_ZR_OPEN_GROTTO, RR_ZORAS_RIVER, ENTRANCE_GROTTO_EXIT(GROTTO_ZR_OPEN_OFFSET) } }, + { { EntranceType::GrottoGrave, RR_ZR_ATOP_LADDER, RR_ZR_FAIRY_GROTTO, ENTRANCE_GROTTO_LOAD(GROTTO_ZR_FAIRY_OFFSET) }, + { EntranceType::GrottoGrave, RR_ZR_FAIRY_GROTTO, RR_ZR_ATOP_LADDER, ENTRANCE_GROTTO_EXIT(GROTTO_ZR_FAIRY_OFFSET) } }, + { { EntranceType::GrottoGrave, RR_ZR_ATOP_LADDER, RR_ZR_OPEN_GROTTO, ENTRANCE_GROTTO_LOAD(GROTTO_ZR_OPEN_OFFSET) }, + { EntranceType::GrottoGrave, RR_ZR_OPEN_GROTTO, RR_ZR_ATOP_LADDER, ENTRANCE_GROTTO_EXIT(GROTTO_ZR_OPEN_OFFSET) } }, { { EntranceType::GrottoGrave, RR_DMC_LOWER_NEARBY, RR_DMC_HAMMER_GROTTO, ENTRANCE_GROTTO_LOAD(GROTTO_DMC_HAMMER_OFFSET) }, { EntranceType::GrottoGrave, RR_DMC_HAMMER_GROTTO, RR_DMC_LOWER_LOCAL, ENTRANCE_GROTTO_EXIT(GROTTO_DMC_HAMMER_OFFSET) } }, { { EntranceType::GrottoGrave, RR_DMC_UPPER_NEARBY, RR_DMC_UPPER_GROTTO, ENTRANCE_GROTTO_LOAD(GROTTO_DMC_UPPER_OFFSET) }, @@ -670,8 +670,7 @@ std::vector EntranceShuffler::AssumeEntrancePool(std::vectorGetOption(RSK_SHUFFLE_OVERWORLD_ENTRANCES) || ctx->GetOption(RSK_SHUFFLE_INTERIOR_ENTRANCES).Is(RO_INTERIOR_ENTRANCE_SHUFFLE_ALL)))) { auto type = entrance->GetType(); - if (((type == EntranceType::Dungeon || type == EntranceType::ThievesHideout || - type == EntranceType::GrottoGrave) && + if (((type == EntranceType::Dungeon || type == EntranceType::GrottoGrave) && entrance->GetReverse()->GetName() != "Spirit Temple Entryway -> Desert Colossus From Spirit Entryway") || (type == EntranceType::Interior && @@ -796,10 +795,11 @@ static bool ValidateWorld(Entrance* entrancePlaced) { bool checkOtherEntranceAccess = (ctx->GetOption(RSK_SHUFFLE_OVERWORLD_ENTRANCES) || ctx->GetOption(RSK_SHUFFLE_INTERIOR_ENTRANCES).Is(RO_INTERIOR_ENTRANCE_SHUFFLE_ALL) || - ctx->GetOption(RSK_SHUFFLE_OVERWORLD_SPAWNS)) && + ctx->GetOption(RSK_SHUFFLE_THIEVES_HIDEOUT_ENTRANCES) || ctx->GetOption(RSK_SHUFFLE_OVERWORLD_SPAWNS)) && (entrancePlaced == nullptr || ctx->GetOption(RSK_MIXED_ENTRANCE_POOLS) || - type == EntranceType::SpecialInterior || type == EntranceType::Overworld || type == EntranceType::Spawn || - type == EntranceType::WarpSong || type == EntranceType::OwlDrop); + type == EntranceType::SpecialInterior || type == EntranceType::Overworld || + type == EntranceType::ThievesHideout || type == EntranceType::Spawn || type == EntranceType::WarpSong || + type == EntranceType::OwlDrop); // Search the world to verify that all necessary conditions are still being held // Conditions will be checked during the search and any that fail will be figured out @@ -1333,6 +1333,7 @@ int EntranceShuffler::ShuffleAllEntrances() { int totalMixedPools = (ctx->GetOption(RSK_MIX_DUNGEON_ENTRANCES) ? 1 : 0) + (ctx->GetOption(RSK_MIX_BOSS_ENTRANCES) ? 1 : 0) + (ctx->GetOption(RSK_MIX_OVERWORLD_ENTRANCES) ? 1 : 0) + (ctx->GetOption(RSK_MIX_INTERIOR_ENTRANCES) ? 1 : 0) + + (ctx->GetOption(RSK_MIX_THIEVES_HIDEOUT_ENTRANCES) ? 1 : 0) + (ctx->GetOption(RSK_MIX_GROTTO_ENTRANCES) ? 1 : 0); if (totalMixedPools < 2) { ctx->GetOption(RSK_MIXED_ENTRANCE_POOLS).Set(RO_GENERIC_OFF); @@ -1340,6 +1341,7 @@ int EntranceShuffler::ShuffleAllEntrances() { ctx->GetOption(RSK_MIX_BOSS_ENTRANCES).Set(RO_GENERIC_OFF); ctx->GetOption(RSK_MIX_OVERWORLD_ENTRANCES).Set(RO_GENERIC_OFF); ctx->GetOption(RSK_MIX_INTERIOR_ENTRANCES).Set(RO_GENERIC_OFF); + ctx->GetOption(RSK_MIX_THIEVES_HIDEOUT_ENTRANCES).Set(RO_GENERIC_OFF); ctx->GetOption(RSK_MIX_GROTTO_ENTRANCES).Set(RO_GENERIC_OFF); } if (ctx->GetOption(RSK_MIXED_ENTRANCE_POOLS)) { diff --git a/soh/soh/Enhancements/randomizer/hook_handlers.cpp b/soh/soh/Enhancements/randomizer/hook_handlers.cpp index 3d808f98a..16205424e 100644 --- a/soh/soh/Enhancements/randomizer/hook_handlers.cpp +++ b/soh/soh/Enhancements/randomizer/hook_handlers.cpp @@ -2294,7 +2294,8 @@ void RandomizerOnActorInitHandler(void* actorRef) { } // Turn MQ switch into toggle - if (actor->id == ACTOR_OBJ_SWITCH && gPlayState->sceneNum == SCENE_BOTTOM_OF_THE_WELL && (actor->params & 7) == 3) { + if (actor->id == ACTOR_OBJ_SWITCH && gPlayState->sceneNum == SCENE_BOTTOM_OF_THE_WELL && + (actor->params & 0x3f07) == 0x303) { auto dungeon = OTRGlobals::Instance->gRandoContext->GetDungeons()->GetDungeonFromScene(SCENE_BOTTOM_OF_THE_WELL); if (dungeon->IsMQ()) { diff --git a/soh/soh/Enhancements/randomizer/item_location.cpp b/soh/soh/Enhancements/randomizer/item_location.cpp index a5245129d..c0b3b862e 100644 --- a/soh/soh/Enhancements/randomizer/item_location.cpp +++ b/soh/soh/Enhancements/randomizer/item_location.cpp @@ -136,6 +136,7 @@ void ItemLocation::SetCheckStatus(RandomizerCheckStatus status_) { if (rc == RC_ARCHIPELAGO_RECEIVED_ITEM) // never count the AP receive trigger as 'collected' return; status = status_; + GameInteractor::Instance->ExecuteHooks(rc, status); } RandomizerCheckStatus ItemLocation::GetCheckStatus() { @@ -144,6 +145,7 @@ RandomizerCheckStatus ItemLocation::GetCheckStatus() { void ItemLocation::SetIsSkipped(bool isSkipped_) { isSkipped = isSkipped_; + GameInteractor::Instance->ExecuteHooks(rc, isSkipped); } bool ItemLocation::GetIsSkipped() { diff --git a/soh/soh/Enhancements/randomizer/location_access/dungeons/bottom_of_the_well.cpp b/soh/soh/Enhancements/randomizer/location_access/dungeons/bottom_of_the_well.cpp index 46a89f7af..62db2164a 100644 --- a/soh/soh/Enhancements/randomizer/location_access/dungeons/bottom_of_the_well.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/dungeons/bottom_of_the_well.cpp @@ -7,24 +7,34 @@ using namespace Rando; void RegionTable_Init_BottomOfTheWell() { // clang-format off // Vanilla/MQ Decider - areaTable[RR_BOTTOM_OF_THE_WELL_ENTRYWAY] = Region("Bottom of the Well Entryway", SCENE_BOTTOM_OF_THE_WELL, {}, {}, { + areaTable[RR_BOTW_ENTRYWAY] = Region("Bottom of the Well Entryway", SCENE_BOTTOM_OF_THE_WELL, {}, {}, { //Exits //Technically involves an fake wall, but passing it lensless is intended in vanilla and it is well telegraphed - Entrance(RR_BOTTOM_OF_THE_WELL_PERIMETER, []{return ctx->GetDungeon(Rando::BOTTOM_OF_THE_WELL)->IsVanilla() && logic->IsChild && logic->CanPassEnemy(RE_BIG_SKULLTULA);}), - Entrance(RR_BOTTOM_OF_THE_WELL_MQ_PERIMETER, []{return ctx->GetDungeon(Rando::BOTTOM_OF_THE_WELL)->IsMQ() && logic->IsChild;}), - Entrance(RR_KAK_WELL, []{return true;}), + //Backshot should be implemented here, or new regions should be added + Entrance(RR_BOTW_CORRIDOR, []{return ctx->GetDungeon(Rando::BOTTOM_OF_THE_WELL)->IsVanilla() && logic->IsChild/*CanCrawl*/;}), + Entrance(RR_BOTW_MQ_PERIMETER, []{return ctx->GetDungeon(Rando::BOTTOM_OF_THE_WELL)->IsMQ() && logic->IsChild/*CanCrawl*/;}), + Entrance(RR_KAK_WELL, []{return true;}), }); #pragma region Vanilla - areaTable[RR_BOTTOM_OF_THE_WELL_PERIMETER] = Region("Bottom of the Well Perimeter", SCENE_BOTTOM_OF_THE_WELL, { + areaTable[RR_BOTW_CORRIDOR] = Region("Bottom of the Well Corridor", SCENE_BOTTOM_OF_THE_WELL, {}, {}, { + //Exits + Entrance(RR_BOTW_ENTRYWAY, []{return logic->IsChild/*CanCrawl && CanClimb*/;}), + Entrance(RR_BOTW_PERIMETER, []{return logic->CanPassEnemy(RE_BIG_SKULLTULA);}), + }); + + areaTable[RR_BOTW_PERIMETER] = Region("Bottom of the Well Perimeter", SCENE_BOTTOM_OF_THE_WELL, { //Events EventAccess(LOGIC_STICK_POT, []{return true;}), EventAccess(LOGIC_NUT_POT, []{return true;}), EventAccess(LOGIC_BOTW_LOWERED_WATER, []{return logic->CanUse(RG_ZELDAS_LULLABY);}), }, { //Locations + LOCATION(RC_BOTTOM_OF_THE_WELL_FRONT_LEFT_FAKE_WALL_CHEST, ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH)), + LOCATION(RC_BOTTOM_OF_THE_WELL_RIGHT_BOTTOM_FAKE_WALL_CHEST, ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH)), LOCATION(RC_BOTTOM_OF_THE_WELL_FRONT_CENTER_BOMBABLE_CHEST, logic->HasExplosives()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BACK_LEFT_BOMBABLE_CHEST, logic->HasExplosives() && (ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH))), LOCATION(RC_BOTTOM_OF_THE_WELL_UNDERWATER_FRONT_CHEST, logic->Get(LOGIC_BOTW_LOWERED_WATER) || logic->CanOpenUnderwaterChest()), LOCATION(RC_BOTTOM_OF_THE_WELL_UNDERWATER_LEFT_CHEST, logic->Get(LOGIC_BOTW_LOWERED_WATER) || logic->CanOpenUnderwaterChest()), LOCATION(RC_BOTTOM_OF_THE_WELL_NEAR_ENTRANCE_POT_1, logic->CanBreakPots()), @@ -32,217 +42,361 @@ void RegionTable_Init_BottomOfTheWell() { LOCATION(RC_BOTTOM_OF_THE_WELL_UNDERWATER_POT, (logic->CanBreakPots() && logic->Get(LOGIC_BOTW_LOWERED_WATER)) || logic->CanUse(RG_BOOMERANG)), }, { //Exits - Entrance(RR_BOTTOM_OF_THE_WELL_ENTRYWAY, []{return logic->IsChild && logic->CanPassEnemy(RE_BIG_SKULLTULA);}), - Entrance(RR_BOTTOM_OF_THE_WELL_BEHIND_FAKE_WALLS, []{return ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH);}), - Entrance(RR_BOTTOM_OF_THE_WELL_SOUTHWEST_ROOM, []{return ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH);}), - Entrance(RR_BOTTOM_OF_THE_WELL_KEESE_BEAMOS_ROOM, []{return logic->IsChild && logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 3);}), - Entrance(RR_BOTTOM_OF_THE_WELL_COFFIN_ROOM, []{return logic->Get(LOGIC_BOTW_LOWERED_WATER) || logic->HasItem(RG_BRONZE_SCALE);}), - Entrance(RR_BOTTOM_OF_THE_WELL_DEAD_HAND_ROOM, []{return logic->Get(LOGIC_BOTW_LOWERED_WATER) && logic->IsChild;}), + Entrance(RR_BOTW_CORRIDOR, []{return logic->CanPassEnemy(RE_BIG_SKULLTULA);}), + Entrance(RR_BOTW_MIDDLE, []{return ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_BOTW_PIT_CAGE, []{return ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_BOTW_HIDDEN_POTS, []{return ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_BOTW_CORNER_CRAWLSPACE, []{return logic->IsChild/*CanCrawl*/;}), + //Climb always needed in case water is lowered out of logic + Entrance(RR_BOTW_BEHIND_MOAT, []{return (logic->Get(LOGIC_BOTW_LOWERED_WATER) || logic->HasItem(RG_BRONZE_SCALE) || + (logic->IsAdult && logic->CanUse(RG_IRON_BOOTS) && logic->CanUse(RG_HOOKSHOT))/*CanClimb*/);}), + Entrance(RR_BOTW_NEAR_BOSS_LOWER, []{return logic->Get(LOGIC_BOTW_LOWERED_WATER) && logic->IsChild/*CanCrawl*/;}), //Falling down into basement requires nothing, but falling down somewhere specific requires lens or lens trick //kinda questionable given several drops are blocked by rocks, but that's how it was handled before and on N64 - Entrance(RR_BOTTOM_OF_THE_WELL_BASEMENT, []{return true;}), + Entrance(RR_BOTW_B3_OOZE, []{return true;}), + Entrance(RR_BOTW_B3_BLOCKED_GRASS, []{return ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH);}), }); //This region combines the Middle with the perimeter's hidden areas. If a warp puts link into the middle without crossing the perimeter or using lens, it will need it's own region - areaTable[RR_BOTTOM_OF_THE_WELL_BEHIND_FAKE_WALLS] = Region("Bottom of the Well Behind Fake Walls", SCENE_BOTTOM_OF_THE_WELL, {}, { + areaTable[RR_BOTW_MIDDLE] = Region("Bottom of the Well Middle", SCENE_BOTTOM_OF_THE_WELL, {}, { //Locations - LOCATION(RC_BOTTOM_OF_THE_WELL_FRONT_LEFT_FAKE_WALL_CHEST, true), - LOCATION(RC_BOTTOM_OF_THE_WELL_RIGHT_BOTTOM_FAKE_WALL_CHEST, true), - LOCATION(RC_BOTTOM_OF_THE_WELL_COMPASS_CHEST, true), //You can just barely pass the spider on the right side without damage or items, but it's probably tight enough to count as as a trick - LOCATION(RC_BOTTOM_OF_THE_WELL_CENTER_SKULLTULA_CHEST, logic->CanPassEnemy(RE_BIG_SKULLTULA) || logic->TakeDamage()), - //Not technically behind a wall, but still logically needs lens due to pits - LOCATION(RC_BOTTOM_OF_THE_WELL_BACK_LEFT_BOMBABLE_CHEST, logic->HasExplosives()), + LOCATION(RC_BOTTOM_OF_THE_WELL_CENTER_SKULLTULA_CHEST, logic->CanPassEnemy(RE_BIG_SKULLTULA) || logic->TakeDamage()), }, { //Exits - Entrance(RR_BOTTOM_OF_THE_WELL_PERIMETER, []{return ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH);}), - Entrance(RR_BOTTOM_OF_THE_WELL_INNER_ROOMS, []{return logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 3);}), - Entrance(RR_BOTTOM_OF_THE_WELL_BASEMENT, []{return true;}), - Entrance(RR_BOTTOM_OF_THE_WELL_BASEMENT_PLATFORM, []{return ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_BOTW_PERIMETER, []{return ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_BOTW_PIT_CAGE, []{return ctx->GetTrickOption(RT_BOTW_PITS) && (ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH));}), + Entrance(RR_BOTW_SKULL_WALL_ROOM, []{return logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 3);}), + Entrance(RR_BOTW_INVISIBLE_PATH, []{return logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 3);}), + Entrance(RR_BOTW_B3_OOZE, []{return true;}), + Entrance(RR_BOTW_B3_PLATFORM, []{return ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH);}), }); - //This area can be reached without lens in logic from basement, but that could require silver rupees if they are shuffled. - areaTable[RR_BOTTOM_OF_THE_WELL_SOUTHWEST_ROOM] = Region("Bottom of the Well Southwest Room", SCENE_BOTTOM_OF_THE_WELL, {}, { + areaTable[RR_BOTW_HIDDEN_POTS] = Region("Bottom of the Well Hidden Pots", SCENE_BOTTOM_OF_THE_WELL, {}, { //Locations LOCATION(RC_BOTTOM_OF_THE_WELL_LEFT_SIDE_POT_1, logic->CanBreakPots()), LOCATION(RC_BOTTOM_OF_THE_WELL_LEFT_SIDE_POT_2, logic->CanBreakPots()), LOCATION(RC_BOTTOM_OF_THE_WELL_LEFT_SIDE_POT_3, logic->CanBreakPots()), }, { //Exits - Entrance(RR_BOTTOM_OF_THE_WELL_PERIMETER, []{return ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_BOTW_PERIMETER, []{return ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + }); + + areaTable[RR_BOTW_CORNER_CRAWLSPACE] = Region("Bottom of the Well Corner Crawlspace", SCENE_BOTTOM_OF_THE_WELL, {}, {}, { + //Exits + Entrance(RR_BOTW_PERIMETER, []{return logic->IsChild;}), + Entrance(RR_BOTW_HIDDEN_PITS_ROOM, []{return logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 3);}), }); //Passing through this area needs lens, but entering doesn't, so that the fire keese can be killed without crossing the pits if enemy drops are ever shuffled - areaTable[RR_BOTTOM_OF_THE_WELL_KEESE_BEAMOS_ROOM] = Region("Bottom of the Well Keese-Beamos Room", SCENE_BOTTOM_OF_THE_WELL, {}, { + areaTable[RR_BOTW_HIDDEN_PITS_ROOM] = Region("Bottom of the Well Hidden Pits Room", SCENE_BOTTOM_OF_THE_WELL, {}, { //Locations LOCATION(RC_BOTTOM_OF_THE_WELL_FIRE_KEESE_CHEST, ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH)), LOCATION(RC_BOTTOM_OF_THE_WELL_FIRE_KEESE_POT_1, logic->CanBreakPots() && (ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH))), }, { //Exits - Entrance(RR_BOTTOM_OF_THE_WELL_PERIMETER, []{return logic->IsChild && logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 3) && (ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH));}), - Entrance(RR_BOTTOM_OF_THE_WELL_LIKE_LIKE_CAGE, []{return ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_BOTW_CORNER_CRAWLSPACE, []{return logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 3) && (ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH));}), + Entrance(RR_BOTW_LOCKED_CAGE, []{return ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH);}), //not sure if this lens check is needed, these holes are a bit too easy to find, but it matches existing logic - Entrance(RR_BOTTOM_OF_THE_WELL_BASEMENT_USEFUL_BOMB_FLOWERS, []{return ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_BOTW_B3_BOMB_FLOWERS, []{return ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH);}), }); - areaTable[RR_BOTTOM_OF_THE_WELL_LIKE_LIKE_CAGE] = Region("Bottom of the Well Like-Like Cage", SCENE_BOTTOM_OF_THE_WELL, {}, { + areaTable[RR_BOTW_LOCKED_CAGE] = Region("Bottom of the Well Locked Cage", SCENE_BOTTOM_OF_THE_WELL, {}, { //Locations LOCATION(RC_BOTTOM_OF_THE_WELL_LIKE_LIKE_CHEST, true), LOCATION(RC_BOTTOM_OF_THE_WELL_GS_LIKE_LIKE_CAGE, logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA, ED_BOOMERANG)), }, { //Exits - Entrance(RR_BOTTOM_OF_THE_WELL_KEESE_BEAMOS_ROOM, []{return true;}), + Entrance(RR_BOTW_HIDDEN_PITS_ROOM, []{return true;}), }); - //If the player can voidwarp into one of these rooms they will need splitting up, and Fake walls will need specifying into middle and the rest moved to perimeter - areaTable[RR_BOTTOM_OF_THE_WELL_INNER_ROOMS] = Region("Bottom of the Well Inner Rooms", SCENE_BOTTOM_OF_THE_WELL, { + areaTable[RR_BOTW_PIT_CAGE] = Region("Bottom of the Well Pit Cage", SCENE_BOTTOM_OF_THE_WELL, {}, { + //Locations + LOCATION(RC_BOTTOM_OF_THE_WELL_COMPASS_CHEST, true), + }, { + //Exits + Entrance(RR_BOTW_PERIMETER, []{return ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_BOTW_MIDDLE, []{return ctx->GetTrickOption(RT_BOTW_PITS) && (ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH));}), + Entrance(RR_BOTW_B3_OOZE, []{return true;}), + }); + + areaTable[RR_BOTW_SKULL_WALL_ROOM] = Region("Bottom of the Well SKull Wall Room", SCENE_BOTTOM_OF_THE_WELL, { //Events EventAccess(LOGIC_DEKU_BABA_STICKS, []{return logic->CanGetDekuBabaSticks();}), EventAccess(LOGIC_DEKU_BABA_NUTS, []{return logic->CanGetDekuBabaNuts();}), }, { //Locations LOCATION(RC_BOTTOM_OF_THE_WELL_GS_WEST_INNER_ROOM, logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA, ED_BOOMERANG)), + }, { + //Exits + Entrance(RR_BOTW_MIDDLE, []{return logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 3);}), + }); + + areaTable[RR_BOTW_INVISIBLE_PATH] = Region("Bottom of the Well Invisible Path", SCENE_BOTTOM_OF_THE_WELL, {}, { + //Locations LOCATION(RC_BOTTOM_OF_THE_WELL_GS_EAST_INNER_ROOM, logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA, ED_BOOMERANG)), }, { //Exits - Entrance(RR_BOTTOM_OF_THE_WELL_BEHIND_FAKE_WALLS, []{return logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 3);}), + Entrance(RR_BOTW_MIDDLE, []{return logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 3);}), + Entrance(RR_BOTW_B3_OOZE, []{return true;}), }); - areaTable[RR_BOTTOM_OF_THE_WELL_COFFIN_ROOM] = Region("Bottom of the Well Coffin Room", SCENE_BOTTOM_OF_THE_WELL, {}, { + areaTable[RR_BOTW_BEHIND_MOAT] = Region("Bottom of the Well Behind Moat", SCENE_BOTTOM_OF_THE_WELL, {}, { //Locations - LOCATION(RC_BOTTOM_OF_THE_WELL_FREESTANDING_KEY, logic->HasFireSourceWithTorch() || logic->CanUse(RG_FAIRY_BOW)), - LOCATION(RC_BOTTOM_OF_THE_WELL_COFFIN_ROOM_FRONT_LEFT_HEART, true), - LOCATION(RC_BOTTOM_OF_THE_WELL_COFFIN_ROOM_MIDDLE_RIGHT_HEART, logic->HasFireSourceWithTorch() || logic->CanUse(RG_FAIRY_BOW)), + LOCATION(RC_BOTTOM_OF_THE_WELL_UNDERWATER_LEFT_CHEST, logic->Get(LOGIC_BOTW_LOWERED_WATER) || logic->CanOpenUnderwaterChest()), }, { //Exits - Entrance(RR_BOTTOM_OF_THE_WELL_PERIMETER, []{return logic->Get(LOGIC_BOTW_LOWERED_WATER) || logic->HasItem(RG_BRONZE_SCALE);}), + //Climb always needed in case water is lowered out of logic + Entrance(RR_BOTW_PERIMETER, []{return (logic->Get(LOGIC_BOTW_LOWERED_WATER) || logic->HasItem(RG_BRONZE_SCALE) || + (logic->IsAdult && logic->CanUse(RG_IRON_BOOTS) && logic->CanUse(RG_HOOKSHOT))/* && CanClimb()*/);}), + Entrance(RR_BOTW_CRYPT, []{return true;}), }); - areaTable[RR_BOTTOM_OF_THE_WELL_DEAD_HAND_ROOM] = Region("Bottom of the Well Dead Hand Room", SCENE_BOTTOM_OF_THE_WELL, {}, { + areaTable[RR_BOTW_CRYPT] = Region("Bottom of the Well Crypt", SCENE_BOTTOM_OF_THE_WELL, {}, { + //Locations + LOCATION(RC_BOTTOM_OF_THE_WELL_FREESTANDING_KEY, logic->HasFireSourceWithTorch() || logic->CanUse(RG_FAIRY_BOW)), + LOCATION(RC_BOTTOM_OF_THE_WELL_COFFIN_ROOM_FRONT_LEFT_HEART, true), + LOCATION(RC_BOTTOM_OF_THE_WELL_COFFIN_ROOM_MIDDLE_RIGHT_HEART, logic->HasFireSourceWithTorch() || logic->CanUse(RG_FAIRY_BOW)), + }, { + //Exits + Entrance(RR_BOTW_BEHIND_MOAT, []{return true;}), + }); + + areaTable[RR_BOTW_NEAR_BOSS_LOWER] = Region("Bottom of the Well Near Boss Lower", SCENE_BOTTOM_OF_THE_WELL, {}, {}, { + //Exits + //Climb always needed in case the water is lowered out of logic + //Adult can ground jump out of the pit without climb but needs a way through the crawlspace + Entrance(RR_BOTW_PERIMETER, []{return logic->IsChild/*CanCrawl*/ && (logic->Get(LOGIC_BOTW_LOWERED_WATER) || logic->HasItem(RG_BRONZE_SCALE))/*&& CanClimb*/;}), + Entrance(RR_BOTW_NEAR_BOSS_UPPER, []{return true/*CanClimb or (isAdult && CanGroundJump)*/;}), + }); + + areaTable[RR_BOTW_NEAR_BOSS_UPPER] = Region("Bottom of the Well Near Boss Upper", SCENE_BOTTOM_OF_THE_WELL, {}, {}, { + //Exits + Entrance(RR_BOTW_NEAR_BOSS_LOWER, []{return true;}), + Entrance(RR_BOTW_DEAD_HAND_ROOM, []{return true;}), + }); + + areaTable[RR_BOTW_DEAD_HAND_ROOM] = Region("Bottom of the Well Dead Hand Room", SCENE_BOTTOM_OF_THE_WELL, {}, { //Locations LOCATION(RC_BOTTOM_OF_THE_WELL_LENS_OF_TRUTH_CHEST, logic->CanKillEnemy(RE_DEAD_HAND)), LOCATION(RC_BOTTOM_OF_THE_WELL_INVISIBLE_CHEST, (ctx->GetTrickOption(RT_LENS_BOTW) || logic->CanUse(RG_LENS_OF_TRUTH))), }, { //Exits - //This assumes we spawned in dead hand's room, if whatever trick made this relevant instead puts us in the previous room, remove the kill Dead Hand check. - Entrance(RR_BOTTOM_OF_THE_WELL_PERIMETER, []{return logic->IsChild && logic->CanKillEnemy(RE_DEAD_HAND);}), + Entrance(RR_BOTW_NEAR_BOSS_UPPER, []{return logic->CanKillEnemy(RE_DEAD_HAND);}), }); - areaTable[RR_BOTTOM_OF_THE_WELL_BASEMENT] = Region("Bottom of the Well Basement", SCENE_BOTTOM_OF_THE_WELL, {}, { + areaTable[RR_BOTW_B3_OOZE] = Region("Bottom of the Well B3 Ooze", SCENE_BOTTOM_OF_THE_WELL, {}, { //Locations - LOCATION(RC_BOTTOM_OF_THE_WELL_MAP_CHEST, logic->BlastOrSmash()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_1, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_2, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_3, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_4, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_5, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_6, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_7, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_8, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_9, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_10, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_11, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_12, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_SUN_FAIRY, logic->CanUse(RG_SUNS_SONG)), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_GRASS_1, logic->CanCutShrubs()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_GRASS_2, logic->CanCutShrubs()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_GRASS_3, logic->CanCutShrubs()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_1, logic->CanCutShrubs() && logic->BlastOrSmash()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_2, logic->CanCutShrubs() && logic->BlastOrSmash()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_3, logic->CanCutShrubs() && logic->BlastOrSmash()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_4, logic->CanCutShrubs() && logic->BlastOrSmash()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_5, logic->CanCutShrubs() && logic->BlastOrSmash()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_6, logic->CanCutShrubs() && logic->BlastOrSmash()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_7, logic->CanCutShrubs() && logic->BlastOrSmash()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_8, logic->CanCutShrubs() && logic->BlastOrSmash()), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_9, logic->CanCutShrubs() && logic->BlastOrSmash()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_1, logic->CanBreakPots()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_2, logic->CanBreakPots()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_3, logic->CanBreakPots()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_4, logic->CanBreakPots()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_5, logic->CanBreakPots()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_6, logic->CanBreakPots()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_7, logic->CanBreakPots()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_8, logic->CanBreakPots()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_9, logic->CanBreakPots()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_10, logic->CanBreakPots()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_11, logic->CanBreakPots()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_POT_12, logic->CanBreakPots()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_SUN_FAIRY, logic->CanUse(RG_SUNS_SONG)), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_GRASS_1, logic->CanCutShrubs()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_GRASS_2, logic->CanCutShrubs()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_GRASS_3, logic->CanCutShrubs()), }, { //Exits - Entrance(RR_BOTTOM_OF_THE_WELL_SOUTHWEST_ROOM, []{return logic->IsChild && logic->CanPassEnemy(RE_BIG_SKULLTULA);}), + Entrance(RR_BOTW_HIDDEN_POTS, []{return true/*CanClimbHigh()*/;}), //It's possible to abuse boulder's limited range of collision detection to detonate the flowers through the boulder with bow, but this is a glitch //the exact range is just past the furthest away plank in the green goo section - Entrance(RR_BOTTOM_OF_THE_WELL_BASEMENT_USEFUL_BOMB_FLOWERS, []{return Here(RR_BOTTOM_OF_THE_WELL_BASEMENT, []{return logic->BlastOrSmash() || logic->CanUse(RG_DINS_FIRE) || (logic->CanUse(RG_STICKS) && ctx->GetTrickOption(RT_BOTW_BASEMENT));});}), + Entrance(RR_BOTW_B3_BOMB_FLOWERS, []{return Here(RR_BOTW_B3_OOZE, []{return logic->BlastOrSmash() || logic->CanUse(RG_DINS_FIRE) || (logic->CanUse(RG_STICKS) && ctx->GetTrickOption(RT_BOTW_BASEMENT));});}), + Entrance(RR_BOTW_B3_BLOCKED_GRASS, []{return Here(RR_BOTW_B3_OOZE, []{return logic->BlastOrSmash();});}), + Entrance(RR_BOTW_B3_CHEST_AREA, []{return Here(RR_BOTW_B3_OOZE, []{return logic->BlastOrSmash();});}), }); - areaTable[RR_BOTTOM_OF_THE_WELL_BASEMENT_USEFUL_BOMB_FLOWERS] = Region("Bottom of the Well Basement Useful Bomb Flowers", SCENE_BOTTOM_OF_THE_WELL, {}, { - //Locations - //Assumes RR_BOTTOM_OF_THE_WELL_BASEMENT access - LOCATION(RC_BOTTOM_OF_THE_WELL_MAP_CHEST, logic->HasItem(RG_GORONS_BRACELET)), - }, { + areaTable[RR_BOTW_B3_BOMB_FLOWERS] = Region("Bottom of the Well B3 Bomb Flowers", SCENE_BOTTOM_OF_THE_WELL, {}, {}, { //Exits - Entrance(RR_BOTTOM_OF_THE_WELL_BASEMENT, []{return logic->CanDetonateUprightBombFlower();}), + Entrance(RR_BOTW_B3_OOZE, []{return logic->CanDetonateUprightBombFlower();}), + Entrance(RR_BOTW_B3_BLOCKED_GRASS, []{return logic->HasItem(RG_GORONS_BRACELET);}), + Entrance(RR_BOTW_B3_CHEST_AREA, []{return logic->HasItem(RG_GORONS_BRACELET);}), }); - areaTable[RR_BOTTOM_OF_THE_WELL_BASEMENT_PLATFORM] = Region("Bottom of the Well Basement Platform", SCENE_BOTTOM_OF_THE_WELL, {}, { + areaTable[RR_BOTW_B3_BLOCKED_GRASS] = Region("Bottom of the Well B3 Blocked Grass", SCENE_BOTTOM_OF_THE_WELL, {}, { //Locations - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_PLATFORM_LEFT_RUPEE, true), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_PLATFORM_BACK_LEFT_RUPEE, true), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_PLATFORM_MIDDLE_RUPEE, true), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_PLATFORM_BACK_RIGHT_RUPEE, true), - LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_PLATFORM_RIGHT_RUPEE, true), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_1, logic->CanCutShrubs()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_2, logic->CanCutShrubs()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_3, logic->CanCutShrubs()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_4, logic->CanCutShrubs()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_5, logic->CanCutShrubs()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_6, logic->CanCutShrubs()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_7, logic->CanCutShrubs()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_8, logic->CanCutShrubs()), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_BEHIND_ROCKS_GRASS_9, logic->CanCutShrubs()), }, { //Exits - Entrance(RR_BOTTOM_OF_THE_WELL_BASEMENT, []{return true;}), + Entrance(RR_BOTW_B3_OOZE, []{return Here(RR_BOTW_B3_BLOCKED_GRASS, []{return logic->BlastOrSmash() || logic->HasItem(RG_GORONS_BRACELET);});}), + }); + + areaTable[RR_BOTW_B3_CHEST_AREA] = Region("Bottom of the Well B3 Chest Area", SCENE_BOTTOM_OF_THE_WELL, {}, { + //Locations + LOCATION(RC_BOTTOM_OF_THE_WELL_MAP_CHEST, true), + }, { + //Exits + Entrance(RR_BOTW_B3_OOZE, []{return Here(RR_BOTW_B3_CHEST_AREA, []{return logic->BlastOrSmash();});}), + }); + + areaTable[RR_BOTW_B3_PLATFORM] = Region("Bottom of the Well B3 Platform", SCENE_BOTTOM_OF_THE_WELL, {}, { + //Locations + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_PLATFORM_LEFT_RUPEE, true), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_PLATFORM_BACK_LEFT_RUPEE, true), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_PLATFORM_MIDDLE_RUPEE, true), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_PLATFORM_BACK_RIGHT_RUPEE, true), + LOCATION(RC_BOTTOM_OF_THE_WELL_BASEMENT_PLATFORM_RIGHT_RUPEE, true), + }, { + //Exits + Entrance(RR_BOTW_B3_OOZE, []{return true;}), }); #pragma endregion #pragma region MQ - areaTable[RR_BOTTOM_OF_THE_WELL_MQ_PERIMETER] = Region("Bottom of the Well MQ Perimeter", SCENE_BOTTOM_OF_THE_WELL, { + areaTable[RR_BOTW_MQ_PERIMETER] = Region("Bottom of the Well MQ Perimeter", SCENE_BOTTOM_OF_THE_WELL, { //Events //technically obsolete due to a wonder item fairy which only needs a projectile, but we don't have an event var for it yet - EventAccess(LOGIC_FAIRY_POT, []{return Here(RR_BOTTOM_OF_THE_WELL_MQ_PERIMETER, []{return logic->BlastOrSmash();}) && logic->CanHitEyeTargets();}), - //It is possible to hit the water switch with a pot from RR_BOTTOM_OF_THE_WELL_MQ_MIDDLE, however the hitbox for making it activate is very unintuitive + EventAccess(LOGIC_FAIRY_POT, []{return Here(RR_BOTW_MQ_PERIMETER, []{return logic->BlastOrSmash();}) && logic->CanHitEyeTargets();}), + //It is possible to hit the water switch with a pot from RR_BOTW_MQ_MIDDLE, however the hitbox for making it activate is very unintuitive //You have to throw the pot from further back to hit the switch from the front instead of the top, trying to hit the "fingers" directly //This unintuitiveness means it should be a trick. ZL is needed to get a clear path to carry the pot - EventAccess(LOGIC_BOTW_LOWERED_WATER, []{return logic->CanJumpslash() || logic->CanUseProjectile();}), + EventAccess(LOGIC_BOTW_LOWERED_WATER, []{return logic->CanHitSwitch(ED_SHORT_JUMPSLASH);}), + EventAccess(LOGIC_BOTW_MQ_OPENED_GATES, []{return logic->CanUse(RG_ZELDAS_LULLABY);}), }, { //Locations - //Implies CanBreakPots() - LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_OUTER_LOBBY_POT, Here(RR_BOTTOM_OF_THE_WELL_MQ_PERIMETER, []{return logic->BlastOrSmash();}) && logic->CanHitEyeTargets()), + //Implies CanBreakPots(). Hitting this with rang through the wall is possible but would be a trick. + //Instead of blowing up the boulder, you can aim through the lower left side with sling(either age) or as child with bow + //Not even bow extension seems to get adult's bow to work + //this would be a trick + LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_OUTER_LOBBY_POT, Here(RR_BOTW_MQ_PERIMETER, []{return logic->BlastOrSmash();}) && logic->CanHitEyeTargets()), LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_BOMB_LEFT_HEART, logic->HasExplosives()), LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_BOMB_RIGHT_HEART, logic->HasExplosives()), }, { //Exits - Entrance(RR_BOTTOM_OF_THE_WELL_ENTRYWAY, []{return logic->IsChild;}), - Entrance(RR_BOTTOM_OF_THE_WELL_MQ_WEST_ROOM_SWITCH, []{return Here(RR_BOTTOM_OF_THE_WELL_MQ_PERIMETER, []{return logic->BlastOrSmash();}) && logic->CanPassEnemy(RE_BIG_SKULLTULA);}), - Entrance(RR_BOTTOM_OF_THE_WELL_MQ_COFFIN_ROOM, []{return (logic->Get(LOGIC_BOTW_LOWERED_WATER) || logic->HasItem(RG_BRONZE_SCALE)) && logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 2);}), - Entrance(RR_BOTTOM_OF_THE_WELL_MQ_LOCKED_CAGE, []{return logic->IsChild && logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 2) && logic->CanUseProjectile();}), - Entrance(RR_BOTTOM_OF_THE_WELL_MQ_DEAD_HAND_ROOM, []{return logic->IsChild && logic->Get(LOGIC_BOTW_LOWERED_WATER);}), - Entrance(RR_BOTTOM_OF_THE_WELL_MQ_MIDDLE, []{return logic->CanUse(RG_ZELDAS_LULLABY);}), - Entrance(RR_BOTTOM_OF_THE_WELL_MQ_BASEMENT, []{return true;}), + Entrance(RR_BOTW_ENTRYWAY, []{return logic->IsChild/*CanCrawl() && CanClimb()*/;}), + Entrance(RR_BOTW_MQ_MIDDLE, []{return logic->Get(LOGIC_BOTW_MQ_OPENED_GATES);}), + Entrance(RR_BOTW_MQ_PIT_CAGE, []{return Here(RR_BOTW_MQ_PERIMETER, []{return logic->BlastOrSmash();}) && logic->CanPassEnemy(RE_BIG_SKULLTULA);}), + //Climb always needed in case water is lowered out of logic + Entrance(RR_BOTW_MQ_BEHIND_MOAT, []{return (logic->Get(LOGIC_BOTW_LOWERED_WATER) || logic->HasItem(RG_BRONZE_SCALE) || + (logic->IsAdult && logic->CanUse(RG_IRON_BOOTS) && logic->CanUse(RG_HOOKSHOT))/*&& CanClimb()*/);}), + Entrance(RR_BOTW_MQ_CORNER_CRAWLSPACE, []{return logic->IsChild/*CanCrawl()*/;}), + Entrance(RR_BOTW_MQ_NEAR_BOSS_LOWER, []{return logic->IsChild/*CanCrawl()*/ && logic->Get(LOGIC_BOTW_LOWERED_WATER);}), + Entrance(RR_BOTW_MQ_B3, []{return true;}), }); - areaTable[RR_BOTTOM_OF_THE_WELL_MQ_WEST_ROOM_SWITCH] = Region("Bottom of the Well MQ West Room Switch", SCENE_BOTTOM_OF_THE_WELL, { + areaTable[RR_BOTW_MQ_MIDDLE] = Region("Bottom of the Well MQ Middle", SCENE_BOTTOM_OF_THE_WELL, {}, { + //Locations + LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_MAP_CHEST, true), + LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_INNER_LOBBY_POT_1, logic->CanBreakPots()), + LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_INNER_LOBBY_POT_2, logic->CanBreakPots()), + LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_INNER_LOBBY_POT_3, logic->CanBreakPots()), + LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_CELL_SUN_FAIRY, logic->CanUse(RG_SUNS_SONG)), + }, { + //Exits + Entrance(RR_BOTW_MQ_PERIMETER, []{return logic->Get(LOGIC_BOTW_MQ_OPENED_GATES);}), + Entrance(RR_BOTW_MQ_PIT_CAGE, []{return (bool)ctx->GetTrickOption(RT_BOTW_PITS);}), + Entrance(RR_BOTW_MQ_B3_PLATFORM, []{return logic->Get(LOGIC_BOTW_MQ_OPENED_MIDDLE_HOLE);}), + Entrance(RR_BOTW_MQ_B3, []{return true;}), + Entrance(RR_BOTW_MQ_INVISIBLE_PATH, []{return true/*str0 or CanHitSwitch(ED_BOMB_THROW)*/;}), + Entrance(RR_BOTW_MQ_GRAVE_ROOM, []{return logic->Get(LOGIC_BOTW_MQ_OPENED_WEST_ROOM);}), + }); + + areaTable[RR_BOTW_MQ_INVISIBLE_PATH] = Region("Bottom of the Well Invisible Path", SCENE_BOTTOM_OF_THE_WELL, {}, { + //Locations + //This location technically involves an invisible platform, but it's intended to do lensless in vanilla and is clearly signposted by pots. + LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_EAST_INNER_ROOM_FREESTANDING_KEY, true), + LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_EAST_INNER_ROOM_POT_1, logic->CanBreakPots()), + LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_EAST_INNER_ROOM_POT_2, logic->CanBreakPots()), + LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_EAST_INNER_ROOM_POT_3, logic->CanBreakPots()), + }, { + //Exits + Entrance(RR_BOTW_MQ_MIDDLE, []{return true;}), + Entrance(RR_BOTW_MQ_B3, []{return true;}), + }); + + areaTable[RR_BOTW_MQ_GRAVE_ROOM] = Region("Bottom of the Well Grave Room", SCENE_BOTTOM_OF_THE_WELL, {}, { + //Locations + //The enemies in this room are invisible and crowd around the player, being awkward to deal with blind unless you already know how. + //the right wall is safe, and can be followed to get behind the grave which you can then pull easily assuming you can tank invisible keese + //Using a deku nut however stuns everything easily. and if you have a melee weapon you can kill the skull through the grave then grab the drop + //though it can be hard to tell where the safe direct path to the grave is without lens. + //Also you get cheap shotted on entry sometimes. + //An MQ lens trick is recommended here, and a review of this room for OHKO logic when that is added is advised. + //In the meantime I assume damage taken or the easy answer (nuts) + LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_GS_WEST_INNER_ROOM, (logic->TakeDamage() || logic->CanUse(RG_NUTS)) && logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA)), + }, { + //Exits + Entrance(RR_BOTW_MQ_MIDDLE, []{return true;}), + }); + + areaTable[RR_BOTW_MQ_PIT_CAGE] = Region("Bottom of the Well MQ Pit Cage", SCENE_BOTTOM_OF_THE_WELL, { //Events EventAccess(LOGIC_BOTW_MQ_OPENED_WEST_ROOM, []{return true;}), }, {}, { //Exits - Entrance(RR_BOTTOM_OF_THE_WELL_MQ_PERIMETER, []{return logic->BlastOrSmash() && (logic->CanPassEnemy(RE_BIG_SKULLTULA) || ctx->GetTrickOption(RT_BOTW_MQ_PITS));}), - Entrance(RR_BOTTOM_OF_THE_WELL_MQ_MIDDLE, []{return (bool)ctx->GetTrickOption(RT_BOTW_MQ_PITS);}), - Entrance(RR_BOTTOM_OF_THE_WELL_MQ_BASEMENT, []{return true;}), + Entrance(RR_BOTW_MQ_PERIMETER, []{return logic->BlastOrSmash() && (logic->CanPassEnemy(RE_BIG_SKULLTULA) || ctx->GetTrickOption(RT_BOTW_PITS));}), + Entrance(RR_BOTW_MQ_MIDDLE, []{return (bool)ctx->GetTrickOption(RT_BOTW_PITS);}), + Entrance(RR_BOTW_MQ_B3, []{return true;}), }); - areaTable[RR_BOTTOM_OF_THE_WELL_MQ_COFFIN_ROOM] = Region("Bottom of the Well MQ Coffin Room", SCENE_BOTTOM_OF_THE_WELL, {}, { + areaTable[RR_BOTW_MQ_BEHIND_MOAT] = Region("Bottom of the Well MQ Behind Moat", SCENE_BOTTOM_OF_THE_WELL, {}, {}, { + //Exits + //Climb always needed in case water is lowered out of logic + Entrance(RR_BOTW_MQ_PERIMETER, []{return (logic->Get(LOGIC_BOTW_LOWERED_WATER)|| logic->HasItem(RG_BRONZE_SCALE) || + (logic->IsAdult && logic->CanUse(RG_IRON_BOOTS) && logic->CanUse(RG_HOOKSHOT))/* && CanClimb*/);}), + Entrance(RR_BOTW_MQ_CRYPT, []{return logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 2);}), + }); + + areaTable[RR_BOTW_MQ_CRYPT] = Region("Bottom of the Well MQ Crypt", SCENE_BOTTOM_OF_THE_WELL, {}, { //Locations LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_GS_COFFIN_ROOM, logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA)), LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_COFFIN_ROOM_FRONT_RIGHT_HEART, logic->HasFireSourceWithTorch() || logic->CanUse(RG_FAIRY_BOW)), LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_COFFIN_ROOM_MIDDLE_LEFT_HEART, logic->HasFireSourceWithTorch() || logic->CanUse(RG_FAIRY_BOW)), }, { //Exits - Entrance(RR_BOTTOM_OF_THE_WELL_MQ_PERIMETER, []{return (logic->Get(LOGIC_BOTW_LOWERED_WATER) || logic->HasItem(RG_BRONZE_SCALE)) && logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 2);}), + Entrance(RR_BOTW_MQ_BEHIND_MOAT, []{return logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 2);}), }); - areaTable[RR_BOTTOM_OF_THE_WELL_MQ_LOCKED_CAGE] = Region("Bottom of the Well MQ Locked Cage", SCENE_BOTTOM_OF_THE_WELL, { + areaTable[RR_BOTW_MQ_CORNER_CRAWLSPACE] = Region("Bottom of the Well MQ Northeast Crawlspace", SCENE_BOTTOM_OF_THE_WELL, {}, {}, { + Entrance(RR_BOTW_MQ_PERIMETER, []{return logic->IsChild;}), + Entrance(RR_BOTW_MQ_FLOORMASTER_ROOM, []{return logic->CanUseProjectile();}), + }); + + areaTable[RR_BOTW_MQ_FLOORMASTER_ROOM] = Region("Bottom of the Well MQ Floormaster Room", SCENE_BOTTOM_OF_THE_WELL, {}, {}, { + Entrance(RR_BOTW_MQ_CORNER_CRAWLSPACE, []{return true;}), + Entrance(RR_BOTW_MQ_LOCKED_CAGE, []{return logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 2);}), + }); + + areaTable[RR_BOTW_MQ_LOCKED_CAGE] = Region("Bottom of the Well MQ Locked Cage", SCENE_BOTTOM_OF_THE_WELL, { //Events EventAccess(LOGIC_BOTW_MQ_OPENED_MIDDLE_HOLE, []{return logic->HasExplosives();}), }, {}, { //Exits - Entrance(RR_BOTTOM_OF_THE_WELL_MQ_PERIMETER, []{return logic->IsChild && logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 2);}), + Entrance(RR_BOTW_MQ_FLOORMASTER_ROOM, []{return logic->IsChild && logic->SmallKeys(SCENE_BOTTOM_OF_THE_WELL, 2);}), }); - areaTable[RR_BOTTOM_OF_THE_WELL_MQ_DEAD_HAND_ROOM] = Region("Bottom of the Well MQ Dead Hand Room", SCENE_BOTTOM_OF_THE_WELL, {}, { + areaTable[RR_BOTW_MQ_NEAR_BOSS_LOWER] = Region("Bottom of the Well MQ Near Boss Lower", SCENE_BOTTOM_OF_THE_WELL, {}, {}, { + //Exits + //Climb always needed in case the water is lowered out of logic + //Adult can ground jump out of the pit without climb but needs a way through the crawlspace + Entrance(RR_BOTW_MQ_PERIMETER, []{return logic->IsChild/*CanCrawl*/ && (logic->Get(LOGIC_BOTW_LOWERED_WATER) || logic->HasItem(RG_BRONZE_SCALE))/*&& CanClimb*/;}), + Entrance(RR_BOTW_MQ_NEAR_BOSS_UPPER, []{return true/*CanClimb*/;}), + }); + + areaTable[RR_BOTW_MQ_NEAR_BOSS_UPPER] = Region("Bottom of the Well MQ Near Boss Upper", SCENE_BOTTOM_OF_THE_WELL, {}, {}, { + //Exits + Entrance(RR_BOTW_MQ_NEAR_BOSS_LOWER, []{return true;}), + Entrance(RR_BOTW_MQ_DEAD_HAND_ROOM, []{return true;}), + }); + + areaTable[RR_BOTW_MQ_DEAD_HAND_ROOM] = Region("Bottom of the Well MQ Dead Hand Room", SCENE_BOTTOM_OF_THE_WELL, {}, { //Locations LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_COMPASS_CHEST, logic->CanKillEnemy(RE_DEAD_HAND)), LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_DEAD_HAND_FREESTANDING_KEY, logic->HasExplosives() || (ctx->GetTrickOption(RT_BOTW_MQ_DEADHAND_KEY) && logic->CanUse(RG_BOOMERANG))), @@ -252,38 +406,10 @@ void RegionTable_Init_BottomOfTheWell() { LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_DEAD_HAND_GRASS_4, logic->CanCutShrubs()), }, { //Exits - //This assumes we spawned in dead hand's room, if whatever trick made this relevant instead puts us in the previous room, remove the kill Dead Hand check. - Entrance(RR_BOTTOM_OF_THE_WELL_MQ_PERIMETER, []{return logic->IsChild && logic->CanKillEnemy(RE_DEAD_HAND);}), + Entrance(RR_BOTW_MQ_NEAR_BOSS_UPPER, []{return logic->CanKillEnemy(RE_DEAD_HAND);}), }); - areaTable[RR_BOTTOM_OF_THE_WELL_MQ_MIDDLE] = Region("Bottom of the Well MQ Middle", SCENE_BOTTOM_OF_THE_WELL, {}, { - //Locations - LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_MAP_CHEST, true), - //This location technically involves an invisible platform, but it's intended to do lensless in vanilla and is clearly signposted by pots. - LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_EAST_INNER_ROOM_FREESTANDING_KEY, true), - //The enemies in this room are invisible and crowd around the player, being awkward to deal with blind unless you already know how. - //the right wall is safe, and can be followed to get behind the grave which you can then pull easily assuming you can tank invisible keese - //Using a deku nut however stuns everything easily. and if you have a melee weapon you can kill the skull through the grave then grab the drop - //though it can be hard to tell where the safe direct path to the grave is without lens. - //Also you get cheap shotted on entry sometimes. - //An MQ lens trick is recommended here, and a review of this room for OHKO logic what that is added is advised. - //In the meantime I assume damage taken or the easy answer (nuts) - LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_GS_WEST_INNER_ROOM, logic->Get(LOGIC_BOTW_MQ_OPENED_WEST_ROOM) && (logic->TakeDamage() || logic->CanUse(RG_NUTS)) && logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA)), - LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_INNER_LOBBY_POT_1, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_INNER_LOBBY_POT_2, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_INNER_LOBBY_POT_3, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_EAST_INNER_ROOM_POT_1, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_EAST_INNER_ROOM_POT_2, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_EAST_INNER_ROOM_POT_3, logic->CanBreakPots()), - LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_CELL_SUN_FAIRY, logic->CanUse(RG_SUNS_SONG)), - }, { - //Exits - //If a relevant trick causes you to be able to warp into here without going through PERIMETER, a new eventAccess will be needed for lowering the gates with ZL - Entrance(RR_BOTTOM_OF_THE_WELL_MQ_BASEMENT_SWITCH_PLATFORM, []{return logic->Get(LOGIC_BOTW_MQ_OPENED_MIDDLE_HOLE);}), - Entrance(RR_BOTTOM_OF_THE_WELL_MQ_BASEMENT, []{return true;}), - }); - - areaTable[RR_BOTTOM_OF_THE_WELL_MQ_BASEMENT] = Region("Bottom of the Well MQ Basement", SCENE_BOTTOM_OF_THE_WELL, {}, { + areaTable[RR_BOTW_MQ_B3] = Region("Bottom of the Well MQ B3", SCENE_BOTTOM_OF_THE_WELL, {}, { //Locations //behind invisible big skulltulas, but with navi spotting it's easy to avoid them, or at worst, tank your way through as they do not block the path LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_GS_BASEMENT, logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA)), @@ -293,19 +419,16 @@ void RegionTable_Init_BottomOfTheWell() { LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_BASEMENT_SUN_FAIRY, logic->CanUse(RG_SUNS_SONG)), }, { //Exits - Entrance(RR_BOTTOM_OF_THE_WELL_MQ_PERIMETER, []{return true;}), + Entrance(RR_BOTW_MQ_PERIMETER, []{return true/*CanClimbHigh()*/;}), }); - areaTable[RR_BOTTOM_OF_THE_WELL_MQ_BASEMENT_SWITCH_PLATFORM] = Region("Bottom of the Well MQ Basement Switch Platform", SCENE_BOTTOM_OF_THE_WELL, {}, { + areaTable[RR_BOTW_MQ_B3_PLATFORM] = Region("Bottom of the Well MQ B3 Platform", SCENE_BOTTOM_OF_THE_WELL, {}, { //Locations - //Assumes RR_BOTTOM_OF_THE_WELL_MQ_BASEMENT access - //it is technically possible to get the chest before you get screamed at without rolling, but hard enough to be a trick if that is the requirement for something to be logical - //With some kind of movement tech it's much easier, easy enough to be default logic, as the redeads don't lock on immediately in addition to the extra speed - //leaving with no requirements for now but up for discussion. - LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_LENS_OF_TRUTH_CHEST, true), + //Assumes RR_BOTW_MQ_B3 access + LOCATION(RC_BOTTOM_OF_THE_WELL_MQ_LENS_OF_TRUTH_CHEST, logic->CanPassEnemy(RE_REDEAD)), }, { //Exits - Entrance(RR_BOTTOM_OF_THE_WELL_MQ_BASEMENT, []{return true;}), + Entrance(RR_BOTW_MQ_B3, []{return true;}), }); #pragma endregion diff --git a/soh/soh/Enhancements/randomizer/location_access/dungeons/shadow_temple.cpp b/soh/soh/Enhancements/randomizer/location_access/dungeons/shadow_temple.cpp index 590af4d93..8514d12be 100644 --- a/soh/soh/Enhancements/randomizer/location_access/dungeons/shadow_temple.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/dungeons/shadow_temple.cpp @@ -9,120 +9,354 @@ void RegionTable_Init_ShadowTemple() { // Vanilla/MQ Decider areaTable[RR_SHADOW_TEMPLE_ENTRYWAY] = Region("Shadow Temple Entryway", SCENE_SHADOW_TEMPLE, {}, {}, { //Exits - Entrance(RR_SHADOW_TEMPLE_BEGINNING, []{return ctx->GetDungeon(SHADOW_TEMPLE)->IsVanilla() && (ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH)) && (logic->CanUse(RG_HOVER_BOOTS) || logic->CanUse(RG_HOOKSHOT));}), + Entrance(RR_SHADOW_TEMPLE_BEGINNING, []{return ctx->GetDungeon(SHADOW_TEMPLE)->IsVanilla() && (logic->CanUse(RG_HOVER_BOOTS) || logic->CanUse(RG_HOOKSHOT));}), Entrance(RR_SHADOW_TEMPLE_MQ_BEGINNING, []{return ctx->GetDungeon(SHADOW_TEMPLE)->IsMQ();}), Entrance(RR_GRAVEYARD_WARP_PAD_REGION, []{return true;}), }); #pragma region Vanilla - areaTable[RR_SHADOW_TEMPLE_BEGINNING] = Region("Shadow Temple Beginning", SCENE_SHADOW_TEMPLE, { + areaTable[RR_SHADOW_TEMPLE_BEGINNING] = Region("Shadow Temple Beginning", SCENE_SHADOW_TEMPLE, {}, {}, { + //Exits + Entrance(RR_SHADOW_TEMPLE_ENTRYWAY, []{return (ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH)) && (logic->CanUse(RG_HOVER_BOOTS) || logic->CanUse(RG_HOOKSHOT));}), + Entrance(RR_SHADOW_TEMPLE_WHISPERING_WALLS_START, []{return ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_SHADOW_TEMPLE_FIRST_BEAMOS, []{return (ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH)) && logic->CanUse(RG_HOVER_BOOTS);}), + }); + + areaTable[RR_SHADOW_TEMPLE_WHISPERING_WALLS_START] = Region("Shadow Temple Whispering Walls Start", SCENE_SHADOW_TEMPLE, { //Events EventAccess(LOGIC_NUT_POT, []{return true;}), }, { //Locations - LOCATION(RC_SHADOW_TEMPLE_MAP_CHEST, logic->CanJumpslashExceptHammer()), - LOCATION(RC_SHADOW_TEMPLE_HOVER_BOOTS_CHEST, logic->CanKillEnemy(RE_DEAD_HAND)), - LOCATION(RC_SHADOW_TEMPLE_NEAR_DEAD_HAND_POT_1, logic->CanBreakPots()), LOCATION(RC_SHADOW_TEMPLE_WHISPERING_WALLS_POT_1, logic->CanBreakPots()), LOCATION(RC_SHADOW_TEMPLE_WHISPERING_WALLS_POT_2, logic->CanBreakPots()), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_BEGINNING, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_WHISPERING_WALLS_SIDE, []{return ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_SHADOW_TEMPLE_WHISPERING_WALLS_END, []{return ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + }); + + // shares RR_SHADOW_TEMPLE_WHISPERING_WALLS_START area with pots, but handles lens access for reaching door at start + areaTable[RR_SHADOW_TEMPLE_WHISPERING_WALLS_SIDE] = Region("Shadow Temple Whispering Walls Side", SCENE_SHADOW_TEMPLE, { + //Events + EventAccess(LOGIC_NUT_POT, []{return true;}), + }, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_WHISPERING_WALLS_POT_1, logic->CanBreakPots()), + LOCATION(RC_SHADOW_TEMPLE_WHISPERING_WALLS_POT_2, logic->CanBreakPots()), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_WHISPERING_WALLS_START, []{return ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_SHADOW_TEMPLE_WHISPERING_WALLS_SIDE_ROOM, []{return true;}), + }); + + areaTable[RR_SHADOW_TEMPLE_WHISPERING_WALLS_END] = Region("Shadow Temple Whispering Walls End", SCENE_SHADOW_TEMPLE, {}, { + //Locations LOCATION(RC_SHADOW_TEMPLE_WHISPERING_WALLS_POT_3, logic->CanBreakPots()), LOCATION(RC_SHADOW_TEMPLE_WHISPERING_WALLS_POT_4, logic->CanBreakPots()), LOCATION(RC_SHADOW_TEMPLE_WHISPERING_WALLS_POT_5, logic->CanBreakPots()), - LOCATION(RC_SHADOW_TEMPLE_MAP_CHEST_POT_1, logic->CanBreakPots()), - LOCATION(RC_SHADOW_TEMPLE_MAP_CHEST_POT_2, logic->CanBreakPots()), + LOCATION(RC_SHADOW_TEMPLE_NEAR_DEAD_HAND_POT_1, logic->CanBreakPots()), }, { //Exits - Entrance(RR_SHADOW_TEMPLE_ENTRYWAY, []{return true;}), - Entrance(RR_SHADOW_TEMPLE_FIRST_BEAMOS, []{return logic->CanUse(RG_HOVER_BOOTS);}), + Entrance(RR_SHADOW_TEMPLE_WHISPERING_WALLS_START, []{return ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_SHADOW_TEMPLE_DEAD_HAND, []{return true;}), }); - areaTable[RR_SHADOW_TEMPLE_FIRST_BEAMOS] = Region("Shadow Temple First Beamos", SCENE_SHADOW_TEMPLE, { - //Events - EventAccess(LOGIC_FAIRY_POT, []{return true;}), //This fairy pot is only on 3DS - }, { + areaTable[RR_SHADOW_TEMPLE_WHISPERING_WALLS_SIDE_ROOM] = Region("Shadow Temple Whispering Walls Side Room", SCENE_SHADOW_TEMPLE, {}, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_MAP_CHEST, logic->CanKillEnemy(RE_REDEAD) && logic->CanKillEnemy(RE_KEESE)), + LOCATION(RC_SHADOW_TEMPLE_MAP_CHEST_POT_1, logic->CanBreakPots()), + LOCATION(RC_SHADOW_TEMPLE_MAP_CHEST_POT_2, logic->CanBreakPots()), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_WHISPERING_WALLS_SIDE, []{return Here(RR_SHADOW_TEMPLE_WHISPERING_WALLS_SIDE_ROOM, []{return logic->CanKillEnemy(RE_REDEAD) && logic->CanKillEnemy(RE_KEESE);});}), + }); + + areaTable[RR_SHADOW_TEMPLE_DEAD_HAND] = Region("Shadow Temple Dead Hand", SCENE_SHADOW_TEMPLE, {}, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_HOVER_BOOTS_CHEST, logic->CanKillEnemy(RE_DEAD_HAND)), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_WHISPERING_WALLS_END, []{return Here(RR_SHADOW_TEMPLE_DEAD_HAND, []{return logic->CanKillEnemy(RE_DEAD_HAND);});}), + }); + + areaTable[RR_SHADOW_TEMPLE_FIRST_BEAMOS] = Region("Shadow Temple First Beamos", SCENE_SHADOW_TEMPLE, {}, { //Locations - LOCATION(RC_SHADOW_TEMPLE_COMPASS_CHEST, logic->CanJumpslashExceptHammer()), - LOCATION(RC_SHADOW_TEMPLE_EARLY_SILVER_RUPEE_CHEST, logic->CanUse(RG_HOVER_BOOTS) || logic->CanUse(RG_HOOKSHOT)), - LOCATION(RC_SHADOW_TEMPLE_GS_NEAR_SHIP, false), LOCATION(RC_SHADOW_TEMPLE_BEAMOS_STORM_FAIRY, logic->CanUse(RG_SONG_OF_STORMS)), }, { //Exits - Entrance(RR_SHADOW_TEMPLE_HUGE_PIT, []{return logic->HasExplosives() && logic->IsAdult && logic->SmallKeys(SCENE_SHADOW_TEMPLE, 1);}), - Entrance(RR_SHADOW_TEMPLE_BEYOND_BOAT, []{return false;}), + Entrance(RR_SHADOW_TEMPLE_BEGINNING, []{return ctx->GetTrickOption(RT_VISIBLE_COLLISION) && (logic->CanUse(RG_HOVER_BOOTS) || logic->CanUse(RG_HOOKSHOT));}), + Entrance(RR_SHADOW_TEMPLE_COMPASS_ROOM, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_SPINNING_BLADES, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_B2_TO_B3_CORRIDOR_B2, []{return logic->HasExplosives() && logic->SmallKeys(SCENE_SHADOW_TEMPLE, 1);}), }); - areaTable[RR_SHADOW_TEMPLE_HUGE_PIT] = Region("Shadow Temple Huge Pit", SCENE_SHADOW_TEMPLE, {}, { + areaTable[RR_SHADOW_TEMPLE_COMPASS_ROOM] = Region("Shadow Temple Compass Room", SCENE_SHADOW_TEMPLE, {}, { //Locations - LOCATION(RC_SHADOW_TEMPLE_INVISIBLE_BLADES_VISIBLE_CHEST, logic->CanJumpslashExceptHammer()), - LOCATION(RC_SHADOW_TEMPLE_INVISIBLE_BLADES_INVISIBLE_CHEST, logic->CanJumpslashExceptHammer()), - LOCATION(RC_SHADOW_TEMPLE_FALLING_SPIKES_LOWER_CHEST, true), - LOCATION(RC_SHADOW_TEMPLE_FALLING_SPIKES_UPPER_CHEST, (ctx->GetTrickOption(RT_SHADOW_UMBRELLA_HOVER) && logic->CanUse(RG_HOVER_BOOTS)) || ctx->GetTrickOption(RT_SHADOW_UMBRELLA_CLIP) || logic->HasItem(RG_GORONS_BRACELET)), - LOCATION(RC_SHADOW_TEMPLE_FALLING_SPIKES_SWITCH_CHEST, (ctx->GetTrickOption(RT_SHADOW_UMBRELLA_HOVER) && logic->CanUse(RG_HOVER_BOOTS)) || ctx->GetTrickOption(RT_SHADOW_UMBRELLA_CLIP) || logic->HasItem(RG_GORONS_BRACELET)), - LOCATION(RC_SHADOW_TEMPLE_INVISIBLE_SPIKES_CHEST, logic->SmallKeys(SCENE_SHADOW_TEMPLE, 2) && ((ctx->GetTrickOption(RT_LENS_SHADOW_PLATFORM) && ctx->GetTrickOption(RT_LENS_SHADOW)) || logic->CanUse(RG_LENS_OF_TRUTH))), - LOCATION(RC_SHADOW_TEMPLE_FREESTANDING_KEY, logic->SmallKeys(SCENE_SHADOW_TEMPLE, 2) && ((ctx->GetTrickOption(RT_LENS_SHADOW_PLATFORM) && ctx->GetTrickOption(RT_LENS_SHADOW)) || logic->CanUse(RG_LENS_OF_TRUTH)) && logic->CanUse(RG_HOOKSHOT) && (logic->CanUse(RG_BOMB_BAG) || logic->HasItem(RG_GORONS_BRACELET) || (ctx->GetTrickOption(RT_SHADOW_FREESTANDING_KEY) && logic->CanUse(RG_BOMBCHU_5)))), - LOCATION(RC_SHADOW_TEMPLE_GS_LIKE_LIKE_ROOM, logic->CanJumpslashExceptHammer()), - LOCATION(RC_SHADOW_TEMPLE_GS_FALLING_SPIKES_ROOM, logic->CanUse(RG_HOOKSHOT) || (ctx->GetTrickOption(RT_SHADOW_UMBRELLA_GS) && logic->CanUse(RG_HOVER_BOOTS) && logic->CanStandingShield() && logic->CanUse(RG_MASTER_SWORD)) || (logic->IsAdult && logic->CanGroundJump())), - LOCATION(RC_SHADOW_TEMPLE_GS_SINGLE_GIANT_POT, logic->SmallKeys(SCENE_SHADOW_TEMPLE, 2) && ((ctx->GetTrickOption(RT_LENS_SHADOW_PLATFORM) && ctx->GetTrickOption(RT_LENS_SHADOW)) || logic->CanUse(RG_LENS_OF_TRUTH)) && logic->CanUse(RG_HOOKSHOT)), - LOCATION(RC_SHADOW_TEMPLE_FALLING_SPIKES_POT_1, logic->CanBreakPots()), - LOCATION(RC_SHADOW_TEMPLE_FALLING_SPIKES_POT_2, logic->CanBreakPots()), - LOCATION(RC_SHADOW_TEMPLE_FALLING_SPIKES_POT_3, logic->CanBreakPots() && (ctx->GetTrickOption(RT_SHADOW_UMBRELLA_HOVER) && logic->CanUse(RG_HOVER_BOOTS)) || ctx->GetTrickOption(RT_SHADOW_UMBRELLA_CLIP) || logic->HasItem(RG_GORONS_BRACELET)), - LOCATION(RC_SHADOW_TEMPLE_FALLING_SPIKES_POT_4, logic->CanBreakPots() && (ctx->GetTrickOption(RT_SHADOW_UMBRELLA_HOVER) && logic->CanUse(RG_HOVER_BOOTS)) || ctx->GetTrickOption(RT_SHADOW_UMBRELLA_CLIP) || logic->HasItem(RG_GORONS_BRACELET)), + LOCATION(RC_SHADOW_TEMPLE_COMPASS_CHEST, logic->CanKillEnemy(RE_GIBDO)), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_FIRST_BEAMOS, []{return Here(RR_SHADOW_TEMPLE_COMPASS_ROOM, []{return logic->CanKillEnemy(RE_GIBDO);});}), + }); + + areaTable[RR_SHADOW_TEMPLE_SPINNING_BLADES] = Region("Shadow Temple Spinning Blades", SCENE_SHADOW_TEMPLE, {}, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_EARLY_SILVER_RUPEE_CHEST, (logic->IsAdult && (logic->CanUse(RG_HOVER_BOOTS) || logic->CanGroundJump())) || logic->CanUse(RG_HOOKSHOT)), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_FIRST_BEAMOS, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_DOCK, []{return logic->Get(LOGIC_SHADOW_SHORTCUT_BLOCK);}), + }); + + areaTable[RR_SHADOW_TEMPLE_B2_TO_B3_CORRIDOR_B2] = Region("Shadow Temple B2 to B3 Corridor B2", SCENE_SHADOW_TEMPLE, {}, {}, { + //Exits + Entrance(RR_SHADOW_TEMPLE_FIRST_BEAMOS, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 1);}), + Entrance(RR_SHADOW_TEMPLE_B2_TO_B3_CORRIDOR_B3, []{return logic->CanPassEnemy(RE_BIG_SKULLTULA);}), + }); + + areaTable[RR_SHADOW_TEMPLE_B2_TO_B3_CORRIDOR_B3] = Region("Shadow Temple B2 to B3 Corridor B3", SCENE_SHADOW_TEMPLE, {}, {}, { + //Exits + Entrance(RR_SHADOW_TEMPLE_B2_TO_B3_CORRIDOR_B2, []{return logic->CanUse(RG_HOOKSHOT);}), + Entrance(RR_SHADOW_TEMPLE_UPPER_HUGE_PIT, []{return logic->CanPassEnemy(RE_BIG_SKULLTULA);}), + //bunnyhovers + lens lets you go from the very top of upper pit to the stationary invisible platform below quite easily + }); + + areaTable[RR_SHADOW_TEMPLE_UPPER_HUGE_PIT] = Region("Shadow Temple Upper Huge Pit", SCENE_SHADOW_TEMPLE, {}, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_PIT_STORM_FAIRY, logic->CanUse(RG_SONG_OF_STORMS)), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_B2_TO_B3_CORRIDOR_B3, []{return logic->CanUse(RG_LONGSHOT);}), + Entrance(RR_SHADOW_TEMPLE_UPPER_HUGE_PIT_DOOR_LEDGE, []{return ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_SHADOW_TEMPLE_LOWER_HUGE_PIT, []{return logic->IsAdult || logic->CanJumpslash() || ctx->GetTrickOption(RT_SHADOW_MQ_HUGE_PIT);}), + }); + + areaTable[RR_SHADOW_TEMPLE_UPPER_HUGE_PIT_DOOR_LEDGE] = Region("Shadow Temple Upper Huge Pit Door Ledge", SCENE_SHADOW_TEMPLE, {}, {}, { + //Exits + Entrance(RR_SHADOW_TEMPLE_UPPER_HUGE_PIT, []{return ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_SHADOW_TEMPLE_INVISIBLE_SPINNING_BLADES, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 2);}), + }); + + areaTable[RR_SHADOW_TEMPLE_LOWER_HUGE_PIT] = Region("Shadow Temple Lower Huge Pit", SCENE_SHADOW_TEMPLE, {}, {},{ + //Exits + Entrance(RR_SHADOW_TEMPLE_UPPER_HUGE_PIT, []{return logic->IsAdult || logic->CanJumpslash();}), + Entrance(RR_SHADOW_TEMPLE_LOWER_HUGE_PIT_DOOR_LEDGE, []{return (ctx->GetTrickOption(RT_LENS_SHADOW_PLATFORM) && ctx->GetTrickOption(RT_LENS_SHADOW)) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_SHADOW_TEMPLE_STONE_UMBRELLA, []{return true;}), + }); + + // See MQ for comments + areaTable[RR_SHADOW_TEMPLE_STONE_UMBRELLA] = Region("Shadow Temple Stone Umbrella", SCENE_SHADOW_TEMPLE, {}, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_FALLING_SPIKES_LOWER_CHEST, true), + LOCATION(RC_SHADOW_TEMPLE_GS_FALLING_SPIKES_ROOM, logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA, ED_BOOMERANG) || (logic->IsAdult && ctx->GetTrickOption(RT_GROUND_JUMP_HARD) && logic->CanGroundJump() && logic->CanJumpslash())), + LOCATION(RC_SHADOW_TEMPLE_FALLING_SPIKES_POT_1, logic->CanBreakPots()), + LOCATION(RC_SHADOW_TEMPLE_FALLING_SPIKES_POT_2, logic->CanBreakPots()), + LOCATION(RC_SHADOW_TEMPLE_FALLING_SPIKES_POT_3, logic->CanUse(RG_BOOMERANG)), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_LOWER_HUGE_PIT, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_STONE_UMBRELLA_UPPER, []{return ctx->GetTrickOption(RT_SHADOW_UMBRELLA_CLIP) || (ctx->GetTrickOption(RT_DAMAGE_BOOST_SIMPLE) && logic->TakeDamage()) || (logic->IsAdult && ((ctx->GetTrickOption(RT_SHADOW_UMBRELLA_HOVER) && logic->CanUse(RG_HOVER_BOOTS)) || logic->HasItem(RG_GORONS_BRACELET)));}), + }); + + areaTable[RR_SHADOW_TEMPLE_STONE_UMBRELLA_UPPER] = Region("Shadow Temple Stone Umbrella Upper", SCENE_SHADOW_TEMPLE, {}, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_FALLING_SPIKES_UPPER_CHEST, true), + LOCATION(RC_SHADOW_TEMPLE_FALLING_SPIKES_SWITCH_CHEST, true), + //Assuming the known setup for RT_SHADOW_UMBRELLA_HOVER and RT_SHADOW_UMBRELLA_GS, probably possible without sword + shield. + LOCATION(RC_SHADOW_TEMPLE_GS_FALLING_SPIKES_ROOM, ctx->GetTrickOption(RT_SHADOW_UMBRELLA_GS) && logic->CanUse(RG_HOVER_BOOTS) && logic->CanStandingShield() && logic->CanUse(RG_MASTER_SWORD)), + LOCATION(RC_SHADOW_TEMPLE_FALLING_SPIKES_POT_3, logic->CanBreakPots()), + LOCATION(RC_SHADOW_TEMPLE_FALLING_SPIKES_POT_4, logic->CanBreakPots()), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_STONE_UMBRELLA, []{return true;}), + }); + + areaTable[RR_SHADOW_TEMPLE_LOWER_HUGE_PIT_DOOR_LEDGE] = Region("Shadow Temple Lower Huge Pit Door Ledge", SCENE_SHADOW_TEMPLE, {}, {}, { + //Exits + Entrance(RR_SHADOW_TEMPLE_LOWER_HUGE_PIT, []{return (ctx->GetTrickOption(RT_LENS_SHADOW_PLATFORM) && ctx->GetTrickOption(RT_LENS_SHADOW)) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_SHADOW_TEMPLE_INVISIBLE_SPIKES, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 2);}), + }); + + areaTable[RR_SHADOW_TEMPLE_INVISIBLE_SPINNING_BLADES] = Region("Shadow Temple Invisible Spinning Blades", SCENE_SHADOW_TEMPLE, {}, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_INVISIBLE_BLADES_VISIBLE_CHEST, logic->CanKillEnemy(RE_LIKE_LIKE) && logic->CanKillEnemy(RE_KEESE)), + LOCATION(RC_SHADOW_TEMPLE_INVISIBLE_BLADES_INVISIBLE_CHEST, logic->CanKillEnemy(RE_LIKE_LIKE) && logic->CanKillEnemy(RE_KEESE) && (ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH))), + LOCATION(RC_SHADOW_TEMPLE_GS_LIKE_LIKE_ROOM, Here(RR_SHADOW_TEMPLE_INVISIBLE_SPINNING_BLADES, []{return logic->CanKillEnemy(RE_LIKE_LIKE) && logic->CanKillEnemy(RE_KEESE);}) && ((logic->IsAdult && logic->CanKillEnemy(RE_GOLD_SKULLTULA, ED_SHORT_JUMPSLASH)) || logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA, ED_BOOMERANG))), //We cannot repeat the MQ invisible blades trick for these hearts as the like-like does not respawn if the room is cleared LOCATION(RC_SHADOW_TEMPLE_INVISIBLE_BLADES_LEFT_HEART, (logic->CanUse(RG_SONG_OF_TIME) && logic->IsAdult) || logic->CanUse(RG_BOOMERANG)), LOCATION(RC_SHADOW_TEMPLE_INVISIBLE_BLADES_RIGHT_HEART, (logic->CanUse(RG_SONG_OF_TIME) && logic->IsAdult) || logic->CanUse(RG_BOOMERANG)), - LOCATION(RC_SHADOW_TEMPLE_PIT_STORM_FAIRY, logic->CanUse(RG_SONG_OF_STORMS)), }, { //Exits - Entrance(RR_SHADOW_TEMPLE_WIND_TUNNEL, []{return ((ctx->GetTrickOption(RT_LENS_SHADOW_PLATFORM) && ctx->GetTrickOption(RT_LENS_SHADOW)) || logic->CanUse(RG_LENS_OF_TRUTH)) && (logic->CanUse(RG_HOOKSHOT) || (ctx->GetTrickOption(RT_GROUND_JUMP_HARD) && logic->CanGroundJump())) && logic->SmallKeys(SCENE_SHADOW_TEMPLE, 3);}), + Entrance(RR_SHADOW_TEMPLE_UPPER_HUGE_PIT_DOOR_LEDGE, []{return true;}), }); - areaTable[RR_SHADOW_TEMPLE_WIND_TUNNEL] = Region("Shadow Temple Wind Tunnel", SCENE_SHADOW_TEMPLE, {}, { + areaTable[RR_SHADOW_TEMPLE_INVISIBLE_SPIKES] = Region("Shadow Temple Invisible Spikes", SCENE_SHADOW_TEMPLE, {}, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_INVISIBLE_SPIKES_CHEST, logic->CanKillEnemy(RE_REDEAD) && (ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH) || logic->TakeDamage() || logic->CanUse(RG_GORON_TUNIC))), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_LOWER_HUGE_PIT_DOOR_LEDGE, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 2);}), + Entrance(RR_SHADOW_TEMPLE_SKULL_JAR, []{return (ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH)) && (logic->CanUse(RG_HOOKSHOT) || (ctx->GetTrickOption(RT_GROUND_JUMP_HARD) && logic->CanGroundJump() && logic->IsAdult && logic->CanUse(RG_HOVER_BOOTS)));}), + Entrance(RR_SHADOW_TEMPLE_INVISIBLE_SPIKES_PLATFORM, []{return (ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH)) && ((ctx->GetTrickOption(RT_GROUND_JUMP_HARD) && logic->CanGroundJump() && logic->IsAdult && logic->CanUse(RG_HOVER_BOOTS)) || + logic->CanUse(Here(RR_SHADOW_TEMPLE_INVISIBLE_SPIKES, []{return logic->CanKillEnemy(RE_REDEAD) && (ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH) || logic->TakeDamage() || logic->CanUse(RG_GORON_TUNIC));}) ? RG_HOOKSHOT : RG_LONGSHOT));}), + }); + + areaTable[RR_SHADOW_TEMPLE_INVISIBLE_SPIKES_PLATFORM] = Region("Shadow Temple Invisible Spikes Platform", SCENE_SHADOW_TEMPLE, {}, {}, { + //Exits + Entrance(RR_SHADOW_TEMPLE_INVISIBLE_SPIKES, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_UPPER_WIND_TUNNEL, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 3);}), + }); + + areaTable[RR_SHADOW_TEMPLE_SKULL_JAR] = Region("Shadow Temple Skull Jar", SCENE_SHADOW_TEMPLE, {}, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_FREESTANDING_KEY, logic->CanUse(RG_BOMB_BAG) || logic->HasItem(RG_GORONS_BRACELET) || (ctx->GetTrickOption(RT_SHADOW_FREESTANDING_KEY) && logic->CanUse(RG_BOMBCHU_5))), + LOCATION(RC_SHADOW_TEMPLE_GS_SINGLE_GIANT_POT, logic->CanKillEnemy(RE_GOLD_SKULLTULA)), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_INVISIBLE_SPIKES, []{return Here(RR_SHADOW_TEMPLE_SKULL_JAR, []{return logic->CanKillEnemy(RE_KEESE);});}), + }); + + areaTable[RR_SHADOW_TEMPLE_UPPER_WIND_TUNNEL] = Region("Shadow Temple Upper Wind Tunnel", SCENE_SHADOW_TEMPLE, {}, {}, { + //Exits + Entrance(RR_SHADOW_TEMPLE_INVISIBLE_SPIKES_PLATFORM, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 3);}), + Entrance(RR_SHADOW_TEMPLE_LOWER_WIND_TUNNEL, []{return (logic->CanUse(RG_HOVER_BOOTS) && logic->CanPassEnemy(RE_BIG_SKULLTULA)) || logic->CanUse(RG_HOOKSHOT);}), + }); + + areaTable[RR_SHADOW_TEMPLE_LOWER_WIND_TUNNEL] = Region("Shadow Temple Lower Wind Tunnel", SCENE_SHADOW_TEMPLE, {}, {}, { + //Exits + Entrance(RR_SHADOW_TEMPLE_UPPER_WIND_TUNNEL, []{return logic->CanUse(RG_HOOKSHOT);}), + Entrance(RR_SHADOW_TEMPLE_WIND_TUNNEL_ALCOVE, []{return ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_SHADOW_TEMPLE_WIND_TUNNEL_HINT_ROOM, []{return true;}), + }); + + areaTable[RR_SHADOW_TEMPLE_WIND_TUNNEL_ALCOVE] = Region("Shadow Temple Wind Tunnel Alcove", SCENE_SHADOW_TEMPLE, {}, {}, { + Entrance(RR_SHADOW_TEMPLE_LOWER_WIND_TUNNEL, []{return (ctx->GetTrickOption(RT_SHADOW_MQ_WINDY_WALKWAY)) || logic->CanUse(RG_HOVER_BOOTS);}), + Entrance(RR_SHADOW_TEMPLE_ROOM_TO_BOAT, []{return true;}), + }); + + areaTable[RR_SHADOW_TEMPLE_WIND_TUNNEL_HINT_ROOM] = Region("Shadow Temple Wind Tunnel Hint Room", SCENE_SHADOW_TEMPLE, {}, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_WIND_HINT_CHEST, logic->CanKillEnemy(RE_REDEAD)), + LOCATION(RC_SHADOW_TEMPLE_WIND_HINT_SUN_FAIRY, logic->CanUse(RG_SUNS_SONG)), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_LOWER_WIND_TUNNEL, []{return logic->CanKillEnemy(RE_REDEAD);}), + }); + + areaTable[RR_SHADOW_TEMPLE_ROOM_TO_BOAT] = Region("Shadow Temple Room to Boat", SCENE_SHADOW_TEMPLE, {}, { //Locations - LOCATION(RC_SHADOW_TEMPLE_WIND_HINT_CHEST, true), LOCATION(RC_SHADOW_TEMPLE_AFTER_WIND_ENEMY_CHEST, logic->CanKillEnemy(RE_GIBDO, ED_CLOSE, true, 2)), - LOCATION(RC_SHADOW_TEMPLE_AFTER_WIND_HIDDEN_CHEST, logic->HasExplosives()), - LOCATION(RC_SHADOW_TEMPLE_GS_NEAR_SHIP, logic->CanUse(RG_LONGSHOT) && logic->SmallKeys(SCENE_SHADOW_TEMPLE, 4)), - LOCATION(RC_SHADOW_TEMPLE_WIND_HINT_SUN_FAIRY, logic->CanUse(RG_SUNS_SONG)), + LOCATION(RC_SHADOW_TEMPLE_AFTER_WIND_HIDDEN_CHEST, (ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH)) && logic->HasExplosives()), LOCATION(RC_SHADOW_TEMPLE_AFTER_WIND_POT_1, logic->CanBreakPots()), LOCATION(RC_SHADOW_TEMPLE_AFTER_WIND_POT_2, logic->CanBreakPots()), - LOCATION(RC_SHADOW_TEMPLE_SCARECROW_NORTH_HEART, logic->CanUse(RG_DISTANT_SCARECROW) && logic->SmallKeys(SCENE_SHADOW_TEMPLE, 4)), - LOCATION(RC_SHADOW_TEMPLE_SCARECROW_SOUTH_HEART, logic->CanUse(RG_DISTANT_SCARECROW) && logic->SmallKeys(SCENE_SHADOW_TEMPLE, 4)), }, { //Exits - Entrance(RR_SHADOW_TEMPLE_BEYOND_BOAT, []{return logic->CanJumpslashExceptHammer() && logic->CanUse(RG_ZELDAS_LULLABY) && logic->SmallKeys(SCENE_SHADOW_TEMPLE, 4);}), + Entrance(RR_SHADOW_TEMPLE_WIND_TUNNEL_ALCOVE, []{return Here(RR_SHADOW_TEMPLE_ROOM_TO_BOAT, []{return logic->CanKillEnemy(RE_GIBDO, ED_CLOSE, true, 2);});}), + Entrance(RR_SHADOW_TEMPLE_DOCK, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 4);}), }); - areaTable[RR_SHADOW_TEMPLE_BEYOND_BOAT] = Region("Shadow Temple Beyond Boat", SCENE_SHADOW_TEMPLE, {}, { + areaTable[RR_SHADOW_TEMPLE_DOCK] = Region("Shadow Temple Dock", SCENE_SHADOW_TEMPLE, { + //Event + EventAccess(LOGIC_SHADOW_SHORTCUT_BLOCK, []{return logic->HasItem(RG_GORONS_BRACELET);}), + }, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_GS_NEAR_SHIP, logic->CanUse(RG_LONGSHOT)), + LOCATION(RC_SHADOW_TEMPLE_SCARECROW_NORTH_HEART, logic->CanUse(RG_DISTANT_SCARECROW)), + LOCATION(RC_SHADOW_TEMPLE_SCARECROW_SOUTH_HEART, logic->CanUse(RG_DISTANT_SCARECROW)), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_ROOM_TO_BOAT, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 4);}), + Entrance(RR_SHADOW_TEMPLE_SPINNING_BLADES, []{return logic->HasItem(RG_GORONS_BRACELET);}), + Entrance(RR_SHADOW_TEMPLE_BEYOND_BOAT, []{return ((logic->IsAdult && logic->HasItem(RG_GORONS_BRACELET)) || logic->CanUse(RG_HOOKSHOT)) && logic->CanUse(RG_ZELDAS_LULLABY);}), + }); + + areaTable[RR_SHADOW_TEMPLE_BEYOND_BOAT] = Region("Shadow Temple Beyond Boat", SCENE_SHADOW_TEMPLE, { + //Events + EventAccess(LOGIC_SHADOW_BRIDGE_BEYOND_BOAT_LOWERED, []{return logic->CanUse(RG_FAIRY_BOW) || (ctx->GetTrickOption(RT_SHADOW_STATUE) && logic->CanUse(RG_BOMBCHU_5));}) + }, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_AFTER_BOAT_POT_1, logic->CanBreakPots()), + LOCATION(RC_SHADOW_TEMPLE_AFTER_BOAT_POT_2, logic->CanBreakPots()), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_MAZE, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_ACROSS_CHASM, []{return logic->Get(LOGIC_SHADOW_BRIDGE_BEYOND_BOAT_LOWERED) || logic->CanUse(RG_DISTANT_SCARECROW);}), + }); + + areaTable[RR_SHADOW_TEMPLE_ACROSS_CHASM] = Region("Shadow Temple Across Chasm", SCENE_SHADOW_TEMPLE, { + //Events + EventAccess(LOGIC_SHADOW_BRIDGE_BEYOND_BOAT_LOWERED, []{return logic->CanDetonateUprightBombFlower();}) + }, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_AFTER_BOAT_POT_3, logic->CanBreakPots()), + LOCATION(RC_SHADOW_TEMPLE_AFTER_BOAT_POT_4, logic->CanBreakPots()), + // don't actually need to use hookshot extension + LOCATION(RC_SHADOW_TEMPLE_AFTER_SHIP_UPPER_LEFT_HEART, logic->CanUse(ctx->GetTrickOption(RT_HOOKSHOT_EXTENSION) && logic->IsAdult && logic->CanUse(RG_SONG_OF_TIME) ? RG_SCARECROW : RG_DISTANT_SCARECROW)), + LOCATION(RC_SHADOW_TEMPLE_AFTER_SHIP_UPPER_RIGHT_HEART, logic->CanUse(ctx->GetTrickOption(RT_HOOKSHOT_EXTENSION) && logic->IsAdult && logic->CanUse(RG_SONG_OF_TIME) ? RG_SCARECROW : RG_DISTANT_SCARECROW)), + // can reach with logic->IsAdult && logic->CanUse(RG_DISTANT_SCARECROW) && logic->CanJumpslash(), but precise enough to be trick + LOCATION(RC_SHADOW_TEMPLE_AFTER_SHIP_LOWER_HEART, (logic->IsAdult && logic->CanUse(RG_SONG_OF_TIME)) || (logic->CanUse(RG_DISTANT_SCARECROW) && logic->CanUse(RG_HOVER_BOOTS))), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_BEYOND_BOAT, []{return logic->Get(LOGIC_SHADOW_BRIDGE_BEYOND_BOAT_LOWERED) && logic->IsAdult;}), + Entrance(RR_SHADOW_TEMPLE_PRE_BOSS_ROOM, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 5);}), + }); + + areaTable[RR_SHADOW_TEMPLE_MAZE] = Region("Shadow Temple Maze", SCENE_SHADOW_TEMPLE, {}, {}, { + //Exits + Entrance(RR_SHADOW_TEMPLE_BEYOND_BOAT, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_X_CROSS, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_THREE_SKULL_JARS, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_WOODEN_SPIKES, []{return true;}), + }); + + areaTable[RR_SHADOW_TEMPLE_X_CROSS] = Region("Shadow Temple X-Cross", SCENE_SHADOW_TEMPLE, {}, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_INVISIBLE_FLOORMASTER_CHEST, (ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH)) && logic->CanKillEnemy(RE_FLOORMASTER)), + LOCATION(RC_SHADOW_TEMPLE_FLOORMASTER_POT_1, logic->CanBreakPots()), + LOCATION(RC_SHADOW_TEMPLE_FLOORMASTER_POT_2, logic->CanBreakPots()), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_MAZE, []{return Here(RR_SHADOW_TEMPLE_X_CROSS, []{return (ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH)) && logic->CanKillEnemy(RE_FLOORMASTER);});}), + }); + + areaTable[RR_SHADOW_TEMPLE_THREE_SKULL_JARS] = Region("Shadow Temple Three Skull Jars", SCENE_SHADOW_TEMPLE, {}, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_GS_TRIPLE_GIANT_POT, logic->HasItem(RG_GORONS_BRACELET) || logic->CanKillEnemy(RE_GOLD_SKULLTULA, ED_SHORT_JUMPSLASH)), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_MAZE, []{return true;}), + }); + + areaTable[RR_SHADOW_TEMPLE_WOODEN_SPIKES] = Region("Shadow Temple Wooden Spikes", SCENE_SHADOW_TEMPLE, {}, { //Locations LOCATION(RC_SHADOW_TEMPLE_SPIKE_WALLS_LEFT_CHEST, logic->CanUse(RG_DINS_FIRE)), LOCATION(RC_SHADOW_TEMPLE_BOSS_KEY_CHEST, logic->CanUse(RG_DINS_FIRE)), - LOCATION(RC_SHADOW_TEMPLE_INVISIBLE_FLOORMASTER_CHEST, logic->CanKillEnemy(RE_FLOORMASTER)), - //RANDOTODO check if child can reach the token - LOCATION(RC_SHADOW_TEMPLE_GS_TRIPLE_GIANT_POT, logic->IsAdult && logic->CanAttack()), - LOCATION(RC_SHADOW_TEMPLE_AFTER_BOAT_POT_1, logic->CanBreakPots()), - LOCATION(RC_SHADOW_TEMPLE_AFTER_BOAT_POT_2, logic->CanBreakPots()), - LOCATION(RC_SHADOW_TEMPLE_AFTER_BOAT_POT_3, logic->CanBreakPots() && (logic->CanUse(RG_FAIRY_BOW) || logic->CanUse(RG_DISTANT_SCARECROW) || (ctx->GetTrickOption(RT_SHADOW_STATUE) && logic->CanUse(RG_BOMBCHU_5)))), - LOCATION(RC_SHADOW_TEMPLE_AFTER_BOAT_POT_4, logic->CanBreakPots() && (logic->CanUse(RG_FAIRY_BOW) || logic->CanUse(RG_DISTANT_SCARECROW) || (ctx->GetTrickOption(RT_SHADOW_STATUE) && logic->CanUse(RG_BOMBCHU_5)))), LOCATION(RC_SHADOW_TEMPLE_SPIKE_WALLS_POT_1, logic->CanBreakPots()), - LOCATION(RC_SHADOW_TEMPLE_FLOORMASTER_POT_1, logic->CanBreakPots()), - LOCATION(RC_SHADOW_TEMPLE_FLOORMASTER_POT_2, logic->CanBreakPots()), - LOCATION(RC_SHADOW_TEMPLE_AFTER_SHIP_UPPER_LEFT_HEART, logic->CanUse(RG_DISTANT_SCARECROW)), - LOCATION(RC_SHADOW_TEMPLE_AFTER_SHIP_UPPER_RIGHT_HEART, logic->CanUse(RG_DISTANT_SCARECROW)), - LOCATION(RC_SHADOW_TEMPLE_AFTER_SHIP_LOWER_HEART, (logic->CanUse(RG_FAIRY_BOW) || logic->CanUse(RG_DISTANT_SCARECROW) || (ctx->GetTrickOption(RT_SHADOW_STATUE) && logic->CanUse(RG_BOMBCHU_5))) && logic->CanUse(RG_SONG_OF_TIME) || (logic->CanUse(RG_DISTANT_SCARECROW) && logic->CanUse(RG_HOVER_BOOTS))), }, { //Exits - Entrance(RR_SHADOW_TEMPLE_BOSS_ENTRYWAY, []{return (logic->CanUse(RG_FAIRY_BOW) || logic->CanUse(RG_DISTANT_SCARECROW) || (ctx->GetTrickOption(RT_SHADOW_STATUE) && logic->CanUse(RG_BOMBCHU_5))) && logic->SmallKeys(SCENE_SHADOW_TEMPLE, 5) && logic->CanUse(RG_HOVER_BOOTS);}) + Entrance(RR_SHADOW_TEMPLE_MAZE, []{return true;}), + }); + + areaTable[RR_SHADOW_TEMPLE_PRE_BOSS_ROOM] = Region("Shadow Temple Pre Boss Room", SCENE_SHADOW_TEMPLE, {}, {}, { + //Exits + Entrance(RR_SHADOW_TEMPLE_BEYOND_BOAT, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 5);}), + Entrance(RR_SHADOW_TEMPLE_BOSS_DOOR, []{return (ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH)) && logic->CanUse(RG_HOVER_BOOTS);}), + }); + + areaTable[RR_SHADOW_TEMPLE_BOSS_DOOR] = Region("Shadow Temple Boss Door", SCENE_SHADOW_TEMPLE, {}, {}, { + //Exits + Entrance(RR_SHADOW_TEMPLE_PRE_BOSS_ROOM, []{return (ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH)) && logic->CanUse(RG_HOVER_BOOTS);}), + Entrance(RR_SHADOW_TEMPLE_BOSS_ENTRYWAY, []{return true;}), }); #pragma endregion #pragma region MQ - //RANDOTODO doublecheck CanAttack when rewriting, as I assumed it only checked adult due to the entrance areaTable[RR_SHADOW_TEMPLE_MQ_BEGINNING] = Region("Shadow Temple MQ Beginning", SCENE_SHADOW_TEMPLE, {}, {}, { //Exits - Entrance(RR_SHADOW_TEMPLE_ENTRYWAY, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_ENTRYWAY, []{return (ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH)) && (logic->CanUse(RG_HOVER_BOOTS) || logic->CanUse(RG_HOOKSHOT));}), Entrance(RR_SHADOW_TEMPLE_MQ_SPINNER_ROOM, []{return logic->CanUse(RG_HOVER_BOOTS) || logic->CanUse(RG_HOOKSHOT);}), }); @@ -133,44 +367,83 @@ void RegionTable_Init_ShadowTemple() { LOCATION(RC_SHADOW_TEMPLE_MQ_TRUTH_SPINNER_SMALL_CRATE_2, logic->CanBreakSmallCrates()), LOCATION(RC_SHADOW_TEMPLE_MQ_TRUTH_SPINNER_SMALL_CRATE_3, logic->CanBreakSmallCrates()), LOCATION(RC_SHADOW_TEMPLE_MQ_TRUTH_SPINNER_SMALL_CRATE_4, logic->CanBreakSmallCrates()), - }, - { - //Exits - Entrance(RR_SHADOW_TEMPLE_ENTRYWAY, []{return true;}), - Entrance(RR_SHADOW_TEMPLE_MQ_FIRST_BEAMOS, []{return Here(RR_SHADOW_TEMPLE_MQ_SPINNER_ROOM, []{return logic->CanUse(RG_HOVER_BOOTS) || (ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH));}) && (logic->CanUse(RG_HOVER_BOOTS) || Here(RR_SHADOW_TEMPLE_MQ_SPINNER_ROOM, []{return logic->CanUse(RG_FIRE_ARROWS);}) || (ctx->GetTrickOption(RT_SHADOW_MQ_GAP) && logic->CanUse(RG_LONGSHOT) && logic->CanJumpslashExceptHammer()));}), - Entrance(RR_SHADOW_TEMPLE_MQ_DEAD_HAND_AREA, []{return Here(RR_SHADOW_TEMPLE_MQ_SPINNER_ROOM, []{return logic->HasExplosives();}) && logic->SmallKeys(SCENE_SHADOW_TEMPLE, 6) && (ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH));}), - }); - - //Assumes we're in the "main" area and needed lens to enter. logic will need changes if a void warp puts us somewhere weird - areaTable[RR_SHADOW_TEMPLE_MQ_DEAD_HAND_AREA] = Region("Shadow Temple MQ Dead Hand Region", SCENE_SHADOW_TEMPLE, {}, { - //Locations - LOCATION(RC_SHADOW_TEMPLE_MQ_COMPASS_CHEST, logic->CanKillEnemy(RE_REDEAD)), - //There's a shared flag tied to some glass here. eye target here and killing an enemy group later in the dungeon toggles. I'm building the logic as "intended", assuming the switch needs flipping - LOCATION(RC_SHADOW_TEMPLE_MQ_HOVER_BOOTS_CHEST, logic->CanKillEnemy(RE_DEAD_HAND) && (logic->IsChild || logic->CanUse(RG_SONG_OF_TIME)) && logic->CanHitEyeTargets()), - LOCATION(RC_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_POT_1, logic->CanBreakPots()), - LOCATION(RC_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_POT_2, logic->CanBreakPots()), - LOCATION(RC_SHADOW_TEMPLE_MQ_ENTRANCE_REDEAD_POT_1, logic->CanBreakPots()), - LOCATION(RC_SHADOW_TEMPLE_MQ_ENTRANCE_REDEAD_POT_2, logic->CanBreakPots()), }, { //Exits - Entrance(RR_SHADOW_TEMPLE_MQ_SPINNER_ROOM, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_ENTRYWAY, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_MQ_FIRST_BEAMOS, []{return Here(RR_SHADOW_TEMPLE_MQ_SPINNER_ROOM, []{return logic->CanUse(RG_HOVER_BOOTS) || (ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH));}) && (logic->CanUse(RG_HOVER_BOOTS) || Here(RR_SHADOW_TEMPLE_MQ_SPINNER_ROOM, []{return logic->CanUse(RG_FIRE_ARROWS);}) || (ctx->GetTrickOption(RT_SHADOW_MQ_GAP) && logic->CanUse(RG_LONGSHOT) && logic->CanJumpslashExceptHammer()));}), + Entrance(RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_START, []{return Here(RR_SHADOW_TEMPLE_MQ_SPINNER_ROOM, []{return logic->HasExplosives();}) && logic->SmallKeys(SCENE_SHADOW_TEMPLE, 6) && (ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH));}), + }); + + areaTable[RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_START] = Region("Shadow Temple MQ Whispering Walls Start", SCENE_SHADOW_TEMPLE, {}, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_POT_1, logic->CanBreakPots()), + LOCATION(RC_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_POT_2, logic->CanBreakPots()), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_MQ_SPINNER_ROOM, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_SIDE, []{return ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_END, []{return (ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH)) && (logic->IsChild || logic->CanUse(RG_SONG_OF_TIME));}), + }); + + // shares RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_START area with pots, but handles lens access for reaching door at start + areaTable[RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_SIDE] = Region("Shadow Temple MQ Whispering Walls Side", SCENE_SHADOW_TEMPLE, {}, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_POT_1, logic->CanBreakPots()), + LOCATION(RC_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_POT_2, logic->CanBreakPots()), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_WHISPERING_WALLS_START, []{return ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_SIDE_ROOM, []{return true;}), + }); + + areaTable[RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_END] = Region("Shadow Temple MQ Whispering Walls End", SCENE_SHADOW_TEMPLE, {}, {}, { + //Exits + Entrance(RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_START, []{return (ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH)) && (logic->IsChild || logic->CanUse(RG_SONG_OF_TIME));}), + //There's a shared flag tied to some glass here. eye target here and killing an enemy group later in the dungeon toggles. I'm building the logic as "intended", assuming the switch needs flipping + Entrance(RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_DEAD_HAND, []{return logic->CanHitEyeTargets();}), + }); + + areaTable[RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_SIDE_ROOM] = Region("Shadow Temple MQ Whispering Walls Redeads", SCENE_SHADOW_TEMPLE, {}, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_MQ_COMPASS_CHEST, logic->CanKillEnemy(RE_REDEAD)), + LOCATION(RC_SHADOW_TEMPLE_MQ_ENTRANCE_REDEAD_POT_1, logic->CanBreakPots()), + LOCATION(RC_SHADOW_TEMPLE_MQ_ENTRANCE_REDEAD_POT_2, logic->CanBreakPots()), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_SIDE, []{return Here(RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_SIDE_ROOM, []{return logic->CanKillEnemy(RE_REDEAD);});}), + }); + + areaTable[RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_DEAD_HAND] = Region("Shadow Temple MQ Whispering Walls Dead Hand", SCENE_SHADOW_TEMPLE, {}, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_MQ_HOVER_BOOTS_CHEST, logic->CanKillEnemy(RE_DEAD_HAND)), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_END, []{return Here(RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_DEAD_HAND, []{return logic->CanKillEnemy(RE_DEAD_HAND);});}), }); - //also includes the B2 gibdo room areaTable[RR_SHADOW_TEMPLE_MQ_FIRST_BEAMOS] = Region("Shadow Temple MQ First Beamos", SCENE_SHADOW_TEMPLE, {}, { //Locations - //Doing this sets the shared flag for the glass in RR_SHADOW_TEMPLE_MQ_DEAD_HAND_AREA, but doesn't seem to affect the chest - LOCATION(RC_SHADOW_TEMPLE_MQ_EARLY_GIBDOS_CHEST, logic->CanKillEnemy(RE_GIBDO) && (ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH))), LOCATION(RC_SHADOW_TEMPLE_MQ_BEAMOS_STORM_FAIRY, logic->CanUse(RG_SONG_OF_STORMS)), }, { //Exits - Entrance(RR_SHADOW_TEMPLE_MQ_UPPER_HUGE_PIT, []{return logic->HasExplosives() && logic->SmallKeys(SCENE_SHADOW_TEMPLE, 2);}), - Entrance(RR_SHADOW_TEMPLE_MQ_B2_SPINNING_BLADE_ROOM, []{return ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_SHADOW_TEMPLE_MQ_SPINNER_ROOM, []{return ctx->GetTrickOption(RT_VISIBLE_COLLISION) && (logic->CanUse(RG_HOVER_BOOTS) || logic->HasFireSource());}), + Entrance(RR_SHADOW_TEMPLE_MQ_B2_GIBDO_ROOM, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_MQ_B2_TO_B3_CORRIDOR_B2, []{return logic->HasExplosives() && logic->SmallKeys(SCENE_SHADOW_TEMPLE, 2);}), + Entrance(RR_SHADOW_TEMPLE_MQ_B2_SPINNING_BLADE_ROOM, []{return true;}), + }); + + areaTable[RR_SHADOW_TEMPLE_MQ_B2_GIBDO_ROOM] = Region("Shadow Temple MQ B2 Gibdo Room", SCENE_SHADOW_TEMPLE, {}, { + //Locations + //Doing this sets the shared flag for the glass in RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS, but doesn't seem to affect the chest + LOCATION(RC_SHADOW_TEMPLE_MQ_EARLY_GIBDOS_CHEST, logic->CanKillEnemy(RE_GIBDO) && (ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH))), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_MQ_FIRST_BEAMOS, []{return Here(RR_SHADOW_TEMPLE_MQ_B2_GIBDO_ROOM, []{return logic->CanKillEnemy(RE_GIBDO);});}), }); areaTable[RR_SHADOW_TEMPLE_MQ_B2_SPINNING_BLADE_ROOM] = Region("Shadow Temple MQ B2 Spinning Blade Room", SCENE_SHADOW_TEMPLE, {}, { //Locations - LOCATION(RC_SHADOW_TEMPLE_MQ_MAP_CHEST, logic->CanPassEnemy(RE_BIG_SKULLTULA) && (logic->CanUse(RG_HOOKSHOT) || (logic->IsAdult && logic->CanUse(RG_HOVER_BOOTS)))), + LOCATION(RC_SHADOW_TEMPLE_MQ_MAP_CHEST, logic->CanPassEnemy(RE_BIG_SKULLTULA) && (logic->CanUse(RG_HOOKSHOT) || (logic->IsAdult && (logic->CanUse(RG_HOVER_BOOTS) || logic->CanGroundJump())))), }, { //Exits Entrance(RR_SHADOW_TEMPLE_MQ_FIRST_BEAMOS, []{return Here(RR_SHADOW_TEMPLE_MQ_B2_SPINNING_BLADE_ROOM, []{return logic->CanKillEnemy(RE_BIG_SKULLTULA) && (logic->CanUse(RG_HOOKSHOT) || (logic->IsAdult && logic->CanUse(RG_HOVER_BOOTS)));});}), @@ -187,21 +460,34 @@ void RegionTable_Init_ShadowTemple() { //WARNING if there's any way past here to ship without already reaching the other side the key logic in this dungeon becomes Quantum }); - //Room exists for if it's ever possible to go backwards or void warp into the middle of shadow - areaTable[RR_SHADOW_TEMPLE_MQ_B2_TO_B3_CORRIDOR] = Region("Shadow Temple MQ B2 to B3 Corridor", SCENE_SHADOW_TEMPLE, {}, {}, { + areaTable[RR_SHADOW_TEMPLE_MQ_B2_TO_B3_CORRIDOR_B2] = Region("Shadow Temple MQ B2 to B3 Corridor B2", SCENE_SHADOW_TEMPLE, {}, {}, { //Exits Entrance(RR_SHADOW_TEMPLE_MQ_FIRST_BEAMOS, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 2);}), Entrance(RR_SHADOW_TEMPLE_MQ_UPPER_HUGE_PIT, []{return true;}), + }); + + areaTable[RR_SHADOW_TEMPLE_MQ_B2_TO_B3_CORRIDOR_B3] = Region("Shadow Temple MQ B2 to B3 Corridor B3", SCENE_SHADOW_TEMPLE, {}, {}, { + //Exits + Entrance(RR_SHADOW_TEMPLE_MQ_B2_TO_B3_CORRIDOR_B3, []{return logic->CanUse(RG_HOOKSHOT);}), + Entrance(RR_SHADOW_TEMPLE_MQ_UPPER_HUGE_PIT, []{return true;}), //bunnyhovers + lens lets you go from the very top of upper pit to the stationary invisible platform below quite easily }); - areaTable[RR_SHADOW_TEMPLE_MQ_UPPER_HUGE_PIT] = Region("Shadow Temple MQ Upper Huge Pit", SCENE_SHADOW_TEMPLE, {}, { + areaTable[RR_SHADOW_TEMPLE_MQ_UPPER_HUGE_PIT] = Region("Shadow Temple MQ Upper Huge Pit", SCENE_SHADOW_TEMPLE, { + //Events + EventAccess(LOGIC_SHADOW_MQ_PIT_STAIRS, []{return logic->HasFireSource();}), + }, { //Locations LOCATION(RC_SHADOW_TEMPLE_MQ_PIT_STORM_FAIRY, logic->CanUse(RG_SONG_OF_STORMS)), }, { //Exits - Entrance(RR_SHADOW_TEMPLE_MQ_LOWER_HUGE_PIT, []{return (logic->HasFireSource() && (ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH))) || ctx->GetTrickOption(RT_SHADOW_MQ_HUGE_PIT);}), - Entrance(RR_SHADOW_TEMPLE_MQ_INVISIBLE_BLADES_ROOM, []{return ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_SHADOW_TEMPLE_MQ_UPPER_HUGE_PIT_DOOR_LEDGE, []{return ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_SHADOW_TEMPLE_MQ_LOWER_HUGE_PIT, []{return (logic->Get(LOGIC_SHADOW_MQ_PIT_STAIRS) && (ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH))) || ctx->GetTrickOption(RT_SHADOW_MQ_HUGE_PIT);}), + }); + + areaTable[RR_SHADOW_TEMPLE_MQ_UPPER_HUGE_PIT_DOOR_LEDGE] = Region("Shadow Temple MQ Upper Huge Pit Door Ledge", SCENE_SHADOW_TEMPLE, {}, {}, { + Entrance(RR_SHADOW_TEMPLE_MQ_UPPER_HUGE_PIT, []{return ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_SHADOW_TEMPLE_MQ_INVISIBLE_BLADES_ROOM, []{return true;}), }); areaTable[RR_SHADOW_TEMPLE_MQ_INVISIBLE_BLADES_ROOM] = Region("Shadow Temple MQ Invisible Blades Room", SCENE_SHADOW_TEMPLE, {}, { @@ -225,22 +511,29 @@ void RegionTable_Init_ShadowTemple() { LOCATION(RC_SHADOW_TEMPLE_MQ_BEAMOS_SILVER_RUPEES_CHEST, logic->CanUse(RG_LONGSHOT)), }, { //Exits - Entrance(RR_SHADOW_TEMPLE_MQ_STONE_UMBRELLA_ROOM, []{return Here(RR_SHADOW_TEMPLE_MQ_LOWER_HUGE_PIT, []{return logic->CanJumpslash() || logic->HasExplosives();});}), - Entrance(RR_SHADOW_TEMPLE_MQ_FLOOR_SPIKES_ROOM, []{return logic->CanUse(RG_HOVER_BOOTS) && (ctx->GetTrickOption(RT_LENS_SHADOW_MQ_PLATFORM) || logic->CanUse(RG_LENS_OF_TRUTH)) && logic->SmallKeys(SCENE_SHADOW_TEMPLE, 3);}), + Entrance(RR_SHADOW_TEMPLE_MQ_B2_TO_B3_CORRIDOR_B3, []{return logic->CanUse(RG_LONGSHOT);}), + Entrance(RR_SHADOW_TEMPLE_MQ_UPPER_HUGE_PIT, []{return logic->Get(LOGIC_SHADOW_MQ_PIT_STAIRS);}), + Entrance(RR_SHADOW_TEMPLE_MQ_LOWER_HUGE_PIT_DOOR_LEDGE, []{return logic->CanUse(RG_HOVER_BOOTS) && (ctx->GetTrickOption(RT_LENS_SHADOW_MQ_PLATFORM) || logic->CanUse(RG_LENS_OF_TRUTH));}), + Entrance(RR_SHADOW_TEMPLE_MQ_STONE_UMBRELLA_ROOM, []{return Here(RR_SHADOW_TEMPLE_MQ_LOWER_HUGE_PIT, []{return logic->CanJumpslash() || logic->HasExplosives();});}), + }); + + areaTable[RR_SHADOW_TEMPLE_MQ_LOWER_HUGE_PIT_DOOR_LEDGE] = Region("Shadow Temple MQ Upper Huge Pit Door Ledge", SCENE_SHADOW_TEMPLE, {}, {}, { + Entrance(RR_SHADOW_TEMPLE_MQ_LOWER_HUGE_PIT, []{return logic->CanUse(RG_HOVER_BOOTS) && (ctx->GetTrickOption(RT_LENS_SHADOW_MQ_PLATFORM) || logic->CanUse(RG_LENS_OF_TRUTH)) && ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH);}), + Entrance(RR_SHADOW_TEMPLE_MQ_INVISIBLE_BLADES_ROOM, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 3);}), }); areaTable[RR_SHADOW_TEMPLE_MQ_STONE_UMBRELLA_ROOM] = Region("Shadow Temple MQ Stone Umbrella Room", SCENE_SHADOW_TEMPLE, {}, { //Locations LOCATION(RC_SHADOW_TEMPLE_MQ_FALLING_SPIKES_LOWER_CHEST, true), - LOCATION(RC_SHADOW_TEMPLE_MQ_GS_FALLING_SPIKES_ROOM, logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA, ED_BOOMERANG) || logic->CanGroundJump()), + LOCATION(RC_SHADOW_TEMPLE_MQ_GS_FALLING_SPIKES_ROOM, logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA, ED_BOOMERANG) || (logic->IsAdult && ctx->GetTrickOption(RT_GROUND_JUMP_HARD) && logic->CanGroundJump() && logic->CanJumpslash())), LOCATION(RC_SHADOW_TEMPLE_MQ_LOWER_UMBRELLA_WEST_POT, logic->CanBreakPots()), LOCATION(RC_SHADOW_TEMPLE_MQ_LOWER_UMBRELLA_EAST_POT, logic->CanBreakPots()), LOCATION(RC_SHADOW_TEMPLE_MQ_UPPER_UMBRELLA_SOUTH_POT, logic->CanUse(RG_BOOMERANG)), }, { //Exits Entrance(RR_SHADOW_TEMPLE_MQ_LOWER_HUGE_PIT, []{return Here(RR_SHADOW_TEMPLE_MQ_STONE_UMBRELLA_ROOM, []{return ctx->GetTrickOption(RT_VISIBLE_COLLISION) || logic->CanHitSwitch();});}), - //Assuming the known setup for RT_SHADOW_UMBRELLA_HOVER, probably possible without sword + shield - Entrance(RR_SHADOW_TEMPLE_MQ_UPPER_STONE_UMBRELLA, []{return ctx->GetTrickOption(RT_SHADOW_UMBRELLA_CLIP) || (logic->IsAdult && (logic->HasItem(RG_GORONS_BRACELET) || (ctx->GetTrickOption(RT_SHADOW_UMBRELLA_HOVER) && logic->CanUse(RG_HOVER_BOOTS) && logic->CanStandingShield() && logic->CanUse(RG_MASTER_SWORD))));}), + //Assuming the known setup for RT_SHADOW_UMBRELLA, probably possible without sword + shield + Entrance(RR_SHADOW_TEMPLE_MQ_UPPER_STONE_UMBRELLA, []{return ctx->GetTrickOption(RT_SHADOW_UMBRELLA_CLIP) || (ctx->GetTrickOption(RT_DAMAGE_BOOST_SIMPLE) && logic->TakeDamage()) || (logic->IsAdult && (logic->HasItem(RG_GORONS_BRACELET) || (ctx->GetTrickOption(RT_SHADOW_UMBRELLA_HOVER) && logic->CanUse(RG_HOVER_BOOTS) && logic->CanStandingShield() && logic->CanUse(RG_MASTER_SWORD))));}), }); areaTable[RR_SHADOW_TEMPLE_MQ_UPPER_STONE_UMBRELLA] = Region("Shadow Temple MQ Upper Stone Umbrella", SCENE_SHADOW_TEMPLE, {}, { @@ -265,15 +558,23 @@ void RegionTable_Init_ShadowTemple() { //Combined these are longshot or (IsAdult && hookshot && (CanJumpslash || (Hover Boots && Here(CanKillRedeads)))) (logic->CanUse(RG_LONGSHOT) || (logic->IsAdult && logic->CanUse(RG_HOOKSHOT) && (logic->CanJumpslash() || (logic->CanUse(RG_HOVER_BOOTS) && Here(RR_SHADOW_TEMPLE_MQ_FLOOR_SPIKES_ROOM, []{return logic->CanKillEnemy(RE_REDEAD);}))))) && //1 rupee is in spikes, needs hovers or damage - (logic->TakeDamage() || logic->CanUse(RG_HOVER_BOOTS));}), + (logic->TakeDamage() || logic->CanUse(RG_HOVER_BOOTS) || logic->CanUse(RG_GORON_TUNIC));}), }, { //Locations LOCATION(RC_SHADOW_TEMPLE_MQ_INVISIBLE_SPIKES_CHEST, logic->CanKillEnemy(RE_REDEAD) && (ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->TakeDamage() || logic->CanUse(RG_LENS_OF_TRUTH))), }, { //Exits - Entrance(RR_SHADOW_TEMPLE_MQ_STALFOS_ROOM, []{return logic->Get(LOGIC_SHADOW_MQ_FLOOR_SPIKES_RUPEES);}), - //We need to assume we can get here with or without the glass platforms - Entrance(RR_SHADOW_TEMPLE_MQ_WIND_TUNNEL, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 4) && (logic->CanUse(RG_LONGSHOT) || (logic->IsAdult && logic->CanUse(RG_HOOKSHOT) && (logic->Get(LOGIC_SHADOW_MQ_FLOOR_SPIKES_RUPEES) || Here(RR_SHADOW_TEMPLE_MQ_FLOOR_SPIKES_ROOM, []{return logic->CanKillEnemy(RE_REDEAD);})))) && (logic->CanJumpslash() || logic->CanUse(RG_HOVER_BOOTS));}), + Entrance(RR_SHADOW_TEMPLE_MQ_LOWER_HUGE_PIT, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 3);}), + Entrance(RR_SHADOW_TEMPLE_MQ_STALFOS_ROOM, []{return logic->Get(LOGIC_SHADOW_MQ_FLOOR_SPIKES_RUPEES);}), + //We need to assume we can get here with or without the glass platforms + Entrance(RR_SHADOW_TEMPLE_MQ_FLOOR_SPIKES_PLATFORM, []{return ((logic->CanUse(RG_LONGSHOT) || (logic->IsAdult && logic->CanUse(RG_HOOKSHOT) && (logic->Get(LOGIC_SHADOW_MQ_FLOOR_SPIKES_RUPEES) || Here(RR_SHADOW_TEMPLE_MQ_FLOOR_SPIKES_ROOM, []{return logic->CanKillEnemy(RE_REDEAD);})))) && (logic->CanJumpslash() || logic->CanUse(RG_HOVER_BOOTS))) || + ((ctx->GetTrickOption(RT_LENS_SHADOW) || logic->CanUse(RG_LENS_OF_TRUTH)) && (ctx->GetTrickOption(RT_GROUND_JUMP_HARD) && logic->CanGroundJump() && logic->IsAdult && logic->CanUse(RG_HOVER_BOOTS)));}), + }); + + areaTable[RR_SHADOW_TEMPLE_MQ_FLOOR_SPIKES_PLATFORM] = Region("Shadow Temple MQ Floor Spikes Platform", SCENE_SHADOW_TEMPLE, {}, {}, { + //Exits + Entrance(RR_SHADOW_TEMPLE_MQ_FLOOR_SPIKES_ROOM, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_MQ_UPPER_WIND_TUNNEL, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 4);}), }); areaTable[RR_SHADOW_TEMPLE_MQ_STALFOS_ROOM] = Region("Shadow Temple MQ Stalfos Room", SCENE_SHADOW_TEMPLE, {}, { @@ -284,11 +585,17 @@ void RegionTable_Init_ShadowTemple() { Entrance(RR_SHADOW_TEMPLE_MQ_FLOOR_SPIKES_ROOM, []{return Here(RR_SHADOW_TEMPLE_MQ_STALFOS_ROOM, []{return logic->CanKillEnemy(RE_STALFOS, ED_CLOSE, true, 2);});}), }); - areaTable[RR_SHADOW_TEMPLE_MQ_WIND_TUNNEL] = Region("Shadow Temple MQ Wind Tunnel", SCENE_SHADOW_TEMPLE, {}, {}, { + areaTable[RR_SHADOW_TEMPLE_MQ_UPPER_WIND_TUNNEL] = Region("Shadow Temple MQ Upper Wind Tunnel", SCENE_SHADOW_TEMPLE, {}, {}, { //Exits - Entrance(RR_SHADOW_TEMPLE_MQ_FLOOR_SPIKES_ROOM, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 4) && logic->CanPassEnemy(RE_BIG_SKULLTULA) && (logic->CanUse(RG_HOOKSHOT));}), - Entrance(RR_SHADOW_TEMPLE_MQ_WIND_HINT_ROOM, []{return logic->CanPassEnemy(RE_BIG_SKULLTULA) && (logic->CanUse(RG_HOOKSHOT) || logic->CanUse(RG_HOVER_BOOTS));}), - Entrance(RR_SHADOW_TEMPLE_MQ_B4_GIBDO_ROOM, []{return logic->CanPassEnemy(RE_BIG_SKULLTULA) && (logic->CanUse(RG_HOOKSHOT) || logic->CanUse(RG_HOVER_BOOTS));}), + Entrance(RR_SHADOW_TEMPLE_MQ_FLOOR_SPIKES_ROOM, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 4);}), + Entrance(RR_SHADOW_TEMPLE_MQ_LOWER_WIND_TUNNEL, []{return (logic->CanUse(RG_HOVER_BOOTS) && logic->CanPassEnemy(RE_BIG_SKULLTULA)) || logic->CanUse(RG_HOOKSHOT);}), + }); + + areaTable[RR_SHADOW_TEMPLE_MQ_LOWER_WIND_TUNNEL] = Region("Shadow Temple MQ Lower Wind Tunnel", SCENE_SHADOW_TEMPLE, {}, {}, { + //Exits + Entrance(RR_SHADOW_TEMPLE_MQ_UPPER_WIND_TUNNEL, []{return logic->CanUse(RG_HOOKSHOT);}), + Entrance(RR_SHADOW_TEMPLE_MQ_WIND_HINT_ROOM, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_MQ_WIND_TUNNEL_ALCOVE, []{return ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH);}), }); areaTable[RR_SHADOW_TEMPLE_MQ_WIND_HINT_ROOM] = Region("Shadow Temple MQ Wind Hint Room", SCENE_SHADOW_TEMPLE, {}, { @@ -298,7 +605,12 @@ void RegionTable_Init_ShadowTemple() { LOCATION(RC_SHADOW_TEMPLE_MQ_WIND_HINT_SUN_FAIRY, logic->CanUse(RG_SUNS_SONG)), }, { //Exits - Entrance(RR_SHADOW_TEMPLE_MQ_WIND_TUNNEL, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_MQ_LOWER_WIND_TUNNEL, []{return true;}), + }); + + areaTable[RR_SHADOW_TEMPLE_MQ_WIND_TUNNEL_ALCOVE] = Region("Shadow Temple MQ Wind Tunnel Alcove", SCENE_SHADOW_TEMPLE, {}, {}, { + Entrance(RR_SHADOW_TEMPLE_MQ_LOWER_WIND_TUNNEL, []{return (ctx->GetTrickOption(RT_SHADOW_MQ_WINDY_WALKWAY)) || logic->CanUse(RG_HOVER_BOOTS);}), + Entrance(RR_SHADOW_TEMPLE_MQ_B4_GIBDO_ROOM, []{return true;}), }); areaTable[RR_SHADOW_TEMPLE_MQ_B4_GIBDO_ROOM] = Region("Shadow Temple MQ B4 Gibdo Room", SCENE_SHADOW_TEMPLE, { @@ -314,8 +626,8 @@ void RegionTable_Init_ShadowTemple() { }, { //Exits //child can make it using the wind strat - Entrance(RR_SHADOW_TEMPLE_MQ_WIND_TUNNEL, []{return (ctx->GetTrickOption(RT_SHADOW_MQ_WINDY_WALKWAY)) || logic->CanUse(RG_HOVER_BOOTS);}), - Entrance(RR_SHADOW_TEMPLE_MQ_DOCK, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 5);}), + Entrance(RR_SHADOW_TEMPLE_MQ_WIND_TUNNEL_ALCOVE, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_MQ_DOCK, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 5);}), }); areaTable[RR_SHADOW_TEMPLE_MQ_DOCK] = Region("Shadow Temple MQ Dock", SCENE_SHADOW_TEMPLE, { @@ -329,23 +641,31 @@ void RegionTable_Init_ShadowTemple() { //Exits Entrance(RR_SHADOW_TEMPLE_MQ_SHORTCUT_PATH, []{return logic->Get(LOGIC_SHADOW_SHORTCUT_BLOCK);}), Entrance(RR_SHADOW_TEMPLE_MQ_B4_GIBDO_ROOM, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 5);}), - //funnily enough, the wheel jump seems to be in logic as there's no strength requirement in N64 - Entrance(RR_SHADOW_TEMPLE_MQ_BEYOND_BOAT, []{return (logic->IsAdult || logic->CanUse(RG_HOOKSHOT)) && logic->CanUse(RG_ZELDAS_LULLABY);}), + Entrance(RR_SHADOW_TEMPLE_MQ_BEYOND_BOAT, []{return ((logic->IsAdult && logic->HasItem(RG_GORONS_BRACELET)) || logic->CanUse(RG_HOOKSHOT)) && logic->CanUse(RG_ZELDAS_LULLABY);}), }); - areaTable[RR_SHADOW_TEMPLE_MQ_BEYOND_BOAT] = Region("Shadow Temple MQ Beyond Boat", SCENE_SHADOW_TEMPLE, {}, { + areaTable[RR_SHADOW_TEMPLE_MQ_BEYOND_BOAT] = Region("Shadow Temple MQ Beyond Boat", SCENE_SHADOW_TEMPLE, { + //Events + EventAccess(LOGIC_SHADOW_BRIDGE_BEYOND_BOAT_LOWERED, []{return logic->CanUse(RG_FAIRY_BOW) || (ctx->GetTrickOption(RT_SHADOW_STATUE) && logic->CanUse(RG_BOMBCHU_5));}) + }, { //Locations //It's a trick on N64 to kill this and drop down to collect this with normal weapons, as doing so without the statue being dropped voids you to before the boat - //hilariously, you can also hit this with a pot before you bring down the statue, but there's no great way to reset it without crossing. the statues collision is very inconvenient afterwards + //hilariously, you can hit this with a pot before you bring down statue, but there's no great way to reset it without crossing. the statue's collision is very inconvenient afterwards LOCATION(RC_SHADOW_TEMPLE_MQ_GS_AFTER_SHIP, logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA, ED_BOOMERANG)), LOCATION(RC_SHADOW_TEMPLE_MQ_BEFORE_CHASM_WEST_POT, logic->CanBreakPots()), LOCATION(RC_SHADOW_TEMPLE_MQ_BEFORE_CHASM_EAST_POT, logic->CanBreakPots()), }, { //Exits - Entrance(RR_SHADOW_TEMPLE_MQ_ACROSS_CHASM, []{return Here(RR_SHADOW_TEMPLE_MQ_BEYOND_BOAT, []{return logic->CanUse(RG_FAIRY_BOW) || (ctx->GetTrickOption(RT_SHADOW_STATUE) && logic->CanUse(RG_BOMBCHU_5));});}), + Entrance(RR_SHADOW_TEMPLE_MQ_ACROSS_CHASM, []{return logic->Get(LOGIC_SHADOW_BRIDGE_BEYOND_BOAT_LOWERED) || (logic->Get(LOGIC_SHADOW_MQ_SWITCH_ACROSS_CHASM) && logic->CanUse(RG_LONGSHOT));}), + Entrance(RR_SHADOW_TEMPLE_MQ_INVISIBLE_MAZE, []{return logic->Get(LOGIC_SHADOW_MQ_SWITCH_ACROSS_CHASM);}), }); - areaTable[RR_SHADOW_TEMPLE_MQ_ACROSS_CHASM] = Region("Shadow Temple MQ Across Chasm", SCENE_SHADOW_TEMPLE, {}, { + areaTable[RR_SHADOW_TEMPLE_MQ_ACROSS_CHASM] = Region("Shadow Temple MQ Across Chasm", SCENE_SHADOW_TEMPLE, { + //Events + EventAccess(LOGIC_SHADOW_BRIDGE_BEYOND_BOAT_LOWERED, []{return logic->CanDetonateUprightBombFlower();}), + EventAccess(LOGIC_SHADOW_MQ_EYE_SWITCH_ACROSS_CHASM, []{return logic->CanHitEyeTargets() && (logic->CanUse(RG_SONG_OF_TIME) || ctx->GetTrickOption(RT_HOOKSHOT_EXTENSION));}), + EventAccess(LOGIC_SHADOW_MQ_SWITCH_ACROSS_CHASM, []{return logic->Get(LOGIC_SHADOW_MQ_EYE_SWITCH_ACROSS_CHASM) && logic->CanUse(RG_LONGSHOT);}), + }, { //Locations LOCATION(RC_SHADOW_TEMPLE_MQ_AFTER_CHASM_WEST_POT, logic->CanBreakPots()), LOCATION(RC_SHADOW_TEMPLE_MQ_AFTER_CHASM_EAST_POT, logic->CanBreakPots()), @@ -355,11 +675,14 @@ void RegionTable_Init_ShadowTemple() { LOCATION(RC_SHADOW_TEMPLE_MQ_AFTER_SHIP_LOWER_HEART, logic->IsAdult), }, { //Exits - Entrance(RR_SHADOW_TEMPLE_MQ_BEYOND_BOAT, []{return Here(RR_SHADOW_TEMPLE_MQ_ACROSS_CHASM, []{return logic->CanDetonateUprightBombFlower();}) && logic->IsAdult;}), - //assumes RR_SHADOW_TEMPLE_MQ_BEYOND_BOAT by previous access. If backwards shadow ever exists remember that child cannot jump onto the statue from this side and make an event for the switch - //Lens isn't needed to reach it but is needed to navigate the next room - Entrance(RR_SHADOW_TEMPLE_MQ_INVISIBLE_MAZE, []{return Here(RR_SHADOW_TEMPLE_MQ_ACROSS_CHASM, []{return logic->CanHitEyeTargets() && logic->CanUse(RG_SONG_OF_TIME) && logic->CanUse(RG_LONGSHOT);});}), - Entrance(RR_SHADOW_TEMPLE_MQ_BOSS_DOOR, []{return logic->CanUse(RG_HOVER_BOOTS) && (ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH));}), + Entrance(RR_SHADOW_TEMPLE_MQ_BEYOND_BOAT, []{return logic->Get(LOGIC_SHADOW_BRIDGE_BEYOND_BOAT_LOWERED) && logic->IsAdult;}), + Entrance(RR_SHADOW_TEMPLE_MQ_PRE_BOSS_ROOM, []{return true;}), + }); + + areaTable[RR_SHADOW_TEMPLE_PRE_BOSS_ROOM] = Region("Shadow Temple MQ Pre Boss Room", SCENE_SHADOW_TEMPLE, {}, {}, { + //Exits + Entrance(RR_SHADOW_TEMPLE_MQ_BEYOND_BOAT, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_MQ_BOSS_DOOR, []{return logic->CanUse(RG_HOVER_BOOTS) && (ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH));}), }); areaTable[RR_SHADOW_TEMPLE_MQ_BOSS_DOOR] = Region("Shadow Temple MQ Boss Door", SCENE_SHADOW_TEMPLE, {}, { @@ -368,22 +691,35 @@ void RegionTable_Init_ShadowTemple() { LOCATION(RC_SHADOW_TEMPLE_MQ_GS_NEAR_BOSS, (logic->CanKillEnemy(RE_GOLD_SKULLTULA, ED_BOMB_THROW) || logic->CanUse(RG_MEGATON_HAMMER)) && (ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH))), }, { //Exits - Entrance(RR_SHADOW_TEMPLE_MQ_ACROSS_CHASM, []{return logic->CanUse(RG_HOVER_BOOTS) && (ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH));}), - Entrance(RR_SHADOW_TEMPLE_BOSS_ENTRYWAY, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_MQ_PRE_BOSS_ROOM, []{return logic->CanUse(RG_HOVER_BOOTS) && (ctx->GetTrickOption(RT_LENS_SHADOW_MQ) || logic->CanUse(RG_LENS_OF_TRUTH));}), + Entrance(RR_SHADOW_TEMPLE_BOSS_ENTRYWAY, []{return true;}), }); - //Assumes lens is checked on entry - areaTable[RR_SHADOW_TEMPLE_MQ_INVISIBLE_MAZE] = Region("Shadow Temple MQ Invisible Maze", SCENE_SHADOW_TEMPLE, {}, { + areaTable[RR_SHADOW_TEMPLE_MQ_INVISIBLE_MAZE] = Region("Shadow Temple MQ Invisible Maze", SCENE_SHADOW_TEMPLE, {}, {}, { + //Exits + Entrance(RR_SHADOW_TEMPLE_MQ_BEYOND_BOAT, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_MQ_X_CROSS, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_MQ_THREE_SKULL_JARS, []{return true;}), + Entrance(RR_SHADOW_TEMPLE_MQ_SPIKE_WALLS_ROOM, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 6);}), + }); + + areaTable[RR_SHADOW_TEMPLE_MQ_X_CROSS] = Region("Shadow Temple MQ X-Cross", SCENE_SHADOW_TEMPLE, {}, { //Locations //don't use CanDetonateUprightBombFlower as blue fire logic would need to account for player having multiple bottles & taking damage multiple times LOCATION(RC_SHADOW_TEMPLE_MQ_BOMB_FLOWER_CHEST, (logic->CanUse(RG_LENS_OF_TRUTH) || ctx->GetTrickOption(RT_LENS_SHADOW_MQ_DEADHAND)) && logic->CanKillEnemy(RE_DEAD_HAND) && (logic->CanDetonateBombFlowers() || logic->HasItem(RG_GORONS_BRACELET))), - LOCATION(RC_SHADOW_TEMPLE_MQ_FREESTANDING_KEY, true), LOCATION(RC_SHADOW_TEMPLE_MQ_DEAD_HAND_POT_1, logic->CanBreakPots()), LOCATION(RC_SHADOW_TEMPLE_MQ_DEAD_HAND_POT_2, logic->CanBreakPots()), }, { //Exits - Entrance(RR_SHADOW_TEMPLE_MQ_BEYOND_BOAT, []{return true;}), - Entrance(RR_SHADOW_TEMPLE_MQ_SPIKE_WALLS_ROOM, []{return logic->SmallKeys(SCENE_SHADOW_TEMPLE, 6);}), + Entrance(RR_SHADOW_TEMPLE_MQ_INVISIBLE_MAZE, []{return true;}), + }); + + areaTable[RR_SHADOW_TEMPLE_MQ_THREE_SKULL_JARS] = Region("Shadow Temple MQ Three Skull Jars", SCENE_SHADOW_TEMPLE, {}, { + //Locations + LOCATION(RC_SHADOW_TEMPLE_MQ_FREESTANDING_KEY, true), + }, { + //Exits + Entrance(RR_SHADOW_TEMPLE_MQ_INVISIBLE_MAZE, []{return true;}), }); areaTable[RR_SHADOW_TEMPLE_MQ_SPIKE_WALLS_ROOM] = Region("Shadow Temple MQ Spike Walls Room", SCENE_SHADOW_TEMPLE, {}, { @@ -401,9 +737,9 @@ void RegionTable_Init_ShadowTemple() { // Boss Room areaTable[RR_SHADOW_TEMPLE_BOSS_ENTRYWAY] = Region("Shadow Temple Boss Entryway", SCENE_SHADOW_TEMPLE, {}, {}, { // Exits - Entrance(RR_SHADOW_TEMPLE_BEYOND_BOAT, []{return ctx->GetDungeon(SHADOW_TEMPLE)->IsVanilla() && false;}), - Entrance(RR_SHADOW_TEMPLE_MQ_BEYOND_BOAT, []{return ctx->GetDungeon(SHADOW_TEMPLE)->IsMQ() && false;}), - Entrance(RR_SHADOW_TEMPLE_BOSS_ROOM, []{return logic->HasItem(RG_SHADOW_TEMPLE_BOSS_KEY);}), + Entrance(RR_SHADOW_TEMPLE_BOSS_DOOR, []{return ctx->GetDungeon(SHADOW_TEMPLE)->IsVanilla() && false;}), + Entrance(RR_SHADOW_TEMPLE_MQ_BOSS_DOOR, []{return ctx->GetDungeon(SHADOW_TEMPLE)->IsMQ() && false;}), + Entrance(RR_SHADOW_TEMPLE_BOSS_ROOM, []{return logic->HasItem(RG_SHADOW_TEMPLE_BOSS_KEY);}), }); areaTable[RR_SHADOW_TEMPLE_BOSS_ROOM] = Region("Shadow Temple Boss Room", SCENE_SHADOW_TEMPLE_BOSS, { diff --git a/soh/soh/Enhancements/randomizer/location_access/overworld/haunted_wasteland.cpp b/soh/soh/Enhancements/randomizer/location_access/overworld/haunted_wasteland.cpp index b1cefc144..20ae61555 100644 --- a/soh/soh/Enhancements/randomizer/location_access/overworld/haunted_wasteland.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/overworld/haunted_wasteland.cpp @@ -24,7 +24,7 @@ void RegionTable_Init_HauntedWasteland() { //Locations LOCATION(RC_WASTELAND_CHEST, logic->HasFireSource()), LOCATION(RC_WASTELAND_BOMBCHU_SALESMAN, logic->CanJumpslash() || logic->CanUse(RG_HOVER_BOOTS)), - LOCATION(RC_WASTELAND_GS, logic->HookshotOrBoomerang() || (logic->IsAdult && logic->CanGroundJump() && logic->CanJumpslash())), + LOCATION(RC_WASTELAND_GS, logic->HookshotOrBoomerang() || (logic->IsAdult && ctx->GetTrickOption(RT_GROUND_JUMP_HARD) && logic->CanGroundJump() && logic->CanJumpslash())), // need to jumpslash immediately with two handed weapons LOCATION(RC_WASTELAND_NEAR_GS_POT_1, logic->CanBreakPots()), LOCATION(RC_WASTELAND_NEAR_GS_POT_2, logic->CanBreakPots()), LOCATION(RC_WASTELAND_NEAR_GS_POT_3, logic->CanBreakPots()), diff --git a/soh/soh/Enhancements/randomizer/location_access/overworld/kakariko.cpp b/soh/soh/Enhancements/randomizer/location_access/overworld/kakariko.cpp index cb6bb78a1..6b39ddfc6 100644 --- a/soh/soh/Enhancements/randomizer/location_access/overworld/kakariko.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/overworld/kakariko.cpp @@ -270,8 +270,8 @@ void RegionTable_Init_Kakariko() { areaTable[RR_KAK_WELL] = Region("Kak Well", SCENE_KAKARIKO_VILLAGE, {}, {}, { //Exits - Entrance(RR_KAKARIKO_VILLAGE, []{return logic->IsAdult || logic->HasItem(RG_BRONZE_SCALE) || logic->Get(LOGIC_DRAIN_WELL);}), - Entrance(RR_BOTTOM_OF_THE_WELL_ENTRYWAY, []{return logic->IsChild || (logic->Get(LOGIC_DRAIN_WELL) && ctx->GetOption(RSK_SHUFFLE_DUNGEON_ENTRANCES).IsNot(RO_DUNGEON_ENTRANCE_SHUFFLE_OFF));}), + Entrance(RR_KAKARIKO_VILLAGE, []{return logic->IsAdult || logic->HasItem(RG_BRONZE_SCALE) || logic->Get(LOGIC_DRAIN_WELL);}), + Entrance(RR_BOTW_ENTRYWAY, []{return logic->IsChild || (logic->Get(LOGIC_DRAIN_WELL) && ctx->GetOption(RSK_SHUFFLE_DUNGEON_ENTRANCES).IsNot(RO_DUNGEON_ENTRANCE_SHUFFLE_OFF));}), }); // clang-format on diff --git a/soh/soh/Enhancements/randomizer/location_access/overworld/thieves_hideout.cpp b/soh/soh/Enhancements/randomizer/location_access/overworld/thieves_hideout.cpp index 77d67824f..69c99ea5c 100644 --- a/soh/soh/Enhancements/randomizer/location_access/overworld/thieves_hideout.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/overworld/thieves_hideout.cpp @@ -4,7 +4,7 @@ using namespace Rando; // clang-format off -// When Thieve's hideout entrances are shuffled, getting caught by guards should behave like void outs to avoid logic headaches. +// When Thieves' Hideout entrances are shuffled, getting caught by guards should behave like void outs to avoid logic headaches. void RegionTable_Init_ThievesHideout() { areaTable[RR_TH_1_TORCH_CELL] = Region("Thieves Hideout 1 Torch Cell", SCENE_THIEVES_HIDEOUT, { //Events diff --git a/soh/soh/Enhancements/randomizer/location_access/overworld/zoras_river.cpp b/soh/soh/Enhancements/randomizer/location_access/overworld/zoras_river.cpp index db8c3cce1..7b1ff3907 100644 --- a/soh/soh/Enhancements/randomizer/location_access/overworld/zoras_river.cpp +++ b/soh/soh/Enhancements/randomizer/location_access/overworld/zoras_river.cpp @@ -31,11 +31,10 @@ void RegionTable_Init_ZoraRiver() { //Events EventAccess(LOGIC_GOSSIP_STONE_FAIRY, []{return logic->CallGossipFairy();}), EventAccess(LOGIC_BEAN_PLANT_FAIRY, []{return logic->IsChild && logic->CanUse(RG_MAGIC_BEAN) && logic->CanUse(RG_SONG_OF_STORMS);}), - EventAccess(LOGIC_BUTTERFLY_FAIRY, []{return logic->CanUse(RG_STICKS);}), - EventAccess(LOGIC_BUG_SHRUB, []{return logic->CanCutShrubs() && (logic->IsChild || logic->CanUse(RG_HOVER_BOOTS) || ctx->GetTrickOption(RT_ZR_LOWER));}), + EventAccess(LOGIC_BUTTERFLY_FAIRY, []{return logic->IsChild && logic->CanUse(RG_STICKS);}), }, { //Locations - LOCATION(RC_ZR_MAGIC_BEAN_SALESMAN, logic->IsChild), + LOCATION(RC_ZR_MAGIC_BEAN_SALESMAN, logic->IsChild/* && CanUse(SPEAK_HYLIAN)*/), LOCATION(RC_ZR_FROGS_OCARINA_GAME, logic->IsChild && logic->CanUse(RG_ZELDAS_LULLABY) && logic->CanUse(RG_SARIAS_SONG) && logic->CanUse(RG_SUNS_SONG) && logic->CanUse(RG_EPONAS_SONG) && logic->CanUse(RG_SONG_OF_TIME) && logic->CanUse(RG_SONG_OF_STORMS)), LOCATION(RC_ZR_FROGS_IN_THE_RAIN, logic->IsChild && logic->CanUse(RG_SONG_OF_STORMS)), LOCATION(RC_ZR_FROGS_ZELDAS_LULLABY, logic->IsChild && logic->CanUse(RG_ZELDAS_LULLABY)), @@ -43,36 +42,62 @@ void RegionTable_Init_ZoraRiver() { LOCATION(RC_ZR_FROGS_SARIAS_SONG, logic->IsChild && logic->CanUse(RG_SARIAS_SONG)), LOCATION(RC_ZR_FROGS_SUNS_SONG, logic->IsChild && logic->CanUse(RG_SUNS_SONG)), LOCATION(RC_ZR_FROGS_SONG_OF_TIME, logic->IsChild && logic->CanUse(RG_SONG_OF_TIME)), - LOCATION(RC_ZR_NEAR_OPEN_GROTTO_FREESTANDING_POH, logic->IsChild || logic->CanUse(RG_HOVER_BOOTS) || (logic->IsAdult && ctx->GetTrickOption(RT_ZR_LOWER))), - LOCATION(RC_ZR_NEAR_DOMAIN_FREESTANDING_POH, logic->IsChild || logic->CanUse(RG_HOVER_BOOTS) || (logic->IsAdult && ctx->GetTrickOption(RT_ZR_UPPER))), - LOCATION(RC_ZR_GS_LADDER, logic->IsChild && logic->CanAttack() && logic->CanGetNightTimeGS()), - LOCATION(RC_ZR_GS_NEAR_RAISED_GROTTOS, logic->IsAdult && logic->HookshotOrBoomerang() && logic->CanGetNightTimeGS()), - LOCATION(RC_ZR_GS_ABOVE_BRIDGE, logic->IsAdult && logic->CanUse(RG_HOOKSHOT) && logic->CanGetNightTimeGS()), + LOCATION(RC_ZR_NEAR_OPEN_GROTTO_FREESTANDING_POH, logic->CanUse(RG_BOOMERANG)), + LOCATION(RC_ZR_NEAR_DOMAIN_FREESTANDING_POH, (logic->IsChild /*&& str0*/) || logic->CanUse(RG_BOOMERANG) || logic->CanUse(RG_HOVER_BOOTS) || (logic->IsAdult && ctx->GetTrickOption(RT_ZR_UPPER))), + LOCATION(RC_ZR_GS_LADDER, logic->IsChild && logic->CanKillEnemy(RE_GOLD_SKULLTULA, ED_SHORT_JUMPSLASH) && logic->CanGetNightTimeGS()), + LOCATION(RC_ZR_GS_NEAR_RAISED_GROTTOS, logic->IsAdult && logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA, ED_LONGSHOT) && logic->CanGetNightTimeGS()), + LOCATION(RC_ZR_GS_ABOVE_BRIDGE, logic->IsAdult && logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA, ED_HOOKSHOT) && logic->CanGetNightTimeGS()), LOCATION(RC_ZR_BEAN_SPROUT_FAIRY_1, logic->IsChild && logic->CanUse(RG_MAGIC_BEAN) && logic->CanUse(RG_SONG_OF_STORMS)), LOCATION(RC_ZR_BEAN_SPROUT_FAIRY_2, logic->IsChild && logic->CanUse(RG_MAGIC_BEAN) && logic->CanUse(RG_SONG_OF_STORMS)), LOCATION(RC_ZR_BEAN_SPROUT_FAIRY_3, logic->IsChild && logic->CanUse(RG_MAGIC_BEAN) && logic->CanUse(RG_SONG_OF_STORMS)), - LOCATION(RC_ZR_NEAR_GROTTOS_GOSSIP_STONE_FAIRY, logic->CallGossipFairy()), - LOCATION(RC_ZR_NEAR_GROTTOS_GOSSIP_STONE_FAIRY_BIG, logic->CanUse(RG_SONG_OF_STORMS)), LOCATION(RC_ZR_NEAR_DOMAIN_GOSSIP_STONE_FAIRY, logic->CallGossipFairy()), LOCATION(RC_ZR_NEAR_DOMAIN_GOSSIP_STONE_FAIRY_BIG, logic->CanUse(RG_SONG_OF_STORMS)), LOCATION(RC_ZR_BENEATH_WATERFALL_LEFT_RUPEE, logic->IsAdult && (logic->HasItem(RG_BRONZE_SCALE) || logic->CanUse(RG_IRON_BOOTS) || logic->CanUse(RG_BOOMERANG))), LOCATION(RC_ZR_BENEATH_WATERFALL_MIDDLE_LEFT_RUPEE, logic->IsAdult && (logic->HasItem(RG_BRONZE_SCALE) || logic->CanUse(RG_IRON_BOOTS) || logic->CanUse(RG_BOOMERANG))), LOCATION(RC_ZR_BENEATH_WATERFALL_MIDDLE_RIGHT_RUPEE, logic->IsAdult && (logic->HasItem(RG_BRONZE_SCALE) || logic->CanUse(RG_IRON_BOOTS) || logic->CanUse(RG_BOOMERANG))), LOCATION(RC_ZR_BENEATH_WATERFALL_RIGHT_RUPEE, logic->IsAdult && (logic->HasItem(RG_BRONZE_SCALE) || logic->CanUse(RG_IRON_BOOTS) || logic->CanUse(RG_BOOMERANG))), - LOCATION(RC_ZR_NEAR_GROTTOS_GOSSIP_STONE, true), LOCATION(RC_ZR_NEAR_DOMAIN_GOSSIP_STONE, true), - LOCATION(RC_ZR_NEAR_FREESTANDING_POH_GRASS, (logic->CanCutShrubs() && (logic->IsChild || logic->CanUse(RG_HOVER_BOOTS) || ctx->GetTrickOption(RT_ZR_LOWER))) || logic->CanUse(RG_BOOMERANG)), + LOCATION(RC_ZR_NEAR_FREESTANDING_POH_GRASS, logic->CanUse(RG_BOOMERANG)), }, { //Exits Entrance(RR_ZR_FRONT, []{return true;}), - Entrance(RR_ZR_OPEN_GROTTO, []{return true;}), - Entrance(RR_ZR_FAIRY_GROTTO, []{return Here(RR_ZORAS_RIVER, []{return logic->BlastOrSmash();});}), + Entrance(RR_ZR_ATOP_LADDER, []{return true/*(logic->IsAdult || str0) && (logic->CanUse(RG_CLIMB) || (logic->IsAdult && logic->CanUse(RG_LONGSHOT)))*/;}), + Entrance(RR_ZR_PILLAR, []{return (logic->IsChild/* && str0*/) || logic->CanUse(RG_HOVER_BOOTS) || (logic->IsAdult && ctx->GetTrickOption(RT_ZR_LOWER));}), Entrance(RR_THE_LOST_WOODS, []{return logic->HasItem(RG_SILVER_SCALE) || logic->CanUse(RG_IRON_BOOTS);}), Entrance(RR_ZR_STORMS_GROTTO, []{return logic->CanOpenStormsGrotto();}), Entrance(RR_ZR_BEHIND_WATERFALL, []{return ctx->GetOption(RSK_SLEEPING_WATERFALL).Is(RO_WATERFALL_OPEN) || Here(RR_ZORAS_RIVER, []{return logic->CanUse(RG_ZELDAS_LULLABY);}) || (logic->IsChild && ctx->GetTrickOption(RT_ZR_CUCCO)) || (logic->IsAdult && logic->CanUse(RG_HOVER_BOOTS) && ctx->GetTrickOption(RT_ZR_HOVERS));}), }); - areaTable[RR_ZR_FROM_SHORTCUT] = Region("ZR From Shortcut", SCENE_ZORAS_RIVER, TIME_DOESNT_PASS, {RA_ZORAS_RIVER}, {}, {}, { + areaTable[RR_ZR_ATOP_LADDER] = Region("ZR Atop Ladder", SCENE_ZORAS_RIVER, { + //Events + EventAccess(LOGIC_GOSSIP_STONE_FAIRY, []{return logic->CallGossipFairy();}), + }, { + //Locations + LOCATION(RC_ZR_GS_NEAR_RAISED_GROTTOS, logic->IsAdult && logic->CanGetEnemyDrop(RE_GOLD_SKULLTULA, ED_BOOMERANG) && logic->CanGetNightTimeGS()), + LOCATION(RC_ZR_NEAR_GROTTOS_GOSSIP_STONE_FAIRY, logic->CallGossipFairy()), + LOCATION(RC_ZR_NEAR_GROTTOS_GOSSIP_STONE_FAIRY_BIG, logic->CanUse(RG_SONG_OF_STORMS)), + LOCATION(RC_ZR_NEAR_GROTTOS_GOSSIP_STONE, true), + }, { + //Exits + Entrance(RR_ZORAS_RIVER, []{return true;}), + Entrance(RR_ZR_PILLAR, []{return (logic->IsChild/* && str0*/) || logic->CanUse(RG_HOVER_BOOTS);}), + Entrance(RR_ZR_OPEN_GROTTO, []{return true;}), + Entrance(RR_ZR_FAIRY_GROTTO, []{return Here(RR_ZR_ATOP_LADDER, []{return logic->BlastOrSmash();});}), + }); + + areaTable[RR_ZR_PILLAR] = Region("ZR Pillar", SCENE_ZORAS_RIVER, { + //Events + EventAccess(LOGIC_BUG_SHRUB, []{return logic->CanCutShrubs();}), + }, { + //Locations + LOCATION(RC_ZR_NEAR_OPEN_GROTTO_FREESTANDING_POH, true), + LOCATION(RC_ZR_NEAR_FREESTANDING_POH_GRASS, logic->CanCutShrubs()), + }, { + //Exits + Entrance(RR_ZORAS_RIVER, []{return true;}), + }); + + areaTable[RR_ZR_FROM_SHORTCUT] = Region("ZR From Shortcut", SCENE_ZORAS_RIVER, {}, {}, { //Exits Entrance(RR_ZORAS_RIVER, []{return logic->Hearts() > 1 || logic->HasItem(RG_BOTTLE_WITH_FAIRY) || logic->HasItem(RG_BRONZE_SCALE);}), Entrance(RR_THE_LOST_WOODS, []{return logic->HasItem(RG_SILVER_SCALE) || logic->CanUse(RG_IRON_BOOTS);}), @@ -99,7 +124,7 @@ void RegionTable_Init_ZoraRiver() { LOCATION(RC_ZR_OPEN_GROTTO_GRASS_4, logic->CanCutShrubs()), }, { //Exits - Entrance(RR_ZORAS_RIVER, []{return true;}), + Entrance(RR_ZR_ATOP_LADDER, []{return true;}), }); areaTable[RR_ZR_FAIRY_GROTTO] = Region("ZR Fairy Grotto", SCENE_GROTTOS, { @@ -117,7 +142,7 @@ void RegionTable_Init_ZoraRiver() { LOCATION(RC_ZR_FAIRY_GROTTO_FAIRY_8, true), }, { //Exits - Entrance(RR_ZORAS_RIVER, []{return true;}), + Entrance(RR_ZR_ATOP_LADDER, []{return true;}), }); areaTable[RR_ZR_STORMS_GROTTO] = Region("ZR Storms Grotto", SCENE_GROTTOS, {}, { diff --git a/soh/soh/Enhancements/randomizer/logic.cpp b/soh/soh/Enhancements/randomizer/logic.cpp index 26e30492f..6c359d529 100644 --- a/soh/soh/Enhancements/randomizer/logic.cpp +++ b/soh/soh/Enhancements/randomizer/logic.cpp @@ -858,9 +858,8 @@ bool Logic::CanPassEnemy(RandomizerEnemy enemy, EnemyDistance distance, bool wal return CanUse(RG_HOOKSHOT) || CanUse(RG_BOOMERANG); case RE_GIBDO: case RE_REDEAD: - // we need a way to check if suns won't force a reload - // RANDOTODO: check if stealthing past these guys works everywhere - return CanUse(RG_HOOKSHOT) || CanUse(RG_SUNS_SONG); + // You can move slowly to avoid getting screamed at + return true; // CanUse(RG_HOOKSHOT) || CanUse(RG_SUNS_SONG); case RE_IRON_KNUCKLE: case RE_BIG_OCTO: return false; diff --git a/soh/soh/Enhancements/randomizer/randomizerTypes.h b/soh/soh/Enhancements/randomizer/randomizerTypes.h index a18a1cf3a..7f09a6ba3 100644 --- a/soh/soh/Enhancements/randomizer/randomizerTypes.h +++ b/soh/soh/Enhancements/randomizer/randomizerTypes.h @@ -314,13 +314,18 @@ typedef enum { LOGIC_SPIRIT_MQ_TIME_TRAVEL_CHEST, LOGIC_SPIRIT_MQ_3SUNS_ENEMIES, LOGIC_SHADOW_SHORTCUT_BLOCK, + LOGIC_SHADOW_BRIDGE_BEYOND_BOAT_LOWERED, LOGIC_SHADOW_MQ_FLOOR_SPIKES_RUPEES, + LOGIC_SHADOW_MQ_PIT_STAIRS, + LOGIC_SHADOW_MQ_SWITCH_ACROSS_CHASM, + LOGIC_SHADOW_MQ_EYE_SWITCH_ACROSS_CHASM, LOGIC_WAKE_UP_ADULT_TALON, LOGIC_KAKARIKO_GATE_OPEN, LOGIC_DELIVER_RUTOS_LETTER, LOGIC_KING_ZORA_THAWED, LOGIC_LINKS_COW, LOGIC_BOTW_LOWERED_WATER, + LOGIC_BOTW_MQ_OPENED_GATES, LOGIC_BOTW_MQ_OPENED_WEST_ROOM, LOGIC_BOTW_MQ_OPENED_MIDDLE_HOLE, LOGIC_GTG_MQ_MAZE_SWITCH, @@ -659,6 +664,8 @@ typedef enum { RR_DMC_DISTANT_PLATFORM, RR_ZR_FRONT, RR_ZORAS_RIVER, + RR_ZR_ATOP_LADDER, + RR_ZR_PILLAR, RR_ZR_FROM_SHORTCUT, RR_ZR_BEHIND_WATERFALL, RR_ZR_OPEN_GROTTO, @@ -691,7 +698,7 @@ typedef enum { RR_WATER_TEMPLE_ENTRYWAY, RR_SPIRIT_TEMPLE_ENTRYWAY, RR_SHADOW_TEMPLE_ENTRYWAY, - RR_BOTTOM_OF_THE_WELL_ENTRYWAY, + RR_BOTW_ENTRYWAY, RR_ICE_CAVERN_ENTRYWAY, RR_GERUDO_TRAINING_GROUND_ENTRYWAY, RR_GANONS_CASTLE_ENTRYWAY, @@ -1059,59 +1066,119 @@ typedef enum { RR_SPIRIT_TEMPLE_BOSS_ROOM, RR_SHADOW_TEMPLE_BEGINNING, + RR_SHADOW_TEMPLE_WHISPERING_WALLS_START, + RR_SHADOW_TEMPLE_WHISPERING_WALLS_SIDE, + RR_SHADOW_TEMPLE_WHISPERING_WALLS_END, + RR_SHADOW_TEMPLE_WHISPERING_WALLS_SIDE_ROOM, + RR_SHADOW_TEMPLE_DEAD_HAND, RR_SHADOW_TEMPLE_FIRST_BEAMOS, - RR_SHADOW_TEMPLE_HUGE_PIT, - RR_SHADOW_TEMPLE_WIND_TUNNEL, + RR_SHADOW_TEMPLE_COMPASS_ROOM, + RR_SHADOW_TEMPLE_SPINNING_BLADES, + RR_SHADOW_TEMPLE_B2_TO_B3_CORRIDOR_B2, + RR_SHADOW_TEMPLE_B2_TO_B3_CORRIDOR_B3, + RR_SHADOW_TEMPLE_UPPER_HUGE_PIT, + RR_SHADOW_TEMPLE_UPPER_HUGE_PIT_DOOR_LEDGE, + RR_SHADOW_TEMPLE_LOWER_HUGE_PIT, + RR_SHADOW_TEMPLE_LOWER_HUGE_PIT_DOOR_LEDGE, + RR_SHADOW_TEMPLE_STONE_UMBRELLA, + RR_SHADOW_TEMPLE_STONE_UMBRELLA_UPPER, + RR_SHADOW_TEMPLE_INVISIBLE_SPINNING_BLADES, + RR_SHADOW_TEMPLE_INVISIBLE_SPIKES, + RR_SHADOW_TEMPLE_INVISIBLE_SPIKES_PLATFORM, + RR_SHADOW_TEMPLE_SKULL_JAR, + RR_SHADOW_TEMPLE_UPPER_WIND_TUNNEL, + RR_SHADOW_TEMPLE_LOWER_WIND_TUNNEL, + RR_SHADOW_TEMPLE_WIND_TUNNEL_ALCOVE, + RR_SHADOW_TEMPLE_WIND_TUNNEL_HINT_ROOM, + RR_SHADOW_TEMPLE_ROOM_TO_BOAT, + RR_SHADOW_TEMPLE_DOCK, RR_SHADOW_TEMPLE_BEYOND_BOAT, + RR_SHADOW_TEMPLE_ACROSS_CHASM, + RR_SHADOW_TEMPLE_MAZE, + RR_SHADOW_TEMPLE_X_CROSS, + RR_SHADOW_TEMPLE_THREE_SKULL_JARS, + RR_SHADOW_TEMPLE_WOODEN_SPIKES, + RR_SHADOW_TEMPLE_PRE_BOSS_ROOM, + RR_SHADOW_TEMPLE_BOSS_DOOR, RR_SHADOW_TEMPLE_MQ_ENTRYWAY, RR_SHADOW_TEMPLE_MQ_BEGINNING, RR_SHADOW_TEMPLE_MQ_SPINNER_ROOM, - RR_SHADOW_TEMPLE_MQ_DEAD_HAND_AREA, + RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_START, + RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_SIDE, + RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_END, + RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_SIDE_ROOM, + RR_SHADOW_TEMPLE_MQ_WHISPERING_WALLS_DEAD_HAND, RR_SHADOW_TEMPLE_MQ_FIRST_BEAMOS, + RR_SHADOW_TEMPLE_MQ_B2_GIBDO_ROOM, RR_SHADOW_TEMPLE_MQ_B2_SPINNING_BLADE_ROOM, RR_SHADOW_TEMPLE_MQ_SHORTCUT_PATH, - RR_SHADOW_TEMPLE_MQ_B2_TO_B3_CORRIDOR, + RR_SHADOW_TEMPLE_MQ_B2_TO_B3_CORRIDOR_B2, + RR_SHADOW_TEMPLE_MQ_B2_TO_B3_CORRIDOR_B3, RR_SHADOW_TEMPLE_MQ_UPPER_HUGE_PIT, + RR_SHADOW_TEMPLE_MQ_UPPER_HUGE_PIT_DOOR_LEDGE, RR_SHADOW_TEMPLE_MQ_INVISIBLE_BLADES_ROOM, RR_SHADOW_TEMPLE_MQ_LOWER_HUGE_PIT, + RR_SHADOW_TEMPLE_MQ_LOWER_HUGE_PIT_DOOR_LEDGE, RR_SHADOW_TEMPLE_MQ_STONE_UMBRELLA_ROOM, RR_SHADOW_TEMPLE_MQ_UPPER_STONE_UMBRELLA, RR_SHADOW_TEMPLE_MQ_FLOOR_SPIKES_ROOM, + RR_SHADOW_TEMPLE_MQ_FLOOR_SPIKES_PLATFORM, RR_SHADOW_TEMPLE_MQ_STALFOS_ROOM, - RR_SHADOW_TEMPLE_MQ_WIND_TUNNEL, + RR_SHADOW_TEMPLE_MQ_UPPER_WIND_TUNNEL, + RR_SHADOW_TEMPLE_MQ_LOWER_WIND_TUNNEL, + RR_SHADOW_TEMPLE_MQ_WIND_TUNNEL_ALCOVE, RR_SHADOW_TEMPLE_MQ_WIND_HINT_ROOM, RR_SHADOW_TEMPLE_MQ_B4_GIBDO_ROOM, RR_SHADOW_TEMPLE_MQ_DOCK, RR_SHADOW_TEMPLE_MQ_BEYOND_BOAT, RR_SHADOW_TEMPLE_MQ_ACROSS_CHASM, + RR_SHADOW_TEMPLE_MQ_PRE_BOSS_ROOM, RR_SHADOW_TEMPLE_MQ_BOSS_DOOR, RR_SHADOW_TEMPLE_MQ_INVISIBLE_MAZE, + RR_SHADOW_TEMPLE_MQ_X_CROSS, + RR_SHADOW_TEMPLE_MQ_THREE_SKULL_JARS, RR_SHADOW_TEMPLE_MQ_SPIKE_WALLS_ROOM, RR_SHADOW_TEMPLE_BOSS_ENTRYWAY, RR_SHADOW_TEMPLE_BOSS_ROOM, - RR_BOTTOM_OF_THE_WELL_PERIMETER, - RR_BOTTOM_OF_THE_WELL_BEHIND_FAKE_WALLS, - RR_BOTTOM_OF_THE_WELL_SOUTHWEST_ROOM, - RR_BOTTOM_OF_THE_WELL_KEESE_BEAMOS_ROOM, - RR_BOTTOM_OF_THE_WELL_LIKE_LIKE_CAGE, - RR_BOTTOM_OF_THE_WELL_INNER_ROOMS, - RR_BOTTOM_OF_THE_WELL_COFFIN_ROOM, - RR_BOTTOM_OF_THE_WELL_DEAD_HAND_ROOM, - RR_BOTTOM_OF_THE_WELL_BASEMENT, - RR_BOTTOM_OF_THE_WELL_BASEMENT_USEFUL_BOMB_FLOWERS, - RR_BOTTOM_OF_THE_WELL_BASEMENT_PLATFORM, + RR_BOTW_CORRIDOR, + RR_BOTW_PERIMETER, + RR_BOTW_MIDDLE, + RR_BOTW_PIT_CAGE, + RR_BOTW_HIDDEN_POTS, + RR_BOTW_CORNER_CRAWLSPACE, + RR_BOTW_HIDDEN_PITS_ROOM, + RR_BOTW_LOCKED_CAGE, + RR_BOTW_SKULL_WALL_ROOM, + RR_BOTW_INVISIBLE_PATH, + RR_BOTW_BEHIND_MOAT, + RR_BOTW_CRYPT, + RR_BOTW_NEAR_BOSS_LOWER, + RR_BOTW_NEAR_BOSS_UPPER, + RR_BOTW_DEAD_HAND_ROOM, + RR_BOTW_B3_OOZE, + RR_BOTW_B3_BOMB_FLOWERS, + RR_BOTW_B3_BLOCKED_GRASS, + RR_BOTW_B3_CHEST_AREA, + RR_BOTW_B3_PLATFORM, - RR_BOTTOM_OF_THE_WELL_MQ_PERIMETER, - RR_BOTTOM_OF_THE_WELL_MQ_WEST_ROOM_SWITCH, - RR_BOTTOM_OF_THE_WELL_MQ_COFFIN_ROOM, - RR_BOTTOM_OF_THE_WELL_MQ_LOCKED_CAGE, - RR_BOTTOM_OF_THE_WELL_MQ_DEAD_HAND_ROOM, - RR_BOTTOM_OF_THE_WELL_MQ_MIDDLE, - RR_BOTTOM_OF_THE_WELL_MQ_BASEMENT, - RR_BOTTOM_OF_THE_WELL_MQ_BASEMENT_SWITCH_PLATFORM, + RR_BOTW_MQ_PERIMETER, + RR_BOTW_MQ_MIDDLE, + RR_BOTW_MQ_INVISIBLE_PATH, + RR_BOTW_MQ_GRAVE_ROOM, + RR_BOTW_MQ_PIT_CAGE, + RR_BOTW_MQ_BEHIND_MOAT, + RR_BOTW_MQ_CRYPT, + RR_BOTW_MQ_CORNER_CRAWLSPACE, + RR_BOTW_MQ_FLOORMASTER_ROOM, + RR_BOTW_MQ_LOCKED_CAGE, + RR_BOTW_MQ_NEAR_BOSS_LOWER, + RR_BOTW_MQ_NEAR_BOSS_UPPER, + RR_BOTW_MQ_DEAD_HAND_ROOM, + RR_BOTW_MQ_B3, + RR_BOTW_MQ_B3_PLATFORM, RR_ICE_CAVERN_BEGINNING, RR_ICE_CAVERN_HUB, @@ -3799,7 +3866,7 @@ typedef enum { RT_LENS_BOTW, RT_BOTW_CHILD_DEADHAND, RT_BOTW_BASEMENT, - RT_BOTW_MQ_PITS, + RT_BOTW_PITS, RT_BOTW_MQ_DEADHAND_KEY, RT_FOREST_FIRST_GS, RT_FOREST_OUTDOORS_EAST_GS, diff --git a/soh/soh/Enhancements/randomizer/randomizer_entrance.c b/soh/soh/Enhancements/randomizer/randomizer_entrance.c index 36e5dd983..82082a9d0 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_entrance.c +++ b/soh/soh/Enhancements/randomizer/randomizer_entrance.c @@ -16,6 +16,7 @@ #include "global.h" #include "entrance.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" extern PlayState* gPlayState; @@ -812,6 +813,8 @@ void Entrance_SetEntranceDiscovered(u16 entranceIndex, u8 isReversedEntrance) { return; } + GameInteractor_ExecuteOnRandoEntranceDiscovered(entranceIndex, isReversedEntrance); + u16 bitsPerIndex = sizeof(u32) * 8; u32 idx = entranceIndex / bitsPerIndex; if (idx < SAVEFILE_ENTRANCES_DISCOVERED_IDX_COUNT) { diff --git a/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.cpp index 0fc9c0279..33b41a3db 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.cpp @@ -3,7 +3,6 @@ #include "soh/cvar_prefixes.h" #include "soh/SohGui/SohGui.hpp" -#include #include #include #include @@ -68,7 +67,7 @@ static std::string spoilerEntranceGroupNames[] = { }; static std::string groupTypeNames[] = { - "One Way", "Overworld", "Interior", "Grotto", "Dungeon", + "One Way", "Overworld", "Interior", "Fortress", "Grotto", "Dungeon", }; // Entrance data for the tracker taken from the 3ds rando entrance tracker, and supplemented with scene/spawn info and @@ -328,32 +327,32 @@ const EntranceData entranceData[] = { { ENTR_GERUDO_TRAINING_GROUND_ENTRANCE, ENTR_GERUDOS_FORTRESS_OUTSIDE_GERUDO_TRAINING_GROUND, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "GF Outside Training Ground", "Gerudo Training Ground Entrance", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON, "gtg", 1}, { ENTRANCE_GROTTO_EXIT(GROTTO_GF_STORMS_OFFSET), ENTRANCE_GROTTO_LOAD(GROTTO_GF_STORMS_OFFSET), {{ SCENE_FAIRYS_FOUNTAIN, 0x00 }}, "GF Fairy Grotto", "GF Storms Grotto Entry", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_GROTTO, ""}, { ENTR_GERUDOS_FORTRESS_OUTSIDE_GERUDO_TRAINING_GROUND, ENTR_GERUDO_TRAINING_GROUND_ENTRANCE, SINGLE_SCENE_INFO(SCENE_GERUDO_TRAINING_GROUND), "Gerudo Training Ground Entrance", "GF Outside Training Ground", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON, "gtg"}, - { ENTR_GERUDOS_FORTRESS_1, ENTR_THIEVES_HIDEOUT_0, SINGLE_SCENE_INFO(SCENE_THIEVES_HIDEOUT), "GF Outskirts", "TH 1 Torch Cell", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_GERUDOS_FORTRESS_2, ENTR_THIEVES_HIDEOUT_1, SINGLE_SCENE_INFO(SCENE_THIEVES_HIDEOUT), "GF Near Grotto", "TH 1 Torch Cell", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_GERUDOS_FORTRESS_3, ENTR_THIEVES_HIDEOUT_2, SINGLE_SCENE_INFO(SCENE_THIEVES_HIDEOUT), "GF Near Grotto", "TH Kitchen Corridor", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_GERUDOS_FORTRESS_4, ENTR_THIEVES_HIDEOUT_3, SINGLE_SCENE_INFO(SCENE_THIEVES_HIDEOUT), "GF Above GTG", "TH Kitchen Corridor", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_GERUDOS_FORTRESS_5, ENTR_THIEVES_HIDEOUT_4, SINGLE_SCENE_INFO(SCENE_THIEVES_HIDEOUT), "GF Near Grotto", "TH Steep Slope Cell", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_GERUDOS_FORTRESS_6, ENTR_THIEVES_HIDEOUT_5, SINGLE_SCENE_INFO(SCENE_THIEVES_HIDEOUT), "GF Bottom of Lower Vines", "TH Steep Slope Cell", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_GERUDOS_FORTRESS_7, ENTR_THIEVES_HIDEOUT_6, SINGLE_SCENE_INFO(SCENE_THIEVES_HIDEOUT), "GF Above GTG", "TH Double Cell", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_GERUDOS_FORTRESS_8, ENTR_THIEVES_HIDEOUT_7, SINGLE_SCENE_INFO(SCENE_THIEVES_HIDEOUT), "GF Top of Lower Vines", "TH Double Cell", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_GERUDOS_FORTRESS_9, ENTR_THIEVES_HIDEOUT_8, SINGLE_SCENE_INFO(SCENE_THIEVES_HIDEOUT), "GF Top of Lower Vines", "TH Kitchen By Corridor", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_GERUDOS_FORTRESS_10, ENTR_THIEVES_HIDEOUT_9, SINGLE_SCENE_INFO(SCENE_THIEVES_HIDEOUT), "GF Near GS", "TH Kitchen Opposite Corridor", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_GERUDOS_FORTRESS_11, ENTR_THIEVES_HIDEOUT_10, SINGLE_SCENE_INFO(SCENE_THIEVES_HIDEOUT), "GF Below Chest", "TH Break Room", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_GERUDOS_FORTRESS_12, ENTR_THIEVES_HIDEOUT_11, SINGLE_SCENE_INFO(SCENE_THIEVES_HIDEOUT), "GF Above Jail", "TH Break Room Corridor", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_GERUDOS_FORTRESS_13, ENTR_THIEVES_HIDEOUT_12, SINGLE_SCENE_INFO(SCENE_THIEVES_HIDEOUT), "GF Below GS", "TH Dead End Cell", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_THIEVES_HIDEOUT_0, ENTR_GERUDOS_FORTRESS_1, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "TH 1 Torch Cell", "GF Outskirts", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_THIEVES_HIDEOUT_1, ENTR_GERUDOS_FORTRESS_2, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "TH 1 Torch Cell", "GF Near Grotto", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_THIEVES_HIDEOUT_2, ENTR_GERUDOS_FORTRESS_3, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "TH Kitchen Corridor", "GF Near Grotto", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_THIEVES_HIDEOUT_3, ENTR_GERUDOS_FORTRESS_4, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "TH Kitchen Corridor", "GF Above GTG", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_THIEVES_HIDEOUT_4, ENTR_GERUDOS_FORTRESS_5, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "TH Steep Slope Cell", "GF Near Grotto", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_THIEVES_HIDEOUT_5, ENTR_GERUDOS_FORTRESS_6, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "TH Steep Slope Cell", "GF Bottom of Lower Vines", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_THIEVES_HIDEOUT_6, ENTR_GERUDOS_FORTRESS_7, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "TH Double Cell", "GF Above GTG", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_THIEVES_HIDEOUT_7, ENTR_GERUDOS_FORTRESS_8, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "TH Double Cell", "GF Top of Lower Vines", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_THIEVES_HIDEOUT_8, ENTR_GERUDOS_FORTRESS_9, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "TH Kitchen By Corridor", "GF Top of Lower Vines", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_THIEVES_HIDEOUT_9, ENTR_GERUDOS_FORTRESS_10, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "TH Kitchen Opposite Corridor", "GF Near GS", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_THIEVES_HIDEOUT_10, ENTR_GERUDOS_FORTRESS_11, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "TH Break Room", "GF Below Chest", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_THIEVES_HIDEOUT_11, ENTR_GERUDOS_FORTRESS_12, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "TH Break Room Corridor", "GF Above Jail", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, - { ENTR_THIEVES_HIDEOUT_12, ENTR_GERUDOS_FORTRESS_13, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "TH Dead End Cell", "GF Below GS", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_DUNGEON}, + { ENTR_GERUDOS_FORTRESS_1, ENTR_THIEVES_HIDEOUT_0, {{ SCENE_THIEVES_HIDEOUT, 2 }}, "TH 1 Torch Cell Turn", "GF Outskirts", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_GERUDOS_FORTRESS_2, ENTR_THIEVES_HIDEOUT_1, {{ SCENE_THIEVES_HIDEOUT, 2 }}, "TH 1 Torch Cell", "GF Near Grotto East", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_GERUDOS_FORTRESS_3, ENTR_THIEVES_HIDEOUT_2, {{ SCENE_THIEVES_HIDEOUT, 3 }}, "TH Kitchen Corridor Lower", "GF Near Grotto North", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_GERUDOS_FORTRESS_4, ENTR_THIEVES_HIDEOUT_3, {{ SCENE_THIEVES_HIDEOUT, 3 }}, "TH Kitchen Corridor Upper", "GF Above GTG", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_GERUDOS_FORTRESS_5, ENTR_THIEVES_HIDEOUT_4, {{ SCENE_THIEVES_HIDEOUT, 4 }}, "TH Steep Slope Cell", "GF Near Grotto", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_GERUDOS_FORTRESS_6, ENTR_THIEVES_HIDEOUT_5, {{ SCENE_THIEVES_HIDEOUT, 4 }}, "TH Steep Slope Cell Two Ramps", "GF Bottom of Lower Vines", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_GERUDOS_FORTRESS_7, ENTR_THIEVES_HIDEOUT_6, {{ SCENE_THIEVES_HIDEOUT, 5 }}, "TH Double Cell Lower", "GF Above GTG Directly", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_GERUDOS_FORTRESS_8, ENTR_THIEVES_HIDEOUT_7, {{ SCENE_THIEVES_HIDEOUT, 5 }}, "TH Double Cell Upper", "GF Top of Lower Vines Across", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_GERUDOS_FORTRESS_9, ENTR_THIEVES_HIDEOUT_8, {{ SCENE_THIEVES_HIDEOUT, 3 }}, "TH Kitchen By Corridor", "GF Top of Lower Vines Near", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_GERUDOS_FORTRESS_10, ENTR_THIEVES_HIDEOUT_9, {{ SCENE_THIEVES_HIDEOUT, 3 }}, "TH Kitchen Opposite Corridor", "GF Near GS", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_GERUDOS_FORTRESS_11, ENTR_THIEVES_HIDEOUT_10, {{ SCENE_THIEVES_HIDEOUT, 0 }}, "TH Break Room", "GF Below Chest", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_GERUDOS_FORTRESS_12, ENTR_THIEVES_HIDEOUT_11, {{ SCENE_THIEVES_HIDEOUT, 0 }}, "TH Break Room Corridor", "GF Above Jail", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_GERUDOS_FORTRESS_13, ENTR_THIEVES_HIDEOUT_12, {{ SCENE_THIEVES_HIDEOUT, 1 }}, "TH Dead End Cell", "GF Below GS", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_THIEVES_HIDEOUT_0, ENTR_GERUDOS_FORTRESS_1, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "GF Outskirts", "TH 1 Torch Cell Turn", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_THIEVES_HIDEOUT_1, ENTR_GERUDOS_FORTRESS_2, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "GF Near Grotto East", "TH 1 Torch Cell", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_THIEVES_HIDEOUT_2, ENTR_GERUDOS_FORTRESS_3, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "GF Near Grotto North", "TH Kitchen Corridor Lower", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_THIEVES_HIDEOUT_3, ENTR_GERUDOS_FORTRESS_4, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "GF Above GTG", "TH Kitchen Corridor Upper", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_THIEVES_HIDEOUT_4, ENTR_GERUDOS_FORTRESS_5, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "GF Near Grotto", "TH Steep Slope Cell", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_THIEVES_HIDEOUT_5, ENTR_GERUDOS_FORTRESS_6, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "GF Bottom of Lower Vines", "TH Steep Slope Cell Two Ramps", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_THIEVES_HIDEOUT_6, ENTR_GERUDOS_FORTRESS_7, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "GF Above GTG Directly", "TH Double Cell Lower", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_THIEVES_HIDEOUT_7, ENTR_GERUDOS_FORTRESS_8, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "GF Top of Lower Vines Across", "TH Double Cell Upper", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_THIEVES_HIDEOUT_8, ENTR_GERUDOS_FORTRESS_9, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "GF Top of Lower Vines Near", "TH Kitchen By Corridor", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_THIEVES_HIDEOUT_9, ENTR_GERUDOS_FORTRESS_10, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "GF Near GS", "TH Kitchen Opposite Corridor", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_THIEVES_HIDEOUT_10, ENTR_GERUDOS_FORTRESS_11, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "GF Below Chest", "TH Break Room", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_THIEVES_HIDEOUT_11, ENTR_GERUDOS_FORTRESS_12, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "GF Above Jail", "TH Break Room Corridor", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, + { ENTR_THIEVES_HIDEOUT_12, ENTR_GERUDOS_FORTRESS_13, SINGLE_SCENE_INFO(SCENE_GERUDOS_FORTRESS), "GF Below GS", "TH Dead End Cell", ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_FORTRESS}, // The Wasteland { ENTR_GERUDOS_FORTRESS_GATE_EXIT, ENTR_HAUNTED_WASTELAND_EAST_EXIT, SINGLE_SCENE_INFO(SCENE_HAUNTED_WASTELAND), "Haunted Wasteland East Exit", "Gerudo Fortress Gate Exit", ENTRANCE_GROUP_HAUNTED_WASTELAND, ENTRANCE_GROUP_GERUDO_FORTRESS, ENTRANCE_TYPE_OVERWORLD, "hw,gf"}, @@ -433,11 +432,13 @@ int16_t LinkIsInArea(const EntranceData* entrance) { // Otherwise check all scenes/spawns // Not all areas require a spawn position to differeniate between another area for (auto info : entrance->scenes) { - // When a spawn position is specified, check that combination - if (info.spawn != -1) { - result = Entrance_SceneAndSpawnAre(info.scene, info.spawn); - } else { // Otherwise just check the current scene + // only check current scene when spawn info missing + if (info.spawn == -1) { result = gPlayState->sceneNum == info.scene; + } else if (gPlayState->sceneNum == SCENE_THIEVES_HIDEOUT) { // group by rooms, not spawn + result = info.scene == SCENE_THIEVES_HIDEOUT && gPlayState->roomCtx.curRoom.num == info.spawn; + } else { // Otherwise just check scene & spawn + result = Entrance_SceneAndSpawnAre(info.scene, info.spawn); } // Return the scene for tracking @@ -514,7 +515,7 @@ void SortEntranceListByType(EntranceOverride* entranceList, u8 byDest) { break; } - size_t entranceIndex = byDest ? tempList[j].override : tempList[j].index; + int16_t entranceIndex = byDest ? tempList[j].override : tempList[j].index; if (entranceData[i].type == k && entranceIndex == entranceData[i].index) { entranceList[idx] = tempList[j]; @@ -838,7 +839,7 @@ void EntranceTrackerWindow::DrawElement() { // Combine destToggle and groupToggle to get a range of 0-3 uint8_t groupType = destToggle + (groupToggle * 2); - size_t groupCount = groupToggle ? ENTRANCE_TYPE_COUNT : SPOILER_ENTRANCE_GROUP_COUNT; + size_t groupCount = groupToggle ? (size_t)ENTRANCE_TYPE_COUNT : (size_t)SPOILER_ENTRANCE_GROUP_COUNT; auto groupNames = groupToggle ? groupTypeNames : spoilerEntranceGroupNames; EntranceOverride* entranceList; @@ -874,7 +875,7 @@ void EntranceTrackerWindow::DrawElement() { uint16_t startIndex = gEntranceTrackingData.GroupOffsets[groupType][i]; bool doAreaScroll = false; - size_t undiscovered = 0; + int undiscovered = 0; std::vector displayEntrances = {}; // Loop over entrances first for filtering @@ -934,10 +935,8 @@ void EntranceTrackerWindow::DrawElement() { } displayEntrances.push_back(entrance); - } else { - if (!isDiscovered) { - undiscovered++; - } + } else if (!isDiscovered) { + undiscovered++; } } @@ -990,8 +989,7 @@ void EntranceTrackerWindow::DrawElement() { ImGui::PushStyleColor(ImGuiCol_Text, color); // Use a non-breaking space to keep the arrow from wrapping to a newline by itself - auto nbsp = u8"\u00A0"; - ImGui::TextWrapped("%s%s-> %s", origSrcName, nbsp, rplcDstName); + ImGui::TextWrapped("%s\u00A0-> %s", origSrcName, rplcDstName); ImGui::PopStyleColor(); } diff --git a/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.h b/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.h index 2d2f7143c..31592cd08 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.h +++ b/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.h @@ -37,6 +37,7 @@ typedef enum { ENTRANCE_TYPE_ONE_WAY, ENTRANCE_TYPE_OVERWORLD, ENTRANCE_TYPE_INTERIOR, + ENTRANCE_TYPE_FORTRESS, ENTRANCE_TYPE_GROTTO, ENTRANCE_TYPE_DUNGEON, ENTRANCE_TYPE_COUNT, diff --git a/soh/soh/Enhancements/randomizer/settings.cpp b/soh/soh/Enhancements/randomizer/settings.cpp index 3879b1f7e..9055f6250 100644 --- a/soh/soh/Enhancements/randomizer/settings.cpp +++ b/soh/soh/Enhancements/randomizer/settings.cpp @@ -751,7 +751,8 @@ void Settings::CreateOptions() { "Bottom of the Well Map Chest with Strength & Sticks", "The chest in the basement can be reached with strength by doing a jump slash with a lit stick to access " "the Bomb Flowers."); - OPT_TRICK(RT_BOTW_MQ_PITS, RCQUEST_MQ, RA_BOTTOM_OF_THE_WELL, { Tricks::Tag::NOVICE }, + // RANDOTODO with doorsanity, this can be relevant in Vanilla + OPT_TRICK(RT_BOTW_PITS, RCQUEST_MQ, RA_BOTTOM_OF_THE_WELL, { Tricks::Tag::NOVICE }, "Bottom of the Well MQ Jump Over the Pits", "While the pits in Bottom of the Well don't allow you to jump just by running straight at them, you can " "still get over them by side-hopping or backflipping across. With explosives, this allows you to access " @@ -986,7 +987,7 @@ void Settings::CreateOptions() { "where the eye is."); OPT_TRICK(RT_SHADOW_UMBRELLA_HOVER, RCQUEST_BOTH, RA_SHADOW_TEMPLE, { Tricks::Tag::EXPERT }, "Shadow Temple Stone Umbrella Skip", - "A very precise Hover Boots movement from off of the lower chest can get you on top of the crushing " + "A very precise Hover Boots movement from off of the lower chest can get you on top of the falling " "spikes without needing to pull the block. Applies to both Vanilla and Master Quest."); OPT_TRICK(RT_SHADOW_UMBRELLA_CLIP, RCQUEST_BOTH, RA_SHADOW_TEMPLE, { Tricks::Tag::NOVICE, Tricks::Tag::GLITCH }, "Shadow Temple Stone Umbrella Clip", @@ -995,7 +996,7 @@ void Settings::CreateOptions() { OPT_TRICK(RT_SHADOW_UMBRELLA_GS, RCQUEST_BOTH, RA_SHADOW_TEMPLE, { Tricks::Tag::EXPERT }, "Shadow Temple Falling Spikes GS with Hover Boots", "After killing the Skulltula, a very precise Hover Boots movement from off of the lower chest can get " - "you on top of the crushing spikes without needing to pull the block. From there, another very precise " + "you on top of the falling spikes without needing to pull the block. From there, another very precise " "Hover Boots movement can be used to obtain the token without needing the Hookshot. Applies to both " "Vanilla and Master Quest."); OPT_TRICK(RT_SHADOW_FREESTANDING_KEY, RCQUEST_VANILLA, RA_SHADOW_TEMPLE, { Tricks::Tag::NOVICE }, diff --git a/soh/soh/Enhancements/randomizer/static_data.cpp b/soh/soh/Enhancements/randomizer/static_data.cpp index 94d4016b8..9cd171605 100644 --- a/soh/soh/Enhancements/randomizer/static_data.cpp +++ b/soh/soh/Enhancements/randomizer/static_data.cpp @@ -225,7 +225,7 @@ StaticData::PopulateTranslationMap(std::unordered_map i if (output.contains(string)) { if (output[string] != key) { // RANDOTODO should this cause an error of some kind? - SPDLOG_DEBUG("\tREPEATED STRING IN " + message.GetEnglish(MF_CLEAN) + "\n\n"); + SPDLOG_DEBUG("REPEATED STRING IN " + message.GetEnglish(MF_CLEAN)); } } else { output[string] = key; @@ -244,7 +244,7 @@ StaticData::PopulateTranslationMap(std::unordered_mapsceneNum != SCENE_GRAVEYARD; - } - break; case VB_BE_VALID_GRAVEDIGGING_SPOT: if (CVarGetInteger(CVAR_ENHANCEMENT("DampeWin"), IS_RANDO)) { EnTk* enTk = va_arg(args, EnTk*); diff --git a/soh/soh/Enhancements/timesplits/TimeSplits.cpp b/soh/soh/Enhancements/timesplits/TimeSplits.cpp index 60ae14159..87de4a8e8 100644 --- a/soh/soh/Enhancements/timesplits/TimeSplits.cpp +++ b/soh/soh/Enhancements/timesplits/TimeSplits.cpp @@ -950,16 +950,6 @@ void TimeSplitsDrawManageList() { ImGui::EndChild(); } -void InitializeSplitDataFile() { - std::string filename = Ship::Context::GetPathRelativeToAppDirectory("timesplitdata.json"); - if (!std::filesystem::exists(filename)) { - json j; - std::ofstream file(filename); - file << j.dump(4); - file.close(); - } -} - void TimeSplitWindow::Draw() { ImGui::PushStyleColor(ImGuiCol_WindowBg, windowColor); GuiWindow::Draw(); @@ -999,7 +989,6 @@ void TimeSplitWindow::InitElement() { ImVec4(1, 1, 1, 1)); Color_RGBA8 defaultColour = { 0, 0, 0, 255 }; windowColor = VecFromRGBA8(CVarGetColor(CVAR_ENHANCEMENT("TimeSplits.WindowColor.Value"), defaultColour)); - InitializeSplitDataFile(); GameInteractor::Instance->RegisterGameHook([](u8 item) { if (item != ITEM_SKULL_TOKEN) { diff --git a/soh/soh/Network/Anchor/Anchor.cpp b/soh/soh/Network/Anchor/Anchor.cpp new file mode 100644 index 000000000..e996ac6a0 --- /dev/null +++ b/soh/soh/Network/Anchor/Anchor.cpp @@ -0,0 +1,212 @@ +#include "Anchor.h" +#include +#include +#include "soh/OTRGlobals.h" +#include "soh/Enhancements/nametag.h" +#include "soh/ObjectExtension/ObjectExtension.h" + +extern "C" { +#include "variables.h" +#include "functions.h" +extern PlayState* gPlayState; +} + +// MARK: - Overrides + +void Anchor::Enable() { + Network::Enable(CVarGetString(CVAR_REMOTE_ANCHOR("Host"), "anchor.hm64.org"), + CVarGetInteger(CVAR_REMOTE_ANCHOR("Port"), 43383)); + ownClientId = CVarGetInteger(CVAR_REMOTE_ANCHOR("LastClientId"), 0); + roomState.ownerClientId = 0; +} + +void Anchor::Disable() { + Network::Disable(); + + clients.clear(); + RefreshClientActors(); +} + +void Anchor::OnConnected() { + SendPacket_Handshake(); + RegisterHooks(); + + if (IsSaveLoaded()) { + SendPacket_RequestTeamState(); + } +} + +void Anchor::OnDisconnected() { + RegisterHooks(); +} + +void Anchor::SendJsonToRemote(nlohmann::json payload) { + if (!isConnected) { + return; + } + + payload["clientId"] = ownClientId; + if (!payload.contains("quiet")) { + SPDLOG_DEBUG("[Anchor] Sending payload:\n{}", payload.dump()); + } + Network::SendJsonToRemote(payload); +} + +void Anchor::OnIncomingJson(nlohmann::json payload) { + // If it doesn't contain a type, it's not a valid payload + if (!payload.contains("type")) { + return; + } + + // If it's not a quiet payload, log it + if (!payload.contains("quiet")) { + SPDLOG_DEBUG("[Anchor] Received payload:\n{}", payload.dump()); + } + + std::string packetType = payload["type"].get(); + + // Ignore packets from mismatched clients, except for ALL_CLIENT_STATE or UPDATE_CLIENT_STATE + if (packetType != ALL_CLIENT_STATE && packetType != UPDATE_CLIENT_STATE) { + if (payload.contains("clientId")) { + uint32_t clientId = payload["clientId"].get(); + if (clients.contains(clientId) && clients[clientId].clientVersion != clientVersion) { + return; + } + } + } + + // Handle PLAYER_UPDATE packets immediately, no need to queue + if (packetType == PLAYER_UPDATE) { + HandlePacket_PlayerUpdate(payload); + return; + } + + // Queue all packets to be processed on the game thread + std::lock_guard lock(incomingPacketQueueMutex); + incomingPacketQueue.push(payload); +} + +void Anchor::ProcessIncomingPacketQueue() { + std::lock_guard lock(incomingPacketQueueMutex); + + while (!incomingPacketQueue.empty()) { + nlohmann::json payload = incomingPacketQueue.front(); + incomingPacketQueue.pop(); + + std::string packetType = payload["type"].get(); + + isProcessingIncomingPacket = true; + + // packetType here is a string so we can't use a switch statement + if (packetType == ALL_CLIENT_STATE) + HandlePacket_AllClientState(payload); + else if (packetType == DAMAGE_PLAYER) + HandlePacket_DamagePlayer(payload); + else if (packetType == DISABLE_ANCHOR) + HandlePacket_DisableAnchor(payload); + else if (packetType == ENTRANCE_DISCOVERED) + HandlePacket_EntranceDiscovered(payload); + else if (packetType == GAME_COMPLETE) + HandlePacket_GameComplete(payload); + else if (packetType == GIVE_ITEM) + HandlePacket_GiveItem(payload); + else if (packetType == PLAYER_SFX) + HandlePacket_PlayerSfx(payload); + else if (packetType == UPDATE_TEAM_STATE) + HandlePacket_UpdateTeamState(payload); + else if (packetType == REQUEST_TEAM_STATE) + HandlePacket_RequestTeamState(payload); + else if (packetType == REQUEST_TELEPORT) + HandlePacket_RequestTeleport(payload); + else if (packetType == SERVER_MESSAGE) + HandlePacket_ServerMessage(payload); + else if (packetType == SET_CHECK_STATUS) + HandlePacket_SetCheckStatus(payload); + else if (packetType == SET_FLAG) + HandlePacket_SetFlag(payload); + else if (packetType == TELEPORT_TO) + HandlePacket_TeleportTo(payload); + else if (packetType == UNSET_FLAG) + HandlePacket_UnsetFlag(payload); + else if (packetType == UPDATE_BEANS_COUNT) + HandlePacket_UpdateBeansCount(payload); + else if (packetType == UPDATE_CLIENT_STATE) + HandlePacket_UpdateClientState(payload); + else if (packetType == UPDATE_ROOM_STATE) + HandlePacket_UpdateRoomState(payload); + else if (packetType == UPDATE_DUNGEON_ITEMS) + HandlePacket_UpdateDungeonItems(payload); + + isProcessingIncomingPacket = false; + } +} + +// MARK: - Misc/Helpers + +// Kills all existing anchor actors and respawns them with the new client data + +struct DummyPlayerClientId { + uint32_t clientId = 0; +}; +static ObjectExtension::Register DummyPlayerClientIdRegister; + +uint32_t Anchor::GetDummyPlayerClientId(const Actor* actor) { + const DummyPlayerClientId* clientId = ObjectExtension::GetInstance().Get(actor); + return clientId != nullptr ? clientId->clientId : 0; +} + +void Anchor::SetDummyPlayerClientId(const Actor* actor, uint32_t clientId) { + ObjectExtension::GetInstance().Set(actor, DummyPlayerClientId{ clientId }); +} + +void Anchor::RefreshClientActors() { + if (!IsSaveLoaded()) { + return; + } + + Actor* actor = gPlayState->actorCtx.actorLists[ACTORCAT_NPC].head; + + while (actor != NULL) { + if (actor->id == ACTOR_EN_OE2 && actor->update == DummyPlayer_Update) { + NameTag_RemoveAllForActor(actor); + Actor_Kill(actor); + } + actor = actor->next; + } + + for (auto& [clientId, client] : clients) { + if (!client.online || client.self) { + continue; + } + + spawningDummyPlayerForClientId = clientId; + // We are using a hook `ShouldActorInit` to override the init/update/draw/destroy functions of the Player we + // spawn We quickly store a mapping of "index" to clientId, then within the init function we use this to get the + // clientId and store it on player->zTargetActiveTimer (unused s32 for the dummy) for convenience + auto dummy = + Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_PLAYER, client.posRot.pos.x, client.posRot.pos.y, + client.posRot.pos.z, client.posRot.rot.x, client.posRot.rot.y, client.posRot.rot.z, 0, false); + client.player = (Player*)dummy; + } + spawningDummyPlayerForClientId = 0; +} + +bool Anchor::IsSaveLoaded() { + if (gPlayState == nullptr) { + return false; + } + + if (GET_PLAYER(gPlayState) == nullptr) { + return false; + } + + if (gSaveContext.fileNum < 0 || gSaveContext.fileNum > 2) { + return false; + } + + if (gSaveContext.gameMode != GAMEMODE_NORMAL) { + return false; + } + + return true; +} diff --git a/soh/soh/Network/Anchor/Anchor.h b/soh/soh/Network/Anchor/Anchor.h new file mode 100644 index 000000000..93c404617 --- /dev/null +++ b/soh/soh/Network/Anchor/Anchor.h @@ -0,0 +1,184 @@ +#ifndef NETWORK_ANCHOR_H +#define NETWORK_ANCHOR_H +#ifdef __cplusplus + +#include "soh/Network/Network.h" +#include +#include +#include + +extern "C" { +#include "variables.h" +#include "z64.h" +} + +void DummyPlayer_Init(Actor* actor, PlayState* play); +void DummyPlayer_Update(Actor* actor, PlayState* play); +void DummyPlayer_Draw(Actor* actor, PlayState* play); +void DummyPlayer_Destroy(Actor* actor, PlayState* play); + +typedef struct { + uint32_t clientId; + std::string name; + Color_RGB8 color; + std::string clientVersion; + std::string teamId; + bool online; + bool self; + uint32_t seed; + bool isSaveLoaded; + bool isGameComplete; + s16 sceneNum; + s32 entranceIndex; + + // Only available in PLAYER_UPDATE packets + s32 linkAge; + PosRot posRot; + Vec3s jointTable[24]; + Vec3s upperLimbRot; + s8 currentBoots; + s8 currentShield; + s8 currentTunic; + u32 stateFlags1; + u32 stateFlags2; + u8 buttonItem0; + s8 itemAction; + s8 heldItemAction; + u8 modelGroup; + s8 invincibilityTimer; + s16 unk_862; + s8 actionVar1; + + // Ptr to the dummy player + Player* player; +} AnchorClient; + +typedef struct { + uint32_t ownerClientId; + u8 pvpMode; // 0 = off, 1 = on, 2 = on with friendly fire + u8 showLocationsMode; // 0 = none, 1 = team, 2 = all + u8 teleportMode; // 0 = off, 1 = team, 2 = all + u8 syncItemsAndFlags; // 0 = off, 1 = on +} RoomState; + +class Anchor : public Network { + private: + uint32_t spawningDummyPlayerForClientId = 0; + bool shouldRefreshActors = false; + bool justLoadedSave = false; + bool isHandlingUpdateTeamState = false; + bool isProcessingIncomingPacket = false; + std::queue incomingPacketQueue; + std::mutex incomingPacketQueueMutex; + + nlohmann::json PrepClientState(); + nlohmann::json PrepRoomState(); + void RegisterHooks(); + void RefreshClientActors(); + void SetDummyPlayerClientId(const Actor* actor, uint32_t clientId); + + void HandlePacket_AllClientState(nlohmann::json payload); + void HandlePacket_ConsumeAdultTradeItem(nlohmann::json payload); + void HandlePacket_DamagePlayer(nlohmann::json payload); + void HandlePacket_DisableAnchor(nlohmann::json payload); + void HandlePacket_EntranceDiscovered(nlohmann::json payload); + void HandlePacket_GameComplete(nlohmann::json payload); + void HandlePacket_GiveItem(nlohmann::json payload); + void HandlePacket_PlayerSfx(nlohmann::json payload); + void HandlePacket_PlayerUpdate(nlohmann::json payload); + void HandlePacket_RequestTeamState(nlohmann::json payload); + void HandlePacket_RequestTeleport(nlohmann::json payload); + void HandlePacket_ServerMessage(nlohmann::json payload); + void HandlePacket_SetCheckStatus(nlohmann::json payload); + void HandlePacket_SetFlag(nlohmann::json payload); + void HandlePacket_TeleportTo(nlohmann::json payload); + void HandlePacket_UnsetFlag(nlohmann::json payload); + void HandlePacket_UpdateBeansCount(nlohmann::json payload); + void HandlePacket_UpdateClientState(nlohmann::json payload); + void HandlePacket_UpdateDungeonItems(nlohmann::json payload); + void HandlePacket_UpdateRoomState(nlohmann::json payload); + void HandlePacket_UpdateTeamState(nlohmann::json payload); + + public: + uint32_t ownClientId; + inline static const std::string clientVersion = (char*)gBuildVersion; + + // Packet types // + inline static const std::string ALL_CLIENT_STATE = "ALL_CLIENT_STATE"; + inline static const std::string DAMAGE_PLAYER = "DAMAGE_PLAYER"; + inline static const std::string DISABLE_ANCHOR = "DISABLE_ANCHOR"; + inline static const std::string ENTRANCE_DISCOVERED = "ENTRANCE_DISCOVERED"; + inline static const std::string GAME_COMPLETE = "GAME_COMPLETE"; + inline static const std::string GIVE_ITEM = "GIVE_ITEM"; + inline static const std::string HANDSHAKE = "HANDSHAKE"; + inline static const std::string PLAYER_SFX = "PLAYER_SFX"; + inline static const std::string PLAYER_UPDATE = "PLAYER_UPDATE"; + inline static const std::string REQUEST_TEAM_STATE = "REQUEST_TEAM_STATE"; + inline static const std::string REQUEST_TELEPORT = "REQUEST_TELEPORT"; + inline static const std::string SERVER_MESSAGE = "SERVER_MESSAGE"; + inline static const std::string SET_CHECK_STATUS = "SET_CHECK_STATUS"; + inline static const std::string SET_FLAG = "SET_FLAG"; + inline static const std::string TELEPORT_TO = "TELEPORT_TO"; + inline static const std::string UNSET_FLAG = "UNSET_FLAG"; + inline static const std::string UPDATE_BEANS_COUNT = "UPDATE_BEANS_COUNT"; + inline static const std::string UPDATE_CLIENT_STATE = "UPDATE_CLIENT_STATE"; + inline static const std::string UPDATE_DUNGEON_ITEMS = "UPDATE_DUNGEON_ITEMS"; + inline static const std::string UPDATE_ROOM_STATE = "UPDATE_ROOM_STATE"; + inline static const std::string UPDATE_TEAM_STATE = "UPDATE_TEAM_STATE"; + + static Anchor* Instance; + std::map clients; + RoomState roomState; + + void Enable(); + void Disable(); + void OnIncomingJson(nlohmann::json payload); + void OnConnected(); + void OnDisconnected(); + void DrawMenu(); + void ProcessIncomingPacketQueue(); + void SendJsonToRemote(nlohmann::json packet); + bool IsSaveLoaded(); + bool CanTeleportTo(uint32_t clientId); + uint32_t GetDummyPlayerClientId(const Actor* actor); + + void SendPacket_ClearTeamState(std::string teamId); + void SendPacket_DamagePlayer(u32 clientId, u8 damageEffect, u8 damage); + void SendPacket_EntranceDiscovered(u16 entranceIndex); + void SendPacket_GameComplete(); + void SendPacket_GiveItem(u16 modId, s16 getItemId); + void SendPacket_Handshake(); + void SendPacket_PlayerSfx(u16 sfxId); + void SendPacket_PlayerUpdate(); + void SendPacket_RequestTeamState(); + void SendPacket_RequestTeleport(u32 clientId); + void SendPacket_SetCheckStatus(RandomizerCheck rc); + void SendPacket_SetFlag(s16 sceneNum, s16 flagType, s16 flag); + void SendPacket_TeleportTo(u32 clientId); + void SendPacket_UnsetFlag(s16 sceneNum, s16 flagType, s16 flag); + void SendPacket_UpdateBeansCount(); + void SendPacket_UpdateClientState(); + void SendPacket_UpdateDungeonItems(); + void SendPacket_UpdateRoomState(); + void SendPacket_UpdateTeamState(); +}; + +typedef enum { + // Starting at 5 to continue from the last value in the PlayerDamageResponseType enum + DUMMY_PLAYER_HIT_RESPONSE_STUN = 5, + DUMMY_PLAYER_HIT_RESPONSE_FIRE, + DUMMY_PLAYER_HIT_RESPONSE_NORMAL, +} DummyPlayerDamageResponseType; + +class AnchorRoomWindow : public Ship::GuiWindow { + public: + using GuiWindow::GuiWindow; + + void InitElement() override{}; + void DrawElement() override; + void Draw() override; + void UpdateElement() override{}; +}; + +#endif // __cplusplus +#endif // NETWORK_ANCHOR_H diff --git a/soh/soh/Network/Anchor/AnchorRoomWindow.cpp b/soh/soh/Network/Anchor/AnchorRoomWindow.cpp new file mode 100644 index 000000000..d0d273e2c --- /dev/null +++ b/soh/soh/Network/Anchor/AnchorRoomWindow.cpp @@ -0,0 +1,129 @@ +#include "Anchor.h" +#include "soh/OTRGlobals.h" + +extern "C" { +#include "variables.h" +#include "functions.h" +extern PlayState* gPlayState; +} + +void AnchorRoomWindow::Draw() { + if (!IsVisible() || !Anchor::Instance->isConnected) { + return; + } + + ImGui::PushStyleColor(ImGuiCol_WindowBg, + ImVec4(0, 0, 0, CVarGetFloat(CVAR_SETTING("Notifications.BgOpacity"), 0.5f))); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + + auto vp = ImGui::GetMainViewport(); + ImGui::SetNextWindowViewport(vp->ID); + + ImGui::Begin("Anchor Room", nullptr, + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoFocusOnAppearing | + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar); + + DrawElement(); + + ImGui::End(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(2); +} + +void AnchorRoomWindow::DrawElement() { + bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), "")); + + if (isGlobalRoom) { + u32 activeClients = 0; + for (auto& [clientId, client] : Anchor::Instance->clients) { + if (client.online) { + activeClients++; + } + } + ImGui::Text("Players Online: %d", activeClients); + return; + } + + // First build a list of teams + std::set teams; + for (auto& [clientId, client] : Anchor::Instance->clients) { + teams.insert(client.teamId); + } + + for (auto& team : teams) { + if (teams.size() > 1) { + ImGui::SeparatorText(team.c_str()); + } + bool isOwnTeam = team == CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + for (auto& [clientId, client] : Anchor::Instance->clients) { + if (client.teamId != team) { + continue; + } + + ImGui::PushID(clientId); + + if (client.clientId == Anchor::Instance->roomState.ownerClientId) { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "%s", ICON_FA_GAVEL); + ImGui::SameLine(); + } + + if (client.self) { + ImGui::TextColored(ImVec4(0.8f, 1.0f, 0.8f, 1.0f), "%s", CVarGetString(CVAR_REMOTE_ANCHOR("Name"), "")); + } else if (!client.online) { + ImGui::TextColored(ImVec4(1, 1, 1, 0.3f), "%s - offline", client.name.c_str()); + ImGui::PopID(); + continue; + } else { + ImGui::Text("%s", client.name.c_str()); + } + + if (Anchor::Instance->roomState.showLocationsMode == 2 || + (Anchor::Instance->roomState.showLocationsMode == 1 && isOwnTeam)) { + if ((client.self ? Anchor::Instance->IsSaveLoaded() : client.isSaveLoaded)) { + ImGui::SameLine(); + ImGui::TextColored( + ImVec4(1, 1, 1, 0.5f), "- %s", + SohUtils::GetSceneName(client.self ? gPlayState->sceneNum : client.sceneNum).c_str()); + } + } + + if (Anchor::Instance->CanTeleportTo(client.clientId)) { + ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + if (ImGui::Button(ICON_FA_LOCATION_ARROW, ImVec2(20.0f, 20.0f))) { + Anchor::Instance->SendPacket_RequestTeleport(client.clientId); + } + ImGui::PopStyleVar(); + } + + if (client.clientVersion != Anchor::clientVersion) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 0, 0, 1), ICON_FA_EXCLAMATION_TRIANGLE); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("Incompatible version! Will not work together!"); + ImGui::Text("Yours: %s", Anchor::clientVersion.c_str()); + ImGui::Text("Theirs: %s", client.clientVersion.c_str()); + ImGui::EndTooltip(); + } + } + uint32_t seed = IS_RANDO ? Rando::Context::GetInstance()->GetSeed() : 0; + if (client.isSaveLoaded && Anchor::Instance->IsSaveLoaded() && client.seed != seed && client.online && + !client.self) { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 0, 0, 1), ICON_FA_EXCLAMATION_TRIANGLE); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("Seed mismatch! Continuing will break things!"); + ImGui::Text("Yours: %u", seed); + ImGui::Text("Theirs: %u", client.seed); + ImGui::EndTooltip(); + } + } + ImGui::PopID(); + } + } +} diff --git a/soh/soh/Network/Anchor/DummyPlayer.cpp b/soh/soh/Network/Anchor/DummyPlayer.cpp new file mode 100644 index 000000000..3bc085413 --- /dev/null +++ b/soh/soh/Network/Anchor/DummyPlayer.cpp @@ -0,0 +1,221 @@ +#include "Anchor.h" +#include "soh/Enhancements/nametag.h" +#include "soh/frame_interpolation.h" + +extern "C" { +#include "macros.h" +#include "variables.h" +#include "functions.h" +extern PlayState* gPlayState; + +void Player_UseItem(PlayState* play, Player* player, s32 item); +void Player_Draw(Actor* actor, PlayState* play); +} + +static DamageTable DummyPlayerDamageTable = { + /* Deku nut */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN), + /* Deku stick */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Slingshot */ DMG_ENTRY(1, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Explosive */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Boomerang */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN), + /* Normal arrow */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Hammer swing */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_KNOCKBACK_LARGE), + /* Hookshot */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_STUN), + /* Kokiri sword */ DMG_ENTRY(1, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Master sword */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Giant's Knife */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Fire arrow */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_FIRE), + /* Ice arrow */ DMG_ENTRY(4, PLAYER_HIT_RESPONSE_ICE_TRAP), + /* Light arrow */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_ELECTRIC_SHOCK), + /* Unk arrow 1 */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_NONE), + /* Unk arrow 2 */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_NONE), + /* Unk arrow 3 */ DMG_ENTRY(2, PLAYER_HIT_RESPONSE_NONE), + /* Fire magic */ DMG_ENTRY(0, DUMMY_PLAYER_HIT_RESPONSE_FIRE), + /* Ice magic */ DMG_ENTRY(3, PLAYER_HIT_RESPONSE_ICE_TRAP), + /* Light magic */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_ELECTRIC_SHOCK), + /* Shield */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE), + /* Mirror Ray */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE), + /* Kokiri spin */ DMG_ENTRY(1, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Giant spin */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Master spin */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Kokiri jump */ DMG_ENTRY(2, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Giant jump */ DMG_ENTRY(8, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Master jump */ DMG_ENTRY(4, DUMMY_PLAYER_HIT_RESPONSE_NORMAL), + /* Unknown 1 */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE), + /* Unblockable */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE), + /* Hammer jump */ DMG_ENTRY(4, PLAYER_HIT_RESPONSE_KNOCKBACK_LARGE), + /* Unknown 2 */ DMG_ENTRY(0, PLAYER_HIT_RESPONSE_NONE), +}; + +void DummyPlayer_Init(Actor* actor, PlayState* play) { + Player* player = (Player*)actor; + + uint32_t clientId = Anchor::Instance->GetDummyPlayerClientId(actor); + + if (!Anchor::Instance->clients.contains(clientId)) { + Actor_Kill(actor); + return; + } + + AnchorClient& client = Anchor::Instance->clients[clientId]; + + // Hack to account for usage of gSaveContext in Player_Init + s32 originalAge = gSaveContext.linkAge; + gSaveContext.linkAge = client.linkAge; + + // #region modeled after EnTorch2_Init and Player_Init + actor->room = -1; + player->itemAction = player->heldItemAction = -1; + player->heldItemId = ITEM_NONE; + Player_UseItem(play, player, ITEM_NONE); + Player_SetModelGroup(player, Player_ActionToModelGroup(player, player->heldItemAction)); + play->playerInit(player, play, gPlayerSkelHeaders[client.linkAge]); + + play->func_11D54(player, play); + // #endregion + + player->cylinder.base.acFlags = AC_ON | AC_TYPE_PLAYER; + player->cylinder.base.ocFlags2 = OC2_TYPE_1; + player->cylinder.info.bumperFlags = BUMP_ON | BUMP_HOOKABLE | BUMP_NO_HITMARK; + player->actor.flags |= ACTOR_FLAG_HOOKSHOT_PULLS_PLAYER; + player->cylinder.dim.radius = 30; + player->actor.colChkInfo.damageTable = &DummyPlayerDamageTable; + + gSaveContext.linkAge = originalAge; + + bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), "")); + + if (!isGlobalRoom) { + NameTag_RegisterForActorWithOptions(actor, client.name.c_str(), {}); + } +} + +void Math_Vec3s_Copy(Vec3s* dest, Vec3s* src) { + dest->x = src->x; + dest->y = src->y; + dest->z = src->z; +} + +// Update the actor with new data from the client +void DummyPlayer_Update(Actor* actor, PlayState* play) { + Player* player = (Player*)actor; + + uint32_t clientId = Anchor::Instance->GetDummyPlayerClientId(actor); + + if (!Anchor::Instance->clients.contains(clientId)) { + Actor_Kill(actor); + return; + } + + AnchorClient& client = Anchor::Instance->clients[clientId]; + + if (client.sceneNum != gPlayState->sceneNum || !client.online || !client.isSaveLoaded) { + actor->world.pos.x = -9999.0f; + actor->world.pos.y = -9999.0f; + actor->world.pos.z = -9999.0f; + actor->shape.shadowAlpha = 0; + return; + } + + actor->shape.shadowAlpha = 255; + Math_Vec3s_Copy(&player->upperLimbRot, &client.upperLimbRot); + Math_Vec3s_Copy(&actor->shape.rot, &client.posRot.rot); + Math_Vec3f_Copy(&actor->world.pos, &client.posRot.pos); + player->skelAnime.jointTable = client.jointTable; + player->currentBoots = client.currentBoots; + player->currentShield = client.currentShield; + player->currentTunic = client.currentTunic; + player->stateFlags1 = client.stateFlags1; + player->stateFlags2 = client.stateFlags2; + player->itemAction = client.itemAction; + player->heldItemAction = client.heldItemAction; + player->invincibilityTimer = client.invincibilityTimer; + player->unk_862 = client.unk_862; + player->av1.actionVar1 = client.actionVar1; + + if (player->modelGroup != client.modelGroup) { + // Hack to account for usage of gSaveContext + s32 originalAge = gSaveContext.linkAge; + gSaveContext.linkAge = client.linkAge; + u8 originalButtonItem0 = gSaveContext.equips.buttonItems[0]; + gSaveContext.equips.buttonItems[0] = client.buttonItem0; + Player_SetModelGroup(player, client.modelGroup); + gSaveContext.linkAge = originalAge; + gSaveContext.equips.buttonItems[0] = originalButtonItem0; + } + + if (Anchor::Instance->roomState.pvpMode == 0 || + (Anchor::Instance->roomState.pvpMode == 1 && + client.teamId == CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"))) { + actor->flags |= ACTOR_FLAG_LOCK_ON_DISABLED; + return; + } + + actor->flags &= ~ACTOR_FLAG_LOCK_ON_DISABLED; + + if (player->cylinder.base.acFlags & AC_HIT && player->invincibilityTimer == 0) { + Anchor::Instance->SendPacket_DamagePlayer(client.clientId, player->actor.colChkInfo.damageEffect, + player->actor.colChkInfo.damage); + if (player->actor.colChkInfo.damageEffect == DUMMY_PLAYER_HIT_RESPONSE_STUN) { + Actor_SetColorFilter(&player->actor, 0, 0xFF, 0, 24); + } else { + player->invincibilityTimer = 20; + } + } + + Collider_UpdateCylinder(&player->actor, &player->cylinder); + + if (!(player->stateFlags2 & PLAYER_STATE2_FROZEN)) { + if (!(player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_HANGING_OFF_LEDGE | + PLAYER_STATE1_CLIMBING_LEDGE | PLAYER_STATE1_ON_HORSE))) { + CollisionCheck_SetOC(play, &play->colChkCtx, &player->cylinder.base); + } + + if (!(player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_DAMAGED)) && + (player->invincibilityTimer <= 0)) { + CollisionCheck_SetAC(play, &play->colChkCtx, &player->cylinder.base); + + if (player->invincibilityTimer < 0) { + CollisionCheck_SetAT(play, &play->colChkCtx, &player->cylinder.base); + } + } + } + + if (player->stateFlags1 & (PLAYER_STATE1_DEAD | PLAYER_STATE1_IN_ITEM_CS | PLAYER_STATE1_IN_CUTSCENE)) { + player->actor.colChkInfo.mass = MASS_IMMOVABLE; + } else { + player->actor.colChkInfo.mass = 50; + } + + Collider_ResetCylinderAC(play, &player->cylinder.base); +} + +void DummyPlayer_Draw(Actor* actor, PlayState* play) { + Player* player = (Player*)actor; + + uint32_t clientId = Anchor::Instance->GetDummyPlayerClientId(actor); + + if (!Anchor::Instance->clients.contains(clientId)) { + Actor_Kill(actor); + return; + } + + AnchorClient& client = Anchor::Instance->clients[clientId]; + + if (client.sceneNum != gPlayState->sceneNum || !client.online || !client.isSaveLoaded) { + return; + } + + // Hack to account for usage of gSaveContext in Player_Draw + s32 originalAge = gSaveContext.linkAge; + gSaveContext.linkAge = client.linkAge; + u8 originalButtonItem0 = gSaveContext.equips.buttonItems[0]; + gSaveContext.equips.buttonItems[0] = client.buttonItem0; + + Player_Draw((Actor*)player, play); + gSaveContext.linkAge = originalAge; + gSaveContext.equips.buttonItems[0] = originalButtonItem0; +} + +void DummyPlayer_Destroy(Actor* actor, PlayState* play) { +} diff --git a/soh/soh/Network/Anchor/HookHandlers.cpp b/soh/soh/Network/Anchor/HookHandlers.cpp new file mode 100644 index 000000000..9a4622c29 --- /dev/null +++ b/soh/soh/Network/Anchor/HookHandlers.cpp @@ -0,0 +1,369 @@ +#include "Anchor.h" +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" + +extern "C" { +#include "variables.h" +#include "functions.h" +#include "src/overlays/actors/ovl_Bg_Bombwall/z_bg_bombwall.h" +#include "src/overlays/actors/ovl_Bg_Breakwall/z_bg_breakwall.h" +#include "src/overlays/actors/ovl_Bg_Haka_Zou/z_bg_haka_zou.h" +#include "src/overlays/actors/ovl_Bg_Hidan_Hamstep/z_bg_hidan_hamstep.h" +#include "src/overlays/actors/ovl_Bg_Hidan_Hrock/z_bg_hidan_hrock.h" +#include "src/overlays/actors/ovl_Bg_Ice_Shelter/z_bg_ice_shelter.h" +#include "src/overlays/actors/ovl_Bg_Jya_Bombchuiwa/z_bg_jya_bombchuiwa.h" +#include "src/overlays/actors/ovl_Bg_Jya_Bombiwa/z_bg_jya_bombiwa.h" +#include "src/overlays/actors/ovl_Bg_Mizu_Bwall/z_bg_mizu_bwall.h" +#include "src/overlays/actors/ovl_Bg_Spot08_Bakudankabe/z_bg_spot08_bakudankabe.h" +#include "src/overlays/actors/ovl_Bg_Spot11_Bakudankabe/z_bg_spot11_bakudankabe.h" +#include "src/overlays/actors/ovl_Bg_Spot17_Bakudankabe/z_bg_spot17_bakudankabe.h" +#include "src/overlays/actors/ovl_Bg_Ydan_Maruta/z_bg_ydan_maruta.h" +#include "src/overlays/actors/ovl_Bg_Ydan_Sp/z_bg_ydan_sp.h" +#include "src/overlays/actors/ovl_Door_Shutter/z_door_shutter.h" +#include "src/overlays/actors/ovl_En_Door/z_en_door.h" +#include "src/overlays/actors/ovl_En_Si/z_en_si.h" +#include "src/overlays/actors/ovl_En_Sw/z_en_sw.h" +#include "src/overlays/actors/ovl_Item_B_Heart/z_item_b_heart.h" +#include "src/overlays/actors/ovl_Obj_Bombiwa/z_obj_bombiwa.h" +#include "src/overlays/actors/ovl_Obj_Hamishi/z_obj_hamishi.h" +#include "src/overlays/actors/ovl_Bg_Hidan_Dalm/z_bg_hidan_dalm.h" +#include "src/overlays/actors/ovl_Bg_Hidan_Kowarerukabe/z_bg_hidan_kowarerukabe.h" + +extern PlayState* gPlayState; + +void func_8086ED70(BgBombwall* bgBombwall, PlayState* play); +void BgBreakwall_Wait(BgBreakwall* bgBreakwall, PlayState* play); +void func_80883000(BgHakaZou* bgHakaZou, PlayState* play); +void func_808887C4(BgHidanHamstep* bgHidanHamstep, PlayState* play); +void func_808896B8(BgHidanHrock* bgHidanHrock, PlayState* play); +void func_8089107C(BgIceShelter* bgIceShelter, PlayState* play); +void func_808911BC(BgIceShelter* bgIceShelter); +void ObjBombiwa_Break(ObjBombiwa* objBombiwa, PlayState* play); +void ObjHamishi_Break(ObjHamishi* objHamishi, PlayState* play); +void BgJyaBombchuiwa_WaitForExplosion(BgJyaBombchuiwa* bgJyaBombchuiwa, PlayState* play); +void BgMizuBwall_Idle(BgMizuBwall* bgMizuBwall, PlayState* play); +void func_808B6BC0(BgSpot17Bakudankabe* bgSpot17Bakudankabe, PlayState* play); +void func_808BF078(BgYdanMaruta* bgYdanMaruta, PlayState* play); +void BgYdanSp_FloorWebIdle(BgYdanSp* bgYdanSp, PlayState* play); +void BgYdanSp_WallWebIdle(BgYdanSp* bgYdanSp, PlayState* play); +void BgYdanSp_BurnWeb(BgYdanSp* bgYdanSp, PlayState* play); +void EnDoor_Idle(EnDoor* enDoor, PlayState* play); +} + +void Anchor::RegisterHooks() { + + // #region Hooks that are required for basic Anchor functionality + + COND_HOOK(OnSceneSpawnActors, isConnected, [&]() { + SendPacket_UpdateClientState(); + + if (IsSaveLoaded()) { + RefreshClientActors(); + } + }); + + COND_HOOK(OnPresentFileSelect, isConnected, [&]() { SendPacket_UpdateClientState(); }); + + COND_ID_HOOK(ShouldActorInit, ACTOR_PLAYER, isConnected, [&](void* actorRef, bool* should) { + Actor* actor = (Actor*)actorRef; + + if (spawningDummyPlayerForClientId != 0) { + SetDummyPlayerClientId(actor, spawningDummyPlayerForClientId); + + // By the time we get here, the actor was already added to the ACTORCAT_PLAYER list, so we need to move it + Actor_ChangeCategory(gPlayState, &gPlayState->actorCtx, actor, ACTORCAT_NPC); + actor->id = ACTOR_EN_OE2; + actor->category = ACTORCAT_NPC; + actor->init = DummyPlayer_Init; + actor->update = DummyPlayer_Update; + actor->draw = DummyPlayer_Draw; + actor->destroy = DummyPlayer_Destroy; + } + }); + + COND_HOOK(OnPlayerUpdate, isConnected, [&]() { + if (justLoadedSave) { + justLoadedSave = false; + SendPacket_RequestTeamState(); + } + + if (shouldRefreshActors) { + shouldRefreshActors = false; + RefreshClientActors(); + } + + SendPacket_PlayerUpdate(); + }); + + COND_HOOK(OnGameFrameUpdate, isConnected, [&]() { ProcessIncomingPacketQueue(); }); + + COND_HOOK(OnPlayerSfx, isConnected, [&](u16 sfxId) { SendPacket_PlayerSfx(sfxId); }); + + COND_HOOK(OnLoadGame, isConnected, [&](s16 fileNum) { justLoadedSave = true; }); + + COND_HOOK(OnSaveFile, isConnected, [&](s16 fileNum, int sectionID) { + if (sectionID == 0) { + SendPacket_UpdateTeamState(); + } + }); + + COND_HOOK(OnFlagSet, isConnected, + [&](s16 flagType, s16 flag) { SendPacket_SetFlag(SCENE_ID_MAX, flagType, flag); }); + + COND_HOOK(OnFlagUnset, isConnected, + [&](s16 flagType, s16 flag) { SendPacket_UnsetFlag(SCENE_ID_MAX, flagType, flag); }); + + COND_HOOK(OnSceneFlagSet, isConnected, + [&](s16 sceneNum, s16 flagType, s16 flag) { SendPacket_SetFlag(sceneNum, flagType, flag); }); + + COND_HOOK(OnSceneFlagUnset, isConnected, + [&](s16 sceneNum, s16 flagType, s16 flag) { SendPacket_UnsetFlag(sceneNum, flagType, flag); }); + + COND_HOOK(OnRandoSetCheckStatus, isConnected, [&](RandomizerCheck rc, RandomizerCheckStatus status) { + if (!isHandlingUpdateTeamState) { + SendPacket_SetCheckStatus(rc); + } + }); + + COND_HOOK(OnRandoSetIsSkipped, isConnected, [&](RandomizerCheck rc, bool isSkipped) { + if (!isHandlingUpdateTeamState) { + SendPacket_SetCheckStatus(rc); + } + }); + + COND_HOOK(OnRandoEntranceDiscovered, isConnected, + [&](u16 entranceIndex, u8 isReversedEntrance) { SendPacket_EntranceDiscovered(entranceIndex); }); + + COND_ID_HOOK(OnBossDefeat, ACTOR_BOSS_GANON2, isConnected, [&](void* refActor) { SendPacket_GameComplete(); }); + + COND_HOOK(OnItemReceive, isConnected, [&](GetItemEntry itemEntry) { + // Handle vanilla dungeon items a bit differently + if (itemEntry.modIndex == MOD_NONE && + (itemEntry.itemId >= ITEM_KEY_BOSS && itemEntry.itemId <= ITEM_KEY_SMALL)) { + SendPacket_UpdateDungeonItems(); + return; + } + + SendPacket_GiveItem(itemEntry.tableId, itemEntry.getItemId); + }); + + COND_HOOK(OnDungeonKeyUsed, isConnected, [&](uint16_t mapIndex) { + // Handle vanilla dungeon items a bit differently + SendPacket_UpdateDungeonItems(); + }); + + // #endregion + + // #region Hooks that are purely to sync actor states across the clients, not super essential + + COND_ID_HOOK(OnActorUpdate, ACTOR_EN_ITEM00, isConnected, [&](void* refActor) { + EnItem00* actor = static_cast(refActor); + + if (Flags_GetCollectible(gPlayState, actor->collectibleFlag)) { + Actor_Kill(&actor->actor); + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_BOMBWALL, isConnected, [&](void* refActor, bool* should) { + BgBombwall* actor = static_cast(refActor); + + if (actor->actionFunc == func_8086ED70 && Flags_GetSwitch(gPlayState, actor->dyna.actor.params & 0x3F)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_BREAKWALL, isConnected, [&](void* refActor, bool* should) { + BgBreakwall* actor = static_cast(refActor); + + if (actor->actionFunc == BgBreakwall_Wait && Flags_GetSwitch(gPlayState, actor->dyna.actor.params & 0x3F)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_HAKA_ZOU, isConnected, [&](void* refActor, bool* should) { + BgHakaZou* actor = static_cast(refActor); + + if (actor->actionFunc == func_80883000 && Flags_GetSwitch(gPlayState, actor->switchFlag)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_HIDAN_HAMSTEP, isConnected, [&](void* refActor, bool* should) { + BgHidanHamstep* actor = static_cast(refActor); + + if (actor->actionFunc == func_808887C4 && Flags_GetSwitch(gPlayState, (actor->dyna.actor.params >> 8) & 0xFF)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_HIDAN_HROCK, isConnected, [&](void* refActor, bool* should) { + BgHidanHrock* actor = static_cast(refActor); + + if (actor->actionFunc == func_808896B8 && Flags_GetSwitch(gPlayState, actor->unk_16A)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_ICE_SHELTER, isConnected, [&](void* refActor, bool* should) { + BgIceShelter* actor = static_cast(refActor); + + if (actor->actionFunc == func_8089107C && Flags_GetSwitch(gPlayState, actor->dyna.actor.params & 0x3F)) { + func_808911BC(actor); + Audio_PlayActorSound2(&actor->dyna.actor, NA_SE_EV_ICE_MELT); + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_JYA_BOMBCHUIWA, isConnected, [&](void* refActor, bool* should) { + BgJyaBombchuiwa* actor = static_cast(refActor); + + if (actor->actionFunc == BgJyaBombchuiwa_WaitForExplosion && + Flags_GetSwitch(gPlayState, actor->actor.params & 0x3F)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_JYA_BOMBIWA, isConnected, [&](void* refActor, bool* should) { + BgJyaBombiwa* actor = static_cast(refActor); + + if (Flags_GetSwitch(gPlayState, actor->dyna.actor.params & 0x3F)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_MIZU_BWALL, isConnected, [&](void* refActor, bool* should) { + BgMizuBwall* actor = static_cast(refActor); + + if (actor->actionFunc == BgMizuBwall_Idle && + Flags_GetSwitch(gPlayState, ((u16)actor->dyna.actor.params >> 8) & 0x3F)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_SPOT08_BAKUDANKABE, isConnected, [&](void* refActor, bool* should) { + BgSpot08Bakudankabe* actor = static_cast(refActor); + + if (Flags_GetSwitch(gPlayState, (actor->dyna.actor.params & 0x3F))) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_SPOT11_BAKUDANKABE, isConnected, [&](void* refActor, bool* should) { + BgSpot11Bakudankabe* actor = static_cast(refActor); + + if (Flags_GetSwitch(gPlayState, (actor->dyna.actor.params & 0x3F))) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_SPOT17_BAKUDANKABE, isConnected, [&](void* refActor, bool* should) { + BgSpot17Bakudankabe* actor = static_cast(refActor); + + if (Flags_GetSwitch(gPlayState, (actor->dyna.actor.params & 0x3F))) { + func_808B6BC0(actor, gPlayState); + SoundSource_PlaySfxAtFixedWorldPos(gPlayState, &actor->dyna.actor.world.pos, 40, NA_SE_EV_WALL_BROKEN); + Sfx_PlaySfxCentered(NA_SE_SY_CORRECT_CHIME); + Actor_Kill(&actor->dyna.actor); + *should = false; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_YDAN_MARUTA, isConnected, [&](void* refActor, bool* should) { + BgYdanMaruta* actor = static_cast(refActor); + + if (actor->actionFunc == func_808BF078 && Flags_GetSwitch(gPlayState, actor->switchFlag)) { + actor->collider.base.acFlags |= AC_HIT; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_BG_YDAN_SP, isConnected, [&](void* refActor, bool* should) { + BgYdanSp* actor = static_cast(refActor); + + if ((actor->actionFunc == BgYdanSp_FloorWebIdle || actor->actionFunc == BgYdanSp_WallWebIdle) && + Flags_GetSwitch(gPlayState, actor->isDestroyedSwitchFlag)) { + BgYdanSp_BurnWeb(actor, gPlayState); + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_DOOR_SHUTTER, isConnected, [&](void* refActor, bool* should) { + DoorShutter* actor = static_cast(refActor); + + if (Flags_GetSwitch(gPlayState, actor->dyna.actor.params & 0x3F)) { + DECR(actor->unk_16E); + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_EN_DOOR, isConnected, [&](void* refActor, bool* should) { + EnDoor* actor = static_cast(refActor); + + if (actor->actionFunc == EnDoor_Idle && Flags_GetSwitch(gPlayState, actor->actor.params & 0x3F)) { + DECR(actor->lockTimer); + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_EN_SI, isConnected, [&](void* refActor, bool* should) { + EnSi* actor = static_cast(refActor); + + if (GET_GS_FLAGS((actor->actor.params & 0x1F00) >> 8) & (actor->actor.params & 0xFF)) { + Actor_Kill(&actor->actor); + *should = false; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_EN_SW, isConnected, [&](void* refActor, bool* should) { + EnSw* actor = static_cast(refActor); + + if (GET_GS_FLAGS((actor->actor.params & 0x1F00) >> 8) & (actor->actor.params & 0xFF)) { + Actor_Kill(&actor->actor); + *should = false; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_ITEM_B_HEART, isConnected, [&](void* refActor, bool* should) { + ItemBHeart* actor = static_cast(refActor); + + if (Flags_GetCollectible(gPlayState, 0x1F)) { + Actor_Kill(&actor->actor); + *should = false; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_OBJ_BOMBIWA, isConnected, [&](void* refActor, bool* should) { + ObjBombiwa* actor = static_cast(refActor); + + if (Flags_GetSwitch(gPlayState, actor->actor.params & 0x3F)) { + ObjBombiwa_Break(actor, gPlayState); + SoundSource_PlaySfxAtFixedWorldPos(gPlayState, &actor->actor.world.pos, 80, NA_SE_EV_WALL_BROKEN); + Actor_Kill(&actor->actor); + *should = false; + } + }); + + COND_ID_HOOK(ShouldActorUpdate, ACTOR_OBJ_HAMISHI, isConnected, [&](void* refActor, bool* should) { + ObjHamishi* actor = static_cast(refActor); + + if (Flags_GetSwitch(gPlayState, actor->actor.params & 0x3F)) { + ObjHamishi_Break(actor, gPlayState); + SoundSource_PlaySfxAtFixedWorldPos(gPlayState, &actor->actor.world.pos, 40, NA_SE_EV_WALL_BROKEN); + Actor_Kill(&actor->actor); + *should = false; + } + }); + + COND_VB_SHOULD(VB_HAMMER_TOTEM_BREAK, isConnected, { + BgHidanDalm* actor = va_arg(args, BgHidanDalm*); + + if (Flags_GetSwitch(gPlayState, actor->switchFlag)) { + *should = true; + } + }); + + COND_VB_SHOULD(VB_FIRE_TEMPLE_BOMBABLE_WALL_BREAK, isConnected, { + BgHidanKowarerukabe* actor = va_arg(args, BgHidanKowarerukabe*); + + if (Flags_GetSwitch(gPlayState, (actor->dyna.actor.params >> 8) & 0x3F)) { + *should = true; + } + }); + + // #endregion +} diff --git a/soh/soh/Network/Anchor/JsonConversions.hpp b/soh/soh/Network/Anchor/JsonConversions.hpp new file mode 100644 index 000000000..b99b78496 --- /dev/null +++ b/soh/soh/Network/Anchor/JsonConversions.hpp @@ -0,0 +1,205 @@ +#ifndef NETWORK_ANCHOR_JSON_CONVERSIONS_H +#define NETWORK_ANCHOR_JSON_CONVERSIONS_H +#ifdef __cplusplus + +#include +#include +#include "Anchor.h" + +extern "C" { +#include "z64.h" +} + +using json = nlohmann::json; + +inline void from_json(const json& j, Color_RGB8& color) { + j.at("r").get_to(color.r); + j.at("g").get_to(color.g); + j.at("b").get_to(color.b); +} + +inline void to_json(json& j, const Color_RGB8& color) { + j = json{ { "r", color.r }, { "g", color.g }, { "b", color.b } }; +} + +inline void to_json(json& j, const Vec3f& vec) { + j = json{ { "x", vec.x }, { "y", vec.y }, { "z", vec.z } }; +} + +inline void to_json(json& j, const Vec3s& vec) { + j = json{ { "x", vec.x }, { "y", vec.y }, { "z", vec.z } }; +} + +inline void from_json(const json& j, Vec3f& vec) { + j.at("x").get_to(vec.x); + j.at("y").get_to(vec.y); + j.at("z").get_to(vec.z); +} + +inline void from_json(const json& j, Vec3s& vec) { + j.at("x").get_to(vec.x); + j.at("y").get_to(vec.y); + j.at("z").get_to(vec.z); +} + +inline void to_json(json& j, const PosRot& posRot) { + j = json{ { "pos", posRot.pos }, { "rot", posRot.rot } }; +} + +inline void from_json(const json& j, PosRot& posRot) { + j.at("pos").get_to(posRot.pos); + j.at("rot").get_to(posRot.rot); +} + +inline void from_json(const json& j, AnchorClient& client) { + j.contains("clientId") ? j.at("clientId").get_to(client.clientId) : client.clientId = 0; + j.contains("name") ? j.at("name").get_to(client.name) : client.name = "???"; + j.contains("color") ? j.at("color").get_to(client.color) : client.color = { 255, 255, 255 }; + j.contains("clientVersion") ? j.at("clientVersion").get_to(client.clientVersion) : client.clientVersion = "???"; + j.contains("teamId") ? j.at("teamId").get_to(client.teamId) : client.teamId = "default"; + j.contains("online") ? j.at("online").get_to(client.online) : client.online = false; + j.contains("seed") ? j.at("seed").get_to(client.seed) : client.seed = 0; + j.contains("isSaveLoaded") ? j.at("isSaveLoaded").get_to(client.isSaveLoaded) : client.isSaveLoaded = false; + j.contains("isGameComplete") ? j.at("isGameComplete").get_to(client.isGameComplete) : client.isGameComplete = false; + j.contains("sceneNum") ? j.at("sceneNum").get_to(client.sceneNum) : client.sceneNum = SCENE_ID_MAX; + j.contains("entranceIndex") ? j.at("entranceIndex").get_to(client.entranceIndex) : client.entranceIndex = 0; + j.contains("self") ? j.at("self").get_to(client.self) : client.self = false; +} + +inline void to_json(json& j, const Inventory& inventory) { + j = json{ { "items", inventory.items }, + { "ammo", inventory.ammo }, + { "equipment", inventory.equipment }, + { "upgrades", inventory.upgrades }, + { "questItems", inventory.questItems }, + { "dungeonItems", inventory.dungeonItems }, + { "dungeonKeys", inventory.dungeonKeys }, + { "defenseHearts", inventory.defenseHearts }, + { "gsTokens", inventory.gsTokens } }; +} + +inline void from_json(const json& j, Inventory& inventory) { + j.at("items").get_to(inventory.items); + j.at("ammo").get_to(inventory.ammo); + j.at("equipment").get_to(inventory.equipment); + j.at("upgrades").get_to(inventory.upgrades); + j.at("questItems").get_to(inventory.questItems); + j.at("dungeonItems").get_to(inventory.dungeonItems); + j.at("dungeonKeys").get_to(inventory.dungeonKeys); + j.at("defenseHearts").get_to(inventory.defenseHearts); + j.at("gsTokens").get_to(inventory.gsTokens); +} + +inline void to_json(json& j, const SohStats& sohStats) { + j = json{ + { "entrancesDiscovered", sohStats.entrancesDiscovered }, + { "fileCreatedAt", sohStats.fileCreatedAt }, + }; +} + +inline void from_json(const json& j, SohStats& sohStats) { + j.at("entrancesDiscovered").get_to(sohStats.entrancesDiscovered); + j.at("fileCreatedAt").get_to(sohStats.fileCreatedAt); +} + +inline void to_json(json& j, const ShipRandomizerSaveContextData& shipRandomizerSaveContextData) { + j = json{ + { "triforcePiecesCollected", shipRandomizerSaveContextData.triforcePiecesCollected }, + }; +} + +inline void from_json(const json& j, ShipRandomizerSaveContextData& shipRandomizerSaveContextData) { + j.at("triforcePiecesCollected").get_to(shipRandomizerSaveContextData.triforcePiecesCollected); +} + +inline void to_json(json& j, const ShipQuestSpecificSaveContextData& shipQuestSpecificSaveContextData) { + j = json{ + { "randomizer", shipQuestSpecificSaveContextData.randomizer }, + }; +} + +inline void from_json(const json& j, ShipQuestSpecificSaveContextData& shipQuestSpecificSaveContextData) { + j.at("randomizer").get_to(shipQuestSpecificSaveContextData.randomizer); +} + +inline void to_json(json& j, const ShipQuestSaveContextData& shipQuestSaveContextData) { + j = json{ + { "id", shipQuestSaveContextData.id }, + { "data", shipQuestSaveContextData.data }, + }; +} + +inline void from_json(const json& j, ShipQuestSaveContextData& shipQuestSaveContextData) { + j.at("id").get_to(shipQuestSaveContextData.id); + j.at("data").get_to(shipQuestSaveContextData.data); +} + +inline void to_json(json& j, const ShipSaveContextData& shipSaveContextData) { + j = json{ + { "stats", shipSaveContextData.stats }, + { "quest", shipSaveContextData.quest }, + { "randomizerInf", shipSaveContextData.randomizerInf }, + }; +} + +inline void from_json(const json& j, ShipSaveContextData& shipSaveContextData) { + j.at("stats").get_to(shipSaveContextData.stats); + j.at("quest").get_to(shipSaveContextData.quest); + j.at("randomizerInf").get_to(shipSaveContextData.randomizerInf); +} + +inline void to_json(json& j, const SaveContext& saveContext) { + std::vector sceneFlagsArray; + for (const auto& sceneFlags : saveContext.sceneFlags) { + sceneFlagsArray.push_back(sceneFlags.chest); + sceneFlagsArray.push_back(sceneFlags.swch); + sceneFlagsArray.push_back(sceneFlags.clear); + sceneFlagsArray.push_back(sceneFlags.collect); + } + + j = json{ + { "healthCapacity", saveContext.healthCapacity }, + { "magicLevel", saveContext.magicLevel }, + { "magicCapacity", saveContext.magicCapacity }, + { "isMagicAcquired", saveContext.isMagicAcquired }, + { "isDoubleMagicAcquired", saveContext.isDoubleMagicAcquired }, + { "isDoubleDefenseAcquired", saveContext.isDoubleDefenseAcquired }, + { "bgsFlag", saveContext.bgsFlag }, + { "swordHealth", saveContext.swordHealth }, + { "sceneFlags", sceneFlagsArray }, + { "eventChkInf", saveContext.eventChkInf }, + { "itemGetInf", saveContext.itemGetInf }, + { "infTable", saveContext.infTable }, + { "gsFlags", saveContext.gsFlags }, + { "inventory", saveContext.inventory }, + { "ship", saveContext.ship }, + }; +} + +inline void from_json(const json& j, SaveContext& saveContext) { + j.at("healthCapacity").get_to(saveContext.healthCapacity); + j.at("magicLevel").get_to(saveContext.magicLevel); + j.at("magicCapacity").get_to(saveContext.magicCapacity); + j.at("isMagicAcquired").get_to(saveContext.isMagicAcquired); + j.at("isDoubleMagicAcquired").get_to(saveContext.isDoubleMagicAcquired); + j.at("isDoubleDefenseAcquired").get_to(saveContext.isDoubleDefenseAcquired); + j.at("bgsFlag").get_to(saveContext.bgsFlag); + j.at("swordHealth").get_to(saveContext.swordHealth); + std::vector sceneFlagsArray; + j.at("sceneFlags").get_to(sceneFlagsArray); + for (int i = 0; i < 124; i++) { + saveContext.sceneFlags[i].chest = sceneFlagsArray[i * 4]; + saveContext.sceneFlags[i].swch = sceneFlagsArray[i * 4 + 1]; + saveContext.sceneFlags[i].clear = sceneFlagsArray[i * 4 + 2]; + saveContext.sceneFlags[i].collect = sceneFlagsArray[i * 4 + 3]; + } + j.at("eventChkInf").get_to(saveContext.eventChkInf); + j.at("itemGetInf").get_to(saveContext.itemGetInf); + j.at("infTable").get_to(saveContext.infTable); + j.at("gsFlags").get_to(saveContext.gsFlags); + j.at("inventory").get_to(saveContext.inventory); + j.at("ship").get_to(saveContext.ship); +} + +#endif // __cplusplus +#endif // NETWORK_ANCHOR_JSON_CONVERSIONS_H diff --git a/soh/soh/Network/Anchor/Menu.cpp b/soh/soh/Network/Anchor/Menu.cpp new file mode 100644 index 000000000..9c4530431 --- /dev/null +++ b/soh/soh/Network/Anchor/Menu.cpp @@ -0,0 +1,241 @@ +#include "Anchor.h" +#include +#include "soh/SohGui/SohGui.hpp" +#include "soh/SohGui/SohMenu.h" +#include "soh/util.h" + +namespace SohGui { +extern std::shared_ptr mSohMenu; +extern std::shared_ptr mAnchorRoomWindow; +} // namespace SohGui + +static const char* pvpModes[3] = { "Off", "On", "On + Friendly Fire" }; +static std::vector teleportModes = { "None", "Team Only", "All" }; +static std::vector showLocationsModes = { "None", "Team Only", "All" }; + +void AnchorMainMenu(WidgetInfo& info) { + auto anchor = Anchor::Instance; + + std::string host = CVarGetString(CVAR_REMOTE_ANCHOR("Host"), "anchor.hm64.org"); + uint16_t port = CVarGetInteger(CVAR_REMOTE_ANCHOR("Port"), 43383); + std::string anchorTeamId = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + std::string anchorRoomId = CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), ""); + std::string anchorName = CVarGetString(CVAR_REMOTE_ANCHOR("Name"), ""); + bool isFormValid = !SohUtils::IsStringEmpty(host) && port > 1024 && port < 65535 && + !SohUtils::IsStringEmpty(anchorRoomId) && !SohUtils::IsStringEmpty(anchorName); + + ImGui::SeparatorText("Connection Settings"); + + ImGui::BeginDisabled(anchor->isEnabled); + ImGui::Text("Host & Port"); + if (UIWidgets::InputString("##Host", &host, + UIWidgets::InputOptions() + .Size(ImGui::GetContentRegionAvail() - + ImVec2((ImGui::GetFontSize() * 5 + ImGui::GetStyle().ItemSpacing.x), 0)) + .Color(THEME_COLOR))) { + CVarSetString(CVAR_REMOTE_ANCHOR("Host"), host.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + + ImGui::SameLine(); + UIWidgets::PushStyleInput(THEME_COLOR); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 5); + if (ImGui::InputScalar("##Port", ImGuiDataType_U16, &port)) { + CVarSetInteger(CVAR_REMOTE_ANCHOR("Port"), port); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + UIWidgets::PopStyleInput(); + + ImGui::Text("Name"); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (UIWidgets::InputString("##Name", &anchorName, UIWidgets::InputOptions().Color(THEME_COLOR))) { + CVarSetString(CVAR_REMOTE_ANCHOR("Name"), anchorName.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + ImGui::Text("Room ID"); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (UIWidgets::InputString("##RoomId", &anchorRoomId, + UIWidgets::InputOptions().IsSecret(anchor->isEnabled).Color(THEME_COLOR))) { + CVarSetString(CVAR_REMOTE_ANCHOR("RoomId"), anchorRoomId.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + ImGui::Text("Team ID (Items & Flags Shared)"); + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + if (UIWidgets::InputString("##TeamId", &anchorTeamId, UIWidgets::InputOptions().Color(THEME_COLOR))) { + CVarSetString(CVAR_REMOTE_ANCHOR("TeamId"), anchorTeamId.c_str()); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + ImGui::Spacing(); + + if (UIWidgets::Button("Restore Defaults", UIWidgets::ButtonOptions() + .Size(ImVec2(ImGui::GetContentRegionAvail().x / 2, 0)) + .Color(UIWidgets::Colors::Red))) { + CVarSetString(CVAR_REMOTE_ANCHOR("Host"), "anchor.hm64.org"); + CVarSetInteger(CVAR_REMOTE_ANCHOR("Port"), 43383); + CVarSetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + CVarSetString(CVAR_REMOTE_ANCHOR("RoomId"), ""); + CVarSetString(CVAR_REMOTE_ANCHOR("Name"), ""); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + + ImGui::SameLine(); + + if (UIWidgets::Button("Global Room", UIWidgets::ButtonOptions() + .Color(UIWidgets::Colors::Blue) + .Tooltip("Always-online public room so you don't have to experience " + "Hyrule alone. PVP and syncing are disabled."))) { + CVarSetString(CVAR_REMOTE_ANCHOR("Host"), "anchor.hm64.org"); + CVarSetInteger(CVAR_REMOTE_ANCHOR("Port"), 43383); + CVarSetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + CVarSetString(CVAR_REMOTE_ANCHOR("RoomId"), "soh-global"); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + + ImGui::EndDisabled(); + + ImGui::Spacing(); + + ImGui::BeginDisabled(!isFormValid); + const char* buttonLabel = anchor->isEnabled ? "Disable" : "Enable"; + UIWidgets::PushStyleButton(anchor->isEnabled ? UIWidgets::ColorValues.at(UIWidgets::Colors::Red) + : UIWidgets::ColorValues.at(UIWidgets::Colors::Green)); + if (ImGui::Button(buttonLabel, ImVec2(-1.0f, 0.0f))) { + if (anchor->isEnabled) { + CVarClear(CVAR_REMOTE_ANCHOR("Enabled")); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + anchor->Disable(); + } else { + CVarSetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 1); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + anchor->Enable(); + } + } + UIWidgets::PopStyleButton(); + ImGui::EndDisabled(); + ImGui::Spacing(); + + if (!anchor->isEnabled) { + return; + } + + if (!anchor->isConnected) { + ImGui::Text("Connecting..."); + return; + } + + ImGui::SeparatorText("Current Room"); + ImGui::Text("%s Connected", ICON_FA_CHECK); + + UIWidgets::PushStyleButton(THEME_COLOR); + if (ImGui::Button("Request Team State")) { + anchor->SendPacket_RequestTeamState(); + } + UIWidgets::Tooltip("Try this if you are missing items or flags that your team members have collected"); + UIWidgets::PopStyleButton(); + + ImGui::SameLine(); + + UIWidgets::WindowButton("Toggle Anchor Room Window", CVAR_WINDOW("AnchorRoom"), SohGui::mAnchorRoomWindow); + if (!SohGui::mAnchorRoomWindow->IsVisible()) { + SohGui::mAnchorRoomWindow->DrawElement(); + } +} + +void AnchorAdminMenu(WidgetInfo& info) { + auto anchor = Anchor::Instance; + bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), "")); + + if (!anchor->isEnabled || !anchor->isConnected || anchor->roomState.ownerClientId != anchor->ownClientId || + isGlobalRoom) { + return; + } + + ImGui::SeparatorText("Room Settings (Admin Only)"); + + UIWidgets::PushStyleButton(THEME_COLOR); + if (ImGui::Button("Clear All Team State")) { + std::set teams; + for (auto& [clientId, client] : Anchor::Instance->clients) { + teams.insert(client.teamId); + } + for (auto& team : teams) { + anchor->SendPacket_ClearTeamState(team); + } + } + UIWidgets::PopStyleButton(); + + if (UIWidgets::CVarCombobox("PvP Mode:", CVAR_REMOTE_ANCHOR("RoomSettings.PvpMode"), pvpModes, + UIWidgets::ComboboxOptions() + .DefaultIndex(1) + .LabelPosition(UIWidgets::LabelPositions::Above) + .Color(THEME_COLOR))) { + anchor->SendPacket_UpdateRoomState(); + } + if (UIWidgets::CVarCombobox("Show Locations For:", CVAR_REMOTE_ANCHOR("RoomSettings.ShowLocationsMode"), + showLocationsModes, + UIWidgets::ComboboxOptions() + .DefaultIndex(1) + .LabelPosition(UIWidgets::LabelPositions::Above) + .Color(THEME_COLOR))) { + anchor->SendPacket_UpdateRoomState(); + } + if (UIWidgets::CVarCombobox("Allow Teleporting To:", CVAR_REMOTE_ANCHOR("RoomSettings.TeleportMode"), teleportModes, + UIWidgets::ComboboxOptions() + .DefaultIndex(1) + .LabelPosition(UIWidgets::LabelPositions::Above) + .Color(THEME_COLOR))) { + anchor->SendPacket_UpdateRoomState(); + } + if (UIWidgets::CVarCheckbox("Sync Items & Flags", CVAR_REMOTE_ANCHOR("RoomSettings.SyncItemsAndFlags"), + UIWidgets::CheckboxOptions().DefaultValue(true).Color(THEME_COLOR))) { + anchor->SendPacket_UpdateRoomState(); + } +} + +void AnchorInstructionsMenu(WidgetInfo& info) { + auto anchor = Anchor::Instance; + + ImGui::SeparatorText("Usage Instructions"); + + ImGui::TextWrapped("1. All players involved should start at the file select screen"); + + ImGui::TextWrapped("2. Come up with a unique Room ID (this is basically your password) and enter it, along with " + "your desired player name and team ID and click Enable"); + + ImGui::TextWrapped("3. The host should configure the randomizer settings and generate a seed, then share the newly " + "generated JSON spoiler file with other players."); + + ImGui::TextWrapped("4. All players should load the same JSON spoiler file (drag it into SoH window), make sure " + "seed icons match, then create a new file."); + + ImGui::TextWrapped("5. All players should now load into their game. IMPORTANT! If using an existing save/seed " + "ensure the player with the most progress loads the file first."); + + ImGui::TextWrapped("6. After everyone has loaded in, verify on the network tab that it doesn't warn about anyone " + "being on a wrong version or seed."); + + ImGui::Spacing(); + + ImGui::TextWrapped( + "Note: Team ID is used to group players together in the same team, sharing items and flags. Make sure all " + "players who want to share progress use the same Team ID. All players with the same Team ID should be using " + "the same randomizer seed, while players on different teams can use different seeds."); +} + +#ifdef ENABLE_REMOTE_CONTROL +void RegisterAnchorMenu() { + WidgetPath path = { "Network", "Anchor", SECTION_COLUMN_1 }; + SohGui::mSohMenu->AddWidget(path, "AnchorMainMenu", WIDGET_CUSTOM) + .CustomFunction(AnchorMainMenu) + .HideInSearch(true); + path.column = SECTION_COLUMN_2; + SohGui::mSohMenu->AddWidget(path, "AnchorAdminMenu", WIDGET_CUSTOM) + .CustomFunction(AnchorAdminMenu) + .HideInSearch(true); + SohGui::mSohMenu->AddWidget(path, "AnchorInstructionsMenu", WIDGET_CUSTOM) + .CustomFunction(AnchorInstructionsMenu) + .HideInSearch(true); +} + +static RegisterMenuInitFunc menuInitFunc(RegisterAnchorMenu); +#endif diff --git a/soh/soh/Network/Anchor/Packets/AllClientState.cpp b/soh/soh/Network/Anchor/Packets/AllClientState.cpp new file mode 100644 index 000000000..08d8d1a55 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/AllClientState.cpp @@ -0,0 +1,71 @@ +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include +#include "soh/OTRGlobals.h" +#include "soh/Notification/Notification.h" + +/** + * ALL_CLIENT_STATE + * + * Contains a list of all clients and their CLIENT_STATE currently connected to the server + * + * The server itself sends this packet to all clients when a client connects or disconnects + */ + +void Anchor::HandlePacket_AllClientState(nlohmann::json payload) { + std::vector newClients = payload["state"].get>(); + bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), "")); + + // add new clients + for (auto& client : newClients) { + if (client.self) { + ownClientId = client.clientId; + CVarSetInteger(CVAR_REMOTE_ANCHOR("LastClientId"), ownClientId); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + clients[client.clientId].self = true; + } else { + clients[client.clientId].self = false; + if (clients.contains(client.clientId)) { + if (clients[client.clientId].online != client.online && !isGlobalRoom) { + Notification::Emit({ + .prefix = client.name, + .message = client.online ? "Connected" : "Disconnected", + }); + } + } else if (client.online && !isGlobalRoom) { + Notification::Emit({ + .prefix = client.name, + .message = "Connected", + }); + } + } + + clients[client.clientId].clientId = client.clientId; + clients[client.clientId].name = client.name; + clients[client.clientId].color = client.color; + clients[client.clientId].clientVersion = client.clientVersion; + clients[client.clientId].teamId = client.teamId; + clients[client.clientId].online = client.online; + clients[client.clientId].seed = client.seed; + clients[client.clientId].isSaveLoaded = client.isSaveLoaded; + clients[client.clientId].isGameComplete = client.isGameComplete; + clients[client.clientId].sceneNum = client.sceneNum; + clients[client.clientId].entranceIndex = client.entranceIndex; + } + + // remove clients that are no longer in the list + std::vector clientsToRemove; + for (auto& [clientId, client] : clients) { + if (std::find_if(newClients.begin(), newClients.end(), + [clientId](AnchorClient& c) { return c.clientId == clientId; }) == newClients.end()) { + clientsToRemove.push_back(clientId); + } + } + // (seperate loop to avoid iterator invalidation) + for (auto& clientId : clientsToRemove) { + clients.erase(clientId); + } + + RefreshClientActors(); +} diff --git a/soh/soh/Network/Anchor/Packets/DamagePlayer.cpp b/soh/soh/Network/Anchor/Packets/DamagePlayer.cpp new file mode 100644 index 000000000..e97abea4f --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/DamagePlayer.cpp @@ -0,0 +1,65 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" + +extern "C" { +#include "macros.h" +#include "functions.h" +extern PlayState* gPlayState; +void func_80838280(Player* player); +} + +/** + * DAMAGE_PLAYER + */ + +void Anchor::SendPacket_DamagePlayer(u32 clientId, u8 damageEffect, u8 damage) { + if (!IsSaveLoaded()) { + return; + } + + nlohmann::json payload; + payload["type"] = DAMAGE_PLAYER; + payload["targetClientId"] = clientId; + payload["damageEffect"] = damageEffect; + payload["damage"] = damage; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_DamagePlayer(nlohmann::json payload) { + uint32_t clientId = payload["clientId"].get(); + if (!clients.contains(clientId) || clients[clientId].player == nullptr) { + return; + } + + AnchorClient& anchorClient = clients[clientId]; + Player* otherPlayer = anchorClient.player; + Player* self = GET_PLAYER(gPlayState); + + // Prevent incoming damage during cutscenes or item get sequences + if (Player_InBlockingCsMode(gPlayState, self) || self->stateFlags1 & PLAYER_STATE1_IN_ITEM_CS || + self->stateFlags1 & PLAYER_STATE1_GETTING_ITEM) { + return; + } + + u8 damageEffect = payload["damageEffect"].get(); + u8 damage = payload["damage"].get(); + + self->actor.colChkInfo.damage = damage * 8; // Arbitrary number currently, need to fine tune + + if (damageEffect == DUMMY_PLAYER_HIT_RESPONSE_FIRE) { + for (int i = 0; i < ARRAY_COUNT(self->bodyFlameTimers); i++) { + self->bodyFlameTimers[i] = Rand_S16Offset(0, 200); + } + self->bodyIsBurning = true; + } else if (damageEffect == DUMMY_PLAYER_HIT_RESPONSE_STUN) { + self->actor.freezeTimer = 20; + Actor_SetColorFilter(&self->actor, 0, 0xFF, 0, 24); + return; + } + + func_80837C0C(gPlayState, self, damageEffect, 4.0f, 5.0f, + Actor_WorldYawTowardActor(&otherPlayer->actor, &self->actor), 20); +} diff --git a/soh/soh/Network/Anchor/Packets/DisableAnchor.cpp b/soh/soh/Network/Anchor/Packets/DisableAnchor.cpp new file mode 100644 index 000000000..914601bf7 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/DisableAnchor.cpp @@ -0,0 +1,14 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" + +/** + * DISABLE_ANCHOR + * + * No current use, potentially will be used for a future feature. + */ + +void Anchor::HandlePacket_DisableAnchor(nlohmann::json payload) { + Disable(); +} diff --git a/soh/soh/Network/Anchor/Packets/EntranceDiscovered.cpp b/soh/soh/Network/Anchor/Packets/EntranceDiscovered.cpp new file mode 100644 index 000000000..6669e28a0 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/EntranceDiscovered.cpp @@ -0,0 +1,33 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Enhancements/randomizer/randomizer_entrance.h" +#include "soh/OTRGlobals.h" + +/** + * ENTRANCE_DISCOVERED + */ + +void Anchor::SendPacket_EntranceDiscovered(u16 entranceIndex) { + if (!IsSaveLoaded() || isProcessingIncomingPacket || !roomState.syncItemsAndFlags) { + return; + } + + nlohmann::json payload; + payload["type"] = ENTRANCE_DISCOVERED; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + payload["entranceIndex"] = entranceIndex; + payload["quiet"] = true; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_EntranceDiscovered(nlohmann::json payload) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + u16 entranceIndex = payload["entranceIndex"].get(); + Entrance_SetEntranceDiscovered(entranceIndex, 1); +} diff --git a/soh/soh/Network/Anchor/Packets/GameComplete.cpp b/soh/soh/Network/Anchor/Packets/GameComplete.cpp new file mode 100644 index 000000000..aa05a6e64 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/GameComplete.cpp @@ -0,0 +1,42 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Notification/Notification.h" +#include "soh/Enhancements/randomizer/3drando/random.hpp" + +const std::string gameCompleteMessages[] = { + "killed Ganon", "saved Zelda", "proved their Courage", + "collected the Triforce", "is the Hero of Time", "proved Mido wrong", +}; + +/** + * GAME_COMPLETE + */ + +void Anchor::SendPacket_GameComplete() { + if (!IsSaveLoaded()) { + return; + } + + nlohmann::json payload; + payload["type"] = GAME_COMPLETE; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_GameComplete(nlohmann::json payload) { + uint32_t clientId = payload["clientId"].get(); + if (!clients.contains(clientId)) { + return; + } + + AnchorClient& anchorClient = clients[clientId]; + anchorClient.isGameComplete = true; + bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), "")); + + Notification::Emit({ + .prefix = isGlobalRoom ? "Someone" : anchorClient.name, + .message = RandomElement(gameCompleteMessages), + }); +} diff --git a/soh/soh/Network/Anchor/Packets/GiveItem.cpp b/soh/soh/Network/Anchor/Packets/GiveItem.cpp new file mode 100644 index 000000000..fbf048d7d --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/GiveItem.cpp @@ -0,0 +1,108 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Notification/Notification.h" +#include "soh/Enhancements/randomizer/randomizer.h" +#include "soh/SohGui/ImGuiUtils.h" +#include "soh/Enhancements/item-tables/ItemTableManager.h" +#include "soh/OTRGlobals.h" + +extern "C" { +#include "functions.h" +extern PlayState* gPlayState; +} + +/** + * GIVE_ITEM + */ + +uint8_t incomingIceTrapsFromAnchor = 0; + +void Anchor::SendPacket_GiveItem(u16 modId, s16 getItemId) { + if (!IsSaveLoaded() || isProcessingIncomingPacket || !roomState.syncItemsAndFlags) { + return; + } + + if (modId == MOD_RANDOMIZER && getItemId == RG_ICE_TRAP && incomingIceTrapsFromAnchor > 0) { + incomingIceTrapsFromAnchor = MAX(incomingIceTrapsFromAnchor - 1, 0); + return; + } + + // Ignore sending master sword in final Ganon fight + if (modId == MOD_RANDOMIZER && getItemId == RG_MASTER_SWORD && gPlayState->sceneNum == SCENE_GANON_BOSS) { + return; + } + + nlohmann::json payload; + payload["type"] = GIVE_ITEM; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + payload["addToQueue"] = true; + payload["modId"] = modId; + payload["getItemId"] = getItemId; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_GiveItem(nlohmann::json payload) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + uint32_t clientId = payload["clientId"].get(); + AnchorClient& client = clients[clientId]; + u16 modId = payload["modId"].get(); + u16 getItemId = payload["getItemId"].get(); + + GetItemEntry getItemEntry; + if (modId == MOD_NONE) { + getItemEntry = ItemTableManager::Instance->RetrieveItemEntry(MOD_NONE, getItemId); + } else { + getItemEntry = Rando::StaticData::RetrieveItem(static_cast(getItemId)).GetGIEntry_Copy(); + } + + if (getItemEntry.modIndex == MOD_NONE) { + if (getItemEntry.getItemId == GI_SWORD_BGS) { + gSaveContext.bgsFlag = true; + } + Item_Give(gPlayState, getItemEntry.itemId); + } else if (getItemEntry.modIndex == MOD_RANDOMIZER) { + if (getItemEntry.getItemId == RG_ICE_TRAP) { + gSaveContext.ship.pendingIceTrapCount++; + incomingIceTrapsFromAnchor++; + } else { + Randomizer_Item_Give(gPlayState, getItemEntry); + } + } + + // Full heal if getting a heart container or piece + if (getItemEntry.gid == GID_HEART_CONTAINER || getItemEntry.gid == GID_HEART_PIECE) { + gSaveContext.healthAccumulator = 0x140; + } + + // Handle if the player gets a 4th heart piece (usually handled in z_message) + s32 heartPieces = (s32)(gSaveContext.inventory.questItems & 0xF0000000) >> (QUEST_HEART_PIECE + 4); + if (heartPieces >= 4) { + gSaveContext.inventory.questItems &= ~0xF0000000; + gSaveContext.inventory.questItems += (heartPieces % 4) << (QUEST_HEART_PIECE + 4); + gSaveContext.healthCapacity += 0x10 * (heartPieces / 4); + gSaveContext.health += 0x10 * (heartPieces / 4); + } + + if (getItemEntry.getItemCategory != ITEM_CATEGORY_JUNK) { + if (getItemEntry.modIndex == MOD_NONE) { + Notification::Emit({ + .itemIcon = GetTextureForItemId(getItemEntry.itemId), + .prefix = client.name, + .message = "found", + .suffix = SohUtils::GetItemName(getItemEntry.itemId), + }); + } else if (getItemEntry.modIndex == MOD_RANDOMIZER) { + Notification::Emit({ + .prefix = client.name, + .message = "found", + .suffix = Rando::StaticData::RetrieveItem((RandomizerGet)getItemEntry.getItemId).GetName().english, + }); + } + } +} diff --git a/soh/soh/Network/Anchor/Packets/Handshake.cpp b/soh/soh/Network/Anchor/Packets/Handshake.cpp new file mode 100644 index 000000000..b93e63278 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/Handshake.cpp @@ -0,0 +1,22 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/OTRGlobals.h" + +/** + * HANDSHAKE + * + * Sent by the client to the server when it first connects to the server, sends over both the local room settings + * in case the room needs to be created, along with the current client state + */ + +void Anchor::SendPacket_Handshake() { + nlohmann::json payload; + payload["type"] = HANDSHAKE; + payload["roomId"] = CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), ""); + payload["roomState"] = PrepRoomState(); + payload["clientState"] = PrepClientState(); + + SendJsonToRemote(payload); +} diff --git a/soh/soh/Network/Anchor/Packets/PlayerSfx.cpp b/soh/soh/Network/Anchor/Packets/PlayerSfx.cpp new file mode 100644 index 000000000..ce4755adf --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/PlayerSfx.cpp @@ -0,0 +1,47 @@ +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include + +extern "C" { +#include "macros.h" +#include "functions.h" +#include "variables.h" +extern PlayState* gPlayState; +} + +/** + * PLAYER_SFX + * + * Sound effects, only sent to other clients in the same scene as the player + */ + +void Anchor::SendPacket_PlayerSfx(u16 sfxId) { + if (!IsSaveLoaded()) { + return; + } + + nlohmann::json payload; + + payload["type"] = PLAYER_SFX; + payload["sfxId"] = sfxId; + payload["quiet"] = true; + + for (auto& [clientId, client] : clients) { + if (client.sceneNum == gPlayState->sceneNum && client.online && client.isSaveLoaded && !client.self) { + payload["targetClientId"] = clientId; + SendJsonToRemote(payload); + } + } +} + +void Anchor::HandlePacket_PlayerSfx(nlohmann::json payload) { + uint32_t clientId = payload["clientId"].get(); + u16 sfxId = payload["sfxId"].get(); + + if (!clients.contains(clientId) || !clients[clientId].player) { + return; + } + + Player_PlaySfx((Actor*)clients[clientId].player, sfxId); +} diff --git a/soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp b/soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp new file mode 100644 index 000000000..a4ee17b76 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/PlayerUpdate.cpp @@ -0,0 +1,111 @@ +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include + +extern "C" { +#include "macros.h" +#include "variables.h" +extern PlayState* gPlayState; +} + +/** + * PLAYER_UPDATE + * + * Contains real-time data necessary to update other clients in the same scene as the player + * + * Sent every frame to other clients within the same scene + * + * Note: This packet is sent _a lot_, so please do not include any unnecessary data in it + */ + +void Anchor::SendPacket_PlayerUpdate() { + if (!IsSaveLoaded()) { + return; + } + + uint32_t currentPlayerCount = 0; + for (auto& [clientId, client] : clients) { + if (client.sceneNum == gPlayState->sceneNum && client.online && client.isSaveLoaded && !client.self) { + currentPlayerCount++; + } + } + if (currentPlayerCount == 0) { + return; + } + + Player* player = GET_PLAYER(gPlayState); + nlohmann::json payload; + + payload["type"] = PLAYER_UPDATE; + payload["sceneNum"] = gPlayState->sceneNum; + payload["entranceIndex"] = gSaveContext.entranceIndex; + payload["linkAge"] = gSaveContext.linkAge; + payload["posRot"]["pos"] = player->actor.world.pos; + payload["posRot"]["rot"] = player->actor.shape.rot; + std::vector jointArray; + for (size_t i = 0; i < 24; i++) { + Vec3s joint = player->skelAnime.jointTable[i]; + jointArray.push_back(joint.x); + jointArray.push_back(joint.y); + jointArray.push_back(joint.z); + } + payload["jointTable"] = jointArray; + payload["upperLimbRot"] = player->upperLimbRot; + payload["currentBoots"] = player->currentBoots; + payload["currentShield"] = player->currentShield; + payload["currentTunic"] = player->currentTunic; + payload["stateFlags1"] = player->stateFlags1; + payload["stateFlags2"] = player->stateFlags2; + payload["buttonItem0"] = gSaveContext.equips.buttonItems[0]; + payload["itemAction"] = player->itemAction; + payload["heldItemAction"] = player->heldItemAction; + payload["modelGroup"] = player->modelGroup; + payload["invincibilityTimer"] = player->invincibilityTimer; + payload["unk_862"] = player->unk_862; + payload["actionVar1"] = player->av1.actionVar1; + payload["quiet"] = true; + + for (auto& [clientId, client] : clients) { + if (client.sceneNum == gPlayState->sceneNum && client.online && client.isSaveLoaded && !client.self) { + payload["targetClientId"] = clientId; + SendJsonToRemote(payload); + } + } +} + +void Anchor::HandlePacket_PlayerUpdate(nlohmann::json payload) { + uint32_t clientId = payload["clientId"].get(); + + if (clients.contains(clientId)) { + auto& client = clients[clientId]; + + if (client.linkAge != payload["linkAge"].get()) { + shouldRefreshActors = true; + } + + client.sceneNum = payload["sceneNum"].get(); + client.entranceIndex = payload["entranceIndex"].get(); + client.linkAge = payload["linkAge"].get(); + client.posRot = payload["posRot"].get(); + std::vector jointArray = payload["jointTable"]; + for (int i = 0; i < 24; i++) { + client.jointTable[i].x = jointArray[i * 3]; + client.jointTable[i].y = jointArray[i * 3 + 1]; + client.jointTable[i].z = jointArray[i * 3 + 2]; + } + client.upperLimbRot = payload["upperLimbRot"].get(); + client.currentBoots = payload["currentBoots"].get(); + client.currentShield = payload["currentShield"].get(); + client.currentTunic = payload["currentTunic"].get(); + client.stateFlags1 = payload["stateFlags1"].get(); + client.stateFlags2 = payload["stateFlags2"].get(); + client.buttonItem0 = payload["buttonItem0"].get(); + client.itemAction = payload["itemAction"].get(); + client.heldItemAction = payload["heldItemAction"].get(); + client.modelGroup = payload["modelGroup"].get(); + client.invincibilityTimer = payload["invincibilityTimer"].get(); + client.unk_862 = payload["unk_862"].get(); + client.actionVar1 = payload["actionVar1"].get(); + } +} diff --git a/soh/soh/Network/Anchor/Packets/RequestTeamState.cpp b/soh/soh/Network/Anchor/Packets/RequestTeamState.cpp new file mode 100644 index 000000000..dda852473 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/RequestTeamState.cpp @@ -0,0 +1,37 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/OTRGlobals.h" + +/** + * REQUEST_TEAM_STATE + * + * Requests team state from the server, which will pass on the request to any connected teammates, or send the last + * known state if no teammates are connected. + * + * This fires when loading into a file while Anchor is connected, or when Anchor is connected while a file is already + * loaded + * + * Note: This can additionally be fired with a button in the menus to fix any desyncs that may have occurred in the save + * state + */ + +void Anchor::SendPacket_RequestTeamState() { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + nlohmann::json payload; + payload["type"] = REQUEST_TEAM_STATE; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_RequestTeamState(nlohmann::json payload) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + SendPacket_UpdateTeamState(); +} diff --git a/soh/soh/Network/Anchor/Packets/RequestTeleport.cpp b/soh/soh/Network/Anchor/Packets/RequestTeleport.cpp new file mode 100644 index 000000000..8bcb1a306 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/RequestTeleport.cpp @@ -0,0 +1,91 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" + +/** + * REQUEST_TELEPORT + * + * Because we don't have all the necessary information to directly teleport to a player, we emit a request, + * in which they will respond with a TELEPORT_TO packet, with the necessary information. + */ + +void Anchor::SendPacket_RequestTeleport(uint32_t clientId) { + if (!CanTeleportTo(clientId)) { + return; + } + + nlohmann::json payload; + payload["type"] = REQUEST_TELEPORT; + payload["targetClientId"] = clientId; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_RequestTeleport(nlohmann::json payload) { + if (!IsSaveLoaded()) { + return; + } + + uint32_t clientId = payload["clientId"].get(); + SendPacket_TeleportTo(clientId); +} + +// Reusable function to check if teleporting to a client is allowed +bool Anchor::CanTeleportTo(uint32_t clientId) { + // Teleporting is disabled + if (roomState.teleportMode == 0) { + return false; + } + + // You're not loaded into a save + if (!IsSaveLoaded()) { + return false; + } + + // The client doesn't exist + if (clients.find(clientId) == clients.end()) { + return false; + } + + AnchorClient& client = clients[clientId]; + + // The client is yourself + if (client.self) { + return false; + } + + // The client isn't online or loaded into a save + if (!client.online || !client.isSaveLoaded) { + return false; + } + + // Teleporting to team only, but the client is not on your team + std::string ownTeamId = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + if (roomState.teleportMode == 1 && client.teamId != ownTeamId) { + return false; + } + + // Problematic scenes for teleporting + if (client.sceneNum == SCENE_ID_MAX || client.sceneNum == SCENE_GROTTOS || client.sceneNum == SCENE_MARKET_DAY || + client.sceneNum == SCENE_MARKET_NIGHT || client.sceneNum == SCENE_MARKET_RUINS || + client.sceneNum == SCENE_MARKET_ENTRANCE_DAY || client.sceneNum == SCENE_MARKET_ENTRANCE_NIGHT || + client.sceneNum == SCENE_MARKET_ENTRANCE_RUINS || client.sceneNum == SCENE_TEMPLE_OF_TIME_EXTERIOR_DAY || + client.sceneNum == SCENE_TEMPLE_OF_TIME_EXTERIOR_NIGHT || + client.sceneNum == SCENE_TEMPLE_OF_TIME_EXTERIOR_RUINS || client.sceneNum == SCENE_BACK_ALLEY_DAY || + client.sceneNum == SCENE_BACK_ALLEY_NIGHT) { + return false; + } + + // Child can't teleport to Ganon's Castle exterior + if (client.sceneNum == SCENE_OUTSIDE_GANONS_CASTLE && gSaveContext.linkAge == LINK_AGE_CHILD) { + return false; + } + + // Adult can't teleport to Hyrule Castle exterior + if (client.sceneNum == SCENE_HYRULE_CASTLE && gSaveContext.linkAge == LINK_AGE_ADULT) { + return false; + } + + return true; +} diff --git a/soh/soh/Network/Anchor/Packets/ServerMessage.cpp b/soh/soh/Network/Anchor/Packets/ServerMessage.cpp new file mode 100644 index 000000000..9c1fdc132 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/ServerMessage.cpp @@ -0,0 +1,17 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Notification/Notification.h" + +/** + * SERVER_MESSAGE + */ + +void Anchor::HandlePacket_ServerMessage(nlohmann::json payload) { + Notification::Emit({ + .prefix = "Server:", + .prefixColor = ImVec4(1.0f, 0.5f, 0.5f, 1.0f), + .message = payload["message"].get(), + }); +} diff --git a/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp b/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp new file mode 100644 index 000000000..74bc44699 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/SetCheckStatus.cpp @@ -0,0 +1,51 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/OTRGlobals.h" + +/** + * SET_CHECK_STATUS + * + * Fired when a check status is updated or skipped + */ + +void Anchor::SendPacket_SetCheckStatus(RandomizerCheck rc) { + if (!IsSaveLoaded() || isProcessingIncomingPacket || !roomState.syncItemsAndFlags) { + return; + } + + auto randoContext = Rando::Context::GetInstance(); + + nlohmann::json payload; + payload["type"] = SET_CHECK_STATUS; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + payload["addToQueue"] = true; + payload["rc"] = rc; + payload["status"] = randoContext->GetItemLocation(rc)->GetCheckStatus(); + payload["skipped"] = randoContext->GetItemLocation(rc)->GetIsSkipped(); + payload["quiet"] = true; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_SetCheckStatus(nlohmann::json payload) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + auto randoContext = Rando::Context::GetInstance(); + + RandomizerCheck rc = payload["rc"].get(); + RandomizerCheckStatus status = payload["status"].get(); + bool skipped = payload["skipped"].get(); + + if (randoContext->GetItemLocation(rc)->GetCheckStatus() != status) { + randoContext->GetItemLocation(rc)->SetCheckStatus(status); + } + if (randoContext->GetItemLocation(rc)->GetIsSkipped() != skipped) { + randoContext->GetItemLocation(rc)->SetIsSkipped(skipped); + } + CheckTracker::RecalculateAllAreaTotals(); + CheckTracker::RecalculateAvailableChecks(); +} diff --git a/soh/soh/Network/Anchor/Packets/SetFlag.cpp b/soh/soh/Network/Anchor/Packets/SetFlag.cpp new file mode 100644 index 000000000..3468bab01 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/SetFlag.cpp @@ -0,0 +1,73 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/OTRGlobals.h" + +extern "C" { +#include "functions.h" + +extern PlayState* gPlayState; +} + +/** + * SET_FLAG + * + * Fired when a flag is set in the save context + */ + +void Anchor::SendPacket_SetFlag(s16 sceneNum, s16 flagType, s16 flag) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + nlohmann::json payload; + payload["type"] = SET_FLAG; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + payload["addToQueue"] = true; + payload["sceneNum"] = sceneNum; + payload["flagType"] = flagType; + payload["flag"] = flag; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_SetFlag(nlohmann::json payload) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + s16 sceneNum = payload["sceneNum"].get(); + s16 flagType = payload["flagType"].get(); + s16 flag = payload["flag"].get(); + + if (sceneNum == SCENE_ID_MAX) { + auto effect = new GameInteractionEffect::SetFlag(); + effect->parameters[0] = flagType; + effect->parameters[1] = flag; + effect->Apply(); + + // Special case: If King Zora moved, and the player has Ruto's Letter, convert it to an empty bottle + if (flagType == FLAG_EVENT_CHECK_INF && flag == EVENTCHKINF_KING_ZORA_MOVED && + Inventory_HasSpecificBottle(ITEM_LETTER_RUTO)) { + Inventory_ReplaceItem(gPlayState, ITEM_LETTER_RUTO, ITEM_BOTTLE); + } + } else { + // Special case: Ignore water temple water level flags, stored at 0x1C, 0x1D, 0x1E. + if (sceneNum == SCENE_WATER_TEMPLE && flagType == FLAG_SCENE_SWITCH && + (flag == 0x1C || flag == 0x1D || flag == 0x1E)) { + return; + } + + // Special case: Ignore forest temple elevator flag, stored at 0x1B. + if (sceneNum == SCENE_FOREST_TEMPLE && flagType == FLAG_SCENE_SWITCH && flag == 0x1B) { + return; + } + + auto effect = new GameInteractionEffect::SetSceneFlag(); + effect->parameters[0] = sceneNum; + effect->parameters[1] = flagType; + effect->parameters[2] = flag; + effect->Apply(); + } +} diff --git a/soh/soh/Network/Anchor/Packets/TeleportTo.cpp b/soh/soh/Network/Anchor/Packets/TeleportTo.cpp new file mode 100644 index 000000000..1d50be449 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/TeleportTo.cpp @@ -0,0 +1,59 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" + +extern "C" { +#include "macros.h" +extern PlayState* gPlayState; +} + +/** + * TELEPORT_TO + * + * See REQUEST_TELEPORT for more information, this is the second part of the process. + */ + +void Anchor::SendPacket_TeleportTo(uint32_t clientId) { + if (!IsSaveLoaded()) { + return; + } + + Player* player = GET_PLAYER(gPlayState); + + nlohmann::json payload; + payload["type"] = TELEPORT_TO; + payload["targetClientId"] = clientId; + payload["entranceIndex"] = gSaveContext.entranceIndex; + payload["roomIndex"] = gPlayState->roomCtx.curRoom.num; + payload["posRot"] = player->actor.world; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_TeleportTo(nlohmann::json payload) { + if (!IsSaveLoaded()) { + return; + } + + s32 entranceIndex = payload["entranceIndex"].get(); + s8 roomIndex = payload["roomIndex"].get(); + PosRot posRot = payload["posRot"].get(); + + gPlayState->nextEntranceIndex = entranceIndex; + gPlayState->transitionTrigger = TRANS_TRIGGER_START; + gPlayState->transitionType = TRANS_TYPE_INSTANT; + gSaveContext.respawn[RESPAWN_MODE_DOWN].entranceIndex = entranceIndex; + gSaveContext.respawn[RESPAWN_MODE_DOWN].roomIndex = roomIndex; + gSaveContext.respawn[RESPAWN_MODE_DOWN].pos = posRot.pos; + gSaveContext.respawn[RESPAWN_MODE_DOWN].yaw = posRot.rot.y; + gSaveContext.respawn[RESPAWN_MODE_DOWN].playerParams = 0xDFF; + gSaveContext.nextTransitionType = TRANS_TYPE_FADE_BLACK_FAST; + gSaveContext.respawnFlag = 1; + static HOOK_ID hookId = 0; + hookId = REGISTER_VB_SHOULD(VB_INFLICT_VOID_DAMAGE, { + *should = false; + GameInteractor::Instance->UnregisterGameHookForID(hookId); + }); +} diff --git a/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp b/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp new file mode 100644 index 000000000..cf2fbf5d4 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UnsetFlag.cpp @@ -0,0 +1,109 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/OTRGlobals.h" + +extern "C" { +#include "functions.h" +#include "soh/Enhancements/randomizer/ShuffleTradeItems.h" +extern PlayState* gPlayState; +} + +/** + * UNSET_FLAG + * + * Fired when a flag is unset in the save context + */ + +void Anchor::SendPacket_UnsetFlag(s16 sceneNum, s16 flagType, s16 flag) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + nlohmann::json payload; + payload["type"] = UNSET_FLAG; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + payload["addToQueue"] = true; + payload["sceneNum"] = sceneNum; + payload["flagType"] = flagType; + payload["flag"] = flag; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UnsetFlag(nlohmann::json payload) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + s16 sceneNum = payload["sceneNum"].get(); + s16 flagType = payload["flagType"].get(); + s16 flag = payload["flag"].get(); + + if (sceneNum == SCENE_ID_MAX) { + auto effect = new GameInteractionEffect::UnsetFlag(); + effect->parameters[0] = flagType; + effect->parameters[1] = flag; + effect->Apply(); + + // Special case: If an adult trade item flag is unset, replace the item if the player has it equipped + if (flagType == FLAG_RANDOMIZER_INF && + (flag >= RAND_INF_ADULT_TRADES_HAS_POCKET_EGG && flag <= RAND_INF_ADULT_TRADES_HAS_CLAIM_CHECK)) { + u16 itemToReplace = ITEM_POCKET_EGG; + switch (flag) { + case RAND_INF_ADULT_TRADES_HAS_POCKET_EGG: + itemToReplace = ITEM_POCKET_EGG; + break; + case RAND_INF_ADULT_TRADES_HAS_POCKET_CUCCO: + itemToReplace = ITEM_POCKET_CUCCO; + break; + case RAND_INF_ADULT_TRADES_HAS_COJIRO: + itemToReplace = ITEM_COJIRO; + break; + case RAND_INF_ADULT_TRADES_HAS_ODD_MUSHROOM: + itemToReplace = ITEM_ODD_MUSHROOM; + break; + case RAND_INF_ADULT_TRADES_HAS_ODD_POTION: + itemToReplace = ITEM_ODD_POTION; + break; + case RAND_INF_ADULT_TRADES_HAS_SAW: + itemToReplace = ITEM_SAW; + break; + case RAND_INF_ADULT_TRADES_HAS_SWORD_BROKEN: + itemToReplace = ITEM_SWORD_BROKEN; + break; + case RAND_INF_ADULT_TRADES_HAS_PRESCRIPTION: + itemToReplace = ITEM_PRESCRIPTION; + break; + case RAND_INF_ADULT_TRADES_HAS_FROG: + itemToReplace = ITEM_FROG; + break; + case RAND_INF_ADULT_TRADES_HAS_EYEDROPS: + itemToReplace = ITEM_EYEDROPS; + break; + case RAND_INF_ADULT_TRADES_HAS_CLAIM_CHECK: + itemToReplace = ITEM_CLAIM_CHECK; + break; + } + Inventory_ReplaceItem(gPlayState, itemToReplace, Randomizer_GetNextAdultTradeItem()); + } + } else { + // Special case: Ignore water temple water level flags, stored at 0x1C, 0x1D, 0x1E. + if (sceneNum == SCENE_WATER_TEMPLE && flagType == FLAG_SCENE_SWITCH && + (flag == 0x1C || flag == 0x1D || flag == 0x1E)) { + return; + } + + // Special case: Ignore forest temple elevator flag, stored at 0x1B. + if (sceneNum == SCENE_FOREST_TEMPLE && flagType == FLAG_SCENE_SWITCH && flag == 0x1B) { + return; + } + + auto effect = new GameInteractionEffect::UnsetSceneFlag(); + effect->parameters[0] = sceneNum; + effect->parameters[1] = flagType; + effect->parameters[2] = flag; + effect->Apply(); + } +} diff --git a/soh/soh/Network/Anchor/Packets/UpdateBeansCount.cpp b/soh/soh/Network/Anchor/Packets/UpdateBeansCount.cpp new file mode 100644 index 000000000..976c7cc24 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UpdateBeansCount.cpp @@ -0,0 +1,39 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/OTRGlobals.h" + +extern "C" { +#include "macros.h" +} + +/** + * UPDATE_BEANS_COUNT + * + * Keeps the client's bean count in sync as they buy/use them + */ + +void Anchor::SendPacket_UpdateBeansCount() { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + nlohmann::json payload; + payload["type"] = UPDATE_BEANS_COUNT; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + payload["addToQueue"] = true; + payload["amount"] = AMMO(ITEM_BEAN); + payload["amountBought"] = BEANS_BOUGHT; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UpdateBeansCount(nlohmann::json payload) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + AMMO(ITEM_BEAN) = payload["amount"].get(); + BEANS_BOUGHT = payload["amountBought"].get(); +} diff --git a/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp b/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp new file mode 100644 index 000000000..7b3c180bb --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UpdateClientState.cpp @@ -0,0 +1,73 @@ +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include +#include "soh/OTRGlobals.h" + +extern "C" { +#include "variables.h" +extern PlayState* gPlayState; +} + +/** + * UPDATE_CLIENT_STATE + * + * Contains a small subset of data that is cached on the server and important for the client to know for various reasons + * + * Sent on various events, such as changing scenes, soft resetting, finishing the game, opening file select, etc. + * + * Note: This packet should be cross version compatible, so if you add anything here don't assume all clients will be + * providing it, consider doing a `contains` check before accessing any version specific data + */ + +nlohmann::json Anchor::PrepClientState() { + nlohmann::json payload; + payload["name"] = CVarGetString(CVAR_REMOTE_ANCHOR("Name"), ""); + payload["color"] = CVarGetColor24(CVAR_REMOTE_ANCHOR("Color"), { 100, 255, 100 }); + payload["clientVersion"] = clientVersion; + payload["teamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + payload["online"] = true; + + if (IsSaveLoaded()) { + payload["seed"] = IS_RANDO ? Rando::Context::GetInstance()->GetSeed() : 0; + payload["isSaveLoaded"] = true; + payload["isGameComplete"] = gSaveContext.ship.stats.gameComplete; + payload["sceneNum"] = gPlayState->sceneNum; + payload["entranceIndex"] = gSaveContext.entranceIndex; + } else { + payload["seed"] = 0; + payload["isSaveLoaded"] = false; + payload["isGameComplete"] = false; + payload["sceneNum"] = SCENE_ID_MAX; + payload["entranceIndex"] = 0x00; + } + + return payload; +} + +void Anchor::SendPacket_UpdateClientState() { + nlohmann::json payload; + payload["type"] = UPDATE_CLIENT_STATE; + payload["state"] = PrepClientState(); + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UpdateClientState(nlohmann::json payload) { + uint32_t clientId = payload["clientId"].get(); + + if (clients.contains(clientId)) { + AnchorClient client = payload["state"].get(); + clients[clientId].clientId = clientId; + clients[clientId].name = client.name; + clients[clientId].color = client.color; + clients[clientId].clientVersion = client.clientVersion; + clients[clientId].teamId = client.teamId; + clients[clientId].online = client.online; + clients[clientId].seed = client.seed; + clients[clientId].isSaveLoaded = client.isSaveLoaded; + clients[clientId].isGameComplete = client.isGameComplete; + clients[clientId].sceneNum = client.sceneNum; + clients[clientId].entranceIndex = client.entranceIndex; + } +} diff --git a/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp b/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp new file mode 100644 index 000000000..9e0a43200 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UpdateDungeonItems.cpp @@ -0,0 +1,38 @@ +#include "soh/Network/Anchor/Anchor.h" +#include +#include +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/OTRGlobals.h" + +/** + * UPDATE_DUNGEON_ITEMS + * + * This is for 2 things, first is updating the dungeon items in vanilla saves, and second is + * for ensuring the amount of keys used is synced as players are using them. + */ + +void Anchor::SendPacket_UpdateDungeonItems() { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + nlohmann::json payload; + payload["type"] = UPDATE_DUNGEON_ITEMS; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + payload["addToQueue"] = true; + payload["mapIndex"] = gSaveContext.mapIndex; + payload["dungeonItems"] = gSaveContext.inventory.dungeonItems[gSaveContext.mapIndex]; + payload["dungeonKeys"] = gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex]; + + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UpdateDungeonItems(nlohmann::json payload) { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + u16 mapIndex = payload["mapIndex"].get(); + gSaveContext.inventory.dungeonItems[mapIndex] = payload["dungeonItems"].get(); + gSaveContext.inventory.dungeonKeys[mapIndex] = payload["dungeonKeys"].get(); +} diff --git a/soh/soh/Network/Anchor/Packets/UpdateRoomState.cpp b/soh/soh/Network/Anchor/Packets/UpdateRoomState.cpp new file mode 100644 index 000000000..327385715 --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UpdateRoomState.cpp @@ -0,0 +1,55 @@ +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include +#include "soh/OTRGlobals.h" + +extern "C" { +#include "variables.h" +extern PlayState* gPlayState; +} + +/** + * UPDATE_ROOM_STATE + */ + +nlohmann::json Anchor::PrepRoomState() { + nlohmann::json payload; + payload["ownerClientId"] = ownClientId; + bool isGlobalRoom = (std::string("soh-global") == CVarGetString(CVAR_REMOTE_ANCHOR("RoomId"), "")); + + if (isGlobalRoom) { + // Global room uses hardcoded settings + payload["pvpMode"] = 0; + payload["showLocationsMode"] = 0; + payload["teleportMode"] = 0; + payload["syncItemsAndFlags"] = 0; + } else { + payload["pvpMode"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.PvpMode"), 1); + payload["showLocationsMode"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.ShowLocationsMode"), 1); + payload["teleportMode"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.TeleportMode"), 1); + payload["syncItemsAndFlags"] = CVarGetInteger(CVAR_REMOTE_ANCHOR("RoomSettings.SyncItemsAndFlags"), 1); + } + + return payload; +} + +void Anchor::SendPacket_UpdateRoomState() { + nlohmann::json payload; + payload["type"] = UPDATE_ROOM_STATE; + payload["state"] = PrepRoomState(); + + Network::SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UpdateRoomState(nlohmann::json payload) { + if (!payload.contains("state")) { + return; + } + + roomState.ownerClientId = payload["state"]["ownerClientId"].get(); + roomState.pvpMode = payload["state"]["pvpMode"].get(); + roomState.showLocationsMode = payload["state"]["showLocationsMode"].get(); + roomState.teleportMode = payload["state"]["teleportMode"].get(); + roomState.syncItemsAndFlags = payload["state"]["syncItemsAndFlags"].get(); +} diff --git a/soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp b/soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp new file mode 100644 index 000000000..9c7c3309a --- /dev/null +++ b/soh/soh/Network/Anchor/Packets/UpdateTeamState.cpp @@ -0,0 +1,300 @@ +#include "soh/Network/Anchor/Anchor.h" +#include "soh/Network/Anchor/JsonConversions.hpp" +#include +#include +#include "soh/Enhancements/randomizer/entrance.h" +#include "soh/Enhancements/randomizer/dungeon.h" +#include "soh/OTRGlobals.h" +#include "soh/Notification/Notification.h" + +extern "C" { +#include "variables.h" +extern PlayState* gPlayState; +} + +/** + * UPDATE_TEAM_STATE + * + * Pushes the current save state to the server for other teammates to use. + * + * Fires when the server passes on a REQUEST_TEAM_STATE packet, or when this client saves the game + * + * When sending this packet we will assume that the team queue has been emptied for this client, so the queue + * stored in the server will be cleared. + * + * When receiving this packet, if there is items in the team queue, we will play them back in order. + */ + +void Anchor::SendPacket_UpdateTeamState() { + if (!IsSaveLoaded() || !roomState.syncItemsAndFlags) { + return; + } + + json payload; + payload["type"] = UPDATE_TEAM_STATE; + payload["targetTeamId"] = CVarGetString(CVAR_REMOTE_ANCHOR("TeamId"), "default"); + + // Assume the team queue has been emptied, so clear it + payload["queue"] = json::array(); + + payload["state"] = gSaveContext; + // manually update current scene flags + payload["state"]["sceneFlags"][gPlayState->sceneNum * 4] = gPlayState->actorCtx.flags.chest; + payload["state"]["sceneFlags"][gPlayState->sceneNum * 4 + 1] = gPlayState->actorCtx.flags.swch; + payload["state"]["sceneFlags"][gPlayState->sceneNum * 4 + 2] = gPlayState->actorCtx.flags.clear; + payload["state"]["sceneFlags"][gPlayState->sceneNum * 4 + 3] = gPlayState->actorCtx.flags.collect; + + // The commented out code below is an attempt at sending the entire randomizer seed over, in hopes that a player + // doesn't have to generate the seed themselves Currently it doesn't work :) + if (IS_RANDO) { + auto randoContext = Rando::Context::GetInstance(); + + payload["state"]["rando"] = json::object(); + payload["state"]["rando"]["itemLocations"] = json::array(); + for (int i = 0; i < RC_MAX; i++) { + payload["state"]["rando"]["itemLocations"][i] = json::array(); + // payload["state"]["rando"]["itemLocations"][i]["rgID"] = + // randoContext->GetItemLocation(i)->GetPlacedRandomizerGet(); + payload["state"]["rando"]["itemLocations"][i][0] = randoContext->GetItemLocation(i)->GetCheckStatus(); + payload["state"]["rando"]["itemLocations"][i][1] = (u8)randoContext->GetItemLocation(i)->GetIsSkipped(); + + // if (randoContext->GetItemLocation(i)->GetPlacedRandomizerGet() == RG_ICE_TRAP) { + // payload["state"]["rando"]["itemLocations"][i]["fakeRgID"] = + // randoContext->GetItemOverride(i).LooksLike(); + // payload["state"]["rando"]["itemLocations"][i]["trickName"] = json::object(); + // payload["state"]["rando"]["itemLocations"][i]["trickName"]["english"] = + // randoContext->GetItemOverride(i).GetTrickName().GetEnglish(); + // payload["state"]["rando"]["itemLocations"][i]["trickName"]["french"] = + // randoContext->GetItemOverride(i).GetTrickName().GetFrench(); + // } + // if (randoContext->GetItemLocation(i)->HasCustomPrice()) { + // payload["state"]["rando"]["itemLocations"][i]["price"] = + // randoContext->GetItemLocation(i)->GetPrice(); + // } + } + + // auto entranceCtx = randoContext->GetEntranceShuffler(); + // for (int i = 0; i < ENTRANCE_OVERRIDES_MAX_COUNT; i++) { + // payload["state"]["rando"]["entrances"][i] = json::object(); + // payload["state"]["rando"]["entrances"][i]["type"] = entranceCtx->entranceOverrides[i].type; + // payload["state"]["rando"]["entrances"][i]["index"] = entranceCtx->entranceOverrides[i].index; + // payload["state"]["rando"]["entrances"][i]["destination"] = entranceCtx->entranceOverrides[i].destination; + // payload["state"]["rando"]["entrances"][i]["override"] = entranceCtx->entranceOverrides[i].override; + // payload["state"]["rando"]["entrances"][i]["overrideDestination"] = + // entranceCtx->entranceOverrides[i].overrideDestination; + // } + + // payload["state"]["rando"]["seed"] = json::array(); + // for (int i = 0; i < randoContext->hashIconIndexes.size(); i++) { + // payload["state"]["rando"]["seed"][i] = randoContext->hashIconIndexes[i]; + // } + // payload["state"]["rando"]["inputSeed"] = randoContext->GetSeedString(); + // payload["state"]["rando"]["finalSeed"] = randoContext->GetSeed(); + + // payload["state"]["rando"]["randoSettings"] = json::array(); + // for (int i = 0; i < RSK_MAX; i++) { + // payload["state"]["rando"]["randoSettings"][i] = + // randoContext->GetOption((RandomizerSettingKey(i))).GetSelectedOptionIndex(); + // } + + // payload["state"]["rando"]["masterQuestDungeonCount"] = randoContext->GetDungeons()->CountMQ(); + // payload["state"]["rando"]["masterQuestDungeons"] = json::array(); + // for (int i = 0; i < randoContext->GetDungeons()->GetDungeonListSize(); i++) { + // payload["state"]["rando"]["masterQuestDungeons"][i] = randoContext->GetDungeon(i)->IsMQ(); + // } + // for (int i = 0; i < randoContext->GetTrials()->GetTrialListSize(); i++) { + // payload["state"]["rando"]["requiredTrials"][i] = randoContext->GetTrial(i)->IsRequired(); + // } + } + + SendJsonToRemote(payload); +} + +void Anchor::SendPacket_ClearTeamState(std::string teamId) { + json payload; + payload["type"] = UPDATE_TEAM_STATE; + payload["targetTeamId"] = teamId; + payload["queue"] = json::array(); + payload["state"] = json::object(); + SendJsonToRemote(payload); +} + +void Anchor::HandlePacket_UpdateTeamState(nlohmann::json payload) { + if (!roomState.syncItemsAndFlags) { + return; + } + + isHandlingUpdateTeamState = true; + // This can happen in between file select and the game starting, so we cant use this check, but we need to ensure we + // be careful to wrap PlayState usage in this check + // if (!IsSaveLoaded()) { + // return; + // } + + if (payload.contains("state")) { + SaveContext loadedData = payload["state"].get(); + + gSaveContext.healthCapacity = loadedData.healthCapacity; + gSaveContext.magicLevel = loadedData.magicLevel; + gSaveContext.magicCapacity = gSaveContext.magic = loadedData.magicCapacity; + gSaveContext.isMagicAcquired = loadedData.isMagicAcquired; + gSaveContext.isDoubleMagicAcquired = loadedData.isDoubleMagicAcquired; + gSaveContext.isDoubleDefenseAcquired = loadedData.isDoubleDefenseAcquired; + gSaveContext.bgsFlag = loadedData.bgsFlag; + gSaveContext.swordHealth = loadedData.swordHealth; + gSaveContext.ship.quest = loadedData.ship.quest; + + for (int i = 0; i < 124; i++) { + if (i == SCENE_WATER_TEMPLE) { + // Keep water temple water level flags + u32 mask = (1 << 0x1C) | (1 << 0x1D) | (1 << 0x1E); + loadedData.sceneFlags[i].swch = + (loadedData.sceneFlags[i].swch & ~mask) | (gSaveContext.sceneFlags[i].swch & mask); + } + + if (i == SCENE_FOREST_TEMPLE) { + // Keep forest temple elevator flag + u32 mask = (1 << 0x1B); + loadedData.sceneFlags[i].swch = + (loadedData.sceneFlags[i].swch & ~mask) | (gSaveContext.sceneFlags[i].swch & mask); + } + + gSaveContext.sceneFlags[i] = loadedData.sceneFlags[i]; + if (IsSaveLoaded() && gPlayState->sceneNum == i) { + gPlayState->actorCtx.flags.chest = loadedData.sceneFlags[i].chest; + gPlayState->actorCtx.flags.swch = loadedData.sceneFlags[i].swch; + gPlayState->actorCtx.flags.clear = loadedData.sceneFlags[i].clear; + gPlayState->actorCtx.flags.collect = loadedData.sceneFlags[i].collect; + } + } + + for (int i = 0; i < 14; i++) { + gSaveContext.eventChkInf[i] = loadedData.eventChkInf[i]; + } + + for (int i = 0; i < 4; i++) { + gSaveContext.itemGetInf[i] = loadedData.itemGetInf[i]; + } + + // Skip last row of infTable, don't want to sync swordless flag + for (int i = 0; i < 29; i++) { + gSaveContext.infTable[i] = loadedData.infTable[i]; + } + + for (int i = 0; i < ceil((RAND_INF_MAX + 15) / 16); i++) { + gSaveContext.ship.randomizerInf[i] = loadedData.ship.randomizerInf[i]; + } + + for (int i = 0; i < 6; i++) { + gSaveContext.gsFlags[i] = loadedData.gsFlags[i]; + } + + gSaveContext.ship.stats.fileCreatedAt = loadedData.ship.stats.fileCreatedAt; + + // Restore master sword state + // Disabling this for now, not really sure I understand why I did this in the past + // u8 hasMasterSword = CHECK_OWNED_EQUIP(EQUIP_TYPE_SWORD, 1); + // if (hasMasterSword) { + // loadedData.inventory.equipment |= 0x2; + // } else { + // loadedData.inventory.equipment &= ~0x2; + // } + + // Restore bottle contents (unless it's ruto's letter) + for (int i = 0; i < 4; i++) { + if (gSaveContext.inventory.items[SLOT_BOTTLE_1 + i] != ITEM_NONE && + gSaveContext.inventory.items[SLOT_BOTTLE_1 + i] != ITEM_LETTER_RUTO) { + loadedData.inventory.items[SLOT_BOTTLE_1 + i] = gSaveContext.inventory.items[SLOT_BOTTLE_1 + i]; + } + } + + // Restore ammo if it's non-zero, unless it's beans + for (int i = 0; i < ARRAY_COUNT(gSaveContext.inventory.ammo); i++) { + if (gSaveContext.inventory.ammo[i] != 0 && i != SLOT(ITEM_BEAN) && i != SLOT(ITEM_BEAN + 1)) { + loadedData.inventory.ammo[i] = gSaveContext.inventory.ammo[i]; + } + } + + gSaveContext.inventory = loadedData.inventory; + + // The commented out code below is an attempt at sending the entire randomizer seed over, in hopes that a player + // doesn't have to generate the seed themselves Currently it doesn't work :) + if (IS_RANDO && payload["state"].contains("rando")) { + auto randoContext = Rando::Context::GetInstance(); + + for (int i = 0; i < RC_MAX; i++) { + // randoContext->GetItemLocation(i)->RefPlacedItem() = + // payload["state"]["rando"]["itemLocations"][i]["rgID"].get(); + OTRGlobals::Instance->gRandoContext->GetItemLocation(i)->SetCheckStatus( + payload["state"]["rando"]["itemLocations"][i][0].get()); + OTRGlobals::Instance->gRandoContext->GetItemLocation(i)->SetIsSkipped( + payload["state"]["rando"]["itemLocations"][i][0].get()); + + // if (payload["state"]["rando"]["itemLocations"][i].contains("fakeRgID")) { + // randoContext->overrides.emplace(static_cast(i), + // Rando::ItemOverride(static_cast(i), + // payload["state"]["rando"]["itemLocations"][i]["fakeRgID"].get())); + // randoContext->GetItemOverride(i).GetTrickName().english = + // payload["state"]["rando"]["itemLocations"][i]["trickName"]["english"].get(); + // randoContext->GetItemOverride(i).GetTrickName().french = + // payload["state"]["rando"]["itemLocations"][i]["trickName"]["french"].get(); + // } + // if (payload["state"]["rando"]["itemLocations"][i].contains("price")) { + // u16 price = payload["state"]["rando"]["itemLocations"][i]["price"].get(); + // if (price > 0) { + // randoContext->GetItemLocation(i)->SetCustomPrice(price); + // } + // } + } + + // auto entranceCtx = randoContext->GetEntranceShuffler(); + // for (int i = 0; i < ENTRANCE_OVERRIDES_MAX_COUNT; i++) { + // entranceCtx->entranceOverrides[i].type = + // payload["state"]["rando"]["entrances"][i]["type"].get(); entranceCtx->entranceOverrides[i].index + // = payload["state"]["rando"]["entrances"][i]["index"].get(); + // entranceCtx->entranceOverrides[i].destination = + // payload["state"]["rando"]["entrances"][i]["destination"].get(); + // entranceCtx->entranceOverrides[i].override = + // payload["state"]["rando"]["entrances"][i]["override"].get(); + // entranceCtx->entranceOverrides[i].overrideDestination = + // payload["state"]["rando"]["entrances"][i]["overrideDestination"].get(); + // } + + // for (int i = 0; i < randoContext->hashIconIndexes.size(); i++) { + // randoContext->hashIconIndexes[i] = payload["state"]["rando"]["seed"][i].get(); + // } + // randoContext->GetSettings()->SetSeedString(payload["state"]["rando"]["inputSeed"].get()); + // randoContext->GetSettings()->SetSeed(payload["state"]["rando"]["finalSeed"].get()); + + // for (int i = 0; i < RSK_MAX; i++) { + // randoContext->GetOption(RandomizerSettingKey(i)).SetSelectedIndex(payload["state"]["rando"]["randoSettings"][i].get()); + // } + + // randoContext->GetDungeons()->ClearAllMQ(); + // for (int i = 0; i < randoContext->GetDungeons()->GetDungeonListSize(); i++) { + // if (payload["state"]["rando"]["masterQuestDungeons"][i].get()) { + // randoContext->GetDungeon(i)->SetMQ(); + // } + // } + + // randoContext->GetTrials()->SkipAll(); + // for (int i = 0; i < randoContext->GetTrials()->GetTrialListSize(); i++) { + // if (payload["state"]["rando"]["requiredTrials"][i].get()) { + // randoContext->GetTrial(i)->SetAsRequired(); + // } + // } + } + + Notification::Emit({ + .message = "Save updated from team", + }); + } + + if (payload.contains("queue")) { + for (auto& item : payload["queue"]) { + nlohmann::json itemPayload = nlohmann::json::parse(item.get()); + incomingPacketQueue.push(itemPayload); + } + } + isHandlingUpdateTeamState = false; +} diff --git a/soh/soh/Network/CrowdControl/CrowdControl.cpp b/soh/soh/Network/CrowdControl/CrowdControl.cpp index fd293d8ba..9709fc452 100644 --- a/soh/soh/Network/CrowdControl/CrowdControl.cpp +++ b/soh/soh/Network/CrowdControl/CrowdControl.cpp @@ -1,5 +1,3 @@ -#ifdef ENABLE_REMOTE_CONTROL - #include "CrowdControl.h" #include "CrowdControlTypes.h" #include @@ -629,4 +627,3 @@ CrowdControl::Effect* CrowdControl::ParseMessage(nlohmann::json dataReceived) { return effect; } -#endif diff --git a/soh/soh/Network/CrowdControl/CrowdControl.h b/soh/soh/Network/CrowdControl/CrowdControl.h index f906961c3..5526a254c 100644 --- a/soh/soh/Network/CrowdControl/CrowdControl.h +++ b/soh/soh/Network/CrowdControl/CrowdControl.h @@ -1,4 +1,3 @@ -#ifdef ENABLE_REMOTE_CONTROL #ifndef NETWORK_CROWD_CONTROL_H #define NETWORK_CROWD_CONTROL_H #ifdef __cplusplus @@ -87,4 +86,3 @@ class CrowdControl : public Network { #endif // __cplusplus #endif // NETWORK_CROWD_CONTROL_H -#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Network.cpp b/soh/soh/Network/Network.cpp index c0bd87e6e..d9edb5550 100644 --- a/soh/soh/Network/Network.cpp +++ b/soh/soh/Network/Network.cpp @@ -1,5 +1,3 @@ -#ifdef ENABLE_REMOTE_CONTROL - #include "Network.h" #include #include @@ -7,6 +5,7 @@ // MARK: - Public void Network::Enable(const char* host, uint16_t port) { +#ifdef ENABLE_REMOTE_CONTROL if (isEnabled) { return; } @@ -23,6 +22,7 @@ void Network::Enable(const char* host, uint16_t port) { } receiveThread = std::thread(&Network::ReceiveFromServer, this); +#endif } void Network::Disable() { @@ -47,8 +47,10 @@ void Network::OnDisconnected() { } void Network::SendDataToRemote(const char* payload) { +#ifdef ENABLE_REMOTE_CONTROL SPDLOG_DEBUG("[Network] Sending data: {}", payload); SDLNet_TCP_Send(networkSocket, payload, strlen(payload) + 1); +#endif } void Network::SendJsonToRemote(nlohmann::json payload) { @@ -58,6 +60,7 @@ void Network::SendJsonToRemote(nlohmann::json payload) { // MARK: - Private void Network::ReceiveFromServer() { +#ifdef ENABLE_REMOTE_CONTROL while (isEnabled) { while (!isConnected && isEnabled) { SPDLOG_TRACE("[Network] Attempting to make connection to server..."); @@ -123,6 +126,7 @@ void Network::ReceiveFromServer() { SPDLOG_INFO("[Network] Ending receiving thread..."); } } +#endif } void Network::HandleRemoteData(char payload[512]) { @@ -141,5 +145,3 @@ void Network::HandleRemoteJson(std::string payload) { OnIncomingJson(jsonPayload); } - -#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Network.h b/soh/soh/Network/Network.h index 6bd4f8164..a04ece71e 100644 --- a/soh/soh/Network/Network.h +++ b/soh/soh/Network/Network.h @@ -1,16 +1,19 @@ -#ifdef ENABLE_REMOTE_CONTROL #ifndef NETWORK_H #define NETWORK_H #ifdef __cplusplus #include +#ifdef ENABLE_REMOTE_CONTROL #include +#endif #include class Network { private: +#ifdef ENABLE_REMOTE_CONTROL IPaddress networkAddress; TCPsocket networkSocket; +#endif std::thread receiveThread; std::string receivedData; @@ -47,4 +50,3 @@ class Network { #endif // __cplusplus #endif // NETWORK_H -#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Sail/Sail.cpp b/soh/soh/Network/Sail/Sail.cpp index 08f23fca5..ddaf059c5 100644 --- a/soh/soh/Network/Sail/Sail.cpp +++ b/soh/soh/Network/Sail/Sail.cpp @@ -1,5 +1,3 @@ -#ifdef ENABLE_REMOTE_CONTROL - #include "Sail.h" #include #include @@ -336,57 +334,20 @@ GameInteractionEffectBase* Sail::EffectFromJson(nlohmann::json payload) { } void Sail::RegisterHooks() { - static HOOK_ID onTransitionEndHook = 0; - static HOOK_ID onLoadGameHook = 0; - static HOOK_ID onExitGameHook = 0; - static HOOK_ID onItemReceiveHook = 0; - static HOOK_ID onEnemyDefeatHook = 0; - static HOOK_ID onActorInitHook = 0; - static HOOK_ID onFlagSetHook = 0; - static HOOK_ID onFlagUnsetHook = 0; - static HOOK_ID onSceneFlagSetHook = 0; - static HOOK_ID onSceneFlagUnsetHook = 0; + COND_HOOK(OnTransitionEnd, isConnected, [&](int32_t sceneNum) { + if (!isConnected || !GameInteractor::IsSaveLoaded()) + return; - GameInteractor::Instance->UnregisterGameHook(onTransitionEndHook); - GameInteractor::Instance->UnregisterGameHook(onLoadGameHook); - GameInteractor::Instance->UnregisterGameHook(onExitGameHook); - GameInteractor::Instance->UnregisterGameHook(onItemReceiveHook); - GameInteractor::Instance->UnregisterGameHook(onEnemyDefeatHook); - GameInteractor::Instance->UnregisterGameHook(onActorInitHook); - GameInteractor::Instance->UnregisterGameHook(onFlagSetHook); - GameInteractor::Instance->UnregisterGameHook(onFlagUnsetHook); - GameInteractor::Instance->UnregisterGameHook(onSceneFlagSetHook); - GameInteractor::Instance->UnregisterGameHook(onSceneFlagUnsetHook); + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnTransitionEnd"; + payload["hook"]["sceneNum"] = sceneNum; - onTransitionEndHook = 0; - onLoadGameHook = 0; - onExitGameHook = 0; - onItemReceiveHook = 0; - onEnemyDefeatHook = 0; - onActorInitHook = 0; - onFlagSetHook = 0; - onFlagUnsetHook = 0; - onSceneFlagSetHook = 0; - onSceneFlagUnsetHook = 0; + SendJsonToRemote(payload); + }); - if (!isConnected) { - return; - } - - onTransitionEndHook = - GameInteractor::Instance->RegisterGameHook([&](int32_t sceneNum) { - if (!isConnected || !GameInteractor::IsSaveLoaded()) - return; - - nlohmann::json payload; - payload["id"] = std::rand(); - payload["type"] = "hook"; - payload["hook"]["type"] = "OnTransitionEnd"; - payload["hook"]["sceneNum"] = sceneNum; - - SendJsonToRemote(payload); - }); - onLoadGameHook = GameInteractor::Instance->RegisterGameHook([&](int32_t fileNum) { + COND_HOOK(OnLoadGame, isConnected, [&](int32_t fileNum) { if (!isConnected || !GameInteractor::IsSaveLoaded()) return; @@ -398,7 +359,8 @@ void Sail::RegisterHooks() { SendJsonToRemote(payload); }); - onExitGameHook = GameInteractor::Instance->RegisterGameHook([&](int32_t fileNum) { + + COND_HOOK(OnExitGame, isConnected, [&](int32_t fileNum) { if (!isConnected || !GameInteractor::IsSaveLoaded()) return; @@ -410,21 +372,21 @@ void Sail::RegisterHooks() { SendJsonToRemote(payload); }); - onItemReceiveHook = - GameInteractor::Instance->RegisterGameHook([&](GetItemEntry itemEntry) { - if (!isConnected || !GameInteractor::IsSaveLoaded()) - return; - nlohmann::json payload; - payload["id"] = std::rand(); - payload["type"] = "hook"; - payload["hook"]["type"] = "OnItemReceive"; - payload["hook"]["tableId"] = itemEntry.tableId; - payload["hook"]["getItemId"] = itemEntry.getItemId; + COND_HOOK(OnItemReceive, isConnected, [&](GetItemEntry itemEntry) { + if (!isConnected || !GameInteractor::IsSaveLoaded()) + return; + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnItemReceive"; + payload["hook"]["tableId"] = itemEntry.tableId; + payload["hook"]["getItemId"] = itemEntry.getItemId; - SendJsonToRemote(payload); - }); - onEnemyDefeatHook = GameInteractor::Instance->RegisterGameHook([&](void* refActor) { + SendJsonToRemote(payload); + }); + + COND_HOOK(OnEnemyDefeat, isConnected, [&](void* refActor) { if (!isConnected || !GameInteractor::IsSaveLoaded()) return; @@ -438,7 +400,8 @@ void Sail::RegisterHooks() { SendJsonToRemote(payload); }); - onActorInitHook = GameInteractor::Instance->RegisterGameHook([&](void* refActor) { + + COND_HOOK(OnActorInit, isConnected, [&](void* refActor) { if (!isConnected || !GameInteractor::IsSaveLoaded()) return; @@ -452,64 +415,58 @@ void Sail::RegisterHooks() { SendJsonToRemote(payload); }); - onFlagSetHook = - GameInteractor::Instance->RegisterGameHook([&](int16_t flagType, int16_t flag) { - if (!isConnected || !GameInteractor::IsSaveLoaded()) - return; - nlohmann::json payload; - payload["id"] = std::rand(); - payload["type"] = "hook"; - payload["hook"]["type"] = "OnFlagSet"; - payload["hook"]["flagType"] = flagType; - payload["hook"]["flag"] = flag; + COND_HOOK(OnFlagSet, isConnected, [&](int16_t flagType, int16_t flag) { + if (!isConnected || !GameInteractor::IsSaveLoaded()) + return; + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnFlagSet"; + payload["hook"]["flagType"] = flagType; + payload["hook"]["flag"] = flag; - SendJsonToRemote(payload); - }); - onFlagUnsetHook = - GameInteractor::Instance->RegisterGameHook([&](int16_t flagType, int16_t flag) { - if (!isConnected || !GameInteractor::IsSaveLoaded()) - return; + SendJsonToRemote(payload); + }); - nlohmann::json payload; - payload["id"] = std::rand(); - payload["type"] = "hook"; - payload["hook"]["type"] = "OnFlagUnset"; - payload["hook"]["flagType"] = flagType; - payload["hook"]["flag"] = flag; + COND_HOOK(OnFlagUnset, isConnected, [&](int16_t flagType, int16_t flag) { + if (!isConnected || !GameInteractor::IsSaveLoaded()) + return; + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnFlagUnset"; + payload["hook"]["flagType"] = flagType; + payload["hook"]["flag"] = flag; - SendJsonToRemote(payload); - }); - onSceneFlagSetHook = GameInteractor::Instance->RegisterGameHook( - [&](int16_t sceneNum, int16_t flagType, int16_t flag) { - if (!isConnected || !GameInteractor::IsSaveLoaded()) - return; + SendJsonToRemote(payload); + }); - nlohmann::json payload; - payload["id"] = std::rand(); - payload["type"] = "hook"; - payload["hook"]["type"] = "OnSceneFlagSet"; - payload["hook"]["flagType"] = flagType; - payload["hook"]["flag"] = flag; - payload["hook"]["sceneNum"] = sceneNum; + COND_HOOK(OnSceneFlagSet, isConnected, [&](int16_t sceneNum, int16_t flagType, int16_t flag) { + if (!isConnected || !GameInteractor::IsSaveLoaded()) + return; + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnSceneFlagSet"; + payload["hook"]["flagType"] = flagType; + payload["hook"]["flag"] = flag; + payload["hook"]["sceneNum"] = sceneNum; - SendJsonToRemote(payload); - }); - onSceneFlagUnsetHook = GameInteractor::Instance->RegisterGameHook( - [&](int16_t sceneNum, int16_t flagType, int16_t flag) { - if (!isConnected || !GameInteractor::IsSaveLoaded()) - return; + SendJsonToRemote(payload); + }); - nlohmann::json payload; - payload["id"] = std::rand(); - payload["type"] = "hook"; - payload["hook"]["type"] = "OnSceneFlagUnset"; - payload["hook"]["flagType"] = flagType; - payload["hook"]["flag"] = flag; - payload["hook"]["sceneNum"] = sceneNum; + COND_HOOK(OnSceneFlagUnset, isConnected, [&](int16_t sceneNum, int16_t flagType, int16_t flag) { + if (!isConnected || !GameInteractor::IsSaveLoaded()) + return; + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnSceneFlagUnset"; + payload["hook"]["flagType"] = flagType; + payload["hook"]["flag"] = flag; + payload["hook"]["sceneNum"] = sceneNum; - SendJsonToRemote(payload); - }); + SendJsonToRemote(payload); + }); } - -#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/Network/Sail/Sail.h b/soh/soh/Network/Sail/Sail.h index 2a41b8723..fa2f0ff58 100644 --- a/soh/soh/Network/Sail/Sail.h +++ b/soh/soh/Network/Sail/Sail.h @@ -1,4 +1,3 @@ -#ifdef ENABLE_REMOTE_CONTROL #ifndef NETWORK_SAIL_H #define NETWORK_SAIL_H #ifdef __cplusplus @@ -22,4 +21,3 @@ class Sail : public Network { #endif // __cplusplus #endif // NETWORK_SAIL_H -#endif // ENABLE_REMOTE_CONTROL diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 9b69bfbac..7598cd637 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include "Enhancements/gameconsole.h" #ifdef _WIN32 @@ -75,14 +76,9 @@ #include "soh/SohGui/ImGuiUtils.h" #include "ActorDB.h" #include "SaveManager.h" - -#ifdef ENABLE_REMOTE_CONTROL #include "soh/Network/CrowdControl/CrowdControl.h" #include "soh/Network/Sail/Sail.h" -CrowdControl* CrowdControl::Instance; -Sail* Sail::Instance; -#endif - +#include "soh/Network/Anchor/Anchor.h" #include "Enhancements/mods.h" #include "Enhancements/game-interactor/GameInteractor.h" #include "Enhancements/randomizer/draw.h" @@ -146,6 +142,9 @@ ItemTableManager* ItemTableManager::Instance; GameInteractor* GameInteractor::Instance; AudioCollection* AudioCollection::Instance; SpeechSynthesizer* SpeechSynthesizer::Instance; +CrowdControl* CrowdControl::Instance; +Sail* Sail::Instance; +Anchor* Anchor::Instance; extern "C" char** cameraStrings; std::vector> cameraStdStrings; @@ -292,10 +291,19 @@ void OTRGlobals::Initialize() { OOT_NTSC_JP_GC, OOT_NTSC_US_GC, OOT_PAL_GC, OOT_PAL_GC_DBG1, OOT_PAL_GC_DBG2, }; - context->InitLogging(); - context->InitGfxDebugger(); +#if (_DEBUG) + auto defaultLogLevel = spdlog::level::trace; +#else + auto defaultLogLevel = spdlog::level::info; +#endif context->InitConfiguration(); context->InitConsoleVariables(); + auto logLevel = + static_cast(CVarGetInteger(CVAR_DEVELOPER_TOOLS("LogLevel"), defaultLogLevel)); + context->InitLogging(logLevel, logLevel); + Ship::Context::GetInstance()->GetLogger()->set_pattern("[%H:%M:%S.%e] [%s:%#] [%l] %v"); + + context->InitGfxDebugger(); context->InitFileDropMgr(); // tell LUS to reserve 3 SoH specific threads (Game, Audio, Save) @@ -320,10 +328,6 @@ void OTRGlobals::Initialize() { context->InitCrashHandler(); context->InitConsole(); - Ship::Context::GetInstance()->GetLogger()->set_level( - (spdlog::level::level_enum)CVarGetInteger(CVAR_DEVELOPER_TOOLS("LogLevel"), 1)); - Ship::Context::GetInstance()->GetLogger()->set_pattern("[%H:%M:%S.%e] [%s:%#] [%l] %v"); - auto sohInputEditorWindow = std::make_shared(CVAR_WINDOW("ControllerConfiguration"), "Configure Controller"); auto sohFast3dWindow = @@ -1286,10 +1290,9 @@ extern "C" void InitOTR(int argc, char* argv[]) { #endif SpeechSynthesizer::Instance->Init(); -#ifdef ENABLE_REMOTE_CONTROL CrowdControl::Instance = new CrowdControl(); Sail::Instance = new Sail(); -#endif + Anchor::Instance = new Anchor(); OTRMessage_Init(); OTRAudio_Init(); @@ -1319,13 +1322,16 @@ extern "C" void InitOTR(int argc, char* argv[]) { srand(now); #ifdef ENABLE_REMOTE_CONTROL SDLNet_Init(); +#endif if (CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0)) { CrowdControl::Instance->Enable(); } if (CVarGetInteger(CVAR_REMOTE_SAIL("Enabled"), 0)) { Sail::Instance->Enable(); } -#endif + if (CVarGetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 0)) { + Anchor::Instance->Enable(); + } } extern "C" void SaveManager_ThreadPoolWait() { @@ -1335,13 +1341,16 @@ extern "C" void SaveManager_ThreadPoolWait() { extern "C" void DeinitOTR() { SaveManager_ThreadPoolWait(); OTRAudio_Exit(); -#ifdef ENABLE_REMOTE_CONTROL if (CVarGetInteger(CVAR_REMOTE_CROWD_CONTROL("Enabled"), 0)) { CrowdControl::Instance->Disable(); } if (CVarGetInteger(CVAR_REMOTE_SAIL("Enabled"), 0)) { Sail::Instance->Disable(); } + if (CVarGetInteger(CVAR_REMOTE_ANCHOR("Enabled"), 0)) { + Anchor::Instance->Disable(); + } +#ifdef ENABLE_REMOTE_CONTROL SDLNet_Quit(); #endif diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp index 4257e9cb4..bf8fa46ed 100644 --- a/soh/soh/SaveManager.cpp +++ b/soh/soh/SaveManager.cpp @@ -161,6 +161,10 @@ SaveManager::SaveManager() { } void SaveManager::LoadRandomizer() { + if (gSaveContext.ship.quest.id != QUEST_RANDOMIZER) { + return; + } + auto randoContext = Rando::Context::GetInstance(); SaveManager::Instance->LoadArray("itemLocations", RC_MAX, [&](size_t i) { SaveManager::Instance->LoadStruct("", [&]() { @@ -260,9 +264,10 @@ void SaveManager::LoadRandomizer() { } void SaveManager::SaveRandomizer(SaveContext* saveContext, int sectionID, bool fullSave) { - - if (saveContext->ship.quest.id != QUEST_RANDOMIZER) + if (saveContext->ship.quest.id != QUEST_RANDOMIZER) { return; + } + auto randoContext = Rando::Context::GetInstance(); SaveManager::Instance->SaveArray("itemLocations", RC_MAX, [&](size_t i) { @@ -460,11 +465,122 @@ void SaveManager::Init() { // Load files to initialize metadata for (int fileNum = 0; fileNum < MaxFiles; fileNum++) { if (std::filesystem::exists(GetFileName(fileNum))) { - LoadFile(fileNum); - saveBlock = nlohmann::json::object(); - OTRGlobals::Instance->gRandoContext->ClearItemLocations(); + StartupCheckAndInitMeta(fileNum); } } + saveBlock = nlohmann::json::object(); +} + +void SaveManager::StartupCheckAndInitMeta(int fileNum) { + saveMtx.lock(); + SPDLOG_INFO("Init Meta - fileNum: {}", fileNum); + std::filesystem::path fileName = GetFileName(fileNum); + + std::ifstream input(fileName); + + bool deleteRando = false; + nlohmann::json metaSaveBlock = nlohmann::json::object(); + input >> metaSaveBlock; + input.close(); + saveMtx.unlock(); + if (!metaSaveBlock.contains("version")) { + SPDLOG_ERROR("Save at " + fileName.string() + " contains no version"); + assert(false); + return; + } + if (metaSaveBlock["sections"].contains("randomizer")) { + if (!metaSaveBlock.contains("fileType") || metaSaveBlock["fileType"] == FILE_TYPE_SAVE_VANILLA) { + SohGui::RegisterPopup( + "Loading old file", + "The file in slot " + std::to_string(fileNum + 1) + + " appears to contain randomizer data, but is a very old format or is empty.\n" + + "The randomizer data has been removed, and this file will be treated as a vanilla " + "file.\nIf this was a vanilla file, it still is, and you shouldn't see this " + "message again.\n" + + "If this was a randomizer file, the file will not work, and should be deleted."); + metaSaveBlock["sections"].erase(metaSaveBlock["sections"].find("randomizer")); + metaSaveBlock["fileType"] = FILE_TYPE_SAVE_VANILLA; + saveMtx.lock(); + std::ofstream output(GetFileName(fileNum)); + output << metaSaveBlock.dump(1); + output.close(); + saveMtx.unlock(); + } + s16 major = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionMajor"]; + s16 minor = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionMinor"]; + s16 patch = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionPatch"]; + // block loading outdated rando save + if (!(major == gBuildVersionMajor && minor == gBuildVersionMinor && patch == gBuildVersionPatch)) { + std::string newFileName = + Ship::Context::GetPathRelativeToAppDirectory("Save") + + ("/file" + std::to_string(fileNum + 1) + "-" + std::to_string(GetUnixTimestamp()) + ".bak"); +#if defined(__SWITCH__) || defined(__WIIU__) + copy_file(fileName.c_str(), newFileName.c_str()); + std::filesystem::remove(fileName); +#else + std::filesystem::rename(fileName, newFileName); +#endif + SohGui::RegisterPopup("Outdated Randomizer Save", + "The SoH version in the file in slot " + std::to_string(fileNum + 1) + + " does not match the currently running version.\n" + + "Non-matching rando saves are unsupported, and the file has been renamed to\n" + + " " + newFileName + "\n" + + "If this was not in error, the file should be deleted."); + return; + } + } + bool isRando = metaSaveBlock["fileType"] == FILE_TYPE_SAVE_RANDO; + + fileMetaInfo[fileNum].valid = true; + nlohmann::json& baseBlock = metaSaveBlock["sections"]["base"]["data"]; + fileMetaInfo[fileNum].deaths = baseBlock["deaths"]; + for (int i = 0; i < ARRAY_COUNT(fileMetaInfo[fileNum].playerName); i++) { + fileMetaInfo[fileNum].playerName[i] = baseBlock["playerName"][i]; + } + fileMetaInfo[fileNum].healthCapacity = baseBlock["healthCapacity"]; + fileMetaInfo[fileNum].questItems = baseBlock["inventory"]["questItems"]; + for (int i = 0; i < ARRAY_COUNT(fileMetaInfo[fileNum].inventoryItems); i++) { + fileMetaInfo[fileNum].inventoryItems[i] = baseBlock["inventory"]["items"][i]; + } + fileMetaInfo[fileNum].equipment = baseBlock["inventory"]["equipment"]; + fileMetaInfo[fileNum].upgrades = baseBlock["inventory"]["upgrades"]; + fileMetaInfo[fileNum].isMagicAcquired = baseBlock["isMagicAcquired"]; + fileMetaInfo[fileNum].isDoubleMagicAcquired = baseBlock["isDoubleMagicAcquired"]; + fileMetaInfo[fileNum].rupees = baseBlock["rupees"]; + fileMetaInfo[fileNum].gsTokens = baseBlock["inventory"]["gsTokens"]; + fileMetaInfo[fileNum].isDoubleDefenseAcquired = baseBlock["isDoubleDefenseAcquired"]; + fileMetaInfo[fileNum].gregFound = false; + fileMetaInfo[fileNum].filenameLanguage = baseBlock["filenameLanguage"]; + fileMetaInfo[fileNum].hasWallet = !isRando; + fileMetaInfo[fileNum].defense = baseBlock["inventory"]["defenseHearts"]; + fileMetaInfo[fileNum].health = baseBlock["health"]; + + fileMetaInfo[fileNum].requiresOriginal = !baseBlock["isMasterQuest"]; + fileMetaInfo[fileNum].requiresMasterQuest = baseBlock["isMasterQuest"]; + + fileMetaInfo[fileNum].randoSave = isRando; + if (isRando) { + nlohmann::json& randoBlock = metaSaveBlock["sections"]["randomizer"]["data"]; + + for (int i = 0; i < ARRAY_COUNT(fileMetaInfo[fileNum].seedHash); i++) { + fileMetaInfo[fileNum].seedHash[i] = randoBlock["seed"][i]; + } + fileMetaInfo[fileNum].gregFound = + (int16_t)baseBlock["randomizerInf"][RAND_INF_GREG_FOUND >> 4] & (1 << (RAND_INF_GREG_FOUND & 0xF)); + fileMetaInfo[fileNum].hasWallet = + (int16_t)baseBlock["randomizerInf"][RAND_INF_HAS_WALLET >> 4] & (1 << (RAND_INF_HAS_WALLET & 0xF)); + fileMetaInfo[fileNum].requiresMasterQuest = randoBlock["masterQuestDungeonCount"] > 0; + // If the file is not marked as Master Quest, it could still theoretically be a rando save with all 12 MQ + // dungeons, in which case we don't actually require a vanilla OTR. + fileMetaInfo[fileNum].requiresOriginal = randoBlock["masterQuestDungeonCount"] < 12; + } + + fileMetaInfo[fileNum].buildVersionMajor = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionMajor"]; + fileMetaInfo[fileNum].buildVersionMinor = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionMinor"]; + fileMetaInfo[fileNum].buildVersionPatch = metaSaveBlock["sections"]["sohStats"]["data"]["buildVersionPatch"]; + SohUtils::CopyStringToCharArray(fileMetaInfo[fileNum].buildVersion, + metaSaveBlock["sections"]["sohStats"]["data"]["buildVersion"], + ARRAY_COUNT(fileMetaInfo[fileNum].buildVersion)); } void SaveManager::InitMeta(int fileNum) { @@ -553,7 +669,6 @@ void SaveManager::InitFileNormal() { gSaveContext.ship.filenameLanguage = (gSaveContext.language == LANGUAGE_JPN) ? NAME_LANGUAGE_NTSC_JPN : NAME_LANGUAGE_NTSC_ENG; } - gSaveContext.n64ddFlag = 0; gSaveContext.healthCapacity = 0x30; gSaveContext.health = 0x30; gSaveContext.magicLevel = 0; @@ -729,7 +844,6 @@ void SaveManager::InitFileDebug() { gSaveContext.ship.filenameLanguage = (gSaveContext.language == LANGUAGE_JPN) ? NAME_LANGUAGE_NTSC_JPN : NAME_LANGUAGE_NTSC_ENG; } - gSaveContext.n64ddFlag = 0; gSaveContext.healthCapacity = 0xE0; gSaveContext.health = 0xE0; gSaveContext.magicLevel = 0; @@ -850,7 +964,6 @@ void SaveManager::InitFileMaxed() { gSaveContext.ship.filenameLanguage = (gSaveContext.language == LANGUAGE_JPN) ? NAME_LANGUAGE_NTSC_JPN : NAME_LANGUAGE_NTSC_ENG; } - gSaveContext.n64ddFlag = 0; gSaveContext.healthCapacity = 0x140; gSaveContext.health = 0x140; gSaveContext.magicLevel = 2; @@ -1018,6 +1131,11 @@ void SaveManager::SaveFileThreaded(int fileNum, SaveContext* saveContext, int se SPDLOG_INFO("Save File - fileNum: {}", fileNum); // Needed for first time save, hasn't changed in forever anyway saveBlock["version"] = 1; + if (IS_RANDO) { + saveBlock["fileType"] = FILE_TYPE_SAVE_RANDO; + } else { + saveBlock["fileType"] = FILE_TYPE_SAVE_VANILLA; + } if (sectionID == SECTION_ID_BASE) { for (auto& sectionHandlerPair : sectionSaveHandlers) { auto& saveFuncInfo = sectionHandlerPair.second; @@ -1083,7 +1201,7 @@ void SaveManager::SaveFileThreaded(int fileNum, SaveContext* saveContext, int se delete saveContext; InitMeta(fileNum); - GameInteractor::Instance->ExecuteHooks(fileNum); + GameInteractor::Instance->ExecuteHooks(fileNum, sectionID); SPDLOG_INFO("Save File Finish - fileNum: {}", fileNum); saveMtx.unlock(); } @@ -1137,62 +1255,20 @@ void SaveManager::LoadFile(int fileNum) { std::ifstream input(fileName); try { - bool deleteRando = false; saveBlock = nlohmann::json::object(); input >> saveBlock; + input.close(); if (!saveBlock.contains("version")) { SPDLOG_ERROR("Save at " + fileName.string() + " contains no version"); assert(false); } + if (saveBlock.contains("fileType") && saveBlock["fileType"] == FILE_TYPE_SAVE_RANDO) { + gSaveContext.ship.quest.id = QUEST_RANDOMIZER; + } switch (saveBlock["version"].get()) { case 1: for (auto& block : saveBlock["sections"].items()) { - bool oldVanilla = - block.value()["data"].empty() || block.value()["data"].contains("aat0") || - block.value()["data"]["entrances"].empty() || - SohUtils::IsStringEmpty(saveBlock["sections"]["sohStats"]["data"]["buildVersion"]); std::string sectionName = block.key(); - if (sectionName == "randomizer") { - bool hasStats = saveBlock["sections"].contains("sohStats"); - if (oldVanilla || !hasStats) { // Vanilla "rando" data - SohGui::RegisterPopup( - "Loading old file", - "The file in slot " + std::to_string(fileNum + 1) + - " appears to contain randomizer data, but is a very old format or is empty.\n" + - "The randomizer data has been removed, and this file will be treated as a vanilla " - "file.\nIf this was a vanilla file, it still is, and you shouldn't see this " - "message again.\n" + - "If this was a randomizer file, the file will not work, and should be deleted."); - deleteRando = true; - continue; - } - s16 major = saveBlock["sections"]["sohStats"]["data"]["buildVersionMajor"]; - s16 minor = saveBlock["sections"]["sohStats"]["data"]["buildVersionMinor"]; - s16 patch = saveBlock["sections"]["sohStats"]["data"]["buildVersionPatch"]; - // block loading outdated rando save - if (!(major == gBuildVersionMajor && minor == gBuildVersionMinor && - patch == gBuildVersionPatch)) { - input.close(); - std::string newFileName = Ship::Context::GetPathRelativeToAppDirectory("Save") + - ("/file" + std::to_string(fileNum + 1) + "-" + - std::to_string(GetUnixTimestamp()) + ".bak"); -#if defined(__SWITCH__) || defined(__WIIU__) - copy_file(fileName.c_str(), newFileName.c_str()); - std::filesystem::remove(fileName); -#else - std::filesystem::rename(fileName, newFileName); -#endif - SohGui::RegisterPopup( - "Outdated Randomizer Save", - "The SoH version in the file in slot " + std::to_string(fileNum + 1) + - " does not match the currently running version.\n" + - "Non-matching rando saves are unsupported, and the file has been renamed to\n" + - " " + newFileName + "\n" + - "If this was not in error, the file should be deleted."); - saveMtx.unlock(); - return; - } - } int sectionVersion = block.value()["version"]; if (sectionName == "randomizer" && sectionVersion != 1) { sectionVersion = 1; @@ -1228,12 +1304,6 @@ void SaveManager::LoadFile(int fileNum) { assert(false); break; } - input.close(); - if (deleteRando) { - saveBlock["sections"].erase(saveBlock["sections"].find("randomizer")); - SaveFile(fileNum); - deleteRando = false; - } InitMeta(fileNum); GameInteractor::Instance->ExecuteHooks(fileNum); } catch (const std::exception& e) { @@ -1349,11 +1419,6 @@ void SaveManager::LoadBaseVersion1() { SaveManager::Instance->LoadData("deaths", gSaveContext.deaths); SaveManager::Instance->LoadArray("playerName", ARRAY_COUNT(gSaveContext.playerName), [](size_t i) { SaveManager::Instance->LoadData("", gSaveContext.playerName[i]); }); - int isRando = 0; - SaveManager::Instance->LoadData("n64ddFlag", isRando); - if (isRando) { - gSaveContext.ship.quest.id = QUEST_RANDOMIZER; - } SaveManager::Instance->LoadData("healthCapacity", gSaveContext.healthCapacity); SaveManager::Instance->LoadData("health", gSaveContext.health); SaveManager::Instance->LoadData("magicLevel", gSaveContext.magicLevel); @@ -1493,11 +1558,6 @@ void SaveManager::LoadBaseVersion2() { SaveManager::Instance->LoadData("deaths", gSaveContext.deaths); SaveManager::Instance->LoadArray("playerName", ARRAY_COUNT(gSaveContext.playerName), [](size_t i) { SaveManager::Instance->LoadData("", gSaveContext.playerName[i]); }); - int isRando = 0; - SaveManager::Instance->LoadData("n64ddFlag", isRando); - if (isRando) { - gSaveContext.ship.quest.id = QUEST_RANDOMIZER; - } SaveManager::Instance->LoadData("healthCapacity", gSaveContext.healthCapacity); SaveManager::Instance->LoadData("health", gSaveContext.health); SaveManager::Instance->LoadData("magicLevel", gSaveContext.magicLevel); @@ -1709,11 +1769,6 @@ void SaveManager::LoadBaseVersion3() { SaveManager::Instance->LoadData("deaths", gSaveContext.deaths); SaveManager::Instance->LoadArray("playerName", ARRAY_COUNT(gSaveContext.playerName), [](size_t i) { SaveManager::Instance->LoadData("", gSaveContext.playerName[i]); }); - int isRando = 0; - SaveManager::Instance->LoadData("n64ddFlag", isRando); - if (isRando) { - gSaveContext.ship.quest.id = QUEST_RANDOMIZER; - } SaveManager::Instance->LoadData("healthCapacity", gSaveContext.healthCapacity); SaveManager::Instance->LoadData("health", gSaveContext.health); SaveManager::Instance->LoadData("magicLevel", gSaveContext.magicLevel); @@ -1929,11 +1984,6 @@ void SaveManager::LoadBaseVersion4() { SaveManager::Instance->LoadData("deaths", gSaveContext.deaths); SaveManager::Instance->LoadArray("playerName", ARRAY_COUNT(gSaveContext.playerName), [](size_t i) { SaveManager::Instance->LoadData("", gSaveContext.playerName[i]); }); - int isRando = 0; - SaveManager::Instance->LoadData("n64ddFlag", isRando); - if (isRando) { - gSaveContext.ship.quest.id = QUEST_RANDOMIZER; - } SaveManager::Instance->LoadData("healthCapacity", gSaveContext.healthCapacity); SaveManager::Instance->LoadData("health", gSaveContext.health); SaveManager::Instance->LoadData("magicLevel", gSaveContext.magicLevel); @@ -2112,7 +2162,6 @@ void SaveManager::SaveBase(SaveContext* saveContext, int sectionID, bool fullSav SaveManager::Instance->SaveArray("playerName", ARRAY_COUNT(saveContext->playerName), [&](size_t i) { SaveManager::Instance->SaveData("", saveContext->playerName[i]); }); - SaveManager::Instance->SaveData("n64ddFlag", saveContext->ship.quest.id == QUEST_RANDOMIZER); SaveManager::Instance->SaveData("healthCapacity", saveContext->healthCapacity); SaveManager::Instance->SaveData("health", saveContext->health); SaveManager::Instance->SaveData("magicLevel", saveContext->magicLevel); diff --git a/soh/soh/SaveManager.h b/soh/soh/SaveManager.h index 7baad1dac..5ec76ce01 100644 --- a/soh/soh/SaveManager.h +++ b/soh/soh/SaveManager.h @@ -166,6 +166,7 @@ class SaveManager { void SaveFileThreaded(int fileNum, SaveContext* saveContext, int sectionID); void InitMeta(int slotNum); + void StartupCheckAndInitMeta(int slotNum); static void InitFileImpl(bool isDebug); static void InitFileNormal(); static void InitFileDebug(); diff --git a/soh/soh/SohGui/SohGui.cpp b/soh/soh/SohGui/SohGui.cpp index 374856fc3..d3646eb42 100644 --- a/soh/soh/SohGui/SohGui.cpp +++ b/soh/soh/SohGui/SohGui.cpp @@ -35,6 +35,7 @@ #include "soh/Network/Archipelago/ArchipelagoSettingsWindow.h" #include "soh/Network/Archipelago/ArchipelagoConsoleWindow.h" #include "soh/Enhancements/mod_menu.h" +#include "soh/Network/Anchor/Anchor.h" namespace SohGui { @@ -101,6 +102,7 @@ std::shared_ptr mRandomizerSettingsWindow; std::shared_ptr mModalWindow; std::shared_ptr mNotificationWindow; std::shared_ptr mTimeDisplayWindow; +std::shared_ptr mAnchorRoomWindow; UIWidgets::Colors GetMenuThemeColor() { return mSohMenu->GetMenuThemeColor(); @@ -214,6 +216,8 @@ void SetupGuiElements() { mNotificationWindow->Show(); mTimeDisplayWindow = std::make_shared(CVAR_WINDOW("TimeDisplayEnabled"), "Additional Timers"); gui->AddGuiWindow(mTimeDisplayWindow); + mAnchorRoomWindow = std::make_shared(CVAR_WINDOW("AnchorRoom"), "Anchor Room"); + gui->AddGuiWindow(mAnchorRoomWindow); } void Destroy() { @@ -251,6 +255,7 @@ void Destroy() { mArchipelagoSettingsWindow = nullptr; mArchipelagoConsoleWindow = nullptr; mTimeDisplayWindow = nullptr; + mAnchorRoomWindow = nullptr; } void RegisterPopup(std::string title, std::string message, std::string button1, std::string button2, diff --git a/soh/soh/SohGui/SohMenu.cpp b/soh/soh/SohGui/SohMenu.cpp index 78616be7e..c5153074e 100644 --- a/soh/soh/SohGui/SohMenu.cpp +++ b/soh/soh/SohGui/SohMenu.cpp @@ -81,27 +81,6 @@ SohMenu::SohMenu(const std::string& consoleVariable, const std::string& name) : Menu(consoleVariable, name, 0, UIWidgets::Colors::LightBlue) { } -#ifndef ENABLE_REMOTE_CONTROL -void SohMenu::AddMenuNetwork() { -#ifndef _DEBUG - // in release builds, the tab doesn't even show - return; -#endif - - // Add Network Menu - AddMenuEntry("Network", CVAR_SETTING("Menu.NetworkSidebarSection")); - - WidgetPath path = { "Network", "Info", SECTION_COLUMN_1 }; - AddSidebarEntry("Network", path.sidebarName, 2); - - AddWidget(path, - ICON_FA_EXCLAMATION_TRIANGLE " The Network features are unavailable because SoH was compiled without " - "network support (\"ENABLE_REMOTE_CONTROL\" build flag).", - WIDGET_TEXT) - .Options(TextOptions().Color(Colors::Orange)); -} -#endif - void SohMenu::InitElement() { Ship::Menu::InitElement(); AddMenuSettings(); diff --git a/soh/soh/SohGui/SohMenuBar.cpp b/soh/soh/SohGui/SohMenuBar.cpp index debfc9cf5..3e5353a1f 100644 --- a/soh/soh/SohGui/SohMenuBar.cpp +++ b/soh/soh/SohGui/SohMenuBar.cpp @@ -18,11 +18,8 @@ #include "soh/Enhancements/mods.h" #include "soh/Notification/Notification.h" #include "soh/Enhancements/cosmetics/authenticGfxPatches.h" -#ifdef ENABLE_REMOTE_CONTROL #include "soh/Network/CrowdControl/CrowdControl.h" #include "soh/Network/Sail/Sail.h" -#endif - #include "soh/Enhancements/audio/AudioEditor.h" #include "soh/Enhancements/controls/InputViewer.h" #include "soh/Enhancements/cosmetics/CosmeticsEditor.h" diff --git a/soh/soh/SohGui/SohMenuDevTools.cpp b/soh/soh/SohGui/SohMenuDevTools.cpp index f9e113b55..288d002f2 100644 --- a/soh/soh/SohGui/SohMenuDevTools.cpp +++ b/soh/soh/SohGui/SohMenuDevTools.cpp @@ -11,6 +11,12 @@ static const std::unordered_map logLevels = { { DEBUG_LOG_OFF, "Off" }, }; +#ifdef _DEBUG +DebugLogOption defaultLogLevel = DEBUG_LOG_TRACE; +#else +DebugLogOption defaultLogLevel = DEBUG_LOG_INFO; +#endif + static const std::unordered_map debugSaveFileModes = { { 0, "Off" }, { 1, "Vanilla" }, @@ -110,10 +116,11 @@ void SohMenu::AddMenuDevTools() { .Options(ComboboxOptions() .Tooltip("The log level determines which messages are printed to the console." " This does not affect the log file output") - .ComboMap(logLevels)) + .ComboMap(logLevels) + .DefaultIndex(defaultLogLevel)) .Callback([](WidgetInfo& info) { Ship::Context::GetInstance()->GetLogger()->set_level( - (spdlog::level::level_enum)CVarGetInteger(CVAR_DEVELOPER_TOOLS("LogLevel"), DEBUG_LOG_DEBUG)); + (spdlog::level::level_enum)CVarGetInteger(CVAR_DEVELOPER_TOOLS("LogLevel"), defaultLogLevel)); }) .PreFunc([](WidgetInfo& info) { info.isHidden = mSohMenu->disabledMap.at(DISABLE_FOR_DEBUG_MODE_OFF).active; }); diff --git a/soh/soh/SohGui/SohMenuEnhancements.cpp b/soh/soh/SohGui/SohMenuEnhancements.cpp index e020d2ab8..149eb6577 100644 --- a/soh/soh/SohGui/SohMenuEnhancements.cpp +++ b/soh/soh/SohGui/SohMenuEnhancements.cpp @@ -732,9 +732,9 @@ void SohMenu::AddMenuEnhancements() { .Options(CheckboxOptions().Tooltip( "Equip items and equipment on the D-pad. If used with \"D-pad on Pause Screen\", you must " "hold C-Up to equip instead of navigate.")); - AddWidget(path, "Assignable Tunics and Boots", WIDGET_CVAR_CHECKBOX) + AddWidget(path, "Assignable Shields, Tunics and Boots", WIDGET_CVAR_CHECKBOX) .CVar(CVAR_ENHANCEMENT("AssignableTunicsAndBoots")) - .Options(CheckboxOptions().Tooltip("Allows equipping the Tunics and Boots to C-Buttons/D-pad.")); + .Options(CheckboxOptions().Tooltip("Allows equipping Shields, Tunics and Boots to C-Buttons/D-pad.")); // TODO: Revist strength toggle, it's currently separate but should probably be locked behind the // Equipment toggle settings or be absorbed by it completely. AddWidget(path, "Equipment Toggle", WIDGET_CVAR_CHECKBOX) diff --git a/soh/soh/SohGui/SohMenuNetwork.cpp b/soh/soh/SohGui/SohMenuNetwork.cpp index b9693f493..f373ec649 100644 --- a/soh/soh/SohGui/SohMenuNetwork.cpp +++ b/soh/soh/SohGui/SohMenuNetwork.cpp @@ -1,4 +1,3 @@ -#ifdef ENABLE_REMOTE_CONTROL #include "SohMenu.h" #include #include @@ -15,9 +14,22 @@ using namespace UIWidgets; void SohMenu::AddMenuNetwork() { // Add Network Menu AddMenuEntry("Network", CVAR_SETTING("Menu.NetworkSidebarSection")); + WidgetPath path; + +#ifndef ENABLE_REMOTE_CONTROL + path = { "Network", "Info", SECTION_COLUMN_1 }; + AddSidebarEntry("Network", path.sidebarName, 2); + + AddWidget(path, + ICON_FA_EXCLAMATION_TRIANGLE " The Network features are unavailable because SoH was compiled without " + "network support (\"ENABLE_REMOTE_CONTROL\" build flag).", + WIDGET_TEXT) + .Options(TextOptions().Color(Colors::Orange)); + return; +#endif // Archipelago - WidgetPath path = { "Network", "Archipelago", SECTION_COLUMN_1 }; + path = { "Network", "Archipelago", SECTION_COLUMN_1 }; AddSidebarEntry(path.sectionName, path.sidebarName, 2); AddWidget(path, "Popout Archipelago Settings Window", WIDGET_WINDOW_BUTTON) .CVar(CVAR_WINDOW("ArchipelagoSettings")) @@ -33,8 +45,7 @@ void SohMenu::AddMenuNetwork() { .Options(WindowButtonOptions().Tooltip("Enables the Archipelago Console Window.")); // Sail - path.sidebarName = "Sail"; - path.column = SECTION_COLUMN_1; + path = { "Network", "Sail", SECTION_COLUMN_1 }; AddSidebarEntry("Network", path.sidebarName, 3); AddWidget(path, @@ -185,7 +196,8 @@ void SohMenu::AddMenuNetwork() { .RaceDisable(true) .Options(CheckboxOptions().Tooltip("Enemies spawned by CrowdControl won't be considered for \"clear enemy " "rooms\", so they don't need to be killed to complete these rooms.")); + path.sidebarName = "Anchor"; + AddSidebarEntry("Network", path.sidebarName, 2); } } // namespace SohGui -#endif \ No newline at end of file diff --git a/soh/soh/SohGui/UIWidgets.cpp b/soh/soh/SohGui/UIWidgets.cpp index 774407583..ca5b4be9b 100644 --- a/soh/soh/SohGui/UIWidgets.cpp +++ b/soh/soh/SohGui/UIWidgets.cpp @@ -767,15 +767,18 @@ bool InputString(const char* label, std::string* value, const InputOptions& opti ImGui::PushStyleColor(ImGuiCol_Border, ColorValues.at(Colors::Red)); } float width = (options.size == ImVec2(0, 0)) ? ImGui::GetContentRegionAvail().x : options.size.x; - if (options.alignment == ComponentAlignments::Left) { - if (options.labelPosition == LabelPositions::Above) { - ImGui::Text(label, *value->c_str()); - } - } else if (options.alignment == ComponentAlignments::Right) { - if (options.labelPosition == LabelPositions::Above) { - ImGui::NewLine(); - ImGui::SameLine(width - ImGui::CalcTextSize(label).x); - ImGui::Text(label, *value->c_str()); + ImVec2 labelSize = ImGui::CalcTextSize(label, NULL, true); + if (labelSize.x != 0) { + if (options.alignment == ComponentAlignments::Left) { + if (options.labelPosition == LabelPositions::Above) { + ImGui::Text(label, *value->c_str()); + } + } else if (options.alignment == ComponentAlignments::Right) { + if (options.labelPosition == LabelPositions::Above) { + ImGui::NewLine(); + ImGui::SameLine(width - ImGui::CalcTextSize(label).x); + ImGui::Text(label, *value->c_str()); + } } } ImGui::SetNextItemWidth(width); diff --git a/soh/soh/cvar_prefixes.h b/soh/soh/cvar_prefixes.h index 8c53fd76e..0ae5fb4cc 100644 --- a/soh/soh/cvar_prefixes.h +++ b/soh/soh/cvar_prefixes.h @@ -16,5 +16,6 @@ #define CVAR_REMOTE_CROWD_CONTROL(var) CVAR_REMOTE("CrowdControl." var) #define CVAR_REMOTE_SAIL(var) CVAR_REMOTE("Sail." var) #define CVAR_REMOTE_ARCHIPELAGO(var) CVAR_REMOTE("Archipelago." var) +#define CVAR_REMOTE_ANCHOR(var) CVAR_REMOTE("Anchor." var) #define CVAR_GAMEPLAY_STATS(var) CVAR_PREFIX_GAMEPLAY_STATS "." var #define CVAR_TIME_DISPLAY(var) CVAR_PREFIX_TIME_DISPLAY "." var \ No newline at end of file diff --git a/soh/soh/util.cpp b/soh/soh/util.cpp index c05f03f0c..159ccfbe2 100644 --- a/soh/soh/util.cpp +++ b/soh/soh/util.cpp @@ -120,6 +120,7 @@ std::vector sceneNames = { "Castle Hedge Maze (Early)", "Sasa Test", "Treasure Chest Room", + "Unknown", }; std::vector itemNamesEng = { diff --git a/soh/soh/util.h b/soh/soh/util.h index 9a31d4be4..f704076b4 100644 --- a/soh/soh/util.h +++ b/soh/soh/util.h @@ -2,6 +2,8 @@ #include #include +typedef enum FileType { FILE_TYPE_SAVE_VANILLA, FILE_TYPE_SAVE_RANDO, FILE_TYPE_PRESET, FILE_TYPE_SPOILER } FileType; + namespace SohUtils { const std::string& GetSceneName(int32_t scene); diff --git a/soh/soh/z_scene_otr.cpp b/soh/soh/z_scene_otr.cpp index d49367024..1da9d729a 100644 --- a/soh/soh/z_scene_otr.cpp +++ b/soh/soh/z_scene_otr.cpp @@ -472,13 +472,12 @@ extern "C" s32 OTRfunc_800973FC(PlayState* play, RoomContext* roomCtx) { gSegments[3] = VIRTUAL_TO_PHYSICAL(roomCtx->unk_34); OTRScene_ExecuteCommands(play, (SOH::Scene*)roomCtx->roomToLoad); - if (!GameInteractor_Should(VB_DRAW_2D_BACKGROUND, true)) { - play->envCtx.skyboxDisabled = false; - } Player_SetBootData(play, GET_PLAYER(play)); Actor_SpawnTransitionActors(play, &play->actorCtx); + GameInteractor_ExecuteAfterSceneCommands(play->sceneNum); + return 1; } diff --git a/soh/src/code/z_actor.c b/soh/src/code/z_actor.c index d8263f5de..d1386a48a 100644 --- a/soh/src/code/z_actor.c +++ b/soh/src/code/z_actor.c @@ -1256,10 +1256,16 @@ void Actor_Init(Actor* actor, PlayState* play) { ActorShape_Init(&actor->shape, 0.0f, NULL, 0.0f); if (Object_IsLoaded(&play->objectCtx, actor->objBankIndex)) { Actor_SetObjectDependency(play, actor); - actor->init(actor, play); - actor->init = NULL; - GameInteractor_ExecuteOnActorInit(actor); + if (GameInteractor_ShouldActorInit(actor)) { + actor->init(actor, play); + actor->init = NULL; + + GameInteractor_ExecuteOnActorInit(actor); + } else { + actor->init = NULL; + Actor_Kill(actor); + } } } @@ -2244,6 +2250,10 @@ void Player_PlaySfx(Actor* actor, u16 sfxId) { Audio_PlaySoundGeneral(sfxId, &actor->projectedPos, 4, &freqMultiplier, &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb); } + + if (actor->id == ACTOR_PLAYER) { + GameInteractor_ExecuteOnPlayerSfx(sfxId); + } } void Audio_PlayActorSound2(Actor* actor, u16 sfxId) { @@ -2624,10 +2634,16 @@ void Actor_UpdateAll(PlayState* play, ActorContext* actorCtx) { if (actor->init != NULL) { if (Object_IsLoaded(&play->objectCtx, actor->objBankIndex)) { Actor_SetObjectDependency(play, actor); - actor->init(actor, play); - actor->init = NULL; - GameInteractor_ExecuteOnActorInit(actor); + if (GameInteractor_ShouldActorInit(actor)) { + actor->init(actor, play); + actor->init = NULL; + + GameInteractor_ExecuteOnActorInit(actor); + } else { + actor->init = NULL; + Actor_Kill(actor); + } } actor = actor->next; } else if (!Object_IsLoaded(&play->objectCtx, actor->objBankIndex)) { @@ -2670,8 +2686,10 @@ void Actor_UpdateAll(PlayState* play, ActorContext* actorCtx) { if (actor->colorFilterTimer != 0) { actor->colorFilterTimer--; } - actor->update(actor, play); - GameInteractor_ExecuteOnActorUpdate(actor); + if (GameInteractor_ShouldActorUpdate(actor)) { + actor->update(actor, play); + GameInteractor_ExecuteOnActorUpdate(actor); + } func_8003F8EC(play, &play->colCtx.dyna, actor); } diff --git a/soh/src/code/z_inventory.c b/soh/src/code/z_inventory.c index 8a51c6263..f2b329ed5 100644 --- a/soh/src/code/z_inventory.c +++ b/soh/src/code/z_inventory.c @@ -1,4 +1,5 @@ #include "global.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" #include "textures/icon_item_static/icon_item_static.h" #include "textures/icon_item_24_static/icon_item_24_static.h" #include "textures/parameter_static/parameter_static.h" @@ -204,20 +205,10 @@ u8 Inventory_DeleteEquipment(PlayState* play, s16 equipment) { if (equipment == EQUIP_TYPE_TUNIC) { gSaveContext.equips.equipment |= EQUIP_VALUE_TUNIC_KOKIRI << (EQUIP_TYPE_TUNIC * 4); - // non-vanilla: remove goron and zora tunics from item buttons if assignable tunics is on - if (CVarGetInteger(CVAR_ENHANCEMENT("AssignableTunicsAndBoots"), 0) && - equipValue != EQUIP_VALUE_TUNIC_KOKIRI) { - ItemID item = (equipValue == EQUIP_VALUE_TUNIC_GORON ? ITEM_TUNIC_GORON : ITEM_TUNIC_ZORA); - for (int i = 1; i < ARRAY_COUNT(gSaveContext.equips.buttonItems); i++) { - if (gSaveContext.equips.buttonItems[i] == item) { - gSaveContext.equips.buttonItems[i] = ITEM_NONE; - gSaveContext.equips.cButtonSlots[i - 1] = SLOT_NONE; - } - } - } - // end non-vanilla } + GameInteractor_ExecuteOnEquipmentDelete(equipment, equipValue); + if (equipment == EQUIP_TYPE_SWORD) { gSaveContext.equips.buttonItems[0] = ITEM_NONE; gSaveContext.infTable[29] = 1; diff --git a/soh/src/overlays/actors/ovl_Bg_Hidan_Dalm/z_bg_hidan_dalm.c b/soh/src/overlays/actors/ovl_Bg_Hidan_Dalm/z_bg_hidan_dalm.c index 725bfacd6..b4e3ca3c3 100644 --- a/soh/src/overlays/actors/ovl_Bg_Hidan_Dalm/z_bg_hidan_dalm.c +++ b/soh/src/overlays/actors/ovl_Bg_Hidan_Dalm/z_bg_hidan_dalm.c @@ -6,6 +6,7 @@ #include "z_bg_hidan_dalm.h" #include "objects/object_hidan_objects/object_hidan_objects.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" #define FLAGS 0 @@ -126,8 +127,10 @@ void BgHidanDalm_Destroy(Actor* thisx, PlayState* play) { void BgHidanDalm_Wait(BgHidanDalm* this, PlayState* play) { Player* player = GET_PLAYER(play); - if ((this->collider.base.acFlags & AC_HIT) && !Player_InCsMode(play) && - (player->meleeWeaponAnimation == 22 || player->meleeWeaponAnimation == 23)) { + if (GameInteractor_Should(VB_HAMMER_TOTEM_BREAK, + (this->collider.base.acFlags & AC_HIT) && !Player_InCsMode(play) && + (player->meleeWeaponAnimation == 22 || player->meleeWeaponAnimation == 23), + this)) { this->collider.base.acFlags &= ~AC_HIT; if ((this->collider.elements[0].info.bumperFlags & BUMP_HIT) || (this->collider.elements[1].info.bumperFlags & BUMP_HIT)) { diff --git a/soh/src/overlays/actors/ovl_Bg_Hidan_Kowarerukabe/z_bg_hidan_kowarerukabe.c b/soh/src/overlays/actors/ovl_Bg_Hidan_Kowarerukabe/z_bg_hidan_kowarerukabe.c index 328d1f4c0..0625c6ab8 100644 --- a/soh/src/overlays/actors/ovl_Bg_Hidan_Kowarerukabe/z_bg_hidan_kowarerukabe.c +++ b/soh/src/overlays/actors/ovl_Bg_Hidan_Kowarerukabe/z_bg_hidan_kowarerukabe.c @@ -8,6 +8,7 @@ #include "objects/gameplay_dangeon_keep/gameplay_dangeon_keep.h" #include "overlays/effects/ovl_Effect_Ss_Kakera/z_eff_ss_kakera.h" #include "objects/object_hidan_objects/object_hidan_objects.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" #define FLAGS 0 @@ -303,7 +304,8 @@ void BgHidanKowarerukabe_Update(Actor* thisx, PlayState* play) { BgHidanKowarerukabe* this = (BgHidanKowarerukabe*)thisx; s32 pad; - if (Actor_GetCollidedExplosive(play, &this->collider.base) != NULL) { + if (GameInteractor_Should(VB_FIRE_TEMPLE_BOMBABLE_WALL_BREAK, + Actor_GetCollidedExplosive(play, &this->collider.base) != NULL, this)) { BgHidanKowarerukabe_Break(this, play); Flags_SetSwitch(play, (this->dyna.actor.params >> 8) & 0x3F); diff --git a/soh/src/overlays/actors/ovl_Door_Gerudo/z_door_gerudo.c b/soh/src/overlays/actors/ovl_Door_Gerudo/z_door_gerudo.c index 34796f980..29eccfdd7 100644 --- a/soh/src/overlays/actors/ovl_Door_Gerudo/z_door_gerudo.c +++ b/soh/src/overlays/actors/ovl_Door_Gerudo/z_door_gerudo.c @@ -6,6 +6,7 @@ #include "z_door_gerudo.h" #include "objects/object_door_gerudo/object_door_gerudo.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" #define FLAGS 0 @@ -103,6 +104,7 @@ void func_8099485C(DoorGerudo* this, PlayState* play) { gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex] -= 1; Flags_SetSwitch(play, this->dyna.actor.params & 0x3F); Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_CHAIN_KEY_UNLOCK); + GameInteractor_ExecuteOnDungeonKeyUsedHooks(gSaveContext.mapIndex); } else { s32 direction = func_80994750(this, play); diff --git a/soh/src/overlays/actors/ovl_Door_Shutter/z_door_shutter.c b/soh/src/overlays/actors/ovl_Door_Shutter/z_door_shutter.c index ea983771f..307aee785 100644 --- a/soh/src/overlays/actors/ovl_Door_Shutter/z_door_shutter.c +++ b/soh/src/overlays/actors/ovl_Door_Shutter/z_door_shutter.c @@ -391,6 +391,7 @@ void func_80996B0C(DoorShutter* this, PlayState* play) { if (this->doorType != SHUTTER_BOSS) { gSaveContext.inventory.dungeonKeys[gSaveContext.mapIndex]--; Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_CHAIN_KEY_UNLOCK); + GameInteractor_ExecuteOnDungeonKeyUsedHooks(gSaveContext.mapIndex); } else { Audio_PlayActorSound2(&this->dyna.actor, NA_SE_EV_CHAIN_KEY_UNLOCK_B); } diff --git a/soh/src/overlays/actors/ovl_En_Door/z_en_door.c b/soh/src/overlays/actors/ovl_En_Door/z_en_door.c index e714cb211..268355094 100644 --- a/soh/src/overlays/actors/ovl_En_Door/z_en_door.c +++ b/soh/src/overlays/actors/ovl_En_Door/z_en_door.c @@ -209,6 +209,7 @@ void EnDoor_Idle(EnDoor* this, PlayState* play) { Flags_SetSwitch(play, this->actor.params & 0x3F); } Audio_PlayActorSound2(&this->actor, NA_SE_EV_CHAIN_KEY_UNLOCK); + GameInteractor_ExecuteOnDungeonKeyUsedHooks(gSaveContext.mapIndex); } } else if (!Player_InCsMode(play)) { if (fabsf(playerPosRelToDoor.y) < 20.0f && fabsf(playerPosRelToDoor.x) < 20.0f && diff --git a/soh/src/overlays/actors/ovl_En_Sw/z_en_sw.h b/soh/src/overlays/actors/ovl_En_Sw/z_en_sw.h index 0c7445397..0523823c9 100644 --- a/soh/src/overlays/actors/ovl_En_Sw/z_en_sw.h +++ b/soh/src/overlays/actors/ovl_En_Sw/z_en_sw.h @@ -6,7 +6,7 @@ struct EnSw; -typedef void (*EnSwActionFunc)(struct EnSw* this, PlayState* play); +typedef void (*EnSwActionFunc)(struct EnSw* thisx, PlayState* play); typedef struct EnSw { /* 0x0000 */ Actor actor; diff --git a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_equipment.c b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_equipment.c index cec05af72..59dfffb1f 100644 --- a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_equipment.c +++ b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_equipment.c @@ -642,8 +642,8 @@ void KaleidoScope_DrawEquipment(PlayState* play) { pauseCtx->unk_1E4 = 7; sEquipTimer = 10; } else if (CVarGetInteger(CVAR_ENHANCEMENT("AssignableTunicsAndBoots"), 0) != 0) { - // Only allow assigning tunic and boots to c-buttons - if (pauseCtx->cursorY[PAUSE_EQUIP] > 1) { + // Only allow assigning shield, tunic and boots to c-buttons + if (pauseCtx->cursorY[PAUSE_EQUIP] > 0) { if (CHECK_OWNED_EQUIP(pauseCtx->cursorY[PAUSE_EQUIP], pauseCtx->cursorX[PAUSE_EQUIP] - 1)) { u16 slot = 0; switch (cursorItem) { @@ -665,6 +665,15 @@ void KaleidoScope_DrawEquipment(PlayState* play) { case ITEM_BOOTS_HOVER: slot = SLOT_BOOTS_HOVER; break; + case ITEM_SHIELD_DEKU: + slot = SLOT_SHIELD_DEKU; + break; + case ITEM_SHIELD_HYLIAN: + slot = SLOT_SHIELD_HYLIAN; + break; + case ITEM_SHIELD_MIRROR: + slot = SLOT_SHIELD_MIRROR; + break; default: break; }