diff --git a/CHANGELOG.md b/CHANGELOG.md index aa52099..751dd6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this library will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.10.2] - 2023-06-29 + +### Removed + +- Lots of old code and things that were not being used + +### Fixed + +- Auto-save and load reliability +- Some UI bugs on auto-load +- Lots of performance and under the hood optimizations + ## [4.10.1] - 2023-06-27 ### Added diff --git a/SleepHunter/Common/UpdatableObject.cs b/SleepHunter/Common/UpdatableObject.cs new file mode 100644 index 0000000..a12ba54 --- /dev/null +++ b/SleepHunter/Common/UpdatableObject.cs @@ -0,0 +1,70 @@ +using System; + +namespace SleepHunter.Common +{ + public abstract class UpdatableObject : ObservableObject, IDisposable + { + protected bool isDisposed; + + public event EventHandler Updated; + + public void Update() + { + CheckIfDisposed(); + + OnUpdate(); + RaiseUpdated(); + } + + public bool TryUpdate() + { + CheckIfDisposed(); + + try + { + Update(); + return true; + } + catch + { + return false; + } + } + + + ~UpdatableObject() => Dispose(false); + + public void RaiseUpdated() + { + CheckIfDisposed(); + Updated?.Invoke(this, EventArgs.Empty); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool isDisposing) + { + if (isDisposed) + return; + + if (isDisposing) + { + + } + + isDisposed = true; + } + + protected abstract void OnUpdate(); + + protected void CheckIfDisposed() + { + if (isDisposed) + throw new ObjectDisposedException(GetType().Name); + } + } +} diff --git a/SleepHunter/Extensions/BinaryReaderExtender.cs b/SleepHunter/Extensions/BinaryReaderExtensions.cs similarity index 96% rename from SleepHunter/Extensions/BinaryReaderExtender.cs rename to SleepHunter/Extensions/BinaryReaderExtensions.cs index ee21176..634417c 100644 --- a/SleepHunter/Extensions/BinaryReaderExtender.cs +++ b/SleepHunter/Extensions/BinaryReaderExtensions.cs @@ -3,7 +3,7 @@ namespace SleepHunter.Extensions { - public static class BinaryReaderExtender + public static class BinaryReaderExtensions { public static string ReadFixedString(this BinaryReader reader, int length) { diff --git a/SleepHunter/Extensions/CharacterExtender.cs b/SleepHunter/Extensions/CharacterExtensions.cs similarity index 95% rename from SleepHunter/Extensions/CharacterExtender.cs rename to SleepHunter/Extensions/CharacterExtensions.cs index 4ad8b94..07ec9ef 100644 --- a/SleepHunter/Extensions/CharacterExtender.cs +++ b/SleepHunter/Extensions/CharacterExtensions.cs @@ -1,7 +1,7 @@  namespace SleepHunter.Extensions { - public static class CharacterExtender + public static class CharacterExtensions { public static bool IsValidHexDigit(this char c, bool allowControl = true) { diff --git a/SleepHunter/Extensions/ControlExtender.cs b/SleepHunter/Extensions/ControlExtensions.cs similarity index 95% rename from SleepHunter/Extensions/ControlExtender.cs rename to SleepHunter/Extensions/ControlExtensions.cs index d6ba16e..5eb319f 100644 --- a/SleepHunter/Extensions/ControlExtender.cs +++ b/SleepHunter/Extensions/ControlExtensions.cs @@ -3,7 +3,7 @@ namespace SleepHunter.Extensions { - public static class ControlExtender + public static class ControlExtensions { public static T FindItem(this ItemsControl control, Func selector) where T : class { diff --git a/SleepHunter/Extensions/DispatcherExtensions.cs b/SleepHunter/Extensions/DispatcherExtensions.cs new file mode 100644 index 0000000..c0b2080 --- /dev/null +++ b/SleepHunter/Extensions/DispatcherExtensions.cs @@ -0,0 +1,13 @@ +using System.Windows.Threading; +using SleepHunter.Threading; + +namespace SleepHunter.Extensions +{ + public static class DispatcherExtensions + { + public static UIThreadAwaitable SwitchToUIThread(this Dispatcher dispatcher) + { + return new UIThreadAwaitable(dispatcher); + } + } +} diff --git a/SleepHunter/Extensions/StringExtender.cs b/SleepHunter/Extensions/StringExtensions.cs similarity index 92% rename from SleepHunter/Extensions/StringExtender.cs rename to SleepHunter/Extensions/StringExtensions.cs index 80e18e3..dc5c4ce 100644 --- a/SleepHunter/Extensions/StringExtender.cs +++ b/SleepHunter/Extensions/StringExtensions.cs @@ -2,7 +2,7 @@ namespace SleepHunter.Extensions { - public static class StringExtender + public static class StringExtensions { public static string StripNumbers(this string text) { diff --git a/SleepHunter/Extensions/TimeSpanExtender.cs b/SleepHunter/Extensions/TimeSpanExtensions.cs similarity index 95% rename from SleepHunter/Extensions/TimeSpanExtender.cs rename to SleepHunter/Extensions/TimeSpanExtensions.cs index 8c5772e..c0e95b9 100644 --- a/SleepHunter/Extensions/TimeSpanExtender.cs +++ b/SleepHunter/Extensions/TimeSpanExtensions.cs @@ -3,12 +3,12 @@ namespace SleepHunter.Extensions { - public static class TimeSpanExtender + public static class TimeSpanExtensions { - static readonly Regex TimeSpanSecondsRegex = new(@"\s*(?-?[0-9]*\.?[0-9]{1,})\s*s\s*"); - static readonly Regex TimeSpanMinutesRegex = new(@"\s*(?-?[0-9]*\.?[0-9]{1,})\s*m\s*"); - static readonly Regex TimeSpanHoursRegex = new(@"\s*(?-?[0-9]*\.?[0-9]{1,})\s*h\s*"); - static readonly Regex TimeSpanDaysRegex = new(@"\s*(?-?[0-9]*\.?[0-9]{1,})\s*d\s*"); + static readonly Regex TimeSpanSecondsRegex = new(@"\s*(?-?[0-9]*\.?[0-9]{1,})\s*s\s*", RegexOptions.Compiled); + static readonly Regex TimeSpanMinutesRegex = new(@"\s*(?-?[0-9]*\.?[0-9]{1,})\s*m\s*", RegexOptions.Compiled); + static readonly Regex TimeSpanHoursRegex = new(@"\s*(?-?[0-9]*\.?[0-9]{1,})\s*h\s*", RegexOptions.Compiled); + static readonly Regex TimeSpanDaysRegex = new(@"\s*(?-?[0-9]*\.?[0-9]{1,})\s*d\s*", RegexOptions.Compiled); public static string ToFractionalEnglish(this TimeSpan timeSpan, bool useShortNotation = false, string format = null) => ToEnglish(timeSpan, useShortNotation, false, true, format); diff --git a/SleepHunter/Extensions/VersionExtender.cs b/SleepHunter/Extensions/VersionExtensions.cs similarity index 88% rename from SleepHunter/Extensions/VersionExtender.cs rename to SleepHunter/Extensions/VersionExtensions.cs index 92ff40a..d31da20 100644 --- a/SleepHunter/Extensions/VersionExtender.cs +++ b/SleepHunter/Extensions/VersionExtensions.cs @@ -2,7 +2,7 @@ namespace SleepHunter.Extensions { - public static class VersionExtender + public static class VersionExtensions { public static bool IsNewerThan(this Version current, Version otherVersion) => current.CompareTo(otherVersion) > 0; diff --git a/SleepHunter/Extensions/WindowExtender.cs b/SleepHunter/Extensions/WindowExtender.cs deleted file mode 100644 index bdd0e98..0000000 --- a/SleepHunter/Extensions/WindowExtender.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Threading; -using System.Windows; -using System.Windows.Threading; - -using SleepHunter.Views; - -namespace SleepHunter.Extensions -{ - public static class WindowExtender - { - public static T InvokeIfRequired(this Dispatcher dispatcher, Func action, T value, DispatcherPriority priority = DispatcherPriority.Normal) - { - if (dispatcher is null) - throw new ArgumentNullException(nameof(dispatcher)); - - if (action is null) - throw new ArgumentNullException(nameof(action)); - - if (dispatcher.Thread != Thread.CurrentThread) - return (T)dispatcher.Invoke(action, priority, null); - else - return action(); - } - - public static void InvokeIfRequired(this Dispatcher dispatcher, Action action, DispatcherPriority priority = DispatcherPriority.Normal) - { - if (dispatcher is null) - throw new ArgumentNullException(nameof(dispatcher)); - - if (action is null) - throw new ArgumentNullException(nameof(action)); - - if (dispatcher.Thread != Thread.CurrentThread) - dispatcher.Invoke(action, priority, null); - else - action(); - } - - public static void InvokeIfRequired(this Dispatcher dispatcher, Action action, T value, DispatcherPriority priority = DispatcherPriority.Normal) - { - if (dispatcher is null) - throw new ArgumentNullException(nameof(dispatcher)); - - if (action is null) - throw new ArgumentNullException(nameof(action)); - - if (dispatcher.Thread != Thread.CurrentThread) - dispatcher.Invoke(action, priority, value); - else - action(value); - } - - public static bool? ShowMessageBox(this Window owner, string windowTitle, string messageText, string subText = null, MessageBoxButton buttons = MessageBoxButton.OK, int width = 420, int height = 280) - { - if (owner == null) - throw new ArgumentNullException(nameof(owner)); - - var messageBox = new MessageBoxWindow - { - Title = windowTitle ?? string.Empty, - Width = width, - Height = height, - MessageText = messageText ?? string.Empty, - SubText = subText ?? string.Empty - }; - - if (buttons == MessageBoxButton.OK) - { - messageBox.CancelButtonColumnWidth = new GridLength(1, GridUnitType.Auto); - messageBox.CancelButtonVisibility = Visibility.Collapsed; - } - - if (buttons.HasFlag(MessageBoxButton.YesNo)) - { - messageBox.OkButtonText = "_Yes"; - messageBox.CancelButtonText = "_No"; - } - - if (!owner.IsLoaded) - owner.Show(); - - messageBox.Owner = owner; - - return messageBox.ShowDialog(); - } - } -} diff --git a/SleepHunter/Extensions/WindowExtensions.cs b/SleepHunter/Extensions/WindowExtensions.cs new file mode 100644 index 0000000..ed5a418 --- /dev/null +++ b/SleepHunter/Extensions/WindowExtensions.cs @@ -0,0 +1,43 @@ +using System; +using SleepHunter.Views; +using System.Windows; + +namespace SleepHunter.Extensions +{ + public static class WindowExtensions + { + public static bool? ShowMessageBox(this Window owner, string windowTitle, string messageText, string subText = null, MessageBoxButton buttons = MessageBoxButton.OK, int width = 420, int height = 280) + { + if (owner == null) + throw new ArgumentNullException(nameof(owner)); + + var messageBox = new MessageBoxWindow + { + Title = windowTitle ?? string.Empty, + Width = width, + Height = height, + MessageText = messageText ?? string.Empty, + SubText = subText ?? string.Empty + }; + + if (buttons == MessageBoxButton.OK) + { + messageBox.CancelButtonColumnWidth = new GridLength(1, GridUnitType.Auto); + messageBox.CancelButtonVisibility = Visibility.Collapsed; + } + + if (buttons.HasFlag(MessageBoxButton.YesNo)) + { + messageBox.OkButtonText = "_Yes"; + messageBox.CancelButtonText = "_No"; + } + + if (!owner.IsLoaded) + owner.Show(); + + messageBox.Owner = owner; + + return messageBox.ShowDialog(); + } + } +} diff --git a/SleepHunter/IO/Process/MemoryVariableExtender.cs b/SleepHunter/IO/Process/MemoryVariableExtender.cs index 52ceb08..b8e0319 100644 --- a/SleepHunter/IO/Process/MemoryVariableExtender.cs +++ b/SleepHunter/IO/Process/MemoryVariableExtender.cs @@ -20,7 +20,7 @@ public static long DereferencePointer(long address, BinaryReader reader) return reference; } - public static bool TryDeferenceValue(this MemoryVariable variable, BinaryReader reader, out long address, bool isStringType = false) + public static bool TryDereferenceValue(this MemoryVariable variable, BinaryReader reader, out long address, bool isStringType = false) { address = DereferenceValue(variable, reader, isStringType); return address != 0; diff --git a/SleepHunter/IO/Process/ProcessManager.cs b/SleepHunter/IO/Process/ProcessManager.cs index 7296553..b6bf5ab 100644 --- a/SleepHunter/IO/Process/ProcessManager.cs +++ b/SleepHunter/IO/Process/ProcessManager.cs @@ -5,6 +5,7 @@ using System.Text; using SleepHunter.Win32; +using SleepHunter.Models; namespace SleepHunter.IO.Process { diff --git a/SleepHunter/Macro/MacroState.cs b/SleepHunter/Macro/MacroState.cs index 2049265..13d6527 100644 --- a/SleepHunter/Macro/MacroState.cs +++ b/SleepHunter/Macro/MacroState.cs @@ -129,9 +129,6 @@ protected virtual void StartMacro(object state = null) try { UpdateClientMacroStatus(); - - if (!CheckMap()) - cancelSource.Cancel(); if (cancelSource.Token.IsCancellationRequested) break; @@ -143,13 +140,13 @@ protected virtual void StartMacro(object state = null) } else { - Thread.Sleep(100); + Thread.Sleep(16); } } finally { if (!cancelSource.IsCancellationRequested) - Thread.Sleep(50); + Thread.Sleep(16); if (cancelSource.IsCancellationRequested) Stop(); @@ -183,15 +180,6 @@ protected void UpdateClientMacroStatus() protected abstract void MacroLoop(object argument); - protected virtual bool CheckMap() - { - if (client == null) - return false; - - client.Update(PlayerFieldFlags.Location); - return true; - } - protected virtual bool CancelTask(bool waitForTask = false) { if (cancelSource != null) @@ -259,8 +247,6 @@ protected virtual void TakeAction(MacroAction action) protected virtual void SaveKnownState() { - client.Update(PlayerFieldFlags.Location); - lastKnownMapName = client.Location.MapName; lastKnownMapNumber = client.Location.MapNumber; lastKnownXCoordinate = client.Location.X; @@ -269,8 +255,6 @@ protected virtual void SaveKnownState() protected virtual void CheckKnownState(bool saveStateAfterCheck = true) { - client.Update(PlayerFieldFlags.Location); - if (!string.Equals(client.Location.MapName, lastKnownMapName) || client.Location.MapNumber != lastKnownMapNumber) { diff --git a/SleepHunter/Macro/PlayerInterfaceExtender.cs b/SleepHunter/Macro/PlayerInterfaceExtender.cs index c832d45..efa6a7b 100644 --- a/SleepHunter/Macro/PlayerInterfaceExtender.cs +++ b/SleepHunter/Macro/PlayerInterfaceExtender.cs @@ -11,8 +11,6 @@ public static class PlayerInterfaceExtender { public static void Disarm(this Player client) { - client.Update(PlayerFieldFlags.Equipment); - if (!client.Equipment.IsEmpty(EquipmentSlot.Weapon | EquipmentSlot.Shield)) WindowAutomator.SendKeystroke(client.Process.WindowHandle, '`'); } @@ -35,7 +33,6 @@ public static bool UseItemAndWait(this Player client, string itemName, TimeSpan itemName = itemName.Trim(); - client.Update(PlayerFieldFlags.Inventory); var slot = client.Inventory.FindItemSlot(itemName); if (slot < 0) @@ -104,8 +101,6 @@ public static void ExpandInventory(this Player client) var pt = new Point(570, 320); pt = pt.ScalePoint(client.Process.WindowScaleX, client.Process.WindowScaleY); - client.Update(PlayerFieldFlags.GameClient); - if (!client.GameClient.IsInventoryExpanded) WindowAutomator.SendMouseClick(client.Process.WindowHandle, MouseButton.Left, (int)pt.X, (int)pt.Y); } @@ -115,8 +110,6 @@ public static void CollapseInventory(this Player client) var pt = new Point(570, 320); pt = pt.ScalePoint(client.Process.WindowScaleX, client.Process.WindowScaleY); - client.Update(PlayerFieldFlags.GameClient); - if (client.GameClient.IsInventoryExpanded) WindowAutomator.SendMouseClick(client.Process.WindowHandle, MouseButton.Left, (int)pt.X, (int)pt.Y); } @@ -139,8 +132,6 @@ public static bool WaitForInventory(this Player client, bool isExpanded, TimeSpa while (true) { - client.Update(PlayerFieldFlags.GameClient); - if (client.GameClient.IsInventoryExpanded == isExpanded) return true; @@ -149,7 +140,7 @@ public static bool WaitForInventory(this Player client, bool isExpanded, TimeSpa if (timeout != TimeSpan.Zero && deltaTime >= timeout) break; - Thread.Sleep(1); + Thread.Sleep(16); } return false; @@ -161,8 +152,6 @@ public static bool WaitForEquipment(this Player client, string itemName, Equipme while (true) { - client.Update(PlayerFieldFlags.Equipment); - if (client.Equipment.IsEquipped(itemName, slot)) return true; @@ -171,7 +160,7 @@ public static bool WaitForEquipment(this Player client, string itemName, Equipme if (timeout != TimeSpan.Zero && deltaTime >= timeout) break; - Thread.Sleep(1); + Thread.Sleep(16); } return false; @@ -183,8 +172,6 @@ public static bool WaitForEquipmentEmpty(this Player client, EquipmentSlot slot, while (true) { - client.Update(PlayerFieldFlags.Equipment); - if (client.Equipment.IsEmpty(slot)) return true; @@ -193,7 +180,7 @@ public static bool WaitForEquipmentEmpty(this Player client, EquipmentSlot slot, if (timeout == TimeSpan.Zero && deltaTime >= timeout) break; - Thread.Sleep(1); + Thread.Sleep(16); } return false; @@ -210,7 +197,6 @@ public static void SwitchToPanel(this Player client, InterfacePanel panel, out b var hwnd = client.Process.WindowHandle; - client.Update(PlayerFieldFlags.GameClient); var currentPanel = client.GameClient.ActivePanel; if (currentPanel.IsSameAs(panel)) @@ -273,8 +259,6 @@ public static bool WaitForPanel(this Player client, InterfacePanel panel, TimeSp while (true) { - client.Update(PlayerFieldFlags.GameClient); - var activePanel = client.GameClient.ActivePanel; if (activePanel.IsSameAs(panel)) @@ -285,7 +269,7 @@ public static bool WaitForPanel(this Player client, InterfacePanel panel, TimeSp if (timeout != TimeSpan.Zero && delaTime >= timeout) break; - Thread.Sleep(1); + Thread.Sleep(16); } return false; diff --git a/SleepHunter/Macro/PlayerMacroState.cs b/SleepHunter/Macro/PlayerMacroState.cs index 463464b..0bc5fd7 100644 --- a/SleepHunter/Macro/PlayerMacroState.cs +++ b/SleepHunter/Macro/PlayerMacroState.cs @@ -391,8 +391,6 @@ public void CancelCasting() protected override void MacroLoop(object argument) { - client.Update(PlayerFieldFlags.GameClient); - // Tick the dispatcher so any scheduled events go off deferredDispatcher.Tick(); @@ -406,10 +404,7 @@ protected override void MacroLoop(object argument) InterfacePanel currentPanel = InterfacePanel.Stats; if (preserveUserPanel) - { - client.Update(PlayerFieldFlags.GameClient); currentPanel = client.GameClient.ActivePanel; - } if (UserSettingsManager.Instance.Settings.FlowerBeforeSpellMacros) { @@ -479,8 +474,6 @@ private bool DoClickWaterAndBedsIfNeeded() if (!LocalStorage.GetBoolOrDefault(LocalStorageKey.UseWaterAndBeds.IsEnabled, false)) return false; - client.Update(PlayerFieldFlags.Stats); - var manaThreshold = LocalStorage.GetIntegerOrDefault(LocalStorageKey.UseWaterAndBeds.ManaThreshold, 1000); if (client.Stats.CurrentMana >= manaThreshold) @@ -517,7 +510,6 @@ private bool DoSkillMacro(out bool didAssail) var skillList = new List(100); var useShiftKey = UserSettingsManager.Instance.Settings.UseShiftForMedeniaPane; - client.Update(PlayerFieldFlags.Skillbook); foreach (var skillName in client.Skillbook.ActiveSkills) { var skill = client.Skillbook.GetSkill(skillName); @@ -531,17 +523,10 @@ private bool DoSkillMacro(out bool didAssail) skillList.Add(skill); } - // Update stats for current HP if any skill might need it for evaluating min/max thresholds - if (skillList.Any(skill => skill.MinHealthPercent.HasValue || skill.MaxHealthPercent.HasValue)) - client.Update(PlayerFieldFlags.Stats); - foreach (var skill in skillList.OrderBy((s) => { return s.OpensDialog; })) { - client.Update(PlayerFieldFlags.GameClient); - if (skill.RequiresDisarm || (skill.IsAssail && UserSettingsManager.Instance.Settings.DisarmForAssails)) { - client.Update(PlayerFieldFlags.Equipment); var isDisarmed = client.Equipment.IsEmpty(EquipmentSlot.Weapon | EquipmentSlot.Shield); if (!isDisarmed) @@ -628,7 +613,6 @@ private bool DoFlowerMacro() if (IsSpellCasting) return false; - client.Update(PlayerFieldFlags.Spellbook); var prioritizeAlts = UserSettingsManager.Instance.Settings.FlowerAltsFirst; if (!client.HasLyliacPlant && !client.HasLyliacVineyard) @@ -785,9 +769,6 @@ private bool FlowerNextAltWaitingForMana() if (waitingAlt == null) return false; - client.Update(PlayerFieldFlags.Location); - waitingAlt.Update(PlayerFieldFlags.Location); - if (!client.Location.IsWithinRange(waitingAlt.Location)) return false; @@ -812,7 +793,6 @@ private bool FlowerNextAltWaitingForMana() private bool ShouldFasSpiorad(int? manaRequirement = null) { - client.Update(PlayerFieldFlags.Spellbook | PlayerFieldFlags.Stats); var autoFasSpiorad = UserSettingsManager.Instance.Settings.UseFasSpiorad; if (!client.HasFasSpiorad) @@ -894,8 +874,6 @@ private SpellQueueItem GetNextSpell() if (spellQueue.Count < 1) return null; - client.Update(PlayerFieldFlags.Spellbook); - var skipOnCooldown = UserSettingsManager.Instance.Settings.SkipSpellsOnCooldown; var currentSpell = SpellQueueRotation switch @@ -963,8 +941,6 @@ private FlowerQueueItem GetNextFlowerTarget() { var prioritizeAlts = UserSettingsManager.Instance.Settings.FlowerAltsFirst; - client.Update(PlayerFieldFlags.Spellbook); - if (!client.HasLyliacPlant) return null; @@ -993,13 +969,9 @@ private FlowerQueueItem GetNextFlowerTarget() if (altClient == null) continue; - client.Update(PlayerFieldFlags.Location); - altClient.Update(PlayerFieldFlags.Location); - if (!client.Location.IsWithinRange(altClient.Location)) continue; - altClient.Update(PlayerFieldFlags.Stats); isWithinManaThreshold = altTarget.ManaThreshold.HasValue && altClient.Stats.CurrentMana < altTarget.ManaThreshold.Value; if (altTarget.IsReady || isWithinManaThreshold) @@ -1024,10 +996,7 @@ private FlowerQueueItem GetNextFlowerTarget() { var altClient = PlayerManager.Instance.GetPlayerByName(currentTarget.Target.CharacterName); if (altClient != null) - { - altClient.Update(PlayerFieldFlags.Stats); isWithinManaThreshold = altClient.Stats.CurrentMana < currentTarget.ManaThreshold.Value; - } } if (currentTarget.Id == currentId) @@ -1047,7 +1016,6 @@ private bool CastSpell(SpellQueueItem item) var useShiftKey = UserSettingsManager.Instance.Settings.UseShiftForMedeniaPane; - client.Update(PlayerFieldFlags.Spellbook); var spell = client.Spellbook.GetSpell(item.Name); if (spell == null) @@ -1061,7 +1029,6 @@ private bool CastSpell(SpellQueueItem item) if (UserSettingsManager.Instance.Settings.RequireManaForSpells) { - client.Update(PlayerFieldFlags.Stats); if (spell.ManaCost > client.Stats.CurrentMana) { IsWaitingOnMana = true; @@ -1090,16 +1057,12 @@ private bool CastSpell(SpellQueueItem item) if (alt == null || !alt.IsLoggedIn) return false; - client.Update(PlayerFieldFlags.Location); - alt.Update(PlayerFieldFlags.Location); - if (!client.Location.IsWithinRange(alt.Location)) return false; } if (!modifiedNumberOfLines.HasValue) { - client.Update(PlayerFieldFlags.Equipment); var weapon = client.Equipment.GetSlot(EquipmentSlot.Weapon); if (weapon != null) @@ -1143,8 +1106,6 @@ private bool SwitchToBestStaff(SpellQueueItem item, out int? numberOfLines, out if (item == null) throw new ArgumentNullException(nameof(item)); - client.Update( PlayerFieldFlags.Inventory | PlayerFieldFlags.Equipment | PlayerFieldFlags.Stats | PlayerFieldFlags.Spellbook); - var equippedStaff = client.Equipment.GetSlot(EquipmentSlot.Weapon); var availableList = new List(client.Inventory.ItemNames); diff --git a/SleepHunter/IO/Process/ClientProcess.cs b/SleepHunter/Models/ClientProcess.cs similarity index 80% rename from SleepHunter/IO/Process/ClientProcess.cs rename to SleepHunter/Models/ClientProcess.cs index b8a4c1f..e03e565 100644 --- a/SleepHunter/IO/Process/ClientProcess.cs +++ b/SleepHunter/Models/ClientProcess.cs @@ -2,11 +2,12 @@ using System.Text; using SleepHunter.Common; +using SleepHunter.IO.Process; using SleepHunter.Win32; -namespace SleepHunter.IO.Process +namespace SleepHunter.Models { - public sealed class ClientProcess : ObservableObject + public sealed class ClientProcess : UpdatableObject { private const int ViewportWidth = 640; private const int ViewportHeight = 480; @@ -19,8 +20,6 @@ public sealed class ClientProcess : ObservableObject private int windowHeight = ViewportHeight; private DateTime creationTime; - public event EventHandler ProcessUpdated; - public int ProcessId { get => processId; @@ -69,7 +68,7 @@ public DateTime CreationTime public ClientProcess() { } - public void Update() + protected override void OnUpdate() { var windowTextLength = NativeMethods.GetWindowTextLength(windowHandle); var windowTextBuffer = new StringBuilder(windowTextLength + 1); @@ -83,7 +82,16 @@ public void Update() WindowHeight = clientRect.Height; } - ProcessUpdated?.Invoke(this, EventArgs.Empty); + if (CreationTime == DateTime.MinValue) + UpdateProcessTime(); + } + + private void UpdateProcessTime() + { + using var accessor = new ProcessMemoryAccessor(processId, ProcessAccess.Read); + + if (NativeMethods.GetProcessTimes(accessor.ProcessHandle, out var creationTime, out _, out _, out _)) + CreationTime = creationTime.FiletimeToDateTime(); } } } diff --git a/SleepHunter/IO/Process/ClientProcessEventArgs.cs b/SleepHunter/Models/ClientProcessEventArgs.cs similarity index 78% rename from SleepHunter/IO/Process/ClientProcessEventArgs.cs rename to SleepHunter/Models/ClientProcessEventArgs.cs index 126e41b..67caf63 100644 --- a/SleepHunter/IO/Process/ClientProcessEventArgs.cs +++ b/SleepHunter/Models/ClientProcessEventArgs.cs @@ -1,12 +1,12 @@ using System; -namespace SleepHunter.IO.Process +namespace SleepHunter.Models { public delegate void ClientProcessEventHandler(object sender, ClientProcessEventArgs e); public sealed class ClientProcessEventArgs : EventArgs { - public ClientProcess Process { get; } + public ClientProcess Process { get; init; } public ClientProcessEventArgs(ClientProcess process) { diff --git a/SleepHunter/Models/ClientState.cs b/SleepHunter/Models/ClientState.cs index 696c009..b69f345 100644 --- a/SleepHunter/Models/ClientState.cs +++ b/SleepHunter/Models/ClientState.cs @@ -7,7 +7,7 @@ namespace SleepHunter.Models { - public sealed class ClientState : ObservableObject + public sealed class ClientState : UpdatableObject { private const string ActivePanelKey = @"ActivePanel"; private const string InventoryExpandedKey = @"InventoryExpanded"; @@ -16,6 +16,9 @@ public sealed class ClientState : ObservableObject private const string SenseOpenKey = @"SenseOpen"; private const string UserChattingKey = @"UserChatting"; + private readonly Stream stream; + private readonly BinaryReader reader; + private string versionKey; private InterfacePanel activePanel; private bool isInventoryExpanded; @@ -24,9 +27,7 @@ public sealed class ClientState : ObservableObject private bool isSenseOpen; private bool isUserChatting; - public event EventHandler ClientUpdated; - - public Player Owner { get; } + public Player Owner { get; init; } public string VersionKey { @@ -73,19 +74,13 @@ public bool IsUserChatting public ClientState(Player owner) { Owner = owner ?? throw new ArgumentNullException(nameof(owner)); - } - public void Update() - { - Update(Owner.Accessor); - ClientUpdated?.Invoke(this, EventArgs.Empty); + stream = owner.Accessor.GetStream(); + reader = new BinaryReader(stream, Encoding.ASCII); } - public void Update(ProcessMemoryAccessor accessor) + protected override void OnUpdate() { - if (accessor == null) - throw new ArgumentNullException(nameof(accessor)); - var version = Owner.Version; if (version == null) @@ -101,9 +96,6 @@ public void Update(ProcessMemoryAccessor accessor) var senseOpenVariable = version.GetVariable(SenseOpenKey); var userChattingVariable = version.GetVariable(UserChattingKey); - using var stream = accessor.GetStream(); - using var reader = new BinaryReader(stream, Encoding.ASCII); - if (activePanelVariable != null && activePanelVariable.TryReadByte(reader, out var activePanelByte)) ActivePanel = (InterfacePanel)activePanelByte; else @@ -135,7 +127,21 @@ public void Update(ProcessMemoryAccessor accessor) IsUserChatting = false; } - public void ResetDefaults() + protected override void Dispose(bool isDisposing) + { + if (isDisposed) + return; + + if (isDisposing) + { + reader?.Dispose(); + stream?.Dispose(); + } + + base.Dispose(isDisposing); + } + + private void ResetDefaults() { ActivePanel = InterfacePanel.Unknown; IsInventoryExpanded = false; diff --git a/SleepHunter/Models/EquipmentSet.cs b/SleepHunter/Models/EquipmentSet.cs index aeb0ff4..5a7fedd 100644 --- a/SleepHunter/Models/EquipmentSet.cs +++ b/SleepHunter/Models/EquipmentSet.cs @@ -4,48 +4,44 @@ using System.IO; using System.Linq; using System.Text; - +using SleepHunter.Common; using SleepHunter.Extensions; using SleepHunter.IO.Process; namespace SleepHunter.Models { - public sealed class EquipmentSet : IEnumerable + public sealed class EquipmentSet : UpdatableObject, IEnumerable { private const string EquipmentKey = @"Equipment"; - public const int EquipmentCount = 18; - private readonly List equipment = new(EquipmentCount); - - public event EventHandler EquipmentUpdated; - - public Player Owner { get; } + private readonly Stream stream; + private readonly BinaryReader reader; + private readonly InventoryItem[] equipment = new InventoryItem[EquipmentCount]; - public int Count => equipment.Count((item) => { return !item.IsEmpty; }); + public Player Owner { get; init; } public IEnumerable SortedBySlot => equipment.OrderBy(item => item.Slot); public EquipmentSet(Player owner) { Owner = owner ?? throw new ArgumentNullException(nameof(owner)); - InitializeEquipment(); - } - private void InitializeEquipment() - { - for (int i = 0; i < EquipmentCount; i++) - { - var item = InventoryItem.MakeEmpty(i); - item.Slot = i + 1; + stream = owner.Accessor.GetStream(); + reader = new BinaryReader(stream, Encoding.ASCII); - equipment.Add(item); + for (var i = 0; i < equipment.Length; i++) + { + equipment[i] = InventoryItem.MakeEmpty(i); + equipment[i].Slot = i + 1; } } public bool IsEquipped(string itemName, EquipmentSlot slot) { + CheckIfDisposed(); + itemName = itemName.Trim(); var isEquipped = true; @@ -108,6 +104,8 @@ public bool IsEquipped(string itemName, EquipmentSlot slot) public bool IsEmpty(EquipmentSlot slot) { + CheckIfDisposed(); + var isEmpty = true; if (slot.HasFlag(EquipmentSlot.Accessory1)) @@ -167,6 +165,12 @@ public bool IsEmpty(EquipmentSlot slot) return isEmpty; } + public InventoryItem GetSlot(EquipmentSlot slot) + { + CheckIfDisposed(); + return equipment[(int)slot]; + } + private bool IsSlotEmpty(EquipmentSlot slot) { var item = GetSlot(slot); @@ -175,17 +179,8 @@ private bool IsSlotEmpty(EquipmentSlot slot) return isEmpty; } - public void Update() - { - Update(Owner.Accessor); - EquipmentUpdated?.Invoke(this, EventArgs.Empty); - } - - public void Update(ProcessMemoryAccessor accessor) + protected override void OnUpdate() { - if (accessor == null) - throw new ArgumentNullException(nameof(accessor)); - var version = Owner.Version; if (version == null) @@ -194,28 +189,23 @@ public void Update(ProcessMemoryAccessor accessor) return; } - var equipmentVariable = version.GetVariable(EquipmentKey); - - if (equipmentVariable == null) + if (!version.TryGetVariable(EquipmentKey, out var equipmentVariable)) { ResetDefaults(); return; } - using var stream = accessor.GetStream(); - using var reader = new BinaryReader(stream, Encoding.ASCII); - - var equipmentPointer = equipmentVariable.DereferenceValue(reader); - - if (equipmentPointer == 0) + if (!equipmentVariable.TryDereferenceValue(reader, out var basePointer)) { ResetDefaults(); return; } - reader.BaseStream.Position = equipmentPointer; + stream.Position = basePointer; + + var entryCount = Math.Min(equipment.Length, equipmentVariable.Count); - for (int i = 0; i < equipmentVariable.Count; i++) + for (var i = 0; i < entryCount; i++) { try { @@ -231,9 +221,23 @@ public void Update(ProcessMemoryAccessor accessor) } } - public void ResetDefaults() + protected override void Dispose(bool isDisposing) { - for (int i = 0; i < equipment.Capacity; i++) + if (isDisposed) + return; + + if (isDisposing) + { + reader?.Dispose(); + stream?.Dispose(); + } + + base.Dispose(isDisposing); + } + + private void ResetDefaults() + { + for (var i = 0; i < equipment.Length; i++) { equipment[i].IsEmpty = true; equipment[i].Name = null; @@ -241,8 +245,6 @@ public void ResetDefaults() } } - public InventoryItem GetSlot(EquipmentSlot slot) => equipment[(int)slot]; - public IEnumerator GetEnumerator() { foreach (var gear in equipment) diff --git a/SleepHunter/Models/Inventory.cs b/SleepHunter/Models/Inventory.cs index 8d27c61..cba80db 100644 --- a/SleepHunter/Models/Inventory.cs +++ b/SleepHunter/Models/Inventory.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.ComponentModel; using System.IO; using System.Linq; using System.Text; @@ -11,23 +10,19 @@ namespace SleepHunter.Models { - public sealed class Inventory : ObservableObject, IEnumerable + public sealed class Inventory : UpdatableObject, IEnumerable { private const string InventoryKey = @"Inventory"; private const string GoldKey = @"Gold"; public static readonly int InventoryCount = 60; - private readonly List inventory = new(InventoryCount); + private readonly Stream stream; + private readonly BinaryReader reader; + private readonly InventoryItem[] inventory = new InventoryItem[InventoryCount]; private int gold; - public event EventHandler InventoryUpdated; - public event EventHandler GoldChanged; - - public Player Owner { get; } - - - public int Count => inventory.Count((item) => { return !item.IsEmpty; }); + public Player Owner { get; init; } public IEnumerable ItemsAndGold => inventory; @@ -37,7 +32,6 @@ public int Gold set => SetProperty(ref gold, value, nameof(Gold), (_) => { UpdateGoldInventoryItem(); - GoldChanged?.Invoke(this, EventArgs.Empty); RaisePropertyChanged(nameof(ItemsAndGold)); }); } @@ -48,21 +42,20 @@ public int Gold public Inventory(Player owner) { Owner = owner ?? throw new ArgumentNullException(nameof(owner)); - InitializeInventory(); - } - private void InitializeInventory() - { - inventory.Clear(); + stream = owner.Accessor.GetStream(); + reader = new BinaryReader(stream, Encoding.ASCII); - for (int i = 0; i < inventory.Capacity; i++) - inventory.Add(InventoryItem.MakeEmpty(i + 1)); + for (var i = 0; i < inventory.Length; i++) + inventory[i] = InventoryItem.MakeEmpty(i + 1); UpdateGoldInventoryItem(); } public InventoryItem GetItem(string itemName) { + CheckIfDisposed(); + itemName = itemName.Trim(); foreach (var item in inventory) @@ -74,6 +67,8 @@ public InventoryItem GetItem(string itemName) public int FindItemSlot(string itemName) { + CheckIfDisposed(); + itemName = itemName.Trim(); foreach (var item in inventory) @@ -83,17 +78,8 @@ public int FindItemSlot(string itemName) return -1; } - public void Update() + protected override void OnUpdate() { - Update(Owner.Accessor); - InventoryUpdated?.Invoke(this, EventArgs.Empty); - } - - public void Update(ProcessMemoryAccessor accessor) - { - if (accessor == null) - throw new ArgumentNullException(nameof(accessor)); - var version = Owner.Version; if (version == null) @@ -102,35 +88,31 @@ public void Update(ProcessMemoryAccessor accessor) return; } - var inventoryVariable = version.GetVariable(InventoryKey); - - if (inventoryVariable == null) + if (!version.TryGetVariable(InventoryKey, out var inventoryVariable)) { ResetDefaults(); return; } - using var stream = accessor.GetStream(); - using var reader = new BinaryReader(stream, Encoding.ASCII); - var inventoryPointer = inventoryVariable.DereferenceValue(reader); - - if (inventoryPointer == 0) + if (!inventoryVariable.TryDereferenceValue(reader, out var basePointer)) { ResetDefaults(); return; } - reader.BaseStream.Position = inventoryPointer; - + stream.Position = basePointer; + + var entryCount = Math.Min(inventory.Length, inventoryVariable.Count); + // Gold is the last item, skip it - for (int i = 0; i < inventoryVariable.Count - 1; i++) + for (var i = 0; i < entryCount - 1; i++) { try { - bool hasItem = reader.ReadInt16() != 0; - ushort iconIndex = reader.ReadUInt16(); + var hasItem = reader.ReadInt16() != 0; + var iconIndex = reader.ReadUInt16(); reader.ReadByte(); - string name = reader.ReadFixedString(inventoryVariable.MaxLength); + var name = reader.ReadFixedString(inventoryVariable.MaxLength); reader.ReadByte(); inventory[i].IsEmpty = !hasItem; @@ -140,14 +122,11 @@ public void Update(ProcessMemoryAccessor accessor) catch { } } - UpdateGold(accessor); + UpdateGold(); } - private void UpdateGold(ProcessMemoryAccessor accessor) + private void UpdateGold() { - if (accessor == null) - throw new ArgumentNullException(nameof(accessor)); - var version = Owner.Version; if (version == null) @@ -162,9 +141,6 @@ private void UpdateGold(ProcessMemoryAccessor accessor) return; } - using var stream = accessor.GetStream(); - using var reader = new BinaryReader(stream, Encoding.ASCII); - if (goldVariable.TryReadUInt32(reader, out var goldValue)) Gold = (int)goldValue; else @@ -179,9 +155,23 @@ private void UpdateGoldInventoryItem() inventory[InventoryCount - 1].Name = $"Gold ({Gold:n0})"; } - public void ResetDefaults() + protected override void Dispose(bool isDisposing) + { + if (isDisposed) + return; + + if (isDisposing) + { + reader?.Dispose(); + stream?.Dispose(); + } + + base.Dispose(isDisposing); + } + + private void ResetDefaults() { - for (int i = 0; i < inventory.Capacity; i++) + for (var i = 0; i < inventory.Length; i++) { inventory[i].IsEmpty = true; inventory[i].Name = null; diff --git a/SleepHunter/Models/MapLocation.cs b/SleepHunter/Models/MapLocation.cs index 5a62c45..237da12 100644 --- a/SleepHunter/Models/MapLocation.cs +++ b/SleepHunter/Models/MapLocation.cs @@ -7,22 +7,23 @@ namespace SleepHunter.Models { - public sealed class MapLocation : ObservableObject + public sealed class MapLocation : UpdatableObject { private const string MapNumberKey = @"MapNumber"; private const string MapNameKey = @"MapName"; private const string MapXKey = @"MapX"; private const string MapYKey = @"MapY"; + private readonly Stream stream; + private readonly BinaryReader reader; + private int mapNumber; private int x; private int y; private string mapName; private string mapHash; - public event EventHandler LocationUpdated; - - public Player Owner { get; } + public Player Owner { get; init; } public int MapNumber { @@ -57,13 +58,21 @@ public string MapHash public MapLocation(Player owner) { Owner = owner ?? throw new ArgumentNullException(nameof(owner)); + + stream = owner.Accessor.GetStream(); + reader = new BinaryReader(stream, Encoding.ASCII); } - public bool IsSameMap(MapLocation other) - => MapNumber == other.MapNumber && string.Equals(MapName, other.MapName, StringComparison.Ordinal); + public bool IsSameMap(MapLocation other) + { + CheckIfDisposed(); + return MapNumber == other.MapNumber && string.Equals(MapName, other.MapName, StringComparison.Ordinal); + } public bool IsWithinRange(MapLocation other, int maxX = 10, int maxY = 10) { + CheckIfDisposed(); + if (!IsSameMap(other)) return false; @@ -73,17 +82,8 @@ public bool IsWithinRange(MapLocation other, int maxX = 10, int maxY = 10) return deltaX <= maxX && deltaY <= maxY; } - public void Update() + protected override void OnUpdate() { - Update(Owner.Accessor); - LocationUpdated?.Invoke(this, EventArgs.Empty); - } - - public void Update(ProcessMemoryAccessor accessor) - { - if (accessor == null) - throw new ArgumentNullException(nameof(accessor)); - var version = Owner.Version; if (version == null) @@ -97,9 +97,6 @@ public void Update(ProcessMemoryAccessor accessor) var mapYVariable = version.GetVariable(MapYKey); var mapNameVariable = version.GetVariable(MapNameKey); - using var stream = accessor.GetStream(); - using var reader = new BinaryReader(stream, Encoding.ASCII); - if (mapNumberVariable != null && mapNumberVariable.TryReadInt32(reader, out var mapNumber)) MapNumber = mapNumber; else @@ -121,7 +118,21 @@ public void Update(ProcessMemoryAccessor accessor) MapName = null; } - public void ResetDefaults() + protected override void Dispose(bool isDisposing) + { + if (isDisposed) + return; + + if (isDisposing) + { + reader?.Dispose(); + stream?.Dispose(); + } + + base.Dispose(isDisposing); + } + + private void ResetDefaults() { MapNumber = 0; X = 0; diff --git a/SleepHunter/Models/Player.cs b/SleepHunter/Models/Player.cs index b6a0d70..04fe299 100644 --- a/SleepHunter/Models/Player.cs +++ b/SleepHunter/Models/Player.cs @@ -1,37 +1,33 @@ using System; using System.IO; using System.Text; -using System.Threading; using SleepHunter.Common; using SleepHunter.IO.Process; using SleepHunter.Macro; using SleepHunter.Settings; -using SleepHunter.Win32; namespace SleepHunter.Models { - public sealed class Player : ObservableObject, IDisposable + public sealed class Player : UpdatableObject, IDisposable { private const string CharacterNameKey = @"CharacterName"; - private readonly SemaphoreSlim updateLock = new(1); + private readonly ProcessMemoryAccessor accessor; + private readonly ClientState gameClient; + private readonly Inventory inventory; + private readonly EquipmentSet equipment; + private readonly Skillbook skillbook; + private readonly Spellbook spellbook; + private readonly PlayerStats stats; + private readonly PlayerModifiers modifiers; + private readonly MapLocation location; + + private readonly Stream stream; + private readonly BinaryReader reader; - private bool isDisposed; private ClientVersion version; - private readonly ProcessMemoryAccessor accessor; + private string name; - private string guild; - private string guildRank; - private string title; - private PlayerClass playerClass; - private Inventory inventory; - private EquipmentSet equipment; - private Skillbook skillbook; - private Spellbook spellbook; - private PlayerStats stats; - private PlayerModifiers modifiers; - private MapLocation location; - private ClientState gameClient; private DateTime? loginTimestamp; private bool isLoggedIn; private string status; @@ -40,20 +36,15 @@ public sealed class Player : ObservableObject, IDisposable private bool isMacroStopped; private Hotkey hotkey; private int selectedTabIndex; - private double? skillbookScrollPosition; - private double? spellbookScrollPosition; - private double? spellQueueScrollPosition; - private double? flowerScrollPosition; private bool hasLyliacPlant; private bool hasLyliacVineyard; private bool hasFasSpiorad; private DateTime lastFlowerTimestamp; - public event EventHandler PlayerUpdated; public event EventHandler LoggedIn; public event EventHandler LoggedOut; - public ClientProcess Process { get; } + public ClientProcess Process { get; init; } public ClientVersion Version { @@ -71,78 +62,22 @@ public string Name set => SetProperty(ref name, value); } - public string Guild - { - get => guild; - set => SetProperty(ref guild, value); - } - - public string GuildRank - { - get => guildRank; - set => SetProperty(ref guildRank, value); - } - - public string Title - { - get => title; - set => SetProperty(ref title, value); - } - - public PlayerClass Class - { - get => playerClass; - set => SetProperty(ref playerClass, value); - } - - public Inventory Inventory - { - get => inventory; - private set => SetProperty(ref inventory, value); - } - - public EquipmentSet Equipment - { - get => equipment; - set => SetProperty(ref equipment, value); - } + public ClientState GameClient => gameClient; - public Skillbook Skillbook - { - get => skillbook; - private set => SetProperty(ref skillbook, value); - } + public Inventory Inventory => inventory; - public Spellbook Spellbook - { - get => spellbook; - private set => SetProperty(ref spellbook, value); - } + public EquipmentSet Equipment => equipment; - public PlayerStats Stats - { - get => stats; - private set => SetProperty(ref stats, value); - } + public Skillbook Skillbook => skillbook; - public PlayerModifiers Modifiers - { - get => modifiers; - private set => SetProperty(ref modifiers, value); - } + public Spellbook Spellbook => spellbook; - public MapLocation Location - { - get => location; - private set => SetProperty(ref location, value); - } + public PlayerStats Stats => stats; - public ClientState GameClient - { - get => gameClient; - private set => SetProperty(ref gameClient, value); - } + public PlayerModifiers Modifiers => modifiers; + public MapLocation Location => location; + public bool IsLoggedIn { get => isLoggedIn; @@ -195,30 +130,6 @@ public int SelectedTabIndex set => SetProperty(ref selectedTabIndex, value); } - public double? SkillbookScrollPosition - { - get => skillbookScrollPosition; - set => SetProperty(ref skillbookScrollPosition, value); - } - - public double? SpellbookScrollPosition - { - get => spellbookScrollPosition; - set => SetProperty(ref spellbookScrollPosition, value); - } - - public double? SpellQueueScrollPosition - { - get => spellQueueScrollPosition; - set => SetProperty(ref spellQueueScrollPosition, value); - } - - public double? FlowerScrollPosition - { - get => flowerScrollPosition; - set => SetProperty(ref flowerScrollPosition, value); - } - public bool HasLyliacPlant { get => hasLyliacPlant; @@ -250,9 +161,10 @@ public Player(ClientProcess process) Process = process ?? throw new ArgumentNullException(nameof(process)); accessor = new ProcessMemoryAccessor(process.ProcessId, ProcessAccess.Read); - if (NativeMethods.GetProcessTimes(accessor.ProcessHandle, out var creationTime, out _, out _, out _)) - process.CreationTime = creationTime.FiletimeToDateTime(); + stream = accessor.GetStream(); + reader = new BinaryReader(stream, Encoding.ASCII); + gameClient = new ClientState(this); inventory = new Inventory(this); equipment = new EquipmentSet(this); skillbook = new Skillbook(this); @@ -260,147 +172,62 @@ public Player(ClientProcess process) stats = new PlayerStats(this); modifiers = new PlayerModifiers(this); location = new MapLocation(this); - gameClient = new ClientState(this); } ~Player() => Dispose(false); - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool isDisposing) + protected override void Dispose(bool isDisposing) { if (isDisposed) return; if (isDisposing) { - skillbook?.Dispose(); - accessor?.Dispose(); - - updateLock?.Dispose(); + gameClient.Dispose(); + inventory.Dispose(); + equipment.Dispose(); + skillbook.Dispose(); + spellbook.Dispose(); + stats.Dispose(); + modifiers.Dispose(); + location.Dispose(); + + stream.Dispose(); + reader.Dispose(); + accessor.Dispose(); } - isDisposed = true; + base.Dispose(isDisposing); } - public async void Update(PlayerFieldFlags updateFields = PlayerFieldFlags.All) + protected override void OnUpdate() { - await updateLock.WaitAsync(); - GameClient.VersionKey = Version?.Key ?? "Unknown"; + Process.TryUpdate(); + gameClient.TryUpdate(); + try { - try - { - if (updateFields.HasFlag(PlayerFieldFlags.GameClient)) - gameClient.Update(accessor); - } - catch { } - - try - { - if (updateFields.HasFlag(PlayerFieldFlags.Window)) - Process.Update(); - } - catch { } - - try - { - if (updateFields.HasFlag(PlayerFieldFlags.Name)) - UpdateName(accessor); - } - catch { } - - try - { - if (updateFields.HasFlag(PlayerFieldFlags.Guild)) - UpdateGuild(accessor); - } - catch { } - - try - { - if (updateFields.HasFlag(PlayerFieldFlags.GuildRank)) - UpdateGuildRank(accessor); - } - catch { } - - try - { - if (updateFields.HasFlag(PlayerFieldFlags.Title)) - UpdateTitle(accessor); - } - catch { } - - try - { - if (updateFields.HasFlag(PlayerFieldFlags.Stats)) - stats.Update(accessor); - } - catch { } - - try - { - if (updateFields.HasFlag(PlayerFieldFlags.Modifiers)) - modifiers.Update(accessor); - } - catch { } - - try - { - if (updateFields.HasFlag(PlayerFieldFlags.Location)) - location.Update(accessor); - } - catch { } - - try - { - if (updateFields.HasFlag(PlayerFieldFlags.Inventory)) - inventory.Update(accessor); - } - catch { } - - try - { - if (updateFields.HasFlag(PlayerFieldFlags.Equipment)) - equipment.Update(accessor); - } - catch { } - - try - { - if (updateFields.HasFlag(PlayerFieldFlags.Skillbook)) - skillbook.Update(accessor); - } - catch { } - - try - { - if (updateFields.HasFlag(PlayerFieldFlags.Spellbook)) - spellbook.Update(accessor); - } - catch { } - } - finally - { - updateLock.Release(); + UpdateName(accessor); } + catch { } + + stats.TryUpdate(); + modifiers.TryUpdate(); + location.TryUpdate(); + inventory.TryUpdate(); + equipment.TryUpdate(); + skillbook.TryUpdate(); + spellbook.TryUpdate(); var wasLoggedIn = IsLoggedIn; - IsLoggedIn = !string.IsNullOrWhiteSpace(Name) && stats.Level > 0; - var isNowLoggedIn = IsLoggedIn; + var isNowLoggedIn = !string.IsNullOrWhiteSpace(Name) && stats.Level > 0; if (isNowLoggedIn && !wasLoggedIn) OnLoggedIn(); else if (wasLoggedIn && !isNowLoggedIn) OnLoggedOut(); - - PlayerUpdated?.Invoke(this, EventArgs.Empty); } private void UpdateName(ProcessMemoryAccessor accessor) @@ -410,45 +237,16 @@ private void UpdateName(ProcessMemoryAccessor accessor) string name = null; - if (version != null && version.ContainsVariable(CharacterNameKey)) - { - using var stream = accessor.GetStream(); - using var reader = new BinaryReader(stream, Encoding.ASCII); - - var nameVariable = version.GetVariable(CharacterNameKey); + if (version != null && version.TryGetVariable(CharacterNameKey, out var nameVariable)) nameVariable.TryReadString(reader, out name); - } if (!string.IsNullOrWhiteSpace(name)) Name = name; } - private void UpdateGuild(ProcessMemoryAccessor accessor) - { - if (accessor == null) - throw new ArgumentNullException(nameof(accessor)); - - // Not currently implemented - } - - private void UpdateGuildRank(ProcessMemoryAccessor accessor) - { - if (accessor == null) - throw new ArgumentNullException(nameof(accessor)); - - // Not currently implemented - } - - private void UpdateTitle(ProcessMemoryAccessor accessor) - { - if (accessor == null) - throw new ArgumentNullException(nameof(accessor)); - - // Not currently implemented - } - private void OnLoggedIn() { + IsLoggedIn = true; LoggedIn?.Invoke(this, EventArgs.Empty); } @@ -457,6 +255,7 @@ void OnLoggedOut() // This memory gets re-allocated when a new character logs into the same client instance skillbook.ResetCooldownPointer(); + IsLoggedIn = false; LoggedOut?.Invoke(this, EventArgs.Empty); } diff --git a/SleepHunter/Models/PlayerManager.cs b/SleepHunter/Models/PlayerManager.cs index 6fd7244..3c71b85 100644 --- a/SleepHunter/Models/PlayerManager.cs +++ b/SleepHunter/Models/PlayerManager.cs @@ -3,8 +3,6 @@ using System.Collections.Concurrent; using System.ComponentModel; using System.Linq; - -using SleepHunter.IO.Process; using SleepHunter.Settings; namespace SleepHunter.Models @@ -22,9 +20,8 @@ private PlayerManager() { } private bool showAllClients; public event PlayerEventHandler PlayerAdded; - public event PropertyChangedEventHandler PlayerPropertyChanged; - public event PlayerEventHandler PlayerUpdated; public event PlayerEventHandler PlayerRemoved; + public event PropertyChangedEventHandler PlayerPropertyChanged; public event PropertyChangedEventHandler PropertyChanged; @@ -99,9 +96,7 @@ public void AddPlayer(Player player) players[player.Process.ProcessId] = player; - if (alreadyExists) - OnPlayerUpdated(player); - else + if (!alreadyExists) OnPlayerAdded(player); } @@ -125,6 +120,7 @@ public Player GetPlayerByName(string playerName) public bool RemovePlayer(int processId) { var wasRemoved = players.TryRemove(processId, out var removedPlayer); + removedPlayer.PropertyChanged -= Player_PropertyChanged; if (wasRemoved) { @@ -162,25 +158,11 @@ public void UpdateClients(Predicate predicate = null) private void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - private void OnPlayerAdded(Player player) - { - player.PropertyChanged += Player_PropertyChanged; - PlayerAdded?.Invoke(this, new PlayerEventArgs(player)); - } + private void OnPlayerAdded(Player player) => PlayerAdded?.Invoke(this, new PlayerEventArgs(player)); private void OnPlayerPropertyChanged(Player player, string propertyName) => PlayerPropertyChanged?.Invoke(player, new PropertyChangedEventArgs(propertyName)); - private void OnPlayerUpdated(Player player) - { - player.PropertyChanged += Player_PropertyChanged; - PlayerUpdated?.Invoke(this, new PlayerEventArgs(player)); - } - - private void OnPlayerRemoved(Player player) - { - player.PropertyChanged -= Player_PropertyChanged; - PlayerRemoved?.Invoke(this, new PlayerEventArgs(player)); - } + private void OnPlayerRemoved(Player player) => PlayerRemoved?.Invoke(this, new PlayerEventArgs(player)); private void Player_PropertyChanged(object sender, PropertyChangedEventArgs e) { diff --git a/SleepHunter/Models/PlayerModifiers.cs b/SleepHunter/Models/PlayerModifiers.cs index 36ee8bd..a94a650 100644 --- a/SleepHunter/Models/PlayerModifiers.cs +++ b/SleepHunter/Models/PlayerModifiers.cs @@ -1,33 +1,21 @@ using System; using SleepHunter.Common; -using SleepHunter.IO.Process; namespace SleepHunter.Models { - public sealed class PlayerModifiers : ObservableObject + public sealed class PlayerModifiers : UpdatableObject { - public event EventHandler ModifiersUpdated; - - public Player Owner { get; } + public Player Owner { get; init; } public PlayerModifiers(Player owner) { Owner = owner ?? throw new ArgumentNullException(nameof(owner)); } - public void Update() + protected override void OnUpdate() { - Update(Owner.Accessor); - ModifiersUpdated?.Invoke(this, EventArgs.Empty); - } - - public void Update(ProcessMemoryAccessor accessor) - { - if (accessor == null) - throw new ArgumentNullException(nameof(accessor)); - - // Not currently implemented + // Does nothing yet } } } diff --git a/SleepHunter/Models/PlayerStats.cs b/SleepHunter/Models/PlayerStats.cs index 198df3a..015bf0a 100644 --- a/SleepHunter/Models/PlayerStats.cs +++ b/SleepHunter/Models/PlayerStats.cs @@ -7,7 +7,7 @@ namespace SleepHunter.Models { - public sealed class PlayerStats : ObservableObject + public sealed class PlayerStats : UpdatableObject { private const string CurrentHealthKey = @"CurrentHealth"; private const string MaximumHealthKey = @"MaximumHealth"; @@ -16,6 +16,9 @@ public sealed class PlayerStats : ObservableObject private const string LevelKey = @"Level"; private const string AbilityLevelKey = @"AbilityLevel"; + private readonly Stream stream; + private readonly BinaryReader reader; + private int currentHealth; private int maximumHealth; private int currentMana; @@ -23,9 +26,7 @@ public sealed class PlayerStats : ObservableObject private int level; private int abilityLevel; - public event EventHandler StatsUpdated; - - public Player Owner { get; } + public Player Owner { get; init; } public int CurrentHealth { @@ -98,19 +99,14 @@ public int AbilityLevel public PlayerStats(Player owner) { Owner = owner ?? throw new ArgumentNullException(nameof(owner)); - } - public void Update() - { - Update(Owner.Accessor); - StatsUpdated?.Invoke(this, EventArgs.Empty); + stream = owner.Accessor.GetStream(); + reader = new BinaryReader(stream, Encoding.ASCII); } - public void Update(ProcessMemoryAccessor accessor) - { - if (accessor == null) - throw new ArgumentNullException(nameof(accessor)); + protected override void OnUpdate() + { var version = Owner.Version; if (version == null) @@ -126,9 +122,6 @@ public void Update(ProcessMemoryAccessor accessor) var levelVariable = version.GetVariable(LevelKey); var abVariable = version.GetVariable(AbilityLevelKey); - using var stream = accessor.GetStream(); - using var reader = new BinaryReader(stream, Encoding.ASCII); - // Current Health if (hpVariable != null && hpVariable.TryReadIntegerString(reader, out var currentHealth)) CurrentHealth = (int)currentHealth; @@ -166,7 +159,21 @@ public void Update(ProcessMemoryAccessor accessor) AbilityLevel = 0; } - public void ResetDefaults() + protected override void Dispose(bool isDisposing) + { + if (isDisposed) + return; + + if (isDisposing) + { + reader?.Dispose(); + stream?.Dispose(); + } + + base.Dispose(isDisposing); + } + + private void ResetDefaults() { CurrentHealth = 0; MaximumHealth = 0; diff --git a/SleepHunter/Models/Skillbook.cs b/SleepHunter/Models/Skillbook.cs index 717590c..bb0f244 100644 --- a/SleepHunter/Models/Skillbook.cs +++ b/SleepHunter/Models/Skillbook.cs @@ -17,7 +17,7 @@ namespace SleepHunter.Models { - public sealed class Skillbook : ObservableObject, IEnumerable, IDisposable + public sealed class Skillbook : UpdatableObject, IEnumerable, IDisposable { private const string SkillbookKey = @"Skillbook"; private const string SkillCooldownsKey = "SkillCooldowns"; @@ -26,20 +26,18 @@ public sealed class Skillbook : ObservableObject, IEnumerable, IDisposabl public const int MedeniaSkillCount = 36; public const int WorldSkillCount = 18; - private bool isDisposed; - private readonly List skills = new(TemuairSkillCount + MedeniaSkillCount + WorldSkillCount); + private readonly Skill[] skills = new Skill[TemuairSkillCount + MedeniaSkillCount + WorldSkillCount]; private readonly ConcurrentDictionary activeSkills = new(StringComparer.OrdinalIgnoreCase); private readonly ProcessMemoryScanner scanner; - nint baseCooldownPointer; + private readonly Stream stream; + private readonly BinaryReader reader; - public event EventHandler SkillbookUpdated; + private nint baseCooldownPointer; - public Player Owner { get; } + public Player Owner { get; init; } - public int Count => skills.Count((skill) => { return !skill.IsEmpty; }); - - public IEnumerable Skills => + public IEnumerable AllSkills => from s in skills select s; public IEnumerable TemuairSkills => @@ -59,75 +57,46 @@ public Skillbook(Player owner) Owner = owner ?? throw new ArgumentNullException(nameof(owner)); scanner = new ProcessMemoryScanner(Owner.ProcessHandle, leaveOpen: true); - InitializeSkillbook(); - } - - ~Skillbook() => Dispose(false); - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool isDisposing) - { - if (isDisposed) - return; - - if (isDisposing) - { - scanner?.Dispose(); - } + stream = owner.Accessor.GetStream(); + reader = new BinaryReader(stream, Encoding.ASCII); - isDisposed = true; + for (var i = 0; i < skills.Length; i++) + skills[i] = Skill.MakeEmpty(i + 1); } - private void InitializeSkillbook() - { - skills.Clear(); - - for (int i = 0; i < skills.Capacity; i++) - skills.Add(Skill.MakeEmpty(i + 1)); - } + ~Skillbook() => Dispose(false); public bool ContainSkill(string skillName) { - skillName = skillName.Trim(); - - foreach (var skill in skills) - if (string.Equals(skill.Name, skillName, StringComparison.OrdinalIgnoreCase)) - return true; - - return false; + CheckIfDisposed(); + return skills.Any(skill => string.Equals(skill.Name, skillName.Trim(), StringComparison.OrdinalIgnoreCase)); } public Skill GetSkill(string skillName) { - skillName = skillName.Trim(); - - foreach (var skill in skills) - if (string.Equals(skill.Name, skillName, StringComparison.OrdinalIgnoreCase)) - return skill; - - return null; + CheckIfDisposed(); + return skills.FirstOrDefault(skill => string.Equals(skill.Name, skillName.Trim(), StringComparison.OrdinalIgnoreCase)); } public bool? IsActive(string skillName) { + CheckIfDisposed(); + if (skillName == null) return null; skillName = skillName.Trim(); - if (activeSkills.ContainsKey(skillName)) - return activeSkills[skillName]; - else - return null; + if (activeSkills.TryGetValue(skillName, out var activeState)) + return activeState; + + return null; } public bool? ToggleActive(string skillName, bool? isActive = null) { + CheckIfDisposed(); + skillName = skillName.Trim(); bool? wasActive = null; @@ -149,17 +118,8 @@ public Skill GetSkill(string skillName) public void ResetCooldownPointer() => baseCooldownPointer = nint.Zero; - public void Update() + protected override void OnUpdate() { - Update(Owner.Accessor); - SkillbookUpdated?.Invoke(this, EventArgs.Empty); - } - - public void Update(ProcessMemoryAccessor accessor) - { - if (accessor == null) - throw new ArgumentNullException(nameof(accessor)); - var version = Owner.Version; if (version == null) @@ -168,36 +128,31 @@ public void Update(ProcessMemoryAccessor accessor) return; } - var skillbookVariable = version.GetVariable(SkillbookKey); - - if (skillbookVariable == null) + if (!version.TryGetVariable(SkillbookKey, out var skillbookVariable)) { ResetDefaults(); return; } - using var stream = accessor.GetStream(); - using var reader = new BinaryReader(stream, Encoding.ASCII); - - var skillbookPointer = skillbookVariable.DereferenceValue(reader); - - if (skillbookPointer == 0) + if (!skillbookVariable.TryDereferenceValue(reader, out var basePointer)) { ResetDefaults(); return; } - reader.BaseStream.Position = skillbookPointer; + stream.Position = basePointer; - for (int i = 0; i < skillbookVariable.Count; i++) + var entryCount = Math.Min(skills.Length, skillbookVariable.Count); + + for (var i = 0; i < entryCount; i++) { SkillMetadata metadata = null; try { - bool hasSkill = reader.ReadInt16() != 0; - ushort iconIndex = reader.ReadUInt16(); - string name = reader.ReadFixedString(skillbookVariable.MaxLength); + var hasSkill = reader.ReadInt16() != 0; + var iconIndex = reader.ReadUInt16(); + var name = reader.ReadFixedString(skillbookVariable.MaxLength); if (!Ability.TryParseLevels(name, out name, out var currentLevel, out var maximumLevel)) { @@ -241,17 +196,32 @@ public void Update(ProcessMemoryAccessor accessor) skills[i].MaxHealthPercent = null; } - skills[i].IsOnCooldown = IsSkillOnCooldown(i, version, reader, accessor.ProcessHandle); + skills[i].IsOnCooldown = IsSkillOnCooldown(i, version, reader, Owner.Accessor.ProcessHandle); } catch { } } } - public void ResetDefaults() + protected override void Dispose(bool isDisposing) + { + if (isDisposed) + return; + + if (isDisposing) + { + scanner?.Dispose(); + reader?.Dispose(); + stream?.Dispose(); + } + + base.Dispose(isDisposing); + } + + private void ResetDefaults() { activeSkills.Clear(); - for (int i = 0; i < skills.Capacity; i++) + for (int i = 0; i < skills.Length; i++) { skills[i].IsEmpty = true; skills[i].Name = null; diff --git a/SleepHunter/Models/Spellbook.cs b/SleepHunter/Models/Spellbook.cs index cf79f52..f297128 100644 --- a/SleepHunter/Models/Spellbook.cs +++ b/SleepHunter/Models/Spellbook.cs @@ -14,7 +14,7 @@ namespace SleepHunter.Models { - public sealed class Spellbook : ObservableObject, IEnumerable + public sealed class Spellbook : UpdatableObject, IEnumerable { private const string SpellbookKey = @"Spellbook"; @@ -22,18 +22,18 @@ public sealed class Spellbook : ObservableObject, IEnumerable public const int MedeniaSpellCount = 36; public const int WorldSpellCount = 18; - private readonly List spells = new(TemuairSpellCount + MedeniaSpellCount + WorldSpellCount); - private readonly ConcurrentDictionary spellCooldownTimestamps = new(); + private readonly Spell[] spells = new Spell[TemuairSpellCount + MedeniaSpellCount + WorldSpellCount]; - private string activeSpell; + private readonly Stream stream; + private readonly BinaryReader reader; - public event EventHandler SpellbookUpdated; + private readonly ConcurrentDictionary spellCooldownTimestamps = new(); - public Player Owner { get; } + private string activeSpell; - public int Count => spells.Count((spell) => { return !spell.IsEmpty; }); + public Player Owner { get; init; } - public IEnumerable Spells => + public IEnumerable AllSpells => from s in spells select s; public IEnumerable TemuairSpells => @@ -54,74 +54,60 @@ public string ActiveSpell public Spellbook(Player owner) { Owner = owner ?? throw new ArgumentNullException(nameof(owner)); - InitialzeSpellbook(); - } - private void InitialzeSpellbook() - { - spells.Clear(); + stream = owner.Accessor.GetStream(); + reader = new BinaryReader(stream, Encoding.ASCII); - for (int i = 0; i < spells.Capacity; i++) - spells.Add(Spell.MakeEmpty(i + 1)); + for (var i = 0; i < spells.Length; i++) + spells[i] = (Spell.MakeEmpty(i + 1)); } public bool ContainSpell(string spellName) { - spellName = spellName.Trim(); - - foreach (var spell in spells) - if (string.Equals(spell.Name, spellName, StringComparison.OrdinalIgnoreCase)) - return true; - - return false; + CheckIfDisposed(); + return spells.Any(spell => string.Equals(spell.Name, spellName.Trim(), StringComparison.OrdinalIgnoreCase)); } public Spell GetSpell(string spellName) { - spellName = spellName.Trim(); - - foreach (var spell in spells) - if (string.Equals(spell.Name, spellName, StringComparison.OrdinalIgnoreCase)) - return spell; - - return null; + CheckIfDisposed(); + return spells.FirstOrDefault(spell => string.Equals(spell.Name, spellName.Trim(), StringComparison.OrdinalIgnoreCase)); } public bool IsActive(string spellName) { + CheckIfDisposed(); + if (spellName == null) return false; - spellName = spellName.Trim(); - - return string.Equals(spellName, activeSpell, StringComparison.OrdinalIgnoreCase); + return string.Equals(activeSpell, spellName.Trim(), StringComparison.OrdinalIgnoreCase); } public void SetCooldownTimestamp(string spellName, DateTime timestamp) { + CheckIfDisposed(); + spellName = spellName.Trim(); spellCooldownTimestamps[spellName] = timestamp; } public bool ClearCooldown(string spellName) { + CheckIfDisposed(); + spellName = spellName.Trim(); return spellCooldownTimestamps.TryRemove(spellName, out _); } - public void ClearAllCooldowns() => spellCooldownTimestamps.Clear(); - - public void Update() + public void ClearAllCooldowns() { - Update(Owner.Accessor); - SpellbookUpdated?.Invoke(this, EventArgs.Empty); + CheckIfDisposed(); + spellCooldownTimestamps.Clear(); } - public void Update(ProcessMemoryAccessor accessor) + protected override void OnUpdate() { - if (accessor == null) - throw new ArgumentNullException(nameof(accessor)); - var version = Owner.Version; if (version == null) @@ -131,34 +117,29 @@ public void Update(ProcessMemoryAccessor accessor) return; } - var spellbookVariable = version.GetVariable(SpellbookKey); - - if (spellbookVariable == null) + if (!version.TryGetVariable(SpellbookKey, out var spellbookVariable)) { ResetDefaults(); UpdateCooldowns(); return; } - using var stream = accessor.GetStream(); - using var reader = new BinaryReader(stream, Encoding.ASCII); - - var spellbookPointer = spellbookVariable.DereferenceValue(reader); - - if (spellbookPointer == 0) + if (!spellbookVariable.TryDereferenceValue(reader, out var basePointer)) { ResetDefaults(); UpdateCooldowns(); return; } - reader.BaseStream.Position = spellbookPointer; + stream.Position = basePointer; + + var foundFasSpiorad = false; + var foundLyliacVineyard = false; + var foundLyliacPlant = false; - bool foundFasSpiorad = false; - bool foundLyliacVineyard = false; - bool foundLyliacPlant = false; + var entryCount = Math.Min(spells.Length, spellbookVariable.Count); - for (int i = 0; i < spellbookVariable.Count; i++) + for (var i = 0; i < entryCount; i++) { SpellMetadata metadata = null; @@ -230,7 +211,7 @@ public void ResetDefaults() { ActiveSpell = null; - for (int i = 0; i < spells.Capacity; i++) + for (int i = 0; i < spells.Length; i++) { spells[i].IsEmpty = true; spells[i].Name = null; @@ -249,7 +230,7 @@ public IEnumerator GetEnumerator() private void UpdateCooldowns() { - for (var i = 0; i < spells.Capacity; i++) + for (var i = 0; i < spells.Length; i++) { var spellName = spells[i].Name; diff --git a/SleepHunter/SleepHunter.csproj b/SleepHunter/SleepHunter.csproj index 47ab93a..ae824d9 100644 --- a/SleepHunter/SleepHunter.csproj +++ b/SleepHunter/SleepHunter.csproj @@ -21,8 +21,8 @@ 2023 Erik 'SiLo' Rogers Dark Ages Automation Tool SleepHunter - 4.10.1.0 - 4.10.1.0 + 4.10.2.0 + 4.10.2.0 x64 diff --git a/SleepHunter/Threading/UITask.cs b/SleepHunter/Threading/UITask.cs new file mode 100644 index 0000000..2075fa9 --- /dev/null +++ b/SleepHunter/Threading/UITask.cs @@ -0,0 +1,23 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace SleepHunter.Threading +{ + [AsyncMethodBuilder(typeof(UITaskMethodBuilder))] + public class UITask + { + private readonly TaskCompletionSource tcs = new(); + + public Task AsTask() => tcs.Task; + + public TaskAwaiter GetAwaiter() => tcs.Task.GetAwaiter(); + + public void SetResult() => tcs.SetResult(); + + public void SetException(Exception exception) => tcs.SetException(exception); + + + public static implicit operator Task(UITask task) => task.AsTask(); + } +} diff --git a/SleepHunter/Threading/UITaskMethodBuilder.cs b/SleepHunter/Threading/UITaskMethodBuilder.cs new file mode 100644 index 0000000..18be20e --- /dev/null +++ b/SleepHunter/Threading/UITaskMethodBuilder.cs @@ -0,0 +1,60 @@ +using System; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Threading; + +namespace SleepHunter.Threading +{ + public sealed class UITaskMethodBuilder + { + private readonly Dispatcher dispatcher; + + public UITask Task { get; init; } = new UITask(); + + public UITaskMethodBuilder(Dispatcher dispatcher) + { + this.dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); + } + + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + { + if (!dispatcher.CheckAccess()) + dispatcher.BeginInvoke(new Action(stateMachine.MoveNext)); + else + stateMachine.MoveNext(); + } + + public static UITaskMethodBuilder Create() => new(Application.Current.Dispatcher); + + public void SetStateMachine(IAsyncStateMachine _) { } + + public void SetResult() => Task.SetResult(); + + public void SetException(Exception exception) => Task.SetException(exception); + + public void AwaitOnCompleted(ref TAwaiter awaiter, TStateMachine stateMachine) + where TAwaiter: INotifyCompletion + where TStateMachine : IAsyncStateMachine + { + awaiter.OnCompleted(ResumeAfterAwait(stateMachine)); + } + + public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, TStateMachine stateMachine) + where TAwaiter: ICriticalNotifyCompletion + where TStateMachine: IAsyncStateMachine + { + awaiter.UnsafeOnCompleted(ResumeAfterAwait(stateMachine)); + } + + private Action ResumeAfterAwait(TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine + { + return () => + { + if (!dispatcher.CheckAccess()) + dispatcher.BeginInvoke(new Action(stateMachine.MoveNext)); + else + stateMachine.MoveNext(); + }; + } + } +} diff --git a/SleepHunter/Threading/UIThreadAwaitable.cs b/SleepHunter/Threading/UIThreadAwaitable.cs new file mode 100644 index 0000000..e298786 --- /dev/null +++ b/SleepHunter/Threading/UIThreadAwaitable.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.CompilerServices; +using System.Windows.Threading; + +namespace SleepHunter.Threading +{ + public readonly struct UIThreadAwaitable : INotifyCompletion + { + private readonly Dispatcher dispatcher; + + public UIThreadAwaitable(Dispatcher dispatcher) + { + this.dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); + } + + public readonly UIThreadAwaitable GetAwaiter() => this; + + public readonly void GetResult() { } + + public readonly bool IsCompleted => dispatcher.CheckAccess(); + + public readonly void OnCompleted(Action continuation) => dispatcher.BeginInvoke(continuation); + } +} diff --git a/SleepHunter/Views/FlowerTargetWindow.xaml.cs b/SleepHunter/Views/FlowerTargetWindow.xaml.cs index 9517dc7..38e7462 100644 --- a/SleepHunter/Views/FlowerTargetWindow.xaml.cs +++ b/SleepHunter/Views/FlowerTargetWindow.xaml.cs @@ -73,35 +73,30 @@ public FlowerTargetWindow() private void InitializeViews() { PlayerManager.Instance.PlayerAdded += OnPlayerCollectionChanged; - PlayerManager.Instance.PlayerUpdated += OnPlayerCollectionChanged; PlayerManager.Instance.PlayerRemoved += OnPlayerCollectionChanged; PlayerManager.Instance.PlayerPropertyChanged += OnPlayerPropertyChanged; } - private void OnPlayerCollectionChanged(object sender, PlayerEventArgs e) + private async void OnPlayerCollectionChanged(object sender, PlayerEventArgs e) { - Dispatcher.InvokeIfRequired(() => - { - BindingOperations.GetBindingExpression(characterComboBox, ListView.ItemsSourceProperty).UpdateTarget(); + await Dispatcher.SwitchToUIThread(); - }, DispatcherPriority.DataBind); + BindingOperations.GetBindingExpression(characterComboBox, ItemsControl.ItemsSourceProperty).UpdateTarget(); } - private void OnPlayerPropertyChanged(object sender, PropertyChangedEventArgs e) + private async void OnPlayerPropertyChanged(object sender, PropertyChangedEventArgs e) { if (sender is not Player player) return; - if (string.Equals("Name", e.PropertyName, StringComparison.OrdinalIgnoreCase) || - string.Equals("IsLoggedIn", e.PropertyName, StringComparison.OrdinalIgnoreCase)) - { - Dispatcher.InvokeIfRequired(() => - { - BindingOperations.GetBindingExpression(characterComboBox, ListView.ItemsSourceProperty).UpdateTarget(); - characterComboBox.Items.Refresh(); + await Dispatcher.SwitchToUIThread(); - }, DispatcherPriority.DataBind); + if (string.Equals(nameof(player.Name), e.PropertyName, StringComparison.OrdinalIgnoreCase) || + string.Equals(nameof(player.IsLoggedIn), e.PropertyName, StringComparison.OrdinalIgnoreCase)) + { + BindingOperations.GetBindingExpression(characterComboBox, ItemsControl.ItemsSourceProperty).UpdateTarget(); + characterComboBox.Items.Refresh(); } } @@ -153,7 +148,7 @@ private bool ValidateFlowerTarget() interval = TimeSpan.Zero; else if (double.TryParse(intervalTextBox.Text.Trim(), out var intervalSeconds) && intervalSeconds >= 0) interval = TimeSpan.FromSeconds(intervalSeconds); - else if (!TimeSpanExtender.TryParse(intervalTextBox.Text.Trim(), out interval) || interval < TimeSpan.Zero) + else if (!TimeSpanExtensions.TryParse(intervalTextBox.Text.Trim(), out interval) || interval < TimeSpan.Zero) { this.ShowMessageBox("Invalid Interval", "Interval must be a valid positive timespan value.", diff --git a/SleepHunter/Views/MainWindow.xaml.cs b/SleepHunter/Views/MainWindow.xaml.cs index d3f580c..e27cefc 100644 --- a/SleepHunter/Views/MainWindow.xaml.cs +++ b/SleepHunter/Views/MainWindow.xaml.cs @@ -57,13 +57,6 @@ public partial class MainWindow : Window, IDisposable private BackgroundWorker flowerUpdateWorker; private PlayerMacroState selectedMacro; - private volatile int closingFlag; - - public bool IsClosing - { - get => Interlocked.And(ref closingFlag, 1) > 0; - set => Interlocked.Exchange(ref closingFlag, value ? 1 : 0); - } public MainWindow() { @@ -298,7 +291,6 @@ private void InitializeHotkeyHook() private void InitializeViews() { PlayerManager.Instance.PlayerAdded += OnPlayerCollectionAdd; - PlayerManager.Instance.PlayerUpdated += OnPlayerCollectionAdd; PlayerManager.Instance.PlayerRemoved += OnPlayerCollectionRemove; PlayerManager.Instance.PlayerPropertyChanged += OnPlayerPropertyChanged; @@ -338,46 +330,46 @@ private void OnPlayerCollectionRemove(object sender, PlayerEventArgs e) SelectNextAvailablePlayer(); } - private void OnPlayerPropertyChanged(object sender, PropertyChangedEventArgs e) + private async void OnPlayerPropertyChanged(object sender, PropertyChangedEventArgs e) { if (sender is not Player player) return; - Dispatcher.InvokeIfRequired(() => + await Dispatcher.SwitchToUIThread(); + + if (string.Equals(nameof(player.IsLoggedIn), e.PropertyName, StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(nameof(player.IsLoggedIn), e.PropertyName, StringComparison.OrdinalIgnoreCase)) - { - if (!player.IsLoggedIn) - OnPlayerLoggedOut(player); - else - OnPlayerLoggedIn(player); - } + if (!player.IsLoggedIn) + OnPlayerLoggedOut(player); + else + OnPlayerLoggedIn(player); + } - clientListBox.Items.Refresh(); + clientListBox.Items.Refresh(); - var selectedPlayer = clientListBox.SelectedItem as Player; + var selectedPlayer = clientListBox.SelectedItem as Player; - if (player == selectedPlayer) - { - var supportsFlowering = selectedPlayer?.Version?.SupportsFlowering ?? false; - var hasLyliacPlant = selectedPlayer?.HasLyliacPlant ?? false; - var hasLyliacVineyard = selectedPlayer?.HasLyliacVineyard ?? false; - - ToggleInventory(selectedPlayer != null); - ToggleSkills(selectedPlayer != null); - ToggleSpells(selectedPlayer != null); - ToggleFlower(supportsFlowering, hasLyliacPlant, hasLyliacVineyard); - ToggleFeatures(selectedPlayer?.Version?.HasFeaturesAvailable ?? false); - } + if (player == selectedPlayer) + { + var supportsFlowering = selectedPlayer?.Version?.SupportsFlowering ?? false; + var hasLyliacPlant = selectedPlayer?.HasLyliacPlant ?? false; + var hasLyliacVineyard = selectedPlayer?.HasLyliacVineyard ?? false; - }, DispatcherPriority.DataBind); + ToggleInventory(selectedPlayer != null); + ToggleSkills(selectedPlayer != null); + ToggleSpells(selectedPlayer != null); + ToggleFlower(supportsFlowering, hasLyliacPlant, hasLyliacVineyard); + ToggleFeatures(selectedPlayer?.Version?.HasFeaturesAvailable ?? false); + } } - private void OnPlayerLoggedIn(Player player) + private async void OnPlayerLoggedIn(Player player) { if (player == null || string.IsNullOrWhiteSpace(player.Name)) return; + await Dispatcher.SwitchToUIThread(); + if (!player.LoginTimestamp.HasValue) player.LoginTimestamp = DateTime.Now; @@ -394,20 +386,13 @@ private void OnPlayerLoggedIn(Player player) if (state != null) { state.StatusChanged += HandleMacroStatusChanged; - state.Client.PlayerUpdated += HandleClientUpdateTick; + state.Client.Updated += HandleClientUpdateTick; } if (autosaveEnabled && state != null) { - Dispatcher.InvokeIfRequired(() => - { - // Ignore if the application is already closing, it will be saved by the closing routine - if (IsClosing) - return; - - logger.LogInfo($"Auto-loading {state.Client.Name} macro state..."); - AutoLoadMacroState(state); - }, DispatcherPriority.DataBind); + logger.LogInfo($"Auto-loading {state.Client.Name} macro state..."); + AutoLoadMacroState(state); } UpdateWindowTitle(); @@ -417,11 +402,13 @@ private void OnPlayerLoggedIn(Player player) state.SpellQueueRotation = UserSettingsManager.Instance.Settings.SpellRotationMode; } - private void OnPlayerLoggedOut(Player player) + private async void OnPlayerLoggedOut(Player player) { if (player == null || string.IsNullOrWhiteSpace(player.Name)) return; + await Dispatcher.SwitchToUIThread(); + player.LoginTimestamp = null; UpdateClientList(); @@ -434,31 +421,21 @@ private void OnPlayerLoggedOut(Player player) if (autosaveEnabled && state != null) { - Dispatcher.InvokeIfRequired(() => - { - // Ignore if the application is already closing, do not auto load - if (IsClosing) - return; - - logger.LogInfo($"Auto-saving {state.Client.Name} macro state..."); - AutoSaveMacroState(state); - }, DispatcherPriority.DataBind); + logger.LogInfo($"Auto-saving {state.Client.Name} macro state..."); + AutoSaveMacroState(state); } - Dispatcher.InvokeIfRequired(() => - { - if (player.HasHotkey) - HotkeyManager.Instance.UnregisterHotkey(windowSource.Handle, player.Hotkey); + if (player.HasHotkey) + HotkeyManager.Instance.UnregisterHotkey(windowSource.Handle, player.Hotkey); - player.Hotkey = null; - }); + player.Hotkey = null; UpdateWindowTitle(); if (state != null) { state.StatusChanged -= HandleMacroStatusChanged; - state.Client.PlayerUpdated -= HandleClientUpdateTick; + state.Client.Updated -= HandleClientUpdateTick; state.ClearSpellQueue(); state.ClearFlowerQueue(); @@ -525,22 +502,16 @@ private void SelectNextAvailablePlayer() } } - private void RefreshInventory() + private async void RefreshInventory() { - if (!CheckAccess()) - { - Dispatcher.InvokeIfRequired(RefreshInventory, DispatcherPriority.DataBind); - return; - } + await Dispatcher.SwitchToUIThread(); + + // Do some stuff with inventory on UI thread } - private void RefreshSpellQueue() + private async void RefreshSpellQueue() { - if (!CheckAccess()) - { - Dispatcher.InvokeIfRequired(RefreshSpellQueue, DispatcherPriority.DataBind); - return; - } + await Dispatcher.SwitchToUIThread(); if (selectedMacro != null) spellQueueRotationComboBox.SelectedValue = selectedMacro.SpellQueueRotation; @@ -554,13 +525,9 @@ private void RefreshSpellQueue() spellQueueListBox.Items.Refresh(); } - private void RefreshFlowerQueue() + private async void RefreshFlowerQueue() { - if (!CheckAccess()) - { - Dispatcher.InvokeIfRequired(RefreshFlowerQueue, DispatcherPriority.DataBind); - return; - } + await Dispatcher.SwitchToUIThread(); var hasItemsInQueue = selectedMacro != null && selectedMacro.FlowerQueueCount > 0; @@ -571,13 +538,9 @@ private void RefreshFlowerQueue() flowerListBox.Items.Refresh(); } - private void RefreshFeatures() + private async void RefreshFeatures() { - if (!CheckAccess()) - { - Dispatcher.InvokeIfRequired(RefreshFeatures, DispatcherPriority.DataBind); - return; - } + await Dispatcher.SwitchToUIThread(); if (selectedMacro is not PlayerMacroState state) return; @@ -960,9 +923,7 @@ private void ActivateHotkey(Key key, ModifierKeys modifiers) } else { - hotkeyPlayer.Update(PlayerFieldFlags.Location); macroState.Start(); - logger.LogInfo($"Started macro state for character: {hotkeyPlayer.Name} (hotkey)"); } } @@ -1038,13 +999,9 @@ private void SetWorldSpellGridWidth(int units) worldSpellListBox.MaxWidth = ((iconSize + IconPadding) * units) + 6; } - private void UpdateUIForMacroStatus(MacroStatus status) + private async void UpdateUIForMacroStatus(MacroStatus status) { - if (!CheckAccess()) - { - Dispatcher.InvokeIfRequired(UpdateUIForMacroStatus, status, DispatcherPriority.DataBind); - return; - } + await Dispatcher.SwitchToUIThread(); switch (status) { @@ -1078,8 +1035,6 @@ private void ToggleSpellQueue(bool showQueue) if (spellQueueListBox == null) return; - logger.LogInfo($"Toggle spell queue panel: {showQueue}"); - if (showQueue) { Grid.SetColumnSpan(tabControl, 1); @@ -1244,8 +1199,6 @@ private void Window_Shown(object sender, EventArgs e) private void Window_Closing(object sender, CancelEventArgs e) { - IsClosing = true; - logger.LogInfo("Application is shutting down"); UserSettingsManager.Instance.Settings.PropertyChanged -= UserSettings_PropertyChanged; @@ -1493,9 +1446,6 @@ private bool LoadMacroState(PlayerMacroState state, string filename, bool showEr state.UseLyliacVineyard = deserialized.UseLyliacVineyard; state.FlowerAlternateCharacters = deserialized.FlowerAlternateCharacters; - // Update skillbook and spellbook just to be sure - state.Client.Update(PlayerFieldFlags.Skillbook | PlayerFieldFlags.Spellbook); - // Add all skill macros to state foreach (var skillMacro in deserialized.Skills) { @@ -1577,7 +1527,8 @@ private bool LoadMacroState(PlayerMacroState state, string filename, bool showEr RefreshFlowerQueue(); RefreshFeatures(); - ToggleSpellQueue(state.QueuedSpells.Count > 0); + if (selectedMacro != null && selectedMacro == state) + ToggleSpellQueue(state.QueuedSpells.Count > 0); } } @@ -2444,20 +2395,19 @@ private void UpdateWindowTitle() Title = $"SleepHunter - {selectedMacro.Client.Name}"; } - private void UpdateToolbarState() + private async void UpdateToolbarState() { - Dispatcher.InvokeIfRequired(() => - { - launchClientButton.IsEnabled = ClientVersionManager.Instance.Versions.Any(v => v.Key != "Auto-Detect"); - loadStateButton.IsEnabled = saveStateButton.IsEnabled = selectedMacro != null && selectedMacro.Client.IsLoggedIn; + await Dispatcher.SwitchToUIThread(); - stopAllMacrosButton.IsEnabled = MacroManager.Instance.Macros.Any(macro => macro.Status == MacroStatus.Running || macro.Status == MacroStatus.Paused); + launchClientButton.IsEnabled = ClientVersionManager.Instance.Versions.Any(v => v.Key != "Auto-Detect"); + loadStateButton.IsEnabled = saveStateButton.IsEnabled = selectedMacro != null && selectedMacro.Client.IsLoggedIn; - if (selectedMacro == null) - startMacroButton.IsEnabled = pauseMacroButton.IsEnabled = stopMacroButton.IsEnabled = false; - else - UpdateUIForMacroStatus(selectedMacro.Status); - }, DispatcherPriority.Normal); + stopAllMacrosButton.IsEnabled = MacroManager.Instance.Macros.Any(macro => macro.Status == MacroStatus.Running || macro.Status == MacroStatus.Paused); + + if (selectedMacro == null) + startMacroButton.IsEnabled = pauseMacroButton.IsEnabled = stopMacroButton.IsEnabled = false; + else + UpdateUIForMacroStatus(selectedMacro.Status); } private void ToggleInventory(bool show = true) @@ -2504,18 +2454,17 @@ private void ToggleFeatures(bool show = true) featuresTab.Visibility = show ? Visibility.Visible : Visibility.Collapsed; } - private void UpdateClientList() + private async void UpdateClientList() { + await Dispatcher.SwitchToUIThread(); + var showAll = PlayerManager.Instance.ShowAllClients; var sortOrder = PlayerManager.Instance.SortOrder; logger.LogInfo($"Updating the client list (showAll = {showAll}, sortOrder = {sortOrder})"); - Dispatcher.InvokeIfRequired(() => - { - clientListBox.GetBindingExpression(ItemsControl.ItemsSourceProperty)?.UpdateTarget(); - clientListBox.Items.Refresh(); - }, DispatcherPriority.DataBind); + clientListBox.GetBindingExpression(ItemsControl.ItemsSourceProperty)?.UpdateTarget(); + clientListBox.Items.Refresh(); } private async void CheckForNewVersion() diff --git a/SleepHunter/Views/SkillEditorWindow.xaml.cs b/SleepHunter/Views/SkillEditorWindow.xaml.cs index eea6cd2..d28c153 100644 --- a/SleepHunter/Views/SkillEditorWindow.xaml.cs +++ b/SleepHunter/Views/SkillEditorWindow.xaml.cs @@ -116,7 +116,7 @@ private bool ValidateSkill() cooldown = TimeSpan.Zero; else if (double.TryParse(cooldownTextBox.Text.Trim(), out var cooldownSeconds) && cooldownSeconds >= 0) cooldown = TimeSpan.FromSeconds(cooldownSeconds); - else if (!TimeSpanExtender.TryParse(cooldownTextBox.Text.Trim(), out cooldown) || cooldown < TimeSpan.Zero) + else if (!TimeSpanExtensions.TryParse(cooldownTextBox.Text.Trim(), out cooldown) || cooldown < TimeSpan.Zero) { this.ShowMessageBox("Invalid Cooldown", "Cooldown must be a valid positive timespan value.", diff --git a/SleepHunter/Views/SpellEditorWindow.xaml.cs b/SleepHunter/Views/SpellEditorWindow.xaml.cs index e60739e..47169ac 100644 --- a/SleepHunter/Views/SpellEditorWindow.xaml.cs +++ b/SleepHunter/Views/SpellEditorWindow.xaml.cs @@ -118,7 +118,7 @@ private bool ValidateSpell() cooldown = TimeSpan.Zero; else if (double.TryParse(cooldownTextBox.Text.Trim(), out var cooldownSeconds) && cooldownSeconds >= 0) cooldown = TimeSpan.FromSeconds(cooldownSeconds); - else if (!TimeSpanExtender.TryParse(cooldownTextBox.Text.Trim(), out cooldown) || cooldown < TimeSpan.Zero) + else if (!TimeSpanExtensions.TryParse(cooldownTextBox.Text.Trim(), out cooldown) || cooldown < TimeSpan.Zero) { this.ShowMessageBox("Invalid Cooldown", "Cooldown must be a valid positive timespan value.", diff --git a/SleepHunter/Views/SpellTargetWindow.xaml.cs b/SleepHunter/Views/SpellTargetWindow.xaml.cs index 310b890..40d9bfb 100644 --- a/SleepHunter/Views/SpellTargetWindow.xaml.cs +++ b/SleepHunter/Views/SpellTargetWindow.xaml.cs @@ -98,35 +98,29 @@ public SpellTargetWindow() private void InitializeViews() { PlayerManager.Instance.PlayerAdded += OnPlayerCollectionChanged; - PlayerManager.Instance.PlayerUpdated += OnPlayerCollectionChanged; PlayerManager.Instance.PlayerRemoved += OnPlayerCollectionChanged; PlayerManager.Instance.PlayerPropertyChanged += OnPlayerPropertyChanged; } - private void OnPlayerCollectionChanged(object sender, PlayerEventArgs e) + private async void OnPlayerCollectionChanged(object sender, PlayerEventArgs e) { - Dispatcher.InvokeIfRequired(() => - { - BindingOperations.GetBindingExpression(characterComboBox, ListView.ItemsSourceProperty).UpdateTarget(); - - }, DispatcherPriority.DataBind); + await Dispatcher.SwitchToUIThread(); + BindingOperations.GetBindingExpression(characterComboBox, ItemsControl.ItemsSourceProperty).UpdateTarget(); } - private void OnPlayerPropertyChanged(object sender, PropertyChangedEventArgs e) + private async void OnPlayerPropertyChanged(object sender, PropertyChangedEventArgs e) { - if (!(sender is Player player)) + if (sender is not Player player) return; - if (string.Equals("Name", e.PropertyName, StringComparison.OrdinalIgnoreCase) || - string.Equals("IsLoggedIn", e.PropertyName, StringComparison.OrdinalIgnoreCase)) - { - Dispatcher.InvokeIfRequired(() => - { - BindingOperations.GetBindingExpression(characterComboBox, ListView.ItemsSourceProperty).UpdateTarget(); - characterComboBox.Items.Refresh(); + await Dispatcher.SwitchToUIThread(); - }, DispatcherPriority.DataBind); + if (string.Equals(nameof(player.Name), e.PropertyName, StringComparison.OrdinalIgnoreCase) || + string.Equals(nameof(player.IsLoggedIn), e.PropertyName, StringComparison.OrdinalIgnoreCase)) + { + BindingOperations.GetBindingExpression(characterComboBox, ItemsControl.ItemsSourceProperty).UpdateTarget(); + characterComboBox.Items.Refresh(); } }