Add MarketBoard service and associated interfaces, test and data widget (#1822)

* Add MarketBoard service and associated interfaces, test and data widget
* Dispose of events properly
* Make listings readonly lists + provide internal list for internal use
* Rename CatalogId to ItemId on interfaces, have kept CatalogId internally as it's technically correct
* Removed RetainerOwnerId from the public interface
* Removed NextCatalogId from the public interface
* Updated test text
* Null events in scoped service disposal
This commit is contained in:
Blair 2024-06-17 03:40:48 +10:00 committed by GitHub
parent a35ae5fdf3
commit e160746d42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1131 additions and 27 deletions

View file

@ -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;
/// <summary>
/// This class provides access to market board events
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
internal class MarketBoard : IInternalDisposableService, IMarketBoard
{
[ServiceManager.ServiceDependency]
private readonly NetworkHandlers networkHandlers = Service<NetworkHandlers>.Get();
/// <summary>
/// Initializes a new instance of the <see cref="MarketBoard"/> class.
/// </summary>
[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);
}
/// <inheritdoc/>
public event IMarketBoard.HistoryReceivedDelegate? HistoryReceived;
/// <inheritdoc/>
public event IMarketBoard.ItemPurchasedDelegate? ItemPurchased;
/// <inheritdoc/>
public event IMarketBoard.OfferingsReceivedDelegate? OfferingsReceived;
/// <inheritdoc/>
public event IMarketBoard.PurchaseRequestedDelegate? PurchaseRequested;
/// <inheritdoc/>
public event IMarketBoard.TaxRatesReceivedDelegate? TaxRatesReceived;
/// <inheritdoc/>
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);
}
}
/// <summary>
/// Plugin scoped version of MarketBoard.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<IMarketBoard>]
#pragma warning restore SA1015
internal class MarketBoardPluginScoped : IInternalDisposableService, IMarketBoard
{
[ServiceManager.ServiceDependency]
private readonly MarketBoard marketBoardService = Service<MarketBoard>.Get();
/// <summary>
/// Initializes a new instance of the <see cref="MarketBoardPluginScoped"/> class.
/// </summary>
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;
}
/// <inheritdoc/>
public event IMarketBoard.HistoryReceivedDelegate? HistoryReceived;
/// <inheritdoc/>
public event IMarketBoard.ItemPurchasedDelegate? ItemPurchased;
/// <inheritdoc/>
public event IMarketBoard.OfferingsReceivedDelegate? OfferingsReceived;
/// <inheritdoc/>
public event IMarketBoard.PurchaseRequestedDelegate? PurchaseRequested;
/// <inheritdoc/>
public event IMarketBoard.TaxRatesReceivedDelegate? TaxRatesReceived;
/// <inheritdoc/>
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);
}
}

View file

