Improvements (#903)

This commit is contained in:
kizer 2022-06-29 18:51:40 +09:00 committed by GitHub
parent e9cd7e0273
commit 716736f022
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 1809 additions and 872 deletions

View file

@ -403,6 +403,96 @@ void xivfixes::redirect_openprocess(bool bApply) {
} }
} }
void xivfixes::backup_userdata_save(bool bApply) {
static const char* LogTag = "[xivfixes:backup_userdata_save]";
static std::optional<hooks::import_hook<decltype(CreateFileW)>> s_hookCreateFileW;
static std::optional<hooks::import_hook<decltype(CloseHandle)>> s_hookCloseHandle;
static std::map<HANDLE, std::pair<std::filesystem::path, std::filesystem::path>> s_handles;
static std::mutex s_mtx;
if (bApply) {
if (!g_startInfo.BootEnabledGameFixes.contains("backup_userdata_save")) {
logging::I("{} Turned off via environment variable.", LogTag);
return;
}
s_hookCreateFileW.emplace("kernel32.dll!CreateFileW (import, backup_userdata_save)", "kernel32.dll", "CreateFileW", 0);
s_hookCloseHandle.emplace("kernel32.dll!CloseHandle (import, backup_userdata_save)", "kernel32.dll", "CloseHandle", 0);
s_hookCreateFileW->set_detour([](LPCWSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile)->HANDLE {
if (dwDesiredAccess != GENERIC_WRITE)
return s_hookCreateFileW->call_original(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
auto path = std::filesystem::path(lpFileName);
const auto ext = unicode::convert<std::string>(path.extension().wstring(), &unicode::lower);
if (ext != ".dat" && ext != ".cfg")
return s_hookCreateFileW->call_original(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
std::filesystem::path temporaryPath = path;
temporaryPath.replace_extension(path.extension().wstring() + L".new");
const auto handle = s_hookCreateFileW->call_original(temporaryPath.c_str(), GENERIC_READ | GENERIC_WRITE | DELETE, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile);
if (handle == INVALID_HANDLE_VALUE)
return handle;
const auto lock = std::lock_guard(s_mtx);
s_handles.try_emplace(handle, std::move(temporaryPath), std::move(path));
return handle;
});
s_hookCloseHandle->set_detour([](HANDLE handle) {
const auto lock = std::lock_guard(s_mtx);
if (const auto it = s_handles.find(handle); it != s_handles.end()) {
std::filesystem::path tempPath(std::move(it->second.first));
std::filesystem::path finalPath(std::move(it->second.second));
s_handles.erase(it);
if (exists(finalPath)) {
std::filesystem::path oldPath = finalPath;
oldPath.replace_extension(finalPath.extension().wstring() + L".old");
try {
rename(finalPath, oldPath);
} catch (const std::exception& e) {
logging::E("{0} Failed to rename {1} to {2}: {3}",
LogTag,
unicode::convert<std::string>(finalPath.c_str()),
unicode::convert<std::string>(oldPath.c_str()),
e.what());
}
}
const auto pathwstr = finalPath.wstring();
std::vector<char> renameInfoBuf(sizeof(FILE_RENAME_INFO) + sizeof(wchar_t) * pathwstr.size() + 2);
auto& renameInfo = *reinterpret_cast<FILE_RENAME_INFO*>(&renameInfoBuf[0]);
renameInfo.ReplaceIfExists = true;
renameInfo.FileNameLength = static_cast<DWORD>(pathwstr.size() * 2);
memcpy(renameInfo.FileName, &pathwstr[0], renameInfo.FileNameLength);
if (!SetFileInformationByHandle(handle, FileRenameInfo, &renameInfoBuf[0], static_cast<DWORD>(renameInfoBuf.size()))) {
logging::E("{0} Failed to rename {1} to {2}: Win32 error {3}(0x{3})",
LogTag,
unicode::convert<std::string>(tempPath.c_str()),
unicode::convert<std::string>(finalPath.c_str()),
GetLastError());
}
}
return s_hookCloseHandle->call_original(handle);
});
logging::I("{} Enable", LogTag);
} else {
if (s_hookCreateFileW) {
logging::I("{} Disable OpenProcess", LogTag);
s_hookCreateFileW.reset();
}
}
}
void xivfixes::apply_all(bool bApply) { void xivfixes::apply_all(bool bApply) {
for (const auto& [taskName, taskFunction] : std::initializer_list<std::pair<const char*, void(*)(bool)>> for (const auto& [taskName, taskFunction] : std::initializer_list<std::pair<const char*, void(*)(bool)>>
{ {
@ -410,6 +500,7 @@ void xivfixes::apply_all(bool bApply) {
{ "prevent_devicechange_crashes", &prevent_devicechange_crashes }, { "prevent_devicechange_crashes", &prevent_devicechange_crashes },
{ "disable_game_openprocess_access_check", &disable_game_openprocess_access_check }, { "disable_game_openprocess_access_check", &disable_game_openprocess_access_check },
{ "redirect_openprocess", &redirect_openprocess }, { "redirect_openprocess", &redirect_openprocess },
{ "backup_userdata_save", &backup_userdata_save },
} }
) { ) {
try { try {

View file

@ -5,6 +5,7 @@ namespace xivfixes {
void prevent_devicechange_crashes(bool bApply); void prevent_devicechange_crashes(bool bApply);
void disable_game_openprocess_access_check(bool bApply); void disable_game_openprocess_access_check(bool bApply);
void redirect_openprocess(bool bApply); void redirect_openprocess(bool bApply);
void backup_userdata_save(bool bApply);
void apply_all(bool bApply); void apply_all(bool bApply);
} }

View file

@ -315,7 +315,7 @@ namespace Dalamud.Injector
startInfo.BootShowConsole = args.Contains("--console"); startInfo.BootShowConsole = args.Contains("--console");
startInfo.BootEnableEtw = args.Contains("--etw"); startInfo.BootEnableEtw = args.Contains("--etw");
startInfo.BootLogPath = GetLogPath("dalamud.boot"); startInfo.BootLogPath = GetLogPath("dalamud.boot");
startInfo.BootEnabledGameFixes = new List<string> { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess" }; startInfo.BootEnabledGameFixes = new List<string> { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess", "backup_userdata_save" };
startInfo.BootDotnetOpenProcessHookMode = 0; startInfo.BootDotnetOpenProcessHookMode = 0;
startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0; startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0;
startInfo.BootWaitMessageBox |= args.Contains("--msgbox2") ? 2 : 0; startInfo.BootWaitMessageBox |= args.Contains("--msgbox2") ? 2 : 0;

View file

@ -190,6 +190,11 @@ namespace Dalamud.Configuration.Internal
/// </summary> /// </summary>
public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information; public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information;
/// <summary>
/// Gets or sets a value indicating whether to write to log files synchronously.
/// </summary>
public bool LogSynchronously { get; set; } = false;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not the debug log should scroll automatically. /// Gets or sets a value indicating whether or not the debug log should scroll automatically.
/// </summary> /// </summary>
@ -261,6 +266,12 @@ namespace Dalamud.Configuration.Internal
/// </summary> /// </summary>
public bool PluginSafeMode { get; set; } public bool PluginSafeMode { get; set; }
/// <summary>
/// Gets or sets a value indicating the wait time between plugin unload and plugin assembly unload.
/// Uses default value that may change between versions if set to null.
/// </summary>
public int? PluginWaitBeforeFree { get; set; }
/// <summary> /// <summary>
/// Gets or sets a list of saved styles. /// Gets or sets a list of saved styles.
/// </summary> /// </summary>

View file

@ -5,21 +5,11 @@ using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.Gui.Internal; using Dalamud.Game.Gui.Internal;
using Dalamud.Game.Internal;
using Dalamud.Game.Network.Internal;
using Dalamud.Hooking.Internal;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal;
using Dalamud.Support;
using Dalamud.Utility;
using Serilog; using Serilog;
using Serilog.Core;
using Serilog.Events;
#if DEBUG #if DEBUG
[assembly: InternalsVisibleTo("Dalamud.CorePlugin")] [assembly: InternalsVisibleTo("Dalamud.CorePlugin")]
@ -33,13 +23,11 @@ namespace Dalamud
/// <summary> /// <summary>
/// The main Dalamud class containing all subsystems. /// The main Dalamud class containing all subsystems.
/// </summary> /// </summary>
internal sealed class Dalamud : IDisposable, IServiceType internal sealed class Dalamud : IServiceType
{ {
#region Internals #region Internals
private readonly ManualResetEvent unloadSignal; private readonly ManualResetEvent unloadSignal;
private readonly ManualResetEvent finishUnloadSignal;
private MonoMod.RuntimeDetour.Hook processMonoHook;
private bool hasDisposedPlugins = false; private bool hasDisposedPlugins = false;
#endregion #endregion
@ -48,22 +36,13 @@ namespace Dalamud
/// Initializes a new instance of the <see cref="Dalamud"/> class. /// Initializes a new instance of the <see cref="Dalamud"/> class.
/// </summary> /// </summary>
/// <param name="info">DalamudStartInfo instance.</param> /// <param name="info">DalamudStartInfo instance.</param>
/// <param name="loggingLevelSwitch">LoggingLevelSwitch to control Serilog level.</param>
/// <param name="finishSignal">Signal signalling shutdown.</param>
/// <param name="configuration">The Dalamud configuration.</param> /// <param name="configuration">The Dalamud configuration.</param>
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param> /// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param>
public Dalamud(DalamudStartInfo info, LoggingLevelSwitch loggingLevelSwitch, ManualResetEvent finishSignal, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent) public Dalamud(DalamudStartInfo info, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent)
{ {
this.LogLevelSwitch = loggingLevelSwitch;
this.unloadSignal = new ManualResetEvent(false); this.unloadSignal = new ManualResetEvent(false);
this.unloadSignal.Reset(); this.unloadSignal.Reset();
this.finishUnloadSignal = finishSignal;
this.finishUnloadSignal.Reset();
SerilogEventSink.Instance.LogLine += SerilogOnLogLine;
ServiceManager.InitializeProvidedServicesAndClientStructs(this, info, configuration); ServiceManager.InitializeProvidedServicesAndClientStructs(this, info, configuration);
if (!configuration.IsResumeGameAfterPluginLoad) if (!configuration.IsResumeGameAfterPluginLoad)
@ -111,11 +90,6 @@ namespace Dalamud
} }
} }
/// <summary>
/// Gets LoggingLevelSwitch for Dalamud and Plugin logs.
/// </summary>
internal LoggingLevelSwitch LogLevelSwitch { get; private set; }
/// <summary> /// <summary>
/// Gets location of stored assets. /// Gets location of stored assets.
/// </summary> /// </summary>
@ -138,14 +112,6 @@ namespace Dalamud
this.unloadSignal.WaitOne(); this.unloadSignal.WaitOne();
} }
/// <summary>
/// Wait for a queued unload to be finalized.
/// </summary>
public void WaitForUnloadFinish()
{
this.finishUnloadSignal?.WaitOne();
}
/// <summary> /// <summary>
/// Dispose subsystems related to plugin handling. /// Dispose subsystems related to plugin handling.
/// </summary> /// </summary>
@ -169,46 +135,6 @@ namespace Dalamud
Service<PluginManager>.GetNullable()?.Dispose(); Service<PluginManager>.GetNullable()?.Dispose();
} }
/// <summary>
/// Dispose Dalamud subsystems.
/// </summary>
public void Dispose()
{
try
{
if (!this.hasDisposedPlugins)
{
this.DisposePlugins();
Thread.Sleep(100);
}
Service<Framework>.GetNullable()?.ExplicitDispose();
Service<ClientState>.GetNullable()?.ExplicitDispose();
this.unloadSignal?.Dispose();
Service<WinSockHandlers>.GetNullable()?.Dispose();
Service<DataManager>.GetNullable()?.ExplicitDispose();
Service<AntiDebug>.GetNullable()?.Dispose();
Service<DalamudAtkTweaks>.GetNullable()?.Dispose();
Service<HookManager>.GetNullable()?.Dispose();
var sigScanner = Service<SigScanner>.Get();
sigScanner.Save();
sigScanner.Dispose();
SerilogEventSink.Instance.LogLine -= SerilogOnLogLine;
this.processMonoHook?.Dispose();
Log.Debug("Dalamud::Dispose() OK!");
}
catch (Exception ex)
{
Log.Error(ex, "Dalamud::Dispose() failed.");
}
}
/// <summary> /// <summary>
/// Replace the built-in exception handler with a debug one. /// Replace the built-in exception handler with a debug one.
/// </summary> /// </summary>
@ -221,13 +147,5 @@ namespace Dalamud
var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(releaseFilter); var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(releaseFilter);
Log.Debug("Reset ExceptionFilter, old: {0}", oldFilter); Log.Debug("Reset ExceptionFilter, old: {0}", oldFilter);
} }
private static void SerilogOnLogLine(object? sender, (string Line, LogEventLevel Level, DateTimeOffset TimeStamp, Exception? Exception) e)
{
if (e.Exception == null)
return;
Troubleshooting.LogException(e.Exception, e.Line);
}
} }
} }

View file

