Skip to content

Commit

Permalink
Merge pull request #25 from ewrogers/client-version
Browse files Browse the repository at this point in the history
Client version
  • Loading branch information
ewrogers authored Jun 21, 2023
2 parents 77bacef + 7bed9f9 commit ab501cb
Show file tree
Hide file tree
Showing 28 changed files with 815 additions and 327 deletions.
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,33 @@ 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] - 2023-06-21

### Added

- `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

- `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
- `User Settings` dialog is now larger

### Removed

- `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
Expand Down
1 change: 1 addition & 0 deletions SleepHunter/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<ResourceDictionary Source="Templates\ClientVersionDataTemplate.xaml"/>
<ResourceDictionary Source="Templates\ColorThemeDataTemplate.xaml"/>
<ResourceDictionary Source="Templates\FlowerQueueDataTemplate.xaml"/>
<ResourceDictionary Source="Templates\InventoryItemDataTemplate.xaml"/>
<ResourceDictionary Source="Templates\MetadataDataTemplates.xaml"/>
<ResourceDictionary Source="Templates\PlayerDataTemplate.xaml"/>
<ResourceDictionary Source="Templates\SpellQueueDataTemplate.xaml"/>
Expand Down
2 changes: 1 addition & 1 deletion SleepHunter/Converters/NumericConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 11 additions & 4 deletions SleepHunter/IO/Process/ProcessManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> windowClassNames = new();
private readonly ConcurrentDictionary<int, ClientProcess> clientProcesses = new();
private readonly ConcurrentQueue<ClientProcess> deadClients = new();
private readonly ConcurrentQueue<ClientProcess> newClients = new();
Expand Down Expand Up @@ -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<ClientProcess> enumProcessCallback = null)
{
var foundClients = new Dictionary<int, ClientProcess>();
var deadClients = new Dictionary<int, ClientProcess>();
var newClients = new Dictionary<int, ClientProcess>();

var registeredClassNames = windowClassNames.Keys.ToList();

NativeMethods.EnumWindows((windowHandle, lParam) =>
{
// Get Process & Thread Id
Expand All @@ -87,8 +94,8 @@ public void ScanForProcesses(Action<ClientProcess> 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
Expand Down
19 changes: 16 additions & 3 deletions SleepHunter/IO/Process/ProcessMemoryAccessor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;

using SleepHunter.Win32;

Expand All @@ -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");

Expand All @@ -46,7 +53,7 @@ public void Dispose()
GC.SuppressFinalize(this);
}

void Dispose(bool isDisposing)
private void Dispose(bool isDisposing)
{
if (isDisposed)
return;
Expand All @@ -62,5 +69,11 @@ void Dispose(bool isDisposing)
processHandle = 0;
isDisposed = true;
}

private void CheckIfDisposed()
{
if (isDisposed)
throw new ObjectDisposedException(GetType().Name);
}
}
}
25 changes: 15 additions & 10 deletions SleepHunter/IO/Process/ProcessMemoryStream.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;

using SleepHunter.Win32;

Expand All @@ -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);
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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;
Expand Down
33 changes: 22 additions & 11 deletions SleepHunter/Models/Ability.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ namespace SleepHunter.Models

public abstract class Ability : ObservableObject
{
private static readonly Regex TrimLevelRegex = new(@"^(?<name>.*)\(Lev:(?<current>[0-9]{1,})/(?<max>[0-9]{1,})\)$");
private static readonly Regex AbilityWithoutLevelRegex = new(@"^(?<name>[ a-z0-9'_-]+)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex AbilityWithLevelRegex = new(@"^(?<name>[ a-z0-9'_-]+)\s*\(Lev:(?<current>[0-9]{1,})/(?<max>[0-9]{1,})\)$", RegexOptions.IgnoreCase| RegexOptions.Compiled);

private bool isEmpty;
private int slot;
Expand Down Expand Up @@ -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;
}
}
}
3 changes: 3 additions & 0 deletions SleepHunter/Models/Inventory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public sealed class Inventory : IEnumerable<InventoryItem>

public int Count => inventory.Count((item) => { return !item.IsEmpty; });

public IEnumerable<InventoryItem> AllItems => inventory;

public IEnumerable<string> ItemNames =>
from i in inventory where !i.IsEmpty && !string.IsNullOrWhiteSpace(i.Name) select i.Name;

Expand Down Expand Up @@ -101,6 +103,7 @@ public void Update(ProcessMemoryAccessor accessor)
}

reader.BaseStream.Position = inventoryPointer;


for (int i = 0; i < inventoryVariable.Count; i++)
{
Expand Down
29 changes: 25 additions & 4 deletions SleepHunter/Models/InventoryItem.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -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 };
}
}
5 changes: 4 additions & 1 deletion SleepHunter/Models/PlayerManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading

0 comments on commit ab501cb

Please sign in to comment.