diff --git a/Daybreak.GWCA/header/EntityNameModule.h b/Daybreak.GWCA/header/EntityNameModule.h new file mode 100644 index 00000000..41fcaecd --- /dev/null +++ b/Daybreak.GWCA/header/EntityNameModule.h @@ -0,0 +1,6 @@ +#pragma once +#include "httplib.h" + +namespace Daybreak::Modules::EntityNameModule { + void GetName(const httplib::Request& req, httplib::Response& res); +} \ No newline at end of file diff --git a/Daybreak.GWCA/header/GameStateModule.h b/Daybreak.GWCA/header/GameStateModule.h new file mode 100644 index 00000000..5eb74750 --- /dev/null +++ b/Daybreak.GWCA/header/GameStateModule.h @@ -0,0 +1,6 @@ +#pragma once +#include "httplib.h" + +namespace Daybreak::Modules::GameStateModule { + void GetGameStateInfo(const httplib::Request&, httplib::Response& res); +} \ No newline at end of file diff --git a/Daybreak.GWCA/header/NameModule.h b/Daybreak.GWCA/header/ItemNameModule.h similarity index 69% rename from Daybreak.GWCA/header/NameModule.h rename to Daybreak.GWCA/header/ItemNameModule.h index 724b40a9..f235d44c 100644 --- a/Daybreak.GWCA/header/NameModule.h +++ b/Daybreak.GWCA/header/ItemNameModule.h @@ -1,6 +1,6 @@ #pragma once #include "httplib.h" -namespace Daybreak::Modules::NameModule { +namespace Daybreak::Modules::ItemNameModule { void GetName(const httplib::Request& req, httplib::Response& res); } \ No newline at end of file diff --git a/Daybreak.GWCA/header/Utils.h b/Daybreak.GWCA/header/Utils.h new file mode 100644 index 00000000..131f26a7 --- /dev/null +++ b/Daybreak.GWCA/header/Utils.h @@ -0,0 +1,7 @@ +#pragma once +#include + +namespace Daybreak::Utils { + std::string WStringToString(const std::wstring& wstr); + bool StringToInt(const std::string& str, int& outValue); +} \ No newline at end of file diff --git a/Daybreak.GWCA/header/payloads/GameStatePayload.h b/Daybreak.GWCA/header/payloads/GameStatePayload.h new file mode 100644 index 00000000..f47bc20c --- /dev/null +++ b/Daybreak.GWCA/header/payloads/GameStatePayload.h @@ -0,0 +1,14 @@ +#pragma once +#include +#include +#include + +using json = nlohmann::json; + +namespace Daybreak { + struct GameStatePayload { + std::list States; + }; + + void to_json(json& j, const GameStatePayload& p); +} \ No newline at end of file diff --git a/Daybreak.GWCA/header/payloads/StatePayload.h b/Daybreak.GWCA/header/payloads/StatePayload.h new file mode 100644 index 00000000..13c1746d --- /dev/null +++ b/Daybreak.GWCA/header/payloads/StatePayload.h @@ -0,0 +1,16 @@ +#pragma once +#include +#include + +using json = nlohmann::json; + +namespace Daybreak { + struct StatePayload { + uint32_t Id = 0; + float PosX = 0; + float PosY = 0; + uint32_t State = 0; + }; + + void to_json(json& j, const StatePayload& p); +} \ No newline at end of file diff --git a/Daybreak.GWCA/source/NameModule.cpp b/Daybreak.GWCA/source/EntityNameModule.cpp similarity index 88% rename from Daybreak.GWCA/source/NameModule.cpp rename to Daybreak.GWCA/source/EntityNameModule.cpp index 60867426..6831c304 100644 --- a/Daybreak.GWCA/source/NameModule.cpp +++ b/Daybreak.GWCA/source/EntityNameModule.cpp @@ -1,5 +1,5 @@ #include "pch.h" -#include "NameModule.h" +#include "EntityNameModule.h" #include "payloads/NamePayload.h" #include #include @@ -14,25 +14,15 @@ #include #include #include +#include "Utils.h" -namespace Daybreak::Modules::NameModule { +namespace Daybreak::Modules::EntityNameModule { std::vector*, std::wstring*>> WaitingList; std::queue>*> PromiseQueue; std::mutex GameThreadMutex; GW::HookEntry GameThreadHook; volatile bool initialized = false; - std::string WStringToString(const std::wstring& wstr) - { - if (wstr.empty()) return std::string(); - - int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), NULL, 0, NULL, NULL); - std::string strTo(size_needed, 0); - WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &strTo[0], size_needed, NULL, NULL); - - return strTo; - } - std::wstring* GetAsyncName(uint32_t id) { NamePayload namePayload; auto agent = GW::Agents::GetAgentByID(id); @@ -90,7 +80,7 @@ namespace Daybreak::Modules::NameModule { WaitingList.erase(WaitingList.begin() + i); NamePayload payload; payload.Id = id; - payload.Name = WStringToString(*name); + payload.Name = Daybreak::Utils::WStringToString(*name); delete(name); promise->set_value(payload); } diff --git a/Daybreak.GWCA/source/GameModule.cpp b/Daybreak.GWCA/source/GameModule.cpp index 50b64d78..3d9e4a6d 100644 --- a/Daybreak.GWCA/source/GameModule.cpp +++ b/Daybreak.GWCA/source/GameModule.cpp @@ -24,6 +24,7 @@ #include #include #include +#include "Utils.h" namespace Daybreak::Modules::GameModule { std::queue*> PromiseQueue; @@ -31,17 +32,6 @@ namespace Daybreak::Modules::GameModule { GW::HookEntry GameThreadHook; 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); - return str; - } - std::list GetQuestLog() { std::list questMetadatas; auto questLog = GW::QuestMgr::GetQuestLog(); @@ -291,7 +281,8 @@ namespace Daybreak::Modules::GameModule { GW::Title gwTitle; Title title; int titleId = 0; - auto name = GetString(gwPlayer->name); + std::wstring nameString(gwPlayer->name); + auto name = Daybreak::Utils::WStringToString(nameString); player.Name = name; for (const auto &t : titles) { diff --git a/Daybreak.GWCA/source/GameStateModule.cpp b/Daybreak.GWCA/source/GameStateModule.cpp new file mode 100644 index 00000000..a4979351 --- /dev/null +++ b/Daybreak.GWCA/source/GameStateModule.cpp @@ -0,0 +1,104 @@ +#include "pch.h" +#include "GameStateModule.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Daybreak::Modules::GameStateModule { + std::queue*> PromiseQueue; + std::mutex GameThreadMutex; + GW::HookEntry GameThreadHook; + volatile bool initialized = false; + + std::list GetLivingAgents() { + 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) { + agentList.push_back(*agent); + } + } + + return agentList; + } + + std::list GetStates( + std::list agents) { + std::list states; + for (auto& agent : agents) { + StatePayload state; + state.Id = agent.agent_id; + state.PosX = agent.pos.x; + state.PosY = agent.pos.y; + state.State = agent.type_map; + states.push_back(state); + } + + return states; + } + + GameStatePayload GetPayload() { + GameStatePayload gamePayload; + if (!GW::Map::GetIsMapLoaded()) { + return gamePayload; + } + + auto agents = GetLivingAgents(); + if (agents.empty()) { + return gamePayload; + } + + auto states = GetStates(agents); + gamePayload.States = states; + return gamePayload; + } + + void EnsureInitialized() { + GameThreadMutex.lock(); + if (!initialized) { + GW::GameThread::RegisterGameThreadCallback(&GameThreadHook, [&](GW::HookStatus*) { + while (!PromiseQueue.empty()) { + auto promise = PromiseQueue.front(); + PromiseQueue.pop(); + try { + auto payload = GetPayload(); + promise->set_value(payload); + } + catch (...) { + GameStatePayload payload; + promise->set_value(payload); + } + } + }); + + initialized = true; + } + + GameThreadMutex.unlock(); + } + + void GetGameStateInfo(const httplib::Request&, httplib::Response& res) { + auto callbackEntry = new GW::HookEntry; + auto response = new std::promise; + + EnsureInitialized(); + PromiseQueue.emplace(response); + json responsePayload = response->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/ItemNameModule.cpp b/Daybreak.GWCA/source/ItemNameModule.cpp new file mode 100644 index 00000000..377f667b --- /dev/null +++ b/Daybreak.GWCA/source/ItemNameModule.cpp @@ -0,0 +1,193 @@ +#include "pch.h" +#include "ItemNameModule.h" +#include "payloads/NamePayload.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Daybreak::Modules::ItemNameModule { + std::vector*, std::wstring*>> WaitingList; + std::queue, std::promise>*> PromiseQueue; + std::mutex GameThreadMutex; + GW::HookEntry GameThreadHook; + volatile bool initialized = false; + + + // TODO #442: Delete this once it is merged into GWCA + GW::Item* GetItemByModelIdAndModifiers(uint32_t modelid, const std::list modifiers, int bagStart, int bagEnd) { + GW::Bag** bags = GW::Items::GetBagArray(); + GW::Bag* bag = NULL; + + for (int bagIndex = bagStart; bagIndex <= bagEnd; ++bagIndex) { + bag = bags[bagIndex]; + if (!(bag && bag->items.valid())) continue; + for (GW::Item* item : bag->items) { + if (item && item->model_id == modelid && item->mod_struct_size == modifiers.size()) { + auto match = true; + auto listIt = modifiers.begin(); + for (uint32_t i = 0; i < item->mod_struct_size; i++) { + // Compare each element from the array to the corresponding element in the list + if (!(item->mod_struct[i] == *listIt)) { + match = false; + break; + } + ++listIt; + } + + if (match) { + return item; + } + } + } + } + + return NULL; + } + + std::wstring* GetAsyncName(uint32_t id, std::list modifiers) { + std::list parsedModifiers; + for (auto mod : modifiers) { + GW::ItemModifier parsedModifier; + parsedModifier.mod = mod; + parsedModifiers.push_back(parsedModifier); + } + + auto item = GetItemByModelIdAndModifiers(id, parsedModifiers, 1, 23); + if (!item) { + return nullptr; + } + + auto name = new std::wstring(); + GW::Items::AsyncGetItemByName(item, *name); + return name; + } + + void EnsureInitialized() { + GameThreadMutex.lock(); + if (!initialized) { + GW::GameThread::RegisterGameThreadCallback(&GameThreadHook, [&](GW::HookStatus*) { + while (!PromiseQueue.empty()) { + auto promiseRequest = PromiseQueue.front(); + std::promise &promise = std::get<2>(*promiseRequest); + uint32_t id = std::get<0>(*promiseRequest); + auto modifiers = std::get<1>(*promiseRequest); + PromiseQueue.pop(); + try { + auto name = GetAsyncName(id, modifiers); + if (!name) { + continue; + } + + WaitingList.emplace_back(id, &promise, name); + } + catch (...) { + NamePayload payload; + promise.set_value(payload); + } + } + + for (size_t i = 0; i < WaitingList.size(); ) { + auto item = &WaitingList[i]; + auto name = std::get<2>(*item); + if (name->empty()) { + i++; + continue; + } + + auto promise = std::get<1>(*item); + auto id = std::get<0>(*item); + WaitingList.erase(WaitingList.begin() + i); + NamePayload payload; + payload.Id = id; + payload.Name = Daybreak::Utils::WStringToString(*name); + delete(name); + promise->set_value(payload); + } + }); + + initialized = true; + } + + GameThreadMutex.unlock(); + } + + std::vector SplitAndRemoveSpaces(const std::string& s) { + std::vector result; + std::stringstream ss(s); + std::string item; + + while (getline(ss, item, ',')) { + item.erase(std::remove(item.begin(), item.end(), ' '), item.end()); + result.push_back(item); + } + + return result; + } + + void GetName(const httplib::Request& req, httplib::Response& res) { + auto callbackEntry = new GW::HookEntry; + uint32_t id = 0; + std::list modifiers; + + auto id_it = req.params.find("id"); + if (id_it == req.params.end()) { + res.status = 400; + res.set_content("Missing id parameter", "text/plain"); + return; + } + else { + auto& idStr = id_it->second; + int parsedId; + if (!Daybreak::Utils::StringToInt(idStr, parsedId)){ + res.status = 400; + res.set_content("Invalid id parameter", "text/plain"); + return; + } + + id = static_cast(parsedId); + } + + auto mod_it = req.params.find("modifiers"); + if (mod_it == req.params.end()) { + res.status = 400; + res.set_content("Missing modifiers parameter", "text/plain"); + return; + } + else { + auto& modifiersStr = mod_it->second; + const auto tokens = SplitAndRemoveSpaces(modifiersStr); + for (const auto& token : tokens) { + int parsedModifier; + if (!Daybreak::Utils::StringToInt(token, parsedModifier)) { + res.status = 400; + res.set_content("Invalid modifier parameter", "text/plain"); + return; + } + + modifiers.push_back(static_cast(parsedModifier)); + } + } + + auto response = new std::tuple, std::promise>(); + std::get<0>(*response) = id; + std::promise& promise = std::get<2>(*response); + std::get<1>(*response) = modifiers; + + 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/LoginModule.cpp b/Daybreak.GWCA/source/LoginModule.cpp index c1cf804b..7b7d0a64 100644 --- a/Daybreak.GWCA/source/LoginModule.cpp +++ b/Daybreak.GWCA/source/LoginModule.cpp @@ -7,6 +7,7 @@ #include #include #include +#include "Utils.h" namespace Daybreak::Modules::LoginModule { std::queue*> PromiseQueue; @@ -17,15 +18,10 @@ namespace Daybreak::Modules::LoginModule { LoginPayload GetPayload() { auto context = GW::GetCharContext(); LoginPayload loginPayload; - std::string emailstr(64, '\0'); - auto length = std::wcstombs(&emailstr[0], context->player_email, 64); - emailstr.resize(length); - loginPayload.Email = emailstr; - - std::string playerstr(20, '\0'); - length = std::wcstombs(&playerstr[0], context->player_name, 20); - playerstr.resize(length); - loginPayload.PlayerName = playerstr; + std::wstring playerEmail(context->player_email); + std::wstring playerName(context->player_name); + loginPayload.Email = Daybreak::Utils::WStringToString(playerEmail); + loginPayload.PlayerName = Daybreak::Utils::WStringToString(playerName); return loginPayload; } diff --git a/Daybreak.GWCA/source/MainPlayerModule.cpp b/Daybreak.GWCA/source/MainPlayerModule.cpp index 0c078d3f..b60307f0 100644 --- a/Daybreak.GWCA/source/MainPlayerModule.cpp +++ b/Daybreak.GWCA/source/MainPlayerModule.cpp @@ -24,6 +24,7 @@ #include #include #include +#include "Utils.h" namespace Daybreak::Modules::MainPlayerModule { std::queue*> PromiseQueue; @@ -31,17 +32,6 @@ namespace Daybreak::Modules::MainPlayerModule { GW::HookEntry GameThreadHook; 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); - return str; - } - std::list GetQuestLog() { std::list questMetadatas; auto questLog = GW::QuestMgr::GetQuestLog(); @@ -291,7 +281,8 @@ namespace Daybreak::Modules::MainPlayerModule { GW::Title gwTitle; Title title; int titleId = 0; - auto name = GetString(gwPlayer->name); + std::wstring gwPlayerName(gwPlayer->name); + auto name = Daybreak::Utils::WStringToString(gwPlayerName); player.Name = name; for (const auto &t : titles) { diff --git a/Daybreak.GWCA/source/Utils.cpp b/Daybreak.GWCA/source/Utils.cpp new file mode 100644 index 00000000..8e63a132 --- /dev/null +++ b/Daybreak.GWCA/source/Utils.cpp @@ -0,0 +1,32 @@ +#include "pch.h" +#include +#include +#include +#include +#include + +namespace Daybreak::Utils { + std::string WStringToString(const std::wstring& wstr) + { + if (wstr.empty()) return std::string(); + + int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), NULL, 0, NULL, NULL); + std::string strTo(size_needed, 0); + WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &strTo[0], size_needed, NULL, NULL); + + return strTo; + } + + bool StringToInt(const std::string& str, int& outValue) { + size_t pos = 0; + unsigned long result = std::stoul(str, &pos); + + // Ensure the entire string was processed + if (pos != str.size()) { + return false; + } + + outValue = static_cast(result); + return true; + } +} \ No newline at end of file diff --git a/Daybreak.GWCA/source/dllmain.cpp b/Daybreak.GWCA/source/dllmain.cpp index 36c7e750..4725bc69 100644 --- a/Daybreak.GWCA/source/dllmain.cpp +++ b/Daybreak.GWCA/source/dllmain.cpp @@ -16,7 +16,9 @@ #include "GameModule.h" #include "MainPlayerModule.h" #include "SessionModule.h" -#include "NameModule.h" +#include "EntityNameModule.h" +#include "GameStateModule.h" +#include "ItemNameModule.h" static FILE* stdout_proxy; static FILE* stderr_proxy; @@ -48,8 +50,10 @@ static DWORD WINAPI ThreadProc(LPVOID lpModule) http::server::Get("/game", Daybreak::Modules::GameModule::GetGameInfo); http::server::Get("/inventory", Daybreak::Modules::InventoryModule::GetInventoryInfo); http::server::Get("/game/mainplayer", Daybreak::Modules::MainPlayerModule::GetMainPlayer); + http::server::Get("/game/state", Daybreak::Modules::GameStateModule::GetGameStateInfo); http::server::Get("/session", Daybreak::Modules::SessionModule::GetSessionInfo); - http::server::Get("/name", Daybreak::Modules::NameModule::GetName); + http::server::Get("/entities/name", Daybreak::Modules::EntityNameModule::GetName); + http::server::Get("/items/name", Daybreak::Modules::ItemNameModule::GetName); http::server::StartServer(); #ifdef BUILD_TYPE_DEBUG diff --git a/Daybreak.GWCA/source/payloads/GameStatePayload.cpp b/Daybreak.GWCA/source/payloads/GameStatePayload.cpp new file mode 100644 index 00000000..a6b10bd3 --- /dev/null +++ b/Daybreak.GWCA/source/payloads/GameStatePayload.cpp @@ -0,0 +1,14 @@ +#include +#include +#include + +using json = nlohmann::json; + +namespace Daybreak { + void to_json(json& j, const GameStatePayload& p) { + j = json + { + {"States", p.States}, + }; + } +} \ No newline at end of file diff --git a/Daybreak.GWCA/source/payloads/StatePayload.cpp b/Daybreak.GWCA/source/payloads/StatePayload.cpp new file mode 100644 index 00000000..7c725a3d --- /dev/null +++ b/Daybreak.GWCA/source/payloads/StatePayload.cpp @@ -0,0 +1,17 @@ +#include +#include +#include + +using json = nlohmann::json; + +namespace Daybreak { + void to_json(json& j, const StatePayload& p) { + j = json + { + {"Id", p.Id}, + {"PosX", p.PosX}, + {"PosY", p.PosY}, + {"State", p.State} + }; + } +} \ No newline at end of file diff --git a/Daybreak/Controls/LivingEntityContextMenu.xaml.cs b/Daybreak/Controls/LivingEntityContextMenu.xaml.cs index 8e85be7e..d1b78e7b 100644 --- a/Daybreak/Controls/LivingEntityContextMenu.xaml.cs +++ b/Daybreak/Controls/LivingEntityContextMenu.xaml.cs @@ -59,7 +59,7 @@ private async void LivingEntityContextMenu_DataContextChanged(object sender, Sys } await this.guildwarsMemoryReader.EnsureInitialized(context.GuildWarsApplicationLaunchContext!.GuildWarsProcess, CancellationToken.None); - this.EntityName = await this.guildwarsMemoryReader.GetNamedEntity(context.LivingEntity!, CancellationToken.None); + this.EntityName = await this.guildwarsMemoryReader.GetEntityName(context.LivingEntity!, CancellationToken.None).ConfigureAwait(true); } private void NpcDefinitionTextBlock_MouseLeftButtonDown(object _, MouseButtonEventArgs e) diff --git a/Daybreak/Controls/Minimap/GuildwarsMinimap.xaml.cs b/Daybreak/Controls/Minimap/GuildwarsMinimap.xaml.cs index 62eb8914..a8f61e7c 100644 --- a/Daybreak/Controls/Minimap/GuildwarsMinimap.xaml.cs +++ b/Daybreak/Controls/Minimap/GuildwarsMinimap.xaml.cs @@ -70,6 +70,8 @@ public partial class GuildwarsMinimap : UserControl [GenerateDependencyProperty] private GameData gameData = new(); [GenerateDependencyProperty] + private GameState gameState = new(); + [GenerateDependencyProperty] private double zoom = 0.08; [GenerateDependencyProperty(InitialValue = true)] private bool drawPositionHistory = true; @@ -88,7 +90,7 @@ public partial class GuildwarsMinimap : UserControl public event EventHandler? PlayerInformationClicked; public event EventHandler? MapIconClicked; public event EventHandler? ProfessionClicked; - public event EventHandler NpcNameClicked; + public event EventHandler? NpcNameClicked; public GuildwarsMinimap() :this( @@ -140,13 +142,42 @@ protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { this.UpdateGameData(); } - else if (e.Property == GameDataProperty && - this.GameData.Valid) + else if (e.Property == GameStateProperty && + this.GameState is not null) { + this.UpdateStates(); this.UpdateGameData(); } } + private void UpdateStates() + { + if (this.GameState is null || + this.GameData is null || + this.GameData.Valid is false) + { + return; + } + + this.GameData.MainPlayer!.Position = this.GameState.States?.FirstOrDefault(state => state.Id == this.GameData.MainPlayer.Id)?.Position ?? new Position(); + foreach(var worldPlayer in this.GameData.WorldPlayers!) + { + worldPlayer.Position = this.GameState.States?.FirstOrDefault(state => state.Id == worldPlayer.Id)?.Position ?? new Position(); + } + + foreach (var partyPlayer in this.GameData.Party!) + { + partyPlayer.Position = this.GameState.States?.FirstOrDefault(state => state.Id == partyPlayer.Id)?.Position ?? new Position(); + } + + foreach (var entity in this.GameData.LivingEntities!) + { + var state = this.GameState.States?.FirstOrDefault(state => state.Id == entity.Id); + entity.Position = state?.Position ?? new Position(); + entity.State = state?.State ?? LivingEntityState.Unknown; + } + } + private void UpdateGameData() { if(this.GameData is null || @@ -168,7 +199,7 @@ private void UpdateGameData() this.TargetEntityModelId = (int?)this.GameData.LivingEntities?.FirstOrDefault(e => e.Id == this.TargetEntityId)?.ModelType ?? 0; var screenVirtualWidth = this.ActualWidth / this.Zoom; var screenVirtualHeight = this.ActualHeight / this.Zoom; - var position = this.GameData.MainPlayer.Position!.Value; + var position = this.GameData?.MainPlayer?.Position ?? new Position(); this.originPoint = new Point( position.X - (screenVirtualWidth / 2) - (this.originOffset.X / this.Zoom), position.Y + (screenVirtualHeight / 2) + (this.originOffset.Y / this.Zoom)); @@ -193,7 +224,7 @@ private void ManageMainPlayerPositionHistory() return; } - var currentPosition = this.GameData.MainPlayer?.Position ?? throw new InvalidOperationException("Unexpected main player null position"); + var currentPosition = this.GameData?.MainPlayer?.Position ?? throw new InvalidOperationException("Unexpected main player null position"); if (this.mainPlayerPositionHistory.Any(oldPosition => PositionsCollide(oldPosition, currentPosition))) { return; diff --git a/Daybreak/Controls/PlayerContextMenu.xaml.cs b/Daybreak/Controls/PlayerContextMenu.xaml.cs index cc64e175..3a741923 100644 --- a/Daybreak/Controls/PlayerContextMenu.xaml.cs +++ b/Daybreak/Controls/PlayerContextMenu.xaml.cs @@ -44,7 +44,7 @@ private async void PlayerContextMenu_DataContextChanged(object sender, System.Wi } await this.guildwarsMemoryReader.EnsureInitialized(context.GuildWarsApplicationLaunchContext!.GuildWarsProcess, CancellationToken.None); - this.PlayerName = await this.guildwarsMemoryReader.GetNamedEntity(context.Player!, CancellationToken.None); + this.PlayerName = await this.guildwarsMemoryReader.GetEntityName(context.Player!, CancellationToken.None).ConfigureAwait(true); } private void TextBlock_MouseLeftButtonDown(object _, MouseButtonEventArgs e) diff --git a/Daybreak/Controls/Templates/BagItemMenu.xaml b/Daybreak/Controls/Templates/BagItemMenu.xaml index 6ca20641..8516aed1 100644 --- a/Daybreak/Controls/Templates/BagItemMenu.xaml +++ b/Daybreak/Controls/Templates/BagItemMenu.xaml @@ -20,7 +20,7 @@ - public partial class BagItemMenu : UserControl { + private readonly IGuildwarsMemoryReader guildwarsMemoryReader; private readonly IItemHashService itemHashService; public event EventHandler? CloseClicked; public event EventHandler? ItemWikiClicked; + [GenerateDependencyProperty] + private string itemName = string.Empty; [GenerateDependencyProperty] private string modHash = string.Empty; public BagItemMenu() - : this(Launch.Launcher.Instance.ApplicationServiceProvider.GetRequiredService()) + : this(Launch.Launcher.Instance.ApplicationServiceProvider.GetRequiredService(), + Launch.Launcher.Instance.ApplicationServiceProvider.GetRequiredService()) { } private BagItemMenu( + IGuildwarsMemoryReader guildwarsMemoryReader, IItemHashService itemHashService) { + this.guildwarsMemoryReader = guildwarsMemoryReader.ThrowIfNull(); this.itemHashService = itemHashService.ThrowIfNull(); this.InitializeComponent(); } @@ -39,7 +48,7 @@ private void HighlightButton_Clicked(object sender, EventArgs e) this.CloseClicked?.Invoke(this, e); } - private void UserControl_DataContextChanged(object _, DependencyPropertyChangedEventArgs e) + private async void UserControl_DataContextChanged(object _, DependencyPropertyChangedEventArgs e) { if (this.DataContext is not IBagContent content) { @@ -47,6 +56,16 @@ private void UserControl_DataContextChanged(object _, DependencyPropertyChangedE } this.ModHash = this.itemHashService.ComputeHash(content); + var name = await this.guildwarsMemoryReader.GetItemName( + content is BagItem bagItem ? bagItem.Item.Id : + content is UnknownBagItem unknownBagItem ? (int)unknownBagItem.ItemId : + 0, + content.Modifiers.Select(mod => mod.Modifier).ToList(), + CancellationToken.None).ConfigureAwait(true); + if (name is not null) + { + this.ItemName = name; + } } private void WikiTextBlock_MouseLeftButtonDown(object _, MouseButtonEventArgs e) diff --git a/Daybreak/Daybreak.csproj b/Daybreak/Daybreak.csproj index 3e69b9ba..58c63b1b 100644 --- a/Daybreak/Daybreak.csproj +++ b/Daybreak/Daybreak.csproj @@ -13,7 +13,7 @@ preview Daybreak.ico true - 0.9.8.132 + 0.9.8.133 true cfb2a489-db80-448d-a969-80270f314c46 True diff --git a/Daybreak/Models/Guildwars/EntityGameState.cs b/Daybreak/Models/Guildwars/EntityGameState.cs new file mode 100644 index 00000000..fba4b233 --- /dev/null +++ b/Daybreak/Models/Guildwars/EntityGameState.cs @@ -0,0 +1,7 @@ +namespace Daybreak.Models.Guildwars; +public sealed class EntityGameState +{ + public int Id { get; set; } + public Position Position { get; set; } + public LivingEntityState State { get; set; } +} diff --git a/Daybreak/Models/Guildwars/GameState.cs b/Daybreak/Models/Guildwars/GameState.cs new file mode 100644 index 00000000..29ec91fb --- /dev/null +++ b/Daybreak/Models/Guildwars/GameState.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; + +namespace Daybreak.Models.Guildwars; +public sealed class GameState +{ + public List? States { get; init; } +} diff --git a/Daybreak/Models/Guildwars/LivingEntity.cs b/Daybreak/Models/Guildwars/LivingEntity.cs index 49c898ca..8ebd62a2 100644 --- a/Daybreak/Models/Guildwars/LivingEntity.cs +++ b/Daybreak/Models/Guildwars/LivingEntity.cs @@ -10,7 +10,7 @@ public sealed class LivingEntity : IEntity public Npc? NpcDefinition { get; init; } - public Position? Position { get; init; } + public Position? Position { get; set; } public Profession? PrimaryProfession { get; init; } @@ -18,7 +18,7 @@ public sealed class LivingEntity : IEntity public int Level { get; init; } - public LivingEntityState State { get; init; } + public LivingEntityState State { get; set; } public LivingEntityAllegiance Allegiance { get; init; } } diff --git a/Daybreak/Models/Guildwars/MainPlayerInformation.cs b/Daybreak/Models/Guildwars/MainPlayerInformation.cs index d127d355..a2fc7d86 100644 --- a/Daybreak/Models/Guildwars/MainPlayerInformation.cs +++ b/Daybreak/Models/Guildwars/MainPlayerInformation.cs @@ -9,7 +9,7 @@ public sealed class MainPlayerInformation : IEntity public TitleInformation? TitleInformation { get; init; } public int Id { get; init; } public int Level { get; init; } - public Position? Position { get; init; } + public Position? Position { get; set; } public Profession? PrimaryProfession { get; init; } public Profession? SecondaryProfession { get; init; } public List? UnlockedProfession { get; init; } diff --git a/Daybreak/Models/Guildwars/PlayerInformation.cs b/Daybreak/Models/Guildwars/PlayerInformation.cs index 61fec969..68cde5db 100644 --- a/Daybreak/Models/Guildwars/PlayerInformation.cs +++ b/Daybreak/Models/Guildwars/PlayerInformation.cs @@ -7,7 +7,7 @@ public sealed class PlayerInformation : IEntity public int Id { get; init; } public uint Timer { get; init; } public int Level { get; init; } - public Position? Position { get; init; } + public Position? Position { get; set; } public Profession? PrimaryProfession { get; init; } public Profession? SecondaryProfession { get; init; } public List? UnlockedProfession { get; init; } diff --git a/Daybreak/Models/Guildwars/WorldPlayerInformation.cs b/Daybreak/Models/Guildwars/WorldPlayerInformation.cs index 96b317a3..d931eaf2 100644 --- a/Daybreak/Models/Guildwars/WorldPlayerInformation.cs +++ b/Daybreak/Models/Guildwars/WorldPlayerInformation.cs @@ -9,7 +9,7 @@ public sealed class WorldPlayerInformation : IEntity public TitleInformation? TitleInformation { get; init; } public int Id { get; init; } public int Level { get; init; } - public Position? Position { get; init; } + public Position? Position { get; set; } public Profession? PrimaryProfession { get; init; } public Profession? SecondaryProfession { get; init; } public List? UnlockedProfession { get; init; } diff --git a/Daybreak/Services/Scanner/GWCAMemoryReader.cs b/Daybreak/Services/Scanner/GWCAMemoryReader.cs index 45b47407..da954ca2 100644 --- a/Daybreak/Services/Scanner/GWCAMemoryReader.cs +++ b/Daybreak/Services/Scanner/GWCAMemoryReader.cs @@ -14,11 +14,13 @@ using Daybreak.Models.Builds; using System.Windows; using Daybreak.Utils; +using System.Text.RegularExpressions; namespace Daybreak.Services.Scanner; public sealed class GWCAMemoryReader : IGuildwarsMemoryReader { + private static readonly Regex ItemNameColorRegex = new(@"|", RegexOptions.Compiled); private readonly IGWCAClient client; private readonly ILogger logger; @@ -517,15 +519,68 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat return default; } - public async Task GetNamedEntity(IEntity entity, CancellationToken cancellationToken) + public async Task ReadGameState(CancellationToken cancellationToken) { - var scopedLogger = this.logger.CreateScopedLogger(nameof(this.GetNamedEntity), string.Empty); + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.GetEntityName), string.Empty); if (this.connectionContextCache is null) { return default; } - var response = await this.client.GetAsync(this.connectionContextCache.Value, $"name?id={entity.Id}", cancellationToken); + var response = await this.client.GetAsync(this.connectionContextCache.Value, $"game/state", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + + try + { + var payload = await response.Content.ReadAsStringAsync(cancellationToken); + var gameStatePayload = payload.Deserialize(); + if (gameStatePayload is null || + gameStatePayload.States is null) + { + return default; + } + + var states = gameStatePayload.States.Select(state => + { + return new EntityGameState + { + Id = (int)state.Id, + Position = new Position { X = state.PosX, Y = state.PosY }, + State = state.State switch + { + 0x08 => LivingEntityState.Dead, + 0xC00 => LivingEntityState.Boss, + 0x40000 => LivingEntityState.Spirit, + 0x40008 => LivingEntityState.ToBeCleanedUp, + 0x400000 => LivingEntityState.Player, + _ => LivingEntityState.Unknown + } + }; + }).ToList(); + + return new GameState { States = states }; + } + catch (Exception ex) + { + scopedLogger.LogError(ex, "Encountered exception while parsing response"); + } + + return default; + } + + public async Task GetEntityName(IEntity entity, CancellationToken cancellationToken) + { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.GetEntityName), string.Empty); + if (this.connectionContextCache is null) + { + return default; + } + + var response = await this.client.GetAsync(this.connectionContextCache.Value, $"entities/name?id={entity.Id}", cancellationToken); if (!response.IsSuccessStatusCode) { scopedLogger.LogError($"Received non-success response {response.StatusCode}"); @@ -554,6 +609,44 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat return default; } + public async Task GetItemName(int id, List modifiers, CancellationToken cancellationToken) + { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.GetItemName), string.Empty); + if (this.connectionContextCache is null) + { + return default; + } + + var response = await this.client.GetAsync(this.connectionContextCache.Value, $"items/name?id={id}&modifiers={string.Join(',', modifiers?.Select(m => m.ToString()) ?? Array.Empty())}", cancellationToken); + if (!response.IsSuccessStatusCode) + { + scopedLogger.LogError($"Received non-success response {response.StatusCode}"); + return default; + } + + try + { + var payload = await response.Content.ReadAsStringAsync(cancellationToken); + var namePayload = payload.Deserialize(); + if (namePayload is null || + namePayload.Id != id || + namePayload.Name!.IsNullOrWhiteSpace()) + { + return default; + } + + var curedName = ItemNameColorRegex.Replace(namePayload.Name!, ""); + return curedName; + + } + catch (Exception ex) + { + scopedLogger.LogError(ex, "Encountered exception while parsing response"); + } + + return default; + } + public void Stop() { } diff --git a/Daybreak/Services/Scanner/GuildwarsMemoryCache.cs b/Daybreak/Services/Scanner/GuildwarsMemoryCache.cs index 0865971c..8cc195f7 100644 --- a/Daybreak/Services/Scanner/GuildwarsMemoryCache.cs +++ b/Daybreak/Services/Scanner/GuildwarsMemoryCache.cs @@ -3,6 +3,7 @@ using Daybreak.Models.LaunchConfigurations; using Daybreak.Services.Scanner.Models; using System; +using System.Collections.Generic; using System.Configuration; using System.Core.Extensions; using System.Threading; @@ -23,8 +24,8 @@ public sealed class GuildwarsMemoryCache : IGuildwarsMemoryCache private readonly CachedData sessionDataCache = new(); private readonly CachedData userDataCache = new(); private readonly CachedData mainPlayerDataCache = new(); - private readonly CachedData connectionDataCache = new(); private readonly CachedData preGameDataCache = new(); + private readonly CachedData gameStateCache = new(); public GuildwarsMemoryCache( IGuildwarsMemoryReader guildwarsMemoryReader, @@ -89,6 +90,11 @@ public async Task EnsureInitialized(GuildWarsApplicationLaunchContext context, C return this.ReadDataInternal(this.preGameDataCache, this.guildwarsMemoryReader.ReadPreGameData, cancellationToken); } + public Task ReadGameState(CancellationToken cancellationToken) + { + return this.ReadDataInternal(this.gameStateCache, this.guildwarsMemoryReader.ReadGameState, cancellationToken); + } + private async Task ReadDataInternal(CachedData cachedData, Func> task, CancellationToken cancellationToken) { if (DateTime.Now - cachedData.SetTime <= TimeSpan.FromMilliseconds(this.liveOptions.Value.MemoryReaderFrequency)) diff --git a/Daybreak/Services/Scanner/IGuildwarsMemoryCache.cs b/Daybreak/Services/Scanner/IGuildwarsMemoryCache.cs index 692fe9c1..9cbd531a 100644 --- a/Daybreak/Services/Scanner/IGuildwarsMemoryCache.cs +++ b/Daybreak/Services/Scanner/IGuildwarsMemoryCache.cs @@ -18,4 +18,5 @@ public interface IGuildwarsMemoryCache Task ReadUserData(CancellationToken cancellationToken); Task ReadMainPlayerData(CancellationToken cancellationToken); Task ReadPreGameData(CancellationToken cancellationToken); + Task ReadGameState(CancellationToken cancellationToken); } diff --git a/Daybreak/Services/Scanner/IGuildwarsMemoryReader.cs b/Daybreak/Services/Scanner/IGuildwarsMemoryReader.cs index ea1d55c5..6b33f97f 100644 --- a/Daybreak/Services/Scanner/IGuildwarsMemoryReader.cs +++ b/Daybreak/Services/Scanner/IGuildwarsMemoryReader.cs @@ -1,4 +1,5 @@ using Daybreak.Models.Guildwars; +using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -18,6 +19,8 @@ public interface IGuildwarsMemoryReader Task ReadSessionData(CancellationToken cancellationToken); Task ReadMainPlayerData(CancellationToken cancellationToken); Task ReadPreGameData(CancellationToken cancellationToken); - Task GetNamedEntity(IEntity entity, CancellationToken cancellationToken); + Task ReadGameState(CancellationToken cancellationToken); + Task GetEntityName(IEntity entity, CancellationToken cancellationToken); + Task GetItemName(int id, List modifiers, CancellationToken cancellationToken); void Stop(); } diff --git a/Daybreak/Services/Scanner/Models/GameStatePayload.cs b/Daybreak/Services/Scanner/Models/GameStatePayload.cs new file mode 100644 index 00000000..d1de34f6 --- /dev/null +++ b/Daybreak/Services/Scanner/Models/GameStatePayload.cs @@ -0,0 +1,7 @@ +using System.Collections.Generic; + +namespace Daybreak.Services.Scanner.Models; +internal sealed class GameStatePayload +{ + public List? States { get; set; } +} diff --git a/Daybreak/Services/Scanner/Models/StatePayload.cs b/Daybreak/Services/Scanner/Models/StatePayload.cs new file mode 100644 index 00000000..ffb0c15a --- /dev/null +++ b/Daybreak/Services/Scanner/Models/StatePayload.cs @@ -0,0 +1,8 @@ +namespace Daybreak.Services.Scanner.Models; +internal sealed class StatePayload +{ + public uint Id { get; set; } + public float PosX { get; set; } + public float PosY { get; set; } + public uint State { get; set; } +} diff --git a/Daybreak/Views/FocusView.xaml b/Daybreak/Views/FocusView.xaml index b7691855..8248b7a9 100644 --- a/Daybreak/Views/FocusView.xaml +++ b/Daybreak/Views/FocusView.xaml @@ -102,6 +102,7 @@ ()) + { + worldPlayer.Position = this.GameData?.WorldPlayers?.FirstOrDefault(w => w.Id == worldPlayer.Id)?.Position ?? new Position(); + } + + foreach (var partyPlayer in maybeGameData.Party ?? new List()) + { + partyPlayer.Position = this.GameData?.Party?.FirstOrDefault(w => w.Id == partyPlayer.Id)?.Position ?? new Position(); + } + + foreach (var entity in maybeGameData.LivingEntities ?? new List()) + { + var oldEntity = this.GameData?.LivingEntities?.FirstOrDefault(w => w.Id == entity.Id); + entity.Position = oldEntity?.Position ?? new Position(); + entity.State = oldEntity?.State ?? LivingEntityState.Unknown; + } + this.GameData = maybeGameData; } catch (InvalidOperationException ex) @@ -429,6 +517,7 @@ private async void FocusView_Loaded(object _, RoutedEventArgs e) if (this.MinimapVisible) { + this.PeriodicallyReadGameState(cancellationToken); this.PeriodicallyReadGameData(cancellationToken); this.PeriodicallyReadPathingData(cancellationToken); }