@ -25,6 +25,11 @@ namespace Dalamud
/// </summary> /// </summary>
public sealed class EntryPoint public sealed class EntryPoint
{ {
/// <summary>
/// Log level switch for runtime log level change.
/// </summary>
public static readonly LoggingLevelSwitch LogLevelSwitch = new(LogEventLevel.Verbose);
/// <summary> /// <summary>
/// A delegate used during initialization of the CLR from Dalamud.Boot. /// A delegate used during initialization of the CLR from Dalamud.Boot.
/// </summary> /// </summary>
@ -107,6 +112,49 @@ namespace Dalamud
msgThread.Join(); msgThread.Join();
} }
/// <summary>
/// Sets up logging.
/// </summary>
/// <param name="baseDirectory">Base directory.</param>
/// <param name="logConsole">Whether to log to console.</param>
/// <param name="logSynchronously">Log synchronously.</param>
internal static void InitLogging(string baseDirectory, bool logConsole, bool logSynchronously)
{
#if DEBUG
var logPath = Path.Combine(baseDirectory, "dalamud.log");
var oldPath = Path.Combine(baseDirectory, "dalamud.log.old");
#else
var logPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log");
var oldPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log.old");
#endif
Log.CloseAndFlush();
CullLogFile(logPath, oldPath, 1 * 1024 * 1024);
CullLogFile(oldPath, null, 10 * 1024 * 1024);
var config = new LoggerConfiguration()
.WriteTo.Sink(SerilogEventSink.Instance)
.MinimumLevel.ControlledBy(LogLevelSwitch);
if (logSynchronously)
{
config = config.WriteTo.File(logPath, fileSizeLimitBytes: null);
}
else
{
config = config.WriteTo.Async(a => a.File(
logPath,
fileSizeLimitBytes: null,
buffered: false,
flushToDiskInterval: TimeSpan.FromSeconds(1)));
}
if (logConsole)
config = config.WriteTo.Console();
Log.Logger = config.CreateLogger();
}
/// <summary> /// <summary>
/// Initialize all Dalamud subsystems and start running on the main thread. /// Initialize all Dalamud subsystems and start running on the main thread.
/// </summary> /// </summary>
@ -115,22 +163,23 @@ namespace Dalamud
private static void RunThread(DalamudStartInfo info, IntPtr mainThreadContinueEvent) private static void RunThread(DalamudStartInfo info, IntPtr mainThreadContinueEvent)
{ {
// Setup logger // Setup logger
var levelSwitch = InitLogging(info.WorkingDirectory, info.BootShowConsole); InitLogging(info.WorkingDirectory!, info.BootShowConsole, true);
SerilogEventSink.Instance.LogLine += SerilogOnLogLine;
// Load configuration first to get some early persistent state, like log level // Load configuration first to get some early persistent state, like log level
var configuration = DalamudConfiguration.Load(info.ConfigurationPath); var configuration = DalamudConfiguration.Load(info.ConfigurationPath!);
// Set the appropriate logging level from the configuration // Set the appropriate logging level from the configuration
#if !DEBUG #if !DEBUG
levelSwitch.MinimumLevel = configuration.LogLevel; if (!configuration.LogSynchronously)
InitLogging(info.WorkingDirectory!, info.BootShowConsole, configuration.LogSynchronously);
LogLevelSwitch.MinimumLevel = configuration.LogLevel;
#endif #endif
// Log any unhandled exception. // Log any unhandled exception.
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
var finishSignal = new ManualResetEvent(false);
try try
{ {
if (info.DelayInitializeMs > 0) if (info.DelayInitializeMs > 0)
@ -148,12 +197,12 @@ namespace Dalamud
if (!Util.IsLinux()) if (!Util.IsLinux())
InitSymbolHandler(info); InitSymbolHandler(info);
var dalamud = new Dalamud(info, levelSwitch, finishSignal, configuration, mainThreadContinueEvent); var dalamud = new Dalamud(info, configuration, mainThreadContinueEvent);
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash}", Util.GetGitHash(), Util.GetGitHashClientStructs()); Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash}", Util.GetGitHash(), Util.GetGitHashClientStructs());
dalamud.WaitForUnload(); dalamud.WaitForUnload();
dalamud.Dispose(); ServiceManager.UnloadAllServices();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -166,11 +215,18 @@ namespace Dalamud
Log.Information("Session has ended."); Log.Information("Session has ended.");
Log.CloseAndFlush(); Log.CloseAndFlush();
SerilogEventSink.Instance.LogLine -= SerilogOnLogLine;
finishSignal.Set();
} }
} }
private static void SerilogOnLogLine(object? sender, (string Line, LogEventLevel Level, DateTimeOffset TimeStamp, Exception? Exception) e)
{
if (e.Exception == null)
return;
Troubleshooting.LogException(e.Exception, e.Line);
}
private static void InitSymbolHandler(DalamudStartInfo info) private static void InitSymbolHandler(DalamudStartInfo info)
{ {
try try
@ -193,33 +249,6 @@ namespace Dalamud
} }
} }
private static LoggingLevelSwitch InitLogging(string baseDirectory, bool logConsole)
{
#if DEBUG
var logPath = Path.Combine(baseDirectory, "dalamud.log");
var oldPath = Path.Combine(baseDirectory, "dalamud.log.old");
#else
var logPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log");
var oldPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log.old");
#endif
CullLogFile(logPath, oldPath, 1 * 1024 * 1024);
CullLogFile(oldPath, null, 10 * 1024 * 1024);
var levelSwitch = new LoggingLevelSwitch(LogEventLevel.Verbose);
var config = new LoggerConfiguration()
.WriteTo.Async(a => a.File(logPath, fileSizeLimitBytes: null, buffered: false, flushToDiskInterval: TimeSpan.FromSeconds(1)))
.WriteTo.Sink(SerilogEventSink.Instance)
.MinimumLevel.ControlledBy(levelSwitch);
if (logConsole)
config = config.WriteTo.Console();
Log.Logger = config.CreateLogger();
return levelSwitch;
}
private static void CullLogFile(string logPath, string? oldPath, int cullingFileSize) private static void CullLogFile(string logPath, string? oldPath, int cullingFileSize)
{ {
try try

View file

@ -107,6 +107,9 @@ namespace Dalamud.Game
private readonly DalamudLinkPayload openInstallerWindowLink; private readonly DalamudLinkPayload openInstallerWindowLink;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private bool hasSeenLoadingMsg; private bool hasSeenLoadingMsg;
private bool hasAutoUpdatedPlugins; private bool hasAutoUpdatedPlugins;
@ -118,7 +121,7 @@ namespace Dalamud.Game
this.openInstallerWindowLink = chatGui.AddChatLinkHandler("Dalamud", 1001, (i, m) => this.openInstallerWindowLink = chatGui.AddChatLinkHandler("Dalamud", 1001, (i, m) =>
{ {
Service<DalamudInterface>.Get().OpenPluginInstaller(); Service<DalamudInterface>.GetNullable()?.OpenPluginInstaller();
}); });
} }
@ -145,11 +148,9 @@ namespace Dalamud.Game
private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled) private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled)
{ {
var configuration = Service<DalamudConfiguration>.Get();
var textVal = message.TextValue; var textVal = message.TextValue;
if (!configuration.DisableRmtFiltering) if (!this.configuration.DisableRmtFiltering)
{ {
var matched = this.rmtRegex.IsMatch(textVal); var matched = this.rmtRegex.IsMatch(textVal);
if (matched) if (matched)
@ -161,8 +162,8 @@ namespace Dalamud.Game
} }
} }
if (configuration.BadWords != null && if (this.configuration.BadWords != null &&
configuration.BadWords.Any(x => !string.IsNullOrEmpty(x) && textVal.Contains(x))) this.configuration.BadWords.Any(x => !string.IsNullOrEmpty(x) && textVal.Contains(x)))
{ {
// This seems to be in the user block list - let's not show it // This seems to be in the user block list - let's not show it
Log.Debug("Blocklist triggered"); Log.Debug("Blocklist triggered");
@ -174,7 +175,9 @@ namespace Dalamud.Game
private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled) private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled)
{ {
var startInfo = Service<DalamudStartInfo>.Get(); var startInfo = Service<DalamudStartInfo>.Get();
var clientState = Service<ClientState.ClientState>.Get(); var clientState = Service<ClientState.ClientState>.GetNullable();
if (clientState == null)
return;
if (type == XivChatType.Notice && !this.hasSeenLoadingMsg) if (type == XivChatType.Notice && !this.hasSeenLoadingMsg)
this.PrintWelcomeMessage(); this.PrintWelcomeMessage();
@ -232,17 +235,19 @@ namespace Dalamud.Game
private void PrintWelcomeMessage() private void PrintWelcomeMessage()
{ {
var chatGui = Service<ChatGui>.Get(); var chatGui = Service<ChatGui>.GetNullable();
var configuration = Service<DalamudConfiguration>.Get(); var pluginManager = Service<PluginManager>.GetNullable();
var pluginManager = Service<PluginManager>.Get(); var dalamudInterface = Service<DalamudInterface>.GetNullable();
var dalamudInterface = Service<DalamudInterface>.Get();
if (chatGui == null || pluginManager == null || dalamudInterface == null)
return;
var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString(); var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString();
chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud vD{0} loaded."), assemblyVersion) chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud vD{0} loaded."), assemblyVersion)
+ string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count(x => x.IsLoaded))); + string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count(x => x.IsLoaded)));
if (configuration.PrintPluginsWelcomeMsg) if (this.configuration.PrintPluginsWelcomeMsg)
{ {
foreach (var plugin in pluginManager.InstalledPlugins.OrderBy(plugin => plugin.Name).Where(x => x.IsLoaded)) foreach (var plugin in pluginManager.InstalledPlugins.OrderBy(plugin => plugin.Name).Where(x => x.IsLoaded))
{ {
@ -250,7 +255,7 @@ namespace Dalamud.Game
} }
} }
if (string.IsNullOrEmpty(configuration.LastVersion) || !assemblyVersion.StartsWith(configuration.LastVersion)) if (string.IsNullOrEmpty(this.configuration.LastVersion) || !assemblyVersion.StartsWith(this.configuration.LastVersion))
{ {
chatGui.PrintChat(new XivChatEntry chatGui.PrintChat(new XivChatEntry
{ {
@ -258,14 +263,14 @@ namespace Dalamud.Game
Type = XivChatType.Notice, Type = XivChatType.Notice,
}); });
if (string.IsNullOrEmpty(configuration.LastChangelogMajorMinor) || (!ChangelogWindow.WarrantsChangelogForMajorMinor.StartsWith(configuration.LastChangelogMajorMinor) && assemblyVersion.StartsWith(ChangelogWindow.WarrantsChangelogForMajorMinor))) if (string.IsNullOrEmpty(this.configuration.LastChangelogMajorMinor) || (!ChangelogWindow.WarrantsChangelogForMajorMinor.StartsWith(this.configuration.LastChangelogMajorMinor) && assemblyVersion.StartsWith(ChangelogWindow.WarrantsChangelogForMajorMinor)))
{ {
dalamudInterface.OpenChangelogWindow(); dalamudInterface.OpenChangelogWindow();
configuration.LastChangelogMajorMinor = ChangelogWindow.WarrantsChangelogForMajorMinor; this.configuration.LastChangelogMajorMinor = ChangelogWindow.WarrantsChangelogForMajorMinor;
} }
configuration.LastVersion = assemblyVersion; this.configuration.LastVersion = assemblyVersion;
configuration.Save(); this.configuration.Save();
} }
this.hasSeenLoadingMsg = true; this.hasSeenLoadingMsg = true;
@ -273,10 +278,12 @@ namespace Dalamud.Game
private void AutoUpdatePlugins() private void AutoUpdatePlugins()
{ {
var chatGui = Service<ChatGui>.Get(); var chatGui = Service<ChatGui>.GetNullable();
var configuration = Service<DalamudConfiguration>.Get(); var pluginManager = Service<PluginManager>.GetNullable();
var pluginManager = Service<PluginManager>.Get(); var notifications = Service<NotificationManager>.GetNullable();
var notifications = Service<NotificationManager>.Get();
if (chatGui == null || pluginManager == null || notifications == null)
return;
if (!pluginManager.ReposReady || pluginManager.InstalledPlugins.Count == 0 || pluginManager.AvailablePlugins.Count == 0) if (!pluginManager.ReposReady || pluginManager.InstalledPlugins.Count == 0 || pluginManager.AvailablePlugins.Count == 0)
{ {
@ -286,7 +293,7 @@ namespace Dalamud.Game
this.hasAutoUpdatedPlugins = true; this.hasAutoUpdatedPlugins = true;
Task.Run(() => pluginManager.UpdatePluginsAsync(!configuration.AutoUpdatePlugins)).ContinueWith(task => Task.Run(() => pluginManager.UpdatePluginsAsync(!this.configuration.AutoUpdatePlugins)).ContinueWith(task =>
{ {
if (task.IsFaulted) if (task.IsFaulted)
{ {
@ -297,15 +304,13 @@ namespace Dalamud.Game
var updatedPlugins = task.Result; var updatedPlugins = task.Result;
if (updatedPlugins != null && updatedPlugins.Any()) if (updatedPlugins != null && updatedPlugins.Any())
{ {
if (configuration.AutoUpdatePlugins) if (this.configuration.AutoUpdatePlugins)
{ {
PluginManager.PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:")); PluginManager.PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:"));
notifications.AddNotification(Loc.Localize("NotificationUpdatedPlugins", "{0} of your plugins were updated.").Format(updatedPlugins.Count), Loc.Localize("NotificationAutoUpdate", "Auto-Update"), NotificationType.Info); notifications.AddNotification(Loc.Localize("NotificationUpdatedPlugins", "{0} of your plugins were updated.").Format(updatedPlugins.Count), Loc.Localize("NotificationAutoUpdate", "Auto-Update"), NotificationType.Info);
} }
else else
{ {
var data = Service<DataManager>.Get();
chatGui.PrintChat(new XivChatEntry chatGui.PrintChat(new XivChatEntry
{ {
Message = new SeString(new List<Payload>() Message = new SeString(new List<Payload>()

View file

@ -20,12 +20,15 @@ namespace Dalamud.Game.ClientState.Buddy
{ {
private const uint InvalidObjectID = 0xE0000000; private const uint InvalidObjectID = 0xE0000000;
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get();
private readonly ClientStateAddressResolver address; private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private BuddyList(ClientState clientState) private BuddyList()
{ {
this.address = clientState.AddressResolver; this.address = this.clientState.AddressResolver;
Log.Verbose($"Buddy list address 0x{this.address.BuddyList.ToInt64():X}"); Log.Verbose($"Buddy list address 0x{this.address.BuddyList.ToInt64():X}");
} }
@ -145,9 +148,7 @@ namespace Dalamud.Game.ClientState.Buddy
/// <returns><see cref="BuddyMember"/> object containing the requested data.</returns> /// <returns><see cref="BuddyMember"/> object containing the requested data.</returns>
public BuddyMember? CreateBuddyMemberReference(IntPtr address) public BuddyMember? CreateBuddyMemberReference(IntPtr address)
{ {
var clientState = Service<ClientState>.Get(); if (this.clientState.LocalContentId == 0)
if (clientState.LocalContentId == 0)
return null; return null;
if (address == IntPtr.Zero) if (address == IntPtr.Zero)

View file

@ -11,6 +11,9 @@ namespace Dalamud.Game.ClientState.Buddy
/// </summary> /// </summary>
public unsafe class BuddyMember public unsafe class BuddyMember
{ {
[ServiceManager.ServiceDependency]
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BuddyMember"/> class. /// Initializes a new instance of the <see cref="BuddyMember"/> class.
/// </summary> /// </summary>
@ -36,7 +39,7 @@ namespace Dalamud.Game.ClientState.Buddy
/// <remarks> /// <remarks>
/// This iterates the actor table, it should be used with care. /// This iterates the actor table, it should be used with care.
/// </remarks> /// </remarks>
public GameObject? GameObject => Service<ObjectTable>.Get().SearchById(this.ObjectId); public GameObject? GameObject => this.objectTable.SearchById(this.ObjectId);
/// <summary> /// <summary>
/// Gets the current health of this buddy. /// Gets the current health of this buddy.

View file

@ -1,22 +1,15 @@
using System; using System;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.ClientState.Aetherytes;
using Dalamud.Game.ClientState.Buddy;
using Dalamud.Game.ClientState.Fates;
using Dalamud.Game.ClientState.GamePad;
using Dalamud.Game.ClientState.JobGauge;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Party;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Game.Network.Internal; using Dalamud.Game.Network.Internal;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using Serilog; using Serilog;
@ -33,6 +26,12 @@ namespace Dalamud.Game.ClientState
private readonly ClientStateAddressResolver address; private readonly ClientStateAddressResolver address;
private readonly Hook<SetupTerritoryTypeDelegate> setupTerritoryTypeHook; private readonly Hook<SetupTerritoryTypeDelegate> setupTerritoryTypeHook;
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
[ServiceManager.ServiceDependency]
private readonly NetworkHandlers networkHandlers = Service<NetworkHandlers>.Get();
private bool lastConditionNone = true; private bool lastConditionNone = true;
private bool lastFramePvP = false; private bool lastFramePvP = false;
@ -42,7 +41,7 @@ namespace Dalamud.Game.ClientState
internal ClientStateAddressResolver AddressResolver => this.address; internal ClientStateAddressResolver AddressResolver => this.address;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private ClientState(Framework framework, NetworkHandlers networkHandlers, SigScanner sigScanner, DalamudStartInfo startInfo) private ClientState(SigScanner sigScanner, DalamudStartInfo startInfo)
{ {
this.address = new ClientStateAddressResolver(); this.address = new ClientStateAddressResolver();
this.address.Setup(sigScanner); this.address.Setup(sigScanner);
@ -53,11 +52,11 @@ namespace Dalamud.Game.ClientState
Log.Verbose($"SetupTerritoryType address 0x{this.address.SetupTerritoryType.ToInt64():X}"); Log.Verbose($"SetupTerritoryType address 0x{this.address.SetupTerritoryType.ToInt64():X}");
this.setupTerritoryTypeHook = new Hook<SetupTerritoryTypeDelegate>(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour); this.setupTerritoryTypeHook = Hook<SetupTerritoryTypeDelegate>.FromAddress(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour);
framework.Update += this.FrameworkOnOnUpdateEvent; this.framework.Update += this.FrameworkOnOnUpdateEvent;
networkHandlers.CfPop += this.NetworkHandlersOnCfPop; this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
} }
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
@ -106,7 +105,7 @@ namespace Dalamud.Game.ClientState
/// <summary> /// <summary>
/// Gets the local player character, if one is present. /// Gets the local player character, if one is present.
/// </summary> /// </summary>
public PlayerCharacter? LocalPlayer => Service<ObjectTable>.Get()[0] as PlayerCharacter; public PlayerCharacter? LocalPlayer => Service<ObjectTable>.GetNullable()?.FirstOrDefault() as PlayerCharacter;
/// <summary> /// <summary>
/// Gets the content ID of the local character. /// Gets the content ID of the local character.
@ -134,10 +133,8 @@ namespace Dalamud.Game.ClientState
void IDisposable.Dispose() void IDisposable.Dispose()
{ {
this.setupTerritoryTypeHook.Dispose(); this.setupTerritoryTypeHook.Dispose();
Service<Conditions.Condition>.Get().ExplicitDispose(); this.framework.Update -= this.FrameworkOnOnUpdateEvent;
Service<GamepadState>.Get().ExplicitDispose(); this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop;
Service<Framework>.Get().Update -= this.FrameworkOnOnUpdateEvent;
Service<NetworkHandlers>.Get().CfPop -= this.NetworkHandlersOnCfPop;
} }
[ServiceManager.CallWhenServicesReady] [ServiceManager.CallWhenServicesReady]
@ -161,11 +158,14 @@ namespace Dalamud.Game.ClientState
this.CfPop?.Invoke(this, e); this.CfPop?.Invoke(this, e);
} }
private void FrameworkOnOnUpdateEvent(Framework framework) private void FrameworkOnOnUpdateEvent(Framework framework1)
{ {
var condition = Service<Conditions.Condition>.Get(); var condition = Service<Conditions.Condition>.GetNullable();
var gameGui = Service<GameGui>.Get(); var gameGui = Service<GameGui>.GetNullable();
var data = Service<DataManager>.Get(); var data = Service<DataManager>.GetNullable();
if (condition == null || gameGui == null || data == null)
return;
if (condition.Any() && this.lastConditionNone == true) if (condition.Any() && this.lastConditionNone == true)
{ {

View file

@ -46,9 +46,9 @@ namespace Dalamud.Game.ClientState.Fates
/// <returns>True or false.</returns> /// <returns>True or false.</returns>
public static bool IsValid(Fate fate) public static bool IsValid(Fate fate)
{ {
var clientState = Service<ClientState>.Get(); var clientState = Service<ClientState>.GetNullable();
if (fate == null) if (fate == null || clientState == null)
return false; return false;
if (clientState.LocalContentId == 0) if (clientState.LocalContentId == 0)

View file

@ -32,7 +32,7 @@ namespace Dalamud.Game.ClientState.GamePad
{ {
var resolver = clientState.AddressResolver; var resolver = clientState.AddressResolver;
Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}"); Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}");
this.gamepadPoll = new Hook<ControllerPoll>(resolver.GamepadPoll, this.GamepadPollDetour); this.gamepadPoll = Hook<ControllerPoll>.FromAddress(resolver.GamepadPoll, this.GamepadPollDetour);
} }
private delegate int ControllerPoll(IntPtr controllerInput); private delegate int ControllerPoll(IntPtr controllerInput);

View file

@ -97,9 +97,9 @@ namespace Dalamud.Game.ClientState.Objects
/// <returns><see cref="GameObject"/> object or inheritor containing the requested data.</returns> /// <returns><see cref="GameObject"/> object or inheritor containing the requested data.</returns>
public unsafe GameObject? CreateObjectReference(IntPtr address) public unsafe GameObject? CreateObjectReference(IntPtr address)
{ {
var clientState = Service<ClientState>.Get(); var clientState = Service<ClientState>.GetNullable();
if (clientState.LocalContentId == 0) if (clientState == null || clientState.LocalContentId == 0)
return null; return null;
if (address == IntPtr.Zero) if (address == IntPtr.Zero)

View file

@ -14,12 +14,18 @@ namespace Dalamud.Game.ClientState.Objects
[ServiceManager.BlockingEarlyLoadedService] [ServiceManager.BlockingEarlyLoadedService]
public sealed unsafe class TargetManager : IServiceType public sealed unsafe class TargetManager : IServiceType
{ {
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get();
[ServiceManager.ServiceDependency]
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
private readonly ClientStateAddressResolver address; private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private TargetManager(ClientState clientState) private TargetManager()
{ {
this.address = clientState.AddressResolver; this.address = this.clientState.AddressResolver;
} }
/// <summary> /// <summary>
@ -32,7 +38,7 @@ namespace Dalamud.Game.ClientState.Objects
/// </summary> /// </summary>
public GameObject? Target public GameObject? Target
{ {
get => Service<ObjectTable>.Get().CreateObjectReference((IntPtr)Struct->Target); get => this.objectTable.CreateObjectReference((IntPtr)Struct->Target);
set => this.SetTarget(value); set => this.SetTarget(value);
} }
@ -41,7 +47,7 @@ namespace Dalamud.Game.ClientState.Objects
/// </summary> /// </summary>
public GameObject? MouseOverTarget public GameObject? MouseOverTarget
{ {
get => Service<ObjectTable>.Get().CreateObjectReference((IntPtr)Struct->MouseOverTarget); get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverTarget);
set => this.SetMouseOverTarget(value); set => this.SetMouseOverTarget(value);
} }
@ -50,7 +56,7 @@ namespace Dalamud.Game.ClientState.Objects
/// </summary> /// </summary>
public GameObject? FocusTarget public GameObject? FocusTarget
{ {
get => Service<ObjectTable>.Get().CreateObjectReference((IntPtr)Struct->FocusTarget); get => this.objectTable.CreateObjectReference((IntPtr)Struct->FocusTarget);
set => this.SetFocusTarget(value); set => this.SetFocusTarget(value);
} }
@ -59,7 +65,7 @@ namespace Dalamud.Game.ClientState.Objects
/// </summary> /// </summary>
public GameObject? PreviousTarget public GameObject? PreviousTarget
{ {
get => Service<ObjectTable>.Get().CreateObjectReference((IntPtr)Struct->PreviousTarget); get => this.objectTable.CreateObjectReference((IntPtr)Struct->PreviousTarget);
set => this.SetPreviousTarget(value); set => this.SetPreviousTarget(value);
} }
@ -68,7 +74,7 @@ namespace Dalamud.Game.ClientState.Objects
/// </summary> /// </summary>
public GameObject? SoftTarget public GameObject? SoftTarget
{ {
get => Service<ObjectTable>.Get().CreateObjectReference((IntPtr)Struct->SoftTarget); get => this.objectTable.CreateObjectReference((IntPtr)Struct->SoftTarget);
set => this.SetSoftTarget(value); set => this.SetSoftTarget(value);
} }

View file

@ -61,9 +61,9 @@ namespace Dalamud.Game.ClientState.Objects.Types
/// <returns>True or false.</returns> /// <returns>True or false.</returns>
public static bool IsValid(GameObject? actor) public static bool IsValid(GameObject? actor)
{ {
var clientState = Service<ClientState>.Get(); var clientState = Service<ClientState>.GetNullable();
if (actor is null) if (actor is null || clientState == null)
return false; return false;
if (clientState.LocalContentId == 0) if (clientState.LocalContentId == 0)

View file

@ -20,12 +20,15 @@ namespace Dalamud.Game.ClientState.Party
private const int GroupLength = 8; private const int GroupLength = 8;
private const int AllianceLength = 20; private const int AllianceLength = 20;
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get();
private readonly ClientStateAddressResolver address; private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private PartyList(ClientState clientState) private PartyList()
{ {
this.address = clientState.AddressResolver; this.address = this.clientState.AddressResolver;
Log.Verbose($"Group manager address 0x{this.address.GroupManager.ToInt64():X}"); Log.Verbose($"Group manager address 0x{this.address.GroupManager.ToInt64():X}");
} }
@ -115,9 +118,7 @@ namespace Dalamud.Game.ClientState.Party
/// <returns>The party member object containing the requested data.</returns> /// <returns>The party member object containing the requested data.</returns>
public PartyMember? CreatePartyMemberReference(IntPtr address) public PartyMember? CreatePartyMemberReference(IntPtr address)
{ {
var clientState = Service<ClientState>.Get(); if (this.clientState.LocalContentId == 0)
if (clientState.LocalContentId == 0)
return null; return null;
if (address == IntPtr.Zero) if (address == IntPtr.Zero)
@ -146,9 +147,7 @@ namespace Dalamud.Game.ClientState.Party
/// <returns>The party member object containing the requested data.</returns> /// <returns>The party member object containing the requested data.</returns>
public PartyMember? CreateAllianceMemberReference(IntPtr address) public PartyMember? CreateAllianceMemberReference(IntPtr address)
{ {
var clientState = Service<ClientState>.Get(); if (this.clientState.LocalContentId == 0)
if (clientState.LocalContentId == 0)
return null; return null;
if (address == IntPtr.Zero) if (address == IntPtr.Zero)

View file

@ -18,7 +18,7 @@ namespace Dalamud.Game.Command
[PluginInterface] [PluginInterface]
[InterfaceVersion("1.0")] [InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService] [ServiceManager.BlockingEarlyLoadedService]
public sealed class CommandManager : IServiceType public sealed class CommandManager : IServiceType, IDisposable
{ {
private readonly Dictionary<string, CommandInfo> commandMap = new(); private readonly Dictionary<string, CommandInfo> commandMap = new();
private readonly Regex commandRegexEn = new(@"^The command (?<command>.+) does not exist\.$", RegexOptions.Compiled); private readonly Regex commandRegexEn = new(@"^The command (?<command>.+) does not exist\.$", RegexOptions.Compiled);
@ -28,6 +28,9 @@ namespace Dalamud.Game.Command
private readonly Regex commandRegexCn = new(@"^^(“|「)(?<command>.+)(”|」)(出现问题:该命令不存在|出現問題:該命令不存在)。$", RegexOptions.Compiled); private readonly Regex commandRegexCn = new(@"^^(“|「)(?<command>.+)(”|」)(出现问题:该命令不存在|出現問題:該命令不存在)。$", RegexOptions.Compiled);
private readonly Regex currentLangCommandRegex; private readonly Regex currentLangCommandRegex;
[ServiceManager.ServiceDependency]
private readonly ChatGui chatGui = Service<ChatGui>.Get();
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private CommandManager(DalamudStartInfo startInfo) private CommandManager(DalamudStartInfo startInfo)
{ {
@ -40,7 +43,7 @@ namespace Dalamud.Game.Command
_ => this.currentLangCommandRegex, _ => this.currentLangCommandRegex,
}; };
Service<ChatGui>.Get().CheckMessageHandled += this.OnCheckMessageHandled; this.chatGui.CheckMessageHandled += this.OnCheckMessageHandled;
} }
/// <summary> /// <summary>
@ -170,5 +173,10 @@ namespace Dalamud.Game.Command
} }
} }
} }
public void Dispose()
{
this.chatGui.CheckMessageHandled -= this.OnCheckMessageHandled;
}
} }
} }

View file

@ -8,10 +8,8 @@ using System.Threading.Tasks;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Game.Gui.Toast; using Dalamud.Game.Gui.Toast;
using Dalamud.Game.Libc;
using Dalamud.Game.Network; using Dalamud.Game.Network;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Interface.Internal;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Utility; using Dalamud.Utility;
@ -32,21 +30,19 @@ namespace Dalamud.Game
private readonly List<RunOnNextTickTaskBase> runOnNextTickTaskList = new(); private readonly List<RunOnNextTickTaskBase> runOnNextTickTaskList = new();
private readonly Stopwatch updateStopwatch = new(); private readonly Stopwatch updateStopwatch = new();
private Hook<OnUpdateDetour> updateHook; private readonly Hook<OnUpdateDetour> updateHook;
private Hook<OnDestroyDetour> freeHook; private readonly Hook<OnRealDestroyDelegate> destroyHook;
private Hook<OnRealDestroyDelegate> destroyHook;
private Thread? frameworkUpdateThread; private Thread? frameworkUpdateThread;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private Framework(GameGui gameGui, GameNetwork gameNetwork, SigScanner sigScanner) private Framework(SigScanner sigScanner)
{ {
this.Address = new FrameworkAddressResolver(); this.Address = new FrameworkAddressResolver();
this.Address.Setup(sigScanner); this.Address.Setup(sigScanner);
this.updateHook = new Hook<OnUpdateDetour>(this.Address.TickAddress, this.HandleFrameworkUpdate); this.updateHook = Hook<OnUpdateDetour>.FromAddress(this.Address.TickAddress, this.HandleFrameworkUpdate);
this.freeHook = new Hook<OnDestroyDetour>(this.Address.FreeAddress, this.HandleFrameworkFree); this.destroyHook = Hook<OnRealDestroyDelegate>.FromAddress(this.Address.DestroyAddress, this.HandleFrameworkDestroy);
this.destroyHook = new Hook<OnRealDestroyDelegate>(this.Address.DestroyAddress, this.HandleFrameworkDestroy);
} }
/// <summary> /// <summary>
@ -113,6 +109,11 @@ namespace Dalamud.Game
/// </summary> /// </summary>
public bool IsInFrameworkUpdateThread => Thread.CurrentThread == this.frameworkUpdateThread; public bool IsInFrameworkUpdateThread => Thread.CurrentThread == this.frameworkUpdateThread;
/// <summary>
/// Gets a value indicating whether game Framework is unloading.
/// </summary>
public bool IsFrameworkUnloading { get; internal set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether to dispatch update events. /// Gets or sets a value indicating whether to dispatch update events.
/// </summary> /// </summary>
@ -124,7 +125,8 @@ namespace Dalamud.Game
/// <typeparam name="T">Return type.</typeparam> /// <typeparam name="T">Return type.</typeparam>
/// <param name="func">Function to call.</param> /// <param name="func">Function to call.</param>
/// <returns>Task representing the pending or already completed function.</returns> /// <returns>Task representing the pending or already completed function.</returns>
public Task<T> RunOnFrameworkThread<T>(Func<T> func) => this.IsInFrameworkUpdateThread ? Task.FromResult(func()) : this.RunOnTick(func); public Task<T> RunOnFrameworkThread<T>(Func<T> func) =>
this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? Task.FromResult(func()) : this.RunOnTick(func);
/// <summary> /// <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. /// 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.
@ -133,7 +135,7 @@ namespace Dalamud.Game
/// <returns>Task representing the pending or already completed function.</returns> /// <returns>Task representing the pending or already completed function.</returns>
public Task RunOnFrameworkThread(Action action) public Task RunOnFrameworkThread(Action action)
{ {
if (this.IsInFrameworkUpdateThread) if (this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading)
{ {
try try
{ {
@ -151,6 +153,24 @@ namespace Dalamud.Game
} }
} }
/// <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>
/// <typeparam name="T">Return type.</typeparam>
/// <param name="func">Function to call.</param>
/// <returns>Task representing the pending or already completed function.</returns>
public Task<T> RunOnFrameworkThread<T>(Func<Task<T>> func) =>
this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? func() : this.RunOnTick(func);
/// <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>
/// <typeparam name="T">Return type.</typeparam>
/// <param name="func">Function to call.</param>
/// <returns>Task representing the pending or already completed function.</returns>
public Task RunOnFrameworkThread(Func<Task> func) =>
this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? func() : this.RunOnTick(func);
/// <summary> /// <summary>
/// Run given function in upcoming Framework.Tick call. /// Run given function in upcoming Framework.Tick call.
/// </summary> /// </summary>
@ -162,6 +182,16 @@ namespace Dalamud.Game
/// <returns>Task representing the pending function.</returns> /// <returns>Task representing the pending function.</returns>
public Task<T> RunOnTick<T>(Func<T> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) public Task<T> RunOnTick<T>(Func<T> 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<T>(cts.Token);
}
var tcs = new TaskCompletionSource<T>(); var tcs = new TaskCompletionSource<T>();
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<T>() this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<T>()
{ {
@ -184,6 +214,16 @@ namespace Dalamud.Game
/// <returns>Task representing the pending function.</returns> /// <returns>Task representing the pending function.</returns>
public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) 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(); var tcs = new TaskCompletionSource();
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskAction() this.runOnNextTickTaskList.Add(new RunOnNextTickTaskAction()
{ {
@ -196,22 +236,88 @@ namespace Dalamud.Game
return tcs.Task; return tcs.Task;
} }
/// <summary>
/// Run given function in upcoming Framework.Tick call.
/// </summary>
/// <typeparam name="T">Return type.</typeparam>
/// <param name="func">Function to call.</param>
/// <param name="delay">Wait for given timespan before calling this function.</param>
/// <param name="delayTicks">Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter.</param>
/// <param name="cancellationToken">Cancellation token which will prevent the execution of this function if wait conditions are not met.</param>
/// <returns>Task representing the pending function.</returns>
public Task<T> RunOnTick<T>(Func<Task<T>> 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<T>(cts.Token);
}
var tcs = new TaskCompletionSource<Task<T>>();
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<Task<T>>()
{
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();
}
/// <summary>
/// Run given function in upcoming Framework.Tick call.
/// </summary>
/// <typeparam name="T">Return type.</typeparam>
/// <param name="func">Function to call.</param>
/// <param name="delay">Wait for given timespan before calling this function.</param>
/// <param name="delayTicks">Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter.</param>
/// <param name="cancellationToken">Cancellation token which will prevent the execution of this function if wait conditions are not met.</param>
/// <returns>Task representing the pending function.</returns>
public Task RunOnTick(Func<Task> 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<Task>();
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<Task>()
{
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();
}
/// <summary> /// <summary>
/// Dispose of managed and unmanaged resources. /// Dispose of managed and unmanaged resources.
/// </summary> /// </summary>
void IDisposable.Dispose() void IDisposable.Dispose()
{ {
Service<GameGui>.GetNullable()?.ExplicitDispose(); this.RunOnFrameworkThread(() =>
Service<GameNetwork>.GetNullable()?.ExplicitDispose(); {
// ReSharper disable once AccessToDisposedClosure
this.updateHook.Disable();
this.updateHook?.Disable(); // ReSharper disable once AccessToDisposedClosure
this.freeHook?.Disable(); this.destroyHook.Disable();
this.destroyHook?.Disable(); }).Wait();
Thread.Sleep(500);
this.updateHook?.Dispose(); this.updateHook.Dispose();
this.freeHook?.Dispose(); this.destroyHook.Dispose();
this.destroyHook?.Dispose();
this.updateStopwatch.Reset(); this.updateStopwatch.Reset();
statsStopwatch.Reset(); statsStopwatch.Reset();
@ -221,7 +327,6 @@ namespace Dalamud.Game
private void ContinueConstruction() private void ContinueConstruction()
{ {
this.updateHook.Enable(); this.updateHook.Enable();
this.freeHook.Enable();
this.destroyHook.Enable(); this.destroyHook.Enable();
} }
@ -314,41 +419,21 @@ namespace Dalamud.Game
} }
original: original:
return this.updateHook.Original(framework); return this.updateHook.OriginalDisposeSafe(framework);
} }
private bool HandleFrameworkDestroy(IntPtr framework) private bool HandleFrameworkDestroy(IntPtr framework)
{ {
if (this.DispatchUpdateEvents) this.IsFrameworkUnloading = true;
{
Log.Information("Framework::Destroy!");
var dalamud = Service<Dalamud>.Get();
dalamud.DisposePlugins();
Log.Information("Framework::Destroy OK!");
}
this.DispatchUpdateEvents = false; this.DispatchUpdateEvents = false;
return this.destroyHook.Original(framework); Log.Information("Framework::Destroy!");
} Service<Dalamud>.Get().Unload();
this.runOnNextTickTaskList.RemoveAll(x => x.Run());
ServiceManager.UnloadAllServices();
Log.Information("Framework::Destroy OK!");
private IntPtr HandleFrameworkFree() return this.destroyHook.OriginalDisposeSafe(framework);
{
Log.Information("Framework::Free!");
// Store the pointer to the original trampoline location
var originalPtr = Marshal.GetFunctionPointerForDelegate(this.freeHook.Original);
var dalamud = Service<Dalamud>.Get();
dalamud.Unload();
dalamud.WaitForUnloadFinish();
Log.Information("Framework::Free OK!");
// Return the original trampoline location to cleanly exit
return originalPtr;
} }
private abstract class RunOnNextTickTaskBase private abstract class RunOnNextTickTaskBase

View file

@ -33,6 +33,12 @@ namespace Dalamud.Game.Gui
private readonly Hook<PopulateItemLinkDelegate> populateItemLinkHook; private readonly Hook<PopulateItemLinkDelegate> populateItemLinkHook;
private readonly Hook<InteractableLinkClickedDelegate> interactableLinkClickedHook; private readonly Hook<InteractableLinkClickedDelegate> interactableLinkClickedHook;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
[ServiceManager.ServiceDependency]
private readonly LibcFunction libcFunction = Service<LibcFunction>.Get();
private IntPtr baseAddress = IntPtr.Zero; private IntPtr baseAddress = IntPtr.Zero;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
@ -41,9 +47,9 @@ namespace Dalamud.Game.Gui
this.address = new ChatGuiAddressResolver(); this.address = new ChatGuiAddressResolver();
this.address.Setup(sigScanner); this.address.Setup(sigScanner);
this.printMessageHook = new Hook<PrintMessageDelegate>(this.address.PrintMessage, this.HandlePrintMessageDetour); this.printMessageHook = Hook<PrintMessageDelegate>.FromAddress(this.address.PrintMessage, this.HandlePrintMessageDetour);
this.populateItemLinkHook = new Hook<PopulateItemLinkDelegate>(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour); this.populateItemLinkHook = Hook<PopulateItemLinkDelegate>.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
this.interactableLinkClickedHook = new Hook<InteractableLinkClickedDelegate>(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour); this.interactableLinkClickedHook = Hook<InteractableLinkClickedDelegate>.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
} }
/// <summary> /// <summary>
@ -150,13 +156,11 @@ namespace Dalamud.Game.Gui
/// <param name="message">A message to send.</param> /// <param name="message">A message to send.</param>
public void Print(string message) public void Print(string message)
{ {
var configuration = Service<DalamudConfiguration>.Get();
// Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message); // Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message);
this.PrintChat(new XivChatEntry this.PrintChat(new XivChatEntry
{ {
Message = message, Message = message,
Type = configuration.GeneralChatType, Type = this.configuration.GeneralChatType,
}); });
} }
@ -167,13 +171,11 @@ namespace Dalamud.Game.Gui
/// <param name="message">A message to send.</param> /// <param name="message">A message to send.</param>
public void Print(SeString message) public void Print(SeString message)
{ {
var configuration = Service<DalamudConfiguration>.Get();
// Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue); // Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue);
this.PrintChat(new XivChatEntry this.PrintChat(new XivChatEntry
{ {
Message = message, Message = message,
Type = configuration.GeneralChatType, Type = this.configuration.GeneralChatType,
}); });
} }
@ -222,10 +224,10 @@ namespace Dalamud.Game.Gui
} }
var senderRaw = (chat.Name ?? string.Empty).Encode(); var senderRaw = (chat.Name ?? string.Empty).Encode();
using var senderOwned = Service<LibcFunction>.Get().NewString(senderRaw); using var senderOwned = this.libcFunction.NewString(senderRaw);
var messageRaw = (chat.Message ?? string.Empty).Encode(); var messageRaw = (chat.Message ?? string.Empty).Encode();
using var messageOwned = Service<LibcFunction>.Get().NewString(messageRaw); using var messageOwned = this.libcFunction.NewString(messageRaw);
this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters); this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters);
} }
@ -364,7 +366,7 @@ namespace Dalamud.Game.Gui
if (!Util.FastByteArrayCompare(originalMessageData, message.RawData)) if (!Util.FastByteArrayCompare(originalMessageData, message.RawData))
{ {
allocatedString = Service<LibcFunction>.Get().NewString(message.RawData); allocatedString = this.libcFunction.NewString(message.RawData);
Log.Debug($"HandlePrintMessageDetour String modified: {originalMessageData}({messagePtr}) -> {message}({allocatedString.Address})"); Log.Debug($"HandlePrintMessageDetour String modified: {originalMessageData}({messagePtr}) -> {message}({allocatedString.Address})");
messagePtr = allocatedString.Address; messagePtr = allocatedString.Address;
} }
@ -379,7 +381,7 @@ namespace Dalamud.Game.Gui
if (!Util.FastByteArrayCompare(originalSenderData, sender.RawData)) if (!Util.FastByteArrayCompare(originalSenderData, sender.RawData))
{ {
allocatedStringSender = Service<LibcFunction>.Get().NewString(sender.RawData); allocatedStringSender = this.libcFunction.NewString(sender.RawData);
Log.Debug( Log.Debug(
$"HandlePrintMessageDetour Sender modified: {originalSenderData}({senderPtr}) -> {sender}({allocatedStringSender.Address})"); $"HandlePrintMessageDetour Sender modified: {originalSenderData}({senderPtr}) -> {sender}({allocatedStringSender.Address})");
senderPtr = allocatedStringSender.Address; senderPtr = allocatedStringSender.Address;

View file

@ -58,11 +58,11 @@ namespace Dalamud.Game.Gui.ContextMenus
{ {
this.openSubContextMenu = Marshal.GetDelegateForFunctionPointer<OpenSubContextMenuDelegate>(this.Address.OpenSubContextMenuPtr); this.openSubContextMenu = Marshal.GetDelegateForFunctionPointer<OpenSubContextMenuDelegate>(this.Address.OpenSubContextMenuPtr);
this.contextMenuOpeningHook = new Hook<ContextMenuOpeningDelegate>(this.Address.ContextMenuOpeningPtr, this.ContextMenuOpeningDetour); this.contextMenuOpeningHook = Hook<ContextMenuOpeningDelegate>.FromAddress(this.Address.ContextMenuOpeningPtr, this.ContextMenuOpeningDetour);
this.contextMenuOpenedHook = new Hook<ContextMenuOpenedDelegate>(this.Address.ContextMenuOpenedPtr, this.ContextMenuOpenedDetour); this.contextMenuOpenedHook = Hook<ContextMenuOpenedDelegate>.FromAddress(this.Address.ContextMenuOpenedPtr, this.ContextMenuOpenedDetour);
this.contextMenuItemSelectedHook = new Hook<ContextMenuItemSelectedDelegate>(this.Address.ContextMenuItemSelectedPtr, this.ContextMenuItemSelectedDetour); this.contextMenuItemSelectedHook = Hook<ContextMenuItemSelectedDelegate>.FromAddress(this.Address.ContextMenuItemSelectedPtr, this.ContextMenuItemSelectedDetour);
this.subContextMenuOpeningHook = new Hook<SubContextMenuOpeningDelegate>(this.Address.SubContextMenuOpeningPtr, this.SubContextMenuOpeningDetour); this.subContextMenuOpeningHook = Hook<SubContextMenuOpeningDelegate>.FromAddress(this.Address.SubContextMenuOpeningPtr, this.SubContextMenuOpeningDetour);
this.subContextMenuOpenedHook = new Hook<ContextMenuOpenedDelegate>(this.Address.SubContextMenuOpenedPtr, this.SubContextMenuOpenedDetour); this.subContextMenuOpenedHook = Hook<ContextMenuOpenedDelegate>.FromAddress(this.Address.SubContextMenuOpenedPtr, this.SubContextMenuOpenedDetour);
} }
} }

View file

@ -22,17 +22,26 @@ namespace Dalamud.Game.Gui.Dtr
{ {
private const uint BaseNodeId = 1000; private const uint BaseNodeId = 1000;
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
[ServiceManager.ServiceDependency]
private readonly GameGui gameGui = Service<GameGui>.Get();
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private List<DtrBarEntry> entries = new(); private List<DtrBarEntry> entries = new();
private uint runningNodeIds = BaseNodeId; private uint runningNodeIds = BaseNodeId;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private DtrBar(DalamudConfiguration configuration, Framework framework) private DtrBar()
{ {
framework.Update += this.Update; this.framework.Update += this.Update;
configuration.DtrOrder ??= new List<string>(); this.configuration.DtrOrder ??= new List<string>();
configuration.DtrIgnore ??= new List<string>(); this.configuration.DtrIgnore ??= new List<string>();
configuration.Save(); this.configuration.Save();
} }
/// <summary> /// <summary>
@ -48,14 +57,13 @@ namespace Dalamud.Game.Gui.Dtr
if (this.entries.Any(x => x.Title == title)) if (this.entries.Any(x => x.Title == title))
throw new ArgumentException("An entry with the same title already exists."); throw new ArgumentException("An entry with the same title already exists.");
var configuration = Service<DalamudConfiguration>.Get();
var node = this.MakeNode(++this.runningNodeIds); var node = this.MakeNode(++this.runningNodeIds);
var entry = new DtrBarEntry(title, node); var entry = new DtrBarEntry(title, node);
entry.Text = text; entry.Text = text;
// Add the entry to the end of the order list, if it's not there already. // Add the entry to the end of the order list, if it's not there already.
if (!configuration.DtrOrder!.Contains(title)) if (!this.configuration.DtrOrder!.Contains(title))
configuration.DtrOrder!.Add(title); this.configuration.DtrOrder!.Add(title);
this.entries.Add(entry); this.entries.Add(entry);
this.ApplySort(); this.ApplySort();
@ -69,7 +77,7 @@ namespace Dalamud.Game.Gui.Dtr
this.RemoveNode(entry.TextNode); this.RemoveNode(entry.TextNode);
this.entries.Clear(); this.entries.Clear();
Service<Framework>.Get().Update -= this.Update; this.framework.Update -= this.Update;
} }
/// <summary> /// <summary>
@ -112,12 +120,11 @@ namespace Dalamud.Game.Gui.Dtr
/// </summary> /// </summary>
internal void ApplySort() internal void ApplySort()
{ {
var configuration = Service<DalamudConfiguration>.Get();
// Sort the current entry list, based on the order in the configuration. // Sort the current entry list, based on the order in the configuration.
var positions = configuration.DtrOrder! var positions = this.configuration
.Select(entry => (entry, index: configuration.DtrOrder!.IndexOf(entry))) .DtrOrder!
.ToDictionary(x => x.entry, x => x.index); .Select(entry => (entry, index: this.configuration.DtrOrder!.IndexOf(entry)))
.ToDictionary(x => x.entry, x => x.index);
this.entries.Sort((x, y) => this.entries.Sort((x, y) =>
{ {
@ -127,13 +134,13 @@ namespace Dalamud.Game.Gui.Dtr
}); });
} }
private static AtkUnitBase* GetDtr() => (AtkUnitBase*)Service<GameGui>.Get().GetAddonByName("_DTR", 1).ToPointer(); private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR", 1).ToPointer();
private void Update(Framework unused) private void Update(Framework unused)
{ {
this.HandleRemovedNodes(); this.HandleRemovedNodes();
var dtr = GetDtr(); var dtr = this.GetDtr();
if (dtr == null) return; if (dtr == null) return;
// The collision node on the DTR element is always the width of its content // The collision node on the DTR element is always the width of its content
@ -147,16 +154,16 @@ namespace Dalamud.Game.Gui.Dtr
var collisionNode = dtr->UldManager.NodeList[1]; var collisionNode = dtr->UldManager.NodeList[1];
if (collisionNode == null) return; if (collisionNode == null) return;
var configuration = Service<DalamudConfiguration>.Get();
// If we are drawing backwards, we should start from the right side of the collision node. That is, // If we are drawing backwards, we should start from the right side of the collision node. That is,
// collisionNode->X + collisionNode->Width. // collisionNode->X + collisionNode->Width.
var runningXPos = configuration.DtrSwapDirection ? collisionNode->X + collisionNode->Width : collisionNode->X; var runningXPos = this.configuration.DtrSwapDirection
? collisionNode->X + collisionNode->Width
: collisionNode->X;
for (var i = 0; i < this.entries.Count; i++) for (var i = 0; i < this.entries.Count; i++)
{ {
var data = this.entries[i]; var data = this.entries[i];
var isHide = configuration.DtrIgnore!.Any(x => x == data.Title) || !data.Shown; var isHide = this.configuration.DtrIgnore!.Any(x => x == data.Title) || !data.Shown;
if (data.Dirty && data.Added && data.Text != null && data.TextNode != null) if (data.Dirty && data.Added && data.Text != null && data.TextNode != null)
{ {
@ -185,9 +192,9 @@ namespace Dalamud.Game.Gui.Dtr
if (!isHide) if (!isHide)
{ {
var elementWidth = data.TextNode->AtkResNode.Width + configuration.DtrSpacing; var elementWidth = data.TextNode->AtkResNode.Width + this.configuration.DtrSpacing;
if (configuration.DtrSwapDirection) if (this.configuration.DtrSwapDirection)
{ {
data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2); data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2);
runningXPos += elementWidth; runningXPos += elementWidth;
@ -209,7 +216,7 @@ namespace Dalamud.Game.Gui.Dtr
/// <returns>True if there are nodes with an ID > 1000.</returns> /// <returns>True if there are nodes with an ID > 1000.</returns>
private bool CheckForDalamudNodes() private bool CheckForDalamudNodes()
{ {
var dtr = GetDtr(); var dtr = this.GetDtr();
if (dtr == null || dtr->RootNode == null) return false; if (dtr == null || dtr->RootNode == null) return false;
for (var i = 0; i < dtr->UldManager.NodeListCount; i++) for (var i = 0; i < dtr->UldManager.NodeListCount; i++)
@ -233,7 +240,7 @@ namespace Dalamud.Game.Gui.Dtr
private bool AddNode(AtkTextNode* node) private bool AddNode(AtkTextNode* node)
{ {
var dtr = GetDtr(); var dtr = this.GetDtr();
if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false;
var lastChild = dtr->RootNode->ChildNode; var lastChild = dtr->RootNode->ChildNode;
@ -253,7 +260,7 @@ namespace Dalamud.Game.Gui.Dtr
private bool RemoveNode(AtkTextNode* node) private bool RemoveNode(AtkTextNode* node)
{ {
var dtr = GetDtr(); var dtr = this.GetDtr();
if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false;
var tmpPrevNode = node->AtkResNode.PrevSiblingNode; var tmpPrevNode = node->AtkResNode.PrevSiblingNode;

View file

@ -36,7 +36,7 @@ namespace Dalamud.Game.Gui.FlyText
this.Address.Setup(sigScanner); this.Address.Setup(sigScanner);
this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer<AddFlyTextDelegate>(this.Address.AddFlyText); this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer<AddFlyTextDelegate>(this.Address.AddFlyText);
this.createFlyTextHook = new Hook<CreateFlyTextDelegate>(this.Address.CreateFlyText, this.CreateFlyTextDetour); this.createFlyTextHook = Hook<CreateFlyTextDelegate>.FromAddress(this.Address.CreateFlyText, this.CreateFlyTextDetour);
} }
/// <summary> /// <summary>

View file

@ -60,23 +60,23 @@ namespace Dalamud.Game.Gui
Log.Verbose($"HandleItemOut address 0x{this.address.HandleItemOut.ToInt64():X}"); Log.Verbose($"HandleItemOut address 0x{this.address.HandleItemOut.ToInt64():X}");
Log.Verbose($"HandleImm address 0x{this.address.HandleImm.ToInt64():X}"); Log.Verbose($"HandleImm address 0x{this.address.HandleImm.ToInt64():X}");
this.setGlobalBgmHook = new Hook<SetGlobalBgmDelegate>(this.address.SetGlobalBgm, this.HandleSetGlobalBgmDetour); this.setGlobalBgmHook = Hook<SetGlobalBgmDelegate>.FromAddress(this.address.SetGlobalBgm, this.HandleSetGlobalBgmDetour);
this.handleItemHoverHook = new Hook<HandleItemHoverDelegate>(this.address.HandleItemHover, this.HandleItemHoverDetour); this.handleItemHoverHook = Hook<HandleItemHoverDelegate>.FromAddress(this.address.HandleItemHover, this.HandleItemHoverDetour);
this.handleItemOutHook = new Hook<HandleItemOutDelegate>(this.address.HandleItemOut, this.HandleItemOutDetour); this.handleItemOutHook = Hook<HandleItemOutDelegate>.FromAddress(this.address.HandleItemOut, this.HandleItemOutDetour);
this.handleActionHoverHook = new Hook<HandleActionHoverDelegate>(this.address.HandleActionHover, this.HandleActionHoverDetour); this.handleActionHoverHook = Hook<HandleActionHoverDelegate>.FromAddress(this.address.HandleActionHover, this.HandleActionHoverDetour);
this.handleActionOutHook = new Hook<HandleActionOutDelegate>(this.address.HandleActionOut, this.HandleActionOutDetour); this.handleActionOutHook = Hook<HandleActionOutDelegate>.FromAddress(this.address.HandleActionOut, this.HandleActionOutDetour);
this.handleImmHook = new Hook<HandleImmDelegate>(this.address.HandleImm, this.HandleImmDetour); this.handleImmHook = Hook<HandleImmDelegate>.FromAddress(this.address.HandleImm, this.HandleImmDetour);
this.getMatrixSingleton = Marshal.GetDelegateForFunctionPointer<GetMatrixSingletonDelegate>(this.address.GetMatrixSingleton); this.getMatrixSingleton = Marshal.GetDelegateForFunctionPointer<GetMatrixSingletonDelegate>(this.address.GetMatrixSingleton);
this.screenToWorldNative = Marshal.GetDelegateForFunctionPointer<ScreenToWorldNativeDelegate>(this.address.ScreenToWorld); this.screenToWorldNative = Marshal.GetDelegateForFunctionPointer<ScreenToWorldNativeDelegate>(this.address.ScreenToWorld);
this.toggleUiHideHook = new Hook<ToggleUiHideDelegate>(this.address.ToggleUiHide, this.ToggleUiHideDetour); this.toggleUiHideHook = Hook<ToggleUiHideDelegate>.FromAddress(this.address.ToggleUiHide, this.ToggleUiHideDetour);
this.utf8StringFromSequenceHook = new Hook<Utf8StringFromSequenceDelegate>(this.address.Utf8StringFromSequence, this.Utf8StringFromSequenceDetour); this.utf8StringFromSequenceHook = Hook<Utf8StringFromSequenceDelegate>.FromAddress(this.address.Utf8StringFromSequence, this.Utf8StringFromSequenceDetour);
} }
// Marshaled delegates // Marshaled delegates
@ -436,12 +436,6 @@ namespace Dalamud.Game.Gui
/// </summary> /// </summary>
void IDisposable.Dispose() void IDisposable.Dispose()
{ {
Service<ChatGui>.Get().ExplicitDispose();
Service<ToastGui>.Get().ExplicitDispose();
Service<FlyTextGui>.Get().ExplicitDispose();
Service<PartyFinderGui>.Get().ExplicitDispose();
Service<ContextMenu>.Get().ExplicitDispose();
Service<DtrBar>.Get().ExplicitDispose();
this.setGlobalBgmHook.Dispose(); this.setGlobalBgmHook.Dispose();
this.handleItemHoverHook.Dispose(); this.handleItemHoverHook.Dispose();
this.handleItemOutHook.Dispose(); this.handleItemOutHook.Dispose();

View file

@ -266,9 +266,9 @@ namespace Dalamud.Game.Gui.Internal
private void ToggleWindow(bool visible) private void ToggleWindow(bool visible)
{ {
if (visible) if (visible)
Service<DalamudInterface>.Get().OpenImeWindow(); Service<DalamudInterface>.GetNullable()?.OpenImeWindow();
else else
Service<DalamudInterface>.Get().CloseImeWindow(); Service<DalamudInterface>.GetNullable()?.CloseImeWindow();
} }
} }
} }

View file

@ -35,7 +35,7 @@ namespace Dalamud.Game.Gui.PartyFinder
this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize); this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize);
this.receiveListingHook = new Hook<ReceiveListingDelegate>(this.address.ReceiveListing, new ReceiveListingDelegate(this.HandleReceiveListingDetour)); this.receiveListingHook = Hook<ReceiveListingDelegate>.FromAddress(this.address.ReceiveListing, new ReceiveListingDelegate(this.HandleReceiveListingDetour));
} }
/// <summary> /// <summary>

View file

@ -39,9 +39,9 @@ namespace Dalamud.Game.Gui.Toast
this.address = new ToastGuiAddressResolver(); this.address = new ToastGuiAddressResolver();
this.address.Setup(sigScanner); this.address.Setup(sigScanner);
this.showNormalToastHook = new Hook<ShowNormalToastDelegate>(this.address.ShowNormalToast, new ShowNormalToastDelegate(this.HandleNormalToastDetour)); this.showNormalToastHook = Hook<ShowNormalToastDelegate>.FromAddress(this.address.ShowNormalToast, new ShowNormalToastDelegate(this.HandleNormalToastDetour));
this.showQuestToastHook = new Hook<ShowQuestToastDelegate>(this.address.ShowQuestToast, new ShowQuestToastDelegate(this.HandleQuestToastDetour)); this.showQuestToastHook = Hook<ShowQuestToastDelegate>.FromAddress(this.address.ShowQuestToast, new ShowQuestToastDelegate(this.HandleQuestToastDetour));
this.showErrorToastHook = new Hook<ShowErrorToastDelegate>(this.address.ShowErrorToast, new ShowErrorToastDelegate(this.HandleErrorToastDetour)); this.showErrorToastHook = Hook<ShowErrorToastDelegate>.FromAddress(this.address.ShowErrorToast, new ShowErrorToastDelegate(this.HandleErrorToastDetour));
} }
#region Event delegates #region Event delegates

View file

@ -9,7 +9,6 @@ using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Interface;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
@ -35,15 +34,21 @@ namespace Dalamud.Game.Internal
private readonly Hook<AtkUnitBaseReceiveGlobalEvent> hookAtkUnitBaseReceiveGlobalEvent; private readonly Hook<AtkUnitBaseReceiveGlobalEvent> hookAtkUnitBaseReceiveGlobalEvent;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
[ServiceManager.ServiceDependency]
private readonly ContextMenu contextMenu = Service<ContextMenu>.Get();
private readonly string locDalamudPlugins; private readonly string locDalamudPlugins;
private readonly string locDalamudSettings; private readonly string locDalamudSettings;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private DalamudAtkTweaks(SigScanner sigScanner, ContextMenu contextMenu) private DalamudAtkTweaks(SigScanner sigScanner)
{ {
var openSystemMenuAddress = sigScanner.ScanText("E8 ?? ?? ?? ?? 32 C0 4C 8B AC 24 ?? ?? ?? ?? 48 8B 8D ?? ?? ?? ??"); var openSystemMenuAddress = sigScanner.ScanText("E8 ?? ?? ?? ?? 32 C0 4C 8B AC 24 ?? ?? ?? ?? 48 8B 8D ?? ?? ?? ??");
this.hookAgentHudOpenSystemMenu = new Hook<AgentHudOpenSystemMenuPrototype>(openSystemMenuAddress, this.AgentHudOpenSystemMenuDetour); this.hookAgentHudOpenSystemMenu = Hook<AgentHudOpenSystemMenuPrototype>.FromAddress(openSystemMenuAddress, this.AgentHudOpenSystemMenuDetour);
var atkValueChangeTypeAddress = sigScanner.ScanText("E8 ?? ?? ?? ?? 45 84 F6 48 8D 4C 24 ??"); var atkValueChangeTypeAddress = sigScanner.ScanText("E8 ?? ?? ?? ?? 45 84 F6 48 8D 4C 24 ??");
this.atkValueChangeType = Marshal.GetDelegateForFunctionPointer<AtkValueChangeType>(atkValueChangeTypeAddress); this.atkValueChangeType = Marshal.GetDelegateForFunctionPointer<AtkValueChangeType>(atkValueChangeTypeAddress);
@ -52,15 +57,15 @@ namespace Dalamud.Game.Internal
this.atkValueSetString = Marshal.GetDelegateForFunctionPointer<AtkValueSetString>(atkValueSetStringAddress); this.atkValueSetString = Marshal.GetDelegateForFunctionPointer<AtkValueSetString>(atkValueSetStringAddress);
var uiModuleRequestMainCommandAddress = sigScanner.ScanText("40 53 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B 01 8B DA 48 8B F1 FF 90 ?? ?? ?? ??"); var uiModuleRequestMainCommandAddress = sigScanner.ScanText("40 53 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B 01 8B DA 48 8B F1 FF 90 ?? ?? ?? ??");
this.hookUiModuleRequestMainCommand = new Hook<UiModuleRequestMainCommand>(uiModuleRequestMainCommandAddress, this.UiModuleRequestMainCommandDetour); this.hookUiModuleRequestMainCommand = Hook<UiModuleRequestMainCommand>.FromAddress(uiModuleRequestMainCommandAddress, this.UiModuleRequestMainCommandDetour);
var atkUnitBaseReceiveGlobalEventAddress = sigScanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 50 44 0F B7 F2 "); var atkUnitBaseReceiveGlobalEventAddress = sigScanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 50 44 0F B7 F2 ");
this.hookAtkUnitBaseReceiveGlobalEvent = new Hook<AtkUnitBaseReceiveGlobalEvent>(atkUnitBaseReceiveGlobalEventAddress, this.AtkUnitBaseReceiveGlobalEventDetour); this.hookAtkUnitBaseReceiveGlobalEvent = Hook<AtkUnitBaseReceiveGlobalEvent>.FromAddress(atkUnitBaseReceiveGlobalEventAddress, this.AtkUnitBaseReceiveGlobalEventDetour);
this.locDalamudPlugins = Loc.Localize("SystemMenuPlugins", "Dalamud Plugins"); this.locDalamudPlugins = Loc.Localize("SystemMenuPlugins", "Dalamud Plugins");
this.locDalamudSettings = Loc.Localize("SystemMenuSettings", "Dalamud Settings"); this.locDalamudSettings = Loc.Localize("SystemMenuSettings", "Dalamud Settings");
contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened; this.contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened;
} }
private delegate void AgentHudOpenSystemMenuPrototype(void* thisPtr, AtkValue* atkValueArgs, uint menuSize); private delegate void AgentHudOpenSystemMenuPrototype(void* thisPtr, AtkValue* atkValueArgs, uint menuSize);
@ -83,11 +88,13 @@ namespace Dalamud.Game.Internal
private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args) private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args)
{ {
var systemText = Service<DataManager>.Get().GetExcelSheet<Addon>()!.GetRow(1059)!.Text.RawString; // "System" var systemText = Service<DataManager>.GetNullable()?.GetExcelSheet<Addon>()?.GetRow(1059)?.Text?.RawString; // "System"
var configuration = Service<DalamudConfiguration>.Get(); var interfaceManager = Service<InterfaceManager>.GetNullable();
var interfaceManager = Service<InterfaceManager>.Get();
if (args.Title == systemText && configuration.DoButtonsSystemMenu && interfaceManager.IsDispatchingEvents) if (systemText == null || interfaceManager == null)
return;
if (args.Title == systemText && this.configuration.DoButtonsSystemMenu && interfaceManager.IsDispatchingEvents)
{ {
var dalamudInterface = Service<DalamudInterface>.Get(); var dalamudInterface = Service<DalamudInterface>.Get();
@ -109,7 +116,7 @@ namespace Dalamud.Game.Internal
// "SendHotkey" // "SendHotkey"
// 3 == Close // 3 == Close
if (cmd == 12 && WindowSystem.HasAnyWindowSystemFocus && *arg == 3 && Service<DalamudConfiguration>.Get().IsFocusManagementEnabled) if (cmd == 12 && WindowSystem.HasAnyWindowSystemFocus && *arg == 3 && this.configuration.IsFocusManagementEnabled)
{ {
Log.Verbose($"Cancelling global event SendHotkey command due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}"); Log.Verbose($"Cancelling global event SendHotkey command due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}");
return IntPtr.Zero; return IntPtr.Zero;
@ -120,14 +127,18 @@ namespace Dalamud.Game.Internal
private void AgentHudOpenSystemMenuDetour(void* thisPtr, AtkValue* atkValueArgs, uint menuSize) private void AgentHudOpenSystemMenuDetour(void* thisPtr, AtkValue* atkValueArgs, uint menuSize)
{ {
if (WindowSystem.HasAnyWindowSystemFocus && Service<DalamudConfiguration>.Get().IsFocusManagementEnabled) if (WindowSystem.HasAnyWindowSystemFocus && this.configuration.IsFocusManagementEnabled)
{ {
Log.Verbose($"Cancelling OpenSystemMenu due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}"); Log.Verbose($"Cancelling OpenSystemMenu due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}");
return; return;
} }
var configuration = Service<DalamudConfiguration>.Get(); var interfaceManager = Service<InterfaceManager>.GetNullable();
var interfaceManager = Service<InterfaceManager>.Get(); if (interfaceManager == null)
{
this.hookAgentHudOpenSystemMenu.Original(thisPtr, atkValueArgs, menuSize);
return;
}
if (!configuration.DoButtonsSystemMenu || !interfaceManager.IsDispatchingEvents) if (!configuration.DoButtonsSystemMenu || !interfaceManager.IsDispatchingEvents)
{ {
@ -207,15 +218,15 @@ namespace Dalamud.Game.Internal
private void UiModuleRequestMainCommandDetour(void* thisPtr, int commandId) private void UiModuleRequestMainCommandDetour(void* thisPtr, int commandId)
{ {
var dalamudInterface = Service<DalamudInterface>.Get(); var dalamudInterface = Service<DalamudInterface>.GetNullable();
switch (commandId) switch (commandId)
{ {
case 69420: case 69420:
dalamudInterface.TogglePluginInstallerWindow(); dalamudInterface?.TogglePluginInstallerWindow();
break; break;
case 69421: case 69421:
dalamudInterface.ToggleSettingsWindow(); dalamudInterface?.ToggleSettingsWindow();
break; break;
default: default:
this.hookUiModuleRequestMainCommand.Original(thisPtr, commandId); this.hookUiModuleRequestMainCommand.Original(thisPtr, commandId);
@ -259,7 +270,7 @@ namespace Dalamud.Game.Internal
this.hookUiModuleRequestMainCommand.Dispose(); this.hookUiModuleRequestMainCommand.Dispose();
this.hookAtkUnitBaseReceiveGlobalEvent.Dispose(); this.hookAtkUnitBaseReceiveGlobalEvent.Dispose();
Service<ContextMenu>.Get().ContextMenuOpened -= this.ContextMenuOnContextMenuOpened; this.contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened;
} }
this.disposed = true; this.disposed = true;

View file

@ -34,8 +34,8 @@ namespace Dalamud.Game.Network
Log.Verbose($"ProcessZonePacketDown address 0x{this.address.ProcessZonePacketDown.ToInt64():X}"); Log.Verbose($"ProcessZonePacketDown address 0x{this.address.ProcessZonePacketDown.ToInt64():X}");
Log.Verbose($"ProcessZonePacketUp address 0x{this.address.ProcessZonePacketUp.ToInt64():X}"); Log.Verbose($"ProcessZonePacketUp address 0x{this.address.ProcessZonePacketUp.ToInt64():X}");
this.processZonePacketDownHook = new Hook<ProcessZonePacketDownDelegate>(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour); this.processZonePacketDownHook = Hook<ProcessZonePacketDownDelegate>.FromAddress(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour);
this.processZonePacketUpHook = new Hook<ProcessZonePacketUpDelegate>(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour); this.processZonePacketUpHook = Hook<ProcessZonePacketUpDelegate>.FromAddress(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour);
} }
/// <summary> /// <summary>

View file

@ -32,7 +32,9 @@ namespace Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis
/// <inheritdoc/> /// <inheritdoc/>
public async Task Upload(MarketBoardItemRequest request) public async Task Upload(MarketBoardItemRequest request)
{ {
var clientState = Service<ClientState.ClientState>.Get(); var clientState = Service<ClientState.ClientState>.GetNullable();
if (clientState == null)
return;
Log.Verbose("Starting Universalis upload."); Log.Verbose("Starting Universalis upload.");
var uploader = clientState.LocalContentId; var uploader = clientState.LocalContentId;
@ -118,7 +120,9 @@ namespace Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis
/// <inheritdoc/> /// <inheritdoc/>
public async Task UploadTax(MarketTaxRates taxRates) public async Task UploadTax(MarketTaxRates taxRates)
{ {
var clientState = Service<ClientState.ClientState>.Get(); var clientState = Service<ClientState.ClientState>.GetNullable();
if (clientState == null)
return;
// ==================================================================================== // ====================================================================================
@ -157,7 +161,9 @@ namespace Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis
/// </remarks> /// </remarks>
public async Task UploadPurchase(MarketBoardPurchaseHandler purchaseHandler) public async Task UploadPurchase(MarketBoardPurchaseHandler purchaseHandler)
{ {
var clientState = Service<ClientState.ClientState>.Get(); var clientState = Service<ClientState.ClientState>.GetNullable();
if (clientState == null)
return;
var itemId = purchaseHandler.CatalogId; var itemId = purchaseHandler.CatalogId;
var worldId = clientState.LocalPlayer?.CurrentWorld.Id ?? 0; var worldId = clientState.LocalPlayer?.CurrentWorld.Id ?? 0;

View file

@ -28,6 +28,9 @@ namespace Dalamud.Game.Network.Internal
private readonly IMarketBoardUploader uploader; private readonly IMarketBoardUploader uploader;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private MarketBoardPurchaseHandler marketBoardPurchaseHandler; private MarketBoardPurchaseHandler marketBoardPurchaseHandler;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
@ -47,14 +50,12 @@ namespace Dalamud.Game.Network.Internal
{ {
var dataManager = Service<DataManager>.GetNullable(); var dataManager = Service<DataManager>.GetNullable();
if (dataManager?.IsDataReady == false) if (dataManager?.IsDataReady != true)
return; return;
var configuration = Service<DalamudConfiguration>.Get();
if (direction == NetworkMessageDirection.ZoneUp) if (direction == NetworkMessageDirection.ZoneUp)
{ {
if (configuration.IsMbCollect) if (this.configuration.IsMbCollect)
{ {
if (opCode == dataManager.ClientOpCodes["MarketBoardPurchaseHandler"]) if (opCode == dataManager.ClientOpCodes["MarketBoardPurchaseHandler"])
{ {
@ -72,7 +73,7 @@ namespace Dalamud.Game.Network.Internal
return; return;
} }
if (configuration.IsMbCollect) if (this.configuration.IsMbCollect)
{ {
if (opCode == dataManager.ServerOpCodes["MarketBoardItemRequestStart"]) if (opCode == dataManager.ServerOpCodes["MarketBoardItemRequestStart"])
{ {
@ -236,8 +237,9 @@ namespace Dalamud.Game.Network.Internal
private unsafe void HandleCfPop(IntPtr dataPtr) private unsafe void HandleCfPop(IntPtr dataPtr)
{ {
var dataManager = Service<DataManager>.Get(); var dataManager = Service<DataManager>.GetNullable();
var configuration = Service<DalamudConfiguration>.Get(); if (dataManager == null)
return;
using var stream = new UnmanagedMemoryStream((byte*)dataPtr.ToPointer(), 64); using var stream = new UnmanagedMemoryStream((byte*)dataPtr.ToPointer(), 64);
using var reader = new BinaryReader(stream); using var reader = new BinaryReader(stream);
@ -266,7 +268,7 @@ namespace Dalamud.Game.Network.Internal
} }
// Flash window // Flash window
if (configuration.DutyFinderTaskbarFlash && !NativeFunctions.ApplicationIsActivated()) if (this.configuration.DutyFinderTaskbarFlash && !NativeFunctions.ApplicationIsActivated())
{ {
var flashInfo = new NativeFunctions.FlashWindowInfo var flashInfo = new NativeFunctions.FlashWindowInfo
{ {
@ -281,9 +283,9 @@ namespace Dalamud.Game.Network.Internal
Task.Run(() => Task.Run(() =>
{ {
if (configuration.DutyFinderChatMessage) if (this.configuration.DutyFinderChatMessage)
{ {
Service<ChatGui>.Get().Print($"Duty pop: {cfcName}"); Service<ChatGui>.GetNullable()?.Print($"Duty pop: {cfcName}");
} }
this.CfPop?.Invoke(this, cfCondition); this.CfPop?.Invoke(this, cfCondition);

View file

@ -18,7 +18,7 @@ namespace Dalamud.Game.Network.Internal
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private WinSockHandlers() private WinSockHandlers()
{ {
this.ws2SocketHook = Hook<SocketDelegate>.FromImport(Process.GetCurrentProcess().MainModule, "ws2_32.dll", "socket", 23, this.OnSocket); this.ws2SocketHook = Hook<SocketDelegate>.FromImport(null, "ws2_32.dll", "socket", 23, this.OnSocket);
this.ws2SocketHook?.Enable(); this.ws2SocketHook?.Enable();
} }

View file

@ -359,6 +359,7 @@ namespace Dalamud.Game
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
this.Save();
Marshal.FreeHGlobal(this.moduleCopyPtr); Marshal.FreeHGlobal(this.moduleCopyPtr);
} }
@ -370,7 +371,14 @@ namespace Dalamud.Game
if (this.cacheFile == null) if (this.cacheFile == null)
return; return;
File.WriteAllText(this.cacheFile.FullName, JsonConvert.SerializeObject(this.textCache)); try
{
File.WriteAllText(this.cacheFile.FullName, JsonConvert.SerializeObject(this.textCache));
}
catch (Exception e)
{
Log.Warning(e, "Failed to save cache to {0}", this.cacheFile);
}
} }
/// <summary> /// <summary>

View file

@ -88,6 +88,22 @@ namespace Dalamud.Hooking
/// <exception cref="ObjectDisposedException">Hook is already disposed.</exception> /// <exception cref="ObjectDisposedException">Hook is already disposed.</exception>
public virtual T Original => this.compatHookImpl != null ? this.compatHookImpl!.Original : throw new NotImplementedException(); public virtual T Original => this.compatHookImpl != null ? this.compatHookImpl!.Original : throw new NotImplementedException();
/// <summary>
/// Gets a delegate function that can be used to call the actual function as if function is not hooked yet.
/// This can be called even after Dispose.
/// </summary>
public T OriginalDisposeSafe
{
get
{
if (this.compatHookImpl != null)
return this.compatHookImpl!.OriginalDisposeSafe;
if (this.IsDisposed)
return Marshal.GetDelegateForFunctionPointer<T>(this.address);
return this.Original;
}
}
/// <summary> /// <summary>
/// Gets a value indicating whether or not the hook is enabled. /// Gets a value indicating whether or not the hook is enabled.
/// </summary> /// </summary>
@ -115,14 +131,17 @@ namespace Dalamud.Hooking
/// <summary> /// <summary>
/// Creates a hook by rewriting import table address. /// Creates a hook by rewriting import table address.
/// </summary> /// </summary>
/// <param name="module">Module to check for.</param> /// <param name="module">Module to check for. Current process' main module if null.</param>
/// <param name="moduleName">Name of the DLL, including the extension.</param> /// <param name="moduleName">Name of the DLL, including the extension.</param>
/// <param name="functionName">Decorated name of the function.</param> /// <param name="functionName">Decorated name of the function.</param>
/// <param name="hintOrOrdinal">Hint or ordinal. 0 to unspecify.</param> /// <param name="hintOrOrdinal">Hint or ordinal. 0 to unspecify.</param>
/// <param name="detour">Callback function. Delegate must have a same original function prototype.</param> /// <param name="detour">Callback function. Delegate must have a same original function prototype.</param>
/// <returns>The hook with the supplied parameters.</returns> /// <returns>The hook with the supplied parameters.</returns>
public static unsafe Hook<T> FromImport(ProcessModule module, string moduleName, string functionName, uint hintOrOrdinal, T detour) public static unsafe Hook<T> FromImport(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour)
{ {
module ??= Process.GetCurrentProcess().MainModule;
if (module == null)
throw new InvalidOperationException("Current module is null?");
var pDos = (PeHeader.IMAGE_DOS_HEADER*)module.BaseAddress; var pDos = (PeHeader.IMAGE_DOS_HEADER*)module.BaseAddress;
var pNt = (PeHeader.IMAGE_FILE_HEADER*)(module.BaseAddress + (int)pDos->e_lfanew + 4); var pNt = (PeHeader.IMAGE_FILE_HEADER*)(module.BaseAddress + (int)pDos->e_lfanew + 4);
var isPe64 = pNt->SizeOfOptionalHeader == Marshal.SizeOf<PeHeader.IMAGE_OPTIONAL_HEADER64>(); var isPe64 = pNt->SizeOfOptionalHeader == Marshal.SizeOf<PeHeader.IMAGE_OPTIONAL_HEADER64>();

View file

@ -27,24 +27,27 @@ namespace Dalamud.Hooking.Internal
internal FunctionPointerVariableHook(IntPtr address, T detour, Assembly callingAssembly) internal FunctionPointerVariableHook(IntPtr address, T detour, Assembly callingAssembly)
: base(address) : base(address)
{ {
var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address); lock (HookManager.HookEnableSyncRoot)
if (!hasOtherHooks)
{ {
MemoryHelper.ReadRaw(this.Address, 0x32, out var original); var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address);
HookManager.Originals[this.Address] = original; if (!hasOtherHooks)
{
MemoryHelper.ReadRaw(this.Address, 0x32, out var original);
HookManager.Originals[this.Address] = original;
}
if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList))
indexList = HookManager.MultiHookTracker[this.Address] = new();
this.pfnOriginal = Marshal.ReadIntPtr(this.Address);
this.originalDelegate = Marshal.GetDelegateForFunctionPointer<T>(this.pfnOriginal);
this.detourDelegate = detour;
// Add afterwards, so the hookIdent starts at 0.
indexList.Add(this);
HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly));
} }
if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList))
indexList = HookManager.MultiHookTracker[this.Address] = new();
this.pfnOriginal = Marshal.ReadIntPtr(this.Address);
this.originalDelegate = Marshal.GetDelegateForFunctionPointer<T>(this.pfnOriginal);
this.detourDelegate = detour;
// Add afterwards, so the hookIdent starts at 0.
indexList.Add(this);
HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly));
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -91,11 +94,15 @@ namespace Dalamud.Hooking.Internal
if (!this.enabled) if (!this.enabled)
{ {
if (!NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf<IntPtr>(), MemoryProtection.ExecuteReadWrite, out var oldProtect)) lock (HookManager.HookEnableSyncRoot)
throw new Win32Exception(Marshal.GetLastWin32Error()); {
if (!NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf<IntPtr>(),
MemoryProtection.ExecuteReadWrite, out var oldProtect))
throw new Win32Exception(Marshal.GetLastWin32Error());
Marshal.WriteIntPtr(this.Address, Marshal.GetFunctionPointerForDelegate(this.detourDelegate)); Marshal.WriteIntPtr(this.Address, Marshal.GetFunctionPointerForDelegate(this.detourDelegate));
NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf<IntPtr>(), oldProtect, out _); NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf<IntPtr>(), oldProtect, out _);
}
} }
} }
@ -106,11 +113,15 @@ namespace Dalamud.Hooking.Internal
if (this.enabled) if (this.enabled)
{ {
if (!NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf<IntPtr>(), MemoryProtection.ExecuteReadWrite, out var oldProtect)) lock (HookManager.HookEnableSyncRoot)
throw new Win32Exception(Marshal.GetLastWin32Error()); {
if (!NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf<IntPtr>(),
MemoryProtection.ExecuteReadWrite, out var oldProtect))
throw new Win32Exception(Marshal.GetLastWin32Error());
Marshal.WriteIntPtr(this.Address, this.pfnOriginal); Marshal.WriteIntPtr(this.Address, this.pfnOriginal);
NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf<IntPtr>(), oldProtect, out _); NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf<IntPtr>(), oldProtect, out _);
}
} }
} }
} }

View file

@ -23,6 +23,11 @@ namespace Dalamud.Hooking.Internal
{ {
} }
/// <summary>
/// Gets sync root object for hook enabling/disabling.
/// </summary>
internal static object HookEnableSyncRoot { get; } = new();
/// <summary> /// <summary>
/// Gets a static list of tracked and registered hooks. /// Gets a static list of tracked and registered hooks.
/// </summary> /// </summary>

View file

@ -22,24 +22,27 @@ namespace Dalamud.Hooking.Internal
internal MinHookHook(IntPtr address, T detour, Assembly callingAssembly) internal MinHookHook(IntPtr address, T detour, Assembly callingAssembly)
: base(address) : base(address)
{ {
var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address); lock (HookManager.HookEnableSyncRoot)
if (!hasOtherHooks)
{ {
MemoryHelper.ReadRaw(this.Address, 0x32, out var original); var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address);
HookManager.Originals[this.Address] = original; if (!hasOtherHooks)
{
MemoryHelper.ReadRaw(this.Address, 0x32, out var original);
HookManager.Originals[this.Address] = original;
}
if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList))
indexList = HookManager.MultiHookTracker[this.Address] = new();
var index = (ulong)indexList.Count;
this.minHookImpl = new MinSharp.Hook<T>(this.Address, detour, index);
// Add afterwards, so the hookIdent starts at 0.
indexList.Add(this);
HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly));
} }
if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList))
indexList = HookManager.MultiHookTracker[this.Address] = new();
var index = (ulong)indexList.Count;
this.minHookImpl = new MinSharp.Hook<T>(this.Address, detour, index);
// Add afterwards, so the hookIdent starts at 0.
indexList.Add(this);
HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly));
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -71,10 +74,13 @@ namespace Dalamud.Hooking.Internal
if (this.IsDisposed) if (this.IsDisposed)
return; return;
this.minHookImpl.Dispose(); lock (HookManager.HookEnableSyncRoot)
{
this.minHookImpl.Dispose();
var index = HookManager.MultiHookTracker[this.Address].IndexOf(this); var index = HookManager.MultiHookTracker[this.Address].IndexOf(this);
HookManager.MultiHookTracker[this.Address][index] = null; HookManager.MultiHookTracker[this.Address][index] = null;
}
base.Dispose(); base.Dispose();
} }
@ -86,7 +92,10 @@ namespace Dalamud.Hooking.Internal
if (!this.minHookImpl.Enabled) if (!this.minHookImpl.Enabled)
{ {
this.minHookImpl.Enable(); lock (HookManager.HookEnableSyncRoot)
{
this.minHookImpl.Enable();
}
} }
} }
@ -97,7 +106,10 @@ namespace Dalamud.Hooking.Internal
if (this.minHookImpl.Enabled) if (this.minHookImpl.Enabled)
{ {
this.minHookImpl.Disable(); lock (HookManager.HookEnableSyncRoot)
{
this.minHookImpl.Disable();
}
} }
} }
} }

View file

@ -19,16 +19,19 @@ namespace Dalamud.Hooking.Internal
internal ReloadedHook(IntPtr address, T detour, Assembly callingAssembly) internal ReloadedHook(IntPtr address, T detour, Assembly callingAssembly)
: base(address) : base(address)
{ {
var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address); lock (HookManager.HookEnableSyncRoot)
if (!hasOtherHooks)
{ {
MemoryHelper.ReadRaw(this.Address, 0x32, out var original); var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address);
HookManager.Originals[this.Address] = original; if (!hasOtherHooks)
{
MemoryHelper.ReadRaw(this.Address, 0x32, out var original);
HookManager.Originals[this.Address] = original;
}
this.hookImpl = ReloadedHooks.Instance.CreateHook<T>(detour, address.ToInt64());
HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly));
} }
this.hookImpl = ReloadedHooks.Instance.CreateHook<T>(detour, address.ToInt64());
HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly));
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -70,11 +73,14 @@ namespace Dalamud.Hooking.Internal
{ {
this.CheckDisposed(); this.CheckDisposed();
if (!this.hookImpl.IsHookActivated) lock (HookManager.HookEnableSyncRoot)
this.hookImpl.Activate(); {
if (!this.hookImpl.IsHookActivated)
this.hookImpl.Activate();
if (!this.hookImpl.IsHookEnabled) if (!this.hookImpl.IsHookEnabled)
this.hookImpl.Enable(); this.hookImpl.Enable();
}
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -82,11 +88,14 @@ namespace Dalamud.Hooking.Internal
{ {
this.CheckDisposed(); this.CheckDisposed();
if (!this.hookImpl.IsHookActivated) lock (HookManager.HookEnableSyncRoot)
return; {
if (!this.hookImpl.IsHookActivated)
return;
if (this.hookImpl.IsHookEnabled) if (this.hookImpl.IsHookEnabled)
this.hookImpl.Disable(); this.hookImpl.Disable();
}
} }
} }
} }

View file

@ -18,7 +18,7 @@ namespace Dalamud.Interface.GameFonts
/// Loads game font for use in ImGui. /// Loads game font for use in ImGui.
/// </summary> /// </summary>
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal class GameFontManager : IDisposable, IServiceType internal class GameFontManager : IServiceType
{ {
private static readonly string?[] FontNames = private static readonly string?[] FontNames =
{ {
@ -158,11 +158,6 @@ namespace Dalamud.Interface.GameFonts
fontPtr.BuildLookupTable(); fontPtr.BuildLookupTable();
} }
/// <inheritdoc/>
public void Dispose()
{
}
/// <summary> /// <summary>
/// Creates a new GameFontHandle, and increases internal font reference counter, and if it's first time use, then the font will be loaded on next font building process. /// Creates a new GameFontHandle, and increases internal font reference counter, and if it's first time use, then the font will be loaded on next font building process.
/// </summary> /// </summary>

View file

@ -60,8 +60,6 @@ namespace Dalamud.Interface.Internal
private readonly TextureWrap logoTexture; private readonly TextureWrap logoTexture;
private readonly TextureWrap tsmLogoTexture; private readonly TextureWrap tsmLogoTexture;
private ulong frameCount = 0;
#if DEBUG #if DEBUG
private bool isImGuiDrawDevMenu = true; private bool isImGuiDrawDevMenu = true;
#else #else
@ -80,7 +78,8 @@ namespace Dalamud.Interface.Internal
private DalamudInterface( private DalamudInterface(
Dalamud dalamud, Dalamud dalamud,
DalamudConfiguration configuration, DalamudConfiguration configuration,
InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene,
PluginImageCache pluginImageCache)
{ {
var interfaceManager = interfaceManagerWithScene.Manager; var interfaceManager = interfaceManagerWithScene.Manager;
this.WindowSystem = new WindowSystem("DalamudCore"); this.WindowSystem = new WindowSystem("DalamudCore");
@ -94,7 +93,7 @@ namespace Dalamud.Interface.Internal
this.imeWindow = new IMEWindow() { IsOpen = false }; this.imeWindow = new IMEWindow() { IsOpen = false };
this.consoleWindow = new ConsoleWindow() { IsOpen = configuration.LogOpenAtStartup }; this.consoleWindow = new ConsoleWindow() { IsOpen = configuration.LogOpenAtStartup };
this.pluginStatWindow = new PluginStatWindow() { IsOpen = false }; this.pluginStatWindow = new PluginStatWindow() { IsOpen = false };
this.pluginWindow = new PluginInstallerWindow() { IsOpen = false }; this.pluginWindow = new PluginInstallerWindow(pluginImageCache) { IsOpen = false };
this.settingsWindow = new SettingsWindow() { IsOpen = false }; this.settingsWindow = new SettingsWindow() { IsOpen = false };
this.selfTestWindow = new SelfTestWindow() { IsOpen = false }; this.selfTestWindow = new SelfTestWindow() { IsOpen = false };
this.styleEditorWindow = new StyleEditorWindow() { IsOpen = false }; this.styleEditorWindow = new StyleEditorWindow() { IsOpen = false };
@ -138,15 +137,20 @@ namespace Dalamud.Interface.Internal
this.tsmLogoTexture = tsmLogoTex; this.tsmLogoTexture = tsmLogoTex;
var tsm = Service<TitleScreenMenu>.Get(); var tsm = Service<TitleScreenMenu>.Get();
tsm.AddEntry(Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), this.tsmLogoTexture, () => this.pluginWindow.IsOpen = true); tsm.AddEntryCore(Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), this.tsmLogoTexture, () => this.pluginWindow.IsOpen = true);
tsm.AddEntry(Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), this.tsmLogoTexture, () => this.settingsWindow.IsOpen = true); tsm.AddEntryCore(Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), this.tsmLogoTexture, () => this.settingsWindow.IsOpen = true);
if (configuration.IsConventionalStaging) if (configuration.IsConventionalStaging)
{ {
tsm.AddEntry(Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), this.tsmLogoTexture, () => this.isImGuiDrawDevMenu = true); tsm.AddEntryCore(Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), this.tsmLogoTexture, () => this.isImGuiDrawDevMenu = true);
} }
} }
/// <summary>
/// Gets the number of frames since Dalamud has loaded.
/// </summary>
public ulong FrameCount { get; private set; }
/// <summary> /// <summary>
/// Gets the <see cref="WindowSystem"/> controlling all Dalamud-internal windows. /// Gets the <see cref="WindowSystem"/> controlling all Dalamud-internal windows.
/// </summary> /// </summary>
@ -373,7 +377,7 @@ namespace Dalamud.Interface.Internal
private void OnDraw() private void OnDraw()
{ {
this.frameCount++; this.FrameCount++;
#if BOOT_AGING #if BOOT_AGING
if (this.frameCount > 500 && !this.signaledBoot) if (this.frameCount > 500 && !this.signaledBoot)
@ -494,9 +498,9 @@ namespace Dalamud.Interface.Internal
{ {
foreach (var logLevel in Enum.GetValues(typeof(LogEventLevel)).Cast<LogEventLevel>()) foreach (var logLevel in Enum.GetValues(typeof(LogEventLevel)).Cast<LogEventLevel>())
{ {
if (ImGui.MenuItem(logLevel + "##logLevelSwitch", string.Empty, dalamud.LogLevelSwitch.MinimumLevel == logLevel)) if (ImGui.MenuItem(logLevel + "##logLevelSwitch", string.Empty, EntryPoint.LogLevelSwitch.MinimumLevel == logLevel))
{ {
dalamud.LogLevelSwitch.MinimumLevel = logLevel; EntryPoint.LogLevelSwitch.MinimumLevel = logLevel;
configuration.LogLevel = logLevel; configuration.LogLevel = logLevel;
configuration.Save(); configuration.Save();
} }
@ -505,6 +509,19 @@ namespace Dalamud.Interface.Internal
ImGui.EndMenu(); ImGui.EndMenu();
} }
var logSynchronously = configuration.LogSynchronously;
if (ImGui.MenuItem("Log Synchronously", null, ref logSynchronously))
{
configuration.LogSynchronously = logSynchronously;
configuration.Save();
var startupInfo = Service<DalamudStartInfo>.Get();
EntryPoint.InitLogging(
startupInfo.WorkingDirectory!,
startupInfo.BootShowConsole,
configuration.LogSynchronously);
}
var antiDebug = Service<AntiDebug>.Get(); var antiDebug = Service<AntiDebug>.Get();
if (ImGui.MenuItem("Enable AntiDebug", null, antiDebug.IsEnabled)) if (ImGui.MenuItem("Enable AntiDebug", null, antiDebug.IsEnabled))
{ {
@ -584,7 +601,7 @@ namespace Dalamud.Interface.Internal
Marshal.ReadByte(IntPtr.Zero); Marshal.ReadByte(IntPtr.Zero);
} }
if (ImGui.MenuItem("Crash game")) if (ImGui.MenuItem("Crash game (nullptr)"))
{ {
unsafe unsafe
{ {
@ -593,6 +610,15 @@ namespace Dalamud.Interface.Internal
} }
} }
if (ImGui.MenuItem("Crash game (non-nullptr)"))
{
unsafe
{
var framework = Framework.Instance();
framework->UIModule = (UIModule*)0x12345678;
}
}
ImGui.Separator(); ImGui.Separator();
var isBeta = configuration.DalamudBetaKey == DalamudConfiguration.DalamudCurrentBetaKey; var isBeta = configuration.DalamudBetaKey == DalamudConfiguration.DalamudCurrentBetaKey;
@ -795,7 +821,7 @@ namespace Dalamud.Interface.Internal
ImGui.PushFont(InterfaceManager.MonoFont); ImGui.PushFont(InterfaceManager.MonoFont);
ImGui.BeginMenu(Util.GetGitHash(), false); ImGui.BeginMenu(Util.GetGitHash(), false);
ImGui.BeginMenu(this.frameCount.ToString("000000"), false); ImGui.BeginMenu(this.FrameCount.ToString("000000"), false);
ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false); ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false);
ImGui.BeginMenu($"{Util.FormatBytes(GC.GetTotalMemory(false))}", false); ImGui.BeginMenu($"{Util.FormatBytes(GC.GetTotalMemory(false))}", false);

View file

@ -57,6 +57,9 @@ namespace Dalamud.Interface.Internal
private readonly HashSet<SpecialGlyphRequest> glyphRequests = new(); private readonly HashSet<SpecialGlyphRequest> glyphRequests = new();
private readonly Dictionary<ImFontPtr, TargetFontModification> loadedFontInfo = new(); private readonly Dictionary<ImFontPtr, TargetFontModification> loadedFontInfo = new();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private readonly ManualResetEvent fontBuildSignal; private readonly ManualResetEvent fontBuildSignal;
private readonly SwapChainVtableResolver address; private readonly SwapChainVtableResolver address;
private readonly Hook<DispatchMessageWDelegate> dispatchMessageWHook; private readonly Hook<DispatchMessageWDelegate> dispatchMessageWHook;
@ -73,12 +76,12 @@ namespace Dalamud.Interface.Internal
private bool isOverrideGameCursor = true; private bool isOverrideGameCursor = true;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private InterfaceManager(SigScanner sigScanner) private InterfaceManager()
{ {
this.dispatchMessageWHook = Hook<DispatchMessageWDelegate>.FromImport( this.dispatchMessageWHook = Hook<DispatchMessageWDelegate>.FromImport(
Process.GetCurrentProcess().MainModule, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour); null, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour);
this.setCursorHook = Hook<SetCursorDelegate>.FromImport( this.setCursorHook = Hook<SetCursorDelegate>.FromImport(
Process.GetCurrentProcess().MainModule, "user32.dll", "SetCursor", 0, this.SetCursorDetour); null, "user32.dll", "SetCursor", 0, this.SetCursorDetour);
this.fontBuildSignal = new ManualResetEvent(false); this.fontBuildSignal = new ManualResetEvent(false);
@ -91,12 +94,6 @@ namespace Dalamud.Interface.Internal
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr ResizeBuffersDelegate(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags); private delegate IntPtr ResizeBuffersDelegate(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate int CreateDXGIFactoryDelegate(Guid riid, out IntPtr ppFactory);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate int IDXGIFactory_CreateSwapChainDelegate(IntPtr pFactory, IntPtr pDevice, IntPtr pDesc, out IntPtr ppSwapChain);
[UnmanagedFunctionPointer(CallingConvention.StdCall)] [UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate IntPtr SetCursorDelegate(IntPtr hCursor); private delegate IntPtr SetCursorDelegate(IntPtr hCursor);
@ -248,20 +245,15 @@ namespace Dalamud.Interface.Internal
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
// HACK: this is usually called on a separate thread from PresentDetour (likely on a dedicated render thread) this.framework.RunOnFrameworkThread(() =>
// and if we aren't already disabled, disposing of the scene and hook can frequently crash due to the hook {
// being disposed of in this thread while it is actively in use in the render thread. this.setCursorHook.Dispose();
// This is a terrible way to prevent issues, but should basically always work to ensure that all outstanding this.presentHook?.Dispose();
// calls to PresentDetour have finished (and Disable means no new ones will start), before we try to cleanup this.resizeBuffersHook?.Dispose();
// So... not great, but much better than constantly crashing on unload this.dispatchMessageWHook.Dispose();
this.Disable(); }).Wait();
Thread.Sleep(500);
this.scene?.Dispose(); this.scene?.Dispose();
this.setCursorHook.Dispose();
this.presentHook?.Dispose();
this.resizeBuffersHook?.Dispose();
this.dispatchMessageWHook.Dispose();
} }
#nullable enable #nullable enable
@ -990,8 +982,8 @@ namespace Dalamud.Interface.Internal
break; break;
} }
this.presentHook = new Hook<PresentDelegate>(this.address.Present, this.PresentDetour); this.presentHook = Hook<PresentDelegate>.FromAddress(this.address.Present, this.PresentDetour);
this.resizeBuffersHook = new Hook<ResizeBuffersDelegate>(this.address.ResizeBuffers, this.ResizeBuffersDetour); this.resizeBuffersHook = Hook<ResizeBuffersDelegate>.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour);
Log.Verbose("===== S W A P C H A I N ====="); Log.Verbose("===== S W A P C H A I N =====");
Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}");
@ -1004,14 +996,6 @@ namespace Dalamud.Interface.Internal
}); });
} }
private void Disable()
{
this.setCursorHook.Disable();
this.presentHook?.Disable();
this.resizeBuffersHook?.Disable();
this.dispatchMessageWHook.Disable();
}
// This is intended to only be called as a handler attached to scene.OnNewRenderFrame // This is intended to only be called as a handler attached to scene.OnNewRenderFrame
private void RebuildFontsInternal() private void RebuildFontsInternal()
{ {
@ -1104,7 +1088,7 @@ namespace Dalamud.Interface.Internal
if (this.lastWantCapture == true && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) if (this.lastWantCapture == true && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor)
return IntPtr.Zero; return IntPtr.Zero;
return this.setCursorHook!.Original(hCursor); return this.setCursorHook.IsDisposed ? User32.SetCursor(new User32.SafeCursorHandle(hCursor, false)).DangerousGetHandle() : this.setCursorHook.Original(hCursor);
} }
private void OnNewInputFrame() private void OnNewInputFrame()