@ -209,6 +209,18 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private event Action<nint>? MarketBoardPurchaseRequestSent;
public IObservable<MarketBoardPurchase> MbPurchaseObservable => this.mbPurchaseObservable;
public IObservable<MarketBoardHistory> MbHistoryObservable => this.mbHistoryObservable;
public IObservable<MarketTaxRates> MbTaxesObservable => this.mbTaxesObservable;
public IObservable<MarketBoardItemRequest> MbItemRequestObservable => this.mbItemRequestObservable;
public IObservable<MarketBoardCurrentOfferings> MbOfferingsObservable => this.mbOfferingsObservable;
public IObservable<MarketBoardPurchaseHandler> MbPurchaseSentObservable => this.mbPurchaseSentObservable;
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
@ -301,7 +313,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private IObservable<List<MarketBoardCurrentOfferings.MarketBoardItemListing>> OnMarketBoardListingsBatch(
IObservable<MarketBoardItemRequest> 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<MarketBoardCurrentOfferings> 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<MarketBoardCurrentOfferings.MarketBoardItemListing>(),
(agg, next) =>
{
agg.AddRange(next.ItemListings);
agg.AddRange(next.InternalItemListings);
return agg;
}));
}
@ -351,14 +363,14 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private IObservable<List<MarketBoardHistory.MarketBoardHistoryListing>> OnMarketBoardSalesBatch(
IObservable<MarketBoardItemRequest> 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<MarketBoardHistory> UntilBatchEnd(MarketBoardItemRequest request)
@ -379,7 +391,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
new List<MarketBoardHistory.MarketBoardHistoryListing>(),
(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)

View file

@ -7,7 +7,7 @@ namespace Dalamud.Game.Network.Structures;
/// <summary>
/// This class represents the current market board offerings from a game network packet.
/// </summary>
public class MarketBoardCurrentOfferings
public class MarketBoardCurrentOfferings : IMarketBoardCurrentOfferings
{
private MarketBoardCurrentOfferings()
{
@ -16,7 +16,10 @@ public class MarketBoardCurrentOfferings
/// <summary>
/// Gets the list of individual item listings.
/// </summary>
public List<MarketBoardItemListing> ItemListings { get; } = new();
IReadOnlyList<IMarketBoardItemListing> IMarketBoardCurrentOfferings.ItemListings => this.InternalItemListings;
internal List<MarketBoardItemListing> InternalItemListings { get; set; } = new List<MarketBoardItemListing>();
/// <summary>
/// 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<MarketBoardItemListing>();
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<IItemMateria>();
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
/// <summary>
/// This class represents the current market board offering of a single item from the <see cref="MarketBoardCurrentOfferings"/> network packet.
/// </summary>
public class MarketBoardItemListing
public class MarketBoardItemListing : IMarketBoardItemListing
{
/// <summary>
/// Initializes a new instance of the <see cref="MarketBoardItemListing"/> class.
@ -119,6 +129,9 @@ public class MarketBoardCurrentOfferings
/// </summary>
public ulong ArtisanId { get; internal set; }
/// <inheritdoc/>
public uint ItemId => this.CatalogId;
/// <summary>
/// Gets the catalog ID.
/// </summary>
@ -147,7 +160,7 @@ public class MarketBoardCurrentOfferings
/// <summary>
/// Gets the list of materia attached to this item.
/// </summary>
public List<ItemMateria> Materia { get; } = new();
public IReadOnlyList<IItemMateria> Materia { get; internal set; } = new List<IItemMateria>();
/// <summary>
/// Gets the amount of attached materia.
@ -202,7 +215,7 @@ public class MarketBoardCurrentOfferings
/// <summary>
/// This represents the materia slotted to an <see cref="MarketBoardItemListing"/>.
/// </summary>
public class ItemMateria
public class ItemMateria : IItemMateria
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemMateria"/> class.
@ -223,3 +236,132 @@ public class MarketBoardCurrentOfferings
}
}
}
/// <summary>
/// An interface that represents the current market board offerings.
/// </summary>
public interface IMarketBoardCurrentOfferings
{
/// <summary>
/// Gets the list of individual item listings.
/// </summary>
IReadOnlyList<IMarketBoardItemListing> ItemListings { get; }
/// <summary>
/// Gets the listing end index.
/// </summary>
int ListingIndexEnd { get; }
/// <summary>
/// Gets the listing start index.
/// </summary>
int ListingIndexStart { get; }
/// <summary>
/// Gets the request ID.
/// </summary>
int RequestId { get; }
}
/// <summary>
/// An interface that represents the current market board offering of a single item from the <see cref="IMarketBoardCurrentOfferings"/>.
/// </summary>
public interface IMarketBoardItemListing
{
/// <summary>
/// Gets the artisan ID.
/// </summary>
ulong ArtisanId { get; }
/// <summary>
/// Gets the item ID.
/// </summary>
uint ItemId { get; }
/// <summary>
/// Gets a value indicating whether the item is HQ.
/// </summary>
bool IsHq { get; }
/// <summary>
/// Gets the item quantity.
/// </summary>
uint ItemQuantity { get; }
/// <summary>
/// Gets the time this offering was last reviewed.
/// </summary>
DateTime LastReviewTime { get; }
/// <summary>
/// Gets the listing ID.
/// </summary>
ulong ListingId { get; }
/// <summary>
/// Gets the list of materia attached to this item.
/// </summary>
IReadOnlyList<IItemMateria> Materia { get; }
/// <summary>
/// Gets the amount of attached materia.
/// </summary>
int MateriaCount { get; }
/// <summary>
/// Gets a value indicating whether this item is on a mannequin.
/// </summary>
bool OnMannequin { get; }
/// <summary>
/// Gets the player name.
/// </summary>
string PlayerName { get; }
/// <summary>
/// Gets the price per unit.
/// </summary>
uint PricePerUnit { get; }
/// <summary>
/// Gets the city ID of the retainer selling the item.
/// </summary>
int RetainerCityId { get; }
/// <summary>
/// Gets the ID of the retainer selling the item.
/// </summary>
ulong RetainerId { get; }
/// <summary>
/// Gets the name of the retainer.
/// </summary>
string RetainerName { get; }
/// <summary>
/// Gets the stain or applied dye of the item.
/// </summary>
int StainId { get; }
/// <summary>
/// Gets the total tax.
/// </summary>
uint TotalTax { get; }
}
/// <summary>
/// An interface that represents the materia slotted to an <see cref="IMarketBoardItemListing"/>.
/// </summary>
public interface IItemMateria
{
/// <summary>
/// Gets the materia index.
/// </summary>
int Index { get; }
/// <summary>
/// Gets the materia ID.
/// </summary>
int MateriaId { get; }
}

View file

@ -7,7 +7,7 @@ namespace Dalamud.Game.Network.Structures;
/// <summary>
/// This class represents the market board history from a game network packet.
/// </summary>
public class MarketBoardHistory
public class MarketBoardHistory : IMarketBoardHistory
{
/// <summary>
/// Initializes a new instance of the <see cref="MarketBoardHistory"/> class.
@ -26,10 +26,17 @@ public class MarketBoardHistory
/// </summary>
public uint CatalogId2 { get; private set; }
public uint ItemId => this.CatalogId;
/// <summary>
/// Gets the list of individual item history listings.
/// Gets the list of individual item listings.
/// </summary>
public List<MarketBoardHistoryListing> HistoryListings { get; } = new();
IReadOnlyList<IMarketBoardHistoryListing> IMarketBoardHistory.HistoryListings => this.InternalHistoryListings;
/// <summary>
/// Gets or sets a list of individual item listings.
/// </summary>
internal List<MarketBoardHistoryListing> InternalHistoryListings { get; set; } = new List<MarketBoardHistoryListing>();
/// <summary>
/// Read a <see cref="MarketBoardHistory"/> object from memory.
@ -53,6 +60,7 @@ public class MarketBoardHistory
return output;
}
var historyListings = new List<MarketBoardHistoryListing>();
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;
}
/// <summary>
/// This class represents the market board history of a single item from the <see cref="MarketBoardHistory"/> network packet.
/// </summary>
public class MarketBoardHistoryListing
public class MarketBoardHistoryListing : IMarketBoardHistoryListing
{
/// <summary>
/// Initializes a new instance of the <see cref="MarketBoardHistoryListing"/> class.
@ -126,3 +136,55 @@ public class MarketBoardHistory
public uint SalePrice { get; internal set; }
}
}
/// <summary>
/// An interface that represents the market board history from the game.
/// </summary>
public interface IMarketBoardHistory
{
/// <summary>
/// Gets the item ID.
/// </summary>
uint ItemId { get; }
/// <summary>
/// Gets the list of individual item history listings.
/// </summary>
IReadOnlyList<IMarketBoardHistoryListing> HistoryListings { get; }
}
/// <summary>
/// An interface that represents the market board history of a single item from <see cref="IMarketBoardHistory"/>.
/// </summary>
public interface IMarketBoardHistoryListing
{
/// <summary>
/// Gets the buyer's name.
/// </summary>
string BuyerName { get; }
/// <summary>
/// Gets a value indicating whether the item is HQ.
/// </summary>
bool IsHq { get; }
/// <summary>
/// Gets a value indicating whether the item is on a mannequin.
/// </summary>
bool OnMannequin { get; }
/// <summary>
/// Gets the time of purchase.
/// </summary>
DateTime PurchaseTime { get; }
/// <summary>
/// Gets the quantity.
/// </summary>
uint Quantity { get; }
/// <summary>
/// Gets the sale price.
/// </summary>
uint SalePrice { get; }
}

