Merge branch 'develop' of github.com:HarbourMasters/Shipwright into aManchipelago

This commit is contained in:
Garrett
2025-11-16 08:12:13 -06:00
110 changed files with 4961 additions and 1115 deletions

1
.gitignore vendored
View File

@@ -417,6 +417,7 @@ tags
shipofharkinian.ini
shipofharkinian.json
imgui.ini
timesplitdata.json
# Switch Stuff

View File

@@ -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)

View File

@@ -431,13 +431,15 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
"ENABLE_DX11;"
">"
"$<$<CONFIG:Release>:"
"NDEBUG"
"NDEBUG;"
">"
"$<$<BOOL:${BUILD_REMOTE_CONTROL}>: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;"
">"
"$<$<CONFIG:Release>:"
"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
"$<$<CONFIG:Debug>:"
"_DEBUG"
"_DEBUG;"
">"
"$<$<CONFIG:Release>:"
"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
"$<$<CONFIG:Debug>:"
"_DEBUG"
"_DEBUG;"
">"
"$<$<CONFIG:Release>:"
"NDEBUG"
"NDEBUG;"
">"
"F3DEX_GBI_2"
"$<$<BOOL:${BUILD_REMOTE_CONTROL}>:ENABLE_REMOTE_CONTROL>"
"SPDLOG_ACTIVE_LEVEL=0;"
"F3DEX_GBI_2;"
"$<$<BOOL:${BUILD_REMOTE_CONTROL}>: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()
################################################################################

View File

@@ -16,6 +16,7 @@
"ClimbSpeed": 3,
"CrawlSpeed": 2,
"CreditsFix": 1,
"CuccosToReturn": 1,
"CustomizeFishing": 1,
"CustomizeFrogsOcarinaGame": 1,
"CustomizeOcarinaGame": 1,

View File

@@ -13,8 +13,8 @@ extern "C"
#include <libultraship/log/luslog.h>
#include <soh/Enhancements/item-tables/ItemTableTypes.h>
#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

View File

@@ -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;

View File

@@ -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 });

View File

@@ -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") });

View File

@@ -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") });

View File

@@ -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);

View File

@@ -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") });

View File

@@ -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 });

View File

@@ -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 });

View File

@@ -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 });

View File

@@ -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::ConsoleWindow>(
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);

View File

@@ -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 });

View File

@@ -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 });

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -13,7 +13,7 @@ extern PlayState* gPlayState;
#define CVAR_NAME CVAR_ENHANCEMENT("3DSceneRender")
#define CVAR_VALUE CVarGetInteger(CVAR_NAME, 0)
std::vector<SceneID> fogControlList = {
std::set<SceneID> fogControlList = {
SCENE_MARKET_ENTRANCE_DAY,
SCENE_MARKET_ENTRANCE_NIGHT,
SCENE_MARKET_ENTRANCE_RUINS,
@@ -46,7 +46,7 @@ std::vector<SceneID> fogControlList = {
SCENE_GRAVEKEEPERS_HUT,
};
std::vector<SceneID> skyboxSceneControlList = {
std::set<SceneID> skyboxSceneControlList = {
SCENE_MARKET_ENTRANCE_DAY,
SCENE_MARKET_ENTRANCE_NIGHT,
SCENE_MARKET_ENTRANCE_RUINS,
@@ -62,7 +62,7 @@ std::vector<SceneID> skyboxSceneControlList = {
SCENE_FOREST_TEMPLE,
};
std::vector<SkyboxId> skyboxIdControlList = {
std::set<SkyboxId> skyboxIdControlList = {
SKYBOX_BAZAAR,
SKYBOX_HOUSE_LINK,
SKYBOX_MARKET_ADULT,
@@ -88,74 +88,49 @@ std::vector<SkyboxId> 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<SceneID>(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<SceneID>(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<SkyboxId>(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 });
static RegisterShipInitFunc initFunc(Register3DPreRenderedScenes, { CVAR_NAME });

View File

@@ -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<PresetSection> 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);

View File

@@ -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(); });
}

View File

@@ -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<EnDoor*>(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 });

View File

@@ -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));

View File

