mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-30 20:33:40 +01:00
merge master into profiles
This commit is contained in:
commit
fe6196d0ad
57 changed files with 1118 additions and 287 deletions
7
.github/workflows/main.yml
vendored
7
.github/workflows/main.yml
vendored
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@
|
|||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Use</PrecompiledHeader>
|
||||
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">26812</DisableSpecificWarnings>
|
||||
<BuildStlModules Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">false</BuildStlModules>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<EnableCOMDATFolding>false</EnableCOMDATFolding>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Lumina" Version="3.10.2" />
|
||||
<PackageReference Include="Lumina.Excel" Version="6.3.2" />
|
||||
<PackageReference Include="Lumina.Excel" Version="6.4.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ namespace Dalamud.CorePlugin
|
|||
/// Initializes a new instance of the <see cref="PluginImpl"/> class.
|
||||
/// </summary>
|
||||
/// <param name="pluginInterface">Dalamud plugin interface.</param>
|
||||
public PluginImpl(DalamudPluginInterface pluginInterface)
|
||||
public PluginImpl(DalamudPluginInterface pluginInterface, PluginLog log)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -68,7 +68,7 @@ namespace Dalamud.CorePlugin
|
|||
|
||||
Service<CommandManager>.Get().AddHandler("/coreplug", new(this.OnCommand) { HelpMessage = $"Access the {this.Name} plugin." });
|
||||
|
||||
PluginLog.Information("CorePlugin ctor!");
|
||||
log.Information("CorePlugin ctor!");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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<string>() { x };
|
||||
}
|
||||
|
||||
var checksum = checksumTable.IndexOf(x[x.Length - 5]);
|
||||
if (checksum == -1)
|
||||
{
|
||||
return new List<string>() { 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<Dictionary<string, int>>(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
|
|||
/// </param>
|
||||
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
|
||||
|
|
|
|||
|
|
@ -153,6 +153,11 @@ internal sealed class DalamudConfiguration : IServiceType
|
|||
/// </summary>
|
||||
public bool ToggleUiHideDuringGpose { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public bool PrintDalamudWelcomeMsg { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not a message containing detailed plugin information should be sent at login.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Feature">
|
||||
<DalamudVersion>7.5.0.2</DalamudVersion>
|
||||
<DalamudVersion>7.6.0.0</DalamudVersion>
|
||||
<Description>XIV Launcher addon framework</Description>
|
||||
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
|
||||
<Version>$(DalamudVersion)</Version>
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
<PackageReference Include="goaaats.Reloaded.Assembler" Version="1.0.14-goat.2" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2021.2.0" />
|
||||
<PackageReference Include="Lumina" Version="3.10.2" />
|
||||
<PackageReference Include="Lumina.Excel" Version="6.3.2" />
|
||||
<PackageReference Include="Lumina.Excel" Version="6.4.0" />
|
||||
<PackageReference Include="MinSharp" Version="1.0.4" />
|
||||
<PackageReference Include="MonoModReorg.RuntimeDetour" Version="23.1.2-prerelease.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/// </summary>
|
||||
public string? PluginDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to the directory for developer plugins.
|
||||
/// </summary>
|
||||
public string? DefaultPluginDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the path to core Dalamud assets.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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<PluginManager>.GetNullable();
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ public class ChatHandlers : IServiceType
|
|||
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
|
||||
|
||||
private bool hasSeenLoadingMsg;
|
||||
private bool hasAutoUpdatedPlugins;
|
||||
private bool startedAutoUpdatingPlugins;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private ChatHandlers(ChatGui chatGui)
|
||||
|
|
@ -129,6 +129,11 @@ public class ChatHandlers : IServiceType
|
|||
/// </summary>
|
||||
public string? LastLink { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not auto-updates have already completed this session.
|
||||
/// </summary>
|
||||
public bool IsAutoUpdateComplete { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Convert a TextPayload to SeString and wrap in italics payloads.
|
||||
/// </summary>
|
||||
|
|
@ -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."));
|
||||
|
|
|
|||
|
|
@ -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<ClientState>.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<UpdateAetheryteListDelegate>(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);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the amount of Aetherytes the local player has unlocked.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Aetheryte Entry at the specified index.
|
||||
/// </summary>
|
||||
/// <param name="index">Index.</param>
|
||||
/// <returns>A <see cref="AetheryteEntry"/> at the specified index.</returns>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,11 +62,6 @@ public sealed class ClientStateAddressResolver : BaseAddressResolver
|
|||
/// </summary>
|
||||
public IntPtr ConditionFlags { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the Telepo instance.
|
||||
/// </summary>
|
||||
public IntPtr Telepo { get; private set; }
|
||||
|
||||
// Functions
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -80,11 +75,6 @@ public sealed class ClientStateAddressResolver : BaseAddressResolver
|
|||
/// </summary>
|
||||
public IntPtr GamepadPoll { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the method which updates the list of available teleport locations.
|
||||
/// </summary>
|
||||
public IntPtr UpdateAetheryteList { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Scan for and setup any configured address pointers.
|
||||
/// </summary>
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public sealed partial class Condition : IServiceType
|
|||
/// <summary>
|
||||
/// The current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has.
|
||||
/// </summary>
|
||||
public const int MaxConditionEntries = 100;
|
||||
public const int MaxConditionEntries = 104;
|
||||
|
||||
private readonly bool[] cache = new bool[MaxConditionEntries];
|
||||
|
||||
|
|
|
|||
|
|
@ -464,4 +464,14 @@ public enum ConditionFlag
|
|||
/// Unable to execute command while recruiting for a non-cross-world party.
|
||||
/// </summary>
|
||||
RecruitingWorldOnly = 98,
|
||||
|
||||
/// <summary>
|
||||
/// Command unavailable in this location.
|
||||
/// </summary>
|
||||
Unknown99 = 99,
|
||||
|
||||
/// <summary>
|
||||
/// Unable to execute command while editing a portrait.
|
||||
/// </summary>
|
||||
EditingPortrait = 100,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@ public enum BattleNpcSubKind : byte
|
|||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
///<summary>
|
||||
/// 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)
|
||||
/// </summary>
|
||||
BattleNpcPart = 1,
|
||||
|
||||
/// <summary>
|
||||
/// BattleNpc representing a Pet.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -102,7 +102,15 @@ public unsafe class Character : GameObject
|
|||
/// <summary>
|
||||
/// Gets the status flags.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying structure.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SetGlobalBgmDelegate> setGlobalBgmHook;
|
||||
private readonly Hook<HandleItemHoverDelegate> handleItemHoverHook;
|
||||
|
|
@ -66,9 +70,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType
|
|||
this.handleImmHook = Hook<HandleImmDelegate>.FromAddress(this.address.HandleImm, this.HandleImmDetour);
|
||||
|
||||
this.getMatrixSingleton = Marshal.GetDelegateForFunctionPointer<GetMatrixSingletonDelegate>(this.address.GetMatrixSingleton);
|
||||
|
||||
this.screenToWorldNative = Marshal.GetDelegateForFunctionPointer<ScreenToWorldNativeDelegate>(this.address.ScreenToWorld);
|
||||
|
||||
|
||||
this.toggleUiHideHook = Hook<ToggleUiHideDelegate>.FromAddress(this.address.ToggleUiHide, this.ToggleUiHideDetour);
|
||||
|
||||
this.utf8StringFromSequenceHook = Hook<Utf8StringFromSequenceDelegate>.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.
|
||||
/// </summary>
|
||||
/// <returns>IntPtr pointing to UI module.</returns>
|
||||
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
|
|||
/// <param name="name">Name of addon to find.</param>
|
||||
/// <param name="index">Index of addon to find (1-indexed).</param>
|
||||
/// <returns>IntPtr.Zero if unable to find UI, otherwise IntPtr pointing to the start of the addon.</returns>
|
||||
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
|
|||
/// <returns>A pointer to the agent interface.</returns>
|
||||
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
|
|||
/// </summary>
|
||||
/// <param name="addon">The addon address.</param>
|
||||
/// <returns>A pointer to the agent interface.</returns>
|
||||
public unsafe IntPtr FindAgentInterface(void* addon) => this.FindAgentInterface((IntPtr)addon);
|
||||
public IntPtr FindAgentInterface(void* addon) => this.FindAgentInterface((IntPtr)addon);
|
||||
|
||||
/// <summary>
|
||||
/// Find the agent associated with an addon, if possible.
|
||||
|
|
@ -430,9 +407,9 @@ public sealed unsafe class GameGui : IDisposable, IServiceType
|
|||
/// <returns>A value indicating whether or not the game is on the title screen.</returns>
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -47,11 +47,6 @@ internal sealed class GameGuiAddressResolver : BaseAddressResolver
|
|||
/// </summary>
|
||||
public IntPtr GetMatrixSingleton { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the native ScreenToWorld method.
|
||||
/// </summary>
|
||||
public IntPtr ScreenToWorld { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the address of the native ToggleUiHide method.
|
||||
/// </summary>
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<UiModuleRequestMainCommand>.FromAddress(uiModuleRequestMainCommandAddress, this.UiModuleRequestMainCommandDetour);
|
||||
|
||||
var atkUnitBaseReceiveGlobalEventAddress = sigScanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 50 44 0F B7 F2 ");
|
||||
var atkUnitBaseReceiveGlobalEventAddress = sigScanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 50 44 0F B7 F2");
|
||||
this.hookAtkUnitBaseReceiveGlobalEvent = Hook<AtkUnitBaseReceiveGlobalEvent>.FromAddress(atkUnitBaseReceiveGlobalEventAddress, this.AtkUnitBaseReceiveGlobalEventDetour);
|
||||
|
||||
this.locDalamudPlugins = Loc.Localize("SystemMenuPlugins", "Dalamud Plugins");
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ public sealed class LibcFunctionAddressResolver : BaseAddressResolver
|
|||
/// <inheritdoc/>
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ?? ?? ?? ??");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HappyHttpClient>.Get().SharedHttpClient;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UniversalisMarketBoardUploader"/> class.
|
||||
/// </summary>
|
||||
|
|
@ -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);
|
||||
|
||||
// ====================================================================================
|
||||
|
||||
|
|
|
|||
54
Dalamud/Interface/ImGuiFileDialog/DriveListLoader.cs
Normal file
54
Dalamud/Interface/ImGuiFileDialog/DriveListLoader.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Dalamud.Interface.ImGuiFileDialog;
|
||||
|
||||
/// <summary>
|
||||
/// A drive list loader. Thread-safety guaranteed.
|
||||
/// </summary>
|
||||
internal class DriveListLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DriveListLoader"/> class.
|
||||
/// </summary>
|
||||
public DriveListLoader()
|
||||
{
|
||||
this.Drives = Array.Empty<DriveInfo>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the drive list. This may be incomplete if the loader is still loading.
|
||||
/// </summary>
|
||||
public IReadOnlyList<DriveInfo> Drives { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not the loader is loading.
|
||||
/// </summary>
|
||||
public bool Loading { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Loads the drive list, asynchronously.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -12,11 +12,13 @@ public partial class FileDialog
|
|||
{
|
||||
private readonly object filesLock = new();
|
||||
|
||||
private readonly DriveListLoader driveListLoader = new();
|
||||
|
||||
private List<FileStruct> files = new();
|
||||
private List<FileStruct> 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<SideBarItem> 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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ public partial class FileDialog
|
|||
private float footerHeight = 0;
|
||||
|
||||
private string selectedSideBar = string.Empty;
|
||||
private List<SideBarItem> drives = new();
|
||||
private List<SideBarItem> quickAccess = new();
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<DispatchMessageWDelegate>.FromImport(
|
||||
null, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour);
|
||||
this.setCursorHook = Hook<SetCursorDelegate>.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<PresentDelegate>.FromAddress(this.address.Present, this.PresentDetour);
|
||||
this.resizeBuffersHook = Hook<ResizeBuffersDelegate>.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<ProcessMessageDelegate>.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<PresentDelegate>.FromAddress(this.address.Present, this.PresentDetour);
|
||||
this.resizeBuffersHook = Hook<ResizeBuffersDelegate>.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour);
|
||||
|
||||
Log.Verbose("===== S W A P C H A I N =====");
|
||||
Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}");
|
||||
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<ProcessMessageDelegate>.FromAddress(wndProcAddress, this.ProcessMessageDetour);
|
||||
|
||||
this.setCursorHook.Enable();
|
||||
this.presentHook.Enable();
|
||||
this.resizeBuffersHook.Enable();
|
||||
this.dispatchMessageWHook.Enable();
|
||||
this.processMessageHook.Enable();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/// </summary>
|
||||
public UiDebug()
|
||||
{
|
||||
var sigScanner = Service<SigScanner>.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<GetAtkStageSingleton>(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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<HappyHttpClient>.Get().SharedHttpClient;
|
||||
this.branches = await client.GetFromJsonAsync<Dictionary<string, VersionEntry>>(BranchInfoUrl);
|
||||
Debug.Assert(this.branches != null, "this.branches != null");
|
||||
|
||||
|
|
|
|||
|
|
@ -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<InterfaceManager.InterfaceManagerWithScene>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly HappyHttpClient happyHttpClient = Service<HappyHttpClient>.Get();
|
||||
|
||||
private readonly BlockingCollection<Tuple<ulong, Func<Task>>> downloadQueue = new();
|
||||
private readonly BlockingCollection<Func<Task>> loadQueue = new();
|
||||
private readonly CancellationTokenSource cancelToken = new();
|
||||
|
|
@ -535,7 +539,7 @@ internal class PluginImageCache : IDisposable, IServiceType
|
|||
var bytes = await this.RunInDownloadQueue<byte[]?>(
|
||||
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<byte[]?>(
|
||||
async () =>
|
||||
{
|
||||
var data = await Util.HttpClient.GetAsync(url);
|
||||
var httpClient = Service<HappyHttpClient>.Get().SharedHttpClient;
|
||||
|
||||
var data = await httpClient.GetAsync(url);
|
||||
if (data.StatusCode == HttpStatusCode.NotFound)
|
||||
return null;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public async Task ReloadChangelogAsync()
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
var client = Service<HappyHttpClient>.Get().SharedHttpClient;
|
||||
this.Changelogs = null;
|
||||
|
||||
var dalamudChangelogs = await client.GetFromJsonAsync<List<DalamudChangelog>>(DalamudChangelogUrl);
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,12 @@ public class SettingsTabGeneral : SettingsTab
|
|||
c => c.DutyFinderChatMessage,
|
||||
(v, c) => c.DutyFinderChatMessage = v),
|
||||
|
||||
new SettingsEntry<bool>(
|
||||
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<bool>(
|
||||
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."),
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|||
/// </summary>
|
||||
/// <param name="objectType">The type of object to create.</param>
|
||||
/// <param name="scopedObjects">Scoped objects to be included in the constructor.</param>
|
||||
/// <param name="scope">The scope to be used to create scoped services.</param>
|
||||
/// <returns>The created object.</returns>
|
||||
public async Task<object?> CreateAsync(Type objectType, params object[] scopedObjects)
|
||||
public async Task<object?> 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<ServiceManager.ScopedService>() != 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 <see cref="RequiredVersionAttribute"/> to lock down versions.
|
||||
/// </summary>
|
||||
/// <param name="instance">The object instance.</param>
|
||||
/// <param name="scopedObjects">Scoped objects.</param>
|
||||
/// <param name="publicScopes">Scoped objects to be injected.</param>
|
||||
/// <param name="scope">The scope to be used to create scoped services.</param>
|
||||
/// <returns>Whether or not the injection was successful.</returns>
|
||||
public async Task<bool> InjectProperties(object instance, params object[] scopedObjects)
|
||||
public async Task<bool> 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<ServiceManager.ScopedService>() != 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a service scope, enabling the creation of objects with scoped services.
|
||||
/// </summary>
|
||||
/// <returns>An implementation of a service scope.</returns>
|
||||
public IServiceScope GetScope() => new ServiceScopeImpl(this);
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<ServiceManager.ScopedService>() == null)
|
||||
{
|
||||
Log.Error("Failed to validate {TypeName}, unable to find any services that satisfy the type", parameter.ParameterType.FullName!);
|
||||
return false;
|
||||
|
|
|
|||
101
Dalamud/IoC/Internal/ServiceScope.cs
Normal file
101
Dalamud/IoC/Internal/ServiceScope.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Dalamud.IoC.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Container enabling the creation of scoped services.
|
||||
/// </summary>
|
||||
internal interface IServiceScope : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Register objects that may be injected to scoped services,
|
||||
/// but not directly to created objects.
|
||||
/// </summary>
|
||||
/// <param name="scopes">The scopes to add.</param>
|
||||
public void RegisterPrivateScopes(params object[] scopes);
|
||||
|
||||
/// <summary>
|
||||
/// Create an object.
|
||||
/// </summary>
|
||||
/// <param name="objectType">The type of object to create.</param>
|
||||
/// <param name="scopedObjects">Scoped objects to be included in the constructor.</param>
|
||||
/// <returns>The created object.</returns>
|
||||
public Task<object?> CreateAsync(Type objectType, params object[] scopedObjects);
|
||||
|
||||
/// <summary>
|
||||
/// Inject <see cref="PluginInterfaceAttribute" /> interfaces into public or static properties on the provided object.
|
||||
/// The properties have to be marked with the <see cref="PluginServiceAttribute" />.
|
||||
/// The properties can be marked with the <see cref="RequiredVersionAttribute" /> to lock down versions.
|
||||
/// </summary>
|
||||
/// <param name="instance">The object instance.</param>
|
||||
/// <param name="scopedObjects">Scoped objects to be injected.</param>
|
||||
/// <returns>Whether or not the injection was successful.</returns>
|
||||
public Task<bool> InjectPropertiesAsync(object instance, params object[] scopedObjects);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of a service scope.
|
||||
/// </summary>
|
||||
internal class ServiceScopeImpl : IServiceScope
|
||||
{
|
||||
private readonly ServiceContainer container;
|
||||
|
||||
private readonly List<object> privateScopedObjects = new();
|
||||
private readonly List<object> scopeCreatedObjects = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServiceScopeImpl" /> class.
|
||||
/// </summary>
|
||||
/// <param name="container">The container this scope will use to create services.</param>
|
||||
public ServiceScopeImpl(ServiceContainer container)
|
||||
{
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RegisterPrivateScopes(params object[] scopes)
|
||||
{
|
||||
this.privateScopedObjects.AddRange(scopes);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<object?> CreateAsync(Type objectType, params object[] scopedObjects)
|
||||
{
|
||||
return this.container.CreateAsync(objectType, scopedObjects, this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<bool> InjectPropertiesAsync(object instance, params object[] scopedObjects)
|
||||
{
|
||||
return this.container.InjectProperties(instance, scopedObjects, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a service scoped to this scope, with private scoped objects.
|
||||
/// </summary>
|
||||
/// <param name="objectType">The type of object to create.</param>
|
||||
/// <param name="scopedObjects">Additional scoped objects.</param>
|
||||
/// <returns>The created object, or null.</returns>
|
||||
public async Task<object?> 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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var createdObject in this.scopeCreatedObjects.OfType<IDisposable>()) createdObject.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
|||
/// <summary>
|
||||
/// Class offering various static methods to allow for logging in plugins.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginLog"/> class.
|
||||
/// Do not use this ctor, inject PluginLog instead.
|
||||
/// </summary>
|
||||
/// <param name="plugin">The plugin this service is scoped for.</param>
|
||||
internal PluginLog(LocalPlugin plugin)
|
||||
{
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a prefix appended to log messages.
|
||||
/// </summary>
|
||||
public string? LogPrefix { get; set; } = null;
|
||||
|
||||
#region Legacy static "Log" prefixed Serilog style methods
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
|
||||
/// <summary>
|
||||
/// Log some information.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
internal void Information(string message)
|
||||
{
|
||||
Serilog.Log.Information($"[{this.plugin.InternalName}] {this.LogPrefix} {message}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class offering logging services, for a specific type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to log for.</typeparam>
|
||||
[PluginInterface]
|
||||
[InterfaceVersion("1.0")]
|
||||
[ServiceManager.ScopedService]
|
||||
public class PluginLog<T> : PluginLog
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginLog{T}"/> class.
|
||||
/// Do not use this ctor, inject PluginLog instead.
|
||||
/// </summary>
|
||||
/// <param name="plugin">The plugin this service is scoped for.</param>
|
||||
internal PluginLog(LocalPlugin plugin)
|
||||
: base(plugin)
|
||||
{
|
||||
this.LogPrefix = typeof(T).Name;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
143
Dalamud/Networking/Http/HappyEyeballsCallback.cs
Normal file
143
Dalamud/Networking/Http/HappyEyeballsCallback.cs
Normal file
|
|
@ -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
|
||||
|
||||
/// <summary>
|
||||
/// A class to provide a <see cref="SocketsHttpHandler.ConnectCallback"/> method to implement a variant of the Happy
|
||||
/// Eyeballs algorithm for HTTP connections to dual-stack servers.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HappyEyeballsCallback"/> class.
|
||||
/// </summary>
|
||||
/// <param name="forcedAddressFamily">Optional override to force a specific AddressFamily.</param>
|
||||
/// <param name="connectionBackoff">Backoff time between concurrent connection attempts.</param>
|
||||
public HappyEyeballsCallback(AddressFamily? forcedAddressFamily = null, int connectionBackoff = 75)
|
||||
{
|
||||
this.forcedAddressFamily = forcedAddressFamily ?? AddressFamily.Unspecified;
|
||||
this.connectionBackoff = connectionBackoff;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The connection callback to provide to a <see cref="SocketsHttpHandler"/>.
|
||||
/// </summary>
|
||||
/// <param name="context">The context for an HTTP connection.</param>
|
||||
/// <param name="token">The cancellation token to abort this request.</param>
|
||||
/// <returns>Returns a Stream for consumption by HttpClient.</returns>
|
||||
public async ValueTask<Stream> ConnectCallback(SocketsHttpConnectionContext context, CancellationToken token)
|
||||
{
|
||||
var sortedRecords = await this.GetSortedAddresses(context.DnsEndPoint.Host, token);
|
||||
|
||||
var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(token);
|
||||
var tasks = new List<Task<NetworkStream>>();
|
||||
|
||||
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<NetworkStream> AttemptConnection(IPAddress address, int port, CancellationToken token, CancellationToken delayToken)
|
||||
{
|
||||
await AsyncUtils.CancellableDelay(-1, delayToken).ConfigureAwait(false);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
return Task.FromCanceled<NetworkStream>(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<List<IPAddress>> 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?");
|
||||
}
|
||||
}
|
||||
}
|
||||
56
Dalamud/Networking/Http/HappyHttpClient.cs
Normal file
56
Dalamud/Networking/Http/HappyHttpClient.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Dalamud.Networking.Http;
|
||||
|
||||
/// <summary>
|
||||
/// A service to help build and manage HttpClients with some semblance of Happy Eyeballs (RFC 8305 - IPv4 fallback)
|
||||
/// awareness.
|
||||
/// </summary>
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal class HappyHttpClient : IDisposable, IServiceType
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HappyHttpClient"/> class.
|
||||
///
|
||||
/// A service to talk to the Smileton Loporrits to build an HTTP Client aware of Happy Eyeballs.
|
||||
/// </summary>
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private HappyHttpClient()
|
||||
{
|
||||
this.SharedHappyEyeballsCallback = new HappyEyeballsCallback();
|
||||
|
||||
this.SharedHttpClient = new HttpClient(new SocketsHttpHandler
|
||||
{
|
||||
AutomaticDecompression = DecompressionMethods.All,
|
||||
ConnectCallback = this.SharedHappyEyeballsCallback.ConnectCallback,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="HttpClient"/> 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.
|
||||
/// </summary>
|
||||
public HttpClient SharedHttpClient { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="HappyEyeballsCallback"/> meant to be shared across any custom <see cref="HttpClient"/>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.
|
||||
/// </summary>
|
||||
public HappyEyeballsCallback SharedHappyEyeballsCallback { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
this.SharedHttpClient.Dispose();
|
||||
this.SharedHappyEyeballsCallback.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
|||
/// </summary>
|
||||
public sealed class DalamudPluginInterface : IDisposable
|
||||
{
|
||||
private readonly string pluginName;
|
||||
private readonly LocalPlugin plugin;
|
||||
private readonly PluginConfigurations configs;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DalamudPluginInterface"/> class.
|
||||
/// Set up the interface and populate all fields needed.
|
||||
/// </summary>
|
||||
/// <param name="pluginName">The internal name of the plugin.</param>
|
||||
/// <param name="assemblyLocation">Location of the assembly.</param>
|
||||
/// <param name="plugin">The plugin this interface belongs to.</param>
|
||||
/// <param name="reason">The reason the plugin was loaded.</param>
|
||||
/// <param name="isDev">A value indicating whether this is a dev plugin.</param>
|
||||
/// <param name="manifest">The local manifest for this plugin.</param>
|
||||
internal DalamudPluginInterface(string pluginName, FileInfo assemblyLocation, PluginLoadReason reason, bool isDev, LocalPluginManifest manifest)
|
||||
internal DalamudPluginInterface(
|
||||
LocalPlugin plugin,
|
||||
PluginLoadReason reason)
|
||||
{
|
||||
this.plugin = plugin;
|
||||
var configuration = Service<DalamudConfiguration>.Get();
|
||||
var dataManager = Service<DataManager>.Get();
|
||||
var localization = Service<Localization>.Get();
|
||||
|
||||
this.UiBuilder = new UiBuilder(pluginName);
|
||||
this.UiBuilder = new UiBuilder(plugin.Name);
|
||||
|
||||
this.pluginName = pluginName;
|
||||
this.AssemblyLocation = assemblyLocation;
|
||||
this.configs = Service<PluginManager>.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
|
|||
/// <param name="langCode">The new language code.</param>
|
||||
public delegate void LanguageChangedDelegate(string langCode);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for events that listen to changes to the list of active plugins.
|
||||
/// </summary>
|
||||
/// <param name="kind">What action caused this event to be fired.</param>
|
||||
/// <param name="affectedThisPlugin">If this plugin was affected by the change.</param>
|
||||
public delegate void ActivePluginsChangedDelegate(PluginListInvalidationKind kind, bool affectedThisPlugin);
|
||||
|
||||
/// <summary>
|
||||
/// Event that gets fired when loc is changed
|
||||
/// </summary>
|
||||
public event LanguageChangedDelegate LanguageChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Event that is fired when the active list of plugins is changed.
|
||||
/// </summary>
|
||||
public event ActivePluginsChangedDelegate ActivePluginsChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reason this plugin was loaded.
|
||||
/// </summary>
|
||||
public PluginLoadReason Reason { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not auto-updates have already completed this session.
|
||||
/// </summary>
|
||||
public bool IsAutoUpdateComplete => Service<ChatHandlers>.Get().IsAutoUpdateComplete;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the repository from which this plugin was installed.
|
||||
///
|
||||
|
|
@ -110,12 +125,12 @@ public sealed class DalamudPluginInterface : IDisposable
|
|||
/// <summary>
|
||||
/// Gets the current internal plugin name.
|
||||
/// </summary>
|
||||
public string InternalName => this.pluginName;
|
||||
|
||||
public string InternalName => this.plugin.InternalName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this is a dev plugin.
|
||||
/// </summary>
|
||||
public bool IsDev { get; }
|
||||
public bool IsDev => this.plugin.IsDev;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this is a testing release of a plugin.
|
||||
|
|
@ -148,7 +163,7 @@ public sealed class DalamudPluginInterface : IDisposable
|
|||
/// <summary>
|
||||
/// Gets the location of your plugin assembly.
|
||||
/// </summary>
|
||||
public FileInfo AssemblyLocation { get; }
|
||||
public FileInfo AssemblyLocation => this.plugin.DllFile;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the directory your plugin configurations are stored in.
|
||||
|
|
@ -158,7 +173,7 @@ public sealed class DalamudPluginInterface : IDisposable
|
|||
/// <summary>
|
||||
/// Gets the config file of your plugin.
|
||||
/// </summary>
|
||||
public FileInfo ConfigFile => this.configs.GetConfigFile(this.pluginName);
|
||||
public FileInfo ConfigFile => this.configs.GetConfigFile(this.plugin.InternalName);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="UiBuilder"/> instance which allows you to draw UI into the game via ImGui draw calls.
|
||||
|
|
@ -193,13 +208,20 @@ public sealed class DalamudPluginInterface : IDisposable
|
|||
/// <summary>
|
||||
/// Gets a list of installed plugin names.
|
||||
/// </summary>
|
||||
[Obsolete($"This property is obsolete. Use {nameof(InstalledPlugins)} instead.")]
|
||||
public List<string> PluginNames => Service<PluginManager>.Get().InstalledPlugins.Select(p => p.Manifest.Name).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of installed plugin internal names.
|
||||
/// </summary>
|
||||
[Obsolete($"This property is obsolete. Use {nameof(InstalledPlugins)} instead.")]
|
||||
public List<string> PluginInternalNames => Service<PluginManager>.Get().InstalledPlugins.Select(p => p.Manifest.InternalName).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of installed plugins along with their current state.
|
||||
/// </summary>
|
||||
public IEnumerable<InstalledPluginState> InstalledPlugins => Service<PluginManager>.Get().InstalledPlugins.Select(p => new InstalledPluginState(p.Name, p.Manifest.InternalName, p.IsLoaded, p.Manifest.EffectiveVersion));
|
||||
|
||||
/// <summary>
|
||||
/// Opens the <see cref="PluginInstallerWindow"/> with the plugin name set as search target.
|
||||
/// </summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the config directory.
|
||||
/// </summary>
|
||||
/// <returns>directory with path of AppData/XIVLauncher/pluginConfig/PluginInternalName.</returns>
|
||||
public string GetPluginConfigDirectory() => this.configs.GetDirectory(this.pluginName);
|
||||
public string GetPluginConfigDirectory() => this.configs.GetDirectory(this.plugin.InternalName);
|
||||
|
||||
/// <summary>
|
||||
/// Get the loc directory.
|
||||
/// </summary>
|
||||
/// <returns>directory with path of AppData/XIVLauncher/pluginConfig/PluginInternalName/loc.</returns>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Register a chat link handler.
|
||||
/// </summary>
|
||||
|
|
@ -386,7 +410,7 @@ public sealed class DalamudPluginInterface : IDisposable
|
|||
/// <returns>Returns an SeString payload for the link.</returns>
|
||||
public DalamudLinkPayload AddChatLinkHandler(uint commandId, Action<uint, SeString> commandAction)
|
||||
{
|
||||
return Service<ChatGui>.Get().AddChatLinkHandler(this.pluginName, commandId, commandAction);
|
||||
return Service<ChatGui>.Get().AddChatLinkHandler(this.plugin.InternalName, commandId, commandAction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -395,7 +419,7 @@ public sealed class DalamudPluginInterface : IDisposable
|
|||
/// <param name="commandId">The ID of the command.</param>
|
||||
public void RemoveChatLinkHandler(uint commandId)
|
||||
{
|
||||
Service<ChatGui>.Get().RemoveChatLinkHandler(this.pluginName, commandId);
|
||||
Service<ChatGui>.Get().RemoveChatLinkHandler(this.plugin.InternalName, commandId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -403,7 +427,7 @@ public sealed class DalamudPluginInterface : IDisposable
|
|||
/// </summary>
|
||||
public void RemoveChatLinkHandler()
|
||||
{
|
||||
Service<ChatGui>.Get().RemoveChatLinkHandler(this.pluginName);
|
||||
Service<ChatGui>.Get().RemoveChatLinkHandler(this.plugin.InternalName);
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
|
@ -419,11 +443,9 @@ public sealed class DalamudPluginInterface : IDisposable
|
|||
{
|
||||
var svcContainer = Service<IoC.Internal.ServiceContainer>.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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -434,13 +456,9 @@ public sealed class DalamudPluginInterface : IDisposable
|
|||
/// <returns>Whether or not the injection succeeded.</returns>
|
||||
public bool Inject(object instance, params object[] scopedObjects)
|
||||
{
|
||||
var svcContainer = Service<IoC.Internal.ServiceContainer>.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<ChatGui>.Get().RemoveChatLinkHandler(this.pluginName);
|
||||
Service<ChatGui>.Get().RemoveChatLinkHandler(this.plugin.InternalName);
|
||||
Service<Localization>.Get().LocalizationChanged -= this.OnLocalizationChanged;
|
||||
Service<DalamudConfiguration>.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved;
|
||||
}
|
||||
|
|
@ -465,6 +483,16 @@ public sealed class DalamudPluginInterface : IDisposable
|
|||
// ignored
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatch the active plugins changed event.
|
||||
/// </summary>
|
||||
/// <param name="kind">What action caused this event to be fired.</param>
|
||||
/// <param name="affectedThisPlugin">If this plugin was affected by the change.</param>
|
||||
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<object> scopedObjects)
|
||||
{
|
||||
return scopedObjects.Append(this).ToArray();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
Dalamud/Plugin/InstalledPluginState.cs
Normal file
5
Dalamud/Plugin/InstalledPluginState.cs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
using System;
|
||||
|
||||
namespace Dalamud.Plugin;
|
||||
|
||||
public record InstalledPluginState(string Name, string InternalName, bool IsLoaded, Version Version);
|
||||
|
|
@ -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<InterfaceManager>]
|
||||
|
||||
// LocalPlugin uses ServiceContainer to create scopes
|
||||
[InherentDependency<ServiceContainer>]
|
||||
|
||||
#pragma warning restore SA1015
|
||||
internal partial class PluginManager : IDisposable, IServiceType
|
||||
{
|
||||
|
|
@ -54,15 +58,6 @@ internal partial class PluginManager : IDisposable, IServiceType
|
|||
/// </summary>
|
||||
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<ProfileManager>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly HappyHttpClient happyHttpClient = Service<HappyHttpClient>.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<FileInfo>();
|
||||
|
||||
foreach (var setting in this.configuration.DevPluginLoadLocations)
|
||||
{
|
||||
|
|
@ -657,11 +644,8 @@ Thanks and have fun!";
|
|||
/// </summary>
|
||||
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<FileInfo>();
|
||||
|
||||
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!";
|
|||
/// </summary>
|
||||
/// <param name="ignoreDisabled">Ignore disabled plugins.</param>
|
||||
/// <param name="dryRun">Perform a dry run, don't install anything.</param>
|
||||
/// <param name="autoUpdate">If this action was performed as part of an auto-update.</param>
|
||||
/// <returns>Success or failure and a list of updated plugin metadata.</returns>
|
||||
public async Task<List<PluginUpdateStatus>> UpdatePluginsAsync(bool ignoreDisabled, bool dryRun)
|
||||
public async Task<List<PluginUpdateStatus>> 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<string> 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);
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin name, directly from the plugin or if it is not loaded from the manifest.
|
||||
/// Gets the plugin name from the manifest.
|
||||
/// </summary>
|
||||
public string Name => this.Manifest.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin internal name from the manifest.
|
||||
/// </summary>
|
||||
public string InternalName => this.Manifest.InternalName;
|
||||
|
||||
/// <summary>
|
||||
/// Gets an optional reason, if the plugin is banned.
|
||||
/// </summary>
|
||||
|
|
@ -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();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service scope for this plugin.
|
||||
/// </summary>
|
||||
public IServiceScope? ServiceScope { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<DalamudConfiguration>.Get();
|
||||
var framework = Service<Framework>.GetNullable();
|
||||
var ioc = await Service<ServiceContainer>.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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<HappyHttpClient>.Get().SharedHappyEyeballsCallback.ConnectCallback,
|
||||
})
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(20),
|
||||
DefaultRequestHeaders =
|
||||
|
|
|
|||
17
Dalamud/Plugin/PluginListInvalidationKind.cs
Normal file
17
Dalamud/Plugin/PluginListInvalidationKind.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
namespace Dalamud.Plugin;
|
||||
|
||||
/// <summary>
|
||||
/// Causes for a change to the plugin list.
|
||||
/// </summary>
|
||||
public enum PluginListInvalidationKind
|
||||
{
|
||||
/// <summary>
|
||||
/// An installer-initiated update reloaded plugins.
|
||||
/// </summary>
|
||||
Update,
|
||||
|
||||
/// <summary>
|
||||
/// An auto-update reloaded plugins.
|
||||
/// </summary>
|
||||
AutoUpdate,
|
||||
}
|
||||
|
|
@ -65,6 +65,11 @@ internal static class ServiceManager
|
|||
/// </summary>
|
||||
BlockingEarlyLoadedService = 1 << 2,
|
||||
|
||||
/// <summary>
|
||||
/// Service that is only instantiable via scopes.
|
||||
/// </summary>
|
||||
ScopedService = 1 << 3,
|
||||
|
||||
/// <summary>
|
||||
/// Service that is loaded automatically when the game starts, synchronously or asynchronously.
|
||||
/// </summary>
|
||||
|
|
@ -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<Type>)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
|
|||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class ScopedService : Service
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the method should be called when the services given in the constructor are ready.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -154,6 +154,10 @@ internal static class Service<T> 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HappyHttpClient>.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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, string>? PluginStates { get; set; }
|
||||
|
||||
public List<string>? EverStartedLoadingPlugins { get; set; }
|
||||
|
||||
public string DalamudVersion { get; set; }
|
||||
|
||||
|
|
|
|||
60
Dalamud/Utility/AsyncUtils.cs
Normal file
60
Dalamud/Utility/AsyncUtils.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Dalamud.Utility;
|
||||
|
||||
/// <summary>
|
||||
/// A set of utilities around and for better asynchronous behavior.
|
||||
/// </summary>
|
||||
public static class AsyncUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>Derived from <a href="https://stackoverflow.com/a/37529395">this StackOverflow post</a>.</remarks>
|
||||
/// <param name="tasks">A list of tasks to race.</param>
|
||||
/// <typeparam name="T">The return type of all raced tasks.</typeparam>
|
||||
/// <exception cref="AggregateException">Thrown when all tasks given to this method fail.</exception>
|
||||
/// <returns>Returns the first task that completes, according to <see cref="Task{TResult}.IsCompletedSuccessfully"/>.</returns>
|
||||
public static Task<T> FirstSuccessfulTask<T>(ICollection<Task<T>> tasks)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<T>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provide a <see cref="Task.Delay(int, CancellationToken)"/> that won't throw an exception when it's canceled.
|
||||
/// </summary>
|
||||
/// <inheritdoc cref="Task.Delay(int, CancellationToken)"/>
|
||||
public static async Task CancellableDelay(int millisecondsDelay, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(millisecondsDelay, cancellationToken);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/// </summary>
|
||||
public static HttpClient HttpClient { get; } = new();
|
||||
[Obsolete($"Use Service<{nameof(HappyHttpClient)}> instead.")]
|
||||
public static HttpClient HttpClient { get; } = Service<HappyHttpClient>.Get().SharedHttpClient;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assembly version of Dalamud.
|
||||
|
|
@ -556,6 +558,56 @@ public static class Util
|
|||
Process.Start(process);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Perform a "zipper merge" (A, 1, B, 2, C, 3) of multiple enumerables, allowing for lists to end early.
|
||||
/// </summary>
|
||||
/// <param name="sources">A set of enumerable sources to combine.</param>
|
||||
/// <typeparam name="TSource">The resulting type of the merged list to return.</typeparam>
|
||||
/// <returns>A new enumerable, consisting of the final merge of all lists.</returns>
|
||||
public static IEnumerable<TSource> ZipperMerge<TSource>(params IEnumerable<TSource>[] sources)
|
||||
{
|
||||
// Borrowed from https://codereview.stackexchange.com/a/263451, thank you!
|
||||
var enumerators = new IEnumerator<TSource>[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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose this object.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit a00585972e44ccc6799091f8f53020b7f8c16f1d
|
||||
Subproject commit 8e25367e5f7d3acbe8e2b2a81121852b1ea1291c
|
||||
Loading…
Add table
Add a link
Reference in a new issue