View file

@ -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.
/// </summary>
public class MarketBoardPurchase
public class MarketBoardPurchase : IMarketBoardPurchase
{
private MarketBoardPurchase()
{
@ -41,3 +41,20 @@ public class MarketBoardPurchase
return output;
}
}
/// <summary>
/// An interface that represents market board purchase information. This message is received from the
/// server when a purchase is made at a market board.
/// </summary>
public interface IMarketBoardPurchase
{
/// <summary>
/// Gets the item ID of the item that was purchased.
/// </summary>
uint CatalogId { get; }
/// <summary>
/// Gets the quantity of the item that was purchased.
/// </summary>
uint ItemQuantity { get; }
}

View file

@ -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.
/// </summary>
public class MarketBoardPurchaseHandler
public class MarketBoardPurchaseHandler : IMarketBoardPurchaseHandler
{
private MarketBoardPurchaseHandler()
{
@ -59,3 +59,35 @@ public class MarketBoardPurchaseHandler
return output;
}
}
/// <summary>
/// An interface that represents market board purchase information. This message is sent from the
/// client when a purchase is made at a market board.
/// </summary>
public interface IMarketBoardPurchaseHandler
{
/// <summary>
/// Gets the object ID of the retainer associated with the sale.
/// </summary>
ulong RetainerId { get; }
/// <summary>
/// Gets the object ID of the item listing.
/// </summary>
ulong ListingId { get; }
/// <summary>
/// Gets the item ID of the item that was purchased.
/// </summary>
uint CatalogId { get; }
/// <summary>
/// Gets the quantity of the item that was purchased.
/// </summary>
uint ItemQuantity { get; }
/// <summary>
/// Gets the unit price of the item.
/// </summary>
uint PricePerUnit { get; }
}

