diff --git a/Dalamud/Game/Marketboard/MarketBoard.cs b/Dalamud/Game/Marketboard/MarketBoard.cs
new file mode 100644
index 000000000..2223cc1a8
--- /dev/null
+++ b/Dalamud/Game/Marketboard/MarketBoard.cs
@@ -0,0 +1,165 @@
+using Dalamud.IoC;
+using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
+
+namespace Dalamud.Game.MarketBoard;
+
+using Network.Internal;
+using Network.Structures;
+
+///
+/// This class provides access to market board events
+///
+[InterfaceVersion("1.0")]
+[ServiceManager.EarlyLoadedService]
+internal class MarketBoard : IInternalDisposableService, IMarketBoard
+{
+ [ServiceManager.ServiceDependency]
+ private readonly NetworkHandlers networkHandlers = Service.Get();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [ServiceManager.ServiceConstructor]
+ public MarketBoard()
+ {
+ this.networkHandlers.MbHistoryObservable.Subscribe(this.OnMbHistory);
+ this.networkHandlers.MbPurchaseObservable.Subscribe(this.OnPurchase);
+ this.networkHandlers.MbOfferingsObservable.Subscribe(this.OnOfferings);
+ this.networkHandlers.MbPurchaseSentObservable.Subscribe(this.OnPurchaseSent);
+ this.networkHandlers.MbTaxesObservable.Subscribe(this.OnTaxRates);
+ }
+
+ ///
+ public event IMarketBoard.HistoryReceivedDelegate? HistoryReceived;
+
+ ///
+ public event IMarketBoard.ItemPurchasedDelegate? ItemPurchased;
+
+ ///
+ public event IMarketBoard.OfferingsReceivedDelegate? OfferingsReceived;
+
+ ///
+ public event IMarketBoard.PurchaseRequestedDelegate? PurchaseRequested;
+
+ ///
+ public event IMarketBoard.TaxRatesReceivedDelegate? TaxRatesReceived;
+
+ ///
+ public void DisposeService()
+ {
+ this.HistoryReceived = null;
+ this.ItemPurchased = null;
+ this.OfferingsReceived = null;
+ this.PurchaseRequested = null;
+ this.TaxRatesReceived = null;
+ }
+
+ private void OnMbHistory(MarketBoardHistory marketBoardHistory)
+ {
+ this.HistoryReceived?.Invoke(marketBoardHistory);
+ }
+
+ private void OnPurchase(MarketBoardPurchase marketBoardHistory)
+ {
+ this.ItemPurchased?.Invoke(marketBoardHistory);
+ }
+
+ private void OnOfferings(MarketBoardCurrentOfferings currentOfferings)
+ {
+ this.OfferingsReceived?.Invoke(currentOfferings);
+ }
+
+ private void OnPurchaseSent(MarketBoardPurchaseHandler purchaseHandler)
+ {
+ this.PurchaseRequested?.Invoke(purchaseHandler);
+ }
+
+ private void OnTaxRates(MarketTaxRates taxRates)
+ {
+ this.TaxRatesReceived?.Invoke(taxRates);
+ }
+}
+
+///
+/// Plugin scoped version of MarketBoard.
+///
+[PluginInterface]
+[InterfaceVersion("1.0")]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class MarketBoardPluginScoped : IInternalDisposableService, IMarketBoard
+{
+ [ServiceManager.ServiceDependency]
+ private readonly MarketBoard marketBoardService = Service.Get();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal MarketBoardPluginScoped()
+ {
+ this.marketBoardService.HistoryReceived += this.OnHistoryReceived;
+ this.marketBoardService.ItemPurchased += this.OnItemPurchased;
+ this.marketBoardService.OfferingsReceived += this.OnOfferingsReceived;
+ this.marketBoardService.PurchaseRequested += this.OnPurchaseRequested;
+ this.marketBoardService.TaxRatesReceived += this.OnTaxRatesReceived;
+ }
+
+ ///
+ public event IMarketBoard.HistoryReceivedDelegate? HistoryReceived;
+
+ ///
+ public event IMarketBoard.ItemPurchasedDelegate? ItemPurchased;
+
+ ///
+ public event IMarketBoard.OfferingsReceivedDelegate? OfferingsReceived;
+
+ ///
+ public event IMarketBoard.PurchaseRequestedDelegate? PurchaseRequested;
+
+ ///
+ public event IMarketBoard.TaxRatesReceivedDelegate? TaxRatesReceived;
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ this.marketBoardService.HistoryReceived -= this.OnHistoryReceived;
+ this.marketBoardService.ItemPurchased -= this.OnItemPurchased;
+ this.marketBoardService.OfferingsReceived -= this.OnOfferingsReceived;
+ this.marketBoardService.PurchaseRequested -= this.OnPurchaseRequested;
+ this.marketBoardService.TaxRatesReceived -= this.OnTaxRatesReceived;
+
+ this.HistoryReceived = null;
+ this.ItemPurchased = null;
+ this.OfferingsReceived = null;
+ this.PurchaseRequested = null;
+ this.TaxRatesReceived = null;
+ }
+
+ private void OnHistoryReceived(IMarketBoardHistory history)
+ {
+ this.HistoryReceived?.Invoke(history);
+ }
+
+ private void OnItemPurchased(IMarketBoardPurchase purchase)
+ {
+ this.ItemPurchased?.Invoke(purchase);
+ }
+
+ private void OnOfferingsReceived(IMarketBoardCurrentOfferings currentOfferings)
+ {
+ this.OfferingsReceived?.Invoke(currentOfferings);
+ }
+
+ private void OnPurchaseRequested(IMarketBoardPurchaseHandler purchaseHandler)
+ {
+ this.PurchaseRequested?.Invoke(purchaseHandler);
+ }
+
+ private void OnTaxRatesReceived(IMarketTaxRates taxRates)
+ {
+ this.TaxRatesReceived?.Invoke(taxRates);
+ }
+}
diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs
index f7091817c..0d08b90d9 100644
--- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs
+++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs
@@ -209,6 +209,18 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private event Action? MarketBoardPurchaseRequestSent;
+ public IObservable MbPurchaseObservable => this.mbPurchaseObservable;
+
+ public IObservable MbHistoryObservable => this.mbHistoryObservable;
+
+ public IObservable MbTaxesObservable => this.mbTaxesObservable;
+
+ public IObservable MbItemRequestObservable => this.mbItemRequestObservable;
+
+ public IObservable MbOfferingsObservable => this.mbOfferingsObservable;
+
+ public IObservable MbPurchaseSentObservable => this.mbPurchaseSentObservable;
+
///
/// Disposes of managed and unmanaged resources.
///
@@ -301,7 +313,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private IObservable> OnMarketBoardListingsBatch(
IObservable start)
{
- var offeringsObservable = this.mbOfferingsObservable.Publish().RefCount();
+ var offeringsObservable = this.MbOfferingsObservable.Publish().RefCount();
void LogEndObserved(MarketBoardCurrentOfferings offerings)
{
@@ -315,7 +327,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
Log.Verbose(
"Observed element of request {RequestId} with {NumListings} listings",
offerings.RequestId,
- offerings.ItemListings.Count);
+ offerings.InternalItemListings.Count);
}
IObservable UntilBatchEnd(MarketBoardItemRequest request)
@@ -327,7 +339,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
}
return offeringsObservable
- .Where(offerings => offerings.ItemListings.All(l => l.CatalogId == request.CatalogId))
+ .Where(offerings => offerings.InternalItemListings.All(l => l.CatalogId == request.CatalogId))
.Skip(totalPackets - 1)
.Do(LogEndObserved);
}
@@ -343,7 +355,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
new List(),
(agg, next) =>
{
- agg.AddRange(next.ItemListings);
+ agg.AddRange(next.InternalItemListings);
return agg;
}));
}
@@ -351,14 +363,14 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private IObservable> OnMarketBoardSalesBatch(
IObservable start)
{
- var historyObservable = this.mbHistoryObservable.Publish().RefCount();
+ var historyObservable = this.MbHistoryObservable.Publish().RefCount();
void LogHistoryObserved(MarketBoardHistory history)
{
Log.Verbose(
"Observed history for item {CatalogId} with {NumSales} sales",
history.CatalogId,
- history.HistoryListings.Count);
+ history.InternalHistoryListings.Count);
}
IObservable UntilBatchEnd(MarketBoardItemRequest request)
@@ -379,7 +391,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
new List(),
(agg, next) =>
{
- agg.AddRange(next.HistoryListings);
+ agg.AddRange(next.InternalHistoryListings);
return agg;
}));
}
@@ -394,7 +406,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
request.AmountToArrive);
}
- var startObservable = this.mbItemRequestObservable
+ var startObservable = this.MbItemRequestObservable
.Where(request => request.Ok).Do(LogStartObserved)
.Publish()
.RefCount();
@@ -450,7 +462,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private IDisposable HandleMarketTaxRates()
{
- return this.mbTaxesObservable
+ return this.MbTaxesObservable
.Where(this.ShouldUpload)
.SubscribeOn(ThreadPoolScheduler.Instance)
.Subscribe(
@@ -476,8 +488,8 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private IDisposable HandleMarketBoardPurchaseHandler()
{
- return this.mbPurchaseSentObservable
- .Zip(this.mbPurchaseObservable)
+ return this.MbPurchaseSentObservable
+ .Zip(this.MbPurchaseObservable)
.Where(this.ShouldUpload)
.SubscribeOn(ThreadPoolScheduler.Instance)
.Subscribe(
@@ -544,7 +556,8 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
{
try
{
- if (eventId == 7 && responseId == 8)
+ // Event ID 0 covers the crystarium, 7 covers all other cities
+ if (eventId is 7 or 0 && responseId == 8)
this.MarketBoardTaxesReceived?.InvokeSafely((nint)args);
}
catch (Exception ex)
diff --git a/Dalamud/Game/Network/Structures/MarketBoardCurrentOfferings.cs b/Dalamud/Game/Network/Structures/MarketBoardCurrentOfferings.cs
index fc782cd51..324a00d65 100644
--- a/Dalamud/Game/Network/Structures/MarketBoardCurrentOfferings.cs
+++ b/Dalamud/Game/Network/Structures/MarketBoardCurrentOfferings.cs
@@ -7,7 +7,7 @@ namespace Dalamud.Game.Network.Structures;
///
/// This class represents the current market board offerings from a game network packet.
///
-public class MarketBoardCurrentOfferings
+public class MarketBoardCurrentOfferings : IMarketBoardCurrentOfferings
{
private MarketBoardCurrentOfferings()
{
@@ -16,7 +16,10 @@ public class MarketBoardCurrentOfferings
///
/// Gets the list of individual item listings.
///
- public List ItemListings { get; } = new();
+ IReadOnlyList IMarketBoardCurrentOfferings.ItemListings => this.InternalItemListings;
+
+ internal List InternalItemListings { get; set; } = new List();
+
///
/// Gets the listing end index.
@@ -45,6 +48,8 @@ public class MarketBoardCurrentOfferings
using var stream = new UnmanagedMemoryStream((byte*)dataPtr.ToPointer(), 1544);
using var reader = new BinaryReader(stream);
+ var listings = new List();
+
for (var i = 0; i < 10; i++)
{
var listingEntry = new MarketBoardItemListing();
@@ -64,6 +69,8 @@ public class MarketBoardCurrentOfferings
reader.ReadUInt16(); // durability
reader.ReadUInt16(); // spiritbond
+ var materiaList = new List();
+
for (var materiaIndex = 0; materiaIndex < 5; materiaIndex++)
{
var materiaVal = reader.ReadUInt16();
@@ -74,9 +81,11 @@ public class MarketBoardCurrentOfferings
};
if (materiaEntry.MateriaId != 0)
- listingEntry.Materia.Add(materiaEntry);
+ materiaList.Add(materiaEntry);
}
+ listingEntry.Materia = materiaList;
+
reader.ReadUInt16();
reader.ReadUInt32();
@@ -92,9 +101,10 @@ public class MarketBoardCurrentOfferings
reader.ReadUInt32();
if (listingEntry.CatalogId != 0)
- output.ItemListings.Add(listingEntry);
+ listings.Add(listingEntry);
}
+ output.InternalItemListings = listings;
output.ListingIndexEnd = reader.ReadByte();
output.ListingIndexStart = reader.ReadByte();
output.RequestId = reader.ReadUInt16();
@@ -105,7 +115,7 @@ public class MarketBoardCurrentOfferings
///
/// This class represents the current market board offering of a single item from the network packet.
///
- public class MarketBoardItemListing
+ public class MarketBoardItemListing : IMarketBoardItemListing
{
///
/// Initializes a new instance of the class.
@@ -119,6 +129,9 @@ public class MarketBoardCurrentOfferings
///
public ulong ArtisanId { get; internal set; }
+ ///
+ public uint ItemId => this.CatalogId;
+
///
/// Gets the catalog ID.
///
@@ -147,7 +160,7 @@ public class MarketBoardCurrentOfferings
///
/// Gets the list of materia attached to this item.
///
- public List Materia { get; } = new();
+ public IReadOnlyList Materia { get; internal set; } = new List();
///
/// Gets the amount of attached materia.
@@ -202,7 +215,7 @@ public class MarketBoardCurrentOfferings
///
/// This represents the materia slotted to an .
///
- public class ItemMateria
+ public class ItemMateria : IItemMateria
{
///
/// Initializes a new instance of the class.
@@ -223,3 +236,132 @@ public class MarketBoardCurrentOfferings
}
}
}
+
+///
+/// An interface that represents the current market board offerings.
+///
+public interface IMarketBoardCurrentOfferings
+{
+ ///
+ /// Gets the list of individual item listings.
+ ///
+ IReadOnlyList ItemListings { get; }
+
+ ///
+ /// Gets the listing end index.
+ ///
+ int ListingIndexEnd { get; }
+
+ ///
+ /// Gets the listing start index.
+ ///
+ int ListingIndexStart { get; }
+
+ ///
+ /// Gets the request ID.
+ ///
+ int RequestId { get; }
+}
+
+///
+/// An interface that represents the current market board offering of a single item from the .
+///
+public interface IMarketBoardItemListing
+{
+ ///
+ /// Gets the artisan ID.
+ ///
+ ulong ArtisanId { get; }
+
+ ///
+ /// Gets the item ID.
+ ///
+ uint ItemId { get; }
+
+ ///
+ /// Gets a value indicating whether the item is HQ.
+ ///
+ bool IsHq { get; }
+
+ ///
+ /// Gets the item quantity.
+ ///
+ uint ItemQuantity { get; }
+
+ ///
+ /// Gets the time this offering was last reviewed.
+ ///
+ DateTime LastReviewTime { get; }
+
+ ///
+ /// Gets the listing ID.
+ ///
+ ulong ListingId { get; }
+
+ ///
+ /// Gets the list of materia attached to this item.
+ ///
+ IReadOnlyList Materia { get; }
+
+ ///
+ /// Gets the amount of attached materia.
+ ///
+ int MateriaCount { get; }
+
+ ///
+ /// Gets a value indicating whether this item is on a mannequin.
+ ///
+ bool OnMannequin { get; }
+
+ ///
+ /// Gets the player name.
+ ///
+ string PlayerName { get; }
+
+ ///
+ /// Gets the price per unit.
+ ///
+ uint PricePerUnit { get; }
+
+ ///
+ /// Gets the city ID of the retainer selling the item.
+ ///
+ int RetainerCityId { get; }
+
+ ///
+ /// Gets the ID of the retainer selling the item.
+ ///
+ ulong RetainerId { get; }
+
+ ///
+ /// Gets the name of the retainer.
+ ///
+ string RetainerName { get; }
+
+ ///
+ /// Gets the stain or applied dye of the item.
+ ///
+ int StainId { get; }
+
+ ///
+ /// Gets the total tax.
+ ///
+ uint TotalTax { get; }
+}
+
+///
+/// An interface that represents the materia slotted to an .
+///
+public interface IItemMateria
+{
+ ///
+ /// Gets the materia index.
+ ///
+ int Index { get; }
+
+ ///
+ /// Gets the materia ID.
+ ///
+ int MateriaId { get; }
+}
+
diff --git a/Dalamud/Game/Network/Structures/MarketBoardHistory.cs b/Dalamud/Game/Network/Structures/MarketBoardHistory.cs
index 148c321db..fd1011bca 100644
--- a/Dalamud/Game/Network/Structures/MarketBoardHistory.cs
+++ b/Dalamud/Game/Network/Structures/MarketBoardHistory.cs
@@ -7,7 +7,7 @@ namespace Dalamud.Game.Network.Structures;
///
/// This class represents the market board history from a game network packet.
///
-public class MarketBoardHistory
+public class MarketBoardHistory : IMarketBoardHistory
{
///
/// Initializes a new instance of the class.
@@ -26,10 +26,17 @@ public class MarketBoardHistory
///
public uint CatalogId2 { get; private set; }
+ public uint ItemId => this.CatalogId;
+
///
- /// Gets the list of individual item history listings.
+ /// Gets the list of individual item listings.
///
- public List HistoryListings { get; } = new();
+ IReadOnlyList IMarketBoardHistory.HistoryListings => this.InternalHistoryListings;
+
+ ///
+ /// Gets or sets a list of individual item listings.
+ ///
+ internal List InternalHistoryListings { get; set; } = new List();
///
/// Read a object from memory.
@@ -53,6 +60,7 @@ public class MarketBoardHistory
return output;
}
+ var historyListings = new List();
for (var i = 0; i < 20; i++)
{
var listingEntry = new MarketBoardHistoryListing
@@ -69,19 +77,21 @@ public class MarketBoardHistory
listingEntry.BuyerName = Encoding.UTF8.GetString(reader.ReadBytes(33)).TrimEnd('\u0000');
listingEntry.NextCatalogId = reader.ReadUInt32();
- output.HistoryListings.Add(listingEntry);
+ historyListings.Add(listingEntry);
if (listingEntry.NextCatalogId == 0)
break;
}
+ output.InternalHistoryListings = historyListings;
+
return output;
}
///
/// This class represents the market board history of a single item from the network packet.
///
- public class MarketBoardHistoryListing
+ public class MarketBoardHistoryListing : IMarketBoardHistoryListing
{
///
/// Initializes a new instance of the class.
@@ -126,3 +136,55 @@ public class MarketBoardHistory
public uint SalePrice { get; internal set; }
}
}
+
+///
+/// An interface that represents the market board history from the game.
+///
+public interface IMarketBoardHistory
+{
+ ///
+ /// Gets the item ID.
+ ///
+ uint ItemId { get; }
+
+ ///
+ /// Gets the list of individual item history listings.
+ ///
+ IReadOnlyList HistoryListings { get; }
+}
+
+///
+/// An interface that represents the market board history of a single item from .
+///
+public interface IMarketBoardHistoryListing
+{
+ ///
+ /// Gets the buyer's name.
+ ///
+ string BuyerName { get; }
+
+ ///
+ /// Gets a value indicating whether the item is HQ.
+ ///
+ bool IsHq { get; }
+
+ ///
+ /// Gets a value indicating whether the item is on a mannequin.
+ ///
+ bool OnMannequin { get; }
+
+ ///
+ /// Gets the time of purchase.
+ ///
+ DateTime PurchaseTime { get; }
+
+ ///
+ /// Gets the quantity.
+ ///
+ uint Quantity { get; }
+
+ ///
+ /// Gets the sale price.
+ ///
+ uint SalePrice { get; }
+}
diff --git a/Dalamud/Game/Network/Structures/MarketBoardPurchase.cs b/Dalamud/Game/Network/Structures/MarketBoardPurchase.cs
index 62d104cff..4221188e4 100644
--- a/Dalamud/Game/Network/Structures/MarketBoardPurchase.cs
+++ b/Dalamud/Game/Network/Structures/MarketBoardPurchase.cs
@@ -6,7 +6,7 @@ namespace Dalamud.Game.Network.Structures;
/// Represents market board purchase information. This message is received from the
/// server when a purchase is made at a market board.
///
-public class MarketBoardPurchase
+public class MarketBoardPurchase : IMarketBoardPurchase
{
private MarketBoardPurchase()
{
@@ -41,3 +41,20 @@ public class MarketBoardPurchase
return output;
}
}
+
+///
+/// An interface that represents market board purchase information. This message is received from the
+/// server when a purchase is made at a market board.
+///
+public interface IMarketBoardPurchase
+{
+ ///
+ /// Gets the item ID of the item that was purchased.
+ ///
+ uint CatalogId { get; }
+
+ ///
+ /// Gets the quantity of the item that was purchased.
+ ///
+ uint ItemQuantity { get; }
+}
diff --git a/Dalamud/Game/Network/Structures/MarketBoardPurchaseHandler.cs b/Dalamud/Game/Network/Structures/MarketBoardPurchaseHandler.cs
index 783e62cda..3fadc70b5 100644
--- a/Dalamud/Game/Network/Structures/MarketBoardPurchaseHandler.cs
+++ b/Dalamud/Game/Network/Structures/MarketBoardPurchaseHandler.cs
@@ -6,7 +6,7 @@ namespace Dalamud.Game.Network.Structures;
/// Represents market board purchase information. This message is sent from the
/// client when a purchase is made at a market board.
///
-public class MarketBoardPurchaseHandler
+public class MarketBoardPurchaseHandler : IMarketBoardPurchaseHandler
{
private MarketBoardPurchaseHandler()
{
@@ -59,3 +59,35 @@ public class MarketBoardPurchaseHandler
return output;
}
}
+
+///
+/// An interface that represents market board purchase information. This message is sent from the
+/// client when a purchase is made at a market board.
+///
+public interface IMarketBoardPurchaseHandler
+{
+ ///
+ /// Gets the object ID of the retainer associated with the sale.
+ ///
+ ulong RetainerId { get; }
+
+ ///
+ /// Gets the object ID of the item listing.
+ ///
+ ulong ListingId { get; }
+
+ ///
+ /// Gets the item ID of the item that was purchased.
+ ///
+ uint CatalogId { get; }
+
+ ///
+ /// Gets the quantity of the item that was purchased.
+ ///
+ uint ItemQuantity { get; }
+
+ ///
+ /// Gets the unit price of the item.
+ ///
+ uint PricePerUnit { get; }
+}
diff --git a/Dalamud/Game/Network/Structures/MarketTaxRates.cs b/Dalamud/Game/Network/Structures/MarketTaxRates.cs
index 42e1d8cce..d5802f106 100644
--- a/Dalamud/Game/Network/Structures/MarketTaxRates.cs
+++ b/Dalamud/Game/Network/Structures/MarketTaxRates.cs
@@ -6,7 +6,7 @@ namespace Dalamud.Game.Network.Structures;
/// This class represents the "Result Dialog" packet. This is also used e.g. for reduction results, but we only care about tax rates.
/// We can do that by checking the "Category" field.
///
-public class MarketTaxRates
+public class MarketTaxRates : IMarketTaxRates
{
private MarketTaxRates()
{
@@ -100,3 +100,49 @@ public class MarketTaxRates
};
}
}
+
+///
+/// An interface that represents the tax rates received by the client when interacting with a retainer vocate.
+///
+public interface IMarketTaxRates
+{
+ ///
+ /// Gets the category of this ResultDialog packet.
+ ///
+ uint Category { get; }
+
+ ///
+ /// Gets the tax rate in Limsa Lominsa.
+ ///
+ uint LimsaLominsaTax { get; }
+
+ ///
+ /// Gets the tax rate in Gridania.
+ ///
+ uint GridaniaTax { get; }
+
+ ///
+ /// Gets the tax rate in Ul'dah.
+ ///
+ uint UldahTax { get; }
+
+ ///
+ /// Gets the tax rate in Ishgard.
+ ///
+ uint IshgardTax { get; }
+
+ ///
+ /// Gets the tax rate in Kugane.
+ ///
+ uint KuganeTax { get; }
+
+ ///
+ /// Gets the tax rate in the Crystarium.
+ ///
+ uint CrystariumTax { get; }
+
+ ///
+ /// Gets the tax rate in the Crystarium.
+ ///
+ uint SharlayanTax { get; }
+}
diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
index 951d3d91c..8ac49aef4 100644
--- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
@@ -43,6 +43,7 @@ internal class DataWindow : Window, IDisposable
new IconBrowserWidget(),
new ImGuiWidget(),
new KeyStateWidget(),
+ new MarketBoardWidget(),
new NetworkMonitorWidget(),
new ObjectTableWidget(),
new PartyListWidget(),
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/MarketBoardWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/MarketBoardWidget.cs
new file mode 100644
index 000000000..73d7c7e84
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/MarketBoardWidget.cs
@@ -0,0 +1,302 @@
+using System.Collections.Concurrent;
+
+using Dalamud.Interface.Utility;
+using Dalamud.Interface.Utility.Raii;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
+
+using System.Globalization;
+
+using Game.MarketBoard;
+using Game.Network.Structures;
+
+///
+/// Widget to display market board events.
+///
+internal class MarketBoardWidget : IDataWindowWidget
+{
+ private readonly ConcurrentQueue<(IMarketBoardHistory MarketBoardHistory, IMarketBoardHistoryListing Listing)> marketBoardHistoryQueue = new();
+ private readonly ConcurrentQueue<(IMarketBoardCurrentOfferings MarketBoardCurrentOfferings, IMarketBoardItemListing Listing)> marketBoardCurrentOfferingsQueue = new();
+ private readonly ConcurrentQueue marketBoardPurchasesQueue = new();
+ private readonly ConcurrentQueue marketBoardPurchaseRequestsQueue = new();
+ private readonly ConcurrentQueue marketTaxRatesQueue = new();
+
+ private bool trackMarketBoard;
+ private int trackedEvents;
+
+ /// Finalizes an instance of the class.
+ ~MarketBoardWidget()
+ {
+ if (this.trackMarketBoard)
+ {
+ this.trackMarketBoard = false;
+ var marketBoard = Service.GetNullable();
+ if (marketBoard != null)
+ {
+ marketBoard.HistoryReceived -= this.MarketBoardHistoryReceived;
+ marketBoard.OfferingsReceived -= this.MarketBoardOfferingsReceived;
+ marketBoard.ItemPurchased -= this.MarketBoardItemPurchased;
+ marketBoard.PurchaseRequested -= this.MarketBoardPurchaseRequested;
+ marketBoard.TaxRatesReceived -= this.TaxRatesReceived;
+ }
+ }
+ }
+
+ ///
+ public string[]? CommandShortcuts { get; init; } = { "marketboard" };
+
+ ///
+ public string DisplayName { get; init; } = "Market Board";
+
+ ///
+ public bool Ready { get; set; }
+
+ ///
+ public void Load()
+ {
+ this.trackMarketBoard = false;
+ this.trackedEvents = 0;
+ this.marketBoardHistoryQueue.Clear();
+ this.marketBoardPurchaseRequestsQueue.Clear();
+ this.marketBoardPurchasesQueue.Clear();
+ this.marketTaxRatesQueue.Clear();
+ this.marketBoardCurrentOfferingsQueue.Clear();
+ this.Ready = true;
+ }
+
+ ///
+ public void Draw()
+ {
+ var marketBoard = Service.Get();
+ if (ImGui.Checkbox("Track MarketBoard Events", ref this.trackMarketBoard))
+ {
+ if (this.trackMarketBoard)
+ {
+ marketBoard.HistoryReceived += this.MarketBoardHistoryReceived;
+ marketBoard.OfferingsReceived += this.MarketBoardOfferingsReceived;
+ marketBoard.ItemPurchased += this.MarketBoardItemPurchased;
+ marketBoard.PurchaseRequested += this.MarketBoardPurchaseRequested;
+ marketBoard.TaxRatesReceived += this.TaxRatesReceived;
+ }
+ else
+ {
+ marketBoard.HistoryReceived -= this.MarketBoardHistoryReceived;
+ marketBoard.OfferingsReceived -= this.MarketBoardOfferingsReceived;
+ marketBoard.ItemPurchased -= this.MarketBoardItemPurchased;
+ marketBoard.PurchaseRequested -= this.MarketBoardPurchaseRequested;
+ marketBoard.TaxRatesReceived -= this.TaxRatesReceived;
+ }
+ }
+
+ ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X / 2);
+ if (ImGui.DragInt("Stored Number of Events", ref this.trackedEvents, 0.1f, 1, 512))
+ {
+ this.trackedEvents = Math.Clamp(this.trackedEvents, 1, 512);
+ }
+
+ if (ImGui.Button("Clear Stored Events"))
+ {
+ this.marketBoardHistoryQueue.Clear();
+ }
+
+ using (var tabBar = ImRaii.TabBar("marketTabs"))
+ {
+ if (tabBar)
+ {
+ using (var tabItem = ImRaii.TabItem("History"))
+ {
+ if (tabItem)
+ {
+ ImGuiTable.DrawTable(string.Empty, this.marketBoardHistoryQueue, this.DrawMarketBoardHistory, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Item ID", "Quantity", "Is HQ?", "Sale Price", "Buyer Name", "Purchase Time");
+ }
+ }
+
+ using (var tabItem = ImRaii.TabItem("Offerings"))
+ {
+ if (tabItem)
+ {
+ ImGuiTable.DrawTable(string.Empty, this.marketBoardCurrentOfferingsQueue, this.DrawMarketBoardCurrentOfferings, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Item ID", "Quantity", "Is HQ?", "Price Per Unit", "Buyer Name", "Retainer Name", "Last Review Time");
+ }
+ }
+
+ using (var tabItem = ImRaii.TabItem("Purchases"))
+ {
+ if (tabItem)
+ {
+ ImGuiTable.DrawTable(string.Empty, this.marketBoardPurchasesQueue, this.DrawMarketBoardPurchases, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Item ID", "Quantity");
+ }
+ }
+
+ using (var tabItem = ImRaii.TabItem("Purchase Requests"))
+ {
+ if (tabItem)
+ {
+ ImGuiTable.DrawTable(string.Empty, this.marketBoardPurchaseRequestsQueue, this.DrawMarketBoardPurchaseRequests, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Item ID", "Quantity", "Price Per Unit", "Listing ID", "Retainer ID");
+ }
+ }
+
+ using (var tabItem = ImRaii.TabItem("Taxes"))
+ {
+ if (tabItem)
+ {
+ ImGuiTable.DrawTable(string.Empty, this.marketTaxRatesQueue, this.DrawMarketTaxRates, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Uldah", "Limsa Lominsa", "Gridania", "Ishgard", "Kugane", "Crystarium", "Sharlayan");
+ }
+ }
+ }
+ }
+ }
+
+ private void TaxRatesReceived(IMarketTaxRates marketTaxRates)
+ {
+ this.marketTaxRatesQueue.Enqueue(marketTaxRates);
+
+ while (this.marketTaxRatesQueue.Count > this.trackedEvents)
+ {
+ this.marketTaxRatesQueue.TryDequeue(out _);
+ }
+ }
+
+ private void MarketBoardPurchaseRequested(IMarketBoardPurchaseHandler marketBoardPurchaseHandler)
+ {
+ this.marketBoardPurchaseRequestsQueue.Enqueue(marketBoardPurchaseHandler);
+
+ while (this.marketBoardPurchaseRequestsQueue.Count > this.trackedEvents)
+ {
+ this.marketBoardPurchaseRequestsQueue.TryDequeue(out _);
+ }
+ }
+
+ private void MarketBoardItemPurchased(IMarketBoardPurchase marketBoardPurchase)
+ {
+ this.marketBoardPurchasesQueue.Enqueue(marketBoardPurchase);
+
+ while (this.marketBoardPurchasesQueue.Count > this.trackedEvents)
+ {
+ this.marketBoardPurchasesQueue.TryDequeue(out _);
+ }
+ }
+
+ private void MarketBoardOfferingsReceived(IMarketBoardCurrentOfferings marketBoardCurrentOfferings)
+ {
+ foreach (var listing in marketBoardCurrentOfferings.ItemListings)
+ {
+ this.marketBoardCurrentOfferingsQueue.Enqueue((marketBoardCurrentOfferings, listing));
+ }
+
+ while (this.marketBoardCurrentOfferingsQueue.Count > this.trackedEvents)
+ {
+ this.marketBoardCurrentOfferingsQueue.TryDequeue(out _);
+ }
+ }
+
+ private void MarketBoardHistoryReceived(IMarketBoardHistory marketBoardHistory)
+ {
+ foreach (var listing in marketBoardHistory.HistoryListings)
+ {
+ this.marketBoardHistoryQueue.Enqueue((marketBoardHistory, listing));
+ }
+
+ while (this.marketBoardHistoryQueue.Count > this.trackedEvents)
+ {
+ this.marketBoardHistoryQueue.TryDequeue(out _);
+ }
+ }
+
+ private void DrawMarketBoardHistory((IMarketBoardHistory History, IMarketBoardHistoryListing Listing) data)
+ {
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.History.ItemId.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.Listing.Quantity.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.Listing.IsHq.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.Listing.SalePrice.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.Listing.BuyerName);
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.Listing.PurchaseTime.ToString(CultureInfo.InvariantCulture));
+ }
+
+ private void DrawMarketBoardCurrentOfferings((IMarketBoardCurrentOfferings MarketBoardCurrentOfferings, IMarketBoardItemListing Listing) data)
+ {
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.Listing.ItemId.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.Listing.ItemQuantity.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.Listing.IsHq.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.Listing.PricePerUnit.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.Listing.PlayerName);
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.Listing.RetainerName);
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.Listing.LastReviewTime.ToString(CultureInfo.InvariantCulture));
+ }
+
+ private void DrawMarketBoardPurchases(IMarketBoardPurchase data)
+ {
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.CatalogId.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.ItemQuantity.ToString());
+ }
+
+ private void DrawMarketBoardPurchaseRequests(IMarketBoardPurchaseHandler data)
+ {
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.CatalogId.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.ItemQuantity.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.PricePerUnit.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.ListingId.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.RetainerId.ToString());
+ }
+
+ private void DrawMarketTaxRates(IMarketTaxRates data)
+ {
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.UldahTax.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.LimsaLominsaTax.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.GridaniaTax.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.IshgardTax.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.KuganeTax.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.CrystariumTax.ToString());
+
+ ImGui.TableNextColumn();
+ ImGui.TextUnformatted(data.SharlayanTax.ToString());
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/MarketBoardAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/MarketBoardAgingStep.cs
new file mode 100644
index 000000000..97768e3c8
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/MarketBoardAgingStep.cs
@@ -0,0 +1,259 @@
+namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps;
+
+using System.Globalization;
+using System.Linq;
+
+using Game.MarketBoard;
+using Game.Network.Structures;
+
+using ImGuiNET;
+
+///
+/// Tests the various market board events
+///
+internal class MarketBoardAgingStep : IAgingStep
+{
+ private SubStep currentSubStep;
+ private bool eventsSubscribed;
+
+ private IMarketBoardHistoryListing? historyListing;
+ private IMarketBoardItemListing? itemListing;
+ private IMarketTaxRates? marketTaxRate;
+ private IMarketBoardPurchaseHandler? marketBoardPurchaseRequest;
+ private IMarketBoardPurchase? marketBoardPurchase;
+
+ private enum SubStep
+ {
+ History,
+ Offerings,
+ PurchaseRequests,
+ Purchases,
+ Taxes,
+ Done,
+ }
+
+ ///
+ public string Name => "Test MarketBoard";
+
+ ///
+ public SelfTestStepResult RunStep()
+ {
+ if (!this.eventsSubscribed)
+ {
+ this.SubscribeToEvents();
+ }
+
+ ImGui.Text($"Testing: {this.currentSubStep.ToString()}");
+
+ switch (this.currentSubStep)
+ {
+ case SubStep.History:
+
+ if (this.historyListing == null)
+ {
+ ImGui.Text("Goto a Market Board. Open any item that has historical sale listings.");
+ }
+ else
+ {
+ ImGui.Text("Does one of the historical sales match this information?");
+ ImGui.Separator();
+ ImGui.Text($"Quantity: {this.historyListing.Quantity.ToString()}");
+ ImGui.Text($"Buyer: {this.historyListing.BuyerName}");
+ ImGui.Text($"Sale Price: {this.historyListing.SalePrice.ToString()}");
+ ImGui.Text($"Purchase Time: {this.historyListing.PurchaseTime.ToString(CultureInfo.InvariantCulture)}");
+ ImGui.Separator();
+ if (ImGui.Button("Looks Correct / Skip"))
+ {
+ this.currentSubStep++;
+ }
+
+ ImGui.SameLine();
+ if (ImGui.Button("No"))
+ {
+ return SelfTestStepResult.Fail;
+ }
+ }
+
+ break;
+ case SubStep.Offerings:
+
+ if (this.itemListing == null)
+ {
+ ImGui.Text("Goto a Market Board. Open any item that has sale listings.");
+ }
+ else
+ {
+ ImGui.Text("Does one of the sales match this information?");
+ ImGui.Separator();
+ ImGui.Text($"Quantity: {this.itemListing.ItemQuantity.ToString()}");
+ ImGui.Text($"Price Per Unit: {this.itemListing.PricePerUnit}");
+ ImGui.Text($"Retainer Name: {this.itemListing.RetainerName}");
+ ImGui.Text($"Is HQ?: {(this.itemListing.IsHq ? "Yes" : "No")}");
+ ImGui.Separator();
+ if (ImGui.Button("Looks Correct / Skip"))
+ {
+ this.currentSubStep++;
+ }
+
+ ImGui.SameLine();
+ if (ImGui.Button("No"))
+ {
+ return SelfTestStepResult.Fail;
+ }
+ }
+
+ break;
+ case SubStep.PurchaseRequests:
+ if (this.marketBoardPurchaseRequest == null)
+ {
+ ImGui.Text("Goto a Market Board. Purchase any item, the cheapest you can find.");
+ }
+ else
+ {
+ ImGui.Text("Does this information match the purchase you made? This is testing the request to the server.");
+ ImGui.Separator();
+ ImGui.Text($"Quantity: {this.marketBoardPurchaseRequest.ItemQuantity.ToString()}");
+ ImGui.Text($"Item ID: {this.marketBoardPurchaseRequest.CatalogId}");
+ ImGui.Text($"Price Per Unit: {this.marketBoardPurchaseRequest.PricePerUnit}");
+ ImGui.Separator();
+ if (ImGui.Button("Looks Correct / Skip"))
+ {
+ this.currentSubStep++;
+ }
+
+ ImGui.SameLine();
+ if (ImGui.Button("No"))
+ {
+ return SelfTestStepResult.Fail;
+ }
+ }
+
+ break;
+ case SubStep.Purchases:
+ if (this.marketBoardPurchase == null)
+ {
+ ImGui.Text("Goto a Market Board. Purchase any item, the cheapest you can find.");
+ }
+ else
+ {
+ ImGui.Text("Does this information match the purchase you made? This is testing the response from the server.");
+ ImGui.Separator();
+ ImGui.Text($"Quantity: {this.marketBoardPurchase.ItemQuantity.ToString()}");
+ ImGui.Text($"Item ID: {this.marketBoardPurchase.CatalogId}");
+ ImGui.Separator();
+ if (ImGui.Button("Looks Correct / Skip"))
+ {
+ this.currentSubStep++;
+ }
+
+ ImGui.SameLine();
+ if (ImGui.Button("No"))
+ {
+ return SelfTestStepResult.Fail;
+ }
+ }
+
+ break;
+ case SubStep.Taxes:
+ if (this.marketTaxRate == null)
+ {
+ ImGui.Text("Goto a Retainer Vocate and talk to then. Click the 'View market tax rates' menu item.");
+ }
+ else
+ {
+ ImGui.Text("Does this market tax rate information look correct?");
+ ImGui.Separator();
+ ImGui.Text($"Uldah: {this.marketTaxRate.UldahTax.ToString()}");
+ ImGui.Text($"Gridania: {this.marketTaxRate.GridaniaTax.ToString()}");
+ ImGui.Text($"Limsa Lominsa: {this.marketTaxRate.LimsaLominsaTax.ToString()}");
+ ImGui.Text($"Ishgard: {this.marketTaxRate.IshgardTax.ToString()}");
+ ImGui.Text($"Kugane: {this.marketTaxRate.KuganeTax.ToString()}");
+ ImGui.Text($"Crystarium: {this.marketTaxRate.CrystariumTax.ToString()}");
+ ImGui.Text($"Sharlayan: {this.marketTaxRate.SharlayanTax.ToString()}");
+ ImGui.Separator();
+ if (ImGui.Button("Looks Correct / Skip"))
+ {
+ this.currentSubStep++;
+ }
+
+ ImGui.SameLine();
+ if (ImGui.Button("No"))
+ {
+ return SelfTestStepResult.Fail;
+ }
+ }
+ break;
+ case SubStep.Done:
+ return SelfTestStepResult.Pass;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ return SelfTestStepResult.Waiting;
+ }
+
+ ///
+ public void CleanUp()
+ {
+ this.currentSubStep = SubStep.History;
+ this.historyListing = null;
+ this.marketTaxRate = null;
+ this.marketBoardPurchase = null;
+ this.marketBoardPurchaseRequest = null;
+ this.itemListing = null;
+ this.UnsubscribeFromEvents();
+ }
+
+ private void SubscribeToEvents()
+ {
+ var marketBoard = Service.Get();
+ marketBoard.HistoryReceived += this.OnHistoryReceived;
+ marketBoard.OfferingsReceived += this.OnOfferingsReceived;
+ marketBoard.ItemPurchased += this.OnItemPurchased;
+ marketBoard.PurchaseRequested += this.OnPurchaseRequested;
+ marketBoard.TaxRatesReceived += this.OnTaxRatesReceived;
+ this.eventsSubscribed = true;
+ }
+
+ private void UnsubscribeFromEvents()
+ {
+ var marketBoard = Service.Get();
+ marketBoard.HistoryReceived -= this.OnHistoryReceived;
+ marketBoard.OfferingsReceived -= this.OnOfferingsReceived;
+ marketBoard.ItemPurchased -= this.OnItemPurchased;
+ marketBoard.PurchaseRequested -= this.OnPurchaseRequested;
+ marketBoard.TaxRatesReceived -= this.OnTaxRatesReceived;
+ this.eventsSubscribed = false;
+ }
+
+ private void OnTaxRatesReceived(IMarketTaxRates marketTaxRates)
+ {
+ this.marketTaxRate = marketTaxRates;
+ }
+
+ private void OnPurchaseRequested(IMarketBoardPurchaseHandler marketBoardPurchaseHandler)
+ {
+ this.marketBoardPurchaseRequest = marketBoardPurchaseHandler;
+ }
+
+ private void OnItemPurchased(IMarketBoardPurchase purchase)
+ {
+ this.marketBoardPurchase = purchase;
+ }
+
+ private void OnOfferingsReceived(IMarketBoardCurrentOfferings marketBoardCurrentOfferings)
+ {
+ if (marketBoardCurrentOfferings.ItemListings.Count != 0)
+ {
+ this.itemListing = marketBoardCurrentOfferings.ItemListings.First();
+ }
+ }
+
+ private void OnHistoryReceived(IMarketBoardHistory marketBoardHistory)
+ {
+ if (marketBoardHistory.HistoryListings.Count != 0)
+ {
+ this.historyListing = marketBoardHistory.HistoryListings.First();
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs
index 8f4a59843..e3172d5c2 100644
--- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs
@@ -44,6 +44,7 @@ internal class SelfTestWindow : Window
new HandledExceptionAgingStep(),
new DutyStateAgingStep(),
new GameConfigAgingStep(),
+ new MarketBoardAgingStep(),
new LogoutEventAgingStep(),
};
diff --git a/Dalamud/Plugin/Services/IMarketBoard.cs b/Dalamud/Plugin/Services/IMarketBoard.cs
new file mode 100644
index 000000000..9126dfcc7
--- /dev/null
+++ b/Dalamud/Plugin/Services/IMarketBoard.cs
@@ -0,0 +1,64 @@
+namespace Dalamud.Plugin.Services;
+
+using Game.Network.Structures;
+
+///
+/// Provides access to market board related events as the client receives/sends them.
+///
+public interface IMarketBoard
+{
+ ///
+ /// A delegate type used with the event.
+ ///
+ /// The historical listings for a particular item on the market board.
+ public delegate void HistoryReceivedDelegate(IMarketBoardHistory history);
+
+ ///
+ /// A delegate type used with the event.
+ ///
+ /// The item that has been purchased.
+ public delegate void ItemPurchasedDelegate(IMarketBoardPurchase purchase);
+
+ ///
+ /// A delegate type used with the event.
+ ///
+ /// The current offerings for a particular item on the market board.
+ public delegate void OfferingsReceivedDelegate(IMarketBoardCurrentOfferings currentOfferings);
+
+ ///
+ /// A delegate type used with the event.
+ ///
+ /// The details about the item being purchased.
+ public delegate void PurchaseRequestedDelegate(IMarketBoardPurchaseHandler purchaseRequested);
+
+ ///
+ /// A delegate type used with the event.
+ ///
+ /// The new tax rates.
+ public delegate void TaxRatesReceivedDelegate(IMarketTaxRates taxRates);
+
+ ///
+ /// Event that fires when historical sale listings are received for a specific item on the market board.
+ ///
+ public event HistoryReceivedDelegate HistoryReceived;
+
+ ///
+ /// Event that fires when a item is purchased on the market board.
+ ///
+ public event ItemPurchasedDelegate ItemPurchased;
+
+ ///
+ /// Event that fires when current offerings are received for a specific item on the market board.
+ ///
+ public event OfferingsReceivedDelegate OfferingsReceived;
+
+ ///
+ /// Event that fires when a player requests to purchase an item from the market board.
+ ///
+ public event PurchaseRequestedDelegate PurchaseRequested;
+
+ ///
+ /// Event that fires when the client receives new tax rates. These events only occur when accessing a retainer vocate and requesting the tax rates.
+ ///
+ public event TaxRatesReceivedDelegate TaxRatesReceived;
+}