mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-01-03 14:23:40 +01:00
Merge pull request #1673 from Soreepeong/fix/igameconfig-racecon
IGameConfig: fix load-time race condition
This commit is contained in:
commit
6116508c57
4 changed files with 130 additions and 33 deletions
|
|
@ -1,4 +1,6 @@
|
|||
using Dalamud.Hooking;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Services;
|
||||
|
|
@ -15,6 +17,11 @@ namespace Dalamud.Game.Config;
|
|||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
|
||||
{
|
||||
private readonly TaskCompletionSource tcsInitialization = new();
|
||||
private readonly TaskCompletionSource<GameConfigSection> tcsSystem = new();
|
||||
private readonly TaskCompletionSource<GameConfigSection> tcsUiConfig = new();
|
||||
private readonly TaskCompletionSource<GameConfigSection> tcsUiControl = new();
|
||||
|
||||
private readonly GameConfigAddressResolver address = new();
|
||||
private Hook<ConfigChangeDelegate>? configChangeHook;
|
||||
|
||||
|
|
@ -23,16 +30,32 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
|
|||
{
|
||||
framework.RunOnTick(() =>
|
||||
{
|
||||
Log.Verbose("[GameConfig] Initializing");
|
||||
var csFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
|
||||
var commonConfig = &csFramework->SystemConfig.CommonSystemConfig;
|
||||
this.System = new GameConfigSection("System", framework, &commonConfig->ConfigBase);
|
||||
this.UiConfig = new GameConfigSection("UiConfig", framework, &commonConfig->UiConfig);
|
||||
this.UiControl = new GameConfigSection("UiControl", framework, () => this.UiConfig.TryGetBool("PadMode", out var padMode) && padMode ? &commonConfig->UiControlGamepadConfig : &commonConfig->UiControlConfig);
|
||||
|
||||
this.address.Setup(sigScanner);
|
||||
this.configChangeHook = Hook<ConfigChangeDelegate>.FromAddress(this.address.ConfigChangeAddress, this.OnConfigChanged);
|
||||
this.configChangeHook.Enable();
|
||||
try
|
||||
{
|
||||
Log.Verbose("[GameConfig] Initializing");
|
||||
var csFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance();
|
||||
var commonConfig = &csFramework->SystemConfig.CommonSystemConfig;
|
||||
this.tcsSystem.SetResult(new("System", framework, &commonConfig->ConfigBase));
|
||||
this.tcsUiConfig.SetResult(new("UiConfig", framework, &commonConfig->UiConfig));
|
||||
this.tcsUiControl.SetResult(
|
||||
new(
|
||||
"UiControl",
|
||||
framework,
|
||||
() => this.UiConfig.TryGetBool("PadMode", out var padMode) && padMode
|
||||
? &commonConfig->UiControlGamepadConfig
|
||||
: &commonConfig->UiControlConfig));
|
||||
|
||||
this.address.Setup(sigScanner);
|
||||
this.configChangeHook = Hook<ConfigChangeDelegate>.FromAddress(
|
||||
this.address.ConfigChangeAddress,
|
||||
this.OnConfigChanged);
|
||||
this.configChangeHook.Enable();
|
||||
this.tcsInitialization.SetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.tcsInitialization.SetExceptionIfIncomplete(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -58,14 +81,19 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
|
|||
public event EventHandler<ConfigChangeEvent>? UiControlChanged;
|
||||
#pragma warning restore 67
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameConfigSection System { get; private set; }
|
||||
/// <summary>
|
||||
/// Gets a task representing the initialization state of this class.
|
||||
/// </summary>
|
||||
public Task InitializationTask => this.tcsInitialization.Task;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameConfigSection UiConfig { get; private set; }
|
||||
public GameConfigSection System => this.tcsSystem.Task.Result;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameConfigSection UiControl { get; private set; }
|
||||
public GameConfigSection UiConfig => this.tcsUiConfig.Task.Result;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameConfigSection UiControl => this.tcsUiControl.Task.Result;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGet(SystemConfigOption option, out bool value) => this.System.TryGet(option.GetName(), out value);
|
||||
|
|
@ -169,6 +197,11 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
|
|||
/// <inheritdoc/>
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
var ode = new ObjectDisposedException(nameof(GameConfig));
|
||||
this.tcsInitialization.SetExceptionIfIncomplete(ode);
|
||||
this.tcsSystem.SetExceptionIfIncomplete(ode);
|
||||
this.tcsUiConfig.SetExceptionIfIncomplete(ode);
|
||||
this.tcsUiControl.SetExceptionIfIncomplete(ode);
|
||||
this.configChangeHook?.Disable();
|
||||
this.configChangeHook?.Dispose();
|
||||
}
|
||||
|
|
@ -220,15 +253,24 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig
|
|||
[ServiceManager.ServiceDependency]
|
||||
private readonly GameConfig gameConfigService = Service<GameConfig>.Get();
|
||||
|
||||
private readonly Task initializationTask;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameConfigPluginScoped"/> class.
|
||||
/// </summary>
|
||||
internal GameConfigPluginScoped()
|
||||
{
|
||||
this.gameConfigService.Changed += this.ConfigChangedForward;
|
||||
this.gameConfigService.System.Changed += this.SystemConfigChangedForward;
|
||||
this.gameConfigService.UiConfig.Changed += this.UiConfigConfigChangedForward;
|
||||
this.gameConfigService.UiControl.Changed += this.UiControlConfigChangedForward;
|
||||
this.initializationTask = this.gameConfigService.InitializationTask.ContinueWith(
|
||||
r =>
|
||||
{
|
||||
if (!r.IsCompletedSuccessfully)
|
||||
return r;
|
||||
this.gameConfigService.System.Changed += this.SystemConfigChangedForward;
|
||||
this.gameConfigService.UiConfig.Changed += this.UiConfigConfigChangedForward;
|
||||
this.gameConfigService.UiControl.Changed += this.UiControlConfigChangedForward;
|
||||
return Task.CompletedTask;
|
||||
}).Unwrap();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -256,9 +298,15 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig
|
|||
public void Dispose()
|
||||
{
|
||||
this.gameConfigService.Changed -= this.ConfigChangedForward;
|
||||
this.gameConfigService.System.Changed -= this.SystemConfigChangedForward;
|
||||
this.gameConfigService.UiConfig.Changed -= this.UiConfigConfigChangedForward;
|
||||
this.gameConfigService.UiControl.Changed -= this.UiControlConfigChangedForward;
|
||||
this.initializationTask.ContinueWith(
|
||||
r =>
|
||||
{
|
||||
if (!r.IsCompletedSuccessfully)
|
||||
return;
|
||||
this.gameConfigService.System.Changed -= this.SystemConfigChangedForward;
|
||||
this.gameConfigService.UiConfig.Changed -= this.UiConfigConfigChangedForward;
|
||||
this.gameConfigService.UiControl.Changed -= this.UiControlConfigChangedForward;
|
||||
});
|
||||
|
||||
this.Changed = null;
|
||||
this.SystemChanged = null;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,23 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Game.Config;
|
||||
using FFXIVClientStructs.FFXIV.Common.Configuration;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
|
||||
namespace Dalamud.Plugin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents the game's configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Accessing <see cref="GameConfigSection"/>-typed properties such as <see cref="System"/>, directly or indirectly
|
||||
/// via <see cref="TryGet(Game.Config.SystemConfigOption,out bool)"/>,
|
||||
/// <see cref="Set(Game.Config.SystemConfigOption,bool)"/>, or alike will block, if the game is not done loading.<br />
|
||||
/// Therefore, avoid accessing configuration from your plugin constructor, especially if your plugin sets
|
||||
/// <see cref="PluginManifest.LoadRequiredState"/> to <c>2</c> and <see cref="PluginManifest.LoadSync"/> to <c>true</c>.
|
||||
/// If property access from the plugin constructor is desired, do the value retrieval asynchronously via
|
||||
/// <see cref="IFramework.RunOnFrameworkThread{T}(Func{T})"/>; do not wait for the result right away.
|
||||
/// </remarks>
|
||||
public interface IGameConfig
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -194,12 +194,14 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
|||
|
||||
try
|
||||
{
|
||||
await using var tempPathStream = File.Open(tempPath, FileMode.Create, FileAccess.Write);
|
||||
await url.DownloadAsync(
|
||||
this.httpClient.SharedHttpClient,
|
||||
tempPathStream,
|
||||
this.cancellationTokenSource.Token);
|
||||
tempPathStream.Dispose();
|
||||
await using (var tempPathStream = File.Open(tempPath, FileMode.Create, FileAccess.Write))
|
||||
{
|
||||
await url.DownloadAsync(
|
||||
this.httpClient.SharedHttpClient,
|
||||
tempPathStream,
|
||||
this.cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
for (var j = RenameAttemptCount; ; j--)
|
||||
{
|
||||
try
|
||||
|
|
@ -265,7 +267,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
|||
/// <inheritdoc/>
|
||||
[Pure]
|
||||
public IDalamudTextureWrap GetDalamudTextureWrap(DalamudAsset asset) =>
|
||||
ExtractResult(this.GetDalamudTextureWrapAsync(asset));
|
||||
this.GetDalamudTextureWrapAsync(asset).Result;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Pure]
|
||||
|
|
@ -332,8 +334,6 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
|||
}
|
||||
}
|
||||
|
||||
private static T ExtractResult<T>(Task<T> t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult();
|
||||
|
||||
private Task<TOut> TransformImmediate<TIn, TOut>(Task<TIn> task, Func<TIn, TOut> transformer)
|
||||
{
|
||||
if (task.IsCompletedSuccessfully)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ using System.Reflection.Emit;
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Data;
|
||||
|
|
@ -697,6 +698,45 @@ public static class Util
|
|||
Marshal.ThrowExceptionForHR(hr.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="TaskCompletionSource.SetException(System.Exception)"/> if the task is incomplete.
|
||||
/// </summary>
|
||||
/// <param name="t">The task.</param>
|
||||
/// <param name="ex">The exception to set.</param>
|
||||
internal static void SetExceptionIfIncomplete(this TaskCompletionSource t, Exception ex)
|
||||
{
|
||||
if (t.Task.IsCompleted)
|
||||
return;
|
||||
try
|
||||
{
|
||||
t.SetException(ex);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="TaskCompletionSource.SetException(System.Exception)"/> if the task is incomplete.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the result.</typeparam>
|
||||
/// <param name="t">The task.</param>
|
||||
/// <param name="ex">The exception to set.</param>
|
||||
internal static void SetExceptionIfIncomplete<T>(this TaskCompletionSource<T> t, Exception ex)
|
||||
{
|
||||
if (t.Task.IsCompleted)
|
||||
return;
|
||||
try
|
||||
{
|
||||
t.SetException(ex);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Print formatted GameObject Information to ImGui.
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue