From d24b3cd97bb332471fcc682e645e67a81995fc00 Mon Sep 17 00:00:00 2001 From: Alexandru Macocian Date: Thu, 26 Oct 2023 21:09:24 +0200 Subject: [PATCH] Complete Daybreak.GWCA implementation Fix metrics view Upgrade dependencies --- .../header/payloads/PathingTrapezoid.h | 1 + Daybreak.GWCA/source/GameModule.cpp | 20 + Daybreak.GWCA/source/MainPlayerModule.cpp | 20 + Daybreak.GWCA/source/PathingModule.cpp | 1 + Daybreak.GWCA/source/dllmain.cpp | 2 +- .../source/payloads/PathingTrapezoid.cpp | 1 + Daybreak/Converters/AttributeJsonConverter.cs | 9 +- Daybreak/Converters/CampaignJsonConverter.cs | 6 +- Daybreak/Converters/ContinentJsonConverter.cs | 6 +- .../Converters/GuildwarsIconJsonConverter.cs | 6 +- Daybreak/Converters/ItemBaseJsonConverter.cs | 6 +- Daybreak/Converters/MapJsonConverter.cs | 6 +- Daybreak/Converters/NpcJsonConverter.cs | 6 +- .../Converters/ProfessionJsonConverter.cs | 6 +- Daybreak/Converters/QuestJsonConverter.cs | 6 +- Daybreak/Converters/RegionJsonConverter.cs | 6 +- Daybreak/Converters/SkillJsonConverter.cs | 6 +- Daybreak/Converters/TitleJsonConverter.cs | 6 +- Daybreak/Daybreak.csproj | 4 +- .../ApplicationLauncher.cs | 58 +- .../Services/Downloads/DownloadService.cs | 2 +- Daybreak/Services/Scanner/GWCAMemoryReader.cs | 668 +++++++++++++++++- .../Scanner/Models/AttributePayload.cs | 10 + .../Scanner/Models/BagContentPayload.cs | 10 + .../Services/Scanner/Models/BagPayload.cs | 7 + .../Services/Scanner/Models/BuildPayload.cs | 11 + .../Scanner/Models/GameDataPayload.cs | 12 + .../Scanner/Models/InventoryPayload.cs | 17 + .../Scanner/Models/LivingEntityPayload.cs | 15 + .../Services/Scanner/Models/LoginPayload.cs | 6 + .../Scanner/Models/MainPlayerPayload.cs | 16 + .../Services/Scanner/Models/MapIconPayload.cs | 8 + .../Services/Scanner/Models/MapPayload.cs | 13 + .../Scanner/Models/PartyPlayerPayload.cs | 9 + .../Scanner/Models/PathingMetadataPayload.cs | 5 + .../Services/Scanner/Models/PathingPayload.cs | 8 + .../Scanner/Models/PathingTrapezoidPayload.cs | 12 + .../Services/Scanner/Models/PreGamePayload.cs | 8 + .../Scanner/Models/QuestMetadataPayload.cs | 10 + .../Services/Scanner/Models/SessionPayload.cs | 9 + .../Services/Scanner/Models/TitlePayload.cs | 12 + .../Services/Scanner/Models/UserPayload.cs | 20 + .../Scanner/Models/WorldPlayerPayload.cs | 7 + Daybreak/Services/UMod/Models/XORingStream.cs | 4 +- Daybreak/Views/FocusView.xaml.cs | 15 + Daybreak/Views/MetricsView.xaml | 11 +- Daybreak/Views/MetricsView.xaml.cs | 23 +- 47 files changed, 1050 insertions(+), 80 deletions(-) create mode 100644 Daybreak/Services/Scanner/Models/AttributePayload.cs create mode 100644 Daybreak/Services/Scanner/Models/BagContentPayload.cs create mode 100644 Daybreak/Services/Scanner/Models/BagPayload.cs create mode 100644 Daybreak/Services/Scanner/Models/BuildPayload.cs create mode 100644 Daybreak/Services/Scanner/Models/GameDataPayload.cs create mode 100644 Daybreak/Services/Scanner/Models/InventoryPayload.cs create mode 100644 Daybreak/Services/Scanner/Models/LivingEntityPayload.cs create mode 100644 Daybreak/Services/Scanner/Models/LoginPayload.cs create mode 100644 Daybreak/Services/Scanner/Models/MainPlayerPayload.cs create mode 100644 Daybreak/Services/Scanner/Models/MapIconPayload.cs create mode 100644 Daybreak/Services/Scanner/Models/MapPayload.cs create mode 100644 Daybreak/Services/Scanner/Models/PartyPlayerPayload.cs create mode 100644 Daybreak/Services/Scanner/Models/PathingMetadataPayload.cs create mode 100644 Daybreak/Services/Scanner/Models/PathingPayload.cs create mode 100644 Daybreak/Services/Scanner/Models/PathingTrapezoidPayload.cs create mode 100644 Daybreak/Services/Scanner/Models/PreGamePayload.cs create mode 100644 Daybreak/Services/Scanner/Models/QuestMetadataPayload.cs create mode 100644 Daybreak/Services/Scanner/Models/SessionPayload.cs create mode 100644 Daybreak/Services/Scanner/Models/TitlePayload.cs create mode 100644 Daybreak/Services/Scanner/Models/UserPayload.cs create mode 100644 Daybreak/Services/Scanner/Models/WorldPlayerPayload.cs diff --git a/Daybreak.GWCA/header/payloads/PathingTrapezoid.h b/Daybreak.GWCA/header/payloads/PathingTrapezoid.h index cfb03a7c..53bad843 100644 --- a/Daybreak.GWCA/header/payloads/PathingTrapezoid.h +++ b/Daybreak.GWCA/header/payloads/PathingTrapezoid.h @@ -8,6 +8,7 @@ using json = nlohmann::json; namespace Daybreak { struct PathingTrapezoid { uint32_t Id; + uint32_t PathingMapId; float XTL; float XTR; float XBL; diff --git a/Daybreak.GWCA/source/GameModule.cpp b/Daybreak.GWCA/source/GameModule.cpp index 8d453024..767c5d39 100644 --- a/Daybreak.GWCA/source/GameModule.cpp +++ b/Daybreak.GWCA/source/GameModule.cpp @@ -32,6 +32,10 @@ namespace Daybreak::Modules::GameModule { volatile bool initialized = false; std::string GetString(wchar_t* chars) { + if (!chars) { + return ""; + } + std::string str(64, '\0'); auto length = std::wcstombs(&str[0], chars, 64); str.resize(length); @@ -41,6 +45,10 @@ namespace Daybreak::Modules::GameModule { std::list GetQuestLog() { std::list questMetadatas; auto questLog = GW::QuestMgr::GetQuestLog(); + if (!questLog) { + return questMetadatas; + } + for (auto& quest : *questLog) { QuestMetadata metadata; metadata.FromId = (uint32_t)quest.map_from; @@ -88,6 +96,10 @@ namespace Daybreak::Modules::GameModule { std::list agentList; const GW::AgentArray* agents_ptr = GW::Agents::GetAgentArray(); GW::AgentArray* agents = GW::Agents::GetAgentArray(); + if (!agents) { + return agentList; + } + for (auto* a : *agents) { const GW::AgentLiving* agent = a ? a->GetAsAgentLiving() : nullptr; if (agent) { @@ -132,6 +144,10 @@ namespace Daybreak::Modules::GameModule { std::list players; auto worldContext = GW::GetWorldContext(); Temp::GWPlayer *tempPlayerArray = (Temp::GWPlayer*)worldContext->players.m_buffer; + if (!tempPlayerArray) { + return players; + } + for (auto i = 0; i < worldContext->players.m_size; i++) { if (tempPlayerArray->agent_id != 0) { players.push_back(*tempPlayerArray); @@ -145,6 +161,10 @@ namespace Daybreak::Modules::GameModule { std::map> GetSkillbars() { std::map> skillbarMap; auto skillbars = GW::SkillbarMgr::GetSkillbarArray(); + if (!skillbars) { + return skillbarMap; + } + for (auto& skillbar : *skillbars) { std::list skillList; skillList.push_back((uint32_t)skillbar.skills[0].skill_id); diff --git a/Daybreak.GWCA/source/MainPlayerModule.cpp b/Daybreak.GWCA/source/MainPlayerModule.cpp index baa99cac..6af986e0 100644 --- a/Daybreak.GWCA/source/MainPlayerModule.cpp +++ b/Daybreak.GWCA/source/MainPlayerModule.cpp @@ -32,6 +32,10 @@ namespace Daybreak::Modules::MainPlayerModule { volatile bool initialized = false; std::string GetString(wchar_t* chars) { + if (!chars) { + return ""; + } + std::string str(64, '\0'); auto length = std::wcstombs(&str[0], chars, 64); str.resize(length); @@ -41,6 +45,10 @@ namespace Daybreak::Modules::MainPlayerModule { std::list GetQuestLog() { std::list questMetadatas; auto questLog = GW::QuestMgr::GetQuestLog(); + if (!questLog) { + return questMetadatas; + } + for (auto& quest : *questLog) { QuestMetadata metadata; metadata.FromId = (uint32_t)quest.map_from; @@ -88,6 +96,10 @@ namespace Daybreak::Modules::MainPlayerModule { std::list agentList; const GW::AgentArray* agents_ptr = GW::Agents::GetAgentArray(); GW::AgentArray* agents = GW::Agents::GetAgentArray(); + if (!agents) { + return agentList; + } + for (auto* a : *agents) { const GW::AgentLiving* agent = a ? a->GetAsAgentLiving() : nullptr; if (agent) { @@ -132,6 +144,10 @@ namespace Daybreak::Modules::MainPlayerModule { std::list players; auto worldContext = GW::GetWorldContext(); Temp::GWPlayer *tempPlayerArray = (Temp::GWPlayer*)worldContext->players.m_buffer; + if (!tempPlayerArray) { + return players; + } + for (auto i = 0; i < worldContext->players.m_size; i++) { if (tempPlayerArray->agent_id != 0) { players.push_back(*tempPlayerArray); @@ -145,6 +161,10 @@ namespace Daybreak::Modules::MainPlayerModule { std::map> GetSkillbars() { std::map> skillbarMap; auto skillbars = GW::SkillbarMgr::GetSkillbarArray(); + if (!skillbars) { + return skillbarMap; + } + for (auto& skillbar : *skillbars) { std::list skillList; skillList.push_back((uint32_t)skillbar.skills[0].skill_id); diff --git a/Daybreak.GWCA/source/PathingModule.cpp b/Daybreak.GWCA/source/PathingModule.cpp index dd5fc30f..920b741b 100644 --- a/Daybreak.GWCA/source/PathingModule.cpp +++ b/Daybreak.GWCA/source/PathingModule.cpp @@ -31,6 +31,7 @@ namespace Daybreak::Modules::PathingModule { auto gwTrapezoid = gwPathingMap.trapezoids[j]; PathingTrapezoid trapezoid; trapezoid.Id = gwTrapezoid.id; + trapezoid.PathingMapId = i; trapezoid.XTL = gwTrapezoid.XTL; trapezoid.XBL = gwTrapezoid.XBL; trapezoid.XTR = gwTrapezoid.XTR; diff --git a/Daybreak.GWCA/source/dllmain.cpp b/Daybreak.GWCA/source/dllmain.cpp index d257d5ea..054a2771 100644 --- a/Daybreak.GWCA/source/dllmain.cpp +++ b/Daybreak.GWCA/source/dllmain.cpp @@ -30,7 +30,7 @@ static DWORD WINAPI ThreadProc(LPVOID lpModule) HMODULE hModule = static_cast(lpModule); #ifdef BUILD_TYPE_DEBUG AllocConsole(); - SetConsoleTitleA("PacketLogger Console"); + SetConsoleTitleA("Daybreak.GWCA Console"); freopen_s(&stdout_proxy, "CONOUT$", "w", stdout); freopen_s(&stderr_proxy, "CONOUT$", "w", stderr); #endif diff --git a/Daybreak.GWCA/source/payloads/PathingTrapezoid.cpp b/Daybreak.GWCA/source/payloads/PathingTrapezoid.cpp index 579be4e2..c17e1e80 100644 --- a/Daybreak.GWCA/source/payloads/PathingTrapezoid.cpp +++ b/Daybreak.GWCA/source/payloads/PathingTrapezoid.cpp @@ -10,6 +10,7 @@ namespace Daybreak { j = json { {"Id", p.Id}, + {"PathingMapId", p.PathingMapId}, {"XTL", p.XTL}, {"XTR", p.XTR}, {"XBL", p.XBL}, diff --git a/Daybreak/Converters/AttributeJsonConverter.cs b/Daybreak/Converters/AttributeJsonConverter.cs index 968b6495..2e7ea000 100644 --- a/Daybreak/Converters/AttributeJsonConverter.cs +++ b/Daybreak/Converters/AttributeJsonConverter.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using Daybreak.Models.Guildwars; +using Newtonsoft.Json; using System; namespace Daybreak.Converters; @@ -25,9 +26,9 @@ public sealed class AttributeJsonConverter : JsonConverter return namedAttribute; case JsonToken.Integer: - var id = reader.ReadAsInt32(); - if (id is not int || - !Models.Guildwars.Attribute.TryParse(id.Value, out var parsedAttribute)) + var id = reader.Value as long?; + if (id is not long || + !Models.Guildwars.Attribute.TryParse((int)id.Value, out var parsedAttribute)) { return default; } diff --git a/Daybreak/Converters/CampaignJsonConverter.cs b/Daybreak/Converters/CampaignJsonConverter.cs index 345a0969..c14da15d 100644 --- a/Daybreak/Converters/CampaignJsonConverter.cs +++ b/Daybreak/Converters/CampaignJsonConverter.cs @@ -26,9 +26,9 @@ public sealed class CampaignJsonConverter : JsonConverter return namedCampaign; case JsonToken.Integer: - var id = reader.ReadAsInt32(); - if (id is not int || - !Campaign.TryParse(id.Value, out var parsedCampaign)) + var id = reader.Value as long?; + if (id is not long || + !Campaign.TryParse((int)id.Value, out var parsedCampaign)) { return default; } diff --git a/Daybreak/Converters/ContinentJsonConverter.cs b/Daybreak/Converters/ContinentJsonConverter.cs index 2f005e5d..46773445 100644 --- a/Daybreak/Converters/ContinentJsonConverter.cs +++ b/Daybreak/Converters/ContinentJsonConverter.cs @@ -26,9 +26,9 @@ public sealed class ContinentJsonConverter : JsonConverter return namedContinent; case JsonToken.Integer: - var id = reader.ReadAsInt32(); - if (id is not int || - !Continent.TryParse(id.Value, out var parsedContinent)) + var id = reader.Value as long?; + if (id is not long || + !Continent.TryParse((int)id.Value, out var parsedContinent)) { return default; } diff --git a/Daybreak/Converters/GuildwarsIconJsonConverter.cs b/Daybreak/Converters/GuildwarsIconJsonConverter.cs index 21e0c7b5..b450d446 100644 --- a/Daybreak/Converters/GuildwarsIconJsonConverter.cs +++ b/Daybreak/Converters/GuildwarsIconJsonConverter.cs @@ -26,9 +26,9 @@ public sealed class GuildwarsIconJsonConverter : JsonConverter return namedGuildwarsIcon; case JsonToken.Integer: - var id = reader.ReadAsInt32(); - if (id is not int || - !GuildwarsIcon.TryParse(id.Value, out var parsedGuildwarsIcon)) + var id = reader.Value as long?; + if (id is not long || + !GuildwarsIcon.TryParse((int)id.Value, out var parsedGuildwarsIcon)) { return default; } diff --git a/Daybreak/Converters/ItemBaseJsonConverter.cs b/Daybreak/Converters/ItemBaseJsonConverter.cs index 36bcb916..42cc68ee 100644 --- a/Daybreak/Converters/ItemBaseJsonConverter.cs +++ b/Daybreak/Converters/ItemBaseJsonConverter.cs @@ -26,9 +26,9 @@ public sealed class ItemBaseJsonConverter : JsonConverter return namedItem; case JsonToken.Integer: - var id = reader.ReadAsInt32(); - if (id is not int || - !ItemBase.TryParse(id.Value, modifiers: default, out var parsedItem)) + var id = reader.Value as long?; + if (id is not long || + !ItemBase.TryParse((int)id.Value, null, out var parsedItem)) { return default; } diff --git a/Daybreak/Converters/MapJsonConverter.cs b/Daybreak/Converters/MapJsonConverter.cs index e6df1c99..c524b328 100644 --- a/Daybreak/Converters/MapJsonConverter.cs +++ b/Daybreak/Converters/MapJsonConverter.cs @@ -26,9 +26,9 @@ public sealed class MapJsonConverter : JsonConverter return namedMap; case JsonToken.Integer: - var id = reader.ReadAsInt32(); - if (id is not int || - !Map.TryParse(id.Value, out var parsedMap)) + var id = reader.Value as long?; + if (id is not long || + !Map.TryParse((int)id.Value, out var parsedMap)) { return default; } diff --git a/Daybreak/Converters/NpcJsonConverter.cs b/Daybreak/Converters/NpcJsonConverter.cs index 400256d8..48bdbdb5 100644 --- a/Daybreak/Converters/NpcJsonConverter.cs +++ b/Daybreak/Converters/NpcJsonConverter.cs @@ -27,9 +27,9 @@ public sealed class NpcJsonConverter : JsonConverter return namedNpc; case JsonToken.Integer: - var id = reader.ReadAsInt32(); - if (id is not int || - !Npc.TryParse(id.Value, out var parsedNpc)) + var id = reader.Value as long?; + if (id is not long || + !Npc.TryParse((int)id.Value, out var parsedNpc)) { return default; } diff --git a/Daybreak/Converters/ProfessionJsonConverter.cs b/Daybreak/Converters/ProfessionJsonConverter.cs index cdc9e5fc..568745cf 100644 --- a/Daybreak/Converters/ProfessionJsonConverter.cs +++ b/Daybreak/Converters/ProfessionJsonConverter.cs @@ -26,9 +26,9 @@ public sealed class ProfessionJsonConverter : JsonConverter return namedProfession; case JsonToken.Integer: - var id = reader.ReadAsInt32(); - if (id is not int || - !Profession.TryParse(id.Value, out var parsedProfession)) + var id = reader.Value as long?; + if (id is not long || + !Profession.TryParse((int)id.Value, out var parsedProfession)) { return default; } diff --git a/Daybreak/Converters/QuestJsonConverter.cs b/Daybreak/Converters/QuestJsonConverter.cs index 883cb84e..2399ac55 100644 --- a/Daybreak/Converters/QuestJsonConverter.cs +++ b/Daybreak/Converters/QuestJsonConverter.cs @@ -26,9 +26,9 @@ public sealed class QuestJsonConverter : JsonConverter return namedQuest; case JsonToken.Integer: - var id = reader.ReadAsInt32(); - if (id is not int || - !Quest.TryParse(id.Value, out var parsedQuest)) + var id = reader.Value as long?; + if (id is not long || + !Quest.TryParse((int)id.Value, out var parsedQuest)) { return default; } diff --git a/Daybreak/Converters/RegionJsonConverter.cs b/Daybreak/Converters/RegionJsonConverter.cs index b0754089..385825bf 100644 --- a/Daybreak/Converters/RegionJsonConverter.cs +++ b/Daybreak/Converters/RegionJsonConverter.cs @@ -26,9 +26,9 @@ public sealed class RegionJsonConverter : JsonConverter return namedRegion; case JsonToken.Integer: - var id = reader.ReadAsInt32(); - if (id is not int || - !Region.TryParse(id.Value, out var parsedRegion)) + var id = reader.Value as long?; + if (id is not long || + !Region.TryParse((int)id.Value, out var parsedRegion)) { return default; } diff --git a/Daybreak/Converters/SkillJsonConverter.cs b/Daybreak/Converters/SkillJsonConverter.cs index 4e10aaed..3e96e364 100644 --- a/Daybreak/Converters/SkillJsonConverter.cs +++ b/Daybreak/Converters/SkillJsonConverter.cs @@ -26,9 +26,9 @@ public sealed class SkillJsonConverter : JsonConverter return namedSkill; case JsonToken.Integer: - var id = reader.ReadAsInt32(); - if (id is not int || - !Skill.TryParse(id.Value, out var parsedSkill)) + var id = reader.Value as long?; + if (id is not long || + !Skill.TryParse((int)id.Value, out var parsedSkill)) { return default; } diff --git a/Daybreak/Converters/TitleJsonConverter.cs b/Daybreak/Converters/TitleJsonConverter.cs index 7743a801..28e56ea1 100644 --- a/Daybreak/Converters/TitleJsonConverter.cs +++ b/Daybreak/Converters/TitleJsonConverter.cs @@ -26,9 +26,9 @@ public sealed class TitleJsonConverter : JsonConverter return namedTitle; case JsonToken.Integer: - var id = reader.ReadAsInt32(); - if (id is not int || - !Title.TryParse(id.Value, out var parsedTitle)) + var id = reader.Value as long?; + if (id is not long || + !Title.TryParse((int)id.Value, out var parsedTitle)) { return default; } diff --git a/Daybreak/Daybreak.csproj b/Daybreak/Daybreak.csproj index 7c83f8ca..3b200614 100644 --- a/Daybreak/Daybreak.csproj +++ b/Daybreak/Daybreak.csproj @@ -91,8 +91,8 @@ - - + + diff --git a/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs b/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs index f54405c1..e7549e8e 100644 --- a/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs +++ b/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs @@ -258,38 +258,10 @@ public void RestartDaybreakAsNormalUser() CloseHandle(clientHandle); } - /* - * Run the actions one by one, to avoid injection issues - */ - foreach (var mod in mods) - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(this.launcherOptions.Value.ModStartupTimeout)); - try - { - await mod.OnGuildwarsStarted(process, cts.Token); - } - catch (TaskCanceledException) - { - this.logger.LogError($"{mod.Name} timeout"); - this.notificationService.NotifyError( - title: $"{mod.Name} timeout", - description: $"Mod timed out while processing {nameof(mod.OnGuildwarsStarted)}"); - } - catch (Exception e) - { - this.KillGuildWarsProcess(process); - this.logger.LogError(e, $"{mod.Name} unhandled exception"); - this.notificationService.NotifyError( - title: $"{mod.Name} exception", - description: $"Mod encountered exception of type {e.GetType().Name} while processing {nameof(mod.OnGuildwarsStarted)}"); - return default; - } - } - var retries = 0; while (retries < MaxRetries) { - await Task.Delay(100); + await Task.Delay(1000); retries++; var gwProcess = Process.GetProcessesByName("gw").FirstOrDefault(); if (gwProcess is null && retries < MaxRetries) @@ -322,6 +294,34 @@ public void RestartDaybreakAsNormalUser() continue; } + /* + * Run the actions one by one, to avoid injection issues + */ + foreach (var mod in mods) + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(this.launcherOptions.Value.ModStartupTimeout)); + try + { + await mod.OnGuildwarsStarted(process, cts.Token); + } + catch (TaskCanceledException) + { + this.logger.LogError($"{mod.Name} timeout"); + this.notificationService.NotifyError( + title: $"{mod.Name} timeout", + description: $"Mod timed out while processing {nameof(mod.OnGuildwarsStarted)}"); + } + catch (Exception e) + { + this.KillGuildWarsProcess(process); + this.logger.LogError(e, $"{mod.Name} unhandled exception"); + this.notificationService.NotifyError( + title: $"{mod.Name} exception", + description: $"Mod encountered exception of type {e.GetType().Name} while processing {nameof(mod.OnGuildwarsStarted)}"); + return default; + } + } + return gwProcess; } diff --git a/Daybreak/Services/Downloads/DownloadService.cs b/Daybreak/Services/Downloads/DownloadService.cs index fee78967..06121cda 100644 --- a/Daybreak/Services/Downloads/DownloadService.cs +++ b/Daybreak/Services/Downloads/DownloadService.cs @@ -46,7 +46,7 @@ public async Task DownloadFile(string downloadUri, string destinationPath, this.logger.LogInformation("Beginning download"); var fileInfo = new FileInfo(destinationPath); fileInfo.Directory?.Create(); - var fileStream = File.Open(destinationPath, FileMode.CreateNew, FileAccess.Write); + var fileStream = File.Open(destinationPath, FileMode.Create, FileAccess.Write); var downloadSize = (double)response.Content!.Headers!.ContentLength!; var buffer = new byte[1024]; var length = 0; diff --git a/Daybreak/Services/Scanner/GWCAMemoryReader.cs b/Daybreak/Services/Scanner/GWCAMemoryReader.cs index 03b024d2..88f5cab9 100644 --- a/Daybreak/Services/Scanner/GWCAMemoryReader.cs +++ b/Daybreak/Services/Scanner/GWCAMemoryReader.cs @@ -7,13 +7,20 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; +using System.Extensions; +using Daybreak.Services.Scanner.Models; +using System.Linq; +using System.Collections.Generic; +using Daybreak.Models.Builds; +using System.Windows; +using Daybreak.Utils; namespace Daybreak.Services.Scanner; public sealed class GWCAMemoryReader : IGuildwarsMemoryReader { private readonly IGWCAClient client; - private readonly ILogger logger; + private readonly ILogger logger; private ConnectionContext? connectionContextCache; @@ -40,115 +47,772 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat public async Task ReadGameData(CancellationToken cancellationToken) { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.ReadGameData), string.Empty); if (this.connectionContextCache is null) { return default; } var response = await this.client.GetAsync(this.connectionContextCache.Value, "game", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + + try + { + var payload = await response.Content.ReadAsStringAsync(cancellationToken); + var gameData = payload.Deserialize(); + if (gameData is null) + { + return default; + } + + return new GameData + { + Valid = true, + CurrentTargetId = (int)gameData.TargetId, + LivingEntities = gameData.LivingEntities?.Select(ParsePayload).ToList(), + MainPlayer = gameData.MainPlayer is not null ? ParsePayload(gameData.MainPlayer) : new MainPlayerInformation(), + Party = gameData.Party?.Select(ParsePayload).ToList() ?? new List(), + WorldPlayers = gameData.WorldPlayers?.Select(ParsePayload).ToList() ?? new List(), + MapIcons = gameData.MapIcons?.Select(m => + { + if (!GuildwarsIcon.TryParse((int)m.Id, out var icon)) + { + return default; + } + + return new MapIcon + { + Affiliation = icon.Affiliation, + Icon = icon, + Position = new Position + { + X = m.PosX, + Y = m.PosY + } + }; + }).OfType().ToList() ?? new List() + }; + } + catch(Exception ex) + { + scopedLogger.LogError(ex, "Encountered exception while parsing response"); + } + return default; } public async Task ReadInventoryData(CancellationToken cancellationToken) { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.ReadInventoryData), string.Empty); if (this.connectionContextCache is null) { return default; } var response = await this.client.GetAsync(this.connectionContextCache.Value, "inventory", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + + try + { + var payload = await response.Content.ReadAsStringAsync(cancellationToken); + var inventoryData = payload.Deserialize(); + if (inventoryData is null) + { + return default; + } + + return new InventoryData + { + Backpack = ParsePayload(inventoryData.Backpack!), + BeltPouch = ParsePayload(inventoryData.BeltPouch!), + EquipmentPack = ParsePayload(inventoryData.EquipmentPack!), + EquippedItems = ParsePayload(inventoryData.EquippedItems!), + MaterialStorage = ParsePayload(inventoryData.MaterialStorage!), + UnclaimedItems = ParsePayload(inventoryData.UnclaimedItems!), + Bags = inventoryData.Bags?.Select(ParsePayload)?.ToList()!, + StoragePanes = inventoryData.StoragePanes?.Select(ParsePayload).ToList()! + }; + } + catch (Exception ex) + { + scopedLogger.LogError(ex, "Encountered exception while parsing response"); + } + return default; } public async Task ReadLoginData(CancellationToken cancellationToken) { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.ReadLoginData), string.Empty); if (this.connectionContextCache is null) { return default; } - var response = await this.client.GetAsync(this.connectionContextCache.Value, "game", cancellationToken); + var response = await this.client.GetAsync(this.connectionContextCache.Value, "login", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + + try + { + var payload = await response.Content.ReadAsStringAsync(cancellationToken); + var loginData = payload.Deserialize(); + if (loginData is null) + { + return default; + } + + return new LoginData + { + Email = loginData.Email ?? string.Empty, + PlayerName = loginData.PlayerName ?? string.Empty + }; + } + catch (Exception ex) + { + scopedLogger.LogError(ex, "Encountered exception while parsing response"); + } + return default; } public async Task ReadMainPlayerData(CancellationToken cancellationToken) { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.ReadMainPlayerData), string.Empty); if (this.connectionContextCache is null) { return default; } var response = await this.client.GetAsync(this.connectionContextCache.Value, "game/mainplayer", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + + try + { + var payload = await response.Content.ReadAsStringAsync(cancellationToken); + var playerData = payload.Deserialize(); + if (playerData is null) + { + return default; + } + + return new MainPlayerData + { + PlayerInformation = ParsePayload(playerData) + }; + } + catch (Exception ex) + { + scopedLogger.LogError(ex, "Encountered exception while parsing response"); + } + return default; } public async Task ReadPathingData(CancellationToken cancellationToken) { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.ReadPathingData), string.Empty); if (this.connectionContextCache is null) { return default; } var response = await this.client.GetAsync(this.connectionContextCache.Value, "pathing", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + + try + { + var payload = await response.Content.ReadAsStringAsync(cancellationToken); + var pathingPayload = payload.Deserialize(); + if (pathingPayload is null) + { + return default; + } + + var trapezoidList = pathingPayload.Trapezoids?.Select(pathingTrapezoid => + new Trapezoid + { + Id = (int)pathingTrapezoid.Id, + PathingMapId = (int)pathingTrapezoid.PathingMapId, + YT = pathingTrapezoid.YT, + YB = pathingTrapezoid.YB, + XBL = pathingTrapezoid.XBL, + XBR = pathingTrapezoid.XBR, + XTL = pathingTrapezoid.XTL, + XTR = pathingTrapezoid.XTR + }).ToList() ?? new List(); + var adjacencyList = pathingPayload.AdjacencyList ?? new List>(); + var pathingMapsCount = pathingPayload.Trapezoids?.Max(p => p.PathingMapId) ?? 0; + var originalPathingMaps = new List>((int)pathingMapsCount); + foreach(var trapezoid in trapezoidList) + { + while (originalPathingMaps.Count <= trapezoid.PathingMapId) + { + originalPathingMaps.Add(new List()); + } + + originalPathingMaps[trapezoid.PathingMapId].Add(trapezoid.Id); + } + + var computedPathingMaps = BuildPathingMaps(trapezoidList, adjacencyList); + var computedAdjacencyList = BuildFinalAdjacencyList(trapezoidList, computedPathingMaps, adjacencyList); + return new PathingData + { + Trapezoids = trapezoidList, + OriginalAdjacencyList = adjacencyList, + OriginalPathingMaps = originalPathingMaps, + ComputedAdjacencyList = computedAdjacencyList, + ComputedPathingMaps = computedPathingMaps + }; + } + catch (Exception ex) + { + scopedLogger.LogError(ex, "Encountered exception while parsing response"); + } + return default; } public async Task ReadPathingMetaData(CancellationToken cancellationToken) { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.ReadPathingMetaData), string.Empty); if (this.connectionContextCache is null) { return default; } var response = await this.client.GetAsync(this.connectionContextCache.Value, "pathing/metadata", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + + try + { + var payload = await response.Content.ReadAsStringAsync(cancellationToken); + var pathingMetadataPayload = payload.Deserialize(); + if (pathingMetadataPayload is null) + { + return default; + } + + return new PathingMetadata + { + TrapezoidCount = (int)pathingMetadataPayload.TrapezoidCount + }; + } + catch (Exception ex) + { + scopedLogger.LogError(ex, "Encountered exception while parsing response"); + } + return default; } public async Task ReadPreGameData(CancellationToken cancellationToken) { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.ReadPreGameData), string.Empty); if (this.connectionContextCache is null) { return default; } var response = await this.client.GetAsync(this.connectionContextCache.Value, "pregame", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + + try + { + var payload = await response.Content.ReadAsStringAsync(cancellationToken); + var preGamePayload = payload.Deserialize(); + if (preGamePayload is null) + { + return default; + } + + return new PreGameData + { + Characters = preGamePayload.Characters, + ChosenCharacterIndex = preGamePayload.ChosenCharacterIndex + }; + } + catch (Exception ex) + { + scopedLogger.LogError(ex, "Encountered exception while parsing response"); + } + return default; } public async Task ReadSessionData(CancellationToken cancellationToken) { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.ReadSessionData), string.Empty); if (this.connectionContextCache is null) { return default; } var response = await this.client.GetAsync(this.connectionContextCache.Value, "session", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + + try + { + var payload = await response.Content.ReadAsStringAsync(cancellationToken); + var sessionPayload = payload.Deserialize(); + if (sessionPayload is null) + { + return default; + } + + _ = Map.TryParse((int)sessionPayload.MapId, out var map); + return new SessionData + { + Session = new SessionInformation + { + CurrentMap = map, + FoesKilled = sessionPayload.FoesKilled, + FoesToKill = sessionPayload.FoesToKill, + InstanceTimer = sessionPayload.InstanceTimer, + InstanceType = sessionPayload.InstanceType switch + { + 0 => InstanceType.Outpost, + 1 => InstanceType.Explorable, + 2 => InstanceType.Loading, + _ => InstanceType.Undefined + } + } + }; + } + catch (Exception ex) + { + scopedLogger.LogError(ex, "Encountered exception while parsing response"); + } + return default; } public async Task ReadUserData(CancellationToken cancellationToken) { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.ReadUserData), string.Empty); if (this.connectionContextCache is null) { return default; } var response = await this.client.GetAsync(this.connectionContextCache.Value, "user", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + + try + { + var payload = await response.Content.ReadAsStringAsync(cancellationToken); + var userPayload = payload.Deserialize(); + if (userPayload is null) + { + return default; + } + + return new UserData + { + User = new UserInformation + { + Email = userPayload.Email, + CurrentKurzickPoints = userPayload.CurrentKurzickPoints, + CurrentLuxonPoints = userPayload.CurrentLuxonPoints, + CurrentImperialPoints = userPayload.CurrentImperialPoints, + CurrentBalthazarPoints = userPayload.CurrentBalthazarPoints, + CurrentSkillPoints = userPayload.CurrentSkillPoints, + TotalKurzickPoints = userPayload.TotalKurzickPoints, + TotalLuxonPoints = userPayload.TotalLuxonPoints, + TotalImperialPoints = userPayload.TotalImperialPoints, + TotalBalthazarPoints = userPayload.TotalBalthazarPoints, + TotalSkillPoints = userPayload.TotalSkillPoints, + MaxKurzickPoints = userPayload.MaxKurzickPoints, + MaxLuxonPoints = userPayload.MaxLuxonPoints, + MaxBalthazarPoints = userPayload.MaxBalthazarPoints, + MaxImperialPoints = userPayload.MaxImperialPoints + } + }; + } + catch (Exception ex) + { + scopedLogger.LogError(ex, "Encountered exception while parsing response"); + } + return default; } public async Task ReadWorldData(CancellationToken cancellationToken) { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.ReadWorldData), string.Empty); if (this.connectionContextCache is null) { return default; } var response = await this.client.GetAsync(this.connectionContextCache.Value, "map", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + + try + { + var payload = await response.Content.ReadAsStringAsync(cancellationToken); + var mapPayload = payload.Deserialize(); + if (mapPayload is null) + { + return default; + } + + _ = Campaign.TryParse((int)mapPayload.Campaign, out var campaign); + _ = Continent.TryParse((int)mapPayload.Continent, out var continent); + _ = Region.TryParse((int)mapPayload.Region, out var region); + _ = Map.TryParse((int)mapPayload.Id, out var map); + + return new WorldData + { + Campaign = campaign, + Continent = continent, + Map = map, + Region = region + }; + } + catch (Exception ex) + { + scopedLogger.LogError(ex, "Encountered exception while parsing response"); + } + return default; } public void Stop() { } + + private static IBagContent ParsePayload(BagContentPayload bagContentPayload) + { + var modifiers = bagContentPayload.Modifiers?.Select(mod => (ItemModifier)mod); + return ItemBase.TryParse((int)bagContentPayload.Id, modifiers, out var item) ? + new BagItem + { + Item = item!, + Count = bagContentPayload.Count, + Modifiers = modifiers!, + Slot = bagContentPayload.Slot + } : + new UnknownBagItem + { + ItemId = bagContentPayload.Id, + Count = bagContentPayload.Count, + Modifiers = modifiers!, + Slot = bagContentPayload.Slot + }; + } + + private static Bag ParsePayload(BagPayload bagPayload) + { + if (bagPayload is null) + { + return new Bag(); + } + + return new Bag + { + Capacity = bagPayload.Items?.Count ?? 0, + Items = bagPayload.Items?.Select(ParsePayload).ToList() ?? new List() + }; + } + + private static MainPlayerInformation ParsePayload(MainPlayerPayload mainPlayerPayload) + { + var worldPlayer = ParsePayload((WorldPlayerPayload)mainPlayerPayload); + _ = Quest.TryParse((int)mainPlayerPayload.CurrentQuest, out var quest); + return new MainPlayerInformation + { + Name = worldPlayer.Name, + CurrentBuild = worldPlayer.CurrentBuild, + CurrentEnergy = mainPlayerPayload.CurrentEnergy, + CurrentHealth = mainPlayerPayload.CurrentHp, + MaxEnergy = mainPlayerPayload.MaxEnergy, + MaxHealth = mainPlayerPayload.MaxHp, + Experience = mainPlayerPayload.Experience, + HardModeUnlocked = mainPlayerPayload.HardModeUnlocked, + Id = worldPlayer.Id, + Level = worldPlayer.Level, + Morale = mainPlayerPayload.Morale, + Position = worldPlayer.Position, + Quest = quest, + PrimaryProfession = worldPlayer.PrimaryProfession, + SecondaryProfession = worldPlayer.SecondaryProfession, + Timer = worldPlayer.Timer, + TitleInformation = worldPlayer.TitleInformation, + UnlockedProfession = worldPlayer.UnlockedProfession, + QuestLog = mainPlayerPayload.QuestLog?.Select(metadata => + { + if (!Quest.TryParse((int)metadata.Id, out var q)) + { + return default; + } + + _ = Map.TryParse((int)metadata.FromId, out var from); + _ = Map.TryParse((int)metadata.ToId, out var to); + return new QuestMetadata + { + From = from, + To = to, + Position = new Position + { + X = metadata.PosX, + Y = metadata.PosY + }, + Quest = q + }; + }).OfType().ToList() + }; + } + + private static WorldPlayerInformation ParsePayload(WorldPlayerPayload worldPlayerPayload) + { + var partyPlayer = ParsePayload((PartyPlayerPayload)worldPlayerPayload); + _ = Title.TryParse((int)(worldPlayerPayload.Title?.Id ?? (uint)Title.None.Id), out var title); + return new WorldPlayerInformation + { + CurrentBuild = partyPlayer.CurrentBuild, + Position = partyPlayer.Position, + PrimaryProfession = partyPlayer.PrimaryProfession, + SecondaryProfession = partyPlayer.SecondaryProfession, + Id = partyPlayer.Id, + Level = partyPlayer.Level, + Name = worldPlayerPayload.Name, + Timer = partyPlayer.Timer, + UnlockedProfession = partyPlayer.UnlockedProfession, + TitleInformation = new TitleInformation + { + CurrentPoints = worldPlayerPayload.Title?.CurrentPoints, + IsPercentage = worldPlayerPayload.Title?.IsPercentage, + MaxTierNumber = worldPlayerPayload.Title?.MaxTierNumber, + TierNumber = worldPlayerPayload.Title?.TierNumber, + PointsForCurrentRank = worldPlayerPayload.Title?.PointsForCurrentRank, + PointsForNextRank = worldPlayerPayload.Title?.PointsForNextRank, + Title = title + } + }; + } + + private static PlayerInformation ParsePayload(PartyPlayerPayload partyPlayerPayload) + { + var livingEntity = ParsePayload((LivingEntityPayload)partyPlayerPayload); + var build = new Build + { + Primary = livingEntity.PrimaryProfession ?? Profession.None, + Secondary = livingEntity.SecondaryProfession ?? Profession.None, + Attributes = partyPlayerPayload.Build?.Attributes?.Select(a => + { + if (!Daybreak.Models.Guildwars.Attribute.TryParse((int)a.Id, out var attribute)) + { + return default; + } + + return new AttributeEntry + { + Attribute = attribute, + Points = (int)a.BaseLevel + }; + }).OfType().ToList() ?? new List(), + Skills = partyPlayerPayload.Build?.Skills?.Select(s => + { + if(!Skill.TryParse((int)s, out var skill)) + { + return Skill.NoSkill; + } + + return skill; + }).ToList() ?? Enumerable.Repeat(Skill.NoSkill, 8).ToList() + }; + return new PlayerInformation + { + Position = livingEntity.Position, + PrimaryProfession = livingEntity.PrimaryProfession, + SecondaryProfession = livingEntity.SecondaryProfession, + CurrentBuild = build, + Id = livingEntity.Id, + Level = livingEntity.Level, + NpcDefinition = livingEntity.NpcDefinition, + ModelType = livingEntity.ModelType ?? 0, + Timer = livingEntity.Timer, + UnlockedProfession = partyPlayerPayload.UnlockedProfession?.Select(id => + { + if (Profession.TryParse((int)id, out var profession)) + { + return profession; + } + + return default; + }).OfType().ToList() ?? new List() + }; + } + + private static LivingEntity ParsePayload(LivingEntityPayload livingEntityPayload) + { + _ = Npc.TryParse((int)livingEntityPayload.NpcDefinition, out var npc); + _ = Profession.TryParse((int)livingEntityPayload.PrimaryProfessionId, out var primaryProfession); + _ = Profession.TryParse((int)livingEntityPayload.SecondaryProfessionId, out var secondaryProfession); + + return new LivingEntity + { + Allegiance = (LivingEntityAllegiance)livingEntityPayload.EntityAllegiance, + Id = (int)livingEntityPayload.Id, + Level = (int)livingEntityPayload.Level, + ModelType = livingEntityPayload.NpcDefinition, + NpcDefinition = npc, + PrimaryProfession = primaryProfession, + SecondaryProfession = secondaryProfession, + State = (LivingEntityState)livingEntityPayload.EntityState, + Timer = livingEntityPayload.Timer, + Position = new Position + { + X = livingEntityPayload.PosX, + Y = livingEntityPayload.PosY, + } + }; + } + + 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); + } } diff --git a/Daybreak/Services/Scanner/Models/AttributePayload.cs b/Daybreak/Services/Scanner/Models/AttributePayload.cs new file mode 100644 index 00000000..a60f519a --- /dev/null +++ b/Daybreak/Services/Scanner/Models/AttributePayload.cs @@ -0,0 +1,10 @@ +namespace Daybreak.Services.Scanner.Models; + +internal class AttributePayload +{ + public uint Id { get; set; } + public uint ActualLevel { get; set; } + public uint BaseLevel { get; set; } + public uint IncrementPoints { get; set; } + public uint DecrementPoints { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/BagContentPayload.cs b/Daybreak/Services/Scanner/Models/BagContentPayload.cs new file mode 100644 index 00000000..d0f50cd4 --- /dev/null +++ b/Daybreak/Services/Scanner/Models/BagContentPayload.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Daybreak.Services.Scanner.Models; +internal sealed class BagContentPayload +{ + public uint Id { get; set; } + public uint Slot { get; set; } + public uint Count { get; set; } + public List? Modifiers { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/BagPayload.cs b/Daybreak/Services/Scanner/Models/BagPayload.cs new file mode 100644 index 00000000..618601e9 --- /dev/null +++ b/Daybreak/Services/Scanner/Models/BagPayload.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; + +namespace Daybreak.Services.Scanner.Models; +internal class BagPayload +{ + public List? Items { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/BuildPayload.cs b/Daybreak/Services/Scanner/Models/BuildPayload.cs new file mode 100644 index 00000000..4c3cac35 --- /dev/null +++ b/Daybreak/Services/Scanner/Models/BuildPayload.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Daybreak.Services.Scanner.Models; + +internal class BuildPayload +{ + public List? Attributes { get; set; } + public uint Primary { get; set; } + public uint Secondary { get; set; } + public List? Skills { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/GameDataPayload.cs b/Daybreak/Services/Scanner/Models/GameDataPayload.cs new file mode 100644 index 00000000..d41e3ef6 --- /dev/null +++ b/Daybreak/Services/Scanner/Models/GameDataPayload.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; + +namespace Daybreak.Services.Scanner.Models; +internal sealed class GameDataPayload +{ + public List? LivingEntities { get; set; } + public MainPlayerPayload? MainPlayer { get; set; } + public List? MapIcons { get; set; } + public List? Party { get; set; } + public List? WorldPlayers { get; set; } + public uint TargetId { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/InventoryPayload.cs b/Daybreak/Services/Scanner/Models/InventoryPayload.cs new file mode 100644 index 00000000..53aa5cd7 --- /dev/null +++ b/Daybreak/Services/Scanner/Models/InventoryPayload.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Daybreak.Services.Scanner.Models; + +internal class InventoryPayload +{ + public uint GoldInStorage { get; set; } + public uint GoldOnCharacter { get; set; } + public BagPayload? Backpack { get; set; } + public BagPayload? BeltPouch { get; set; } + public BagPayload? EquipmentPack { get; set; } + public BagPayload? MaterialStorage { get; set; } + public BagPayload? UnclaimedItems { get; set; } + public BagPayload? EquippedItems { get; set; } + public List? Bags { get; set; } + public List? StoragePanes { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/LivingEntityPayload.cs b/Daybreak/Services/Scanner/Models/LivingEntityPayload.cs new file mode 100644 index 00000000..fe383cd8 --- /dev/null +++ b/Daybreak/Services/Scanner/Models/LivingEntityPayload.cs @@ -0,0 +1,15 @@ +namespace Daybreak.Services.Scanner.Models; + +internal class LivingEntityPayload +{ + public uint Id { get; set; } + public uint EntityState { get; set; } + public uint EntityAllegiance { get; set; } + public uint Level { get; set; } + public uint NpcDefinition { get; set; } + public float PosX { get; set; } + public float PosY { get; set; } + public uint PrimaryProfessionId { get; set; } + public uint SecondaryProfessionId { get; set; } + public uint Timer { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/LoginPayload.cs b/Daybreak/Services/Scanner/Models/LoginPayload.cs new file mode 100644 index 00000000..e80486ce --- /dev/null +++ b/Daybreak/Services/Scanner/Models/LoginPayload.cs @@ -0,0 +1,6 @@ +namespace Daybreak.Services.Scanner.Models; +internal sealed class LoginPayload +{ + public string? Email { get; set; } + public string? PlayerName { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/MainPlayerPayload.cs b/Daybreak/Services/Scanner/Models/MainPlayerPayload.cs new file mode 100644 index 00000000..ff748950 --- /dev/null +++ b/Daybreak/Services/Scanner/Models/MainPlayerPayload.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Daybreak.Services.Scanner.Models; + +internal class MainPlayerPayload : WorldPlayerPayload +{ + public uint CurrentEnergy { get; set; } + public uint CurrentHp { get; set; } + public uint CurrentQuest { get; set; } + public uint Experience { get; set; } + public bool HardModeUnlocked { get; set; } + public uint MaxHp { get; set; } + public uint MaxEnergy { get; set; } + public uint Morale { get; set; } + public List? QuestLog { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/MapIconPayload.cs b/Daybreak/Services/Scanner/Models/MapIconPayload.cs new file mode 100644 index 00000000..0dee713b --- /dev/null +++ b/Daybreak/Services/Scanner/Models/MapIconPayload.cs @@ -0,0 +1,8 @@ +namespace Daybreak.Services.Scanner.Models; +internal class MapIconPayload +{ + public uint Affiliation { get; set; } + public uint Id { get; set; } + public float PosX { get; set; } + public float PosY { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/MapPayload.cs b/Daybreak/Services/Scanner/Models/MapPayload.cs new file mode 100644 index 00000000..a67eae43 --- /dev/null +++ b/Daybreak/Services/Scanner/Models/MapPayload.cs @@ -0,0 +1,13 @@ +namespace Daybreak.Services.Scanner.Models; + +internal sealed class MapPayload +{ + public bool IsLoaded { get; set; } + public uint Id { get; set; } + public uint InstanceType { get; set; } + public uint Timer { get; set; } + public uint Campaign { get; set; } + public uint Continent { get; set; } + public uint Region { get; set; } + public uint Type { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/PartyPlayerPayload.cs b/Daybreak/Services/Scanner/Models/PartyPlayerPayload.cs new file mode 100644 index 00000000..ff9d19e9 --- /dev/null +++ b/Daybreak/Services/Scanner/Models/PartyPlayerPayload.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Daybreak.Services.Scanner.Models; + +internal class PartyPlayerPayload : LivingEntityPayload +{ + public BuildPayload? Build { get; set; } + public List? UnlockedProfession { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/PathingMetadataPayload.cs b/Daybreak/Services/Scanner/Models/PathingMetadataPayload.cs new file mode 100644 index 00000000..a8669155 --- /dev/null +++ b/Daybreak/Services/Scanner/Models/PathingMetadataPayload.cs @@ -0,0 +1,5 @@ +namespace Daybreak.Services.Scanner.Models; +internal sealed class PathingMetadataPayload +{ + public uint TrapezoidCount { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/PathingPayload.cs b/Daybreak/Services/Scanner/Models/PathingPayload.cs new file mode 100644 index 00000000..e1bff55e --- /dev/null +++ b/Daybreak/Services/Scanner/Models/PathingPayload.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Daybreak.Services.Scanner.Models; +internal sealed class PathingPayload +{ + public List? Trapezoids { get; set; } + public List>? AdjacencyList { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/PathingTrapezoidPayload.cs b/Daybreak/Services/Scanner/Models/PathingTrapezoidPayload.cs new file mode 100644 index 00000000..ee939961 --- /dev/null +++ b/Daybreak/Services/Scanner/Models/PathingTrapezoidPayload.cs @@ -0,0 +1,12 @@ +namespace Daybreak.Services.Scanner.Models; +internal sealed class PathingTrapezoidPayload +{ + public uint Id { get; set; } + public uint PathingMapId { get; set; } + public float XTL { get; set; } + public float XTR { get; set; } + public float XBL { get; set; } + public float XBR { get; set; } + public float YT { get; set; } + public float YB { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/PreGamePayload.cs b/Daybreak/Services/Scanner/Models/PreGamePayload.cs new file mode 100644 index 00000000..0c424d38 --- /dev/null +++ b/Daybreak/Services/Scanner/Models/PreGamePayload.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Daybreak.Services.Scanner.Models; +internal sealed class PreGamePayload +{ + public int ChosenCharacterIndex { get; set; } + public List? Characters { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/QuestMetadataPayload.cs b/Daybreak/Services/Scanner/Models/QuestMetadataPayload.cs new file mode 100644 index 00000000..614680a8 --- /dev/null +++ b/Daybreak/Services/Scanner/Models/QuestMetadataPayload.cs @@ -0,0 +1,10 @@ +namespace Daybreak.Services.Scanner.Models; + +internal class QuestMetadataPayload +{ + public uint FromId { get; set; } + public uint Id { get; set; } + public float PosX { get; set; } + public float PosY { get; set; } + public uint ToId { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/SessionPayload.cs b/Daybreak/Services/Scanner/Models/SessionPayload.cs new file mode 100644 index 00000000..f4ff0164 --- /dev/null +++ b/Daybreak/Services/Scanner/Models/SessionPayload.cs @@ -0,0 +1,9 @@ +namespace Daybreak.Services.Scanner.Models; +internal sealed class SessionPayload +{ + public uint FoesKilled { get; set; } + public uint FoesToKill { get; set; } + public uint MapId { get; set; } + public uint InstanceTimer { get; set; } + public uint InstanceType { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/TitlePayload.cs b/Daybreak/Services/Scanner/Models/TitlePayload.cs new file mode 100644 index 00000000..9d11194d --- /dev/null +++ b/Daybreak/Services/Scanner/Models/TitlePayload.cs @@ -0,0 +1,12 @@ +namespace Daybreak.Services.Scanner.Models; + +internal class TitlePayload +{ + public uint CurrentPoints { get; set; } + public uint Id { get; set; } + public bool IsPercentage { get; set; } + public uint MaxTierNumber { get; set; } + public uint PointsForCurrentRank { get; set; } + public uint PointsForNextRank { get; set; } + public uint TierNumber { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/UserPayload.cs b/Daybreak/Services/Scanner/Models/UserPayload.cs new file mode 100644 index 00000000..adfad661 --- /dev/null +++ b/Daybreak/Services/Scanner/Models/UserPayload.cs @@ -0,0 +1,20 @@ +namespace Daybreak.Services.Scanner.Models; + +internal sealed class UserPayload +{ + public string? Email { get; set; } + public uint CurrentKurzickPoints { get; set; } + public uint CurrentLuxonPoints { get; set; } + public uint CurrentImperialPoints { get; set; } + public uint CurrentBalthazarPoints { get; set; } + public uint CurrentSkillPoints { get; set; } + public uint TotalKurzickPoints { get; set; } + public uint TotalLuxonPoints { get; set; } + public uint TotalImperialPoints { get; set; } + public uint TotalBalthazarPoints { get; set; } + public uint TotalSkillPoints { get; set; } + public uint MaxKurzickPoints { get; set; } + public uint MaxLuxonPoints { get; set; } + public uint MaxImperialPoints { get; set; } + public uint MaxBalthazarPoints { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/WorldPlayerPayload.cs b/Daybreak/Services/Scanner/Models/WorldPlayerPayload.cs new file mode 100644 index 00000000..14f0378e --- /dev/null +++ b/Daybreak/Services/Scanner/Models/WorldPlayerPayload.cs @@ -0,0 +1,7 @@ +namespace Daybreak.Services.Scanner.Models; + +internal class WorldPlayerPayload : PartyPlayerPayload +{ + public string? Name { get; set; } + public TitlePayload? Title { get; set; } +} diff --git a/Daybreak/Services/UMod/Models/XORingStream.cs b/Daybreak/Services/UMod/Models/XORingStream.cs index ed7860a4..0a6cd755 100644 --- a/Daybreak/Services/UMod/Models/XORingStream.cs +++ b/Daybreak/Services/UMod/Models/XORingStream.cs @@ -53,7 +53,7 @@ public XORStream(Stream stream) this.innerStream.Position -= 2; //-1 for the ReadByte and -1 to go one position in the back } - var lastZeroPosition = this.innerStream.Position; + var lastZeroPosition = this.innerStream.Position - 1; this.innerStream.Position = 0; this.innerStreamLength = lastZeroPosition; } @@ -160,7 +160,7 @@ private byte XOR(byte b, long position) */ if (position > this.originalStreamLength - 4) { - return (byte)(b ^ TPF_XOROdd); + return (byte)(b ^ TPF_XOREven); } return (position % 2 == 0) ? (byte)(b ^ TPF_XOREven) : (byte)(b ^ TPF_XOROdd); diff --git a/Daybreak/Views/FocusView.xaml.cs b/Daybreak/Views/FocusView.xaml.cs index d549c8e5..79b170d4 100644 --- a/Daybreak/Views/FocusView.xaml.cs +++ b/Daybreak/Views/FocusView.xaml.cs @@ -164,6 +164,11 @@ await Task.WhenAll( Task.Delay(1000, cancellationToken)).ConfigureAwait(true); } + catch (InvalidOperationException ex) + { + this.logger.LogError(ex, "Encountered invalid operation exception. Cancelling periodic reading"); + return; + } catch (Exception ex) { this.logger.LogError(ex, "Encountered non-terminating exception. Silently continuing"); @@ -202,6 +207,11 @@ await Task.WhenAll( this.GameData = maybeGameData; } + catch (InvalidOperationException ex) + { + this.logger.LogError(ex, "Encountered invalid operation exception. Cancelling periodic reading"); + return; + } catch (Exception ex) { this.logger.LogError(ex, "Encountered non-terminating exception. Silently continuing"); @@ -304,6 +314,11 @@ await Task.WhenAll( this.MainPlayerDataValid = true; this.Browser.Visibility = this.minimapMaximized ? Visibility.Hidden : Visibility.Visible; } + catch (InvalidOperationException ex) + { + this.logger.LogError(ex, "Encountered invalid operation exception. Cancelling periodic main player reading"); + return; + } catch (Exception ex) { this.logger.LogError(ex, "Encountered non-terminating exception. Silently continuing"); diff --git a/Daybreak/Views/MetricsView.xaml b/Daybreak/Views/MetricsView.xaml index 32767ddb..c8520aa8 100644 --- a/Daybreak/Views/MetricsView.xaml +++ b/Daybreak/Views/MetricsView.xaml @@ -38,6 +38,11 @@ + + + + + - + diff --git a/Daybreak/Views/MetricsView.xaml.cs b/Daybreak/Views/MetricsView.xaml.cs index 9464512c..c9d58d3a 100644 --- a/Daybreak/Views/MetricsView.xaml.cs +++ b/Daybreak/Views/MetricsView.xaml.cs @@ -26,6 +26,7 @@ namespace Daybreak.Views; public partial class MetricsView : UserControl { private readonly SolidColorPaint backgroundPaint; + private readonly SolidColorPaint transparentPaint = new(new SKColor(0, 0, 0, 0)); private readonly SolidColorPaint foregroundPaint; private readonly SolidColorPaint accentPaint; private readonly IMetricsService metricsService; @@ -102,13 +103,25 @@ metricSet.Metrics is null || return; } + cartesianChart.DrawMargin = new LiveChartsCore.Measure.Margin(30); cartesianChart.XAxes = new Axis[] { new Axis { Name = "Time", + Labeler = (ticks) => + { + return new DateTime((long)ticks).ToString("HH:mm:ss"); + }, LabelsPaint = this.foregroundPaint, - Labeler = (ticks) => new DateTime((long)ticks).ToString("HH:mm:ss") + SeparatorsPaint = this.transparentPaint, + CrosshairLabelsPaint = this.transparentPaint, + CrosshairPaint = this.transparentPaint, + NamePaint = this.foregroundPaint, + SubseparatorsPaint = this.transparentPaint, + SubticksPaint = this.transparentPaint, + TicksPaint = this.transparentPaint, + ZeroPaint = this.transparentPaint, } }; @@ -118,6 +131,14 @@ metricSet.Metrics is null || { Name = metricSet.Instrument.Unit, LabelsPaint = this.foregroundPaint, + SeparatorsPaint = this.transparentPaint, + CrosshairLabelsPaint = this.transparentPaint, + CrosshairPaint = this.transparentPaint, + NamePaint = this.foregroundPaint, + SubseparatorsPaint = this.transparentPaint, + SubticksPaint = this.transparentPaint, + TicksPaint = this.transparentPaint, + ZeroPaint = this.transparentPaint, } };