Add reshade unwrapping options

This commit is contained in:
Soreepeong 2024-07-22 20:29:26 +09:00
parent 80ac97fea8
commit d71fbc52fb
9 changed files with 513 additions and 75 deletions

View file

@ -8,6 +8,7 @@ using System.Runtime.InteropServices;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.Style;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal.AutoUpdate;
@ -441,6 +442,9 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary>
public bool WindowIsImmersive { get; set; } = false;
/// <summary>Gets or sets the mode specifying how to handle ReShade.</summary>
public ReShadeHandlingMode ReShadeHandlingMode { get; set; } = ReShadeHandlingMode.ReShadeAddon;
/// <summary>
/// Gets or sets hitch threshold for game network up in milliseconds.
/// </summary>

View file

@ -17,6 +17,7 @@ using Dalamud.Hooking.Internal;
using Dalamud.Hooking.WndProcHook;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal.ManagedAsserts;
using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.ManagedFontAtlas.Internals;
using Dalamud.Interface.Style;
@ -72,7 +73,7 @@ internal partial class InterfaceManager : IInternalDisposableService
private readonly ConcurrentBag<IDisposable> deferredDisposeDisposables = new();
[ServiceManager.ServiceDependency]
private readonly WndProcHookManager wndProcHookManager = Service<WndProcHookManager>.Get();
private readonly DalamudConfiguration dalamudConfiguration = Service<DalamudConfiguration>.Get();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
@ -82,6 +83,9 @@ internal partial class InterfaceManager : IInternalDisposableService
[UsedImplicitly]
private readonly HookManager hookManager = Service<HookManager>.Get();
[ServiceManager.ServiceDependency]
private readonly WndProcHookManager wndProcHookManager = Service<WndProcHookManager>.Get();
private readonly ConcurrentQueue<Action> runBeforeImGuiRender = new();
private readonly ConcurrentQueue<Action> runAfterImGuiRender = new();
@ -90,7 +94,7 @@ internal partial class InterfaceManager : IInternalDisposableService
private Hook<SetCursorDelegate>? setCursorHook;
private Hook<DxgiPresentDelegate>? dxgiPresentHook;
private Hook<ResizeBuffersDelegate>? resizeBuffersHook;
private ReShadeHandling.ReShadeAddonInterface? reShadeAddonInterface;
private ReShadeAddonInterface? reShadeAddonInterface;
private IFontAtlas? dalamudAtlas;
private ILockedImFont? defaultFontResourceLock;
@ -759,17 +763,23 @@ internal partial class InterfaceManager : IInternalDisposableService
this.SetCursorDetour);
Log.Verbose("===== S W A P C H A I N =====");
if (ReShadeHandling.ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface))
if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.UnwrapReShade)
{
if (SwapChainHelper.UnwrapReShade())
Log.Verbose("Unwrapped ReShade.");
}
if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.ReShadeAddon &&
ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface))
{
this.resizeBuffersHook = Hook<ResizeBuffersDelegate>.FromAddress(
(nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers,
this.AsReShadeAddonResizeBuffersDetour);
Log.Verbose($"ResizeBuffers address {Util.DescribeAddress(this.resizeBuffersHook!.Address)}");
Log.Verbose(
"Registered as a ReShade({name}: 0x{addr:X}) addon.",
ReShadeHandling.ReShadeAddonInterface.ReShadeModule!.FileName,
ReShadeHandling.ReShadeAddonInterface.ReShadeModule!.BaseAddress);
ReShadeAddonInterface.ReShadeModule!.FileName,
ReShadeAddonInterface.ReShadeModule!.BaseAddress);
this.reShadeAddonInterface.InitSwapChain += this.ReShadeAddonInterfaceOnInitSwapChain;
this.reShadeAddonInterface.DestroySwapChain += this.ReShadeAddonInterfaceOnDestroySwapChain;
this.reShadeAddonInterface.ReShadeOverlay += this.ReShadeAddonInterfaceOnReShadeOverlay;
@ -779,16 +789,18 @@ internal partial class InterfaceManager : IInternalDisposableService
this.resizeBuffersHook = Hook<ResizeBuffersDelegate>.FromAddress(
(nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers,
this.AsHookResizeBuffersDetour);
Log.Verbose($"ResizeBuffers address {Util.DescribeAddress(this.resizeBuffersHook!.Address)}");
var addr = (nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present;
this.dxgiPresentHook = Hook<DxgiPresentDelegate>.FromAddress(addr, this.PresentDetour);
Log.Verbose($"IDXGISwapChain::Present address {Util.DescribeAddress(addr)}");
this.dxgiPresentHook = Hook<DxgiPresentDelegate>.FromAddress(
(nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present,
this.PresentDetour);
}
Log.Verbose($"IDXGISwapChain::ResizeBuffers address: {Util.DescribeAddress(this.resizeBuffersHook.Address)}");
Log.Verbose($"IDXGISwapChain::Present address: {Util.DescribeAddress(this.dxgiPresentHook?.Address ?? 0)}");
this.setCursorHook.Enable();
this.dxgiPresentHook?.Enable();
this.resizeBuffersHook?.Enable();
this.resizeBuffersHook.Enable();
}
private IntPtr SetCursorDetour(IntPtr hCursor)

View file

@ -0,0 +1,14 @@
namespace Dalamud.Interface.Internal.ReShadeHandling;
/// <summary>Available handling modes for working with ReShade.</summary>
internal enum ReShadeHandlingMode
{
/// <summary>Register as a ReShade addon, and draw on reshade_overlay event.</summary>
ReShadeAddon,
/// <summary>Unwraps ReShade from the swap chain obtained from the game.</summary>
UnwrapReShade,
/// <summary>Do not do anything special about it. ReShade will process Dalamud rendered stuff.</summary>
None = -1,
}

View file

@ -0,0 +1,205 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using TerraFX.Interop.Windows;
using static TerraFX.Interop.Windows.Windows;
namespace Dalamud.Interface.Internal.ReShadeHandling;
/// <summary>
/// Peels ReShade off stuff.
/// </summary>
[SuppressMessage(
"StyleCop.CSharp.LayoutRules",
"SA1519:Braces should not be omitted from multi-line child statement",
Justification = "Multiple fixed blocks")]
internal static unsafe class ReShadeUnwrapper
{
/// <summary>Unwraps <typeparamref name="T"/> if it is wrapped by ReShade.</summary>
/// <param name="comptr">[inout] The COM pointer to an instance of <typeparamref name="T"/>.</param>
/// <typeparam name="T">A COM type that is or extends <see cref="IUnknown"/>.</typeparam>
/// <returns><c>true</c> if peeled.</returns>
public static bool Unwrap<T>(ComPtr<T>* comptr)
where T : unmanaged, IUnknown.Interface
{
if (typeof(T).GetNestedType("Vtbl`1") is not { } vtblType)
return false;
nint vtblSize = vtblType.GetFields().Length * sizeof(nint);
var changed = false;
while (comptr->Get() != null && IsReShadedComObject(comptr->Get()))
{
// Expectation: the pointer to the underlying object should come early after the overriden vtable.
for (nint i = sizeof(nint); i <= 0x20; i += sizeof(nint))
{
var ppObjectBehind = (nint)comptr->Get() + i;
// Is the thing directly pointed from the address an actual something in the memory?
if (!IsValidReadableMemoryAddress(ppObjectBehind, 8))
continue;
var pObjectBehind = *(nint*)ppObjectBehind;
// Is the address of vtable readable?
if (!IsValidReadableMemoryAddress(pObjectBehind, sizeof(nint)))
continue;
var pObjectBehindVtbl = *(nint*)pObjectBehind;
// Is the vtable itself readable?
if (!IsValidReadableMemoryAddress(pObjectBehindVtbl, vtblSize))
continue;
// Are individual functions in vtable executable?
var valid = true;
for (var j = 0; valid && j < vtblSize; j += sizeof(nint))
valid &= IsValidExecutableMemoryAddress(*(nint*)(pObjectBehindVtbl + j), 1);
if (!valid)
continue;
// Interpret the object as an IUnknown.
// Note that `using` is not used, and `Attach` is used. We do not alter the reference count yet.
var punk = default(ComPtr<IUnknown>);
punk.Attach((IUnknown*)pObjectBehind);
// Is the IUnknown object also the type we want?
using var comptr2 = default(ComPtr<T>);
if (punk.As(&comptr2).FAILED)
continue;
comptr2.Swap(comptr);
changed = true;
break;
}
if (!changed)
break;
}
return changed;
}
private static bool BelongsInReShadeDll(nint ptr)
{
foreach (ProcessModule processModule in Process.GetCurrentProcess().Modules)
{
if (ptr < processModule.BaseAddress || ptr >= processModule.BaseAddress + processModule.ModuleMemorySize)
continue;
fixed (byte* pfn0 = "ReShadeRegisterAddon"u8)
fixed (byte* pfn1 = "ReShadeUnregisterAddon"u8)
fixed (byte* pfn2 = "ReShadeRegisterEvent"u8)
fixed (byte* pfn3 = "ReShadeUnregisterEvent"u8)
{
if (GetProcAddress((HMODULE)processModule.BaseAddress, (sbyte*)pfn0) == 0)
continue;
if (GetProcAddress((HMODULE)processModule.BaseAddress, (sbyte*)pfn1) == 0)
continue;
if (GetProcAddress((HMODULE)processModule.BaseAddress, (sbyte*)pfn2) == 0)
continue;
if (GetProcAddress((HMODULE)processModule.BaseAddress, (sbyte*)pfn3) == 0)
continue;
}
return true;
}
return false;
}
private static bool IsReShadedComObject<T>(T* obj)
where T : unmanaged, IUnknown.Interface
{
if (!IsValidReadableMemoryAddress((nint)obj, sizeof(nint)))
return false;
try
{
var vtbl = (nint**)Marshal.ReadIntPtr((nint)obj);
if (!IsValidReadableMemoryAddress((nint)vtbl, sizeof(nint) * 3))
return false;
for (var i = 0; i < 3; i++)
{
var pfn = Marshal.ReadIntPtr((nint)(vtbl + i));
if (!IsValidExecutableMemoryAddress(pfn, 1))
return false;
if (!BelongsInReShadeDll(pfn))
return false;
}
return true;
}
catch
{
return false;
}
}
private static bool IsValidReadableMemoryAddress(nint p, nint size)
{
while (size > 0)
{
if (!IsValidUserspaceMemoryAddress(p))
return false;
MEMORY_BASIC_INFORMATION mbi;
if (VirtualQuery((void*)p, &mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION)) == 0)
return false;
if (mbi is not
{
State: MEM.MEM_COMMIT,
Protect: PAGE.PAGE_READONLY or PAGE.PAGE_READWRITE or PAGE.PAGE_EXECUTE_READ
or PAGE.PAGE_EXECUTE_READWRITE,
})
return false;
var regionSize = (nint)((mbi.RegionSize + 0xFFFUL) & ~0x1000UL);
var checkedSize = ((nint)mbi.BaseAddress + regionSize) - p;
size -= checkedSize;
p += checkedSize;
}
return true;
}
private static bool IsValidExecutableMemoryAddress(nint p, nint size)
{
while (size > 0)
{
if (!IsValidUserspaceMemoryAddress(p))
return false;
MEMORY_BASIC_INFORMATION mbi;
if (VirtualQuery((void*)p, &mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION)) == 0)
return false;
if (mbi is not
{
State: MEM.MEM_COMMIT,
Protect: PAGE.PAGE_EXECUTE or PAGE.PAGE_EXECUTE_READ or PAGE.PAGE_EXECUTE_READWRITE
or PAGE.PAGE_EXECUTE_WRITECOPY,
})
return false;
var regionSize = (nint)((mbi.RegionSize + 0xFFFUL) & ~0x1000UL);
var checkedSize = ((nint)mbi.BaseAddress + regionSize) - p;
size -= checkedSize;
p += checkedSize;
}
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsValidUserspaceMemoryAddress(nint p)
{
// https://learn.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/virtual-address-spaces
// A 64-bit process on 64-bit Windows has a virtual address space within the 128-terabyte range
// 0x000'00000000 through 0x7FFF'FFFFFFFF.
return p >= 0x10000 && p <= unchecked((nint)0x7FFF_FFFFFFFFUL);
}
}

View file

@ -1,5 +1,7 @@
using System.Threading;
using Dalamud.Interface.Internal.ReShadeHandling;
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using TerraFX.Interop.DirectX;
@ -10,12 +12,17 @@ namespace Dalamud.Interface.Internal;
/// <summary>Helper for dealing with swap chains.</summary>
internal static unsafe class SwapChainHelper
{
private static IDXGISwapChain* foundGameDeviceSwapChain;
/// <summary>Gets the game's active instance of IDXGISwapChain that is initialized.</summary>
/// <value>Address of the game's instance of IDXGISwapChain, or <c>null</c> if not available (yet.)</value>
public static IDXGISwapChain* GameDeviceSwapChain
{
get
{
if (foundGameDeviceSwapChain is not null)
return foundGameDeviceSwapChain;
var kernelDev = Device.Instance();
if (kernelDev == null)
return null;
@ -29,7 +36,7 @@ internal static unsafe class SwapChainHelper
if (swapChain->BackBuffer == null)
return null;
return (IDXGISwapChain*)swapChain->DXGISwapChain;
return foundGameDeviceSwapChain = (IDXGISwapChain*)swapChain->DXGISwapChain;
}
}
@ -80,4 +87,18 @@ internal static unsafe class SwapChainHelper
while (GameDeviceSwapChain is null)
Thread.Yield();
}
/// <summary>
/// Make <see cref="GameDeviceSwapChain"/> store address of unwrapped swap chain, if it was wrapped with ReShade.
/// </summary>
/// <returns><c>true</c> if it was wrapped with ReShade.</returns>
public static bool UnwrapReShade()
{
using var swapChain = new ComPtr<IDXGISwapChain>(GameDeviceSwapChain);
if (!ReShadeUnwrapper.Unwrap(&swapChain))
return false;
foundGameDeviceSwapChain = swapChain.Get();
return true;
}
}

