diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7ccf0c2d1..d780df2fc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -46,7 +46,7 @@ jobs: - uses: actions/checkout@v2 with: repository: goatcorp/dalamud-distrib - ssh-key: ${{ secrets.DEPLOY_SSH }} + token: ${{ secrets.UPDATE_PAT }} - uses: actions/download-artifact@v2 with: name: dalamud-artifact @@ -96,13 +96,8 @@ jobs: - name: Commit changes shell: bash env: - DEPLOY_SSH: ${{ secrets.DEPLOY_SSH }} - GIT_SSH_COMMAND: "ssh -o StrictHostKeyChecking=no" GH_BRANCH: ${{ steps.extract_branch.outputs.branch }} run: | - eval "$(ssh-agent -s)" - ssh-add - <<< "${DEPLOY_SSH}" - git config --global user.name "Actions User" git config --global user.email "actions@github.com" diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj index 2e0f19aca..d4cb1528f 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj @@ -66,6 +66,7 @@ _DEBUG;%(PreprocessorDefinitions) Use 26812 + false false diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index aea9fe452..25919af07 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -28,7 +28,7 @@ - + all diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index 5ed6d02ac..d352ad2c8 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -54,7 +54,7 @@ namespace Dalamud.CorePlugin /// Initializes a new instance of the class. /// /// Dalamud plugin interface. - public PluginImpl(DalamudPluginInterface pluginInterface) + public PluginImpl(DalamudPluginInterface pluginInterface, PluginLog log) { try { @@ -68,7 +68,7 @@ namespace Dalamud.CorePlugin Service.Get().AddHandler("/coreplug", new(this.OnCommand) { HelpMessage = $"Access the {this.Name} plugin." }); - PluginLog.Information("CorePlugin ctor!"); + log.Information("CorePlugin ctor!"); } catch (Exception ex) { diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index c29fada83..fbefbd92a 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -200,15 +200,21 @@ namespace Dalamud.Injector var logFile = new FileInfo(logPath); if (!logFile.Exists) + { logFile.Create(); + } if (logFile.Length <= cullingFileSize) + { return; + } var amountToCull = logFile.Length - cullingFileSize; if (amountToCull < bufferSize) + { return; + } using var reader = new BinaryReader(logFile.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); using var writer = new BinaryWriter(logFile.Open(FileMode.Open, FileAccess.Write, FileShare.ReadWrite)); @@ -247,7 +253,6 @@ namespace Dalamud.Injector var workingDirectory = startInfo.WorkingDirectory; var configurationPath = startInfo.ConfigurationPath; var pluginDirectory = startInfo.PluginDirectory; - var defaultPluginDirectory = startInfo.DefaultPluginDirectory; var assetDirectory = startInfo.AssetDirectory; var delayInitializeMs = startInfo.DelayInitializeMs; var logName = startInfo.LogName; @@ -257,25 +262,41 @@ namespace Dalamud.Injector for (var i = 2; i < args.Count; i++) { if (args[i].StartsWith(key = "--dalamud-working-directory=")) + { workingDirectory = args[i][key.Length..]; + } else if (args[i].StartsWith(key = "--dalamud-configuration-path=")) + { configurationPath = args[i][key.Length..]; + } else if (args[i].StartsWith(key = "--dalamud-plugin-directory=")) + { pluginDirectory = args[i][key.Length..]; - else if (args[i].StartsWith(key = "--dalamud-dev-plugin-directory=")) - defaultPluginDirectory = args[i][key.Length..]; + } else if (args[i].StartsWith(key = "--dalamud-asset-directory=")) + { assetDirectory = args[i][key.Length..]; + } else if (args[i].StartsWith(key = "--dalamud-delay-initialize=")) + { delayInitializeMs = int.Parse(args[i][key.Length..]); + } else if (args[i].StartsWith(key = "--dalamud-client-language=")) + { languageStr = args[i][key.Length..].ToLowerInvariant(); + } else if (args[i].StartsWith(key = "--dalamud-tspack-b64=")) + { troubleshootingData = Encoding.UTF8.GetString(Convert.FromBase64String(args[i][key.Length..])); + } else if (args[i].StartsWith(key = "--logname=")) + { logName = args[i][key.Length..]; + } else + { continue; + } args.RemoveAt(i); i--; @@ -287,33 +308,49 @@ namespace Dalamud.Injector workingDirectory ??= Directory.GetCurrentDirectory(); configurationPath ??= Path.Combine(xivlauncherDir, "dalamudConfig.json"); pluginDirectory ??= Path.Combine(xivlauncherDir, "installedPlugins"); - defaultPluginDirectory ??= Path.Combine(xivlauncherDir, "devPlugins"); assetDirectory ??= Path.Combine(xivlauncherDir, "dalamudAssets", "dev"); ClientLanguage clientLanguage; if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "english").Length))] == key[0..len]) + { clientLanguage = ClientLanguage.English; + } else if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "japanese").Length))] == key[0..len]) + { clientLanguage = ClientLanguage.Japanese; + } else if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "日本語").Length))] == key[0..len]) + { clientLanguage = ClientLanguage.Japanese; + } else if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "german").Length))] == key[0..len]) + { clientLanguage = ClientLanguage.German; + } else if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "deutsch").Length))] == key[0..len]) + { clientLanguage = ClientLanguage.German; + } else if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "french").Length))] == key[0..len]) + { clientLanguage = ClientLanguage.French; + } else if (languageStr[0..(len = Math.Min(languageStr.Length, (key = "français").Length))] == key[0..len]) + { clientLanguage = ClientLanguage.French; + } else if (int.TryParse(languageStr, out var languageInt) && Enum.IsDefined((ClientLanguage)languageInt)) + { clientLanguage = (ClientLanguage)languageInt; + } else + { throw new CommandLineException($"\"{languageStr}\" is not a valid supported language."); + } startInfo.WorkingDirectory = workingDirectory; startInfo.ConfigurationPath = configurationPath; startInfo.PluginDirectory = pluginDirectory; - startInfo.DefaultPluginDirectory = defaultPluginDirectory; startInfo.AssetDirectory = assetDirectory; startInfo.Language = clientLanguage; startInfo.DelayInitializeMs = delayInitializeMs; @@ -350,10 +387,14 @@ namespace Dalamud.Injector exeSpaces += " "; if (particularCommand is null or "help") + { Console.WriteLine("{0} help [command]", exeName); + } if (particularCommand is null or "inject") + { Console.WriteLine("{0} inject [-h/--help] [-a/--all] [--warn] [--fix-acl] [--se-debug-privilege] [pid1] [pid2] [pid3] ...", exeName); + } if (particularCommand is null or "launch") { @@ -367,7 +408,7 @@ namespace Dalamud.Injector } Console.WriteLine("Specifying dalamud start info: [--dalamud-working-directory=path] [--dalamud-configuration-path=path]"); - Console.WriteLine(" [--dalamud-plugin-directory=path] [--dalamud-dev-plugin-directory=path]"); + Console.WriteLine(" [--dalamud-plugin-directory=path]"); Console.WriteLine(" [--dalamud-asset-directory=path] [--dalamud-delay-initialize=0(ms)]"); Console.WriteLine(" [--dalamud-client-language=0-3|j(apanese)|e(nglish)|d|g(erman)|f(rench)]"); @@ -431,7 +472,7 @@ namespace Dalamud.Injector } else { - throw new CommandLineException($"\"{args[i]}\" is not a command line argument."); + Log.Warning($"\"{args[i]}\" is not a valid command line argument, ignoring."); } } @@ -505,29 +546,53 @@ namespace Dalamud.Injector } if (args[i] == "-h" || args[i] == "--help") + { showHelp = true; + } else if (args[i] == "-f" || args[i] == "--fake-arguments") + { useFakeArguments = true; + } else if (args[i] == "--without-dalamud") + { withoutDalamud = true; + } else if (args[i] == "--no-wait") + { waitForGameWindow = false; + } else if (args[i] == "--no-fix-acl" || args[i] == "--no-acl-fix") + { noFixAcl = true; + } else if (args[i] == "-g") + { gamePath = args[++i]; + } else if (args[i].StartsWith("--game=")) + { gamePath = args[i].Split('=', 2)[1]; + } else if (args[i] == "-m") + { mode = args[++i]; + } else if (args[i].StartsWith("--mode=")) + { mode = args[i].Split('=', 2)[1]; + } else if (args[i].StartsWith("--handle-owner=")) + { handleOwner = IntPtr.Parse(args[i].Split('=', 2)[1]); + } else if (args[i] == "--") + { parsingGameArgument = true; + } else - throw new CommandLineException($"\"{args[i]}\" is not a command line argument."); + { + Log.Warning($"\"{args[i]}\" is not a valid command line argument, ignoring."); + } } var checksumTable = "fX1pGtdS5CAP4_VL"; @@ -536,11 +601,15 @@ namespace Dalamud.Injector gameArguments = gameArguments.SelectMany(x => { if (!x.StartsWith("//**sqex0003") || !x.EndsWith("**//")) + { return new List() { x }; + } var checksum = checksumTable.IndexOf(x[x.Length - 5]); if (checksum == -1) + { return new List() { x }; + } var encData = Convert.FromBase64String(x.Substring(12, x.Length - 12 - 5).Replace('-', '+').Replace('_', '/').Replace('*', '=')); var rawData = new byte[encData.Length]; @@ -554,13 +623,25 @@ namespace Dalamud.Injector encryptArguments = true; var args = argDelimiterRegex.Split(rawString).Skip(1).Select(y => string.Join('=', kvDelimiterRegex.Split(y, 2)).Replace(" ", " ")).ToList(); if (!args.Any()) + { continue; + } + if (!args.First().StartsWith("T=")) + { continue; + } + if (!uint.TryParse(args.First().Substring(2), out var tickCount)) + { continue; + } + if (tickCount >> 16 != i) + { continue; + } + return args.Skip(1); } @@ -712,7 +793,9 @@ namespace Dalamud.Injector if (handleOwner != IntPtr.Zero) { if (!DuplicateHandle(Process.GetCurrentProcess().Handle, process.Handle, handleOwner, out processHandleForOwner, 0, false, DuplicateOptions.SameAccess)) + { Log.Warning("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error()); + } } Console.WriteLine($"{{\"pid\": {process.Id}, \"handle\": {processHandleForOwner}}}"); @@ -755,7 +838,9 @@ namespace Dalamud.Injector helperProcess.BeginErrorReadLine(); helperProcess.WaitForExit(); if (helperProcess.ExitCode != 0) + { return -1; + } var result = JsonSerializer.CreateDefault().Deserialize>(new JsonTextReader(helperProcess.StandardOutput)); var pid = result["pid"]; @@ -812,7 +897,9 @@ namespace Dalamud.Injector var startInfoAddress = startInfoBuffer.Add(startInfoBytes); if (startInfoAddress == 0) + { throw new Exception("Unable to allocate start info JSON"); + } injector.GetFunctionAddress(bootModule, "Initialize", out var initAddress); injector.CallRemoteFunction(initAddress, startInfoAddress, out var exitCode); @@ -847,7 +934,10 @@ namespace Dalamud.Injector /// private static string EncodeParameterArgument(string argument, bool force = false) { - if (argument == null) throw new ArgumentNullException(nameof(argument)); + if (argument == null) + { + throw new ArgumentNullException(nameof(argument)); + } // Unless we're told otherwise, don't quote unless we actually // need to do so --- hopefully avoid problems if programs won't diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index c870fa09d..0d29b8cb7 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -153,6 +153,11 @@ internal sealed class DalamudConfiguration : IServiceType /// public bool ToggleUiHideDuringGpose { get; set; } = true; + /// + /// Gets or sets a value indicating whether or not a message containing Dalamud's current version and the number of loaded plugins should be sent at login. + /// + public bool PrintDalamudWelcomeMsg { get; set; } = true; + /// /// Gets or sets a value indicating whether or not a message containing detailed plugin information should be sent at login. /// diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 20a5918b7..116ebd008 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.5.0.2 + 7.6.0.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) @@ -68,7 +68,7 @@ - + diff --git a/Dalamud/DalamudStartInfo.cs b/Dalamud/DalamudStartInfo.cs index 658934005..4c8e7566d 100644 --- a/Dalamud/DalamudStartInfo.cs +++ b/Dalamud/DalamudStartInfo.cs @@ -30,7 +30,6 @@ public record DalamudStartInfo : IServiceType this.ConfigurationPath = other.ConfigurationPath; this.LogName = other.LogName; this.PluginDirectory = other.PluginDirectory; - this.DefaultPluginDirectory = other.DefaultPluginDirectory; this.AssetDirectory = other.AssetDirectory; this.Language = other.Language; this.GameVersion = other.GameVersion; @@ -72,11 +71,6 @@ public record DalamudStartInfo : IServiceType /// public string? PluginDirectory { get; set; } - /// - /// Gets or sets the path to the directory for developer plugins. - /// - public string? DefaultPluginDirectory { get; set; } - /// /// Gets or sets the path to core Dalamud assets. /// diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index d3c28011f..33e09e221 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -97,7 +97,8 @@ public sealed class EntryPoint var oldPathOld = Path.Combine(baseDirectory, "..", "..", "..", $"{logFileName}.log.old"); #endif Log.CloseAndFlush(); - + +#if DEBUG var oldFileOld = new FileInfo(oldPathOld); if (oldFileOld.Exists) { @@ -109,6 +110,23 @@ public sealed class EntryPoint } CullLogFile(logPath, 1 * 1024 * 1024, oldPath, 10 * 1024 * 1024); +#else + try + { + if (File.Exists(logPath)) + File.Delete(logPath); + + if (File.Exists(oldPath)) + File.Delete(oldPath); + + if (File.Exists(oldPathOld)) + File.Delete(oldPathOld); + } + catch + { + // ignored + } +#endif var config = new LoggerConfiguration() .WriteTo.Sink(SerilogEventSink.Instance) @@ -156,6 +174,8 @@ public sealed class EntryPoint AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; + var unloadFailed = false; + try { if (info.DelayInitializeMs > 0) @@ -180,7 +200,15 @@ public sealed class EntryPoint dalamud.WaitForUnload(); - ServiceManager.UnloadAllServices(); + try + { + ServiceManager.UnloadAllServices(); + } + catch (Exception ex) + { + Log.Fatal(ex, "Could not unload services."); + unloadFailed = true; + } } catch (Exception ex) { @@ -195,6 +223,11 @@ public sealed class EntryPoint Log.CloseAndFlush(); SerilogEventSink.Instance.LogLine -= SerilogOnLogLine; } + + // If we didn't unload services correctly, we need to kill the process. + // We will never signal to Framework. + if (unloadFailed) + Environment.Exit(-1); } private static void SerilogOnLogLine(object? sender, (string Line, LogEvent LogEvent) ev) @@ -327,7 +360,7 @@ public sealed class EntryPoint } var pluginInfo = string.Empty; - var supportText = ", please visit us on Discord for more help."; + var supportText = ", please visit us on Discord for more help"; try { var pm = Service.GetNullable(); diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 9250ef70c..4827c0d4f 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -110,7 +110,7 @@ public class ChatHandlers : IServiceType private readonly DalamudConfiguration configuration = Service.Get(); private bool hasSeenLoadingMsg; - private bool hasAutoUpdatedPlugins; + private bool startedAutoUpdatingPlugins; [ServiceManager.ServiceConstructor] private ChatHandlers(ChatGui chatGui) @@ -129,6 +129,11 @@ public class ChatHandlers : IServiceType /// public string? LastLink { get; private set; } + /// + /// Gets a value indicating whether or not auto-updates have already completed this session. + /// + public bool IsAutoUpdateComplete { get; private set; } + /// /// Convert a TextPayload to SeString and wrap in italics payloads. /// @@ -185,7 +190,7 @@ public class ChatHandlers : IServiceType if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg) this.PrintWelcomeMessage(); - if (!this.hasAutoUpdatedPlugins) + if (!this.startedAutoUpdatingPlugins) this.AutoUpdatePlugins(); #if !DEBUG && false @@ -243,8 +248,10 @@ public class ChatHandlers : IServiceType 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 (this.configuration.PrintDalamudWelcomeMsg) { + 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 (this.configuration.PrintPluginsWelcomeMsg) { @@ -287,13 +294,16 @@ public class ChatHandlers : IServiceType if (!pluginManager.ReposReady || pluginManager.InstalledPlugins.Count == 0 || pluginManager.AvailablePlugins.Count == 0) { // Plugins aren't ready yet. + // TODO: We should retry. This sucks, because it means we won't ever get here again until another notice. return; } - this.hasAutoUpdatedPlugins = true; + this.startedAutoUpdatingPlugins = true; - Task.Run(() => pluginManager.UpdatePluginsAsync(true, !this.configuration.AutoUpdatePlugins)).ContinueWith(task => + Task.Run(() => pluginManager.UpdatePluginsAsync(true, !this.configuration.AutoUpdatePlugins, true)).ContinueWith(task => { + this.IsAutoUpdateComplete = true; + if (task.IsFaulted) { Log.Error(task.Exception, Loc.Localize("DalamudPluginUpdateCheckFail", "Could not check for plugin updates.")); diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs index 46b285c68..9f8a62faf 100644 --- a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs +++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs @@ -1,10 +1,9 @@ -using System; using System.Collections; using System.Collections.Generic; -using System.Runtime.InteropServices; using Dalamud.IoC; using Dalamud.IoC.Internal; +using FFXIVClientStructs.FFXIV.Client.Game.UI; using Serilog; namespace Dalamud.Game.ClientState.Aetherytes; @@ -15,28 +14,23 @@ namespace Dalamud.Game.ClientState.Aetherytes; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed partial class AetheryteList : IServiceType +public sealed unsafe partial class AetheryteList : IServiceType { [ServiceManager.ServiceDependency] private readonly ClientState clientState = Service.Get(); - private readonly ClientStateAddressResolver address; - private readonly UpdateAetheryteListDelegate updateAetheryteListFunc; + + private readonly Telepo* telepoInstance = Telepo.Instance(); [ServiceManager.ServiceConstructor] private AetheryteList() { - this.address = this.clientState.AddressResolver; - this.updateAetheryteListFunc = Marshal.GetDelegateForFunctionPointer(this.address.UpdateAetheryteList); - - Log.Verbose($"Teleport address 0x{this.address.Telepo.ToInt64():X}"); + Log.Verbose($"Teleport address 0x{((nint)this.telepoInstance).ToInt64():X}"); } - private delegate void UpdateAetheryteListDelegate(IntPtr telepo, byte arg1); - /// /// Gets the amount of Aetherytes the local player has unlocked. /// - public unsafe int Length + public int Length { get { @@ -45,21 +39,19 @@ public sealed partial class AetheryteList : IServiceType this.Update(); - if (TelepoStruct->TeleportList.First == TelepoStruct->TeleportList.Last) + if (this.telepoInstance->TeleportList.First == this.telepoInstance->TeleportList.Last) return 0; - return (int)TelepoStruct->TeleportList.Size(); + return (int)this.telepoInstance->TeleportList.Size(); } } - private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Telepo* TelepoStruct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Telepo*)this.address.Telepo; - /// /// Gets a Aetheryte Entry at the specified index. /// /// Index. /// A at the specified index. - public unsafe AetheryteEntry? this[int index] + public AetheryteEntry? this[int index] { get { @@ -71,7 +63,7 @@ public sealed partial class AetheryteList : IServiceType if (this.clientState.LocalPlayer == null) return null; - return new AetheryteEntry(TelepoStruct->TeleportList.Get((ulong)index)); + return new AetheryteEntry(this.telepoInstance->TeleportList.Get((ulong)index)); } } @@ -81,7 +73,7 @@ public sealed partial class AetheryteList : IServiceType if (this.clientState.LocalPlayer == null) return; - this.updateAetheryteListFunc(this.address.Telepo, 0); + this.telepoInstance->UpdateAetheryteList(); } } diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs index 34ae44e04..369e620be 100644 --- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs +++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs @@ -62,11 +62,6 @@ public sealed class ClientStateAddressResolver : BaseAddressResolver /// public IntPtr ConditionFlags { get; private set; } - /// - /// Gets the address of the Telepo instance. - /// - public IntPtr Telepo { get; private set; } - // Functions /// @@ -80,11 +75,6 @@ public sealed class ClientStateAddressResolver : BaseAddressResolver /// public IntPtr GamepadPoll { get; private set; } - /// - /// Gets the address of the method which updates the list of available teleport locations. - /// - public IntPtr UpdateAetheryteList { get; private set; } - /// /// Scan for and setup any configured address pointers. /// @@ -115,9 +105,5 @@ public sealed class ClientStateAddressResolver : BaseAddressResolver this.TargetManager = sig.GetStaticAddressFromSig("48 8B 05 ?? ?? ?? ?? 48 8D 0D ?? ?? ?? ?? FF 50 ?? 48 85 DB"); this.GamepadPoll = sig.ScanText("40 ?? 57 41 ?? 48 81 EC ?? ?? ?? ?? 44 0F ?? ?? ?? ?? ?? ?? ?? 48 8B"); - - this.Telepo = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? 48 8B 12"); - - this.UpdateAetheryteList = sig.ScanText("E8 ?? ?? ?? ?? 48 89 46 68 4C 8D 45 50"); } } diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index 8fcf59b00..f611a01c6 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -17,7 +17,7 @@ public sealed partial class Condition : IServiceType /// /// The current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has. /// - public const int MaxConditionEntries = 100; + public const int MaxConditionEntries = 104; private readonly bool[] cache = new bool[MaxConditionEntries]; diff --git a/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs b/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs index 3c68d2e43..120b76df6 100644 --- a/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs +++ b/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs @@ -464,4 +464,14 @@ public enum ConditionFlag /// Unable to execute command while recruiting for a non-cross-world party. /// RecruitingWorldOnly = 98, + + /// + /// Command unavailable in this location. + /// + Unknown99 = 99, + + /// + /// Unable to execute command while editing a portrait. + /// + EditingPortrait = 100, } diff --git a/Dalamud/Game/ClientState/Objects/Enums/BattleNpcSubKind.cs b/Dalamud/Game/ClientState/Objects/Enums/BattleNpcSubKind.cs index dd6057d36..f9019ed77 100644 --- a/Dalamud/Game/ClientState/Objects/Enums/BattleNpcSubKind.cs +++ b/Dalamud/Game/ClientState/Objects/Enums/BattleNpcSubKind.cs @@ -10,6 +10,12 @@ public enum BattleNpcSubKind : byte /// None = 0, + /// + /// Weak Spots / Battle NPC parts + /// Eg: Titan's Heart (Naval), Tioman's left and right wing (Sohm Al), Golem Soulstone (The Sunken Temple of Qarn) + /// + BattleNpcPart = 1, + /// /// BattleNpc representing a Pet. /// diff --git a/Dalamud/Game/ClientState/Objects/Types/Character.cs b/Dalamud/Game/ClientState/Objects/Types/Character.cs index 291148708..afc94bd0f 100644 --- a/Dalamud/Game/ClientState/Objects/Types/Character.cs +++ b/Dalamud/Game/ClientState/Objects/Types/Character.cs @@ -102,7 +102,15 @@ public unsafe class Character : GameObject /// /// Gets the status flags. /// - public StatusFlags StatusFlags => (StatusFlags)this.Struct->StatusFlags; + public StatusFlags StatusFlags => + (this.Struct->IsHostile ? StatusFlags.Hostile : StatusFlags.None) | + (this.Struct->InCombat ? StatusFlags.InCombat : StatusFlags.None) | + (this.Struct->IsWeaponDrawn ? StatusFlags.WeaponOut : StatusFlags.None) | + (this.Struct->IsOffhandDrawn ? StatusFlags.OffhandOut : StatusFlags.None) | + (this.Struct->IsPartyMember ? StatusFlags.PartyMember : StatusFlags.None) | + (this.Struct->IsAllianceMember ? StatusFlags.AllianceMember : StatusFlags.None) | + (this.Struct->IsFriend ? StatusFlags.Friend : StatusFlags.None) | + (this.Struct->IsCasting ? StatusFlags.IsCasting : StatusFlags.None); /// /// Gets the underlying structure. diff --git a/Dalamud/Game/Gui/ChatGuiAddressResolver.cs b/Dalamud/Game/Gui/ChatGuiAddressResolver.cs index a94dec1a7..4686d5725 100644 --- a/Dalamud/Game/Gui/ChatGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/ChatGuiAddressResolver.cs @@ -96,7 +96,7 @@ public sealed class ChatGuiAddressResolver : BaseAddressResolver // PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 FA F2 B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); // PopulateItemLinkObject = sig.ScanText( "48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); 5.0 - this.PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? ?? FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); + this.PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? ?? FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); this.InteractableLinkClicked = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 80 BB ?? ?? ?? ?? ?? 0F 85 ?? ?? ?? ?? 80 BB") + 9; } diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index d3b479642..59c136416 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -11,9 +11,14 @@ using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Common.Component.BGCollision; using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; using Serilog; +using SharpDX; + +using Vector2 = System.Numerics.Vector2; +using Vector3 = System.Numerics.Vector3; namespace Dalamud.Game.Gui; @@ -28,7 +33,6 @@ public sealed unsafe class GameGui : IDisposable, IServiceType private readonly GameGuiAddressResolver address; private readonly GetMatrixSingletonDelegate getMatrixSingleton; - private readonly ScreenToWorldNativeDelegate screenToWorldNative; private readonly Hook setGlobalBgmHook; private readonly Hook handleItemHoverHook; @@ -66,9 +70,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType this.handleImmHook = Hook.FromAddress(this.address.HandleImm, this.HandleImmDetour); this.getMatrixSingleton = Marshal.GetDelegateForFunctionPointer(this.address.GetMatrixSingleton); - - this.screenToWorldNative = Marshal.GetDelegateForFunctionPointer(this.address.ScreenToWorld); - + this.toggleUiHideHook = Hook.FromAddress(this.address.ToggleUiHide, this.ToggleUiHideDetour); this.utf8StringFromSequenceHook = Hook.FromAddress(this.address.Utf8StringFromSequence, this.Utf8StringFromSequenceDetour); @@ -79,9 +81,6 @@ public sealed unsafe class GameGui : IDisposable, IServiceType [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate IntPtr GetMatrixSingletonDelegate(); - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private unsafe delegate bool ScreenToWorldNativeDelegate(float* camPos, float* clipPos, float rayDistance, float* worldPos, int* unknown); - // Hooked delegates [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -246,18 +245,15 @@ public sealed unsafe class GameGui : IDisposable, IServiceType var matrixSingleton = this.getMatrixSingleton(); // Read current ViewProjectionMatrix plus game window size - var viewProjectionMatrix = default(SharpDX.Matrix); + var viewProjectionMatrix = default(Matrix); float width, height; - unsafe - { - var rawMatrix = (float*)(matrixSingleton + 0x1b4).ToPointer(); + var rawMatrix = (float*)(matrixSingleton + 0x1b4).ToPointer(); - for (var i = 0; i < 16; i++, rawMatrix++) - viewProjectionMatrix[i] = *rawMatrix; + for (var i = 0; i < 16; i++, rawMatrix++) + viewProjectionMatrix[i] = *rawMatrix; - width = *rawMatrix; - height = *(rawMatrix + 1); - } + width = *rawMatrix; + height = *(rawMatrix + 1); viewProjectionMatrix.Invert(); @@ -277,38 +273,19 @@ public sealed unsafe class GameGui : IDisposable, IServiceType var clipPos = camPosOne - camPos; clipPos.Normalize(); - bool isSuccess; - unsafe + // This array is larger than necessary because it contains more info than we currently use + var worldPosArray = default(RaycastHit); + + // Theory: this is some kind of flag on what type of things the ray collides with + var unknown = stackalloc int[3] { - var camPosArray = camPos.ToArray(); - var clipPosArray = clipPos.ToArray(); + 0x4000, + 0x4000, + 0x0, + }; - // This array is larger than necessary because it contains more info than we currently use - var worldPosArray = stackalloc float[32]; - - // Theory: this is some kind of flag on what type of things the ray collides with - var unknown = stackalloc int[3] - { - 0x4000, - 0x4000, - 0x0, - }; - - fixed (float* pCamPos = camPosArray) - { - fixed (float* pClipPos = clipPosArray) - { - isSuccess = this.screenToWorldNative(pCamPos, pClipPos, rayDistance, worldPosArray, unknown); - } - } - - worldPos = new Vector3 - { - X = worldPosArray[0], - Y = worldPosArray[1], - Z = worldPosArray[2], - }; - } + var isSuccess = BGCollisionModule.Raycast2(camPos.ToSystem(), clipPos.ToSystem(), rayDistance, &worldPosArray, unknown); + worldPos = worldPosArray.Point; return isSuccess; } @@ -317,7 +294,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType /// Gets a pointer to the game's UI module. /// /// IntPtr pointing to UI module. - public unsafe IntPtr GetUIModule() + public IntPtr GetUIModule() { var framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance(); if (framework == null) @@ -336,9 +313,9 @@ public sealed unsafe class GameGui : IDisposable, IServiceType /// Name of addon to find. /// Index of addon to find (1-indexed). /// IntPtr.Zero if unable to find UI, otherwise IntPtr pointing to the start of the addon. - public unsafe IntPtr GetAddonByName(string name, int index = 1) + public IntPtr GetAddonByName(string name, int index = 1) { - var atkStage = FFXIVClientStructs.FFXIV.Component.GUI.AtkStage.GetSingleton(); + var atkStage = AtkStage.GetSingleton(); if (atkStage == null) return IntPtr.Zero; @@ -360,7 +337,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType /// A pointer to the agent interface. public IntPtr FindAgentInterface(string addonName) { - var addon = this.GetAddonByName(addonName, 1); + var addon = this.GetAddonByName(addonName); return this.FindAgentInterface(addon); } @@ -369,7 +346,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType /// /// The addon address. /// A pointer to the agent interface. - public unsafe IntPtr FindAgentInterface(void* addon) => this.FindAgentInterface((IntPtr)addon); + public IntPtr FindAgentInterface(void* addon) => this.FindAgentInterface((IntPtr)addon); /// /// Find the agent associated with an addon, if possible. @@ -430,9 +407,9 @@ public sealed unsafe class GameGui : IDisposable, IServiceType /// A value indicating whether or not the game is on the title screen. internal bool IsOnTitleScreen() { - var charaSelect = this.GetAddonByName("CharaSelect", 1); - var charaMake = this.GetAddonByName("CharaMake", 1); - var titleDcWorldMap = this.GetAddonByName("TitleDCWorldMap", 1); + var charaSelect = this.GetAddonByName("CharaSelect"); + var charaMake = this.GetAddonByName("CharaMake"); + var titleDcWorldMap = this.GetAddonByName("TitleDCWorldMap"); if (charaMake != nint.Zero || charaSelect != nint.Zero || titleDcWorldMap != nint.Zero) return false; diff --git a/Dalamud/Game/Gui/GameGuiAddressResolver.cs b/Dalamud/Game/Gui/GameGuiAddressResolver.cs index fda4acb99..e45b07487 100644 --- a/Dalamud/Game/Gui/GameGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/GameGuiAddressResolver.cs @@ -47,11 +47,6 @@ internal sealed class GameGuiAddressResolver : BaseAddressResolver /// public IntPtr GetMatrixSingleton { get; private set; } - /// - /// Gets the address of the native ScreenToWorld method. - /// - public IntPtr ScreenToWorld { get; private set; } - /// /// Gets the address of the native ToggleUiHide method. /// @@ -66,13 +61,12 @@ internal sealed class GameGuiAddressResolver : BaseAddressResolver protected override void Setup64Bit(SigScanner sig) { this.SetGlobalBgm = sig.ScanText("4C 8B 15 ?? ?? ?? ?? 4D 85 D2 74 58"); - this.HandleItemHover = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 5C 24 ?? 48 89 AE ?? ?? ?? ??"); + this.HandleItemHover = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 5C 24 ?? 48 89 AE ?? ?? ?? ?? 48 89 AE ?? ?? ?? ??"); this.HandleItemOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 4D"); this.HandleActionHover = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 83 F8 0F"); this.HandleActionOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B DA 48 8B F9 4D 85 C0 74 1F"); this.HandleImm = sig.ScanText("E8 ?? ?? ?? ?? 84 C0 75 10 48 83 FF 09"); this.GetMatrixSingleton = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 4C 24 ?? 48 89 4c 24 ?? 4C 8D 4D ?? 4C 8D 44 24 ??"); - this.ScreenToWorld = sig.ScanText("48 83 EC 48 48 8B 05 ?? ?? ?? ?? 4D 8B D1"); this.ToggleUiHide = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 0F B6 B9 ?? ?? ?? ?? B8 ?? ?? ?? ??"); this.Utf8StringFromSequence = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8D 41 22 66 C7 41 ?? ?? ?? 48 89 01 49 8B D8"); } diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index 9ed5d7ab9..60e61b2f7 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -56,7 +56,7 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType 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 = Hook.FromAddress(uiModuleRequestMainCommandAddress, this.UiModuleRequestMainCommandDetour); - var atkUnitBaseReceiveGlobalEventAddress = sigScanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 50 44 0F B7 F2 "); + var atkUnitBaseReceiveGlobalEventAddress = sigScanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 50 44 0F B7 F2"); this.hookAtkUnitBaseReceiveGlobalEvent = Hook.FromAddress(atkUnitBaseReceiveGlobalEventAddress, this.AtkUnitBaseReceiveGlobalEventDetour); this.locDalamudPlugins = Loc.Localize("SystemMenuPlugins", "Dalamud Plugins"); diff --git a/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs b/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs index 4189bc280..89b721a87 100644 --- a/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs +++ b/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs @@ -22,7 +22,7 @@ public sealed class LibcFunctionAddressResolver : BaseAddressResolver /// protected override void Setup64Bit(SigScanner sig) { - this.StdStringFromCstring = sig.ScanText("48895C2408 4889742410 57 4883EC20 488D4122 66C741200101 488901 498BD8"); - this.StdStringDeallocate = sig.ScanText("80792100 7512 488B5108 41B833000000 488B09 E9??????00 C3"); + this.StdStringFromCstring = sig.ScanText("48 89 5C 24 08 48 89 74 24 10 57 48 83 EC 20 48 8D 41 22 66 C7 41 20 01 01 48 89 01 49 8B D8"); + this.StdStringDeallocate = sig.ScanText("80 79 21 00 75 12 48 8B 51 08 41 B8 33 00 00 00 48 8B 09 E9 ?? ?? ?? 00 C3"); } } diff --git a/Dalamud/Game/Network/GameNetworkAddressResolver.cs b/Dalamud/Game/Network/GameNetworkAddressResolver.cs index 5be85bd35..c698ee813 100644 --- a/Dalamud/Game/Network/GameNetworkAddressResolver.cs +++ b/Dalamud/Game/Network/GameNetworkAddressResolver.cs @@ -22,7 +22,7 @@ public sealed class GameNetworkAddressResolver : BaseAddressResolver { // ProcessZonePacket = sig.ScanText("48 89 74 24 18 57 48 83 EC 50 8B F2 49 8B F8 41 0F B7 50 02 8B CE E8 ?? ?? 7A FF 0F B7 57 02 8D 42 89 3D 5F 02 00 00 0F 87 60 01 00 00 4C 8D 05"); // ProcessZonePacket = sig.ScanText("48 89 74 24 18 57 48 83 EC 50 8B F2 49 8B F8 41 0F B7 50 02 8B CE E8 ?? ?? 73 FF 0F B7 57 02 8D 42 ?? 3D ?? ?? 00 00 0F 87 60 01 00 00 4C 8D 05"); - this.ProcessZonePacketDown = sig.ScanText("48 89 5C 24 ?? 56 48 83 EC 50 8B F2"); + this.ProcessZonePacketDown = sig.ScanText("40 53 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 44 24 ?? 8B F2"); this.ProcessZonePacketUp = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC 70 8B 81 ?? ?? ?? ??"); } } diff --git a/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs b/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs index b31c4d217..b3175cad3 100644 --- a/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs +++ b/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis.Types; using Dalamud.Game.Network.Structures; -using Dalamud.Utility; +using Dalamud.Networking.Http; using Newtonsoft.Json; using Serilog; @@ -22,6 +22,8 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader private const string ApiKey = "GGD6RdSfGyRiHM5WDnAo0Nj9Nv7aC5NDhMj3BebT"; + private readonly HttpClient httpClient = Service.Get().SharedHttpClient; + /// /// Initializes a new instance of the class. /// @@ -97,7 +99,7 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader var uploadPath = "/upload"; var uploadData = JsonConvert.SerializeObject(uploadObject); Log.Verbose("{ListingPath}: {ListingUpload}", uploadPath, uploadData); - await Util.HttpClient.PostAsync($"{ApiBase}{uploadPath}/{ApiKey}", new StringContent(uploadData, Encoding.UTF8, "application/json")); + await this.httpClient.PostAsync($"{ApiBase}{uploadPath}/{ApiKey}", new StringContent(uploadData, Encoding.UTF8, "application/json")); // ==================================================================================== @@ -133,7 +135,7 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader var taxUpload = JsonConvert.SerializeObject(taxUploadObject); Log.Verbose("{TaxPath}: {TaxUpload}", taxPath, taxUpload); - await Util.HttpClient.PostAsync($"{ApiBase}{taxPath}/{ApiKey}", new StringContent(taxUpload, Encoding.UTF8, "application/json")); + await this.httpClient.PostAsync($"{ApiBase}{taxPath}/{ApiKey}", new StringContent(taxUpload, Encoding.UTF8, "application/json")); // ==================================================================================== @@ -175,7 +177,7 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader message.Headers.Add("Authorization", ApiKey); message.Content = content; - await Util.HttpClient.SendAsync(message); + await this.httpClient.SendAsync(message); // ==================================================================================== diff --git a/Dalamud/Interface/ImGuiFileDialog/DriveListLoader.cs b/Dalamud/Interface/ImGuiFileDialog/DriveListLoader.cs new file mode 100644 index 000000000..5898fefdf --- /dev/null +++ b/Dalamud/Interface/ImGuiFileDialog/DriveListLoader.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Dalamud.Interface.ImGuiFileDialog; + +/// +/// A drive list loader. Thread-safety guaranteed. +/// +internal class DriveListLoader +{ + /// + /// Initializes a new instance of the class. + /// + public DriveListLoader() + { + this.Drives = Array.Empty(); + } + + /// + /// Gets the drive list. This may be incomplete if the loader is still loading. + /// + public IReadOnlyList Drives { get; private set; } + + /// + /// Gets a value indicating whether or not the loader is loading. + /// + public bool Loading { get; private set; } + + /// + /// Loads the drive list, asynchronously. + /// + /// A representing the asynchronous operation. + public async Task LoadDrivesAsync() + { + this.Loading = true; + try + { + await this.InitDrives(); + } + finally + { + this.Loading = false; + } + } + + private async Task InitDrives() + { + // Force async to avoid this being invoked synchronously unless it's awaited. + await Task.Yield(); + this.Drives = DriveInfo.GetDrives(); + } +} diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs index c34d047d9..ba71c8cfa 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs @@ -12,11 +12,13 @@ public partial class FileDialog { private readonly object filesLock = new(); + private readonly DriveListLoader driveListLoader = new(); + private List files = new(); private List filteredFiles = new(); private SortingField currentSortingField = SortingField.FileName; - private bool[] sortDescending = new[] { false, false, false, false }; + private bool[] sortDescending = { false, false, false, false }; private enum FileStructType { @@ -296,12 +298,14 @@ public partial class FileDialog } } + private IEnumerable GetDrives() + { + return this.driveListLoader.Drives.Select(drive => new SideBarItem(drive.Name, drive.Name, FontAwesomeIcon.Server)); + } + private void SetupSideBar() { - foreach (var drive in DriveInfo.GetDrives()) - { - this.drives.Add(new SideBarItem(drive.Name, drive.Name, FontAwesomeIcon.Server)); - } + _ = this.driveListLoader.LoadDrivesAsync(); var personal = Path.GetDirectoryName(Environment.GetFolderPath(Environment.SpecialFolder.Personal)); diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Structs.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Structs.cs index 1373c9189..07e3bc20f 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Structs.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Structs.cs @@ -31,7 +31,6 @@ public partial class FileDialog this.Text = text; this.Location = location; this.Icon = icon; - this.Exists = !this.Location.IsNullOrEmpty() && Directory.Exists(this.Location); } public string Text { get; init; } @@ -40,8 +39,6 @@ public partial class FileDialog public FontAwesomeIcon Icon { get; init; } - public bool Exists { get; init; } - public bool CheckExistence() => !this.Location.IsNullOrEmpty() && Directory.Exists(this.Location); } diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs index c55722b4a..d3be8da95 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Numerics; +using Dalamud.Utility; using ImGuiNET; namespace Dalamud.Interface.ImGuiFileDialog; @@ -316,7 +317,7 @@ public partial class FileDialog ImGui.SetCursorPosY(ImGui.GetCursorPosY() + Scaled(5)); var idx = 0; - foreach (var qa in this.drives.Concat(this.quickAccess).Where(qa => qa.Exists)) + foreach (var qa in this.GetDrives().Concat(this.quickAccess).Where(qa => !qa.Location.IsNullOrEmpty())) { ImGui.PushID(idx++); ImGui.SetCursorPosX(Scaled(25)); diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs index fe224be76..aec5e9af4 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs @@ -52,7 +52,6 @@ public partial class FileDialog private float footerHeight = 0; private string selectedSideBar = string.Empty; - private List drives = new(); private List quickAccess = new(); /// @@ -259,6 +258,7 @@ public partial class FileDialog private void SetPath(string path) { + this.searchBuffer = string.Empty; this.selectedSideBar = string.Empty; this.currentPath = path; this.files.Clear(); diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 8848cd481..07d557967 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -517,7 +517,7 @@ internal class DalamudInterface : IDisposable, IServiceType } catch (Exception ex) { - PluginLog.Error(ex, "Error during OnDraw"); + Log.Error(ex, "Error during OnDraw"); } } @@ -877,7 +877,7 @@ internal class DalamudInterface : IDisposable, IServiceType foreach (var plugin in pluginManager.InstalledPlugins) { // TODO: some more here, state maybe? - PluginLog.Information($"{plugin.Name}"); + Log.Information($"{plugin.Name}"); } } diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 9a8da773c..cde3f1c42 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -20,12 +20,12 @@ using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Style; using Dalamud.Interface.Windowing; +using Dalamud.Logging.Internal; using Dalamud.Utility; using Dalamud.Utility.Timing; using ImGuiNET; using ImGuiScene; using PInvoke; -using Serilog; // general dev notes, here because it's easiest @@ -47,6 +47,8 @@ namespace Dalamud.Interface.Internal; [ServiceManager.BlockingEarlyLoadedService] internal class InterfaceManager : IDisposable, IServiceType { + private static ModuleLog Log = new ModuleLog("IM"); + private const float DefaultFontSizePt = 12.0f; private const float DefaultFontSizePx = DefaultFontSizePt * 4.0f / 3.0f; private const ushort Fallback1Codepoint = 0x3013; // Geta mark; FFXIV uses this to indicate that a glyph is missing. @@ -76,16 +78,41 @@ internal class InterfaceManager : IDisposable, IServiceType private bool isOverrideGameCursor = true; [ServiceManager.ServiceConstructor] - private InterfaceManager() + private InterfaceManager(SigScanner sigScanner) { + Log.Information("ctor called"); + this.dispatchMessageWHook = Hook.FromImport( null, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour); this.setCursorHook = Hook.FromImport( null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); + Log.Information("Import hooks applied"); this.fontBuildSignal = new ManualResetEvent(false); this.address = new SwapChainVtableResolver(); + this.address.Setup(); + Log.Information("Resolver setup complete"); + + Log.Information("===== S W A P C H A I N ====="); + Log.Information($"Is ReShade: {this.address.IsReshade}"); + Log.Information($"Present address 0x{this.address.Present.ToInt64():X}"); + Log.Information($"ResizeBuffers address 0x{this.address.ResizeBuffers.ToInt64():X}"); + + this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); + this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); + Log.Information("Present and ResizeBuffers hooked"); + + var wndProcAddress = sigScanner.ScanText("E8 ?? ?? ?? ?? 80 7C 24 ?? ?? 74 ?? B8"); + Log.Information($"WndProc address 0x{wndProcAddress.ToInt64():X}"); + this.processMessageHook = Hook.FromAddress(wndProcAddress, this.ProcessMessageDetour); + + this.setCursorHook.Enable(); + this.presentHook.Enable(); + this.resizeBuffersHook.Enable(); + this.dispatchMessageWHook.Enable(); + this.processMessageHook.Enable(); + Log.Information("Hooks enabled"); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -982,10 +1009,9 @@ internal class InterfaceManager : IDisposable, IServiceType } [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction(SigScanner sigScanner, Framework framework) + private void ContinueConstruction() { - this.address.Setup(sigScanner); - framework.RunOnFrameworkThread(() => + this.framework.RunOnFrameworkThread(() => { while ((this.GameWindowHandle = NativeFunctions.FindWindowEx(IntPtr.Zero, this.GameWindowHandle, "FFXIVGAME", IntPtr.Zero)) != IntPtr.Zero) { @@ -1004,23 +1030,6 @@ internal class InterfaceManager : IDisposable, IServiceType { Log.Error(ex, "Could not enable immersive mode"); } - - this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); - this.resizeBuffersHook = Hook.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}"); - Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); - - var wndProcAddress = sigScanner.ScanText("E8 ?? ?? ?? ?? 80 7C 24 ?? ?? 74 ?? B8"); - Log.Verbose($"WndProc address 0x{wndProcAddress.ToInt64():X}"); - this.processMessageHook = Hook.FromAddress(wndProcAddress, this.ProcessMessageDetour); - - this.setCursorHook.Enable(); - this.presentHook.Enable(); - this.resizeBuffersHook.Enable(); - this.dispatchMessageWHook.Enable(); - this.processMessageHook.Enable(); }); } diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index 75c94a61d..3b3c4e003 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -19,7 +19,6 @@ internal unsafe class UiDebug { private const int UnitListCount = 18; - private readonly GetAtkStageSingleton getAtkStageSingleton; private readonly bool[] selectedInList = new bool[UnitListCount]; private readonly string[] listNames = new string[UnitListCount] { @@ -52,9 +51,6 @@ internal unsafe class UiDebug /// public UiDebug() { - var sigScanner = Service.Get(); - var getSingletonAddr = sigScanner.ScanText("E8 ?? ?? ?? ?? 41 B8 01 00 00 00 48 8D 15 ?? ?? ?? ?? 48 8B 48 20 E8 ?? ?? ?? ?? 48 8B CF"); - this.getAtkStageSingleton = Marshal.GetDelegateForFunctionPointer(getSingletonAddr); } private delegate AtkStage* GetAtkStageSingleton(); @@ -445,7 +441,7 @@ internal unsafe class UiDebug { var foundSelected = false; var noResults = true; - var stage = this.getAtkStageSingleton(); + var stage = AtkStage.GetSingleton(); var unitManagers = &stage->RaptureAtkUnitManager->AtkUnitManager.DepthLayerOneList; diff --git a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs index dd2d911ff..b599fb58f 100644 --- a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs +++ b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs @@ -3,13 +3,13 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Net.Http; using System.Net.Http.Json; using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Windowing; +using Dalamud.Networking.Http; using ImGuiNET; using Newtonsoft.Json; @@ -40,7 +40,7 @@ public class BranchSwitcherWindow : Window { Task.Run(async () => { - using var client = new HttpClient(); + var client = Service.Get().SharedHttpClient; this.branches = await client.GetFromJsonAsync>(BranchInfoUrl); Debug.Assert(this.branches != null, "this.branches != null"); diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 4614fbad2..0aeb0722d 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Dalamud.Game; +using Dalamud.Networking.Http; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; @@ -48,6 +49,9 @@ internal class PluginImageCache : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly InterfaceManager.InterfaceManagerWithScene imWithScene = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly HappyHttpClient happyHttpClient = Service.Get(); + private readonly BlockingCollection>> downloadQueue = new(); private readonly BlockingCollection> loadQueue = new(); private readonly CancellationTokenSource cancelToken = new(); @@ -535,7 +539,7 @@ internal class PluginImageCache : IDisposable, IServiceType var bytes = await this.RunInDownloadQueue( async () => { - var data = await Util.HttpClient.GetAsync(url); + var data = await this.happyHttpClient.SharedHttpClient.GetAsync(url); if (data.StatusCode == HttpStatusCode.NotFound) return null; @@ -627,7 +631,9 @@ internal class PluginImageCache : IDisposable, IServiceType var bytes = await this.RunInDownloadQueue( async () => { - var data = await Util.HttpClient.GetAsync(url); + var httpClient = Service.Get().SharedHttpClient; + + var data = await httpClient.GetAsync(url); if (data.StatusCode == HttpStatusCode.NotFound) return null; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs index 646285561..a3a965e80 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs @@ -1,14 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net.Http; using System.Net.Http.Json; -using System.Threading; using System.Threading.Tasks; - +using Dalamud.Networking.Http; using Dalamud.Plugin.Internal; using Dalamud.Utility; -using Serilog; namespace Dalamud.Interface.Internal.Windows.PluginInstaller; @@ -42,7 +39,7 @@ internal class DalamudChangelogManager /// A representing the asynchronous operation. public async Task ReloadChangelogAsync() { - using var client = new HttpClient(); + var client = Service.Get().SharedHttpClient; this.Changelogs = null; var dalamudChangelogs = await client.GetFromJsonAsync>(DalamudChangelogUrl); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index a50be905f..3b3804baa 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2294,7 +2294,8 @@ internal class PluginInstallerWindow : Window, IDisposable disabled = disabled || (plugin.IsOrphaned && !plugin.IsLoaded); // Disable everything if the plugin failed to load - disabled = disabled || plugin.State == PluginState.LoadError || plugin.State == PluginState.DependencyResolutionFailed; + // Now handled by the first case below + // disabled = disabled || plugin.State == PluginState.LoadError || plugin.State == PluginState.DependencyResolutionFailed; // Disable everything if we're loading plugins disabled = disabled || plugin.State == PluginState.Loading || plugin.State == PluginState.Unloading; @@ -2360,7 +2361,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.EndPopup(); } - if (plugin.State == PluginState.UnloadError && !plugin.IsDev) + if (plugin.State is PluginState.UnloadError or PluginState.LoadError or PluginState.DependencyResolutionFailed && !plugin.IsDev) { ImGuiComponents.DisabledButton(FontAwesomeIcon.Frown); @@ -3182,7 +3183,7 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginButtonToolTip_UpdateSingle(string version) => Loc.Localize("InstallerUpdateSingle", "Update to {0}").Format(version); - public static string PluginButtonToolTip_UnloadFailed => Loc.Localize("InstallerUnloadFailedTooltip", "Plugin unload failed, please restart your game and try again."); + public static string PluginButtonToolTip_UnloadFailed => Loc.Localize("InstallerLoadUnloadFailedTooltip", "Plugin load/unload failed, please restart your game and try again."); public static string PluginButtonToolTip_NeedsToBeInDefault => Loc.Localize("InstallerUnloadNeedsToBeInDefault", "This plugin is in one or more collections. If you want to enable or disable it, please do so by enabling or disabling the collections it is in.\nIf you want to manage it manually, remove it from all collections."); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs index 53869ca16..ea345e9cf 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabGeneral.cs @@ -50,6 +50,12 @@ public class SettingsTabGeneral : SettingsTab c => c.DutyFinderChatMessage, (v, c) => c.DutyFinderChatMessage = v), + new SettingsEntry( + Loc.Localize("DalamudSettingsPrintDalamudWelcomeMsg", "Display Dalamud's welcome message"), + Loc.Localize("DalamudSettingsPrintDalamudWelcomeMsgHint", "Display Dalamud's welcome message in FFXIV chat when logging in with a character."), + c => c.PrintDalamudWelcomeMsg, + (v, c) => c.PrintDalamudWelcomeMsg = v), + new SettingsEntry( Loc.Localize("DalamudSettingsPrintPluginsWelcomeMsg", "Display loaded plugins in the welcome message"), Loc.Localize("DalamudSettingsPrintPluginsWelcomeMsgHint", "Display loaded plugins in FFXIV chat when logging in with a character."), diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index c722807dd..13adccffd 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -156,6 +156,14 @@ public class SettingsTabLook : SettingsTab interfaceManager.RebuildFonts(); } + ImGui.SameLine(); + if (ImGui.Button("24pt##DalamudSettingsGlobalUiScaleReset24")) + { + this.globalUiScale = 24.0f / 12.0f; + ImGui.GetIO().FontGlobalScale = this.globalUiScale; + interfaceManager.RebuildFonts(); + } + ImGui.SameLine(); if (ImGui.Button("36pt##DalamudSettingsGlobalUiScaleReset36")) { diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs index 4e69dcb1a..3e73454f3 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs @@ -67,7 +67,7 @@ public class DevPluginsSettingsEntry : SettingsEntry } } - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsDevPluginLocationsHint", "Add additional dev plugin load locations.\nThese can be either the directory or DLL path.")); + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsDevPluginLocationsHint", "Add dev plugin load locations.\nThese can be either the directory or DLL path.")); ImGuiHelpers.ScaledDummy(5); diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index 041049643..18d294a3e 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -6,6 +6,7 @@ using System.Runtime.Serialization; using System.Threading.Tasks; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal.Types; namespace Dalamud.IoC.Internal; @@ -45,9 +46,12 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// /// The type of object to create. /// Scoped objects to be included in the constructor. + /// The scope to be used to create scoped services. /// The created object. - public async Task CreateAsync(Type objectType, params object[] scopedObjects) + public async Task CreateAsync(Type objectType, object[] scopedObjects, IServiceScope? scope = null) { + var scopeImpl = scope as ServiceScopeImpl; + var ctor = this.FindApplicableCtor(objectType, scopedObjects); if (ctor == null) { @@ -76,11 +80,22 @@ internal class ServiceContainer : IServiceProvider, IServiceType parameters .Select(async p => { + if (p.parameterType.GetCustomAttribute() != null) + { + if (scopeImpl == null) + { + Log.Error("Failed to create {TypeName}, depends on scoped service but no scope", objectType.FullName!); + return null; + } + + return await scopeImpl.CreatePrivateScopedObject(p.parameterType, scopedObjects); + } + var service = await this.GetService(p.parameterType, scopedObjects); if (service == null) { - Log.Error("Requested service type {TypeName} was not available (null)", p.parameterType.FullName!); + Log.Error("Requested ctor service type {TypeName} was not available (null)", p.parameterType.FullName!); } return service; @@ -95,7 +110,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType var instance = FormatterServices.GetUninitializedObject(objectType); - if (!await this.InjectProperties(instance, scopedObjects)) + if (!await this.InjectProperties(instance, scopedObjects, scope)) { Log.Error("Failed to create {TypeName}, a requested property service type could not be satisfied", objectType.FullName!); return null; @@ -112,10 +127,12 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// The properties can be marked with the to lock down versions. /// /// The object instance. - /// Scoped objects. + /// Scoped objects to be injected. + /// The scope to be used to create scoped services. /// Whether or not the injection was successful. - public async Task InjectProperties(object instance, params object[] scopedObjects) + public async Task InjectProperties(object instance, object[] publicScopes, IServiceScope? scope = null) { + var scopeImpl = scope as ServiceScopeImpl; var objectType = instance.GetType(); var props = objectType.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | @@ -136,7 +153,21 @@ internal class ServiceContainer : IServiceProvider, IServiceType foreach (var prop in props) { - var service = await this.GetService(prop.propertyInfo.PropertyType, scopedObjects); + object service = null; + + if (prop.propertyInfo.PropertyType.GetCustomAttribute() != null) + { + if (scopeImpl == null) + { + Log.Error("Failed to create {TypeName}, depends on scoped service but no scope", objectType.FullName!); + } + else + { + service = await scopeImpl.CreatePrivateScopedObject(prop.propertyInfo.PropertyType, publicScopes); + } + } + + service ??= await this.GetService(prop.propertyInfo.PropertyType, publicScopes); if (service == null) { @@ -150,6 +181,12 @@ internal class ServiceContainer : IServiceProvider, IServiceType return true; } + /// + /// Get a service scope, enabling the creation of objects with scoped services. + /// + /// An implementation of a service scope. + public IServiceScope GetScope() => new ServiceScopeImpl(this); + /// object? IServiceProvider.GetService(Type serviceType) => this.GetService(serviceType); @@ -185,7 +222,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType } // resolve dependency from scoped objects - var scoped = scopedObjects.FirstOrDefault(o => o.GetType() == serviceType); + var scoped = scopedObjects.FirstOrDefault(o => o.GetType().IsAssignableTo(serviceType)); if (scoped == default) { return null; @@ -211,7 +248,12 @@ internal class ServiceContainer : IServiceProvider, IServiceType .Union(this.instances.Keys) .ToArray(); - var ctors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance); + // Allow resolving non-public ctors for Dalamud types + var ctorFlags = BindingFlags.Public | BindingFlags.Instance; + if (type.Assembly == Assembly.GetExecutingAssembly()) + ctorFlags |= BindingFlags.NonPublic; + + var ctors = type.GetConstructors(ctorFlags); foreach (var ctor in ctors) { if (this.ValidateCtor(ctor, types)) @@ -228,8 +270,10 @@ internal class ServiceContainer : IServiceProvider, IServiceType var parameters = ctor.GetParameters(); foreach (var parameter in parameters) { - var contains = types.Contains(parameter.ParameterType); - if (!contains) + var contains = types.Any(x => x.IsAssignableTo(parameter.ParameterType)); + + // Scoped services are created on-demand + if (!contains && parameter.ParameterType.GetCustomAttribute() == null) { Log.Error("Failed to validate {TypeName}, unable to find any services that satisfy the type", parameter.ParameterType.FullName!); return false; diff --git a/Dalamud/IoC/Internal/ServiceScope.cs b/Dalamud/IoC/Internal/ServiceScope.cs new file mode 100644 index 000000000..01c18a8b2 --- /dev/null +++ b/Dalamud/IoC/Internal/ServiceScope.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Dalamud.IoC.Internal; + +/// +/// Container enabling the creation of scoped services. +/// +internal interface IServiceScope : IDisposable +{ + /// + /// Register objects that may be injected to scoped services, + /// but not directly to created objects. + /// + /// The scopes to add. + public void RegisterPrivateScopes(params object[] scopes); + + /// + /// Create an object. + /// + /// The type of object to create. + /// Scoped objects to be included in the constructor. + /// The created object. + public Task CreateAsync(Type objectType, params object[] scopedObjects); + + /// + /// Inject interfaces into public or static properties on the provided object. + /// The properties have to be marked with the . + /// The properties can be marked with the to lock down versions. + /// + /// The object instance. + /// Scoped objects to be injected. + /// Whether or not the injection was successful. + public Task InjectPropertiesAsync(object instance, params object[] scopedObjects); +} + +/// +/// Implementation of a service scope. +/// +internal class ServiceScopeImpl : IServiceScope +{ + private readonly ServiceContainer container; + + private readonly List privateScopedObjects = new(); + private readonly List scopeCreatedObjects = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The container this scope will use to create services. + public ServiceScopeImpl(ServiceContainer container) + { + this.container = container; + } + + /// + public void RegisterPrivateScopes(params object[] scopes) + { + this.privateScopedObjects.AddRange(scopes); + } + + /// + public Task CreateAsync(Type objectType, params object[] scopedObjects) + { + return this.container.CreateAsync(objectType, scopedObjects, this); + } + + /// + public Task InjectPropertiesAsync(object instance, params object[] scopedObjects) + { + return this.container.InjectProperties(instance, scopedObjects, this); + } + + /// + /// Create a service scoped to this scope, with private scoped objects. + /// + /// The type of object to create. + /// Additional scoped objects. + /// The created object, or null. + public async Task CreatePrivateScopedObject(Type objectType, params object[] scopedObjects) + { + var instance = this.scopeCreatedObjects.FirstOrDefault(x => x.GetType() == objectType); + if (instance != null) + return instance; + + instance = + await this.container.CreateAsync(objectType, scopedObjects.Concat(this.privateScopedObjects).ToArray()); + if (instance != null) + this.scopeCreatedObjects.Add(instance); + + return instance; + } + + /// + public void Dispose() + { + foreach (var createdObject in this.scopeCreatedObjects.OfType()) createdObject.Dispose(); + } +} diff --git a/Dalamud/Logging/PluginLog.cs b/Dalamud/Logging/PluginLog.cs index b2f2a5065..83cc90bc4 100644 --- a/Dalamud/Logging/PluginLog.cs +++ b/Dalamud/Logging/PluginLog.cs @@ -1,6 +1,9 @@ using System; using System.Reflection; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal.Types; using Serilog; using Serilog.Events; @@ -9,9 +12,29 @@ namespace Dalamud.Logging; /// /// Class offering various static methods to allow for logging in plugins. /// -public static class PluginLog +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +public class PluginLog : IServiceType, IDisposable { - #region "Log" prefixed Serilog style methods + private readonly LocalPlugin plugin; + + /// + /// Initializes a new instance of the class. + /// Do not use this ctor, inject PluginLog instead. + /// + /// The plugin this service is scoped for. + internal PluginLog(LocalPlugin plugin) + { + this.plugin = plugin; + } + + /// + /// Gets or sets a prefix appended to log messages. + /// + public string? LogPrefix { get; set; } = null; + + #region Legacy static "Log" prefixed Serilog style methods /// /// Log a templated message to the in-game debug log. @@ -134,7 +157,7 @@ public static class PluginLog #endregion - #region Serilog style methods + #region Legacy static Serilog style methods /// /// Log a templated verbose message to the in-game debug log. @@ -254,6 +277,25 @@ public static class PluginLog public static void LogRaw(LogEventLevel level, Exception? exception, string messageTemplate, params object[] values) => WriteLog(Assembly.GetCallingAssembly().GetName().Name, level, messageTemplate, exception, values); + #region New instanced methods + + /// + /// Log some information. + /// + /// The message. + internal void Information(string message) + { + Serilog.Log.Information($"[{this.plugin.InternalName}] {this.LogPrefix} {message}"); + } + + #endregion + + /// + void IDisposable.Dispose() + { + // ignored + } + private static ILogger GetPluginLogger(string? pluginName) { return Serilog.Log.ForContext("SourceContext", pluginName ?? string.Empty); @@ -272,3 +314,24 @@ public static class PluginLog values); } } + +/// +/// Class offering logging services, for a specific type. +/// +/// The type to log for. +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +public class PluginLog : PluginLog +{ + /// + /// Initializes a new instance of the class. + /// Do not use this ctor, inject PluginLog instead. + /// + /// The plugin this service is scoped for. + internal PluginLog(LocalPlugin plugin) + : base(plugin) + { + this.LogPrefix = typeof(T).Name; + } +} diff --git a/Dalamud/Networking/Http/HappyEyeballsCallback.cs b/Dalamud/Networking/Http/HappyEyeballsCallback.cs new file mode 100644 index 000000000..af854fb2c --- /dev/null +++ b/Dalamud/Networking/Http/HappyEyeballsCallback.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Logging.Internal; +using Dalamud.Utility; + +namespace Dalamud.Networking.Http; + +// Inspired by and adapted from https://github.com/jellyfin/jellyfin/pull/8598 + +/// +/// A class to provide a method to implement a variant of the Happy +/// Eyeballs algorithm for HTTP connections to dual-stack servers. +/// +public class HappyEyeballsCallback : IDisposable +{ + private static readonly ModuleLog Log = new("HTTP"); + + /* + * ToDo: Eventually add in some kind of state management to cache DNS and IP Family. + * For now, this is ignored as the HTTPClient will keep connections alive, but there are benefits to sharing + * cached lookups between different clients. We just need to be able to easily expire those lookups first. + */ + + private readonly AddressFamily forcedAddressFamily; + private readonly int connectionBackoff; + + /// + /// Initializes a new instance of the class. + /// + /// Optional override to force a specific AddressFamily. + /// Backoff time between concurrent connection attempts. + public HappyEyeballsCallback(AddressFamily? forcedAddressFamily = null, int connectionBackoff = 75) + { + this.forcedAddressFamily = forcedAddressFamily ?? AddressFamily.Unspecified; + this.connectionBackoff = connectionBackoff; + } + + /// + public void Dispose() + { + GC.SuppressFinalize(this); + } + + /// + /// The connection callback to provide to a . + /// + /// The context for an HTTP connection. + /// The cancellation token to abort this request. + /// Returns a Stream for consumption by HttpClient. + public async ValueTask ConnectCallback(SocketsHttpConnectionContext context, CancellationToken token) + { + var sortedRecords = await this.GetSortedAddresses(context.DnsEndPoint.Host, token); + + var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(token); + var tasks = new List>(); + + var delayCts = CancellationTokenSource.CreateLinkedTokenSource(linkedToken.Token); + for (var i = 0; i < sortedRecords.Count; i++) + { + var record = sortedRecords[i]; + + delayCts.CancelAfter(this.connectionBackoff * i); + + var task = this.AttemptConnection(record, context.DnsEndPoint.Port, linkedToken.Token, delayCts.Token); + tasks.Add(task); + + var nextDelayCts = CancellationTokenSource.CreateLinkedTokenSource(linkedToken.Token); + _ = task.ContinueWith(_ => { nextDelayCts.Cancel(); }, TaskContinuationOptions.OnlyOnFaulted); + delayCts = nextDelayCts; + } + + var stream = await AsyncUtils.FirstSuccessfulTask(tasks).ConfigureAwait(false); + Log.Verbose($"Established connection to {context.DnsEndPoint.Host} at {stream.Socket.RemoteEndPoint}"); + + // If we're here, it means we have a successful connection. A failure to connect would have caused the above + // line to explode, so we're safe to clean everything up. + linkedToken.Cancel(); + tasks.ForEach(task => { task.ContinueWith(this.CleanupConnectionTask); }); + + return stream; + } + + private async Task AttemptConnection(IPAddress address, int port, CancellationToken token, CancellationToken delayToken) + { + await AsyncUtils.CancellableDelay(-1, delayToken).ConfigureAwait(false); + + if (token.IsCancellationRequested) + { + return Task.FromCanceled(token).Result; + } + + var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp) + { + NoDelay = true, + }; + + try + { + await socket.ConnectAsync(address, port, token).ConfigureAwait(false); + return new NetworkStream(socket, ownsSocket: true); + } + catch + { + socket.Dispose(); + throw; + } + } + + private async Task> GetSortedAddresses(string hostname, CancellationToken token) + { + // This method abuses DNS ordering and LINQ a bit. We can normally assume that "addresses" will be provided in + // the order the system wants to use. GroupBy will return its groups *in the order they're discovered*. Meaning, + // the first group created will always be the "preferred" group, and all other groups are in preference order. + // This means a straight zipper merge is nice and clean and gives us most -> least preferred, repeating. + var dnsRecords = await Dns.GetHostAddressesAsync(hostname, this.forcedAddressFamily, token); + + var groups = dnsRecords + .GroupBy(a => a.AddressFamily) + .Select(g => g.Select(v => v)).ToArray(); + + return Util.ZipperMerge(groups).ToList(); + } + + private void CleanupConnectionTask(Task task) + { + // marks the exception as handled as well, nifty! + // will also handle canceled cases, which aren't explicitly faulted. + var exception = task.Exception; + + if (task.IsFaulted) + { + Log.Verbose(exception!, "A HappyEyeballs connection task failed. Are there network issues?"); + } + } +} diff --git a/Dalamud/Networking/Http/HappyHttpClient.cs b/Dalamud/Networking/Http/HappyHttpClient.cs new file mode 100644 index 000000000..8459f1453 --- /dev/null +++ b/Dalamud/Networking/Http/HappyHttpClient.cs @@ -0,0 +1,56 @@ +using System; +using System.Net; +using System.Net.Http; + +namespace Dalamud.Networking.Http; + +/// +/// A service to help build and manage HttpClients with some semblance of Happy Eyeballs (RFC 8305 - IPv4 fallback) +/// awareness. +/// +[ServiceManager.BlockingEarlyLoadedService] +internal class HappyHttpClient : IDisposable, IServiceType +{ + /// + /// Initializes a new instance of the class. + /// + /// A service to talk to the Smileton Loporrits to build an HTTP Client aware of Happy Eyeballs. + /// + [ServiceManager.ServiceConstructor] + private HappyHttpClient() + { + this.SharedHappyEyeballsCallback = new HappyEyeballsCallback(); + + this.SharedHttpClient = new HttpClient(new SocketsHttpHandler + { + AutomaticDecompression = DecompressionMethods.All, + ConnectCallback = this.SharedHappyEyeballsCallback.ConnectCallback, + }); + } + + /// + /// Gets a meant to be shared across all (standard) requests made by the application, + /// where custom configurations are not required. + /// + /// May or may not have been properly tested by the Loporrits. + /// + public HttpClient SharedHttpClient { get; } + + /// + /// Gets a meant to be shared across any custom s that + /// need to be made in other parts of the application. + /// + /// This should be used when shared callback state is desired across multiple clients, as sharing the SocketsHandler + /// may lead to GC issues. + /// + public HappyEyeballsCallback SharedHappyEyeballsCallback { get; } + + /// + void IDisposable.Dispose() + { + this.SharedHttpClient.Dispose(); + this.SharedHappyEyeballsCallback.Dispose(); + + GC.SuppressFinalize(this); + } +} diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 3ca9a3b38..35b8bbbc7 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -10,6 +10,7 @@ using System.Reflection; using Dalamud.Configuration; using Dalamud.Configuration.Internal; using Dalamud.Data; +using Dalamud.Game; using Dalamud.Game.Gui; using Dalamud.Game.Text; using Dalamud.Game.Text.Sanitizer; @@ -32,33 +33,30 @@ namespace Dalamud.Plugin; /// public sealed class DalamudPluginInterface : IDisposable { - private readonly string pluginName; + private readonly LocalPlugin plugin; private readonly PluginConfigurations configs; /// /// Initializes a new instance of the class. /// Set up the interface and populate all fields needed. /// - /// The internal name of the plugin. - /// Location of the assembly. + /// The plugin this interface belongs to. /// The reason the plugin was loaded. - /// A value indicating whether this is a dev plugin. - /// The local manifest for this plugin. - internal DalamudPluginInterface(string pluginName, FileInfo assemblyLocation, PluginLoadReason reason, bool isDev, LocalPluginManifest manifest) + internal DalamudPluginInterface( + LocalPlugin plugin, + PluginLoadReason reason) { + this.plugin = plugin; var configuration = Service.Get(); var dataManager = Service.Get(); var localization = Service.Get(); - this.UiBuilder = new UiBuilder(pluginName); + this.UiBuilder = new UiBuilder(plugin.Name); - this.pluginName = pluginName; - this.AssemblyLocation = assemblyLocation; this.configs = Service.Get().PluginConfigs; this.Reason = reason; - this.IsDev = isDev; - this.SourceRepository = isDev ? LocalPluginManifest.FlagDevPlugin : manifest.InstalledFromUrl; - this.IsTesting = manifest.Testing; + this.SourceRepository = this.IsDev ? LocalPluginManifest.FlagDevPlugin : plugin.Manifest.InstalledFromUrl; + this.IsTesting = plugin.IsTesting; this.LoadTime = DateTime.Now; this.LoadTimeUTC = DateTime.UtcNow; @@ -88,16 +86,33 @@ public sealed class DalamudPluginInterface : IDisposable /// The new language code. public delegate void LanguageChangedDelegate(string langCode); + /// + /// Delegate for events that listen to changes to the list of active plugins. + /// + /// What action caused this event to be fired. + /// If this plugin was affected by the change. + public delegate void ActivePluginsChangedDelegate(PluginListInvalidationKind kind, bool affectedThisPlugin); + /// /// Event that gets fired when loc is changed /// public event LanguageChangedDelegate LanguageChanged; + /// + /// Event that is fired when the active list of plugins is changed. + /// + public event ActivePluginsChangedDelegate ActivePluginsChanged; + /// /// Gets the reason this plugin was loaded. /// public PluginLoadReason Reason { get; } + /// + /// Gets a value indicating whether or not auto-updates have already completed this session. + /// + public bool IsAutoUpdateComplete => Service.Get().IsAutoUpdateComplete; + /// /// Gets the repository from which this plugin was installed. /// @@ -110,12 +125,12 @@ public sealed class DalamudPluginInterface : IDisposable /// /// Gets the current internal plugin name. /// - public string InternalName => this.pluginName; - + public string InternalName => this.plugin.InternalName; + /// /// Gets a value indicating whether this is a dev plugin. /// - public bool IsDev { get; } + public bool IsDev => this.plugin.IsDev; /// /// Gets a value indicating whether this is a testing release of a plugin. @@ -148,7 +163,7 @@ public sealed class DalamudPluginInterface : IDisposable /// /// Gets the location of your plugin assembly. /// - public FileInfo AssemblyLocation { get; } + public FileInfo AssemblyLocation => this.plugin.DllFile; /// /// Gets the directory your plugin configurations are stored in. @@ -158,7 +173,7 @@ public sealed class DalamudPluginInterface : IDisposable /// /// Gets the config file of your plugin. /// - public FileInfo ConfigFile => this.configs.GetConfigFile(this.pluginName); + public FileInfo ConfigFile => this.configs.GetConfigFile(this.plugin.InternalName); /// /// Gets the instance which allows you to draw UI into the game via ImGui draw calls. @@ -193,13 +208,20 @@ public sealed class DalamudPluginInterface : IDisposable /// /// Gets a list of installed plugin names. /// + [Obsolete($"This property is obsolete. Use {nameof(InstalledPlugins)} instead.")] public List PluginNames => Service.Get().InstalledPlugins.Select(p => p.Manifest.Name).ToList(); /// /// Gets a list of installed plugin internal names. /// + [Obsolete($"This property is obsolete. Use {nameof(InstalledPlugins)} instead.")] public List PluginInternalNames => Service.Get().InstalledPlugins.Select(p => p.Manifest.InternalName).ToList(); + /// + /// Gets a list of installed plugins along with their current state. + /// + public IEnumerable InstalledPlugins => Service.Get().InstalledPlugins.Select(p => new InstalledPluginState(p.Name, p.Manifest.InternalName, p.IsLoaded, p.Manifest.EffectiveVersion)); + /// /// Opens the with the plugin name set as search target. /// @@ -213,7 +235,7 @@ public sealed class DalamudPluginInterface : IDisposable } dalamudInterface.OpenPluginInstallerPluginInstalled(); - dalamudInterface.SetPluginInstallerSearchText(this.pluginName); + dalamudInterface.SetPluginInstallerSearchText(this.plugin.InternalName); return true; } @@ -332,7 +354,7 @@ public sealed class DalamudPluginInterface : IDisposable if (currentConfig == null) return; - this.configs.Save(currentConfig, this.pluginName); + this.configs.Save(currentConfig, this.plugin.InternalName); } /// @@ -354,30 +376,32 @@ public sealed class DalamudPluginInterface : IDisposable { var mi = this.configs.GetType().GetMethod("LoadForType"); var fn = mi.MakeGenericMethod(type); - return (IPluginConfiguration)fn.Invoke(this.configs, new object[] { this.pluginName }); + return (IPluginConfiguration)fn.Invoke(this.configs, new object[] { this.plugin.InternalName }); } } // this shouldn't be a thing, I think, but just in case - return this.configs.Load(this.pluginName); + return this.configs.Load(this.plugin.InternalName); } /// /// Get the config directory. /// /// directory with path of AppData/XIVLauncher/pluginConfig/PluginInternalName. - public string GetPluginConfigDirectory() => this.configs.GetDirectory(this.pluginName); + public string GetPluginConfigDirectory() => this.configs.GetDirectory(this.plugin.InternalName); /// /// Get the loc directory. /// /// directory with path of AppData/XIVLauncher/pluginConfig/PluginInternalName/loc. - public string GetPluginLocDirectory() => this.configs.GetDirectory(Path.Combine(this.pluginName, "loc")); + public string GetPluginLocDirectory() => this.configs.GetDirectory(Path.Combine(this.plugin.InternalName, "loc")); #endregion #region Chat Links + // TODO API9: Move to chatgui, don't allow passing own commandId + /// /// Register a chat link handler. /// @@ -386,7 +410,7 @@ public sealed class DalamudPluginInterface : IDisposable /// Returns an SeString payload for the link. public DalamudLinkPayload AddChatLinkHandler(uint commandId, Action commandAction) { - return Service.Get().AddChatLinkHandler(this.pluginName, commandId, commandAction); + return Service.Get().AddChatLinkHandler(this.plugin.InternalName, commandId, commandAction); } /// @@ -395,7 +419,7 @@ public sealed class DalamudPluginInterface : IDisposable /// The ID of the command. public void RemoveChatLinkHandler(uint commandId) { - Service.Get().RemoveChatLinkHandler(this.pluginName, commandId); + Service.Get().RemoveChatLinkHandler(this.plugin.InternalName, commandId); } /// @@ -403,7 +427,7 @@ public sealed class DalamudPluginInterface : IDisposable /// public void RemoveChatLinkHandler() { - Service.Get().RemoveChatLinkHandler(this.pluginName); + Service.Get().RemoveChatLinkHandler(this.plugin.InternalName); } #endregion @@ -419,11 +443,9 @@ public sealed class DalamudPluginInterface : IDisposable { var svcContainer = Service.Get(); - var realScopedObjects = new object[scopedObjects.Length + 1]; - realScopedObjects[0] = this; - Array.Copy(scopedObjects, 0, realScopedObjects, 1, scopedObjects.Length); - - return (T)svcContainer.CreateAsync(typeof(T), realScopedObjects).GetAwaiter().GetResult(); + return (T)this.plugin.ServiceScope!.CreateAsync( + typeof(T), + this.GetPublicIocScopes(scopedObjects)).GetAwaiter().GetResult(); } /// @@ -434,13 +456,9 @@ public sealed class DalamudPluginInterface : IDisposable /// Whether or not the injection succeeded. public bool Inject(object instance, params object[] scopedObjects) { - var svcContainer = Service.Get(); - - var realScopedObjects = new object[scopedObjects.Length + 1]; - realScopedObjects[0] = this; - Array.Copy(scopedObjects, 0, realScopedObjects, 1, scopedObjects.Length); - - return svcContainer.InjectProperties(instance, realScopedObjects).GetAwaiter().GetResult(); + return this.plugin.ServiceScope!.InjectPropertiesAsync( + instance, + this.GetPublicIocScopes(scopedObjects)).GetAwaiter().GetResult(); } #endregion @@ -451,7 +469,7 @@ public sealed class DalamudPluginInterface : IDisposable void IDisposable.Dispose() { this.UiBuilder.ExplicitDispose(); - Service.Get().RemoveChatLinkHandler(this.pluginName); + Service.Get().RemoveChatLinkHandler(this.plugin.InternalName); Service.Get().LocalizationChanged -= this.OnLocalizationChanged; Service.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved; } @@ -465,6 +483,16 @@ public sealed class DalamudPluginInterface : IDisposable // ignored } + /// + /// Dispatch the active plugins changed event. + /// + /// What action caused this event to be fired. + /// If this plugin was affected by the change. + internal void NotifyActivePluginsChanged(PluginListInvalidationKind kind, bool affectedThisPlugin) + { + this.ActivePluginsChanged?.Invoke(kind, affectedThisPlugin); + } + private void OnLocalizationChanged(string langCode) { this.UiLanguage = langCode; @@ -475,4 +503,9 @@ public sealed class DalamudPluginInterface : IDisposable { this.GeneralChatType = dalamudConfiguration.GeneralChatType; } + + private object[] GetPublicIocScopes(IEnumerable scopedObjects) + { + return scopedObjects.Append(this).ToArray(); + } } diff --git a/Dalamud/Plugin/InstalledPluginState.cs b/Dalamud/Plugin/InstalledPluginState.cs new file mode 100644 index 000000000..322db3423 --- /dev/null +++ b/Dalamud/Plugin/InstalledPluginState.cs @@ -0,0 +1,5 @@ +using System; + +namespace Dalamud.Plugin; + +public record InstalledPluginState(string Name, string InternalName, bool IsLoaded, Version Version); diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 781732776..645d2975b 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -22,6 +22,7 @@ using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface.Internal; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; +using Dalamud.Networking.Http; using Dalamud.Plugin.Internal.Exceptions; using Dalamud.Plugin.Internal.Profiles; using Dalamud.Plugin.Internal.Types; @@ -41,6 +42,9 @@ namespace Dalamud.Plugin.Internal; // DalamudTextureWrap registers textures to dispose with IM [InherentDependency] +// LocalPlugin uses ServiceContainer to create scopes +[InherentDependency] + #pragma warning restore SA1015 internal partial class PluginManager : IDisposable, IServiceType { @@ -54,15 +58,6 @@ internal partial class PluginManager : IDisposable, IServiceType /// public const int PluginWaitBeforeFreeDefault = 1000; // upped from 500ms, seems more stable - private const string DevPluginsDisclaimerFilename = "DONT_USE_THIS_FOLDER.txt"; - - private const string DevPluginsDisclaimerText = @"Hey! -The devPlugins folder is deprecated and will be removed soon. Please don't use it anymore for plugin development. -Instead, open the Dalamud settings and add the path to your plugins build output folder as a dev plugin location. -Remove your devPlugin from this folder. - -Thanks and have fun!"; - private static readonly ModuleLog Log = new("PLUGINM"); private readonly object pluginListLock = new(); @@ -81,22 +76,17 @@ Thanks and have fun!"; [ServiceManager.ServiceDependency] private readonly ProfileManager profileManager = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly HappyHttpClient happyHttpClient = Service.Get(); + [ServiceManager.ServiceConstructor] private PluginManager() { this.pluginDirectory = new DirectoryInfo(this.startInfo.PluginDirectory!); - this.devPluginDirectory = new DirectoryInfo(this.startInfo.DefaultPluginDirectory!); if (!this.pluginDirectory.Exists) this.pluginDirectory.Create(); - if (!this.devPluginDirectory.Exists) - this.devPluginDirectory.Create(); - - var disclaimerFileName = Path.Combine(this.devPluginDirectory.FullName, DevPluginsDisclaimerFilename); - if (!File.Exists(disclaimerFileName)) - File.WriteAllText(disclaimerFileName, DevPluginsDisclaimerText); - this.SafeMode = EnvironmentConfiguration.DalamudNoPlugins || this.configuration.PluginSafeMode || this.startInfo.NoLoadPlugins; try @@ -391,9 +381,6 @@ Thanks and have fun!"; if (!this.pluginDirectory.Exists) this.pluginDirectory.Create(); - if (!this.devPluginDirectory.Exists) - this.devPluginDirectory.Create(); - // Add installed plugins. These are expected to be in a specific format so we can look for exactly that. foreach (var pluginDir in this.pluginDirectory.GetDirectories()) { @@ -434,7 +421,7 @@ Thanks and have fun!"; } // devPlugins are more freeform. Look for any dll and hope to get lucky. - var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories).ToList(); + var devDllFiles = new List(); foreach (var setting in this.configuration.DevPluginLoadLocations) { @@ -657,11 +644,8 @@ Thanks and have fun!"; /// public void ScanDevPlugins() { - if (!this.devPluginDirectory.Exists) - this.devPluginDirectory.Create(); - // devPlugins are more freeform. Look for any dll and hope to get lucky. - var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories).ToList(); + var devDllFiles = new List(); foreach (var setting in this.configuration.DevPluginLoadLocations) { @@ -736,7 +720,7 @@ Thanks and have fun!"; var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall; var version = useTesting ? repoManifest.TestingAssemblyVersion : repoManifest.AssemblyVersion; - var response = await Util.HttpClient.GetAsync(downloadUrl); + var response = await this.happyHttpClient.SharedHttpClient.GetAsync(downloadUrl); response.EnsureSuccessStatusCode(); var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory.FullName, repoManifest.InternalName, version?.ToString() ?? string.Empty)); @@ -1078,8 +1062,9 @@ Thanks and have fun!"; /// /// Ignore disabled plugins. /// Perform a dry run, don't install anything. + /// If this action was performed as part of an auto-update. /// Success or failure and a list of updated plugin metadata. - public async Task> UpdatePluginsAsync(bool ignoreDisabled, bool dryRun) + public async Task> UpdatePluginsAsync(bool ignoreDisabled, bool dryRun, bool autoUpdate = false) { Log.Information("Starting plugin update"); @@ -1104,6 +1089,9 @@ Thanks and have fun!"; } this.NotifyInstalledPluginsChanged(); + this.NotifyPluginsForStateChange( + autoUpdate ? PluginListInvalidationKind.AutoUpdate : PluginListInvalidationKind.Update, + updatedList.Select(x => x.InternalName)); Log.Information("Plugin update OK."); @@ -1394,6 +1382,20 @@ Thanks and have fun!"; this.OnInstalledPluginsChanged?.InvokeSafely(); } + private void NotifyPluginsForStateChange(PluginListInvalidationKind kind, IEnumerable affectedInternalNames) + { + foreach (var installedPlugin in this.InstalledPlugins) + { + if (!installedPlugin.IsLoaded || installedPlugin.DalamudInterface == null) + continue; + + installedPlugin.DalamudInterface.NotifyActivePluginsChanged( + kind, + // ReSharper disable once PossibleMultipleEnumeration + affectedInternalNames.Contains(installedPlugin.Manifest.InternalName)); + } + } + private static class Locs { public static string DalamudPluginUpdateSuccessful(string name, Version version) => Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}.").Format(name, version); diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 2ef0269a3..7811939b4 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -11,6 +11,7 @@ using Dalamud.Game.Gui.Dtr; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.IoC.Internal; +using Dalamud.Logging; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Exceptions; using Dalamud.Plugin.Internal.Loader; @@ -181,10 +182,15 @@ internal class LocalPlugin : IDisposable public AssemblyName? AssemblyName { get; private set; } /// - /// Gets the plugin name, directly from the plugin or if it is not loaded from the manifest. + /// Gets the plugin name from the manifest. /// public string Name => this.Manifest.Name; + /// + /// Gets the plugin internal name from the manifest. + /// + public string InternalName => this.Manifest.InternalName; + /// /// Gets an optional reason, if the plugin is banned. /// @@ -247,6 +253,11 @@ internal class LocalPlugin : IDisposable public bool ApplicableForLoad => !this.IsBanned && !this.IsDecommissioned && !this.IsOrphaned && !this.IsOutdated && !(!this.IsDev && this.State == PluginState.UnloadError) && this.CheckPolicy(); + /// + /// Gets the service scope for this plugin. + /// + public IServiceScope? ServiceScope { get; private set; } + /// public void Dispose() { @@ -268,6 +279,9 @@ internal class LocalPlugin : IDisposable this.DalamudInterface?.ExplicitDispose(); this.DalamudInterface = null; + this.ServiceScope?.Dispose(); + this.ServiceScope = null; + this.pluginType = null; this.pluginAssembly = null; @@ -314,8 +328,13 @@ internal class LocalPlugin : IDisposable 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"); + if (!this.IsDev) + { + throw new InvalidPluginOperationException( + $"Unable to load {this.Name}, load previously faulted, unload first"); + } + + break; case PluginState.UnloadError: if (!this.IsDev) { @@ -423,17 +442,20 @@ internal class LocalPlugin : IDisposable PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new PluginPatchData(this.DllFile); this.DalamudInterface = - new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, this.DllFile, reason, this.IsDev, this.Manifest); + new DalamudPluginInterface(this, reason); + + this.ServiceScope = ioc.GetScope(); + this.ServiceScope.RegisterPrivateScopes(this); // Add this LocalPlugin as a private scope, so services can get it if (this.Manifest.LoadSync && this.Manifest.LoadRequiredState is 0 or 1) { this.instance = await framework.RunOnFrameworkThread( - () => ioc.CreateAsync(this.pluginType!, this.DalamudInterface!)) as IDalamudPlugin; + () => this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!)) as IDalamudPlugin; } else { this.instance = - await ioc.CreateAsync(this.pluginType!, this.DalamudInterface!) as IDalamudPlugin; + await this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!) as IDalamudPlugin; } if (this.instance == null) @@ -458,7 +480,9 @@ internal class LocalPlugin : IDisposable catch (Exception ex) { this.State = PluginState.LoadError; - Log.Error(ex, $"Error while loading {this.Name}"); + + if (ex is not BannedPluginException) + Log.Error(ex, $"Error while loading {this.Name}"); throw; } @@ -479,6 +503,7 @@ internal class LocalPlugin : IDisposable { var configuration = Service.Get(); var framework = Service.GetNullable(); + var ioc = await Service.GetAsync(); await this.pluginLoadStateLock.WaitAsync(); try @@ -517,6 +542,9 @@ internal class LocalPlugin : IDisposable this.DalamudInterface?.ExplicitDispose(); this.DalamudInterface = null; + this.ServiceScope?.Dispose(); + this.ServiceScope = null; + this.pluginType = null; this.pluginAssembly = null; diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs index e0373ff33..25189aadd 100644 --- a/Dalamud/Plugin/Internal/Types/PluginRepository.cs +++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs @@ -3,11 +3,13 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using Dalamud.Logging.Internal; +using Dalamud.Networking.Http; using Newtonsoft.Json; namespace Dalamud.Plugin.Internal.Types; @@ -24,7 +26,11 @@ internal class PluginRepository private static readonly ModuleLog Log = new("PLUGINR"); - private static readonly HttpClient HttpClient = new() + private static readonly HttpClient HttpClient = new(new SocketsHttpHandler + { + AutomaticDecompression = DecompressionMethods.All, + ConnectCallback = Service.Get().SharedHappyEyeballsCallback.ConnectCallback, + }) { Timeout = TimeSpan.FromSeconds(20), DefaultRequestHeaders = diff --git a/Dalamud/Plugin/PluginListInvalidationKind.cs b/Dalamud/Plugin/PluginListInvalidationKind.cs new file mode 100644 index 000000000..4e7782703 --- /dev/null +++ b/Dalamud/Plugin/PluginListInvalidationKind.cs @@ -0,0 +1,17 @@ +namespace Dalamud.Plugin; + +/// +/// Causes for a change to the plugin list. +/// +public enum PluginListInvalidationKind +{ + /// + /// An installer-initiated update reloaded plugins. + /// + Update, + + /// + /// An auto-update reloaded plugins. + /// + AutoUpdate, +} diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index f237d8e57..c13d39f4c 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -65,6 +65,11 @@ internal static class ServiceManager /// BlockingEarlyLoadedService = 1 << 2, + /// + /// Service that is only instantiable via scopes. + /// + ScopedService = 1 << 3, + /// /// Service that is loaded automatically when the game starts, synchronously or asynchronously. /// @@ -133,10 +138,12 @@ internal static class ServiceManager foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) { var serviceKind = serviceType.GetServiceKind(); - if (serviceKind == ServiceKind.None) + if (serviceKind is ServiceKind.None or ServiceKind.ScopedService) continue; - Debug.Assert(!serviceKind.HasFlag(ServiceKind.ManualService), "Regular services should never end up here"); + Debug.Assert( + !serviceKind.HasFlag(ServiceKind.ManualService) && !serviceKind.HasFlag(ServiceKind.ScopedService), + "Regular and scoped services should never be loaded early"); var genericWrappedServiceType = typeof(Service<>).MakeGenericType(serviceType); @@ -302,6 +309,13 @@ internal static class ServiceManager if (!serviceType.IsAssignableTo(typeof(IServiceType))) continue; + // Scoped services shall never be unloaded here. + // Their lifetime must be managed by the IServiceScope that owns them. If it leaks, it's their fault. + if (serviceType.GetServiceKind() == ServiceKind.ScopedService) + continue; + + Log.Verbose("Calling GetDependencyServices for '{ServiceName}'", serviceType.FullName!); + dependencyServicesMap[serviceType] = ((List)typeof(Service<>) .MakeGenericType(serviceType) @@ -389,6 +403,9 @@ internal static class ServiceManager if (attr.IsAssignableTo(typeof(EarlyLoadedService))) return ServiceKind.EarlyLoadedService; + + if (attr.IsAssignableTo(typeof(ScopedService))) + return ServiceKind.ScopedService; return ServiceKind.ManualService; } @@ -435,6 +452,15 @@ internal static class ServiceManager { } + /// + /// Indicates that the class is a service that will be created specifically for a + /// service scope, and that it cannot be created outside of a scope. + /// + [AttributeUsage(AttributeTargets.Class)] + public class ScopedService : Service + { + } + /// /// Indicates that the method should be called when the services given in the constructor are ready. /// diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index 0e7d75369..a1a56a9f0 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -154,6 +154,10 @@ internal static class Service where T : IServiceType if (attr == null) continue; + // Scoped plugin services lifetime is tied to their scopes. They go away when LocalPlugin goes away. + if (serviceType.GetServiceKind() == ServiceManager.ServiceKind.ScopedService) + continue; + ServiceManager.Log.Verbose("PluginManager MUST depend on {Type}", serviceType.FullName!); res.Add(serviceType); } diff --git a/Dalamud/Support/BugBait.cs b/Dalamud/Support/BugBait.cs index 084c49d9d..ce74c03ec 100644 --- a/Dalamud/Support/BugBait.cs +++ b/Dalamud/Support/BugBait.cs @@ -1,7 +1,7 @@ using System.Net.Http; using System.Text; using System.Threading.Tasks; - +using Dalamud.Networking.Http; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using Newtonsoft.Json; @@ -42,9 +42,11 @@ internal static class BugBait { model.Exception = Troubleshooting.LastException == null ? "Was included, but none happened" : Troubleshooting.LastException?.ToString(); } + + var httpClient = Service.Get().SharedHttpClient; var postContent = new StringContent(JsonConvert.SerializeObject(model), Encoding.UTF8, "application/json"); - var response = await Util.HttpClient.PostAsync(BugBaitUrl, postContent); + var response = await httpClient.PostAsync(BugBaitUrl, postContent); response.EnsureSuccessStatusCode(); } diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs index 922fc5e50..ef1897eeb 100644 --- a/Dalamud/Support/Troubleshooting.cs +++ b/Dalamud/Support/Troubleshooting.cs @@ -68,6 +68,8 @@ public static class Troubleshooting var payload = new TroubleshootingPayload { LoadedPlugins = pluginManager?.InstalledPlugins?.Select(x => x.Manifest)?.OrderByDescending(x => x.InternalName).ToArray(), + PluginStates = pluginManager?.InstalledPlugins?.Where(x => !x.IsDev).ToDictionary(x => x.Manifest.InternalName, x => x.IsBanned ? "Banned" : x.State.ToString()), + EverStartedLoadingPlugins = pluginManager?.InstalledPlugins.Where(x => x.HasEverStartedLoad).Select(x => x.InternalName).ToList(), DalamudVersion = Util.AssemblyVersion, DalamudGitHash = Util.GetGitHash(), GameVersion = startInfo.GameVersion.ToString(), @@ -100,7 +102,11 @@ public static class Troubleshooting private class TroubleshootingPayload { - public LocalPluginManifest[] LoadedPlugins { get; set; } + public LocalPluginManifest[]? LoadedPlugins { get; set; } + + public Dictionary? PluginStates { get; set; } + + public List? EverStartedLoadingPlugins { get; set; } public string DalamudVersion { get; set; } diff --git a/Dalamud/Utility/AsyncUtils.cs b/Dalamud/Utility/AsyncUtils.cs new file mode 100644 index 000000000..d252bd5d5 --- /dev/null +++ b/Dalamud/Utility/AsyncUtils.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Dalamud.Utility; + +/// +/// A set of utilities around and for better asynchronous behavior. +/// +public static class AsyncUtils +{ + /// + /// Race a set of tasks, returning either the first to succeed or an aggregate of all exceptions. This helper does + /// not perform any automatic cancellation of losing tasks, nor does it handle exceptions of losing tasks. + /// + /// Derived from this StackOverflow post. + /// A list of tasks to race. + /// The return type of all raced tasks. + /// Thrown when all tasks given to this method fail. + /// Returns the first task that completes, according to . + public static Task FirstSuccessfulTask(ICollection> tasks) + { + var tcs = new TaskCompletionSource(); + var remainingTasks = tasks.Count; + + foreach (var task in tasks) + { + task.ContinueWith(t => + { + if (t.IsCompletedSuccessfully) + { + tcs.TrySetResult(t.Result); + } + else if (Interlocked.Decrement(ref remainingTasks) == 0) + { + tcs.SetException(new AggregateException(tasks.SelectMany(f => f.Exception?.InnerExceptions))); + } + }); + } + + return tcs.Task; + } + + /// + /// Provide a that won't throw an exception when it's canceled. + /// + /// + public static async Task CancellableDelay(int millisecondsDelay, CancellationToken cancellationToken) + { + try + { + await Task.Delay(millisecondsDelay, cancellationToken); + } + catch (TaskCanceledException) + { + } + } +} diff --git a/Dalamud/Utility/Signatures/SignatureHelper.cs b/Dalamud/Utility/Signatures/SignatureHelper.cs index b58cec9d8..bd99b8515 100755 --- a/Dalamud/Utility/Signatures/SignatureHelper.cs +++ b/Dalamud/Utility/Signatures/SignatureHelper.cs @@ -7,6 +7,7 @@ using Dalamud.Game; using Dalamud.Hooking; using Dalamud.Logging; using Dalamud.Utility.Signatures.Wrappers; +using Serilog; namespace Dalamud.Utility.Signatures; @@ -151,7 +152,7 @@ public static class SignatureHelper var ctor = actualType.GetConstructor(new[] { typeof(IntPtr), hookDelegateType }); if (ctor == null) { - PluginLog.Error("Error in SignatureHelper: could not find Hook constructor"); + Log.Error("Error in SignatureHelper: could not find Hook constructor"); continue; } diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index e04be3334..4b8fe6822 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -17,6 +17,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Logging.Internal; +using Dalamud.Networking.Http; using ImGuiNET; using Lumina.Excel.GeneratedSheets; using Microsoft.Win32; @@ -40,7 +41,8 @@ public static class Util /// Gets an httpclient for usage. /// Do NOT await this. /// - public static HttpClient HttpClient { get; } = new(); + [Obsolete($"Use Service<{nameof(HappyHttpClient)}> instead.")] + public static HttpClient HttpClient { get; } = Service.Get().SharedHttpClient; /// /// Gets the assembly version of Dalamud. @@ -556,6 +558,56 @@ public static class Util Process.Start(process); } + /// + /// Perform a "zipper merge" (A, 1, B, 2, C, 3) of multiple enumerables, allowing for lists to end early. + /// + /// A set of enumerable sources to combine. + /// The resulting type of the merged list to return. + /// A new enumerable, consisting of the final merge of all lists. + public static IEnumerable ZipperMerge(params IEnumerable[] sources) + { + // Borrowed from https://codereview.stackexchange.com/a/263451, thank you! + var enumerators = new IEnumerator[sources.Length]; + try + { + for (var i = 0; i < sources.Length; i++) + { + enumerators[i] = sources[i].GetEnumerator(); + } + + var hasNext = new bool[enumerators.Length]; + + bool MoveNext() + { + var anyHasNext = false; + for (var i = 0; i < enumerators.Length; i++) + { + anyHasNext |= hasNext[i] = enumerators[i].MoveNext(); + } + + return anyHasNext; + } + + while (MoveNext()) + { + for (var i = 0; i < enumerators.Length; i++) + { + if (hasNext[i]) + { + yield return enumerators[i].Current; + } + } + } + } + finally + { + foreach (var enumerator in enumerators) + { + enumerator?.Dispose(); + } + } + } + /// /// Dispose this object. /// diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index a00585972..8e25367e5 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit a00585972e44ccc6799091f8f53020b7f8c16f1d +Subproject commit 8e25367e5f7d3acbe8e2b2a81121852b1ea1291c