From a5f0fe422ac2e45f0e2e4caea4347f23b2d098d0 Mon Sep 17 00:00:00 2001
From: karashiiro <49822414+karashiiro@users.noreply.github.com>
Date: Wed, 15 Feb 2023 09:26:32 -0800
Subject: [PATCH] feat(network): use Rx.NET to refactor message handlers
This rephrases network messages as observables of data, which
handlers subscribe to in order to receive strongly-typed data.
This breaks up the massive network message handler into an
event stream, which should make it more maintainable in the
future.
---
Dalamud/Dalamud.csproj | 1 +
.../Game/Network/Internal/NetworkHandlers.cs | 623 +++++++++++-------
.../Structures/MarketBoardPurchaseHandler.cs | 15 +-
3 files changed, 393 insertions(+), 246 deletions(-)
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index 9cb0784a1..26ff61776 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -82,6 +82,7 @@
+
diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs
index 968d97682..9523b08d4 100644
--- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs
+++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs
@@ -3,6 +3,9 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
+using System.Reactive;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
@@ -22,23 +25,46 @@ namespace Dalamud.Game.Network.Internal;
/// This class handles network notifications and uploading market board data.
///
[ServiceManager.EarlyLoadedService]
-internal class NetworkHandlers : IServiceType
+internal class NetworkHandlers : IDisposable, IServiceType
{
- private readonly List marketBoardRequests = new();
-
private readonly IMarketBoardUploader uploader;
+ private readonly List marketBoardRequests;
+ private readonly ISubject messages;
+
+ private readonly IDisposable handleMarketBoardItemRequest;
+ private readonly IDisposable handleMarketBoardOfferings;
+ private readonly IDisposable handleMarketBoardHistory;
+ private readonly IDisposable handleMarketTaxRates;
+ private readonly IDisposable handleMarketBoardPurchaseHandler;
+ private readonly IDisposable handleMarketBoardPurchase;
+ private readonly IDisposable handleCfPop;
+
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service.Get();
- private MarketBoardPurchaseHandler marketBoardPurchaseHandler;
+ private MarketBoardPurchaseHandler? marketBoardPurchaseHandler;
+
+ private bool disposing;
[ServiceManager.ServiceConstructor]
private NetworkHandlers(GameNetwork gameNetwork)
{
this.uploader = new UniversalisMarketBoardUploader();
+ this.marketBoardRequests = new List();
+ this.CfPop = (_, _) => { };
- gameNetwork.NetworkMessage += this.OnNetworkMessage;
+ this.messages = new Subject();
+
+ this.handleMarketBoardItemRequest = this.HandleMarketBoardItemRequest();
+ this.handleMarketBoardOfferings = this.HandleMarketBoardOfferings();
+ this.handleMarketBoardHistory = this.HandleMarketBoardHistory();
+ this.handleMarketTaxRates = this.HandleMarketTaxRates();
+ this.handleMarketBoardPurchaseHandler = this.HandleMarketBoardPurchaseHandler();
+ this.handleMarketBoardPurchase = this.HandleMarketBoardPurchase();
+ this.handleCfPop = this.HandleCfPop();
+
+ gameNetwork.NetworkMessage += this.ObserveNetworkMessage;
}
///
@@ -46,249 +72,368 @@ internal class NetworkHandlers : IServiceType
///
public event EventHandler CfPop;
- private void OnNetworkMessage(IntPtr dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction)
+ ///
+ /// Disposes of managed and unmanaged resources.
+ ///
+ public void Dispose()
{
- var dataManager = Service.GetNullable();
-
- if (dataManager?.IsDataReady != true)
- return;
-
- if (direction == NetworkMessageDirection.ZoneUp)
- {
- if (this.configuration.IsMbCollect)
- {
- if (opCode == dataManager.ClientOpCodes["MarketBoardPurchaseHandler"])
- {
- this.marketBoardPurchaseHandler = MarketBoardPurchaseHandler.Read(dataPtr);
- return;
- }
- }
-
- return;
- }
-
- if (opCode == dataManager.ServerOpCodes["CfNotifyPop"])
- {
- this.HandleCfPop(dataPtr);
- return;
- }
-
- if (this.configuration.IsMbCollect)
- {
- if (opCode == dataManager.ServerOpCodes["MarketBoardItemRequestStart"])
- {
- var data = MarketBoardItemRequest.Read(dataPtr);
- this.marketBoardRequests.Add(data);
-
- Log.Verbose($"NEW MB REQUEST START: item#{data.CatalogId} amount#{data.AmountToArrive}");
- return;
- }
-
- if (opCode == dataManager.ServerOpCodes["MarketBoardOfferings"])
- {
- var listing = MarketBoardCurrentOfferings.Read(dataPtr);
-
- var request = this.marketBoardRequests.LastOrDefault(r => r.CatalogId == listing.ItemListings[0].CatalogId && !r.IsDone);
-
- if (request == default)
- {
- Log.Error($"Market Board data arrived without a corresponding request: item#{listing.ItemListings[0].CatalogId}");
- return;
- }
-
- if (request.Listings.Count + listing.ItemListings.Count > request.AmountToArrive)
- {
- Log.Error($"Too many Market Board listings received for request: {request.Listings.Count + listing.ItemListings.Count} > {request.AmountToArrive} item#{listing.ItemListings[0].CatalogId}");
- return;
- }
-
- if (request.ListingsRequestId != -1 && request.ListingsRequestId != listing.RequestId)
- {
- Log.Error($"Non-matching RequestIds for Market Board data request: {request.ListingsRequestId}, {listing.RequestId}");
- return;
- }
-
- if (request.ListingsRequestId == -1 && request.Listings.Count > 0)
- {
- Log.Error($"Market Board data request sequence break: {request.ListingsRequestId}, {request.Listings.Count}");
- return;
- }
-
- if (request.ListingsRequestId == -1)
- {
- request.ListingsRequestId = listing.RequestId;
- Log.Verbose($"First Market Board packet in sequence: {listing.RequestId}");
- }
-
- request.Listings.AddRange(listing.ItemListings);
-
- Log.Verbose(
- "Added {0} ItemListings to request#{1}, now {2}/{3}, item#{4}",
- listing.ItemListings.Count,
- request.ListingsRequestId,
- request.Listings.Count,
- request.AmountToArrive,
- request.CatalogId);
-
- if (request.IsDone)
- {
- Log.Verbose(
- "Market Board request finished, starting upload: request#{0} item#{1} amount#{2}",
- request.ListingsRequestId,
- request.CatalogId,
- request.AmountToArrive);
-
- Task.Run(() => this.uploader.Upload(request))
- .ContinueWith((task) => Log.Error(task.Exception, "Market Board offerings data upload failed."), TaskContinuationOptions.OnlyOnFaulted);
- }
-
- return;
- }
-
- if (opCode == dataManager.ServerOpCodes["MarketBoardHistory"])
- {
- var listing = MarketBoardHistory.Read(dataPtr);
-
- var request = this.marketBoardRequests.LastOrDefault(r => r.CatalogId == listing.CatalogId);
-
- if (request == default)
- {
- Log.Error($"Market Board data arrived without a corresponding request: item#{listing.CatalogId}");
- return;
- }
-
- if (request.ListingsRequestId != -1)
- {
- Log.Error($"Market Board data history sequence break: {request.ListingsRequestId}, {request.Listings.Count}");
- return;
- }
-
- request.History.AddRange(listing.HistoryListings);
-
- Log.Verbose("Added history for item#{0}", listing.CatalogId);
-
- if (request.AmountToArrive == 0)
- {
- Log.Verbose("Request had 0 amount, uploading now");
-
- Task.Run(() => this.uploader.Upload(request))
- .ContinueWith((task) => Log.Error(task.Exception, "Market Board history data upload failed."), TaskContinuationOptions.OnlyOnFaulted);
- }
- }
-
- if (opCode == dataManager.ServerOpCodes["MarketTaxRates"])
- {
- var category = (uint)Marshal.ReadInt32(dataPtr);
-
- // Result dialog packet does not contain market tax rates
- if (category != 720905)
- {
- return;
- }
-
- var taxes = MarketTaxRates.Read(dataPtr);
-
- if (taxes.Category != 0xb0009)
- return;
-
- Log.Verbose(
- "MarketTaxRates: limsa#{0} grid#{1} uldah#{2} ish#{3} kugane#{4} cr#{5} sh#{6}",
- taxes.LimsaLominsaTax,
- taxes.GridaniaTax,
- taxes.UldahTax,
- taxes.IshgardTax,
- taxes.KuganeTax,
- taxes.CrystariumTax,
- taxes.SharlayanTax);
-
- Task.Run(() => this.uploader.UploadTax(taxes))
- .ContinueWith((task) => Log.Error(task.Exception, "Market Board tax data upload failed."), TaskContinuationOptions.OnlyOnFaulted);
-
- return;
- }
-
- if (opCode == dataManager.ServerOpCodes["MarketBoardPurchase"])
- {
- if (this.marketBoardPurchaseHandler == null)
- return;
-
- var purchase = MarketBoardPurchase.Read(dataPtr);
-
- var sameQty = purchase.ItemQuantity == this.marketBoardPurchaseHandler.ItemQuantity;
- var itemMatch = purchase.CatalogId == this.marketBoardPurchaseHandler.CatalogId;
- var itemMatchHq = purchase.CatalogId == this.marketBoardPurchaseHandler.CatalogId + 1_000_000;
-
- // Transaction succeeded
- if (sameQty && (itemMatch || itemMatchHq))
- {
- Log.Verbose($"Bought {purchase.ItemQuantity}x {this.marketBoardPurchaseHandler.CatalogId} for {this.marketBoardPurchaseHandler.PricePerUnit * purchase.ItemQuantity} gils, listing id is {this.marketBoardPurchaseHandler.ListingId}");
-
- var handler = this.marketBoardPurchaseHandler; // Capture the object so that we don't pass in a null one when the task starts.
-
- Task.Run(() => this.uploader.UploadPurchase(handler))
- .ContinueWith((task) => Log.Error(task.Exception, "Market Board purchase data upload failed."), TaskContinuationOptions.OnlyOnFaulted);
- }
-
- this.marketBoardPurchaseHandler = null;
- return;
- }
- }
+ this.disposing = true;
+ this.Dispose(this.disposing);
}
- private unsafe void HandleCfPop(IntPtr dataPtr)
+ ///
+ /// Disposes of managed and unmanaged resources.
+ ///
+ /// Whether or not to execute the disposal.
+ protected void Dispose(bool shouldDispose)
+ {
+ if (!shouldDispose)
+ return;
+
+ this.handleMarketBoardItemRequest.Dispose();
+ this.handleMarketBoardOfferings.Dispose();
+ this.handleMarketBoardHistory.Dispose();
+ this.handleMarketTaxRates.Dispose();
+ this.handleMarketBoardPurchaseHandler.Dispose();
+ this.handleMarketBoardPurchase.Dispose();
+ this.handleCfPop.Dispose();
+ }
+
+ private void ObserveNetworkMessage(
+ IntPtr dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction)
{
var dataManager = Service.GetNullable();
- if (dataManager == null)
- return;
-
- using var stream = new UnmanagedMemoryStream((byte*)dataPtr.ToPointer(), 64);
- using var reader = new BinaryReader(stream);
-
- var notifyType = reader.ReadByte();
- stream.Position += 0x1B;
- var conditionId = reader.ReadUInt16();
-
- if (notifyType != 3)
- return;
-
- var cfConditionSheet = dataManager.GetExcelSheet()!;
- var cfCondition = cfConditionSheet.GetRow(conditionId);
-
- if (cfCondition == null)
+ this.messages.OnNext(new NetworkMessage
{
- Log.Error($"CFC key {conditionId} not in Lumina data.");
- return;
- }
+ DataManager = dataManager,
+ Data = dataPtr,
+ Opcode = opCode,
+ SourceActorId = sourceActorId,
+ TargetActorId = targetActorId,
+ Direction = direction,
+ });
+ }
- var cfcName = cfCondition.Name.ToString();
- if (cfcName.IsNullOrEmpty())
- {
- cfcName = "Duty Roulette";
- cfCondition.Image = 112324;
- }
+ private IObservable OnNetworkMessage()
+ {
+ return this.messages.Where(message => message.DataManager?.IsDataReady == true);
+ }
- // Flash window
- if (this.configuration.DutyFinderTaskbarFlash && !NativeFunctions.ApplicationIsActivated())
- {
- var flashInfo = new NativeFunctions.FlashWindowInfo
- {
- Size = (uint)Marshal.SizeOf(),
- Count = uint.MaxValue,
- Timeout = 0,
- Flags = NativeFunctions.FlashWindow.All | NativeFunctions.FlashWindow.TimerNoFG,
- Hwnd = Process.GetCurrentProcess().MainWindowHandle,
- };
- NativeFunctions.FlashWindowEx(ref flashInfo);
- }
+ private IObservable OnMarketBoardItemRequest()
+ {
+ return this.OnNetworkMessage()
+ .Where(message => message.Direction == NetworkMessageDirection.ZoneDown)
+ .Where(message => message.Opcode ==
+ message.DataManager?.ServerOpCodes["MarketBoardItemRequestStart"])
+ .Select(message => MarketBoardItemRequest.Read(message.Data));
+ }
- Task.Run(() =>
- {
- if (this.configuration.DutyFinderChatMessage)
- {
- Service.GetNullable()?.Print($"Duty pop: {cfcName}");
- }
+ private IObservable OnMarketBoardOfferings()
+ {
+ return this.OnNetworkMessage()
+ .Where(message => message.Direction == NetworkMessageDirection.ZoneDown)
+ .Where(message => message.Opcode == message.DataManager?.ServerOpCodes["MarketBoardOfferings"])
+ .Select(message => MarketBoardCurrentOfferings.Read(message.Data));
+ }
- this.CfPop?.InvokeSafely(this, cfCondition);
- }).ContinueWith((task) => Log.Error(task.Exception, "CfPop.Invoke failed."), TaskContinuationOptions.OnlyOnFaulted);
+ private IObservable OnMarketBoardHistory()
+ {
+ return this.OnNetworkMessage()
+ .Where(message => message.Direction == NetworkMessageDirection.ZoneDown)
+ .Where(message => message.Opcode == message.DataManager?.ServerOpCodes["MarketBoardHistory"])
+ .Select(message => MarketBoardHistory.Read(message.Data));
+ }
+
+ private IObservable OnMarketTaxRates()
+ {
+ return this.OnNetworkMessage()
+ .Where(message => message.Direction == NetworkMessageDirection.ZoneDown)
+ .Where(message => message.Opcode == message.DataManager?.ServerOpCodes["MarketTaxRates"])
+ .Where(message =>
+ {
+ // Only some categories of the result dialog packet contain market tax rates
+ var category = (uint)Marshal.ReadInt32(message.Data);
+ return category == 720905;
+ })
+ .Select(message => MarketTaxRates.Read(message.Data))
+ .Where(taxes => taxes.Category == 0xb0009);
+ }
+
+ private IObservable OnMarketBoardPurchaseHandler()
+ {
+ return this.OnNetworkMessage()
+ .Where(message => message.Direction == NetworkMessageDirection.ZoneUp)
+ .Where(message => message.Opcode == message.DataManager?.ClientOpCodes["MarketBoardPurchaseHandler"])
+ .Select(message => MarketBoardPurchaseHandler.Read(message.Data));
+ }
+
+ private IObservable OnMarketBoardPurchase()
+ {
+ return this.OnNetworkMessage()
+ .Where(message => message.Direction == NetworkMessageDirection.ZoneDown)
+ .Where(message => message.Opcode == message.DataManager?.ServerOpCodes["MarketBoardPurchase"])
+ .Select(message => MarketBoardPurchase.Read(message.Data));
+ }
+
+ private IObservable OnCfNotifyPop()
+ {
+ return this.OnNetworkMessage()
+ .Where(message => message.Direction == NetworkMessageDirection.ZoneDown)
+ .Where(message => message.Opcode == message.DataManager?.ServerOpCodes["CfNotifyPop"]);
+ }
+
+ private IDisposable HandleMarketBoardItemRequest()
+ {
+ return this.OnMarketBoardItemRequest()
+ .Where(_ => this.configuration.IsMbCollect)
+ .Subscribe(request =>
+ {
+ this.marketBoardRequests.Add(request);
+ Log.Verbose($"NEW MB REQUEST START: item#{request.CatalogId} amount#{request.AmountToArrive}");
+ });
+ }
+
+ private IDisposable HandleMarketBoardOfferings()
+ {
+ return this.OnMarketBoardOfferings()
+ .Where(_ => this.configuration.IsMbCollect)
+ .Subscribe(listing =>
+ {
+ var request =
+ this.marketBoardRequests.LastOrDefault(
+ r => r.CatalogId == listing.ItemListings[0].CatalogId && !r.IsDone);
+
+ if (request == default)
+ {
+ Log.Error(
+ $"Market Board data arrived without a corresponding request: item#{listing.ItemListings[0].CatalogId}");
+ return;
+ }
+
+ if (request.Listings.Count + listing.ItemListings.Count > request.AmountToArrive)
+ {
+ Log.Error(
+ $"Too many Market Board listings received for request: {request.Listings.Count + listing.ItemListings.Count} > {request.AmountToArrive} item#{listing.ItemListings[0].CatalogId}");
+ return;
+ }
+
+ if (request.ListingsRequestId != -1 && request.ListingsRequestId != listing.RequestId)
+ {
+ Log.Error(
+ $"Non-matching RequestIds for Market Board data request: {request.ListingsRequestId}, {listing.RequestId}");
+ return;
+ }
+
+ if (request.ListingsRequestId == -1 && request.Listings.Count > 0)
+ {
+ Log.Error(
+ $"Market Board data request sequence break: {request.ListingsRequestId}, {request.Listings.Count}");
+ return;
+ }
+
+ if (request.ListingsRequestId == -1)
+ {
+ request.ListingsRequestId = listing.RequestId;
+ Log.Verbose($"First Market Board packet in sequence: {listing.RequestId}");
+ }
+
+ request.Listings.AddRange(listing.ItemListings);
+
+ Log.Verbose(
+ "Added {0} ItemListings to request#{1}, now {2}/{3}, item#{4}",
+ listing.ItemListings.Count,
+ request.ListingsRequestId,
+ request.Listings.Count,
+ request.AmountToArrive,
+ request.CatalogId);
+
+ if (request.IsDone)
+ {
+ Log.Verbose(
+ "Market Board request finished, starting upload: request#{0} item#{1} amount#{2}",
+ request.ListingsRequestId,
+ request.CatalogId,
+ request.AmountToArrive);
+
+ Task.Run(() => this.uploader.Upload(request))
+ .ContinueWith(
+ task => Log.Error(task.Exception, "Market Board offerings data upload failed."),
+ TaskContinuationOptions.OnlyOnFaulted);
+ }
+ });
+ }
+
+ private IDisposable HandleMarketBoardHistory()
+ {
+ return this.OnMarketBoardHistory()
+ .Where(_ => this.configuration.IsMbCollect)
+ .Subscribe(listing =>
+ {
+ var request = this.marketBoardRequests.LastOrDefault(r => r.CatalogId == listing.CatalogId);
+
+ if (request == default)
+ {
+ Log.Error(
+ $"Market Board data arrived without a corresponding request: item#{listing.CatalogId}");
+ return;
+ }
+
+ if (request.ListingsRequestId != -1)
+ {
+ Log.Error(
+ $"Market Board data history sequence break: {request.ListingsRequestId}, {request.Listings.Count}");
+ return;
+ }
+
+ request.History.AddRange(listing.HistoryListings);
+
+ Log.Verbose("Added history for item#{0}", listing.CatalogId);
+
+ if (request.AmountToArrive == 0)
+ {
+ Log.Verbose("Request had 0 amount, uploading now");
+
+ Task.Run(() => this.uploader.Upload(request))
+ .ContinueWith(
+ (task) => Log.Error(task.Exception, "Market Board history data upload failed."),
+ TaskContinuationOptions.OnlyOnFaulted);
+ }
+ });
+ }
+
+ private IDisposable HandleMarketTaxRates()
+ {
+ return this.OnMarketTaxRates()
+ .Where(_ => this.configuration.IsMbCollect)
+ .Subscribe(taxes =>
+ {
+ Log.Verbose(
+ "MarketTaxRates: limsa#{0} grid#{1} uldah#{2} ish#{3} kugane#{4} cr#{5} sh#{6}",
+ taxes.LimsaLominsaTax,
+ taxes.GridaniaTax,
+ taxes.UldahTax,
+ taxes.IshgardTax,
+ taxes.KuganeTax,
+ taxes.CrystariumTax,
+ taxes.SharlayanTax);
+
+ Task.Run(() => this.uploader.UploadTax(taxes))
+ .ContinueWith(
+ task => Log.Error(task.Exception, "Market Board tax data upload failed."),
+ TaskContinuationOptions.OnlyOnFaulted);
+ });
+ }
+
+ private IDisposable HandleMarketBoardPurchaseHandler()
+ {
+ return this.OnMarketBoardPurchaseHandler()
+ .Where(_ => this.configuration.IsMbCollect)
+ .Subscribe(handler => { this.marketBoardPurchaseHandler = handler; });
+ }
+
+ private IDisposable HandleMarketBoardPurchase()
+ {
+ return this.OnMarketBoardPurchase()
+ .Where(_ => this.configuration.IsMbCollect)
+ .Subscribe(purchase =>
+ {
+ if (this.marketBoardPurchaseHandler == null)
+ return;
+
+ var sameQty = purchase.ItemQuantity == this.marketBoardPurchaseHandler.ItemQuantity;
+ var itemMatch = purchase.CatalogId == this.marketBoardPurchaseHandler.CatalogId;
+ var itemMatchHq = purchase.CatalogId == this.marketBoardPurchaseHandler.CatalogId + 1_000_000;
+
+ // Transaction succeeded
+ if (sameQty && (itemMatch || itemMatchHq))
+ {
+ Log.Verbose(
+ $"Bought {purchase.ItemQuantity}x {this.marketBoardPurchaseHandler.CatalogId} for {this.marketBoardPurchaseHandler.PricePerUnit * purchase.ItemQuantity} gils, listing id is {this.marketBoardPurchaseHandler.ListingId}");
+
+ var handler =
+ this.marketBoardPurchaseHandler; // Capture the object so that we don't pass in a null one when the task starts.
+
+ Task.Run(() => this.uploader.UploadPurchase(handler))
+ .ContinueWith(
+ task => Log.Error(task.Exception, "Market Board purchase data upload failed."),
+ TaskContinuationOptions.OnlyOnFaulted);
+ }
+
+ this.marketBoardPurchaseHandler = null;
+ });
+ }
+
+ private unsafe IDisposable HandleCfPop()
+ {
+ return this.OnCfNotifyPop()
+ .Subscribe(message =>
+ {
+ using var stream = new UnmanagedMemoryStream((byte*)message.Data.ToPointer(), 64);
+ using var reader = new BinaryReader(stream);
+
+ var notifyType = reader.ReadByte();
+ stream.Position += 0x1B;
+ var conditionId = reader.ReadUInt16();
+
+ if (notifyType != 3)
+ return;
+
+ var cfConditionSheet = message.DataManager!.GetExcelSheet()!;
+ var cfCondition = cfConditionSheet.GetRow(conditionId);
+
+ if (cfCondition == null)
+ {
+ Log.Error($"CFC key {conditionId} not in Lumina data.");
+ return;
+ }
+
+ var cfcName = cfCondition.Name.ToString();
+ if (cfcName.IsNullOrEmpty())
+ {
+ cfcName = "Duty Roulette";
+ cfCondition.Image = 112324;
+ }
+
+ // Flash window
+ if (this.configuration.DutyFinderTaskbarFlash && !NativeFunctions.ApplicationIsActivated())
+ {
+ var flashInfo = new NativeFunctions.FlashWindowInfo
+ {
+ Size = (uint)Marshal.SizeOf(),
+ Count = uint.MaxValue,
+ Timeout = 0,
+ Flags = NativeFunctions.FlashWindow.All | NativeFunctions.FlashWindow.TimerNoFG,
+ Hwnd = Process.GetCurrentProcess().MainWindowHandle,
+ };
+ NativeFunctions.FlashWindowEx(ref flashInfo);
+ }
+
+ Task.Run(() =>
+ {
+ if (this.configuration.DutyFinderChatMessage)
+ {
+ Service.GetNullable()?.Print($"Duty pop: {cfcName}");
+ }
+
+ this.CfPop.InvokeSafely(this, cfCondition);
+ }).ContinueWith(
+ task => Log.Error(task.Exception, "CfPop.Invoke failed."),
+ TaskContinuationOptions.OnlyOnFaulted);
+ });
+ }
+
+ private class NetworkMessage
+ {
+ public DataManager? DataManager { get; init; }
+
+ public IntPtr Data { get; init; }
+
+ public ushort Opcode { get; init; }
+
+ public uint SourceActorId { get; init; }
+
+ public uint TargetActorId { get; init; }
+
+ public NetworkMessageDirection Direction { get; init; }
}
}
diff --git a/Dalamud/Game/Network/Structures/MarketBoardPurchaseHandler.cs b/Dalamud/Game/Network/Structures/MarketBoardPurchaseHandler.cs
index fdc7bd83a..0d0c18329 100644
--- a/Dalamud/Game/Network/Structures/MarketBoardPurchaseHandler.cs
+++ b/Dalamud/Game/Network/Structures/MarketBoardPurchaseHandler.cs
@@ -48,13 +48,14 @@ public class MarketBoardPurchaseHandler
using var stream = new UnmanagedMemoryStream((byte*)dataPtr.ToPointer(), 1544);
using var reader = new BinaryReader(stream);
- var output = new MarketBoardPurchaseHandler();
-
- output.RetainerId = reader.ReadUInt64();
- output.ListingId = reader.ReadUInt64();
- output.CatalogId = reader.ReadUInt32();
- output.ItemQuantity = reader.ReadUInt32();
- output.PricePerUnit = reader.ReadUInt32();
+ var output = new MarketBoardPurchaseHandler
+ {
+ RetainerId = reader.ReadUInt64(),
+ ListingId = reader.ReadUInt64(),
+ CatalogId = reader.ReadUInt32(),
+ ItemQuantity = reader.ReadUInt32(),
+ PricePerUnit = reader.ReadUInt32(),
+ };
return output;
}