Merge branch 'net5'

This commit is contained in:
goaaats 2022-06-29 11:55:03 +02:00
commit fe3b487a05
No known key found for this signature in database
GPG key ID: 49E2AA8C6A76498B
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) {
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 },
{ "disable_game_openprocess_access_check", &disable_game_openprocess_access_check },
{ "redirect_openprocess", &redirect_openprocess },
{ "backup_userdata_save", &backup_userdata_save },
}
) {
try {

View file

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

View file

@ -315,7 +315,7 @@ namespace Dalamud.Injector
startInfo.BootShowConsole = args.Contains("--console");
startInfo.BootEnableEtw = args.Contains("--etw");
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.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0;
startInfo.BootWaitMessageBox |= args.Contains("--msgbox2") ? 2 : 0;

View file

@ -190,6 +190,11 @@ namespace Dalamud.Configuration.Internal
/// </summary>
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>
/// Gets or sets a value indicating whether or not the debug log should scroll automatically.
/// </summary>
@ -261,6 +266,12 @@ namespace Dalamud.Configuration.Internal
/// </summary>
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>
/// Gets or sets a list of saved styles.
/// </summary>

View file

@ -5,21 +5,11 @@ using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.Gui.Internal;
using Dalamud.Game.Internal;
using Dalamud.Game.Network.Internal;
using Dalamud.Hooking.Internal;
using Dalamud.Interface.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Support;
using Dalamud.Utility;
using Serilog;
using Serilog.Core;
using Serilog.Events;
#if DEBUG
[assembly: InternalsVisibleTo("Dalamud.CorePlugin")]
@ -33,13 +23,11 @@ namespace Dalamud
/// <summary>
/// The main Dalamud class containing all subsystems.
/// </summary>
internal sealed class Dalamud : IDisposable, IServiceType
internal sealed class Dalamud : IServiceType
{
#region Internals
private readonly ManualResetEvent unloadSignal;
private readonly ManualResetEvent finishUnloadSignal;
private MonoMod.RuntimeDetour.Hook processMonoHook;
private bool hasDisposedPlugins = false;
#endregion
@ -48,22 +36,13 @@ namespace Dalamud
/// Initializes a new instance of the <see cref="Dalamud"/> class.
/// </summary>
/// <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="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.Reset();
this.finishUnloadSignal = finishSignal;
this.finishUnloadSignal.Reset();
SerilogEventSink.Instance.LogLine += SerilogOnLogLine;
ServiceManager.InitializeProvidedServicesAndClientStructs(this, info, configuration);
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>
/// Gets location of stored assets.
/// </summary>
@ -138,14 +112,6 @@ namespace Dalamud
this.unloadSignal.WaitOne();
}
/// <summary>
/// Wait for a queued unload to be finalized.
/// </summary>
public void WaitForUnloadFinish()
{
this.finishUnloadSignal?.WaitOne();
}
/// <summary>
/// Dispose subsystems related to plugin handling.
/// </summary>
@ -169,46 +135,6 @@ namespace Dalamud
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>
/// Replace the built-in exception handler with a debug one.
/// </summary>
@ -221,13 +147,5 @@ namespace Dalamud
var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(releaseFilter);
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>
public sealed class EntryPoint
{
/// <summary>
/// Log level switch for runtime log level change.
/// </summary>
public static readonly LoggingLevelSwitch LogLevelSwitch = new(LogEventLevel.Verbose);
/// <summary>
/// A delegate used during initialization of the CLR from Dalamud.Boot.
/// </summary>
@ -107,6 +112,49 @@ namespace Dalamud
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>
/// Initialize all Dalamud subsystems and start running on the main thread.
/// </summary>
@ -115,22 +163,23 @@ namespace Dalamud
private static void RunThread(DalamudStartInfo info, IntPtr mainThreadContinueEvent)
{
// 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
var configuration = DalamudConfiguration.Load(info.ConfigurationPath);
var configuration = DalamudConfiguration.Load(info.ConfigurationPath!);
// Set the appropriate logging level from the configuration
#if !DEBUG
levelSwitch.MinimumLevel = configuration.LogLevel;
if (!configuration.LogSynchronously)
InitLogging(info.WorkingDirectory!, info.BootShowConsole, configuration.LogSynchronously);
LogLevelSwitch.MinimumLevel = configuration.LogLevel;
#endif
// Log any unhandled exception.
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
var finishSignal = new ManualResetEvent(false);
try
{
if (info.DelayInitializeMs > 0)
@ -148,12 +197,12 @@ namespace Dalamud
if (!Util.IsLinux())
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());
dalamud.WaitForUnload();
dalamud.Dispose();
ServiceManager.UnloadAllServices();
}
catch (Exception ex)
{
@ -166,11 +215,18 @@ namespace Dalamud
Log.Information("Session has ended.");
Log.CloseAndFlush();
finishSignal.Set();
SerilogEventSink.Instance.LogLine -= SerilogOnLogLine;
}
}
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)
{
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)
{
try

View file

@ -107,6 +107,9 @@ namespace Dalamud.Game
private readonly DalamudLinkPayload openInstallerWindowLink;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private bool hasSeenLoadingMsg;
private bool hasAutoUpdatedPlugins;
@ -118,7 +121,7 @@ namespace Dalamud.Game
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)
{
var configuration = Service<DalamudConfiguration>.Get();
var textVal = message.TextValue;
if (!configuration.DisableRmtFiltering)
if (!this.configuration.DisableRmtFiltering)
{
var matched = this.rmtRegex.IsMatch(textVal);
if (matched)
@ -161,8 +162,8 @@ namespace Dalamud.Game
}
}
if (configuration.BadWords != null &&
configuration.BadWords.Any(x => !string.IsNullOrEmpty(x) && textVal.Contains(x)))
if (this.configuration.BadWords != null &&
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
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)
{
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)
this.PrintWelcomeMessage();
@ -232,17 +235,19 @@ namespace Dalamud.Game
private void PrintWelcomeMessage()
{
var chatGui = Service<ChatGui>.Get();
var configuration = Service<DalamudConfiguration>.Get();
var pluginManager = Service<PluginManager>.Get();
var dalamudInterface = Service<DalamudInterface>.Get();
var chatGui = Service<ChatGui>.GetNullable();
var pluginManager = Service<PluginManager>.GetNullable();
var dalamudInterface = Service<DalamudInterface>.GetNullable();
if (chatGui == null || pluginManager == null || dalamudInterface == null)
return;
var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString();
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)));
if (configuration.PrintPluginsWelcomeMsg)
if (this.configuration.PrintPluginsWelcomeMsg)
{
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
{
@ -258,14 +263,14 @@ namespace Dalamud.Game
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();
configuration.LastChangelogMajorMinor = ChangelogWindow.WarrantsChangelogForMajorMinor;
this.configuration.LastChangelogMajorMinor = ChangelogWindow.WarrantsChangelogForMajorMinor;
}
configuration.LastVersion = assemblyVersion;
configuration.Save();
this.configuration.LastVersion = assemblyVersion;
this.configuration.Save();
}
this.hasSeenLoadingMsg = true;
@ -273,10 +278,12 @@ namespace Dalamud.Game
private void AutoUpdatePlugins()
{
var chatGui = Service<ChatGui>.Get();
var configuration = Service<DalamudConfiguration>.Get();
var pluginManager = Service<PluginManager>.Get();
var notifications = Service<NotificationManager>.Get();
var chatGui = Service<ChatGui>.GetNullable();
var pluginManager = Service<PluginManager>.GetNullable();
var notifications = Service<NotificationManager>.GetNullable();
if (chatGui == null || pluginManager == null || notifications == null)
return;
if (!pluginManager.ReposReady || pluginManager.InstalledPlugins.Count == 0 || pluginManager.AvailablePlugins.Count == 0)
{
@ -286,7 +293,7 @@ namespace Dalamud.Game
this.hasAutoUpdatedPlugins = true;
Task.Run(() => pluginManager.UpdatePluginsAsync(!configuration.AutoUpdatePlugins)).ContinueWith(task =>
Task.Run(() => pluginManager.UpdatePluginsAsync(!this.configuration.AutoUpdatePlugins)).ContinueWith(task =>
{
if (task.IsFaulted)
{
@ -297,15 +304,13 @@ namespace Dalamud.Game
var updatedPlugins = task.Result;
if (updatedPlugins != null && updatedPlugins.Any())
{
if (configuration.AutoUpdatePlugins)
if (this.configuration.AutoUpdatePlugins)
{
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);
}
else
{
var data = Service<DataManager>.Get();
chatGui.PrintChat(new XivChatEntry
{
Message = new SeString(new List<Payload>()

View file

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

View file

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

View file

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

View file

@ -46,9 +46,9 @@ namespace Dalamud.Game.ClientState.Fates
/// <returns>True or false.</returns>
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;
if (clientState.LocalContentId == 0)

View file

@ -32,7 +32,7 @@ namespace Dalamud.Game.ClientState.GamePad
{
var resolver = clientState.AddressResolver;
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);

View file

@ -97,9 +97,9 @@ namespace Dalamud.Game.ClientState.Objects
/// <returns><see cref="GameObject"/> object or inheritor containing the requested data.</returns>
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;
if (address == IntPtr.Zero)

View file

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

View file

@ -61,9 +61,9 @@ namespace Dalamud.Game.ClientState.Objects.Types
/// <returns>True or false.</returns>
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;
if (clientState.LocalContentId == 0)

View file

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

View file

@ -18,7 +18,7 @@ namespace Dalamud.Game.Command
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class CommandManager : IServiceType
public sealed class CommandManager : IServiceType, IDisposable
{
private readonly Dictionary<string, CommandInfo> commandMap = new();
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 currentLangCommandRegex;
[ServiceManager.ServiceDependency]
private readonly ChatGui chatGui = Service<ChatGui>.Get();
[ServiceManager.ServiceConstructor]
private CommandManager(DalamudStartInfo startInfo)
{
@ -40,7 +43,7 @@ namespace Dalamud.Game.Command
_ => this.currentLangCommandRegex,
};
Service<ChatGui>.Get().CheckMessageHandled += this.OnCheckMessageHandled;
this.chatGui.CheckMessageHandled += this.OnCheckMessageHandled;
}
/// <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.Toast;
using Dalamud.Game.Libc;
using Dalamud.Game.Network;
using Dalamud.Hooking;
using Dalamud.Interface.Internal;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Utility;
@ -32,21 +30,19 @@ namespace Dalamud.Game
private readonly List<RunOnNextTickTaskBase> runOnNextTickTaskList = new();
private readonly Stopwatch updateStopwatch = new();
private Hook<OnUpdateDetour> updateHook;
private Hook<OnDestroyDetour> freeHook;
private Hook<OnRealDestroyDelegate> destroyHook;
private readonly Hook<OnUpdateDetour> updateHook;
private readonly Hook<OnRealDestroyDelegate> destroyHook;
private Thread? frameworkUpdateThread;
[ServiceManager.ServiceConstructor]
private Framework(GameGui gameGui, GameNetwork gameNetwork, SigScanner sigScanner)
private Framework(SigScanner sigScanner)
{
this.Address = new FrameworkAddressResolver();
this.Address.Setup(sigScanner);
this.updateHook = new Hook<OnUpdateDetour>(this.Address.TickAddress, this.HandleFrameworkUpdate);
this.freeHook = new Hook<OnDestroyDetour>(this.Address.FreeAddress, this.HandleFrameworkFree);
this.destroyHook = new Hook<OnRealDestroyDelegate>(this.Address.DestroyAddress, this.HandleFrameworkDestroy);
this.updateHook = Hook<OnUpdateDetour>.FromAddress(this.Address.TickAddress, this.HandleFrameworkUpdate);
this.destroyHook = Hook<OnRealDestroyDelegate>.FromAddress(this.Address.DestroyAddress, this.HandleFrameworkDestroy);
}
/// <summary>
@ -113,6 +109,11 @@ namespace Dalamud.Game
/// </summary>
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>
/// Gets or sets a value indicating whether to dispatch update events.
/// </summary>
@ -124,7 +125,8 @@ namespace Dalamud.Game
/// <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<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>
/// 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>
public Task RunOnFrameworkThread(Action action)
{
if (this.IsInFrameworkUpdateThread)
if (this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading)
{
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>
/// Run given function in upcoming Framework.Tick call.
/// </summary>
@ -162,6 +182,16 @@ namespace Dalamud.Game
/// <returns>Task representing the pending function.</returns>
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>();
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<T>()
{
@ -184,6 +214,16 @@ namespace Dalamud.Game
/// <returns>Task representing the pending function.</returns>
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();
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskAction()
{
@ -196,22 +236,88 @@ namespace Dalamud.Game
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>
/// Dispose of managed and unmanaged resources.
/// </summary>
void IDisposable.Dispose()
{
Service<GameGui>.GetNullable()?.ExplicitDispose();
Service<GameNetwork>.GetNullable()?.ExplicitDispose();
this.RunOnFrameworkThread(() =>
{
// ReSharper disable once AccessToDisposedClosure
this.updateHook.Disable();
this.updateHook?.Disable();
this.freeHook?.Disable();
this.destroyHook?.Disable();
Thread.Sleep(500);
// ReSharper disable once AccessToDisposedClosure
this.destroyHook.Disable();
}).Wait();
this.updateHook?.Dispose();
this.freeHook?.Dispose();
this.destroyHook?.Dispose();
this.updateHook.Dispose();
this.destroyHook.Dispose();
this.updateStopwatch.Reset();
statsStopwatch.Reset();
@ -221,7 +327,6 @@ namespace Dalamud.Game
private void ContinueConstruction()
{
this.updateHook.Enable();
this.freeHook.Enable();
this.destroyHook.Enable();
}
@ -314,41 +419,21 @@ namespace Dalamud.Game
}
original:
return this.updateHook.Original(framework);
return this.updateHook.OriginalDisposeSafe(framework);
}
private bool HandleFrameworkDestroy(IntPtr framework)
{
if (this.DispatchUpdateEvents)
{
Log.Information("Framework::Destroy!");
var dalamud = Service<Dalamud>.Get();
dalamud.DisposePlugins();
Log.Information("Framework::Destroy OK!");
}
this.IsFrameworkUnloading = true;
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()
{
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;
return this.destroyHook.OriginalDisposeSafe(framework);
}
private abstract class RunOnNextTickTaskBase

View file

@ -33,6 +33,12 @@ namespace Dalamud.Game.Gui
private readonly Hook<PopulateItemLinkDelegate> populateItemLinkHook;
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;
[ServiceManager.ServiceConstructor]
@ -41,9 +47,9 @@ namespace Dalamud.Game.Gui
this.address = new ChatGuiAddressResolver();
this.address.Setup(sigScanner);
this.printMessageHook = new Hook<PrintMessageDelegate>(this.address.PrintMessage, this.HandlePrintMessageDetour);
this.populateItemLinkHook = new Hook<PopulateItemLinkDelegate>(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
this.interactableLinkClickedHook = new Hook<InteractableLinkClickedDelegate>(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
this.printMessageHook = Hook<PrintMessageDelegate>.FromAddress(this.address.PrintMessage, this.HandlePrintMessageDetour);
this.populateItemLinkHook = Hook<PopulateItemLinkDelegate>.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
this.interactableLinkClickedHook = Hook<InteractableLinkClickedDelegate>.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
}
/// <summary>
@ -150,13 +156,11 @@ namespace Dalamud.Game.Gui
/// <param name="message">A message to send.</param>
public void Print(string message)
{
var configuration = Service<DalamudConfiguration>.Get();
// Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message);
this.PrintChat(new XivChatEntry
{
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>
public void Print(SeString message)
{
var configuration = Service<DalamudConfiguration>.Get();
// Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue);
this.PrintChat(new XivChatEntry
{
Message = message,
Type = configuration.GeneralChatType,
Type = this.configuration.GeneralChatType,
});
}
@ -222,10 +224,10 @@ namespace Dalamud.Game.Gui
}
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();
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);
}
@ -364,7 +366,7 @@ namespace Dalamud.Game.Gui
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})");
messagePtr = allocatedString.Address;
}
@ -379,7 +381,7 @@ namespace Dalamud.Game.Gui
if (!Util.FastByteArrayCompare(originalSenderData, sender.RawData))
{
allocatedStringSender = Service<LibcFunction>.Get().NewString(sender.RawData);
allocatedStringSender = this.libcFunction.NewString(sender.RawData);
Log.Debug(
$"HandlePrintMessageDetour Sender modified: {originalSenderData}({senderPtr}) -> {sender}({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.contextMenuOpeningHook = new Hook<ContextMenuOpeningDelegate>(this.Address.ContextMenuOpeningPtr, this.ContextMenuOpeningDetour);
this.contextMenuOpenedHook = new Hook<ContextMenuOpenedDelegate>(this.Address.ContextMenuOpenedPtr, this.ContextMenuOpenedDetour);
this.contextMenuItemSelectedHook = new Hook<ContextMenuItemSelectedDelegate>(this.Address.ContextMenuItemSelectedPtr, this.ContextMenuItemSelectedDetour);
this.subContextMenuOpeningHook = new Hook<SubContextMenuOpeningDelegate>(this.Address.SubContextMenuOpeningPtr, this.SubContextMenuOpeningDetour);
this.subContextMenuOpenedHook = new Hook<ContextMenuOpenedDelegate>(this.Address.SubContextMenuOpenedPtr, this.SubContextMenuOpenedDetour);
this.contextMenuOpeningHook = Hook<ContextMenuOpeningDelegate>.FromAddress(this.Address.ContextMenuOpeningPtr, this.ContextMenuOpeningDetour);
this.contextMenuOpenedHook = Hook<ContextMenuOpenedDelegate>.FromAddress(this.Address.ContextMenuOpenedPtr, this.ContextMenuOpenedDetour);
this.contextMenuItemSelectedHook = Hook<ContextMenuItemSelectedDelegate>.FromAddress(this.Address.ContextMenuItemSelectedPtr, this.ContextMenuItemSelectedDetour);
this.subContextMenuOpeningHook = Hook<SubContextMenuOpeningDelegate>.FromAddress(this.Address.SubContextMenuOpeningPtr, this.SubContextMenuOpeningDetour);
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;
[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 uint runningNodeIds = BaseNodeId;
[ServiceManager.ServiceConstructor]
private DtrBar(DalamudConfiguration configuration, Framework framework)
private DtrBar()
{
framework.Update += this.Update;
this.framework.Update += this.Update;
configuration.DtrOrder ??= new List<string>();
configuration.DtrIgnore ??= new List<string>();
configuration.Save();
this.configuration.DtrOrder ??= new List<string>();
this.configuration.DtrIgnore ??= new List<string>();
this.configuration.Save();
}
/// <summary>
@ -48,14 +57,13 @@ namespace Dalamud.Game.Gui.Dtr
if (this.entries.Any(x => x.Title == title))
throw new ArgumentException("An entry with the same title already exists.");
var configuration = Service<DalamudConfiguration>.Get();
var node = this.MakeNode(++this.runningNodeIds);
var entry = new DtrBarEntry(title, node);
entry.Text = text;
// Add the entry to the end of the order list, if it's not there already.
if (!configuration.DtrOrder!.Contains(title))
configuration.DtrOrder!.Add(title);
if (!this.configuration.DtrOrder!.Contains(title))
this.configuration.DtrOrder!.Add(title);
this.entries.Add(entry);
this.ApplySort();
@ -69,7 +77,7 @@ namespace Dalamud.Game.Gui.Dtr
this.RemoveNode(entry.TextNode);
this.entries.Clear();
Service<Framework>.Get().Update -= this.Update;
this.framework.Update -= this.Update;
}
/// <summary>
@ -112,12 +120,11 @@ namespace Dalamud.Game.Gui.Dtr
/// </summary>
internal void ApplySort()
{
var configuration = Service<DalamudConfiguration>.Get();
// Sort the current entry list, based on the order in the configuration.
var positions = configuration.DtrOrder!
.Select(entry => (entry, index: configuration.DtrOrder!.IndexOf(entry)))
.ToDictionary(x => x.entry, x => x.index);
var positions = this.configuration
.DtrOrder!
.Select(entry => (entry, index: this.configuration.DtrOrder!.IndexOf(entry)))
.ToDictionary(x => x.entry, x => x.index);
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)
{
this.HandleRemovedNodes();
var dtr = GetDtr();
var dtr = this.GetDtr();
if (dtr == null) return;
// 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];
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,
// 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++)
{
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)
{
@ -185,9 +192,9 @@ namespace Dalamud.Game.Gui.Dtr
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);
runningXPos += elementWidth;
@ -209,7 +216,7 @@ namespace Dalamud.Game.Gui.Dtr
/// <returns>True if there are nodes with an ID > 1000.</returns>
private bool CheckForDalamudNodes()
{
var dtr = GetDtr();
var dtr = this.GetDtr();
if (dtr == null || dtr->RootNode == null) return false;
for (var i = 0; i < dtr->UldManager.NodeListCount; i++)
@ -233,7 +240,7 @@ namespace Dalamud.Game.Gui.Dtr
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;
var lastChild = dtr->RootNode->ChildNode;
@ -253,7 +260,7 @@ namespace Dalamud.Game.Gui.Dtr
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;
var tmpPrevNode = node->AtkResNode.PrevSiblingNode;

View file

@ -36,7 +36,7 @@ namespace Dalamud.Game.Gui.FlyText
this.Address.Setup(sigScanner);
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>

View file

@ -60,23 +60,23 @@ namespace Dalamud.Game.Gui
Log.Verbose($"HandleItemOut address 0x{this.address.HandleItemOut.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.handleItemOutHook = new Hook<HandleItemOutDelegate>(this.address.HandleItemOut, this.HandleItemOutDetour);
this.handleItemHoverHook = Hook<HandleItemHoverDelegate>.FromAddress(this.address.HandleItemHover, this.HandleItemHoverDetour);
this.handleItemOutHook = Hook<HandleItemOutDelegate>.FromAddress(this.address.HandleItemOut, this.HandleItemOutDetour);
this.handleActionHoverHook = new Hook<HandleActionHoverDelegate>(this.address.HandleActionHover, this.HandleActionHoverDetour);
this.handleActionOutHook = new Hook<HandleActionOutDelegate>(this.address.HandleActionOut, this.HandleActionOutDetour);
this.handleActionHoverHook = Hook<HandleActionHoverDelegate>.FromAddress(this.address.HandleActionHover, this.HandleActionHoverDetour);
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.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
@ -436,12 +436,6 @@ namespace Dalamud.Game.Gui
/// </summary>
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.handleItemHoverHook.Dispose();
this.handleItemOutHook.Dispose();

View file

@ -266,9 +266,9 @@ namespace Dalamud.Game.Gui.Internal
private void ToggleWindow(bool visible)
{
if (visible)
Service<DalamudInterface>.Get().OpenImeWindow();
Service<DalamudInterface>.GetNullable()?.OpenImeWindow();
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.receiveListingHook = new Hook<ReceiveListingDelegate>(this.address.ReceiveListing, new ReceiveListingDelegate(this.HandleReceiveListingDetour));
this.receiveListingHook = Hook<ReceiveListingDelegate>.FromAddress(this.address.ReceiveListing, new ReceiveListingDelegate(this.HandleReceiveListingDetour));
}
/// <summary>

View file

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

View file

@ -9,7 +9,6 @@ using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Hooking;
using Dalamud.Interface;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Windowing;
using FFXIVClientStructs.FFXIV.Component.GUI;
@ -35,15 +34,21 @@ namespace Dalamud.Game.Internal
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 locDalamudSettings;
[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 ?? ?? ?? ??");
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 ??");
this.atkValueChangeType = Marshal.GetDelegateForFunctionPointer<AtkValueChangeType>(atkValueChangeTypeAddress);
@ -52,15 +57,15 @@ namespace Dalamud.Game.Internal
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 ?? ?? ?? ??");
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 ");
this.hookAtkUnitBaseReceiveGlobalEvent = new Hook<AtkUnitBaseReceiveGlobalEvent>(atkUnitBaseReceiveGlobalEventAddress, this.AtkUnitBaseReceiveGlobalEventDetour);
this.hookAtkUnitBaseReceiveGlobalEvent = Hook<AtkUnitBaseReceiveGlobalEvent>.FromAddress(atkUnitBaseReceiveGlobalEventAddress, this.AtkUnitBaseReceiveGlobalEventDetour);
this.locDalamudPlugins = Loc.Localize("SystemMenuPlugins", "Dalamud Plugins");
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);
@ -83,11 +88,13 @@ namespace Dalamud.Game.Internal
private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args)
{
var systemText = Service<DataManager>.Get().GetExcelSheet<Addon>()!.GetRow(1059)!.Text.RawString; // "System"
var configuration = Service<DalamudConfiguration>.Get();
var interfaceManager = Service<InterfaceManager>.Get();
var systemText = Service<DataManager>.GetNullable()?.GetExcelSheet<Addon>()?.GetRow(1059)?.Text?.RawString; // "System"
var interfaceManager = Service<InterfaceManager>.GetNullable();
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();
@ -109,7 +116,7 @@ namespace Dalamud.Game.Internal
// "SendHotkey"
// 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}");
return IntPtr.Zero;
@ -120,14 +127,18 @@ namespace Dalamud.Game.Internal
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}");
return;
}
var configuration = Service<DalamudConfiguration>.Get();
var interfaceManager = Service<InterfaceManager>.Get();
var interfaceManager = Service<InterfaceManager>.GetNullable();
if (interfaceManager == null)
{
this.hookAgentHudOpenSystemMenu.Original(thisPtr, atkValueArgs, menuSize);
return;
}
if (!configuration.DoButtonsSystemMenu || !interfaceManager.IsDispatchingEvents)
{
@ -207,15 +218,15 @@ namespace Dalamud.Game.Internal
private void UiModuleRequestMainCommandDetour(void* thisPtr, int commandId)
{
var dalamudInterface = Service<DalamudInterface>.Get();
var dalamudInterface = Service<DalamudInterface>.GetNullable();
switch (commandId)
{
case 69420:
dalamudInterface.TogglePluginInstallerWindow();
dalamudInterface?.TogglePluginInstallerWindow();
break;
case 69421:
dalamudInterface.ToggleSettingsWindow();
dalamudInterface?.ToggleSettingsWindow();
break;
default:
this.hookUiModuleRequestMainCommand.Original(thisPtr, commandId);
@ -259,7 +270,7 @@ namespace Dalamud.Game.Internal
this.hookUiModuleRequestMainCommand.Dispose();
this.hookAtkUnitBaseReceiveGlobalEvent.Dispose();
Service<ContextMenu>.Get().ContextMenuOpened -= this.ContextMenuOnContextMenuOpened;
this.contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened;
}
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($"ProcessZonePacketUp address 0x{this.address.ProcessZonePacketUp.ToInt64():X}");
this.processZonePacketDownHook = new Hook<ProcessZonePacketDownDelegate>(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour);
this.processZonePacketUpHook = new Hook<ProcessZonePacketUpDelegate>(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour);
this.processZonePacketDownHook = Hook<ProcessZonePacketDownDelegate>.FromAddress(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour);
this.processZonePacketUpHook = Hook<ProcessZonePacketUpDelegate>.FromAddress(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour);
}
/// <summary>

View file

@ -32,7 +32,9 @@ namespace Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis
/// <inheritdoc/>
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.");
var uploader = clientState.LocalContentId;
@ -118,7 +120,9 @@ namespace Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis
/// <inheritdoc/>
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>
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 worldId = clientState.LocalPlayer?.CurrentWorld.Id ?? 0;

View file

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

View file

@ -18,7 +18,7 @@ namespace Dalamud.Game.Network.Internal
[ServiceManager.ServiceConstructor]
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();
}

View file

@ -359,6 +359,7 @@ namespace Dalamud.Game
/// </summary>
public void Dispose()
{
this.Save();
Marshal.FreeHGlobal(this.moduleCopyPtr);
}
@ -370,7 +371,14 @@ namespace Dalamud.Game
if (this.cacheFile == null)
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>

View file

@ -88,6 +88,22 @@ namespace Dalamud.Hooking
/// <exception cref="ObjectDisposedException">Hook is already disposed.</exception>
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>
/// Gets a value indicating whether or not the hook is enabled.
/// </summary>
@ -115,14 +131,17 @@ namespace Dalamud.Hooking
/// <summary>
/// Creates a hook by rewriting import table address.
/// </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="functionName">Decorated name of the function.</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>
/// <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 pNt = (PeHeader.IMAGE_FILE_HEADER*)(module.BaseAddress + (int)pDos->e_lfanew + 4);
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)
: base(address)
{
var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address);
if (!hasOtherHooks)
lock (HookManager.HookEnableSyncRoot)
{
MemoryHelper.ReadRaw(this.Address, 0x32, out var original);
HookManager.Originals[this.Address] = original;
var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address);
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/>
@ -91,11 +94,15 @@ namespace Dalamud.Hooking.Internal
if (!this.enabled)
{
if (!NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf<IntPtr>(), MemoryProtection.ExecuteReadWrite, out var oldProtect))
throw new Win32Exception(Marshal.GetLastWin32Error());
lock (HookManager.HookEnableSyncRoot)
{
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));
NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf<IntPtr>(), oldProtect, out _);
Marshal.WriteIntPtr(this.Address, Marshal.GetFunctionPointerForDelegate(this.detourDelegate));
NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf<IntPtr>(), oldProtect, out _);
}
}
}
@ -106,11 +113,15 @@ namespace Dalamud.Hooking.Internal
if (this.enabled)
{
if (!NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf<IntPtr>(), MemoryProtection.ExecuteReadWrite, out var oldProtect))
throw new Win32Exception(Marshal.GetLastWin32Error());
lock (HookManager.HookEnableSyncRoot)
{
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);
NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf<IntPtr>(), oldProtect, out _);
Marshal.WriteIntPtr(this.Address, this.pfnOriginal);
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>
/// Gets a static list of tracked and registered hooks.
/// </summary>

View file

@ -22,24 +22,27 @@ namespace Dalamud.Hooking.Internal
internal MinHookHook(IntPtr address, T detour, Assembly callingAssembly)
: base(address)
{
var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address);
if (!hasOtherHooks)
lock (HookManager.HookEnableSyncRoot)
{
MemoryHelper.ReadRaw(this.Address, 0x32, out var original);
HookManager.Originals[this.Address] = original;
var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address);
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/>
@ -71,10 +74,13 @@ namespace Dalamud.Hooking.Internal
if (this.IsDisposed)
return;
this.minHookImpl.Dispose();
lock (HookManager.HookEnableSyncRoot)
{
this.minHookImpl.Dispose();
var index = HookManager.MultiHookTracker[this.Address].IndexOf(this);
HookManager.MultiHookTracker[this.Address][index] = null;
var index = HookManager.MultiHookTracker[this.Address].IndexOf(this);
HookManager.MultiHookTracker[this.Address][index] = null;
}
base.Dispose();
}
@ -86,7 +92,10 @@ namespace Dalamud.Hooking.Internal
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)
{
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)
: base(address)
{
var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address);
if (!hasOtherHooks)
lock (HookManager.HookEnableSyncRoot)
{
MemoryHelper.ReadRaw(this.Address, 0x32, out var original);
HookManager.Originals[this.Address] = original;
var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address);
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/>
@ -70,11 +73,14 @@ namespace Dalamud.Hooking.Internal
{
this.CheckDisposed();
if (!this.hookImpl.IsHookActivated)
this.hookImpl.Activate();
lock (HookManager.HookEnableSyncRoot)
{
if (!this.hookImpl.IsHookActivated)
this.hookImpl.Activate();
if (!this.hookImpl.IsHookEnabled)
this.hookImpl.Enable();
if (!this.hookImpl.IsHookEnabled)
this.hookImpl.Enable();
}
}
/// <inheritdoc/>
@ -82,11 +88,14 @@ namespace Dalamud.Hooking.Internal
{
this.CheckDisposed();
if (!this.hookImpl.IsHookActivated)
return;
lock (HookManager.HookEnableSyncRoot)
{
if (!this.hookImpl.IsHookActivated)
return;
if (this.hookImpl.IsHookEnabled)
this.hookImpl.Disable();
if (this.hookImpl.IsHookEnabled)
this.hookImpl.Disable();
}
}
}
}

View file

@ -18,7 +18,7 @@ namespace Dalamud.Interface.GameFonts
/// Loads game font for use in ImGui.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class GameFontManager : IDisposable, IServiceType
internal class GameFontManager : IServiceType
{
private static readonly string?[] FontNames =
{
@ -158,11 +158,6 @@ namespace Dalamud.Interface.GameFonts
fontPtr.BuildLookupTable();
}
/// <inheritdoc/>
public void Dispose()
{
}
/// <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.
/// </summary>

View file

@ -60,8 +60,6 @@ namespace Dalamud.Interface.Internal
private readonly TextureWrap logoTexture;
private readonly TextureWrap tsmLogoTexture;
private ulong frameCount = 0;
#if DEBUG
private bool isImGuiDrawDevMenu = true;
#else
@ -80,7 +78,8 @@ namespace Dalamud.Interface.Internal
private DalamudInterface(
Dalamud dalamud,
DalamudConfiguration configuration,
InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene)
InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene,
PluginImageCache pluginImageCache)
{
var interfaceManager = interfaceManagerWithScene.Manager;
this.WindowSystem = new WindowSystem("DalamudCore");
@ -94,7 +93,7 @@ namespace Dalamud.Interface.Internal
this.imeWindow = new IMEWindow() { IsOpen = false };
this.consoleWindow = new ConsoleWindow() { IsOpen = configuration.LogOpenAtStartup };
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.selfTestWindow = new SelfTestWindow() { IsOpen = false };
this.styleEditorWindow = new StyleEditorWindow() { IsOpen = false };
@ -138,15 +137,20 @@ namespace Dalamud.Interface.Internal
this.tsmLogoTexture = tsmLogoTex;
var tsm = Service<TitleScreenMenu>.Get();
tsm.AddEntry(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("TSMDalamudPlugins", "Plugin Installer"), this.tsmLogoTexture, () => this.pluginWindow.IsOpen = true);
tsm.AddEntryCore(Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), this.tsmLogoTexture, () => this.settingsWindow.IsOpen = true);
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>
/// Gets the <see cref="WindowSystem"/> controlling all Dalamud-internal windows.
/// </summary>
@ -373,7 +377,7 @@ namespace Dalamud.Interface.Internal
private void OnDraw()
{
this.frameCount++;
this.FrameCount++;
#if BOOT_AGING
if (this.frameCount > 500 && !this.signaledBoot)
@ -494,9 +498,9 @@ namespace Dalamud.Interface.Internal
{
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.Save();
}
@ -505,6 +509,19 @@ namespace Dalamud.Interface.Internal
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();
if (ImGui.MenuItem("Enable AntiDebug", null, antiDebug.IsEnabled))
{
@ -584,7 +601,7 @@ namespace Dalamud.Interface.Internal
Marshal.ReadByte(IntPtr.Zero);
}
if (ImGui.MenuItem("Crash game"))
if (ImGui.MenuItem("Crash game (nullptr)"))
{
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();
var isBeta = configuration.DalamudBetaKey == DalamudConfiguration.DalamudCurrentBetaKey;
@ -795,7 +821,7 @@ namespace Dalamud.Interface.Internal
ImGui.PushFont(InterfaceManager.MonoFont);
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($"{Util.FormatBytes(GC.GetTotalMemory(false))}", false);

View file

@ -57,6 +57,9 @@ namespace Dalamud.Interface.Internal
private readonly HashSet<SpecialGlyphRequest> glyphRequests = new();
private readonly Dictionary<ImFontPtr, TargetFontModification> loadedFontInfo = new();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private readonly ManualResetEvent fontBuildSignal;
private readonly SwapChainVtableResolver address;
private readonly Hook<DispatchMessageWDelegate> dispatchMessageWHook;
@ -73,12 +76,12 @@ namespace Dalamud.Interface.Internal
private bool isOverrideGameCursor = true;
[ServiceManager.ServiceConstructor]
private InterfaceManager(SigScanner sigScanner)
private InterfaceManager()
{
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(
Process.GetCurrentProcess().MainModule, "user32.dll", "SetCursor", 0, this.SetCursorDetour);
null, "user32.dll", "SetCursor", 0, this.SetCursorDetour);
this.fontBuildSignal = new ManualResetEvent(false);
@ -91,12 +94,6 @@ namespace Dalamud.Interface.Internal
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
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)]
private delegate IntPtr SetCursorDelegate(IntPtr hCursor);
@ -248,20 +245,15 @@ namespace Dalamud.Interface.Internal
/// </summary>
public void Dispose()
{
// HACK: this is usually called on a separate thread from PresentDetour (likely on a dedicated render thread)
// 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 is a terrible way to prevent issues, but should basically always work to ensure that all outstanding
// calls to PresentDetour have finished (and Disable means no new ones will start), before we try to cleanup
// So... not great, but much better than constantly crashing on unload
this.Disable();
Thread.Sleep(500);
this.framework.RunOnFrameworkThread(() =>
{
this.setCursorHook.Dispose();
this.presentHook?.Dispose();
this.resizeBuffersHook?.Dispose();
this.dispatchMessageWHook.Dispose();
}).Wait();
this.scene?.Dispose();
this.setCursorHook.Dispose();
this.presentHook?.Dispose();
this.resizeBuffersHook?.Dispose();
this.dispatchMessageWHook.Dispose();
}
#nullable enable
@ -999,8 +991,8 @@ namespace Dalamud.Interface.Internal
break;
}
this.presentHook = new Hook<PresentDelegate>(this.address.Present, this.PresentDetour);
this.resizeBuffersHook = new Hook<ResizeBuffersDelegate>(this.address.ResizeBuffers, this.ResizeBuffersDetour);
this.presentHook = Hook<PresentDelegate>.FromAddress(this.address.Present, this.PresentDetour);
this.resizeBuffersHook = Hook<ResizeBuffersDelegate>.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour);
Log.Verbose("===== S W A P C H A I N =====");
Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}");
@ -1013,14 +1005,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
private void RebuildFontsInternal()
{
@ -1113,7 +1097,7 @@ namespace Dalamud.Interface.Internal
if (this.lastWantCapture == true && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor)
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()

View file

@ -123,7 +123,6 @@ namespace Dalamud.Interface.Internal.Windows
// Options menu
if (ImGui.BeginPopup("Options"))
{
var dalamud = Service<Dalamud>.Get();
var configuration = Service<DalamudConfiguration>.Get();
if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll))
@ -138,10 +137,10 @@ namespace Dalamud.Interface.Internal.Windows
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))
{
dalamud.LogLevelSwitch.MinimumLevel = (LogEventLevel)prevLevel;
EntryPoint.LogLevelSwitch.MinimumLevel = (LogEventLevel)prevLevel;
configuration.LogLevel = (LogEventLevel)prevLevel;
configuration.Save();
}