View file

@ -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.
/// </summary>
public class MarketTaxRates
public class MarketTaxRates : IMarketTaxRates
{
private MarketTaxRates()
{
@ -100,3 +100,49 @@ public class MarketTaxRates
};
}
}
/// <summary>
/// An interface that represents the tax rates received by the client when interacting with a retainer vocate.
/// </summary>
public interface IMarketTaxRates
{
/// <summary>
/// Gets the category of this ResultDialog packet.
/// </summary>
uint Category { get; }
/// <summary>
/// Gets the tax rate in Limsa Lominsa.
/// </summary>
uint LimsaLominsaTax { get; }
/// <summary>
/// Gets the tax rate in Gridania.
/// </summary>
uint GridaniaTax { get; }
/// <summary>
/// Gets the tax rate in Ul'dah.
/// </summary>
uint UldahTax { get; }
/// <summary>
/// Gets the tax rate in Ishgard.
/// </summary>
uint IshgardTax { get; }
/// <summary>
/// Gets the tax rate in Kugane.
/// </summary>
uint KuganeTax { get; }
/// <summary>
/// Gets the tax rate in the Crystarium.
/// </summary>
uint CrystariumTax { get; }
/// <summary>
/// Gets the tax rate in the Crystarium.
/// </summary>
uint SharlayanTax { get; }
}

View file

@ -43,6 +43,7 @@ internal class DataWindow : Window, IDisposable
new IconBrowserWidget(),
new ImGuiWidget(),
new KeyStateWidget(),
new MarketBoardWidget(),
new NetworkMonitorWidget(),
new ObjectTableWidget(),
new PartyListWidget(),

View file

