diff --git a/Dalamud/Game/Config/ConfigChangeEvent.cs b/Dalamud/Game/Config/ConfigChangeEvent.cs new file mode 100644 index 000000000..941033c61 --- /dev/null +++ b/Dalamud/Game/Config/ConfigChangeEvent.cs @@ -0,0 +1,7 @@ +using System; + +namespace Dalamud.Game.Config; + +public abstract record ConfigChangeEvent(Enum Option); + +public record ConfigChangeEvent(T ConfigOption) : ConfigChangeEvent(ConfigOption) where T : Enum; diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index 5587787c9..a41b60936 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -1,6 +1,10 @@ -using Dalamud.IoC; +using System; +using Dalamud.Hooking; +using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; +using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Common.Configuration; using Serilog; namespace Dalamud.Game.Config; @@ -14,10 +18,13 @@ namespace Dalamud.Game.Config; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed class GameConfig : IServiceType, IGameConfig +public sealed class GameConfig : IServiceType, IGameConfig, IDisposable { + private readonly GameConfigAddressResolver address = new(); + private Hook? configChangeHook; + [ServiceManager.ServiceConstructor] - private unsafe GameConfig(Framework framework) + private unsafe GameConfig(Framework framework, SigScanner sigScanner) { framework.RunOnTick(() => { @@ -27,9 +34,18 @@ private unsafe GameConfig(Framework framework) this.System = new GameConfigSection("System", framework, &commonConfig->ConfigBase); this.UiConfig = new GameConfigSection("UiConfig", framework, &commonConfig->UiConfig); this.UiControl = new GameConfigSection("UiControl", framework, () => this.UiConfig.TryGetBool("PadMode", out var padMode) && padMode ? &commonConfig->UiControlGamepadConfig : &commonConfig->UiControlConfig); + + this.address.Setup(sigScanner); + this.configChangeHook = Hook.FromAddress(this.address.ConfigChangeAddress, this.OnConfigChanged); + this.configChangeHook?.Enable(); }); } + private unsafe delegate nint ConfigChangeDelegate(ConfigBase* configBase, ConfigEntry* configEntry); + + /// + public event EventHandler Changed; + /// public GameConfigSection System { get; private set; } @@ -110,4 +126,43 @@ private unsafe GameConfig(Framework framework) /// public void Set(UiControlOption option, string value) => this.UiControl.Set(option.GetName(), value); + + /// + void IDisposable.Dispose() + { + this.configChangeHook?.Disable(); + this.configChangeHook?.Dispose(); + } + + private unsafe nint OnConfigChanged(ConfigBase* configBase, ConfigEntry* configEntry) + { + var returnValue = this.configChangeHook!.Original(configBase, configEntry); + try + { + ConfigChangeEvent? eventArgs = null; + + if (configBase == this.System.GetConfigBase()) + { + eventArgs = this.System.InvokeChange(configEntry); + } + else if (configBase == this.UiConfig.GetConfigBase()) + { + eventArgs = this.UiConfig.InvokeChange(configEntry); + } + else if (configBase == this.UiControl.GetConfigBase()) + { + eventArgs = this.UiControl.InvokeChange(configEntry); + } + + if (eventArgs == null) return returnValue; + + this.Changed?.InvokeSafely(this, eventArgs); + } + catch (Exception ex) + { + Log.Error(ex, $"Exception thrown handing {nameof(this.OnConfigChanged)} events."); + } + + return returnValue; + } } diff --git a/Dalamud/Game/Config/GameConfigAddressResolver.cs b/Dalamud/Game/Config/GameConfigAddressResolver.cs new file mode 100644 index 000000000..6a207807a --- /dev/null +++ b/Dalamud/Game/Config/GameConfigAddressResolver.cs @@ -0,0 +1,18 @@ +namespace Dalamud.Game.Config; + +/// +/// Game config system address resolver. +/// +public sealed class GameConfigAddressResolver : BaseAddressResolver +{ + /// + /// Gets the address of the method called when any config option is changed. + /// + public nint ConfigChangeAddress { get; private set; } + + /// + protected override void Setup64Bit(SigScanner scanner) + { + this.ConfigChangeAddress = scanner.ScanText("E8 ?? ?? ?? ?? 48 8B 3F 49 3B 3E"); + } +} diff --git a/Dalamud/Game/Config/GameConfigSection.cs b/Dalamud/Game/Config/GameConfigSection.cs index 107b0d4a8..7b2751901 100644 --- a/Dalamud/Game/Config/GameConfigSection.cs +++ b/Dalamud/Game/Config/GameConfigSection.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using Dalamud.Memory; +using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Common.Configuration; using Serilog; @@ -16,6 +17,12 @@ public class GameConfigSection private readonly Framework framework; private readonly Dictionary indexMap = new(); private readonly Dictionary nameMap = new(); + private readonly Dictionary enumMap = new(); + + /// + /// Event which is fired when a game config option is changed within the section. + /// + public event EventHandler Changed; /// /// Initializes a new instance of the class. @@ -59,7 +66,10 @@ internal GameConfigSection(string sectionName, Framework framework, GetConfigBas /// public string SectionName { get; } - private GetConfigBaseDelegate GetConfigBase { get; } + /// + /// Gets the pointer to the config section container. + /// + internal GetConfigBaseDelegate GetConfigBase { get; } /// /// Attempts to get a boolean config option. @@ -380,6 +390,35 @@ public unsafe void Set(string name, string value) }); } + /// + /// Invokes a change event within the config section. + /// + /// The config entry that was changed. + /// SystemConfigOption, UiConfigOption, or UiControlOption. + /// The ConfigChangeEvent record. + internal unsafe ConfigChangeEvent? InvokeChange(ConfigEntry* entry) where TEnum : Enum + { + if (!this.enumMap.TryGetValue(entry->Index, out var enumObject)) + { + if (entry->Name == null) return null; + var name = MemoryHelper.ReadStringNullTerminated(new IntPtr(entry->Name)); + if (Enum.TryParse(typeof(TEnum), name, out enumObject)) + { + this.enumMap.Add(entry->Index, enumObject); + } + else + { + enumObject = null; + this.enumMap.Add(entry->Index, null); + } + } + + if (enumObject == null) return null; + var eventArgs = new ConfigChangeEvent((TEnum)enumObject); + this.Changed?.InvokeSafely(this, eventArgs); + return eventArgs; + } + private unsafe bool TryGetIndex(string name, out uint index) { if (this.indexMap.TryGetValue(name, out index)) diff --git a/Dalamud/Plugin/Services/IGameConfig.cs b/Dalamud/Plugin/Services/IGameConfig.cs index bbff123c0..f0607c39e 100644 --- a/Dalamud/Plugin/Services/IGameConfig.cs +++ b/Dalamud/Plugin/Services/IGameConfig.cs @@ -1,6 +1,8 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; using Dalamud.Game.Config; +using FFXIVClientStructs.FFXIV.Common.Configuration; namespace Dalamud.Plugin.Services; @@ -9,6 +11,11 @@ namespace Dalamud.Plugin.Services; /// public interface IGameConfig { + /// + /// Event which is fired when a game config option is changed. + /// + public event EventHandler Changed; + /// /// Gets the collection of config options that persist between characters. ///