@@ -38,6 +38,11 @@ void GameInteractor_ExecuteOnItemReceiveHooks(GetItemEntry itemEntry) {
GameInteractor::Instance->ExecuteHooksForFilter<GameInteractor::OnItemReceive>(itemEntry);
}
void GameInteractor_ExecuteOnEquipmentDelete(int16_t equipmentType, uint16_t equipValue) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnEquipmentDelete>(equipmentType, equipValue);
GameInteractor::Instance->ExecuteHooksForFilter<GameInteractor::OnEquipmentDelete>(equipmentType, equipValue);
}
void GameInteractor_ExecuteOnSaleEndHooks(GetItemEntry itemEntry) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnSaleEnd>(itemEntry);
GameInteractor::Instance->ExecuteHooksForFilter<GameInteractor::OnSaleEnd>(itemEntry);
@@ -97,6 +102,10 @@ void GameInteractor_ExecuteOnSetDoAction(uint16_t action) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnSetDoAction>(action);
}
void GameInteractor_ExecuteOnPlayerSfx(u16 sfxId) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnPlayerSfx>(sfxId);
}
void GameInteractor_ExecuteOnOcarinaSongAction() {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnOcarinaSongAction>();
}
@@ -109,6 +118,19 @@ void GameInteractor_ExecuteOnShopSlotChangeHooks(uint8_t cursorIndex, int16_t pr
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnShopSlotChange>(cursorIndex, price);
}
void GameInteractor_ExecuteOnDungeonKeyUsedHooks(uint16_t mapIndex) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnDungeonKeyUsed>(mapIndex);
}
bool GameInteractor_ShouldActorInit(void* actor) {
bool result = true;
GameInteractor::Instance->ExecuteHooks<GameInteractor::ShouldActorInit>(actor, &result);
GameInteractor::Instance->ExecuteHooksForID<GameInteractor::ShouldActorInit>(((Actor*)actor)->id, actor, &result);
GameInteractor::Instance->ExecuteHooksForPtr<GameInteractor::ShouldActorInit>((uintptr_t)actor, actor, &result);
GameInteractor::Instance->ExecuteHooksForFilter<GameInteractor::ShouldActorInit>(actor, &result);
return result;
}
void GameInteractor_ExecuteOnActorInit(void* actor) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnActorInit>(actor);
GameInteractor::Instance->ExecuteHooksForID<GameInteractor::OnActorInit>(((Actor*)actor)->id, actor);
@@ -123,6 +145,15 @@ void GameInteractor_ExecuteOnActorSpawn(void* actor) {
GameInteractor::Instance->ExecuteHooksForFilter<GameInteractor::OnActorSpawn>(actor);
}
bool GameInteractor_ShouldActorUpdate(void* actor) {
bool result = true;
GameInteractor::Instance->ExecuteHooks<GameInteractor::ShouldActorUpdate>(actor, &result);
GameInteractor::Instance->ExecuteHooksForID<GameInteractor::ShouldActorUpdate>(((Actor*)actor)->id, actor, &result);
GameInteractor::Instance->ExecuteHooksForPtr<GameInteractor::ShouldActorUpdate>((uintptr_t)actor, actor, &result);
GameInteractor::Instance->ExecuteHooksForFilter<GameInteractor::ShouldActorUpdate>(actor, &result);
return result;
}
void GameInteractor_ExecuteOnActorUpdate(void* actor) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnActorUpdate>(actor);
GameInteractor::Instance->ExecuteHooksForID<GameInteractor::OnActorUpdate>(((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<GameInteractor::OnSaveFile>(fileNum);
void GameInteractor_ExecuteOnSaveFile(int32_t fileNum, int32_t sectionID) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnSaveFile>(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<GameInteractor::OnSeqPlayerInit>(playerIdx, seqId);
}
// MARK: - Rando
void GameInteractor_ExecuteOnRandoEntranceDiscovered(u16 entranceIndex, u8 isReversedEntrance) {
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnRandoEntranceDiscovered>(entranceIndex,
isReversedEntrance);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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<std::string, std::string> tempMods;
if (modsPath.length() > 0 && std::filesystem::exists(modsPath)) {
std::vector<std::filesystem::path> 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<std::string> 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();
}
}
}

View File

@@ -2,14 +2,9 @@
#include <libultraship/bridge.h>
#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 <soh/Enhancements/item-tables/ItemTableManager.h>
#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 <z64.h>
@@ -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<GameInteractor::OnLoadGame>([](int16_t fileNum) {
hasAffectedHealth = false;
UpdatePermanentHeartLossState();
});
GameInteractor::Instance->RegisterGameHook<GameInteractor::OnPlayerUpdate>([]() {
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<GameInteractor::OnGameFrameUpdate>([]() {
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::ConsoleWindow>(
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<GameInteractor::OnSceneInit>(
[](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<GameInteractor::OnBossDefeat>([](void* refActor) {
Actor* actor = static_cast<Actor*>(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<GameInteractor::OnGameFrameUpdate>([]() {
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<GameInteractor::OnAssetAltChange>([]() {
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();
}

View File

@@ -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());
}
}

View File

@@ -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()) {

View File

@@ -9,7 +9,6 @@ class ItemLocation;
void AddItemToPool(std::vector<RandomizerGet>& pool, const RandomizerGet item, size_t count = 1);
RandomizerGet GetJunkItem();
void PlaceJunkInExcludedLocation(const RandomizerCheck il);
void GenerateItemPool();
extern std::vector<RandomizerGet> ItemPool;

View File

@@ -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();

View File

@@ -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)) ||

View File

@@ -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<Entrance*> EntranceShuffler::AssumeEntrancePool(std::vector<Entrance
(ctx->GetOption(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)) {

View File

@@ -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()) {

View File

@@ -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<GameInteractor::OnRandoSetCheckStatus>(rc, status);
}
RandomizerCheckStatus ItemLocation::GetCheckStatus() {
@@ -144,6 +145,7 @@ RandomizerCheckStatus ItemLocation::GetCheckStatus() {
void ItemLocation::SetIsSkipped(bool isSkipped_) {
isSkipped = isSkipped_;
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnRandoSetIsSkipped>(rc, isSkipped);
}
bool ItemLocation::GetIsSkipped() {

View File

@@ -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

View File

@@ -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, {

View File

@@ -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()),

View File

@@ -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

View File

@@ -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

View File

@@ -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, {}, {

View File

@@ -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;

View File

@@ -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,

View File

@@ -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) {

View File

@@ -3,7 +3,6 @@
#include "soh/cvar_prefixes.h"
#include "soh/SohGui/SohGui.hpp"
#include <map>
#include <string>
#include <vector>
#include <libultraship/libultraship.h>
@@ -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<EntranceOverride> 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();
}

View File

@@ -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,

View File

@@ -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 },

View File

@@ -225,7 +225,7 @@ StaticData::PopulateTranslationMap(std::unordered_map<uint32_t, CustomMessage> 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_map<uint32_t, RandomizerHintTe
if (output.contains(string)) {
if (output[string] != key) {
// RANDOTODO should this cause an error of some kind?
SPDLOG_DEBUG("\tREPEATED STRING WITH " + string + "\n\n");
SPDLOG_DEBUG("REPEATED STRING WITH " + string);
}
} else {
output[string] = key;

View File

@@ -709,11 +709,6 @@ void TimeSaverOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_li
*should = false;
}
break;
case VB_DAMPE_IN_GRAVEYARD_DESPAWN:
if (CVarGetInteger(CVAR_ENHANCEMENT("DampeAllNight"), 0)) {
*should = LINK_IS_ADULT || gPlayState->sceneNum != SCENE_GRAVEYARD;
}
break;
case VB_BE_VALID_GRAVEDIGGING_SPOT:
if (CVarGetInteger(CVAR_ENHANCEMENT("DampeWin"), IS_RANDO)) {
EnTk* enTk = va_arg(args, EnTk*);

View File

@@ -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<GameInteractor::OnTimestamp>([](u8 item) {
if (item != ITEM_SKULL_TOKEN) {

View File

@@ -0,0 +1,212 @@
#include "Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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<std::string>();
// 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<uint32_t>();
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<std::mutex> lock(incomingPacketQueueMutex);
incomingPacketQueue.push(payload);
}
void Anchor::ProcessIncomingPacketQueue() {
std::lock_guard<std::mutex> lock(incomingPacketQueueMutex);
while (!incomingPacketQueue.empty()) {
nlohmann::json payload = incomingPacketQueue.front();
incomingPacketQueue.pop();
std::string packetType = payload["type"].get<std::string>();
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<DummyPlayerClientId> DummyPlayerClientIdRegister;
uint32_t Anchor::GetDummyPlayerClientId(const Actor* actor) {
const DummyPlayerClientId* clientId = ObjectExtension::GetInstance().Get<DummyPlayerClientId>(actor);
return clientId != nullptr ? clientId->clientId : 0;
}
void Anchor::SetDummyPlayerClientId(const Actor* actor, uint32_t clientId) {
ObjectExtension::GetInstance().Set<DummyPlayerClientId>(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;
}

View File

@@ -0,0 +1,184 @@
#ifndef NETWORK_ANCHOR_H
#define NETWORK_ANCHOR_H
#ifdef __cplusplus
#include "soh/Network/Network.h"
#include <libultraship/libultraship.h>
#include <queue>
#include <mutex>
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<nlohmann::json> 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<uint32_t, AnchorClient> 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

View File

@@ -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<std::string> 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();
}
}
}

View File

@@ -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) {
}

View File

@@ -0,0 +1,369 @@
#include "Anchor.h"
#include <libultraship/libultraship.h>
#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<EnItem00*>(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<BgBombwall*>(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<BgBreakwall*>(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<BgHakaZou*>(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<BgHidanHamstep*>(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<BgHidanHrock*>(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<BgIceShelter*>(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<BgJyaBombchuiwa*>(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<BgJyaBombiwa*>(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<BgMizuBwall*>(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<BgSpot08Bakudankabe*>(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<BgSpot11Bakudankabe*>(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<BgSpot17Bakudankabe*>(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<BgYdanMaruta*>(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<BgYdanSp*>(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<DoorShutter*>(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<EnDoor*>(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<EnSi*>(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<EnSw*>(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<ItemBHeart*>(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<ObjBombiwa*>(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<ObjHamishi*>(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
}

View File

@@ -0,0 +1,205 @@
#ifndef NETWORK_ANCHOR_JSON_CONVERSIONS_H
#define NETWORK_ANCHOR_JSON_CONVERSIONS_H
#ifdef __cplusplus
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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<u32> 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<u32> 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

View File

@@ -0,0 +1,241 @@
#include "Anchor.h"
#include <libultraship/libultraship.h>
#include "soh/SohGui/SohGui.hpp"
#include "soh/SohGui/SohMenu.h"
#include "soh/util.h"
namespace SohGui {
extern std::shared_ptr<SohMenu> mSohMenu;
extern std::shared_ptr<AnchorRoomWindow> mAnchorRoomWindow;
} // namespace SohGui
static const char* pvpModes[3] = { "Off", "On", "On + Friendly Fire" };
static std::vector<const char*> teleportModes = { "None", "Team Only", "All" };
static std::vector<const char*> 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<std::string> 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

View File

@@ -0,0 +1,71 @@
#include "soh/Network/Anchor/Anchor.h"
#include "soh/Network/Anchor/JsonConversions.hpp"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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<AnchorClient> newClients = payload["state"].get<std::vector<AnchorClient>>();
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<uint32_t> 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();
}

View File

@@ -0,0 +1,65 @@
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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<uint32_t>();
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>();
u8 damage = payload["damage"].get<u8>();
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);
}

View File

@@ -0,0 +1,14 @@
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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();
}

View File

@@ -0,0 +1,33 @@
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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<u16>();
Entrance_SetEntranceDiscovered(entranceIndex, 1);
}

View File

@@ -0,0 +1,42 @@
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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<uint32_t>();
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),
});
}

View File

@@ -0,0 +1,108 @@
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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<uint32_t>();
AnchorClient& client = clients[clientId];
u16 modId = payload["modId"].get<u16>();
u16 getItemId = payload["getItemId"].get<u16>();
GetItemEntry getItemEntry;
if (modId == MOD_NONE) {
getItemEntry = ItemTableManager::Instance->RetrieveItemEntry(MOD_NONE, getItemId);
} else {
getItemEntry = Rando::StaticData::RetrieveItem(static_cast<RandomizerGet>(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,
});
}
}
}

View File

@@ -0,0 +1,22 @@
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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);
}

View File

@@ -0,0 +1,47 @@
#include "soh/Network/Anchor/Anchor.h"
#include "soh/Network/Anchor/JsonConversions.hpp"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
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<uint32_t>();
u16 sfxId = payload["sfxId"].get<u16>();
if (!clients.contains(clientId) || !clients[clientId].player) {
return;
}
Player_PlaySfx((Actor*)clients[clientId].player, sfxId);
}

View File

@@ -0,0 +1,111 @@
#include "soh/Network/Anchor/Anchor.h"
#include "soh/Network/Anchor/JsonConversions.hpp"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
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<int> 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<uint32_t>();
if (clients.contains(clientId)) {
auto& client = clients[clientId];
if (client.linkAge != payload["linkAge"].get<s32>()) {
shouldRefreshActors = true;
}
client.sceneNum = payload["sceneNum"].get<s16>();
client.entranceIndex = payload["entranceIndex"].get<s32>();
client.linkAge = payload["linkAge"].get<s32>();
client.posRot = payload["posRot"].get<PosRot>();
std::vector<int> 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<Vec3s>();
client.currentBoots = payload["currentBoots"].get<s8>();
client.currentShield = payload["currentShield"].get<s8>();
client.currentTunic = payload["currentTunic"].get<s8>();
client.stateFlags1 = payload["stateFlags1"].get<u32>();
client.stateFlags2 = payload["stateFlags2"].get<u32>();
client.buttonItem0 = payload["buttonItem0"].get<u8>();
client.itemAction = payload["itemAction"].get<s8>();
client.heldItemAction = payload["heldItemAction"].get<s8>();
client.modelGroup = payload["modelGroup"].get<u8>();
client.invincibilityTimer = payload["invincibilityTimer"].get<s8>();
client.unk_862 = payload["unk_862"].get<s16>();
client.actionVar1 = payload["actionVar1"].get<s8>();
}
}

