From f702588b9df1de31a7adfd4d52efce217d4340fe Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Sat, 11 Jan 2025 08:51:31 +0000 Subject: [PATCH] Move option change handlers out of `options.cpp` Fixes #7638 --- Source/CMakeLists.txt | 18 +++- Source/controls/controller_buttons.cpp | 14 ++- Source/discord/discord.cpp | 8 ++ Source/engine/render/scrollrt.cpp | 9 ++ Source/engine/render/text_render.cpp | 10 ++ Source/engine/sound.cpp | 20 ++++ Source/game_mode.cpp | 29 ++++++ Source/game_mode.hpp | 5 + Source/lua/lua.cpp | 4 + Source/options.cpp | 134 ++++--------------------- Source/options.h | 5 +- Source/qol/monhealthbar.cpp | 12 +++ Source/qol/xpbar.cpp | 12 +++ Source/utils/display.cpp | 31 ++++++ 14 files changed, 184 insertions(+), 127 deletions(-) diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 52ccb7d3166..732b74d90ca 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -39,7 +39,6 @@ set(libdevilutionx_SRCS controls/axis_direction.cpp controls/controller.cpp - controls/controller_buttons.cpp controls/controller_motion.cpp controls/devices/joystick.cpp controls/devices/kbcontroller.cpp @@ -213,6 +212,13 @@ target_link_dependencies(libdevilutionx_codec PRIVATE libdevilutionx_log ) +add_devilutionx_object_library(libdevilutionx_controller_buttons + controls/controller_buttons.cpp +) +target_link_dependencies(libdevilutionx_controller_buttons + DevilutionX::SDL +) + add_devilutionx_object_library(libdevilutionx_crawl crawl.cpp ) @@ -250,6 +256,10 @@ target_link_dependencies(libdevilutionx_format_int PUBLIC add_devilutionx_object_library(libdevilutionx_game_mode game_mode.cpp ) +target_link_dependencies(libdevilutionx_game_mode PRIVATE + tl + libdevilutionx_options +) add_devilutionx_object_library(libdevilutionx_gendung levels/crypt.cpp @@ -406,12 +416,9 @@ add_devilutionx_object_library(libdevilutionx_options ) target_link_dependencies(libdevilutionx_options PUBLIC DevilutionX::SDL - SDL_audiolib::SDL_audiolib fmt::fmt tl - ${LUA_LIBRARIES} - sol2::sol2 - libdevilutionx_game_mode + libdevilutionx_controller_buttons libdevilutionx_logged_fstream libdevilutionx_strings libdevilutionx_ini @@ -632,6 +639,7 @@ target_link_dependencies(libdevilutionx PUBLIC libdevilutionx_clx_render libdevilutionx_codec libdevilutionx_config + libdevilutionx_controller_buttons libdevilutionx_crawl libdevilutionx_direction libdevilutionx_surface diff --git a/Source/controls/controller_buttons.cpp b/Source/controls/controller_buttons.cpp index 50949436d47..20133aa7326 100644 --- a/Source/controls/controller_buttons.cpp +++ b/Source/controls/controller_buttons.cpp @@ -1,6 +1,6 @@ #include "controller_buttons.h" -#include "plrctrls.h" +#include "controls/game_controls.h" namespace devilution { namespace { @@ -280,17 +280,21 @@ std::string_view ToXboxIcon(ControllerButton button) } // namespace +// Defined in `plrctrls.cpp`. +// Declared here to avoid having to depend on it in tests. +extern GamepadLayout GamepadType; + std::string_view ToString(ControllerButton button) { switch (GamepadType) { - case devilution::GamepadLayout::PlayStation: + case GamepadLayout::PlayStation: return ToPlayStationIcon(button); - case devilution::GamepadLayout::Nintendo: + case GamepadLayout::Nintendo: return ToNintendoIcon(button); - case devilution::GamepadLayout::Xbox: + case GamepadLayout::Xbox: return ToXboxIcon(button); default: - case devilution::GamepadLayout::Generic: + case GamepadLayout::Generic: return ToGenericButtonText(button); } } diff --git a/Source/discord/discord.cpp b/Source/discord/discord.cpp index 160d735d114..7ec110b1c02 100644 --- a/Source/discord/discord.cpp +++ b/Source/discord/discord.cpp @@ -28,6 +28,14 @@ #include "utils/str_cat.hpp" namespace devilution { +namespace { +void IsHellfireChanged() +{ + discord_manager::UpdateMenu(true); +} +const auto IsHellfireChangedHandler = (AddIsHellfireChangeHandler(IsHellfireChanged), true); +} // namespace + namespace discord_manager { // App ID used for DevilutionX's Diablo (classic Diablo's is 496571953147150354) diff --git a/Source/engine/render/scrollrt.cpp b/Source/engine/render/scrollrt.cpp index 68c8b3a3434..2f1d50e3d1c 100644 --- a/Source/engine/render/scrollrt.cpp +++ b/Source/engine/render/scrollrt.cpp @@ -1386,6 +1386,15 @@ void DrawMain(int dwHgt, bool drawDesc, bool drawHp, bool drawMana, bool drawSba } } +void OptionShowFPSChanged() +{ + if (*GetOptions().Graphics.showFPS) + EnableFrameCount(); + else + frameflag = false; +} +const auto OptionChangeHandlerShowFPS = (GetOptions().Graphics.showFPS.SetValueChangedCallback(OptionShowFPSChanged), true); + } // namespace Displacement GetOffsetForWalking(const AnimationInfo &animationInfo, const Direction dir, bool cameraMode /*= false*/) diff --git a/Source/engine/render/text_render.cpp b/Source/engine/render/text_render.cpp index c45bab349eb..e211b9306c1 100644 --- a/Source/engine/render/text_render.cpp +++ b/Source/engine/render/text_render.cpp @@ -26,6 +26,7 @@ #include "engine/render/clx_render.hpp" #include "engine/render/primitive_render.hpp" #include "engine/ticks.hpp" +#include "options.h" #include "utils/algorithm/container.hpp" #include "utils/display.h" #include "utils/is_of.hpp" @@ -475,6 +476,15 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect, return static_cast(remaining.data() - text.data()); } +void OptionLanguageCodeChanged() +{ + UnloadFonts(); + LanguageInitialize(); + LoadLanguageArchive(); +} + +const auto OptionChangeHandlerResolution = (GetOptions().Language.code.SetValueChangedCallback(OptionLanguageCodeChanged), true); + } // namespace void LoadSmallSelectionSpinner() diff --git a/Source/engine/sound.cpp b/Source/engine/sound.cpp index 5724b5354c1..a1ad69748b5 100644 --- a/Source/engine/sound.cpp +++ b/Source/engine/sound.cpp @@ -150,6 +150,26 @@ int CapVolume(int volume) return std::clamp(volume, VOLUME_MIN, VOLUME_MAX); } +void OptionAudioChanged() +{ + effects_cleanup_sfx(); + music_stop(); + snd_deinit(); + snd_init(); + music_start(TMUSIC_INTRO); + if (gbRunGame) + sound_init(); + else + ui_sound_init(); +} + +const auto OptionChangeSampleRate = (GetOptions().Audio.sampleRate.SetValueChangedCallback(OptionAudioChanged), true); +const auto OptionChangeChannels = (GetOptions().Audio.channels.SetValueChangedCallback(OptionAudioChanged), true); +const auto OptionChangeBufferSize = (GetOptions().Audio.bufferSize.SetValueChangedCallback(OptionAudioChanged), true); +const auto OptionChangeResamplingQuality = (GetOptions().Audio.resamplingQuality.SetValueChangedCallback(OptionAudioChanged), true); +const auto OptionChangeResampler = (GetOptions().Audio.resampler.SetValueChangedCallback(OptionAudioChanged), true); +const auto OptionChangeDevice = (GetOptions().Audio.device.SetValueChangedCallback(OptionAudioChanged), true); + } // namespace void ClearDuplicateSounds() diff --git a/Source/game_mode.cpp b/Source/game_mode.cpp index 6a881b723cd..7830961b192 100644 --- a/Source/game_mode.cpp +++ b/Source/game_mode.cpp @@ -1,8 +1,37 @@ #include "game_mode.hpp" +#include + +#include "options.h" + namespace devilution { +namespace { +std::vector> IsHellfireChangeHandlers; + +void OptionGameModeChanged() +{ + gbIsHellfire = *GetOptions().GameMode.gameMode == StartUpGameMode::Hellfire; + for (tl::function_ref handler : IsHellfireChangeHandlers) { + handler(); + } +} +const auto OptionChangeHandlerGameMode = (GetOptions().GameMode.gameMode.SetValueChangedCallback(OptionGameModeChanged), true); + +void OptionSharewareChanged() +{ + gbIsSpawn = *GetOptions().GameMode.shareware; +} +const auto OptionChangeHandlerShareware = (GetOptions().GameMode.shareware.SetValueChangedCallback(OptionSharewareChanged), true); +} // namespace + bool gbIsSpawn; bool gbIsHellfire; bool gbVanilla; bool forceHellfire; + +void AddIsHellfireChangeHandler(tl::function_ref callback) +{ + IsHellfireChangeHandlers.push_back(callback); +} + } // namespace devilution diff --git a/Source/game_mode.hpp b/Source/game_mode.hpp index 60ac5e8f076..0be30d72e4f 100644 --- a/Source/game_mode.hpp +++ b/Source/game_mode.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include "utils/attributes.h" namespace devilution { @@ -13,4 +15,7 @@ extern DVL_API_FOR_TEST bool gbVanilla; /** Whether the Hellfire mode is required (forced). */ extern bool forceHellfire; +/** Adds a handler to be called then `gbIsHellfire` changes after the initial startup. */ +void AddIsHellfireChangeHandler(tl::function_ref callback); + } // namespace devilution diff --git a/Source/lua/lua.cpp b/Source/lua/lua.cpp index 2c80301213c..cca6b2994ff 100644 --- a/Source/lua/lua.cpp +++ b/Source/lua/lua.cpp @@ -244,6 +244,10 @@ void LuaInitialize() // Used by the custom require implementation. lua["setEnvironment"] = [](const sol::environment &env, const sol::function &fn) { sol::set_environment(env, fn); }; + for (OptionEntryBase *mod : GetOptions().Mods.GetEntries()) { + mod->SetValueChangedCallback(LuaReloadActiveMods); + } + LuaReloadActiveMods(); } diff --git a/Source/options.cpp b/Source/options.cpp index 74882a8ebc2..ec96674779b 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -3,34 +3,34 @@ * * Load and save options from the diablo.ini file. */ +#include "options.h" #include #include #include #include +#include +#include +#include #include #include +#include #include #include +#include +#include "appfat.h" #include "control.h" #include "controls/controller.h" +#include "controls/controller_buttons.h" #include "controls/game_controls.h" #include "controls/plrctrls.h" -#include "discord/discord.h" #include "engine/assets.hpp" #include "engine/demomode.h" #include "engine/sound_defs.hpp" -#include "game_mode.hpp" -#include "hwcursor.hpp" -#include "lua/lua.hpp" -#include "options.h" #include "platform/locale.hpp" -#include "qol/monhealthbar.h" -#include "qol/xpbar.h" #include "utils/algorithm/container.hpp" -#include "utils/display.h" #include "utils/file_util.h" #include "utils/ini.hpp" #include "utils/is_of.hpp" @@ -141,85 +141,6 @@ bool HardwareCursorDefault() } #endif -void OptionGrabInputChanged() -{ -#ifdef USE_SDL1 - SDL_WM_GrabInput(*GetOptions().Gameplay.grabInput ? SDL_GRAB_ON : SDL_GRAB_OFF); -#else - if (ghMainWnd != nullptr) - SDL_SetWindowGrab(ghMainWnd, *GetOptions().Gameplay.grabInput ? SDL_TRUE : SDL_FALSE); -#endif -} - -void OptionExperienceBarChanged() -{ - if (!gbRunGame) - return; - if (*GetOptions().Gameplay.experienceBar) - InitXPBar(); - else - FreeXPBar(); -} - -void OptionEnemyHealthBarChanged() -{ - if (!gbRunGame) - return; - if (*GetOptions().Gameplay.enemyHealthBar) - InitMonsterHealthBar(); - else - FreeMonsterHealthBar(); -} - -#if !defined(USE_SDL1) || defined(__3DS__) -void ResizeWindowAndUpdateResolutionOptions() -{ - ResizeWindow(); -#ifndef __3DS__ - GetOptions().Graphics.resolution.InvalidateList(); -#endif -} -#endif - -void OptionShowFPSChanged() -{ - if (*GetOptions().Graphics.showFPS) - EnableFrameCount(); - else - frameflag = false; -} - -void OptionLanguageCodeChanged() -{ - UnloadFonts(); - LanguageInitialize(); - LoadLanguageArchive(); -} - -void OptionGameModeChanged() -{ - gbIsHellfire = *GetOptions().GameMode.gameMode == StartUpGameMode::Hellfire; - discord_manager::UpdateMenu(true); -} - -void OptionSharewareChanged() -{ - gbIsSpawn = *GetOptions().GameMode.shareware; -} - -void OptionAudioChanged() -{ - effects_cleanup_sfx(); - music_stop(); - snd_deinit(); - snd_init(); - music_start(TMUSIC_INTRO); - if (gbRunGame) - sound_init(); - else - ui_sound_init(); -} - } // namespace Options &GetOptions() @@ -319,14 +240,13 @@ OptionEntryFlags OptionEntryBase::GetFlags() const { return flags; } -void OptionEntryBase::SetValueChangedCallback(std::function callback) +void OptionEntryBase::SetValueChangedCallback(tl::function_ref callback) { - this->callback = std::move(callback); + callback_ = callback; } void OptionEntryBase::NotifyValueChanged() { - if (callback) - callback(); + if (callback_.has_value()) (*callback_)(); } void OptionEntryBoolean::LoadFromIni(std::string_view category) @@ -470,8 +390,6 @@ GameModeOptions::GameModeOptions() , shareware("Shareware", OptionEntryFlags::NeedDiabloMpq | OptionEntryFlags::RecreateUI, N_("Restrict to Shareware"), N_("Makes the game compatible with the demo. Enables multiplayer with friends who don't own a full copy of Diablo."), false) { - gameMode.SetValueChangedCallback(OptionGameModeChanged); - shareware.SetValueChangedCallback(OptionSharewareChanged); } std::vector GameModeOptions::GetEntries() { @@ -552,12 +470,6 @@ AudioOptions::AudioOptions() , bufferSize("Buffer Size", OptionEntryFlags::CantChangeInGame, N_("Buffer Size"), N_("Buffer size (number of frames per channel)."), DEFAULT_AUDIO_BUFFER_SIZE, { 1024, 2048, 5120 }) , resamplingQuality("Resampling Quality", OptionEntryFlags::CantChangeInGame, N_("Resampling Quality"), N_("Quality of the resampler, from 0 (lowest) to 10 (highest)."), DEFAULT_AUDIO_RESAMPLING_QUALITY, { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }) { - sampleRate.SetValueChangedCallback(OptionAudioChanged); - channels.SetValueChangedCallback(OptionAudioChanged); - bufferSize.SetValueChangedCallback(OptionAudioChanged); - resamplingQuality.SetValueChangedCallback(OptionAudioChanged); - resampler.SetValueChangedCallback(OptionAudioChanged); - device.SetValueChangedCallback(OptionAudioChanged); } std::vector AudioOptions::GetEntries() { @@ -810,6 +722,13 @@ size_t OptionEntryAudioDevice::GetListSize() const std::string_view OptionEntryAudioDevice::GetListDescription(size_t index) const { + // TODO: Fix the following problems with this function: + // 1. The `string_view` result of `GetDeviceName` is used in the UI but per SDL documentation: + // > If you need to keep the string for any length of time, you should make your own copy of it, + // > as it will be invalid next time any of several other SDL functions are called. + // + // 2. `GetLineWidth` introduces a circular dependency on `text_render` which we'd like to avoid. + // The clipping should be done in the UI instead (settingsmenu.cpp). constexpr int MaxWidth = 500; std::string_view deviceName = GetDeviceName(index); @@ -897,17 +816,6 @@ GraphicsOptions::GraphicsOptions() , limitFPS("FPS Limiter", OptionEntryFlags::None, N_("FPS Limiter"), N_("FPS is limited to avoid high CPU load. Limit considers refresh rate."), true) , showFPS("Show FPS", OptionEntryFlags::None, N_("Show FPS"), N_("Displays the FPS in the upper left corner of the screen."), false) { - resolution.SetValueChangedCallback(ResizeWindow); - fullscreen.SetValueChangedCallback(SetFullscreenMode); -#if !defined(USE_SDL1) || defined(__3DS__) - fitToScreen.SetValueChangedCallback(ResizeWindowAndUpdateResolutionOptions); -#endif -#ifndef USE_SDL1 - scaleQuality.SetValueChangedCallback(ReinitializeTexture); - integerScaling.SetValueChangedCallback(ReinitializeIntegerScale); - vSync.SetValueChangedCallback(ReinitializeRenderer); -#endif - showFPS.SetValueChangedCallback(OptionShowFPSChanged); } std::vector GraphicsOptions::GetEntries() { @@ -988,10 +896,8 @@ GameplayOptions::GameplayOptions() }) , skipLoadingScreenThresholdMs("Skip loading screen threshold, ms", OptionEntryFlags::Invisible, "", "", 0) { - grabInput.SetValueChangedCallback(OptionGrabInputChanged); - experienceBar.SetValueChangedCallback(OptionExperienceBarChanged); - enemyHealthBar.SetValueChangedCallback(OptionEnemyHealthBarChanged); } + std::vector GameplayOptions::GetEntries() { return { @@ -1188,7 +1094,6 @@ void OptionEntryLanguageCode::SetActiveListIndex(size_t index) LanguageOptions::LanguageOptions() : OptionCategoryBase("Language", N_("Language"), N_("Language Settings")) { - code.SetValueChangedCallback(OptionLanguageCodeChanged); } std::vector LanguageOptions::GetEntries() { @@ -1854,7 +1759,6 @@ ModOptions::ModEntry::ModEntry(std::string_view name) : name(name) , enabled(this->name, OptionEntryFlags::None, this->name.c_str(), "", false) { - enabled.SetValueChangedCallback(LuaReloadActiveMods); } namespace { diff --git a/Source/options.h b/Source/options.h index d76798c9d19..60c152ff110 100644 --- a/Source/options.h +++ b/Source/options.h @@ -9,6 +9,7 @@ #include #include +#include #include "controls/controller.h" #include "controls/controller_buttons.h" @@ -113,7 +114,7 @@ class OptionEntryBase { [[nodiscard]] virtual OptionEntryType GetType() const = 0; [[nodiscard]] OptionEntryFlags GetFlags() const; - void SetValueChangedCallback(std::function callback); + void SetValueChangedCallback(tl::function_ref callback); [[nodiscard]] virtual std::string_view GetValueDescription() const = 0; virtual void LoadFromIni(std::string_view category) = 0; @@ -128,7 +129,7 @@ class OptionEntryBase { void NotifyValueChanged(); private: - std::function callback; + std::optional> callback_; }; class OptionEntryBoolean : public OptionEntryBase { diff --git a/Source/qol/monhealthbar.cpp b/Source/qol/monhealthbar.cpp index ed60b90b95d..3a1b9e188b3 100644 --- a/Source/qol/monhealthbar.cpp +++ b/Source/qol/monhealthbar.cpp @@ -28,6 +28,18 @@ OptionalOwnedClxSpriteList health; OptionalOwnedClxSpriteList healthBlue; OptionalOwnedClxSpriteList playerExpTags; +void OptionEnemyHealthBarChanged() +{ + if (!gbRunGame) + return; + if (*GetOptions().Gameplay.enemyHealthBar) + InitMonsterHealthBar(); + else + FreeMonsterHealthBar(); +} + +const auto OptionChangeHandler = (GetOptions().Gameplay.enemyHealthBar.SetValueChangedCallback(OptionEnemyHealthBarChanged), true); + } // namespace void InitMonsterHealthBar() diff --git a/Source/qol/xpbar.cpp b/Source/qol/xpbar.cpp index 32604e88432..b096c02afc6 100644 --- a/Source/qol/xpbar.cpp +++ b/Source/qol/xpbar.cpp @@ -50,6 +50,18 @@ void DrawEndCap(const Surface &out, Point point, int idx, const ColorGradient &g out.SetPixel({ point.x, point.y + 3 }, gradient[idx / 2]); } +void OptionExperienceBarChanged() +{ + if (!gbRunGame) + return; + if (*GetOptions().Gameplay.experienceBar) + InitXPBar(); + else + FreeXPBar(); +} + +const auto OptionChangeHandler = (GetOptions().Gameplay.experienceBar.SetValueChangedCallback(OptionExperienceBarChanged), true); + } // namespace void InitXPBar() diff --git a/Source/utils/display.cpp b/Source/utils/display.cpp index 66a15b69c15..b4eb625bb00 100644 --- a/Source/utils/display.cpp +++ b/Source/utils/display.cpp @@ -193,6 +193,37 @@ Size GetPreferredWindowSize() return windowSize; } +const auto OptionChangeHandlerResolution = (GetOptions().Graphics.resolution.SetValueChangedCallback(ResizeWindow), true); +const auto OptionChangeHandlerFullscreen = (GetOptions().Graphics.fullscreen.SetValueChangedCallback(SetFullscreenMode), true); + +void OptionGrabInputChanged() +{ +#ifdef USE_SDL1 + SDL_WM_GrabInput(*GetOptions().Gameplay.grabInput ? SDL_GRAB_ON : SDL_GRAB_OFF); +#else + if (ghMainWnd != nullptr) + SDL_SetWindowGrab(ghMainWnd, *GetOptions().Gameplay.grabInput ? SDL_TRUE : SDL_FALSE); +#endif +} +const auto OptionChangeHandlerGrabInput = (GetOptions().Gameplay.grabInput.SetValueChangedCallback(OptionGrabInputChanged), true); + +#if !defined(USE_SDL1) || defined(__3DS__) +void ResizeWindowAndUpdateResolutionOptions() +{ + ResizeWindow(); +#ifndef __3DS__ + GetOptions().Graphics.resolution.InvalidateList(); +#endif +} +const auto OptionChangeHandlerFitToScreen = (GetOptions().Graphics.fitToScreen.SetValueChangedCallback(ResizeWindowAndUpdateResolutionOptions), true); +#endif + +#ifndef USE_SDL1 +const auto OptionChangeHandlerScaleQuality = (GetOptions().Graphics.scaleQuality.SetValueChangedCallback(ReinitializeTexture), true); +const auto OptionChangeHandlerIntegerScaling = (GetOptions().Graphics.integerScaling.SetValueChangedCallback(ReinitializeIntegerScale), true); +const auto OptionChangeHandlerVSync = (GetOptions().Graphics.vSync.SetValueChangedCallback(ReinitializeRenderer), true); +#endif + } // namespace void AdjustToScreenGeometry(Size windowSize)