View file

@ -1,8 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using CheapLoc;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Interface.Internal.Windows.Settings.Widgets;
using Dalamud.Interface.Utility;
@ -11,28 +13,39 @@ using Dalamud.Utility;
namespace Dalamud.Interface.Internal.Windows.Settings.Tabs;
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")]
[SuppressMessage(
"StyleCop.CSharp.DocumentationRules",
"SA1600:Elements should be documented",
Justification = "Internals")]
public class SettingsTabExperimental : SettingsTab
{
public override SettingsEntry[] Entries { get; } =
{
[
new SettingsEntry<bool>(
Loc.Localize("DalamudSettingsPluginTest", "Get plugin testing builds"),
string.Format(
Loc.Localize("DalamudSettingsPluginTestHint", "Receive testing prereleases for selected plugins.\nTo opt-in to testing builds for a plugin, you have to right click it in the \"{0}\" tab of the plugin installer and select \"{1}\"."),
Loc.Localize(
"DalamudSettingsPluginTestHint",
"Receive testing prereleases for selected plugins.\nTo opt-in to testing builds for a plugin, you have to right click it in the \"{0}\" tab of the plugin installer and select \"{1}\"."),
PluginCategoryManager.Locs.Group_Installed,
PluginInstallerWindow.Locs.PluginContext_TestingOptIn),
c => c.DoPluginTest,
(v, c) => c.DoPluginTest = v),
new HintSettingsEntry(
Loc.Localize("DalamudSettingsPluginTestWarning", "Testing plugins may contain bugs or crash your game. Please only enable this if you are aware of the risks."),
Loc.Localize(
"DalamudSettingsPluginTestWarning",
"Testing plugins may contain bugs or crash your game. Please only enable this if you are aware of the risks."),
ImGuiColors.DalamudRed),
new GapSettingsEntry(5),
new SettingsEntry<bool>(
Loc.Localize("DalamudSettingEnablePluginUIAdditionalOptions", "Add a button to the title bar of plugin windows to open additional options"),
Loc.Localize("DalamudSettingEnablePluginUIAdditionalOptionsHint", "This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."),
Loc.Localize(
"DalamudSettingEnablePluginUIAdditionalOptions",
"Add a button to the title bar of plugin windows to open additional options"),
Loc.Localize(
"DalamudSettingEnablePluginUIAdditionalOptionsHint",
"This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."),
c => c.EnablePluginUiAdditionalOptions,
(v, c) => c.EnablePluginUiAdditionalOptions = v),
@ -40,7 +53,9 @@ public class SettingsTabExperimental : SettingsTab
new ButtonSettingsEntry(
Loc.Localize("DalamudSettingsClearHidden", "Clear hidden plugins"),
Loc.Localize("DalamudSettingsClearHiddenHint", "Restore plugins you have previously hidden from the plugin installer."),
Loc.Localize(
"DalamudSettingsClearHiddenHint",
"Restore plugins you have previously hidden from the plugin installer."),
() =>
{
Service<DalamudConfiguration>.Get().HiddenPluginInternalName.Clear();
@ -55,6 +70,45 @@ public class SettingsTabExperimental : SettingsTab
new ThirdRepoSettingsEntry(),
new GapSettingsEntry(5, true),
new EnumSettingsEntry<ReShadeHandlingMode>(
Loc.Localize("DalamudSettingsReShadeHandlingMode", "ReShade handling mode"),
Loc.Localize(
"DalamudSettingsReShadeHandlingModeHint",
"You may try different options to work around problems you may encounter.\nRestart is required for changes to take effect."),
c => c.ReShadeHandlingMode,
(v, c) => c.ReShadeHandlingMode = v,
fallbackValue: ReShadeHandlingMode.ReShadeAddon)
{
FriendlyEnumNameGetter = x => x switch
{
ReShadeHandlingMode.ReShadeAddon => Loc.Localize(
"DalamudSettingsReShadeHandlingModeReShadeAddon",
"ReShade addon"),
ReShadeHandlingMode.UnwrapReShade => Loc.Localize(
"DalamudSettingsReShadeHandlingModeUnwrapReShade",
"Unwrap ReShade"),
ReShadeHandlingMode.None => Loc.Localize(
"DalamudSettingsReShadeHandlingModeNone",
"Do not handle"),
_ => "<invalid>",
},
FriendlyEnumDescriptionGetter = x => x switch
{
ReShadeHandlingMode.ReShadeAddon => Loc.Localize(
"DalamudSettingsReShadeHandlingModeReShadeAddonDescription",
"Dalamud will register itself as a ReShade addon. Most compatibility is expected, but multi-monitor window option won't work too well."),
ReShadeHandlingMode.UnwrapReShade => Loc.Localize(
"DalamudSettingsReShadeHandlingModeUnwrapReShadeDescription",
"Dalamud will exclude itself from all ReShade handling. Multi-monitor windows should work fine with this mode, but it may not be supported and crash in future ReShade versions."),
ReShadeHandlingMode.None => Loc.Localize(
"DalamudSettingsReShadeHandlingModeNoneDescription",
"No special handling will be done for ReShade. Dalamud will be under the effect of ReShade postprocessing."),
_ => "<invalid>",
},
},
/* Disabling profiles after they've been enabled doesn't make much sense, at least not if the user has already created profiles.
new GapSettingsEntry(5, true),
@ -64,7 +118,7 @@ public class SettingsTabExperimental : SettingsTab
c => c.ProfilesEnabled,
(v, c) => c.ProfilesEnabled = v),
*/
};
];
public override string Title => Loc.Localize("DalamudSettingsExperimental", "Experimental");
@ -72,7 +126,9 @@ public class SettingsTabExperimental : SettingsTab
{
base.Draw();
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "Total memory used by Dalamud & Plugins: " + Util.FormatBytes(GC.GetTotalMemory(false)));
ImGuiHelpers.SafeTextColoredWrapped(
ImGuiColors.DalamudGrey,
"Total memory used by Dalamud & Plugins: " + Util.FormatBytes(GC.GetTotalMemory(false)));
ImGuiHelpers.ScaledDummy(15);
}
}

View file

@ -15,7 +15,7 @@ public class SettingsTabGeneral : SettingsTab
new GapSettingsEntry(5),
new SettingsEntry<XivChatType>(
new EnumSettingsEntry<XivChatType>(
Loc.Localize("DalamudSettingsChannel", "Dalamud Chat Channel"),
Loc.Localize("DalamudSettingsChannelHint", "Select the chat channel that is to be used for general Dalamud messages."),
c => c.GeneralChatType,

View file

@ -0,0 +1,175 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
namespace Dalamud.Interface.Internal.Windows.Settings.Widgets;
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")]
internal sealed class EnumSettingsEntry<T> : SettingsEntry
where T : struct, Enum
{
private readonly LoadSettingDelegate load;
private readonly SaveSettingDelegate save;
private readonly Action<T>? change;
private readonly T fallbackValue;
private T valueBacking;
public EnumSettingsEntry(
string name,
string description,
LoadSettingDelegate load,
SaveSettingDelegate save,
Action<T>? change = null,
Func<T, string?>? warning = null,
Func<T, string?>? validity = null,
Func<bool>? visibility = null,
T fallbackValue = default)
{
this.load = load;
this.save = save;
this.change = change;
this.Name = name;
this.Description = description;
this.CheckWarning = warning;
this.CheckValidity = validity;
this.CheckVisibility = visibility;
this.fallbackValue = fallbackValue;
}
public delegate T LoadSettingDelegate(DalamudConfiguration config);
public delegate void SaveSettingDelegate(T value, DalamudConfiguration config);
public T Value
{
get => this.valueBacking;
set
{
if (Equals(value, this.valueBacking))
return;
this.valueBacking = value;
this.change?.Invoke(value);
}
}
public string Description { get; }
public Action<EnumSettingsEntry<T>>? CustomDraw { get; init; }
public Func<T, string?>? CheckValidity { get; init; }
public Func<T, string?>? CheckWarning { get; init; }
public Func<bool>? CheckVisibility { get; init; }
public Func<T, string> FriendlyEnumNameGetter { get; init; } = x => x.ToString();
public Func<T, string> FriendlyEnumDescriptionGetter { get; init; } = _ => string.Empty;
public override bool IsVisible => this.CheckVisibility?.Invoke() ?? true;
public override void Draw()
{
Debug.Assert(this.Name != null, "this.Name != null");
if (this.CustomDraw is not null)
{
this.CustomDraw.Invoke(this);
}
else
{
ImGuiHelpers.SafeTextWrapped(this.Name);
var idx = this.valueBacking;
var values = Enum.GetValues<T>();
if (!values.Contains(idx))
{
idx = Enum.IsDefined(this.fallbackValue)
? this.fallbackValue
: throw new InvalidOperationException("No fallback value for enum");
this.valueBacking = idx;
}
if (ImGui.BeginCombo($"###{this.Id.ToString()}", this.FriendlyEnumNameGetter(idx)))
{
foreach (var value in values)
{
if (ImGui.Selectable(this.FriendlyEnumNameGetter(value), idx.Equals(value)))
{
this.valueBacking = value;
}
}
ImGui.EndCombo();
}
}
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
{
var desc = this.FriendlyEnumDescriptionGetter(this.valueBacking);
if (!string.IsNullOrWhiteSpace(desc))
{
ImGuiHelpers.SafeTextWrapped(desc);
ImGuiHelpers.ScaledDummy(2);
}
ImGuiHelpers.SafeTextWrapped(this.Description);
}
if (this.CheckValidity != null)
{
var validityMsg = this.CheckValidity.Invoke(this.Value);
this.IsValid = string.IsNullOrEmpty(validityMsg);
if (!this.IsValid)
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
ImGui.Text(validityMsg);
}
}
}
else
{
this.IsValid = true;
}
var warningMessage = this.CheckWarning?.Invoke(this.Value);
if (warningMessage != null)
{
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
{
ImGui.Text(warningMessage);
}
}
}
public override void Load()
{
this.valueBacking = this.load(Service<DalamudConfiguration>.Get());
if (this.CheckValidity != null)
{
this.IsValid = this.CheckValidity(this.Value) == null;
}
else
{
this.IsValid = true;
}
}
public override void Save() => this.save(this.Value, Service<DalamudConfiguration>.Get());
}

View file

@ -1,15 +1,13 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Numerics;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility;
using ImGuiNET;
namespace Dalamud.Interface.Internal.Windows.Settings.Widgets;
@ -22,7 +20,6 @@ internal sealed class SettingsEntry<T> : SettingsEntry
private readonly Action<T?>? change;
private object? valueBacking;
private object? fallbackValue;
public SettingsEntry(
string name,
@ -32,8 +29,7 @@ internal sealed class SettingsEntry<T> : SettingsEntry
Action<T?>? change = null,
Func<T?, string?>? warning = null,
Func<T?, string?>? validity = null,
Func<bool>? visibility = null,
object? fallbackValue = null)
Func<bool>? visibility = null)
{
this.load = load;
this.save = save;
@ -43,8 +39,6 @@ internal sealed class SettingsEntry<T> : SettingsEntry
this.CheckWarning = warning;
this.CheckValidity = validity;
this.CheckVisibility = visibility;
this.fallbackValue = fallbackValue;
}
public delegate T? LoadSettingDelegate(DalamudConfiguration config);
@ -118,34 +112,6 @@ internal sealed class SettingsEntry<T> : SettingsEntry
this.change?.Invoke(this.Value);
}
}
else if (type.IsEnum)
{
ImGuiHelpers.SafeTextWrapped(this.Name);
var idx = (Enum)(this.valueBacking ?? 0);
var values = Enum.GetValues(type);
var descriptions =
values.Cast<Enum>().ToDictionary(x => x, x => x.GetAttribute<SettingsAnnotationAttribute>() ?? new SettingsAnnotationAttribute(x.ToString(), string.Empty));
if (!descriptions.ContainsKey(idx))
{
idx = (Enum)this.fallbackValue ?? throw new Exception("No fallback value for enum");
this.valueBacking = idx;
}
if (ImGui.BeginCombo($"###{this.Id.ToString()}", descriptions[idx].FriendlyName))
{
foreach (Enum value in values)
{
if (ImGui.Selectable(descriptions[value].FriendlyName, idx.Equals(value)))
{
this.valueBacking = value;
}
}
ImGui.EndCombo();
}
}
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
{
@ -197,18 +163,3 @@ internal sealed class SettingsEntry<T> : SettingsEntry
public override void Save() => this.save(this.Value, Service<DalamudConfiguration>.Get());
}
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")]
[AttributeUsage(AttributeTargets.Field)]
internal class SettingsAnnotationAttribute : Attribute
{
public SettingsAnnotationAttribute(string friendlyName, string description)
{
this.FriendlyName = friendlyName;
this.Description = description;
}
public string FriendlyName { get; set; }
public string Description { get; set; }
}