diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7990fe7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.vs/ +obj/ +bin/ +*.user \ No newline at end of file diff --git a/CustomResolution.sln b/CustomResolution.sln new file mode 100644 index 0000000..f15276d --- /dev/null +++ b/CustomResolution.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32505.426 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomResolution", "CustomResolution\CustomResolution.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5ECEDCE5-D60F-4A8A-AB33-4131F5C7371C}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF} + EndGlobalSection +EndGlobal diff --git a/CustomResolution/CustomResolution.csproj b/CustomResolution/CustomResolution.csproj index fcd9e70..f3b05e7 100644 --- a/CustomResolution/CustomResolution.csproj +++ b/CustomResolution/CustomResolution.csproj @@ -26,9 +26,7 @@ - - all - + $(DalamudLibPath)FFXIVClientStructs.dll false diff --git a/CustomResolution/FastReflectionHelper.cs b/CustomResolution/FastReflectionHelper.cs new file mode 100644 index 0000000..1b0f2ba --- /dev/null +++ b/CustomResolution/FastReflectionHelper.cs @@ -0,0 +1,133 @@ +using System; +using System.Reflection; +using System.Reflection.Emit; + +namespace MonoMod.Utils; + +public delegate object? FastReflectionDelegate(object? target, params object?[] args); + +public static class FastReflectionHelper +{ + private static readonly Type[] _dynamicMethodDelegateArgs = { typeof(object), typeof(object?[]) }; + + public static FastReflectionDelegate CreateFastDelegate(MethodBase method, bool directBoxValueAccess = true) + { + var dm = new DynamicMethod($"FastReflection<{method}>", typeof(object), _dynamicMethodDelegateArgs); + var il = dm.GetILGenerator(); + + var args = method.GetParameters(); + + var generateLocalBoxValuePtr = true; + + if (!method.IsStatic) + { + il.Emit(OpCodes.Ldarg_0); + + if (method.DeclaringType?.IsValueType ?? false) + { + il.Emit(OpCodes.Unbox_Any, method.DeclaringType); + } + } + + for (var i = 0; i < args.Length; i++) + { + var argType = args[i].ParameterType; + var argIsByRef = argType.IsByRef; + + if (argIsByRef) + { + argType = argType.GetElementType()!; + } + + var argIsValueType = argType.IsValueType; + + if (argIsByRef && argIsValueType && !directBoxValueAccess) + { + // Used later when storing back the reference to the new box in the array. + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldc_I4, i); + } + + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldc_I4, i); + + if (argIsByRef && !argIsValueType) + { + il.Emit(OpCodes.Ldelema, typeof(object)); + } + else + { + il.Emit(OpCodes.Ldelem_Ref); + + if (argIsValueType) + { + if (!argIsByRef || !directBoxValueAccess) + { + // if !directBoxValueAccess, create a new box if required + il.Emit(OpCodes.Unbox_Any, argType); + + if (argIsByRef) + { + // box back + il.Emit(OpCodes.Box, argType); + + // store new box value address to local 0 + il.Emit(OpCodes.Dup); + il.Emit(OpCodes.Unbox, argType); + + if (generateLocalBoxValuePtr) + { + generateLocalBoxValuePtr = false; + il.DeclareLocal(typeof(void*)); + } + + il.Emit(OpCodes.Stloc_0); + + // arr and index set up already + il.Emit(OpCodes.Stelem_Ref); + + // load address back to stack + il.Emit(OpCodes.Ldloc_0); + } + } + else + { + // if directBoxValueAccess, emit unbox (get value address) + il.Emit(OpCodes.Unbox, argType); + } + } + } + } + + if (method.IsConstructor) + { + il.Emit(OpCodes.Newobj, (ConstructorInfo) method); + } + else if (method.IsFinal || !method.IsVirtual) + { + il.Emit(OpCodes.Call, (MethodInfo) method); + } + else + { + il.Emit(OpCodes.Callvirt, (MethodInfo) method); + } + + var returnType = method.IsConstructor ? method.DeclaringType! : ((MethodInfo) method).ReturnType; + + if (returnType != typeof(void)) + { + if (returnType.IsValueType) + { + il.Emit(OpCodes.Box, returnType); + } + } + else + { + il.Emit(OpCodes.Ldnull); + } + + il.Emit(OpCodes.Ret); + + return (FastReflectionDelegate) dm.CreateDelegate(typeof(FastReflectionDelegate)); + } +} diff --git a/CustomResolution/Hooks/CursorPosHooks.cs b/CustomResolution/Hooks/CursorPosHooks.cs new file mode 100644 index 0000000..58a829a --- /dev/null +++ b/CustomResolution/Hooks/CursorPosHooks.cs @@ -0,0 +1,58 @@ +using CustomResolution.WndProcHookManagerProxyApi; +using Dalamud.Hooking; +using System; +using System.Linq; +using System.Reflection.Emit; +using System.Runtime.InteropServices; +using TerraFX.Interop.Windows; + + +namespace CustomResolution.Hooks; + +// THIS. IS. UGLY. +public sealed unsafe class CursorPosHooks : IDisposable +{ + private readonly Hook _getCursorPosHook; + private readonly Hook _setCursorPosHook; + + public CursorPosHooks() + { + _getCursorPosHook = Service.GameInteropProvider.HookFromSymbol("user32.dll", "GetCursorPos", GetCursorPosDetour); + _getCursorPosHook.Enable(); + + _setCursorPosHook = Service.GameInteropProvider.HookFromSymbol("user32.dll", "SetCursorPos", SetCursorPosDetour); + _setCursorPosHook.Enable(); + } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private unsafe delegate bool GetCursorPosDelegate(POINT* lpPoint); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private unsafe delegate bool SetCursorPosDelegate(int x, int y); + + public void Dispose() + { + _getCursorPosHook.Dispose(); + } + + private bool GetCursorPosDetour(POINT* lpPoint) + { + var rv = _getCursorPosHook.Original(lpPoint); + + if (!rv) + { + return false; + } + + Service.Plugin.ConvertCoordsWinToGame(ref lpPoint->x, ref lpPoint->y); + + return rv; + } + + private bool SetCursorPosDetour(int x, int y) + { + Service.Plugin.ConvertCoordsGameToWin(ref x, ref y); + + return _setCursorPosHook.Original(x, y); + } +} diff --git a/CustomResolution/Hooks/WndProcHook.cs b/CustomResolution/Hooks/WndProcHook.cs new file mode 100644 index 0000000..c26094c --- /dev/null +++ b/CustomResolution/Hooks/WndProcHook.cs @@ -0,0 +1,132 @@ +using CustomResolution.WndProcHookManagerProxyApi; +using System; +using System.Linq; +using System.Reflection.Emit; +using TerraFX.Interop.Windows; + + +namespace CustomResolution.Hooks; + +// THIS. IS. UGLY. +public sealed class WndProcHook : IDisposable +{ + private const uint WM_MOUSEFIRST = 0x0200; + private const uint WM_MOUSELAST = 0x0209; + + private static WndProcHook? _instance; + + private readonly WndProcHookManager _manager; + + private bool _applied = false; + private DynamicMethod? _genMethod; + private Delegate? _genDelegate; + + public WndProcHook() + { + _instance = this; + + _manager = new(); + + Apply(); + } + + public void Dispose() + { + if (!_applied) + { + return; + } + + if (_manager.PreWndProc is not { } preWndProcProp) + { + Service.PluginLog.Information("CustomResolution couldn't obtain the PreWndProc event."); + + return; + } + + preWndProcProp.RemoveEventHandler(_manager.ProxiedValue, _genDelegate); + + _applied = false; + } + + public static void InvokeStatic(object args) + { + if (_instance is { } instance) + { + instance.Invoke(new WndProcEventArgs(instance._manager, args)); + } + } + + private static void ParamToCoords(LPARAM param, out int x, out int y) + { + x = (int)(param.Value & 0xFFFF); + y = (int)(param.Value >> 16 & 0xFFFF); + } + + private static LPARAM CoordsToParam(int x, int y) + { + nint value = 0; + value |= x & 0xFFFF; + value |= y >> 16 & 0xFFFF; + + return value; + } + + private void Invoke(WndProcEventArgs args) + { + if (!(WM_MOUSEFIRST <= args.Message && args.Message <= WM_MOUSELAST)) + { + return; + } + + ParamToCoords(args.LParam, out int x, out int y); + + Service.Plugin.ConvertCoordsWinToGame(ref x, ref y); + + args.LParam = CoordsToParam(x, y); + } + + private void Apply() + { + if (!_manager.Refresh()) + { + return; + } + + if (_applied) + { + Dispose(); + } + + if (_manager.PreWndProc is not { } preWndProcProp) + { + Service.PluginLog.Information("CustomResolution couldn't obtain the PreWndProc event."); + + return; + } + + if (_genDelegate is null) + { + var delegateType = preWndProcProp.EventHandlerType!; + var delegateInvoke = delegateType.GetMethod("Invoke")!; + + _genMethod = new DynamicMethod( + "CustomResolution_PreWndProc", + delegateInvoke.ReturnType, + delegateInvoke.GetParameters().Select(p => p.ParameterType).ToArray() + ); + + var il = _genMethod.GetILGenerator(); + + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, typeof(WndProcHook).GetMethod("InvokeStatic")!); + il.Emit(OpCodes.Ret); + + _genDelegate = _genMethod.CreateDelegate(delegateType); + } + + preWndProcProp.AddEventHandler(_manager.ProxiedValue, _genDelegate); + + _applied = true; + } +} diff --git a/CustomResolution/NativeMethods.txt b/CustomResolution/NativeMethods.txt deleted file mode 100644 index 2f68cf2..0000000 --- a/CustomResolution/NativeMethods.txt +++ /dev/null @@ -1 +0,0 @@ -GetClientRect diff --git a/CustomResolution/Plugin.cs b/CustomResolution/Plugin.cs index 3e28ed7..4432fa9 100644 --- a/CustomResolution/Plugin.cs +++ b/CustomResolution/Plugin.cs @@ -2,11 +2,12 @@ using Dalamud.Plugin; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; +using FFXIVClientStructs.FFXIV.Client.UI; using System; using System.Collections.Generic; using System.Linq; -using Windows.Win32; -using Windows.Win32.Foundation; +using TerraFX.Interop.Windows; +using static FFXIVClientStructs.FFXIV.Client.UI.Misc.GroupPoseModule; namespace CustomResolution; @@ -27,6 +28,9 @@ public sealed unsafe class Plugin : IDalamudPlugin Service.PluginUI = new(); + Service.WndProcHook = new(); + Service.CursorPosHooks = new(); + _cmds = typeof(Plugin).Assembly.GetTypes() .Where(t => !t.IsAbstract && typeof(Cmd).IsAssignableFrom(t)) .Select(t => (Cmd) Activator.CreateInstance(t)!) @@ -61,26 +65,66 @@ public sealed unsafe class Plugin : IDalamudPlugin cmd.Dispose(); } + Service.CursorPosHooks.Dispose(); + Service.WndProcHook.Dispose(); + Service.PluginUI.Dispose(); Service.Plugin = null!; } + public void ConvertCoordsWinToGame(ref int x, ref int y) + { + if (CurrentWidth == CurrentWindowWidth && CurrentHeight == CurrentWindowHeight) + { + return; + } + + float scaleX = CurrentWidth / (float) CurrentWindowWidth; + float scaleY = CurrentHeight / (float) CurrentWindowHeight; + + x = (int) Math.Round(x * scaleX); + y = (int) Math.Round(y * scaleY); + } + + public void ConvertCoordsGameToWin(ref int x, ref int y) + { + if (CurrentWidth == CurrentWindowWidth && CurrentHeight == CurrentWindowHeight) + { + return; + } + + float scaleX = CurrentWindowWidth / (float) CurrentWidth; + float scaleY = CurrentWindowHeight / (float) CurrentHeight; + + x = (int) Math.Round(x * scaleX); + y = (int) Math.Round(y * scaleY); + } + public void Update() { var dev = Device.Instance(); - var width = dev->Width; - var height = dev->Height; + RECT rect = default; - PInvoke.GetClientRect((HWND) (IntPtr) dev->hWnd, out var rect); + TerraFX.Interop.Windows.Windows.GetClientRect((HWND) (IntPtr) dev->hWnd, &rect); + + int rectWidth = rect.right - rect.left; + int rectHeight = rect.bottom - rect.top; + + if ((rectWidth <= 0 || rectHeight <= 0) && !_unloading) + { + return; + } + + uint width, height; if (Service.Config.IsScale || _unloading) { var scale = _unloading ? 1f : Service.Config.Scale; - width = (uint) Math.Round(rect.Width * scale); - height = (uint) Math.Round(rect.Height * scale); + width = (uint) Math.Round(rectWidth * scale); + height = (uint) Math.Round(rectHeight * scale); } else { @@ -90,6 +134,8 @@ public sealed unsafe class Plugin : IDalamudPlugin if (width != dev->Width || height != dev->Height) { + Service.PluginLog.Info($"Changing resolution to {width} x {height}"); + if (width < 256) { width = 256; @@ -107,8 +153,8 @@ public sealed unsafe class Plugin : IDalamudPlugin CurrentWidth = width; CurrentHeight = height; - CurrentWindowWidth = (uint) rect.Width; - CurrentWindowHeight = (uint) rect.Height; + CurrentWindowWidth = (uint) rectWidth; + CurrentWindowHeight = (uint) rectHeight; } private void OnFrameworkUpdate(IFramework framework) diff --git a/CustomResolution/PluginUI.cs b/CustomResolution/PluginUI.cs index b05ec61..c00a3cd 100644 --- a/CustomResolution/PluginUI.cs +++ b/CustomResolution/PluginUI.cs @@ -41,6 +41,6 @@ public sealed class PluginUI : IDisposable private void ShowConfigWindow() { - + SettingsVisible = true; } } diff --git a/CustomResolution/Service.cs b/CustomResolution/Service.cs index 4424099..e676e86 100644 --- a/CustomResolution/Service.cs +++ b/CustomResolution/Service.cs @@ -1,6 +1,6 @@ -using Dalamud.IoC; +using CustomResolution.Hooks; +using Dalamud.IoC; using Dalamud.Plugin; -using System; using Dalamud.Plugin.Services; namespace CustomResolution; @@ -13,6 +13,9 @@ public sealed class Service public static PluginUI PluginUI { get; internal set; } = null!; + public static WndProcHook WndProcHook { get; internal set; } = null!; + public static CursorPosHooks CursorPosHooks { get; internal set; } = null!; + [PluginService] public static DalamudPluginInterface PluginInterface { get; private set; } = null!; @@ -26,7 +29,8 @@ public sealed class Service public static IClientState ClientState { get; private set; } = null!; [PluginService] - public static IPluginLog PluginLog { get; private set; } = null!; + public static IGameInteropProvider GameInteropProvider { get; private set; } = null!; - public static Func? GetPluginInstance { get; private set; } + [PluginService] + public static IPluginLog PluginLog { get; private set; } = null!; } diff --git a/CustomResolution/WndProcHookManagerProxyApi/IWndProcHookManagerProxy.cs b/CustomResolution/WndProcHookManagerProxyApi/IWndProcHookManagerProxy.cs new file mode 100644 index 0000000..731bf42 --- /dev/null +++ b/CustomResolution/WndProcHookManagerProxyApi/IWndProcHookManagerProxy.cs @@ -0,0 +1,8 @@ +namespace CustomResolution.WndProcHookManagerProxyApi; + +public interface IWndProcHookManagerProxy +{ + WndProcHookManager WndProcHookManager { get; } + object ProxiedValue { get; } + bool IsValid => WndProcHookManager is not null && ProxiedValue is not null; +} diff --git a/CustomResolution/WndProcHookManagerProxyApi/WndProcEventArgs.cs b/CustomResolution/WndProcHookManagerProxyApi/WndProcEventArgs.cs new file mode 100644 index 0000000..6cef981 --- /dev/null +++ b/CustomResolution/WndProcHookManagerProxyApi/WndProcEventArgs.cs @@ -0,0 +1,35 @@ +using TerraFX.Interop.Windows; + +namespace CustomResolution.WndProcHookManagerProxyApi; + +// This could be optimized in a thousand different ways (better dynamic methods, source-gen this class), but eh. + +public readonly record struct WndProcEventArgs(WndProcHookManager WndProcHookManager, object ProxiedValue) : IWndProcHookManagerProxy +{ + public HWND Hwnd => (HWND) WndProcHookManager.GetValue(ProxiedValue, nameof(Hwnd))!; + public int ViewportId => (int) WndProcHookManager.GetValue(ProxiedValue, nameof(ViewportId))!; + + public uint Message + { + get => (uint) WndProcHookManager.GetValue(ProxiedValue, nameof(Message))!; + set => WndProcHookManager.SetValue(ProxiedValue, nameof(Message), value); + } + + public WPARAM WParam + { + get => (WPARAM) WndProcHookManager.GetValue(ProxiedValue, nameof(WParam))!; + set => WndProcHookManager.SetValue(ProxiedValue, nameof(WParam), value); + } + + public LPARAM LParam + { + get => (LPARAM) WndProcHookManager.GetValue(ProxiedValue, nameof(LParam))!; + set => WndProcHookManager.SetValue(ProxiedValue, nameof(LParam), value); + } + + public bool SuppressCall + { + get => (bool) WndProcHookManager.GetValue(ProxiedValue, nameof(SuppressCall))!; + set => WndProcHookManager.SetValue(ProxiedValue, nameof(SuppressCall), value); + } +} diff --git a/CustomResolution/WndProcHookManagerProxyApi/WndProcHookManager.cs b/CustomResolution/WndProcHookManagerProxyApi/WndProcHookManager.cs new file mode 100644 index 0000000..4c4dd16 --- /dev/null +++ b/CustomResolution/WndProcHookManagerProxyApi/WndProcHookManager.cs @@ -0,0 +1,289 @@ +using Dalamud.IoC; +using MonoMod.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace CustomResolution.WndProcHookManagerProxyApi; + +public class WndProcHookManager +{ + internal const BindingFlags _BindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static; + + private object? _realManager; + private Type? _realManagerType; + + private WeakReference? _lastManager; + private readonly Dictionary<(Type, string), FastReflectionDelegate> _methodCache = new(); + private readonly Dictionary<(Type, string), MemberInfo> _memberCache = new(); + + public WndProcHookManager() + { + Refresh(); + } + + public bool IsAvailable => _realManager is not null; + public object? ProxiedValue => _realManager; + + public EventInfo? PreWndProc { get; private set; } + + public bool Refresh() + { + if (IsAvailable) + { + return true; + } + + Service.PluginLog.Information("Refreshing WndProcHookManagerProxyApi"); + + try + { + if (typeof(PluginServiceAttribute).Assembly.GetType("Dalamud.Service`1") is not { } serviceContainerContainer) + { + Service.PluginLog.Warning("CustomResolution couldn't find the service container types."); + + Unset(); + + return false; + } + + if (typeof(PluginServiceAttribute).Assembly.GetType("Dalamud.Hooking.WndProcHook.WndProcHookManager") is not { } wndProcHookManagerType) + { + Service.PluginLog.Warning("CustomResolution couldn't find the WndProcHookManager type."); + + Unset(); + + return false; + } + + _realManagerType = wndProcHookManagerType; + + serviceContainerContainer = serviceContainerContainer.MakeGenericType(wndProcHookManagerType); + + if (serviceContainerContainer.GetMethod("Get")?.Invoke(null, Array.Empty()) is not object manager) + { + Service.PluginLog.Warning("CustomResolution couldn't obtain the WndProcHookManager instance."); + + Unset(); + + return false; + } + + _realManager = manager; + } + catch (Exception e) + { + Service.PluginLog.Warning($"CustomResolution couldn't obtain the WndProcHookManager service: {e}"); + + Unset(); + + return false; + } + + + Service.PluginLog.Information($"WndProcHookManager found: {_realManagerType.Assembly.FullName}"); + + if (_lastManager?.Target != _realManager) + { + Service.PluginLog.Information($"Different WndProcHookManager, clearing cache"); + + ClearCache(); + + _lastManager = new WeakReference(_realManager); + } + + PreWndProc = _realManagerType.GetEvent("PreWndProc"); + + return true; + } + + internal FastReflectionDelegate GetMethod(Type target, string name, Func? getMethod = null) + { + if (_methodCache.TryGetValue((target, name), out var fun)) + { + return fun; + } + + return _methodCache[(target, name)] = FastReflectionHelper.CreateFastDelegate(getMethod?.Invoke(target) ?? target.GetMethods().FirstOrDefault(m => m.Name == name) ?? target.GetMethod(name, _BindingFlags)!); + } + + internal object? InvokeStatic(string typeName, string name, params object?[] args) + { + if (_realManagerType?.Assembly.GetType(typeName) is not { } type) + { + Service.PluginLog.Information($"GetStaticValue failed, type \"{typeName}\" not found"); + + return null; + } + + return GetMethod(type, name).Invoke(null, args); + } + + internal object? Invoke(object proxiedValue, string name, params object?[] args) + { + return GetMethod(proxiedValue.GetType(), name).Invoke(proxiedValue, args); + } + + internal object? GetStaticValue(string typeName, string name, params object?[] args) + { + if (_realManagerType?.Assembly.GetType(typeName) is not { } type) + { + Service.PluginLog.Information($"GetStaticValue failed, type \"{typeName}\" not found"); + + return null; + } + + if (!_memberCache.TryGetValue((type, name), out var member)) + { + member = _memberCache[(type, name)] = type.GetMember(name, _BindingFlags).First(); + } + + if (member is FieldInfo field) + { + return field.GetValue(null); + } + + if (member is PropertyInfo property) + { + return property.GetValue(null, args); + } + + return InvokeStatic(typeName, name, args); + } + + internal object? GetValue(object proxiedValue, string name, params object?[] args) + { + if (!_memberCache.TryGetValue((proxiedValue.GetType(), name), out var member)) + { + member = _memberCache[(proxiedValue.GetType(), name)] = proxiedValue.GetType().GetMember(name, _BindingFlags).First(); + } + + if (member is FieldInfo field) + { + return field.GetValue(proxiedValue); + } + + if (member is PropertyInfo property) + { + return property.GetValue(proxiedValue, args); + } + + return Invoke(proxiedValue, name, args); + } + + internal void SetStaticValue(string typeName, string name, params object?[] args) + { + if (_realManagerType?.Assembly.GetType(typeName) is not { } type) + { + Service.PluginLog.Information($"GetStaticValue failed, type \"{typeName}\" not found"); + + return; + } + + if (!_memberCache.TryGetValue((type, name), out var member)) + { + member = _memberCache[(type, name)] = type.GetMember(name, _BindingFlags).First(); + } + + if (member is FieldInfo field) + { + field.SetValue(null, args[0]); + + return; + } + + if (member is PropertyInfo property) + { + property.SetValue(null, args.First(), args.Length > 1 ? args.Skip(1).ToArray() : null); + + return; + } + + InvokeStatic(typeName, name, args); + } + + internal void SetValue(object proxiedValue, string name, params object?[] args) + { + if (!_memberCache.TryGetValue((proxiedValue.GetType(), name), out var member)) + { + member = _memberCache[(proxiedValue.GetType(), name)] = proxiedValue.GetType().GetMember(name, _BindingFlags).First(); + } + + if (member is FieldInfo field) + { + field.SetValue(proxiedValue, args[0]); + + return; + } + + if (member is PropertyInfo property) + { + property.SetValue(proxiedValue, args.First(), args.Length > 1 ? args.Skip(1).ToArray() : null); + + return; + } + + Invoke(proxiedValue, name, args); + } + + internal ITuple ConvertValueTuple(Type targetType, object proxiedValue) + { + var rawTuple = (ITuple) proxiedValue; + var dstItems = new object?[rawTuple.Length]; + var rawTypes = proxiedValue.GetType().GenericTypeArguments; + var dstTypes = targetType.GenericTypeArguments; + + for (var i = 0; i < rawTuple.Length; i++) + { + var raw = rawTuple[i]; + + if (dstTypes[i].IsAssignableFrom(rawTypes[i])) + { + dstItems[i] = raw; + } + else if (raw is not null) + { + var dstType = dstTypes[i]; + + var isNullable = dstType.FullName?.StartsWith("System.Nullable`1[[") ?? false; + + if (isNullable) + { + dstType = dstType.GenericTypeArguments[0]; + } + + dstItems[i] = Activator.CreateInstance(dstType, new object[] { this, raw }); + + if (isNullable) + { + dstItems[i] = Activator.CreateInstance(dstTypes[i], new object[] { dstItems[i]! }); + } + } + else + { + dstItems[i] = null; + } + } + + return (ITuple) Activator.CreateInstance(targetType, dstItems)!; + } + + private void Unset() + { + _realManager = default; + _realManagerType = default; + + PreWndProc = default; + + ClearCache(); + } + + private void ClearCache() + { + _lastManager = null; + _methodCache.Clear(); + _memberCache.Clear(); + } +} diff --git a/CustomResolution/packages.lock.json b/CustomResolution/packages.lock.json index 88459a5..75e5c9b 100644 --- a/CustomResolution/packages.lock.json +++ b/CustomResolution/packages.lock.json @@ -8,46 +8,11 @@ "resolved": "2.1.12", "contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg==" }, - "Microsoft.Windows.CsWin32": { + "TerraFX.Interop.Windows": { "type": "Direct", - "requested": "[0.3.49-beta, )", - "resolved": "0.3.49-beta", - "contentHash": "/iAXplVDKESFyLz/MShuVYVx62YFvFePpq3qrzREl8KScQqG6Z2kV7r9UJTjQS1g+hfy3V+e4m1x9lsu24YNSg==", - "dependencies": { - "Microsoft.Windows.SDK.Win32Docs": "0.1.42-alpha", - "Microsoft.Windows.SDK.Win32Metadata": "55.0.45-preview", - "Microsoft.Windows.WDK.Win32Metadata": "0.9.9-experimental", - "System.Memory": "4.5.5", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "Microsoft.Windows.SDK.Win32Docs": { - "type": "Transitive", - "resolved": "0.1.42-alpha", - "contentHash": "Z/9po23gUA9aoukirh2ItMU2ZS9++Js9Gdds9fu5yuMojDrmArvY2y+tq9985tR3cxFxpZO1O35Wjfo0khj5HA==" - }, - "Microsoft.Windows.SDK.Win32Metadata": { - "type": "Transitive", - "resolved": "55.0.45-preview", - "contentHash": "2i1kVIk8rMuMCYtoeYeWbmCRLqNgb63Berac3Gv+J/pjp0oucopasKgLQNslryhsgBl4xex9ENhJX2MXEW5vdA==" - }, - "Microsoft.Windows.WDK.Win32Metadata": { - "type": "Transitive", - "resolved": "0.9.9-experimental", - "contentHash": "cncPlxLqR25mFOuvpxZPgXgJqRmXyV5eTULj/9w8AQvO/c1xJDEapohRsWkYaia7hCR/EwEzBHr7RU9u93T03w==", - "dependencies": { - "Microsoft.Windows.SDK.Win32Metadata": "55.0.45-preview" - } - }, - "System.Memory": { - "type": "Transitive", - "resolved": "4.5.5", - "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + "requested": "[10.0.22621.2, )", + "resolved": "10.0.22621.2", + "contentHash": "lORoYCoURS33Vi7PDvubRugLF2+l5v3rX2oVEqNhpBLjs3aZpqapRvTHPKVwsC+dGrsGuqJ/3yXuxVUb0vl3vg==" } } } diff --git a/README.md b/README.md new file mode 100644 index 0000000..8230a8e --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# CustomResolution +### Gluing PatMe and Ffxiv2Mqtt together +Basic plugin that pokes Ffxiv2Mqtt every time PatMe detects a pat. + +![preview of MQTTExplorer showing the ffxiv/patme/cid/emote topics](./preview.png) diff --git a/release.bat b/release.bat new file mode 100644 index 0000000..a38f64a --- /dev/null +++ b/release.bat @@ -0,0 +1,10 @@ +@set src=.\CustomResolution\bin\x64\Release\CustomResolution +@set dst=O:\home\ade\wwwext\xiv\CustomResolution + +dotnet build -c Release + +rmdir /s /q %dst% +xcopy %src%\ %dst%\ /E +xcopy .\CustomResolution\images\icon.png %dst%\ + +pause \ No newline at end of file