diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
index dbe95ea23f..6a102d7789 100644
--- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs
+++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
@@ -2,6 +2,7 @@
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Threading.Tasks;
using Dalamud.Game.Text;
using Dalamud.Interface.Internal.Windows.PluginInstaller;
@@ -39,6 +40,8 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
[JsonIgnore]
private bool isSaveQueued;
+ private Task? writeTask;
+
///
/// Delegate for the event that occurs when the dalamud configuration is saved.
///
@@ -488,6 +491,9 @@ public void Dispose()
{
// Make sure that we save, if a save is queued while we are shutting down
this.Update();
+
+ // Wait for the write to finish
+ this.writeTask?.Wait();
}
///
@@ -508,7 +514,8 @@ private void Save()
{
ThreadSafety.AssertMainThread();
- Service.Get().WriteAllText(
+ this.writeTask?.Wait();
+ this.writeTask = Service.Get().WriteAllTextAsync(
this.configPath, JsonConvert.SerializeObject(this, SerializerSettings));
this.DalamudConfigurationSaved?.Invoke(this);
}
diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs
index de5e071c11..a7509dee6c 100644
--- a/Dalamud/Configuration/PluginConfigurations.cs
+++ b/Dalamud/Configuration/PluginConfigurations.cs
@@ -1,4 +1,5 @@
using System.IO;
+using System.Threading.Tasks;
using Dalamud.Storage;
using Newtonsoft.Json;
@@ -11,6 +12,7 @@ namespace Dalamud.Configuration;
public sealed class PluginConfigurations
{
private readonly DirectoryInfo configDirectory;
+ private Task? currentWriteTask;
///
/// Initializes a new instance of the class.
@@ -32,10 +34,32 @@ public PluginConfigurations(string storageFolder)
/// Plugin configuration.
/// Plugin name.
/// WorkingPluginId of the plugin.
+ [Obsolete("Use SaveAsync instead.")]
public void Save(IPluginConfiguration config, string pluginName, Guid workingPluginId)
{
- Service.Get()
- .WriteAllText(this.GetConfigFile(pluginName).FullName, SerializeConfig(config), workingPluginId);
+ // TODO(api10): This API should be async-only. Remove the sync version!
+ // Yes, this means that all plugins will be blocking each other when writing configs for now, but that's fine until
+ // we can make this API async
+ this.currentWriteTask?.Wait();
+ this.currentWriteTask = Service.Get()
+ .WriteAllTextAsync(this.GetConfigFile(pluginName).FullName, SerializeConfig(config), workingPluginId);
+ }
+
+ ///
+ /// Save/Load plugin configuration.
+ /// NOTE: Save/Load are still using Type information for now,
+ /// despite LoadForType superseding Load and not requiring or using it.
+ /// It might be worth removing the Type info from Save, to strip it from all future saved configs,
+ /// and then Load() can probably be removed entirely.
+ ///
+ /// Plugin configuration.
+ /// Plugin name.
+ /// WorkingPluginId of the plugin.
+ /// A representing the asynchronous operation.
+ public async Task SaveAsync(IPluginConfiguration config, string pluginName, Guid workingPluginId)
+ {
+ await Service.Get()
+ .WriteAllTextAsync(this.GetConfigFile(pluginName).FullName, SerializeConfig(config), workingPluginId);
}
///
diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs
index 8cce5c2864..47c38b227f 100644
--- a/Dalamud/Game/Text/SeStringHandling/SeString.cs
+++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs
@@ -368,17 +368,6 @@ public static SeString CreateMapLinkWithInstance(uint territoryId, uint mapId, i
return null;
}
- private static string GetMapLinkNameString(string placeName, int? instance, string coordinateString)
- {
- var instanceString = string.Empty;
- if (instance is > 0 and < 10)
- {
- instanceString = (SeIconChar.Instance1 + instance.Value - 1).ToIconString();
- }
-
- return $"{placeName}{instanceString} {coordinateString}";
- }
-
///
/// Creates an SeString representing an entire payload chain that can be used to link party finder listings in the chat log.
///
@@ -512,4 +501,15 @@ public override string ToString()
{
return this.TextValue;
}
+
+ private static string GetMapLinkNameString(string placeName, int? instance, string coordinateString)
+ {
+ var instanceString = string.Empty;
+ if (instance is > 0 and < 10)
+ {
+ instanceString = (SeIconChar.Instance1 + instance.Value - 1).ToIconString();
+ }
+
+ return $"{placeName}{instanceString} {coordinateString}";
+ }
}
diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs
index a6c4e243c0..816352d808 100644
--- a/Dalamud/Interface/Internal/DalamudInterface.cs
+++ b/Dalamud/Interface/Internal/DalamudInterface.cs
@@ -249,7 +249,6 @@ public void OpenPluginStats()
///
/// Opens the on the plugin installed.
///
- /// The page of the installer to open.
public void OpenPluginInstaller()
{
this.pluginWindow.OpenTo(this.configuration.PluginInstallerOpen);
@@ -394,6 +393,7 @@ public void ToggleDataWindow(string dataKind = null)
///
/// Toggles the .
///
+ /// The page of the installer to open.
public void TogglePluginInstallerWindowTo(PluginInstallerWindow.PluginInstallerOpenKind kind) => this.pluginWindow.ToggleTo(kind);
///
diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
index ba47d2c8e4..5b03ecf7e7 100644
--- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
@@ -50,6 +50,7 @@ internal class DataWindow : Window
new DataShareWidget(),
new NetworkMonitorWidget(),
new IconBrowserWidget(),
+ new VfsWidget(),
};
private readonly IOrderedEnumerable orderedModules;
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/VfsWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/VfsWidget.cs
new file mode 100644
index 0000000000..dc01cc8473
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/VfsWidget.cs
@@ -0,0 +1,102 @@
+using System.Diagnostics;
+using System.IO;
+
+using Dalamud.Configuration.Internal;
+using Dalamud.Storage;
+using ImGuiNET;
+using Serilog;
+
+namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
+
+///
+/// Widget for displaying configuration info.
+///
+internal class VfsWidget : IDataWindowWidget
+{
+ private int numBytes = 1024;
+ private int reps = 1;
+
+ ///
+ public string[]? CommandShortcuts { get; init; } = { "vfs" };
+
+ ///
+ public string DisplayName { get; init; } = "VFS";
+
+ ///
+ public bool Ready { get; set; }
+
+ ///
+ public void Load()
+ {
+ this.Ready = true;
+ }
+
+ ///
+ public void Draw()
+ {
+ var service = Service.Get();
+ var dalamud = Service.Get();
+
+ ImGui.InputInt("Num bytes", ref this.numBytes);
+ ImGui.InputInt("Reps", ref this.reps);
+
+ var path = Path.Combine(dalamud.StartInfo.WorkingDirectory!, "test.bin");
+
+ if (ImGui.Button("Write"))
+ {
+ Log.Information("=== WRITING ===");
+ var data = new byte[this.numBytes];
+ var stopwatch = new Stopwatch();
+ var acc = 0L;
+
+ for (var i = 0; i < this.reps; i++)
+ {
+ stopwatch.Restart();
+ service.WriteAllBytesAsync(path, data).GetAwaiter().GetResult();
+ stopwatch.Stop();
+ acc += stopwatch.ElapsedMilliseconds;
+ Log.Information("Turn {Turn} took {Ms}ms", i, stopwatch.ElapsedMilliseconds);
+ }
+
+ Log.Information("Took {Ms}ms in total", acc);
+ }
+
+ if (ImGui.Button("Read"))
+ {
+ Log.Information("=== READING ===");
+ var stopwatch = new Stopwatch();
+ var acc = 0L;
+
+ for (var i = 0; i < this.reps; i++)
+ {
+ stopwatch.Restart();
+ service.ReadAllBytes(path);
+ stopwatch.Stop();
+ acc += stopwatch.ElapsedMilliseconds;
+ Log.Information("Turn {Turn} took {Ms}ms", i, stopwatch.ElapsedMilliseconds);
+ }
+
+ Log.Information("Took {Ms}ms in total", acc);
+ }
+
+ if (ImGui.Button("Test Config"))
+ {
+ var config = Service.Get();
+
+ Log.Information("=== READING ===");
+ var stopwatch = new Stopwatch();
+ var acc = 0L;
+
+ for (var i = 0; i < this.reps; i++)
+ {
+ stopwatch.Restart();
+ config.ForceSave();
+ stopwatch.Stop();
+ acc += stopwatch.ElapsedMilliseconds;
+ Log.Information("Turn {Turn} took {Ms}ms", i, stopwatch.ElapsedMilliseconds);
+ }
+
+ Log.Information("Took {Ms}ms in total", acc);
+ }
+ }
+}
diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs
index 82f19aa492..b098717bd7 100644
--- a/Dalamud/Plugin/DalamudPluginInterface.cs
+++ b/Dalamud/Plugin/DalamudPluginInterface.cs
@@ -1,4 +1,3 @@
-using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -6,6 +5,7 @@
using System.IO;
using System.Linq;
using System.Reflection;
+using System.Threading.Tasks;
using Dalamud.Configuration;
using Dalamud.Configuration.Internal;
@@ -338,12 +338,30 @@ public ICallGateSubscriber GetIpcSubscribe
/// Save a plugin configuration(inheriting IPluginConfiguration).
///
/// The current configuration.
+ [Obsolete("Prefer SavePluginConfigAsync() to avoid blocking the main thread.")]
public void SavePluginConfig(IPluginConfiguration? currentConfig)
{
if (currentConfig == null)
return;
+#pragma warning disable CS0618 // Type or member is obsolete
this.configs.Save(currentConfig, this.plugin.InternalName, this.plugin.Manifest.WorkingPluginId);
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+
+ ///
+ /// Save a plugin configuration(inheriting IPluginConfiguration).
+ ///
+ /// The current configuration.
+ /// A representing the asynchronous operation.
+ public async Task SavePluginConfigAsync(IPluginConfiguration? currentConfig)
+ {
+ if (currentConfig == null)
+ return;
+
+#pragma warning disable CS0618 // Type or member is obsolete
+ await this.configs.SaveAsync(currentConfig, this.plugin.InternalName, this.plugin.Manifest.WorkingPluginId);
+#pragma warning restore CS0618 // Type or member is obsolete
}
///
diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs
index 9feb17c0d5..38ec7b2f01 100644
--- a/Dalamud/Storage/ReliableFileStorage.cs
+++ b/Dalamud/Storage/ReliableFileStorage.cs
@@ -1,5 +1,6 @@
using System.IO;
using System.Text;
+using System.Threading.Tasks;
using Dalamud.Logging.Internal;
using Dalamud.Utility;
@@ -91,8 +92,9 @@ public bool Exists(string path, Guid containerId = default)
/// Path to write to.
/// The contents of the file.
/// Container to write to.
- public void WriteAllText(string path, string? contents, Guid containerId = default)
- => this.WriteAllText(path, contents, Encoding.UTF8, containerId);
+ /// A representing the asynchronous operation.
+ public async Task WriteAllTextAsync(string path, string? contents, Guid containerId = default)
+ => await this.WriteAllTextAsync(path, contents, Encoding.UTF8, containerId);
///
/// Write all text to a file.
@@ -101,19 +103,21 @@ public void WriteAllText(string path, string? contents, Guid containerId = defau
/// The contents of the file.
/// The encoding to write with.
/// Container to write to.
- public void WriteAllText(string path, string? contents, Encoding encoding, Guid containerId = default)
+ /// A representing the asynchronous operation.
+ public async Task WriteAllTextAsync(string path, string? contents, Encoding encoding, Guid containerId = default)
{
var bytes = encoding.GetBytes(contents ?? string.Empty);
- this.WriteAllBytes(path, bytes, containerId);
+ await this.WriteAllBytesAsync(path, bytes, containerId);
}
-
+
///
/// Write all bytes to a file.
///
/// Path to write to.
/// The contents of the file.
/// Container to write to.
- public void WriteAllBytes(string path, byte[] bytes, Guid containerId = default)
+ /// A representing the asynchronous operation.
+ public Task WriteAllBytesAsync(string path, byte[] bytes, Guid containerId = default)
{
ArgumentException.ThrowIfNullOrEmpty(path);
@@ -122,7 +126,7 @@ public void WriteAllBytes(string path, byte[] bytes, Guid containerId = default)
if (this.db == null)
{
Util.WriteAllBytesSafe(path, bytes);
- return;
+ return Task.CompletedTask;
}
this.db.RunInTransaction(() =>
@@ -148,6 +152,8 @@ public void WriteAllBytes(string path, byte[] bytes, Guid containerId = default)
Util.WriteAllBytesSafe(path, bytes);
});
}
+
+ return Task.CompletedTask;
}
///
@@ -258,6 +264,8 @@ public byte[] ReadAllBytes(string path, bool forceBackup = false, Guid container
if (forceBackup)
{
+ Log.Information("Reading from db");
+
// If the db failed to load, act as if the file does not exist
if (this.db == null)
throw new FileNotFoundException("Backup database was not available");