Skip to content

Commit

Permalink
Merge branch 'release/1.7.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
fmauNeko committed Jul 14, 2024
2 parents 9f64710 + f344ad1 commit 2d3f6af
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 144 deletions.
11 changes: 0 additions & 11 deletions MarketBoardPlugin/Dalamud.Plugin.Bootstrap.targets

This file was deleted.

9 changes: 0 additions & 9 deletions MarketBoardPlugin/GUI/MarketBoardConfigWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,6 @@ public override void Draw()

this.Checkbox("Hide Ko-Fi button", "Toggles whether the Ko-Fi button should be hidden", this.Plugin.Config.KofiHidden, (v) => this.Plugin.Config.KofiHidden = v);

var marketBufferSize = this.Plugin.Config.MarketBufferSize;
ImGui.Text("Number of buffered item : ");
ImGui.InputInt("###marketBufferSize", ref marketBufferSize);
if (this.Plugin.Config.MarketBufferSize != marketBufferSize)
{
this.Plugin.Config.MarketBufferSize = marketBufferSize;
this.Plugin.PluginInterface.SavePluginConfig(this.Plugin.Config);
}

var itemRefreshTimeout = this.Plugin.Config.ItemRefreshTimeout;
ImGui.Text("Item buffer Timeout (ms) :");
ImGui.InputInt("###refreshTimeout", ref itemRefreshTimeout);
Expand Down
103 changes: 34 additions & 69 deletions MarketBoardPlugin/GUI/MarketBoardWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ public class MarketBoardWindow : Window, IDisposable

private readonly IFontHandle titleFontHandle;

private readonly Dictionary<uint, MarketDataResponse> marketDataCache;

private readonly string[] categoryLabels = new[] { "All", "Weapons", "Equipments", "Others", "Furniture" };

private Dictionary<ItemSearchCategory, List<Item>> sortedCategoriesAndItems;

private bool isDisposed;
Expand All @@ -77,7 +81,6 @@ public class MarketBoardWindow : Window, IDisposable
private int lastlvlmax = 100;
private int itemCategory;
private int lastItemCategory;
private string[] categoryLabels = new[] { "All", "Weapons", "Equipments", "Others", "Furniture" };
private Item selectedItem;

private ClassJob selectedClassJob;
Expand All @@ -90,12 +93,8 @@ public class MarketBoardWindow : Window, IDisposable

private int selectedWorld = -1;

private int previousSelectedWorld = -1;

private MarketDataResponse? marketData;

private readonly List<MarketDataResponse?> marketBuffer;

private int selectedListing = -1;

private int selectedHistory = -1;
Expand Down Expand Up @@ -127,7 +126,7 @@ public MarketBoardWindow(MBPlugin plugin)
MaximumSize = new Vector2(float.MaxValue, float.MaxValue),
};

this.marketBuffer = new List<MarketDataResponse>();
this.marketDataCache = [];
this.items = plugin.DataManager.GetExcelSheet<Item>();
this.classJobs = plugin.DataManager.GetExcelSheet<ClassJob>()?
.Where(cj => cj.RowId != 0)
Expand Down Expand Up @@ -198,7 +197,7 @@ public void Dispose()
/// </summary>
public void ResetMarketData()
{
this.marketBuffer.Clear();
this.marketDataCache.Clear();
this.RefreshMarketData();
}

Expand Down Expand Up @@ -554,11 +553,10 @@ void SelectClassJob(ClassJob classJob)
var isSelected = this.selectedWorld == this.worldList.IndexOf(world);
if (ImGui.Selectable(world.Item2, isSelected))
{
this.previousSelectedWorld = this.selectedWorld;
this.selectedWorld = this.worldList.IndexOf(world);
this.plugin.Config.CrossDataCenter = this.selectedWorld == 0;
this.plugin.Config.CrossWorld = this.selectedWorld == 1;
this.RefreshMarketData();
this.ResetMarketData();
}

if (isSelected)
Expand Down Expand Up @@ -1107,92 +1105,59 @@ private void HandleHoveredItemChange(object sender, ulong itemId)