View file

@ -123,7 +123,6 @@ namespace Dalamud.Interface.Internal.Windows
// Options menu // Options menu
if (ImGui.BeginPopup("Options")) if (ImGui.BeginPopup("Options"))
{ {
var dalamud = Service<Dalamud>.Get();
var configuration = Service<DalamudConfiguration>.Get(); var configuration = Service<DalamudConfiguration>.Get();
if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll)) if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll))
@ -138,10 +137,10 @@ namespace Dalamud.Interface.Internal.Windows
configuration.Save(); configuration.Save();
} }
var prevLevel = (int)dalamud.LogLevelSwitch.MinimumLevel; var prevLevel = (int)EntryPoint.LogLevelSwitch.MinimumLevel;
if (ImGui.Combo("Log Level", ref prevLevel, Enum.GetValues(typeof(LogEventLevel)).Cast<LogEventLevel>().Select(x => x.ToString()).ToArray(), 6)) if (ImGui.Combo("Log Level", ref prevLevel, Enum.GetValues(typeof(LogEventLevel)).Cast<LogEventLevel>().Select(x => x.ToString()).ToArray(), 6))
{ {
dalamud.LogLevelSwitch.MinimumLevel = (LogEventLevel)prevLevel; EntryPoint.LogLevelSwitch.MinimumLevel = (LogEventLevel)prevLevel;
configuration.LogLevel = (LogEventLevel)prevLevel; configuration.LogLevel = (LogEventLevel)prevLevel;
configuration.Save(); configuration.Save();
} }

