From 9af262c1bb950eb4b471ffa4412dc84be5f4beb6 Mon Sep 17 00:00:00 2001 From: Jordyn Hardyman <42100286+Jameriquiah@users.noreply.github.com> Date: Sat, 3 Jan 2026 13:07:19 -0500 Subject: [PATCH] Disable Fixed Camera Enhancement (#6083) Co-authored-by: PurpleHato --- .../Graphics/DisableFixedCamera.cpp | 229 ++++++++++++++++++ .../GameInteractor_HookTable.h | 1 + .../game-interactor/GameInteractor_Hooks.cpp | 4 + .../game-interactor/GameInteractor_Hooks.h | 1 + soh/soh/SohGui/SohMenuEnhancements.cpp | 16 ++ soh/src/code/z_play.c | 3 +- 6 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 soh/soh/Enhancements/Graphics/DisableFixedCamera.cpp diff --git a/soh/soh/Enhancements/Graphics/DisableFixedCamera.cpp b/soh/soh/Enhancements/Graphics/DisableFixedCamera.cpp new file mode 100644 index 000000000..dc6f47dfa --- /dev/null +++ b/soh/soh/Enhancements/Graphics/DisableFixedCamera.cpp @@ -0,0 +1,229 @@ +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ShipInit.hpp" +#include +#include + +extern "C" { +#include "functions.h" +#include "variables.h" +#include "z64bgcheck.h" +} + +#define CVAR_DISABLE_FIXED_CAMERA_NAME CVAR_ENHANCEMENT("DisableFixedCamera") +#define CVAR_DISABLE_FIXED_CAMERA_VALUE CVarGetInteger(CVAR_DISABLE_FIXED_CAMERA_NAME, 0) +static const std::set fixedCameraSceneList = { + SCENE_MARKET_ENTRANCE_DAY, + SCENE_MARKET_ENTRANCE_NIGHT, + SCENE_MARKET_ENTRANCE_RUINS, + SCENE_BACK_ALLEY_DAY, + SCENE_BACK_ALLEY_NIGHT, + SCENE_MARKET_DAY, + SCENE_MARKET_NIGHT, + SCENE_MARKET_RUINS, + SCENE_CASTLE_COURTYARD_ZELDA, + SCENE_TEMPLE_OF_TIME_EXTERIOR_DAY, + SCENE_TEMPLE_OF_TIME_EXTERIOR_NIGHT, + SCENE_TEMPLE_OF_TIME_EXTERIOR_RUINS, + SCENE_FOREST_TEMPLE, + SCENE_KNOW_IT_ALL_BROS_HOUSE, + SCENE_TWINS_HOUSE, + SCENE_MIDOS_HOUSE, + SCENE_SARIAS_HOUSE, + SCENE_BACK_ALLEY_HOUSE, + SCENE_POTION_SHOP_GRANNY, + SCENE_SHOOTING_GALLERY, + SCENE_LINKS_HOUSE, + SCENE_DOG_LADY_HOUSE, + SCENE_STABLE, + SCENE_IMPAS_HOUSE, + SCENE_KAKARIKO_CENTER_GUEST_HOUSE, + SCENE_CARPENTERS_TENT, + SCENE_GRAVEKEEPERS_HUT, +}; + +static int16_t sSetNormalCam = -1; +static bool sIsCamApplied = false; +static int sCheckItemCamState = -1; +static s16 sStoreLastCamType = -1; + +extern "C" void DisableFixedCamera_CheckCameraState(PlayState* play); + +struct CamDataBackup { + CamData* original = nullptr; + CamData* copy = nullptr; + size_t len = 0; + bool active = false; +}; + +static std::unordered_map sCamDataBackups; + +static void DisableFixedCamera_ResetState() { + sSetNormalCam = -1; + sIsCamApplied = false; + sCheckItemCamState = -1; + sStoreLastCamType = -1; +} + +static void DisableFixedCamera_RestoreAllCameraData() { + for (auto& [colHeader, backup] : sCamDataBackups) { + if (colHeader != nullptr && backup.active) { + colHeader->cameraDataList = backup.original; + backup.active = false; + } + delete[] backup.copy; + backup.copy = nullptr; + backup.original = nullptr; + backup.len = 0; + } + sCamDataBackups.clear(); +} + +// Helper to check if a camera type is a fixed camera +static bool IsFixedCameraType(s16 type) { + return type == CAM_SET_PREREND_FIXED || type == CAM_SET_PREREND_PIVOT || type == CAM_SET_PIVOT_FROM_SIDE; +} + +static void RegisterDisableFixedCamera() { + const bool disableFixedCamEnabled = CVAR_DISABLE_FIXED_CAMERA_VALUE != 0; + + COND_HOOK(OnCameraState, disableFixedCamEnabled, + [](PlayState* play) { DisableFixedCamera_CheckCameraState(play); }); + + if (!disableFixedCamEnabled) { + DisableFixedCamera_RestoreAllCameraData(); + DisableFixedCamera_ResetState(); + } +} + +static RegisterShipInitFunc initFunc(RegisterDisableFixedCamera, { CVAR_DISABLE_FIXED_CAMERA_NAME }); + +static void DisableFixedCamera_RestoreCameraData(CollisionHeader* colHeader) { + if (colHeader == nullptr) { + return; + } + + auto it = sCamDataBackups.find(colHeader); + if (it == sCamDataBackups.end() || !it->second.active) { + return; + } + + colHeader->cameraDataList = it->second.original; + it->second.active = false; +} + +extern "C" void DisableFixedCamera_SetNormalCamera(PlayState* play) { + CollisionHeader* colHeader = BgCheck_GetCollisionHeader(&play->colCtx, BGCHECK_SCENE); + if (colHeader != nullptr && colHeader->cameraDataList != nullptr && colHeader->cameraDataListLen > 0) { + CamDataBackup& backup = sCamDataBackups[colHeader]; + if (backup.copy == nullptr) { + backup.original = colHeader->cameraDataList; + backup.len = colHeader->cameraDataListLen; + backup.copy = new CamData[backup.len]; + memcpy(backup.copy, backup.original, sizeof(CamData) * backup.len); + } + if (colHeader->cameraDataList != backup.copy) { + colHeader->cameraDataList = backup.copy; + } + backup.active = true; + + for (size_t i = 0; i < colHeader->cameraDataListLen; i++) { + if (IsFixedCameraType(colHeader->cameraDataList[i].cameraSType)) { + colHeader->cameraDataList[i].cameraSType = CAM_SET_NORMAL0; + } + } + } + play->unk_1242B = 0; + if (IsFixedCameraType(play->mainCamera.setting)) { + play->mainCamera.setting = CAM_SET_NORMAL0; + play->mainCamera.prevSetting = CAM_SET_NORMAL0; + } + Camera_ChangeSetting(&play->mainCamera, CAM_SET_NORMAL0); + Camera_ChangeMode(&play->mainCamera, CAM_MODE_NORMAL); +} + +extern "C" void DisableFixedCamera_CheckCameraState(PlayState* play) { + const bool disableFixedCamEnabled = CVarGetInteger(CVAR_DISABLE_FIXED_CAMERA_NAME, 0) != 0; + if (!disableFixedCamEnabled) { + CollisionHeader* colHeader = BgCheck_GetCollisionHeader(&play->colCtx, BGCHECK_SCENE); + DisableFixedCamera_RestoreCameraData(colHeader); + return; + } + // prevents normal cam from taking effect during open cutscene to avoid crash + if (play->sceneNum == SCENE_LINKS_HOUSE && gSaveContext.cutsceneIndex == 0xFFF1) { + return; + } + // Only compute player state if we're in a relevant scene + const bool isInFixedCameraScene = fixedCameraSceneList.contains(static_cast(play->sceneNum)); + if (!isInFixedCameraScene) { + bool sceneChanged = play->sceneNum != sSetNormalCam; + if (sceneChanged) { + sSetNormalCam = play->sceneNum; + sIsCamApplied = false; + sStoreLastCamType = -1; + // Clean up backups when leaving fixed camera scenes + for (auto& [key, backup] : sCamDataBackups) { + delete[] backup.copy; + } + sCamDataBackups.clear(); + } + DisableFixedCamera_RestoreCameraData(BgCheck_GetCollisionHeader(&play->colCtx, BGCHECK_SCENE)); + return; + } + + bool sceneChanged = play->sceneNum != sSetNormalCam; + bool itemCamChanged = false; + + if (sceneChanged) { + sSetNormalCam = play->sceneNum; + sIsCamApplied = false; + sStoreLastCamType = -1; + } + + Player* player = (Player*)play->actorCtx.actorLists[ACTORCAT_PLAYER].head; + + bool ocarinaPulling = player && (player->stateFlags2 & PLAYER_STATE2_OCARINA_PLAYING); + bool bottleUsing = false; + if (player) { + bool inItemCs = (player->stateFlags1 & PLAYER_STATE1_IN_ITEM_CS) != 0; + bool isBottleAction = + (player->itemAction >= PLAYER_IA_BOTTLE) && (player->itemAction <= PLAYER_IA_BOTTLE_FAIRY); + bottleUsing = inItemCs && isBottleAction; + } + bool itemCamActive = ocarinaPulling || bottleUsing; + + if (sCheckItemCamState == -1) { + sCheckItemCamState = itemCamActive; + } else if (sCheckItemCamState != static_cast(itemCamActive)) { + sCheckItemCamState = itemCamActive; + itemCamChanged = true; + } + + if (!sceneChanged && !itemCamChanged) { + return; + } + + // sets cam when ocarina or bottle is used and sets it back to normal when done + if (itemCamChanged && itemCamActive) { + if (play->mainCamera.camDataIdx >= 0) { + sStoreLastCamType = play->mainCamera.camDataIdx; + } + Camera_ChangeSetting(&play->mainCamera, CAM_SET_TURN_AROUND); + Camera_ChangeMode(&play->mainCamera, CAM_MODE_NORMAL); + if (sStoreLastCamType >= 0) { + play->mainCamera.camDataIdx = sStoreLastCamType; + } + return; + } + if (itemCamChanged && !itemCamActive) { + DisableFixedCamera_SetNormalCamera(play); + if (sStoreLastCamType >= 0) { + play->mainCamera.camDataIdx = sStoreLastCamType; + } + return; + } + + if (!sIsCamApplied) { + DisableFixedCamera_SetNormalCamera(play); + sIsCamApplied = true; + } +} diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h index d5fa7221e..58f3a2763 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h @@ -11,6 +11,7 @@ DEFINE_HOOK(OnLoadGame, (int32_t fileNum)); DEFINE_HOOK(OnExitGame, (int32_t fileNum)); DEFINE_HOOK(OnGameStateMainStart, ()); DEFINE_HOOK(OnGameFrameUpdate, ()); +DEFINE_HOOK(OnCameraState, (PlayState * play)); DEFINE_HOOK(OnItemReceive, (GetItemEntry itemEntry)); DEFINE_HOOK(OnEquipmentDelete, (int16_t equipmentType, uint16_t equipValue)); DEFINE_HOOK(OnSaleEnd, (GetItemEntry itemEntry)); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp index e3e570fe4..8bac2f225 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp @@ -29,6 +29,10 @@ void GameInteractor_ExecuteOnGameFrameUpdate() { GameInteractor::Instance->ExecuteHooks(); } +void GameInteractor_ExecuteOnCameraState(PlayState* play) { + GameInteractor::Instance->ExecuteHooks(play); +} + void GameInteractor_ExecuteOnItemReceiveHooks(GetItemEntry itemEntry) { GameInteractor::Instance->ExecuteHooks(itemEntry); GameInteractor::Instance->ExecuteHooksForFilter(itemEntry); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h index 5aafd93de..3ead54c4b 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h @@ -14,6 +14,7 @@ void GameInteractor_ExecuteOnLoadGame(int32_t fileNum); void GameInteractor_ExecuteOnExitGame(int32_t fileNum); void GameInteractor_ExecuteOnGameStateMainStart(); void GameInteractor_ExecuteOnGameFrameUpdate(); +void GameInteractor_ExecuteOnCameraState(PlayState* play); void GameInteractor_ExecuteOnItemReceiveHooks(GetItemEntry itemEntry); void GameInteractor_ExecuteOnEquipmentDelete(int16_t equipmentType, uint16_t equipValue); void GameInteractor_ExecuteOnSaleEndHooks(GetItemEntry itemEntry); diff --git a/soh/soh/SohGui/SohMenuEnhancements.cpp b/soh/soh/SohGui/SohMenuEnhancements.cpp index 1136a656c..f0ac0d5b0 100644 --- a/soh/soh/SohGui/SohMenuEnhancements.cpp +++ b/soh/soh/SohGui/SohMenuEnhancements.cpp @@ -560,6 +560,22 @@ void SohMenu::AddMenuEnhancements() { .Options(CheckboxOptions().Tooltip("Disables 2D pre-rendered backgrounds. Enable this when using a mod that " "implements 3D backdrops for these areas.\n" "Requires Scene Change to alter.")); + AddWidget(path, "Disable Fixed Camera", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_ENHANCEMENT("DisableFixedCamera")) + .RaceDisable(false) + .PreFunc([](WidgetInfo& info) { + if (CVarGetInteger(CVAR_ENHANCEMENT("3DSceneRender"), 0) == 0) { + CVarSetInteger(CVAR_ENHANCEMENT("DisableFixedCamera"), 0); + info.options->disabled = true; + } else { + info.options->disabled = false; + } + info.options->disabledTooltip = "Requires \"Disable 2D Pre-Rendered Scenes\" to be enabled."; + }) + .Options(CheckboxOptions().Tooltip( + "Disables the fixed camera in maps that use 2D pre-rendered backgrounds. Enable this when using a mod " + "that implements 3D backdrops for these areas.\n" + "Requires Scene Change to alter.")); AddWidget(path, "Ingame Text Spacing: %d", WIDGET_CVAR_SLIDER_INT) .CVar(CVAR_ENHANCEMENT("TextSpacing")) .RaceDisable(false) diff --git a/soh/src/code/z_play.c b/soh/src/code/z_play.c index 38a040a8a..4890fa2a4 100644 --- a/soh/src/code/z_play.c +++ b/soh/src/code/z_play.c @@ -631,7 +631,6 @@ void Play_Init(GameState* thisx) { } else { play->unk_1242B = 0; } - Interface_SetSceneRestrictions(play); Environment_PlaySceneSequence(play); gSaveContext.seqId = play->sequenceCtx.seqId; @@ -1303,6 +1302,8 @@ void Play_Update(PlayState* play) { skip: PLAY_LOG(3801); + GameInteractor_ExecuteOnCameraState(play); + if (!isPaused || gDbgCamEnabled) { s32 i;