Skip to content

Commit

Permalink
Setup GWCA whisper functionality (#636)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexMacocian committed Apr 12, 2024
1 parent 9d7c6c2 commit 48955a7
Show file tree
Hide file tree
Showing 14 changed files with 148 additions and 21 deletions.
1 change: 1 addition & 0 deletions Daybreak.GWCA/header/Server.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ namespace http {
void StopServer();
void SetLogger(httplib::Logger logger);
void Get(const std::string& pattern, httplib::Server::Handler handler);
void Post(const std::string& pattern, httplib::Server::Handler handler);
}
}
1 change: 1 addition & 0 deletions Daybreak.GWCA/header/Utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@

namespace Daybreak::Utils {
std::string WStringToString(const std::wstring& wstr);
std::wstring StringToWString(const std::string& str);
bool StringToInt(const std::string& str, int& outValue);
}
6 changes: 6 additions & 0 deletions Daybreak.GWCA/header/WhisperModule.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#pragma once
#include "httplib.h"

namespace Daybreak::Modules::WhisperModule {
void PostWhisper(const httplib::Request&, httplib::Response& res);
}
4 changes: 4 additions & 0 deletions Daybreak.GWCA/source/Server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ namespace http {
server.Get(pattern, handler);
}

void Post(const std::string& pattern, httplib::Server::Handler handler) {
server.Post(pattern, handler);
}

void SetLogger(httplib::Logger logger) {
server.set_logger(logger);
}
Expand Down
10 changes: 10 additions & 0 deletions Daybreak.GWCA/source/Utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@
#include <cstdint>