View file

@ -7,7 +7,6 @@ using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Game;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Types;
@ -20,7 +19,8 @@ namespace Dalamud.Interface.Internal.Windows
/// <summary>
/// A cache for plugin icons and images.
/// </summary>
internal class PluginImageCache : IDisposable
[ServiceManager.EarlyLoadedService]
internal class PluginImageCache : IDisposable, IServiceType
{
/// <summary>
/// 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 BlockingCollection<Func<Task>> downloadQueue = new();
private BlockingCollection<Action> loadQueue = new();
private CancellationTokenSource downloadToken = new();
private Thread downloadThread;
private readonly BlockingCollection<Tuple<ulong, Func<Task>>> downloadQueue = new();
private readonly BlockingCollection<Func<Task>> loadQueue = new();
private readonly CancellationTokenSource cancelToken = new();
private readonly Task downloadTask;
private readonly Task loadTask;
private Dictionary<string, TextureWrap?> pluginIconMap = new();
private Dictionary<string, TextureWrap?[]> pluginImagesMap = new();
private readonly Dictionary<string, TextureWrap?> pluginIconMap = 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>
/// Initializes a new instance of the <see cref="PluginImageCache"/> class.
/// Gets the fallback empty texture.
/// </summary>
public PluginImageCache()
{
var dalamud = Service<Dalamud>.Get();
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;
}
public TextureWrap EmptyTexture => this.emptyTextureTask.IsCompleted
? this.emptyTextureTask.Result
: this.emptyTextureTask.GetAwaiter().GetResult();
/// <summary>
/// Gets the default plugin icon.
/// </summary>
public TextureWrap DefaultIcon { get; }
public TextureWrap DefaultIcon => this.defaultIconTask.IsCompleted
? this.defaultIconTask.Result
: this.defaultIconTask.GetAwaiter().GetResult();
/// <summary>
/// Gets the plugin trouble icon overlay.
/// </summary>
public TextureWrap TroubleIcon { get; }
public TextureWrap TroubleIcon => this.troubleIconTask.IsCompleted
? this.troubleIconTask.Result
: this.troubleIconTask.GetAwaiter().GetResult();
/// <summary>
/// Gets the plugin update icon overlay.
/// </summary>
public TextureWrap UpdateIcon { get; }
public TextureWrap UpdateIcon => this.updateIconTask.IsCompleted
? this.updateIconTask.Result
: this.updateIconTask.GetAwaiter().GetResult();
/// <summary>
/// Gets the plugin installed icon overlay.
/// </summary>
public TextureWrap InstalledIcon { get; }
public TextureWrap InstalledIcon => this.installedIconTask.IsCompleted
? this.installedIconTask.Result
: this.installedIconTask.GetAwaiter().GetResult();
/// <summary>
/// Gets the third party plugin icon overlay.
/// </summary>
public TextureWrap ThirdIcon { get; }
public TextureWrap ThirdIcon => this.thirdIconTask.IsCompleted
? this.thirdIconTask.Result
: this.thirdIconTask.GetAwaiter().GetResult();
/// <summary>
/// Gets the installed third party plugin icon overlay.
/// </summary>
public TextureWrap ThirdInstalledIcon { get; }
public TextureWrap ThirdInstalledIcon => this.thirdInstalledIconTask.IsCompleted
? this.thirdInstalledIconTask.Result
: this.thirdInstalledIconTask.GetAwaiter().GetResult();
/// <summary>
/// Gets the core plugin icon.
/// </summary>
public TextureWrap CorePluginIcon { get; }
public TextureWrap CorePluginIcon => this.corePluginIconTask.IsCompleted
? this.corePluginIconTask.Result
: this.corePluginIconTask.GetAwaiter().GetResult();
/// <inheritdoc/>
public void Dispose()
{
var framework = Service<Framework>.Get();
framework.Update -= this.FrameworkOnUpdate;
this.cancelToken.Cancel();
this.downloadQueue.CompleteAdding();
this.loadQueue.CompleteAdding();
this.DefaultIcon?.Dispose();
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))
if (!Task.WaitAll(new[] { this.loadTask, this.downloadTask }, 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.downloadQueue?.CompleteAdding();
this.downloadQueue?.Dispose();
this.cancelToken.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)
{
@ -181,12 +212,22 @@ namespace Dalamud.Interface.Internal.Windows
if (this.pluginIconMap.TryGetValue(manifest.InternalName, out iconTexture))
return true;
iconTexture = null;
this.pluginIconMap.Add(manifest.InternalName, iconTexture);
this.pluginIconMap.Add(manifest.InternalName, null);
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;
@ -209,39 +250,120 @@ namespace Dalamud.Interface.Internal.Windows
imageTextures = Array.Empty<TextureWrap>();
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;
}
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
{
if (!this.loadQueue.TryTake(out var loadAction, 0, this.downloadToken.Token))
return;
loadAction.Invoke();
icon = interfaceManager.LoadImage(bytes);
}
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
{
if (!this.downloadQueue.TryTake(out var task, -1, this.downloadToken.Token))
return;
token.ThrowIfCancellationRequested();
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)
{
@ -252,52 +374,56 @@ namespace Dalamud.Interface.Internal.Windows
{
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");
}
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)
{
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)
{
var file = this.GetPluginIconFileInfo(plugin);
@ -306,7 +432,8 @@ namespace Dalamud.Interface.Internal.Windows
Log.Verbose($"Fetching icon for {manifest.InternalName} from {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;
this.pluginIconMap[manifest.InternalName] = icon;
@ -349,9 +476,10 @@ namespace Dalamud.Interface.Internal.Windows
data.EnsureSuccessStatusCode();
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;
this.pluginIconMap[manifest.InternalName] = icon;
@ -366,39 +494,6 @@ namespace Dalamud.Interface.Internal.Windows
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 })
{
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}");
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;
Log.Verbose($"Plugin image{i + 1} for {manifest.InternalName} loaded from disk");
@ -490,17 +586,16 @@ namespace Dalamud.Interface.Internal.Windows
if (didAny)
{
this.loadQueue.Add(() =>
this.loadQueue.Add(async () =>
{
var pluginImages = new TextureWrap[urls.Count];
for (var i = 0; i < imageBytes.Length; 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;
pluginImages[i] = image;
@ -551,7 +646,9 @@ namespace Dalamud.Interface.Internal.Windows
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"));
if (devUrl.Exists)
@ -562,8 +659,12 @@ namespace Dalamud.Interface.Internal.Windows
private List<FileInfo?> GetPluginImageFileInfos(LocalPlugin? plugin)
{
var pluginDir = plugin.DllFile.Directory;
var output = new List<FileInfo>();
var pluginDir = plugin?.DllFile.Directory;
if (pluginDir == null)
return output;
for (var i = 1; i <= 5; i++)
{
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 changelogTextColor = new(0.812f, 1.000f, 0.816f, 1.000f);
private readonly PluginImageCache imageCache;
private readonly PluginCategoryManager categoryManager = new();
private readonly PluginImageCache imageCache = new();
private readonly DalamudChangelogManager dalamudChangelogManager = new();
private readonly List<int> openPluginCollapsibles = new();
@ -87,12 +87,14 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
/// <summary>
/// Initializes a new instance of the <see cref="PluginInstallerWindow"/> class.
/// </summary>
public PluginInstallerWindow()
/// <param name="imageCache">An instance of <see cref="PluginImageCache"/> class.</param>
public PluginInstallerWindow(PluginImageCache imageCache)
: base(
Locs.WindowTitle + (Service<DalamudConfiguration>.Get().DoPluginTest ? Locs.WindowTitleMod_Testing : string.Empty) + "###XlPluginInstaller",
ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar)
{
this.IsOpen = true;
this.imageCache = imageCache;
this.Size = new Vector2(830, 570);
this.SizeCondition = ImGuiCond.FirstUseEver;
@ -200,6 +202,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5 * ImGuiHelpers.GlobalScale));
var searchInputWidth = 240 * ImGuiHelpers.GlobalScale;
var searchClearButtonWidth = 40 * ImGuiHelpers.GlobalScale;
var sortByText = Locs.SortBy_Label;
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
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);
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.SetCursorPosX(windowSize.X - sortSelectWidth);
ImGui.SetNextItemWidth(selectableWidth);
@ -552,7 +570,8 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
var i = 0;
foreach (var manifest in categoryManifestsList)
{
var remoteManifest = manifest as RemotePluginManifest;
if (manifest is not RemotePluginManifest remoteManifest)
continue;
var (isInstalled, plugin) = this.IsManifestInstalled(remoteManifest);
ImGui.PushID($"{manifest.InternalName}{manifest.AssemblyVersion}");
@ -1087,51 +1106,38 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
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 cursorBeforeImage = ImGui.GetCursorPos();
ImGui.Image(iconTex.ImGuiHandle, iconSize);
ImGui.SameLine();
var rectOffset = ImGui.GetWindowContentRegionMin() + ImGui.GetWindowPos();
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 };
if (updateAvailable)
{
ImGui.SetCursorPos(cursorBeforeImage);
ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize);
ImGui.SameLine();
}
else if (trouble)
{
ImGui.SetCursorPos(cursorBeforeImage);
ImGui.Image(this.imageCache.TroubleIcon.ImGuiHandle, iconSize);
ImGui.SameLine();
}
else if (isLoaded && isThirdParty)
{
ImGui.SetCursorPos(cursorBeforeImage);
ImGui.Image(this.imageCache.ThirdInstalledIcon.ImGuiHandle, iconSize);
ImGui.SameLine();
}
else if (isThirdParty)
{
ImGui.SetCursorPos(cursorBeforeImage);
ImGui.Image(this.imageCache.ThirdIcon.ImGuiHandle, iconSize);
ImGui.SameLine();
}
else if (isLoaded)
{
ImGui.SetCursorPos(cursorBeforeImage);
ImGui.Image(this.imageCache.InstalledIcon.ImGuiHandle, iconSize);
ImGui.SameLine();
}
else
ImGui.Dummy(iconSize);
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(5);
ImGui.SameLine();
@ -1208,23 +1214,32 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
var startCursor = ImGui.GetCursorPos();
var iconSize = ImGuiHelpers.ScaledVector2(64, 64);
TextureWrap icon;
if (log is PluginChangelogEntry pluginLog)
var cursorBeforeImage = ImGui.GetCursorPos();
var rectOffset = ImGui.GetWindowContentRegionMin() + ImGui.GetWindowPos();
if (ImGui.IsRectVisible(rectOffset + cursorBeforeImage, rectOffset + cursorBeforeImage + iconSize))
{
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)
TextureWrap icon;
if (log is PluginChangelogEntry pluginLog)
{
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
{
icon = this.imageCache.CorePluginIcon;
ImGui.Dummy(iconSize);
}
ImGui.Image(icon.ImGuiHandle, iconSize);
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(5);
@ -1644,7 +1659,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
this.installStatus = OperationStatus.InProgress;
Task.Run(() => pluginManager.DeleteConfiguration(plugin))
Task.Run(() => pluginManager.DeleteConfigurationAsync(plugin))
.ContinueWith(task =>
{
this.installStatus = OperationStatus.Idle;
@ -1670,7 +1685,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
// Disable everything if the plugin is outdated
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);
}
@ -1686,7 +1701,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
{
Task.Run(() =>
{
var unloadTask = Task.Run(() => plugin.Unload())
var unloadTask = Task.Run(() => plugin.UnloadAsync())
.ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_UnloadFail(plugin.Name));
unloadTask.Wait();

View file

@ -57,6 +57,8 @@ namespace Dalamud.Interface.Internal.Windows
private int dtrSpacing;
private bool dtrSwapDirection;
private int? pluginWaitBeforeFree;
private List<ThirdPartyRepoSettings> thirdRepoList;
private bool thirdRepoListChanged;
private string thirdRepoTempUrl = string.Empty;
@ -113,6 +115,8 @@ namespace Dalamud.Interface.Internal.Windows
this.dtrSpacing = configuration.DtrSpacing;
this.dtrSwapDirection = configuration.DtrSwapDirection;
this.pluginWaitBeforeFree = configuration.PluginWaitBeforeFree;
this.doPluginTest = configuration.DoPluginTest;
this.thirdRepoList = configuration.ThirdRepoList.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 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
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.DtrSwapDirection = this.dtrSwapDirection;
configuration.PluginWaitBeforeFree = this.pluginWaitBeforeFree;
configuration.DoPluginTest = this.doPluginTest;
configuration.ThirdRepoList = this.thirdRepoList.Select(x => x.Clone()).ToList();
configuration.DevPluginLoadLocations = this.devPluginLocations.Select(x => x.Clone()).ToList();

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using ImGuiScene;
@ -47,37 +48,140 @@ namespace Dalamud.Interface
throw new ArgumentException("Texture must be 64x64");
}
var entry = new TitleScreenMenuEntry(text, texture, onTriggered);
this.entries.Add(entry);
return entry;
lock (this.entries)
{
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>
/// Remove an entry from the title screen menu.
/// </summary>
/// <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>
/// Class representing an entry in the title screen menu.
/// </summary>
public class TitleScreenMenuEntry
public class TitleScreenMenuEntry : IComparable<TitleScreenMenuEntry>
{
private readonly Action onTriggered;
/// <summary>
/// Initializes a new instance of the <see cref="TitleScreenMenuEntry"/> class.
/// </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="texture">The texture to show.</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.Texture = texture;
this.onTriggered = onTriggered;
}
/// <summary>
/// Gets the priority of this entry.
/// </summary>
public ulong Priority { get; init; }
/// <summary>
/// Gets or sets the name of this entry.
/// </summary>
@ -88,6 +192,11 @@ namespace Dalamud.Interface
/// </summary>
public TextureWrap Texture { get; set; }
/// <summary>
/// Gets the calling assembly of this entry.
/// </summary>
internal Assembly? CallingAssembly { get; init; }
/// <summary>
/// Gets the internal ID of this entry.
/// </summary>
@ -100,6 +209,32 @@ namespace Dalamud.Interface
{
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.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Gui;
using Dalamud.Interface.GameFonts;
@ -24,6 +25,8 @@ namespace Dalamud.Interface
{
private readonly Stopwatch stopwatch;
private readonly string namespaceName;
private readonly InterfaceManager interfaceManager = Service<InterfaceManager>.Get();
private readonly GameFontManager gameFontManager = Service<GameFontManager>.Get();
private bool hasErrorWindow = false;
private bool lastFrameUiHideState = false;
@ -38,11 +41,10 @@ namespace Dalamud.Interface
this.stopwatch = new Stopwatch();
this.namespaceName = namespaceName;
var interfaceManager = Service<InterfaceManager>.Get();
interfaceManager.Draw += this.OnDraw;
interfaceManager.BuildFonts += this.OnBuildFonts;
interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts;
interfaceManager.ResizeBuffers += this.OnResizeBuffers;
this.interfaceManager.Draw += this.OnDraw;
this.interfaceManager.BuildFonts += this.OnBuildFonts;
this.interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts;
this.interfaceManager.ResizeBuffers += this.OnResizeBuffers;
}
/// <summary>
@ -109,12 +111,12 @@ namespace Dalamud.Interface
/// <summary>
/// Gets the game's active Direct3D device.
/// </summary>
public Device Device => Service<InterfaceManager.InterfaceManagerWithScene>.Get().Manager.Device!;
public Device Device => this.InterfaceManagerWithScene.Device!;
/// <summary>
/// Gets the game's main window handle.
/// </summary>
public IntPtr WindowHandlePtr => Service<InterfaceManager.InterfaceManagerWithScene>.Get().Manager.WindowHandlePtr;
public IntPtr WindowHandlePtr => this.InterfaceManagerWithScene.WindowHandlePtr;
/// <summary>
/// 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>
public bool OverrideGameCursor
{
get => Service<InterfaceManager>.Get().OverrideGameCursor;
set => Service<InterfaceManager>.Get().OverrideGameCursor = value;
get => this.interfaceManager.OverrideGameCursor;
set => this.interfaceManager.OverrideGameCursor = value;
}
/// <summary>
@ -157,7 +159,9 @@ namespace Dalamud.Interface
{
get
{
var condition = Service<Condition>.Get();
var condition = Service<Condition>.GetNullable();
if (condition == null)
return false;
return condition[ConditionFlag.OccupiedInCutSceneEvent]
|| condition[ConditionFlag.WatchingCutscene78];
}
@ -170,7 +174,9 @@ namespace Dalamud.Interface
{
get
{
var condition = Service<Condition>.Get();
var condition = Service<Condition>.GetNullable();
if (condition == null)
return false;
return condition[ConditionFlag.WatchingCutscene];
}
}
@ -178,7 +184,12 @@ namespace Dalamud.Interface
/// <summary>
/// Gets a value indicating whether this plugin should modify the game's interface at this time.
/// </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>
/// Gets or sets a value indicating whether statistics about UI draw time should be collected.
@ -209,13 +220,20 @@ namespace Dalamud.Interface
/// </summary>
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>
/// Loads an image from the specified file.
/// </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 TextureWrap LoadImage(string filePath)
=> Service<InterfaceManager.InterfaceManagerWithScene>.Get().Manager.LoadImage(filePath);
=> this.InterfaceManagerWithScene?.LoadImage(filePath)
?? throw new InvalidOperationException("Load failed.");
/// <summary>
/// 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>
/// <returns>A <see cref="TextureWrap"/> object wrapping the created image. Use <see cref="TextureWrap.ImGuiHandle"/> inside ImGui.Image().</returns>
public TextureWrap LoadImage(byte[] imageData)
=> Service<InterfaceManager.InterfaceManagerWithScene>.Get().Manager.LoadImage(imageData);
=> this.InterfaceManagerWithScene?.LoadImage(imageData)
?? throw new InvalidOperationException("Load failed.");
/// <summary>
/// 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>
/// <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)
=> 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>
/// Gets a game font.
/// </summary>
/// <param name="style">Font to get.</param>
/// <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>
/// Call this to queue a rebuild of the font atlas.<br/>
@ -251,7 +355,7 @@ namespace Dalamud.Interface
public void RebuildFonts()
{
Log.Verbose("[FONT] {0} plugin is initiating FONT REBUILD", this.namespaceName);
Service<InterfaceManager>.Get().RebuildFonts();
this.interfaceManager.RebuildFonts();
}
/// <summary>
@ -262,19 +366,25 @@ namespace Dalamud.Interface
/// <param name="type">The type of the notification.</param>
/// <param name="msDelay">The time the notification should be displayed for.</param>
public void AddNotification(
string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = 3000) =>
Service<NotificationManager>.Get().AddNotification(content, title, type, msDelay);
string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = 3000)
{
Service<NotificationManager>
.GetAsync()
.ContinueWith(task =>
{
if (task.IsCompletedSuccessfully)
task.Result.AddNotification(content, title, type, msDelay);
});
}
/// <summary>
/// Unregister the UiBuilder. Do not call this in plugin code.
/// </summary>
void IDisposable.Dispose()
{
var interfaceManager = Service<InterfaceManager>.Get();
interfaceManager.Draw -= this.OnDraw;
interfaceManager.BuildFonts -= this.OnBuildFonts;
interfaceManager.ResizeBuffers -= this.OnResizeBuffers;
this.interfaceManager.Draw -= this.OnDraw;
this.interfaceManager.BuildFonts -= this.OnBuildFonts;
this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers;
}
/// <summary>
@ -304,8 +414,9 @@ namespace Dalamud.Interface
private void OnDraw()
{
var configuration = Service<DalamudConfiguration>.Get();
var gameGui = Service<GameGui>.Get();
var interfaceManager = Service<InterfaceManager>.Get();
var gameGui = Service<GameGui>.GetNullable();
if (gameGui == null)
return;
if ((gameGui.GameUiHidden && configuration.ToggleUiHide &&
!(this.DisableUserUiHide || this.DisableAutomaticUiHide)) ||
@ -329,7 +440,7 @@ namespace Dalamud.Interface
this.ShowUi?.Invoke();
}
if (!interfaceManager.FontsReady)
if (!this.interfaceManager.FontsReady)
return;
ImGui.PushID(this.namespaceName);

View file

@ -20,6 +20,9 @@ namespace Dalamud.Logging.Internal
private static readonly ConcurrentQueue<TaskInfo> NewlyCreatedTasks = new();
private static bool clearRequested = false;
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private MonoMod.RuntimeDetour.Hook? scheduleAndStartHook;
private bool enabled = false;
@ -111,8 +114,7 @@ namespace Dalamud.Logging.Internal
this.ApplyPatch();
var framework = Service<Framework>.Get();
framework.Update += this.FrameworkOnUpdate;
this.framework.Update += this.FrameworkOnUpdate;
this.enabled = true;
}
@ -121,8 +123,7 @@ namespace Dalamud.Logging.Internal
{
this.scheduleAndStartHook?.Dispose();
var framework = Service<Framework>.Get();
framework.Update -= this.FrameworkOnUpdate;
this.framework.Update -= this.FrameworkOnUpdate;
}
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.Payloads;
using Dalamud.Interface;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Ipc;

View file

@ -38,6 +38,11 @@ internal partial class PluginManager : IDisposable, IServiceType
/// </summary>
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 readonly object pluginListLock = new();
@ -207,16 +212,43 @@ internal partial class PluginManager : IDisposable, IServiceType
/// <inheritdoc/>
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();
}
catch (Exception ex)
{
Log.Error(ex, $"Error disposing {plugin.Name}");
try
{
plugin.UnloadAsync(true, false).Wait();
}
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();
@ -891,7 +923,7 @@ internal partial class PluginManager : IDisposable, IServiceType
{
try
{
plugin.Unload();
await plugin.UnloadAsync();
}
catch (Exception ex)
{
@ -963,23 +995,30 @@ internal partial class PluginManager : IDisposable, IServiceType
/// </summary>
/// <param name="plugin">The plugin.</param>
/// <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");
if (plugin.IsLoaded)
plugin.Unload();
await plugin.UnloadAsync();
// Let's wait so any handles on files in plugin configurations can be closed
Thread.Sleep(500);
this.PluginConfigs.Delete(plugin.Name);
Thread.Sleep(500);
for (var waitUntil = Environment.TickCount64 + 1000; Environment.TickCount64 < waitUntil;)
{
try
{
this.PluginConfigs.Delete(plugin.Name);
break;
}
catch (IOException)
{
await Task.Delay(100);
}
}
// 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>

View file

@ -2,10 +2,13 @@ using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Game.Gui.Dtr;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Exceptions;
@ -27,6 +30,8 @@ internal class LocalPlugin : IDisposable
private readonly FileInfo disabledFile;
private readonly FileInfo testingFile;
private readonly SemaphoreSlim pluginLoadStateLock = new(1);
private PluginLoader? loader;
private Assembly? pluginAssembly;
private Type? pluginType;
@ -208,8 +213,20 @@ internal class LocalPlugin : IDisposable
/// <inheritdoc/>
public void Dispose()
{
this.instance?.Dispose();
this.instance = null;
var framework = Service<Framework>.GetNullable();
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 = null;
@ -217,6 +234,8 @@ internal class LocalPlugin : IDisposable
this.pluginType = null;
this.pluginAssembly = null;
if (this.loader != null && didPluginDispose)
Thread.Sleep(configuration.PluginWaitBeforeFree ?? PluginManager.PluginWaitBeforeFreeDefault);
this.loader?.Dispose();
}
@ -228,54 +247,73 @@ internal class LocalPlugin : IDisposable
/// <returns>A task.</returns>
public async Task LoadAsync(PluginLoadReason reason, bool reloading = false)
{
var startInfo = Service<DalamudStartInfo>.Get();
var configuration = Service<DalamudConfiguration>.Get();
var pluginManager = Service<PluginManager>.Get();
var configuration = await Service<DalamudConfiguration>.GetAsync();
var framework = await Service<Framework>.GetAsync();
var ioc = await Service<ServiceContainer>.GetAsync();
var pluginManager = await Service<PluginManager>.GetAsync();
var startInfo = await Service<DalamudStartInfo>.GetAsync();
// Allowed: Unloaded
switch (this.State)
{
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());
}
// UiBuilder constructor requires the following two.
await Service<InterfaceManager>.GetAsync();
await Service<GameFontManager>.GetAsync();
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.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.");
}
if (this.Manifest.LoadRequiredState == 0)
_ = await Service<InterfaceManager.InterfaceManagerWithScene>.GetAsync();
await this.pluginLoadStateLock.WaitAsync();
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);
if (reloading || this.IsDev)
@ -309,7 +347,8 @@ internal class LocalPlugin : IDisposable
this.AssemblyName = this.pluginAssembly.GetName();
// 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
var assemblyName = this.pluginAssembly.GetName().Name;
@ -319,7 +358,8 @@ internal class LocalPlugin : IDisposable
if (otherPlugin == this || otherPlugin.instance == null)
continue;
var otherPluginAssemblyName = otherPlugin.instance.GetType().Assembly.GetName().Name;
var otherPluginAssemblyName =
otherPlugin.instance.GetType().Assembly.GetName().Name;
if (otherPluginAssemblyName == assemblyName && otherPluginAssemblyName != null)
{
this.State = PluginState.Unloaded;
@ -330,17 +370,29 @@ internal class LocalPlugin : IDisposable
}
// 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)
{
this.State = PluginState.LoadError;
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;
}
@ -363,6 +415,10 @@ internal class LocalPlugin : IDisposable
throw;
}
finally
{
this.pluginLoadStateLock.Release();
}
}
/// <summary>
@ -370,31 +426,40 @@ internal class LocalPlugin : IDisposable
/// in the plugin list until it has been actually disposed.
/// </summary>
/// <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)
switch (this.State)
{
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());
}
var configuration = Service<DalamudConfiguration>.Get();
var framework = Service<Framework>.GetNullable();
await this.pluginLoadStateLock.WaitAsync();
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}");
this.instance?.Dispose();
if (this.Manifest.CanUnloadAsync || framework == null)
this.instance?.Dispose();
else
await framework.RunOnFrameworkThread(() => this.instance?.Dispose());
this.instance = null;
this.DalamudInterface?.ExplicitDispose();
@ -405,6 +470,8 @@ internal class LocalPlugin : IDisposable
if (!reloading)
{
if (waitBeforeLoaderDispose && this.loader != null)
await Task.Delay(configuration.PluginWaitBeforeFree ?? PluginManager.PluginWaitBeforeFreeDefault);
this.loader?.Dispose();
this.loader = null;
}
@ -419,6 +486,13 @@ internal class LocalPlugin : IDisposable
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>
@ -427,12 +501,7 @@ internal class LocalPlugin : IDisposable
/// <returns>A task.</returns>
public async Task ReloadAsync()
{
this.Unload(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.UnloadAsync(true);
await this.LoadAsync(PluginLoadReason.Reload, true);
}
@ -444,7 +513,8 @@ internal class LocalPlugin : IDisposable
// Allowed: Unloaded, UnloadError
switch (this.State)
{
case PluginState.InProgress:
case PluginState.Loading:
case PluginState.Unloading:
case PluginState.Loaded:
case PluginState.LoadError:
throw new InvalidPluginOperationException($"Unable to enable {this.Name}, still loaded");
@ -471,7 +541,8 @@ internal class LocalPlugin : IDisposable
// Allowed: Unloaded, UnloadError
switch (this.State)
{
case PluginState.InProgress:
case PluginState.Loading:
case PluginState.Unloading:
case PluginState.Loaded:
case PluginState.LoadError:
throw new InvalidPluginOperationException($"Unable to disable {this.Name}, still loaded");

View file

@ -156,6 +156,12 @@ internal record PluginManifest
[JsonProperty]
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>
/// Gets a list of screenshot image URLs to show in the plugin installer.
/// </summary>

View file

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

View file

@ -25,6 +25,8 @@ namespace Dalamud
private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new();
private static readonly List<Type> LoadedServices = new();
/// <summary>
/// Gets task that gets completed when all blocking early loading services are done loading.
/// </summary>
@ -38,20 +40,35 @@ namespace Dalamud
/// <param name="configuration">Instance of <see cref="DalamudConfiguration"/>.</param>
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.
var cacheDir = new DirectoryInfo(Path.Combine(startInfo.WorkingDirectory!, "cachedSigs"));
if (!cacheDir.Exists)
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"))
{
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()
{
using var serviceInitializeTimings = Timings.Start("Services Init");
var service = typeof(Service<>);
var earlyLoadingServices = new HashSet<Type>();
var blockingEarlyLoadingServices = new HashSet<Type>();
@ -76,12 +92,14 @@ namespace Dalamud
if (attr?.IsAssignableTo(typeof(EarlyLoadedService)) != true)
continue;
var getTask = (Task)service.MakeGenericType(serviceType).InvokeMember(
"GetAsync",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null,
null,
null);
var getTask = (Task)typeof(Service<>)
.MakeGenericType(serviceType)
.InvokeMember(
"GetAsync",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null,
null,
null);
if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedService)))
{
@ -94,7 +112,7 @@ namespace Dalamud
}
dependencyServicesMap[serviceType] =
(List<Type>)service
(List<Type>)typeof(Service<>)
.MakeGenericType(serviceType)
.InvokeMember(
"GetDependencyServices",
@ -118,9 +136,9 @@ namespace Dalamud
}
}).ConfigureAwait(false);
var tasks = new List<Task>();
try
{
var tasks = new List<Task>();
var servicesToLoad = new HashSet<Type>();
servicesToLoad.UnionWith(earlyLoadingServices);
servicesToLoad.UnionWith(blockingEarlyLoadingServices);
@ -133,13 +151,25 @@ namespace Dalamud
x => getAsyncTaskMap.GetValueOrDefault(x)?.IsCompleted == false))
continue;
tasks.Add((Task)service.MakeGenericType(serviceType).InvokeMember(
"StartLoader",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null,
null,
null));
tasks.Add((Task)typeof(Service<>)
.MakeGenericType(serviceType)
.InvokeMember(
"StartLoader",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic,
null,
null,
null));
servicesToLoad.Remove(serviceType);
tasks.Add(tasks.Last().ContinueWith(task =>
{
if (task.IsFaulted)
return;
lock (LoadedServices)
{
LoadedServices.Add(serviceType);
}
}));
}
if (!tasks.Any())
@ -172,10 +202,49 @@ namespace Dalamud
// 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;
}
}
/// <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>
/// Indicates that this constructor will be called for early initialization.
/// </summary>

View file

@ -19,8 +19,7 @@ namespace Dalamud
/// <typeparam name="T">The class you want to store in the service locator.</typeparam>
internal static class Service<T> where T : IServiceType
{
// ReSharper disable once StaticMemberInGenericType
private static readonly TaskCompletionSource<T> InstanceTcs = new();
private static TaskCompletionSource<T> instanceTcs = new();
static Service()
{
@ -31,50 +30,28 @@ namespace Dalamud
ServiceManager.Log.Debug("Service<{0}>: Static ctor called", typeof(T).Name);
if (exposeToPlugins)
Service<ServiceContainer>.Get().RegisterSingleton(InstanceTcs.Task);
Service<ServiceContainer>.Get().RegisterSingleton(instanceTcs.Task);
}
/// <summary>
/// Initializes the service.
/// Specifies how to handle the cases of failed services when calling <see cref="Service{T}.GetNullable"/>.
/// </summary>
/// <returns>The object.</returns>
[UsedImplicitly]
public static Task<T> StartLoader()
public enum ExceptionPropagationMode
{
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");
/// <summary>
/// Propagate all exceptions.
/// </summary>
PropagateAll,
return Task.Run(Timings.AttachTimingHandle(async () =>
{
ServiceManager.Log.Debug("Service<{0}>: Begin construction", typeof(T).Name);
try
{
var instance = await ConstructObject();
InstanceTcs.SetResult(instance);
/// <summary>
/// Propagate all exceptions, except for <see cref="UnloadedException"/>.
/// </summary>
PropagateNonUnloaded,
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;
}
}));
/// <summary>
/// Treat all exceptions as null.
/// </summary>
None,
}
/// <summary>
@ -83,7 +60,7 @@ namespace Dalamud
/// <param name="obj">Object to set.</param>
public static void Provide(T obj)
{
InstanceTcs.SetResult(obj);
instanceTcs.SetResult(obj);
ServiceManager.Log.Debug("Service<{0}>: Provided", typeof(T).Name);
}
@ -94,7 +71,7 @@ namespace Dalamud
public static void ProvideException(Exception exception)
{
ServiceManager.Log.Error(exception, "Service<{0}>: Error", typeof(T).Name);
InstanceTcs.SetException(exception);
instanceTcs.SetException(exception);
}
/// <summary>
@ -103,9 +80,9 @@ namespace Dalamud
/// <returns>The object.</returns>
public static T Get()
{
if (!InstanceTcs.Task.IsCompleted)
InstanceTcs.Task.Wait();
return InstanceTcs.Task.Result;
if (!instanceTcs.Task.IsCompleted)
instanceTcs.Task.Wait();
return instanceTcs.Task.Result;
}
/// <summary>
@ -113,13 +90,27 @@ namespace Dalamud
/// </summary>
/// <returns>The object.</returns>
[UsedImplicitly]
public static Task<T> GetAsync() => InstanceTcs.Task;
public static Task<T> GetAsync() => instanceTcs.Task;
/// <summary>
/// Attempt to pull the instance out of the service locator.
/// </summary>
/// <param name="propagateException">Specifies which exceptions to propagate.</param>
/// <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>
/// 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();
}
[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)
{
var task = (Task)typeof(Service<>)
@ -180,5 +242,19 @@ namespace Dalamud
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.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Logging.Internal;
using ImGuiNET;
using Microsoft.Win32;
using Serilog;
@ -536,6 +537,31 @@ namespace Dalamud.Utility
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)
{
if (type.IsPointer)