private void RefreshMarketData()
{
this.marketData = null;

if (this.currentRefreshTask?.Status != TaskStatus.RanToCompletion)
{
this.plugin.Log.Debug("Cancelling previous refresh task.");
this.currentRefreshCancellationTokenSource?.Cancel();
}

this.currentRefreshCancellationTokenSource?.Dispose();
this.currentRefreshCancellationTokenSource = new CancellationTokenSource();

this.currentRefreshTask = Task.Run(
async () =>
{
var cachedItem = this.marketBuffer.FirstOrDefault(data => data?.ItemId == this.selectedItem.RowId, null);
if (cachedItem != null)
var cachedItem = this.marketDataCache.GetValueOrDefault(this.selectedItem.RowId);
if (
cachedItem != default(MarketDataResponse)
&& DateTimeOffset.Now.ToUnixTimeMilliseconds() - cachedItem.FetchTimestamp < this.plugin.Config.ItemRefreshTimeout)
{
this.marketData = cachedItem;
return;
}
if (cachedItem == null || this.selectedWorld != this.previousSelectedWorld || DateTimeOffset.Now.ToUnixTimeMilliseconds() - cachedItem.FetchTimestamp > this.plugin.Config.ItemRefreshTimeout)
{
this.previousSelectedWorld = this.selectedWorld;
if (cachedItem == null)
{
if (this.marketData != null)
{
this.marketBuffer.Add(this.marketData);
}
if (this.marketBuffer.Count > this.plugin.Config.MarketBufferSize)
{
this.marketBuffer.RemoveAt(0);
}
this.marketData = null;
}
try
{
this.marketData = await this.plugin.UniversalisClient
.GetMarketData(
this.selectedItem.RowId,
this.worldList[this.selectedWorld].Item1,
50,
this.currentRefreshCancellationTokenSource.Token)
.ConfigureAwait(false);
}
catch (AggregateException ae)
{
this.plugin.Log.Warning(ae, $"Failed to fetch market data for item {this.selectedItem.RowId} from Universalis.");
foreach (var ex in ae.InnerExceptions)
{
this.plugin.Log.Warning(ex, "Inner exception");
}
this.marketDataCache.Remove(this.selectedItem.RowId);
this.marketData = null;
}
if (cachedItem != null)
{
this.marketBuffer.Remove(cachedItem);
this.marketBuffer.Add(this.marketData);
}
try
{
this.marketData = await this.plugin.UniversalisClient
.GetMarketData(
this.selectedItem.RowId,
this.worldList[this.selectedWorld].Item1,
50,
this.currentRefreshCancellationTokenSource.Token)
.ConfigureAwait(false);
}
},
this.currentRefreshCancellationTokenSource.Token);

this.currentRefreshTask.ContinueWith(
t =>
{
if (t.IsFaulted)
catch (AggregateException ae)
{
this.plugin.Log.Warning(t.Exception, "Failed to fetch market data.");
this.plugin.Log.Warning(ae, $"Failed to fetch market data for item {this.selectedItem.RowId} from Universalis.");
foreach (var ex in t.Exception.InnerExceptions)
foreach (var ex in ae.InnerExceptions)
{
this.plugin.Log.Warning(ex, "Inner exception");
}
this.marketData = null;
}
else if (t.IsCanceled)
if (this.marketData != null)
{
this.plugin.Log.Debug("Market data refresh task was cancelled.");
this.marketDataCache.Add(this.selectedItem.RowId, this.marketData);
}
this.currentRefreshCancellationTokenSource.Dispose();
},
TaskScheduler.Current);
this.currentRefreshCancellationTokenSource.Token);
}
}
}
96 changes: 74 additions & 22 deletions MarketBoardPlugin/Helpers/UniversalisClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,55 @@
namespace MarketBoardPlugin.Helpers
{
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

using MarketBoardPlugin.Models.Universalis;
using Polly;

/// <summary>
/// Universalis API Client.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="UniversalisClient"/> class.
/// </remarks>
/// <param name="plugin">The plugin instance.</param>
public class UniversalisClient(MBPlugin plugin)
public class UniversalisClient : IDisposable
{
private readonly MBPlugin plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
private readonly MBPlugin plugin;

private readonly HttpClient client;

private readonly ResiliencePipeline resiliencePipeline;

private bool disposedValue;

/// <summary>
/// Initializes a new instance of the <see cref="UniversalisClient"/> class.
/// </summary>
/// <param name="plugin">The <see cref="MBPlugin"/> instance.</param>
/// <exception cref="ArgumentNullException">One of the required arguments is null.</exception>
public UniversalisClient(MBPlugin plugin)
{
ArgumentNullException.ThrowIfNull(plugin);

this.plugin = plugin;

this.client = new HttpClient
{
BaseAddress = new Uri("https://universalis.app/api/"),
};

this.resiliencePipeline = new ResiliencePipelineBuilder()
.AddRetry(new()
{
BackoffType = DelayBackoffType.Exponential,
MaxRetryAttempts = 3,
ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>(),
UseJitter = true,
})
.Build();
}

/// <summary>
/// Retrieves market data for a specific item from the Universalis API.
Expand All @@ -33,34 +63,56 @@ public class UniversalisClient(MBPlugin plugin)
/// <param name="historyCount">The number of historical entries to retrieve.</param>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
/// <returns>A <see cref="MarketDataResponse"/> object containing the retrieved market data, or null if the operation fails.</returns>
public async Task<MarketDataResponse?> GetMarketData(uint itemId, string worldName, int historyCount, CancellationToken cancellationToken)
public async Task<MarketDataResponse> GetMarketData(uint itemId, string worldName, int historyCount, CancellationToken cancellationToken)
{
var uriBuilder = new UriBuilder($"https://universalis.app/api/{worldName}/{itemId}?entries={historyCount}");

cancellationToken.ThrowIfCancellationRequested();

using var client = new HttpClient();

MarketDataResponse? res;

try
{
res = await client
.GetFromJsonAsync<MarketDataResponse>(uriBuilder.Uri, cancellationToken)
using var content = await this.resiliencePipeline.ExecuteAsync(
async (ct) =>
await this.client.GetStreamAsync(new Uri($"{worldName}/{itemId}?entries={historyCount}", UriKind.Relative), ct).ConfigureAwait(false),
cancellationToken)
.ConfigureAwait(false);

cancellationToken.ThrowIfCancellationRequested();

var parsedRes = await JsonSerializer
.DeserializeAsync<MarketDataResponse>(content, cancellationToken: cancellationToken)
.ConfigureAwait(false) ?? throw new InvalidOperationException($"Failed to parse market data for item {itemId} on world {worldName}.");

parsedRes.FetchTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
return parsedRes;
}
catch (HttpRequestException ex)
{
this.plugin.Log.Warning(ex, $"Failed to fetch market data for item {itemId} on world {worldName}.");
return null;
throw;
}

if (res != null)
catch (JsonException ex)
{
res.FetchTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();
this.plugin.Log.Warning(ex, $"Failed to parse market data for item {itemId} on world {worldName}.");
throw;
}
}

return res;
/// <inheritdoc/>
public void Dispose()
{
this.Dispose(disposing: true);
GC.SuppressFinalize(this);
}

/// <inheritdoc/>
protected virtual void Dispose(bool disposing)
{
if (!this.disposedValue)
{
if (disposing)
{
this.client.Dispose();
}

this.disposedValue = true;
}
}
}
}
5 changes: 0 additions & 5 deletions MarketBoardPlugin/MBPluginConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,6 @@ public class MBPluginConfig : IPluginConfiguration
/// </summary>
public bool KofiHidden { get; set; }