View File

@@ -0,0 +1,37 @@
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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();
}

View File

@@ -0,0 +1,91 @@
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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<uint32_t>();
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;
}

View File

@@ -0,0 +1,17 @@
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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<std::string>(),
});
}

View File

@@ -0,0 +1,51 @@
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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<RandomizerCheck>();
RandomizerCheckStatus status = payload["status"].get<RandomizerCheckStatus>();
bool skipped = payload["skipped"].get<bool>();
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();
}

View File

@@ -0,0 +1,73 @@
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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>();
s16 flagType = payload["flagType"].get<s16>();
s16 flag = payload["flag"].get<s16>();
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();
}
}

View File

@@ -0,0 +1,59 @@
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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<s32>();
s8 roomIndex = payload["roomIndex"].get<s8>();
PosRot posRot = payload["posRot"].get<PosRot>();
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<GameInteractor::OnVanillaBehavior>(hookId);
});
}

View File

@@ -0,0 +1,109 @@
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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>();
s16 flagType = payload["flagType"].get<s16>();
s16 flag = payload["flag"].get<s16>();
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();
}
}

View File

@@ -0,0 +1,39 @@
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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<s8>();
BEANS_BOUGHT = payload["amountBought"].get<s8>();
}

View File

@@ -0,0 +1,73 @@
#include "soh/Network/Anchor/Anchor.h"
#include "soh/Network/Anchor/JsonConversions.hpp"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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<uint32_t>();
if (clients.contains(clientId)) {
AnchorClient client = payload["state"].get<AnchorClient>();
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;
}
}