@ -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;
/// <summary>
/// Widget to display market board events.
/// </summary>
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<IMarketBoardPurchase> marketBoardPurchasesQueue = new();
private readonly ConcurrentQueue<IMarketBoardPurchaseHandler> marketBoardPurchaseRequestsQueue = new();
private readonly ConcurrentQueue<IMarketTaxRates> marketTaxRatesQueue = new();
private bool trackMarketBoard;
private int trackedEvents;
/// <summary> Finalizes an instance of the <see cref="MarketBoardWidget"/> class. </summary>
~MarketBoardWidget()
{
if (this.trackMarketBoard)
{
this.trackMarketBoard = false;
var marketBoard = Service<MarketBoard>.GetNullable();
if (marketBoard != null)
{
marketBoard.HistoryReceived -= this.MarketBoardHistoryReceived;
marketBoard.OfferingsReceived -= this.MarketBoardOfferingsReceived;
marketBoard.ItemPurchased -= this.MarketBoardItemPurchased;
marketBoard.PurchaseRequested -= this.MarketBoardPurchaseRequested;
marketBoard.TaxRatesReceived -= this.TaxRatesReceived;
}
}
}
/// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = { "marketboard" };
/// <inheritdoc/>
public string DisplayName { get; init; } = "Market Board";
/// <inheritdoc/>
public bool Ready { get; set; }
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public void Draw()
{
var marketBoard = Service<MarketBoard>.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());
}
}

View file

@ -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;
/// <summary>
/// Tests the various market board events
/// </summary>
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,
}
/// <inheritdoc/>
public string Name => "Test MarketBoard";
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
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<MarketBoard>.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<MarketBoard>.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();
}
}
}

View file

@ -44,6 +44,7 @@ internal class SelfTestWindow : Window
new HandledExceptionAgingStep(),
new DutyStateAgingStep(),
new GameConfigAgingStep(),
new MarketBoardAgingStep(),
new LogoutEventAgingStep(),
};

View file

@ -0,0 +1,64 @@
namespace Dalamud.Plugin.Services;
using Game.Network.Structures;
/// <summary>
/// Provides access to market board related events as the client receives/sends them.
/// </summary>
public interface IMarketBoard
{
/// <summary>
/// A delegate type used with the <see cref="HistoryReceived"/> event.
/// </summary>
/// <param name="history">The historical listings for a particular item on the market board.</param>
public delegate void HistoryReceivedDelegate(IMarketBoardHistory history);
/// <summary>
/// A delegate type used with the <see cref="ItemPurchased"/> event.
/// </summary>
/// <param name="purchase">The item that has been purchased.</param>
public delegate void ItemPurchasedDelegate(IMarketBoardPurchase purchase);
/// <summary>
/// A delegate type used with the <see cref="OfferingsReceived"/> event.
/// </summary>
/// <param name="currentOfferings">The current offerings for a particular item on the market board.</param>
public delegate void OfferingsReceivedDelegate(IMarketBoardCurrentOfferings currentOfferings);
/// <summary>
/// A delegate type used with the <see cref="PurchaseRequested"/> event.
/// </summary>
/// <param name="purchaseRequested">The details about the item being purchased.</param>
public delegate void PurchaseRequestedDelegate(IMarketBoardPurchaseHandler purchaseRequested);
/// <summary>
/// A delegate type used with the <see cref="PurchaseRequested"/> event.
/// </summary>
/// <param name="taxRates">The new tax rates.</param>
public delegate void TaxRatesReceivedDelegate(IMarketTaxRates taxRates);
/// <summary>
/// Event that fires when historical sale listings are received for a specific item on the market board.
/// </summary>
public event HistoryReceivedDelegate HistoryReceived;
/// <summary>
/// Event that fires when a item is purchased on the market board.
/// </summary>
public event ItemPurchasedDelegate ItemPurchased;
/// <summary>
/// Event that fires when current offerings are received for a specific item on the market board.
/// </summary>
public event OfferingsReceivedDelegate OfferingsReceived;
/// <summary>
/// Event that fires when a player requests to purchase an item from the market board.
/// </summary>
public event PurchaseRequestedDelegate PurchaseRequested;
/// <summary>
/// Event that fires when the client receives new tax rates. These events only occur when accessing a retainer vocate and requesting the tax rates.
/// </summary>
public event TaxRatesReceivedDelegate TaxRatesReceived;
}