From fb0f7169d78182f31b9ebc60c6265e30e40bc2f4 Mon Sep 17 00:00:00 2001 From: Adam Bird Date: Sat, 16 Dec 2023 20:50:32 -0500 Subject: [PATCH 01/24] fix jabu mq minimap mark points (#3494) --- soh/src/overlays/misc/ovl_map_mark_data/z_map_mark_data.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/soh/src/overlays/misc/ovl_map_mark_data/z_map_mark_data.c b/soh/src/overlays/misc/ovl_map_mark_data/z_map_mark_data.c index a73ca8265..fec1d8033 100644 --- a/soh/src/overlays/misc/ovl_map_mark_data/z_map_mark_data.c +++ b/soh/src/overlays/misc/ovl_map_mark_data/z_map_mark_data.c @@ -1762,6 +1762,13 @@ static MapMarkData sMapMarkJabuJabuBellyMq[] = { } }, { MAP_MARK_NONE, 0, { 0 } }, }, + // Jabu-Jabu's Belly minimap 16 + // SoH [General] - This entry corresponds to Big Octorok's room and is missing in the MQ game + // N64 hardware does an OoB read and lands on MQ Forest Temple room 0 + // To avoid UB with OoB for SoH, the correct entry is now added below + { + { MAP_MARK_NONE, 0, { 0 } }, + }, }; static MapMarkData sMapMarkForestTempleMq[] = { From 35301556d93fc4f3e76267549b08d48e8581e570 Mon Sep 17 00:00:00 2001 From: Malkierian Date: Sat, 16 Dec 2023 18:51:25 -0700 Subject: [PATCH 02/24] Added "Always show gold skulltula" to check tracker options. Toggleable without restart. (#3505) Made "Hide right-side shop checks" toggleable without restart. --- .../randomizer/randomizer_check_tracker.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp index 003c8c135..d9c2424af 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_check_tracker.cpp @@ -78,6 +78,7 @@ bool initialized; bool doAreaScroll; bool previousShowHidden = false; bool hideShopRightChecks = true; +bool alwaysShowGS = false; std::map startingShopItem = { { SCENE_KOKIRI_SHOP, RC_KF_SHOP_ITEM_1 }, { SCENE_BAZAAR, RC_MARKET_BAZAAR_ITEM_1 }, @@ -434,7 +435,6 @@ void CheckTrackerLoadGame(int32_t fileNum) { } else { realRcObj = rcObj; } - if (!IsVisibleInCheckTracker(realRcObj)) continue; checksByArea.find(realRcObj.rcArea)->second.push_back(realRcObj); if (rcTrackerData.status == RCSHOW_SAVED || rcTrackerData.skipped) { @@ -514,6 +514,10 @@ void CheckTrackerTransition(uint32_t sceneNum) { } void CheckTrackerFrame() { + if (IS_RANDO) { + hideShopRightChecks = CVarGetInteger("gCheckTrackerOptionHideRightShopChecks", 1); + alwaysShowGS = CVarGetInteger("gCheckTrackerOptionAlwaysShowGSLocs", 0); + } if (!GameInteractor::IsSaveLoaded()) { return; } @@ -1084,7 +1088,6 @@ void LoadSettings() { showLinksPocket = IS_RANDO ? // don't show Link's Pocket if not randomizer, or if rando and pocket is disabled OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_LINKS_POCKET) != RO_LINKS_POCKET_NOTHING :false; - hideShopRightChecks = IS_RANDO ? CVarGetInteger("gCheckTrackerOptionHideRightShopChecks", 1) : false; if (IS_RANDO) { switch (OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_SHUFFLE_TOKENS)) { @@ -1148,7 +1151,7 @@ bool IsVisibleInCheckTracker(RandomizerCheckObject rcObj) { ) && (rcObj.rcType != RCTYPE_MERCHANT || showMerchants) && (rcObj.rcType != RCTYPE_OCARINA || showOcarinas) && - (rcObj.rcType != RCTYPE_SKULL_TOKEN || + (rcObj.rcType != RCTYPE_SKULL_TOKEN || alwaysShowGS || (showOverworldTokens && RandomizerCheckObjects::AreaIsOverworld(rcObj.rcArea)) || (showDungeonTokens && RandomizerCheckObjects::AreaIsDungeon(rcObj.rcArea)) ) && @@ -1518,7 +1521,9 @@ void CheckTrackerSettingsWindow::DrawElement() { UIWidgets::EnhancementCheckbox("Vanilla/MQ Dungeon Spoilers", "gCheckTrackerOptionMQSpoilers"); UIWidgets::Tooltip("If enabled, Vanilla/MQ dungeons will show on the tracker immediately. Otherwise, Vanilla/MQ dungeon locations must be unlocked."); UIWidgets::EnhancementCheckbox("Hide right-side shop item checks", "gCheckTrackerOptionHideRightShopChecks", false, "", UIWidgets::CheckboxGraphics::Cross, true); - UIWidgets::Tooltip("If enabled, will prevent the tracker from displaying slots 1-4 in all shops. Requires save reload."); + UIWidgets::Tooltip("If enabled, will prevent the tracker from displaying slots 1-4 in all shops."); + UIWidgets::EnhancementCheckbox("Always show gold skulltulas", "gCheckTrackerOptionAlwaysShowGSLocs", false, ""); + UIWidgets::Tooltip("If enabled, will show GS locations in the tracker regardless of tokensanity settings."); ImGui::TableNextColumn(); From 269e9faa46fd86dadff575a26c718b679a759756 Mon Sep 17 00:00:00 2001 From: Adam Bird Date: Sat, 16 Dec 2023 20:51:44 -0500 Subject: [PATCH 03/24] change adult shooting gallery reward and add message (#3506) --- soh/soh/Enhancements/custom-message/CustomMessageTypes.h | 1 + soh/soh/Enhancements/randomizer/randomizer.cpp | 7 +++++++ soh/soh/OTRGlobals.cpp | 2 ++ .../overlays/actors/ovl_En_Syateki_Man/z_en_syateki_man.c | 7 ++++++- 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/custom-message/CustomMessageTypes.h b/soh/soh/Enhancements/custom-message/CustomMessageTypes.h index 916e1a7f8..d93bae983 100644 --- a/soh/soh/Enhancements/custom-message/CustomMessageTypes.h +++ b/soh/soh/Enhancements/custom-message/CustomMessageTypes.h @@ -51,6 +51,7 @@ typedef enum { TEXT_WARP_NOCTURNE_OF_SHADOW = 0x891, TEXT_WARP_PRELUDE_OF_LIGHT = 0x892, TEXT_WARP_RANDOM_REPLACED_TEXT = 0x9200, + TEXT_SHOOTING_GALLERY_MAN_COME_BACK_WITH_BOW = 0x9210, TEXT_LAKE_HYLIA_WATER_SWITCH_SIGN = 0x346, // 0x3yy for cuttable sign range TEXT_LAKE_HYLIA_WATER_SWITCH_NAVI = 0x1B3, // 0x1yy for Navi msg range } TextIDs; diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index 7f2613935..fe1c4f4e2 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -494,6 +494,13 @@ void Randomizer::LoadHintLocations(const char* spoilerFileName) { "Zu {{location}}?\x1B&%gOK&No%w\x02", "Se téléporter vers&{{location}}?\x1B&%gOK!&Non%w\x02")); + // Bow Shooting Gallery reminder + CustomMessageManager::Instance->CreateMessage(Randomizer::hintMessageTableID, TEXT_SHOOTING_GALLERY_MAN_COME_BACK_WITH_BOW, + CustomMessage("Come back when you have your own&bow and you'll get a %rdifferent prize%w!", + "Komm wieder sobald du deinen eigenen&Bogen hast, um einen %rspeziellen Preis%w zu&erhalten!", + "J'aurai %rune autre récompense%w pour toi&lorsque tu auras ton propre arc.")); + + // Lake Hylia water level system CustomMessageManager::Instance->CreateMessage(Randomizer::hintMessageTableID, TEXT_LAKE_HYLIA_WATER_SWITCH_SIGN, CustomMessage("Water level control system.&Keep away!", "Wasserstand Kontrollsystem&Finger weg!", diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 483445965..3e1c8bba2 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -2489,6 +2489,8 @@ extern "C" int CustomMessage_RetrieveIfExists(PlayState* play) { messageEntry = OTRGlobals::Instance->gRandomizer->GetWarpSongMessage(textId, Randomizer_GetSettingValue(RSK_WARP_SONG_HINTS) == RO_GENERIC_OFF); } else if (textId == TEXT_LAKE_HYLIA_WATER_SWITCH_NAVI || textId == TEXT_LAKE_HYLIA_WATER_SWITCH_SIGN) { messageEntry = CustomMessageManager::Instance->RetrieveMessage(Randomizer::hintMessageTableID, textId); + } else if (textId == TEXT_SHOOTING_GALLERY_MAN_COME_BACK_WITH_BOW) { + messageEntry = CustomMessageManager::Instance->RetrieveMessage(Randomizer::hintMessageTableID, TEXT_SHOOTING_GALLERY_MAN_COME_BACK_WITH_BOW); } else if (textId == 0x3052 || (textId >= 0x3069 && textId <= 0x3070)) { //Fire Temple gorons u16 choice = Random(0, NUM_GORON_MESSAGES); messageEntry = OTRGlobals::Instance->gRandomizer->GetGoronMessage(choice); diff --git a/soh/src/overlays/actors/ovl_En_Syateki_Man/z_en_syateki_man.c b/soh/src/overlays/actors/ovl_En_Syateki_Man/z_en_syateki_man.c index 888b0ef34..6ab91ddc0 100644 --- a/soh/src/overlays/actors/ovl_En_Syateki_Man/z_en_syateki_man.c +++ b/soh/src/overlays/actors/ovl_En_Syateki_Man/z_en_syateki_man.c @@ -3,6 +3,7 @@ #include "overlays/actors/ovl_En_Syateki_Itm/z_en_syateki_itm.h" #include "objects/object_ossan/object_ossan.h" #include "soh/Enhancements/randomizer/randomizer_entrance.h" +#include "soh/Enhancements/custom-message/CustomMessageTypes.h" #define FLAGS (ACTOR_FLAG_TARGETABLE | ACTOR_FLAG_FRIENDLY | ACTOR_FLAG_UPDATE_WHILE_CULLED | ACTOR_FLAG_NO_LOCKON) @@ -371,7 +372,8 @@ void EnSyatekiMan_EndGame(EnSyatekiMan* this, PlayState* play) { this->getItemId = GI_RUPEE_PURPLE; } } else { - if(IS_RANDO && !Flags_GetTreasure(play, 0x1F)) { + // Only give the adult rando reward when the player has a quiver + if (IS_RANDO && !Flags_GetTreasure(play, 0x1F) && CUR_UPG_VALUE(UPG_QUIVER) > 0) { this->getItemEntry = Randomizer_GetItemFromKnownCheck(RC_KAK_SHOOTING_GALLERY_REWARD, GI_QUIVER_50); this->getItemId = this->getItemEntry.getItemId; Flags_SetTreasure(play, 0x1F); @@ -448,6 +450,9 @@ void EnSyatekiMan_FinishPrize(EnSyatekiMan* this, PlayState* play) { Flags_SetItemGetInf(ITEMGETINF_0D); } else if ((this->getItemId == GI_QUIVER_40) || (this->getItemId == GI_QUIVER_50)) { Flags_SetItemGetInf(ITEMGETINF_0E); + } else if (IS_RANDO && LINK_IS_ADULT && CUR_UPG_VALUE(UPG_QUIVER) == 0) { + // In Rando without a quiver, display a message reminding the player to come back with a bow + Message_StartTextbox(play, TEXT_SHOOTING_GALLERY_MAN_COME_BACK_WITH_BOW, NULL); } this->gameResult = SYATEKI_RESULT_NONE; this->actor.parent = this->tempGallery; From f65b711376d28fda6c508f79ff8e943d5cda6961 Mon Sep 17 00:00:00 2001 From: Adam Bird Date: Sat, 16 Dec 2023 20:52:07 -0500 Subject: [PATCH 04/24] fix led crash when set to health (#3507) --- soh/soh/OTRGlobals.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 3e1c8bba2..872cf5130 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -2100,10 +2100,10 @@ Color_RGB8 GetColorForControllerLED() { if (source == LED_SOURCE_CUSTOM) { color = CVarGetColor24("gLedPort1Color", { 255, 255, 255 }); } - if (criticalOverride || source == LED_SOURCE_HEALTH) { + if (gPlayState && (criticalOverride || source == LED_SOURCE_HEALTH)) { if (HealthMeter_IsCritical()) { color = { 0xFF, 0, 0 }; - } else if (source == LED_SOURCE_HEALTH) { + } else if (gSaveContext.healthCapacity != 0 && source == LED_SOURCE_HEALTH) { if (gSaveContext.health / gSaveContext.healthCapacity <= 0.4f) { color = { 0xFF, 0xFF, 0 }; } else { From ea49196baeed6ae9f07f3c9fcd61eaa0f6b01801 Mon Sep 17 00:00:00 2001 From: Adam Bird Date: Sat, 16 Dec 2023 20:52:36 -0500 Subject: [PATCH 05/24] retain gameplay stats window size (#3508) --- soh/soh/Enhancements/gameplaystats.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/gameplaystats.cpp b/soh/soh/Enhancements/gameplaystats.cpp index a6e5a47e8..efcf44efb 100644 --- a/soh/soh/Enhancements/gameplaystats.cpp +++ b/soh/soh/Enhancements/gameplaystats.cpp @@ -618,7 +618,7 @@ void DrawGameplayStatsOptionsTab() { } void GameplayStatsWindow::DrawElement() { - ImGui::SetNextWindowSize(ImVec2(480, 550), ImGuiCond_Appearing); + ImGui::SetNextWindowSize(ImVec2(480, 550), ImGuiCond_FirstUseEver); if (!ImGui::Begin("Gameplay Stats", &mIsVisible, ImGuiWindowFlags_NoFocusOnAppearing)) { ImGui::End(); return; From 907b770676d56b4428288191226032e62eba233f Mon Sep 17 00:00:00 2001 From: Adam Bird Date: Sat, 16 Dec 2023 20:56:14 -0500 Subject: [PATCH 06/24] more swordless fixes when time traveling (#3510) --- soh/src/code/z_parameter.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/soh/src/code/z_parameter.c b/soh/src/code/z_parameter.c index 20debfd79..a4d61f877 100644 --- a/soh/src/code/z_parameter.c +++ b/soh/src/code/z_parameter.c @@ -1521,6 +1521,13 @@ void Inventory_SwapAgeEquipment(void) { } } + // In Rando, when switching to adult for the second+ time, if a sword was not previously + // equiped in MS shuffle, then we need to set the swordless flag again + if (IS_RANDO && Randomizer_GetSettingValue(RSK_SHUFFLE_MASTER_SWORD) && + gSaveContext.equips.buttonItems[0] == ITEM_NONE) { + Flags_SetInfTable(INFTABLE_SWORDLESS); + } + gSaveContext.equips.equipment = gSaveContext.adultEquips.equipment; } } else { @@ -1589,6 +1596,13 @@ void Inventory_SwapAgeEquipment(void) { } } + // In Rando, when switching to child from a swordless adult, and child Link previously had a + // sword equiped, then we need to unset the swordless flag to match + if (IS_RANDO && Randomizer_GetSettingValue(RSK_SHUFFLE_MASTER_SWORD) && + gSaveContext.equips.buttonItems[0] != ITEM_NONE) { + Flags_UnsetInfTable(INFTABLE_SWORDLESS); + } + gSaveContext.equips.equipment = gSaveContext.childEquips.equipment; gSaveContext.equips.equipment &= (u16) ~(0xF << (EQUIP_TYPE_SWORD * 4)); gSaveContext.equips.equipment |= EQUIP_VALUE_SWORD_KOKIRI << (EQUIP_TYPE_SWORD * 4); From e2f1cebfb57dc8d6ca4aa275e27222fb425abe5c Mon Sep 17 00:00:00 2001 From: Adam Bird Date: Sat, 16 Dec 2023 20:56:36 -0500 Subject: [PATCH 07/24] fix condition causing enemies to always be small (#3512) --- soh/soh/Enhancements/mods.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/soh/soh/Enhancements/mods.cpp b/soh/soh/Enhancements/mods.cpp index 95503fda9..6692dd5c9 100644 --- a/soh/soh/Enhancements/mods.cpp +++ b/soh/soh/Enhancements/mods.cpp @@ -1038,8 +1038,8 @@ void RegisterRandomizedEnemySizes() { uint8_t excludedEnemy = actor->id == ACTOR_EN_BROB || actor->id == ACTOR_EN_DHA || (actor->id == ACTOR_BOSS_SST && actor->params == -1); // Dodongo, Volvagia and Dead Hand are always smaller because they're impossible when bigger. - uint8_t smallOnlyEnemy = - actor->id == ACTOR_BOSS_DODONGO || actor->id == ACTOR_BOSS_FD || actor->id == ACTOR_BOSS_FD2 || ACTOR_EN_DH; + uint8_t smallOnlyEnemy = actor->id == ACTOR_BOSS_DODONGO || actor->id == ACTOR_BOSS_FD || + actor->id == ACTOR_BOSS_FD2 || actor->id == ACTOR_EN_DH; // Only apply to enemies and bosses. if (!CVarGetInteger("gRandomizedEnemySizes", 0) || (actor->category != ACTORCAT_ENEMY && actor->category != ACTORCAT_BOSS) || excludedEnemy) { From fcf2141266f001f4a1e7e0ece7da497c09c3cdd6 Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Sun, 17 Dec 2023 02:40:09 +0000 Subject: [PATCH 08/24] Fix JSON parsing every frame on file select (#3513) * Fix JSON parsing every frame on file select * string (#73) --------- Co-authored-by: briaguya <70942617+briaguya-ai@users.noreply.github.com> --- .../game-interactor/GameInteractor.h | 2 + .../Enhancements/randomizer/randomizer.cpp | 29 +--- soh/soh/OTRGlobals.cpp | 145 ++++++++++-------- soh/soh/OTRGlobals.h | 1 - .../ovl_file_choose/z_file_choose.c | 14 +- .../ovl_file_choose/z_file_nameset_PAL.c | 1 - 6 files changed, 98 insertions(+), 94 deletions(-) diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor.h b/soh/soh/Enhancements/game-interactor/GameInteractor.h index 809ccda09..4f8ef259b 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor.h @@ -94,6 +94,7 @@ void GameInteractor_SetTriforceHuntCreditsWarpActive(uint8_t state); #include #include +#include #define DEFINE_HOOK(name, type) \ struct name { \ @@ -193,6 +194,7 @@ public: DEFINE_HOOK(OnSetGameLanguage, void()); + DEFINE_HOOK(OnFileDropped, void(std::string filePath)); DEFINE_HOOK(OnAssetAltChange, void()); // Helpers diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index fe1c4f4e2..4b9a84eff 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -364,31 +364,16 @@ std::unordered_map SpoilerfileSettingNameToEn #pragma GCC push_options #pragma GCC optimize ("O0") bool Randomizer::SpoilerFileExists(const char* spoilerFileName) { - try { - if (strcmp(spoilerFileName, "") != 0) { - std::ifstream spoilerFileStream(SohUtils::Sanitize(spoilerFileName)); - if (!spoilerFileStream) { - return false; - } - - json spoilerFileJson; - spoilerFileStream >> spoilerFileJson; - - if (!spoilerFileJson.contains("version") || !spoilerFileJson.contains("finalSeed")) { - return false; - } - + if (strcmp(spoilerFileName, "") != 0) { + std::ifstream spoilerFileStream(SohUtils::Sanitize(spoilerFileName)); + if (!spoilerFileStream) { + return false; + } else { return true; } - - return false; - } catch (std::exception& e) { - SPDLOG_ERROR("Error checking if spoiler file exists: {}", e.what()); - return false; - } catch (...) { - SPDLOG_ERROR("Error checking if spoiler file exists"); - return false; } + + return false; } #pragma GCC pop_options #pragma optimize("", on) diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 872cf5130..a789aa8d9 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -118,6 +118,8 @@ CrowdControl* CrowdControl::Instance; #include "soh/config/ConfigUpdaters.h" +void SoH_ProcessDroppedFiles(std::string filePath); + OTRGlobals* OTRGlobals::Instance; SaveManager* SaveManager::Instance; CustomMessageManager* CustomMessageManager::Instance; @@ -1057,6 +1059,11 @@ extern "C" void InitOTR() { InitMods(); ActorDB::AddBuiltInCustomActors(); + // #region SOH [Randomizer] TODO: Remove these and refactor spoiler file handling for randomizer + CVarClear("gRandomizerNewFileDropped"); + CVarClear("gRandomizerDroppedFile"); + // #endregion + GameInteractor::Instance->RegisterGameHook(SoH_ProcessDroppedFiles); time_t now = time(NULL); tm *tm_now = localtime(&now); @@ -1233,6 +1240,16 @@ extern "C" void Graph_StartFrame() { } } #endif + + if (CVarGetInteger("gNewFileDropped", 0)) { + std::string filePath = SohUtils::Sanitize(CVarGetString("gDroppedFile", "")); + if (!filePath.empty()) { + GameInteractor::Instance->ExecuteHooks(filePath); + } + CVarClear("gNewFileDropped"); + CVarClear("gDroppedFile"); + } + OTRGlobals::Instance->context->GetWindow()->StartFrame(); } @@ -2580,70 +2597,74 @@ extern "C" void Gfx_RegisterBlendedTexture(const char* name, u8* mask, u8* repla gfx_register_blended_texture(name, mask, replacement); } -// #region SOH [TODO] Ideally this should move to being event based, it's currently run every frame on the file select screen -extern "C" void SoH_ProcessDroppedFiles() { - const char* droppedFile = CVarGetString("gDroppedFile", ""); - if (CVarGetInteger("gNewFileDropped", 0) && strcmp(droppedFile, "") != 0) { - try { - std::ifstream configStream(SohUtils::Sanitize(droppedFile)); - if (!configStream) { - return; - } - - nlohmann::json configJson; - configStream >> configJson; - - if (!configJson.contains("CVars")) { - return; - } - - clearCvars(enhancementsCvars); - clearCvars(cheatCvars); - clearCvars(randomizerCvars); - - // Flatten everything under CVars into a single array - auto cvars = configJson["CVars"].flatten(); - - for (auto& [key, value] : cvars.items()) { - // Replace slashes with dots in key, and remove leading dot - std::string path = key; - std::replace(path.begin(), path.end(), '/', '.'); - if (path[0] == '.') { - path.erase(0, 1); - } - if (value.is_string()) { - CVarSetString(path.c_str(), value.get().c_str()); - } else if (value.is_number_integer()) { - CVarSetInteger(path.c_str(), value.get()); - } else if (value.is_number_float()) { - CVarSetFloat(path.c_str(), value.get()); - } - } - - auto gui = LUS::Context::GetInstance()->GetWindow()->GetGui(); - gui->GetGuiWindow("Console")->Hide(); - gui->GetGuiWindow("Actor Viewer")->Hide(); - gui->GetGuiWindow("Collision Viewer")->Hide(); - gui->GetGuiWindow("Save Editor")->Hide(); - gui->GetGuiWindow("Display List Viewer")->Hide(); - gui->GetGuiWindow("Stats")->Hide(); - std::dynamic_pointer_cast(LUS::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console"))->ClearBindings(); - - gui->SaveConsoleVariablesOnNextTick(); - - uint32_t finalHash = boost::hash_32{}(configJson.dump()); - gui->GetGameOverlay()->TextDrawNotification(30.0f, true, "Configuration Loaded. Hash: %d", finalHash); - } catch (std::exception& e) { - SPDLOG_ERROR("Failed to load config file: {}", e.what()); - auto gui = LUS::Context::GetInstance()->GetWindow()->GetGui(); - gui->GetGameOverlay()->TextDrawNotification(30.0f, true, "Failed to load config file"); - return; - } catch (...) { - SPDLOG_ERROR("Failed to load config file"); - auto gui = LUS::Context::GetInstance()->GetWindow()->GetGui(); - gui->GetGameOverlay()->TextDrawNotification(30.0f, true, "Failed to load config file"); +void SoH_ProcessDroppedFiles(std::string filePath) { + try { + std::ifstream configStream(filePath); + if (!configStream) { return; } + + nlohmann::json configJson; + configStream >> configJson; + + // #region SOH [Randomizer] TODO: Refactor spoiler file handling for randomizer + if (configJson.contains("version") && configJson.contains("finalSeed")) { + CVarSetString("gRandomizerDroppedFile", filePath.c_str()); + CVarSetInteger("gRandomizerNewFileDropped", 1); + return; + } + // #endregion + + if (!configJson.contains("CVars")) { + return; + } + + clearCvars(enhancementsCvars); + clearCvars(cheatCvars); + clearCvars(randomizerCvars); + + // Flatten everything under CVars into a single array + auto cvars = configJson["CVars"].flatten(); + + for (auto& [key, value] : cvars.items()) { + // Replace slashes with dots in key, and remove leading dot + std::string path = key; + std::replace(path.begin(), path.end(), '/', '.'); + if (path[0] == '.') { + path.erase(0, 1); + } + if (value.is_string()) { + CVarSetString(path.c_str(), value.get().c_str()); + } else if (value.is_number_integer()) { + CVarSetInteger(path.c_str(), value.get()); + } else if (value.is_number_float()) { + CVarSetFloat(path.c_str(), value.get()); + } + } + + auto gui = LUS::Context::GetInstance()->GetWindow()->GetGui(); + gui->GetGuiWindow("Console")->Hide(); + gui->GetGuiWindow("Actor Viewer")->Hide(); + gui->GetGuiWindow("Collision Viewer")->Hide(); + gui->GetGuiWindow("Save Editor")->Hide(); + gui->GetGuiWindow("Display List Viewer")->Hide(); + gui->GetGuiWindow("Stats")->Hide(); + std::dynamic_pointer_cast(LUS::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console"))->ClearBindings(); + + gui->SaveConsoleVariablesOnNextTick(); + + uint32_t finalHash = boost::hash_32{}(configJson.dump()); + gui->GetGameOverlay()->TextDrawNotification(30.0f, true, "Configuration Loaded. Hash: %d", finalHash); + } catch (std::exception& e) { + SPDLOG_ERROR("Failed to load config file: {}", e.what()); + auto gui = LUS::Context::GetInstance()->GetWindow()->GetGui(); + gui->GetGameOverlay()->TextDrawNotification(30.0f, true, "Failed to load config file"); + return; + } catch (...) { + SPDLOG_ERROR("Failed to load config file"); + auto gui = LUS::Context::GetInstance()->GetWindow()->GetGui(); + gui->GetGameOverlay()->TextDrawNotification(30.0f, true, "Failed to load config file"); + return; } } // #endregion diff --git a/soh/soh/OTRGlobals.h b/soh/soh/OTRGlobals.h index 88c57ced3..9b42e6895 100644 --- a/soh/soh/OTRGlobals.h +++ b/soh/soh/OTRGlobals.h @@ -175,7 +175,6 @@ void EntranceTracker_SetLastEntranceOverride(s16 entranceIndex); void Gfx_RegisterBlendedTexture(const char* name, u8* mask, u8* replacement); void SaveManager_ThreadPoolWait(); void CheckTracker_OnMessageClose(); -void SoH_ProcessDroppedFiles(); int32_t GetGIID(uint32_t itemID); #endif diff --git a/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c b/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c index 9e6150593..bb8dd14d4 100644 --- a/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c +++ b/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c @@ -1030,18 +1030,18 @@ void FileChoose_UpdateRandomizer() { fileSelectSpoilerFileLoaded = false; } - if ((CVarGetInteger("gNewFileDropped", 0) != 0) || (CVarGetInteger("gNewSeedGenerated", 0) != 0) || + if ((CVarGetInteger("gRandomizerNewFileDropped", 0) != 0) || (CVarGetInteger("gNewSeedGenerated", 0) != 0) || (!fileSelectSpoilerFileLoaded && SpoilerFileExists(CVarGetString("gSpoilerLog", "")))) { - if (CVarGetInteger("gNewFileDropped", 0) != 0) { - CVarSetString("gSpoilerLog", CVarGetString("gDroppedFile", "None")); + if (CVarGetInteger("gRandomizerNewFileDropped", 0) != 0) { + CVarSetString("gSpoilerLog", CVarGetString("gRandomizerDroppedFile", "None")); } bool silent = true; - if ((CVarGetInteger("gNewFileDropped", 0) != 0) || (CVarGetInteger("gNewSeedGenerated", 0) != 0)) { + if ((CVarGetInteger("gRandomizerNewFileDropped", 0) != 0) || (CVarGetInteger("gNewSeedGenerated", 0) != 0)) { silent = false; } CVarSetInteger("gNewSeedGenerated", 0); - CVarSetInteger("gNewFileDropped", 0); - CVarSetString("gDroppedFile", ""); + CVarSetInteger("gRandomizerNewFileDropped", 0); + CVarSetString("gRandomizerDroppedFile", ""); fileSelectSpoilerFileLoaded = false; const char* fileLoc = CVarGetString("gSpoilerLog", ""); Randomizer_LoadSettings(fileLoc); @@ -1076,7 +1076,6 @@ void FileChoose_UpdateMainMenu(GameState* thisx) { Input* input = &this->state.input[0]; bool dpad = CVarGetInteger("gDpadText", 0); - SoH_ProcessDroppedFiles(); FileChoose_UpdateRandomizer(); if (CHECK_BTN_ALL(input->press.button, BTN_START) || CHECK_BTN_ALL(input->press.button, BTN_A)) { @@ -1267,7 +1266,6 @@ void FileChoose_UpdateQuestMenu(GameState* thisx) { s8 i = 0; bool dpad = CVarGetInteger("gDpadText", 0); - SoH_ProcessDroppedFiles(); FileChoose_UpdateRandomizer(); if (ABS(this->stickRelX) > 30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DLEFT | BTN_DRIGHT))) { diff --git a/soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_PAL.c b/soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_PAL.c index 377c23eb3..5c5cb3e6d 100644 --- a/soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_PAL.c +++ b/soh/src/overlays/gamestates/ovl_file_choose/z_file_nameset_PAL.c @@ -456,7 +456,6 @@ void FileChoose_DrawNameEntry(GameState* thisx) { this->prevConfigMode = CM_MAIN_MENU; this->configMode = CM_NAME_ENTRY_TO_MAIN; CVarSetInteger("gOnFileSelectNameEntry", 0); - CVarSetInteger("gNewFileDropped", 0); this->nameBoxAlpha[this->buttonIndex] = this->nameAlpha[this->buttonIndex] = 200; this->connectorAlpha[this->buttonIndex] = 255; func_800AA000(300.0f, 0xB4, 0x14, 0x64); From a6b4e0b7fd19c875bd3f6df1638bdc2379566b7f Mon Sep 17 00:00:00 2001 From: Adam Bird Date: Sun, 17 Dec 2023 11:06:01 -0500 Subject: [PATCH 09/24] Fix entrance tracker crash (#3502) --- soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.cpp index 82ad8d47d..c4af7dd6d 100644 --- a/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer_entrance_tracker.cpp @@ -302,7 +302,7 @@ const EntranceData entranceData[] = { // Gerudo Area { ENTR_HYRULE_FIELD_5, ENTR_GERUDO_VALLEY_0, SINGLE_SCENE_INFO(SCENE_GERUDO_VALLEY), "GV", "Hyrule Field", ENTRANCE_GROUP_GERUDO_VALLEY, ENTRANCE_GROUP_HYRULE_FIELD, ENTRANCE_TYPE_OVERWORLD, "hf"}, { ENTR_GERUDOS_FORTRESS_0, ENTR_GERUDO_VALLEY_3, SINGLE_SCENE_INFO(SCENE_GERUDO_VALLEY), "GV", "GF", ENTRANCE_GROUP_GERUDO_VALLEY, ENTRANCE_GROUP_GERUDO_VALLEY, ENTRANCE_TYPE_OVERWORLD, "gerudo fortress"}, - { ENTR_GERUDOS_FORTRESS_0_5, -1, SINGLE_SCENE_INFO(SCENE_GERUDO_VALLEY), "GV", "Lake Hylia", ENTRANCE_GROUP_GERUDO_VALLEY, ENTRANCE_GROUP_LAKE_HYLIA, ENTRANCE_TYPE_OVERWORLD, "lh"}, + { ENTR_LAKE_HYLIA_1, -1, SINGLE_SCENE_INFO(SCENE_GERUDO_VALLEY), "GV", "Lake Hylia", ENTRANCE_GROUP_GERUDO_VALLEY, ENTRANCE_GROUP_LAKE_HYLIA, ENTRANCE_TYPE_OVERWORLD, "lh"}, { ENTR_CARPENTERS_TENT_0, ENTR_GERUDO_VALLEY_4, SINGLE_SCENE_INFO(SCENE_GERUDO_VALLEY), "GV", "Carpenters' Tent", ENTRANCE_GROUP_GERUDO_VALLEY, ENTRANCE_GROUP_GERUDO_VALLEY, ENTRANCE_TYPE_INTERIOR, "", 1}, { ENTRANCE_RANDO_GROTTO_LOAD(GROTTO_GV_OCTOROK_OFFSET), ENTRANCE_RANDO_GROTTO_EXIT(GROTTO_GV_OCTOROK_OFFSET), SINGLE_SCENE_INFO(SCENE_GERUDO_VALLEY), "GV", "GV Octorok Grotto", ENTRANCE_GROUP_GERUDO_VALLEY, ENTRANCE_GROUP_GERUDO_VALLEY, ENTRANCE_TYPE_GROTTO, "", 1}, { ENTRANCE_RANDO_GROTTO_LOAD(GROTTO_GV_STORMS_OFFSET), ENTRANCE_RANDO_GROTTO_EXIT(GROTTO_GV_STORMS_OFFSET), SINGLE_SCENE_INFO(SCENE_GERUDO_VALLEY), "GV", "GV Storms Grotto", ENTRANCE_GROUP_GERUDO_VALLEY, ENTRANCE_GROUP_GERUDO_VALLEY, ENTRANCE_TYPE_GROTTO, "scrubs", 1}, From d370ca93fda88e7bd69858868dfe891ef265ff29 Mon Sep 17 00:00:00 2001 From: Adam Bird Date: Sun, 17 Dec 2023 13:23:07 -0500 Subject: [PATCH 10/24] [Tweak] Improve KD lava effect performance and stability (#3501) * improve kd lava performance and stability * enum typo * account for rock tex size --- .../actors/ovl_Boss_Dodongo/z_boss_dodongo.c | 75 +++++++++++++------ 1 file changed, 53 insertions(+), 22 deletions(-) diff --git a/soh/src/overlays/actors/ovl_Boss_Dodongo/z_boss_dodongo.c b/soh/src/overlays/actors/ovl_Boss_Dodongo/z_boss_dodongo.c index 6a0d49579..deb761641 100644 --- a/soh/src/overlays/actors/ovl_Boss_Dodongo/z_boss_dodongo.c +++ b/soh/src/overlays/actors/ovl_Boss_Dodongo/z_boss_dodongo.c @@ -69,7 +69,7 @@ static u8 sMaskTex16x32[16 * 32] = { { 0 } }; static u8 sMaskTex32x16[32 * 16] = { { 0 } }; static u8 sMaskTex8x8[8 * 8] = { { 0 } }; static u8 sMaskTex8x32[8 * 32] = { { 0 } }; -static u8 sMaskTexLava[32 * 64] = { { 0 } }; +static u8 sMaskTexLava[LAVA_TEX_WIDTH * LAVA_TEX_HEIGHT] = { { 0 } }; static u32* sLavaFloorModifiedTexRaw = NULL; static u32* sLavaWavyTexRaw = NULL; @@ -112,6 +112,20 @@ void BossDodongo_RegisterBlendedLavaTextureUpdate() { u32* lavaTex = ResourceGetDataByName(sLavaFloorLavaTex); size_t lavaSize = ResourceGetSizeByName(sLavaFloorLavaTex); size_t floorSize = ResourceGetSizeByName(gDodongosCavernBossLavaFloorTex); + size_t rockSize = ResourceGetSizeByName(sLavaFloorRockTex); + + // If the sizes don't match, then don't bother with the blended effect to avoid crashing + if (floorSize != lavaSize || floorSize != rockSize) { + uint8_t maskVal = !!Flags_GetClear(gPlayState, gPlayState->roomCtx.curRoom.num); + + if (sMaskTexLava[0] != maskVal) { + for (int i = 0; i < ARRAY_COUNT(sMaskTexLava); i++) { + sMaskTexLava[i] = maskVal; + } + } + Gfx_RegisterBlendedTexture(gDodongosCavernBossLavaFloorTex, sMaskTexLava, NULL); + return; + } sLavaFloorModifiedTexRaw = malloc(lavaSize); sLavaWavyTexRaw = malloc(floorSize); @@ -121,7 +135,6 @@ void BossDodongo_RegisterBlendedLavaTextureUpdate() { // When KD is dead, just immediately copy the rock texture if (Flags_GetClear(gPlayState, gPlayState->roomCtx.curRoom.num)) { u32* rockTex = ResourceGetDataByName(sLavaFloorRockTex); - size_t rockSize = ResourceGetSizeByName(sLavaFloorRockTex); memcpy(sLavaFloorModifiedTexRaw, rockTex, rockSize); } @@ -145,6 +158,13 @@ void BossDodongo_RegisterBlendedLavaTextureUpdate() { Gfx_RegisterBlendedTexture(gDodongosCavernBossLavaFloorTex, sMaskTexLava, sLavaWavyTex); } + // Set all true for the lava as it will always replace the scene texture + if (sMaskTexLava[0] == 0) { + for (int i = 0; i < ARRAY_COUNT(sMaskTexLava); i++) { + sMaskTexLava[i] = 1; + } + } + gfx_texture_cache_clear(); } @@ -170,6 +190,11 @@ void func_808C12C4(u8* arg1, s16 arg2) { // Same as func_808C1554 but works with u32 values for RGBA32 raw textures void func_808C1554_Raw(void* arg0, void* floorTex, s32 arg2, f32 arg3) { + // Raw lava not registered, so abort the wave modification + if (sLavaWavyTexRaw == NULL || sLavaFloorModifiedTexRaw == NULL) { + return; + } + u16 width = ResourceGetTexWidthByName(arg0); s32 size = ResourceGetTexHeightByName(arg0) * width; @@ -203,9 +228,6 @@ void func_808C1554_Raw(void* arg0, void* floorTex, s32 arg2, f32 arg3) { } free(sp54); - - // Need to clear the cache after updating sLavaWavyTexRaw - gfx_texture_cache_clear(); } // Modified to support CPU modified texture with the resource system @@ -233,9 +255,6 @@ void func_808C1554(void* arg0, void* floorTex, s32 arg2, f32 arg3) { temp_s3[i + temp2] = sp54[i + i2]; } } - - // Need to clear the cache after updating sLavaWavyTex - gfx_texture_cache_clear(); } void func_808C17C8(PlayState* play, Vec3f* arg1, Vec3f* arg2, Vec3f* arg3, f32 arg4, s16 arg5) { @@ -325,7 +344,7 @@ void BossDodongo_Init(Actor* thisx, PlayState* play) { this->actor.flags &= ~ACTOR_FLAG_TARGETABLE; // #region SOH [General] - // Init mask values for all blended textures + // Init mask values for all KD blended textures for (int i = 0; i < ARRAY_COUNT(sMaskTex8x16); i++) { sMaskTex8x16[i] = 0; } @@ -341,10 +360,6 @@ void BossDodongo_Init(Actor* thisx, PlayState* play) { for (int i = 0; i < ARRAY_COUNT(sMaskTex32x16); i++) { sMaskTex32x16[i] = 0; } - // Set all true for the lava as it will always replace the scene texture - for (int i = 0; i < ARRAY_COUNT(sMaskTexLava); i++) { - sMaskTexLava[i] = 1; - } // Register all blended textures Gfx_RegisterBlendedTexture(object_kingdodongo_Tex_015890, sMaskTex8x16, NULL); @@ -1174,15 +1189,23 @@ void BossDodongo_Update(Actor* thisx, PlayState* play2) { for (i2 = 0; i2 < 20; i2++) { s16 new_var = this->unk_1C2 & (LAVA_TEX_SIZE - 1); - // Compute the index to a scaled position (scaling pseudo x,y as a 1D value) - s32 indexStart = ((new_var % LAVA_TEX_WIDTH) * widthScale) + ((new_var / LAVA_TEX_WIDTH) * width * heightScale); - // From the starting index, apply extra pixels right/down based on the scale - for (size_t j = 0; j < heightScale; j++) { - for (size_t i3 = 0; i3 < widthScale; i3++) { - s32 scaledIndex = (indexStart + i3 + (j * width)) & (size - 1); - ptr1[scaledIndex] = ptr2[scaledIndex]; + // Raw lava must be registered, otherwise skip the effect for incompatible texture pack + // and instead set the mask to simulate the lava disappearing by turning black + if (sLavaFloorModifiedTexRaw != NULL) { + // Compute the index to a scaled position (scaling pseudo x,y as a 1D value) + s32 indexStart = + ((new_var % LAVA_TEX_WIDTH) * widthScale) + ((new_var / LAVA_TEX_WIDTH) * width * heightScale); + + // From the starting index, apply extra pixels right/down based on the scale + for (size_t j = 0; j < heightScale; j++) { + for (size_t i3 = 0; i3 < widthScale; i3++) { + s32 scaledIndex = (indexStart + i3 + (j * width)) & (size - 1); + ptr1[scaledIndex] = ptr2[scaledIndex]; + } } + } else { + sMaskTexLava[new_var] = 1; } this->unk_1C2 += 37; @@ -1322,8 +1345,16 @@ void BossDodongo_Draw(Actor* thisx, PlayState* play) { gSPInvalidateTexCache(POLY_OPA_DISP++, sMaskTex32x16); } - if (this->unk_1C6 != 0) { - gSPInvalidateTexCache(POLY_OPA_DISP++, sMaskTexLava); + gSPInvalidateTexCache(POLY_OPA_DISP++, sMaskTexLava); + + // Using WORK_DISP to invalidate these textures as they are used in drawing the scene textures which happens + // before actors are drawn. WORK_DISP comes before POLAY_OPA_DISP. It is probably not meant for this, but it + // at least works for now. + // Alternatively, having a way to invalidate just these pointers from the Update func should be sufficient. + if (sLavaFloorModifiedTexRaw != NULL) { + gSPInvalidateTexCache(WORK_DISP++, sLavaWavyTexRaw); + } else { + gSPInvalidateTexCache(WORK_DISP++, sLavaWavyTex); } if ((this->unk_1C0 >= 2) && (this->unk_1C0 & 1)) { From e6fc34e4c234aeafee5cfad166798a9676c1db83 Mon Sep 17 00:00:00 2001 From: briaguya <70942617+briaguya-ai@users.noreply.github.com> Date: Sun, 17 Dec 2023 10:36:38 -0800 Subject: [PATCH 11/24] bump lus to latest 1.x (#3528) --- libultraship | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libultraship b/libultraship index 59427a67b..b4abd7c36 160000 --- a/libultraship +++ b/libultraship @@ -1 +1 @@ -Subproject commit 59427a67bf9af060a4928bb72e3acce3b0782177 +Subproject commit b4abd7c366b1fb38b2cd80ffb91e129035bee0ea From 86044a1c5071c9a0e00699939fa26946aeefbaff Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Sun, 17 Dec 2023 19:41:33 +0000 Subject: [PATCH 12/24] Remote GI Work (#3073) Co-authored-by: David Chavez --- .github/workflows/generate-builds.yml | 17 +- .github/workflows/macports-deps.txt | 2 +- CMakeLists.txt | 6 - soh/CMakeLists.txt | 14 +- .../crowd-control/CrowdControl.cpp | 373 ++++++-------- .../Enhancements/crowd-control/CrowdControl.h | 17 +- soh/soh/Enhancements/debugconsole.cpp | 64 +-- .../game-interactor/GameInteractionEffect.cpp | 4 +- .../game-interactor/GameInteractionEffect.h | 90 ++-- .../game-interactor/GameInteractor.cpp | 2 +- .../game-interactor/GameInteractor.h | 43 +- .../game-interactor/GameInteractor_Remote.cpp | 182 +++++++ .../game-interactor/GameInteractor_Sail.cpp | 471 ++++++++++++++++++ .../game-interactor/GameInteractor_Sail.h | 29 ++ soh/soh/OTRGlobals.cpp | 44 +- soh/soh/SohGui.cpp | 3 +- soh/soh/SohMenuBar.cpp | 150 +++++- 17 files changed, 1155 insertions(+), 356 deletions(-) create mode 100644 soh/soh/Enhancements/game-interactor/GameInteractor_Remote.cpp create mode 100644 soh/soh/Enhancements/game-interactor/GameInteractor_Sail.cpp create mode 100644 soh/soh/Enhancements/game-interactor/GameInteractor_Sail.h diff --git a/.github/workflows/generate-builds.yml b/.github/workflows/generate-builds.yml index f95051515..c54d017a2 100644 --- a/.github/workflows/generate-builds.yml +++ b/.github/workflows/generate-builds.yml @@ -32,17 +32,6 @@ jobs: make -j 10 sudo make install sudo cp -av /usr/local/lib/libSDL* /lib/x86_64-linux-gnu/ - - name: Install latest SDL_net - if: ${{ !vars.LINUX_RUNNER }} - run: | - export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" - wget https://www.libsdl.org/projects/SDL_net/release/SDL2_net-2.2.0.tar.gz - tar -xzf SDL2_net-2.2.0.tar.gz - cd SDL2_net-2.2.0 - ./configure - make -j 10 - sudo make install - sudo cp -av /usr/local/lib/libSDL* /lib/x86_64-linux-gnu/ - name: Generate soh.otr run: | export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" @@ -102,7 +91,7 @@ jobs: - name: Build SoH run: | export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" - cmake --no-warn-unused-cli -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" + cmake --no-warn-unused-cli -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -DBUILD_REMOTE_CONTROL=1 cmake --build build-cmake --config Release --parallel 10 mv soh.otr build-cmake/soh (cd build-cmake && cpack) @@ -171,7 +160,7 @@ jobs: - name: Build SoH run: | export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" - cmake --no-warn-unused-cli -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE:STRING=Release + cmake --no-warn-unused-cli -H. -Bbuild-cmake -GNinja -DCMAKE_BUILD_TYPE:STRING=Release -DBUILD_REMOTE_CONTROL=1 cmake --build build-cmake --config Release -j3 (cd build-cmake && cpack -G External) @@ -297,7 +286,7 @@ jobs: VCPKG_ROOT: ${{github.workspace}}/vcpkg run: | set $env:PATH="$env:USERPROFILE/.cargo/bin;$env:PATH" - cmake -S . -B build-windows -G Ninja -DCMAKE_MAKE_PROGRAM=ninja -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + cmake -S . -B build-windows -G Ninja -DCMAKE_MAKE_PROGRAM=ninja -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DBUILD_REMOTE_CONTROL=1 cmake --build build-windows --config Release --parallel 10 mkdir soh-windows diff --git a/.github/workflows/macports-deps.txt b/.github/workflows/macports-deps.txt index 02c11cc68..6f5948557 100644 --- a/.github/workflows/macports-deps.txt +++ b/.github/workflows/macports-deps.txt @@ -1 +1 @@ -libsdl2 +universal libpng +universal glew +universal +libsdl2 +universal libsdl2_net +universal libpng +universal glew +universal diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ae0360d8..ab4367b8b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,12 +13,6 @@ set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT soh) add_compile_options($<$:/MP>) add_compile_options($<$:/utf-8>) -if (CMAKE_SYSTEM_NAME MATCHES "Windows|Linux") - if(NOT DEFINED BUILD_CROWD_CONTROL) - set(BUILD_CROWD_CONTROL ON) - endif() -endif() - if (CMAKE_SYSTEM_NAME STREQUAL "Windows") include(CMake/automate-vcpkg.cmake) diff --git a/soh/CMakeLists.txt b/soh/CMakeLists.txt index fd0c6ac25..8b46a27f7 100644 --- a/soh/CMakeLists.txt +++ b/soh/CMakeLists.txt @@ -154,7 +154,7 @@ list(FILTER soh__Enhancements EXCLUDE REGEX "soh/Enhancements/gfx.*") # handle crowd control removals list(REMOVE_ITEM soh__Enhancements "soh/Enhancements/crowd-control/soh.cs") list(REMOVE_ITEM soh__Enhancements "soh/Enhancements/crowd-control/soh.ccpak") -if (!BUILD_CROWD_CONTROL) +if (!BUILD_REMOTE_CONTROL) list(FILTER soh__Enhancements EXCLUDE REGEX "soh/Enhancements/crowd-control/*") endif() @@ -354,7 +354,7 @@ endif() find_package(SDL2) set(SDL2-INCLUDE ${SDL2_INCLUDE_DIRS}) -if (BUILD_CROWD_CONTROL) +if (BUILD_REMOTE_CONTROL) find_package(SDL2_net) set(SDL2-NET-INCLUDE ${SDL_NET_INCLUDE_DIRS}) endif() @@ -408,9 +408,9 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") "$<$:" "NDEBUG" ">" - "$<$:ENABLE_CROWD_CONTROL>" + "$<$:ENABLE_REMOTE_CONTROL>" "INCLUDE_GAME_PRINTF;" - "ENABLE_CROWD_CONTROL;" + "ENABLE_REMOTE_CONTROL;" "UNICODE;" "_UNICODE" STORMLIB_NO_AUTO_LINK @@ -455,7 +455,7 @@ elseif ("${CMAKE_CXX_COMPILER_ID}" MATCHES "GNU|Clang|AppleClang") "$<$:" "NDEBUG" ">" - "$<$:ENABLE_CROWD_CONTROL>" + "$<$:ENABLE_REMOTE_CONTROL>" "SPDLOG_ACTIVE_LEVEL=0;" "_CONSOLE;" "_CRT_SECURE_NO_WARNINGS;" @@ -689,7 +689,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") "glu32;" "SDL2::SDL2;" "SDL2::SDL2main;" - "$<$:SDL2_net::SDL2_net-static>" + "$<$:SDL2_net::SDL2_net-static>" "glfw;" "winmm;" "imm32;" @@ -742,7 +742,7 @@ else() "ZAPDUtils;" "ZAPDLib;" SDL2::SDL2 - "$<$:SDL2_net::SDL2_net>" + "$<$:SDL2_net::SDL2_net>" ${CMAKE_DL_LIBS} Threads::Threads ) diff --git a/soh/soh/Enhancements/crowd-control/CrowdControl.cpp b/soh/soh/Enhancements/crowd-control/CrowdControl.cpp index d2e8e03b1..9c0d5cb2d 100644 --- a/soh/soh/Enhancements/crowd-control/CrowdControl.cpp +++ b/soh/soh/Enhancements/crowd-control/CrowdControl.cpp @@ -1,4 +1,4 @@ -#ifdef ENABLE_CROWD_CONTROL +#ifdef ENABLE_REMOTE_CONTROL #include "CrowdControl.h" #include "CrowdControlTypes.h" @@ -17,25 +17,17 @@ extern "C" { extern PlayState* gPlayState; } -void CrowdControl::Init() { - SDLNet_Init(); -} - -void CrowdControl::Shutdown() { - SDLNet_Quit(); -} - void CrowdControl::Enable() { if (isEnabled) { return; } - if (SDLNet_ResolveHost(&ip, "127.0.0.1", 43384) == -1) { - SPDLOG_ERROR("[CrowdControl] SDLNet_ResolveHost: {}", SDLNet_GetError()); - } - isEnabled = true; - ccThreadReceive = std::thread(&CrowdControl::ListenToServer, this); + GameInteractor::Instance->EnableRemoteInteractor(); + GameInteractor::Instance->RegisterRemoteJsonHandler([&](nlohmann::json payload) { + HandleRemoteData(payload); + }); + ccThreadProcess = std::thread(&CrowdControl::ProcessActiveEffects, this); } @@ -45,87 +37,42 @@ void CrowdControl::Disable() { } isEnabled = false; - ccThreadReceive.join(); ccThreadProcess.join(); + GameInteractor::Instance->DisableRemoteInteractor(); } -void CrowdControl::ListenToServer() { - while (isEnabled) { - while (!connected && isEnabled) { - SPDLOG_TRACE("[CrowdControl] Attempting to make connection to server..."); - tcpsock = SDLNet_TCP_Open(&ip); +void CrowdControl::HandleRemoteData(nlohmann::json payload) { + Effect* incomingEffect = ParseMessage(payload); + if (!incomingEffect) { + return; + } - if (tcpsock) { - connected = true; - SPDLOG_TRACE("[CrowdControl] Connection to server established!"); + // If effect is not a timed effect, execute and return result. + if (!incomingEffect->timeRemaining) { + EffectResult result = CrowdControl::ExecuteEffect(incomingEffect); + EmitMessage(incomingEffect->id, incomingEffect->timeRemaining, result); + } else { + // If another timed effect is already active that conflicts with the incoming effect. + bool isConflictingEffectActive = false; + for (Effect* effect : activeEffects) { + if (effect != incomingEffect && effect->category == incomingEffect->category && effect->id < incomingEffect->id) { + isConflictingEffectActive = true; + EmitMessage(incomingEffect->id, incomingEffect->timeRemaining, EffectResult::Retry); break; } } - SDLNet_SocketSet socketSet = SDLNet_AllocSocketSet(1); - if (tcpsock) { - SDLNet_TCP_AddSocket(socketSet, tcpsock); - } - - // Listen to socket messages - while (connected && tcpsock && isEnabled) { - // we check first if socket has data, to not block in the TCP_Recv - int socketsReady = SDLNet_CheckSockets(socketSet, 0); - - if (socketsReady == -1) { - SPDLOG_ERROR("[CrowdControl] SDLNet_CheckSockets: {}", SDLNet_GetError()); - break; + if (!isConflictingEffectActive) { + // Check if effect can be applied, if it can't, let CC know. + EffectResult result = CrowdControl::CanApplyEffect(incomingEffect); + if (result == EffectResult::Retry || result == EffectResult::Failure) { + EmitMessage(incomingEffect->id, incomingEffect->timeRemaining, result); + return; } - if (socketsReady == 0) { - continue; - } - - int len = SDLNet_TCP_Recv(tcpsock, &received, sizeof(received)); - if (!len || !tcpsock || len == -1) { - SPDLOG_ERROR("[CrowdControl] SDLNet_TCP_Recv: {}", SDLNet_GetError()); - break; - } - - Effect* incomingEffect = ParseMessage(received); - if (!incomingEffect) { - continue; - } - - // If effect is not a timed effect, execute and return result. - if (!incomingEffect->timeRemaining) { - EffectResult result = CrowdControl::ExecuteEffect(incomingEffect); - EmitMessage(tcpsock, incomingEffect->id, incomingEffect->timeRemaining, result); - } else { - // If another timed effect is already active that conflicts with the incoming effect. - bool isConflictingEffectActive = false; - for (Effect* effect : activeEffects) { - if (effect != incomingEffect && effect->category == incomingEffect->category && effect->id < incomingEffect->id) { - isConflictingEffectActive = true; - EmitMessage(tcpsock, incomingEffect->id, incomingEffect->timeRemaining, EffectResult::Retry); - break; - } - } - - if (!isConflictingEffectActive) { - // Check if effect can be applied, if it can't, let CC know. - EffectResult result = CrowdControl::CanApplyEffect(incomingEffect); - if (result == EffectResult::Retry || result == EffectResult::Failure) { - EmitMessage(tcpsock, incomingEffect->id, incomingEffect->timeRemaining, result); - continue; - } - - activeEffectsMutex.lock(); - activeEffects.push_back(incomingEffect); - activeEffectsMutex.unlock(); - } - } - } - - if (connected) { - SDLNet_TCP_Close(tcpsock); - connected = false; - SPDLOG_TRACE("[CrowdControl] Ending Listen thread..."); + activeEffectsMutex.lock(); + activeEffects.push_back(incomingEffect); + activeEffectsMutex.unlock(); } } } @@ -147,13 +94,13 @@ void CrowdControl::ProcessActiveEffects() { if (effect->timeRemaining <= 0) { it = activeEffects.erase(std::remove(activeEffects.begin(), activeEffects.end(), effect), activeEffects.end()); - GameInteractor::RemoveEffect(effect->giEffect); + GameInteractor::RemoveEffect(dynamic_cast(effect->giEffect)); delete effect; } else { // If we have a success after previously being paused, tell CC to resume timer. if (effect->isPaused) { effect->isPaused = false; - EmitMessage(tcpsock, effect->id, effect->timeRemaining, EffectResult::Resumed); + EmitMessage(effect->id, effect->timeRemaining, EffectResult::Resumed); // If not paused before, subtract time from the timer and send a Success event if // the result is different from the last time this was ran. // Timed events are put on a thread that runs once per second. @@ -161,7 +108,7 @@ void CrowdControl::ProcessActiveEffects() { effect->timeRemaining -= 1000; if (result != effect->lastExecutionResult) { effect->lastExecutionResult = result; - EmitMessage(tcpsock, effect->id, effect->timeRemaining, EffectResult::Success); + EmitMessage(effect->id, effect->timeRemaining, EffectResult::Success); } } it++; @@ -169,7 +116,7 @@ void CrowdControl::ProcessActiveEffects() { } else { // Timed effects only do Success or Retry if (!effect->isPaused && effect->timeRemaining > 0) { effect->isPaused = true; - EmitMessage(tcpsock, effect->id, effect->timeRemaining, EffectResult::Paused); + EmitMessage(effect->id, effect->timeRemaining, EffectResult::Paused); } it++; } @@ -182,7 +129,7 @@ void CrowdControl::ProcessActiveEffects() { SPDLOG_TRACE("[CrowdControl] Ending Process thread..."); } -void CrowdControl::EmitMessage(TCPsocket socket, uint32_t eventId, long timeRemaining, EffectResult status) { +void CrowdControl::EmitMessage(uint32_t eventId, long timeRemaining, EffectResult status) { nlohmann::json payload; payload["id"] = eventId; @@ -190,8 +137,9 @@ void CrowdControl::EmitMessage(TCPsocket socket, uint32_t eventId, long timeRema payload["timeRemaining"] = timeRemaining; payload["status"] = status; - std::string jsonPayload = payload.dump(); - SDLNet_TCP_Send(socket, jsonPayload.c_str(), jsonPayload.size() + 1); + SPDLOG_INFO("[CrowdControl] Sending payload:\n{}", payload.dump()); + + GameInteractor::Instance->TransmitJsonToRemote(payload); } CrowdControl::EffectResult CrowdControl::ExecuteEffect(Effect* effect) { @@ -229,13 +177,14 @@ CrowdControl::EffectResult CrowdControl::TranslateGiEnum(GameInteractionEffectQu return result; } -CrowdControl::Effect* CrowdControl::ParseMessage(char payload[512]) { - nlohmann::json dataReceived = nlohmann::json::parse(payload, nullptr, false); - if (dataReceived.is_discarded()) { - SPDLOG_ERROR("Error parsing JSON"); +CrowdControl::Effect* CrowdControl::ParseMessage(nlohmann::json dataReceived) { + if (!dataReceived.contains("id") || !dataReceived.contains("type")) { + SPDLOG_ERROR("[CrowdControl] Invalid payload received:\n{}", dataReceived); return nullptr; } + SPDLOG_INFO("[CrowdControl] Received payload:\n{}", dataReceived.dump()); + Effect* effect = new Effect(); effect->lastExecutionResult = EffectResult::Initiate; effect->id = dataReceived["id"]; @@ -333,13 +282,13 @@ CrowdControl::Effect* CrowdControl::ParseMessage(char payload[512]) { effect->category = kEffectCatDamageTaken; effect->timeRemaining = 30000; effect->giEffect = new GameInteractionEffect::ModifyDefenseModifier(); - effect->giEffect->parameters[0] = 2; + dynamic_cast(effect->giEffect)->parameters[0] = 2; break; case kEffectTakeDoubleDamage: effect->category = kEffectCatDamageTaken; effect->timeRemaining = 30000; effect->giEffect = new GameInteractionEffect::ModifyDefenseModifier(); - effect->giEffect->parameters[0] = -2; + dynamic_cast(effect->giEffect)->parameters[0] = -2; break; case kEffectOneHitKo: effect->category = kEffectCatDamageTaken; @@ -356,37 +305,37 @@ CrowdControl::Effect* CrowdControl::ParseMessage(char payload[512]) { effect->category = kEffectCatSpeed; effect->timeRemaining = 30000; effect->giEffect = new GameInteractionEffect::ModifyRunSpeedModifier(); - effect->giEffect->parameters[0] = 2; + dynamic_cast(effect->giEffect)->parameters[0] = 2; break; case kEffectDecreaseSpeed: effect->category = kEffectCatSpeed; effect->timeRemaining = 30000; effect->giEffect = new GameInteractionEffect::ModifyRunSpeedModifier(); - effect->giEffect->parameters[0] = -2; + dynamic_cast(effect->giEffect)->parameters[0] = -2; break; case kEffectLowGravity: effect->category = kEffectCatGravity; effect->timeRemaining = 30000; effect->giEffect = new GameInteractionEffect::ModifyGravity(); - effect->giEffect->parameters[0] = GI_GRAVITY_LEVEL_LIGHT; + dynamic_cast(effect->giEffect)->parameters[0] = GI_GRAVITY_LEVEL_LIGHT; break; case kEffectHighGravity: effect->category = kEffectCatGravity; effect->timeRemaining = 30000; effect->giEffect = new GameInteractionEffect::ModifyGravity(); - effect->giEffect->parameters[0] = GI_GRAVITY_LEVEL_HEAVY; + dynamic_cast(effect->giEffect)->parameters[0] = GI_GRAVITY_LEVEL_HEAVY; break; case kEffectForceIronBoots: effect->category = kEffectCatBoots; effect->timeRemaining = 30000; effect->giEffect = new GameInteractionEffect::ForceEquipBoots(); - effect->giEffect->parameters[0] = EQUIP_VALUE_BOOTS_IRON; + dynamic_cast(effect->giEffect)->parameters[0] = EQUIP_VALUE_BOOTS_IRON; break; case kEffectForceHoverBoots: effect->category = kEffectCatBoots; effect->timeRemaining = 30000; effect->giEffect = new GameInteractionEffect::ForceEquipBoots(); - effect->giEffect->parameters[0] = EQUIP_VALUE_BOOTS_HOVER; + dynamic_cast(effect->giEffect)->parameters[0] = EQUIP_VALUE_BOOTS_HOVER; break; case kEffectSlipperyFloor: effect->category = kEffectCatSlipperyFloor; @@ -412,23 +361,23 @@ CrowdControl::Effect* CrowdControl::ParseMessage(char payload[512]) { // Hurt or Heal Link case kEffectEmptyHeart: effect->giEffect = new GameInteractionEffect::ModifyHealth(); - effect->giEffect->parameters[0] = receivedParameter * -1; + dynamic_cast(effect->giEffect)->parameters[0] = receivedParameter * -1; break; case kEffectFillHeart: effect->giEffect = new GameInteractionEffect::ModifyHealth(); - effect->giEffect->parameters[0] = receivedParameter; + dynamic_cast(effect->giEffect)->parameters[0] = receivedParameter; break; case kEffectKnockbackLinkWeak: effect->giEffect = new GameInteractionEffect::KnockbackPlayer(); - effect->giEffect->parameters[0] = 1; + dynamic_cast(effect->giEffect)->parameters[0] = 1; break; case kEffectKnockbackLinkStrong: effect->giEffect = new GameInteractionEffect::KnockbackPlayer(); - effect->giEffect->parameters[0] = 3; + dynamic_cast(effect->giEffect)->parameters[0] = 3; break; case kEffectKnockbackLinkMega: effect->giEffect = new GameInteractionEffect::KnockbackPlayer(); - effect->giEffect->parameters[0] = 6; + dynamic_cast(effect->giEffect)->parameters[0] = 6; break; case kEffectBurnLink: effect->giEffect = new GameInteractionEffect::BurnPlayer(); @@ -441,109 +390,109 @@ CrowdControl::Effect* CrowdControl::ParseMessage(char payload[512]) { break; case kEffectKillLink: effect->giEffect = new GameInteractionEffect::SetPlayerHealth(); - effect->giEffect->parameters[0] = 0; + dynamic_cast(effect->giEffect)->parameters[0] = 0; break; // Give Items and Consumables case kEffectAddHeartContainer: effect->giEffect = new GameInteractionEffect::ModifyHeartContainers(); - effect->giEffect->parameters[0] = 1; + dynamic_cast(effect->giEffect)->parameters[0] = 1; break; case kEffectFillMagic: effect->giEffect = new GameInteractionEffect::FillMagic(); break; case kEffectAddRupees: effect->giEffect = new GameInteractionEffect::ModifyRupees(); - effect->giEffect->parameters[0] = receivedParameter; + dynamic_cast(effect->giEffect)->parameters[0] = receivedParameter; break; case kEffectGiveDekuShield: effect->giEffect = new GameInteractionEffect::GiveOrTakeShield(); - effect->giEffect->parameters[0] = ITEM_SHIELD_DEKU; + dynamic_cast(effect->giEffect)->parameters[0] = ITEM_SHIELD_DEKU; break; case kEffectGiveHylianShield: effect->giEffect = new GameInteractionEffect::GiveOrTakeShield(); - effect->giEffect->parameters[0] = ITEM_SHIELD_HYLIAN; + dynamic_cast(effect->giEffect)->parameters[0] = ITEM_SHIELD_HYLIAN; break; case kEffectRefillSticks: effect->giEffect = new GameInteractionEffect::AddOrTakeAmmo(); - effect->giEffect->parameters[0] = receivedParameter; - effect->giEffect->parameters[1] = ITEM_STICK; + dynamic_cast(effect->giEffect)->parameters[0] = receivedParameter; + dynamic_cast(effect->giEffect)->parameters[1] = ITEM_STICK; break; case kEffectRefillNuts: effect->giEffect = new GameInteractionEffect::AddOrTakeAmmo(); - effect->giEffect->parameters[0] = receivedParameter; - effect->giEffect->parameters[1] = ITEM_NUT; + dynamic_cast(effect->giEffect)->parameters[0] = receivedParameter; + dynamic_cast(effect->giEffect)->parameters[1] = ITEM_NUT; break; case kEffectRefillBombs: effect->giEffect = new GameInteractionEffect::AddOrTakeAmmo(); - effect->giEffect->parameters[0] = receivedParameter; - effect->giEffect->parameters[1] = ITEM_BOMB; + dynamic_cast(effect->giEffect)->parameters[0] = receivedParameter; + dynamic_cast(effect->giEffect)->parameters[1] = ITEM_BOMB; break; case kEffectRefillSeeds: effect->giEffect = new GameInteractionEffect::AddOrTakeAmmo(); - effect->giEffect->parameters[0] = receivedParameter; - effect->giEffect->parameters[1] = ITEM_SLINGSHOT; + dynamic_cast(effect->giEffect)->parameters[0] = receivedParameter; + dynamic_cast(effect->giEffect)->parameters[1] = ITEM_SLINGSHOT; break; case kEffectRefillArrows: effect->giEffect = new GameInteractionEffect::AddOrTakeAmmo(); - effect->giEffect->parameters[0] = receivedParameter; - effect->giEffect->parameters[1] = ITEM_BOW; + dynamic_cast(effect->giEffect)->parameters[0] = receivedParameter; + dynamic_cast(effect->giEffect)->parameters[1] = ITEM_BOW; break; case kEffectRefillBombchus: effect->giEffect = new GameInteractionEffect::AddOrTakeAmmo(); - effect->giEffect->parameters[0] = receivedParameter; - effect->giEffect->parameters[1] = ITEM_BOMBCHU; + dynamic_cast(effect->giEffect)->parameters[0] = receivedParameter; + dynamic_cast(effect->giEffect)->parameters[1] = ITEM_BOMBCHU; break; // Take Items and Consumables case kEffectRemoveHeartContainer: effect->giEffect = new GameInteractionEffect::ModifyHeartContainers(); - effect->giEffect->parameters[0] = -1; + dynamic_cast(effect->giEffect)->parameters[0] = -1; break; case kEffectEmptyMagic: effect->giEffect = new GameInteractionEffect::EmptyMagic(); break; case kEffectRemoveRupees: effect->giEffect = new GameInteractionEffect::ModifyRupees(); - effect->giEffect->parameters[0] = receivedParameter * -1; + dynamic_cast(effect->giEffect)->parameters[0] = receivedParameter * -1; break; case kEffectTakeDekuShield: effect->giEffect = new GameInteractionEffect::GiveOrTakeShield(); - effect->giEffect->parameters[0] = -ITEM_SHIELD_DEKU; + dynamic_cast(effect->giEffect)->parameters[0] = -ITEM_SHIELD_DEKU; break; case kEffectTakeHylianShield: effect->giEffect = new GameInteractionEffect::GiveOrTakeShield(); - effect->giEffect->parameters[0] = -ITEM_SHIELD_HYLIAN; + dynamic_cast(effect->giEffect)->parameters[0] = -ITEM_SHIELD_HYLIAN; break; case kEffectTakeSticks: effect->giEffect = new GameInteractionEffect::AddOrTakeAmmo(); - effect->giEffect->parameters[0] = receivedParameter * -1; - effect->giEffect->parameters[1] = ITEM_STICK; + dynamic_cast(effect->giEffect)->parameters[0] = receivedParameter * -1; + dynamic_cast(effect->giEffect)->parameters[1] = ITEM_STICK; break; case kEffectTakeNuts: effect->giEffect = new GameInteractionEffect::AddOrTakeAmmo(); - effect->giEffect->parameters[0] = receivedParameter * -1; - effect->giEffect->parameters[1] = ITEM_NUT; + dynamic_cast(effect->giEffect)->parameters[0] = receivedParameter * -1; + dynamic_cast(effect->giEffect)->parameters[1] = ITEM_NUT; break; case kEffectTakeBombs: effect->giEffect = new GameInteractionEffect::AddOrTakeAmmo(); - effect->giEffect->parameters[0] = receivedParameter * -1; - effect->giEffect->parameters[1] = ITEM_BOMB; + dynamic_cast(effect->giEffect)->parameters[0] = receivedParameter * -1; + dynamic_cast(effect->giEffect)->parameters[1] = ITEM_BOMB; break; case kEffectTakeSeeds: effect->giEffect = new GameInteractionEffect::AddOrTakeAmmo(); - effect->giEffect->parameters[0] = receivedParameter * -1; - effect->giEffect->parameters[1] = ITEM_SLINGSHOT; + dynamic_cast(effect->giEffect)->parameters[0] = receivedParameter * -1; + dynamic_cast(effect->giEffect)->parameters[1] = ITEM_SLINGSHOT; break; case kEffectTakeArrows: effect->giEffect = new GameInteractionEffect::AddOrTakeAmmo(); - effect->giEffect->parameters[0] = receivedParameter * -1; - effect->giEffect->parameters[1] = ITEM_BOW; + dynamic_cast(effect->giEffect)->parameters[0] = receivedParameter * -1; + dynamic_cast(effect->giEffect)->parameters[1] = ITEM_BOW; break; case kEffectTakeBombchus: effect->giEffect = new GameInteractionEffect::AddOrTakeAmmo(); - effect->giEffect->parameters[0] = receivedParameter * -1; - effect->giEffect->parameters[1] = ITEM_BOMBCHU; + dynamic_cast(effect->giEffect)->parameters[0] = receivedParameter * -1; + dynamic_cast(effect->giEffect)->parameters[1] = ITEM_BOMBCHU; break; // Link Size Modifiers @@ -551,25 +500,25 @@ CrowdControl::Effect* CrowdControl::ParseMessage(char payload[512]) { effect->category = kEffectCatLinkSize; effect->timeRemaining = 30000; effect->giEffect = new GameInteractionEffect::ModifyLinkSize(); - effect->giEffect->parameters[0] = GI_LINK_SIZE_GIANT; + dynamic_cast(effect->giEffect)->parameters[0] = GI_LINK_SIZE_GIANT; break; case kEffectMinishLink: effect->category = kEffectCatLinkSize; effect->timeRemaining = 30000; effect->giEffect = new GameInteractionEffect::ModifyLinkSize(); - effect->giEffect->parameters[0] = GI_LINK_SIZE_MINISH; + dynamic_cast(effect->giEffect)->parameters[0] = GI_LINK_SIZE_MINISH; break; case kEffectPaperLink: effect->category = kEffectCatLinkSize; effect->timeRemaining = 30000; effect->giEffect = new GameInteractionEffect::ModifyLinkSize(); - effect->giEffect->parameters[0] = GI_LINK_SIZE_PAPER; + dynamic_cast(effect->giEffect)->parameters[0] = GI_LINK_SIZE_PAPER; break; case kEffectSquishedLink: effect->category = kEffectCatLinkSize; effect->timeRemaining = 30000; effect->giEffect = new GameInteractionEffect::ModifyLinkSize(); - effect->giEffect->parameters[0] = GI_LINK_SIZE_SQUISHED; + dynamic_cast(effect->giEffect)->parameters[0] = GI_LINK_SIZE_SQUISHED; break; case kEffectInvisibleLink: effect->category = kEffectCatLinkSize; @@ -585,11 +534,11 @@ CrowdControl::Effect* CrowdControl::ParseMessage(char payload[512]) { break; case kEffectSetTimeToDawn: effect->giEffect = new GameInteractionEffect::SetTimeOfDay(); - effect->giEffect->parameters[0] = GI_TIMEOFDAY_DAWN; + dynamic_cast(effect->giEffect)->parameters[0] = GI_TIMEOFDAY_DAWN; break; case kEffectSetTimeToDusk: effect->giEffect = new GameInteractionEffect::SetTimeOfDay(); - effect->giEffect->parameters[0] = GI_TIMEOFDAY_DUSK; + dynamic_cast(effect->giEffect)->parameters[0] = GI_TIMEOFDAY_DUSK; break; // Visual Effects @@ -632,186 +581,186 @@ CrowdControl::Effect* CrowdControl::ParseMessage(char payload[512]) { effect->category = kEffectCatRandomButtons; effect->timeRemaining = 30000; effect->giEffect = new GameInteractionEffect::PressRandomButton(); - effect->giEffect->parameters[0] = 30; + dynamic_cast(effect->giEffect)->parameters[0] = 30; break; case kEffectClearCbuttons: effect->giEffect = new GameInteractionEffect::ClearAssignedButtons(); - effect->giEffect->parameters[0] = GI_BUTTONS_CBUTTONS; + dynamic_cast(effect->giEffect)->parameters[0] = GI_BUTTONS_CBUTTONS; break; case kEffectClearDpad: effect->giEffect = new GameInteractionEffect::ClearAssignedButtons(); - effect->giEffect->parameters[0] = GI_BUTTONS_DPAD; + dynamic_cast(effect->giEffect)->parameters[0] = GI_BUTTONS_DPAD; break; // Teleport Player case kEffectTpLinksHouse: effect->giEffect = new GameInteractionEffect::TeleportPlayer(); - effect->giEffect->parameters[0] = GI_TP_DEST_LINKSHOUSE; + dynamic_cast(effect->giEffect)->parameters[0] = GI_TP_DEST_LINKSHOUSE; break; case kEffectTpMinuet: effect->giEffect = new GameInteractionEffect::TeleportPlayer(); - effect->giEffect->parameters[0] = GI_TP_DEST_MINUET; + dynamic_cast(effect->giEffect)->parameters[0] = GI_TP_DEST_MINUET; break; case kEffectTpBolero: effect->giEffect = new GameInteractionEffect::TeleportPlayer(); - effect->giEffect->parameters[0] = GI_TP_DEST_BOLERO; + dynamic_cast(effect->giEffect)->parameters[0] = GI_TP_DEST_BOLERO; break; case kEffectTpSerenade: effect->giEffect = new GameInteractionEffect::TeleportPlayer(); - effect->giEffect->parameters[0] = GI_TP_DEST_SERENADE; + dynamic_cast(effect->giEffect)->parameters[0] = GI_TP_DEST_SERENADE; break; case kEffectTpRequiem: effect->giEffect = new GameInteractionEffect::TeleportPlayer(); - effect->giEffect->parameters[0] = GI_TP_DEST_REQUIEM; + dynamic_cast(effect->giEffect)->parameters[0] = GI_TP_DEST_REQUIEM; break; case kEffectTpNocturne: effect->giEffect = new GameInteractionEffect::TeleportPlayer(); - effect->giEffect->parameters[0] = GI_TP_DEST_NOCTURNE; + dynamic_cast(effect->giEffect)->parameters[0] = GI_TP_DEST_NOCTURNE; break; case kEffectTpPrelude: effect->giEffect = new GameInteractionEffect::TeleportPlayer(); - effect->giEffect->parameters[0] = GI_TP_DEST_PRELUDE; + dynamic_cast(effect->giEffect)->parameters[0] = GI_TP_DEST_PRELUDE; break; // Tunic Color (Bidding War) case kEffectTunicRed: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_TUNICS; - effect->giEffect->parameters[1] = GI_COLOR_RED; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_TUNICS; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_RED; break; case kEffectTunicGreen: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_TUNICS; - effect->giEffect->parameters[1] = GI_COLOR_GREEN; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_TUNICS; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_GREEN; break; case kEffectTunicBlue: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_TUNICS; - effect->giEffect->parameters[1] = GI_COLOR_BLUE; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_TUNICS; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_BLUE; break; case kEffectTunicOrange: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_TUNICS; - effect->giEffect->parameters[1] = GI_COLOR_ORANGE; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_TUNICS; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_ORANGE; break; case kEffectTunicYellow: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_TUNICS; - effect->giEffect->parameters[1] = GI_COLOR_YELLOW; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_TUNICS; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_YELLOW; break; case kEffectTunicPurple: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_TUNICS; - effect->giEffect->parameters[1] = GI_COLOR_PURPLE; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_TUNICS; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_PURPLE; break; case kEffectTunicPink: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_TUNICS; - effect->giEffect->parameters[1] = GI_COLOR_PINK; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_TUNICS; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_PINK; break; case kEffectTunicBrown: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_TUNICS; - effect->giEffect->parameters[1] = GI_COLOR_BROWN; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_TUNICS; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_BROWN; break; case kEffectTunicBlack: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_TUNICS; - effect->giEffect->parameters[1] = GI_COLOR_BLACK; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_TUNICS; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_BLACK; break; // Navi Color (Bidding War) case kEffectNaviRed: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_NAVI; - effect->giEffect->parameters[1] = GI_COLOR_RED; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_NAVI; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_RED; break; case kEffectNaviGreen: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_NAVI; - effect->giEffect->parameters[1] = GI_COLOR_GREEN; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_NAVI; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_GREEN; break; case kEffectNaviBlue: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_NAVI; - effect->giEffect->parameters[1] = GI_COLOR_BLUE; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_NAVI; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_BLUE; break; case kEffectNaviOrange: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_NAVI; - effect->giEffect->parameters[1] = GI_COLOR_ORANGE; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_NAVI; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_ORANGE; break; case kEffectNaviYellow: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_NAVI; - effect->giEffect->parameters[1] = GI_COLOR_YELLOW; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_NAVI; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_YELLOW; break; case kEffectNaviPurple: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_NAVI; - effect->giEffect->parameters[1] = GI_COLOR_PURPLE; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_NAVI; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_PURPLE; break; case kEffectNaviPink: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_NAVI; - effect->giEffect->parameters[1] = GI_COLOR_PINK; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_NAVI; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_PINK; break; case kEffectNaviBrown: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_NAVI; - effect->giEffect->parameters[1] = GI_COLOR_BROWN; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_NAVI; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_BROWN; break; case kEffectNaviBlack: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_NAVI; - effect->giEffect->parameters[1] = GI_COLOR_BLACK; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_NAVI; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_BLACK; break; // Link's Hair Color (Bidding War) case kEffectHairRed: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_HAIR; - effect->giEffect->parameters[1] = GI_COLOR_RED; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_HAIR; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_RED; break; case kEffectHairGreen: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_HAIR; - effect->giEffect->parameters[1] = GI_COLOR_GREEN; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_HAIR; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_GREEN; break; case kEffectHairBlue: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_HAIR; - effect->giEffect->parameters[1] = GI_COLOR_BLUE; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_HAIR; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_BLUE; break; case kEffectHairOrange: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_HAIR; - effect->giEffect->parameters[1] = GI_COLOR_ORANGE; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_HAIR; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_ORANGE; break; case kEffectHairYellow: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_HAIR; - effect->giEffect->parameters[1] = GI_COLOR_YELLOW; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_HAIR; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_YELLOW; break; case kEffectHairPurple: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_HAIR; - effect->giEffect->parameters[1] = GI_COLOR_PURPLE; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_HAIR; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_PURPLE; break; case kEffectHairPink: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_HAIR; - effect->giEffect->parameters[1] = GI_COLOR_PINK; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_HAIR; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_PINK; break; case kEffectHairBrown: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_HAIR; - effect->giEffect->parameters[1] = GI_COLOR_BROWN; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_HAIR; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_BROWN; break; case kEffectHairBlack: effect->giEffect = new GameInteractionEffect::SetCosmeticsColor(); - effect->giEffect->parameters[0] = GI_COSMETICS_HAIR; - effect->giEffect->parameters[1] = GI_COLOR_BLACK; + dynamic_cast(effect->giEffect)->parameters[0] = GI_COSMETICS_HAIR; + dynamic_cast(effect->giEffect)->parameters[1] = GI_COLOR_BLACK; break; default: diff --git a/soh/soh/Enhancements/crowd-control/CrowdControl.h b/soh/soh/Enhancements/crowd-control/CrowdControl.h index 672e6d361..bb06cc5b1 100644 --- a/soh/soh/Enhancements/crowd-control/CrowdControl.h +++ b/soh/soh/Enhancements/crowd-control/CrowdControl.h @@ -1,4 +1,4 @@ -#ifdef ENABLE_CROWD_CONTROL +#ifdef ENABLE_REMOTE_CONTROL #ifndef _CROWDCONTROL_C #define _CROWDCONTROL_C @@ -73,33 +73,24 @@ class CrowdControl { EffectResult lastExecutionResult; } Effect; - std::thread ccThreadReceive; std::thread ccThreadProcess; - TCPsocket tcpsock; - IPaddress ip; - bool isEnabled; - bool connected; - - char received[512]; std::vector activeEffects; std::mutex activeEffectsMutex; - void ListenToServer(); + void HandleRemoteData(nlohmann::json payload); void ProcessActiveEffects(); - void EmitMessage(TCPsocket socket, uint32_t eventId, long timeRemaining, EffectResult status); - Effect* ParseMessage(char payload[512]); + void EmitMessage(uint32_t eventId, long timeRemaining, EffectResult status); + Effect* ParseMessage(nlohmann::json payload); EffectResult ExecuteEffect(Effect* effect); EffectResult CanApplyEffect(Effect *effect); EffectResult TranslateGiEnum(GameInteractionEffectQueryResult giResult); public: static CrowdControl* Instance; - void Init(); - void Shutdown(); void Enable(); void Disable(); }; diff --git a/soh/soh/Enhancements/debugconsole.cpp b/soh/soh/Enhancements/debugconsole.cpp index 6bc441d03..3e5a8e624 100644 --- a/soh/soh/Enhancements/debugconsole.cpp +++ b/soh/soh/Enhancements/debugconsole.cpp @@ -99,7 +99,7 @@ static bool ActorSpawnHandler(std::shared_ptr Console, const std:: static bool KillPlayerHandler(std::shared_ptr Console, const std::vector&, std::string* output) { GameInteractionEffectBase* effect = new GameInteractionEffect::SetPlayerHealth(); - effect->parameters[0] = 0; + dynamic_cast(effect)->parameters[0] = 0; GameInteractionEffectQueryResult result = GameInteractor::ApplyEffect(effect); if (result == GameInteractionEffectQueryResult::Possible) { INFO_MESSAGE("[SOH] You've met with a terrible fate, haven't you?"); @@ -130,7 +130,7 @@ static bool SetPlayerHealthHandler(std::shared_ptr Console, const } GameInteractionEffectBase* effect = new GameInteractionEffect::SetPlayerHealth(); - effect->parameters[0] = health; + dynamic_cast(effect)->parameters[0] = health; GameInteractionEffectQueryResult result = GameInteractor::ApplyEffect(effect); if (result == GameInteractionEffectQueryResult::Possible) { INFO_MESSAGE("[SOH] Player health updated to %d", health); @@ -247,8 +247,8 @@ static bool AddAmmoHandler(std::shared_ptr Console, const std::vec } GameInteractionEffectBase* effect = new GameInteractionEffect::AddOrTakeAmmo(); - effect->parameters[0] = amount; - effect->parameters[1] = it->second; + dynamic_cast(effect)->parameters[0] = amount; + dynamic_cast(effect)->parameters[1] = it->second; GameInteractionEffectQueryResult result = GameInteractor::ApplyEffect(effect); if (result == GameInteractionEffectQueryResult::Possible) { @@ -287,8 +287,8 @@ static bool TakeAmmoHandler(std::shared_ptr Console, const std::ve } GameInteractionEffectBase* effect = new GameInteractionEffect::AddOrTakeAmmo(); - effect->parameters[0] = -amount; - effect->parameters[1] = it->second; + dynamic_cast(effect)->parameters[0] = -amount; + dynamic_cast(effect)->parameters[1] = it->second; GameInteractionEffectQueryResult result = GameInteractor::ApplyEffect(effect); if (result == GameInteractionEffectQueryResult::Possible) { @@ -577,7 +577,7 @@ static bool InvisibleHandler(std::shared_ptr Console, const std::v return 1; } - GameInteractionEffectBase* effect = new GameInteractionEffect::InvisibleLink(); + RemovableGameInteractionEffect* effect = new GameInteractionEffect::InvisibleLink(); GameInteractionEffectQueryResult result = state ? GameInteractor::ApplyEffect(effect) : GameInteractor::RemoveEffect(effect); if (result == GameInteractionEffectQueryResult::Possible) { @@ -604,8 +604,8 @@ static bool GiantLinkHandler(std::shared_ptr Console, const std::v return 1; } - GameInteractionEffectBase* effect = new GameInteractionEffect::ModifyLinkSize(); - effect->parameters[0] = GI_LINK_SIZE_GIANT; + RemovableGameInteractionEffect* effect = new GameInteractionEffect::ModifyLinkSize(); + dynamic_cast(effect)->parameters[0] = GI_LINK_SIZE_GIANT; GameInteractionEffectQueryResult result = state ? GameInteractor::ApplyEffect(effect) : GameInteractor::RemoveEffect(effect); if (result == GameInteractionEffectQueryResult::Possible) { @@ -632,8 +632,8 @@ static bool MinishLinkHandler(std::shared_ptr Console, const std:: return 1; } - GameInteractionEffectBase* effect = new GameInteractionEffect::ModifyLinkSize(); - effect->parameters[0] = GI_LINK_SIZE_MINISH; + RemovableGameInteractionEffect* effect = new GameInteractionEffect::ModifyLinkSize(); + dynamic_cast(effect)->parameters[0] = GI_LINK_SIZE_MINISH; GameInteractionEffectQueryResult result = state ? GameInteractor::ApplyEffect(effect) : GameInteractor::RemoveEffect(effect); if (result == GameInteractionEffectQueryResult::Possible) { @@ -666,7 +666,7 @@ static bool AddHeartContainerHandler(std::shared_ptr Console, cons } GameInteractionEffectBase* effect = new GameInteractionEffect::ModifyHeartContainers(); - effect->parameters[0] = hearts; + dynamic_cast(effect)->parameters[0] = hearts; GameInteractionEffectQueryResult result = GameInteractor::ApplyEffect(effect); if (result == GameInteractionEffectQueryResult::Possible) { INFO_MESSAGE("[SOH] Added %d heart containers", hearts); @@ -697,7 +697,7 @@ static bool RemoveHeartContainerHandler(std::shared_ptr Console, c } GameInteractionEffectBase* effect = new GameInteractionEffect::ModifyHeartContainers(); - effect->parameters[0] = -hearts; + dynamic_cast(effect)->parameters[0] = -hearts; GameInteractionEffectQueryResult result = GameInteractor::ApplyEffect(effect); if (result == GameInteractionEffectQueryResult::Possible) { INFO_MESSAGE("[SOH] Removed %d heart containers", hearts); @@ -717,7 +717,7 @@ static bool GravityHandler(std::shared_ptr Console, const std::vec GameInteractionEffectBase* effect = new GameInteractionEffect::ModifyGravity(); try { - effect->parameters[0] = LUS::Math::clamp(std::stoi(args[1], nullptr, 10), GI_GRAVITY_LEVEL_LIGHT, GI_GRAVITY_LEVEL_HEAVY); + dynamic_cast(effect)->parameters[0] = LUS::Math::clamp(std::stoi(args[1], nullptr, 10), GI_GRAVITY_LEVEL_LIGHT, GI_GRAVITY_LEVEL_HEAVY); } catch (std::invalid_argument const& ex) { ERROR_MESSAGE("[SOH] Gravity value must be a number."); return 1; @@ -747,7 +747,7 @@ static bool NoUIHandler(std::shared_ptr Console, const std::vector return 1; } - GameInteractionEffectBase* effect = new GameInteractionEffect::NoUI(); + RemovableGameInteractionEffect* effect = new GameInteractionEffect::NoUI(); GameInteractionEffectQueryResult result = state ? GameInteractor::ApplyEffect(effect) : GameInteractor::RemoveEffect(effect); @@ -782,7 +782,7 @@ static bool DefenseModifierHandler(std::shared_ptr Console, const GameInteractionEffectBase* effect = new GameInteractionEffect::ModifyDefenseModifier(); try { - effect->parameters[0] = std::stoi(args[1], nullptr, 10); + dynamic_cast(effect)->parameters[0] = std::stoi(args[1], nullptr, 10); } catch (std::invalid_argument const& ex) { ERROR_MESSAGE("[SOH] Defense modifier value must be a number."); return 1; @@ -790,7 +790,7 @@ static bool DefenseModifierHandler(std::shared_ptr Console, const GameInteractionEffectQueryResult result = GameInteractor::ApplyEffect(effect); if (result == GameInteractionEffectQueryResult::Possible) { - INFO_MESSAGE("[SOH] Defense modifier set to %d", effect->parameters[0]); + INFO_MESSAGE("[SOH] Defense modifier set to %d", dynamic_cast(effect)->parameters[0]); return 0; } else { INFO_MESSAGE("[SOH] Command failed: Could not set defense modifier."); @@ -812,7 +812,7 @@ static bool DamageHandler(std::shared_ptr Console, const std::vect return 1; } - effect->parameters[0] = -value; + dynamic_cast(effect)->parameters[0] = -value; } catch (std::invalid_argument const& ex) { ERROR_MESSAGE("[SOH] Damage value must be a number."); return 1; @@ -842,7 +842,7 @@ static bool HealHandler(std::shared_ptr Console, const std::vector return 1; } - effect->parameters[0] = value; + dynamic_cast(effect)->parameters[0] = value; } catch (std::invalid_argument const& ex) { ERROR_MESSAGE("[SOH] Damage value must be a number."); return 1; @@ -898,7 +898,7 @@ static bool NoZHandler(std::shared_ptr Console, const std::vector< return 1; } - GameInteractionEffectBase* effect = new GameInteractionEffect::DisableZTargeting(); + RemovableGameInteractionEffect* effect = new GameInteractionEffect::DisableZTargeting(); GameInteractionEffectQueryResult result = state ? GameInteractor::ApplyEffect(effect) : GameInteractor::RemoveEffect(effect); @@ -926,7 +926,7 @@ static bool OneHitKOHandler(std::shared_ptr Console, const std::ve return 1; } - GameInteractionEffectBase* effect = new GameInteractionEffect::OneHitKO(); + RemovableGameInteractionEffect* effect = new GameInteractionEffect::OneHitKO(); GameInteractionEffectQueryResult result = state ? GameInteractor::ApplyEffect(effect) : GameInteractor::RemoveEffect(effect); @@ -954,7 +954,7 @@ static bool PacifistHandler(std::shared_ptr Console, const std::ve return 1; } - GameInteractionEffectBase* effect = new GameInteractionEffect::PacifistMode(); + RemovableGameInteractionEffect* effect = new GameInteractionEffect::PacifistMode(); GameInteractionEffectQueryResult result = state ? GameInteractor::ApplyEffect(effect) : GameInteractor::RemoveEffect(effect); @@ -982,8 +982,8 @@ static bool PaperLinkHandler(std::shared_ptr Console, const std::v return 1; } - GameInteractionEffectBase* effect = new GameInteractionEffect::ModifyLinkSize(); - effect->parameters[0] = GI_LINK_SIZE_PAPER; + RemovableGameInteractionEffect* effect = new GameInteractionEffect::ModifyLinkSize(); + dynamic_cast(effect)->parameters[0] = GI_LINK_SIZE_PAPER; GameInteractionEffectQueryResult result = state ? GameInteractor::ApplyEffect(effect) : GameInteractor::RemoveEffect(effect); @@ -1011,7 +1011,7 @@ static bool RainstormHandler(std::shared_ptr Console, const std::v return 1; } - GameInteractionEffectBase* effect = new GameInteractionEffect::WeatherRainstorm(); + RemovableGameInteractionEffect* effect = new GameInteractionEffect::WeatherRainstorm(); GameInteractionEffectQueryResult result = state ? GameInteractor::ApplyEffect(effect) : GameInteractor::RemoveEffect(effect); @@ -1039,7 +1039,7 @@ static bool ReverseControlsHandler(std::shared_ptr Console, const return 1; } - GameInteractionEffectBase* effect = new GameInteractionEffect::ReverseControls(); + RemovableGameInteractionEffect* effect = new GameInteractionEffect::ReverseControls(); GameInteractionEffectQueryResult result = state ? GameInteractor::ApplyEffect(effect) : GameInteractor::RemoveEffect(effect); @@ -1062,7 +1062,7 @@ static bool UpdateRupeesHandler(std::shared_ptr Console, const std GameInteractionEffectBase* effect = new GameInteractionEffect::ModifyRupees(); try { - effect->parameters[0] = std::stoi(args[1], nullptr, 10); + dynamic_cast(effect)->parameters[0] = std::stoi(args[1], nullptr, 10); } catch (std::invalid_argument const& ex) { ERROR_MESSAGE("[SOH] Rupee value must be a number."); return 1; @@ -1086,7 +1086,7 @@ static bool SpeedModifierHandler(std::shared_ptr Console, const st GameInteractionEffectBase* effect = new GameInteractionEffect::ModifyRunSpeedModifier(); try { - effect->parameters[0] = std::stoi(args[1], nullptr, 10); + dynamic_cast(effect)->parameters[0] = std::stoi(args[1], nullptr, 10); } catch (std::invalid_argument const& ex) { ERROR_MESSAGE("[SOH] Speed modifier value must be a number."); return 1; @@ -1121,7 +1121,7 @@ static bool BootsHandler(std::shared_ptr Console, const std::vecto } GameInteractionEffectBase* effect = new GameInteractionEffect::ForceEquipBoots(); - effect->parameters[0] = it->second; + dynamic_cast(effect)->parameters[0] = it->second; GameInteractionEffectQueryResult result = GameInteractor::ApplyEffect(effect); if (result == GameInteractionEffectQueryResult::Possible) { @@ -1152,7 +1152,7 @@ static bool GiveShieldHandler(std::shared_ptr Console, const std:: } GameInteractionEffectBase* effect = new GameInteractionEffect::GiveOrTakeShield(); - effect->parameters[0] = it->second; + dynamic_cast(effect)->parameters[0] = it->second; GameInteractionEffectQueryResult result = GameInteractor::ApplyEffect(effect); if (result == GameInteractionEffectQueryResult::Possible) { @@ -1177,7 +1177,7 @@ static bool TakeShieldHandler(std::shared_ptr Console, const std:: } GameInteractionEffectBase* effect = new GameInteractionEffect::GiveOrTakeShield(); - effect->parameters[0] = it->second * -1; + dynamic_cast(effect)->parameters[0] = it->second * -1; GameInteractionEffectQueryResult result = GameInteractor::ApplyEffect(effect); if (result == GameInteractionEffectQueryResult::Possible) { @@ -1203,7 +1203,7 @@ static bool KnockbackHandler(std::shared_ptr Console, const std::v return 1; } - effect->parameters[0] = value; + dynamic_cast(effect)->parameters[0] = value; } catch (std::invalid_argument const& ex) { ERROR_MESSAGE("[SOH] Knockback value must be a number."); return 1; diff --git a/soh/soh/Enhancements/game-interactor/GameInteractionEffect.cpp b/soh/soh/Enhancements/game-interactor/GameInteractionEffect.cpp index 206b26426..a0469d0c3 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractionEffect.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractionEffect.cpp @@ -31,11 +31,11 @@ GameInteractionEffectQueryResult GameInteractionEffectBase::Apply() { } /// For most effects, CanBeRemoved is the same as CanBeApplied. When its not: please override `CanBeRemoved`. -GameInteractionEffectQueryResult GameInteractionEffectBase::CanBeRemoved() { +GameInteractionEffectQueryResult RemovableGameInteractionEffect::CanBeRemoved() { return CanBeApplied(); } -GameInteractionEffectQueryResult GameInteractionEffectBase::Remove() { +GameInteractionEffectQueryResult RemovableGameInteractionEffect::Remove() { GameInteractionEffectQueryResult result = CanBeRemoved(); if (result != GameInteractionEffectQueryResult::Possible) { return result; diff --git a/soh/soh/Enhancements/game-interactor/GameInteractionEffect.h b/soh/soh/Enhancements/game-interactor/GameInteractionEffect.h index c4a7f7eac..ebc1b6fac 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractionEffect.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractionEffect.h @@ -15,38 +15,46 @@ enum GameInteractionEffectQueryResult { class GameInteractionEffectBase { public: virtual GameInteractionEffectQueryResult CanBeApplied() = 0; - virtual GameInteractionEffectQueryResult CanBeRemoved(); GameInteractionEffectQueryResult Apply(); - GameInteractionEffectQueryResult Remove(); - int32_t parameters[3]; - - protected: +protected: virtual void _Apply() = 0; +}; + +class RemovableGameInteractionEffect: public GameInteractionEffectBase { +public: + virtual GameInteractionEffectQueryResult CanBeRemoved(); + GameInteractionEffectQueryResult Remove(); +protected: virtual void _Remove() {}; }; +class ParameterizedGameInteractionEffect { +public: + int32_t parameters[3]; +}; + namespace GameInteractionEffect { - class SetSceneFlag: public GameInteractionEffectBase { + class SetSceneFlag: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; }; - class UnsetSceneFlag: public GameInteractionEffectBase { + class UnsetSceneFlag: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; }; - class SetFlag: public GameInteractionEffectBase { + class SetFlag: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; }; - class UnsetFlag: public GameInteractionEffectBase { + class UnsetFlag: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; }; - class ModifyHeartContainers: public GameInteractionEffectBase { + class ModifyHeartContainers: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; }; @@ -61,29 +69,29 @@ namespace GameInteractionEffect { void _Apply() override; }; - class ModifyRupees: public GameInteractionEffectBase { + class ModifyRupees: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; }; - class NoUI: public GameInteractionEffectBase { + class NoUI: public RemovableGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class ModifyGravity: public GameInteractionEffectBase { + class ModifyGravity: public RemovableGameInteractionEffect, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class ModifyHealth: public GameInteractionEffectBase { + class ModifyHealth: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; }; - class SetPlayerHealth: public GameInteractionEffectBase { + class SetPlayerHealth: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; }; @@ -103,98 +111,98 @@ namespace GameInteractionEffect { void _Apply() override; }; - class KnockbackPlayer: public GameInteractionEffectBase { + class KnockbackPlayer: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; }; - class ModifyLinkSize: public GameInteractionEffectBase { + class ModifyLinkSize: public RemovableGameInteractionEffect, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class InvisibleLink : public GameInteractionEffectBase { + class InvisibleLink : public RemovableGameInteractionEffect, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class PacifistMode : public GameInteractionEffectBase { + class PacifistMode : public RemovableGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class DisableZTargeting: public GameInteractionEffectBase { + class DisableZTargeting: public RemovableGameInteractionEffect, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class WeatherRainstorm: public GameInteractionEffectBase { + class WeatherRainstorm: public RemovableGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class ReverseControls: public GameInteractionEffectBase { + class ReverseControls: public RemovableGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class ForceEquipBoots: public GameInteractionEffectBase { + class ForceEquipBoots: public RemovableGameInteractionEffect, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class ModifyRunSpeedModifier: public GameInteractionEffectBase { + class ModifyRunSpeedModifier: public RemovableGameInteractionEffect, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class OneHitKO : public GameInteractionEffectBase { + class OneHitKO : public RemovableGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class ModifyDefenseModifier: public GameInteractionEffectBase { + class ModifyDefenseModifier: public RemovableGameInteractionEffect, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class GiveOrTakeShield: public GameInteractionEffectBase { + class GiveOrTakeShield: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; }; - class TeleportPlayer: public GameInteractionEffectBase { + class TeleportPlayer: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; }; - class ClearAssignedButtons: public GameInteractionEffectBase { + class ClearAssignedButtons: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; }; - class SetTimeOfDay: public GameInteractionEffectBase { + class SetTimeOfDay: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; }; - class SetCollisionViewer: public GameInteractionEffectBase { + class SetCollisionViewer: public RemovableGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class SetCosmeticsColor: public GameInteractionEffectBase { + class SetCosmeticsColor: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; }; @@ -204,52 +212,52 @@ namespace GameInteractionEffect { void _Apply() override; }; - class PressButton: public GameInteractionEffectBase { + class PressButton: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; }; - class PressRandomButton: public GameInteractionEffectBase { + class PressRandomButton: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; }; - class AddOrTakeAmmo: public GameInteractionEffectBase { + class AddOrTakeAmmo: public GameInteractionEffectBase, public ParameterizedGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; }; - class RandomBombFuseTimer: public GameInteractionEffectBase { + class RandomBombFuseTimer: public RemovableGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class DisableLedgeGrabs: public GameInteractionEffectBase { + class DisableLedgeGrabs: public RemovableGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class RandomWind: public GameInteractionEffectBase { + class RandomWind: public RemovableGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class RandomBonks: public GameInteractionEffectBase { + class RandomBonks: public RemovableGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class PlayerInvincibility: public GameInteractionEffectBase { + class PlayerInvincibility: public RemovableGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; }; - class SlipperyFloor: public GameInteractionEffectBase { + class SlipperyFloor: public RemovableGameInteractionEffect { GameInteractionEffectQueryResult CanBeApplied() override; void _Apply() override; void _Remove() override; diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor.cpp index eb330947b..d6641133c 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor.cpp @@ -31,7 +31,7 @@ GameInteractionEffectQueryResult GameInteractor::ApplyEffect(GameInteractionEffe return effect->Apply(); } -GameInteractionEffectQueryResult GameInteractor::RemoveEffect(GameInteractionEffectBase* effect) { +GameInteractionEffectQueryResult GameInteractor::RemoveEffect(RemovableGameInteractionEffect* effect) { return effect->Remove(); } diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor.h b/soh/soh/Enhancements/game-interactor/GameInteractor.h index 56f0f4701..91244e592 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor.h @@ -7,6 +7,11 @@ #include "soh/Enhancements/item-tables/ItemTableTypes.h" #include +typedef enum { + GI_SCHEME_SAIL, + GI_SCHEME_CROWD_CONTROL, +} GIScheme; + typedef enum { /* 0x00 */ GI_LINK_SIZE_NORMAL, /* 0x01 */ GI_LINK_SIZE_GIANT, @@ -92,10 +97,15 @@ void GameInteractor_SetTriforceHuntCreditsWarpActive(uint8_t state); #ifdef __cplusplus - +#include #include #include +#ifdef ENABLE_REMOTE_CONTROL +#include +#include +#endif + #define DEFINE_HOOK(name, type) \ struct name { \ typedef std::function fn; \ @@ -132,10 +142,24 @@ public: static void SetPacifistMode(bool active); }; + #ifdef ENABLE_REMOTE_CONTROL + bool isRemoteInteractorEnabled; + bool isRemoteInteractorConnected; + + void EnableRemoteInteractor(); + void DisableRemoteInteractor(); + void RegisterRemoteDataHandler(std::function method); + void RegisterRemoteJsonHandler(std::function method); + void RegisterRemoteConnectedHandler(std::function method); + void RegisterRemoteDisconnectedHandler(std::function method); + void TransmitDataToRemote(const char* payload); + void TransmitJsonToRemote(nlohmann::json packet); + #endif + // Effects static GameInteractionEffectQueryResult CanApplyEffect(GameInteractionEffectBase* effect); static GameInteractionEffectQueryResult ApplyEffect(GameInteractionEffectBase* effect); - static GameInteractionEffectQueryResult RemoveEffect(GameInteractionEffectBase* effect); + static GameInteractionEffectQueryResult RemoveEffect(RemovableGameInteractionEffect* effect); // Game Hooks template struct RegisteredGameHooks { inline static std::vector functions; }; @@ -238,6 +262,21 @@ public: static GameInteractionEffectQueryResult SpawnEnemyWithOffset(uint32_t enemyId, int32_t enemyParams); static GameInteractionEffectQueryResult SpawnActor(uint32_t actorId, int32_t actorParams); }; + + private: + #ifdef ENABLE_REMOTE_CONTROL + IPaddress remoteIP; + TCPsocket remoteSocket; + std::thread remoteThreadReceive; + std::function remoteDataHandler; + std::function remoteJsonHandler; + std::function remoteConnectedHandler; + std::function remoteDisconnectedHandler; + + void ReceiveFromServer(); + void HandleRemoteData(char payload[512]); + void HandleRemoteJson(std::string payload); + #endif }; #endif /* __cplusplus */ diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Remote.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Remote.cpp new file mode 100644 index 000000000..2cbd6b379 --- /dev/null +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Remote.cpp @@ -0,0 +1,182 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "GameInteractor.h" +#include +#include +#include +#include +#include +#include +#include + +// MARK: - Remote + +void GameInteractor::EnableRemoteInteractor() { + if (isRemoteInteractorEnabled) { + return; + } + + if (SDLNet_ResolveHost(&remoteIP, CVarGetString("gRemote.IP", "127.0.0.1"), CVarGetInteger("gRemote.Port", 43384)) == -1) { + SPDLOG_ERROR("[GameInteractor] SDLNet_ResolveHost: {}", SDLNet_GetError()); + } + + isRemoteInteractorEnabled = true; + + // First check if there is a thread running, if so, join it + if (remoteThreadReceive.joinable()) { + remoteThreadReceive.join(); + } + + remoteThreadReceive = std::thread(&GameInteractor::ReceiveFromServer, this); +} + +/** + * Raw data handler + * + * If you are developing a new remote, you should probably use the json methods instead. This + * method requires you to parse the data and ensure packets are complete manually, we cannot + * gaurentee that the data will be complete, or that it will only contain one packet with this + */ +void GameInteractor::RegisterRemoteDataHandler(std::function method) { + remoteDataHandler = method; +} + +/** + * Json handler + * + * This method will be called when a complete json packet is received. All json packets must + * be delimited by a null terminator (\0). + */ +void GameInteractor::RegisterRemoteJsonHandler(std::function method) { + remoteJsonHandler = method; +} + +void GameInteractor::RegisterRemoteConnectedHandler(std::function method) { + remoteConnectedHandler = method; +} + +void GameInteractor::RegisterRemoteDisconnectedHandler(std::function method) { + remoteDisconnectedHandler = method; +} + +void GameInteractor::DisableRemoteInteractor() { + if (!isRemoteInteractorEnabled) { + return; + } + + isRemoteInteractorEnabled = false; + remoteThreadReceive.join(); + remoteDataHandler = nullptr; + remoteJsonHandler = nullptr; + remoteConnectedHandler = nullptr; + remoteDisconnectedHandler = nullptr; +} + +void GameInteractor::TransmitDataToRemote(const char* payload) { + SDLNet_TCP_Send(remoteSocket, payload, strlen(payload) + 1); +} + +// Appends a newline character to the end of the json payload and sends it to the remote +void GameInteractor::TransmitJsonToRemote(nlohmann::json payload) { + TransmitDataToRemote(payload.dump().c_str()); +} + +// MARK: - Private + +std::string receivedData; + +void GameInteractor::ReceiveFromServer() { + while (isRemoteInteractorEnabled) { + while (!isRemoteInteractorConnected && isRemoteInteractorEnabled) { + SPDLOG_TRACE("[GameInteractor] Attempting to make connection to server..."); + remoteSocket = SDLNet_TCP_Open(&remoteIP); + + if (remoteSocket) { + isRemoteInteractorConnected = true; + SPDLOG_INFO("[GameInteractor] Connection to server established!"); + + if (remoteConnectedHandler) { + remoteConnectedHandler(); + } + break; + } + } + + SDLNet_SocketSet socketSet = SDLNet_AllocSocketSet(1); + if (remoteSocket) { + SDLNet_TCP_AddSocket(socketSet, remoteSocket); + } + + // Listen to socket messages + while (isRemoteInteractorConnected && remoteSocket && isRemoteInteractorEnabled) { + // we check first if socket has data, to not block in the TCP_Recv + int socketsReady = SDLNet_CheckSockets(socketSet, 0); + + if (socketsReady == -1) { + SPDLOG_ERROR("[GameInteractor] SDLNet_CheckSockets: {}", SDLNet_GetError()); + break; + } + + if (socketsReady == 0) { + continue; + } + + char remoteDataReceived[512]; + memset(remoteDataReceived, 0, sizeof(remoteDataReceived)); + int len = SDLNet_TCP_Recv(remoteSocket, &remoteDataReceived, sizeof(remoteDataReceived)); + if (!len || !remoteSocket || len == -1) { + SPDLOG_ERROR("[GameInteractor] SDLNet_TCP_Recv: {}", SDLNet_GetError()); + break; + } + + HandleRemoteData(remoteDataReceived); + + receivedData.append(remoteDataReceived, len); + + // Proess all complete packets + size_t delimiterPos = receivedData.find('\0'); + while (delimiterPos != std::string::npos) { + // Extract the complete packet until the delimiter + std::string packet = receivedData.substr(0, delimiterPos); + // Remove the packet (including the delimiter) from the received data + receivedData.erase(0, delimiterPos + 1); + HandleRemoteJson(packet); + // Find the next delimiter + delimiterPos = receivedData.find('\0'); + } + } + + if (isRemoteInteractorConnected) { + SDLNet_TCP_Close(remoteSocket); + isRemoteInteractorConnected = false; + if (remoteDisconnectedHandler) { + remoteDisconnectedHandler(); + } + SPDLOG_INFO("[GameInteractor] Ending receiving thread..."); + } + } +} + +void GameInteractor::HandleRemoteData(char payload[512]) { + if (remoteDataHandler) { + remoteDataHandler(payload); + return; + } +} + +void GameInteractor::HandleRemoteJson(std::string payload) { + nlohmann::json jsonPayload; + try { + jsonPayload = nlohmann::json::parse(payload); + } catch (const std::exception& e) { + SPDLOG_ERROR("[GameInteractor] Failed to parse json: \n{}\n{}\n", payload, e.what()); + return; + } + + if (remoteJsonHandler) { + remoteJsonHandler(jsonPayload); + return; + } +} + +#endif diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Sail.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Sail.cpp new file mode 100644 index 000000000..a209d8c50 --- /dev/null +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Sail.cpp @@ -0,0 +1,471 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#include "GameInteractor_Sail.h" +#include +#include +#include + +template +bool IsType(const SrcType* src) { + return dynamic_cast(src) != nullptr; +} + +void GameInteractorSail::Enable() { + if (isEnabled) { + return; + } + + isEnabled = true; + GameInteractor::Instance->EnableRemoteInteractor(); + GameInteractor::Instance->RegisterRemoteJsonHandler([&](nlohmann::json payload) { + HandleRemoteJson(payload); + }); + GameInteractor::Instance->RegisterRemoteConnectedHandler([&]() { + RegisterHooks(); + }); +} + +void GameInteractorSail::Disable() { + if (!isEnabled) { + return; + } + + isEnabled = false; + GameInteractor::Instance->DisableRemoteInteractor(); +} + +void GameInteractorSail::HandleRemoteJson(nlohmann::json payload) { + SPDLOG_INFO("[GameInteractorSail] Received payload: \n{}", payload.dump()); + + nlohmann::json responsePayload; + responsePayload["type"] = "result"; + responsePayload["status"] = "failure"; + + try { + if (!payload.contains("id")) { + SPDLOG_ERROR("[GameInteractorSail] Received payload without ID"); + GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + return; + } + + responsePayload["id"] = payload["id"]; + + if (!payload.contains("type")) { + SPDLOG_ERROR("[GameInteractorSail] Received payload without type"); + GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + return; + } + + std::string payloadType = payload["type"].get(); + + if (payloadType == "command") { + if (!payload.contains("command")) { + SPDLOG_ERROR("[GameInteractorSail] Received command payload without command"); + GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + return; + } + + std::string command = payload["command"].get(); + std::reinterpret_pointer_cast(LUS::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console"))->Dispatch(command); + responsePayload["status"] = "success"; + GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + return; + } else if (payloadType == "effect") { + if (!payload.contains("effect") || !payload["effect"].contains("type")) { + SPDLOG_ERROR("[GameInteractorSail] Received effect payload without effect type"); + GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + return; + } + + std::string effectType = payload["effect"]["type"].get(); + + // Special case for "command" effect, so we can also run commands from the `simple_twitch_sail` script + if (effectType == "command") { + if (!payload["effect"].contains("command")) { + SPDLOG_ERROR("[GameInteractorSail] Received command effect payload without command"); + GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + return; + } + + std::string command = payload["effect"]["command"].get(); + std::reinterpret_pointer_cast(LUS::Context::GetInstance()->GetWindow()->GetGui()->GetGuiWindow("Console"))->Dispatch(command); + responsePayload["status"] = "success"; + GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + return; + } + + if (effectType != "apply" && effectType != "remove") { + SPDLOG_ERROR("[GameInteractorSail] Received effect payload with unknown effect type: {}", effectType); + GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + return; + } + + if (!GameInteractor::IsSaveLoaded()) { + responsePayload["status"] = "try_again"; + GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + return; + } + + GameInteractionEffectBase* giEffect = EffectFromJson(payload["effect"]); + if (giEffect) { + GameInteractionEffectQueryResult result; + if (effectType == "remove") { + if (IsType(giEffect)) { + result = dynamic_cast(giEffect)->Remove(); + } else { + result = GameInteractionEffectQueryResult::NotPossible; + } + } else { + result = giEffect->Apply(); + } + + if (result == GameInteractionEffectQueryResult::Possible) { + responsePayload["status"] = "success"; + } else if (result == GameInteractionEffectQueryResult::TemporarilyNotPossible) { + responsePayload["status"] = "try_again"; + } + GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + return; + } + } else { + SPDLOG_ERROR("[GameInteractorSail] Unknown payload type: {}", payloadType); + GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + return; + } + + // If we get here, something went wrong, send the failure response + SPDLOG_ERROR("[GameInteractorSail] Failed to handle remote JSON, sending failure response"); + GameInteractor::Instance->TransmitJsonToRemote(responsePayload); + } catch (const std::exception& e) { + SPDLOG_ERROR("[GameInteractorSail] Exception handling remote JSON: {}", e.what()); + } catch (...) { + SPDLOG_ERROR("[GameInteractorSail] Unknown exception handling remote JSON"); + } +} + +GameInteractionEffectBase* GameInteractorSail::EffectFromJson(nlohmann::json payload) { + if (!payload.contains("name")) { + return nullptr; + } + + std::string name = payload["name"].get(); + + if (name == "SetSceneFlag") { + auto effect = new GameInteractionEffect::SetSceneFlag(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + effect->parameters[1] = payload["parameters"][1].get(); + effect->parameters[2] = payload["parameters"][2].get(); + } + return effect; + } else if (name == "UnsetSceneFlag") { + auto effect = new GameInteractionEffect::UnsetSceneFlag(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + effect->parameters[1] = payload["parameters"][1].get(); + effect->parameters[2] = payload["parameters"][2].get(); + } + return effect; + } else if (name == "SetFlag") { + auto effect = new GameInteractionEffect::SetFlag(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + effect->parameters[1] = payload["parameters"][1].get(); + } + return effect; + } else if (name == "UnsetFlag") { + auto effect = new GameInteractionEffect::UnsetFlag(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + effect->parameters[1] = payload["parameters"][1].get(); + } + return effect; + } else if (name == "ModifyHeartContainers") { + auto effect = new GameInteractionEffect::ModifyHeartContainers(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + } + return effect; + } else if (name == "FillMagic") { + return new GameInteractionEffect::FillMagic(); + } else if (name == "EmptyMagic") { + return new GameInteractionEffect::EmptyMagic(); + } else if (name == "ModifyRupees") { + auto effect = new GameInteractionEffect::ModifyRupees(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + } + return effect; + } else if (name == "NoUI") { + return new GameInteractionEffect::NoUI(); + } else if (name == "ModifyGravity") { + auto effect = new GameInteractionEffect::ModifyGravity(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + } + return effect; + } else if (name == "ModifyHealth") { + auto effect = new GameInteractionEffect::ModifyHealth(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + } + return effect; + } else if (name == "SetPlayerHealth") { + auto effect = new GameInteractionEffect::SetPlayerHealth(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + } + return effect; + } else if (name == "FreezePlayer") { + return new GameInteractionEffect::FreezePlayer(); + } else if (name == "BurnPlayer") { + return new GameInteractionEffect::BurnPlayer(); + } else if (name == "ElectrocutePlayer") { + return new GameInteractionEffect::ElectrocutePlayer(); + } else if (name == "KnockbackPlayer") { + auto effect = new GameInteractionEffect::KnockbackPlayer(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + } + return effect; + } else if (name == "ModifyLinkSize") { + auto effect = new GameInteractionEffect::ModifyLinkSize(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + } + return effect; + } else if (name == "InvisibleLink") { + return new GameInteractionEffect::InvisibleLink(); + } else if (name == "PacifistMode") { + return new GameInteractionEffect::PacifistMode(); + } else if (name == "DisableZTargeting") { + return new GameInteractionEffect::DisableZTargeting(); + } else if (name == "WeatherRainstorm") { + return new GameInteractionEffect::WeatherRainstorm(); + } else if (name == "ReverseControls") { + return new GameInteractionEffect::ReverseControls(); + } else if (name == "ForceEquipBoots") { + auto effect = new GameInteractionEffect::ForceEquipBoots(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + } + return effect; + } else if (name == "ModifyRunSpeedModifier") { + auto effect = new GameInteractionEffect::ModifyRunSpeedModifier(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + } + return effect; + } else if (name == "OneHitKO") { + return new GameInteractionEffect::OneHitKO(); + } else if (name == "ModifyDefenseModifier") { + auto effect = new GameInteractionEffect::ModifyDefenseModifier(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + } + return effect; + } else if (name == "GiveOrTakeShield") { + auto effect = new GameInteractionEffect::GiveOrTakeShield(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + } + return effect; + } else if (name == "TeleportPlayer") { + auto effect = new GameInteractionEffect::TeleportPlayer(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + } + return effect; + } else if (name == "ClearAssignedButtons") { + auto effect = new GameInteractionEffect::ClearAssignedButtons(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + } + return effect; + } else if (name == "SetTimeOfDay") { + auto effect = new GameInteractionEffect::SetTimeOfDay(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + } + return effect; + } else if (name == "SetCollisionViewer") { + return new GameInteractionEffect::SetCollisionViewer(); + } else if (name == "SetCosmeticsColor") { + auto effect = new GameInteractionEffect::SetCosmeticsColor(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + effect->parameters[1] = payload["parameters"][1].get(); + } + return effect; + } else if (name == "RandomizeCosmetics") { + return new GameInteractionEffect::RandomizeCosmetics(); + } else if (name == "PressButton") { + auto effect = new GameInteractionEffect::PressButton(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + } + return effect; + } else if (name == "PressRandomButton") { + auto effect = new GameInteractionEffect::PressRandomButton(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + } + return effect; + } else if (name == "AddOrTakeAmmo") { + auto effect = new GameInteractionEffect::AddOrTakeAmmo(); + if (payload.contains("parameters")) { + effect->parameters[0] = payload["parameters"][0].get(); + effect->parameters[1] = payload["parameters"][1].get(); + } + return effect; + } else if (name == "RandomBombFuseTimer") { + return new GameInteractionEffect::RandomBombFuseTimer(); + } else if (name == "DisableLedgeGrabs") { + return new GameInteractionEffect::DisableLedgeGrabs(); + } else if (name == "RandomWind") { + return new GameInteractionEffect::RandomWind(); + } else if (name == "RandomBonks") { + return new GameInteractionEffect::RandomBonks(); + } else if (name == "PlayerInvincibility") { + return new GameInteractionEffect::PlayerInvincibility(); + } else if (name == "SlipperyFloor") { + return new GameInteractionEffect::SlipperyFloor(); + } else { + SPDLOG_INFO("[GameInteractorSail] Unknown effect name: {}", name); + return nullptr; + } +} + +// Workaround until we have a way to unregister hooks +static bool hasRegisteredHooks = false; + +void GameInteractorSail::RegisterHooks() { + if (hasRegisteredHooks) { + return; + } + hasRegisteredHooks = true; + + GameInteractor::Instance->RegisterGameHook([](int32_t sceneNum) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; + + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnTransitionEnd"; + payload["hook"]["sceneNum"] = sceneNum; + + GameInteractor::Instance->TransmitJsonToRemote(payload); + }); + GameInteractor::Instance->RegisterGameHook([](int32_t fileNum) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; + + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnLoadGame"; + payload["hook"]["fileNum"] = fileNum; + + GameInteractor::Instance->TransmitJsonToRemote(payload); + }); + GameInteractor::Instance->RegisterGameHook([](int32_t fileNum) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; + + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnExitGame"; + payload["hook"]["fileNum"] = fileNum; + + GameInteractor::Instance->TransmitJsonToRemote(payload); + }); + GameInteractor::Instance->RegisterGameHook([](GetItemEntry itemEntry) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !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; + + GameInteractor::Instance->TransmitJsonToRemote(payload); + }); + GameInteractor::Instance->RegisterGameHook([](void* refActor) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; + + Actor* actor = (Actor*)refActor; + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnEnemyDefeat"; + payload["hook"]["actorId"] = actor->id; + payload["hook"]["params"] = actor->params; + + GameInteractor::Instance->TransmitJsonToRemote(payload); + }); + GameInteractor::Instance->RegisterGameHook([](void* refActor) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !GameInteractor::IsSaveLoaded()) return; + + Actor* actor = (Actor*)refActor; + nlohmann::json payload; + payload["id"] = std::rand(); + payload["type"] = "hook"; + payload["hook"]["type"] = "OnActorInit"; + payload["hook"]["actorId"] = actor->id; + payload["hook"]["params"] = actor->params; + + GameInteractor::Instance->TransmitJsonToRemote(payload); + }); + GameInteractor::Instance->RegisterGameHook([](int16_t flagType, int16_t flag) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !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; + + GameInteractor::Instance->TransmitJsonToRemote(payload); + }); + GameInteractor::Instance->RegisterGameHook([](int16_t flagType, int16_t flag) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !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; + + GameInteractor::Instance->TransmitJsonToRemote(payload); + }); + GameInteractor::Instance->RegisterGameHook([](int16_t sceneNum, int16_t flagType, int16_t flag) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !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; + + GameInteractor::Instance->TransmitJsonToRemote(payload); + }); + GameInteractor::Instance->RegisterGameHook([](int16_t sceneNum, int16_t flagType, int16_t flag) { + if (!GameInteractor::Instance->isRemoteInteractorConnected || !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; + + GameInteractor::Instance->TransmitJsonToRemote(payload); + }); +} + +#endif diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Sail.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Sail.h new file mode 100644 index 000000000..cb90c65c7 --- /dev/null +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Sail.h @@ -0,0 +1,29 @@ +#ifdef ENABLE_REMOTE_CONTROL + +#ifdef __cplusplus +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "./GameInteractor.h" + +class GameInteractorSail { + private: + bool isEnabled; + + void HandleRemoteJson(nlohmann::json payload); + GameInteractionEffectBase* EffectFromJson(nlohmann::json payload); + void RegisterHooks(); + public: + static GameInteractorSail* Instance; + void Enable(); + void Disable(); +}; +#endif +#endif diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 1c7169ccf..3068da96f 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -81,9 +81,11 @@ #include "SohGui.hpp" #include "ActorDB.h" -#ifdef ENABLE_CROWD_CONTROL +#ifdef ENABLE_REMOTE_CONTROL #include "Enhancements/crowd-control/CrowdControl.h" +#include "Enhancements/game-interactor/GameInteractor_Sail.h" CrowdControl* CrowdControl::Instance; +GameInteractorSail* GameInteractorSail::Instance; #endif #include "Enhancements/mods.h" @@ -1097,7 +1099,12 @@ extern "C" void InitOTR() { SpeechSynthesizer::Instance = new SAPISpeechSynthesizer(); SpeechSynthesizer::Instance->Init(); #endif - + +#ifdef ENABLE_REMOTE_CONTROL + CrowdControl::Instance = new CrowdControl(); + GameInteractorSail::Instance = new GameInteractorSail(); +#endif + clearMtx = (uintptr_t)&gMtxClear; OTRMessage_Init(); OTRAudio_Init(); @@ -1117,13 +1124,17 @@ extern "C" void InitOTR() { } srand(now); -#ifdef ENABLE_CROWD_CONTROL - CrowdControl::Instance = new CrowdControl(); - CrowdControl::Instance->Init(); - if (CVarGetInteger("gCrowdControl", 0)) { - CrowdControl::Instance->Enable(); - } else { - CrowdControl::Instance->Disable(); +#ifdef ENABLE_REMOTE_CONTROL + SDLNet_Init(); + if (CVarGetInteger("gRemote.Enabled", 0)) { + switch (CVarGetInteger("gRemote.Scheme", GI_SCHEME_SAIL)) { + case GI_SCHEME_SAIL: + GameInteractorSail::Instance->Enable(); + break; + case GI_SCHEME_CROWD_CONTROL: + CrowdControl::Instance->Enable(); + break; + } } #endif @@ -1140,9 +1151,18 @@ extern "C" void SaveManager_ThreadPoolWait() { extern "C" void DeinitOTR() { SaveManager_ThreadPoolWait(); OTRAudio_Exit(); -#ifdef ENABLE_CROWD_CONTROL - CrowdControl::Instance->Disable(); - CrowdControl::Instance->Shutdown(); +#ifdef ENABLE_REMOTE_CONTROL + if (CVarGetInteger("gRemote.Enabled", 0)) { + switch (CVarGetInteger("gRemote.Scheme", GI_SCHEME_SAIL)) { + case GI_SCHEME_SAIL: + GameInteractorSail::Instance->Disable(); + break; + case GI_SCHEME_CROWD_CONTROL: + CrowdControl::Instance->Disable(); + break; + } + } + SDLNet_Quit(); #endif // Destroying gui here because we have shared ptrs to LUS objects which output to SPDLOG which is destroyed before these shared ptrs. diff --git a/soh/soh/SohGui.cpp b/soh/soh/SohGui.cpp index c30f52db2..6dba67a05 100644 --- a/soh/soh/SohGui.cpp +++ b/soh/soh/SohGui.cpp @@ -31,8 +31,9 @@ #include "soh/resource/type/Skeleton.h" #include "libultraship/libultraship.h" -#ifdef ENABLE_CROWD_CONTROL +#ifdef ENABLE_REMOTE_CONTROL #include "Enhancements/crowd-control/CrowdControl.h" +#include "Enhancements/game-interactor/GameInteractor_Sail.h" #endif #include "Enhancements/game-interactor/GameInteractor.h" diff --git a/soh/soh/SohMenuBar.cpp b/soh/soh/SohMenuBar.cpp index 6c3cc1749..31d19a8c1 100644 --- a/soh/soh/SohMenuBar.cpp +++ b/soh/soh/SohMenuBar.cpp @@ -1,5 +1,6 @@ #include "SohMenuBar.h" #include "ImGui/imgui.h" +#include "regex" #include "public/bridge/consolevariablebridge.h" #include #include "UIWidgets.hpp" @@ -10,8 +11,9 @@ #include "soh/Enhancements/presets.h" #include "soh/Enhancements/mods.h" #include "Enhancements/cosmetics/authenticGfxPatches.h" -#ifdef ENABLE_CROWD_CONTROL +#ifdef ENABLE_REMOTE_CONTROL #include "Enhancements/crowd-control/CrowdControl.h" +#include "Enhancements/game-interactor/GameInteractor_Sail.h" #endif @@ -1116,17 +1118,6 @@ void DrawEnhancementsMenu() { UIWidgets::Spacer(0); if (ImGui::BeginMenu("Extra Modes")) { - #ifdef ENABLE_CROWD_CONTROL - if (UIWidgets::PaddedEnhancementCheckbox("Crowd Control", "gCrowdControl", false, false)) { - if (CVarGetInteger("gCrowdControl", 0)) { - CrowdControl::Instance->Enable(); - } else { - CrowdControl::Instance->Disable(); - } - } - UIWidgets::Tooltip("Will attempt to connect to the Crowd Control server. Check out crowdcontrol.live for more information."); - #endif - UIWidgets::PaddedText("Mirrored World", true, false); if (UIWidgets::EnhancementCombobox("gMirroredWorldMode", mirroredWorldModes, MIRRORED_WORLD_OFF) && gPlayState != NULL) { UpdateMirrorModeState(gPlayState->sceneNum); @@ -1520,6 +1511,135 @@ void DrawDeveloperToolsMenu() { } } +bool isStringEmpty(std::string str) { + // Remove spaces at the beginning of the string + std::string::size_type start = str.find_first_not_of(' '); + // Remove spaces at the end of the string + std::string::size_type end = str.find_last_not_of(' '); + + // Check if the string is empty after stripping spaces + if (start == std::string::npos || end == std::string::npos) + return true; // The string is empty + else + return false; // The string is not empty +} + +#ifdef ENABLE_REMOTE_CONTROL +void DrawRemoteControlMenu() { + if (ImGui::BeginMenu("Network")) { + static std::string ip = CVarGetString("gRemote.IP", "127.0.0.1"); + static uint16_t port = CVarGetInteger("gRemote.Port", 43384); + bool isFormValid = !isStringEmpty(CVarGetString("gRemote.IP", "127.0.0.1")) && port > 1024 && port < 65535; + + const char* remoteOptions[2] = { "Sail", "Crowd Control"}; + + ImGui::BeginDisabled(GameInteractor::Instance->isRemoteInteractorEnabled); + ImGui::Text("Remote Interaction Scheme"); + if (UIWidgets::EnhancementCombobox("gRemote.Scheme", remoteOptions, GI_SCHEME_SAIL)) { + switch (CVarGetInteger("gRemote.Scheme", GI_SCHEME_SAIL)) { + case GI_SCHEME_SAIL: + case GI_SCHEME_CROWD_CONTROL: + CVarSetString("gRemote.IP", "127.0.0.1"); + CVarSetInteger("gRemote.Port", 43384); + ip = "127.0.0.1"; + port = 43384; + break; + } + LUS::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + switch (CVarGetInteger("gRemote.Scheme", GI_SCHEME_SAIL)) { + case GI_SCHEME_SAIL: + UIWidgets::InsertHelpHoverText( + "Sail is a networking protocol designed to facilitate remote " + "control of the Ship of Harkinian client. It is intended to " + "be utilized alongside a Sail server, for which we provide a " + "few straightforward implementations on our GitHub. The current " + "implementations available allow integration with Twitch chat " + "and SAMMI Bot, feel free to contribute your own!\n" + "\n" + "Click the question mark to copy the link to the Sail Github " + "page to your clipboard." + ); + if (ImGui::IsItemClicked()) { + ImGui::SetClipboardText("https://github.com/HarbourMasters/sail"); + } + break; + case GI_SCHEME_CROWD_CONTROL: + UIWidgets::InsertHelpHoverText( + "Crowd Control is a platform that allows viewers to interact " + "with a streamer's game in real time.\n" + "\n" + "Click the question mark to copy the link to the Crowd Control " + "website to your clipboard." + ); + if (ImGui::IsItemClicked()) { + ImGui::SetClipboardText("https://crowdcontrol.live"); + } + break; + } + + ImGui::Text("Remote IP & Port"); + if (ImGui::InputText("##gRemote.IP", (char*)ip.c_str(), ip.capacity() + 1)) { + CVarSetString("gRemote.IP", ip.c_str()); + LUS::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + + ImGui::SameLine(); + ImGui::PushItemWidth(ImGui::GetFontSize() * 5); + if (ImGui::InputScalar("##gRemote.Port", ImGuiDataType_U16, &port)) { + CVarSetInteger("gRemote.Port", port); + LUS::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + } + + ImGui::PopItemWidth(); + ImGui::EndDisabled(); + + ImGui::Spacing(); + + ImGui::BeginDisabled(!isFormValid); + const char* buttonLabel = GameInteractor::Instance->isRemoteInteractorEnabled ? "Disable" : "Enable"; + if (ImGui::Button(buttonLabel, ImVec2(-1.0f, 0.0f))) { + if (GameInteractor::Instance->isRemoteInteractorEnabled) { + CVarSetInteger("gRemote.Enabled", 0); + LUS::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + switch (CVarGetInteger("gRemote.Scheme", GI_SCHEME_SAIL)) { + case GI_SCHEME_SAIL: + GameInteractorSail::Instance->Disable(); + break; + case GI_SCHEME_CROWD_CONTROL: + CrowdControl::Instance->Disable(); + break; + } + } else { + CVarSetInteger("gRemote.Enabled", 1); + LUS::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick(); + switch (CVarGetInteger("gRemote.Scheme", GI_SCHEME_SAIL)) { + case GI_SCHEME_SAIL: + GameInteractorSail::Instance->Enable(); + break; + case GI_SCHEME_CROWD_CONTROL: + CrowdControl::Instance->Enable(); + break; + } + } + } + ImGui::EndDisabled(); + + if (GameInteractor::Instance->isRemoteInteractorEnabled) { + ImGui::Spacing(); + if (GameInteractor::Instance->isRemoteInteractorConnected) { + ImGui::Text("Connected"); + } else { + ImGui::Text("Connecting..."); + } + } + + ImGui::Dummy(ImVec2(0.0f, 0.0f)); + ImGui::EndMenu(); + } +} +#endif + extern std::shared_ptr mRandomizerSettingsWindow; extern std::shared_ptr mItemTrackerWindow; extern std::shared_ptr mItemTrackerSettingsWindow; @@ -1658,6 +1778,12 @@ void SohMenuBar::DrawElement() { ImGui::SetCursorPosY(0.0f); + #ifdef ENABLE_REMOTE_CONTROL + DrawRemoteControlMenu(); + + ImGui::SetCursorPosY(0.0f); + #endif + DrawRandomizerMenu(); ImGui::PopStyleVar(1); From f655ab592d1a0dcebd8831c70808ff76de29f3b1 Mon Sep 17 00:00:00 2001 From: Adam Bird Date: Sun, 17 Dec 2023 15:42:34 -0500 Subject: [PATCH 13/24] Re-implement Pause menu Dungeon map texture effects (#3496) * first pass implement dungeon maps * wrap up map dungeon implementation * add comments and enums, rename vars * bump lus --- .../ovl_kaleido_scope/z_kaleido_map_PAL.c | 13 +- .../misc/ovl_kaleido_scope/z_kaleido_scope.h | 4 + .../ovl_kaleido_scope/z_kaleido_scope_PAL.c | 133 +++++++++++++++++- 3 files changed, 138 insertions(+), 12 deletions(-) diff --git a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_map_PAL.c b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_map_PAL.c index 238a1e4e2..28723d3d0 100644 --- a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_map_PAL.c +++ b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_map_PAL.c @@ -343,7 +343,7 @@ void KaleidoScope_DrawDungeonMap(PlayState* play, GraphicsContext* gfxCtx) { // Offset the U value of each vertex to be in the mirror boundary for the map textures if (mirroredWorld) { for (size_t i = 0; i < 8; i++) { - pauseCtx->mapPageVtx[60 + i].v.tc[0] += 48 << 5; + pauseCtx->mapPageVtx[60 + i].v.tc[0] += MAP_48x85_TEX_WIDTH << 5; } } @@ -353,8 +353,9 @@ void KaleidoScope_DrawDungeonMap(PlayState* play, GraphicsContext* gfxCtx) { gSPInvalidateTexCache(POLY_KAL_DISP++, interfaceCtx->mapSegment[0]); gSPInvalidateTexCache(POLY_KAL_DISP++, interfaceCtx->mapSegment[1]); - gDPLoadTextureBlock_4b(POLY_KAL_DISP++, interfaceCtx->mapSegmentName[0], G_IM_FMT_CI, 48, 85, 0, G_TX_WRAP | mirrorMode, - G_TX_WRAP | G_TX_NOMIRROR, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); + gDPLoadTextureBlock_4b(POLY_KAL_DISP++, interfaceCtx->mapSegmentName[0], G_IM_FMT_CI, MAP_48x85_TEX_WIDTH, + MAP_48x85_TEX_HEIGHT, 0, G_TX_WRAP | mirrorMode, G_TX_WRAP | G_TX_NOMIRROR, G_TX_NOMASK, + G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); // Swap vertices to render left half on the right and vice-versa if (mirroredWorld) { @@ -363,9 +364,9 @@ void KaleidoScope_DrawDungeonMap(PlayState* play, GraphicsContext* gfxCtx) { gSP1Quadrangle(POLY_KAL_DISP++, 0, 2, 3, 1, 0); } - gDPLoadTextureBlock_4b(POLY_KAL_DISP++, interfaceCtx->mapSegmentName[1], G_IM_FMT_CI, 48, 85, 0, - G_TX_WRAP | mirrorMode, G_TX_WRAP | G_TX_NOMIRROR, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, - G_TX_NOLOD); + gDPLoadTextureBlock_4b(POLY_KAL_DISP++, interfaceCtx->mapSegmentName[1], G_IM_FMT_CI, MAP_48x85_TEX_WIDTH, + MAP_48x85_TEX_HEIGHT, 0, G_TX_WRAP | mirrorMode, G_TX_WRAP | G_TX_NOMIRROR, G_TX_NOMASK, + G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); if (mirroredWorld) { gSP1Quadrangle(POLY_KAL_DISP++, 0, 2, 3, 1, 0); diff --git a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope.h b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope.h index 6f4ed7ad2..8c214d9b0 100644 --- a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope.h +++ b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope.h @@ -13,6 +13,10 @@ extern u8 gItemAgeReqs[]; extern u8 gAreaGsFlags[]; extern bool gSelectingMask; +#define MAP_48x85_TEX_WIDTH 48 +#define MAP_48x85_TEX_HEIGHT 85 +#define MAP_48x85_TEX_SIZE ((MAP_48x85_TEX_WIDTH * MAP_48x85_TEX_HEIGHT) / 2) // 48x85 CI4 texture + #define AGE_REQ_ADULT LINK_AGE_ADULT #define AGE_REQ_CHILD LINK_AGE_CHILD #define AGE_REQ_NONE 9 diff --git a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c index 35490f079..877ea7e6f 100644 --- a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c +++ b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c @@ -1205,6 +1205,8 @@ Gfx* KaleidoScope_DrawPageSections(Gfx* gfx, Vtx* vertices, void** textures) { return gfx; } +static uint8_t mapBlendMask[MAP_48x85_TEX_WIDTH * MAP_48x85_TEX_HEIGHT]; + void KaleidoScope_DrawPages(PlayState* play, GraphicsContext* gfxCtx) { static Color_RGB8 D_8082ACF4[12] = { { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 }, { 255, 255, 0 }, { 0, 0, 0 }, @@ -1373,6 +1375,10 @@ void KaleidoScope_DrawPages(PlayState* play, GraphicsContext* gfxCtx) { } } + // Need to invalidate the blend mask every frame. Ideally this would be done in KaleidoScope_DrawDungeonMap + // but the reference is not shared between files + gSPInvalidateTexCache(POLY_KAL_DISP++, mapBlendMask); + if (pauseCtx->pageIndex) { // pageIndex != PAUSE_ITEM gDPPipeSync(OVERLAY_DISP++); gDPSetCombineMode(OVERLAY_DISP++, G_CC_MODULATEIA, G_CC_MODULATEIA); @@ -3315,13 +3321,118 @@ void KaleidoScope_UpdateCursorSize(PauseContext* pauseCtx) { pauseCtx->cursorVtx[14].v.ob[1] = pauseCtx->cursorVtx[15].v.ob[1] = pauseCtx->cursorVtx[12].v.ob[1] - 16; } +// Modifed map texture buffers for registered blend effects and the room indicator color +static uint8_t mapLeftTexModified[MAP_48x85_TEX_SIZE]; +static uint8_t mapRightTexModified[MAP_48x85_TEX_SIZE]; +static uint8_t* mapLeftTexModifiedRaw = NULL; +static uint8_t* mapRightTexModifiedRaw = NULL; + +// Load dungeon maps into the interface context +// SoH [General] - Modified to account for our resource system and HD textures void KaleidoScope_LoadDungeonMap(PlayState* play) { InterfaceContext* interfaceCtx = &play->interfaceCtx; + // Free old textures + if (mapLeftTexModifiedRaw != NULL) { + free(mapLeftTexModifiedRaw); + mapLeftTexModifiedRaw = NULL; + } + if (mapRightTexModifiedRaw != NULL) { + free(mapRightTexModifiedRaw); + mapRightTexModifiedRaw = NULL; + } + + // Unload original textures to bypass cache result for lookups + ResourceMgr_UnloadOriginalWhenAltExists(sDungeonMapTexs[R_MAP_TEX_INDEX]); + ResourceMgr_UnloadOriginalWhenAltExists(sDungeonMapTexs[R_MAP_TEX_INDEX + 1]); + interfaceCtx->mapSegmentName[0] = sDungeonMapTexs[R_MAP_TEX_INDEX]; interfaceCtx->mapSegmentName[1] = sDungeonMapTexs[R_MAP_TEX_INDEX + 1]; - interfaceCtx->mapSegment[0] = ResourceGetDataByName(sDungeonMapTexs[R_MAP_TEX_INDEX]); - interfaceCtx->mapSegment[1] = ResourceGetDataByName(sDungeonMapTexs[R_MAP_TEX_INDEX + 1]); + + // When the texture is HD (raw) we need to copy a dynamic amount of data + // Otherwise the original asset has a static size + if (ResourceMgr_TexIsRaw(interfaceCtx->mapSegmentName[0])) { + u32 width = ResourceGetTexWidthByName(interfaceCtx->mapSegmentName[0]); + u32 height = ResourceGetTexHeightByName(interfaceCtx->mapSegmentName[0]); + size_t size = (width * height) / 2; // account for CI4 size + + // Resource size being larger than the calculated CI size means it is most likely not a CI4 texture + // Abort early end undo the blended effect by clearing the mask to avoid crashing + if (size < ResourceGetTexSizeByName(interfaceCtx->mapSegmentName[0])) { + if (mapBlendMask[0] != 0) { + for (size_t i = 0; i < ARRAY_COUNT(mapBlendMask); i++) { + mapBlendMask[i] = 0; + } + } + + interfaceCtx->mapSegment[0] = NULL; + interfaceCtx->mapSegment[1] = NULL; + + Gfx_RegisterBlendedTexture(interfaceCtx->mapSegmentName[0], mapBlendMask, NULL); + Gfx_RegisterBlendedTexture(interfaceCtx->mapSegmentName[1], mapBlendMask, NULL); + return; + } + + u8* map1TexRaw = ResourceGetDataByName(interfaceCtx->mapSegmentName[0]); + u8* map2TexRaw = ResourceGetDataByName(interfaceCtx->mapSegmentName[1]); + + mapLeftTexModifiedRaw = malloc(size); + mapRightTexModifiedRaw = malloc(size); + + memcpy(mapLeftTexModifiedRaw, map1TexRaw, size); + memcpy(mapRightTexModifiedRaw, map2TexRaw, size); + + interfaceCtx->mapSegment[0] = mapLeftTexModifiedRaw; + interfaceCtx->mapSegment[1] = mapRightTexModifiedRaw; + } else { + u8* map1Tex = ResourceGetDataByName(interfaceCtx->mapSegmentName[0]); + u8* map2Tex = ResourceGetDataByName(interfaceCtx->mapSegmentName[1]); + + memcpy(mapLeftTexModified, map1Tex, MAP_48x85_TEX_SIZE); + memcpy(mapRightTexModified, map2Tex, MAP_48x85_TEX_SIZE); + + interfaceCtx->mapSegment[0] = mapLeftTexModified; + interfaceCtx->mapSegment[1] = mapRightTexModified; + } + + // Mark and register the blend mask for the copied textures + if (mapBlendMask[0] != 1) { + for (size_t i = 0; i < ARRAY_COUNT(mapBlendMask); i++) { + mapBlendMask[i] = 1; + } + } + + Gfx_RegisterBlendedTexture(interfaceCtx->mapSegmentName[0], mapBlendMask, interfaceCtx->mapSegment[0]); + Gfx_RegisterBlendedTexture(interfaceCtx->mapSegmentName[1], mapBlendMask, interfaceCtx->mapSegment[1]); +} + +static uint8_t registeredDungeonMapTextureHook = false; + +void KaleidoScope_RegisterUpdatedDungeonMapTexture() { + if (gPlayState == NULL) { + return; + } + + PauseContext* pauseCtx = &gPlayState->pauseCtx; + + // Kaleido is not open in a dungeon so there is nothing to do + if (R_PAUSE_MENU_MODE < 3 || pauseCtx->state < 4 || pauseCtx->state > 7 || !sInDungeonScene) { + return; + } + + KaleidoScope_UpdateDungeonMap(gPlayState); + + // KaleidoScope_UpdateDungeonMap will update the palette index for the current floor if the cursor is on the floor + // If the player toggles alt assets while the cursor is not in the floor level, then we handle the palette index here + if (gPlayState->sceneNum >= SCENE_DEKU_TREE && gPlayState->sceneNum <= SCENE_TREASURE_BOX_SHOP && + (VREG(30) + 3) == pauseCtx->dungeonMapSlot && (VREG(30) + 3) != pauseCtx->cursorPoint[PAUSE_MAP]) { + + InterfaceContext* interfaceCtx = &gPlayState->interfaceCtx; + int32_t size = ResourceGetTexSizeByName(interfaceCtx->mapSegmentName[0]); + + KaleidoScope_OverridePalIndexCI4(interfaceCtx->mapSegment[0], size, interfaceCtx->mapPaletteIndex, 14); + KaleidoScope_OverridePalIndexCI4(interfaceCtx->mapSegment[1], size, interfaceCtx->mapPaletteIndex, 14); + } } void KaleidoScope_UpdateDungeonMap(PlayState* play) { @@ -3333,19 +3444,29 @@ void KaleidoScope_UpdateDungeonMap(PlayState* play) { KaleidoScope_LoadDungeonMap(play); Map_SetFloorPalettesData(play, pauseCtx->dungeonMapSlot - 3); + s32 size = MAP_48x85_TEX_SIZE; + + if (ResourceMgr_TexIsRaw(interfaceCtx->mapSegmentName[0])) { + size = ResourceGetTexSizeByName(interfaceCtx->mapSegmentName[0]); + } + if ((play->sceneNum >= SCENE_DEKU_TREE) && (play->sceneNum <= SCENE_TREASURE_BOX_SHOP)) { if ((VREG(30) + 3) == pauseCtx->cursorPoint[PAUSE_MAP]) { - // HDTODO: Handle Runtime Modified Textures (HD) - KaleidoScope_OverridePalIndexCI4(interfaceCtx->mapSegment[0], 2040, interfaceCtx->mapPaletteIndex, 14); + KaleidoScope_OverridePalIndexCI4(interfaceCtx->mapSegment[0], size, interfaceCtx->mapPaletteIndex, 14); } } if ((play->sceneNum >= SCENE_DEKU_TREE) && (play->sceneNum <= SCENE_TREASURE_BOX_SHOP)) { if ((VREG(30) + 3) == pauseCtx->cursorPoint[PAUSE_MAP]) { - // HDTODO: Handle Runtime Modified Textures (HD) - KaleidoScope_OverridePalIndexCI4(interfaceCtx->mapSegment[1], 2040, interfaceCtx->mapPaletteIndex, 14); + KaleidoScope_OverridePalIndexCI4(interfaceCtx->mapSegment[1], size, interfaceCtx->mapPaletteIndex, 14); } } + + // Register alt listener to update the blended dungeon map textures on alt toggle + if (!registeredDungeonMapTextureHook) { + registeredDungeonMapTextureHook = true; + GameInteractor_RegisterOnAssetAltChange(KaleidoScope_RegisterUpdatedDungeonMapTexture); + } } void KaleidoScope_Update(PlayState* play) From b5caf33a9b8df77d5628eef8fd9391409642392c Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Mon, 18 Dec 2023 03:19:30 +0000 Subject: [PATCH 14/24] Initialize GameInteractor before SaveManager so it can correctly set up a hook (#3535) --- soh/soh/OTRGlobals.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index a789aa8d9..4c564afde 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -1037,9 +1037,9 @@ extern "C" void InitOTR() { OTRGlobals::Instance = new OTRGlobals(); CustomMessageManager::Instance = new CustomMessageManager(); ItemTableManager::Instance = new ItemTableManager(); + GameInteractor::Instance = new GameInteractor(); SaveManager::Instance = new SaveManager(); SohGui::SetupGuiElements(); - GameInteractor::Instance = new GameInteractor(); AudioCollection::Instance = new AudioCollection(); ActorDB::Instance = new ActorDB(); #ifdef __APPLE__ From 865bcc57a7a565b16ce60d2630da736811580760 Mon Sep 17 00:00:00 2001 From: inspectredc <78732756+inspectredc@users.noreply.github.com> Date: Mon, 18 Dec 2023 03:20:09 +0000 Subject: [PATCH 15/24] Fix Logical Error With Darunias Door Entrance (#3529) --- .../3drando/location_access/locacc_death_mountain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/randomizer/3drando/location_access/locacc_death_mountain.cpp b/soh/soh/Enhancements/randomizer/3drando/location_access/locacc_death_mountain.cpp index 8988b045b..19960f1be 100644 --- a/soh/soh/Enhancements/randomizer/3drando/location_access/locacc_death_mountain.cpp +++ b/soh/soh/Enhancements/randomizer/3drando/location_access/locacc_death_mountain.cpp @@ -104,7 +104,7 @@ void AreaTable_Init_DeathMountain() { Entrance(DEATH_MOUNTAIN_TRAIL, {[]{return true;}}), Entrance(GC_WOODS_WARP, {[]{return GCWoodsWarpOpen;}}), Entrance(GC_SHOP, {[]{return (IsAdult && StopGCRollingGoronAsAdult) || (IsChild && (CanBlastOrSmash || GoronBracelet || GoronCityChildFire || CanUse(BOW)));}}), - Entrance(GC_DARUNIAS_CHAMBER, {[]{return (IsAdult && StopGCRollingGoronAsAdult) || GCDaruniasDoorOpenChild;}}), + Entrance(GC_DARUNIAS_CHAMBER, {[]{return (IsAdult && StopGCRollingGoronAsAdult) || (IsChild && GCDaruniasDoorOpenChild);}}), Entrance(GC_GROTTO_PLATFORM, {[]{return IsAdult && ((CanPlay(SongOfTime) && ((EffectiveHealth > 2) || CanUse(GORON_TUNIC) || CanUse(LONGSHOT) || CanUse(NAYRUS_LOVE))) || (EffectiveHealth > 1 && CanUse(GORON_TUNIC) && CanUse(HOOKSHOT)) || (CanUse(NAYRUS_LOVE) && CanUse(HOOKSHOT)) || (EffectiveHealth > 2 && CanUse(HOOKSHOT) && LogicGoronCityGrotto));}}), }); From f607afc7547655a8a75f849453545db9777ad2c1 Mon Sep 17 00:00:00 2001 From: inspectredc <78732756+inspectredc@users.noreply.github.com> Date: Mon, 18 Dec 2023 03:46:35 +0000 Subject: [PATCH 16/24] Add player state dead check to Player_UseTunicBoots (#3530) * Add player state dead check to Player_UseTunicBoots * Update soh/src/overlays/actors/ovl_player_actor/z_player.c --------- Co-authored-by: Garrett Cox --- soh/src/overlays/actors/ovl_player_actor/z_player.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/soh/src/overlays/actors/ovl_player_actor/z_player.c b/soh/src/overlays/actors/ovl_player_actor/z_player.c index 52dc2a3e4..ca7333497 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -10629,7 +10629,14 @@ void Player_UseTunicBoots(Player* this, PlayState* play) { s32 i; s32 item; s32 actionParam; - if (!(this->stateFlags1 & PLAYER_STATE1_INPUT_DISABLED || this->stateFlags1 & PLAYER_STATE1_IN_ITEM_CS || this->stateFlags1 & PLAYER_STATE1_IN_CUTSCENE || this->stateFlags1 & PLAYER_STATE1_TEXT_ON_SCREEN || this->stateFlags2 & PLAYER_STATE2_OCARINA_PLAYING)) { + if (!( + this->stateFlags1 & PLAYER_STATE1_INPUT_DISABLED || + this->stateFlags1 & PLAYER_STATE1_IN_ITEM_CS || + this->stateFlags1 & PLAYER_STATE1_IN_CUTSCENE || + this->stateFlags1 & PLAYER_STATE1_TEXT_ON_SCREEN || + this->stateFlags1 & PLAYER_STATE1_DEAD || + this->stateFlags2 & PLAYER_STATE2_OCARINA_PLAYING + )) { for (i = 0; i < ARRAY_COUNT(D_80854388); i++) { if (CHECK_BTN_ALL(sControlInput->press.button, D_80854388[i])) { break; From ef910a02f7b370091130a3617bd8dd51d91becbc Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Mon, 18 Dec 2023 03:47:02 +0000 Subject: [PATCH 17/24] Remove use of static variable in en_box (#3536) --- soh/src/overlays/actors/ovl_En_Box/z_en_box.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/soh/src/overlays/actors/ovl_En_Box/z_en_box.c b/soh/src/overlays/actors/ovl_En_Box/z_en_box.c index 9e8705c33..4f1ef7362 100644 --- a/soh/src/overlays/actors/ovl_En_Box/z_en_box.c +++ b/soh/src/overlays/actors/ovl_En_Box/z_en_box.c @@ -75,7 +75,6 @@ static InitChainEntry sInitChain[] = { }; static UNK_TYPE sUnused; -GetItemEntry sItem; Gfx gSkullTreasureChestChestSideAndLidDL[116] = {0}; Gfx gGoldTreasureChestChestSideAndLidDL[116] = {0}; @@ -472,7 +471,7 @@ void EnBox_WaitOpen(EnBox* this, PlayState* play) { func_8002DBD0(&this->dyna.actor, &sp4C, &player->actor.world.pos); if (sp4C.z > -50.0f && sp4C.z < 0.0f && fabsf(sp4C.y) < 10.0f && fabsf(sp4C.x) < 20.0f && Player_IsFacingActor(&this->dyna.actor, 0x3000, play)) { - sItem = Randomizer_GetItemFromActor(this->dyna.actor.id, play->sceneNum, this->dyna.actor.params, this->dyna.actor.params >> 5 & 0x7F); + GetItemEntry sItem = Randomizer_GetItemFromActor(this->dyna.actor.id, play->sceneNum, this->dyna.actor.params, this->dyna.actor.params >> 5 & 0x7F); GetItemEntry blueRupee = ItemTable_RetrieveEntry(MOD_NONE, GI_RUPEE_BLUE); // RANDOTODO treasure chest game rando @@ -628,7 +627,7 @@ void EnBox_Update(Actor* thisx, PlayState* play) { } if (((!IS_RANDO && ((this->dyna.actor.params >> 5 & 0x7F) == 0x7C)) || - (IS_RANDO && ABS(sItem.getItemId) == RG_ICE_TRAP)) && + (IS_RANDO && this->getItemEntry.getItemId == RG_ICE_TRAP)) && this->actionFunc == EnBox_Open && this->skelanime.curFrame > 45 && this->iceSmokeTimer < 100) { if (!CVarGetInteger("gAddTraps.enabled", 0)) { EnBox_SpawnIceSmoke(this, play); From 60faf3f750e29643ffa688b9b5fe752a9d2162c0 Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Mon, 18 Dec 2023 04:47:39 +0000 Subject: [PATCH 18/24] Bump version to MacReady Echo 8.0.4 (#3537) --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ae0360d8..c3b85024b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,8 +5,8 @@ set(CMAKE_CXX_STANDARD 20 CACHE STRING "The C++ standard to use") set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version") -project(Ship VERSION 8.0.3 LANGUAGES C CXX) -set(PROJECT_BUILD_NAME "MacReady Delta" CACHE STRING "") +project(Ship VERSION 8.0.4 LANGUAGES C CXX) +set(PROJECT_BUILD_NAME "MacReady Echo" CACHE STRING "") set(PROJECT_TEAM "github.com/harbourmasters" CACHE STRING "") set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT soh) From 1d7ad522225a46c2da614c6f96350e09658d75d4 Mon Sep 17 00:00:00 2001 From: Adam Bird Date: Tue, 19 Dec 2023 00:55:32 -0500 Subject: [PATCH 19/24] fix remote control define when flag not set in windows (#3534) --- soh/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/soh/CMakeLists.txt b/soh/CMakeLists.txt index 8b46a27f7..6a6959038 100644 --- a/soh/CMakeLists.txt +++ b/soh/CMakeLists.txt @@ -410,7 +410,6 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Windows") ">" "$<$:ENABLE_REMOTE_CONTROL>" "INCLUDE_GAME_PRINTF;" - "ENABLE_REMOTE_CONTROL;" "UNICODE;" "_UNICODE" STORMLIB_NO_AUTO_LINK From 66c41a8012189c5a5afd840341ce5148ca29a20e Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Fri, 1 Dec 2023 23:54:59 -0600 Subject: [PATCH 20/24] Clean up func_8084ABD8 --- .../actors/ovl_player_actor/z_player.c | 83 +++++++++++-------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/soh/src/overlays/actors/ovl_player_actor/z_player.c b/soh/src/overlays/actors/ovl_player_actor/z_player.c index 64429ed03..41cf88537 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -11822,58 +11822,69 @@ void Player_Destroy(Actor* thisx, PlayState* play) { //first person manipulate player actor s16 func_8084ABD8(PlayState* play, Player* this, s32 arg2, s16 arg3) { - s32 temp1; - s16 temp2; - s16 temp3; - bool gInvertAimingXAxis = (CVarGetInteger("gInvertAimingXAxis", 0) && !CVarGetInteger("gMirroredWorld", 0)) || (!CVarGetInteger("gInvertAimingXAxis", 0) && CVarGetInteger("gMirroredWorld", 0)); + s32 temp1 = 0; + s16 temp2 = 0; + s16 temp3 = 0; + s8 invertXAxisMulti = ((CVarGetInteger("gInvertAimingXAxis", 0) && !CVarGetInteger("gMirroredWorld", 0)) || (!CVarGetInteger("gInvertAimingXAxis", 0) && CVarGetInteger("gMirroredWorld", 0))) ? -1 : 1; + s8 invertYAxisMulti = CVarGetInteger("gInvertAimingYAxis", 1) ? 1 : -1; + f32 xAxisMulti = CVarGetFloat("gFirstPersonCameraSensitivityX", 1.0f); + f32 yAxisMulti = CVarGetFloat("gFirstPersonCameraSensitivityY", 1.0f); - if (!func_8002DD78(this) && !func_808334B4(this) && (arg2 == 0) && !CVarGetInteger("gDisableAutoCenterViewFirstPerson", 0)) { - temp2 = sControlInput->rel.stick_y * 240.0f * (CVarGetInteger("gInvertAimingYAxis", 1) ? 1 : -1); // Sensitivity not applied here because higher than default sensitivies will allow the camera to escape the autocentering, and glitch out massively - Math_SmoothStepToS(&this->actor.focus.rot.x, temp2, 14, 4000, 30); + if (!func_8002DD78(this) && !func_808334B4(this) && (arg2 == 0)) { // First person without weapon + // Y Axis + if (CVarGetInteger("gRightStickAiming", 0) && fabsf(sControlInput->cur.right_stick_y) > 15.0f) { + temp2 += sControlInput->cur.right_stick_y * 240.0f * invertYAxisMulti * yAxisMulti; + } + if (fabsf(sControlInput->cur.gyro_x) > 0.01f) { + temp2 += (-sControlInput->cur.gyro_x) * 750.0f; + } + if (CVarGetInteger("gDisableAutoCenterViewFirstPerson", 0)) { + this->actor.focus.rot.x += temp2 * 0.1f; + this->actor.focus.rot.x = CLAMP(this->actor.focus.rot.x, -14000, 14000); + } else { + Math_SmoothStepToS(&this->actor.focus.rot.x, temp2, 14, 4000, 30); + } - temp2 = sControlInput->rel.stick_x * -16.0f * (gInvertAimingXAxis ? -1 : 1) * (CVarGetFloat("gFirstPersonCameraSensitivityX", 1.0f)); + // X Axis + temp2 = 0; + if (CVarGetInteger("gRightStickAiming", 0) && fabsf(sControlInput->cur.right_stick_x) > 15.0f) { + temp2 += sControlInput->cur.right_stick_x * -16.0f * invertXAxisMulti * xAxisMulti; + } + if (fabsf(sControlInput->cur.gyro_y) > 0.01f) { + temp2 += (sControlInput->cur.gyro_y) * 750.0f * invertXAxisMulti; + } temp2 = CLAMP(temp2, -3000, 3000); this->actor.focus.rot.y += temp2; - } else { + } else { // First person with weapon + // Y Axis temp1 = (this->stateFlags1 & PLAYER_STATE1_ON_HORSE) ? 3500 : 14000; - temp3 = ((sControlInput->rel.stick_y >= 0) ? 1 : -1) * - (s32)((1.0f - Math_CosS(sControlInput->rel.stick_y * 200)) * 1500.0f * - (CVarGetInteger("gInvertAimingYAxis", 1) ? 1 : -1)) * (CVarGetFloat("gFirstPersonCameraSensitivityY", 1.0f)); - this->actor.focus.rot.x += temp3; - + if (CVarGetInteger("gRightStickAiming", 0) && fabsf(sControlInput->cur.right_stick_y) > 15.0f) { + temp3 += ((sControlInput->cur.right_stick_y >= 0) ? 1 : -1) * + (s32)((1.0f - Math_CosS(sControlInput->cur.right_stick_y * 200)) * 1500.0f) * invertYAxisMulti * yAxisMulti; + } if (fabsf(sControlInput->cur.gyro_x) > 0.01f) { - this->actor.focus.rot.x -= (sControlInput->cur.gyro_x) * 750.0f; + temp3 += (-sControlInput->cur.gyro_x) * 750.0f; } - - if (fabsf(sControlInput->cur.right_stick_y) > 15.0f && CVarGetInteger("gRightStickAiming", 0) != 0) { - this->actor.focus.rot.x -= - (sControlInput->cur.right_stick_y) * 10.0f * (CVarGetInteger("gInvertAimingYAxis", 1) ? -1 : 1) * (CVarGetFloat("gFirstPersonCameraSensitivityY", 1.0f)); - } - + this->actor.focus.rot.x += temp3; this->actor.focus.rot.x = CLAMP(this->actor.focus.rot.x, -temp1, temp1); + // X Axis temp1 = 19114; temp2 = this->actor.focus.rot.y - this->actor.shape.rot.y; - temp3 = ((sControlInput->rel.stick_x >= 0) ? 1 : -1) * - (s32)((1.0f - Math_CosS(sControlInput->rel.stick_x * 200)) * -1500.0f * - (gInvertAimingXAxis ? -1 : 1)) * (CVarGetFloat("gFirstPersonCameraSensitivityX", 1.0f)); - temp2 += temp3; - - this->actor.focus.rot.y = CLAMP(temp2, -temp1, temp1) + this->actor.shape.rot.y; - + temp3 = 0; + if (CVarGetInteger("gRightStickAiming", 0) && fabsf(sControlInput->cur.right_stick_x) > 15.0f) { + temp3 += ((sControlInput->cur.right_stick_x >= 0) ? 1 : -1) * + (s32)((1.0f - Math_CosS(sControlInput->cur.right_stick_x * 200)) * -1500.0f) * invertXAxisMulti * xAxisMulti; + } if (fabsf(sControlInput->cur.gyro_y) > 0.01f) { - this->actor.focus.rot.y += (sControlInput->cur.gyro_y) * 750.0f * (CVarGetInteger("gMirroredWorld", 0) ? -1 : 1); - } - - if (fabsf(sControlInput->cur.right_stick_x) > 15.0f && CVarGetInteger("gRightStickAiming", 0) != 0) { - this->actor.focus.rot.y += - (sControlInput->cur.right_stick_x) * 10.0f * (gInvertAimingXAxis ? 1 : -1) * (CVarGetFloat("gFirstPersonCameraSensitivityX", 1.0f)); + temp3 += (sControlInput->cur.gyro_y) * 750.0f * invertXAxisMulti; } + temp2 += temp3; + this->actor.focus.rot.y = CLAMP(temp2, -temp1, temp1) + this->actor.shape.rot.y; } this->unk_6AE |= 2; - return func_80836AB8(this, (play->shootingGalleryStatus != 0) || func_8002DD78(this) || func_808334B4(this)) - - arg3; + return func_80836AB8(this, (play->shootingGalleryStatus != 0) || func_8002DD78(this) || func_808334B4(this)) - arg3; } void func_8084AEEC(Player* this, f32* arg1, f32 arg2, s16 arg3) { From 2cb3a3664efac8b0d72f9a670f21f1d0d48d1e8c Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Fri, 1 Dec 2023 23:57:20 -0600 Subject: [PATCH 21/24] Implement gMoveWhileFirstPerson --- .../controls/GameControlEditor.cpp | 7 ++- .../actors/ovl_player_actor/z_player.c | 43 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/soh/soh/Enhancements/controls/GameControlEditor.cpp b/soh/soh/Enhancements/controls/GameControlEditor.cpp index a452bf4b1..4643dc31d 100644 --- a/soh/soh/Enhancements/controls/GameControlEditor.cpp +++ b/soh/soh/Enhancements/controls/GameControlEditor.cpp @@ -225,6 +225,10 @@ namespace GameControlEditor { window->BeginGroupPanelPublic("Aiming/First-Person Camera", ImGui::GetContentRegionAvail()); UIWidgets::PaddedEnhancementCheckbox("Right Stick Aiming", "gRightStickAiming"); DrawHelpIcon("Allows for aiming with the right stick in:\n-First-Person/C-Up view\n-Weapon Aiming"); + if (CVarGetInteger("gRightStickAiming", 0)) { + UIWidgets::PaddedEnhancementCheckbox("Allow moving while in first person mode", "gMoveWhileFirstPerson"); + DrawHelpIcon("Changes the left stick to move the player while in first person mode"); + } UIWidgets::PaddedEnhancementCheckbox("Invert Aiming X Axis", "gInvertAimingXAxis"); DrawHelpIcon("Inverts the Camera X Axis in:\n-First-Person/C-Up view\n-Weapon Aiming"); UIWidgets::PaddedEnhancementCheckbox("Invert Aiming Y Axis", "gInvertAimingYAxis", true, true, false, "", UIWidgets::CheckboxGraphics::Cross, true); @@ -237,7 +241,8 @@ namespace GameControlEditor { DrawHelpIcon("Prevents the C-Up view from auto-centering, allowing for Gyro Aiming"); if (UIWidgets::PaddedEnhancementCheckbox("Enable Custom Aiming/First-Person sensitivity", "gEnableFirstPersonSensitivity", true, false)) { if (!CVarGetInteger("gEnableFirstPersonSensitivity", 0)) { - CVarClear("gFirstPersonCameraSensitivity"); + CVarClear("gFirstPersonCameraSensitivityX"); + CVarClear("gFirstPersonCameraSensitivityY"); } } if (CVarGetInteger("gEnableFirstPersonSensitivity", 0)) { diff --git a/soh/src/overlays/actors/ovl_player_actor/z_player.c b/soh/src/overlays/actors/ovl_player_actor/z_player.c index 41cf88537..f192fe1ce 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -11832,6 +11832,9 @@ s16 func_8084ABD8(PlayState* play, Player* this, s32 arg2, s16 arg3) { if (!func_8002DD78(this) && !func_808334B4(this) && (arg2 == 0)) { // First person without weapon // Y Axis + if (!CVarGetInteger("gMoveWhileFirstPerson", 0)) { + temp2 += sControlInput->rel.stick_y * 240.0f * invertYAxisMulti * yAxisMulti; + } if (CVarGetInteger("gRightStickAiming", 0) && fabsf(sControlInput->cur.right_stick_y) > 15.0f) { temp2 += sControlInput->cur.right_stick_y * 240.0f * invertYAxisMulti * yAxisMulti; } @@ -11847,6 +11850,9 @@ s16 func_8084ABD8(PlayState* play, Player* this, s32 arg2, s16 arg3) { // X Axis temp2 = 0; + if (!CVarGetInteger("gMoveWhileFirstPerson", 0)) { + temp2 += sControlInput->rel.stick_x * -16.0f * invertXAxisMulti * xAxisMulti; + } if (CVarGetInteger("gRightStickAiming", 0) && fabsf(sControlInput->cur.right_stick_x) > 15.0f) { temp2 += sControlInput->cur.right_stick_x * -16.0f * invertXAxisMulti * xAxisMulti; } @@ -11858,6 +11864,11 @@ s16 func_8084ABD8(PlayState* play, Player* this, s32 arg2, s16 arg3) { } else { // First person with weapon // Y Axis temp1 = (this->stateFlags1 & PLAYER_STATE1_ON_HORSE) ? 3500 : 14000; + + if (!CVarGetInteger("gMoveWhileFirstPerson", 0)) { + temp3 += ((sControlInput->rel.stick_y >= 0) ? 1 : -1) * + (s32)((1.0f - Math_CosS(sControlInput->rel.stick_y * 200)) * 1500.0f) * invertYAxisMulti * yAxisMulti; + } if (CVarGetInteger("gRightStickAiming", 0) && fabsf(sControlInput->cur.right_stick_y) > 15.0f) { temp3 += ((sControlInput->cur.right_stick_y >= 0) ? 1 : -1) * (s32)((1.0f - Math_CosS(sControlInput->cur.right_stick_y * 200)) * 1500.0f) * invertYAxisMulti * yAxisMulti; @@ -11872,6 +11883,10 @@ s16 func_8084ABD8(PlayState* play, Player* this, s32 arg2, s16 arg3) { temp1 = 19114; temp2 = this->actor.focus.rot.y - this->actor.shape.rot.y; temp3 = 0; + if (!CVarGetInteger("gMoveWhileFirstPerson", 0)) { + temp3 = ((sControlInput->rel.stick_x >= 0) ? 1 : -1) * + (s32)((1.0f - Math_CosS(sControlInput->rel.stick_x * 200)) * -1500.0f) * invertXAxisMulti * xAxisMulti; + } if (CVarGetInteger("gRightStickAiming", 0) && fabsf(sControlInput->cur.right_stick_x) > 15.0f) { temp3 += ((sControlInput->cur.right_stick_x >= 0) ? 1 : -1) * (s32)((1.0f - Math_CosS(sControlInput->cur.right_stick_x * 200)) * -1500.0f) * invertXAxisMulti * xAxisMulti; @@ -11883,6 +11898,34 @@ s16 func_8084ABD8(PlayState* play, Player* this, s32 arg2, s16 arg3) { this->actor.focus.rot.y = CLAMP(temp2, -temp1, temp1) + this->actor.shape.rot.y; } + if (CVarGetInteger("gMoveWhileFirstPerson", 0)) { + f32 movementSpeed = LINK_IS_ADULT ? 9.0f : 8.25f; + if (CVarGetInteger("gMMBunnyHood", BUNNY_HOOD_VANILLA) != BUNNY_HOOD_VANILLA && this->currentMask == PLAYER_MASK_BUNNY) { + movementSpeed *= 1.5f; + } + + f32 relX = (sControlInput->rel.stick_x / 10 * -invertXAxisMulti); + f32 relY = (sControlInput->rel.stick_y / 10); + + // Normalize so that diagonal movement isn't faster + f32 relMag = sqrtf((relX * relX) + (relY * relY)); + if (relMag > 1.0f) { + relX /= relMag; + relY /= relMag; + } + + // Determine what left and right mean based on camera angle + f32 relX2 = relX * Math_CosS(this->actor.focus.rot.y) + relY * Math_SinS(this->actor.focus.rot.y); + f32 relY2 = relY * Math_CosS(this->actor.focus.rot.y) - relX * Math_SinS(this->actor.focus.rot.y); + + // Calculate distance for footstep sound + f32 distance = sqrtf((relX2 * relX2) + (relY2 * relY2)) * movementSpeed; + func_8084029C(this, distance / 4.5f); + + this->actor.world.pos.x += (relX2 * movementSpeed) + this->actor.colChkInfo.displacement.x; + this->actor.world.pos.z += (relY2 * movementSpeed) + this->actor.colChkInfo.displacement.z; + } + this->unk_6AE |= 2; return func_80836AB8(this, (play->shootingGalleryStatus != 0) || func_8002DD78(this) || func_808334B4(this)) - arg3; } From e0930809d4d7c8eaea16b1673dedfcfa2f151122 Mon Sep 17 00:00:00 2001 From: "Tina H. (sheepytina)" <99330992+sheepytina@users.noreply.github.com> Date: Sun, 24 Dec 2023 08:19:41 +1100 Subject: [PATCH 22/24] Advanced controls for aspect ratio, resolution, and integer scaling. (#3130) * Advanced Resolution Settings first working version with most features implemented * Update advancedResolutionEditor.cpp Added auto-resizing logic for Pixel Perfect Mode. Minor fixes. * Tweaks and tidying up. Disable integer scale slider if automatic sizing is overriding it. Don't offer these UI options on Apple. Removed unused code. Updated LUS. * Update libultraship * Filenames and style fixes Filenames and folders now more closely match rest of project. Tidied newlines/comments. (SohMenuBar.cpp) Label of button changed to fit menu. (ResolutionEditor.cpp) Default window size improved. * Update libultraship (However, I still need to make the GUI controls acknowledge the new constraints.) * Update libultraship (and changed the name of some cvars) * Added constraints to the inputs. Added a fps drop warning. * Tweaks based on feedback * Update libultraship * Enabled on Apple - For currently ongoing Retina DPI troubleshooting. (Also removed the duplicated N64 Mode toggle.) * Update libultraship * Update LUS, update CVar names, small tweaks And one significant fix: Enhancement checkboxes in ResolutionEditor now default to off. * Add Additional Settings and the accursed horizontal resolution field. There's still a few bugs with it that I haven't squashed, but I need to stop for now and just commit what I've got. (This is honestly causing more problems than it solves, but i'm tired of getting questions about it.) * Resolved many of the lingering bugs with the previous commit * Horizontal Resolution field now properly acknowledges resolution bounds. * Don't show "Horiz. pixel count" field if not enforcing aspect ratio. Additionally: * Don't change settings if selecting "Custom" from preset dropdowns. * Added a missing horizontal pixel count clamp check. * Tidied up redundant behaviour. * Additional comments, and a checkbox to disable aspect correction on consoles. * Change how frame rate threshold is calculated. * More minor UI tweaks. * Added missing CVarSave() calls where needed. Added a short update countdown for the numerical CVars. This is intended to prevent CVarSave() from being called too often. * Added a helpful button to cover a potential support issue. * "Fit Automatically" has been moved to LUS and is now smarter. This will require another PR in LUS to be opened by me. * Swap to new branch for libultraship * Even more clever integer scaling behavior. "IntegerScale" is itself now a CVar group. * Tidy up comments. * Fix a typo that prevented `IsDroppingFrames()` from working (Maybe more than a mere typo, but a typo was involved.) * Remove unused and unnecessary variables. * Group "Integer Scaling" under its own collapsing header * Changed label for the Enabled advanced settings checkbox. * Update libultraship + Formatting pass on ResolutionEditor.cpp * Add `(Select "Off" to disable.)` help text for the aspect ratio setting and hide UI elements accordingly. Only show the fields if user chooses Custom. Padding has been shifted accordingly too. Also fixed a long standing error with the Y field disappearing when modifying X. * Well I suppose that's no-longer necessary. * Update libultraship with commits from main branch (up to e5df3a9) * Tweak comments. * Save current ImGui Combo items as a console variable to improve user experience. * Change language of NeverExceedBounds checkbox description to be more affirmative, so it makes more sense. Add tooltip for NeverExceedBounds checkbox. Tweak some comments related to additional settings. * Add list of colours to use with TextColored elements. * Move some UI elements around. Add an extra MSAA slider to the editor window. * Integer Scaling header is DefaultOpen if player has Pixel Perfect Mode active upon window creation. + Amend tooltips. * Fix a minor oversight with default configuration. Fixes an issue where default aspect ratio settings on a fresh SoH configuration weren't matching the defaults assigned in libultraship. The default values are now 16:9, matching LUS. Additionally, the combo box now defaults specifically to the 16:9 preset instead of "Custom". (Fixing the defaults in LUS to be 4:3 isn't worth a LUS bump, so this slight workaround will do for the sake of this PR.) * Make resolution slider `disabled` condition a variable, for readability. * Small tweak to combo item saving * Use `SCREEN_HEIGHT` and `SCREEN_WIDTH` for constraints * Simplify "Show a horizontal resolution field" logic by using pixel dimensions as the aspect ratio directly, since now this view hides the aspect ratio setting from the user anyway. * Correct aspect ratio visualiser to be un-inverted + actually display it as a ratio. * Remove update flags from combo boxes + remove update countdown + remove non-functioning 'IsBoolArrayTrue' function. (The countdown was an okay idea but I didn't implement it correctly. It's better to just keep it simple.) * Code review suggestion: disable UI elements conditionally (+ tweaks to code style) * Invisible tweaks to the Integer Scaling-related Additional Settings This looks like a lot but it's mostly just re-arranging a cluttered area of the code for clarity. Actual changes to functionality are: * Help text now "disabled" along with the checkbox. * The NeverExceedBounds checkbox will now reset the unused ExceedBoundsBy cvar if it's been changed. * Assorted small tweaks to comments and variable declarations. * Code review suggestion: tweak "Window exceeded" warning condition * Missed a thingy. --- .../resolution-editor/ResolutionEditor.cpp | 491 ++++++++++++++++++ .../resolution-editor/ResolutionEditor.h | 16 + soh/soh/SohGui.cpp | 6 + soh/soh/SohMenuBar.cpp | 23 +- 4 files changed, 534 insertions(+), 2 deletions(-) create mode 100644 soh/soh/Enhancements/resolution-editor/ResolutionEditor.cpp create mode 100644 soh/soh/Enhancements/resolution-editor/ResolutionEditor.h diff --git a/soh/soh/Enhancements/resolution-editor/ResolutionEditor.cpp b/soh/soh/Enhancements/resolution-editor/ResolutionEditor.cpp new file mode 100644 index 000000000..11a7cd75a --- /dev/null +++ b/soh/soh/Enhancements/resolution-editor/ResolutionEditor.cpp @@ -0,0 +1,491 @@ +#include "ResolutionEditor.h" +#include +#include + +#include +#include + +/* Console Variables are grouped under gAdvancedResolution. (e.g. "gAdvancedResolution.Enabled") + + The following cvars are used in Libultraship and can be edited here: + - Enabled - Turns Advanced Resolution Mode on. + - AspectRatioX, AspectRatioY - Aspect ratio controls. To toggle off, set either to zero. + - VerticalPixelCount, VerticalResolutionToggle - Resolution controls. + - PixelPerfectMode, IntegerScale.Factor - Pixel Perfect Mode a.k.a. integer scaling controls. + - IntegerScale.FitAutomatically - Automatic resizing for Pixel Perfect Mode. + - IntegerScale.NeverExceedBounds - Prevents manual resizing from exceeding screen bounds. + + The following cvars are also implemented in LUS for niche use cases: + - IgnoreAspectCorrection - Stretch framebuffer to fill screen. + This is something of a power-user setting for niche setups that most people won't need or care about, + but may be useful if playing the Switch/Wii U ports on a 4:3 television. + - IntegerScale.ExceedBoundsBy - Offset the max screen bounds, usually by +1. + This isn't that useful at the moment, so it's unused here. +*/ + +namespace AdvancedResolutionSettings { +enum setting { UPDATE_aspectRatioX, UPDATE_aspectRatioY, UPDATE_verticalPixelCount }; + +const char* aspectRatioPresetLabels[] = { + "Off", "Custom", "Original (4:3)", "Widescreen (16:9)", "Nintendo 3DS (5:3)", "16:10 (8:5)", "Ultrawide (21:9)" +}; +const float aspectRatioPresetsX[] = { 0.0f, 16.0f, 4.0f, 16.0f, 5.0f, 16.0f, 21.0f }; +const float aspectRatioPresetsY[] = { 0.0f, 9.0f, 3.0f, 9.0f, 3.0f, 10.0f, 9.0f }; +const int default_aspectRatio = 1; // Default combo list option + +const char* pixelCountPresetLabels[] = { "Custom", "Native N64 (240p)", "2x (480p)", "3x (720p)", "4x (960p)", + "5x (1200p)", "6x (1440p)", "Full HD (1080p)", "4K (2160p)" }; +const int pixelCountPresets[] = { 480, 240, 480, 720, 960, 1200, 1440, 1080, 2160 }; +const int default_pixelCount = 0; // Default combo list option + +// Resolution clamp values as hardcoded in LUS::Gui::ApplyResolutionChanges() +const uint32_t minVerticalPixelCount = SCREEN_HEIGHT; +const uint32_t maxVerticalPixelCount = 4320; // 18x native, or 8K TV resolution + +const unsigned short default_maxIntegerScaleFactor = 6; // Default size of Integer scale factor slider. + +enum messageType { MESSAGE_ERROR, MESSAGE_WARNING, MESSAGE_QUESTION, MESSAGE_INFO, MESSAGE_GRAY_75 }; +const ImVec4 messageColor[]{ + { 0.85f, 0.0f, 0.0f, 1.0f }, // MESSAGE_ERROR + { 0.85f, 0.85f, 0.0f, 1.0f }, // MESSAGE_WARNING + { 0.0f, 0.85f, 0.85f, 1.0f }, // MESSAGE_QUESTION + { 0.0f, 0.85f, 0.55f, 1.0f }, // MESSAGE_INFO + { 0.75f, 0.75f, 0.75f, 1.0f } // MESSAGE_GRAY_75 +}; +const float enhancementSpacerHeight = 19.0f; + +void AdvancedResolutionSettingsWindow::InitElement() { +} + +void AdvancedResolutionSettingsWindow::DrawElement() { + ImGui::SetNextWindowSize(ImVec2(497, 599), ImGuiCond_FirstUseEver); + if (ImGui::Begin("Advanced Resolution Settings", &mIsVisible)) { + // Initialise update flags. + bool update[3]; + for (uint8_t i = 0; i < sizeof(update); i++) + update[i] = false; + + // Initialise integer scale bounds. + short max_integerScaleFactor = default_maxIntegerScaleFactor; // default value, which may or may not get + // overridden depending on viewport res + + short integerScale_maximumBounds = 1; // can change when window is resized + // This is mostly just for UX purposes, as Fit Automatically logic is part of LUS. + if (((float)gfx_current_game_window_viewport.width / gfx_current_game_window_viewport.height) > + ((float)gfx_current_dimensions.width / gfx_current_dimensions.height)) { + // Scale to window height + integerScale_maximumBounds = gfx_current_game_window_viewport.height / gfx_current_dimensions.height; + } else { + // Scale to window width + integerScale_maximumBounds = gfx_current_game_window_viewport.width / gfx_current_dimensions.width; + } + // Lower-clamping maximum bounds value to 1 is no-longer necessary as that's accounted for in LUS. + // Letting it go below 1 in this Editor will even allow for checking if screen bounds are being exceeded. + if (default_maxIntegerScaleFactor < integerScale_maximumBounds) { + max_integerScaleFactor = + integerScale_maximumBounds + CVarGetInteger("gAdvancedResolution.IntegerScale.ExceedBoundsBy", 0); + } + + // Combo List defaults + static int item_aspectRatio = CVarGetInteger("gAdvancedResolution.UIComboItem.AspectRatio", 3); + static int item_pixelCount = CVarGetInteger("gAdvancedResolution.UIComboItem.PixelCount", default_pixelCount); + // Stored Values for non-UIWidgets elements + static float aspectRatioX = + CVarGetFloat("gAdvancedResolution.AspectRatioX", aspectRatioPresetsX[item_aspectRatio]); + static float aspectRatioY = + CVarGetFloat("gAdvancedResolution.AspectRatioY", aspectRatioPresetsY[item_aspectRatio]); + static int verticalPixelCount = + CVarGetInteger("gAdvancedResolution.VerticalPixelCount", pixelCountPresets[item_pixelCount]); + // Additional settings + static bool showHorizontalResField = false; + static int horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; + // Disabling flags + const bool disabled_everything = !CVarGetInteger("gAdvancedResolution.Enabled", 0); + const bool disabled_pixelCount = !CVarGetInteger("gAdvancedResolution.VerticalResolutionToggle", 0); + +#ifdef __APPLE__ + // Display HiDPI warning. (Remove this once we can definitively say it's fixed.) + ImGui::TextColored(messageColor[MESSAGE_INFO], + ICON_FA_INFO_CIRCLE " These settings may behave incorrectly on Retina displays."); + UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); +#endif + + if (ImGui::CollapsingHeader("Original Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + // The original resolution slider (for convenience) + const bool disabled_resolutionSlider = (CVarGetInteger("gAdvancedResolution.VerticalResolutionToggle", 0) && + CVarGetInteger("gAdvancedResolution.Enabled", 0)) || + CVarGetInteger("gLowResMode", 0); + if (UIWidgets::EnhancementSliderFloat("Internal Resolution: %d %%", "##IMul", "gInternalResolution", 0.5f, + 2.0f, "", 1.0f, true, true, disabled_resolutionSlider)) { + LUS::Context::GetInstance()->GetWindow()->SetResolutionMultiplier( + CVarGetFloat("gInternalResolution", 1)); + } + UIWidgets::Tooltip("Multiplies your output resolution by the value entered."); + + // The original MSAA slider (also for convenience) +#ifndef __WIIU__ + if (UIWidgets::PaddedEnhancementSliderInt("MSAA: %d", "##IMSAA", "gMSAAValue", 1, 8, "", 1, true, true, + false)) { + LUS::Context::GetInstance()->GetWindow()->SetMsaaLevel(CVarGetInteger("gMSAAValue", 1)); + }; + UIWidgets::Tooltip( + "Activates multi-sample anti-aliasing when above 1x, up to 8x for 8 samples for every pixel.\n\n" + " " ICON_FA_INFO_CIRCLE + " (Higher MSAA with low resolution can approximate an authentic \"real N64\" look!)"); +#endif + + // N64 Mode toggle (again for convenience) + // UIWidgets::PaddedEnhancementCheckbox("(Enhancements>Graphics) N64 Mode", "gLowResMode", false, false, false, "", UIWidgets::CheckboxGraphics::Cross, false); + } + + UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); + // Activator + UIWidgets::PaddedEnhancementCheckbox("Enable advanced settings.", "gAdvancedResolution.Enabled", false, false, + false, "", UIWidgets::CheckboxGraphics::Cross, false); + // Error/Warning display + if (!CVarGetInteger("gLowResMode", 0)) { + if (IsDroppingFrames()) { // Significant frame drop warning + ImGui::TextColored(messageColor[MESSAGE_WARNING], + ICON_FA_EXCLAMATION_TRIANGLE " Significant frame rate (FPS) drops may be occuring."); + UIWidgets::Spacer(2); + } else { // No warnings + UIWidgets::Spacer(enhancementSpacerHeight); + } + } else { // N64 Mode warning + ImGui::TextColored(messageColor[MESSAGE_QUESTION], + ICON_FA_QUESTION_CIRCLE " \"N64 Mode\" is overriding these settings."); + ImGui::SameLine(); + if (ImGui::Button("Click to disable")) { + CVarSetInteger("gLowResMode", 0); + CVarSave(); + } + } + // Resolution visualiser + ImGui::Text("Viewport dimensions: %d x %d", gfx_current_game_window_viewport.width, + gfx_current_game_window_viewport.height); + ImGui::Text("Internal resolution: %d x %d", gfx_current_dimensions.width, gfx_current_dimensions.height); + + UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); + if (disabled_everything) { // Hide aspect ratio controls. + UIWidgets::DisableComponent(ImGui::GetStyle().Alpha * 0.5f); + } + + // Aspect Ratio + ImGui::Text("Force aspect ratio:"); + ImGui::SameLine(); + ImGui::TextColored(messageColor[MESSAGE_GRAY_75], "(Select \"Off\" to disable.)"); + // Presets + if (ImGui::Combo(" ", &item_aspectRatio, aspectRatioPresetLabels, + IM_ARRAYSIZE(aspectRatioPresetLabels)) && + item_aspectRatio != default_aspectRatio) { // don't change anything if "Custom" is selected. + aspectRatioX = aspectRatioPresetsX[item_aspectRatio]; + aspectRatioY = aspectRatioPresetsY[item_aspectRatio]; + + if (showHorizontalResField) { + horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; + } + + CVarSetFloat("gAdvancedResolution.AspectRatioX", aspectRatioX); + CVarSetFloat("gAdvancedResolution.AspectRatioY", aspectRatioY); + CVarSetInteger("gAdvancedResolution.UIComboItem.AspectRatio", item_aspectRatio); + CVarSave(); + } + // Hide aspect ratio input fields if using one of the presets. + if (item_aspectRatio == default_aspectRatio && !showHorizontalResField) { + // Declare input interaction bools outside of IF statement to prevent Y field from disappearing. + const bool input_X = ImGui::InputFloat("X", &aspectRatioX, 0.1f, 1.0f, "%.3f"); + const bool input_Y = ImGui::InputFloat("Y", &aspectRatioY, 0.1f, 1.0f, "%.3f"); + if (input_X || input_Y) { + item_aspectRatio = default_aspectRatio; + update[UPDATE_aspectRatioX] = true; + update[UPDATE_aspectRatioY] = true; + } + } else if (showHorizontalResField) { // Show calculated aspect ratio + if (item_aspectRatio) { + UIWidgets::Spacer(2); + const float resolvedAspectRatio = (float)gfx_current_dimensions.width / gfx_current_dimensions.height; + ImGui::Text("Aspect ratio: %.2f:1", resolvedAspectRatio); + } else { + UIWidgets::Spacer(enhancementSpacerHeight); + } + } + + if (disabled_everything) { // Hide aspect ratio controls. + UIWidgets::ReEnableComponent("disabledTooltipText"); + } + UIWidgets::Spacer(0); + + // Vertical Resolution + UIWidgets::PaddedEnhancementCheckbox("Set fixed vertical resolution (disables Resolution slider)", + "gAdvancedResolution.VerticalResolutionToggle", true, false, + disabled_everything, "", UIWidgets::CheckboxGraphics::Cross, false); + UIWidgets::Tooltip( + "Override the resolution scale slider and use the settings below, irrespective of window size."); + if (disabled_pixelCount || disabled_everything) { // Hide pixel count controls. + UIWidgets::DisableComponent(ImGui::GetStyle().Alpha * 0.5f); + } + if (ImGui::Combo("Pixel Count Presets", &item_pixelCount, pixelCountPresetLabels, + IM_ARRAYSIZE(pixelCountPresetLabels)) && + item_pixelCount != default_pixelCount) { // don't change anything if "Custom" is selected. + verticalPixelCount = pixelCountPresets[item_pixelCount]; + + if (showHorizontalResField) { + horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; + } + + CVarSetInteger("gAdvancedResolution.VerticalPixelCount", verticalPixelCount); + CVarSetInteger("gAdvancedResolution.UIComboItem.PixelCount", item_pixelCount); + CVarSave(); + } + // Horizontal Resolution, if visibility is enabled for it. + if (showHorizontalResField) { + // Only show the field if Aspect Ratio is being enforced. + if ((aspectRatioX > 0.0f) && (aspectRatioY > 0.0f)) { + // So basically we're "faking" this one by setting aspectRatioX instead. + if (ImGui::InputInt("Horiz. Pixel Count", &horizontalPixelCount, 8, 320)) { + item_aspectRatio = default_aspectRatio; + if (horizontalPixelCount < SCREEN_WIDTH) { + horizontalPixelCount = SCREEN_WIDTH; + } + aspectRatioX = horizontalPixelCount; + aspectRatioY = verticalPixelCount; + update[UPDATE_aspectRatioX] = true; + update[UPDATE_aspectRatioY] = true; + } + } else { // Display a notice instead. + ImGui::TextColored(messageColor[MESSAGE_QUESTION], + ICON_FA_QUESTION_CIRCLE " \"Force aspect ratio\" required."); + // ImGui::Text(" "); + ImGui::SameLine(); + if (ImGui::Button("Click to resolve")) { + item_aspectRatio = default_aspectRatio; // Set it to Custom + aspectRatioX = aspectRatioPresetsX[2]; // but use the 4:3 defaults + aspectRatioY = aspectRatioPresetsY[2]; + update[UPDATE_aspectRatioX] = true; + update[UPDATE_aspectRatioY] = true; + horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; + } + } + } + // Vertical Resolution part 2 + if (ImGui::InputInt("Vertical Pixel Count", &verticalPixelCount, 8, 240)) { + item_pixelCount = default_pixelCount; + update[UPDATE_verticalPixelCount] = true; + + // Account for the natural instinct to enter horizontal first. + // Ignore vertical resolutions that are below the lower clamp constant. + if (showHorizontalResField && !(verticalPixelCount < minVerticalPixelCount)) { + item_aspectRatio = default_aspectRatio; + aspectRatioX = horizontalPixelCount; + aspectRatioY = verticalPixelCount; + update[UPDATE_aspectRatioX] = true; + update[UPDATE_aspectRatioY] = true; + } + } + if (disabled_pixelCount || disabled_everything) { // Hide pixel count controls. + UIWidgets::ReEnableComponent("disabledTooltipText"); + } + + UIWidgets::Spacer(0); + + // Integer scaling settings group (Pixel-perfect Mode) + static const ImGuiTreeNodeFlags IntegerScalingResolvedImGuiFlag = + CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0) ? ImGuiTreeNodeFlags_DefaultOpen + : ImGuiTreeNodeFlags_None; + if (ImGui::CollapsingHeader("Integer Scaling Settings", IntegerScalingResolvedImGuiFlag)) { + const bool disabled_pixelPerfectMode = + !CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0) || disabled_everything; + // Pixel-perfect Mode + UIWidgets::PaddedEnhancementCheckbox("Pixel-perfect Mode", "gAdvancedResolution.PixelPerfectMode", true, + true, disabled_pixelCount || disabled_everything, "", + UIWidgets::CheckboxGraphics::Cross, false); + UIWidgets::Tooltip("Don't scale image to fill window."); + if (disabled_pixelCount && CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0)) { + CVarSetInteger("gAdvancedResolution.PixelPerfectMode", 0); + CVarSave(); + } + + // Integer Scaling + UIWidgets::EnhancementSliderInt( + "Integer scale factor: %d", "##ARSIntScale", "gAdvancedResolution.IntegerScale.Factor", 1, + max_integerScaleFactor, "%d", 1, true, + disabled_pixelPerfectMode || CVarGetInteger("gAdvancedResolution.IntegerScale.FitAutomatically", 0)); + UIWidgets::Tooltip("Integer scales the image. Only available in pixel-perfect mode."); + // Display warning if size is being clamped or if framebuffer is larger than viewport. + if (!disabled_pixelPerfectMode && + (CVarGetInteger("gAdvancedResolution.IntegerScale.NeverExceedBounds", 1) && + CVarGetInteger("gAdvancedResolution.IntegerScale.Factor", 1) > integerScale_maximumBounds)) { + ImGui::SameLine(); + ImGui::TextColored(messageColor[MESSAGE_WARNING], ICON_FA_EXCLAMATION_TRIANGLE " Window exceeded."); + } + + UIWidgets::PaddedEnhancementCheckbox( + "Automatically scale image to fit viewport", "gAdvancedResolution.IntegerScale.FitAutomatically", true, + true, disabled_pixelPerfectMode, "", UIWidgets::CheckboxGraphics::Cross, false); + UIWidgets::Tooltip("Automatically sets scale factor to fit window. Only available in pixel-perfect mode."); + if (CVarGetInteger("gAdvancedResolution.IntegerScale.FitAutomatically", 0)) { + // This is just here to update the value shown on the slider. + // The function in LUS to handle this setting will ignore IntegerScaleFactor while active. + CVarSetInteger("gAdvancedResolution.IntegerScale.Factor", integerScale_maximumBounds); + // CVarSave(); + } + } // End of integer scaling settings + + UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); + + // Collapsible panel for additional settings + if (ImGui::CollapsingHeader("Additional Settings")) { + UIWidgets::Spacer(0); + +#if defined(__SWITCH__) || defined(__WIIU__) + // Disable aspect correction, stretching the framebuffer to fill the viewport. + // This option is only really needed on systems limited to 16:9 TV resolutions, such as consoles. + // The associated cvar is still functional on PC platforms if you want to use it though. + UIWidgets::PaddedEnhancementCheckbox("Disable aspect correction and stretch the output image.\n" + "(Might be useful for 4:3 televisions!)\n" + "Not available in Pixel Perfect Mode.", + "gAdvancedResolution.IgnoreAspectCorrection", false, true, + CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0) || + disabled_everything, + "", UIWidgets::CheckboxGraphics::Cross, false); +#else + if (CVarGetInteger("gAdvancedResolution.IgnoreAspectCorrection", 0)) { + // This setting is intentionally not exposed on PC platforms, + // but may be accidentally activated for varying reasons. + // Having this button should hopefully prevent support headaches. + ImGui::TextColored(messageColor[MESSAGE_QUESTION], ICON_FA_QUESTION_CIRCLE + " If the image is stretched and you don't know why, click this."); + if (ImGui::Button("Click to reenable aspect correction.")) { + CVarSetInteger("gAdvancedResolution.IgnoreAspectCorrection", 0); + CVarSave(); + } + UIWidgets::Spacer(2); + } +#endif + + // A requested addition; an alternative way of displaying the resolution field. + if (ImGui::Checkbox("Show a horizontal resolution field, instead of aspect ratio.", &showHorizontalResField)) { + if (!showHorizontalResField && (aspectRatioX > 0.0f)) { // when turning this setting off + // Refresh relevant values + aspectRatioX = aspectRatioY * horizontalPixelCount / verticalPixelCount; + horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; + } else { // when turning this setting on + item_aspectRatio = default_aspectRatio; + if (aspectRatioX > 0.0f) { + // Refresh relevant values in the opposite order + horizontalPixelCount = (verticalPixelCount / aspectRatioY) * aspectRatioX; + aspectRatioX = aspectRatioY * horizontalPixelCount / verticalPixelCount; + } + } + update[UPDATE_aspectRatioX] = true; + } + + // Beginning of Integer Scaling additional settings. + { + // UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); + + // Integer Scaling - Never Exceed Bounds. + const bool disabled_neverExceedBounds = + !CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0) || + CVarGetInteger("gAdvancedResolution.IntegerScale.FitAutomatically", 0) || disabled_everything; + const bool checkbox_neverExceedBounds = + UIWidgets::PaddedEnhancementCheckbox("Prevent integer scaling from exceeding screen bounds.\n" + "(Makes screen bounds take priority over specified factor.)", + "gAdvancedResolution.IntegerScale.NeverExceedBounds", + true, false, disabled_neverExceedBounds, "", + UIWidgets::CheckboxGraphics::Cross, true); + UIWidgets::Tooltip( + "Prevents integer scaling factor from exceeding screen bounds.\n\n" + "Enabled: Will clamp the scaling factor and display a gentle warning in the resolution editor.\n" + "Disabled: Will allow scaling to exceed screen bounds, for users who want to crop overscan.\n\n" + " " ICON_FA_INFO_CIRCLE + " Please note that exceeding screen bounds may show a scroll bar on-screen."); + + // Initialise the (currently unused) "Exceed Bounds By" cvar if it's been changed. + if (checkbox_neverExceedBounds && + CVarGetInteger("gAdvancedResolution.IntegerScale.ExceedBoundsBy", 0)) { + CVarSetInteger("gAdvancedResolution.IntegerScale.ExceedBoundsBy", 0); + CVarSave(); + } + + // Integer Scaling - Exceed Bounds By 1x/Offset. + // A popular feature in some retro frontends/upscalers, sometimes called "crop overscan" or "1080p 5x". + /* + UIWidgets::PaddedEnhancementCheckbox("Allow integer scale factor to go +1 above maximum screen bounds.", "gAdvancedResolution.IntegerScale.ExceedBoundsBy", false, false, !CVarGetInteger("gAdvancedResolution.PixelPerfectMode", 0) || disabled_everything, "", UIWidgets::CheckboxGraphics::Cross, false); + */ + // It does actually function as expected, but exceeding the bottom of the screen shows a scroll bar. + // I've ended up commenting this one out because of the scroll bar, and for simplicity. + + // Display an info message about the scroll bar. + if (!CVarGetInteger("gAdvancedResolution.IntegerScale.NeverExceedBounds", 1) || + CVarGetInteger("gAdvancedResolution.IntegerScale.ExceedBoundsBy", 0)) { + if (disabled_neverExceedBounds) { // Dim this help text accordingly + UIWidgets::DisableComponent(ImGui::GetStyle().Alpha * 0.5f); + } + ImGui::TextColored(messageColor[MESSAGE_INFO], + " " ICON_FA_INFO_CIRCLE + " A scroll bar may become visible if screen bounds are exceeded."); + if (disabled_neverExceedBounds) { // Dim this help text accordingly + UIWidgets::ReEnableComponent("disabledTooltipText"); + } + + // Another support helper button, to disable the unused "Exceed Bounds By" cvar. + // (Remove this button if uncommenting the checkbox.) + if (CVarGetInteger("gAdvancedResolution.IntegerScale.ExceedBoundsBy", 0)) { + if (ImGui::Button("Click to reset a console variable that may be causing this.")) { + CVarSetInteger("gAdvancedResolution.IntegerScale.ExceedBoundsBy", 0); + CVarSave(); + } + } + } else { + ImGui::Text(" "); + } + // UIWidgets::PaddedSeparator(true, true, 3.0f, 3.0f); + } // End of Integer Scaling additional settings. + + } // End of additional settings + + // Clamp and update the cvars that don't use UIWidgets + if (update[UPDATE_aspectRatioX] || update[UPDATE_aspectRatioY] || update[UPDATE_verticalPixelCount]) { + if (update[UPDATE_aspectRatioX]) { + if (aspectRatioX < 0.0f) { + aspectRatioX = 0.0f; + } + CVarSetFloat("gAdvancedResolution.AspectRatioX", aspectRatioX); + } + if (update[UPDATE_aspectRatioY]) { + if (aspectRatioY < 0.0f) { + aspectRatioY = 0.0f; + } + CVarSetFloat("gAdvancedResolution.AspectRatioY", aspectRatioY); + } + if (update[UPDATE_verticalPixelCount]) { + // There's a upper and lower clamp on the Libultraship side too, + // so clamping it here is entirely visual, so the vertical resolution field reflects it. + if (verticalPixelCount < minVerticalPixelCount) { + verticalPixelCount = minVerticalPixelCount; + } + if (verticalPixelCount > maxVerticalPixelCount) { + verticalPixelCount = maxVerticalPixelCount; + } + CVarSetInteger("gAdvancedResolution.VerticalPixelCount", verticalPixelCount); + } + CVarSetInteger("gAdvancedResolution.UIComboItem.AspectRatio", item_aspectRatio); + CVarSetInteger("gAdvancedResolution.UIComboItem.PixelCount", item_pixelCount); + CVarSave(); + } + } + ImGui::End(); +} + +void AdvancedResolutionSettingsWindow::UpdateElement() { +} + +bool AdvancedResolutionSettingsWindow::IsDroppingFrames() { + // a rather imprecise way of checking for frame drops. + // but it's mostly there to inform the player of large drops. + const short targetFPS = CVarGetInteger("gInterpolationFPS", 20); + const float threshold = targetFPS / 20.0f + 4.1f; + return ImGui::GetIO().Framerate < targetFPS - threshold; +} +} // namespace AdvancedResolutionSettings diff --git a/soh/soh/Enhancements/resolution-editor/ResolutionEditor.h b/soh/soh/Enhancements/resolution-editor/ResolutionEditor.h new file mode 100644 index 000000000..68680c824 --- /dev/null +++ b/soh/soh/Enhancements/resolution-editor/ResolutionEditor.h @@ -0,0 +1,16 @@ +#pragma once +#include + +namespace AdvancedResolutionSettings { +class AdvancedResolutionSettingsWindow : public LUS::GuiWindow { + private: + bool IsDroppingFrames(); + + public: + using LUS::GuiWindow::GuiWindow; + + void InitElement() override; + void DrawElement() override; + void UpdateElement() override; +}; +} // namespace AdvancedResolutionSettings diff --git a/soh/soh/SohGui.cpp b/soh/soh/SohGui.cpp index 6dba67a05..ecc7dd234 100644 --- a/soh/soh/SohGui.cpp +++ b/soh/soh/SohGui.cpp @@ -38,6 +38,7 @@ #include "Enhancements/game-interactor/GameInteractor.h" #include "Enhancements/cosmetics/authenticGfxPatches.h" +#include "Enhancements/resolution-editor/ResolutionEditor.h" bool ToggleAltAssetsAtEndOfFrame = false; bool isBetaQuestEnabled = false; @@ -128,6 +129,8 @@ namespace SohGui { std::shared_ptr mItemTrackerWindow; std::shared_ptr mRandomizerSettingsWindow; + std::shared_ptr mAdvancedResolutionSettingsWindow; + void SetupGuiElements() { auto gui = LUS::Context::GetInstance()->GetWindow()->GetGui(); @@ -187,9 +190,12 @@ namespace SohGui { gui->AddGuiWindow(mItemTrackerSettingsWindow); mRandomizerSettingsWindow = std::make_shared("gRandomizerSettingsEnabled", "Randomizer Settings"); gui->AddGuiWindow(mRandomizerSettingsWindow); + mAdvancedResolutionSettingsWindow = std::make_shared("gAdvancedResolutionEditorEnabled", "Advanced Resolution Settings"); + gui->AddGuiWindow(mAdvancedResolutionSettingsWindow); } void Destroy() { + mAdvancedResolutionSettingsWindow = nullptr; mRandomizerSettingsWindow = nullptr; mItemTrackerWindow = nullptr; mItemTrackerSettingsWindow = nullptr; diff --git a/soh/soh/SohMenuBar.cpp b/soh/soh/SohMenuBar.cpp index 31d19a8c1..4e488521f 100644 --- a/soh/soh/SohMenuBar.cpp +++ b/soh/soh/SohMenuBar.cpp @@ -30,6 +30,7 @@ #include "Enhancements/randomizer/randomizer_entrance_tracker.h" #include "Enhancements/randomizer/randomizer_item_tracker.h" #include "Enhancements/randomizer/randomizer_settings_window.h" +#include "Enhancements/resolution-editor/ResolutionEditor.h" extern bool ToggleAltAssetsAtEndOfFrame; extern bool isBetaQuestEnabled; @@ -177,6 +178,7 @@ void DrawShipMenu() { extern std::shared_ptr mInputEditorWindow; extern std::shared_ptr mGameControlEditorWindow; +extern std::shared_ptr mAdvancedResolutionSettingsWindow; void DrawSettingsMenu() { if (ImGui::BeginMenu("Settings")) @@ -262,11 +264,28 @@ void DrawSettingsMenu() { if (ImGui::BeginMenu("Graphics")) { #ifndef __APPLE__ - if (UIWidgets::EnhancementSliderFloat("Internal Resolution: %d %%", "##IMul", "gInternalResolution", 0.5f, 2.0f, "", 1.0f, true)) { + const bool disabled_resolutionSlider = CVarGetInteger("gAdvancedResolution.VerticalResolutionToggle", 0) && + CVarGetInteger("gAdvancedResolution.Enabled", 0); + if (UIWidgets::EnhancementSliderFloat("Internal Resolution: %d %%", "##IMul", "gInternalResolution", 0.5f, + 2.0f, "", 1.0f, true, true, disabled_resolutionSlider)) { LUS::Context::GetInstance()->GetWindow()->SetResolutionMultiplier(CVarGetFloat("gInternalResolution", 1)); - }; + } UIWidgets::Tooltip("Multiplies your output resolution by the value inputted, as a more intensive but effective form of anti-aliasing"); #endif + + if (mAdvancedResolutionSettingsWindow) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(12.0f, 6.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ButtonTextAlign, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.22f, 0.38f, 0.56f, 1.0f)); + UIWidgets::Spacer(0); + if (ImGui::Button(GetWindowButtonText("Advanced Resolution", CVarGetInteger("gAdvancedResolutionEditorEnabled", 0)).c_str(), ImVec2(-1.0f, 0.0f))) { + mAdvancedResolutionSettingsWindow->ToggleVisibility(); + } + ImGui::PopStyleColor(1); + ImGui::PopStyleVar(3); + } + #ifndef __WIIU__ if (UIWidgets::PaddedEnhancementSliderInt("MSAA: %d", "##IMSAA", "gMSAAValue", 1, 8, "", 1, true, true, false)) { LUS::Context::GetInstance()->GetWindow()->SetMsaaLevel(CVarGetInteger("gMSAAValue", 1)); From 0a59ce6d9921c73effb04fee79c1b153f158accc Mon Sep 17 00:00:00 2001 From: briaguya <70942617+briaguya-ai@users.noreply.github.com> Date: Sat, 23 Dec 2023 18:12:52 -0500 Subject: [PATCH 23/24] upgrade to docking imgui 1.90.0 (#3402) https://github.com/ocornut/imgui/commit/ce0d0ac8298ce164b5d862577e8b087d92f6e90e --- libultraship | 2 +- soh/soh/Enhancements/audio/AudioEditor.h | 3 +++ soh/soh/Enhancements/controls/GameControlEditor.cpp | 3 +++ soh/soh/Enhancements/controls/SohInputEditorWindow.h | 3 +++ soh/soh/Enhancements/debugconsole.cpp | 4 ++++ soh/soh/Enhancements/randomizer/randomizer.cpp | 4 ++++ soh/soh/SohGui.cpp | 4 +++- soh/soh/SohMenuBar.cpp | 3 +++ soh/soh/UIWidgets.cpp | 3 +++ soh/soh/UIWidgets.hpp | 3 +++ 10 files changed, 30 insertions(+), 2 deletions(-) diff --git a/libultraship b/libultraship index 7a8d314a0..d25e0e977 160000 --- a/libultraship +++ b/libultraship @@ -1 +1 @@ -Subproject commit 7a8d314a0ecc7d6ece1b12626a0ae917ee4ed666 +Subproject commit d25e0e977755ec09c22d6540963ffcd825954a23 diff --git a/soh/soh/Enhancements/audio/AudioEditor.h b/soh/soh/Enhancements/audio/AudioEditor.h index dc058371d..766006ecc 100644 --- a/soh/soh/Enhancements/audio/AudioEditor.h +++ b/soh/soh/Enhancements/audio/AudioEditor.h @@ -4,6 +4,9 @@ #ifdef __cplusplus #include +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif #include class AudioEditor : public LUS::GuiWindow { diff --git a/soh/soh/Enhancements/controls/GameControlEditor.cpp b/soh/soh/Enhancements/controls/GameControlEditor.cpp index 4643dc31d..98de43f34 100644 --- a/soh/soh/Enhancements/controls/GameControlEditor.cpp +++ b/soh/soh/Enhancements/controls/GameControlEditor.cpp @@ -7,6 +7,9 @@ #include #include +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif #include #include #include diff --git a/soh/soh/Enhancements/controls/SohInputEditorWindow.h b/soh/soh/Enhancements/controls/SohInputEditorWindow.h index 089719a58..32443bb54 100644 --- a/soh/soh/Enhancements/controls/SohInputEditorWindow.h +++ b/soh/soh/Enhancements/controls/SohInputEditorWindow.h @@ -2,6 +2,9 @@ #include "stdint.h" #include +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif #include #include #include diff --git a/soh/soh/Enhancements/debugconsole.cpp b/soh/soh/Enhancements/debugconsole.cpp index 3e5a8e624..2c910a067 100644 --- a/soh/soh/Enhancements/debugconsole.cpp +++ b/soh/soh/Enhancements/debugconsole.cpp @@ -17,6 +17,10 @@ #include #include +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include #include #undef PATH_HACK #undef Path diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp index a850222bc..24c258506 100644 --- a/soh/soh/Enhancements/randomizer/randomizer.cpp +++ b/soh/soh/Enhancements/randomizer/randomizer.cpp @@ -12,6 +12,10 @@ #include "3drando/rando_main.hpp" #include "3drando/random.hpp" #include "../../UIWidgets.hpp" +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include #include #include "../custom-message/CustomMessageTypes.h" #include "../item-tables/ItemTableManager.h" diff --git a/soh/soh/SohGui.cpp b/soh/soh/SohGui.cpp index ecc7dd234..fa7072f5a 100644 --- a/soh/soh/SohGui.cpp +++ b/soh/soh/SohGui.cpp @@ -8,8 +8,10 @@ #include "SohGui.hpp" #include -#include +#ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include #include #include #include diff --git a/soh/soh/SohMenuBar.cpp b/soh/soh/SohMenuBar.cpp index 4e488521f..904a57558 100644 --- a/soh/soh/SohMenuBar.cpp +++ b/soh/soh/SohMenuBar.cpp @@ -1,4 +1,7 @@ #include "SohMenuBar.h" +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif #include "ImGui/imgui.h" #include "regex" #include "public/bridge/consolevariablebridge.h" diff --git a/soh/soh/UIWidgets.cpp b/soh/soh/UIWidgets.cpp index 15bb088a3..be8edf1c4 100644 --- a/soh/soh/UIWidgets.cpp +++ b/soh/soh/UIWidgets.cpp @@ -7,7 +7,10 @@ #include "UIWidgets.hpp" +#ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS +#endif +#include #include #include diff --git a/soh/soh/UIWidgets.hpp b/soh/soh/UIWidgets.hpp index b18487977..031a46991 100644 --- a/soh/soh/UIWidgets.hpp +++ b/soh/soh/UIWidgets.hpp @@ -12,6 +12,9 @@ #include #include #include +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif #include namespace UIWidgets { From 776c3a51ee0abe57b5c3b1df53088016d7009d84 Mon Sep 17 00:00:00 2001 From: briaguya <70942617+briaguya-ai@users.noreply.github.com> Date: Sat, 23 Dec 2023 18:48:06 -0500 Subject: [PATCH 24/24] stick sensitivity (#3725) --- libultraship | 2 +- .../controls/SohInputEditorWindow.cpp | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/libultraship b/libultraship index d25e0e977..15d57d806 160000 --- a/libultraship +++ b/libultraship @@ -1 +1 @@ -Subproject commit d25e0e977755ec09c22d6540963ffcd825954a23 +Subproject commit 15d57d806e39d7f19783e26acc1a062d402169c7 diff --git a/soh/soh/Enhancements/controls/SohInputEditorWindow.cpp b/soh/soh/Enhancements/controls/SohInputEditorWindow.cpp index ae8e8cc27..4d0b6b64d 100644 --- a/soh/soh/Enhancements/controls/SohInputEditorWindow.cpp +++ b/soh/soh/Enhancements/controls/SohInputEditorWindow.cpp @@ -636,6 +636,45 @@ void SohInputEditorWindow::DrawStickSection(uint8_t port, uint8_t stick, int32_t ImGui::EndGroup(); ImGui::SetNextItemOpen(true, ImGuiCond_Once); if (ImGui::TreeNode(StringHelper::Sprintf("Analog Stick Options##%d", id).c_str())) { + ImGui::Text("Sensitivity:"); + + int32_t sensitivityPercentage = controllerStick->GetSensitivityPercentage(); + if (sensitivityPercentage == 0) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("-##Sensitivity%d", id).c_str())) { + controllerStick->SetSensitivity(sensitivityPercentage - 1); + } + ImGui::PopButtonRepeat(); + if (sensitivityPercentage == 0) { + ImGui::EndDisabled(); + } + ImGui::SameLine(0.0f, 0.0f); + ImGui::SetNextItemWidth(SCALE_IMGUI_SIZE(160.0f)); + if (ImGui::SliderInt(StringHelper::Sprintf("##Sensitivity%d", id).c_str(), &sensitivityPercentage, 0, 200, "%d%%", + ImGuiSliderFlags_AlwaysClamp)) { + controllerStick->SetSensitivity(sensitivityPercentage); + } + ImGui::SameLine(0.0f, 0.0f); + if (sensitivityPercentage == 200) { + ImGui::BeginDisabled(); + } + ImGui::PushButtonRepeat(true); + if (ImGui::Button(StringHelper::Sprintf("+##Sensitivity%d", id).c_str())) { + controllerStick->SetSensitivity(sensitivityPercentage + 1); + } + ImGui::PopButtonRepeat(); + if (sensitivityPercentage == 200) { + ImGui::EndDisabled(); + } + if (!controllerStick->SensitivityIsDefault()) { + ImGui::SameLine(); + if (ImGui::Button(StringHelper::Sprintf("Reset to Default###resetStickSensitivity%d", id).c_str())) { + controllerStick->ResetSensitivityToDefault(); + } + } + ImGui::Text("Deadzone:"); int32_t deadzonePercentage = controllerStick->GetDeadzonePercentage();