diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index d7cfc33c9..2ed930a93 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -153,6 +153,19 @@ namespace Dalamud Service.Set(); Log.Information("[T2] NH OK!"); + try + { + Service.Set().Initialize(this.AssetDirectory.FullName); + } + catch (Exception e) + { + Log.Error(e, "Could not initialize DataManager."); + this.Unload(); + return; + } + + Log.Information("[T2] Data OK!"); + var clientState = Service.Set(); Log.Information("[T2] CS OK!"); @@ -192,19 +205,6 @@ namespace Dalamud Log.Information(e, "Could not init IME."); } - try - { - Service.Set().Initialize(this.AssetDirectory.FullName); - } - catch (Exception e) - { - Log.Error(e, "Could not initialize DataManager."); - this.Unload(); - return; - } - - Log.Information("[T2] Data OK!"); - #pragma warning disable CS0618 // Type or member is obsolete Service.Set(); #pragma warning restore CS0618 // Type or member is obsolete diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index 3e50ffc38..52b63ed9e 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -125,6 +125,7 @@ namespace Dalamud.Game.ClientState /// public void Enable() { + Service.Get().Enable(); Service.Get().Enable(); this.setupTerritoryTypeHook.Enable(); } @@ -135,6 +136,7 @@ namespace Dalamud.Game.ClientState public void Dispose() { this.setupTerritoryTypeHook.Dispose(); + Service.Get().Dispose(); Service.Get().Dispose(); Service.Get().Update -= this.FrameworkOnOnUpdateEvent; Service.Get().CfPop -= this.NetworkHandlersOnCfPop; diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs index 334bf0198..11b77e2df 100644 --- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs +++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs @@ -47,6 +47,11 @@ namespace Dalamud.Game.ClientState /// public IntPtr KeyboardState { get; private set; } + /// + /// Gets the address of the keyboard state index array which translates the VK enumeration to the key state. + /// + public IntPtr KeyboardStateIndexArray { get; private set; } + /// /// Gets the address of the target manager. /// @@ -93,9 +98,11 @@ namespace Dalamud.Game.ClientState this.SetupTerritoryType = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B F9 66 89 91 ?? ?? ?? ??"); - // This resolves to a fixed offset only, without the base address added in, - // so GetStaticAddressFromSig() can't be used. lea rcx, ds:1DB9F74h[rax*4] + // These resolve to fixed offsets only, without the base address added in, so GetStaticAddressFromSig() can't be used. + // lea rcx, ds:1DB9F74h[rax*4] KeyboardState + // movzx edx, byte ptr [rbx+rsi+1D5E0E0h] KeyboardStateIndexArray this.KeyboardState = sig.ScanText("48 8D 0C 85 ?? ?? ?? ?? 8B 04 31 85 C2 0F 85") + 0x4; + this.KeyboardStateIndexArray = sig.ScanText("0F B6 94 33 ?? ?? ?? ?? 84 D2") + 0x4; this.ConditionFlags = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? BA ?? ?? ?? ?? E8 ?? ?? ?? ?? B0 01 48 83 C4 30"); diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index 4d679a180..597f4725b 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -2,6 +2,7 @@ using System; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Serilog; namespace Dalamud.Game.ClientState.Conditions { @@ -10,45 +11,61 @@ namespace Dalamud.Game.ClientState.Conditions /// [PluginInterface] [InterfaceVersion("1.0")] - public class Condition + public sealed partial class Condition { /// /// The current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has. /// public const int MaxConditionEntries = 100; + private readonly bool[] cache = new bool[MaxConditionEntries]; + /// /// Initializes a new instance of the class. /// /// The ClientStateAddressResolver instance. internal Condition(ClientStateAddressResolver resolver) { - this.ConditionArrayBase = resolver.ConditionFlags; + this.Address = resolver.ConditionFlags; } /// - /// Gets the condition array base pointer. - /// Would typically be private but is used in /xldata windows. + /// A delegate type used with the event. /// - internal IntPtr ConditionArrayBase { get; private set; } + /// The changed condition. + /// The value the condition is set to. + public delegate void ConditionChangeDelegate(ConditionFlag flag, bool value); + + /// + /// Event that gets fired when a condition is set. + /// Should only get fired for actual changes, so the previous value will always be !value. + /// + public event ConditionChangeDelegate? ConditionChange; + + /// + /// Gets the condition array base pointer. + /// + public IntPtr Address { get; private set; } /// /// Check the value of a specific condition/state flag. /// /// The condition flag to check. - public unsafe bool this[ConditionFlag flag] + public unsafe bool this[int flag] { get { - var idx = (int)flag; - - if (idx < 0 || idx >= MaxConditionEntries) + if (flag < 0 || flag >= MaxConditionEntries) return false; - return *(bool*)(this.ConditionArrayBase + idx); + return *(bool*)(this.Address + flag); } } + /// + public unsafe bool this[ConditionFlag flag] + => this[(int)flag]; + /// /// Check if any condition flags are set. /// @@ -57,8 +74,7 @@ namespace Dalamud.Game.ClientState.Conditions { for (var i = 0; i < MaxConditionEntries; i++) { - var typedCondition = (ConditionFlag)i; - var cond = this[typedCondition]; + var cond = this[i]; if (cond) return true; @@ -66,5 +82,77 @@ namespace Dalamud.Game.ClientState.Conditions return false; } + + /// + /// Enables the hooks of the Condition class function. + /// + public void Enable() + { + // Initialization + for (var i = 0; i < MaxConditionEntries; i++) + this.cache[i] = this[i]; + + Service.Get().Update += this.FrameworkUpdate; + } + + private void FrameworkUpdate(Framework framework) + { + for (var i = 0; i < MaxConditionEntries; i++) + { + var value = this[i]; + + if (value != this.cache[i]) + { + this.cache[i] = value; + + try + { + this.ConditionChange?.Invoke((ConditionFlag)i, value); + } + catch (Exception ex) + { + Log.Error(ex, $"While invoking {nameof(this.ConditionChange)}, an exception was thrown."); + } + } + } + } + } + + /// + /// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc. + /// + public sealed partial class Condition : IDisposable + { + private bool isDisposed; + + /// + /// Finalizes an instance of the class. + /// + ~Condition() + { + this.Dispose(false); + } + + /// + /// Disposes this instance, alongside its hooks. + /// + public void Dispose() + { + GC.SuppressFinalize(this); + this.Dispose(true); + } + + private void Dispose(bool disposing) + { + if (this.isDisposed) + return; + + if (disposing) + { + Service.Get().Update -= this.FrameworkUpdate; + } + + this.isDisposed = true; + } } } diff --git a/Dalamud/Game/ClientState/Keys/KeyState.cs b/Dalamud/Game/ClientState/Keys/KeyState.cs index 974f4b01f..0c8956006 100644 --- a/Dalamud/Game/ClientState/Keys/KeyState.cs +++ b/Dalamud/Game/ClientState/Keys/KeyState.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Runtime.InteropServices; using Dalamud.IoC; @@ -10,6 +11,15 @@ namespace Dalamud.Game.ClientState.Keys /// /// Wrapper around the game keystate buffer, which contains the pressed state for all keyboard keys, indexed by virtual vkCode. /// + /// + /// The stored key state is actually a combination field, however the below ephemeral states are consumed each frame. Setting + /// the value may be mildly useful, however retrieving the value is largely pointless. In testing, it wasn't possible without + /// setting the statue manually. + /// index & 0 = key pressed. + /// index & 1 = key down (ephemeral). + /// index & 2 = key up (ephemeral). + /// index & 3 = short key press (ephemeral). + /// [PluginInterface] [InterfaceVersion("1.0")] public class KeyState @@ -18,7 +28,9 @@ namespace Dalamud.Game.ClientState.Keys // but there is other state data past this point, and keys beyond here aren't // generally valid for most things anyway private const int MaxKeyCodeIndex = 0xA0; - private IntPtr bufferBase; + private readonly IntPtr bufferBase; + private readonly IntPtr indexBase; + private VirtualKey[] validVirtualKeyCache = null; /// /// Initializes a new instance of the class. @@ -29,40 +41,81 @@ namespace Dalamud.Game.ClientState.Keys var moduleBaseAddress = Service.Get().Module.BaseAddress; this.bufferBase = moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardState); + this.indexBase = moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardStateIndexArray); Log.Verbose($"Keyboard state buffer address 0x{this.bufferBase.ToInt64():X}"); } /// - /// Get or set the keypressed state for a given vkCode. + /// Get or set the key-pressed state for a given vkCode. /// /// The virtual key to change. /// Whether the specified key is currently pressed. - public bool this[int vkCode] + /// If the vkCode is not valid. Refer to or . + /// If the set value is non-zero. + public unsafe bool this[int vkCode] { - get - { - if (vkCode < 0 || vkCode > MaxKeyCodeIndex) - throw new ArgumentException($"Keycode state only appears to be valid up to {MaxKeyCodeIndex}"); + get => this.GetRawValue(vkCode) != 0; + set => this.SetRawValue(vkCode, value ? 1 : 0); + } - return Marshal.ReadInt32(this.bufferBase + (4 * vkCode)) != 0; - } - - set - { - if (vkCode < 0 || vkCode > MaxKeyCodeIndex) - throw new ArgumentException($"Keycode state only appears to be valid up to {MaxKeyCodeIndex}"); - - Marshal.WriteInt32(this.bufferBase + (4 * vkCode), value ? 1 : 0); - } + /// + public bool this[VirtualKey vkCode] + { + get => this[(int)vkCode]; + set => this[(int)vkCode] = value; } /// - /// Get or set the keypressed state for a given VirtualKey enum. + /// Gets the value in the index array. /// - /// The virtual key to change. - /// Whether the specified key is currently pressed. - public bool this[VirtualKey vk] => this[(int)vk]; + /// The virtual key to change. + /// The raw value stored in the index array. + /// If the vkCode is not valid. Refer to or . + public int GetRawValue(int vkCode) + => this.GetRefValue(vkCode); + + /// + public int GetRawValue(VirtualKey vkCode) + => this.GetRawValue((int)vkCode); + + /// + /// Sets the value in the index array. + /// + /// The virtual key to change. + /// The raw value to set in the index array. + /// If the vkCode is not valid. Refer to or . + /// If the set value is non-zero. + public void SetRawValue(int vkCode, int value) + { + if (value != 0) + throw new ArgumentOutOfRangeException(nameof(value), "Dalamud does not support pressing keys, only preventing them via zero or False. If you have a valid use-case for this, please contact the dev team."); + + this.GetRefValue(vkCode) = value; + } + + /// + public void SetRawValue(VirtualKey vkCode, int value) + => this.SetRawValue((int)vkCode, value); + + /// + /// Gets a value indicating whether the given VirtualKey code is regarded as valid input by the game. + /// + /// Virtual key code. + /// If the code is valid. + public bool IsVirtualKeyValid(int vkCode) + => vkCode > 0 && vkCode < MaxKeyCodeIndex && this.ConvertVirtualKey(vkCode) != 0; + + /// + public bool IsVirtualKeyValid(VirtualKey vkCode) + => this.IsVirtualKeyValid((int)vkCode); + + /// + /// Gets an array of virtual keys the game considers valid input. + /// + /// An array of valid virtual keys. + public VirtualKey[] GetValidVirtualKeys() + => this.validVirtualKeyCache ??= Enum.GetValues().Where(vk => this.IsVirtualKeyValid(vk)).ToArray(); /// /// Clears the pressed state for all keys. @@ -71,8 +124,40 @@ namespace Dalamud.Game.ClientState.Keys { for (var i = 0; i < MaxKeyCodeIndex; i++) { - Marshal.WriteInt32(this.bufferBase + (i * 4), 0); + this.GetRefValue(i) = 0; } } + + /// + /// Converts a virtual key into the equivalent value that the game uses. + /// Valid values are non-zero. + /// + /// Virtual key. + /// Converted value. + private unsafe byte ConvertVirtualKey(int vkCode) + { + if (vkCode <= 0 || vkCode >= 240) + return 0; + + return *(byte*)(this.indexBase + vkCode); + } + + /// + /// Gets the raw value from the key state array. + /// + /// Virtual key code. + /// A reference to the indexed array. + private unsafe ref int GetRefValue(int vkCode) + { + if (vkCode < 0 || vkCode > MaxKeyCodeIndex) + throw new ArgumentException($"Keycode state is only valid up to {MaxKeyCodeIndex}"); + + vkCode = this.ConvertVirtualKey(vkCode); + + if (vkCode == 0) + throw new ArgumentException($"Keycode state is only valid for certain values. Reference GetValidVirtualKeys for help."); + + return ref *(int*)(this.bufferBase + (4 * vkCode)); + } } } diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index c801f85d4..2ba584ae1 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -25,6 +25,7 @@ namespace Dalamud.Game public sealed class Framework : IDisposable { private static Stopwatch statsStopwatch = new(); + private Stopwatch updateStopwatch = new(); private Hook updateHook; private Hook destroyHook; @@ -92,6 +93,21 @@ namespace Dalamud.Game /// public FrameworkAddressResolver Address { get; } + /// + /// Gets the last time that the Framework Update event was triggered. + /// + public DateTime LastUpdate { get; private set; } = DateTime.MinValue; + + /// + /// Gets the last time in UTC that the Framework Update event was triggered. + /// + public DateTime LastUpdateUTC { get; private set; } = DateTime.MinValue; + + /// + /// Gets the delta between the last Framework Update and the currently executing one. + /// + public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero; + /// /// Gets or sets a value indicating whether to dispatch update events. /// @@ -127,6 +143,9 @@ namespace Dalamud.Game this.updateHook?.Dispose(); this.destroyHook?.Dispose(); this.realDestroyHook?.Dispose(); + + this.updateStopwatch.Reset(); + statsStopwatch.Reset(); } private void HookVTable() @@ -173,6 +192,13 @@ namespace Dalamud.Game if (this.DispatchUpdateEvents) { + this.updateStopwatch.Stop(); + this.UpdateDelta = TimeSpan.FromMilliseconds(this.updateStopwatch.ElapsedMilliseconds); + this.updateStopwatch.Restart(); + + this.LastUpdate = DateTime.Now; + this.LastUpdateUTC = DateTime.UtcNow; + try { if (StatsEnabled && this.Update != null) @@ -180,16 +206,23 @@ namespace Dalamud.Game // Stat Tracking for Framework Updates var invokeList = this.Update.GetInvocationList(); var notUpdated = StatsHistory.Keys.ToList(); + // Individually invoke OnUpdate handlers and time them. foreach (var d in invokeList) { statsStopwatch.Restart(); d.Method.Invoke(d.Target, new object[] { this }); statsStopwatch.Stop(); + var key = $"{d.Target}::{d.Method.Name}"; - if (notUpdated.Contains(key)) notUpdated.Remove(key); - if (!StatsHistory.ContainsKey(key)) StatsHistory.Add(key, new List()); + if (notUpdated.Contains(key)) + notUpdated.Remove(key); + + if (!StatsHistory.ContainsKey(key)) + StatsHistory.Add(key, new List()); + StatsHistory[key].Add(statsStopwatch.Elapsed.TotalMilliseconds); + if (StatsHistory[key].Count > 1000) { StatsHistory[key].RemoveRange(0, StatsHistory[key].Count - 1000); diff --git a/Dalamud/Interface/Colors/ImGuiColors.cs b/Dalamud/Interface/Colors/ImGuiColors.cs index b540c288a..0e6ac69f5 100644 --- a/Dalamud/Interface/Colors/ImGuiColors.cs +++ b/Dalamud/Interface/Colors/ImGuiColors.cs @@ -15,17 +15,17 @@ namespace Dalamud.Interface.Colors /// /// Gets grey used in dalamud. /// - public static Vector4 DalamudGrey { get; } = new Vector4(0.70f, 0.70f, 0.70f, 1.00f); + public static Vector4 DalamudGrey { get; } = new Vector4(0.7f, 0.7f, 0.7f, 1f); /// /// Gets grey used in dalamud. /// - public static Vector4 DalamudGrey2 { get; } = new Vector4(0.7f, 0.7f, 0.7f, 1.0f); + public static Vector4 DalamudGrey2 { get; } = new Vector4(0.7f, 0.7f, 0.7f, 1f); /// /// Gets grey used in dalamud. /// - public static Vector4 DalamudGrey3 { get; } = new Vector4(0.5f, 0.5f, 0.5f, 1.0f); + public static Vector4 DalamudGrey3 { get; } = new Vector4(0.5f, 0.5f, 0.5f, 1f); /// /// Gets white used in dalamud. @@ -45,16 +45,16 @@ namespace Dalamud.Interface.Colors /// /// Gets tank blue (UIColor37). /// - public static Vector4 TankBlue { get; } = new Vector4(0, 0.6f, 1, 1); + public static Vector4 TankBlue { get; } = new Vector4(0f, 0.6f, 1f, 1f); /// /// Gets healer green (UIColor504). /// - public static Vector4 HealerGreen { get; } = new Vector4(0, 0.8f, 0.1333333f, 1); + public static Vector4 HealerGreen { get; } = new Vector4(0f, 0.8f, 0.1333333f, 1f); /// /// Gets dps red (UIColor545). /// - public static Vector4 DPSRed { get; } = new Vector4(0.7058824f, 0, 0, 1); + public static Vector4 DPSRed { get; } = new Vector4(0.7058824f, 0f, 0f, 1f); } } diff --git a/Dalamud/Interface/Internal/Windows/DataWindow.cs b/Dalamud/Interface/Internal/Windows/DataWindow.cs index a831511b1..9963d55f2 100644 --- a/Dalamud/Interface/Internal/Windows/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/DataWindow.cs @@ -13,6 +13,7 @@ using Dalamud.Game.ClientState.GamePad; using Dalamud.Game.ClientState.JobGauge; using Dalamud.Game.ClientState.JobGauge.Enums; using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; @@ -22,6 +23,7 @@ using Dalamud.Game.Gui; using Dalamud.Game.Gui.FlyText; using Dalamud.Game.Gui.Toast; using Dalamud.Game.Text; +using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; using Dalamud.Memory; @@ -131,6 +133,7 @@ namespace Dalamud.Interface.Internal.Windows FlyText, ImGui, Tex, + KeyState, Gamepad, } @@ -288,6 +291,10 @@ namespace Dalamud.Interface.Internal.Windows this.DrawTex(); break; + case DataKind.KeyState: + this.DrawKeyState(); + break; + case DataKind.Gamepad: this.DrawGamepad(); break; @@ -674,7 +681,7 @@ namespace Dalamud.Interface.Internal.Windows var condition = Service.Get(); #if DEBUG - ImGui.Text($"ptr: 0x{condition.ConditionArrayBase.ToInt64():X}"); + ImGui.Text($"ptr: 0x{condition.Address.ToInt64():X}"); #endif ImGui.Text("Current Conditions:"); @@ -1150,6 +1157,32 @@ namespace Dalamud.Interface.Internal.Windows } } + private void DrawKeyState() + { + var keyState = Service.Get(); + + ImGui.Columns(4); + + var i = 0; + foreach (var vkCode in keyState.GetValidVirtualKeys()) + { + var code = (int)vkCode; + var value = keyState[code]; + + ImGui.PushStyleColor(ImGuiCol.Text, value ? ImGuiColors.HealerGreen : ImGuiColors.DPSRed); + + ImGui.Text($"{vkCode} ({code})"); + + ImGui.PopStyleColor(); + + i++; + if (i % 24 == 0) + ImGui.NextColumn(); + } + + ImGui.Columns(1); + } + private void DrawGamepad() { var gamepadState = Service.Get(); diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 40894ea25..9de7ae45b 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -99,7 +99,7 @@ namespace Dalamud.Plugin /// /// Gets the timespan delta from when this plugin was loaded. /// - public TimeSpan DeltaLoadTime => DateTime.Now - this.LoadTime; + public TimeSpan LoadTimeDelta => DateTime.Now - this.LoadTime; /// /// Gets the directory Dalamud assets are stored in. diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index dbc0bcfb0..24f26cbb9 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit dbc0bcfb01e32a1b53986ecf9e56f594731b1ea5 +Subproject commit 24f26cbb924e8941165e8b33817fcd2e2cb014f2