using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Game.Gui;
using Dalamud.Game.Gui.Toast;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Utility;
using Serilog;
namespace Dalamud.Game;
///
/// This class represents the Framework of the native game client and grants access to various subsystems.
///
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class Framework : IDisposable, IServiceType
{
private readonly GameLifecycle lifecycle;
private static Stopwatch statsStopwatch = new();
private readonly Stopwatch updateStopwatch = new();
private readonly HitchDetector hitchDetector;
private readonly Hook updateHook;
private readonly Hook destroyHook;
private readonly object runOnNextTickTaskListSync = new();
private List runOnNextTickTaskList = new();
private List runOnNextTickTaskList2 = new();
private Thread? frameworkUpdateThread;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service.Get();
[ServiceManager.ServiceConstructor]
private Framework(SigScanner sigScanner, GameLifecycle lifecycle)
{
this.lifecycle = lifecycle;
this.hitchDetector = new HitchDetector("FrameworkUpdate", this.configuration.FrameworkUpdateHitch);
this.Address = new FrameworkAddressResolver();
this.Address.Setup(sigScanner);
this.updateHook = Hook.FromAddress(this.Address.TickAddress, this.HandleFrameworkUpdate);
this.destroyHook = Hook.FromAddress(this.Address.DestroyAddress, this.HandleFrameworkDestroy);
}
///
/// A delegate type used with the event.
///
/// The Framework instance.
public delegate void OnUpdateDelegate(Framework framework);
///
/// A delegate type used during the native Framework::destroy.
///
/// The native Framework address.
/// A value indicating if the call was successful.
public delegate bool OnRealDestroyDelegate(IntPtr framework);
///
/// A delegate type used during the native Framework::free.
///
/// The native Framework address.
public delegate IntPtr OnDestroyDelegate();
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate bool OnUpdateDetour(IntPtr framework);
private delegate IntPtr OnDestroyDetour(); // OnDestroyDelegate
///
/// Event that gets fired every time the game framework updates.
///
public event OnUpdateDelegate Update;
///
/// Gets or sets a value indicating whether the collection of stats is enabled.
///
public static bool StatsEnabled { get; set; }
///
/// Gets the stats history mapping.
///
public static Dictionary> StatsHistory { get; } = new();
///
/// Gets a raw pointer to the instance of Client::Framework.
///
public FrameworkAddressResolver Address { get; }
///
/// Gets the last time that the Framework Update event was triggered.
///
public DateTime LastUpdate { get; private set; } = DateTime.MinValue;
///
/// Gets the last time in UTC that the Framework Update event was triggered.
///
public DateTime LastUpdateUTC { get; private set; } = DateTime.MinValue;
///
/// Gets the delta between the last Framework Update and the currently executing one.
///
public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero;
///
/// Gets a value indicating whether currently executing code is running in the game's framework update thread.
///
public bool IsInFrameworkUpdateThread => Thread.CurrentThread == this.frameworkUpdateThread;
///
/// Gets a value indicating whether game Framework is unloading.
///
public bool IsFrameworkUnloading { get; internal set; }
///
/// Gets or sets a value indicating whether to dispatch update events.
///
internal bool DispatchUpdateEvents { get; set; } = true;
///
/// 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.
///
/// Return type.
/// Function to call.
/// Task representing the pending or already completed function.
public Task RunOnFrameworkThread(Func func) =>
this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? Task.FromResult(func()) : this.RunOnTick(func);
///
/// 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.
///
/// Function to call.
/// Task representing the pending or already completed function.
public Task RunOnFrameworkThread(Action action)
{
if (this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading)
{
try
{
action();
return Task.CompletedTask;
}
catch (Exception ex)
{
return Task.FromException(ex);
}
}
else
{
return this.RunOnTick(action);
}
}
///
/// 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.
///
/// Return type.
/// Function to call.
/// Task representing the pending or already completed function.
public Task RunOnFrameworkThread(Func> func) =>
this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? func() : this.RunOnTick(func);
///
/// 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.
///
/// Function to call.
/// Task representing the pending or already completed function.
public Task RunOnFrameworkThread(Func func) =>
this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? func() : this.RunOnTick(func);
///
/// Run given function in upcoming Framework.Tick call.
///
/// Return type.
/// Function to call.
/// Wait for given timespan before calling this function.
/// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter.
/// Cancellation token which will prevent the execution of this function if wait conditions are not met.
/// Task representing the pending function.
public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
{
if (this.IsFrameworkUnloading)
{
if (delay == default && delayTicks == default)
return this.RunOnFrameworkThread(func);
var cts = new CancellationTokenSource();
cts.Cancel();
return Task.FromCanceled(cts.Token);
}
var tcs = new TaskCompletionSource();
lock (this.runOnNextTickTaskListSync)
{
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc()
{
RemainingTicks = delayTicks,
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
CancellationToken = cancellationToken,
TaskCompletionSource = tcs,
Func = func,
});
}
return tcs.Task;
}
///
/// Run given function in upcoming Framework.Tick call.
///
/// Function to call.
/// Wait for given timespan before calling this function.
/// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter.
/// Cancellation token which will prevent the execution of this function if wait conditions are not met.
/// Task representing the pending function.
public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
{
if (this.IsFrameworkUnloading)
{
if (delay == default && delayTicks == default)
return this.RunOnFrameworkThread(action);
var cts = new CancellationTokenSource();
cts.Cancel();
return Task.FromCanceled(cts.Token);
}
var tcs = new TaskCompletionSource();
lock (this.runOnNextTickTaskListSync)
{
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskAction()
{
RemainingTicks = delayTicks,
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
CancellationToken = cancellationToken,
TaskCompletionSource = tcs,
Action = action,
});
}
return tcs.Task;
}
///
/// Run given function in upcoming Framework.Tick call.
///
/// Return type.
/// Function to call.
/// Wait for given timespan before calling this function.
/// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter.
/// Cancellation token which will prevent the execution of this function if wait conditions are not met.
/// Task representing the pending function.
public Task RunOnTick(Func> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
{
if (this.IsFrameworkUnloading)
{
if (delay == default && delayTicks == default)
return this.RunOnFrameworkThread(func);
var cts = new CancellationTokenSource();
cts.Cancel();
return Task.FromCanceled(cts.Token);
}
var tcs = new TaskCompletionSource>();
lock (this.runOnNextTickTaskListSync)
{
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc>()
{
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();
}
///
/// Run given function in upcoming Framework.Tick call.
///
/// Function to call.
/// Wait for given timespan before calling this function.
/// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter.
/// Cancellation token which will prevent the execution of this function if wait conditions are not met.
/// Task representing the pending function.
public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
{
if (this.IsFrameworkUnloading)
{
if (delay == default && delayTicks == default)
return this.RunOnFrameworkThread(func);
var cts = new CancellationTokenSource();
cts.Cancel();
return Task.FromCanceled(cts.Token);
}
var tcs = new TaskCompletionSource();
lock (this.runOnNextTickTaskListSync)
{
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc()
{
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();
}
///
/// Dispose of managed and unmanaged resources.
///
void IDisposable.Dispose()
{
this.RunOnFrameworkThread(() =>
{
// ReSharper disable once AccessToDisposedClosure
this.updateHook.Disable();
// ReSharper disable once AccessToDisposedClosure
this.destroyHook.Disable();
}).Wait();
this.updateHook.Dispose();
this.destroyHook.Dispose();
this.updateStopwatch.Reset();
statsStopwatch.Reset();
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
this.updateHook.Enable();
this.destroyHook.Enable();
}
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;
ThreadSafety.MarkMainThread();
this.hitchDetector.Start();
try
{
var chatGui = Service.GetNullable();
var toastGui = Service.GetNullable();
var config = Service.GetNullable();
if (chatGui == null || toastGui == null)
goto original;
chatGui.UpdateQueue();
toastGui.UpdateQueue();
config?.Update();
}
catch (Exception ex)
{
Log.Error(ex, "Exception while handling Framework::Update hook.");
}
if (this.DispatchUpdateEvents)
{
this.updateStopwatch.Stop();
this.UpdateDelta = TimeSpan.FromMilliseconds(this.updateStopwatch.ElapsedMilliseconds);
this.updateStopwatch.Restart();
this.LastUpdate = DateTime.Now;
this.LastUpdateUTC = DateTime.UtcNow;
void AddToStats(string key, double ms)
{
if (!StatsHistory.ContainsKey(key))
StatsHistory.Add(key, new List());
StatsHistory[key].Add(ms);
if (StatsHistory[key].Count > 1000)
{
StatsHistory[key].RemoveRange(0, StatsHistory[key].Count - 1000);
}
}
if (StatsEnabled)
{
statsStopwatch.Restart();
this.RunPendingTickTasks();
statsStopwatch.Stop();
AddToStats(nameof(this.RunPendingTickTasks), statsStopwatch.Elapsed.TotalMilliseconds);
}
else
{
this.RunPendingTickTasks();
}
if (StatsEnabled && this.Update != null)
{
// Stat Tracking for Framework Updates
var invokeList = this.Update.GetInvocationList();
var notUpdated = StatsHistory.Keys.ToList();
// Individually invoke OnUpdate handlers and time them.
foreach (var d in invokeList)
{
statsStopwatch.Restart();
try
{
d.Method.Invoke(d.Target, new object[] { this });
}
catch (Exception ex)
{
Log.Error(ex, "Exception while dispatching Framework::Update event.");
}
statsStopwatch.Stop();
var key = $"{d.Target}::{d.Method.Name}";
if (notUpdated.Contains(key))
notUpdated.Remove(key);
AddToStats(key, statsStopwatch.Elapsed.TotalMilliseconds);
}
// Cleanup handlers that are no longer being called
foreach (var key in notUpdated)
{
if (key == nameof(this.RunPendingTickTasks))
continue;
if (StatsHistory[key].Count > 0)
{
StatsHistory[key].RemoveAt(0);
}
else
{
StatsHistory.Remove(key);
}
}
}
else
{
this.Update?.InvokeSafely(this);
}
}
this.hitchDetector.Stop();
original:
return this.updateHook.OriginalDisposeSafe(framework);
}
private bool HandleFrameworkDestroy(IntPtr framework)
{
this.IsFrameworkUnloading = true;
this.DispatchUpdateEvents = false;
// All the same, for now...
this.lifecycle.SetShuttingDown();
this.lifecycle.SetUnloading();
Log.Information("Framework::Destroy!");
Service.Get().Unload();
this.RunPendingTickTasks();
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 : RunOnNextTickTaskBase
{
internal TaskCompletionSource TaskCompletionSource { get; init; }
internal Func 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();
}
}
}