View file

@ -7,7 +7,6 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types;
@ -20,7 +19,8 @@ namespace Dalamud.Interface.Internal.Windows
/// <summary> /// <summary>
/// A cache for plugin icons and images. /// A cache for plugin icons and images.
/// </summary> /// </summary>
internal class PluginImageCache : IDisposable [ServiceManager.EarlyLoadedService]
internal class PluginImageCache : IDisposable, IServiceType
{ {
/// <summary> /// <summary>
/// Maximum plugin image width. /// Maximum plugin image width.
@ -44,102 +44,133 @@ namespace Dalamud.Interface.Internal.Windows
private const string MainRepoImageUrl = "https://raw.githubusercontent.com/goatcorp/DalamudPlugins/api6/{0}/{1}/images/{2}"; private const string MainRepoImageUrl = "https://raw.githubusercontent.com/goatcorp/DalamudPlugins/api6/{0}/{1}/images/{2}";
private BlockingCollection<Func<Task>> downloadQueue = new(); private readonly BlockingCollection<Tuple<ulong, Func<Task>>> downloadQueue = new();
private BlockingCollection<Action> loadQueue = new(); private readonly BlockingCollection<Func<Task>> loadQueue = new();
private CancellationTokenSource downloadToken = new(); private readonly CancellationTokenSource cancelToken = new();
private Thread downloadThread; private readonly Task downloadTask;
private readonly Task loadTask;
private Dictionary<string, TextureWrap?> pluginIconMap = new(); private readonly Dictionary<string, TextureWrap?> pluginIconMap = new();
private Dictionary<string, TextureWrap?[]> pluginImagesMap = new(); private readonly Dictionary<string, TextureWrap?[]> pluginImagesMap = new();
private readonly Task<TextureWrap> emptyTextureTask;
private readonly Task<TextureWrap> defaultIconTask;
private readonly Task<TextureWrap> troubleIconTask;
private readonly Task<TextureWrap> updateIconTask;
private readonly Task<TextureWrap> installedIconTask;
private readonly Task<TextureWrap> thirdIconTask;
private readonly Task<TextureWrap> thirdInstalledIconTask;
private readonly Task<TextureWrap> corePluginIconTask;
[ServiceManager.ServiceConstructor]
private PluginImageCache(Dalamud dalamud)
{
var imwst = Service<InterfaceManager.InterfaceManagerWithScene>.GetAsync();
Task<TextureWrap>? TaskWrapIfNonNull(TextureWrap? tw) => tw == null ? null : Task.FromResult(tw!);
this.emptyTextureTask = imwst.ContinueWith(task => task.Result.Manager.LoadImageRaw(new byte[64], 8, 8, 4)!);
this.defaultIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "defaultIcon.png"))) ?? this.emptyTextureTask).Unwrap();
this.troubleIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "troubleIcon.png"))) ?? this.emptyTextureTask).Unwrap();
this.updateIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "updateIcon.png"))) ?? this.emptyTextureTask).Unwrap();
this.installedIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "installedIcon.png"))) ?? this.emptyTextureTask).Unwrap();
this.thirdIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "thirdIcon.png"))) ?? this.emptyTextureTask).Unwrap();
this.thirdInstalledIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "thirdInstalledIcon.png"))) ?? this.emptyTextureTask).Unwrap();
this.corePluginIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "tsmLogo.png"))) ?? this.emptyTextureTask).Unwrap();
this.downloadTask = Task.Factory.StartNew(
() => this.DownloadTask(8), TaskCreationOptions.LongRunning);
this.loadTask = Task.Factory.StartNew(
() => this.LoadTask(Environment.ProcessorCount), TaskCreationOptions.LongRunning);
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PluginImageCache"/> class. /// Gets the fallback empty texture.
/// </summary> /// </summary>
public PluginImageCache() public TextureWrap EmptyTexture => this.emptyTextureTask.IsCompleted
{ ? this.emptyTextureTask.Result
var dalamud = Service<Dalamud>.Get(); : this.emptyTextureTask.GetAwaiter().GetResult();
var interfaceManager = Service<InterfaceManager>.Get();
this.DefaultIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "defaultIcon.png"))!;
this.TroubleIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "troubleIcon.png"))!;
this.UpdateIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "updateIcon.png"))!;
this.InstalledIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "installedIcon.png"))!;
this.ThirdIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "thirdIcon.png"))!;
this.ThirdInstalledIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "thirdInstalledIcon.png"))!;
this.CorePluginIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "tsmLogo.png"))!;
if (this.DefaultIcon == null || this.TroubleIcon == null || this.UpdateIcon == null || this.InstalledIcon == null ||
this.ThirdIcon == null || this.ThirdInstalledIcon == null || this.CorePluginIcon == null)
{
throw new Exception("Plugin overlay images could not be loaded.");
}
this.downloadThread = new Thread(this.DownloadTask);
this.downloadThread.Start();
var framework = Service<Framework>.Get();
framework.Update += this.FrameworkOnUpdate;
}
/// <summary> /// <summary>
/// Gets the default plugin icon. /// Gets the default plugin icon.
/// </summary> /// </summary>
public TextureWrap DefaultIcon { get; } public TextureWrap DefaultIcon => this.defaultIconTask.IsCompleted
? this.defaultIconTask.Result
: this.defaultIconTask.GetAwaiter().GetResult();
/// <summary> /// <summary>
/// Gets the plugin trouble icon overlay. /// Gets the plugin trouble icon overlay.
/// </summary> /// </summary>
public TextureWrap TroubleIcon { get; } public TextureWrap TroubleIcon => this.troubleIconTask.IsCompleted
? this.troubleIconTask.Result
: this.troubleIconTask.GetAwaiter().GetResult();
/// <summary> /// <summary>
/// Gets the plugin update icon overlay. /// Gets the plugin update icon overlay.
/// </summary> /// </summary>
public TextureWrap UpdateIcon { get; } public TextureWrap UpdateIcon => this.updateIconTask.IsCompleted
? this.updateIconTask.Result
: this.updateIconTask.GetAwaiter().GetResult();
/// <summary> /// <summary>
/// Gets the plugin installed icon overlay. /// Gets the plugin installed icon overlay.
/// </summary> /// </summary>
public TextureWrap InstalledIcon { get; } public TextureWrap InstalledIcon => this.installedIconTask.IsCompleted
? this.installedIconTask.Result
: this.installedIconTask.GetAwaiter().GetResult();
/// <summary> /// <summary>
/// Gets the third party plugin icon overlay. /// Gets the third party plugin icon overlay.
/// </summary> /// </summary>
public TextureWrap ThirdIcon { get; } public TextureWrap ThirdIcon => this.thirdIconTask.IsCompleted
? this.thirdIconTask.Result
: this.thirdIconTask.GetAwaiter().GetResult();
/// <summary> /// <summary>
/// Gets the installed third party plugin icon overlay. /// Gets the installed third party plugin icon overlay.
/// </summary> /// </summary>
public TextureWrap ThirdInstalledIcon { get; } public TextureWrap ThirdInstalledIcon => this.thirdInstalledIconTask.IsCompleted
? this.thirdInstalledIconTask.Result
: this.thirdInstalledIconTask.GetAwaiter().GetResult();
/// <summary> /// <summary>
/// Gets the core plugin icon. /// Gets the core plugin icon.
/// </summary> /// </summary>
public TextureWrap CorePluginIcon { get; } public TextureWrap CorePluginIcon => this.corePluginIconTask.IsCompleted
? this.corePluginIconTask.Result
: this.corePluginIconTask.GetAwaiter().GetResult();
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() public void Dispose()
{ {
var framework = Service<Framework>.Get(); this.cancelToken.Cancel();
framework.Update -= this.FrameworkOnUpdate; this.downloadQueue.CompleteAdding();
this.loadQueue.CompleteAdding();
this.DefaultIcon?.Dispose(); if (!Task.WaitAll(new[] { this.loadTask, this.downloadTask }, 4000))
this.TroubleIcon?.Dispose();
this.UpdateIcon?.Dispose();
this.InstalledIcon?.Dispose();
this.ThirdIcon?.Dispose();
this.ThirdInstalledIcon?.Dispose();
this.CorePluginIcon?.Dispose();
this.downloadToken?.Cancel();
if (!this.downloadThread.Join(4000))
{ {
Log.Error("Plugin Image Download thread has not cancelled in time"); Log.Error("Plugin Image download/load thread has not cancelled in time");
} }
this.downloadToken?.Dispose(); this.cancelToken.Dispose();
this.downloadQueue?.CompleteAdding(); this.downloadQueue.Dispose();
this.downloadQueue?.Dispose(); this.loadQueue.Dispose();
foreach (var task in new[]
{
this.defaultIconTask,
this.troubleIconTask,
this.updateIconTask,
this.installedIconTask,
this.thirdIconTask,
this.thirdInstalledIconTask,
this.corePluginIconTask,
})
{
task.Wait();
if (task.IsCompletedSuccessfully)
task.Result.Dispose();
}
foreach (var icon in this.pluginIconMap.Values) foreach (var icon in this.pluginIconMap.Values)
{ {
@ -181,12 +212,22 @@ namespace Dalamud.Interface.Internal.Windows
if (this.pluginIconMap.TryGetValue(manifest.InternalName, out iconTexture)) if (this.pluginIconMap.TryGetValue(manifest.InternalName, out iconTexture))
return true; return true;
iconTexture = null; this.pluginIconMap.Add(manifest.InternalName, null);
this.pluginIconMap.Add(manifest.InternalName, iconTexture);
if (!this.downloadQueue.IsCompleted) try
{ {
this.downloadQueue.Add(async () => await this.DownloadPluginIconAsync(plugin, manifest, isThirdParty)); if (!this.downloadQueue.IsCompleted)
{
this.downloadQueue.Add(
Tuple.Create(
Service<DalamudInterface>.GetNullable()?.FrameCount ?? 0,
() => this.DownloadPluginIconAsync(plugin, manifest, isThirdParty)),
this.cancelToken.Token);
}
}
catch (ObjectDisposedException)
{
// pass
} }
return false; return false;
@ -209,39 +250,120 @@ namespace Dalamud.Interface.Internal.Windows
imageTextures = Array.Empty<TextureWrap>(); imageTextures = Array.Empty<TextureWrap>();
this.pluginImagesMap.Add(manifest.InternalName, imageTextures); this.pluginImagesMap.Add(manifest.InternalName, imageTextures);
if (!this.downloadQueue.IsCompleted) try
{ {
this.downloadQueue.Add(async () => await this.DownloadPluginImagesAsync(plugin, manifest, isThirdParty)); if (!this.downloadQueue.IsCompleted)
{
this.downloadQueue.Add(
Tuple.Create(
Service<DalamudInterface>.GetNullable()?.FrameCount ?? 0,
() => this.DownloadPluginImagesAsync(plugin, manifest, isThirdParty)),
this.cancelToken.Token);
}
}
catch (ObjectDisposedException)
{
// pass
} }
return false; return false;
} }
private void FrameworkOnUpdate(Framework framework) private static async Task<TextureWrap?> TryLoadIcon(
byte[] bytes,
string name,
string? loc,
PluginManifest manifest,
int maxWidth,
int maxHeight,
bool requireSquare)
{ {
var interfaceManager = (await Service<InterfaceManager.InterfaceManagerWithScene>.GetAsync()).Manager;
var framework = await Service<Framework>.GetAsync();
TextureWrap? icon;
// FIXME(goat): This is a hack around this call failing randomly in certain situations. Might be related to not being called on the main thread.
try try
{ {
if (!this.loadQueue.TryTake(out var loadAction, 0, this.downloadToken.Token)) icon = interfaceManager.LoadImage(bytes);
return;
loadAction.Invoke();
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "An unhandled exception occurred in image loader framework dispatcher"); Log.Error(ex, "Access violation during load plugin {name} from {Loc} (Async Thread)", name, loc);
try
{
icon = await framework.RunOnFrameworkThread(() => interfaceManager.LoadImage(bytes));
}
catch (Exception ex2)
{
Log.Error(ex2, "Access violation during load plugin {name} from {Loc} (Framework Thread)", name, loc);
return null;
}
} }
if (icon == null)
{
Log.Error($"Could not load {name} for {manifest.InternalName} at {loc}");
return null;
}
if (icon.Width > maxWidth || icon.Height > maxHeight)
{
Log.Error($"Plugin {name} for {manifest.InternalName} at {loc} was larger than the maximum allowed resolution ({maxWidth}x{maxHeight}).");
icon.Dispose();
return null;
}
if (requireSquare && icon.Height != icon.Width)
{
Log.Error($"Plugin {name} for {manifest.InternalName} at {loc} was not square.");
icon.Dispose();
return null;
}
return icon!;
} }
private async void DownloadTask() private async Task DownloadTask(int concurrency)
{ {
while (!this.downloadToken.Token.IsCancellationRequested) var token = this.cancelToken.Token;
var runningTasks = new List<Task>();
var pendingFuncs = new List<Tuple<ulong, Func<Task>>>();
while (true)
{ {
try try
{ {
if (!this.downloadQueue.TryTake(out var task, -1, this.downloadToken.Token)) token.ThrowIfCancellationRequested();
return; if (!pendingFuncs.Any())
{
if (!this.downloadQueue.TryTake(out var taskTuple, -1, token))
return;
await task.Invoke(); pendingFuncs.Add(taskTuple);
}
token.ThrowIfCancellationRequested();
while (this.downloadQueue.TryTake(out var taskTuple, 0, token))
pendingFuncs.Add(taskTuple);
// Process most recently requested items first in terms of frame index.
pendingFuncs = pendingFuncs.OrderBy(x => x.Item1).ToList();
var item1 = pendingFuncs.Last().Item1;
while (pendingFuncs.Any() && pendingFuncs.Last().Item1 == item1)
{
token.ThrowIfCancellationRequested();
while (runningTasks.Count >= concurrency)
{
await Task.WhenAny(runningTasks);
runningTasks.RemoveAll(task => task.IsCompleted);
}
token.ThrowIfCancellationRequested();
runningTasks.Add(Task.Run(pendingFuncs.Last().Item2, token));
pendingFuncs.RemoveAt(pendingFuncs.Count - 1);
}
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@ -252,52 +374,56 @@ namespace Dalamud.Interface.Internal.Windows
{ {
Log.Error(ex, "An unhandled exception occurred in the plugin image downloader"); Log.Error(ex, "An unhandled exception occurred in the plugin image downloader");
} }
while (runningTasks.Count >= concurrency)
{
await Task.WhenAny(runningTasks);
runningTasks.RemoveAll(task => task.IsCompleted);
}
} }
await Task.WhenAll(runningTasks);
Log.Debug("Plugin image downloader has shutdown"); Log.Debug("Plugin image downloader has shutdown");
} }
private async Task LoadTask(int concurrency)
{
await Service<InterfaceManager.InterfaceManagerWithScene>.GetAsync();
var token = this.cancelToken.Token;
var runningTasks = new List<Task>();
while (true)
{
try
{
token.ThrowIfCancellationRequested();
while (runningTasks.Count >= concurrency)
{
await Task.WhenAny(runningTasks);
runningTasks.RemoveAll(task => task.IsCompleted);
}
if (!this.loadQueue.TryTake(out var func, -1, token))
return;
runningTasks.Add(Task.Run(func, token));
}
catch (OperationCanceledException)
{
// Shutdown signal.
break;
}
catch (Exception ex)
{
Log.Error(ex, "An unhandled exception occurred in the plugin image loader");
}
}
await Task.WhenAll(runningTasks);
Log.Debug("Plugin image loader has shutdown");
}
private async Task DownloadPluginIconAsync(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty) private async Task DownloadPluginIconAsync(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty)
{ {
var interfaceManager = Service<InterfaceManager>.Get();
var pluginManager = Service<PluginManager>.Get();
static bool TryLoadIcon(byte[] bytes, string? loc, PluginManifest manifest, InterfaceManager interfaceManager, out TextureWrap? icon)
{
// FIXME(goat): This is a hack around this call failing randomly in certain situations. Might be related to not being called on the main thread.
try
{
icon = interfaceManager.LoadImage(bytes);
}
catch (AccessViolationException ex)
{
Log.Error(ex, "Access violation during load plugin icon from {Loc}", loc);
icon = null;
return false;
}
if (icon == null)
{
Log.Error($"Could not load icon for {manifest.InternalName} at {loc}");
return false;
}
if (icon.Width > PluginIconWidth || icon.Height > PluginIconHeight)
{
Log.Error($"Icon for {manifest.InternalName} at {loc} was larger than the maximum allowed resolution ({PluginIconWidth}x{PluginIconHeight}).");
return false;
}
if (icon.Height != icon.Width)
{
Log.Error($"Icon for {manifest.InternalName} at {loc} was not square.");
return false;
}
return true;
}
if (plugin != null && plugin.IsDev) if (plugin != null && plugin.IsDev)
{ {
var file = this.GetPluginIconFileInfo(plugin); var file = this.GetPluginIconFileInfo(plugin);
@ -306,7 +432,8 @@ namespace Dalamud.Interface.Internal.Windows
Log.Verbose($"Fetching icon for {manifest.InternalName} from {file.FullName}"); Log.Verbose($"Fetching icon for {manifest.InternalName} from {file.FullName}");
var bytes = await File.ReadAllBytesAsync(file.FullName); var bytes = await File.ReadAllBytesAsync(file.FullName);
if (!TryLoadIcon(bytes, file.FullName, manifest, interfaceManager, out var icon)) var icon = await TryLoadIcon(bytes, "icon", file.FullName, manifest, PluginIconWidth, PluginIconHeight, true);
if (icon == null)
return; return;
this.pluginIconMap[manifest.InternalName] = icon; this.pluginIconMap[manifest.InternalName] = icon;
@ -349,9 +476,10 @@ namespace Dalamud.Interface.Internal.Windows
data.EnsureSuccessStatusCode(); data.EnsureSuccessStatusCode();
var bytes = await data.Content.ReadAsByteArrayAsync(); var bytes = await data.Content.ReadAsByteArrayAsync();
this.loadQueue.Add(() => this.loadQueue.Add(async () =>
{ {
if (!TryLoadIcon(bytes, url, manifest, interfaceManager, out var icon)) var icon = await TryLoadIcon(bytes, "icon", url, manifest, PluginIconWidth, PluginIconHeight, true);
if (icon == null)
return; return;
this.pluginIconMap[manifest.InternalName] = icon; this.pluginIconMap[manifest.InternalName] = icon;
@ -366,39 +494,6 @@ namespace Dalamud.Interface.Internal.Windows
private async Task DownloadPluginImagesAsync(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty) private async Task DownloadPluginImagesAsync(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty)
{ {
var interfaceManager = Service<InterfaceManager>.Get();
var pluginManager = Service<PluginManager>.Get();
static bool TryLoadImage(int i, byte[] bytes, string loc, PluginManifest manifest, InterfaceManager interfaceManager, out TextureWrap? image)
{
// FIXME(goat): This is a hack around this call failing randomly in certain situations. Might be related to not being called on the main thread.
try
{
image = interfaceManager.LoadImage(bytes);
}
catch (AccessViolationException ex)
{
Log.Error(ex, "Access violation during load plugin image from {Loc}", loc);
image = null;
return false;
}
if (image == null)
{
Log.Error($"Could not load image{i + 1} for {manifest.InternalName} at {loc}");
return false;
}
if (image.Width > PluginImageWidth || image.Height > PluginImageHeight)
{
Log.Error($"Plugin image{i + 1} for {manifest.InternalName} at {loc} was larger than the maximum allowed resolution ({PluginImageWidth}x{PluginImageHeight}).");
return false;
}
return true;
}
if (plugin is { IsDev: true }) if (plugin is { IsDev: true })
{ {
var files = this.GetPluginImageFileInfos(plugin); var files = this.GetPluginImageFileInfos(plugin);
@ -415,7 +510,8 @@ namespace Dalamud.Interface.Internal.Windows
Log.Verbose($"Loading image{i + 1} for {manifest.InternalName} from {file.FullName}"); Log.Verbose($"Loading image{i + 1} for {manifest.InternalName} from {file.FullName}");
var bytes = await File.ReadAllBytesAsync(file.FullName); var bytes = await File.ReadAllBytesAsync(file.FullName);
if (!TryLoadImage(i, bytes, file.FullName, manifest, interfaceManager, out var image)) var image = await TryLoadIcon(bytes, $"image{i + 1}", file.FullName, manifest, PluginImageWidth, PluginImageHeight, true);
if (image == null)
continue; continue;
Log.Verbose($"Plugin image{i + 1} for {manifest.InternalName} loaded from disk"); Log.Verbose($"Plugin image{i + 1} for {manifest.InternalName} loaded from disk");
@ -490,17 +586,16 @@ namespace Dalamud.Interface.Internal.Windows
if (didAny) if (didAny)
{ {
this.loadQueue.Add(() => this.loadQueue.Add(async () =>
{ {
var pluginImages = new TextureWrap[urls.Count]; var pluginImages = new TextureWrap[urls.Count];
for (var i = 0; i < imageBytes.Length; i++) for (var i = 0; i < imageBytes.Length; i++)
{ {
var bytes = imageBytes[i]; var bytes = imageBytes[i];
if (bytes == null)
continue;
if (!TryLoadImage(i, bytes, "queue", manifest, interfaceManager, out var image)) var image = await TryLoadIcon(bytes, $"image{i + 1}", "queue", manifest, PluginImageWidth, PluginImageHeight, true);
if (image == null)
continue; continue;
pluginImages[i] = image; pluginImages[i] = image;
@ -551,7 +646,9 @@ namespace Dalamud.Interface.Internal.Windows
private FileInfo? GetPluginIconFileInfo(LocalPlugin? plugin) private FileInfo? GetPluginIconFileInfo(LocalPlugin? plugin)
{ {
var pluginDir = plugin.DllFile.Directory; var pluginDir = plugin?.DllFile.Directory;
if (pluginDir == null)
return null;
var devUrl = new FileInfo(Path.Combine(pluginDir.FullName, "images", "icon.png")); var devUrl = new FileInfo(Path.Combine(pluginDir.FullName, "images", "icon.png"));
if (devUrl.Exists) if (devUrl.Exists)
@ -562,8 +659,12 @@ namespace Dalamud.Interface.Internal.Windows
private List<FileInfo?> GetPluginImageFileInfos(LocalPlugin? plugin) private List<FileInfo?> GetPluginImageFileInfos(LocalPlugin? plugin)
{ {
var pluginDir = plugin.DllFile.Directory;
var output = new List<FileInfo>(); var output = new List<FileInfo>();
var pluginDir = plugin?.DllFile.Directory;
if (pluginDir == null)
return output;
for (var i = 1; i <= 5; i++) for (var i = 1; i <= 5; i++)
{ {
var devUrl = new FileInfo(Path.Combine(pluginDir.FullName, "images", $"image{i}.png")); var devUrl = new FileInfo(Path.Combine(pluginDir.FullName, "images", $"image{i}.png"));

View file

@ -37,8 +37,8 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
private readonly Vector4 changelogBgColor = new(0.114f, 0.584f, 0.192f, 0.678f); private readonly Vector4 changelogBgColor = new(0.114f, 0.584f, 0.192f, 0.678f);
private readonly Vector4 changelogTextColor = new(0.812f, 1.000f, 0.816f, 1.000f); private readonly Vector4 changelogTextColor = new(0.812f, 1.000f, 0.816f, 1.000f);
private readonly PluginImageCache imageCache;
private readonly PluginCategoryManager categoryManager = new(); private readonly PluginCategoryManager categoryManager = new();
private readonly PluginImageCache imageCache = new();
private readonly DalamudChangelogManager dalamudChangelogManager = new(); private readonly DalamudChangelogManager dalamudChangelogManager = new();
private readonly List<int> openPluginCollapsibles = new(); private readonly List<int> openPluginCollapsibles = new();
@ -87,12 +87,14 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PluginInstallerWindow"/> class. /// Initializes a new instance of the <see cref="PluginInstallerWindow"/> class.
/// </summary> /// </summary>
public PluginInstallerWindow() /// <param name="imageCache">An instance of <see cref="PluginImageCache"/> class.</param>
public PluginInstallerWindow(PluginImageCache imageCache)
: base( : base(
Locs.WindowTitle + (Service<DalamudConfiguration>.Get().DoPluginTest ? Locs.WindowTitleMod_Testing : string.Empty) + "###XlPluginInstaller", Locs.WindowTitle + (Service<DalamudConfiguration>.Get().DoPluginTest ? Locs.WindowTitleMod_Testing : string.Empty) + "###XlPluginInstaller",
ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar) ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar)
{ {
this.IsOpen = true; this.IsOpen = true;
this.imageCache = imageCache;
this.Size = new Vector2(830, 570); this.Size = new Vector2(830, 570);
this.SizeCondition = ImGuiCond.FirstUseEver; this.SizeCondition = ImGuiCond.FirstUseEver;
@ -200,6 +202,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5 * ImGuiHelpers.GlobalScale)); ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5 * ImGuiHelpers.GlobalScale));
var searchInputWidth = 240 * ImGuiHelpers.GlobalScale; var searchInputWidth = 240 * ImGuiHelpers.GlobalScale;
var searchClearButtonWidth = 40 * ImGuiHelpers.GlobalScale;
var sortByText = Locs.SortBy_Label; var sortByText = Locs.SortBy_Label;
var sortByTextWidth = ImGui.CalcTextSize(sortByText).X; var sortByTextWidth = ImGui.CalcTextSize(sortByText).X;
@ -224,13 +227,28 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
// Shift down a little to align with the middle of the header text // Shift down a little to align with the middle of the header text
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (headerTextSize.Y / 4) - 2); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (headerTextSize.Y / 4) - 2);
ImGui.SetCursorPosX(windowSize.X - sortSelectWidth - style.ItemSpacing.X - searchInputWidth); ImGui.SetCursorPosX(windowSize.X - sortSelectWidth - style.ItemSpacing.X - searchInputWidth - searchClearButtonWidth);
var searchTextChanged = false;
ImGui.SetNextItemWidth(searchInputWidth); ImGui.SetNextItemWidth(searchInputWidth);
if (ImGui.InputTextWithHint("###XlPluginInstaller_Search", Locs.Header_SearchPlaceholder, ref this.searchText, 100)) searchTextChanged |= ImGui.InputTextWithHint(
"###XlPluginInstaller_Search",
Locs.Header_SearchPlaceholder,
ref this.searchText,
100);
ImGui.SameLine();
ImGui.SetNextItemWidth(searchClearButtonWidth);
if (ImGuiComponents.IconButton(FontAwesomeIcon.Times))
{ {
this.UpdateCategoriesOnSearchChange(); this.searchText = string.Empty;
searchTextChanged = true;
} }
if (searchTextChanged)
this.UpdateCategoriesOnSearchChange();
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetCursorPosX(windowSize.X - sortSelectWidth); ImGui.SetCursorPosX(windowSize.X - sortSelectWidth);
ImGui.SetNextItemWidth(selectableWidth); ImGui.SetNextItemWidth(selectableWidth);
@ -552,7 +570,8 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
var i = 0; var i = 0;
foreach (var manifest in categoryManifestsList) foreach (var manifest in categoryManifestsList)
{ {
var remoteManifest = manifest as RemotePluginManifest; if (manifest is not RemotePluginManifest remoteManifest)
continue;
var (isInstalled, plugin) = this.IsManifestInstalled(remoteManifest); var (isInstalled, plugin) = this.IsManifestInstalled(remoteManifest);
ImGui.PushID($"{manifest.InternalName}{manifest.AssemblyVersion}"); ImGui.PushID($"{manifest.InternalName}{manifest.AssemblyVersion}");
@ -1087,51 +1106,38 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
ImGui.SetCursorPos(startCursor); ImGui.SetCursorPos(startCursor);
var iconTex = this.imageCache.DefaultIcon;
var hasIcon = this.imageCache.TryGetIcon(plugin, manifest, isThirdParty, out var cachedIconTex);
if (hasIcon && cachedIconTex != null)
{
iconTex = cachedIconTex;
}
var iconSize = ImGuiHelpers.ScaledVector2(64, 64); var iconSize = ImGuiHelpers.ScaledVector2(64, 64);
var cursorBeforeImage = ImGui.GetCursorPos(); var cursorBeforeImage = ImGui.GetCursorPos();
ImGui.Image(iconTex.ImGuiHandle, iconSize); var rectOffset = ImGui.GetWindowContentRegionMin() + ImGui.GetWindowPos();
ImGui.SameLine(); if (ImGui.IsRectVisible(rectOffset + cursorBeforeImage, rectOffset + cursorBeforeImage + iconSize))
{
var iconTex = this.imageCache.DefaultIcon;
var hasIcon = this.imageCache.TryGetIcon(plugin, manifest, isThirdParty, out var cachedIconTex);
if (hasIcon && cachedIconTex != null)
{
iconTex = cachedIconTex;
}
ImGui.Image(iconTex.ImGuiHandle, iconSize);
ImGui.SameLine();
ImGui.SetCursorPos(cursorBeforeImage);
}
var isLoaded = plugin is { IsLoaded: true }; var isLoaded = plugin is { IsLoaded: true };
if (updateAvailable) if (updateAvailable)
{
ImGui.SetCursorPos(cursorBeforeImage);
ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize); ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize);
ImGui.SameLine();
}
else if (trouble) else if (trouble)
{
ImGui.SetCursorPos(cursorBeforeImage);
ImGui.Image(this.imageCache.TroubleIcon.ImGuiHandle, iconSize); ImGui.Image(this.imageCache.TroubleIcon.ImGuiHandle, iconSize);
ImGui.SameLine();
}
else if (isLoaded && isThirdParty) else if (isLoaded && isThirdParty)
{
ImGui.SetCursorPos(cursorBeforeImage);
ImGui.Image(this.imageCache.ThirdInstalledIcon.ImGuiHandle, iconSize); ImGui.Image(this.imageCache.ThirdInstalledIcon.ImGuiHandle, iconSize);
ImGui.SameLine();
}
else if (isThirdParty) else if (isThirdParty)
{
ImGui.SetCursorPos(cursorBeforeImage);
ImGui.Image(this.imageCache.ThirdIcon.ImGuiHandle, iconSize); ImGui.Image(this.imageCache.ThirdIcon.ImGuiHandle, iconSize);
ImGui.SameLine();
}
else if (isLoaded) else if (isLoaded)
{
ImGui.SetCursorPos(cursorBeforeImage);
ImGui.Image(this.imageCache.InstalledIcon.ImGuiHandle, iconSize); ImGui.Image(this.imageCache.InstalledIcon.ImGuiHandle, iconSize);
ImGui.SameLine(); else
} ImGui.Dummy(iconSize);
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
ImGui.SameLine(); ImGui.SameLine();
@ -1208,23 +1214,32 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
var startCursor = ImGui.GetCursorPos(); var startCursor = ImGui.GetCursorPos();
var iconSize = ImGuiHelpers.ScaledVector2(64, 64); var iconSize = ImGuiHelpers.ScaledVector2(64, 64);
var cursorBeforeImage = ImGui.GetCursorPos();
TextureWrap icon; var rectOffset = ImGui.GetWindowContentRegionMin() + ImGui.GetWindowPos();
if (log is PluginChangelogEntry pluginLog) if (ImGui.IsRectVisible(rectOffset + cursorBeforeImage, rectOffset + cursorBeforeImage + iconSize))
{ {
icon = this.imageCache.DefaultIcon; TextureWrap icon;
var hasIcon = this.imageCache.TryGetIcon(pluginLog.Plugin, pluginLog.Plugin.Manifest, pluginLog.Plugin.Manifest.IsThirdParty, out var cachedIconTex); if (log is PluginChangelogEntry pluginLog)
if (hasIcon && cachedIconTex != null)
{ {
icon = cachedIconTex; icon = this.imageCache.DefaultIcon;
var hasIcon = this.imageCache.TryGetIcon(pluginLog.Plugin, pluginLog.Plugin.Manifest, pluginLog.Plugin.Manifest.IsThirdParty, out var cachedIconTex);
if (hasIcon && cachedIconTex != null)
{
icon = cachedIconTex;
}
} }
else
{
icon = this.imageCache.CorePluginIcon;
}
ImGui.Image(icon.ImGuiHandle, iconSize);
} }
else else
{ {
icon = this.imageCache.CorePluginIcon; ImGui.Dummy(iconSize);
} }
ImGui.Image(icon.ImGuiHandle, iconSize);
ImGui.SameLine(); ImGui.SameLine();
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
@ -1644,7 +1659,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
this.installStatus = OperationStatus.InProgress; this.installStatus = OperationStatus.InProgress;
Task.Run(() => pluginManager.DeleteConfiguration(plugin)) Task.Run(() => pluginManager.DeleteConfigurationAsync(plugin))
.ContinueWith(task => .ContinueWith(task =>
{ {
this.installStatus = OperationStatus.Idle; this.installStatus = OperationStatus.Idle;
@ -1670,7 +1685,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
// Disable everything if the plugin is outdated // Disable everything if the plugin is outdated
disabled = disabled || (plugin.IsOutdated && !configuration.LoadAllApiLevels) || plugin.IsBanned; disabled = disabled || (plugin.IsOutdated && !configuration.LoadAllApiLevels) || plugin.IsBanned;
if (plugin.State == PluginState.InProgress) if (plugin.State == PluginState.Loading || plugin.State == PluginState.Unloading)
{ {
ImGuiComponents.DisabledButton(Locs.PluginButton_Working); ImGuiComponents.DisabledButton(Locs.PluginButton_Working);
} }
@ -1686,7 +1701,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
{ {
Task.Run(() => Task.Run(() =>
{ {
var unloadTask = Task.Run(() => plugin.Unload()) var unloadTask = Task.Run(() => plugin.UnloadAsync())
.ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_UnloadFail(plugin.Name)); .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_UnloadFail(plugin.Name));
unloadTask.Wait(); unloadTask.Wait();

View file

@ -57,6 +57,8 @@ namespace Dalamud.Interface.Internal.Windows
private int dtrSpacing; private int dtrSpacing;
private bool dtrSwapDirection; private bool dtrSwapDirection;
private int? pluginWaitBeforeFree;
private List<ThirdPartyRepoSettings> thirdRepoList; private List<ThirdPartyRepoSettings> thirdRepoList;
private bool thirdRepoListChanged; private bool thirdRepoListChanged;
private string thirdRepoTempUrl = string.Empty; private string thirdRepoTempUrl = string.Empty;
@ -113,6 +115,8 @@ namespace Dalamud.Interface.Internal.Windows
this.dtrSpacing = configuration.DtrSpacing; this.dtrSpacing = configuration.DtrSpacing;
this.dtrSwapDirection = configuration.DtrSwapDirection; this.dtrSwapDirection = configuration.DtrSwapDirection;
this.pluginWaitBeforeFree = configuration.PluginWaitBeforeFree;
this.doPluginTest = configuration.DoPluginTest; this.doPluginTest = configuration.DoPluginTest;
this.thirdRepoList = configuration.ThirdRepoList.Select(x => x.Clone()).ToList(); this.thirdRepoList = configuration.ThirdRepoList.Select(x => x.Clone()).ToList();
this.devPluginLocations = configuration.DevPluginLoadLocations.Select(x => x.Clone()).ToList(); this.devPluginLocations = configuration.DevPluginLoadLocations.Select(x => x.Clone()).ToList();
@ -561,6 +565,34 @@ namespace Dalamud.Interface.Internal.Windows
var configuration = Service<DalamudConfiguration>.Get(); var configuration = Service<DalamudConfiguration>.Get();
var pluginManager = Service<PluginManager>.Get(); var pluginManager = Service<PluginManager>.Get();
var useCustomPluginWaitBeforeFree = this.pluginWaitBeforeFree.HasValue;
if (ImGui.Checkbox(
Loc.Localize("DalamudSettingsPluginCustomizeWaitTime", "Customize wait time for plugin unload"),
ref useCustomPluginWaitBeforeFree))
{
if (!useCustomPluginWaitBeforeFree)
this.pluginWaitBeforeFree = null;
else
this.pluginWaitBeforeFree = PluginManager.PluginWaitBeforeFreeDefault;
}
if (useCustomPluginWaitBeforeFree)
{
var waitTime = this.pluginWaitBeforeFree ?? PluginManager.PluginWaitBeforeFreeDefault;
if (ImGui.SliderInt(
"Wait time###DalamudSettingsPluginCustomizeWaitTimeSlider",
ref waitTime,
0,
5000))
{
this.pluginWaitBeforeFree = waitTime;
}
}
ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsPluginCustomizeWaitTimeHint", "Configure the wait time between stopping plugin and completely unloading plugin. If you are experiencing crashes when exiting the game, try increasing this value."));
ImGuiHelpers.ScaledDummy(12);
#region Plugin testing #region Plugin testing
ImGui.Checkbox(Loc.Localize("DalamudSettingsPluginTest", "Get plugin testing builds"), ref this.doPluginTest); ImGui.Checkbox(Loc.Localize("DalamudSettingsPluginTest", "Get plugin testing builds"), ref this.doPluginTest);
@ -974,6 +1006,8 @@ namespace Dalamud.Interface.Internal.Windows
configuration.DtrSpacing = this.dtrSpacing; configuration.DtrSpacing = this.dtrSpacing;
configuration.DtrSwapDirection = this.dtrSwapDirection; configuration.DtrSwapDirection = this.dtrSwapDirection;
configuration.PluginWaitBeforeFree = this.pluginWaitBeforeFree;
configuration.DoPluginTest = this.doPluginTest; configuration.DoPluginTest = this.doPluginTest;
configuration.ThirdRepoList = this.thirdRepoList.Select(x => x.Clone()).ToList(); configuration.ThirdRepoList = this.thirdRepoList.Select(x => x.Clone()).ToList();
configuration.DevPluginLoadLocations = this.devPluginLocations.Select(x => x.Clone()).ToList(); configuration.DevPluginLoadLocations = this.devPluginLocations.Select(x => x.Clone()).ToList();

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using ImGuiScene; using ImGuiScene;
@ -47,37 +48,140 @@ namespace Dalamud.Interface
throw new ArgumentException("Texture must be 64x64"); throw new ArgumentException("Texture must be 64x64");
} }
var entry = new TitleScreenMenuEntry(text, texture, onTriggered); lock (this.entries)
this.entries.Add(entry); {
return entry; var entriesOfAssembly = this.entries.Where(x => x.CallingAssembly == Assembly.GetCallingAssembly()).ToList();
var priority = entriesOfAssembly.Any()
? unchecked(entriesOfAssembly.Select(x => x.Priority).Max() + 1)
: 0;
var entry = new TitleScreenMenuEntry(Assembly.GetCallingAssembly(), priority, text, texture, onTriggered);
var i = this.entries.BinarySearch(entry);
if (i < 0)
i = ~i;
this.entries.Insert(i, entry);
return entry;
}
}
/// <summary>
/// Adds a new entry to the title screen menu.
/// </summary>
/// <param name="priority">Priority of the entry.</param>
/// <param name="text">The text to show.</param>
/// <param name="texture">The texture to show.</param>
/// <param name="onTriggered">The action to execute when the option is selected.</param>
/// <returns>A <see cref="TitleScreenMenu"/> object that can be used to manage the entry.</returns>
/// <exception cref="ArgumentException">Thrown when the texture provided does not match the required resolution(64x64).</exception>
public TitleScreenMenuEntry AddEntry(ulong priority, string text, TextureWrap texture, Action onTriggered)
{
if (texture.Height != TextureSize || texture.Width != TextureSize)
{
throw new ArgumentException("Texture must be 64x64");
}
lock (this.entries)
{
var entry = new TitleScreenMenuEntry(Assembly.GetCallingAssembly(), priority, text, texture, onTriggered);
var i = this.entries.BinarySearch(entry);
if (i < 0)
i = ~i;
this.entries.Insert(i, entry);
return entry;
}
} }
/// <summary> /// <summary>
/// Remove an entry from the title screen menu. /// Remove an entry from the title screen menu.
/// </summary> /// </summary>
/// <param name="entry">The entry to remove.</param> /// <param name="entry">The entry to remove.</param>
public void RemoveEntry(TitleScreenMenuEntry entry) => this.entries.Remove(entry); public void RemoveEntry(TitleScreenMenuEntry entry)
{
lock (this.entries)
{
this.entries.Remove(entry);
}
}
/// <summary>
/// Adds a new entry to the title screen menu.
/// </summary>
/// <param name="priority">Priority of the entry.</param>
/// <param name="text">The text to show.</param>
/// <param name="texture">The texture to show.</param>
/// <param name="onTriggered">The action to execute when the option is selected.</param>
/// <returns>A <see cref="TitleScreenMenu"/> object that can be used to manage the entry.</returns>
/// <exception cref="ArgumentException">Thrown when the texture provided does not match the required resolution(64x64).</exception>
internal TitleScreenMenuEntry AddEntryCore(ulong priority, string text, TextureWrap texture, Action onTriggered)
{
if (texture.Height != TextureSize || texture.Width != TextureSize)
{
throw new ArgumentException("Texture must be 64x64");
}
lock (this.entries)
{
var entry = new TitleScreenMenuEntry(null, priority, text, texture, onTriggered);
this.entries.Add(entry);
return entry;
}
}
/// <summary>
/// Adds a new entry to the title screen menu.
/// </summary>
/// <param name="text">The text to show.</param>
/// <param name="texture">The texture to show.</param>
/// <param name="onTriggered">The action to execute when the option is selected.</param>
/// <returns>A <see cref="TitleScreenMenu"/> object that can be used to manage the entry.</returns>
/// <exception cref="ArgumentException">Thrown when the texture provided does not match the required resolution(64x64).</exception>
internal TitleScreenMenuEntry AddEntryCore(string text, TextureWrap texture, Action onTriggered)
{
if (texture.Height != TextureSize || texture.Width != TextureSize)
{
throw new ArgumentException("Texture must be 64x64");
}
lock (this.entries)
{
var entriesOfAssembly = this.entries.Where(x => x.CallingAssembly == null).ToList();
var priority = entriesOfAssembly.Any()
? unchecked(entriesOfAssembly.Select(x => x.Priority).Max() + 1)
: 0;
var entry = new TitleScreenMenuEntry(null, priority, text, texture, onTriggered);
this.entries.Add(entry);
return entry;
}
}
/// <summary> /// <summary>
/// Class representing an entry in the title screen menu. /// Class representing an entry in the title screen menu.
/// </summary> /// </summary>
public class TitleScreenMenuEntry public class TitleScreenMenuEntry : IComparable<TitleScreenMenuEntry>
{ {
private readonly Action onTriggered; private readonly Action onTriggered;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TitleScreenMenuEntry"/> class. /// Initializes a new instance of the <see cref="TitleScreenMenuEntry"/> class.
/// </summary> /// </summary>
/// <param name="callingAssembly">The calling assembly.</param>
/// <param name="priority">The priority of this entry.</param>
/// <param name="text">The text to show.</param> /// <param name="text">The text to show.</param>
/// <param name="texture">The texture to show.</param> /// <param name="texture">The texture to show.</param>
/// <param name="onTriggered">The action to execute when the option is selected.</param> /// <param name="onTriggered">The action to execute when the option is selected.</param>
internal TitleScreenMenuEntry(string text, TextureWrap texture, Action onTriggered) internal TitleScreenMenuEntry(Assembly? callingAssembly, ulong priority, string text, TextureWrap texture, Action onTriggered)
{ {
this.CallingAssembly = callingAssembly;
this.Priority = priority;
this.Name = text; this.Name = text;
this.Texture = texture; this.Texture = texture;
this.onTriggered = onTriggered; this.onTriggered = onTriggered;
} }
/// <summary>
/// Gets the priority of this entry.
/// </summary>
public ulong Priority { get; init; }
/// <summary> /// <summary>
/// Gets or sets the name of this entry. /// Gets or sets the name of this entry.
/// </summary> /// </summary>
@ -88,6 +192,11 @@ namespace Dalamud.Interface
/// </summary> /// </summary>
public TextureWrap Texture { get; set; } public TextureWrap Texture { get; set; }
/// <summary>
/// Gets the calling assembly of this entry.
/// </summary>
internal Assembly? CallingAssembly { get; init; }
/// <summary> /// <summary>
/// Gets the internal ID of this entry. /// Gets the internal ID of this entry.
/// </summary> /// </summary>
@ -100,6 +209,32 @@ namespace Dalamud.Interface
{ {
this.onTriggered(); this.onTriggered();
} }
/// <inheritdoc/>
public int CompareTo(TitleScreenMenuEntry? other)
{
if (other == null)
return 1;
if (this.CallingAssembly != other.CallingAssembly)
{
if (this.CallingAssembly == null && other.CallingAssembly == null)
return 0;
if (this.CallingAssembly == null && other.CallingAssembly != null)
return -1;
if (this.CallingAssembly != null && other.CallingAssembly == null)
return 1;
return string.Compare(
this.CallingAssembly!.FullName!,
other.CallingAssembly!.FullName!,
StringComparison.CurrentCultureIgnoreCase);
}
if (this.Priority != other.Priority)
return this.Priority.CompareTo(other.Priority);
if (this.Name != other.Name)
return string.Compare(this.Name, other.Name, StringComparison.InvariantCultureIgnoreCase);
return string.Compare(this.Name, other.Name, StringComparison.InvariantCulture);
}
} }
} }
} }

View file

@ -1,8 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Interface.GameFonts; using Dalamud.Interface.GameFonts;
@ -24,6 +25,8 @@ namespace Dalamud.Interface
{ {
private readonly Stopwatch stopwatch; private readonly Stopwatch stopwatch;
private readonly string namespaceName; private readonly string namespaceName;
private readonly InterfaceManager interfaceManager = Service<InterfaceManager>.Get();
private readonly GameFontManager gameFontManager = Service<GameFontManager>.Get();
private bool hasErrorWindow = false; private bool hasErrorWindow = false;
private bool lastFrameUiHideState = false; private bool lastFrameUiHideState = false;
@ -38,11 +41,10 @@ namespace Dalamud.Interface
this.stopwatch = new Stopwatch(); this.stopwatch = new Stopwatch();
this.namespaceName = namespaceName; this.namespaceName = namespaceName;
var interfaceManager = Service<InterfaceManager>.Get(); this.interfaceManager.Draw += this.OnDraw;
interfaceManager.Draw += this.OnDraw; this.interfaceManager.BuildFonts += this.OnBuildFonts;
interfaceManager.BuildFonts += this.OnBuildFonts; this.interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts;
interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts; this.interfaceManager.ResizeBuffers += this.OnResizeBuffers;
interfaceManager.ResizeBuffers += this.OnResizeBuffers;
} }
/// <summary> /// <summary>
@ -109,12 +111,12 @@ namespace Dalamud.Interface
/// <summary> /// <summary>
/// Gets the game's active Direct3D device. /// Gets the game's active Direct3D device.
/// </summary> /// </summary>
public Device Device => Service<InterfaceManager.InterfaceManagerWithScene>.Get().Manager.Device!; public Device Device => this.InterfaceManagerWithScene.Device!;
/// <summary> /// <summary>
/// Gets the game's main window handle. /// Gets the game's main window handle.
/// </summary> /// </summary>
public IntPtr WindowHandlePtr => Service<InterfaceManager.InterfaceManagerWithScene>.Get().Manager.WindowHandlePtr; public IntPtr WindowHandlePtr => this.InterfaceManagerWithScene.WindowHandlePtr;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether this plugin should hide its UI automatically when the game's UI is hidden. /// Gets or sets a value indicating whether this plugin should hide its UI automatically when the game's UI is hidden.
@ -141,8 +143,8 @@ namespace Dalamud.Interface
/// </summary> /// </summary>
public bool OverrideGameCursor public bool OverrideGameCursor
{ {
get => Service<InterfaceManager>.Get().OverrideGameCursor; get => this.interfaceManager.OverrideGameCursor;
set => Service<InterfaceManager>.Get().OverrideGameCursor = value; set => this.interfaceManager.OverrideGameCursor = value;
} }
/// <summary> /// <summary>
@ -157,7 +159,9 @@ namespace Dalamud.Interface
{ {
get get
{ {
var condition = Service<Condition>.Get(); var condition = Service<Condition>.GetNullable();
if (condition == null)
return false;
return condition[ConditionFlag.OccupiedInCutSceneEvent] return condition[ConditionFlag.OccupiedInCutSceneEvent]
|| condition[ConditionFlag.WatchingCutscene78]; || condition[ConditionFlag.WatchingCutscene78];
} }
@ -170,7 +174,9 @@ namespace Dalamud.Interface
{ {
get get
{ {
var condition = Service<Condition>.Get(); var condition = Service<Condition>.GetNullable();
if (condition == null)
return false;
return condition[ConditionFlag.WatchingCutscene]; return condition[ConditionFlag.WatchingCutscene];
} }
} }
@ -178,7 +184,12 @@ namespace Dalamud.Interface
/// <summary> /// <summary>
/// Gets a value indicating whether this plugin should modify the game's interface at this time. /// Gets a value indicating whether this plugin should modify the game's interface at this time.
/// </summary> /// </summary>
public bool ShouldModifyUi => Service<InterfaceManager>.GetNullable()?.IsDispatchingEvents ?? true; public bool ShouldModifyUi => this.interfaceManager.IsDispatchingEvents;
/// <summary>
/// Gets a value indicating whether UI functions can be used.
/// </summary>
public bool UiPrepared => Service<InterfaceManager.InterfaceManagerWithScene>.GetNullable() != null;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether statistics about UI draw time should be collected. /// Gets or sets a value indicating whether statistics about UI draw time should be collected.
@ -209,13 +220,20 @@ namespace Dalamud.Interface
/// </summary> /// </summary>
internal List<long> DrawTimeHistory { get; set; } = new List<long>(); internal List<long> DrawTimeHistory { get; set; } = new List<long>();
private InterfaceManager? InterfaceManagerWithScene =>
Service<InterfaceManager.InterfaceManagerWithScene>.GetNullable()?.Manager;
private Task<InterfaceManager> InterfaceManagerWithSceneAsync =>
Service<InterfaceManager.InterfaceManagerWithScene>.GetAsync().ContinueWith(task => task.Result.Manager);
/// <summary> /// <summary>
/// Loads an image from the specified file. /// Loads an image from the specified file.
/// </summary> /// </summary>
/// <param name="filePath">The full filepath to the image.</param> /// <param name="filePath">The full filepath to the image.</param>
/// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image().</returns> /// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image().</returns>
public TextureWrap LoadImage(string filePath) public TextureWrap LoadImage(string filePath)
=> Service<InterfaceManager.InterfaceManagerWithScene>.Get().Manager.LoadImage(filePath); => this.InterfaceManagerWithScene?.LoadImage(filePath)
?? throw new InvalidOperationException("Load failed.");
/// <summary> /// <summary>
/// Loads an image from a byte stream, such as a png downloaded into memory. /// Loads an image from a byte stream, such as a png downloaded into memory.
@ -223,7 +241,8 @@ namespace Dalamud.Interface
/// <param name="imageData">A byte array containing the raw image data.</param> /// <param name="imageData">A byte array containing the raw image data.</param>
/// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image().</returns> /// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image().</returns>
public TextureWrap LoadImage(byte[] imageData) public TextureWrap LoadImage(byte[] imageData)
=> Service<InterfaceManager.InterfaceManagerWithScene>.Get().Manager.LoadImage(imageData); => this.InterfaceManagerWithScene?.LoadImage(imageData)
?? throw new InvalidOperationException("Load failed.");
/// <summary> /// <summary>
/// Loads an image from raw unformatted pixel data, with no type or header information. To load formatted data, use <see cref="LoadImage(byte[])"/>. /// Loads an image from raw unformatted pixel data, with no type or header information. To load formatted data, use <see cref="LoadImage(byte[])"/>.
@ -234,14 +253,99 @@ namespace Dalamud.Interface
/// <param name="numChannels">The number of channels (bytes per pixel) of the image contained in <paramref name="imageData"/>. This should usually be 4.</param> /// <param name="numChannels">The number of channels (bytes per pixel) of the image contained in <paramref name="imageData"/>. This should usually be 4.</param>
/// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image().</returns> /// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image().</returns>
public TextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels) public TextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels)
=> Service<InterfaceManager.InterfaceManagerWithScene>.Get().Manager.LoadImageRaw(imageData, width, height, numChannels); => this.InterfaceManagerWithScene?.LoadImageRaw(imageData, width, height, numChannels)
?? throw new InvalidOperationException("Load failed.");
/// <summary>
/// Asynchronously loads an image from the specified file, when it's possible to do so.
/// </summary>
/// <param name="filePath">The full filepath to the image.</param>
/// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image().</returns>
public Task<TextureWrap> LoadImageAsync(string filePath) => Task.Run(
async () =>
(await this.InterfaceManagerWithSceneAsync).LoadImage(filePath)
?? throw new InvalidOperationException("Load failed."));
/// <summary>
/// Asynchronously loads an image from a byte stream, such as a png downloaded into memory, when it's possible to do so.
/// </summary>
/// <param name="imageData">A byte array containing the raw image data.</param>
/// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image().</returns>
public Task<TextureWrap> LoadImageAsync(byte[] imageData) => Task.Run(
async () =>
(await this.InterfaceManagerWithSceneAsync).LoadImage(imageData)
?? throw new InvalidOperationException("Load failed."));
/// <summary>
/// Asynchronously loads an image from raw unformatted pixel data, with no type or header information, when it's possible to do so. To load formatted data, use <see cref="LoadImage(byte[])"/>.
/// </summary>
/// <param name="imageData">A byte array containing the raw pixel data.</param>
/// <param name="width">The width of the image contained in <paramref name="imageData"/>.</param>
/// <param name="height">The height of the image contained in <paramref name="imageData"/>.</param>
/// <param name="numChannels">The number of channels (bytes per pixel) of the image contained in <paramref name="imageData"/>. This should usually be 4.</param>
/// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image().</returns>
public Task<TextureWrap> LoadImageRawAsync(byte[] imageData, int width, int height, int numChannels) => Task.Run(
async () =>
(await this.InterfaceManagerWithSceneAsync).LoadImageRaw(imageData, width, height, numChannels)
?? throw new InvalidOperationException("Load failed."));
/// <summary>
/// Waits for UI to become available for use.
/// </summary>
/// <returns>A task that completes when the game's Present has been called at least once.</returns>
public Task WaitForUi() => this.InterfaceManagerWithSceneAsync;
/// <summary>
/// Waits for UI to become available for use.
/// </summary>
/// <param name="func">Function to call.</param>
/// <param name="runInFrameworkThread">Specifies whether to call the function from the framework thread.</param>
/// <returns>A task that completes when the game's Present has been called at least once.</returns>
/// <typeparam name="T">Return type.</typeparam>
public Task<T> RunWhenUiPrepared<T>(Func<T> func, bool runInFrameworkThread = false)
{
if (runInFrameworkThread)
{
return this.InterfaceManagerWithSceneAsync
.ContinueWith(_ => Service<Framework>.Get().RunOnFrameworkThread(func))
.Unwrap();
}
else
{
return this.InterfaceManagerWithSceneAsync
.ContinueWith(_ => func());
}
}
/// <summary>
/// Waits for UI to become available for use.
/// </summary>
/// <param name="func">Function to call.</param>
/// <param name="runInFrameworkThread">Specifies whether to call the function from the framework thread.</param>
/// <returns>A task that completes when the game's Present has been called at least once.</returns>
/// <typeparam name="T">Return type.</typeparam>
public Task<T> RunWhenUiPrepared<T>(Func<Task<T>> func, bool runInFrameworkThread = false)
{
if (runInFrameworkThread)
{
return this.InterfaceManagerWithSceneAsync
.ContinueWith(_ => Service<Framework>.Get().RunOnFrameworkThread(func))
.Unwrap();
}
else
{
return this.InterfaceManagerWithSceneAsync
.ContinueWith(_ => func())
.Unwrap();
}
}
/// <summary> /// <summary>
/// Gets a game font. /// Gets a game font.
/// </summary> /// </summary>
/// <param name="style">Font to get.</param> /// <param name="style">Font to get.</param>
/// <returns>Handle to the game font which may or may not be available for use yet.</returns> /// <returns>Handle to the game font which may or may not be available for use yet.</returns>
public GameFontHandle GetGameFontHandle(GameFontStyle style) => Service<GameFontManager>.Get().NewFontRef(style); public GameFontHandle GetGameFontHandle(GameFontStyle style) => this.gameFontManager.NewFontRef(style);
/// <summary> /// <summary>
/// Call this to queue a rebuild of the font atlas.<br/> /// Call this to queue a rebuild of the font atlas.<br/>
@ -251,7 +355,7 @@ namespace Dalamud.Interface
public void RebuildFonts() public void RebuildFonts()
{ {
Log.Verbose("[FONT] {0} plugin is initiating FONT REBUILD", this.namespaceName); Log.Verbose("[FONT] {0} plugin is initiating FONT REBUILD", this.namespaceName);
Service<InterfaceManager>.Get().RebuildFonts(); this.interfaceManager.RebuildFonts();
} }
/// <summary> /// <summary>
@ -262,19 +366,25 @@ namespace Dalamud.Interface
/// <param name="type">The type of the notification.</param> /// <param name="type">The type of the notification.</param>
/// <param name="msDelay">The time the notification should be displayed for.</param> /// <param name="msDelay">The time the notification should be displayed for.</param>
public void AddNotification( public void AddNotification(
string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = 3000) => string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = 3000)
Service<NotificationManager>.Get().AddNotification(content, title, type, msDelay); {
Service<NotificationManager>
.GetAsync()
.ContinueWith(task =>
{
if (task.IsCompletedSuccessfully)
task.Result.AddNotification(content, title, type, msDelay);
});
}
/// <summary> /// <summary>
/// Unregister the UiBuilder. Do not call this in plugin code. /// Unregister the UiBuilder. Do not call this in plugin code.
/// </summary> /// </summary>
void IDisposable.Dispose() void IDisposable.Dispose()
{ {
var interfaceManager = Service<InterfaceManager>.Get(); this.interfaceManager.Draw -= this.OnDraw;
this.interfaceManager.BuildFonts -= this.OnBuildFonts;
interfaceManager.Draw -= this.OnDraw; this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers;
interfaceManager.BuildFonts -= this.OnBuildFonts;
interfaceManager.ResizeBuffers -= this.OnResizeBuffers;
} }
/// <summary> /// <summary>
@ -304,8 +414,9 @@ namespace Dalamud.Interface
private void OnDraw() private void OnDraw()
{ {
var configuration = Service<DalamudConfiguration>.Get(); var configuration = Service<DalamudConfiguration>.Get();
var gameGui = Service<GameGui>.Get(); var gameGui = Service<GameGui>.GetNullable();
var interfaceManager = Service<InterfaceManager>.Get(); if (gameGui == null)
return;
if ((gameGui.GameUiHidden && configuration.ToggleUiHide && if ((gameGui.GameUiHidden && configuration.ToggleUiHide &&
!(this.DisableUserUiHide || this.DisableAutomaticUiHide)) || !(this.DisableUserUiHide || this.DisableAutomaticUiHide)) ||
@ -329,7 +440,7 @@ namespace Dalamud.Interface
this.ShowUi?.Invoke(); this.ShowUi?.Invoke();
} }
if (!interfaceManager.FontsReady) if (!this.interfaceManager.FontsReady)
return; return;
ImGui.PushID(this.namespaceName); ImGui.PushID(this.namespaceName);

View file

@ -20,6 +20,9 @@ namespace Dalamud.Logging.Internal
private static readonly ConcurrentQueue<TaskInfo> NewlyCreatedTasks = new(); private static readonly ConcurrentQueue<TaskInfo> NewlyCreatedTasks = new();
private static bool clearRequested = false; private static bool clearRequested = false;
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private MonoMod.RuntimeDetour.Hook? scheduleAndStartHook; private MonoMod.RuntimeDetour.Hook? scheduleAndStartHook;
private bool enabled = false; private bool enabled = false;
@ -111,8 +114,7 @@ namespace Dalamud.Logging.Internal
this.ApplyPatch(); this.ApplyPatch();
var framework = Service<Framework>.Get(); this.framework.Update += this.FrameworkOnUpdate;
framework.Update += this.FrameworkOnUpdate;
this.enabled = true; this.enabled = true;
} }
@ -121,8 +123,7 @@ namespace Dalamud.Logging.Internal
{ {
this.scheduleAndStartHook?.Dispose(); this.scheduleAndStartHook?.Dispose();
var framework = Service<Framework>.Get(); this.framework.Update -= this.FrameworkOnUpdate;
framework.Update -= this.FrameworkOnUpdate;
} }
private static bool AddToActiveTasksHook(Func<Task, bool> orig, Task self) private static bool AddToActiveTasksHook(Func<Task, bool> orig, Task self)

View file

@ -15,6 +15,7 @@ using Dalamud.Game.Text.Sanitizer;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;

View file

@ -38,6 +38,11 @@ internal partial class PluginManager : IDisposable, IServiceType
/// </summary> /// </summary>
public const int DalamudApiLevel = 6; public const int DalamudApiLevel = 6;
/// <summary>
/// Default time to wait between plugin unload and plugin assembly unload.
/// </summary>
public const int PluginWaitBeforeFreeDefault = 500;
private static readonly ModuleLog Log = new("PLUGINM"); private static readonly ModuleLog Log = new("PLUGINM");
private readonly object pluginListLock = new(); private readonly object pluginListLock = new();
@ -207,16 +212,43 @@ internal partial class PluginManager : IDisposable, IServiceType
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() public void Dispose()
{ {
foreach (var plugin in this.InstalledPlugins) if (this.InstalledPlugins.Any())
{ {
try // Unload them first, just in case some of plugin codes are still running via callbacks initiated externally.
foreach (var plugin in this.InstalledPlugins.Where(plugin => !plugin.Manifest.CanUnloadAsync))
{ {
plugin.Dispose(); try
} {
catch (Exception ex) plugin.UnloadAsync(true, false).Wait();
{ }
Log.Error(ex, $"Error disposing {plugin.Name}"); catch (Exception ex)
{
Log.Error(ex, $"Error unloading {plugin.Name}");
}
} }
Task.WaitAll(this.InstalledPlugins
.Where(plugin => plugin.Manifest.CanUnloadAsync)
.Select(plugin => Task.Run(async () =>
{
try
{
await plugin.UnloadAsync(true, false);
}
catch (Exception ex)
{
Log.Error(ex, $"Error unloading {plugin.Name}");
}
})).ToArray());
// Just in case plugins still have tasks running that they didn't cancel when they should have,
// give them some time to complete it.
Thread.Sleep(this.configuration.PluginWaitBeforeFree ?? PluginWaitBeforeFreeDefault);
// Now that we've waited enough, dispose the whole plugin.
// Since plugins should have been unloaded above, this should be done quickly.
foreach (var plugin in this.InstalledPlugins)
plugin.ExplicitDisposeIgnoreExceptions($"Error disposing {plugin.Name}", Log);
} }
this.assemblyLocationMonoHook?.Dispose(); this.assemblyLocationMonoHook?.Dispose();
@ -891,7 +923,7 @@ internal partial class PluginManager : IDisposable, IServiceType
{ {
try try
{ {
plugin.Unload(); await plugin.UnloadAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -963,23 +995,30 @@ internal partial class PluginManager : IDisposable, IServiceType
/// </summary> /// </summary>
/// <param name="plugin">The plugin.</param> /// <param name="plugin">The plugin.</param>
/// <exception cref="Exception">Throws if the plugin is still loading/unloading.</exception> /// <exception cref="Exception">Throws if the plugin is still loading/unloading.</exception>
public void DeleteConfiguration(LocalPlugin plugin) /// <returns>The task.</returns>
public async Task DeleteConfigurationAsync(LocalPlugin plugin)
{ {
if (plugin.State == PluginState.InProgress) if (plugin.State == PluginState.Loading || plugin.State == PluginState.Unloaded)
throw new Exception("Cannot delete configuration for a loading/unloading plugin"); throw new Exception("Cannot delete configuration for a loading/unloading plugin");
if (plugin.IsLoaded) if (plugin.IsLoaded)
plugin.Unload(); await plugin.UnloadAsync();
// Let's wait so any handles on files in plugin configurations can be closed for (var waitUntil = Environment.TickCount64 + 1000; Environment.TickCount64 < waitUntil;)
Thread.Sleep(500); {
try
this.PluginConfigs.Delete(plugin.Name); {
this.PluginConfigs.Delete(plugin.Name);
Thread.Sleep(500); break;
}
catch (IOException)
{
await Task.Delay(100);
}
}
// Let's indicate "installer" here since this is supposed to be a fresh install // Let's indicate "installer" here since this is supposed to be a fresh install
plugin.LoadAsync(PluginLoadReason.Installer).Wait(); await plugin.LoadAsync(PluginLoadReason.Installer);
} }
/// <summary> /// <summary>

View file

@ -2,10 +2,13 @@ using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.Gui.Dtr; using Dalamud.Game.Gui.Dtr;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Exceptions; using Dalamud.Plugin.Internal.Exceptions;
@ -27,6 +30,8 @@ internal class LocalPlugin : IDisposable
private readonly FileInfo disabledFile; private readonly FileInfo disabledFile;
private readonly FileInfo testingFile; private readonly FileInfo testingFile;
private readonly SemaphoreSlim pluginLoadStateLock = new(1);
private PluginLoader? loader; private PluginLoader? loader;
private Assembly? pluginAssembly; private Assembly? pluginAssembly;
private Type? pluginType; private Type? pluginType;
@ -208,8 +213,20 @@ internal class LocalPlugin : IDisposable
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() public void Dispose()
{ {
this.instance?.Dispose(); var framework = Service<Framework>.GetNullable();
this.instance = null; var configuration = Service<DalamudConfiguration>.Get();
var didPluginDispose = false;
if (this.instance != null)
{
didPluginDispose = true;
if (this.Manifest.CanUnloadAsync || framework == null)
this.instance.Dispose();
else
framework.RunOnFrameworkThread(() => this.instance.Dispose()).Wait();
this.instance = null;
}
this.DalamudInterface?.ExplicitDispose(); this.DalamudInterface?.ExplicitDispose();
this.DalamudInterface = null; this.DalamudInterface = null;
@ -217,6 +234,8 @@ internal class LocalPlugin : IDisposable
this.pluginType = null; this.pluginType = null;
this.pluginAssembly = null; this.pluginAssembly = null;
if (this.loader != null && didPluginDispose)
Thread.Sleep(configuration.PluginWaitBeforeFree ?? PluginManager.PluginWaitBeforeFreeDefault);
this.loader?.Dispose(); this.loader?.Dispose();
} }
@ -228,54 +247,73 @@ internal class LocalPlugin : IDisposable
/// <returns>A task.</returns> /// <returns>A task.</returns>
public async Task LoadAsync(PluginLoadReason reason, bool reloading = false) public async Task LoadAsync(PluginLoadReason reason, bool reloading = false)
{ {
var startInfo = Service<DalamudStartInfo>.Get(); var configuration = await Service<DalamudConfiguration>.GetAsync();
var configuration = Service<DalamudConfiguration>.Get(); var framework = await Service<Framework>.GetAsync();
var pluginManager = Service<PluginManager>.Get(); var ioc = await Service<ServiceContainer>.GetAsync();
var pluginManager = await Service<PluginManager>.GetAsync();
var startInfo = await Service<DalamudStartInfo>.GetAsync();
// Allowed: Unloaded // UiBuilder constructor requires the following two.
switch (this.State) await Service<InterfaceManager>.GetAsync();
{ await Service<GameFontManager>.GetAsync();
case PluginState.InProgress:
throw new InvalidPluginOperationException($"Unable to load {this.Name}, already working");
case PluginState.Loaded:
throw new InvalidPluginOperationException($"Unable to load {this.Name}, already loaded");
case PluginState.LoadError:
throw new InvalidPluginOperationException($"Unable to load {this.Name}, load previously faulted, unload first");
case PluginState.UnloadError:
throw new InvalidPluginOperationException($"Unable to load {this.Name}, unload previously faulted, restart Dalamud");
case PluginState.Unloaded:
break;
default:
throw new ArgumentOutOfRangeException(this.State.ToString());
}
if (pluginManager.IsManifestBanned(this.Manifest)) if (this.Manifest.LoadRequiredState == 0)
throw new BannedPluginException($"Unable to load {this.Name}, banned"); _ = await Service<InterfaceManager.InterfaceManagerWithScene>.GetAsync();
if (this.Manifest.ApplicableVersion < startInfo.GameVersion)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version");
if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !configuration.LoadAllApiLevels)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level");
if (this.Manifest.Disabled)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, disabled");
this.State = PluginState.InProgress;
Log.Information($"Loading {this.DllFile.Name}");
if (this.DllFile.DirectoryName != null && File.Exists(Path.Combine(this.DllFile.DirectoryName, "Dalamud.dll")))
{
Log.Error("==== IMPORTANT MESSAGE TO {0}, THE DEVELOPER OF {1} ====", this.Manifest.Author!, this.Manifest.InternalName);
Log.Error("YOU ARE INCLUDING DALAMUD DEPENDENCIES IN YOUR BUILDS!!!");
Log.Error("You may not be able to load your plugin. \"<Private>False</Private>\" needs to be set in your csproj.");
Log.Error("If you are using ILMerge, do not merge anything other than your direct dependencies.");
Log.Error("Do not merge FFXIVClientStructs.Generators.dll.");
Log.Error("Please refer to https://github.com/goatcorp/Dalamud/discussions/603 for more information.");
}
await this.pluginLoadStateLock.WaitAsync();
try try
{ {
switch (this.State)
{
case PluginState.Loaded:
throw new InvalidPluginOperationException($"Unable to load {this.Name}, already loaded");
case PluginState.LoadError:
throw new InvalidPluginOperationException(
$"Unable to load {this.Name}, load previously faulted, unload first");
case PluginState.UnloadError:
throw new InvalidPluginOperationException(
$"Unable to load {this.Name}, unload previously faulted, restart Dalamud");
case PluginState.Unloaded:
break;
case PluginState.Loading:
case PluginState.Unloading:
default:
throw new ArgumentOutOfRangeException(this.State.ToString());
}
if (pluginManager.IsManifestBanned(this.Manifest))
throw new BannedPluginException($"Unable to load {this.Name}, banned");
if (this.Manifest.ApplicableVersion < startInfo.GameVersion)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version");
if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !configuration.LoadAllApiLevels)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level");
if (this.Manifest.Disabled)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, disabled");
this.State = PluginState.Loading;
Log.Information($"Loading {this.DllFile.Name}");
if (this.DllFile.DirectoryName != null &&
File.Exists(Path.Combine(this.DllFile.DirectoryName, "Dalamud.dll")))
{
Log.Error(
"==== IMPORTANT MESSAGE TO {0}, THE DEVELOPER OF {1} ====",
this.Manifest.Author!,
this.Manifest.InternalName);
Log.Error(
"YOU ARE INCLUDING DALAMUD DEPENDENCIES IN YOUR BUILDS!!!");
Log.Error(
"You may not be able to load your plugin. \"<Private>False</Private>\" needs to be set in your csproj.");
Log.Error(
"If you are using ILMerge, do not merge anything other than your direct dependencies.");
Log.Error("Do not merge FFXIVClientStructs.Generators.dll.");
Log.Error(
"Please refer to https://github.com/goatcorp/Dalamud/discussions/603 for more information.");
}
this.loader ??= PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig); this.loader ??= PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig);
if (reloading || this.IsDev) if (reloading || this.IsDev)
@ -309,7 +347,8 @@ internal class LocalPlugin : IDisposable
this.AssemblyName = this.pluginAssembly.GetName(); this.AssemblyName = this.pluginAssembly.GetName();
// Find the plugin interface implementation. It is guaranteed to exist after checking in the ctor. // Find the plugin interface implementation. It is guaranteed to exist after checking in the ctor.
this.pluginType ??= this.pluginAssembly.GetTypes().First(type => type.IsAssignableTo(typeof(IDalamudPlugin))); this.pluginType ??= this.pluginAssembly.GetTypes()
.First(type => type.IsAssignableTo(typeof(IDalamudPlugin)));
// Check for any loaded plugins with the same assembly name // Check for any loaded plugins with the same assembly name
var assemblyName = this.pluginAssembly.GetName().Name; var assemblyName = this.pluginAssembly.GetName().Name;
@ -319,7 +358,8 @@ internal class LocalPlugin : IDisposable
if (otherPlugin == this || otherPlugin.instance == null) if (otherPlugin == this || otherPlugin.instance == null)
continue; continue;
var otherPluginAssemblyName = otherPlugin.instance.GetType().Assembly.GetName().Name; var otherPluginAssemblyName =
otherPlugin.instance.GetType().Assembly.GetName().Name;
if (otherPluginAssemblyName == assemblyName && otherPluginAssemblyName != null) if (otherPluginAssemblyName == assemblyName && otherPluginAssemblyName != null)
{ {
this.State = PluginState.Unloaded; this.State = PluginState.Unloaded;
@ -330,17 +370,29 @@ internal class LocalPlugin : IDisposable
} }
// Update the location for the Location and CodeBase patches // Update the location for the Location and CodeBase patches
PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new PluginPatchData(this.DllFile); PluginManager.PluginLocations[this.pluginType.Assembly.FullName] =
new PluginPatchData(this.DllFile);
this.DalamudInterface = new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, this.DllFile, reason, this.IsDev); this.DalamudInterface =
new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, this.DllFile, reason, this.IsDev);
if (this.Manifest.LoadSync && this.Manifest.LoadRequiredState is 0 or 1)
{
this.instance = await framework.RunOnFrameworkThread(
() => ioc.CreateAsync(this.pluginType!, this.DalamudInterface!)) as IDalamudPlugin;
}
else
{
this.instance =
await ioc.CreateAsync(this.pluginType!, this.DalamudInterface!) as IDalamudPlugin;
}
var ioc = Service<ServiceContainer>.Get();
this.instance = await ioc.CreateAsync(this.pluginType, this.DalamudInterface) as IDalamudPlugin;
if (this.instance == null) if (this.instance == null)
{ {
this.State = PluginState.LoadError; this.State = PluginState.LoadError;
this.DalamudInterface.ExplicitDispose(); this.DalamudInterface.ExplicitDispose();
Log.Error($"Error while loading {this.Name}, failed to bind and call the plugin constructor"); Log.Error(
$"Error while loading {this.Name}, failed to bind and call the plugin constructor");
return; return;
} }
@ -363,6 +415,10 @@ internal class LocalPlugin : IDisposable
throw; throw;
} }
finally
{
this.pluginLoadStateLock.Release();
}
} }
/// <summary> /// <summary>
@ -370,31 +426,40 @@ internal class LocalPlugin : IDisposable
/// in the plugin list until it has been actually disposed. /// in the plugin list until it has been actually disposed.
/// </summary> /// </summary>
/// <param name="reloading">Unload while reloading.</param> /// <param name="reloading">Unload while reloading.</param>
public void Unload(bool reloading = false) /// <param name="waitBeforeLoaderDispose">Wait before disposing loader.</param>
/// <returns>The task.</returns>
public async Task UnloadAsync(bool reloading = false, bool waitBeforeLoaderDispose = true)
{ {
// Allowed: Loaded, LoadError(we are cleaning this up while we're at it) var configuration = Service<DalamudConfiguration>.Get();
switch (this.State) var framework = Service<Framework>.GetNullable();
{
case PluginState.InProgress:
throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already working");
case PluginState.Unloaded:
throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already unloaded");
case PluginState.UnloadError:
throw new InvalidPluginOperationException($"Unable to unload {this.Name}, unload previously faulted, restart Dalamud");
case PluginState.Loaded:
break;
case PluginState.LoadError:
break;
default:
throw new ArgumentOutOfRangeException(this.State.ToString());
}
await this.pluginLoadStateLock.WaitAsync();
try try
{ {
this.State = PluginState.InProgress; switch (this.State)
{
case PluginState.Unloaded:
throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already unloaded");
case PluginState.UnloadError:
throw new InvalidPluginOperationException(
$"Unable to unload {this.Name}, unload previously faulted, restart Dalamud");
case PluginState.Loaded:
case PluginState.LoadError:
break;
case PluginState.Loading:
case PluginState.Unloading:
default:
throw new ArgumentOutOfRangeException(this.State.ToString());
}
this.State = PluginState.Unloading;
Log.Information($"Unloading {this.DllFile.Name}"); Log.Information($"Unloading {this.DllFile.Name}");
this.instance?.Dispose(); if (this.Manifest.CanUnloadAsync || framework == null)
this.instance?.Dispose();
else
await framework.RunOnFrameworkThread(() => this.instance?.Dispose());
this.instance = null; this.instance = null;
this.DalamudInterface?.ExplicitDispose(); this.DalamudInterface?.ExplicitDispose();
@ -405,6 +470,8 @@ internal class LocalPlugin : IDisposable
if (!reloading) if (!reloading)
{ {
if (waitBeforeLoaderDispose && this.loader != null)
await Task.Delay(configuration.PluginWaitBeforeFree ?? PluginManager.PluginWaitBeforeFreeDefault);
this.loader?.Dispose(); this.loader?.Dispose();
this.loader = null; this.loader = null;
} }
@ -419,6 +486,13 @@ internal class LocalPlugin : IDisposable
throw; throw;
} }
finally
{
// We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates.
Service<DtrBar>.GetNullable()?.HandleRemovedNodes();
this.pluginLoadStateLock.Release();
}
} }
/// <summary> /// <summary>
@ -427,12 +501,7 @@ internal class LocalPlugin : IDisposable
/// <returns>A task.</returns> /// <returns>A task.</returns>
public async Task ReloadAsync() public async Task ReloadAsync()
{ {
this.Unload(true); await this.UnloadAsync(true);
// We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates.
var dtr = Service<DtrBar>.Get();
dtr.HandleRemovedNodes();
await this.LoadAsync(PluginLoadReason.Reload, true); await this.LoadAsync(PluginLoadReason.Reload, true);
} }
@ -444,7 +513,8 @@ internal class LocalPlugin : IDisposable
// Allowed: Unloaded, UnloadError // Allowed: Unloaded, UnloadError
switch (this.State) switch (this.State)
{ {
case PluginState.InProgress: case PluginState.Loading:
case PluginState.Unloading:
case PluginState.Loaded: case PluginState.Loaded:
case PluginState.LoadError: case PluginState.LoadError:
throw new InvalidPluginOperationException($"Unable to enable {this.Name}, still loaded"); throw new InvalidPluginOperationException($"Unable to enable {this.Name}, still loaded");
@ -471,7 +541,8 @@ internal class LocalPlugin : IDisposable
// Allowed: Unloaded, UnloadError // Allowed: Unloaded, UnloadError
switch (this.State) switch (this.State)
{ {
case PluginState.InProgress: case PluginState.Loading:
case PluginState.Unloading:
case PluginState.Loaded: case PluginState.Loaded:
case PluginState.LoadError: case PluginState.LoadError:
throw new InvalidPluginOperationException($"Unable to disable {this.Name}, still loaded"); throw new InvalidPluginOperationException($"Unable to disable {this.Name}, still loaded");

View file

@ -156,6 +156,12 @@ internal record PluginManifest
[JsonProperty] [JsonProperty]
public int LoadPriority { get; init; } public int LoadPriority { get; init; }
/// <summary>
/// Gets a value indicating whether the plugin can be unloaded asynchronously.
/// </summary>
[JsonProperty]
public bool CanUnloadAsync { get; init; }
/// <summary> /// <summary>
/// Gets a list of screenshot image URLs to show in the plugin installer. /// Gets a list of screenshot image URLs to show in the plugin installer.
/// </summary> /// </summary>

View file

@ -16,9 +16,9 @@ internal enum PluginState
UnloadError, UnloadError,
/// <summary> /// <summary>
/// Currently loading. /// Currently unloading.
/// </summary> /// </summary>
InProgress, Unloading,
/// <summary> /// <summary>
/// Load is successful. /// Load is successful.
@ -29,4 +29,9 @@ internal enum PluginState
/// Plugin has thrown an error during loading. /// Plugin has thrown an error during loading.
/// </summary> /// </summary>
LoadError, LoadError,
/// <summary>
/// Currently loading.
/// </summary>
Loading,
} }

View file

@ -25,6 +25,8 @@ namespace Dalamud
private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new(); private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new();
private static readonly List<Type> LoadedServices = new();
/// <summary> /// <summary>
/// Gets task that gets completed when all blocking early loading services are done loading. /// Gets task that gets completed when all blocking early loading services are done loading.
/// </summary> /// </summary>
@ -38,20 +40,35 @@ namespace Dalamud
/// <param name="configuration">Instance of <see cref="DalamudConfiguration"/>.</param> /// <param name="configuration">Instance of <see cref="DalamudConfiguration"/>.</param>
public static void InitializeProvidedServicesAndClientStructs(Dalamud dalamud, DalamudStartInfo startInfo, DalamudConfiguration configuration) public static void InitializeProvidedServicesAndClientStructs(Dalamud dalamud, DalamudStartInfo startInfo, DalamudConfiguration configuration)
{ {
Service<Dalamud>.Provide(dalamud);
Service<DalamudStartInfo>.Provide(startInfo);
Service<DalamudConfiguration>.Provide(configuration);
Service<ServiceContainer>.Provide(new ServiceContainer());
// Initialize the process information. // Initialize the process information.
var cacheDir = new DirectoryInfo(Path.Combine(startInfo.WorkingDirectory!, "cachedSigs")); var cacheDir = new DirectoryInfo(Path.Combine(startInfo.WorkingDirectory!, "cachedSigs"));
if (!cacheDir.Exists) if (!cacheDir.Exists)
cacheDir.Create(); cacheDir.Create();
Service<SigScanner>.Provide(new SigScanner(true, new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}.json"))));
lock (LoadedServices)
{
Service<Dalamud>.Provide(dalamud);
LoadedServices.Add(typeof(Dalamud));
Service<DalamudStartInfo>.Provide(startInfo);
LoadedServices.Add(typeof(DalamudStartInfo));
Service<DalamudConfiguration>.Provide(configuration);
LoadedServices.Add(typeof(DalamudConfiguration));
Service<ServiceContainer>.Provide(new ServiceContainer());
LoadedServices.Add(typeof(ServiceContainer));
Service<SigScanner>.Provide(
new SigScanner(
true, new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}.json"))));
LoadedServices.Add(typeof(SigScanner));
}
using (Timings.Start("CS Resolver Init")) using (Timings.Start("CS Resolver Init"))
{ {
FFXIVClientStructs.Resolver.InitializeParallel(new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}_cs.json"))); FFXIVClientStructs.Resolver.InitializeParallel(
new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}_cs.json")));
} }
} }
@ -62,7 +79,6 @@ namespace Dalamud
public static async Task InitializeEarlyLoadableServices() public static async Task InitializeEarlyLoadableServices()
{ {
using var serviceInitializeTimings = Timings.Start("Services Init"); using var serviceInitializeTimings = Timings.Start("Services Init");
var service = typeof(Service<>);
var earlyLoadingServices = new HashSet<Type>(); var earlyLoadingServices = new HashSet<Type>();
var blockingEarlyLoadingServices = new HashSet<Type>(); var blockingEarlyLoadingServices = new HashSet<Type>();
@ -76,12 +92,14 @@ namespace Dalamud
if (attr?.IsAssignableTo(typeof(EarlyLoadedService)) != true) if (attr?.IsAssignableTo(typeof(EarlyLoadedService)) != true)
continue; continue;
var getTask = (Task)service.MakeGenericType(serviceType).InvokeMember( var getTask = (Task)typeof(Service<>)
"GetAsync", .MakeGenericType(serviceType)
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, .InvokeMember(
null, "GetAsync",
null, BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null); null,
null,
null);
if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedService))) if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedService)))
{ {
@ -94,7 +112,7 @@ namespace Dalamud
} }
dependencyServicesMap[serviceType] = dependencyServicesMap[serviceType] =
(List<Type>)service (List<Type>)typeof(Service<>)
.MakeGenericType(serviceType) .MakeGenericType(serviceType)
.InvokeMember( .InvokeMember(
"GetDependencyServices", "GetDependencyServices",
@ -118,9 +136,9 @@ namespace Dalamud
} }
}).ConfigureAwait(false); }).ConfigureAwait(false);
var tasks = new List<Task>();
try try
{ {
var tasks = new List<Task>();
var servicesToLoad = new HashSet<Type>(); var servicesToLoad = new HashSet<Type>();
servicesToLoad.UnionWith(earlyLoadingServices); servicesToLoad.UnionWith(earlyLoadingServices);
servicesToLoad.UnionWith(blockingEarlyLoadingServices); servicesToLoad.UnionWith(blockingEarlyLoadingServices);
@ -133,13 +151,25 @@ namespace Dalamud
x => getAsyncTaskMap.GetValueOrDefault(x)?.IsCompleted == false)) x => getAsyncTaskMap.GetValueOrDefault(x)?.IsCompleted == false))
continue; continue;
tasks.Add((Task)service.MakeGenericType(serviceType).InvokeMember( tasks.Add((Task)typeof(Service<>)
"StartLoader", .MakeGenericType(serviceType)
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, .InvokeMember(
null, "StartLoader",
null, BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic,
null)); null,
null,
null));
servicesToLoad.Remove(serviceType); servicesToLoad.Remove(serviceType);
tasks.Add(tasks.Last().ContinueWith(task =>
{
if (task.IsFaulted)
return;
lock (LoadedServices)
{
LoadedServices.Add(serviceType);
}
}));
} }
if (!tasks.Any()) if (!tasks.Any())
@ -172,10 +202,49 @@ namespace Dalamud
// don't care, as this means task result/exception has already been set // don't care, as this means task result/exception has already been set
} }
while (tasks.Any())
{
await Task.WhenAny(tasks);
tasks.RemoveAll(x => x.IsCompleted);
}
UnloadAllServices();
throw; throw;
} }
} }
/// <summary>
/// Unloads all services, in the reverse order of load.
/// </summary>
public static void UnloadAllServices()
{
var framework = Service<Framework>.GetNullable(Service<Framework>.ExceptionPropagationMode.None);
if (framework is { IsInFrameworkUpdateThread: false, IsFrameworkUnloading: false })
{
framework.RunOnFrameworkThread(UnloadAllServices).Wait();
return;
}
lock (LoadedServices)
{
while (LoadedServices.Any())
{
var serviceType = LoadedServices.Last();
LoadedServices.RemoveAt(LoadedServices.Count - 1);
typeof(Service<>)
.MakeGenericType(serviceType)
.InvokeMember(
"Unset",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic,
null,
null,
null);
}
}
}
/// <summary> /// <summary>
/// Indicates that this constructor will be called for early initialization. /// Indicates that this constructor will be called for early initialization.
/// </summary> /// </summary>

View file

@ -19,8 +19,7 @@ namespace Dalamud
/// <typeparam name="T">The class you want to store in the service locator.</typeparam> /// <typeparam name="T">The class you want to store in the service locator.</typeparam>
internal static class Service<T> where T : IServiceType internal static class Service<T> where T : IServiceType
{ {
// ReSharper disable once StaticMemberInGenericType private static TaskCompletionSource<T> instanceTcs = new();
private static readonly TaskCompletionSource<T> InstanceTcs = new();
static Service() static Service()
{ {
@ -31,50 +30,28 @@ namespace Dalamud
ServiceManager.Log.Debug("Service<{0}>: Static ctor called", typeof(T).Name); ServiceManager.Log.Debug("Service<{0}>: Static ctor called", typeof(T).Name);
if (exposeToPlugins) if (exposeToPlugins)
Service<ServiceContainer>.Get().RegisterSingleton(InstanceTcs.Task); Service<ServiceContainer>.Get().RegisterSingleton(instanceTcs.Task);
} }
/// <summary> /// <summary>
/// Initializes the service. /// Specifies how to handle the cases of failed services when calling <see cref="Service{T}.GetNullable"/>.
/// </summary> /// </summary>
/// <returns>The object.</returns> public enum ExceptionPropagationMode
[UsedImplicitly]
public static Task<T> StartLoader()
{ {
var attr = typeof(T).GetCustomAttribute<ServiceManager.Service>(true)?.GetType(); /// <summary>
if (attr?.IsAssignableTo(typeof(ServiceManager.EarlyLoadedService)) != true) /// Propagate all exceptions.
throw new InvalidOperationException($"{typeof(T).Name} is not an EarlyLoadedService"); /// </summary>
PropagateAll,
return Task.Run(Timings.AttachTimingHandle(async () => /// <summary>
{ /// Propagate all exceptions, except for <see cref="UnloadedException"/>.
ServiceManager.Log.Debug("Service<{0}>: Begin construction", typeof(T).Name); /// </summary>
try PropagateNonUnloaded,
{
var instance = await ConstructObject();
InstanceTcs.SetResult(instance);
foreach (var method in typeof(T).GetMethods( /// <summary>
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) /// Treat all exceptions as null.
{ /// </summary>
if (method.GetCustomAttribute<ServiceManager.CallWhenServicesReady>(true) == null) None,
continue;
ServiceManager.Log.Debug("Service<{0}>: Calling {1}", typeof(T).Name, method.Name);
var args = await Task.WhenAll(method.GetParameters().Select(
x => ResolveServiceFromTypeAsync(x.ParameterType)));
method.Invoke(instance, args);
}
ServiceManager.Log.Debug("Service<{0}>: Construction complete", typeof(T).Name);
return instance;
}
catch (Exception e)
{
ServiceManager.Log.Error(e, "Service<{0}>: Construction failure", typeof(T).Name);
InstanceTcs.SetException(e);
throw;
}
}));
} }
/// <summary> /// <summary>
@ -83,7 +60,7 @@ namespace Dalamud
/// <param name="obj">Object to set.</param> /// <param name="obj">Object to set.</param>
public static void Provide(T obj) public static void Provide(T obj)
{ {
InstanceTcs.SetResult(obj); instanceTcs.SetResult(obj);
ServiceManager.Log.Debug("Service<{0}>: Provided", typeof(T).Name); ServiceManager.Log.Debug("Service<{0}>: Provided", typeof(T).Name);
} }
@ -94,7 +71,7 @@ namespace Dalamud
public static void ProvideException(Exception exception) public static void ProvideException(Exception exception)
{ {
ServiceManager.Log.Error(exception, "Service<{0}>: Error", typeof(T).Name); ServiceManager.Log.Error(exception, "Service<{0}>: Error", typeof(T).Name);
InstanceTcs.SetException(exception); instanceTcs.SetException(exception);
} }
/// <summary> /// <summary>
@ -103,9 +80,9 @@ namespace Dalamud
/// <returns>The object.</returns> /// <returns>The object.</returns>
public static T Get() public static T Get()
{ {
if (!InstanceTcs.Task.IsCompleted) if (!instanceTcs.Task.IsCompleted)
InstanceTcs.Task.Wait(); instanceTcs.Task.Wait();
return InstanceTcs.Task.Result; return instanceTcs.Task.Result;
} }
/// <summary> /// <summary>
@ -113,13 +90,27 @@ namespace Dalamud
/// </summary> /// </summary>
/// <returns>The object.</returns> /// <returns>The object.</returns>
[UsedImplicitly] [UsedImplicitly]
public static Task<T> GetAsync() => InstanceTcs.Task; public static Task<T> GetAsync() => instanceTcs.Task;
/// <summary> /// <summary>
/// Attempt to pull the instance out of the service locator. /// Attempt to pull the instance out of the service locator.
/// </summary> /// </summary>
/// <param name="propagateException">Specifies which exceptions to propagate.</param>
/// <returns>The object if registered, null otherwise.</returns> /// <returns>The object if registered, null otherwise.</returns>
public static T? GetNullable() => InstanceTcs.Task.IsCompleted ? InstanceTcs.Task.Result : default; public static T? GetNullable(ExceptionPropagationMode propagateException = ExceptionPropagationMode.PropagateNonUnloaded)
{
if (instanceTcs.Task.IsCompletedSuccessfully)
return instanceTcs.Task.Result;
if (instanceTcs.Task.IsFaulted && propagateException != ExceptionPropagationMode.None)
{
if (propagateException == ExceptionPropagationMode.PropagateNonUnloaded
&& instanceTcs.Task.Exception!.InnerExceptions.FirstOrDefault() is UnloadedException)
return default;
throw instanceTcs.Task.Exception!;
}
return default;
}
/// <summary> /// <summary>
/// Gets an enumerable containing Service&lt;T&gt;s that are required for this Service to initialize without blocking. /// Gets an enumerable containing Service&lt;T&gt;s that are required for this Service to initialize without blocking.
@ -142,6 +133,77 @@ namespace Dalamud
.ToList(); .ToList();
} }
[UsedImplicitly]
private static Task<T> StartLoader()
{
if (instanceTcs.Task.IsCompleted)
throw new InvalidOperationException($"{typeof(T).Name} is already loaded or disposed.");
var attr = typeof(T).GetCustomAttribute<ServiceManager.Service>(true)?.GetType();
if (attr?.IsAssignableTo(typeof(ServiceManager.EarlyLoadedService)) != true)
throw new InvalidOperationException($"{typeof(T).Name} is not an EarlyLoadedService");
return Task.Run(Timings.AttachTimingHandle(async () =>
{
ServiceManager.Log.Debug("Service<{0}>: Begin construction", typeof(T).Name);
try
{
var instance = await ConstructObject();
instanceTcs.SetResult(instance);
foreach (var method in typeof(T).GetMethods(
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
if (method.GetCustomAttribute<ServiceManager.CallWhenServicesReady>(true) == null)
continue;
ServiceManager.Log.Debug("Service<{0}>: Calling {1}", typeof(T).Name, method.Name);
var args = await Task.WhenAll(method.GetParameters().Select(
x => ResolveServiceFromTypeAsync(x.ParameterType)));
method.Invoke(instance, args);
}
ServiceManager.Log.Debug("Service<{0}>: Construction complete", typeof(T).Name);
return instance;
}
catch (Exception e)
{
ServiceManager.Log.Error(e, "Service<{0}>: Construction failure", typeof(T).Name);
instanceTcs.SetException(e);
throw;
}
}));
}
[UsedImplicitly]
private static void Unset()
{
if (!instanceTcs.Task.IsCompletedSuccessfully)
return;
var instance = instanceTcs.Task.Result;
if (instance is IDisposable disposable)
{
ServiceManager.Log.Debug("Service<{0}>: Disposing", typeof(T).Name);
try
{
disposable.Dispose();
ServiceManager.Log.Debug("Service<{0}>: Disposed", typeof(T).Name);
}
catch (Exception e)
{
ServiceManager.Log.Warning(e, "Service<{0}>: Dispose failure", typeof(T).Name);
}
}
else
{
ServiceManager.Log.Debug("Service<{0}>: Unset", typeof(T).Name);
}
instanceTcs = new TaskCompletionSource<T>();
instanceTcs.SetException(new UnloadedException());
}
private static async Task<object?> ResolveServiceFromTypeAsync(Type type) private static async Task<object?> ResolveServiceFromTypeAsync(Type type)
{ {
var task = (Task)typeof(Service<>) var task = (Task)typeof(Service<>)
@ -180,5 +242,19 @@ namespace Dalamud
return (T)ctor.Invoke(args)!; return (T)ctor.Invoke(args)!;
} }
} }
/// <summary>
/// Exception thrown when service is attempted to be retrieved when it's unloaded.
/// </summary>
public class UnloadedException : InvalidOperationException
{
/// <summary>
/// Initializes a new instance of the <see cref="UnloadedException"/> class.
/// </summary>
public UnloadedException()
: base("Service is unloaded.")
{
}
}
} }
} }

View file

@ -15,6 +15,7 @@ using Dalamud.Game;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Logging.Internal;
using ImGuiNET; using ImGuiNET;
using Microsoft.Win32; using Microsoft.Win32;
using Serilog; using Serilog;
@ -536,6 +537,31 @@ namespace Dalamud.Utility
obj.Dispose(); obj.Dispose();
} }
/// <summary>
/// Dispose this object.
/// </summary>
/// <param name="obj">The object to dispose.</param>
/// <param name="logMessage">Log message to print, if specified and an error occurs.</param>
/// <param name="moduleLog">Module logger, if any.</param>
/// <typeparam name="T">The type of object to dispose.</typeparam>
internal static void ExplicitDisposeIgnoreExceptions<T>(this T obj, string? logMessage = null, ModuleLog? moduleLog = null) where T : IDisposable
{
try
{
obj.Dispose();
}
catch (Exception e)
{
if (logMessage == null)
return;
if (moduleLog != null)
moduleLog.Error(e, logMessage);
else
Log.Error(e, logMessage);
}
}
private static unsafe void ShowValue(ulong addr, IEnumerable<string> path, Type type, object value) private static unsafe void ShowValue(ulong addr, IEnumerable<string> path, Type type, object value)
{ {
if (type.IsPointer) if (type.IsPointer)