From 65830f91250d4d1622d4b9f3b1e920e83ebdb7df Mon Sep 17 00:00:00 2001 From: Macocian Alexandru Victor Date: Sun, 18 Feb 2024 22:55:07 +0100 Subject: [PATCH] Release 0.9.9.18 (#595) * Setup release 0.9.9.18 * Minimap transparency Closes #594 * Toggle to display exploration percentage Closes #593 --- Daybreak.GWCA/header/TitleInfoModule.h | 6 + .../header/payloads/TitleInfoPayload.h | 19 ++ Daybreak.GWCA/source/TitleInfoModule.cpp | 164 ++++++++++++++++++ Daybreak.GWCA/source/dllmain.cpp | 2 + .../source/payloads/TitleInfoPayload.cpp | 20 +++ .../Configuration/Options/FocusViewOptions.cs | 8 + Daybreak/Daybreak.csproj | 2 +- Daybreak/Launch/MinimapWindow.xaml | 16 +- Daybreak/Launch/MinimapWindow.xaml.cs | 2 + .../Models/FocusView/CartoProgressContext.cs | 64 +++++++ Daybreak/Models/Guildwars/Title.cs | 6 +- .../Guildwars/TitleInformationExtended.cs | 12 ++ Daybreak/Services/Scanner/GWCAMemoryReader.cs | 47 +++++ .../Scanner/IGuildwarsMemoryReader.cs | 1 + .../Scanner/Models/TitleInfoPayload.cs | 11 ++ Daybreak/Views/FocusView.xaml | 29 +++- Daybreak/Views/FocusView.xaml.cs | 133 +++++++++++++- GWCA | 2 +- 18 files changed, 529 insertions(+), 15 deletions(-) create mode 100644 Daybreak.GWCA/header/TitleInfoModule.h create mode 100644 Daybreak.GWCA/header/payloads/TitleInfoPayload.h create mode 100644 Daybreak.GWCA/source/TitleInfoModule.cpp create mode 100644 Daybreak.GWCA/source/payloads/TitleInfoPayload.cpp create mode 100644 Daybreak/Models/FocusView/CartoProgressContext.cs create mode 100644 Daybreak/Models/Guildwars/TitleInformationExtended.cs create mode 100644 Daybreak/Services/Scanner/Models/TitleInfoPayload.cs diff --git a/Daybreak.GWCA/header/TitleInfoModule.h b/Daybreak.GWCA/header/TitleInfoModule.h new file mode 100644 index 00000000..f787bcf9 --- /dev/null +++ b/Daybreak.GWCA/header/TitleInfoModule.h @@ -0,0 +1,6 @@ +#pragma once +#include "httplib.h" + +namespace Daybreak::Modules::TitleInfoModule { + void GetTitleInfo(const httplib::Request& req, httplib::Response& res); +} \ No newline at end of file diff --git a/Daybreak.GWCA/header/payloads/TitleInfoPayload.h b/Daybreak.GWCA/header/payloads/TitleInfoPayload.h new file mode 100644 index 00000000..9478a534 --- /dev/null +++ b/Daybreak.GWCA/header/payloads/TitleInfoPayload.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include + +using json = nlohmann::json; + +namespace Daybreak { + struct TitleInfoPayload { + uint32_t TitleId = 0; + uint32_t TitleTierId = 0; + uint32_t CurrentTier = 0; + uint32_t CurrentPoints = 0; + uint32_t PointsNeededNextRank = 0; + bool IsPercentageBased = false; + std::string TitleName = ""; + }; + + void to_json(json& j, const TitleInfoPayload& p); +} \ No newline at end of file diff --git a/Daybreak.GWCA/source/TitleInfoModule.cpp b/Daybreak.GWCA/source/TitleInfoModule.cpp new file mode 100644 index 00000000..6b775bc3 --- /dev/null +++ b/Daybreak.GWCA/source/TitleInfoModule.cpp @@ -0,0 +1,164 @@ +#include "pch.h" +#include "TitleInfoModule.h" +#include "payloads/TitleInfoPayload.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "Utils.h" + +namespace Daybreak::Modules::TitleInfoModule { + const int MaxTries = 20; + std::vector*, std::wstring*, int>> WaitingList; + std::queue>*> PromiseQueue; + std::mutex GameThreadMutex; + GW::HookEntry GameThreadHook; + volatile bool initialized = false; + + std::wstring* GetAsyncName(uint32_t titleTierIndex) { + const auto worldContext = GW::GetWorldContext(); + const auto tiers = &worldContext->title_tiers; + const auto tier = &tiers->at(titleTierIndex); + auto description = new std::wstring(); + GW::UI::AsyncDecodeStr(tier->tier_name_enc, description); + return description; + } + + TitleInfoPayload GetPayload(uint32_t id) { + TitleInfoPayload payload; + if (!GW::Map::GetIsMapLoaded()) { + return payload; + } + + const auto title = GW::PlayerMgr::GetTitleTrack((GW::Constants::TitleID)id); + if (!title) { + return payload; + } + + if (title->current_points == 0 && + title->current_title_tier_index == 0 && + title->next_title_tier_index == 0 && + title->max_title_rank == 0) { + return payload; + } + + const auto tierIndex = title->current_title_tier_index; + const auto worldContext = GW::GetWorldContext(); + const auto tiers = &worldContext->title_tiers; + const auto tier = &tiers->at(tierIndex); + + payload.TitleId = id; + payload.TitleTierId = tierIndex; + payload.CurrentPoints = title->current_points; + payload.PointsNeededNextRank = title->points_needed_next_rank; + payload.IsPercentageBased = title->is_percentage_based(); + payload.CurrentTier = tier->tier_number; + return payload; + } + + void EnsureInitialized() { + GameThreadMutex.lock(); + if (!initialized) { + GW::GameThread::RegisterGameThreadCallback(&GameThreadHook, [&](GW::HookStatus*) { + while (!PromiseQueue.empty()) { + auto promiseRequest = PromiseQueue.front(); + std::promise &promise = std::get<1>(*promiseRequest); + uint32_t id = std::get<0>(*promiseRequest); + PromiseQueue.pop(); + try { + const auto payload = GetPayload(id); + if (payload.TitleId == 0 && + payload.CurrentPoints == 0 && + payload.PointsNeededNextRank == 0 && + payload.TitleTierId == 0) { + TitleInfoPayload payload; + promise.set_value(payload); + continue; + } + + auto name = GetAsyncName(payload.TitleTierId); + if (!name) { + continue; + } + + WaitingList.emplace_back(payload, &promise, name, 0); + } + catch (...) { + TitleInfoPayload payload; + promise.set_value(payload); + } + } + + for (auto i = 0; i < WaitingList.size(); ) { + auto item = &WaitingList[i]; + auto name = std::get<2>(*item); + auto& tries = std::get<3>(*item); + if (name->empty() && + tries < MaxTries) { + tries += 1; + i++; + continue; + } + + auto promise = std::get<1>(*item); + auto payload = std::get<0>(*item); + payload.TitleName = Utils::WStringToString(*name); + WaitingList.erase(WaitingList.begin() + i); + delete(name); + promise->set_value(payload); + } + }); + + initialized = true; + } + + GameThreadMutex.unlock(); + } + + void GetTitleInfo(const httplib::Request& req, httplib::Response& res) { + auto callbackEntry = new GW::HookEntry; + uint32_t id = 0; + auto it = req.params.find("id"); + if (it == req.params.end()) { + res.status = 400; + res.set_content("Missing id parameter", "text/plain"); + return; + } + else { + auto idStr = it->second; + size_t pos = 0; + auto result = std::stoul(idStr, &pos); + if (pos != idStr.size()) { + res.status = 400; + res.set_content("Invalid id parameter", "text/plain"); + return; + } + + id = static_cast(result); + } + + auto response = new std::tuple>(); + std::get<0>(*response) = id; + std::promise& promise = std::get<1>(*response); + + EnsureInitialized(); + PromiseQueue.emplace(response); + + json responsePayload = promise.get_future().get(); + delete callbackEntry; + delete response; + res.set_content(responsePayload.dump(), "text/json"); + } +} \ No newline at end of file diff --git a/Daybreak.GWCA/source/dllmain.cpp b/Daybreak.GWCA/source/dllmain.cpp index 69315e6a..333b8c6d 100644 --- a/Daybreak.GWCA/source/dllmain.cpp +++ b/Daybreak.GWCA/source/dllmain.cpp @@ -24,6 +24,7 @@ #include "EntityNameModule.h" #include "GameStateModule.h" #include "ItemNameModule.h" +#include "TitleInfoModule.h" #include volatile bool initialized; @@ -72,6 +73,7 @@ static DWORD WINAPI StartHttpServer(LPVOID) http::server::Get("/session", Daybreak::Modules::SessionModule::GetSessionInfo); http::server::Get("/entities/name", Daybreak::Modules::EntityNameModule::GetName); http::server::Get("/items/name", Daybreak::Modules::ItemNameModule::GetName); + http::server::Get("/titles/info", Daybreak::Modules::TitleInfoModule::GetTitleInfo); http::server::StartServer(); return 0; } diff --git a/Daybreak.GWCA/source/payloads/TitleInfoPayload.cpp b/Daybreak.GWCA/source/payloads/TitleInfoPayload.cpp new file mode 100644 index 00000000..20785188 --- /dev/null +++ b/Daybreak.GWCA/source/payloads/TitleInfoPayload.cpp @@ -0,0 +1,20 @@ +#include +#include +#include + +using json = nlohmann::json; + +namespace Daybreak { + void to_json(json& j, const TitleInfoPayload& p) { + j = json + { + {"TitleId", p.TitleId}, + {"TitleTierId", p.TitleTierId}, + {"TitleName", p.TitleName}, + {"CurrentPoints", p.CurrentPoints}, + {"PointsNeededNextRank", p.PointsNeededNextRank}, + {"IsPercentageBased", p.IsPercentageBased}, + {"CurrentTier", p.CurrentTier} + }; + } +} \ No newline at end of file diff --git a/Daybreak/Configuration/Options/FocusViewOptions.cs b/Daybreak/Configuration/Options/FocusViewOptions.cs index 0275d36d..46be64f7 100644 --- a/Daybreak/Configuration/Options/FocusViewOptions.cs +++ b/Daybreak/Configuration/Options/FocusViewOptions.cs @@ -54,6 +54,14 @@ public sealed class FocusViewOptions [OptionName(Name = "Minimap Rotation", Description = "When enabled, the minimap will rotate according to the player camera")] public bool MinimapRotationEnabled { get; set; } = true; + [JsonProperty(nameof(MinimapTransparency))] + [OptionName(Name = "Minimap Transparency", Description = "When enabled, the minimap window will be transparent")] + public bool MinimapTransparency { get; set; } = true; + + [JsonProperty(nameof(ShowCartoProgress))] + [OptionName(Name = "Show Cartographer Progress", Description = "When enabled, the minimap will show live cartographer progress")] + public bool ShowCartoProgress { get; set; } = false; + [JsonProperty(nameof(BrowserHistory))] [OptionIgnore] public BrowserHistory BrowserHistory { get; set; } = new(); diff --git a/Daybreak/Daybreak.csproj b/Daybreak/Daybreak.csproj index c1e23235..8eb80e7a 100644 --- a/Daybreak/Daybreak.csproj +++ b/Daybreak/Daybreak.csproj @@ -11,7 +11,7 @@ preview Daybreak.ico true - 0.9.9.17 + 0.9.9.18 true cfb2a489-db80-448d-a969-80270f314c46 True diff --git a/Daybreak/Launch/MinimapWindow.xaml b/Daybreak/Launch/MinimapWindow.xaml index 096b944e..8f6d6979 100644 --- a/Daybreak/Launch/MinimapWindow.xaml +++ b/Daybreak/Launch/MinimapWindow.xaml @@ -19,19 +19,29 @@ ShowSystemMenuOnRightClick="False" ShowInTaskbar="True" ShowCloseButton="True" + Background="Transparent" + AllowsTransparency="True" Title="Daybreak Minimap" Topmost="{Binding ElementName=_this, Path=Pinned, Mode=OneWay}" x:Name="_this" Height="450" Width="800"> - + + + - + - diff --git a/Daybreak/Launch/MinimapWindow.xaml.cs b/Daybreak/Launch/MinimapWindow.xaml.cs index 5f582f1b..da79a8c0 100644 --- a/Daybreak/Launch/MinimapWindow.xaml.cs +++ b/Daybreak/Launch/MinimapWindow.xaml.cs @@ -8,6 +8,8 @@ namespace Daybreak.Launch; /// public partial class MinimapWindow : MetroWindow { + [GenerateDependencyProperty] + private bool opaque = false; [GenerateDependencyProperty] private bool pinned = false; [GenerateDependencyProperty] diff --git a/Daybreak/Models/FocusView/CartoProgressContext.cs b/Daybreak/Models/FocusView/CartoProgressContext.cs new file mode 100644 index 00000000..240499dd --- /dev/null +++ b/Daybreak/Models/FocusView/CartoProgressContext.cs @@ -0,0 +1,64 @@ +using Daybreak.Models.Guildwars; +using System.ComponentModel; + +namespace Daybreak.Models.FocusView; +public sealed class CartoProgressContext : INotifyPropertyChanged +{ + private TitleInformationExtended? titleInformationExtended; + private double percentage; + private Continent? continent; + private string? resolvedName; + private string? titleName; + + public event PropertyChangedEventHandler? PropertyChanged; + + public TitleInformationExtended? TitleInformationExtended + { + get => this.titleInformationExtended; + set + { + this.titleInformationExtended = value; + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.TitleInformationExtended))); + } + } + + public double Percentage + { + get => this.percentage; + set + { + this.percentage = value; + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Percentage))); + } + } + + public Continent? Continent + { + get => this.continent; + set + { + this.continent = value; + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Continent))); + } + } + + public string? ResolvedName + { + get => this.resolvedName; + set + { + this.resolvedName = value; + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.ResolvedName))); + } + } + + public string? TitleName + { + get => this.titleName; + set + { + this.titleName = value; + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.TitleName))); + } + } +} diff --git a/Daybreak/Models/Guildwars/Title.cs b/Daybreak/Models/Guildwars/Title.cs index e6828f3a..cefe45bf 100644 --- a/Daybreak/Models/Guildwars/Title.cs +++ b/Daybreak/Models/Guildwars/Title.cs @@ -11,8 +11,8 @@ public sealed class Title : IWikiEntity { public static readonly Title None = new() { Id = 0xFF, }; public static readonly Title Hero = new() { Id = 0, Name = "Hero", WikiUrl = "https://wiki.guildwars.com/wiki/Hero_(title)", Tiers = ["Hero", "Fierce Hero", "Mighty Hero", "Deadly Hero", "Terrifying Hero", "Conquering Hero", "Subjugating Hero", "Vanquishing Hero", "Renowed Hero", "Illustrious Hero", "Eminent Hero", "King's Hero", "Emperor's Hero", "Balthazar's Hero", "Legendary Hero"] }; - public static readonly Title TyrianCartographer = new() { Id = 1, Name = "Cartographer", WikiUrl = "https://wiki.guildwars.com/wiki/Cartographer", Tiers = ["Tyrian Explorer", "Tyrian Pathfinder", "Tyrian Trailblazer", "Tyrian Cartographer", "Tyrian Master Cartographer", "Tyrian Grandmaster Cartographer"] }; - public static readonly Title CanthanCartographer = new() { Id = 2, Name = "Cartographer", WikiUrl = "https://wiki.guildwars.com/wiki/Cartographer", Tiers = ["Canthan Explorer", "Canthan Pathfinder", "Canthan Trailblazer", "Canthan Cartographer", "Canthan Master Cartographer", "Canthan Grandmaster Cartographer"] }; + public static readonly Title TyrianCartographer = new() { Id = 1, Name = "Tyrian Cartographer", WikiUrl = "https://wiki.guildwars.com/wiki/Cartographer", Tiers = ["Tyrian Explorer", "Tyrian Pathfinder", "Tyrian Trailblazer", "Tyrian Cartographer", "Tyrian Master Cartographer", "Tyrian Grandmaster Cartographer"] }; + public static readonly Title CanthanCartographer = new() { Id = 2, Name = "Canthan Cartographer", WikiUrl = "https://wiki.guildwars.com/wiki/Cartographer", Tiers = ["Canthan Explorer", "Canthan Pathfinder", "Canthan Trailblazer", "Canthan Cartographer", "Canthan Master Cartographer", "Canthan Grandmaster Cartographer"] }; public static readonly Title Gladiator = new() { Id = 3, Name = "Gladiator", WikiUrl = "https://wiki.guildwars.com/wiki/Gladiator", Tiers = ["Gladiator", "Fierce Gladiator", "Mighty Gladiator", "Deadly Gladiator", "Terrifying Gladiator", "Conquering Gladiator", "Subjugating Gladiator", "Vanquishing Gladiator", "King's Gladiator", "Emperor's Gladiator", "Balthazar's Gladiator", "Legendary Gladiator"] }; public static readonly Title Champion = new() { Id = 4, Name = "Champion", WikiUrl = "https://wiki.guildwars.com/wiki/Champion", Tiers = ["Champion", "Fierce Champion", "Mighty Champion", "Deadly Champion", "Terrifying Champion", "Conquering Champion", "Subjugating Champion", "Vanquishing Champion", "King's Champion", "Emperor's Champion", "Balthazar's Champion", "Legendary Champion"] }; public static readonly Title Kurzick = new() { Id = 5, Name = "Faction Allegiance", WikiUrl = "https://wiki.guildwars.com/wiki/Allegiance_rank", Tiers = ["Kurzick Supporter", "Friend of the Kurzicks", "Companion of the Kurzicks", "Ally of the Kurzicks", "Sentinel of the Kurzicks", "Steward of the Kurzicks", "Defender of the Kurzicks", "Warden of the Kurzicks", "Bastion of the Kurzicks", "Champion of the Kurzicks", "Hero of the Kurzicks", "Savior of the Kurzicks"] }; @@ -25,7 +25,7 @@ public sealed class Title : IWikiEntity public static readonly Title Lucky = new() { Id = 15, Name = "Lucky", WikiUrl = "https://wiki.guildwars.com/wiki/Lucky_and_Unlucky", Tiers = ["Charmed", "Lucky", "Favored", "Prosperous", "Golden", "Blessed by Fate"] }; public static readonly Title Unlucky = new() { Id = 16, Name = "Unlucky", WikiUrl = "https://wiki.guildwars.com/wiki/Lucky_and_Unlucky", Tiers = ["Hapless", "Unlucky", "Unfavored", "Tragic", "Wretched", "Jinxed", "Cursed by Fate"] }; public static readonly Title Sunspear = new() { Id = 17, Name = "Sunspear", WikiUrl = "https://wiki.guildwars.com/wiki/Sunspear_rank", Tiers = ["Sunspear Sergeant", "Sunspear Master Sergeant", "Second Spear", "First Spear", "Sunspear Captain", "Sunspear Commander", "Sunspear General", "Sunspear Castellan", "Spearmarshal", "Legendary Spearmarshal"] }; - public static readonly Title ElonianCartographer = new() { Id = 18, Name = "Cartographer", WikiUrl = "https://wiki.guildwars.com/wiki/Cartographer", Tiers = ["Elonian Explorer", "Elonian Pathfinder", "Elonian Trailblazer", "Elonian Cartographer", "Elonian Master Cartographer", "Elonian Grandmaster Cartographer"] }; + public static readonly Title ElonianCartographer = new() { Id = 18, Name = "Elonian Cartographer", WikiUrl = "https://wiki.guildwars.com/wiki/Cartographer", Tiers = ["Elonian Explorer", "Elonian Pathfinder", "Elonian Trailblazer", "Elonian Cartographer", "Elonian Master Cartographer", "Elonian Grandmaster Cartographer"] }; public static readonly Title ProtectorElona = new() { Id = 19, Name = "Protector of Elona", WikiUrl = "https://wiki.guildwars.com/wiki/Protector", Tiers = ["Protector of Elona"] }; public static readonly Title Lightbringer = new() { Id = 20, Name = "Lightbringer", WikiUrl = "https://wiki.guildwars.com/wiki/Lightbringer_rank", Tiers = ["Lightbringer", "Adept Lightbringer", "Brave Lightbringer", "Mighty Lightbringer", "Conquering Lightbringer", "Vanquishing Lightbringer", "Revered Lightbringer", "Holy Lightbringer"] }; public static readonly Title LegendaryDefenderOfAscalon = new() { Id = 21, Name = "Defender of Ascalon", WikiUrl = "https://wiki.guildwars.com/wiki/Defender_of_Ascalon", Tiers = ["Legendary Defender of Ascalon"] }; diff --git a/Daybreak/Models/Guildwars/TitleInformationExtended.cs b/Daybreak/Models/Guildwars/TitleInformationExtended.cs new file mode 100644 index 00000000..a0a874f4 --- /dev/null +++ b/Daybreak/Models/Guildwars/TitleInformationExtended.cs @@ -0,0 +1,12 @@ +namespace Daybreak.Models.Guildwars; + +public sealed class TitleInformationExtended +{ + public string? TitleName { get; set; } + public bool? IsPercentageBased { get; set; } + public uint CurrentPoints { get; set; } + public uint PointsNeededNextRank { get; set; } + public uint TitleId { get; set; } + public uint TitleTierId { get; set; } + public uint CurrentTier { get; set; } +} diff --git a/Daybreak/Services/Scanner/GWCAMemoryReader.cs b/Daybreak/Services/Scanner/GWCAMemoryReader.cs index 313b00c4..19bea654 100644 --- a/Daybreak/Services/Scanner/GWCAMemoryReader.cs +++ b/Daybreak/Services/Scanner/GWCAMemoryReader.cs @@ -669,6 +669,53 @@ public async Task EnsureInitialized(uint processId, CancellationToken cancellati return default; } + public async Task GetTitleInformation(int id, CancellationToken cancellationToken) + { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.GetTitleInformation), string.Empty); + if (this.connectionContextCache is null) + { + return default; + } + + try + { + var response = await this.client.GetAsync(this.connectionContextCache.Value, $"titles/info?id={id}", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + + var payload = await response.Content.ReadAsStringAsync(cancellationToken); + var titleInfoPayload = payload.Deserialize(); + if (titleInfoPayload is null || + titleInfoPayload.TitleId != id || + (titleInfoPayload.CurrentPoints == 0 && titleInfoPayload.PointsNeededNextRank == 0)) + { + return default; + } + + return new TitleInformationExtended + { + CurrentPoints = titleInfoPayload.CurrentPoints, + TitleId = titleInfoPayload.TitleId, + TitleName = titleInfoPayload.TitleName, + TitleTierId = titleInfoPayload.TitleTierId, + IsPercentageBased = titleInfoPayload.IsPercentageBased, + PointsNeededNextRank = titleInfoPayload.PointsNeededNextRank, + CurrentTier = titleInfoPayload.CurrentTier + }; + + } + catch (Exception ex) + { + scopedLogger.LogError(ex, "Encountered exception while parsing response"); + this.faulty = true; + } + + return default; + } + public void Stop() { this.connectionContextCache = default; diff --git a/Daybreak/Services/Scanner/IGuildwarsMemoryReader.cs b/Daybreak/Services/Scanner/IGuildwarsMemoryReader.cs index 36d231d7..ff7a635a 100644 --- a/Daybreak/Services/Scanner/IGuildwarsMemoryReader.cs +++ b/Daybreak/Services/Scanner/IGuildwarsMemoryReader.cs @@ -21,5 +21,6 @@ public interface IGuildwarsMemoryReader Task ReadGameState(CancellationToken cancellationToken); Task GetEntityName(IEntity entity, CancellationToken cancellationToken); Task GetItemName(int id, List modifiers, CancellationToken cancellationToken); + Task GetTitleInformation(int id, CancellationToken cancellationToken); void Stop(); } diff --git a/Daybreak/Services/Scanner/Models/TitleInfoPayload.cs b/Daybreak/Services/Scanner/Models/TitleInfoPayload.cs new file mode 100644 index 00000000..8bc3a13e --- /dev/null +++ b/Daybreak/Services/Scanner/Models/TitleInfoPayload.cs @@ -0,0 +1,11 @@ +namespace Daybreak.Services.Scanner.Models; +public sealed class TitleInfoPayload +{ + public uint CurrentPoints { get; set; } + public uint PointsNeededNextRank { get; set; } + public uint TitleId { get; set; } + public uint TitleTierId { get; set; } + public uint CurrentTier { get; set; } + public string? TitleName { get; set; } + public bool IsPercentageBased { get; set; } +} diff --git a/Daybreak/Views/FocusView.xaml b/Daybreak/Views/FocusView.xaml index bd9eb228..ca7fe918 100644 --- a/Daybreak/Views/FocusView.xaml +++ b/Daybreak/Views/FocusView.xaml @@ -128,7 +128,7 @@ @@ -148,18 +148,39 @@ ProfessionClicked="GuildwarsMinimap_ProfessionClicked" NpcNameClicked="GuildwarsMinimap_NpcNameClicked" ClipToBounds="True" - Background="{DynamicResource Daybreak.Brushes.Background}" + Background="Transparent" BorderBrush="{DynamicResource MahApps.Brushes.ThemeForeground}" BorderThickness="1"/> + + + + + + + + + Background="{DynamicResource Daybreak.Brushes.Background}"> c.Regions?.Any(r => r.Maps?.Contains(maybeMap) is true) is true); + if (maybeCurrentContinent == Continent.Tyria) + { + var titleInfo = (await this.guildwarsMemoryReader.GetTitleInformation(Title.TyrianCartographer.Id, cancellationToken))!; + this.CartoTitle = titleInfo is null ? + default : + new CartoProgressContext + { + Continent = Continent.Tyria, + ResolvedName = titleInfo.TitleName, + Percentage = titleInfo.CurrentPoints / 10d, + TitleInformationExtended = titleInfo, + TitleName = Title.TyrianCartographer.Name + }; + } + else if (maybeCurrentContinent == Continent.Cantha) + { + var titleInfo = (await this.guildwarsMemoryReader.GetTitleInformation(Title.CanthanCartographer.Id, cancellationToken))!; + this.CartoTitle = titleInfo is null ? + default : + new CartoProgressContext + { + Continent = Continent.Cantha, + ResolvedName = titleInfo.TitleName, + Percentage = titleInfo.CurrentPoints / 10d, + TitleInformationExtended = titleInfo, + TitleName = Title.CanthanCartographer.Name + }; + } + else if (maybeCurrentContinent == Continent.Elona) + { + var titleInfo = (await this.guildwarsMemoryReader.GetTitleInformation(Title.ElonianCartographer.Id, cancellationToken))!; + this.CartoTitle = titleInfo is null ? + default : + new CartoProgressContext + { + Continent = Continent.Elona, + ResolvedName = titleInfo.TitleName, + Percentage = titleInfo.CurrentPoints / 10d, + TitleInformationExtended = titleInfo, + TitleName = Title.ElonianCartographer.Name + }; + } + + this.ShowCartoProgress = this.CartoTitle is not null; + } + else + { + this.ShowCartoProgress = false; + } + + retries = 0; + } + catch (InvalidOperationException ex) + { + scopedLogger.LogError(ex, "Encountered invalid operation exception. Cancelling periodic reading"); + return; + } + catch (Exception ex) when (ex is TimeoutException or OperationCanceledException or HttpRequestException) + { + if (this.DataContext is not GuildWarsApplicationLaunchContext context) + { + continue; + } + + scopedLogger.LogError(ex, "Encountered timeout. Verifying connection"); + try + { + await this.guildwarsMemoryCache.EnsureInitialized(context, cancellationToken); + } + catch (InvalidOperationException innerEx) + { + retries++; + if (retries >= MaxRetries) + { + scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Returning to launcher view"); + this.notificationService.NotifyError( + title: "GuildWars unresponsive", + description: "Could not connect to Guild Wars instance. Returning to Launcher view"); + this.viewManager.ShowView(); + } + else + { + scopedLogger.LogError(innerEx, "Could not ensure connection is initialized. Backing off before retrying"); + await Task.Delay(UninitializedBackoff, cancellationToken); + } + } + } + catch (Exception ex) + { + scopedLogger.LogError(ex, "Encountered non-terminating exception. Silently continuing"); + } + } + } + private async void PeriodicallyReadGameData(CancellationToken cancellationToken) { var scopedLogger = this.logger.CreateScopedLogger(nameof(this.PeriodicallyReadGameData), string.Empty); @@ -608,6 +735,7 @@ private async void FocusView_Loaded(object _, RoutedEventArgs e) this.PeriodicallyReadGameState(cancellationToken); this.PeriodicallyReadGameData(cancellationToken); this.PeriodicallyReadPathingData(cancellationToken); + this.PeriodicallyReadCartoData(cancellationToken); } } @@ -785,7 +913,8 @@ private void MinimapExtractButton_Clicked(object sender, EventArgs e) this.minimapWindow = new() { - Resources = this.Resources + Resources = this.Resources, + Opaque = !this.liveUpdateableOptions.Value.MinimapTransparency }; var minimapWindowOptions = this.minimapWindowOptions.Value.ThrowIfNull(); diff --git a/GWCA b/GWCA index 37ff5a33..45975456 160000 --- a/GWCA +++ b/GWCA @@ -1 +1 @@ -Subproject commit 37ff5a33a47901d616e1e880c484c1cbf98a767c +Subproject commit 45975456afedfbcbd2c4dee397b31d3ad63423e3