namespace Daybreak::Utils {
std::wstring StringToWString(const std::string& str) {
if (str.empty()) return std::wstring();

int size_needed = MultiByteToWideChar(CP_UTF8, 0, &str[0], (int)str.size(), NULL, 0);
std::wstring wstrTo(size_needed, 0);
MultiByteToWideChar(CP_UTF8, 0, &str[0], (int)str.size(), &wstrTo[0], size_needed);

return wstrTo;
}

std::string WStringToString(const std::wstring& wstr)
{
if (wstr.empty()) return std::string();
Expand Down
51 changes: 51 additions & 0 deletions Daybreak.GWCA/source/WhisperModule.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#include "pch.h"
#include "WhisperModule.h"
#include <GWCA/Managers/GameThreadMgr.h>
#include <GWCA/Managers/MapMgr.h>
#include <GWCA/Managers/ChatMgr.h>
#include <queue>
#include <string>
#include <Utils.h>

namespace Daybreak::Modules::WhisperModule {
std::queue<std::wstring> PromiseQueue;
std::mutex GameThreadMutex;
GW::HookEntry GameThreadHook;
volatile bool initialized = false;

void PostMessage(std::wstring message) {
if (!GW::Map::GetIsMapLoaded()) {
return;
}

GW::Chat::WriteChat(GW::Chat::CHANNEL_WHISPER, message.c_str(), L"Daybreak", false);
}

void EnsureInitialized() {
GameThreadMutex.lock();
if (!initialized) {
GW::GameThread::RegisterGameThreadCallback(&GameThreadHook, [&](GW::HookStatus*) {
while (!PromiseQueue.empty()) {
auto message = PromiseQueue.front();
PromiseQueue.pop();
try {
PostMessage(message);
}
catch (...) {
}
}
});

initialized = true;
}

GameThreadMutex.unlock();
}

void PostWhisper(const httplib::Request& req, httplib::Response& res) {
EnsureInitialized();
auto wMessage = Utils::StringToWString(req.body);
PromiseQueue.emplace(wMessage);
res.set_content("Okay", "text/plain");
}
}
2 changes: 2 additions & 0 deletions Daybreak.GWCA/source/dllmain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "GameStateModule.h"
#include "ItemNameModule.h"
#include "TitleInfoModule.h"
#include "WhisperModule.h"
#include <mutex>

volatile bool initialized;
Expand Down Expand Up @@ -74,6 +75,7 @@ static DWORD WINAPI StartHttpServer(LPVOID)
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::Post("/whisper", Daybreak::Modules::WhisperModule::PostWhisper);
http::server::StartServer();
return 0;
}
Expand Down
4 changes: 4 additions & 0 deletions Daybreak/Configuration/Options/MemoryReaderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ internal sealed class MemoryReaderOptions
[OptionRange<double>(MinValue = 0, MaxValue = 1000)]
[OptionName(Name = "Memory Reader Frequency", Description = "Measured in ms. Sets how often should the launcher polls information from the game. Actual frequency is capped by the memory reading speed")]
public double MemoryReaderFrequency { get; set; } = 0;

[JsonProperty(nameof(AllowWhispers))]
[OptionName(Name = "Allow Whispers", Description = "If enabled, Daybreak will be allowed to send whispers to the player. Mainly used in notifications")]
public bool AllowWhispers { get; set; } = true;
}
4 changes: 2 additions & 2 deletions Daybreak/Controls/MenuList.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,13 @@ private async void PeriodicallyCheckUnopenedNotifications(CancellationToken canc
{
while (!cancellationToken.IsCancellationRequested)
{
var unopenedNotifications = this.notificationStorage.GetPendingNotifications().ToList();
var unopenedNotifications = this.notificationStorage.GetPendingNotifications();
if (unopenedNotifications.Any())
{
await this.Dispatcher.InvokeAsync(() =>
{
this.ShowingNotificationCount = true;
this.NotificationCount = unopenedNotifications.Count;
this.NotificationCount = unopenedNotifications.Count();
});
}
else
Expand Down
6 changes: 6 additions & 0 deletions Daybreak/Services/GWCA/GWCAClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,10 @@ public async Task<HttpResponseMessage> GetAsync(ConnectionContext connectionCont
var scopedLogger = this.logger.CreateScopedLogger(nameof(this.GetAsync), subPath);
return await this.httpClient.GetAsync($"{UrlTemplate.Replace(PortPlaceholder, connectionContext.Port.ToString())}/{subPath}", cancellationToken);
}

public async Task<HttpResponseMessage> PostAsync(ConnectionContext connectionContext, string subPath, HttpContent httpContent, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger(nameof(this.GetAsync), subPath);
return await this.httpClient.PostAsync($"{UrlTemplate.Replace(PortPlaceholder, connectionContext.Port.ToString())}/{subPath}", httpContent, cancellationToken);
}
}
2 changes: 2 additions & 0 deletions Daybreak/Services/GWCA/IGWCAClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ public interface IGWCAClient
Task<bool> CheckAlive(ConnectionContext connectionContext, CancellationToken cancellationToken);

Task<HttpResponseMessage> GetAsync(ConnectionContext connectionContext, string subPath, CancellationToken cancellationToken);

Task<HttpResponseMessage> PostAsync(ConnectionContext connectionContext, string subPath, HttpContent httpContent, CancellationToken cancellationToken);
}
9 changes: 5 additions & 4 deletions Daybreak/Services/PriceChecker/PriceCheckerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ private async Task PeriodicallyCheckPrices(CancellationToken cancellationToken)
inventoryData.Backpack!.Items.Concat(
inventoryData.Bags.SelectMany(b => b!.Items).Concat(
inventoryData.BeltPouch!.Items.Concat(
inventoryData.EquippedItems!.Items.Concat(
inventoryData.EquipmentPack!.Items.Concat(
inventoryData.UnclaimedItems!.Items))))
.Select(i =>
{
Expand Down Expand Up @@ -132,9 +132,10 @@ private async Task PeriodicallyCheckPrices(CancellationToken cancellationToken)
var name = await this.guildwarsMemoryReader.GetItemName((int)modelId, item.Modifiers.Select(m => (uint)m).ToList(), cancellationToken);
this.notificationService.NotifyInformation(
title: $"Expensive item detected",
description: $"The following components have been identified in the item{(name is null ? string.Empty : $" {name}")}:\n{string.Join('\n', itemsWithPrice.Select(t => $"{t.item.Name}: {t.Item2}g"))}",
description: $"{(name is null ? string.Empty : $"{name}\n")}{string.Join('\n', itemsWithPrice.Select(t => $"{t.item.Name}: {t.Item2}g"))}",
expirationTime: DateTime.MaxValue,
persistent: false);
persistent: true);
await this.guildwarsMemoryReader.SendWhisper($"<c=#f96677>Expensive item detected:\n<c=#ffffff>{(name is null ? string.Empty : $"{name}\n")}{string.Join('\n', itemsWithPrice.Select(t => $"<c=#8fce00>{t.item.Name}: {t.Item2}g<c=#ffffff>"))}", CancellationToken.None);
}

}
Expand All @@ -150,7 +151,7 @@ private async Task PeriodicallyCheckPrices(CancellationToken cancellationToken)
private bool TryGetPrice(string id, out double price)
{
price = 0;
var priceCheckDTO = this.priceCache.FindOne(b => b.Id == id);
var priceCheckDTO = this.priceCache.Find(b => b.Id == id).FirstOrDefault();
if (priceCheckDTO is null)
{
return false;
Expand Down
68 changes: 53 additions & 15 deletions Daybreak/Services/Scanner/GWCAMemoryReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@
using Daybreak.Utils;
using System.Text.RegularExpressions;
using Daybreak.Services.Pathfinding;
using System.Net.Http;
using System.Configuration;
using Daybreak.Configuration.Options;

namespace Daybreak.Services.Scanner;

public sealed partial class GWCAMemoryReader : IGuildwarsMemoryReader
internal sealed partial class GWCAMemoryReader : IGuildwarsMemoryReader
{
private static readonly Regex ItemNameColorRegex = GenerateItemNameColorRegex();
private readonly IPathfinder pathfinder;
private readonly IGWCAClient client;
private readonly ILiveOptions<MemoryReaderOptions> liveOptions;
private readonly ILogger<GWCAMemoryReader> logger;

private bool faulty = false;
Expand All @@ -32,10 +36,12 @@ public sealed partial class GWCAMemoryReader : IGuildwarsMemoryReader
public GWCAMemoryReader(
IPathfinder pathfinder,
IGWCAClient gWCAClient,
ILiveOptions<MemoryReaderOptions> liveOptions,
ILogger<GWCAMemoryReader> logger)
{
this.pathfinder = pathfinder.ThrowIfNull();
this.client = gWCAClient.ThrowIfNull();
this.liveOptions = liveOptions.ThrowIfNull();
this.logger = logger.ThrowIfNull();
}

Expand Down Expand Up @@ -76,7 +82,7 @@ public async Task EnsureInitialized(uint processId, CancellationToken cancellati

try
{
var response = await this.client.GetAsync(this.connectionContextCache.Value, "game", cancellationToken);
using var response = await this.client.GetAsync(this.connectionContextCache.Value, "game", cancellationToken);
if (!response.IsSuccessStatusCode)
{
scopedLogger.LogError($"Received non-success response {response.StatusCode}");
Expand Down Expand Up @@ -137,7 +143,7 @@ public async Task EnsureInitialized(uint processId, CancellationToken cancellati

try
{
var response = await this.client.GetAsync(this.connectionContextCache.Value, "inventory", cancellationToken);
using var response = await this.client.GetAsync(this.connectionContextCache.Value, "inventory", cancellationToken);
if (!response.IsSuccessStatusCode)
{
scopedLogger.LogError($"Received non-success response {response.StatusCode}");
Expand Down Expand Up @@ -182,7 +188,7 @@ public async Task EnsureInitialized(uint processId, CancellationToken cancellati

try
{
var response = await this.client.GetAsync(this.connectionContextCache.Value, "login", cancellationToken);
using var response = await this.client.GetAsync(this.connectionContextCache.Value, "login", cancellationToken);
if (!response.IsSuccessStatusCode)
{
scopedLogger.LogError($"Received non-success response {response.StatusCode}");
Expand Down Expand Up @@ -221,7 +227,7 @@ public async Task EnsureInitialized(uint processId, CancellationToken cancellati

try
{
var response = await this.client.GetAsync(this.connectionContextCache.Value, "game/mainplayer", cancellationToken);
using var response = await this.client.GetAsync(this.connectionContextCache.Value, "game/mainplayer", cancellationToken);
if (!response.IsSuccessStatusCode)
{
scopedLogger.LogError($"Received non-success response {response.StatusCode}");
Expand Down Expand Up @@ -259,7 +265,7 @@ public async Task EnsureInitialized(uint processId, CancellationToken cancellati

try
{
var response = await this.client.GetAsync(this.connectionContextCache.Value, "pathing", cancellationToken);
using var response = await this.client.GetAsync(this.connectionContextCache.Value, "pathing", cancellationToken);
if (!response.IsSuccessStatusCode)
{
scopedLogger.LogError($"Received non-success response {response.StatusCode}");
Expand Down Expand Up @@ -325,7 +331,7 @@ public async Task EnsureInitialized(uint processId, CancellationToken cancellati

try
{
var response = await this.client.GetAsync(this.connectionContextCache.Value, "pathing/metadata", cancellationToken);
using var response = await this.client.GetAsync(this.connectionContextCache.Value, "pathing/metadata", cancellationToken);
if (!response.IsSuccessStatusCode)
{
scopedLogger.LogError($"Received non-success response {response.StatusCode}");
Expand Down Expand Up @@ -363,7 +369,7 @@ public async Task EnsureInitialized(uint processId, CancellationToken cancellati

try
{
var response = await this.client.GetAsync(this.connectionContextCache.Value, "pregame", cancellationToken);
using var response = await this.client.GetAsync(this.connectionContextCache.Value, "pregame", cancellationToken);
if (!response.IsSuccessStatusCode)
{
scopedLogger.LogError($"Received non-success response {response.StatusCode}");
Expand Down Expand Up @@ -402,7 +408,7 @@ public async Task EnsureInitialized(uint processId, CancellationToken cancellati

try
{
var response = await this.client.GetAsync(this.connectionContextCache.Value, "session", cancellationToken);
using var response = await this.client.GetAsync(this.connectionContextCache.Value, "session", cancellationToken);
if (!response.IsSuccessStatusCode)
{
scopedLogger.LogError($"Received non-success response {response.StatusCode}");
Expand Down Expand Up @@ -454,7 +460,7 @@ public async Task EnsureInitialized(uint processId, CancellationToken cancellati

try
{
var response = await this.client.GetAsync(this.connectionContextCache.Value, "user", cancellationToken);
using var response = await this.client.GetAsync(this.connectionContextCache.Value, "user", cancellationToken);
if (!response.IsSuccessStatusCode)
{
scopedLogger.LogError($"Received non-success response {response.StatusCode}");
Expand Down Expand Up @@ -509,7 +515,7 @@ public async Task EnsureInitialized(uint processId, CancellationToken cancellati

try
{
var response = await this.client.GetAsync(this.connectionContextCache.Value, "map", cancellationToken);
using var response = await this.client.GetAsync(this.connectionContextCache.Value, "map", cancellationToken);
if (!response.IsSuccessStatusCode)
{
scopedLogger.LogError($"Received non-success response {response.StatusCode}");
Expand Down Expand Up @@ -555,7 +561,7 @@ public async Task EnsureInitialized(uint processId, CancellationToken cancellati

try
{
var response = await this.client.GetAsync(this.connectionContextCache.Value, $"game/state", cancellationToken);
using var response = await this.client.GetAsync(this.connectionContextCache.Value, $"game/state", cancellationToken);
if (!response.IsSuccessStatusCode)
{
scopedLogger.LogError($"Received non-success response {response.StatusCode}");
Expand Down Expand Up @@ -611,7 +617,7 @@ public async Task EnsureInitialized(uint processId, CancellationToken cancellati

try
{
var response = await this.client.GetAsync(this.connectionContextCache.Value, $"entities/name?id={entity.Id}", cancellationToken);
using 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}");
Expand Down Expand Up @@ -649,7 +655,7 @@ public async Task EnsureInitialized(uint processId, CancellationToken cancellati

try
{
var response = await this.client.GetAsync(this.connectionContextCache.Value, $"items/name?id={id}&modifiers={string.Join(',', modifiers?.Select(m => m.ToString()) ?? Array.Empty<string>())}", cancellationToken);
using var response = await this.client.GetAsync(this.connectionContextCache.Value, $"items/name?id={id}&modifiers={string.Join(',', modifiers?.Select(m => m.ToString()) ?? Array.Empty<string>())}", cancellationToken);
if (!response.IsSuccessStatusCode)
{
scopedLogger.LogError($"Received non-success response {response.StatusCode}");
Expand Down Expand Up @@ -688,7 +694,7 @@ public async Task EnsureInitialized(uint processId, CancellationToken cancellati

try
{
var response = await this.client.GetAsync(this.connectionContextCache.Value, $"titles/info?id={id}", cancellationToken);
using 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}");
Expand Down Expand Up @@ -725,6 +731,38 @@ public async Task EnsureInitialized(uint processId, CancellationToken cancellati
return default;
}

public async Task<bool> SendWhisper(string message, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger(nameof(this.GetTitleInformation), string.Empty);
if (this.connectionContextCache is null)
{
return false;
}

if (!this.liveOptions.Value.AllowWhispers)
{
return false;
}

try
{
using var textContent = new StringContent(message);
using var response = await this.client.PostAsync(this.connectionContextCache.Value, $"whisper", textContent, cancellationToken);
if (!response.IsSuccessStatusCode)
{
scopedLogger.LogError($"Received non-success response {response.StatusCode}");
return false;
}
}
catch (Exception ex)
{
scopedLogger.LogError(ex, "Encountered exception while parsing response");
this.faulty = true;
}

return false;
}

public void Stop()
{
this.connectionContextCache = default;
Expand Down
1 change: 1 addition & 0 deletions Daybreak/Services/Scanner/IGuildwarsMemoryReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ public interface IGuildwarsMemoryReader
Task<string?> GetEntityName(IEntity entity, CancellationToken cancellationToken);
Task<string?> GetItemName(int id, List<uint> modifiers, CancellationToken cancellationToken);
Task<TitleInformationExtended?> GetTitleInformation(int id, CancellationToken cancellationToken);
Task<bool> SendWhisper(string message, CancellationToken cancellationToken);
void Stop();
}

0 comments on commit 48955a7

Please sign in to comment.