mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 18:27:23 +01:00
Merge pull request #1710 from goatcorp/net8-rollup
[net8] Rollup changes from master
This commit is contained in:
commit
e50f9cda99
12 changed files with 701 additions and 241 deletions
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
|
@ -41,11 +42,13 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
[ServiceManager.ServiceDependency]
|
||||
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
|
||||
|
||||
private readonly object runOnNextTickTaskListSync = new();
|
||||
private List<RunOnNextTickTaskBase> runOnNextTickTaskList = new();
|
||||
private List<RunOnNextTickTaskBase> runOnNextTickTaskList2 = new();
|
||||
private readonly CancellationTokenSource frameworkDestroy;
|
||||
private readonly ThreadBoundTaskScheduler frameworkThreadTaskScheduler;
|
||||
|
||||
private Thread? frameworkUpdateThread;
|
||||
private readonly ConcurrentDictionary<TaskCompletionSource, (ulong Expire, CancellationToken CancellationToken)>
|
||||
tickDelayedTaskCompletionSources = new();
|
||||
|
||||
private ulong tickCounter;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private Framework(TargetSigScanner sigScanner, GameLifecycle lifecycle)
|
||||
|
|
@ -56,6 +59,14 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
this.addressResolver = new FrameworkAddressResolver();
|
||||
this.addressResolver.Setup(sigScanner);
|
||||
|
||||
this.frameworkDestroy = new();
|
||||
this.frameworkThreadTaskScheduler = new();
|
||||
this.FrameworkThreadTaskFactory = new(
|
||||
this.frameworkDestroy.Token,
|
||||
TaskCreationOptions.None,
|
||||
TaskContinuationOptions.None,
|
||||
this.frameworkThreadTaskScheduler);
|
||||
|
||||
this.updateHook = Hook<OnUpdateDetour>.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate);
|
||||
this.destroyHook = Hook<OnRealDestroyDelegate>.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy);
|
||||
|
||||
|
|
@ -97,14 +108,17 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
/// <inheritdoc/>
|
||||
public DateTime LastUpdateUTC { get; private set; } = DateTime.MinValue;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TaskFactory FrameworkThreadTaskFactory { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsInFrameworkUpdateThread => Thread.CurrentThread == this.frameworkUpdateThread;
|
||||
public bool IsInFrameworkUpdateThread => this.frameworkThreadTaskScheduler.IsOnBoundThread;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsFrameworkUnloading { get; internal set; }
|
||||
public bool IsFrameworkUnloading => this.frameworkDestroy.IsCancellationRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of update sub-delegates that didn't get updated this frame.
|
||||
|
|
@ -116,6 +130,19 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
/// </summary>
|
||||
internal bool DispatchUpdateEvents { get; set; } = true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (this.frameworkDestroy.IsCancellationRequested)
|
||||
return Task.FromCanceled(this.frameworkDestroy.Token);
|
||||
if (numTicks <= 0)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var tcs = new TaskCompletionSource();
|
||||
this.tickDelayedTaskCompletionSources[tcs] = (this.tickCounter + (ulong)numTicks, cancellationToken);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<T> RunOnFrameworkThread<T>(Func<T> func) =>
|
||||
this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? Task.FromResult(func()) : this.RunOnTick(func);
|
||||
|
|
@ -162,20 +189,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
return Task.FromCanceled<T>(cts.Token);
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<T>();
|
||||
lock (this.runOnNextTickTaskListSync)
|
||||
{
|
||||
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<T>()
|
||||
if (cancellationToken == default)
|
||||
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
|
||||
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
|
||||
new[]
|
||||
{
|
||||
RemainingTicks = delayTicks,
|
||||
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
|
||||
CancellationToken = cancellationToken,
|
||||
TaskCompletionSource = tcs,
|
||||
Func = func,
|
||||
});
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
Task.Delay(delay, cancellationToken),
|
||||
this.DelayTicks(delayTicks, cancellationToken),
|
||||
},
|
||||
_ => func(),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -191,20 +214,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
return Task.FromCanceled(cts.Token);
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource();
|
||||
lock (this.runOnNextTickTaskListSync)
|
||||
{
|
||||
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskAction()
|
||||
if (cancellationToken == default)
|
||||
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
|
||||
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
|
||||
new[]
|
||||
{
|
||||
RemainingTicks = delayTicks,
|
||||
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
|
||||
CancellationToken = cancellationToken,
|
||||
TaskCompletionSource = tcs,
|
||||
Action = action,
|
||||
});
|
||||
}
|
||||
|
||||
return tcs.Task;
|
||||
Task.Delay(delay, cancellationToken),
|
||||
this.DelayTicks(delayTicks, cancellationToken),
|
||||
},
|
||||
_ => action(),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -220,20 +239,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
return Task.FromCanceled<T>(cts.Token);
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<Task<T>>();
|
||||
lock (this.runOnNextTickTaskListSync)
|
||||
{
|
||||
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<Task<T>>()
|
||||
if (cancellationToken == default)
|
||||
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
|
||||
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
|
||||
new[]
|
||||
{
|
||||
RemainingTicks = delayTicks,
|
||||
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
|
||||
CancellationToken = cancellationToken,
|
||||
TaskCompletionSource = tcs,
|
||||
Func = func,
|
||||
});
|
||||
}
|
||||
|
||||
return tcs.Task.ContinueWith(x => x.Result, cancellationToken).Unwrap();
|
||||
Task.Delay(delay, cancellationToken),
|
||||
this.DelayTicks(delayTicks, cancellationToken),
|
||||
},
|
||||
_ => func(),
|
||||
cancellationToken).Unwrap();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -249,20 +264,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
return Task.FromCanceled(cts.Token);
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<Task>();
|
||||
lock (this.runOnNextTickTaskListSync)
|
||||
{
|
||||
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<Task>()
|
||||
if (cancellationToken == default)
|
||||
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
|
||||
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
|
||||
new[]
|
||||
{
|
||||
RemainingTicks = delayTicks,
|
||||
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
|
||||
CancellationToken = cancellationToken,
|
||||
TaskCompletionSource = tcs,
|
||||
Func = func,
|
||||
});
|
||||
}
|
||||
|
||||
return tcs.Task.ContinueWith(x => x.Result, cancellationToken).Unwrap();
|
||||
Task.Delay(delay, cancellationToken),
|
||||
this.DelayTicks(delayTicks, cancellationToken),
|
||||
},
|
||||
_ => func(),
|
||||
cancellationToken).Unwrap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -338,23 +349,9 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
}
|
||||
}
|
||||
|
||||
private void RunPendingTickTasks()
|
||||
{
|
||||
if (this.runOnNextTickTaskList.Count == 0 && this.runOnNextTickTaskList2.Count == 0)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
lock (this.runOnNextTickTaskListSync)
|
||||
(this.runOnNextTickTaskList, this.runOnNextTickTaskList2) = (this.runOnNextTickTaskList2, this.runOnNextTickTaskList);
|
||||
|
||||
this.runOnNextTickTaskList2.RemoveAll(x => x.Run());
|
||||
}
|
||||
}
|
||||
|
||||
private bool HandleFrameworkUpdate(IntPtr framework)
|
||||
{
|
||||
this.frameworkUpdateThread ??= Thread.CurrentThread;
|
||||
this.frameworkThreadTaskScheduler.BoundThread ??= Thread.CurrentThread;
|
||||
|
||||
ThreadSafety.MarkMainThread();
|
||||
|
||||
|
|
@ -388,18 +385,30 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
|
||||
this.LastUpdate = DateTime.Now;
|
||||
this.LastUpdateUTC = DateTime.UtcNow;
|
||||
this.tickCounter++;
|
||||
foreach (var (k, (expiry, ct)) in this.tickDelayedTaskCompletionSources)
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
k.SetCanceled(ct);
|
||||
else if (expiry <= this.tickCounter)
|
||||
k.SetResult();
|
||||
else
|
||||
continue;
|
||||
|
||||
this.tickDelayedTaskCompletionSources.Remove(k, out _);
|
||||
}
|
||||
|
||||
if (StatsEnabled)
|
||||
{
|
||||
StatsStopwatch.Restart();
|
||||
this.RunPendingTickTasks();
|
||||
this.frameworkThreadTaskScheduler.Run();
|
||||
StatsStopwatch.Stop();
|
||||
|
||||
AddToStats(nameof(this.RunPendingTickTasks), StatsStopwatch.Elapsed.TotalMilliseconds);
|
||||
AddToStats(nameof(this.frameworkThreadTaskScheduler), StatsStopwatch.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.RunPendingTickTasks();
|
||||
this.frameworkThreadTaskScheduler.Run();
|
||||
}
|
||||
|
||||
if (StatsEnabled && this.Update != null)
|
||||
|
|
@ -411,7 +420,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
// Cleanup handlers that are no longer being called
|
||||
foreach (var key in this.NonUpdatedSubDelegates)
|
||||
{
|
||||
if (key == nameof(this.RunPendingTickTasks))
|
||||
if (key == nameof(this.FrameworkThreadTaskFactory))
|
||||
continue;
|
||||
|
||||
if (StatsHistory[key].Count > 0)
|
||||
|
|
@ -438,8 +447,11 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
|
||||
private bool HandleFrameworkDestroy(IntPtr framework)
|
||||
{
|
||||
this.IsFrameworkUnloading = true;
|
||||
this.frameworkDestroy.Cancel();
|
||||
this.DispatchUpdateEvents = false;
|
||||
foreach (var k in this.tickDelayedTaskCompletionSources.Keys)
|
||||
k.SetCanceled(this.frameworkDestroy.Token);
|
||||
this.tickDelayedTaskCompletionSources.Clear();
|
||||
|
||||
// All the same, for now...
|
||||
this.lifecycle.SetShuttingDown();
|
||||
|
|
@ -447,95 +459,12 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
|
||||
Log.Information("Framework::Destroy!");
|
||||
Service<Dalamud>.Get().Unload();
|
||||
this.RunPendingTickTasks();
|
||||
this.frameworkThreadTaskScheduler.Run();
|
||||
ServiceManager.WaitForServiceUnload();
|
||||
Log.Information("Framework::Destroy OK!");
|
||||
|
||||
return this.destroyHook.OriginalDisposeSafe(framework);
|
||||
}
|
||||
|
||||
private abstract class RunOnNextTickTaskBase
|
||||
{
|
||||
internal int RemainingTicks { get; set; }
|
||||
|
||||
internal long RunAfterTickCount { get; init; }
|
||||
|
||||
internal CancellationToken CancellationToken { get; init; }
|
||||
|
||||
internal bool Run()
|
||||
{
|
||||
if (this.CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
this.CancelImpl();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.RemainingTicks > 0)
|
||||
this.RemainingTicks -= 1;
|
||||
if (this.RemainingTicks > 0)
|
||||
return false;
|
||||
|
||||
if (this.RunAfterTickCount > Environment.TickCount64)
|
||||
return false;
|
||||
|
||||
this.RunImpl();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected abstract void RunImpl();
|
||||
|
||||
protected abstract void CancelImpl();
|
||||
}
|
||||
|
||||
private class RunOnNextTickTaskFunc<T> : RunOnNextTickTaskBase
|
||||
{
|
||||
internal TaskCompletionSource<T> TaskCompletionSource { get; init; }
|
||||
|
||||
internal Func<T> Func { get; init; }
|
||||
|
||||
protected override void RunImpl()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.TaskCompletionSource.SetResult(this.Func());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.TaskCompletionSource.SetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void CancelImpl()
|
||||
{
|
||||
this.TaskCompletionSource.SetCanceled();
|
||||
}
|
||||
}
|
||||
|
||||
private class RunOnNextTickTaskAction : RunOnNextTickTaskBase
|
||||
{
|
||||
internal TaskCompletionSource TaskCompletionSource { get; init; }
|
||||
|
||||
internal Action Action { get; init; }
|
||||
|
||||
protected override void RunImpl()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Action();
|
||||
this.TaskCompletionSource.SetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.TaskCompletionSource.SetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void CancelImpl()
|
||||
{
|
||||
this.TaskCompletionSource.SetCanceled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -586,6 +515,10 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework
|
|||
this.Update = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) =>
|
||||
this.frameworkService.DelayTicks(numTicks, cancellationToken);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<T> RunOnFrameworkThread<T>(Func<T> func)
|
||||
=> this.frameworkService.RunOnFrameworkThread(func);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using Dalamud.Configuration.Internal;
|
|||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.FontIdentifier;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility;
|
||||
|
||||
|
|
@ -84,11 +85,22 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
private IFontHandle? fontHandle;
|
||||
private SingleFontSpec selectedFont;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SingleFontChooserDialog"/> class.
|
||||
/// </summary>
|
||||
private bool popupPositionChanged;
|
||||
private bool popupSizeChanged;
|
||||
private Vector2 popupPosition = new(float.NaN);
|
||||
private Vector2 popupSize = new(float.NaN);
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="SingleFontChooserDialog"/> class.</summary>
|
||||
/// <param name="newAsyncAtlas">A new instance of <see cref="IFontAtlas"/> created using
|
||||
/// <see cref="FontAtlasAutoRebuildMode.Async"/> as its auto-rebuild mode.</param>
|
||||
/// <remarks>The passed instance of <see cref="newAsyncAtlas"/> will be disposed after use. If you pass an atlas
|
||||
/// that is already being used, then all the font handles under the passed atlas will be invalidated upon disposing
|
||||
/// this font chooser. Consider using <see cref="SingleFontChooserDialog(UiBuilder, bool, string?)"/> for automatic
|
||||
/// handling of font atlas derived from a <see cref="UiBuilder"/>, or even <see cref="CreateAuto"/> for automatic
|
||||
/// registration and unregistration of <see cref="Draw"/> event handler in addition to automatic disposal of this
|
||||
/// class and the temporary font atlas for this font chooser dialog.</remarks>
|
||||
[Obsolete("See remarks, and use the other constructor.", false)]
|
||||
[Api10ToDo("Make private.")]
|
||||
public SingleFontChooserDialog(IFontAtlas newAsyncAtlas)
|
||||
{
|
||||
this.counter = Interlocked.Increment(ref counterStatic);
|
||||
|
|
@ -99,6 +111,39 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
Encoding.UTF8.GetBytes("Font preview.\n0123456789!", this.fontPreviewText);
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
// TODO: Api10ToDo; Remove this pragma warning disable line
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="SingleFontChooserDialog"/> class.</summary>
|
||||
/// <param name="uiBuilder">The relevant instance of UiBuilder.</param>
|
||||
/// <param name="isGlobalScaled">Whether the fonts in the atlas is global scaled.</param>
|
||||
/// <param name="debugAtlasName">Atlas name for debugging purposes.</param>
|
||||
/// <remarks>
|
||||
/// <para>The passed <see cref="UiBuilder"/> is only used for creating a temporary font atlas. It will not
|
||||
/// automatically register a hander for <see cref="UiBuilder.Draw"/>.</para>
|
||||
/// <para>Consider using <see cref="CreateAuto"/> for automatic registration and unregistration of
|
||||
/// <see cref="Draw"/> event handler in addition to automatic disposal of this class and the temporary font atlas
|
||||
/// for this font chooser dialog.</para>
|
||||
/// </remarks>
|
||||
public SingleFontChooserDialog(UiBuilder uiBuilder, bool isGlobalScaled = true, string? debugAtlasName = null)
|
||||
: this(uiBuilder.CreateFontAtlas(FontAtlasAutoRebuildMode.Async, isGlobalScaled, debugAtlasName))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="SingleFontChooserDialog"/> class.</summary>
|
||||
/// <param name="factory">An instance of <see cref="FontAtlasFactory"/>.</param>
|
||||
/// <param name="debugAtlasName">The temporary atlas name.</param>
|
||||
internal SingleFontChooserDialog(FontAtlasFactory factory, string debugAtlasName)
|
||||
: this(factory.CreateFontAtlas(debugAtlasName, FontAtlasAutoRebuildMode.Async))
|
||||
{
|
||||
}
|
||||
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
// TODO: Api10ToDo; Remove this pragma warning restore line
|
||||
|
||||
/// <summary>Called when the selected font spec has changed.</summary>
|
||||
public event Action<SingleFontSpec>? SelectedFontSpecChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title of this font chooser dialog popup.
|
||||
/// </summary>
|
||||
|
|
@ -153,6 +198,8 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
this.useAdvancedOptions |= Math.Abs(value.LineHeight - 1f) > 0.000001;
|
||||
this.useAdvancedOptions |= value.GlyphOffset != default;
|
||||
this.useAdvancedOptions |= value.LetterSpacing != 0f;
|
||||
|
||||
this.SelectedFontSpecChanged?.Invoke(this.selectedFont);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,15 +213,55 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
/// </summary>
|
||||
public bool IgnorePreviewGlobalScale { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="SingleFontChooserDialog"/> that will automatically draw and dispose itself as
|
||||
/// needed.
|
||||
/// <summary>Gets or sets a value indicating whether this popup should be modal, blocking everything behind from
|
||||
/// being interacted.</summary>
|
||||
/// <remarks>If <c>true</c>, then <see cref="ImGui.BeginPopupModal(string, ref bool, ImGuiWindowFlags)"/> will be
|
||||
/// used. Otherwise, <see cref="ImGui.Begin(string, ref bool, ImGuiWindowFlags)"/> will be used.</remarks>
|
||||
public bool IsModal { get; set; } = true;
|
||||
|
||||
/// <summary>Gets or sets the window flags.</summary>
|
||||
public ImGuiWindowFlags WindowFlags { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the popup window position.</summary>
|
||||
/// <remarks>
|
||||
/// <para>Setting the position only works before the first call to <see cref="Draw"/>.</para>
|
||||
/// <para>If any of the coordinates are <see cref="float.NaN"/>, default position will be used.</para>
|
||||
/// <para>The position will be clamped into the work area of the selected monitor.</para>
|
||||
/// </remarks>
|
||||
public Vector2 PopupPosition
|
||||
{
|
||||
get => this.popupPosition;
|
||||
set
|
||||
{
|
||||
this.popupPositionChanged = true;
|
||||
this.popupPosition = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the popup window size.</summary>
|
||||
/// <remarks>
|
||||
/// <para>Setting the size only works before the first call to <see cref="Draw"/>.</para>
|
||||
/// <para>If any of the coordinates are <see cref="float.NaN"/>, default size will be used.</para>
|
||||
/// <para>The size will be clamped into the work area of the selected monitor.</para>
|
||||
/// </remarks>
|
||||
public Vector2 PopupSize
|
||||
{
|
||||
get => this.popupSize;
|
||||
set
|
||||
{
|
||||
this.popupSizeChanged = true;
|
||||
this.popupSize = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates a new instance of <see cref="SingleFontChooserDialog"/> that will automatically draw and
|
||||
/// dispose itself as needed; calling <see cref="Draw"/> and <see cref="Dispose"/> are handled automatically.
|
||||
/// </summary>
|
||||
/// <param name="uiBuilder">An instance of <see cref="UiBuilder"/>.</param>
|
||||
/// <returns>The new instance of <see cref="SingleFontChooserDialog"/>.</returns>
|
||||
public static SingleFontChooserDialog CreateAuto(UiBuilder uiBuilder)
|
||||
{
|
||||
var fcd = new SingleFontChooserDialog(uiBuilder.CreateFontAtlas(FontAtlasAutoRebuildMode.Async));
|
||||
var fcd = new SingleFontChooserDialog(uiBuilder);
|
||||
uiBuilder.Draw += fcd.Draw;
|
||||
fcd.tcs.Task.ContinueWith(
|
||||
r =>
|
||||
|
|
@ -187,6 +274,14 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
return fcd;
|
||||
}
|
||||
|
||||
/// <summary>Gets the default popup size before clamping to monitor work area.</summary>
|
||||
/// <returns>The default popup size.</returns>
|
||||
public static Vector2 GetDefaultPopupSizeNonClamped()
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
return new Vector2(40, 30) * ImGui.GetTextLineHeight();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
@ -204,13 +299,28 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
ImGui.GetIO().WantTextInput = false;
|
||||
}
|
||||
|
||||
/// <summary>Sets <see cref="PopupSize"/> and <see cref="PopupPosition"/> to be at the center of the current window
|
||||
/// being drawn.</summary>
|
||||
/// <param name="preferredPopupSize">The preferred popup size.</param>
|
||||
public void SetPopupPositionAndSizeToCurrentWindowCenter(Vector2 preferredPopupSize)
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
this.PopupSize = preferredPopupSize;
|
||||
this.PopupPosition = ImGui.GetWindowPos() + ((ImGui.GetWindowSize() - preferredPopupSize) / 2);
|
||||
}
|
||||
|
||||
/// <summary>Sets <see cref="PopupSize"/> and <see cref="PopupPosition"/> to be at the center of the current window
|
||||
/// being drawn.</summary>
|
||||
public void SetPopupPositionAndSizeToCurrentWindowCenter() =>
|
||||
this.SetPopupPositionAndSizeToCurrentWindowCenter(GetDefaultPopupSizeNonClamped());
|
||||
|
||||
/// <summary>
|
||||
/// Draws this dialog.
|
||||
/// </summary>
|
||||
public void Draw()
|
||||
{
|
||||
if (this.firstDraw)
|
||||
ImGui.OpenPopup(this.popupImGuiName);
|
||||
const float popupMinWidth = 320;
|
||||
const float popupMinHeight = 240;
|
||||
|
||||
ImGui.GetIO().WantCaptureKeyboard = true;
|
||||
ImGui.GetIO().WantTextInput = true;
|
||||
|
|
@ -220,12 +330,70 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
var open = true;
|
||||
ImGui.SetNextWindowSize(new(640, 480), ImGuiCond.Appearing);
|
||||
if (!ImGui.BeginPopupModal(this.popupImGuiName, ref open) || !open)
|
||||
if (this.firstDraw)
|
||||
{
|
||||
this.Cancel();
|
||||
return;
|
||||
if (this.IsModal)
|
||||
ImGui.OpenPopup(this.popupImGuiName);
|
||||
}
|
||||
|
||||
if (this.firstDraw || this.popupPositionChanged || this.popupSizeChanged)
|
||||
{
|
||||
var preferProvidedSize = !float.IsNaN(this.popupSize.X) && !float.IsNaN(this.popupSize.Y);
|
||||
var size = preferProvidedSize ? this.popupSize : GetDefaultPopupSizeNonClamped();
|
||||
size.X = Math.Max(size.X, popupMinWidth);
|
||||
size.Y = Math.Max(size.Y, popupMinHeight);
|
||||
|
||||
var preferProvidedPos = !float.IsNaN(this.popupPosition.X) && !float.IsNaN(this.popupPosition.Y);
|
||||
var monitorLocatorPos = preferProvidedPos ? this.popupPosition + (size / 2) : ImGui.GetMousePos();
|
||||
|
||||
var monitors = ImGui.GetPlatformIO().Monitors;
|
||||
var preferredMonitor = 0;
|
||||
var preferredDistance = GetDistanceFromMonitor(monitorLocatorPos, monitors[0]);
|
||||
for (var i = 1; i < monitors.Size; i++)
|
||||
{
|
||||
var distance = GetDistanceFromMonitor(monitorLocatorPos, monitors[i]);
|
||||
if (distance < preferredDistance)
|
||||
{
|
||||
preferredMonitor = i;
|
||||
preferredDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
var lt = monitors[preferredMonitor].WorkPos;
|
||||
var workSize = monitors[preferredMonitor].WorkSize;
|
||||
size.X = Math.Min(size.X, workSize.X);
|
||||
size.Y = Math.Min(size.Y, workSize.Y);
|
||||
var rb = (lt + workSize) - size;
|
||||
|
||||
var pos =
|
||||
preferProvidedPos
|
||||
? new(Math.Clamp(this.PopupPosition.X, lt.X, rb.X), Math.Clamp(this.PopupPosition.Y, lt.Y, rb.Y))
|
||||
: (lt + rb) / 2;
|
||||
|
||||
ImGui.SetNextWindowSize(size, ImGuiCond.Always);
|
||||
ImGui.SetNextWindowPos(pos, ImGuiCond.Always);
|
||||
this.popupPositionChanged = this.popupSizeChanged = false;
|
||||
}
|
||||
|
||||
ImGui.SetNextWindowSizeConstraints(new(popupMinWidth, popupMinHeight), new(float.MaxValue));
|
||||
if (this.IsModal)
|
||||
{
|
||||
var open = true;
|
||||
if (!ImGui.BeginPopupModal(this.popupImGuiName, ref open, this.WindowFlags) || !open)
|
||||
{
|
||||
this.Cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var open = true;
|
||||
if (!ImGui.Begin(this.popupImGuiName, ref open, this.WindowFlags) || !open)
|
||||
{
|
||||
ImGui.End();
|
||||
this.Cancel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var framePad = ImGui.GetStyle().FramePadding;
|
||||
|
|
@ -261,12 +429,36 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
|
||||
ImGui.EndChild();
|
||||
|
||||
ImGui.EndPopup();
|
||||
this.popupPosition = ImGui.GetWindowPos();
|
||||
this.popupSize = ImGui.GetWindowSize();
|
||||
if (this.IsModal)
|
||||
ImGui.EndPopup();
|
||||
else
|
||||
ImGui.End();
|
||||
|
||||
this.firstDraw = false;
|
||||
this.firstDrawAfterRefresh = false;
|
||||
}
|
||||
|
||||
private static float GetDistanceFromMonitor(Vector2 point, ImGuiPlatformMonitorPtr monitor)
|
||||
{
|
||||
var lt = monitor.MainPos;
|
||||
var rb = monitor.MainPos + monitor.MainSize;
|
||||
var xoff =
|
||||
point.X < lt.X
|
||||
? lt.X - point.X
|
||||
: point.X > rb.X
|
||||
? point.X - rb.X
|
||||
: 0;
|
||||
var yoff =
|
||||
point.Y < lt.Y
|
||||
? lt.Y - point.Y
|
||||
: point.Y > rb.Y
|
||||
? point.Y - rb.Y
|
||||
: 0;
|
||||
return MathF.Sqrt((xoff * xoff) + (yoff * yoff));
|
||||
}
|
||||
|
||||
private void DrawChoices()
|
||||
{
|
||||
var lineHeight = ImGui.GetTextLineHeight();
|
||||
|
|
@ -338,15 +530,20 @@ public sealed class SingleFontChooserDialog : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
if (this.IgnorePreviewGlobalScale)
|
||||
if (this.fontHandle is null)
|
||||
{
|
||||
this.fontHandle ??= this.selectedFont.CreateFontHandle(
|
||||
this.atlas,
|
||||
tk => tk.OnPreBuild(e => e.SetFontScaleMode(e.Font, FontScaleMode.UndoGlobalScale)));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.fontHandle ??= this.selectedFont.CreateFontHandle(this.atlas);
|
||||
if (this.IgnorePreviewGlobalScale)
|
||||
{
|
||||
this.fontHandle = this.selectedFont.CreateFontHandle(
|
||||
this.atlas,
|
||||
tk => tk.OnPreBuild(e => e.SetFontScaleMode(e.Font, FontScaleMode.UndoGlobalScale)));
|
||||
}
|
||||
else
|
||||
{
|
||||
this.fontHandle = this.selectedFont.CreateFontHandle(this.atlas);
|
||||
}
|
||||
|
||||
this.SelectedFontSpecChanged?.InvokeSafely(this.selectedFont);
|
||||
}
|
||||
|
||||
if (this.fontHandle is null)
|
||||
|
|
|
|||
|
|
@ -90,11 +90,6 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
this.Size = new Vector2(500, 400);
|
||||
this.SizeCondition = ImGuiCond.FirstUseEver;
|
||||
|
||||
this.SizeConstraints = new WindowSizeConstraints
|
||||
{
|
||||
MinimumSize = new Vector2(600.0f, 200.0f),
|
||||
};
|
||||
|
||||
this.RespectCloseHotkey = false;
|
||||
|
||||
this.logLinesLimit = configuration.LogLinesLimit;
|
||||
|
|
@ -555,10 +550,24 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
if (ImGui.IsItemHovered()) ImGui.SetTooltip("Kill game");
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX(
|
||||
ImGui.GetContentRegionMax().X - (2 * 200.0f * ImGuiHelpers.GlobalScale) - ImGui.GetStyle().ItemSpacing.X);
|
||||
|
||||
ImGui.PushItemWidth(200.0f * ImGuiHelpers.GlobalScale);
|
||||
var inputWidth = 200.0f * ImGuiHelpers.GlobalScale;
|
||||
var nextCursorPosX = ImGui.GetContentRegionMax().X - (2 * inputWidth) - ImGui.GetStyle().ItemSpacing.X;
|
||||
var breakInputLines = nextCursorPosX < 0;
|
||||
if (ImGui.GetCursorPosX() > nextCursorPosX)
|
||||
{
|
||||
ImGui.NewLine();
|
||||
inputWidth = ImGui.GetWindowWidth() - (ImGui.GetStyle().WindowPadding.X * 2);
|
||||
|
||||
if (!breakInputLines)
|
||||
inputWidth = (inputWidth - ImGui.GetStyle().ItemSpacing.X) / 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.SetCursorPosX(nextCursorPosX);
|
||||
}
|
||||
|
||||
ImGui.PushItemWidth(inputWidth);
|
||||
if (ImGui.InputTextWithHint(
|
||||
"##textHighlight",
|
||||
"regex highlight",
|
||||
|
|
@ -583,8 +592,10 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
log.HighlightMatches = null;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.PushItemWidth(200.0f * ImGuiHelpers.GlobalScale);
|
||||
if (!breakInputLines)
|
||||
ImGui.SameLine();
|
||||
|
||||
ImGui.PushItemWidth(inputWidth);
|
||||
if (ImGui.InputTextWithHint(
|
||||
"##textFilter",
|
||||
"regex global filter",
|
||||
|
|
@ -1082,6 +1093,8 @@ internal class ConsoleWindow : Window, IDisposable
|
|||
lastc = currc;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.Dummy(screenPos - ImGui.GetCursorScreenPos());
|
||||
}
|
||||
|
||||
private record LogEntry
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
|||
private bool useBold;
|
||||
private bool useMinimumBuild;
|
||||
|
||||
private SingleFontChooserDialog? chooserDialog;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string[]? CommandShortcuts { get; init; }
|
||||
|
||||
|
|
@ -126,32 +128,75 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
|||
if (ImGui.Button("Test Lock"))
|
||||
Task.Run(this.TestLock);
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Choose Editor Font"))
|
||||
{
|
||||
var fcd = new SingleFontChooserDialog(
|
||||
Service<FontAtlasFactory>.Get().CreateFontAtlas(
|
||||
$"{nameof(GamePrebakedFontsTestWidget)}:EditorFont",
|
||||
FontAtlasAutoRebuildMode.Async));
|
||||
fcd.SelectedFont = this.fontSpec;
|
||||
fcd.IgnorePreviewGlobalScale = !this.atlasScaleMode;
|
||||
Service<InterfaceManager>.Get().Draw += fcd.Draw;
|
||||
fcd.ResultTask.ContinueWith(
|
||||
r => Service<Framework>.Get().RunOnFrameworkThread(
|
||||
() =>
|
||||
{
|
||||
Service<InterfaceManager>.Get().Draw -= fcd.Draw;
|
||||
fcd.Dispose();
|
||||
if (this.chooserDialog is null)
|
||||
{
|
||||
DoNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.chooserDialog.Cancel();
|
||||
this.chooserDialog.ResultTask.ContinueWith(_ => Service<Framework>.Get().RunOnFrameworkThread(DoNext));
|
||||
this.chooserDialog = null;
|
||||
}
|
||||
|
||||
_ = r.Exception;
|
||||
if (!r.IsCompletedSuccessfully)
|
||||
return;
|
||||
void DoNext()
|
||||
{
|
||||
var fcd = new SingleFontChooserDialog(
|
||||
Service<FontAtlasFactory>.Get(),
|
||||
$"{nameof(GamePrebakedFontsTestWidget)}:EditorFont");
|
||||
this.chooserDialog = fcd;
|
||||
fcd.SelectedFont = this.fontSpec;
|
||||
fcd.IgnorePreviewGlobalScale = !this.atlasScaleMode;
|
||||
fcd.IsModal = false;
|
||||
Service<InterfaceManager>.Get().Draw += fcd.Draw;
|
||||
var prevSpec = this.fontSpec;
|
||||
fcd.SelectedFontSpecChanged += spec =>
|
||||
{
|
||||
this.fontSpec = spec;
|
||||
Log.Information("Selected font: {font}", this.fontSpec);
|
||||
this.fontDialogHandle?.Dispose();
|
||||
this.fontDialogHandle = null;
|
||||
};
|
||||
fcd.ResultTask.ContinueWith(
|
||||
r => Service<Framework>.Get().RunOnFrameworkThread(
|
||||
() =>
|
||||
{
|
||||
Service<InterfaceManager>.Get().Draw -= fcd.Draw;
|
||||
fcd.Dispose();
|
||||
|
||||
this.fontSpec = r.Result;
|
||||
Log.Information("Selected font: {font}", this.fontSpec);
|
||||
this.fontDialogHandle?.Dispose();
|
||||
this.fontDialogHandle = null;
|
||||
}));
|
||||
_ = r.Exception;
|
||||
var spec = r.IsCompletedSuccessfully ? r.Result : prevSpec;
|
||||
if (this.fontSpec != spec)
|
||||
{
|
||||
this.fontSpec = spec;
|
||||
this.fontDialogHandle?.Dispose();
|
||||
this.fontDialogHandle = null;
|
||||
}
|
||||
|
||||
this.chooserDialog = null;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (this.chooserDialog is not null)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted($"{this.chooserDialog.PopupPosition}, {this.chooserDialog.PopupSize}");
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Random Location"))
|
||||
{
|
||||
var monitors = ImGui.GetPlatformIO().Monitors;
|
||||
var monitor = monitors[Random.Shared.Next() % monitors.Size];
|
||||
this.chooserDialog.PopupPosition = monitor.WorkPos + (monitor.WorkSize * new Vector2(
|
||||
Random.Shared.NextSingle(),
|
||||
Random.Shared.NextSingle()));
|
||||
this.chooserDialog.PopupSize = monitor.WorkSize * new Vector2(
|
||||
Random.Shared.NextSingle(),
|
||||
Random.Shared.NextSingle());
|
||||
}
|
||||
}
|
||||
|
||||
this.privateAtlas ??=
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
// ReSharper disable MethodSupportsCancellation // Using alternative method of cancelling tasks by throwing exceptions.
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
using Serilog;
|
||||
|
||||
|
|
@ -18,6 +27,12 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
/// </summary>
|
||||
internal class TaskSchedulerWidget : IDataWindowWidget
|
||||
{
|
||||
private readonly FileDialogManager fileDialogManager = new();
|
||||
private readonly byte[] urlBytes = new byte[2048];
|
||||
private readonly byte[] localPathBytes = new byte[2048];
|
||||
|
||||
private Task? downloadTask = null;
|
||||
private (long Downloaded, long Total, float Percentage) downloadState;
|
||||
private CancellationTokenSource taskSchedulerCancelSource = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -33,11 +48,16 @@ internal class TaskSchedulerWidget : IDataWindowWidget
|
|||
public void Load()
|
||||
{
|
||||
this.Ready = true;
|
||||
Encoding.UTF8.GetBytes(
|
||||
"https://geo.mirror.pkgbuild.com/iso/2024.01.01/archlinux-2024.01.01-x86_64.iso",
|
||||
this.urlBytes);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Draw()
|
||||
{
|
||||
var framework = Service<Framework>.Get();
|
||||
|
||||
if (ImGui.Button("Clear list"))
|
||||
{
|
||||
TaskTracker.Clear();
|
||||
|
|
@ -84,8 +104,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget
|
|||
{
|
||||
Thread.Sleep(200);
|
||||
|
||||
string a = null;
|
||||
a.Contains("dalamud"); // Intentional null exception.
|
||||
_ = ((string)null)!.Contains("dalamud"); // Intentional null exception.
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -94,36 +113,156 @@ internal class TaskSchedulerWidget : IDataWindowWidget
|
|||
|
||||
if (ImGui.Button("ASAP"))
|
||||
{
|
||||
Task.Run(async () => await Service<Framework>.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedulerCancelSource.Token));
|
||||
_ = framework.RunOnTick(() => Log.Information("Framework.Update - ASAP"), cancellationToken: this.taskSchedulerCancelSource.Token);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGui.Button("In 1s"))
|
||||
{
|
||||
Task.Run(async () => await Service<Framework>.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1)));
|
||||
_ = framework.RunOnTick(() => Log.Information("Framework.Update - In 1s"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGui.Button("In 60f"))
|
||||
{
|
||||
Task.Run(async () => await Service<Framework>.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedulerCancelSource.Token, delayTicks: 60));
|
||||
_ = framework.RunOnTick(() => Log.Information("Framework.Update - In 60f"), cancellationToken: this.taskSchedulerCancelSource.Token, delayTicks: 60);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGui.Button("In 1s+120f"))
|
||||
{
|
||||
_ = framework.RunOnTick(() => Log.Information("Framework.Update - In 1s+120f"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1), delayTicks: 120);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGui.Button("In 2s+60f"))
|
||||
{
|
||||
_ = framework.RunOnTick(() => Log.Information("Framework.Update - In 2s+60f"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(2), delayTicks: 60);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGui.Button("Every 60 frames"))
|
||||
{
|
||||
_ = framework.RunOnTick(
|
||||
async () =>
|
||||
{
|
||||
for (var i = 0L; ; i++)
|
||||
{
|
||||
Log.Information($"Loop #{i}; MainThread={ThreadSafety.IsMainThread}");
|
||||
await framework.DelayTicks(60, this.taskSchedulerCancelSource.Token);
|
||||
}
|
||||
},
|
||||
cancellationToken: this.taskSchedulerCancelSource.Token);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGui.Button("Error in 1s"))
|
||||
{
|
||||
Task.Run(async () => await Service<Framework>.Get().RunOnTick(() => throw new Exception("Test Exception"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1)));
|
||||
_ = framework.RunOnTick(() => throw new Exception("Test Exception"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGui.Button("As long as it's in Framework Thread"))
|
||||
{
|
||||
Task.Run(async () => await Service<Framework>.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from non-framework.update thread"); }));
|
||||
Service<Framework>.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from framework.update thread"); }).Wait();
|
||||
Task.Run(async () => await framework.RunOnFrameworkThread(() => { Log.Information("Task dispatched from non-framework.update thread"); }));
|
||||
framework.RunOnFrameworkThread(() => { Log.Information("Task dispatched from framework.update thread"); }).Wait();
|
||||
}
|
||||
|
||||
if (ImGui.CollapsingHeader("Download"))
|
||||
{
|
||||
ImGui.InputText("URL", this.urlBytes, (uint)this.urlBytes.Length);
|
||||
ImGui.InputText("Local Path", this.localPathBytes, (uint)this.localPathBytes.Length);
|
||||
ImGui.SameLine();
|
||||
|
||||
if (ImGuiComponents.IconButton("##localpathpicker", FontAwesomeIcon.File))
|
||||
{
|
||||
var defaultFileName = Encoding.UTF8.GetString(this.urlBytes).Split('\0', 2)[0].Split('/').Last();
|
||||
this.fileDialogManager.SaveFileDialog(
|
||||
"Choose a local path",
|
||||
"*",
|
||||
defaultFileName,
|
||||
string.Empty,
|
||||
(accept, newPath) =>
|
||||
{
|
||||
if (accept)
|
||||
{
|
||||
this.localPathBytes.AsSpan().Clear();
|
||||
Encoding.UTF8.GetBytes(newPath, this.localPathBytes.AsSpan());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ImGui.TextUnformatted($"{this.downloadState.Downloaded:##,###}/{this.downloadState.Total:##,###} ({this.downloadState.Percentage:0.00}%)");
|
||||
|
||||
using var disabled =
|
||||
ImRaii.Disabled(this.downloadTask?.IsCompleted is false || this.localPathBytes[0] == 0);
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted("Download");
|
||||
ImGui.SameLine();
|
||||
var downloadUsingGlobalScheduler = ImGui.Button("using default scheduler");
|
||||
ImGui.SameLine();
|
||||
var downloadUsingFramework = ImGui.Button("using Framework.Update");
|
||||
if (downloadUsingGlobalScheduler || downloadUsingFramework)
|
||||
{
|
||||
var url = Encoding.UTF8.GetString(this.urlBytes).Split('\0', 2)[0];
|
||||
var localPath = Encoding.UTF8.GetString(this.localPathBytes).Split('\0', 2)[0];
|
||||
var ct = this.taskSchedulerCancelSource.Token;
|
||||
this.downloadState = default;
|
||||
var factory = downloadUsingGlobalScheduler
|
||||
? Task.Factory
|
||||
: framework.FrameworkThreadTaskFactory;
|
||||
this.downloadState = default;
|
||||
this.downloadTask = factory.StartNew(
|
||||
async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var to = File.Create(localPath);
|
||||
using var client = new HttpClient();
|
||||
using var conn = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct);
|
||||
this.downloadState.Total = conn.Content.Headers.ContentLength ?? -1L;
|
||||
await using var from = conn.Content.ReadAsStream(ct);
|
||||
var buffer = new byte[8192];
|
||||
while (true)
|
||||
{
|
||||
if (downloadUsingFramework)
|
||||
ThreadSafety.AssertMainThread();
|
||||
if (downloadUsingGlobalScheduler)
|
||||
ThreadSafety.AssertNotMainThread();
|
||||
var len = await from.ReadAsync(buffer, ct);
|
||||
if (len == 0)
|
||||
break;
|
||||
await to.WriteAsync(buffer.AsMemory(0, len), ct);
|
||||
this.downloadState.Downloaded += len;
|
||||
if (this.downloadState.Total >= 0)
|
||||
{
|
||||
this.downloadState.Percentage =
|
||||
(100f * this.downloadState.Downloaded) / this.downloadState.Total;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "Failed to download {from} to {to}.", url, localPath);
|
||||
try
|
||||
{
|
||||
File.Delete(localPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
},
|
||||
cancellationToken: ct).Unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.Button("Drown in tasks"))
|
||||
|
|
@ -244,6 +383,8 @@ internal class TaskSchedulerWidget : IDataWindowWidget
|
|||
|
||||
ImGui.PopStyleColor(1);
|
||||
}
|
||||
|
||||
this.fileDialogManager.Draw();
|
||||
}
|
||||
|
||||
private async Task TestTaskInTaskDelay(CancellationToken token)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ using Dalamud.Interface.GameFonts;
|
|||
using Dalamud.Interface.ImGuiFontChooserDialog;
|
||||
using Dalamud.Interface.Internal.Windows.PluginInstaller;
|
||||
using Dalamud.Interface.Internal.Windows.Settings.Widgets;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility;
|
||||
|
|
@ -199,10 +198,10 @@ public class SettingsTabLook : SettingsTab
|
|||
if (ImGui.Button(Loc.Localize("DalamudSettingChooseDefaultFont", "Choose Default Font")))
|
||||
{
|
||||
var faf = Service<FontAtlasFactory>.Get();
|
||||
var fcd = new SingleFontChooserDialog(
|
||||
faf.CreateFontAtlas($"{nameof(SettingsTabLook)}:Default", FontAtlasAutoRebuildMode.Async));
|
||||
var fcd = new SingleFontChooserDialog(faf, $"{nameof(SettingsTabLook)}:Default");
|
||||
fcd.SelectedFont = (SingleFontSpec)this.defaultFontSpec;
|
||||
fcd.FontFamilyExcludeFilter = x => x is DalamudDefaultFontAndFamilyId;
|
||||
fcd.SetPopupPositionAndSizeToCurrentWindowCenter();
|
||||
interfaceManager.Draw += fcd.Draw;
|
||||
fcd.ResultTask.ContinueWith(
|
||||
r => Service<Framework>.Get().RunOnFrameworkThread(
|
||||
|
|
|
|||
|
|
@ -82,21 +82,25 @@ public interface IFontAtlas : IDisposable
|
|||
/// </example>
|
||||
public IDisposable SuppressAutoRebuild();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="IFontHandle"/> from game's built-in fonts.
|
||||
/// </summary>
|
||||
/// <summary>Creates a new <see cref="IFontHandle"/> from game's built-in fonts.</summary>
|
||||
/// <param name="style">Font to use.</param>
|
||||
/// <returns>Handle to a font that may or may not be ready yet.</returns>
|
||||
/// <remarks>This function does not throw. <see cref="IFontHandle.LoadException"/> will be populated instead, if
|
||||
/// the build procedure has failed. <see cref="IFontHandle.Push"/> can be used regardless of the state of the font
|
||||
/// handle.</remarks>
|
||||
public IFontHandle NewGameFontHandle(GameFontStyle style);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new IFontHandle using your own callbacks.
|
||||
/// </summary>
|
||||
/// <summary>Creates a new IFontHandle using your own callbacks.</summary>
|
||||
/// <param name="buildStepDelegate">Callback for <see cref="IFontAtlas.BuildStepChange"/>.</param>
|
||||
/// <returns>Handle to a font that may or may not be ready yet.</returns>
|
||||
/// <remarks>
|
||||
/// Consider calling <see cref="IFontAtlasBuildToolkitPreBuild.AttachExtraGlyphsForDalamudLanguage"/> to support
|
||||
/// glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language users.
|
||||
/// <para>Consider calling <see cref="IFontAtlasBuildToolkitPreBuild.AttachExtraGlyphsForDalamudLanguage"/> to
|
||||
/// support glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language
|
||||
/// users.</para>
|
||||
/// <para>This function does not throw, even if <paramref name="buildStepDelegate"/> would throw exceptions.
|
||||
/// Instead, if it fails, the returned handle will contain an <see cref="IFontHandle.LoadException"/> property
|
||||
/// containing the exception happened during the build process. <see cref="IFontHandle.Push"/> can be used even if
|
||||
/// the build process has not been completed yet or failed.</para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <b>On initialization</b>:
|
||||
|
|
|
|||
|
|
@ -58,10 +58,27 @@ public interface IFontHandle : IDisposable
|
|||
/// <returns>A disposable object that will pop the font on dispose.</returns>
|
||||
/// <exception cref="InvalidOperationException">If called outside of the main thread.</exception>
|
||||
/// <remarks>
|
||||
/// This function uses <see cref="ImGui.PushFont"/>, and may do extra things.
|
||||
/// <para>This function uses <see cref="ImGui.PushFont"/>, and may do extra things.
|
||||
/// Use <see cref="IDisposable.Dispose"/> or <see cref="Pop"/> to undo this operation.
|
||||
/// Do not use <see cref="ImGui.PopFont"/>.
|
||||
/// Do not use <see cref="ImGui.PopFont"/>.</para>
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <b>Push a font with `using` clause.</b>
|
||||
/// <code>
|
||||
/// using (fontHandle.Push())
|
||||
/// ImGui.TextUnformatted("Test");
|
||||
/// </code>
|
||||
/// <b>Push a font with a matching call to <see cref="Pop"/>.</b>
|
||||
/// <code>
|
||||
/// fontHandle.Push();
|
||||
/// ImGui.TextUnformatted("Test 2");
|
||||
/// </code>
|
||||
/// <b>Push a font between two choices.</b>
|
||||
/// <code>
|
||||
/// using ((someCondition ? myFontHandle : dalamudPluginInterface.UiBuilder.MonoFontHandle).Push())
|
||||
/// ImGui.TextUnformatted("Test 3");
|
||||
/// </code>
|
||||
/// </example>
|
||||
IDisposable Push();
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -136,13 +136,18 @@ internal abstract class FontHandle : IFontHandle
|
|||
/// An instance of <see cref="ILockedImFont"/> that <b>must</b> be disposed after use on success;
|
||||
/// <c>null</c> with <paramref name="errorMessage"/> populated on failure.
|
||||
/// </returns>
|
||||
/// <exception cref="ObjectDisposedException">Still may be thrown.</exception>
|
||||
public ILockedImFont? TryLock(out string? errorMessage)
|
||||
{
|
||||
IFontHandleSubstance? prevSubstance = default;
|
||||
while (true)
|
||||
{
|
||||
var substance = this.Manager.Substance;
|
||||
if (this.manager is not { } nonDisposedManager)
|
||||
{
|
||||
errorMessage = "The font handle has been disposed.";
|
||||
return null;
|
||||
}
|
||||
|
||||
var substance = nonDisposedManager.Substance;
|
||||
|
||||
// Does the associated IFontAtlas have a built substance?
|
||||
if (substance is null)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ public interface IFramework
|
|||
/// </summary>
|
||||
public DateTime LastUpdateUTC { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="TaskFactory"/> that runs tasks during Framework Update event.
|
||||
/// </summary>
|
||||
public TaskFactory FrameworkThreadTaskFactory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the delta between the last Framework Update and the currently executing one.
|
||||
/// </summary>
|
||||
|
|
@ -44,6 +49,14 @@ public interface IFramework
|
|||
/// </summary>
|
||||
public bool IsFrameworkUnloading { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a task that completes after the given number of ticks.
|
||||
/// </summary>
|
||||
/// <param name="numTicks">Number of ticks to delay.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A new <see cref="Task"/> that gets resolved after specified number of ticks happen.</returns>
|
||||
public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call.
|
||||
/// </summary>
|
||||
|
|
@ -65,6 +78,7 @@ public interface IFramework
|
|||
/// <typeparam name="T">Return type.</typeparam>
|
||||
/// <param name="func">Function to call.</param>
|
||||
/// <returns>Task representing the pending or already completed function.</returns>
|
||||
[Obsolete($"Use {nameof(RunOnTick)} instead.")]
|
||||
public Task<T> RunOnFrameworkThread<T>(Func<Task<T>> func);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -72,6 +86,7 @@ public interface IFramework
|
|||
/// </summary>
|
||||
/// <param name="func">Function to call.</param>
|
||||
/// <returns>Task representing the pending or already completed function.</returns>
|
||||
[Obsolete($"Use {nameof(RunOnTick)} instead.")]
|
||||
public Task RunOnFrameworkThread(Func<Task> func);
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
|||
.Where(x => x is not DalamudAsset.Empty4X4)
|
||||
.Where(x => x.GetAttribute<DalamudAssetAttribute>()?.Required is false)
|
||||
.Select(this.CreateStreamAsync)
|
||||
.Select(x => x.ToContentDisposedTask()))
|
||||
.Select(x => x.ToContentDisposedTask(true)))
|
||||
.ContinueWith(r => Log.Verbose($"Optional assets load state: {r}"));
|
||||
}
|
||||
|
||||
|
|
@ -99,6 +99,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
|||
.Concat(this.fileStreams.Values)
|
||||
.Concat(this.textureWraps.Values)
|
||||
.Where(x => x is not null)
|
||||
.Select(x => x.ContinueWith(r => { _ = r.Exception; }))
|
||||
.ToArray());
|
||||
this.scopedFinalizer.Dispose();
|
||||
}
|
||||
|
|
|
|||
90
Dalamud/Utility/ThreadBoundTaskScheduler.cs
Normal file
90
Dalamud/Utility/ThreadBoundTaskScheduler.cs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Dalamud.Utility;
|
||||
|
||||
/// <summary>
|
||||
/// A task scheduler that runs tasks on a specific thread.
|
||||
/// </summary>
|
||||
internal class ThreadBoundTaskScheduler : TaskScheduler
|
||||
{
|
||||
private const byte Scheduled = 0;
|
||||
private const byte Running = 1;
|
||||
|
||||
private readonly ConcurrentDictionary<Task, byte> scheduledTasks = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ThreadBoundTaskScheduler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="boundThread">The thread to bind this task scheduelr to.</param>
|
||||
public ThreadBoundTaskScheduler(Thread? boundThread = null)
|
||||
{
|
||||
this.BoundThread = boundThread;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the thread this task scheduler is bound to.
|
||||
/// </summary>
|
||||
public Thread? BoundThread { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether we're on the bound thread.
|
||||
/// </summary>
|
||||
public bool IsOnBoundThread => Thread.CurrentThread == this.BoundThread;
|
||||
|
||||
/// <summary>
|
||||
/// Runs queued tasks.
|
||||
/// </summary>
|
||||
public void Run()
|
||||
{
|
||||
foreach (var task in this.scheduledTasks.Keys)
|
||||
{
|
||||
if (!this.scheduledTasks.TryUpdate(task, Running, Scheduled))
|
||||
continue;
|
||||
|
||||
_ = this.TryExecuteTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<Task> GetScheduledTasks()
|
||||
{
|
||||
return this.scheduledTasks.Keys;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void QueueTask(Task task)
|
||||
{
|
||||
this.scheduledTasks[task] = Scheduled;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool TryDequeue(Task task)
|
||||
{
|
||||
if (!this.scheduledTasks.TryRemove(task, out _))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
|
||||
{
|
||||
if (!this.IsOnBoundThread)
|
||||
return false;
|
||||
|
||||
if (taskWasPreviouslyQueued && !this.scheduledTasks.TryUpdate(task, Running, Scheduled))
|
||||
return false;
|
||||
|
||||
_ = this.TryExecuteTask(task);
|
||||
return true;
|
||||
}
|
||||
|
||||
private new bool TryExecuteTask(Task task)
|
||||
{
|
||||
var r = base.TryExecuteTask(task);
|
||||
this.scheduledTasks.Remove(task, out _);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue