From d2005e293073bf57082144e6217f7c4aa0814f47 Mon Sep 17 00:00:00 2001 From: Alexandru Macocian Date: Sat, 28 Oct 2023 14:14:57 +0200 Subject: [PATCH 1/2] Deprecate memory reader Switch completely to GWCA plugin Fix InventoryModule item ids Fix InventoryModule modifiers FIx BloogumClient image retrieval --- Daybreak.GWCA/source/InventoryModule.cpp | 4 +- .../Configuration/Options/FocusViewOptions.cs | 4 - .../Configuration/ProjectConfiguration.cs | 9 +- Daybreak/Services/Bloogum/BloogumClient.cs | 20 +- .../Services/Scanner/CompositeMemoryReader.cs | 182 --- .../Services/Scanner/GuildwarsMemoryReader.cs | 1431 ----------------- Daybreak/Services/Scanner/IMemoryScanner.cs | 28 - Daybreak/Services/Scanner/MemoryScanner.cs | 405 ----- 8 files changed, 19 insertions(+), 2064 deletions(-) delete mode 100644 Daybreak/Services/Scanner/CompositeMemoryReader.cs delete mode 100644 Daybreak/Services/Scanner/GuildwarsMemoryReader.cs delete mode 100644 Daybreak/Services/Scanner/IMemoryScanner.cs delete mode 100644 Daybreak/Services/Scanner/MemoryScanner.cs diff --git a/Daybreak.GWCA/source/InventoryModule.cpp b/Daybreak.GWCA/source/InventoryModule.cpp index 1bcfa26d..032c078a 100644 --- a/Daybreak.GWCA/source/InventoryModule.cpp +++ b/Daybreak.GWCA/source/InventoryModule.cpp @@ -28,12 +28,12 @@ namespace Daybreak::Modules::InventoryModule { continue; } - bagContent.Id = item->item_id; + bagContent.Id = item->model_id; bagContent.Slot = item->slot; bagContent.Count = item->quantity; auto modifier = item->mod_struct; for (auto i = 0; i < item->mod_struct_size; i++) { - bagContent.Modifiers.push_back((uint32_t)modifier); + bagContent.Modifiers.push_back((uint32_t)(modifier->mod)); modifier++; } diff --git a/Daybreak/Configuration/Options/FocusViewOptions.cs b/Daybreak/Configuration/Options/FocusViewOptions.cs index 33bfddf9..d69469bf 100644 --- a/Daybreak/Configuration/Options/FocusViewOptions.cs +++ b/Daybreak/Configuration/Options/FocusViewOptions.cs @@ -11,10 +11,6 @@ public sealed class FocusViewOptions [OptionName(Name = "Enabled", Description = "If true, the focus view is enabled, showing live information from the game")] public bool Enabled { get; set; } = true; - [JsonProperty(nameof(GWCAIntegration))] - [OptionName(Name = "GWCA Integration", Description = "If true, focus view will use the GWCA plugin to read the GuildWars memory. This should be true in almost all cases")] - public bool GWCAIntegration { get; set; } = true; - [OptionName(Name = "Inventory Component Enabled", Description = "If true, the focus view will show a component with the inventory contents")] public bool InventoryComponentVisible { get; set; } diff --git a/Daybreak/Configuration/ProjectConfiguration.cs b/Daybreak/Configuration/ProjectConfiguration.cs index eb6fea29..74f280e3 100644 --- a/Daybreak/Configuration/ProjectConfiguration.cs +++ b/Daybreak/Configuration/ProjectConfiguration.cs @@ -208,6 +208,8 @@ public override void RegisterServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -219,7 +221,6 @@ public override void RegisterServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -244,12 +245,6 @@ public override void RegisterServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); - - //TODO: Composite will be the main reader. Composite will select between GuildwarsMemoryReader and GWCAMemoryReader depending on the options. Remove once GWCA is stable - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); } public override void RegisterViews(IViewProducer viewProducer) diff --git a/Daybreak/Services/Bloogum/BloogumClient.cs b/Daybreak/Services/Bloogum/BloogumClient.cs index f0ae263c..4b4ef06e 100644 --- a/Daybreak/Services/Bloogum/BloogumClient.cs +++ b/Daybreak/Services/Bloogum/BloogumClient.cs @@ -1,4 +1,5 @@ -using Daybreak.Services.Bloogum.Models; +using Daybreak.Models.Guildwars; +using Daybreak.Services.Bloogum.Models; using Daybreak.Services.Images; using Daybreak.Services.Scanner; using Microsoft.Extensions.Logging; @@ -65,14 +66,23 @@ private async Task GetImageUri(bool localized) return GetRandomScreenShot(); } - var worldInfo = await this.guildwarsMemoryCache.ReadWorldData(CancellationToken.None); - if (worldInfo is null) + WorldData? worldInfo = default; + try + { + worldInfo = await this.guildwarsMemoryCache.ReadWorldData(CancellationToken.None); + if (worldInfo is null) + { + return GetRandomScreenShot(); + } + } + catch (Exception ex) when (ex is TimeoutException or TaskCanceledException or HttpRequestException) { + this.logger.LogInformation("Could not retrieve world data. Returning random screenshot"); return GetRandomScreenShot(); } - var validLocations = Location.Locations.Where(l => l.Region == worldInfo.Region).ToList(); - var validCategories = validLocations.SelectMany(l => l.Categories).Where(c => c.Map == worldInfo.Map).ToList(); + var validLocations = Location.Locations.Where(l => l.Region == worldInfo!.Region).ToList(); + var validCategories = validLocations.SelectMany(l => l.Categories).Where(c => c.Map == worldInfo!.Map).ToList(); if (validCategories.None()) { if (validLocations.None()) diff --git a/Daybreak/Services/Scanner/CompositeMemoryReader.cs b/Daybreak/Services/Scanner/CompositeMemoryReader.cs deleted file mode 100644 index 7b86509d..00000000 --- a/Daybreak/Services/Scanner/CompositeMemoryReader.cs +++ /dev/null @@ -1,182 +0,0 @@ -using Daybreak.Configuration.Options; -using Daybreak.Models.Guildwars; -using Microsoft.Extensions.Logging; -using System; -using System.CodeDom; -using System.Configuration; -using System.Core.Extensions; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; - -namespace Daybreak.Services.Scanner; - -public sealed class CompositeMemoryReader : IGuildwarsMemoryReader -{ - private readonly GuildwarsMemoryReader guildwarsMemoryReader; - private readonly GWCAMemoryReader gWCAMemoryReader; - private readonly ILiveOptions liveOptions; - private readonly ILogger logger; - - public CompositeMemoryReader( - GuildwarsMemoryReader guildwarsMemoryReader, - GWCAMemoryReader gWCAMemoryReader, - ILiveOptions liveOptions, - ILogger logger) - { - this.guildwarsMemoryReader = guildwarsMemoryReader.ThrowIfNull(); - this.gWCAMemoryReader = gWCAMemoryReader.ThrowIfNull(); - this.liveOptions = liveOptions.ThrowIfNull(); - this.logger = logger.ThrowIfNull(); - } - - public Task EnsureInitialized(Process process, CancellationToken cancellationToken) - { - if (this.liveOptions.Value.GWCAIntegration) - { - return this.gWCAMemoryReader.EnsureInitialized(process, cancellationToken); - } - else - { - return this.guildwarsMemoryReader.EnsureInitialized(process, cancellationToken); - } - } - - public async Task GetNamedEntity(IEntity entity, CancellationToken cancellationToken) - { - if (!this.liveOptions.Value.GWCAIntegration) - { - return default; - } - - return await this.gWCAMemoryReader.GetNamedEntity(entity, cancellationToken); - } - - public Task ReadGameData(CancellationToken cancellationToken) - { - if (this.liveOptions.Value.GWCAIntegration) - { - return this.gWCAMemoryReader.ReadGameData(cancellationToken); - } - else - { - return this.guildwarsMemoryReader.ReadGameData(cancellationToken); - } - } - - public Task ReadInventoryData(CancellationToken cancellationToken) - { - if (this.liveOptions.Value.GWCAIntegration) - { - return this.gWCAMemoryReader.ReadInventoryData(cancellationToken); - } - else - { - return this.guildwarsMemoryReader.ReadInventoryData(cancellationToken); - } - } - - public Task ReadLoginData(CancellationToken cancellationToken) - { - if (this.liveOptions.Value.GWCAIntegration) - { - return this.gWCAMemoryReader.ReadLoginData(cancellationToken); - } - else - { - return this.guildwarsMemoryReader.ReadLoginData(cancellationToken); - } - } - - public Task ReadMainPlayerData(CancellationToken cancellationToken) - { - if (this.liveOptions.Value.GWCAIntegration) - { - return this.gWCAMemoryReader.ReadMainPlayerData(cancellationToken); - } - else - { - return this.guildwarsMemoryReader.ReadMainPlayerData(cancellationToken); - } - } - - public Task ReadPathingData(CancellationToken cancellationToken) - { - if (this.liveOptions.Value.GWCAIntegration) - { - return this.gWCAMemoryReader.ReadPathingData(cancellationToken); - } - else - { - return this.guildwarsMemoryReader.ReadPathingData(cancellationToken); - } - } - - public Task ReadPathingMetaData(CancellationToken cancellationToken) - { - if (this.liveOptions.Value.GWCAIntegration) - { - return this.gWCAMemoryReader.ReadPathingMetaData(cancellationToken); - } - else - { - return this.guildwarsMemoryReader.ReadPathingMetaData(cancellationToken); - } - } - - public Task ReadPreGameData(CancellationToken cancellationToken) - { - if (this.liveOptions.Value.GWCAIntegration) - { - return this.gWCAMemoryReader.ReadPreGameData(cancellationToken); - } - else - { - return this.guildwarsMemoryReader.ReadPreGameData(cancellationToken); - } - } - - public Task ReadSessionData(CancellationToken cancellationToken) - { - if (this.liveOptions.Value.GWCAIntegration) - { - return this.gWCAMemoryReader.ReadSessionData(cancellationToken); - } - else - { - return this.guildwarsMemoryReader.ReadSessionData(cancellationToken); - } - } - - public Task ReadUserData(CancellationToken cancellationToken) - { - if (this.liveOptions.Value.GWCAIntegration) - { - return this.gWCAMemoryReader.ReadUserData(cancellationToken); - } - else - { - return this.guildwarsMemoryReader.ReadUserData(cancellationToken); - } - } - - public Task ReadWorldData(CancellationToken cancellationToken) - { - if (this.liveOptions.Value.GWCAIntegration) - { - return this.gWCAMemoryReader.ReadWorldData(cancellationToken); - } - else - { - return this.guildwarsMemoryReader.ReadWorldData(cancellationToken); - } - } - - public void Stop() - { - if (!this.liveOptions.Value.GWCAIntegration) - { - this.guildwarsMemoryReader.Stop(); - } - } -} diff --git a/Daybreak/Services/Scanner/GuildwarsMemoryReader.cs b/Daybreak/Services/Scanner/GuildwarsMemoryReader.cs deleted file mode 100644 index 6a9f57cd..00000000 --- a/Daybreak/Services/Scanner/GuildwarsMemoryReader.cs +++ /dev/null @@ -1,1431 +0,0 @@ -using Daybreak.Models.Builds; -using Daybreak.Models.Guildwars; -using Daybreak.Models.Interop; -using Daybreak.Models.LaunchConfigurations; -using Daybreak.Models.Metrics; -using Daybreak.Services.ApplicationLauncher; -using Daybreak.Services.Metrics; -using Daybreak.Utils; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Core.Extensions; -using System.Diagnostics; -using System.Diagnostics.Metrics; -using System.Extensions; -using System.Globalization; -using System.Linq; -using System.Logging; -using System.Net; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; - -namespace Daybreak.Services.Scanner; - -public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader -{ - private const int MaxTrapezoidCount = 1000000; - private const int RetryInitializationCount = 5; - private const string LatencyMeterName = "Memory Reader Latency"; - private const string LatencyMeterUnitsName = "Milliseconds"; - private const string LatencyMeterDescription = "Amount of milliseconds elapsed while reading memory. P95 aggregation"; - - private readonly IMemoryScanner memoryScanner; - private readonly Histogram latencyMeter; - private readonly ILogger logger; - - private uint playerIdPointer; - private uint entityArrayPointer; - private uint targetIdPointer; - private uint instanceInfoPointer; - private uint preGameContextPointer; - - public GuildwarsMemoryReader( - IMemoryScanner memoryScanner, - IMetricsService metricsService, - ILogger logger) - { - this.memoryScanner = memoryScanner.ThrowIfNull(); - this.latencyMeter = metricsService.ThrowIfNull().CreateHistogram(LatencyMeterName, LatencyMeterUnitsName, LatencyMeterDescription, AggregationTypes.P95); - this.logger = logger.ThrowIfNull(); - } - - public async Task EnsureInitialized(Process process, CancellationToken cancellationToken) - { - var scoppedLogger = this.logger.CreateScopedLogger(nameof(this.EnsureInitialized), default!); - var currentGuildwarsProcess = process.ThrowIfNull(); - if (currentGuildwarsProcess is null) - { - scoppedLogger.LogWarning($"Process is null. {nameof(GuildwarsMemoryReader)} will not start"); - return; - } - - try - { - if (cancellationToken.IsCancellationRequested) - { - throw new TaskCanceledException(); - } - - await this.InitializeSafe(currentGuildwarsProcess, scoppedLogger); - } - catch(Exception e) - { - scoppedLogger.LogError(e, "Encountered exception during initialization"); - } - } - - public void Stop() - { - var scoppedLogger = this.logger.CreateScopedLogger(nameof(this.Stop), default!); - scoppedLogger.LogInformation($"Stopping {nameof(GuildwarsMemoryReader)}"); - this.memoryScanner?.EndScanner(); - } - - public Task ReadLoginData(CancellationToken cancellationToken) - { - if (this.memoryScanner.Scanning is false) - { - return Task.FromResult(default); - } - - return Task.Run(() => this.SafeReadGameMemory(this.ReadLoginDataInternal), cancellationToken); - } - - public Task ReadGameData(CancellationToken cancellationToken) - { - if (this.memoryScanner.Scanning is false) - { - return Task.FromResult(default); - } - - return Task.Run(() => this.SafeReadGameMemory(this.ReadGameDataInternal), cancellationToken); - } - - public Task ReadPathingData(CancellationToken cancellationToken) - { - if (this.memoryScanner.Scanning is false) - { - return Task.FromResult(default); - } - - return Task.Run(() => this.SafeReadGameMemory(this.ReadPathingDataInternal), cancellationToken); - } - - public Task ReadPathingMetaData(CancellationToken cancellationToken) - { - if (this.memoryScanner.Scanning is false) - { - return Task.FromResult(default); - } - - return Task.Run(() => this.SafeReadGameMemory(this.ReadPathingMetaDataInternal), cancellationToken); - } - - public Task ReadInventoryData(CancellationToken cancellationToken) - { - if (this.memoryScanner.Scanning is false) - { - return Task.FromResult(default); - } - - return Task.Run(() => this.SafeReadGameMemory(this.ReadInventoryDataInternal), cancellationToken); - } - - public Task ReadWorldData(CancellationToken cancellationToken) - { - if (this.memoryScanner.Scanning is false) - { - return Task.FromResult(default); - } - - return Task.Run(() => this.SafeReadGameMemory(this.ReadWorldDataInternal), cancellationToken); - } - - public Task ReadUserData(CancellationToken cancellationToken) - { - if (this.memoryScanner.Scanning is false) - { - return Task.FromResult(default); - } - - return Task.Run(() => this.SafeReadGameMemory(this.ReadUserDataInternal), cancellationToken); - } - - public Task ReadSessionData(CancellationToken cancellationToken) - { - if (this.memoryScanner.Scanning is false) - { - return Task.FromResult(default); - } - - return Task.Run(() => this.SafeReadGameMemory(this.ReadSessionDataInternal), cancellationToken); - } - - public Task ReadMainPlayerData(CancellationToken cancellationToken) - { - if (this.memoryScanner.Scanning is false) - { - return Task.FromResult(default); - } - - return Task.Run(() => this.SafeReadGameMemory(this.ReadMainPlayerDataInternal), cancellationToken); - } - - public Task ReadConnectionData(CancellationToken cancellationToken) - { - if (this.memoryScanner.Scanning is false) - { - return Task.FromResult(default); - } - - return Task.Run(() => this.SafeReadGameMemory(this.ReadConnectionData), cancellationToken); - } - - public Task ReadPreGameData(CancellationToken cancellationToken) - { - if (this.memoryScanner.Scanning is false) - { - return Task.FromResult(default); - } - - return Task.Run(() => this.SafeReadGameMemory(this.ReadPreGameData), cancellationToken); - } - - public Task GetNamedEntity(IEntity entity, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - private async Task InitializeSafe(Process process, ScopedLogger scopedLogger) - { - if (this.memoryScanner.Process is null || - this.memoryScanner.Process.HasExited || - this.memoryScanner.Process.MainModule?.FileName != process?.MainModule?.FileName) - { - if (this.memoryScanner.Scanning) - { - scopedLogger.LogInformation("Scanner is already scanning a different process. Restart scanner and target the new process"); - this.memoryScanner.EndScanner(); - } - - scopedLogger.LogInformation($"Initializing {nameof(GuildwarsMemoryReader)}"); - await this.ResilientBeginScanner(scopedLogger, process!); - } - - if (this.memoryScanner.Scanning is false && - process?.HasExited is false) - { - scopedLogger.LogInformation($"Initializing {nameof(GuildwarsMemoryReader)}"); - await this.ResilientBeginScanner(scopedLogger, process!); - } - } - - private async Task ResilientBeginScanner(ScopedLogger scopedLogger, Process process) - { - for (var i = 0; i < RetryInitializationCount; i++) - { - try - { - scopedLogger.LogInformation("Initializing scanner"); - this.memoryScanner.BeginScanner(process!); - break; - } - catch (Exception e) - { - scopedLogger.LogError(e, "Error during initialization"); - await Task.Delay(1000); - } - } - } - - private T? SafeReadGameMemory(Func readingFunction) - { - try - { - var stopWatch = Stopwatch.StartNew(); - var ret = readingFunction(); - stopWatch.Stop(); - this.latencyMeter.Record(stopWatch.Elapsed.TotalMilliseconds); - - return ret; - } - catch(Exception e) - { - this.logger.LogError(e, "Exception encountered when reading game memory"); - return default; - } - } - - private LoginData? ReadLoginDataInternal() - { - var globalContext = this.GetGlobalContext(); - if (!globalContext.GameContext.IsValid() || - !globalContext.UserContext.IsValid() || - !globalContext.InstanceContext.IsValid()) - { - return default; - } - - var userContext = this.memoryScanner.Read(globalContext.UserContext, UserContext.BaseOffset); - var loginData = new LoginData - { - Email = userContext.PlayerEmailFirstChar + (userContext.PlayerEmailSecondChar + userContext.PlayerEmailRemaining), - PlayerName = userContext.PlayerName - }; - - if (!IsValidEmail(loginData.Email)) - { - return default; - } - - return loginData; - } - - private GameData? ReadGameDataInternal() - { - var globalContext = this.GetGlobalContext(); - if (!globalContext.GameContext.IsValid() || - !globalContext.UserContext.IsValid() || - !globalContext.InstanceContext.IsValid()) - { - return default; - } - - // GameContext struct is offset by 0x07C due to the memory layout of the structure. - var gameContext = this.memoryScanner.Read(globalContext.GameContext, GameContext.BaseOffset); - // InstanceContext struct is offset by 0x01AC due to memory layout of the structure. - var instanceContext = this.memoryScanner.Read(globalContext.InstanceContext, InstanceContext.BaseOffset); - - if (gameContext.MapEntities.Size > 10000 || - gameContext.Professions.Size > 10000 || - gameContext.Players.Size > 1000 || - gameContext.QuestLog.Size > 10000 || - gameContext.Skillbars.Size > 100 || - gameContext.PartyAttributes.Size > 1000 || - gameContext.Titles.Size > 1000 || - gameContext.TitlesTiers.Size > 10000) - { - return default; - } - - var mapEntities = this.memoryScanner.ReadArray(gameContext.MapEntities); - var mapIcons = this.memoryScanner.ReadArray(gameContext.MissionMapIcons); - var professions = this.memoryScanner.ReadArray(gameContext.Professions); - var players = this.memoryScanner.ReadArray(gameContext.Players); - var quests = this.memoryScanner.ReadArray(gameContext.QuestLog); - var skills = this.memoryScanner.ReadArray(gameContext.Skillbars); - var partyAttributes = this.memoryScanner.ReadArray(gameContext.PartyAttributes); - var playerEntityId = this.memoryScanner.ReadPtrChain(this.GetPlayerIdPointer(), 0x0, 0x0); - var targetEntityId = this.memoryScanner.ReadPtrChain(this.GetTargetIdPointer(), 0x0, 0x0); - var titles = this.memoryScanner.ReadArray(gameContext.Titles); - var titleTiers = this.memoryScanner.ReadArray(gameContext.TitlesTiers); - var npcs = this.memoryScanner.ReadArray(gameContext.Npcs); - - // The following lines would retrieve all entities, including item entities. - var entityPointersArray = this.memoryScanner.ReadPtrChain(this.GetEntityArrayPointer(), 0x0, 0x0); - if (entityPointersArray.Size > 10000) - { - return new GameData { Valid = false }; - } - - var entityPointers = this.memoryScanner.ReadArray(entityPointersArray.Buffer, entityPointersArray.Size); - var entities = entityPointers.Select(ptr => (ptr, this.memoryScanner.Read(ptr))).ToArray(); - - return this.AggregateGameData( - gameContext, - instanceContext, - mapEntities, - entities.Select(e => e.Item2).ToArray(), - players, - professions, - quests, - skills, - partyAttributes, - titles, - titleTiers, - mapIcons, - npcs, - playerEntityId, - targetEntityId); - } - - private PathingData? ReadPathingDataInternal() - { - var globalContext = this.GetGlobalContext(); - var mapContext = this.memoryScanner.Read(globalContext.MapContext); - - var pathingMapContext = this.memoryScanner.ReadPtrChain(mapContext.PathingMapContextPtr, 0x0, 0x0); - if (!pathingMapContext.PathingMapArray.IsValidArray(true) || - pathingMapContext.PathingMapArray.Size > 10000) - { - return default; - } - - var pathingMaps = this.memoryScanner.ReadArray(pathingMapContext.PathingMapArray); - var trapezoidsCount = (int)pathingMaps.Select(p => p.TrapezoidCount).Sum(count => count); - if (trapezoidsCount > MaxTrapezoidCount) - { - return default; - } - - var trapezoidList = new List(); - var adjacencyList = new List>(); - var ogPathingMapList = new List>(); - for (var pathingMapIndex = 0; pathingMapIndex < pathingMaps.Length; pathingMapIndex++) - { - var pathingMap = pathingMaps[pathingMapIndex]; - var pathingMapList = new List(); - ogPathingMapList.Add(pathingMapList); - var pathingTrapezoids = this.memoryScanner.ReadArray(pathingMap.TrapezoidArray, pathingMap.TrapezoidCount); - foreach(var trapezoid in pathingTrapezoids) - { - pathingMapList.Add((int)trapezoid.Id); - trapezoidList.Add(new Trapezoid - { - Id = (int)trapezoid.Id, - PathingMapId = pathingMapIndex, - XTL = trapezoid.XTL, - XTR = trapezoid.XTR, - YT = trapezoid.YT, - XBL = trapezoid.XBL, - XBR = trapezoid.XBR, - YB = trapezoid.YB, - }); - - var trapezoidAdjacencyList = new List(); - adjacencyList.Add(trapezoidAdjacencyList); - foreach(var adjacentAddress in new uint[] - { - trapezoid.AdjacentPathingTrapezoid1, - trapezoid.AdjacentPathingTrapezoid2, - trapezoid.AdjacentPathingTrapezoid3, - trapezoid.AdjacentPathingTrapezoid4 - }) - { - if (adjacentAddress == 0) - { - continue; - } - - var adjacentTrapezoid = this.memoryScanner.Read(adjacentAddress); - trapezoidAdjacencyList.Add((int)adjacentTrapezoid.Id); - } - } - } - - var computedPathingMaps = BuildPathingMaps(trapezoidList, adjacencyList); - var computedAdjacencyList = BuildFinalAdjacencyList(trapezoidList, computedPathingMaps, adjacencyList); - - return new PathingData - { - Trapezoids = trapezoidList, - OriginalAdjacencyList = adjacencyList, - ComputedPathingMaps = computedPathingMaps, - OriginalPathingMaps = ogPathingMapList, - ComputedAdjacencyList = computedAdjacencyList - }; - } - - private PathingMetadata? ReadPathingMetaDataInternal() - { - var globalContext = this.GetGlobalContext(); - var mapContext = this.memoryScanner.Read(globalContext.MapContext); - - var pathingMapContext = this.memoryScanner.ReadPtrChain(mapContext.PathingMapContextPtr, 0x0, 0x0); - if (!pathingMapContext.PathingMapArray.IsValidArray(true) || - pathingMapContext.PathingMapArray.Size > 10000) - { - return default; - } - - var pathingMaps = this.memoryScanner.ReadArray(pathingMapContext.PathingMapArray); - return new PathingMetadata { TrapezoidCount = (int)pathingMaps.Select(p => p.TrapezoidCount).Sum(count => count) }; - } - - private InventoryData? ReadInventoryDataInternal() - { - var globalContext = this.GetGlobalContext(); - if (!globalContext.GameContext.IsValid() || - !globalContext.UserContext.IsValid() || - !globalContext.InstanceContext.IsValid()) - { - return default; - } - - var itemContext = this.memoryScanner.Read(globalContext.ItemContext); - var inventory = this.memoryScanner.Read(itemContext.Inventory); - var inventoryData = new InventoryData - { - Backpack = this.GetBag(inventory.Backpack, 1, true), - BeltPouch = this.GetBag(inventory.BeltPouch, 1, true), - Bags = new List - { - this.GetBag(inventory.Bag1, 1, true), - this.GetBag(inventory.Bag2, 1, true) - }, - EquipmentPack = this.GetBag(inventory.EquipmentPack, 1, true), - MaterialStorage = this.GetBag(inventory.MaterialStorage, 5, true), - UnclaimedItems = this.GetBag(inventory.UnclaimedItems, 3, true), - EquippedItems = this.GetBag(inventory.EquippedItems, 2, true), - StoragePanes = inventory.StoragePanes.Select(b => this.GetBag(b, 4, false)).ToList() - }; - - return inventoryData; - } - - private WorldData? ReadWorldDataInternal() - { - var globalContext = this.GetGlobalContext(); - if (!globalContext.GameContext.IsValid() || - !globalContext.UserContext.IsValid() || - !globalContext.InstanceContext.IsValid()) - { - return default; - } - - var userContext = this.memoryScanner.Read(globalContext.UserContext, UserContext.BaseOffset); - var instanceInfo = this.memoryScanner.ReadPtrChain(this.GetInstanceInfoPointer(), 0x0, 0x0); - var areaInfo = this.memoryScanner.Read(instanceInfo.AreaInfo); - - _ = Map.TryParse((int)userContext.MapId, out var map); - _ = Region.TryParse((int)areaInfo.RegionId, out var region); - _ = Continent.TryParse((int)areaInfo.ContinentId, out var continent); - _ = Campaign.TryParse((int)areaInfo.CampaignId, out var campaign); - - return new WorldData - { - Campaign = campaign, - Continent = continent, - Region = region, - Map = map - }; - } - - private UserData? ReadUserDataInternal() - { - var globalContext = this.GetGlobalContext(); - if (!globalContext.GameContext.IsValid() || - !globalContext.UserContext.IsValid() || - !globalContext.InstanceContext.IsValid()) - { - return default; - } - - // GameContext struct is offset by 0x07C due to the memory layout of the structure. - var gameContext = this.memoryScanner.Read(globalContext.GameContext, GameContext.BaseOffset); - // UserContext struct is offset by 0x074 due to the memory layout of the structure. - var userContext = this.memoryScanner.Read(globalContext.UserContext, UserContext.BaseOffset); - - var userInformation = new UserInformation - { - Email = userContext.PlayerEmailFirstChar + (userContext.PlayerEmailSecondChar + userContext.PlayerEmailRemaining), - CurrentKurzickPoints = gameContext.CurrentKurzick, - TotalKurzickPoints = gameContext.TotalKurzick, - MaxKurzickPoints = gameContext.MaxKurzick, - CurrentLuxonPoints = gameContext.CurrentLuxon, - TotalLuxonPoints = gameContext.TotalLuxon, - MaxLuxonPoints = gameContext.MaxLuxon, - CurrentImperialPoints = gameContext.CurrentImperial, - TotalImperialPoints = gameContext.TotalImperial, - MaxImperialPoints = gameContext.MaxImperial, - CurrentBalthazarPoints = gameContext.CurrentBalthazar, - TotalBalthazarPoints = gameContext.TotalBalthazar, - MaxBalthazarPoints = gameContext.MaxBalthazar, - CurrentSkillPoints = gameContext.CurrentSkillPoints, - TotalSkillPoints = gameContext.TotalSkillPoints - }; - - return new UserData { User = userInformation }; - } - - private SessionData? ReadSessionDataInternal() - { - var globalContext = this.GetGlobalContext(); - if (!globalContext.GameContext.IsValid() || - !globalContext.UserContext.IsValid() || - !globalContext.InstanceContext.IsValid()) - { - return default; - } - - // GameContext struct is offset by 0x07C due to the memory layout of the structure. - var gameContext = this.memoryScanner.Read(globalContext.GameContext, GameContext.BaseOffset); - // InstanceContext struct is offset by 0x01AC due to memory layout of the structure. - var instanceContext = this.memoryScanner.Read(globalContext.InstanceContext, InstanceContext.BaseOffset); - // UserContext struct is offset by 0x074 due to the memory layout of the structure. - var userContext = this.memoryScanner.Read(globalContext.UserContext, UserContext.BaseOffset); - var instanceInfo = this.memoryScanner.ReadPtrChain(this.GetInstanceInfoPointer(), 0x0, 0x0); - - _ = Map.TryParse((int)userContext.MapId, out var currentMap); - var sessionInformation = new SessionInformation - { - FoesKilled = gameContext.FoesKilled, - FoesToKill = gameContext.FoesToKill, - CurrentMap = currentMap, - InstanceTimer = instanceContext.Timer, - InstanceType = instanceInfo.InstanceType switch - { - Daybreak.Models.Interop.InstanceType.Outpost => Daybreak.Models.Guildwars.InstanceType.Outpost, - Daybreak.Models.Interop.InstanceType.Explorable => Daybreak.Models.Guildwars.InstanceType.Explorable, - Daybreak.Models.Interop.InstanceType.Loading => Daybreak.Models.Guildwars.InstanceType.Loading, - _ => Daybreak.Models.Guildwars.InstanceType.Undefined - } - }; - - return new SessionData { Session = sessionInformation }; - } - - private MainPlayerData? ReadMainPlayerDataInternal() - { - var globalContext = this.GetGlobalContext(); - if (!globalContext.GameContext.IsValid() || - !globalContext.UserContext.IsValid() || - !globalContext.InstanceContext.IsValid()) - { - return default; - } - - // GameContext struct is offset by 0x07C due to the memory layout of the structure. - var gameContext = this.memoryScanner.Read(globalContext.GameContext, GameContext.BaseOffset); - // InstanceContext struct is offset by 0x01AC due to memory layout of the structure. - var instanceContext = this.memoryScanner.Read(globalContext.InstanceContext, InstanceContext.BaseOffset); - - var playerEntityId = this.memoryScanner.ReadPtrChain(this.GetPlayerIdPointer(), 0x0, 0x0); - var mapEntity = this.memoryScanner.ReadItemAtIndex(gameContext.MapEntities, playerEntityId); - var entityPointersArray = this.memoryScanner.ReadPtrChain(this.GetEntityArrayPointer(), 0x0, 0x0); - var entity = this.memoryScanner.ReadPtrChain((uint)(entityPointersArray.Buffer + (sizeof(uint) * playerEntityId)), 0x0, 0x0); - if (mapEntity.MaxHealth != entity.MaxHealth || - mapEntity.MaxEnergy != entity.MaxEnergy || - mapEntity.MaxHealth < 0 || mapEntity.MaxHealth > 10000 || - mapEntity.MaxEnergy < 0 || mapEntity.MaxEnergy > 10000 || - gameContext.Professions.Size == 0 || - gameContext.Players.Size == 0 || - gameContext.Skillbars.Size == 0 || - gameContext.PartyAttributes.Size == 0 || - gameContext.Titles.Size == 0 || - gameContext.TitlesTiers.Size == 0) - { - return default; - } - - var professions = this.memoryScanner.ReadArray(gameContext.Professions); - var players = this.memoryScanner.ReadArray(gameContext.Players); - var quests = this.memoryScanner.ReadArray(gameContext.QuestLog); - var skills = this.memoryScanner.ReadArray(gameContext.Skillbars); - var partyAttributes = this.memoryScanner.ReadArray(gameContext.PartyAttributes); - var titles = this.memoryScanner.ReadArray(gameContext.Titles); - var titleTiers = this.memoryScanner.ReadArray(gameContext.TitlesTiers); - var mainPlayerInfo = this.GetMainPlayerInformation( - gameContext, - instanceContext, - players.FirstOrDefault(p => p.AgentId == playerEntityId), - mapEntity, - professions.FirstOrDefault(p => p.AgentId == playerEntityId), - quests, - skills.FirstOrDefault(p => p.AgentId == playerEntityId), - partyAttributes.FirstOrDefault(p => p.AgentId == playerEntityId), - titles, - titleTiers, - entity); - - return new MainPlayerData - { - PlayerInformation = mainPlayerInfo - }; - } - - private ConnectionData? ReadConnectionData() - { - /* - * IP Address in long format is at one of the following addresses: - * startAddress + 00629204 -> +0x18 -> +0x44 -> +0x1A4 - */ - - var ipAddressContext = this.memoryScanner.ReadPtrChain(this.memoryScanner.ModuleStartAddress, finalPointerOffset: 0x1A4, 0x00629204, 0x18, 0x44); - - if (!IPAddress.TryParse($"{ipAddressContext.Byte1}.{ipAddressContext.Byte2}.{ipAddressContext.Byte3}.{ipAddressContext.Byte4}", out var ipAddress)) - { - return default; - } - - return new ConnectionData { IPAddress = ipAddress }; - } - - private PreGameData? ReadPreGameData() - { - var preGameContextPtr = this.GetPreGameContextPointer(); - if (preGameContextPtr == 0) - { - return default; - } - - var preGameContext = this.memoryScanner.ReadPtrChain(preGameContextPtr, 0x0, 0x0, 0x0); - if (preGameContext.LoginCharacters.Capacity > 20 || - preGameContext.LoginCharacters.Size > 20 || - !preGameContext.LoginCharacters.Buffer.IsValid()) - { - return default; - } - - // Detect corrupt memory by checking that player names are longer than 3 characters - var loginCharacters = this.memoryScanner.ReadArray(preGameContext.LoginCharacters); - if (loginCharacters.Any(c => c.CharacterName != "" && c.CharacterName.Length < 2)) - { - return default; - } - - return new PreGameData - { - ChosenCharacterIndex = preGameContext.LoginSelectionIndex > 0 && preGameContext.LoginSelectionIndex < loginCharacters.Length ? - (int)preGameContext.LoginSelectionIndex : - -1, - Characters = loginCharacters.Select(c => c.CharacterName).ToList() - }; - } - - private uint GetPlayerIdPointer() - { - if (this.playerIdPointer == 0) - { - this.playerIdPointer = this.memoryScanner.ScanForPtr(new byte[] { 0x5D, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x55, 0x8B, 0xEC, 0x53 }, "xx????xxxx") - 0xE; - } - - return this.playerIdPointer; - } - - private uint GetEntityArrayPointer() - { - if (this.entityArrayPointer == 0) - { - this.entityArrayPointer = this.memoryScanner.ScanForPtr(new byte[] { 0xFF, 0x50, 0x10, 0x47, 0x83, 0xC6, 0x04, 0x3B, 0xFB, 0x75, 0xE1 }, "xxxxxxxxxxx") + 0xD; - } - - return this.entityArrayPointer; - } - - private uint GetTargetIdPointer() - { - if (this.targetIdPointer == 0) - { - this.targetIdPointer = this.memoryScanner.ScanForPtr(new byte[] { 0x3B, 0xDF, 0x0F, 0x95 }, "xxxx") + 0xB; - } - - return this.targetIdPointer; - } - - private uint GetInstanceInfoPointer() - { - if (this.instanceInfoPointer == 0) - { - this.instanceInfoPointer = this.memoryScanner.ScanForPtr(new byte[] { 0x6A, 0x2C, 0x50, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x83, 0xC4, 0x08, 0xC7 }, "xxxx????xxxx") + 0xD; - } - - return this.instanceInfoPointer; - } - - private uint GetPreGameContextPointer() - { - if (this.preGameContextPointer == 0) - { - this.preGameContextPointer = this.memoryScanner.ScanForAssertion("p:\\code\\gw\\ui\\uipregame.cpp", "!s_scene") + 0x34; - } - - return this.preGameContextPointer; - } - - private Bag? GetBag(GuildwarsPointer bagInfoPtr, uint expectedBagType, bool returnEmptyBag) - { - var bagInfo = this.memoryScanner.Read(bagInfoPtr); - if (!bagInfo.Items.Buffer.IsValid() || - bagInfo.Items.Size > 100 || - bagInfo.Type != expectedBagType) - { - return default; - } - - var itemInfos = this.memoryScanner.ReadArray(bagInfo.Items); - var items = new List(); - for(var i = 0; i < itemInfos.Length; i++) - { - var itemInfo = itemInfos[i]; - if (itemInfo.Quantity == 0 || - itemInfo.Quantity > 250 || - itemInfo.ModifierArrayAddress == 0 || - itemInfo.ModifierCount > 200 || - itemInfo.ModelId == 0) - { - continue; - } - - if (itemInfo.ContainingBagAddress != bagInfoPtr.Address) - { - continue; - } - - var modifiers = this.memoryScanner.ReadArray(itemInfo.ModifierArrayAddress, itemInfo.ModifierCount); - var parsedModifiers = modifiers.Select(modifier => new Daybreak.Models.Guildwars.ItemModifier { Modifier = modifier.Modifier }).ToList(); - if (ItemBase.TryParse((int)itemInfo.ModelId, parsedModifiers, out var item) && - item is not ItemBase.Unknown) - { - items.Add(new BagItem - { - Item = item ?? throw new InvalidOperationException($"Unable to create {nameof(BagItem)}. Expected item returned null"), - Slot = itemInfo.Slot, - Count = itemInfo.Quantity, - Modifiers = parsedModifiers - }); - } - else - { - items.Add(new UnknownBagItem - { - ItemId = itemInfo.ModelId, - Slot = itemInfo.Slot, - Count = itemInfo.Quantity, - Modifiers = parsedModifiers - }); - } - } - - if (items.Count == 0 && - !returnEmptyBag) - { - return default; - } - - return new Bag - { - Items = items, - Capacity = (int)Math.Max(bagInfo.ItemsCount, itemInfos.Length) - }; - } - - private GlobalContext GetGlobalContext() - { - /* - * The following offsets were reverse-engineered using pointer scanning. All of the following ones seem to currently work. - * If any breaks, try the other ones from the list below. - * startAddress + 00629204 -> +0x18 - * startAddress + 006296B4 -> +0x18 - * startAddress + 00AA9CE0 -> +0xC -> +0xC - */ - return this.memoryScanner.ReadPtrChain(this.memoryScanner.ModuleStartAddress, finalPointerOffset: 0x0, 0x00629204, 0x18); - } - - private unsafe GameData AggregateGameData( - GameContext gameContext, - InstanceContext instanceContext, - MapEntityContext[] mapEntities, - EntityContext[] entities, - PlayerContext[] players, - ProfessionsContext[] professions, - QuestContext[] quests, - SkillbarContext[] skills, - PartyAttributesContext[] partyAttributes, - TitleContext[] titles, - TitleTierContext[] titleTiers, - MapIconContext[] mapIcons, - NpcContext[] npcs, - int mainPlayerEntityId, - int targetEntityId) - { - var partyMembers = professions - .Where(p => p.AgentId != mainPlayerEntityId) - .Select(p => GetPlayerInformation( - (int)p.AgentId, - instanceContext, - mapEntities.Skip((int)p.AgentId).FirstOrDefault(), - professions.FirstOrDefault(prof => prof.AgentId == p.AgentId), - skills.FirstOrDefault(s => s.AgentId == p.AgentId), - partyAttributes.OfType().FirstOrDefault(attr => attr.AgentId == p.AgentId), - entities.OfType().FirstOrDefault(e => e.AgentId == p.AgentId))) - .ToList(); - - var parsedMapIcons = GetMapIcons(mapIcons); - - var mainPlayer = this.GetMainPlayerInformation( - gameContext, - instanceContext, - players.FirstOrDefault(p => p.AgentId == mainPlayerEntityId), - mapEntities.Skip(mainPlayerEntityId).FirstOrDefault(), - professions.FirstOrDefault(p => p.AgentId == mainPlayerEntityId), - quests, - skills.OfType().FirstOrDefault(s => s.AgentId == mainPlayerEntityId), - partyAttributes.OfType().FirstOrDefault(p => p.AgentId == mainPlayerEntityId), - titles, - titleTiers, - entities.FirstOrDefault(e => e.AgentId == mainPlayerEntityId)); - - var worldPlayers = players - .Where(p => p.AgentId != mainPlayerEntityId && p.AgentId != 0) - .Select(p => - this.GetWorldPlayerInformation( - p, - instanceContext, - mapEntities.Skip(p.AgentId).First(), - professions.FirstOrDefault(prof => prof.AgentId == p.AgentId), - skills.OfType().FirstOrDefault(s => s.AgentId == p.AgentId), - partyAttributes.OfType().FirstOrDefault(attr => attr.AgentId == p.AgentId), - titles, - titleTiers, - entities.FirstOrDefault(e => e.AgentId == p.AgentId))) - .ToList(); - - var remainingEntities = entities - .Where(e => e.EntityType == EntityType.Living) - .Where(e => e.AgentId != mainPlayer.Id && partyMembers.None(p => p.Id == e.AgentId) && worldPlayers.None(p => p.Id == e.AgentId)) - .ToArray(); - - var livingEntities = this.GetLivingEntities(remainingEntities, npcs); - - return new GameData - { - Party = partyMembers, - MainPlayer = mainPlayer, - WorldPlayers = worldPlayers, - LivingEntities = livingEntities, - MapIcons = parsedMapIcons, - CurrentTargetId = targetEntityId, - Valid = mapEntities.Length > 0 || players.Length > 0 || professions.Length > 0 || partyAttributes.Length > 0 || entities.Length > 0 - }; - } - - private List GetLivingEntities( - EntityContext[] livingEntities, - NpcContext[] npcs) - { - var list = new List(); - foreach(var livingEntity in livingEntities) - { - _ = Profession.TryParse((int)livingEntity.PrimaryProfessionId, out var primaryProfession); - _ = Profession.TryParse((int)livingEntity.SecondaryProfessionId, out var secondaryProfession); - if (primaryProfession == Profession.None) - { - var maybeNpcContext = npcs.Skip(livingEntity.EntityModelType).FirstOrDefault(); - if (Profession.TryParse((int)maybeNpcContext.Primary, out var actualPrimaryProfession)) - { - primaryProfession = actualPrimaryProfession; - } - } - - var state = LivingEntityState.Unknown; - switch (livingEntity.State) - { - case EntityState.Player: - state = LivingEntityState.Player; - break; - case EntityState.Spirit: - state = LivingEntityState.Spirit; - break; - case EntityState.Boss: - state = LivingEntityState.Boss; - break; - case EntityState.Dead: - state = LivingEntityState.Dead; - break; - case EntityState.ToBeCleanedUp: - state = LivingEntityState.ToBeCleanedUp; - break; - } - - var allegiance = LivingEntityAllegiance.Unknown; - switch (livingEntity.Allegiance) - { - case EntityAllegiance.AllyNonAttackable: - allegiance = LivingEntityAllegiance.AllyNonAttackable; - break; - case EntityAllegiance.Neutral: - allegiance = LivingEntityAllegiance.Neutral; - break; - case EntityAllegiance.Enemy: - allegiance = LivingEntityAllegiance.Enemy; - break; - case EntityAllegiance.SpiritOrPet: - allegiance = LivingEntityAllegiance.SpiritOrPet; - break; - case EntityAllegiance.Minion: - allegiance = LivingEntityAllegiance.Minion; - break; - case EntityAllegiance.NpcOrMinipet: - allegiance = LivingEntityAllegiance.NpcOrMinipet; - break; - } - - if (!Npc.TryParse(livingEntity.EntityModelType, out var npc)) - { - npc = Npc.Unknown; - } - - list.Add(new LivingEntity - { - Id = (int)livingEntity.AgentId, - Timer = livingEntity.Timer, - Level = (int)livingEntity.Level, - NpcDefinition = npc, - ModelType = livingEntity.EntityModelType, - Position = new Position { X = livingEntity.Position.X, Y = livingEntity.Position.Y }, - PrimaryProfession = primaryProfession, - SecondaryProfession = secondaryProfession, - State = state, - Allegiance = allegiance - }); - } - - return list; - } - - private MainPlayerInformation GetMainPlayerInformation( - GameContext gameContext, - InstanceContext instanceContext, - PlayerContext playerContext, - MapEntityContext mapEntity, - ProfessionsContext professions, - QuestContext[] quests, - SkillbarContext skillbar, - PartyAttributesContext partyAttributes, - TitleContext[] titles, - TitleTierContext[] titleTiers, - EntityContext entity) - { - var playerInformation = this.GetWorldPlayerInformation(playerContext, instanceContext, mapEntity, professions, skillbar, partyAttributes, titles, titleTiers, entity); - _ = Quest.TryParse((int)gameContext.QuestId, out var quest); - var questLog = quests - .Select(q => - { - _ = Quest.TryParse((int)q.QuestId, out var parsedQuest); - _ = Map.TryParse((int)q.MapFrom, out var mapFrom); - _ = Map.TryParse((int)q.MapTo, out var mapTo); - return new QuestMetadata - { - Quest = parsedQuest, - From = mapFrom, - To = mapTo, - Position = float.IsFinite(q.Marker.X) && float.IsFinite(q.Marker.Y) ? - new Position { X = q.Marker.X, Y = q.Marker.Y } : - default - }; - }) - .Where(q => q.Quest is not null) - .ToList(); - - return new MainPlayerInformation - { - Id = playerInformation.Id, - Timer = playerInformation.Timer, - Position = playerInformation.Position, - PrimaryProfession = playerInformation.PrimaryProfession, - SecondaryProfession = playerInformation.SecondaryProfession, - UnlockedProfession = playerInformation.UnlockedProfession, - HardModeUnlocked = gameContext.HardModeUnlocked == 1, - CurrentEnergy = playerInformation.MaxEnergy * entity.CurrentEnergyPercentage, - CurrentHealth = playerInformation.MaxHealth * entity.CurrentHealthPercentage, - MaxEnergy = playerInformation.MaxEnergy, - MaxHealth = playerInformation.MaxHealth, - EnergyRegen = playerInformation.EnergyRegen, - HealthRegen = playerInformation.HealthRegen, - Quest = quest, - QuestLog = questLog, - Name = playerInformation.Name, - Experience = gameContext.Experience, - Level = (int)gameContext.Level, - Morale = gameContext.Morale, - CurrentBuild = playerInformation.CurrentBuild, - TitleInformation = playerInformation.TitleInformation - }; - } - - private WorldPlayerInformation GetWorldPlayerInformation( - PlayerContext playerContext, - InstanceContext instanceContext, - MapEntityContext mapEntity, - ProfessionsContext professions, - SkillbarContext skillbar, - PartyAttributesContext partyAttributes, - TitleContext[] titles, - TitleTierContext[] titleTiers, - EntityContext entity) - { - var name = this.memoryScanner.Read(playerContext.NamePointer); - var playerInformation = GetPlayerInformation(playerContext.AgentId, instanceContext, mapEntity, professions, skillbar, partyAttributes, entity); - var maybeCurrentTitleContext = (TitleContext?)null; - var maybeCurrentTitle = (Title?)null; - for(var i = 0; i < titles.Length; i++) - { - var title = titles[i]; - if (title.CurrentTitleTierIndex == playerContext.ActiveTitleTier) - { - maybeCurrentTitleContext = title; - _ = Title.TryParse(i, out maybeCurrentTitle); - break; - } - } - - var maybeTitleTier = (TitleTierContext?)null; - if (maybeCurrentTitleContext is TitleContext currentTitle) - { - maybeTitleTier = titleTiers[currentTitle.CurrentTitleTierIndex]; - } - - var titleInformation = (TitleInformation?)null; - if (maybeCurrentTitleContext is not null && maybeTitleTier is not null) - { - titleInformation = new TitleInformation - { - CurrentPoints = maybeCurrentTitleContext?.CurrentPoints, - IsPercentage = maybeCurrentTitleContext?.IsPercentage, - PointsForCurrentRank = maybeCurrentTitleContext?.PointsNeededForCurrentRank, - PointsForNextRank = maybeTitleTier?.TierNumber == maybeCurrentTitleContext?.MaxTitleRank ? - maybeCurrentTitleContext?.CurrentPoints : - maybeCurrentTitleContext?.PointsNeededForNextRank, - TierNumber = maybeTitleTier?.TierNumber, - MaxTierNumber = maybeCurrentTitleContext?.MaxTitleRank, - Title = maybeCurrentTitle - }; - } - - return new WorldPlayerInformation - { - Id = playerInformation.Id, - Timer = playerInformation.Timer, - Level = playerInformation.Level, - Position = playerInformation.Position, - PrimaryProfession = playerInformation.PrimaryProfession, - SecondaryProfession = playerInformation.SecondaryProfession, - UnlockedProfession = playerInformation.UnlockedProfession, - CurrentEnergy = playerInformation.CurrentEnergy, - CurrentHealth = playerInformation.CurrentHealth, - MaxEnergy = playerInformation.MaxEnergy, - MaxHealth = playerInformation.MaxHealth, - EnergyRegen = playerInformation.EnergyRegen, - HealthRegen = playerInformation.HealthRegen, - Name = name, - CurrentBuild = playerInformation.CurrentBuild, - TitleInformation = titleInformation, - }; - } - - private static PlayerInformation GetPlayerInformation( - int playerId, - InstanceContext instanceContext, - MapEntityContext mapEntity, - ProfessionsContext professionContext, - SkillbarContext? skillbar, - PartyAttributesContext? partyAttributes, - EntityContext entity) - { - _ = Profession.TryParse((int)professionContext.CurrentPrimary, out var primaryProfession); - _ = Profession.TryParse((int)professionContext.CurrentSecondary, out var secondaryProfession); - var unlockedProfessions = Profession.Professions - .Where(p => professionContext.ProfessionUnlocked(p.Id)) - .Append(primaryProfession) - .Where(p => p is not null && p != Profession.None) - .OrderBy(p => p.Id) - .ToList(); - Build? build = null; - if (skillbar is SkillbarContext skillbarContext && - partyAttributes is PartyAttributesContext attributesContext && - partyAttributes.Value.Attributes is not null && - primaryProfession is not null && - secondaryProfession is not null) - { - var attributes = (primaryProfession.PrimaryAttribute is null ? - new List() : - new List { primaryProfession.PrimaryAttribute! }) - .Concat(primaryProfession.Attributes) - .Concat(secondaryProfession.Attributes) - .Select(a => new AttributeEntry { Attribute = a }) - .ToList(); - foreach(var attribute in attributesContext.Attributes) - { - if (attribute.Id < 0 || attribute.Id > 44) - { - continue; - } - - var maybeAttributeEntry = attributes.FirstOrDefault(a => a.Attribute!.Id == attribute.Id); - if (maybeAttributeEntry is not AttributeEntry attributeEntry) - { - continue; - } - - attributeEntry.Points = (int)attribute.BaseLevel; - } - - var skillContexts = new SkillContext[] - { - skillbarContext.Skill0, - skillbarContext.Skill1, - skillbarContext.Skill2, - skillbarContext.Skill3, - skillbarContext.Skill4, - skillbarContext.Skill5, - skillbarContext.Skill6, - skillbarContext.Skill7, - }; - - build = new Build - { - Primary = primaryProfession, - Secondary = secondaryProfession, - Attributes = attributes, - Skills = skillContexts.Select(s => - Skill.TryParse((int)s.Id, out var parsedSkill) ? - parsedSkill : - Skill.NoSkill).ToList() - }; - } - - (var currentHp, var currentEnergy) = ApplyEnergyAndHealthRegen(instanceContext, mapEntity); - if (!Npc.TryParse(entity.EntityModelType, out var npc)) - { - npc = Npc.Unknown; - } - - return new PlayerInformation - { - Id = playerId, - Timer = entity.Timer, - Level = entity.Level, - NpcDefinition = npc, - ModelType = entity.EntityModelType, - PrimaryProfession = primaryProfession, - SecondaryProfession = secondaryProfession, - UnlockedProfession = unlockedProfessions, - CurrentHealth = currentHp, - CurrentEnergy = currentEnergy, - MaxHealth = mapEntity.MaxHealth, - MaxEnergy = mapEntity.MaxEnergy, - HealthRegen = mapEntity.HealthRegen, - EnergyRegen = mapEntity.EnergyRegen, - CurrentBuild = build, - Position = new Position - { - X = entity.Position.X, - Y = entity.Position.Y - } - }; - } - - private static List GetMapIcons(MapIconContext[] mapIconContexts) - { - var retList = new List(); - foreach(var mapIconContext in mapIconContexts) - { - var affiliation = mapIconContext.Affiliation switch - { - TeamColor.Gray => Affiliation.Gray, - TeamColor.GrayNeutral => Affiliation.GrayNeutral, - TeamColor.Teal => Affiliation.Teal, - TeamColor.Yellow => Affiliation.Yellow, - TeamColor.Purple => Affiliation.Purple, - TeamColor.Blue => Affiliation.Blue, - TeamColor.Red => Affiliation.Red, - TeamColor.Green => Affiliation.Green, - _ => throw new InvalidOperationException($"Unknown affiliation {mapIconContext.Affiliation}") - }; - - if (GuildwarsIcon.TryParse((int)mapIconContext.Id, out var icon)) - { - retList.Add(new MapIcon - { - Icon = icon, - Position = new Position { X = mapIconContext.X, Y = mapIconContext.Y }, - Affiliation = affiliation - }); - } - } - - return retList; - } - - private static (float CurrentHp, float CurrentEnergy) ApplyEnergyAndHealthRegen(InstanceContext instanceContext, MapEntityContext entityContext) - { - var lastKnownHp = entityContext.CurrentHealth; - var lastKnownEnergy = entityContext.CurrentEnergy; - var hpRegen = entityContext.HealthRegen; - var energyRegen = entityContext.EnergyRegen; - var millisSinceLastKnownInformation = instanceContext.Timer - (uint)entityContext.SkillTimestamp; - var currentHp = lastKnownHp + (hpRegen * ((float)millisSinceLastKnownInformation / 1000)); - var currentEnergy = lastKnownEnergy + (energyRegen * ((float)millisSinceLastKnownInformation / 1000)); - return ( - currentHp > entityContext.MaxHealth ? entityContext.MaxHealth : currentHp, - currentEnergy > entityContext.MaxEnergy ? entityContext.MaxEnergy : currentEnergy); - } - - private static string ParseAndCleanWCharArray(byte[] bytes) - { - var str = Encoding.Unicode.GetString(bytes); - var indexOfNull = str.IndexOf('\0'); - if (indexOfNull > 0) - { - str = str[..indexOfNull]; - } - - return str; - } - - private static List> BuildPathingMaps(List trapezoids, List> adjacencyArray) - { - var visited = new bool[trapezoids.Count]; - var pathingMaps = new List>(); - foreach (var trapezoid in trapezoids) - { - if (visited[trapezoid.Id] is false) - { - var currentPathingMap = new List(); - pathingMaps.Add(currentPathingMap); - var queue = new Queue(); - queue.Enqueue(trapezoid.Id); - while (queue.TryDequeue(out var currentId)) - { - if (visited[currentId] is true) - { - continue; - } - - visited[currentId] = true; - currentPathingMap.Add(currentId); - foreach (var adjacentId in adjacencyArray[currentId]) - { - queue.Enqueue(adjacentId); - } - } - } - } - - return pathingMaps; - } - - private static List> BuildFinalAdjacencyList(List trapezoids, List> computedPathingMaps, List> originalAdjacencyList) - { - var adjacencyList = new List>(); - for(var i = 0; i < trapezoids.Count; i++) - { - adjacencyList.Add(originalAdjacencyList[i].ToList()); - } - - for(var i = 0; i < computedPathingMaps.Count; i++) - { - var currentPathingMap = computedPathingMaps[i]; - Parallel.For(i + 1, computedPathingMaps.Count - 1, j => - { - var otherPathingMap = computedPathingMaps[j]; - foreach (var currentTrapezoidId in currentPathingMap) - { - var currentPoints = MathUtils.GetTrapezoidPoints(trapezoids[currentTrapezoidId]); - foreach (var otherTrapezoidId in otherPathingMap) - { - var otherPoints = MathUtils.GetTrapezoidPoints(trapezoids[otherTrapezoidId]); - if (TrapezoidsAdjacent(currentPoints, otherPoints)) - { - adjacencyList[currentTrapezoidId].Add(otherTrapezoidId); - adjacencyList[otherTrapezoidId].Add(currentTrapezoidId); - } - } - } - }); - } - - return adjacencyList; - } - - private static bool TrapezoidsAdjacent(Point[] currentPoints, Point[] otherPoints) - { - var curBoundingRectangle = GetBoundingRectangle(currentPoints); - var otherBoundingRectangle = GetBoundingRectangle(otherPoints); - if (!curBoundingRectangle.IntersectsWith(otherBoundingRectangle)) - { - return false; - } - - for (var x = 0; x < currentPoints.Length; x++) - { - for (var y = 0; y < otherPoints.Length; y++) - { - if (MathUtils.LineSegmentsIntersect(currentPoints[x], currentPoints[(x + 1) % currentPoints.Length], - otherPoints[y], otherPoints[(y + 1) % otherPoints.Length], out _, epsilon: 0.1)) - { - return true; - } - } - } - - return false; - } - - private static Rect GetBoundingRectangle(Point[] points) - { - var minX = double.MaxValue; - var maxX = double.MinValue; - var minY = double.MaxValue; - var maxY = double.MinValue; - for (var i = 0; i < points.Length; i++) - { - var curPoint = points[i]; - if (curPoint.X < minX) minX = curPoint.X; - if (curPoint.X > maxX) maxX = curPoint.X; - if (curPoint.Y < minY) minY = curPoint.Y; - if (curPoint.Y > maxY) maxY = curPoint.Y; - } - - return new Rect(minX, minY, maxX - minX, maxY - minY); - } - - private static unsafe string ParseAndCleanWCharPointer(byte* bytes, int byteCount) - { - var str = Encoding.Unicode.GetString(bytes, byteCount); - var indexOfNull = str.IndexOf('\0'); - if (indexOfNull > 0) - { - str = str[..indexOfNull]; - } - - return str; - } - - // https://learn.microsoft.com/en-us/dotnet/standard/base-types/how-to-verify-that-strings-are-in-valid-email-format - private static bool IsValidEmail(string email) - { - if (string.IsNullOrWhiteSpace(email)) - return false; - - try - { - // Normalize the domain - email = Regex.Replace(email, @"(@)(.+)$", DomainMapper, RegexOptions.None, TimeSpan.FromMilliseconds(200)); - - // Examines the domain part of the email and normalizes it. - string DomainMapper(Match match) - { - // Use IdnMapping class to convert Unicode domain names. - var idn = new IdnMapping(); - - // Pull out and process domain name (throws ArgumentException on invalid) - string domainName = idn.GetAscii(match.Groups[2].Value); - - return match.Groups[1].Value + domainName; - } - } - catch (RegexMatchTimeoutException) - { - return false; - } - catch (ArgumentException) - { - return false; - } - - try - { - return Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$", RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(250)); - } - catch (RegexMatchTimeoutException) - { - return false; - } - } -} diff --git a/Daybreak/Services/Scanner/IMemoryScanner.cs b/Daybreak/Services/Scanner/IMemoryScanner.cs deleted file mode 100644 index c91457be..00000000 --- a/Daybreak/Services/Scanner/IMemoryScanner.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Daybreak.Models.Interop; -using System.Diagnostics; - -namespace Daybreak.Services.Scanner; - -public interface IMemoryScanner -{ - public uint ModuleStartAddress { get; } - public byte[]? Memory { get; } - public uint Size { get; } - public bool Scanning { get; } - public Process? Process { get; } - - void BeginScanner(Process process); - void EndScanner(); - T Read(GuildwarsPointer pointer, uint offset = 0); - T Read(uint address); - T[] ReadArray(uint address, uint size); - T[] ReadArray(GuildwarsArray guildwarsArray); - T[] ReadArray(GuildwarsPointerArray guildwarsPointerArray); - T ReadItemAtIndex(uint address, int index); - T ReadItemAtIndex(GuildwarsArray guildwarsArray, int index); - byte[]? ReadBytes(uint address, uint size); - string ReadWString(uint address, uint maxsize); - T ReadPtrChain(uint Base, uint finalPointerOffset = 0, params uint[] offsets); - uint ScanForAssertion(string? assertionFile, string? assertionMessage); - uint ScanForPtr(byte[] pattern, string? mask = default, bool readptr = false); -} diff --git a/Daybreak/Services/Scanner/MemoryScanner.cs b/Daybreak/Services/Scanner/MemoryScanner.cs deleted file mode 100644 index 8b6e170a..00000000 --- a/Daybreak/Services/Scanner/MemoryScanner.cs +++ /dev/null @@ -1,405 +0,0 @@ -using Daybreak.Models.Interop; -using Daybreak.Utils; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Core.Extensions; -using System.Diagnostics; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; - -namespace Daybreak.Services.Scanner; - -/// -/// Memory scanner to perform memory scans over the executable. -/// Big thanks to https://github.com/GregLando113/GWCA for inspiration on how should the scanner work. -/// -public sealed class MemoryScanner : IMemoryScanner -{ - private const double MaximumReadSize = 10e8; - private const double MaximumArraySize = 10e5; - private static readonly object LockObject = new(); - - private readonly ILogger logger; - - private bool scanning; - - public uint ModuleStartAddress { get; private set; } - public byte[]? Memory { get; private set; } - public uint Size { get; private set; } - public bool Scanning - { - get - { - while(Monitor.TryEnter(LockObject) is false) - { - } - - var value = this.scanning; - Monitor.Exit(LockObject); - - return value; - } - set - { - while (Monitor.TryEnter(LockObject) is false) - { - } - - this.scanning = value; - Monitor.Exit(LockObject); - } - } - public Process? Process { get; private set; } - - public MemoryScanner( - ILogger logger) - { - this.logger = logger.ThrowIfNull(); - } - - public void BeginScanner(Process process) - { - _ = process.ThrowIfNull(); - - - if (this.Scanning) - { - throw new InvalidOperationException("Scanner is already running"); - } - - while (Monitor.TryEnter(LockObject) is false) - { - } - - this.Process = process; - (var startAddress, var size) = this.GetModuleInfo(process); - if (startAddress == 0 && - size == 0) - { - Monitor.Exit(LockObject); - return; - } - - this.ModuleStartAddress = startAddress; - this.Size = size; - this.Memory = this.ReadBytesNonLocking(this.ModuleStartAddress, this.Size); - - Monitor.Exit(LockObject); - - this.Scanning = true; - } - - public void EndScanner() - { - if (!this.Scanning) - { - return; - } - - while (Monitor.TryEnter(LockObject) is false) - { - } - - this.Memory = default; - this.Size = default; - this.ModuleStartAddress = default; - this.Scanning = false; - Monitor.Exit(LockObject); - } - - public T Read(GuildwarsPointer pointer, uint offset = 0) - { - if (pointer is GuildwarsPointer) - { - return (T)(this.ReadWString(pointer.Address, 256) as object); - } - - return this.Read(pointer.Address + offset); - } - - public T Read(uint address) - { - this.ValidateReadScanner(); - var size = Marshal.SizeOf(typeof(T)); - var buffer = Marshal.AllocHGlobal(size); - - NativeMethods.ReadProcessMemory(this.Process!.Handle, - address, - buffer, - (uint)size, - out _ - ); - - var ret = (T)Marshal.PtrToStructure(buffer, typeof(T))!; - Marshal.FreeHGlobal(buffer); - - return ret; - } - - public T[] ReadArray(uint address, uint size) - { - this.ValidateReadScanner(); - if (size > MaximumArraySize) - { - throw new InvalidOperationException($"Expected size to read is too large. Array size {size}"); - } - - var itemSize = Marshal.SizeOf(typeof(T)); - var readSize = (int)size * itemSize; - if (readSize > MaximumReadSize) - { - throw new InvalidOperationException($"Expected size to read is too large. Size {readSize}"); - } - - var buffer = Marshal.AllocHGlobal((int)readSize); - - NativeMethods.ReadProcessMemory(this.Process!.Handle, - address, - buffer, - (uint)readSize, - out _ - ); - - var retArray = new T[size]; - var arrayPointer = buffer; - for (var i = 0; i < size; i++) - { - retArray[i] = (T)Marshal.PtrToStructure(arrayPointer, typeof(T))!; - arrayPointer += itemSize; - } - - Marshal.FreeHGlobal(buffer); - return retArray; - } - - public T[] ReadArray(GuildwarsArray guildwarsArray) - { - return this.ReadArray(guildwarsArray.Buffer.Address, guildwarsArray.Size); - } - - public T ReadItemAtIndex(uint address, int index) - { - var itemSize = Marshal.SizeOf(typeof(T)); - var itemAtIndexAddress = address + (index * itemSize); - return this.Read((uint)itemAtIndexAddress); - } - - public T ReadItemAtIndex(GuildwarsArray guildwarsArray, int index) - { - return this.ReadItemAtIndex(guildwarsArray.Buffer.Address, index); - } - - public T[] ReadArray(GuildwarsPointerArray guildwarsPointerArray) - { - var ptrs = this.ReadArray(guildwarsPointerArray.Buffer.Address, guildwarsPointerArray.Size); - var retList = new List(); - foreach(var ptr in ptrs) - { - retList.Add(this.Read(ptr)); - } - - return retList.ToArray(); - } - - public byte[]? ReadBytes(uint address, uint size) - { - this.ValidateReadScanner(); - return this.ReadBytesNonLocking(address, size); - } - - public string ReadWString(uint address, uint maxsize) - { - this.ValidateReadScanner(); - var rawbytes = this.ReadBytes(address, maxsize); - if (rawbytes == null) - { - return ""; - } - - var ret = Encoding.Unicode.GetString(rawbytes); - if (ret.Contains('\0')) - { - ret = ret[..ret.IndexOf('\0')]; - } - - return ret; - } - - public T ReadPtrChain(uint Base, uint finalPointerOffset = 0, params uint[] offsets) - { - this.ValidateReadScanner(); - foreach (var offset in offsets) - { - Base = this.Read(Base + offset); - } - - return this.Read(Base + finalPointerOffset); - } - - public uint ScanForPtr(byte[] pattern, string? mask = default, bool readptr = false) - { - this.ValidateReadScanner(); - if (pattern?.Length == 0) - { - throw new ArgumentException("Pattern cannot be empty"); - } - - for (var scan = 0U; scan < this.Size; ++scan) - { - if (this.Memory![scan] != pattern![0]) - { - continue; - } - - var matched = true; - for (var patternIndex = 0; patternIndex < pattern.Length; ++patternIndex) - { - if (mask is not null && - mask.Length > patternIndex && - mask[patternIndex] == '?') - { - continue; - } - - var memoryValue = this.Memory[scan + patternIndex]; - var signatureValue = pattern[patternIndex]; - if (memoryValue != signatureValue) - { - matched = false; - break; - } - } - - if (matched) - { - if (readptr) - { - return BitConverter.ToUInt32(this.Memory, (int)scan); - } - - return this.ModuleStartAddress + scan; - } - } - - return 0; - } - - public uint ScanForAssertion(string? assertionFile, string? assertionMessage) - { - this.ValidateReadScanner(); - var mask = new StringBuilder(64); - for (var i = 0; i < 64; i++) - { - mask.Append('\0'); - } - - var assertionBytes = new byte[] { 0xBA, 0x0, 0x0, 0x0, 0x0, 0xB9, 0x0, 0x0, 0x0, 0x0 }; - var assertionMask = new StringBuilder("x????x????"); - if (assertionMessage is not null) - { - var assertionMessageBytes = Encoding.ASCII.GetBytes(assertionMessage); - for (var i = 0; i < assertionMessage.Length; i++) - { - mask[i] = 'x'; - } - - mask[assertionMessage.Length] = 'x'; - mask[assertionMessage.Length + 1] = '\0'; - var rdataPtr = this.ScanForPtr(assertionMessageBytes, mask.ToString(), false); - if (rdataPtr == 0) - { - return 0; - } - - assertionBytes[6] = (byte)rdataPtr; - assertionBytes[7] = (byte)(rdataPtr >> 8); - assertionBytes[8] = (byte)(rdataPtr >> 16); - assertionBytes[9] = (byte)(rdataPtr >> 24); - assertionMask[6] = assertionMask[7] = assertionMask[8] = assertionMask[9] = 'x'; - } - - if (assertionFile is not null) - { - var assertionFileBytes = Encoding.ASCII.GetBytes(assertionFile); - for (var i = 0; i < assertionFile.Length; i++) - { - mask[i] = 'x'; - } - - mask[assertionFile.Length] = 'x'; - mask[assertionFile.Length + 1] = '\0'; - var rdataPtr = this.ScanForPtr(assertionFileBytes, mask.ToString(), false); - - assertionBytes[1] = (byte)rdataPtr; - assertionBytes[2] = (byte)(rdataPtr >> 8); - assertionBytes[3] = (byte)(rdataPtr >> 16); - assertionBytes[4] = (byte)(rdataPtr >> 24); - assertionMask[1] = assertionMask[2] = assertionMask[3] = assertionMask[4] = 'x'; - } - - return this.ScanForPtr(assertionBytes, assertionMask.ToString(), false); - } - - private byte[]? ReadBytesNonLocking(uint address, uint size) - { - if (size > MaximumReadSize) - { - throw new InvalidOperationException($"Expected size to read is too large. Size {size}"); - } - - var buffer = Marshal.AllocHGlobal((int)size); - - NativeMethods.ReadProcessMemory(this.Process!.Handle, - address, - buffer, - size, - out _ - ); - - var ret = new byte[size]; - Marshal.Copy(buffer, ret, 0, (int)size); - Marshal.FreeHGlobal(buffer); - - return ret; - } - - private void ValidateReadScanner() - { - if (!this.Scanning) - { - throw new InvalidOperationException("Scanner is not running"); - } - - if (this.Process is null || - this.Process?.HasExited is true) - { - throw new InvalidOperationException("Process has exited"); - } - } - - private (uint StartAddress, uint Size) GetModuleInfo(Process process) - { - try - { - var name = process.ProcessName; - var modules = process.Modules; - foreach (var module in modules.OfType()) - { - if (module.ModuleName != null && - module.ModuleName.StartsWith(name, StringComparison.OrdinalIgnoreCase)) - { - return ((uint)module.BaseAddress.ToInt32(), (uint)module.ModuleMemorySize); - } - } - } - catch (Exception e) - { - this.logger.LogError(e, "Failed to get module info"); - } - - return (0, 0); - } -} From e6b5cd721c2868f1040a1bfedf82c5f8843c59a8 Mon Sep 17 00:00:00 2001 From: Alexandru Macocian Date: Sat, 28 Oct 2023 14:15:38 +0200 Subject: [PATCH 2/2] Increment version --- Daybreak/Daybreak.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Daybreak/Daybreak.csproj b/Daybreak/Daybreak.csproj index 81c1522d..6806f6bb 100644 --- a/Daybreak/Daybreak.csproj +++ b/Daybreak/Daybreak.csproj @@ -13,7 +13,7 @@ preview Daybreak.ico true - 0.9.8.129 + 0.9.8.130 true cfb2a489-db80-448d-a969-80270f314c46 True