/// <summary>
/// Gets or sets a value indicating the maximum size of the Market item buffer.
/// </summary>
public int MarketBufferSize { get; set; } = 10;

/// <summary>
/// Gets or sets a value indicating the number of ms an item can be cached.
/// </summary>
Expand Down
18 changes: 5 additions & 13 deletions MarketBoardPlugin/MarketBoardPlugin.csproj
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="Dalamud.Plugin.Bootstrap.targets" />

<Project Sdk="Dalamud.NET.Sdk/9.0.2">
<PropertyGroup>
<AssemblyName>MarketBoardPlugin</AssemblyName>
<Authors>Florian "fmauNeko" Maunier</Authors>
<DocumentationFile>bin/$(Configuration)/$(TargetFramework)/$(AssemblyName).xml</DocumentationFile>
<DebugType>PdbOnly</DebugType>
<GenerateFullPaths>true</GenerateFullPaths>
<MSBuildGitHashCommand>git rev-parse --short HEAD</MSBuildGitHashCommand>
<AssemblyVersion>1.7.0</AssemblyVersion>
<FileVersion>1.7.0</FileVersion>
<Version>1.7.0</Version>
<AssemblyVersion>1.7.1</AssemblyVersion>
<FileVersion>1.7.1</FileVersion>
<Version>1.7.1</Version>
<Company>Florian Maunier</Company>
<Description>Market board plugin for Dalamud.</Description>
<Copyright>Copyright (c) Florian Maunier. All rights reserved.</Copyright>
Expand Down Expand Up @@ -41,12 +38,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Dalamud.DrunkenToad" Version="1.9.2" />
<PackageReference Include="MSBuildGitHash" Version="2.0.2"
Condition="'$(Configuration)'!='Release'">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Polly.Core" Version="8.4.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Loading

0 comments on commit 2d3f6af

Please sign in to comment.