From 43845ac5fb5e789520a60208d04dc1a971417d81 Mon Sep 17 00:00:00 2001 From: Erik Rogers Date: Tue, 20 Jun 2023 00:10:34 -0400 Subject: [PATCH 01/10] feat: use new client detection --- SleepHunter/Settings/ClientSignature.cs | 51 ++++ SleepHunter/Settings/ClientVersion.cs | 18 +- SleepHunter/Settings/ClientVersionManager.cs | 11 +- SleepHunter/Views/MainWindow.xaml.cs | 245 ++++--------------- 4 files changed, 119 insertions(+), 206 deletions(-) create mode 100644 SleepHunter/Settings/ClientSignature.cs diff --git a/SleepHunter/Settings/ClientSignature.cs b/SleepHunter/Settings/ClientSignature.cs new file mode 100644 index 0000000..2bfbc5f --- /dev/null +++ b/SleepHunter/Settings/ClientSignature.cs @@ -0,0 +1,51 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Xml.Serialization; +using SleepHunter.Common; + +namespace SleepHunter.Settings +{ + [Serializable] + public sealed class ClientSignature : ObservableObject + { + private long address; + private string value; + + [XmlIgnore] + public long Address + { + get => address; + set => SetProperty(ref address, value, onChanged: (s) => { RaisePropertyChanged(nameof(AddressHex)); }); + } + + [XmlAttribute("Address")] + [DefaultValue("0")] + public string AddressHex + { + get => address.ToString("X"); + set + { + if (long.TryParse(value, NumberStyles.HexNumber, null, out var parsedLong)) + address = parsedLong; + } + } + + public string Value + { + get => value; + set => SetProperty(ref this.value, value); + } + + public ClientSignature() + : this(0, string.Empty) { } + + public ClientSignature(long address, string value) + { + Address = address; + Value = value; + } + + public override string ToString() => $"{AddressHex} = {Value}"; + } +} diff --git a/SleepHunter/Settings/ClientVersion.cs b/SleepHunter/Settings/ClientVersion.cs index 8fbd56f..0681417 100644 --- a/SleepHunter/Settings/ClientVersion.cs +++ b/SleepHunter/Settings/ClientVersion.cs @@ -15,7 +15,8 @@ public sealed class ClientVersion : ObservableObject public static readonly ClientVersion AutoDetect = new("Auto-Detect"); private string key; - private string hash; + private bool isDefault; + private ClientSignature signature; private int versionNumber; private long multipleInstanceAddress; private long introVideoAddress; @@ -29,11 +30,18 @@ public string Key set => SetProperty(ref key, value); } - [XmlAttribute("Hash")] - public string Hash + [XmlAttribute("IsDefault")] + public bool IsDefault { - get => hash; - set => SetProperty(ref hash, value); + get => isDefault; + set => SetProperty(ref isDefault, value); + } + + [XmlElement("Signature")] + public ClientSignature Signature + { + get => signature; + set => SetProperty(ref signature, value); } [XmlAttribute("Value")] diff --git a/SleepHunter/Settings/ClientVersionManager.cs b/SleepHunter/Settings/ClientVersionManager.cs index d439a4d..b5b36b2 100644 --- a/SleepHunter/Settings/ClientVersionManager.cs +++ b/SleepHunter/Settings/ClientVersionManager.cs @@ -38,6 +38,8 @@ public ClientVersion this[string key] public IEnumerable Versions => from v in clientVersions.Values orderby v.Key select v; + public ClientVersion DefaultVersion => clientVersions.Values.FirstOrDefault(version => version.IsDefault); + public void AddVersion(ClientVersion version) { if (version == null) @@ -130,15 +132,6 @@ public void SaveToStream(Stream stream) serializer.Serialize(stream, collection, namespaces); } - public string DetectVersion(string hash) - { - foreach (var version in clientVersions.Values) - if (string.Equals(version.Hash, hash, StringComparison.OrdinalIgnoreCase)) - return version.Key; - - return null; - } - void OnVersionAdded(ClientVersion version) { if (version == null) diff --git a/SleepHunter/Views/MainWindow.xaml.cs b/SleepHunter/Views/MainWindow.xaml.cs index a349c0b..f26af70 100644 --- a/SleepHunter/Views/MainWindow.xaml.cs +++ b/SleepHunter/Views/MainWindow.xaml.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; -using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using System.Windows; @@ -36,17 +35,6 @@ public partial class MainWindow : Window, IDisposable { private const int WM_HOTKEY = 0x312; - private enum ClientLoadResult - { - Success = 0, - ClientPathInvalid, - HashError, - AutoDetectFailed, - BadVersion, - CreateProcessFailed, - PatchingFailed - } - private static readonly int IconPadding = 14; private readonly ILogger logger; @@ -128,31 +116,33 @@ private void LaunchClient() startNewClientButton.IsEnabled = false; var clientPath = UserSettingsManager.Instance.Settings.ClientPath; - var result = ClientLoadResult.Success; logger.LogInfo($"Attempting to launch client executable: {clientPath}"); try { - // Ensure Client Path Exists if (!File.Exists(clientPath)) { - result = ClientLoadResult.ClientPathInvalid; logger.LogError("Client executable not found, unable to launch"); return; } - var clientVersion = DetectClientVersion(clientPath, out result); - - if (result != ClientLoadResult.Success) - return; - - var processInformation = StartClientProcess(clientPath, out result); - - if (result != ClientLoadResult.Success) - return; + var processInformation = StartClientProcess(clientPath); + + if (!TryDetectClientVersion(processInformation, out var detectedVersion)) + { + logger.LogWarn("Unable to determine client version, using default version"); + detectedVersion = ClientVersionManager.Instance.DefaultVersion; + } - PatchClient(processInformation, clientVersion, out result); + if (detectedVersion != null) + { + PatchClient(processInformation, detectedVersion); + } + else + { + logger.LogWarn("No client version, unable to apply patches"); + } } catch (Exception ex) { @@ -167,77 +157,12 @@ private void LaunchClient() } finally { - HandleClientLoadResult(result); startNewClientButton.IsEnabled = true; } } - private ClientVersion DetectClientVersion(string clientPath, out ClientLoadResult result) + private ProcessInformation StartClientProcess(string clientPath) { - ClientVersion clientVersion = null; - var clientHash = string.Empty; - var clientKey = UserSettingsManager.Instance.Settings.SelectedVersion; - - result = ClientLoadResult.Success; - Stream inputStream = null; - - // Get MD5 Hash and Detect Version - try - { - if (string.Equals("Auto-Detect", clientKey, StringComparison.OrdinalIgnoreCase)) - { - logger.LogInfo($"Attempting to auto-detect client version for executable: {clientPath}"); - - inputStream = File.Open(clientPath, FileMode.Open, FileAccess.Read, FileShare.Read); - using (var md5 = MD5.Create()) - { - md5.ComputeHash(inputStream); - inputStream.Close(); - - clientHash = BitConverter.ToString(md5.Hash).Replace("-", string.Empty); - } - - clientKey = ClientVersionManager.Instance.DetectVersion(clientHash); - - if (clientKey == null) - { - result = ClientLoadResult.AutoDetectFailed; - logger.LogError($"No client version known for hash: {clientHash.ToLowerInvariant()}"); - - return null; - } - } - } - catch (Exception ex) - { - logger.LogError("Failed to calculate client hash"); - logger.LogException(ex); - - result = ClientLoadResult.HashError; - return clientVersion; - } - finally { inputStream?.Dispose(); } - - // Get Version from Manager - clientVersion = ClientVersionManager.Instance.GetVersion(clientKey); - - if (clientVersion == null) - { - result = ClientLoadResult.BadVersion; - logger.LogWarn($"Unknown client version, key = {clientKey}"); - } - else - { - logger.LogInfo($"Client executable was detected as version: {clientVersion.Key} (md5 = {clientVersion.Hash.ToLowerInvariant()})"); - } - - return clientVersion; - } - - private ProcessInformation StartClientProcess(string clientPath, out ClientLoadResult result) - { - result = ClientLoadResult.Success; - // Create Process var startupInfo = new StartupInfo { Size = Marshal.SizeOf(typeof(StartupInfo)) }; @@ -261,8 +186,11 @@ private ProcessInformation StartClientProcess(string clientPath, out ClientLoadR // Ensure the process was actually created if (!wasCreated || processInformation.ProcessId == 0) { - result = ClientLoadResult.CreateProcessFailed; - logger.LogError("Failed to create client process"); + var errorCode = Marshal.GetLastPInvokeError(); + var errorMessage = Marshal.GetLastPInvokeErrorMessage(); + logger.LogError($"Failed to create client process, code = {errorCode}, message = {errorMessage}"); + + throw new Win32Exception(errorCode, "Unable to create client process"); } else { @@ -272,14 +200,44 @@ private ProcessInformation StartClientProcess(string clientPath, out ClientLoadR return processInformation; } - private void PatchClient(ProcessInformation process, ClientVersion version, out ClientLoadResult result) + private static bool TryDetectClientVersion(ProcessInformation process, out ClientVersion detectedVersion) + { + detectedVersion = null; + + using var stream = new ProcessMemoryStream(process.ProcessHandle, ProcessAccess.Read, true); + using var reader = new BinaryReader(stream, Encoding.ASCII); + + foreach (var version in ClientVersionManager.Instance.Versions) + { + // Skip with invalid or missing signatures + if (version.Signature == null || string.IsNullOrWhiteSpace(version.Signature.Value)) + continue; + + var signatureLength = version.Signature.Value.Length; + + // Read the signature from the process + stream.Position = version.Signature.Address; + var readValue = reader.ReadFixedString(signatureLength); + + // If signature matches the expected value, assume this client version + if (string.Equals(readValue, version.Signature.Value)) + { + detectedVersion = version; + return true; + } + } + + return false; + } + + private void PatchClient(ProcessInformation process, ClientVersion version) { var patchMultipleInstances = UserSettingsManager.Instance.Settings.AllowMultipleInstances; var patchIntroVideo = UserSettingsManager.Instance.Settings.SkipIntroVideo; var patchNoWalls = UserSettingsManager.Instance.Settings.NoWalls; var pid = process.ProcessId; - logger.LogInfo($"Attempting to patch client process {pid}"); + logger.LogInfo($"Attempting to patch client process {pid}, version = {version.Key}"); try { @@ -320,15 +278,6 @@ private void PatchClient(ProcessInformation process, ClientVersion version, out writer.Write((byte)0x17); // +0x17 writer.Write((byte)0x90); // NOP } - - result = ClientLoadResult.Success; - } - catch (Exception ex) - { - logger.LogError($"Failed to patch client process {pid}"); - logger.LogException(ex); - - result = ClientLoadResult.PatchingFailed; } finally { @@ -339,94 +288,6 @@ private void PatchClient(ProcessInformation process, ClientVersion version, out } } - private void HandleClientLoadResult(ClientLoadResult result) - { - // Client Path Invalid - if (result == ClientLoadResult.ClientPathInvalid) - { - var showClientPathSetting = this.ShowMessageBox("Invalid Client Path", - "The client path specified in the settings does not exist.\nDo you wish to set it now?", - "New clients cannot be started until this value is set to a valid path.", - MessageBoxButton.YesNo, - 460, 260); - - if (showClientPathSetting.Value) - ShowSettingsWindow(SettingsWindow.GameClientTabIndex); - } - - // Hash IO Error - if (result == ClientLoadResult.HashError) - { - this.ShowMessageBox("IO Error", - "An I/O error occured when trying to read the client executable.", - "You must have read permissions for the file.", - MessageBoxButton.OK, - 460, 240); - } - - // Auto-Detect Error - if (result == ClientLoadResult.AutoDetectFailed) - { - var showClientVersionSetting = this.ShowMessageBox("Auto-Detect Failed", - "The client version could not be detected from the file.\nDo you want to set it manually?", - "New clients cannot be started unless version detection is successful.\nYou may manually select a client version instead.", - MessageBoxButton.YesNo, - 460, 260); - - if (showClientVersionSetting.Value) - ShowSettingsWindow(SettingsWindow.GameClientTabIndex); - } - - // Bad- Version Error - if (result == ClientLoadResult.BadVersion) - { - var showClientVersionSetting = this.ShowMessageBox("Invalid Client Version", - "The client version selected is invalid.\nWould you like to select another one?", - "New clients cannot be started until a valid client version is selected.\nAuto-detection may also be selected.", - MessageBoxButton.YesNo, - 460, 240); - - if (showClientVersionSetting.Value) - ShowSettingsWindow(SettingsWindow.GameClientTabIndex); - } - - // Bad- Version Error - if (result == ClientLoadResult.BadVersion) - { - var showClientVersionSetting = this.ShowMessageBox("Invalid Client Version", - "The client version selected is invalid.\nWould you like to select another one?", - "New clients cannot be started until a valid client version is selected.\nAuto-detection may also be selected.", - MessageBoxButton.YesNo, - 460, 240); - - if (showClientVersionSetting.Value) - ShowSettingsWindow(SettingsWindow.GameClientTabIndex); - } - - // Create Process - if (result == ClientLoadResult.CreateProcessFailed) - { - var showClientVersionSetting = this.ShowMessageBox("Failed to Launch Client", - "An error occured trying to launch the game client.\nDo you want to check the client settings?", - "Check that the client path is correct.\nAnti-virus or other security software may be preventing this.", - MessageBoxButton.YesNo, - 420, 240); - - if (showClientVersionSetting.Value) - ShowSettingsWindow(SettingsWindow.GameClientTabIndex); - } - - // Patching - if (result == ClientLoadResult.PatchingFailed) - { - this.ShowMessageBox("Patching Error", - "An error occured trying to patch the game client.", - "The client should continue to work but features such as multiple instances or skipping the intro video may not be patched properly.\n\nIn some cases the client may crash immediately.", - MessageBoxButton.OK, - 460, 260); - } - } - private void InitializeLogger() { if (!UserSettingsManager.Instance.Settings.LoggingEnabled) From c40154e345dc1f570b7b0df25991dd1cb875d843 Mon Sep 17 00:00:00 2001 From: Erik Rogers Date: Tue, 20 Jun 2023 00:32:59 -0400 Subject: [PATCH 02/10] feat: new client version detection --- SleepHunter/Resources/Versions.xml | 3 ++- SleepHunter/Settings/ClientSignature.cs | 2 ++ SleepHunter/Views/MainWindow.xaml.cs | 12 ++++++------ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/SleepHunter/Resources/Versions.xml b/SleepHunter/Resources/Versions.xml index 7c4f0d2..212ff6d 100644 --- a/SleepHunter/Resources/Versions.xml +++ b/SleepHunter/Resources/Versions.xml @@ -1,7 +1,8 @@ - + + 57A7CE 42E61F 5FD874 diff --git a/SleepHunter/Settings/ClientSignature.cs b/SleepHunter/Settings/ClientSignature.cs index 2bfbc5f..72f29be 100644 --- a/SleepHunter/Settings/ClientSignature.cs +++ b/SleepHunter/Settings/ClientSignature.cs @@ -31,6 +31,8 @@ public string AddressHex } } + [XmlAttribute] + [DefaultValue("")] public string Value { get => value; diff --git a/SleepHunter/Views/MainWindow.xaml.cs b/SleepHunter/Views/MainWindow.xaml.cs index f26af70..0ba820b 100644 --- a/SleepHunter/Views/MainWindow.xaml.cs +++ b/SleepHunter/Views/MainWindow.xaml.cs @@ -134,15 +134,15 @@ private void LaunchClient() logger.LogWarn("Unable to determine client version, using default version"); detectedVersion = ClientVersionManager.Instance.DefaultVersion; } + else + { + logger.LogInfo($"Detected client pid {processInformation.ProcessId} version as {detectedVersion.Key}"); + } if (detectedVersion != null) - { PatchClient(processInformation, detectedVersion); - } else - { - logger.LogWarn("No client version, unable to apply patches"); - } + logger.LogWarn($"No client version, unable to apply patches to pid {processInformation.ProcessId}"); } catch (Exception ex) { @@ -596,7 +596,7 @@ private void RefreshFlowerQueue() private void LoadVersions() { - var versionsFile = ClientVersionManager.VersionsFile; + var versionsFile = Path.Combine(Environment.CurrentDirectory, ClientVersionManager.VersionsFile); logger.LogInfo($"Attempting to load client versions from file: {versionsFile}"); try From dfaa056aaef5bae8ecb78f1067ab943993913257 Mon Sep 17 00:00:00 2001 From: Erik Rogers Date: Tue, 20 Jun 2023 01:00:32 -0400 Subject: [PATCH 03/10] feat: allow multiple window class names --- CHANGELOG.md | 17 +++++++++++++++++ SleepHunter/IO/Process/ProcessManager.cs | 15 +++++++++++---- SleepHunter/Resources/Versions.xml | 4 +++- SleepHunter/Settings/ClientVersion.cs | 21 ++++++++++++++++----- SleepHunter/Views/MainWindow.xaml.cs | 10 ++++++++++ 5 files changed, 57 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9514d2c..a541b62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ 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.8.0] - Unreleased + +### Added + +- `Signature` definition for `ClientVersion`, which allows version to be detected by signature bytes instead of hash +- `ExecutableName` and `WindowClassName` properties for `ClientVersion` to support other clients + +### Changed + +- Launched clients now detect version based on the new signature definitions +- Process manager can detect other clients based on version definitions + +### Removed + +- `Value` in `ClientVersion`, as it was never used +- `Hash` in `ClientVersion`, now using signature-based detection instead + ## [4.7.0] - 2023-06-16 ### Added diff --git a/SleepHunter/IO/Process/ProcessManager.cs b/SleepHunter/IO/Process/ProcessManager.cs index 6e0c03e..7296553 100644 --- a/SleepHunter/IO/Process/ProcessManager.cs +++ b/SleepHunter/IO/Process/ProcessManager.cs @@ -10,13 +10,12 @@ namespace SleepHunter.IO.Process { public sealed class ProcessManager { - private const string WindowClassName = "DarkAges"; - private static readonly ProcessManager instance = new(); public static ProcessManager Instance => instance; private ProcessManager() { } + private readonly ConcurrentDictionary windowClassNames = new(); private readonly ConcurrentDictionary clientProcesses = new(); private readonly ConcurrentQueue deadClients = new(); private readonly ConcurrentQueue newClients = new(); @@ -71,12 +70,20 @@ public ClientProcess DequeueNewClient() public void ClearDeadClients() => deadClients.Clear(); public void ClearNewClients() => newClients.Clear(); + public void RegisterWindowClassName(string className) + => windowClassNames.TryAdd(className, className); + + public bool UnregisterWindowClassName(string className) + => windowClassNames.TryRemove(className, out _); + public void ScanForProcesses(Action enumProcessCallback = null) { var foundClients = new Dictionary(); var deadClients = new Dictionary(); var newClients = new Dictionary(); + var registeredClassNames = windowClassNames.Keys.ToList(); + NativeMethods.EnumWindows((windowHandle, lParam) => { // Get Process & Thread Id @@ -87,8 +94,8 @@ public void ScanForProcesses(Action enumProcessCallback = null) var classNameLength = NativeMethods.GetClassName(windowHandle, classNameBuffer, classNameBuffer.Capacity); var className = classNameBuffer.ToString(); - // Check Class Name (DA) - if (!string.Equals(WindowClassName, className, StringComparison.OrdinalIgnoreCase)) + // Check Class Name from Registered Values + if (!registeredClassNames.Contains(className, StringComparer.OrdinalIgnoreCase)) return true; // Get Window Title diff --git a/SleepHunter/Resources/Versions.xml b/SleepHunter/Resources/Versions.xml index 212ff6d..b0bebea 100644 --- a/SleepHunter/Resources/Versions.xml +++ b/SleepHunter/Resources/Versions.xml @@ -1,8 +1,10 @@ - + + Darkages.exe + DarkAges 57A7CE 42E61F 5FD874 diff --git a/SleepHunter/Settings/ClientVersion.cs b/SleepHunter/Settings/ClientVersion.cs index 0681417..801f1ff 100644 --- a/SleepHunter/Settings/ClientVersion.cs +++ b/SleepHunter/Settings/ClientVersion.cs @@ -12,12 +12,16 @@ namespace SleepHunter.Settings [Serializable] public sealed class ClientVersion : ObservableObject { + private const string DefaultExecutableName = "Darkages.exe"; + private const string DefaultWindowClassName = "DarkAges"; + public static readonly ClientVersion AutoDetect = new("Auto-Detect"); private string key; private bool isDefault; private ClientSignature signature; - private int versionNumber; + private string executableName = DefaultExecutableName; + private string windowClassName = DefaultWindowClassName; private long multipleInstanceAddress; private long introVideoAddress; private long noWallAddress; @@ -44,11 +48,18 @@ public ClientSignature Signature set => SetProperty(ref signature, value); } - [XmlAttribute("Value")] - public int VersionNumber + [XmlElement] + public string ExecutableName + { + get => executableName; + set => SetProperty(ref executableName, value); + } + + [XmlElement] + public string WindowClassName { - get => versionNumber; - set => SetProperty(ref versionNumber, value); + get => windowClassName; + set => SetProperty(ref windowClassName, value); } [XmlIgnore] diff --git a/SleepHunter/Views/MainWindow.xaml.cs b/SleepHunter/Views/MainWindow.xaml.cs index 0ba820b..850a099 100644 --- a/SleepHunter/Views/MainWindow.xaml.cs +++ b/SleepHunter/Views/MainWindow.xaml.cs @@ -605,6 +605,16 @@ private void LoadVersions() { ClientVersionManager.Instance.LoadFromFile(versionsFile); logger.LogInfo("Client versions successfully loaded"); + + // Register all window class names so they can be detected + foreach (var version in ClientVersionManager.Instance.Versions) + { + if (string.IsNullOrWhiteSpace(version.WindowClassName)) + continue; + + ProcessManager.Instance.RegisterWindowClassName(version.WindowClassName); + logger.LogInfo($"Registered window class name: {version.WindowClassName} (version = {version.Key})"); + } } else { From 75eb32135d76cb135a3ab897e7bc912efda50aee Mon Sep 17 00:00:00 2001 From: Erik Rogers Date: Tue, 20 Jun 2023 02:25:26 -0400 Subject: [PATCH 04/10] feat: detect client version --- CHANGELOG.md | 4 + .../IO/Process/ProcessMemoryAccessor.cs | 19 ++- SleepHunter/IO/Process/ProcessMemoryStream.cs | 25 ++-- SleepHunter/Models/Ability.cs | 33 +++-- SleepHunter/Models/Inventory.cs | 1 + SleepHunter/Models/PlayerManager.cs | 5 +- SleepHunter/Models/PlayerStats.cs | 20 +-- SleepHunter/Models/Skillbook.cs | 18 +-- SleepHunter/Models/Spellbook.cs | 1 + SleepHunter/Resources/Versions.xml | 130 ++++++++++++++++++ SleepHunter/Settings/ClientVersionManager.cs | 34 +++++ SleepHunter/Views/MainWindow.xaml.cs | 32 +---- SleepHunter/Win32/NativeMethods.cs | 57 ++++---- 13 files changed, 274 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a541b62..4f0beaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Value` in `ClientVersion`, as it was never used - `Hash` in `ClientVersion`, now using signature-based detection instead +### Fixed + +- Parsing of skills/spell names with no level text + ## [4.7.0] - 2023-06-16 ### Added diff --git a/SleepHunter/IO/Process/ProcessMemoryAccessor.cs b/SleepHunter/IO/Process/ProcessMemoryAccessor.cs index 4a9c7fe..83f6036 100644 --- a/SleepHunter/IO/Process/ProcessMemoryAccessor.cs +++ b/SleepHunter/IO/Process/ProcessMemoryAccessor.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using System.IO; +using System.Runtime.InteropServices; using SleepHunter.Win32; @@ -25,13 +26,19 @@ public ProcessMemoryAccessor(int processId, ProcessAccess access = ProcessAccess processHandle = NativeMethods.OpenProcess(access.ToWin32Flags(), false, processId); if (processHandle == 0) - throw new Win32Exception(); + throw new Win32Exception(Marshal.GetLastPInvokeError(), $"Unable to open process {processId}: {Marshal.GetLastPInvokeErrorMessage()}"); } - public Stream GetStream() => new ProcessMemoryStream(processHandle, ProcessAccess.Read, leaveOpen: true); + public Stream GetStream() + { + CheckIfDisposed(); + return new ProcessMemoryStream(processHandle, ProcessAccess.Read, leaveOpen: true); + } public Stream GetWriteableStream() { + CheckIfDisposed(); + if (!access.HasFlag(ProcessAccess.Write)) throw new InvalidOperationException("Accessor is not writeable"); @@ -46,7 +53,7 @@ public void Dispose() GC.SuppressFinalize(this); } - void Dispose(bool isDisposing) + private void Dispose(bool isDisposing) { if (isDisposed) return; @@ -62,5 +69,11 @@ void Dispose(bool isDisposing) processHandle = 0; isDisposed = true; } + + private void CheckIfDisposed() + { + if (isDisposed) + throw new ObjectDisposedException(GetType().Name); + } } } diff --git a/SleepHunter/IO/Process/ProcessMemoryStream.cs b/SleepHunter/IO/Process/ProcessMemoryStream.cs index fe95771..c9793c4 100644 --- a/SleepHunter/IO/Process/ProcessMemoryStream.cs +++ b/SleepHunter/IO/Process/ProcessMemoryStream.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using System.IO; +using System.Runtime.InteropServices; using SleepHunter.Win32; @@ -11,8 +12,8 @@ internal sealed class ProcessMemoryStream : Stream private bool isDisposed; private nint processHandle; private readonly ProcessAccess access; - private long position = 0x400000; - private byte[] internalBuffer = new byte[0x100]; + private long position = 0x40_0000; + private byte[] internalBuffer = new byte[256]; private readonly bool leaveOpen; public override bool CanRead => processHandle != 0 && access.HasFlag(ProcessAccess.Read); @@ -62,25 +63,27 @@ public override int Read(byte[] buffer, int offset, int count) CheckIfDisposed(); CheckBufferSize(count); - bool success = NativeMethods.ReadProcessMemory(processHandle, (nint)position, internalBuffer, (nint)count, out var numberOfBytesRead); + var readPosition = position; + bool success = NativeMethods.ReadProcessMemory(processHandle, (nint)position, internalBuffer, count, out var numberOfBytesRead); if (!success || numberOfBytesRead != count) - throw new Win32Exception(); + throw new Win32Exception(Marshal.GetLastPInvokeError(), $"Unable to read process memory at 0x{readPosition:X}: {Marshal.GetLastPInvokeErrorMessage()}"); position += numberOfBytesRead; Buffer.BlockCopy(internalBuffer, 0, buffer, offset, count); - return numberOfBytesRead; + return (int)numberOfBytesRead; } public override int ReadByte() { CheckIfDisposed(); + var readPosition = position; bool success = NativeMethods.ReadProcessMemory(processHandle, (nint)position, internalBuffer, 1, out var numberOfBytesRead); if (!success || numberOfBytesRead != 1) - throw new Win32Exception(); + throw new Win32Exception(Marshal.GetLastPInvokeError(), $"Unable to read process memory at 0x{readPosition:X}: {Marshal.GetLastPInvokeErrorMessage()}"); position += numberOfBytesRead; @@ -123,10 +126,11 @@ public override void Write(byte[] buffer, int offset, int count) Buffer.BlockCopy(buffer, offset, internalBuffer, 0, count); + var writePosition = position; bool success = NativeMethods.WriteProcessMemory(processHandle, (nint)position, internalBuffer, count, out var numberOfBytesWritten); if (!success || numberOfBytesWritten != count) - throw new Win32Exception(); + throw new Win32Exception(Marshal.GetLastPInvokeError(), $"Unable to write process memory at 0x{writePosition:X}: {Marshal.GetLastPInvokeErrorMessage()}"); position += numberOfBytesWritten; } @@ -137,10 +141,11 @@ public override void WriteByte(byte value) internalBuffer[0] = value; + var writePosition = position; bool success = NativeMethods.WriteProcessMemory(processHandle, (nint)position, internalBuffer, 1, out var numberOfBytesWritten); if (!success || numberOfBytesWritten != 1) - throw new Win32Exception(); + throw new Win32Exception(Marshal.GetLastPInvokeError(), $"Unable to write process memory at 0x{writePosition:X}: {Marshal.GetLastPInvokeErrorMessage()}"); position += numberOfBytesWritten; } @@ -164,13 +169,13 @@ protected override void Dispose(bool isDisposing) isDisposed = true; } - void CheckIfDisposed() + private void CheckIfDisposed() { if (isDisposed) throw new ObjectDisposedException(GetType().Name); } - void CheckBufferSize(int count, bool copyContents = false) + private void CheckBufferSize(int count, bool copyContents = false) { if (internalBuffer.Length >= count) return; diff --git a/SleepHunter/Models/Ability.cs b/SleepHunter/Models/Ability.cs index 8d69250..5c805c7 100644 --- a/SleepHunter/Models/Ability.cs +++ b/SleepHunter/Models/Ability.cs @@ -10,7 +10,8 @@ namespace SleepHunter.Models public abstract class Ability : ObservableObject { - private static readonly Regex TrimLevelRegex = new(@"^(?.*)\(Lev:(?[0-9]{1,})/(?[0-9]{1,})\)$"); + private static readonly Regex AbilityWithoutLevelRegex = new(@"^(?[ a-z0-9'_-]+)$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex AbilityWithLevelRegex = new(@"^(?[ a-z0-9'_-]+)\s*\(Lev:(?[0-9]{1,})/(?[0-9]{1,})\)$", RegexOptions.IgnoreCase| RegexOptions.Compiled); private bool isEmpty; private int slot; @@ -144,16 +145,26 @@ public static bool TryParseLevels(string skillSpellText, out string name, out in currentLevel = 0; maximumLevel = 0; - var match = TrimLevelRegex.Match(skillSpellText); - - if (!match.Success) - return false; - - name = match.Groups["name"].Value.Trim(); - _ = int.TryParse(match.Groups["current"].Value, out currentLevel); - _ = int.TryParse(match.Groups["max"].Value, out maximumLevel); - - return true; + var match = AbilityWithLevelRegex.Match(skillSpellText); + + if (match.Success) + { + name = match.Groups["name"].Value.Trim(); + _ = int.TryParse(match.Groups["current"].Value, out currentLevel); + _ = int.TryParse(match.Groups["max"].Value, out maximumLevel); + return true; + } + + match = AbilityWithoutLevelRegex.Match(skillSpellText); + if (match.Success) + { + name = match.Groups["name"].Value.Trim(); + currentLevel = 0; + maximumLevel = 0; + return true; + } + + return false; } } } diff --git a/SleepHunter/Models/Inventory.cs b/SleepHunter/Models/Inventory.cs index c782627..9c76254 100644 --- a/SleepHunter/Models/Inventory.cs +++ b/SleepHunter/Models/Inventory.cs @@ -101,6 +101,7 @@ public void Update(ProcessMemoryAccessor accessor) } reader.BaseStream.Position = inventoryPointer; + for (int i = 0; i < inventoryVariable.Count; i++) { diff --git a/SleepHunter/Models/PlayerManager.cs b/SleepHunter/Models/PlayerManager.cs index 3d46ea7..6fd7244 100644 --- a/SleepHunter/Models/PlayerManager.cs +++ b/SleepHunter/Models/PlayerManager.cs @@ -81,7 +81,10 @@ public void AddNewClient(ClientProcess process, ClientVersion version = null) var player = new Player(process) { Version = version }; player.PropertyChanged += Player_PropertyChanged; - player.Version ??= ClientVersionManager.Instance.Versions.FirstOrDefault(v => v.Key != "Auto-Detect"); + if (ClientVersionManager.TryDetectClientVersion(process.ProcessId, out var clientVersion)) + player.Version = clientVersion; + else + player.Version = ClientVersionManager.Instance.DefaultVersion; AddPlayer(player); player.Update(); diff --git a/SleepHunter/Models/PlayerStats.cs b/SleepHunter/Models/PlayerStats.cs index 7a958d4..198df3a 100644 --- a/SleepHunter/Models/PlayerStats.cs +++ b/SleepHunter/Models/PlayerStats.cs @@ -119,36 +119,36 @@ public void Update(ProcessMemoryAccessor accessor) return; } - var currentHealthVariable = version.GetVariable(CurrentHealthKey); - var maximumHealthVariable = version.GetVariable(MaximumHealthKey); - var currentManaVariable = version.GetVariable(CurrentManaKey); - var maximumManaVariable = version.GetVariable(MaximumManaKey); + var hpVariable = version.GetVariable(CurrentHealthKey); + var maxHpVariable = version.GetVariable(MaximumHealthKey); + var mpVariable = version.GetVariable(CurrentManaKey); + var maxMpVariable = version.GetVariable(MaximumManaKey); var levelVariable = version.GetVariable(LevelKey); - var abilityLevelVariable = version.GetVariable(AbilityLevelKey); + var abVariable = version.GetVariable(AbilityLevelKey); using var stream = accessor.GetStream(); using var reader = new BinaryReader(stream, Encoding.ASCII); // Current Health - if (currentHealthVariable != null && currentHealthVariable.TryReadIntegerString(reader, out var currentHealth)) + if (hpVariable != null && hpVariable.TryReadIntegerString(reader, out var currentHealth)) CurrentHealth = (int)currentHealth; else CurrentHealth = 0; // Max Health - if (maximumHealthVariable != null && maximumHealthVariable.TryReadIntegerString(reader, out var maximumHealth)) + if (maxHpVariable != null && maxHpVariable.TryReadIntegerString(reader, out var maximumHealth)) MaximumHealth = (int)maximumHealth; else MaximumHealth = 0; // Current Mana - if (currentManaVariable != null && currentManaVariable.TryReadIntegerString(reader, out var currentMana)) + if (mpVariable != null && mpVariable.TryReadIntegerString(reader, out var currentMana)) CurrentMana = (int)currentMana; else CurrentMana = 0; // Max Mana - if (maximumManaVariable != null && maximumManaVariable.TryReadIntegerString(reader, out var maximumMana)) + if (maxMpVariable != null && maxMpVariable.TryReadIntegerString(reader, out var maximumMana)) MaximumMana = (int)maximumMana; else MaximumMana = 0; @@ -160,7 +160,7 @@ public void Update(ProcessMemoryAccessor accessor) Level = 0; // Ability Level - if (abilityLevelVariable != null && abilityLevelVariable.TryReadIntegerString(reader, out var abilityLevel)) + if (abVariable != null && abVariable.TryReadIntegerString(reader, out var abilityLevel)) AbilityLevel = (int)abilityLevel; else AbilityLevel = 0; diff --git a/SleepHunter/Models/Skillbook.cs b/SleepHunter/Models/Skillbook.cs index b5752df..4a016c9 100644 --- a/SleepHunter/Models/Skillbook.cs +++ b/SleepHunter/Models/Skillbook.cs @@ -176,18 +176,18 @@ public void Update(ProcessMemoryAccessor accessor) return; } - using var stream = accessor.GetStream(); - using var reader = new BinaryReader(stream, Encoding.ASCII); + using var stream = accessor.GetStream(); + using var reader = new BinaryReader(stream, Encoding.ASCII); - var skillbookPointer = skillbookVariable.DereferenceValue(reader); + var skillbookPointer = skillbookVariable.DereferenceValue(reader); - if (skillbookPointer == 0) - { - ResetDefaults(); - return; - } + if (skillbookPointer == 0) + { + ResetDefaults(); + return; + } - reader.BaseStream.Position = skillbookPointer; + reader.BaseStream.Position = skillbookPointer; for (int i = 0; i < skillbookVariable.Count; i++) { diff --git a/SleepHunter/Models/Spellbook.cs b/SleepHunter/Models/Spellbook.cs index 9b95d39..b0ddac1 100644 --- a/SleepHunter/Models/Spellbook.cs +++ b/SleepHunter/Models/Spellbook.cs @@ -150,6 +150,7 @@ public void Update(ProcessMemoryAccessor accessor) } reader.BaseStream.Position = spellbookPointer; + bool foundFasSpiorad = false; bool foundLyliacVineyard = false; bool foundLyliacPlant = false; diff --git a/SleepHunter/Resources/Versions.xml b/SleepHunter/Resources/Versions.xml index b0bebea..b0ef42d 100644 --- a/SleepHunter/Resources/Versions.xml +++ b/SleepHunter/Resources/Versions.xml @@ -127,6 +127,136 @@ + + + Zolian 9.1.1.exe + DarkAges + 58D559 + 42F455 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SleepHunter/Settings/ClientVersionManager.cs b/SleepHunter/Settings/ClientVersionManager.cs index b5b36b2..b1fde20 100644 --- a/SleepHunter/Settings/ClientVersionManager.cs +++ b/SleepHunter/Settings/ClientVersionManager.cs @@ -3,7 +3,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; using System.Xml.Serialization; +using SleepHunter.Extensions; +using SleepHunter.IO.Process; namespace SleepHunter.Settings { @@ -132,6 +135,37 @@ public void SaveToStream(Stream stream) serializer.Serialize(stream, collection, namespaces); } + public static bool TryDetectClientVersion(int processId, out ClientVersion detectedVersion) + { + detectedVersion = null; + + using var accessor = new ProcessMemoryAccessor(processId, ProcessAccess.Read); + using var stream = accessor.GetStream(); + using var reader = new BinaryReader(stream, Encoding.ASCII, leaveOpen: true); + + foreach (var version in Instance.Versions) + { + // Skip with invalid or missing signatures + if (version.Signature == null || string.IsNullOrWhiteSpace(version.Signature.Value)) + continue; + + var signatureLength = version.Signature.Value.Length; + + // Read the signature from the process + stream.Position = version.Signature.Address; + var readValue = reader.ReadFixedString(signatureLength); + + // If signature matches the expected value, assume this client version + if (string.Equals(readValue, version.Signature.Value)) + { + detectedVersion = version; + return true; + } + } + + return false; + } + void OnVersionAdded(ClientVersion version) { if (version == null) diff --git a/SleepHunter/Views/MainWindow.xaml.cs b/SleepHunter/Views/MainWindow.xaml.cs index 850a099..189877c 100644 --- a/SleepHunter/Views/MainWindow.xaml.cs +++ b/SleepHunter/Views/MainWindow.xaml.cs @@ -129,7 +129,7 @@ private void LaunchClient() var processInformation = StartClientProcess(clientPath); - if (!TryDetectClientVersion(processInformation, out var detectedVersion)) + if (!ClientVersionManager.TryDetectClientVersion(processInformation.ProcessId, out var detectedVersion)) { logger.LogWarn("Unable to determine client version, using default version"); detectedVersion = ClientVersionManager.Instance.DefaultVersion; @@ -200,36 +200,6 @@ private ProcessInformation StartClientProcess(string clientPath) return processInformation; } - private static bool TryDetectClientVersion(ProcessInformation process, out ClientVersion detectedVersion) - { - detectedVersion = null; - - using var stream = new ProcessMemoryStream(process.ProcessHandle, ProcessAccess.Read, true); - using var reader = new BinaryReader(stream, Encoding.ASCII); - - foreach (var version in ClientVersionManager.Instance.Versions) - { - // Skip with invalid or missing signatures - if (version.Signature == null || string.IsNullOrWhiteSpace(version.Signature.Value)) - continue; - - var signatureLength = version.Signature.Value.Length; - - // Read the signature from the process - stream.Position = version.Signature.Address; - var readValue = reader.ReadFixedString(signatureLength); - - // If signature matches the expected value, assume this client version - if (string.Equals(readValue, version.Signature.Value)) - { - detectedVersion = version; - return true; - } - } - - return false; - } - private void PatchClient(ProcessInformation process, ClientVersion version) { var patchMultipleInstances = UserSettingsManager.Instance.Settings.AllowMultipleInstances; diff --git a/SleepHunter/Win32/NativeMethods.cs b/SleepHunter/Win32/NativeMethods.cs index 602620d..26ee30c 100644 --- a/SleepHunter/Win32/NativeMethods.cs +++ b/SleepHunter/Win32/NativeMethods.cs @@ -11,55 +11,55 @@ namespace SleepHunter.Win32 internal static class NativeMethods { - [DllImport("user32", EntryPoint = "EnumWindows", CharSet = CharSet.Auto)] + [DllImport("user32", EntryPoint = "EnumWindows", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool EnumWindows(EnumWindowsProc enumWindowProc, nint lParam); - [DllImport("user32", EntryPoint = "GetClassName", CharSet = CharSet.Unicode)] + [DllImport("user32", EntryPoint = "GetClassName", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern int GetClassName(nint windowHandle, StringBuilder className, int maxLength); - [DllImport("user32", EntryPoint = "GetWindowTextLength", CharSet = CharSet.Auto)] + [DllImport("user32", EntryPoint = "GetWindowTextLength", CharSet = CharSet.Auto, SetLastError = true)] internal static extern int GetWindowTextLength(nint windowHandle); - [DllImport("user32", EntryPoint = "GetWindowText", CharSet = CharSet.Unicode)] + [DllImport("user32", EntryPoint = "GetWindowText", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern int GetWindowText(nint windowHandle, StringBuilder windowText, int maxLength); - [DllImport("user32", EntryPoint = "SetWindowText", CharSet = CharSet.Auto)] + [DllImport("user32", EntryPoint = "SetWindowText", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool SetWindowText(nint windowHandle, string windowText); - [DllImport("user32", EntryPoint = "GetWindowThreadProcessId", CharSet = CharSet.Auto)] + [DllImport("user32", EntryPoint = "GetWindowThreadProcessId", CharSet = CharSet.Auto, SetLastError = true)] internal static extern int GetWindowThreadProcessId(nint windowHandle, out int processId); - [DllImport("user32", EntryPoint = "GetClientRect", CharSet = CharSet.Auto)] + [DllImport("user32", EntryPoint = "GetClientRect", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool GetClientRect(nint windowHandle, out Rect clientRectangle); - [DllImport("user32", EntryPoint = "PostMessage", CharSet = CharSet.Auto)] + [DllImport("user32", EntryPoint = "PostMessage", CharSet = CharSet.Auto, SetLastError = true)] internal static extern bool PostMessage(nint windowHandle, uint message, nuint wParam, nuint lParam); - [DllImport("user32", EntryPoint = "SendMessage", CharSet = CharSet.Auto)] + [DllImport("user32", EntryPoint = "SendMessage", CharSet = CharSet.Auto, SetLastError = true)] internal static extern nint SendMessage(nint windowHandle, uint message, nuint wParam, nuint lParam); - [DllImport("user32", EntryPoint = "VkKeyScan", CharSet = CharSet.Auto)] + [DllImport("user32", EntryPoint = "VkKeyScan", CharSet = CharSet.Auto, SetLastError = true)] internal static extern ushort VkKeyScan(char character); - [DllImport("user32", EntryPoint = "MapVirtualKey", CharSet = CharSet.Auto)] + [DllImport("user32", EntryPoint = "MapVirtualKey", CharSet = CharSet.Auto, SetLastError = true)] internal static extern uint MapVirtualKey(uint keyCode, VirtualKeyMapMode mapMode); - [DllImport("user32", EntryPoint = "SetForegroundWindow", CharSet = CharSet.Auto)] + [DllImport("user32", EntryPoint = "SetForegroundWindow", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool SetForegroundWindow(nint windowHandle); - [DllImport("user32", EntryPoint = "RegisterHotKey", CharSet = CharSet.Auto)] + [DllImport("user32", EntryPoint = "RegisterHotKey", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool RegisterHotKey(nint windowHandle, int hotkeyId, ModifierKeys modifiers, int virtualKey); - [DllImport("user32", EntryPoint = "UnregisterHotKey", CharSet = CharSet.Auto)] + [DllImport("user32", EntryPoint = "UnregisterHotKey", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool UnregisterHotKey(nint windowHandle, int hotkeyId); - [DllImport("kernel32", EntryPoint = "CreateProcess", CharSet = CharSet.Unicode)] + [DllImport("kernel32", EntryPoint = "CreateProcess", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool CreateProcess(string applicationPath, string commandLineArgs, @@ -72,45 +72,42 @@ internal static extern bool CreateProcess(string applicationPath, ref StartupInfo startupInfo, out ProcessInformation processInformation); - [DllImport("kernel32", EntryPoint = "GlobalAddAtom", CharSet = CharSet.Unicode)] + [DllImport("kernel32", EntryPoint = "GlobalAddAtom", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern ushort GlobalAddAtom(string atomName); - [DllImport("kernel32", EntryPoint = "GlobalDeleteAtom", CharSet = CharSet.Auto)] + [DllImport("kernel32", EntryPoint = "GlobalDeleteAtom", CharSet = CharSet.Auto, SetLastError = true)] internal static extern ushort GlobalDeleteAtom(ushort atom); [DllImport("kernel32", EntryPoint = "GetProcessTimes", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool GetProcessTimes(nint processHandle, out FILETIME creationTime, out FILETIME exitTime, out FILETIME kernelTIme, out FILETIME userTime); - [DllImport("kernel32", EntryPoint = "OpenProcess", CharSet = CharSet.Auto)] + [DllImport("kernel32", EntryPoint = "OpenProcess", CharSet = CharSet.Auto, SetLastError = true)] internal static extern nint OpenProcess(ProcessAccessFlags desiredAccess, bool inheritHandle, int processId); - [DllImport("kernel32", EntryPoint = "ReadProcessMemory", CharSet = CharSet.Auto)] + [DllImport("kernel32", EntryPoint = "ReadProcessMemory", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool ReadProcessMemory(nint processHandle, nint baseAddress, byte[] buffer, nint count, out int numberOfBytesRead); + internal static extern bool ReadProcessMemory(nint processHandle, nint baseAddress, byte[] buffer, int count, out nint numberOfBytesRead); - [DllImport("kernel32", EntryPoint = "WriteProcessMemory", CharSet = CharSet.Auto)] + [DllImport("kernel32", EntryPoint = "WriteProcessMemory", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] - internal static extern bool WriteProcessMemory(nint processHandle, nint baseAddress, byte[] buffer, nint count, out int numberOfBytesWritten); + internal static extern bool WriteProcessMemory(nint processHandle, nint baseAddress, byte[] buffer, int count, out nint numberOfBytesWritten); - [DllImport("kernel32", EntryPoint = "VirtualQueryEx", CharSet = CharSet.Auto)] + [DllImport("kernel32", EntryPoint = "VirtualQueryEx", CharSet = CharSet.Auto, SetLastError = true)] internal static extern nint VirtualQueryEx(nint processHandle, nint baseAddress, out MemoryBasicInformation memoryInformation, nint size); - [DllImport("kernel32", EntryPoint = "ResumeThread", CharSet = CharSet.Auto)] + [DllImport("kernel32", EntryPoint = "ResumeThread", CharSet = CharSet.Auto, SetLastError = true)] internal static extern int ResumeThread(nint threadHandle); - [DllImport("kernel32", EntryPoint = "CloseHandle", CharSet = CharSet.Auto)] + [DllImport("kernel32", EntryPoint = "CloseHandle", CharSet = CharSet.Auto, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool CloseHandle(nint handle); - [DllImport("kernel32", EntryPoint = "GetLastError", CharSet = CharSet.Auto)] - internal static extern int GetLastError(); - - [DllImport("kernel32.dll")] + [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool GetPhysicallyInstalledSystemMemory(out long totalMemoryKilobytes); - [DllImport("kernel32.dll")] + [DllImport("kernel32.dll", SetLastError = true)] internal static extern void GetNativeSystemInfo(out SystemInfo systemInfo); } } From da45ffd9d17ffadf550a1227b7b546925517beb3 Mon Sep 17 00:00:00 2001 From: Erik Rogers Date: Tue, 20 Jun 2023 02:35:03 -0400 Subject: [PATCH 05/10] feat: basic inventory tab --- CHANGELOG.md | 1 + SleepHunter/Views/MainWindow.xaml | 5 +++++ SleepHunter/Views/MainWindow.xaml.cs | 10 ++++++++++ 3 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f0beaa..7a3617b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `Inventory` tab to view items - `Signature` definition for `ClientVersion`, which allows version to be detected by signature bytes instead of hash - `ExecutableName` and `WindowClassName` properties for `ClientVersion` to support other clients diff --git a/SleepHunter/Views/MainWindow.xaml b/SleepHunter/Views/MainWindow.xaml index 8d0a2e7..2d98abb 100644 --- a/SleepHunter/Views/MainWindow.xaml +++ b/SleepHunter/Views/MainWindow.xaml @@ -229,6 +229,11 @@ SelectionChanged="tabControl_SelectionChanged" Margin="-1,-1,0,0" Padding="0"> + + + + + diff --git a/SleepHunter/Views/MainWindow.xaml.cs b/SleepHunter/Views/MainWindow.xaml.cs index 189877c..8ced275 100644 --- a/SleepHunter/Views/MainWindow.xaml.cs +++ b/SleepHunter/Views/MainWindow.xaml.cs @@ -78,6 +78,7 @@ public MainWindow() LoadStaves(); CalculateLines(); + ToggleInventory(false); ToggleSkills(false); ToggleSpells(false); ToggleSpellQueue(false); @@ -347,12 +348,14 @@ private void OnPlayerPropertyChanged(object sender, PropertyChangedEventArgs e) { if (selectedPlayer == null) { + ToggleInventory(false); ToggleSkills(false); ToggleSpells(false); ToggleFlower(false); } else { + ToggleInventory(true); ToggleSkills(true); ToggleSpells(true); ToggleFlower(selectedPlayer.HasLyliacPlant, selectedPlayer.HasLyliacVineyard); @@ -1627,6 +1630,7 @@ private void clientListBox_SelectionChanged(object sender, SelectionChangedEvent selectedMacro = null; UpdateWindowTitle(); + ToggleInventory(false); ToggleSkills(false); ToggleSpells(false); ToggleFlower(); @@ -1652,6 +1656,7 @@ private void clientListBox_SelectionChanged(object sender, SelectionChangedEvent if (prevSelectedMacro == null && selectedMacro?.QueuedSpells.Count > 0) ToggleSpellQueue(true); + ToggleInventory(player.IsLoggedIn); ToggleSkills(player.IsLoggedIn); ToggleSpells(player.IsLoggedIn); ToggleFlower(player.HasLyliacPlant, player.HasLyliacVineyard); @@ -2119,6 +2124,11 @@ private void UpdateToolbarState() }, DispatcherPriority.Normal); } + private void ToggleInventory(bool show = true) + { + inventoryTab.IsEnabled = show; + } + private void ToggleSkills(bool show = true) { temuairSkillListBox.Visibility = medeniaSkillListBox.Visibility = worldSkillListBox.Visibility = (show ? Visibility.Visible : Visibility.Collapsed); From f07f5d575534196b6c6f34cf6d739837f14fff56 Mon Sep 17 00:00:00 2001 From: Erik Rogers Date: Wed, 21 Jun 2023 00:49:38 -0400 Subject: [PATCH 06/10] fix: numeric formatting --- CHANGELOG.md | 1 + SleepHunter/Converters/NumericConverter.cs | 2 +- SleepHunter/Resources/Versions.xml | 60 ++++++++++------------ 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a3617b..c8ce32d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Launched clients now detect version based on the new signature definitions - Process manager can detect other clients based on version definitions +- HP/MP formatting threshold increased to 10k for "thousands" shorthand ### Removed diff --git a/SleepHunter/Converters/NumericConverter.cs b/SleepHunter/Converters/NumericConverter.cs index 48e39ec..e6d686c 100644 --- a/SleepHunter/Converters/NumericConverter.cs +++ b/SleepHunter/Converters/NumericConverter.cs @@ -6,7 +6,7 @@ namespace SleepHunter.Converters { public sealed class NumericConverter : IValueConverter { - private const int ThousandsThreshold = 1_000; + private const int ThousandsThreshold = 10_000; private const int MillionsThreshold = 1_000_000; public object Convert(object value, Type targetType, object parameter, CultureInfo culture) diff --git a/SleepHunter/Resources/Versions.xml b/SleepHunter/Resources/Versions.xml index b0ef42d..bef9b31 100644 --- a/SleepHunter/Resources/Versions.xml +++ b/SleepHunter/Resources/Versions.xml @@ -131,74 +131,68 @@ Zolian 9.1.1.exe DarkAges - 58D559 - 42F455 + 78D559 + 62F455 - - - + + + - + - + - + - + - + - + - - - + - + - + - - - + - + - - - + - + - + @@ -209,39 +203,39 @@ - + - + - + - + - + - + - + @@ -250,7 +244,7 @@ - + From 291f5977708a6a223120bf97a95b231591a3d357 Mon Sep 17 00:00:00 2001 From: Erik Rogers Date: Wed, 21 Jun 2023 02:04:22 -0400 Subject: [PATCH 07/10] feat: inventory view --- CHANGELOG.md | 3 +- SleepHunter/App.xaml | 1 + SleepHunter/Models/Inventory.cs | 2 + SleepHunter/Models/InventoryItem.cs | 29 +++- SleepHunter/Obsidian.xaml | 54 ++++++++ SleepHunter/Settings/UserSettings.cs | 31 ++++- .../Templates/InventoryItemDataTemplate.xaml | 131 ++++++++++++++++++ SleepHunter/Views/MainWindow.xaml | 12 +- SleepHunter/Views/MainWindow.xaml.cs | 45 +++++- 9 files changed, 293 insertions(+), 15 deletions(-) create mode 100644 SleepHunter/Templates/InventoryItemDataTemplate.xaml diff --git a/CHANGELOG.md b/CHANGELOG.md index c8ce32d..a843d29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `Inventory` tab to view items +- `Inventory` tab to view items (just text for now) - `Signature` definition for `ClientVersion`, which allows version to be detected by signature bytes instead of hash - `ExecutableName` and `WindowClassName` properties for `ClientVersion` to support other clients ### Changed +- `UserSettings` is now version `1.6` - Launched clients now detect version based on the new signature definitions - Process manager can detect other clients based on version definitions - HP/MP formatting threshold increased to 10k for "thousands" shorthand diff --git a/SleepHunter/App.xaml b/SleepHunter/App.xaml index fe10862..4e5ca85 100644 --- a/SleepHunter/App.xaml +++ b/SleepHunter/App.xaml @@ -36,6 +36,7 @@ + diff --git a/SleepHunter/Models/Inventory.cs b/SleepHunter/Models/Inventory.cs index 9c76254..62f38de 100644 --- a/SleepHunter/Models/Inventory.cs +++ b/SleepHunter/Models/Inventory.cs @@ -24,6 +24,8 @@ public sealed class Inventory : IEnumerable public int Count => inventory.Count((item) => { return !item.IsEmpty; }); + public IEnumerable AllItems => inventory; + public IEnumerable ItemNames => from i in inventory where !i.IsEmpty && !string.IsNullOrWhiteSpace(i.Name) select i.Name; diff --git a/SleepHunter/Models/InventoryItem.cs b/SleepHunter/Models/InventoryItem.cs index 3b1d1b3..99dabac 100644 --- a/SleepHunter/Models/InventoryItem.cs +++ b/SleepHunter/Models/InventoryItem.cs @@ -1,13 +1,19 @@ -using SleepHunter.Common; +using System.Text.RegularExpressions; +using System.Windows.Media; +using SleepHunter.Common; namespace SleepHunter.Models { public sealed class InventoryItem : ObservableObject { + private static readonly Regex ColorTextRegex = new(@"{=[a-z]", RegexOptions.IgnoreCase | RegexOptions.Compiled); + private bool isEmpty; private int slot; private int iconIndex; private string name; + private int quantity; + private ImageSource icon; public bool IsEmpty { @@ -30,22 +36,37 @@ public int IconIndex public string Name { get => name; - set => SetProperty(ref name, value); + set => SetProperty(ref name, value, nameof(Name), (_) => RaisePropertyChanged(nameof(DisplayName))); + } + + public string DisplayName => ColorTextRegex.Replace(Name, string.Empty); + + public int Quantity + { + get => quantity; + set => SetProperty(ref quantity, value); + } + + public ImageSource Icon + { + get => icon; + set => SetProperty(ref icon, value); } private InventoryItem() { } - public InventoryItem(int slot, string name, int iconIndex = 0) + public InventoryItem(int slot, string name, int iconIndex = 0, int quantity = 1) { this.slot = slot; this.name = name; this.iconIndex = iconIndex; + this.quantity = quantity; isEmpty = false; } public override string ToString() => Name ?? "Unknown Item"; - public static InventoryItem MakeEmpty(int slot) => new() { Slot = slot, IsEmpty = true }; + public static InventoryItem MakeEmpty(int slot) => new() { Slot = slot, IsEmpty = true, Quantity = 0 }; } } diff --git a/SleepHunter/Obsidian.xaml b/SleepHunter/Obsidian.xaml index 25b9d85..fedb957 100644 --- a/SleepHunter/Obsidian.xaml +++ b/SleepHunter/Obsidian.xaml @@ -1287,6 +1287,60 @@ + + + + + + + + diff --git a/SleepHunter/Views/MainWindow.xaml.cs b/SleepHunter/Views/MainWindow.xaml.cs index 8ced275..20262c7 100644 --- a/SleepHunter/Views/MainWindow.xaml.cs +++ b/SleepHunter/Views/MainWindow.xaml.cs @@ -66,7 +66,7 @@ public MainWindow() LoadThemes(); LoadSettings(); ApplyTheme(); - UpdateSkillSpellGridWidths(); + UpdateListBoxGridWidths(); LoadVersions(); @@ -533,6 +533,15 @@ private void SelectNextAvailablePlayer() } } + private void RefreshInventory() + { + if (!CheckAccess()) + { + Dispatcher.InvokeIfRequired(RefreshSpellQueue, DispatcherPriority.DataBind); + return; + } + } + private void RefreshSpellQueue() { if (!CheckAccess()) @@ -941,16 +950,29 @@ private void ActivateHotkey(Key key, ModifierKeys modifiers) } } - private void UpdateSkillSpellGridWidths() + private void UpdateListBoxGridWidths() { var settings = UserSettingsManager.Instance.Settings; + SetInventoryGridWidth(settings.InventoryGridWidth); SetSkillGridWidth(settings.SkillGridWidth); SetWorldSkillGridWidth(settings.WorldSkillGridWidth); SetSpellGridWidth(settings.SpellGridWidth); SetWorldSpellGridWidth(settings.WorldSpellGridWidth); } + private void SetInventoryGridWidth(int units) + { + if (units < 1) + { + inventoryListBox.MaxWidth = double.PositiveInfinity; + return; + } + + var iconSize = UserSettingsManager.Instance.Settings.InventoryIconSize; + inventoryListBox.MaxWidth = ((iconSize + IconPadding) * units) + 6; + } + private void SetSkillGridWidth(int units) { if (units < 1) @@ -1663,6 +1685,8 @@ private void clientListBox_SelectionChanged(object sender, SelectionChangedEvent if (selectedMacro != null) { + RefreshInventory(); + spellQueueRotationComboBox.SelectedValue = selectedMacro.SpellQueueRotation; spellQueueListBox.ItemsSource = selectedMacro.QueuedSpells; @@ -1856,6 +1880,15 @@ private void TabSelected(TabItem tab) ToggleFlower(selectedMacro.Client.HasLyliacPlant, selectedMacro.Client.HasLyliacVineyard); } + private void inventoryListBox_ItemDoubleClick(object sender, MouseButtonEventArgs e) + { + // Only handle left-click + if (e.ChangedButton != MouseButton.Left) + return; + + // Do nothing for now + } + private void skillListBox_ItemDoubleClick(object sender, MouseButtonEventArgs e) { // Only handle left-click @@ -2029,6 +2062,9 @@ private void UserSettings_PropertyChanged(object sender, PropertyChangedEventArg UpdateClientList(); } + if (string.Equals(nameof(settings.InventoryGridWidth), e.PropertyName, StringComparison.OrdinalIgnoreCase)) + SetInventoryGridWidth(settings.InventoryGridWidth); + if (string.Equals(nameof(settings.SkillGridWidth), e.PropertyName, StringComparison.OrdinalIgnoreCase)) SetSkillGridWidth(settings.SkillGridWidth); @@ -2041,8 +2077,11 @@ private void UserSettings_PropertyChanged(object sender, PropertyChangedEventArg if (string.Equals(nameof(settings.WorldSpellGridWidth), e.PropertyName, StringComparison.OrdinalIgnoreCase)) SetWorldSpellGridWidth(settings.WorldSpellGridWidth); + if (string.Equals(nameof(settings.InventoryIconSize), e.PropertyName, StringComparison.OrdinalIgnoreCase)) + UpdateListBoxGridWidths(); + if (string.Equals(nameof(settings.SkillIconSize), e.PropertyName, StringComparison.OrdinalIgnoreCase)) - UpdateSkillSpellGridWidths(); + UpdateListBoxGridWidths(); // Debug settings From d687d025a832d7a4068850b1aa5aa2e49c4d22e4 Mon Sep 17 00:00:00 2001 From: Erik Rogers Date: Wed, 21 Jun 2023 02:17:41 -0400 Subject: [PATCH 08/10] feat: add inventory ui options --- SleepHunter/Views/SettingsWindow.xaml | 86 ++++++++++++++++++++++----- 1 file changed, 70 insertions(+), 16 deletions(-) diff --git a/SleepHunter/Views/SettingsWindow.xaml b/SleepHunter/Views/SettingsWindow.xaml index 50f6e97..a75c621 100644 --- a/SleepHunter/Views/SettingsWindow.xaml +++ b/SleepHunter/Views/SettingsWindow.xaml @@ -6,7 +6,7 @@ xmlns:ctl="clr-namespace:SleepHunter.Controls" xmlns:settings="clr-namespace:SleepHunter.Settings" Title="Settings" - Width="640" Height="540" + Width="720" Height="620" ResizeMode="NoResize" Style="{StaticResource ObsidianWindow}" WindowStartupLocation="CenterOwner"> @@ -159,6 +159,9 @@ + + + @@ -219,16 +222,67 @@ Style="{StaticResource ObsidianSeparator}" Margin="4,8"/> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - @@ -394,7 +448,7 @@ - From 20cc0ebec7dd996f70a724949247177c08cd0bc0 Mon Sep 17 00:00:00 2001 From: Erik Rogers Date: Wed, 21 Jun 2023 02:23:50 -0400 Subject: [PATCH 09/10] chore: docs and version --- CHANGELOG.md | 2 ++ SleepHunter/SleepHunter.csproj | 4 ++-- docs/src/SUMMARY.md | 1 + docs/src/main-window/inventory.md | 16 ++++++++++++++++ docs/src/settings/game-client.md | 9 +-------- 5 files changed, 22 insertions(+), 10 deletions(-) create mode 100644 docs/src/main-window/inventory.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a843d29..fbca79e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `Inventory` tab to view items (just text for now) +- Inventory display options under `User Interface` settings - `Signature` definition for `ClientVersion`, which allows version to be detected by signature bytes instead of hash - `ExecutableName` and `WindowClassName` properties for `ClientVersion` to support other clients @@ -18,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Launched clients now detect version based on the new signature definitions - Process manager can detect other clients based on version definitions - HP/MP formatting threshold increased to 10k for "thousands" shorthand +- `User Settings` dialog is now larger ### Removed diff --git a/SleepHunter/SleepHunter.csproj b/SleepHunter/SleepHunter.csproj index 3d7e815..56a8170 100644 --- a/SleepHunter/SleepHunter.csproj +++ b/SleepHunter/SleepHunter.csproj @@ -18,11 +18,11 @@ git https://github.com/ewrogers/SleepHunter4 https://github.com/ewrogers/SleepHunter4 - 4.7.0.0 2023 Erik 'SiLo' Rogers Dark Ages Automation Tool SleepHunter - 4.7.0.0 + 4.8.0.0 + 4.8.0.0 x64 diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index d57201a..20c4ecc 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -14,6 +14,7 @@ - [Toolbar](./main-window/toolbar.md) - [Character List](./main-window/character-list.md) +- [Inventory Tab](./main-window/inventory.md) - [Skills Tab](./main-window/skills-tab.md) - [Spells Tab](./main-window/spells-tab.md) - [Flowering Tab](./main-window/flowering-tab.md) diff --git a/docs/src/main-window/inventory.md b/docs/src/main-window/inventory.md new file mode 100644 index 0000000..07c12e7 --- /dev/null +++ b/docs/src/main-window/inventory.md @@ -0,0 +1,16 @@ +# Inventory Tab + +![image](../screenshots/skills-tab.png) + +Inventory items are arranged as a grid of icon buttons similar to the game client, displaying the name of the item. + +## Grid Layout + +By default, items are arranged with 12 items per row. +You can change the number of items displayed per row in the [Settings](../settings.md) window. + +The character's gold will be displayed in the last slot. + +## Tooltip Help + +You can mouse over an item to see the tooltip for that item. diff --git a/docs/src/settings/game-client.md b/docs/src/settings/game-client.md index 66f41b3..08a2161 100644 --- a/docs/src/settings/game-client.md +++ b/docs/src/settings/game-client.md @@ -12,17 +12,10 @@ It is also used to determine the path to the Dark Ages game client data files fo ## Client Version This setting determines the version of the Dark Ages game client and how any runtime patches should be applied. -The default is `Auto-Detect` and will automatically detect the version of the Dark Ages game client based on the executable MD5 hash. +The default is `Auto-Detect` and will automatically detect the version of the Dark Ages game client based on the client signature. You should not change this unless you are using a custom Dark Ages game client. -## DirectDraw Compatibility Fix - -This setting will copy a patched `ddraw.dll` from the [DDrawCompat](https://github.com/narzoul/DDrawCompat) repo into the client folder. -By default this is `Enabled` for x86/x64 operating systems. - -This fixes the flickering mouse cursor issue on modern computers. - ## Allow Multiple Instances This setting determines when the "multiple instances" patch should be applied when the Dark Ages game client is started. From 7bed9f9b722375784ae56a46ea860efd2e75a75f Mon Sep 17 00:00:00 2001 From: Erik Rogers Date: Wed, 21 Jun 2023 02:27:04 -0400 Subject: [PATCH 10/10] chore: release info --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbca79e..3824963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,15 @@ 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.8.0] - Unreleased +## [4.8.0] - 2023-06-21 ### Added -- `Inventory` tab to view items (just text for now) -- Inventory display options under `User Interface` settings +- `Inventory` tab to view items (names only for now) +- Inventory grid display options under `User Interface` settings - `Signature` definition for `ClientVersion`, which allows version to be detected by signature bytes instead of hash - `ExecutableName` and `WindowClassName` properties for `ClientVersion` to support other clients +- Client version for `Zolian 9.1.1` memory offsets ([Zolian Server](https://www.thebucknetwork.com/Zolian)) ### Changed