View File

@@ -0,0 +1,38 @@
#include "soh/Network/Anchor/Anchor.h"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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<u16>();
gSaveContext.inventory.dungeonItems[mapIndex] = payload["dungeonItems"].get<u8>();
gSaveContext.inventory.dungeonKeys[mapIndex] = payload["dungeonKeys"].get<s8>();
}

View File

@@ -0,0 +1,55 @@
#include "soh/Network/Anchor/Anchor.h"
#include "soh/Network/Anchor/JsonConversions.hpp"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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<uint32_t>();
roomState.pvpMode = payload["state"]["pvpMode"].get<u8>();
roomState.showLocationsMode = payload["state"]["showLocationsMode"].get<u8>();
roomState.teleportMode = payload["state"]["teleportMode"].get<u8>();
roomState.syncItemsAndFlags = payload["state"]["syncItemsAndFlags"].get<u8>();
}

View File

@@ -0,0 +1,300 @@
#include "soh/Network/Anchor/Anchor.h"
#include "soh/Network/Anchor/JsonConversions.hpp"
#include <nlohmann/json.hpp>
#include <libultraship/libultraship.h>
#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<SaveContext>();
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<RandomizerGet>();
OTRGlobals::Instance->gRandoContext->GetItemLocation(i)->SetCheckStatus(
payload["state"]["rando"]["itemLocations"][i][0].get<RandomizerCheckStatus>());
OTRGlobals::Instance->gRandoContext->GetItemLocation(i)->SetIsSkipped(
payload["state"]["rando"]["itemLocations"][i][0].get<u8>());
// if (payload["state"]["rando"]["itemLocations"][i].contains("fakeRgID")) {
// randoContext->overrides.emplace(static_cast<RandomizerCheck>(i),
// Rando::ItemOverride(static_cast<RandomizerCheck>(i),
// payload["state"]["rando"]["itemLocations"][i]["fakeRgID"].get<RandomizerGet>()));
// randoContext->GetItemOverride(i).GetTrickName().english =
// payload["state"]["rando"]["itemLocations"][i]["trickName"]["english"].get<std::string>();
// randoContext->GetItemOverride(i).GetTrickName().french =
// payload["state"]["rando"]["itemLocations"][i]["trickName"]["french"].get<std::string>();
// }
// if (payload["state"]["rando"]["itemLocations"][i].contains("price")) {
// u16 price = payload["state"]["rando"]["itemLocations"][i]["price"].get<u16>();
// 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<u16>(); entranceCtx->entranceOverrides[i].index
// = payload["state"]["rando"]["entrances"][i]["index"].get<s16>();
// entranceCtx->entranceOverrides[i].destination =
// payload["state"]["rando"]["entrances"][i]["destination"].get<s16>();
// entranceCtx->entranceOverrides[i].override =
// payload["state"]["rando"]["entrances"][i]["override"].get<s16>();
// entranceCtx->entranceOverrides[i].overrideDestination =
// payload["state"]["rando"]["entrances"][i]["overrideDestination"].get<s16>();
// }
// for (int i = 0; i < randoContext->hashIconIndexes.size(); i++) {
// randoContext->hashIconIndexes[i] = payload["state"]["rando"]["seed"][i].get<u8>();
// }
// randoContext->GetSettings()->SetSeedString(payload["state"]["rando"]["inputSeed"].get<std::string>());
// randoContext->GetSettings()->SetSeed(payload["state"]["rando"]["finalSeed"].get<u32>());
// for (int i = 0; i < RSK_MAX; i++) {
// randoContext->GetOption(RandomizerSettingKey(i)).SetSelectedIndex(payload["state"]["rando"]["randoSettings"][i].get<u8>());
// }
// randoContext->GetDungeons()->ClearAllMQ();
// for (int i = 0; i < randoContext->GetDungeons()->GetDungeonListSize(); i++) {
// if (payload["state"]["rando"]["masterQuestDungeons"][i].get<bool>()) {
// randoContext->GetDungeon(i)->SetMQ();
// }
// }
// randoContext->GetTrials()->SkipAll();
// for (int i = 0; i < randoContext->GetTrials()->GetTrialListSize(); i++) {
// if (payload["state"]["rando"]["requiredTrials"][i].get<bool>()) {
// 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<std::string>());
incomingPacketQueue.push(itemPayload);
}
}
isHandlingUpdateTeamState = false;
}

View File

@@ -1,5 +1,3 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "CrowdControl.h"
#include "CrowdControlTypes.h"
#include <libultraship/bridge.h>
@@ -629,4 +627,3 @@ CrowdControl::Effect* CrowdControl::ParseMessage(nlohmann::json dataReceived) {
return effect;
}
#endif

View File

@@ -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

View File

@@ -1,5 +1,3 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "Network.h"
#include <spdlog/spdlog.h>
#include <libultraship/libultraship.h>
@@ -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

View File

@@ -1,16 +1,19 @@
#ifdef ENABLE_REMOTE_CONTROL
#ifndef NETWORK_H
#define NETWORK_H
#ifdef __cplusplus
#include <thread>
#ifdef ENABLE_REMOTE_CONTROL
#include <SDL2/SDL_net.h>
#endif
#include <nlohmann/json.hpp>
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

View File

@@ -1,5 +1,3 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "Sail.h"
#include <libultraship/bridge.h>
#include <libultraship/libultraship.h>
@@ -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<GameInteractor::OnTransitionEnd>(onTransitionEndHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnLoadGame>(onLoadGameHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnExitGame>(onExitGameHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnItemReceive>(onItemReceiveHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnEnemyDefeat>(onEnemyDefeatHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorInit>(onActorInitHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnFlagSet>(onFlagSetHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnFlagUnset>(onFlagUnsetHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneFlagSet>(onSceneFlagSetHook);
GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneFlagUnset>(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<GameInteractor::OnTransitionEnd>([&](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<GameInteractor::OnLoadGame>([&](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<GameInteractor::OnExitGame>([&](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<GameInteractor::OnItemReceive>([&](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<GameInteractor::OnEnemyDefeat>([&](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<GameInteractor::OnActorInit>([&](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<GameInteractor::OnFlagSet>([&](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<GameInteractor::OnFlagUnset>([&](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<GameInteractor::OnSceneFlagSet>(
[&](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<GameInteractor::OnSceneFlagUnset>(
[&](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

View File

@@ -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

View File

@@ -12,6 +12,7 @@
#include <fast/resource/type/DisplayList.h>
#include <ship/window/Window.h>
#include <soh/GameVersions.h>
#include <spdlog/sinks/rotating_file_sink.h>
#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<std::shared_ptr<std::string>> 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<spdlog::level::level_enum>(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<SohInputEditorWindow>(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

View File

@@ -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<GameInteractor::OnSaveFile>(fileNum);
GameInteractor::Instance->ExecuteHooks<GameInteractor::OnSaveFile>(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<int>()) {
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<GameInteractor::OnLoadFile>(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);

View File

@@ -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();

View File

@@ -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<RandomizerSettingsWindow> mRandomizerSettingsWindow;
std::shared_ptr<SohModalWindow> mModalWindow;
std::shared_ptr<Notification::Window> mNotificationWindow;
std::shared_ptr<TimeDisplayWindow> mTimeDisplayWindow;
std::shared_ptr<AnchorRoomWindow> mAnchorRoomWindow;
UIWidgets::Colors GetMenuThemeColor() {
return mSohMenu->GetMenuThemeColor();
@@ -214,6 +216,8 @@ void SetupGuiElements() {
mNotificationWindow->Show();
mTimeDisplayWindow = std::make_shared<TimeDisplayWindow>(CVAR_WINDOW("TimeDisplayEnabled"), "Additional Timers");
gui->AddGuiWindow(mTimeDisplayWindow);
mAnchorRoomWindow = std::make_shared<AnchorRoomWindow>(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,

View File

@@ -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();

View File

@@ -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"

View File

@@ -11,6 +11,12 @@ static const std::unordered_map<int32_t, const char*> logLevels = {
{ DEBUG_LOG_OFF, "Off" },
};
#ifdef _DEBUG
DebugLogOption defaultLogLevel = DEBUG_LOG_TRACE;
#else
DebugLogOption defaultLogLevel = DEBUG_LOG_INFO;
#endif
static const std::unordered_map<int32_t, const char*> 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; });

View File

@@ -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)

View File

@@ -1,4 +1,3 @@
#ifdef ENABLE_REMOTE_CONTROL
#include "SohMenu.h"
#include <soh/Notification/Notification.h>
#include <soh/Network/Network.h>
@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -120,6 +120,7 @@ std::vector<std::string> sceneNames = {
"Castle Hedge Maze (Early)",
"Sasa Test",
"Treasure Chest Room",
"Unknown",
};
std::vector<std::string> itemNamesEng = {

View File

@@ -2,6 +2,8 @@
#include <string>
#include <stdint.h>
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);

Some files were not shown because too many files have changed in this diff Show More