Merge branch 'master' into dragdropb

This commit is contained in:
Ottermandias 2023-06-13 20:59:05 +02:00
commit bd8da4bebf
72 changed files with 2683 additions and 637 deletions

View file

@ -140,3 +140,8 @@ indent_style = space
indent_size = 4
tab_width = 4
dotnet_style_parentheses_in_other_operators=always_for_clarity:silent
[*.{yaml,yml}]
indent_style = space
indent_size = 2
tab_width = 2

1
.github/CODEOWNERS vendored Normal file
View file

@ -0,0 +1 @@
* @goatcorp/dalamud-maintainers

View file

@ -1,5 +1,10 @@
name: Tag Build
on: [push]
on:
push:
branches:
- master
tags-ignore:
- '*' # don't needlessly execute on tags
jobs:
tag:

View file

@ -6,12 +6,12 @@
using System.Diagnostics.CodeAnalysis;
// General
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "Preventing long lines", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "I like regions", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1123:Do not place regions within elements", Justification = "I like regions in elements too", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1127:Generic type constraints should be on their own line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "Preventing long lines")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "I like regions")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1123:Do not place regions within elements", Justification = "I like regions in elements too")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1127:Generic type constraints should be on their own line", Justification = "I like this better")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "I like this better")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")]

View file

@ -54,6 +54,7 @@ namespace Dalamud.CorePlugin
/// Initializes a new instance of the <see cref="PluginImpl"/> class.
/// </summary>
/// <param name="pluginInterface">Dalamud plugin interface.</param>
/// <param name="log">Logging service.</param>
public PluginImpl(DalamudPluginInterface pluginInterface, PluginLog log)
{
try

View file

@ -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);
}
@ -658,10 +739,12 @@ namespace Dalamud.Injector
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
[System.Runtime.InteropServices.DllImport("c")]
static extern ulong clock_gettime_nsec_np(int clock_id);
#pragma warning disable SA1300
static extern ulong clock_gettime_nsec_np(int clockId);
#pragma warning restore SA1300
const int CLOCK_MONOTONIC_RAW = 4;
var rawTickCountFixed = (clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) / 1000000);
var rawTickCountFixed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) / 1000000;
Log.Information("ArgumentBuilder::DeriveKey() fixing up rawTickCount from {0} to {1} on macOS", rawTickCount, rawTickCountFixed);
rawTickCount = (uint)rawTickCountFixed;
}
@ -683,21 +766,27 @@ namespace Dalamud.Injector
gameArgumentString = string.Join(" ", gameArguments.Select(x => EncodeParameterArgument(x)));
}
var process = GameStart.LaunchGame(Path.GetDirectoryName(gamePath), gamePath, gameArgumentString, noFixAcl, (Process p) =>
{
if (!withoutDalamud && mode == "entrypoint")
var process = GameStart.LaunchGame(
Path.GetDirectoryName(gamePath),
gamePath,
gameArgumentString,
noFixAcl,
p =>
{
var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath);
Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo));
if (RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo)) != 0)
if (!withoutDalamud && mode == "entrypoint")
{
Log.Error("[HOOKS] RewriteRemoteEntryPointW failed");
throw new Exception("RewriteRemoteEntryPointW failed");
}
var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath);
Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo));
if (RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo)) != 0)
{
Log.Error("[HOOKS] RewriteRemoteEntryPointW failed");
throw new Exception("RewriteRemoteEntryPointW failed");
}
Log.Verbose("RewriteRemoteEntryPointW called!");
}
}, waitForGameWindow);
Log.Verbose("RewriteRemoteEntryPointW called!");
}
},
waitForGameWindow);
Log.Verbose("Game process started with PID {0}", process.Id);
@ -712,7 +801,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 +846,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 +905,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 +942,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

View file

@ -211,6 +211,9 @@ namespace Dalamud.Injector
}
}
/// <summary>
/// Claim a SE Debug Privilege.
/// </summary>
public static void ClaimSeDebug()
{
var hToken = PInvoke.INVALID_HANDLE_VALUE;
@ -345,8 +348,6 @@ namespace Dalamud.Injector
private static class PInvoke
{
#region Constants
public static readonly IntPtr INVALID_HANDLE_VALUE = new(-1);
public const string SE_DEBUG_NAME = "SeDebugPrivilege";
public const UInt32 STANDARD_RIGHTS_ALL = 0x001F0000;
@ -369,6 +370,8 @@ namespace Dalamud.Injector
public const UInt32 ERROR_NO_TOKEN = 0x000003F0;
public static readonly IntPtr INVALID_HANDLE_VALUE = new(-1);
public enum MULTIPLE_TRUSTEE_OPERATION
{
NO_MULTIPLE_TRUSTEE,
@ -431,7 +434,7 @@ namespace Dalamud.Injector
SecurityAnonymous,
SecurityIdentification,
SecurityImpersonation,
SecurityDelegation
SecurityDelegation,
}
#endregion
@ -485,8 +488,7 @@ namespace Dalamud.Injector
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool ImpersonateSelf(
SECURITY_IMPERSONATION_LEVEL impersonationLevel
);
SECURITY_IMPERSONATION_LEVEL impersonationLevel);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool OpenProcessToken(
@ -496,10 +498,10 @@ namespace Dalamud.Injector
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool OpenThreadToken(
IntPtr ThreadHandle,
uint DesiredAccess,
bool OpenAsSelf,
out IntPtr TokenHandle);
IntPtr threadHandle,
uint desiredAccess,
bool openAsSelf,
out IntPtr tokenHandle);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, ref LUID lpLuid);

View file

@ -7,7 +7,7 @@ using System.Diagnostics.CodeAnalysis;
// General
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "I'll make what I want static", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better")]
[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "I'll make what I want static")]

View file

@ -1,8 +1,16 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Dalamud.Injector
{
[SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "Legacy code")]
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1413:Use trailing comma in multi-line initializers", Justification = "Legacy code")]
[SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1519:Braces should not be omitted from multi-line child statement", Justification = "Legacy code")]
[SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1414:Tuple types in signatures should have element names", Justification = "Legacy code")]
[SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "Legacy code")]
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Legacy code")]
[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204:Static elements should appear before instance elements", Justification = "Legacy code")]
internal class LegacyBlowfish
{
#region P-Array and S-Boxes
@ -203,10 +211,9 @@ namespace Dalamud.Injector
private static readonly int Rounds = 16;
/// <summary>
/// Initialize a new blowfish.
/// Initializes a new instance of the <see cref="LegacyBlowfish"/> class.
/// </summary>
/// <param name="key">The key to use.</param>
/// <param name="fucked">Whether or not a sign confusion should be introduced during key init. This is needed for SE's implementation of blowfish.</param>
public LegacyBlowfish(byte[] key)
{
foreach (var (i, keyFragment) in WrappingUInt32(key, this.p.Length))
@ -306,7 +313,9 @@ namespace Dalamud.Injector
for (var j = 0; j < 4 && enumerator.MoveNext(); j++)
{
#pragma warning disable CS0675
n = (uint)((n << 8) | (sbyte)enumerator.Current); // NOTE(goat): THIS IS A BUG! SE's implementation wrongly uses signed numbers for this, so we need to as well.
#pragma warning restore CS0675
}
yield return (i, n);

View file

@ -6,6 +6,7 @@ using System.Linq;
using Dalamud.Game.Text;
using Dalamud.Interface.Style;
using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Utility;
using Newtonsoft.Json;
using Serilog;
@ -271,6 +272,21 @@ internal sealed class DalamudConfiguration : IServiceType
/// </summary>
public string ChosenStyle { get; set; } = "Dalamud Standard";
/// <summary>
/// Gets or sets a list of saved plugin profiles.
/// </summary>
public List<ProfileModel>? SavedProfiles { get; set; }
/// <summary>
/// Gets or sets the default plugin profile.
/// </summary>
public ProfileModel? DefaultProfile { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not profiles are enabled.
/// </summary>
public bool ProfilesEnabled { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether or not Dalamud RMT filtering should be disabled.
/// </summary>

View file

@ -1,7 +1,9 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@ -98,6 +100,19 @@ internal sealed class Dalamud : IServiceType
/// Gets location of stored assets.
/// </summary>
internal DirectoryInfo AssetDirectory => new(Service<DalamudStartInfo>.Get().AssetDirectory!);
/// <summary>
/// Signal to the crash handler process that we should restart the game.
/// </summary>
public static void RestartGame()
{
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern void RaiseException(uint dwExceptionCode, uint dwExceptionFlags, uint nNumberOfArguments, IntPtr lpArguments);
RaiseException(0x12345678, 0, 0, IntPtr.Zero);
Process.GetCurrentProcess().Kill();
}
/// <summary>
/// Queue an unload of Dalamud when it gets the chance.

View file

@ -8,7 +8,7 @@
</PropertyGroup>
<PropertyGroup Label="Feature">
<DalamudVersion>7.5.1.0</DalamudVersion>
<DalamudVersion>7.6.5.0</DalamudVersion>
<Description>XIV Launcher addon framework</Description>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>

View file

@ -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>

View file

@ -68,7 +68,7 @@ public sealed class EntryPoint
{
try
{
return Marshal.StringToHGlobalUni(Environment.StackTrace);
return Marshal.StringToHGlobalUni(new StackTrace(1).ToString());
}
catch (Exception e)
{

View file

@ -248,7 +248,8 @@ public class ChatHandlers : IServiceType
var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString();
if (this.configuration.PrintDalamudWelcomeMsg) {
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)));
}

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Serilog;
@ -14,7 +15,8 @@ namespace Dalamud.Game.ClientState.Aetherytes;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed unsafe partial class AetheryteList : IServiceType
[ResolveVia<IAetheryteList>]
public sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList
{
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get();
@ -27,9 +29,7 @@ public sealed unsafe partial class AetheryteList : IServiceType
Log.Verbose($"Teleport address 0x{((nint)this.telepoInstance).ToInt64():X}");
}
/// <summary>
/// Gets the amount of Aetherytes the local player has unlocked.
/// </summary>
/// <inheritdoc/>
public int Length
{
get
@ -46,11 +46,7 @@ public sealed unsafe partial class AetheryteList : IServiceType
}
}
/// <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>
/// <inheritdoc/>
public AetheryteEntry? this[int index]
{
get
@ -80,7 +76,7 @@ public sealed unsafe partial class AetheryteList : IServiceType
/// <summary>
/// This collection represents the list of available Aetherytes in the Teleport window.
/// </summary>
public sealed partial class AetheryteList : IReadOnlyCollection<AetheryteEntry>
public sealed partial class AetheryteList
{
/// <inheritdoc/>
public int Count => this.Length;

View file

@ -10,9 +10,9 @@ public enum BattleNpcSubKind : byte
/// </summary>
None = 0,
///<summary>
/// <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)
/// Eg: Titan's Heart (Naval), Tioman's left and right wing (Sohm Al), Golem Soulstone (The Sunken Temple of Qarn).
/// </summary>
BattleNpcPart = 1,

View file

@ -22,42 +22,42 @@ public unsafe class BattleChara : Character
/// <summary>
/// Gets the current status effects.
/// </summary>
public StatusList StatusList => new(&this.Struct->StatusManager);
public StatusList StatusList => new(this.Struct->GetStatusManager);
/// <summary>
/// Gets a value indicating whether the chara is currently casting.
/// </summary>
public bool IsCasting => this.Struct->SpellCastInfo.IsCasting > 0;
public bool IsCasting => this.Struct->GetCastInfo->IsCasting > 0;
/// <summary>
/// Gets a value indicating whether the cast is interruptible.
/// </summary>
public bool IsCastInterruptible => this.Struct->SpellCastInfo.Interruptible > 0;
public bool IsCastInterruptible => this.Struct->GetCastInfo->Interruptible > 0;
/// <summary>
/// Gets the spell action type of the spell being cast by the actor.
/// </summary>
public byte CastActionType => (byte)this.Struct->SpellCastInfo.ActionType;
public byte CastActionType => (byte)this.Struct->GetCastInfo->ActionType;
/// <summary>
/// Gets the spell action ID of the spell being cast by the actor.
/// </summary>
public uint CastActionId => this.Struct->SpellCastInfo.ActionID;
public uint CastActionId => this.Struct->GetCastInfo->ActionID;
/// <summary>
/// Gets the object ID of the target currently being cast at by the chara.
/// </summary>
public uint CastTargetObjectId => this.Struct->SpellCastInfo.CastTargetID;
public uint CastTargetObjectId => this.Struct->GetCastInfo->CastTargetID;
/// <summary>
/// Gets the current casting time of the spell being cast by the chara.
/// </summary>
public float CurrentCastTime => this.Struct->SpellCastInfo.CurrentCastTime;
public float CurrentCastTime => this.Struct->GetCastInfo->CurrentCastTime;
/// <summary>
/// Gets the total casting time of the spell being cast by the chara.
/// </summary>
public float TotalCastTime => this.Struct->SpellCastInfo.TotalCastTime;
public float TotalCastTime => this.Struct->GetCastInfo->TotalCastTime;
/// <summary>
/// Gets the underlying structure.

View file

@ -77,7 +77,7 @@ public unsafe class Character : GameObject
/// Gets a byte array describing the visual appearance of this Chara.
/// Indexed by <see cref="CustomizeIndex"/>.
/// </summary>
public byte[] Customize => MemoryHelper.Read<byte>((IntPtr)this.Struct->CustomizeData, 28);
public byte[] Customize => MemoryHelper.Read<byte>((IntPtr)this.Struct->DrawData.CustomizeData.Data, 28);
/// <summary>
/// Gets the Free Company tag of this chara.

View file

@ -67,7 +67,8 @@ public class GameConfigSection
/// <param name="name">Name of the config option.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public unsafe bool TryGetBool(string name, out bool value) {
public unsafe bool TryGetBool(string name, out bool value)
{
value = false;
if (!this.TryGetIndex(name, out var index))
{
@ -97,7 +98,8 @@ public class GameConfigSection
/// <param name="name">Name of the config option.</param>
/// <returns>Value of the config option.</returns>
/// <exception cref="ConfigOptionNotFoundException">Thrown if the config option is not found.</exception>
public bool GetBool(string name) {
public bool GetBool(string name)
{
if (!this.TryGetBool(name, out var value))
{
throw new ConfigOptionNotFoundException(this.SectionName, name);
@ -114,7 +116,8 @@ public class GameConfigSection
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
public unsafe void Set(string name, bool value) {
public unsafe void Set(string name, bool value)
{
if (!this.TryGetIndex(name, out var index))
{
throw new ConfigOptionNotFoundException(this.SectionName, name);
@ -139,7 +142,8 @@ public class GameConfigSection
/// <param name="name">Name of the config option.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public unsafe bool TryGetUInt(string name, out uint value) {
public unsafe bool TryGetUInt(string name, out uint value)
{
value = 0;
if (!this.TryGetIndex(name, out var index))
{
@ -169,7 +173,8 @@ public class GameConfigSection
/// <param name="name">Name of the config option.</param>
/// <returns>Value of the config option.</returns>
/// <exception cref="ConfigOptionNotFoundException">Thrown if the config option is not found.</exception>
public uint GetUInt(string name) {
public uint GetUInt(string name)
{
if (!this.TryGetUInt(name, out var value))
{
throw new ConfigOptionNotFoundException(this.SectionName, name);
@ -186,8 +191,10 @@ public class GameConfigSection
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
public unsafe void Set(string name, uint value) {
this.framework.RunOnFrameworkThread(() => {
public unsafe void Set(string name, uint value)
{
this.framework.RunOnFrameworkThread(() =>
{
if (!this.TryGetIndex(name, out var index))
{
throw new ConfigOptionNotFoundException(this.SectionName, name);
@ -213,7 +220,8 @@ public class GameConfigSection
/// <param name="name">Name of the config option.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public unsafe bool TryGetFloat(string name, out float value) {
public unsafe bool TryGetFloat(string name, out float value)
{
value = 0;
if (!this.TryGetIndex(name, out var index))
{
@ -243,7 +251,8 @@ public class GameConfigSection
/// <param name="name">Name of the config option.</param>
/// <returns>Value of the config option.</returns>
/// <exception cref="ConfigOptionNotFoundException">Thrown if the config option is not found.</exception>
public float GetFloat(string name) {
public float GetFloat(string name)
{
if (!this.TryGetFloat(name, out var value))
{
throw new ConfigOptionNotFoundException(this.SectionName, name);
@ -260,8 +269,10 @@ public class GameConfigSection
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
public unsafe void Set(string name, float value) {
this.framework.RunOnFrameworkThread(() => {
public unsafe void Set(string name, float value)
{
this.framework.RunOnFrameworkThread(() =>
{
if (!this.TryGetIndex(name, out var index))
{
throw new ConfigOptionNotFoundException(this.SectionName, name);
@ -287,7 +298,8 @@ public class GameConfigSection
/// <param name="name">Name of the config option.</param>
/// <param name="value">The returned value of the config option.</param>
/// <returns>A value representing the success.</returns>
public unsafe bool TryGetString(string name, out string value) {
public unsafe bool TryGetString(string name, out string value)
{
value = string.Empty;
if (!this.TryGetIndex(name, out var index))
{
@ -327,7 +339,8 @@ public class GameConfigSection
/// <param name="name">Name of the config option.</param>
/// <returns>Value of the config option.</returns>
/// <exception cref="ConfigOptionNotFoundException">Thrown if the config option is not found.</exception>
public string GetString(string name) {
public string GetString(string name)
{
if (!this.TryGetString(name, out var value))
{
throw new ConfigOptionNotFoundException(this.SectionName, name);
@ -344,8 +357,10 @@ public class GameConfigSection
/// <param name="value">New value of the config option.</param>
/// <exception cref="ConfigOptionNotFoundException">Throw if the config option is not found.</exception>
/// <exception cref="UnreachableException">Thrown if the name of the config option is found, but the struct was not.</exception>
public unsafe void Set(string name, string value) {
this.framework.RunOnFrameworkThread(() => {
public unsafe void Set(string name, string value)
{
this.framework.RunOnFrameworkThread(() =>
{
if (!this.TryGetIndex(name, out var index))
{
throw new ConfigOptionNotFoundException(this.SectionName, name);
@ -365,7 +380,8 @@ public class GameConfigSection
});
}
private unsafe bool TryGetIndex(string name, out uint index) {
private unsafe bool TryGetIndex(string name, out uint index)
{
if (this.indexMap.TryGetValue(name, out index))
{
return true;
@ -373,14 +389,16 @@ public class GameConfigSection
var configBase = this.GetConfigBase();
var e = configBase->ConfigEntry;
for (var i = 0U; i < configBase->ConfigCount; i++, e++) {
for (var i = 0U; i < configBase->ConfigCount; i++, e++)
{
if (e->Name == null)
{
continue;
}
var eName = MemoryHelper.ReadStringNullTerminated(new IntPtr(e->Name));
if (eName.Equals(name)) {
if (eName.Equals(name))
{
this.indexMap.TryAdd(name, i);
this.nameMap.TryAdd(i, name);
index = i;
@ -392,7 +410,8 @@ public class GameConfigSection
return false;
}
private unsafe bool TryGetEntry(uint index, out ConfigEntry* entry) {
private unsafe bool TryGetEntry(uint index, out ConfigEntry* entry)
{
entry = null;
var configBase = this.GetConfigBase();
if (configBase->ConfigEntry == null || index >= configBase->ConfigCount)

View file

@ -25,25 +25,25 @@ namespace Dalamud.Game;
[ServiceManager.BlockingEarlyLoadedService]
public sealed class Framework : IDisposable, IServiceType
{
private static readonly Stopwatch StatsStopwatch = new();
private readonly GameLifecycle lifecycle;
private static Stopwatch statsStopwatch = new();
private readonly Stopwatch updateStopwatch = new();
private readonly HitchDetector hitchDetector;
private readonly Hook<OnUpdateDetour> updateHook;
private readonly Hook<OnRealDestroyDelegate> destroyHook;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private readonly object runOnNextTickTaskListSync = new();
private List<RunOnNextTickTaskBase> runOnNextTickTaskList = new();
private List<RunOnNextTickTaskBase> runOnNextTickTaskList2 = new();
private Thread? frameworkUpdateThread;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
[ServiceManager.ServiceConstructor]
private Framework(SigScanner sigScanner, GameLifecycle lifecycle)
{
@ -346,7 +346,7 @@ public sealed class Framework : IDisposable, IServiceType
this.destroyHook.Dispose();
this.updateStopwatch.Reset();
statsStopwatch.Reset();
StatsStopwatch.Reset();
}
[ServiceManager.CallWhenServicesReady]
@ -420,11 +420,11 @@ public sealed class Framework : IDisposable, IServiceType
if (StatsEnabled)
{
statsStopwatch.Restart();
StatsStopwatch.Restart();
this.RunPendingTickTasks();
statsStopwatch.Stop();
StatsStopwatch.Stop();
AddToStats(nameof(this.RunPendingTickTasks), statsStopwatch.Elapsed.TotalMilliseconds);
AddToStats(nameof(this.RunPendingTickTasks), StatsStopwatch.Elapsed.TotalMilliseconds);
}
else
{
@ -440,7 +440,7 @@ public sealed class Framework : IDisposable, IServiceType
// Individually invoke OnUpdate handlers and time them.
foreach (var d in invokeList)
{
statsStopwatch.Restart();
StatsStopwatch.Restart();
try
{
d.Method.Invoke(d.Target, new object[] { this });
@ -450,13 +450,13 @@ public sealed class Framework : IDisposable, IServiceType
Log.Error(ex, "Exception while dispatching Framework::Update event.");
}
statsStopwatch.Stop();
StatsStopwatch.Stop();
var key = $"{d.Target}::{d.Method.Name}";
if (notUpdated.Contains(key))
notUpdated.Remove(key);
AddToStats(key, statsStopwatch.Elapsed.TotalMilliseconds);
AddToStats(key, StatsStopwatch.Elapsed.TotalMilliseconds);
}
// Cleanup handlers that are no longer being called

View file

@ -1,13 +1,12 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Dalamud.Configuration.Internal;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Utility;
using Serilog;
using Serilog.Core;
namespace Dalamud.Game.Network;
@ -26,11 +25,11 @@ public sealed class GameNetwork : IDisposable, IServiceType
private readonly HitchDetector hitchDetectorUp;
private readonly HitchDetector hitchDetectorDown;
private IntPtr baseAddress;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private IntPtr baseAddress;
[ServiceManager.ServiceConstructor]
private GameNetwork(SigScanner sigScanner)
{

View file

@ -459,4 +459,259 @@ public enum BitmapFontIcon : uint
/// The Island Sanctuary icon.
/// </summary>
IslandSanctuary = 116,
/// <summary>
/// The Physical Damage icon.
/// </Summary>
DamagePhysical = 117,
/// <summary>
/// The Magical Damage icon.
/// </Summary>
DamageMagical = 118,
/// <summary>
/// The Special Damage icon.
/// </Summary>
DamageSpecial = 119,
/// <summary>
/// A gold star icon with an orange exclamation mark.
/// </Summary>
GoldStarProblem = 120,
/// <summary>
/// A blue star icon.
/// </Summary>
BlueStar = 121,
/// <summary>
/// A blue star icon with an orange exclamation mark.
/// </Summary>
BlueStarProblem = 121,
/// <summary>
/// The Disconnecting icon.
/// </Summary>
Disconnecting = 124,
/// <summary>
/// The Do Not Disturb icon.
/// </Summary>
DoNotDisturb = 125,
/// <summary>
/// The Meteor icon.
/// </Summary>
Meteor = 126,
/// <summary>
/// The Role Playing icon.
/// </Summary>
RolePlaying = 127,
/// <summary>
/// The Gladiator icon.
/// </Summary>
Gladiator = 128,
/// <summary>
/// The Pugilist icon.
/// </Summary>
Pugilist = 129,
/// <summary>
/// The Marauder icon.
/// </Summary>
Marauder = 130,
/// <summary>
/// The Lancer icon.
/// </Summary>
Lancer = 131,
/// <summary>
/// The Archer icon.
/// </Summary>
Archer = 132,
/// <summary>
/// The Conjurer icon.
/// </Summary>
Conjurer = 133,
/// <summary>
/// The Thaumaturge icon.
/// </Summary>
Thaumaturge = 134,
/// <summary>
/// The Carpenter icon.
/// </Summary>
Carpenter = 135,
/// <summary>
/// The Blacksmith icon.
/// </Summary>
Blacksmith = 136,
/// <summary>
/// The Armorer icon.
/// </Summary>
Armorer = 137,
/// <summary>
/// The Goldsmith icon.
/// </Summary>
Goldsmith = 138,
/// <summary>
/// The Leatherworker icon.
/// </Summary>
Leatherworker = 139,
/// <summary>
/// The Weaver icon.
/// </Summary>
Weaver = 140,
/// <summary>
/// The Alchemist icon.
/// </Summary>
Alchemist = 131,
/// <summary>
/// The Culinarian icon.
/// </Summary>
Culinarian = 132,
/// <summary>
/// The Miner icon.
/// </Summary>
Miner = 143,
/// <summary>
/// The Botanist icon.
/// </Summary>
Botanist = 144,
/// <summary>
/// The Fisher icon.
/// </Summary>
Fisher = 145,
/// <summary>
/// The Paladin icon.
/// </Summary>
Paladin = 146,
/// <summary>
/// The Monk icon.
/// </Summary>
Monk = 147,
/// <summary>
/// The Warrior icon.
/// </Summary>
Warrior = 148,
/// <summary>
/// The Dragoon icon.
/// </Summary>
Dragoon = 149,
/// <summary>
/// The Bard icon.
/// </Summary>
Bard = 150,
/// <summary>
/// The White Mage icon.
/// </Summary>
WhiteMage = 151,
/// <summary>
/// The Black Mage icon.
/// </Summary>
BlackMage = 152,
/// <summary>
/// The Arcanist icon.
/// </Summary>
Arcanist = 153,
/// <summary>
/// The Summoner icon.
/// </Summary>
Summoner = 154,
/// <summary>
/// The Scholar icon.
/// </Summary>
Scholar = 155,
/// <summary>
/// The Rogue icon.
/// </Summary>
Rogue = 156,
/// <summary>
/// The Ninja icon.
/// </Summary>
Ninja = 157,
/// <summary>
/// The Machinist icon.
/// </Summary>
Machinist = 158,
/// <summary>
/// The Dark Knight icon.
/// </Summary>
DarkKnight = 159,
/// <summary>
/// The Astrologian icon.
/// </Summary>
Astrologian = 160,
/// <summary>
/// The Samurai icon.
/// </Summary>
Samurai = 161,
/// <summary>
/// The Red Mage icon.
/// </Summary>
RedMage = 162,
/// <summary>
/// The Blue Mage icon.
/// </Summary>
BlueMage = 163,
/// <summary>
/// The Gunbreaker icon.
/// </Summary>
Gunbreaker = 164,
/// <summary>
/// The Dancer icon.
/// </Summary>
Dancer = 165,
/// <summary>
/// The Reaper icon.
/// </Summary>
Reaper = 166,
/// <summary>
/// The Sage icon.
/// </Summary>
Sage = 167,
/// <summary>
/// The Waiting For Duty Finder icon.
/// </summary>
WaitingForDutyFinder = 168,
}

View file

@ -6,12 +6,12 @@
using System.Diagnostics.CodeAnalysis;
// General
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "Preventing long lines", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "I like regions", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1123:Do not place regions within elements", Justification = "I like regions in elements too", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1127:Generic type constraints should be on their own line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1028:Code should not contain trailing whitespace", Justification = "I don't care anymore", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "Preventing long lines")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "I like regions")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1123:Do not place regions within elements", Justification = "I like regions in elements too")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1127:Generic type constraints should be on their own line", Justification = "I like this better")]
[assembly: SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1028:Code should not contain trailing whitespace", Justification = "I don't care anymore")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")]

View file

@ -35,12 +35,8 @@ public sealed class AsmHook : IDisposable, IDalamudHook
{
address = HookManager.FollowJmp(address);
var hasOtherHooks = HookManager.Originals.ContainsKey(address);
if (!hasOtherHooks)
{
MemoryHelper.ReadRaw(address, 0x32, out var original);
HookManager.Originals[address] = original;
}
// We cannot call TrimAfterHook here because the hook is activated by the caller.
HookManager.RegisterUnhooker(address);
this.address = address;
this.hookImpl = ReloadedHooks.Instance.CreateAsmHook(assembly, address.ToInt64(), (Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour)asmHookBehaviour);
@ -65,12 +61,8 @@ public sealed class AsmHook : IDisposable, IDalamudHook
{
address = HookManager.FollowJmp(address);
var hasOtherHooks = HookManager.Originals.ContainsKey(address);
if (!hasOtherHooks)
{
MemoryHelper.ReadRaw(address, 0x32, out var original);
HookManager.Originals[address] = original;
}
// We cannot call TrimAfterHook here because the hook is activated by the caller.
HookManager.RegisterUnhooker(address);
this.address = address;
this.hookImpl = ReloadedHooks.Instance.CreateAsmHook(assembly, address.ToInt64(), (Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour)asmHookBehaviour);

View file

@ -41,12 +41,7 @@ internal class FunctionPointerVariableHook<T> : Hook<T>
{
lock (HookManager.HookEnableSyncRoot)
{
var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address);
if (!hasOtherHooks)
{
MemoryHelper.ReadRaw(this.Address, 0x32, out var original);
HookManager.Originals[this.Address] = original;
}
var unhooker = HookManager.RegisterUnhooker(this.Address, 8, 8);
if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList))
{
@ -104,6 +99,8 @@ internal class FunctionPointerVariableHook<T> : Hook<T>
// Add afterwards, so the hookIdent starts at 0.
indexList.Add(this);
unhooker.TrimAfterHook();
HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly));
}
}

View file

@ -16,7 +16,10 @@ namespace Dalamud.Hooking.Internal;
[ServiceManager.EarlyLoadedService]
internal class HookManager : IDisposable, IServiceType
{
private static readonly ModuleLog Log = new("HM");
/// <summary>
/// Logger shared with <see cref="Unhooker"/>.
/// </summary>
internal static readonly ModuleLog Log = new("HM");
[ServiceManager.ServiceConstructor]
private HookManager()
@ -34,21 +37,48 @@ internal class HookManager : IDisposable, IServiceType
internal static ConcurrentDictionary<Guid, HookInfo> TrackedHooks { get; } = new();
/// <summary>
/// Gets a static dictionary of original code for a hooked address.
/// Gets a static dictionary of unhookers for a hooked address.
/// </summary>
internal static ConcurrentDictionary<IntPtr, byte[]> Originals { get; } = new();
internal static ConcurrentDictionary<IntPtr, Unhooker> Unhookers { get; } = new();
/// <summary>
/// Gets a static dictionary of the number of hooks on a given address.
/// </summary>
internal static ConcurrentDictionary<IntPtr, List<IDalamudHook?>> MultiHookTracker { get; } = new();
/// <summary>
/// Creates a new Unhooker instance for the provided address if no such unhooker was already registered, or returns
/// an existing instance if the address was registered previously. By default, the unhooker will restore between 0
/// and 0x32 bytes depending on the detected size of the hook. To specify the minimum and maximum bytes restored
/// manually, use <see cref="RegisterUnhooker(System.IntPtr, int, int)"/>.
/// </summary>
/// <param name="address">The address of the instruction.</param>
/// <returns>A new Unhooker instance.</returns>
public static Unhooker RegisterUnhooker(IntPtr address)
{
return RegisterUnhooker(address, 0, 0x32);
}
/// <summary>
/// Creates a new Unhooker instance for the provided address if no such unhooker was already registered, or returns
/// an existing instance if the address was registered previously.
/// </summary>
/// <param name="address">The address of the instruction.</param>
/// <param name="minBytes">The minimum amount of bytes to restore when unhooking.</param>
/// <param name="maxBytes">The maximum amount of bytes to restore when unhooking.</param>
/// <returns>A new Unhooker instance.</returns>
public static Unhooker RegisterUnhooker(IntPtr address, int minBytes, int maxBytes)
{
Log.Verbose($"Registering hook at 0x{address.ToInt64():X} (minBytes=0x{minBytes:X}, maxBytes=0x{maxBytes:X})");
return Unhookers.GetOrAdd(address, _ => new Unhooker(address, minBytes, maxBytes));
}
/// <inheritdoc/>
public void Dispose()
{
RevertHooks();
TrackedHooks.Clear();
Originals.Clear();
Unhookers.Clear();
}
/// <summary>
@ -60,7 +90,7 @@ internal class HookManager : IDisposable, IServiceType
{
while (true)
{
var hasOtherHooks = HookManager.Originals.ContainsKey(address);
var hasOtherHooks = HookManager.Unhookers.ContainsKey(address);
if (hasOtherHooks)
{
// This address has been hooked already. Do not follow a jmp into a trampoline of our own making.
@ -124,28 +154,11 @@ internal class HookManager : IDisposable, IServiceType
return address;
}
private static unsafe void RevertHooks()
private static void RevertHooks()
{
foreach (var (address, originalBytes) in Originals)
foreach (var unhooker in Unhookers.Values)
{
var i = 0;
var current = (byte*)address;
// Find how many bytes have been modified by comparing to the saved original
for (; i < originalBytes.Length; i++)
{
if (current[i] == originalBytes[i])
break;
}
var snippet = originalBytes[0..i];
if (i > 0)
{
Log.Verbose($"Reverting hook at 0x{address.ToInt64():X} ({snippet.Length} bytes)");
MemoryHelper.ChangePermission(address, i, MemoryProtection.ExecuteReadWrite, out var oldPermissions);
MemoryHelper.WriteRaw(address, snippet);
MemoryHelper.ChangePermission(address, i, oldPermissions);
}
unhooker.Unhook();
}
}
}

View file

@ -1,8 +1,6 @@
using System;
using System.Reflection;
using Dalamud.Memory;
namespace Dalamud.Hooking.Internal;
/// <summary>
@ -24,12 +22,7 @@ internal class MinHookHook<T> : Hook<T> where T : Delegate
{
lock (HookManager.HookEnableSyncRoot)
{
var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address);
if (!hasOtherHooks)
{
MemoryHelper.ReadRaw(this.Address, 0x32, out var original);
HookManager.Originals[this.Address] = original;
}
var unhooker = HookManager.RegisterUnhooker(this.Address);
if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList))
indexList = HookManager.MultiHookTracker[this.Address] = new();
@ -41,6 +34,8 @@ internal class MinHookHook<T> : Hook<T> where T : Delegate
// Add afterwards, so the hookIdent starts at 0.
indexList.Add(this);
unhooker.TrimAfterHook();
HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly));
}
}

View file

@ -1,7 +1,6 @@
using System;
using System.Reflection;
using Dalamud.Memory;
using Reloaded.Hooks;
namespace Dalamud.Hooking.Internal;
@ -25,17 +24,13 @@ internal class ReloadedHook<T> : Hook<T> where T : Delegate
{
lock (HookManager.HookEnableSyncRoot)
{
var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address);
if (!hasOtherHooks)
{
MemoryHelper.ReadRaw(this.Address, 0x32, out var original);
HookManager.Originals[this.Address] = original;
}
var unhooker = HookManager.RegisterUnhooker(address);
this.hookImpl = ReloadedHooks.Instance.CreateHook<T>(detour, address.ToInt64());
this.hookImpl.Activate();
this.hookImpl.Disable();
unhooker.TrimAfterHook();
HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly));
}
}

View file

@ -0,0 +1,102 @@
using System;
using Dalamud.Memory;
namespace Dalamud.Hooking.Internal;
/// <summary>
/// A class which stores a copy of the bytes at a location which will be hooked in the future, such that those bytes can
/// be restored later to "unhook" the function.
/// </summary>
public class Unhooker
{
private readonly IntPtr address;
private readonly int minBytes;
private byte[] originalBytes;
private bool trimmed;
/// <summary>
/// Initializes a new instance of the <see cref="Unhooker"/> class. Upon creation, the Unhooker stores a copy of
/// the bytes stored at the provided address, and can be used to restore these bytes when the hook should be
/// removed. As such this class should be instantiated before the function is actually hooked.
/// </summary>
/// <param name="address">The address which will be hooked.</param>
/// <param name="minBytes">The minimum amount of bytes to restore when unhooking.</param>
/// <param name="maxBytes">The maximum amount of bytes to restore when unhooking.</param>
internal Unhooker(IntPtr address, int minBytes, int maxBytes)
{
if (minBytes < 0 || minBytes > maxBytes)
{
throw new ArgumentException($"minBytes ({minBytes}) must be <= maxBytes ({maxBytes}) and nonnegative.");
}
this.address = address;
this.minBytes = minBytes;
MemoryHelper.ReadRaw(address, maxBytes, out this.originalBytes);
}
/// <summary>
/// When called after a hook is created, checks the pre-hook original bytes and post-hook modified bytes, trimming
/// the original bytes stored and removing unmodified bytes from the end of the byte sequence. Assuming no
/// concurrent actions modified the same address space, this should result in storing only the minimum bytes
/// required to unhook the function.
/// </summary>
public void TrimAfterHook()
{
if (this.trimmed)
{
return;
}
var len = int.Max(this.GetFullHookLength(), this.minBytes);
this.originalBytes = this.originalBytes[..len];
this.trimmed = true;
}
/// <summary>
/// Attempts to unhook the function by replacing the hooked bytes with the original bytes. If
/// <see cref="TrimAfterHook"/> was called, the trimmed original bytes stored at that time will be used for
/// unhooking. Otherwise, a naive algorithm which only restores bytes until the first unchanged byte will be used in
/// order to avoid overwriting adjacent data.
/// </summary>
public void Unhook()
{
var len = this.trimmed ? this.originalBytes.Length : int.Max(this.GetNaiveHookLength(), this.minBytes);
if (len > 0)
{
HookManager.Log.Verbose($"Reverting hook at 0x{this.address.ToInt64():X} ({len} bytes, trimmed={this.trimmed})");
MemoryHelper.ChangePermission(this.address, len, MemoryProtection.ExecuteReadWrite, out var oldPermissions);
MemoryHelper.WriteRaw(this.address, this.originalBytes[..len]);
MemoryHelper.ChangePermission(this.address, len, oldPermissions);
}
}
private unsafe int GetNaiveHookLength()
{
var current = (byte*)this.address;
for (var i = 0; i < this.originalBytes.Length; i++)
{
if (current[i] == this.originalBytes[i])
{
return i;
}
}
return 0;
}
private unsafe int GetFullHookLength()
{
var current = (byte*)this.address;
for (var i = this.originalBytes.Length - 1; i >= 0; i--)
{
if (current[i] != this.originalBytes[i])
{
return int.Max(i + 1, this.minBytes);
}
}
return 0;
}
}

View file

@ -29,13 +29,19 @@ internal class DalamudCommands : IServiceType
HelpMessage = Loc.Localize("DalamudUnloadHelp", "Unloads XIVLauncher in-game addon."),
ShowInHelp = false,
});
commandManager.AddHandler("/xlkill", new CommandInfo(this.OnKillCommand)
{
HelpMessage = "Kill the game.",
ShowInHelp = false,
});
commandManager.AddHandler("/xlrestart", new CommandInfo(this.OnRestartCommand)
{
HelpMessage = "Restart the game.",
ShowInHelp = false,
});
commandManager.AddHandler("/xlhelp", new CommandInfo(this.OnHelpCommand)
{
HelpMessage = Loc.Localize("DalamudCmdInfoHelp", "Shows list of commands available. If an argument is provided, shows help for that command."),
@ -147,12 +153,17 @@ internal class DalamudCommands : IServiceType
Service<ChatGui>.Get().Print("Unloading...");
Service<Dalamud>.Get().Unload();
}
private void OnKillCommand(string command, string arguments)
{
Process.GetCurrentProcess().Kill();
}
private void OnRestartCommand(string command, string arguments)
{
Dalamud.RestartGame();
}
private void OnHelpCommand(string command, string arguments)
{
var chatGui = Service<ChatGui>.Get();

View file

@ -717,12 +717,7 @@ internal class DalamudInterface : IDisposable, IServiceType
if (ImGui.MenuItem("Restart game"))
{
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern void RaiseException(uint dwExceptionCode, uint dwExceptionFlags, uint nNumberOfArguments, IntPtr lpArguments);
RaiseException(0x12345678, 0, 0, IntPtr.Zero);
Process.GetCurrentProcess().Kill();
Dalamud.RestartGame();
}
if (ImGui.MenuItem("Kill game"))
@ -802,6 +797,11 @@ internal class DalamudInterface : IDisposable, IServiceType
ImGui.SetWindowFocus(null);
}
if (ImGui.MenuItem("Clear stacks"))
{
Service<InterfaceManager>.Get().ClearStacks();
}
if (ImGui.MenuItem("Dump style"))
{
var info = string.Empty;

View file

@ -435,6 +435,15 @@ internal class InterfaceManager : IDisposable, IServiceType
return null;
}
/// <summary>
/// Clear font, style, and color stack. Dangerous, only use when you know
/// no one else has something pushed they may try to pop.
/// </summary>
public void ClearStacks()
{
this.scene?.ClearStacksOnContext();
}
/// <summary>
/// Toggle Windows 11 immersive mode on the game window.
/// </summary>
@ -892,6 +901,7 @@ internal class InterfaceManager : IDisposable, IServiceType
Log.Verbose("[FONT] ImGui.IO.Build will be called.");
ioFonts.Build();
gameFontManager.AfterIoFontsBuild();
this.ClearStacks();
Log.Verbose("[FONT] ImGui.IO.Build OK!");
gameFontManager.AfterBuildFonts();

View file

@ -28,6 +28,7 @@ internal class PluginCategoryManager
new(11, "special.devIconTester", () => Locs.Category_IconTester),
new(12, "special.dalamud", () => Locs.Category_Dalamud),
new(13, "special.plugins", () => Locs.Category_Plugins),
new(14, "special.profiles", () => Locs.Category_PluginProfiles, CategoryInfo.AppearCondition.ProfilesEnabled),
new(FirstTagBasedCategoryId + 0, "other", () => Locs.Category_Other),
new(FirstTagBasedCategoryId + 1, "jobs", () => Locs.Category_Jobs),
new(FirstTagBasedCategoryId + 2, "ui", () => Locs.Category_UI),
@ -43,7 +44,7 @@ internal class PluginCategoryManager
private GroupInfo[] groupList =
{
new(GroupKind.DevTools, () => Locs.Group_DevTools, 10, 11),
new(GroupKind.Installed, () => Locs.Group_Installed, 0, 1),
new(GroupKind.Installed, () => Locs.Group_Installed, 0, 1, 14),
new(GroupKind.Available, () => Locs.Group_Available, 0),
new(GroupKind.Changelog, () => Locs.Group_Changelog, 0, 12, 13),
@ -352,6 +353,11 @@ internal class PluginCategoryManager
/// Check if plugin testing is enabled.
/// </summary>
DoPluginTest,
/// <summary>
/// Check if plugin profiles are enabled.
/// </summary>
ProfilesEnabled,
}
/// <summary>
@ -430,6 +436,8 @@ internal class PluginCategoryManager
public static string Category_IconTester => "Image/Icon Tester";
public static string Category_PluginProfiles => Loc.Localize("InstallerCategoryPluginProfiles", "Plugin Collections");
public static string Category_Other => Loc.Localize("InstallerCategoryOther", "Other");
public static string Category_Jobs => Loc.Localize("InstallerCategoryJobs", "Jobs");

View file

@ -1792,7 +1792,8 @@ internal class DataWindow : Window
ImGui.TableNextColumn();
ImGui.TextUnformatted(string.Join(", ", share.Users));
}
} finally
}
finally
{
ImGui.EndTable();
}

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Json;
using System.Threading.Tasks;
using Dalamud.Networking.Http;
using Dalamud.Plugin.Internal;
using Dalamud.Utility;

View file

@ -15,12 +15,14 @@ using Dalamud.Game.Command;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Raii;
using Dalamud.Interface.Style;
using Dalamud.Interface.Windowing;
using Dalamud.Logging.Internal;
using Dalamud.Plugin;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Exceptions;
using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Support;
using Dalamud.Utility;
@ -48,6 +50,8 @@ internal class PluginInstallerWindow : Window, IDisposable
private readonly object listLock = new();
private readonly ProfileManagerWidget profileManagerWidget;
private DalamudChangelogManager? dalamudChangelogManager;
private Task? dalamudChangelogRefreshTask;
private CancellationTokenSource? dalamudChangelogRefreshTaskCts;
@ -148,6 +152,8 @@ internal class PluginInstallerWindow : Window, IDisposable
});
this.timeLoaded = DateTime.Now;
this.profileManagerWidget = new(this);
}
private enum OperationStatus
@ -166,6 +172,7 @@ internal class PluginInstallerWindow : Window, IDisposable
UpdatingAll,
Installing,
Manager,
ProfilesLoading,
}
private enum PluginSortKind
@ -212,6 +219,8 @@ internal class PluginInstallerWindow : Window, IDisposable
this.updatePluginCount = 0;
this.updatedPlugins = null;
}
this.profileManagerWidget.Reset();
}
/// <inheritdoc/>
@ -284,17 +293,96 @@ internal class PluginInstallerWindow : Window, IDisposable
this.searchText = text;
}
/// <summary>
/// Start a plugin install and handle errors visually.
/// </summary>
/// <param name="manifest">The manifest to install.</param>
/// <param name="useTesting">Install the testing version.</param>
public void StartInstall(RemotePluginManifest manifest, bool useTesting)
{
var pluginManager = Service<PluginManager>.Get();
var notifications = Service<NotificationManager>.Get();
this.installStatus = OperationStatus.InProgress;
this.loadingIndicatorKind = LoadingIndicatorKind.Installing;
Task.Run(() => pluginManager.InstallPluginAsync(manifest, useTesting || manifest.IsTestingExclusive, PluginLoadReason.Installer))
.ContinueWith(task =>
{
// There is no need to set as Complete for an individual plugin installation
this.installStatus = OperationStatus.Idle;
if (this.DisplayErrorContinuation(task, Locs.ErrorModal_InstallFail(manifest.Name)))
{
// Fine as long as we aren't in an error state
if (task.Result.State is PluginState.Loaded or PluginState.Unloaded)
{
notifications.AddNotification(Locs.Notifications_PluginInstalled(manifest.Name), Locs.Notifications_PluginInstalledTitle, NotificationType.Success);
}
else
{
notifications.AddNotification(Locs.Notifications_PluginNotInstalled(manifest.Name), Locs.Notifications_PluginNotInstalledTitle, NotificationType.Error);
this.ShowErrorModal(Locs.ErrorModal_InstallFail(manifest.Name));
}
}
});
}
/// <summary>
/// A continuation task that displays any errors received into the error modal.
/// </summary>
/// <param name="task">The previous task.</param>
/// <param name="state">An error message to be displayed.</param>
/// <returns>A value indicating whether to continue with the next task.</returns>
public bool DisplayErrorContinuation(Task task, object state)
{
if (task.IsFaulted)
{
var errorModalMessage = state as string;
foreach (var ex in task.Exception.InnerExceptions)
{
if (ex is PluginException)
{
Log.Error(ex, "Plugin installer threw an error");
#if DEBUG
if (!string.IsNullOrEmpty(ex.Message))
errorModalMessage += $"\n\n{ex.Message}";
#endif
}
else
{
Log.Error(ex, "Plugin installer threw an unexpected error");
#if DEBUG
if (!string.IsNullOrEmpty(ex.Message))
errorModalMessage += $"\n\n{ex.Message}";
#endif
}
}
this.ShowErrorModal(errorModalMessage);
return false;
}
return true;
}
private void DrawProgressOverlay()
{
var pluginManager = Service<PluginManager>.Get();
var profileManager = Service<ProfileManager>.Get();
var isWaitingManager = !pluginManager.PluginsReady ||
!pluginManager.ReposReady;
var isWaitingProfiles = profileManager.IsBusy;
var isLoading = this.AnyOperationInProgress ||
isWaitingManager;
isWaitingManager || isWaitingProfiles;
if (isWaitingManager)
this.loadingIndicatorKind = LoadingIndicatorKind.Manager;
else if (isWaitingProfiles)
this.loadingIndicatorKind = LoadingIndicatorKind.ProfilesLoading;
if (!isLoading)
return;
@ -378,6 +466,9 @@ internal class PluginInstallerWindow : Window, IDisposable
}
}
break;
case LoadingIndicatorKind.ProfilesLoading:
ImGuiHelpers.CenteredText("Collections are being applied...");
break;
default:
throw new ArgumentOutOfRangeException();
@ -386,9 +477,7 @@ internal class PluginInstallerWindow : Window, IDisposable
if (DateTime.Now - this.timeLoaded > TimeSpan.FromSeconds(90) && !pluginManager.PluginsReady)
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGuiHelpers.CenteredText("This is embarrassing, but...");
ImGuiHelpers.CenteredText("one of your plugins may be blocking the installer.");
ImGuiHelpers.CenteredText("You should tell us about this, please keep this window open.");
ImGuiHelpers.CenteredText("One of your plugins may be blocking the installer.");
ImGui.PopStyleColor();
}
@ -433,54 +522,57 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.SetCursorPosX(windowSize.X - sortSelectWidth - (style.ItemSpacing.X * 2) - searchInputWidth - searchClearButtonWidth);
var searchTextChanged = false;
ImGui.SetNextItemWidth(searchInputWidth);
searchTextChanged |= ImGui.InputTextWithHint(
"###XlPluginInstaller_Search",
Locs.Header_SearchPlaceholder,
ref this.searchText,
100);
ImGui.SameLine();
ImGui.SetCursorPosY(downShift);
ImGui.SetNextItemWidth(searchClearButtonWidth);
if (ImGuiComponents.IconButton(FontAwesomeIcon.Times))
var isProfileManager =
this.categoryManager.CurrentGroupIdx == 1 && this.categoryManager.CurrentCategoryIdx == 2;
// Disable search if profile editor
using (ImRaii.Disabled(isProfileManager))
{
this.searchText = string.Empty;
searchTextChanged = true;
}
var searchTextChanged = false;
ImGui.SetNextItemWidth(searchInputWidth);
searchTextChanged |= ImGui.InputTextWithHint(
"###XlPluginInstaller_Search",
Locs.Header_SearchPlaceholder,
ref this.searchText,
100);
if (searchTextChanged)
this.UpdateCategoriesOnSearchChange();
ImGui.SameLine();
ImGui.SetCursorPosY(downShift);
// Changelog group
var isSortDisabled = this.categoryManager.CurrentGroupIdx == 3;
if (isSortDisabled)
ImGui.BeginDisabled();
ImGui.SameLine();
ImGui.SetCursorPosY(downShift);
ImGui.SetNextItemWidth(selectableWidth);
if (ImGui.BeginCombo(sortByText, this.filterText, ImGuiComboFlags.NoArrowButton))
{
foreach (var selectable in sortSelectables)
ImGui.SetNextItemWidth(searchClearButtonWidth);
if (ImGuiComponents.IconButton(FontAwesomeIcon.Times))
{
if (ImGui.Selectable(selectable.Localization))
{
this.sortKind = selectable.SortKind;
this.filterText = selectable.Localization;
lock (this.listLock)
this.ResortPlugins();
}
this.searchText = string.Empty;
searchTextChanged = true;
}
ImGui.EndCombo();
if (searchTextChanged)
this.UpdateCategoriesOnSearchChange();
}
if (isSortDisabled)
ImGui.EndDisabled();
// Disable sort if changelogs or profile editor
using (ImRaii.Disabled(this.categoryManager.CurrentGroupIdx == 3 || isProfileManager))
{
ImGui.SameLine();
ImGui.SetCursorPosY(downShift);
ImGui.SetNextItemWidth(selectableWidth);
if (ImGui.BeginCombo(sortByText, this.filterText, ImGuiComboFlags.NoArrowButton))
{
foreach (var selectable in sortSelectables)
{
if (ImGui.Selectable(selectable.Localization))
{
this.sortKind = selectable.SortKind;
this.filterText = selectable.Localization;
lock (this.listLock)
this.ResortPlugins();
}
}
ImGui.EndCombo();
}
}
}
private void DrawFooter()
@ -1089,6 +1181,10 @@ internal class PluginInstallerWindow : Window, IDisposable
if (!Service<DalamudConfiguration>.Get().DoPluginTest)
continue;
break;
case PluginCategoryManager.CategoryInfo.AppearCondition.ProfilesEnabled:
if (!Service<DalamudConfiguration>.Get().ProfilesEnabled)
continue;
break;
default:
throw new ArgumentOutOfRangeException();
}
@ -1194,6 +1290,10 @@ internal class PluginInstallerWindow : Window, IDisposable
case 1:
this.DrawInstalledPluginList(true);
break;
case 2:
this.profileManagerWidget.Draw();
break;
}
break;
@ -1533,7 +1633,7 @@ internal class PluginInstallerWindow : Window, IDisposable
ImGui.SetCursorPos(startCursor);
var pluginDisabled = plugin is { IsDisabled: true };
var pluginDisabled = plugin is { IsWantedByAnyProfile: false };
var iconSize = ImGuiHelpers.ScaledVector2(64, 64);
var cursorBeforeImage = ImGui.GetCursorPos();
@ -1831,27 +1931,7 @@ internal class PluginInstallerWindow : Window, IDisposable
var buttonText = Locs.PluginButton_InstallVersion(versionString);
if (ImGui.Button($"{buttonText}##{buttonText}{index}"))
{
this.installStatus = OperationStatus.InProgress;
this.loadingIndicatorKind = LoadingIndicatorKind.Installing;
Task.Run(() => pluginManager.InstallPluginAsync(manifest, useTesting || manifest.IsTestingExclusive, PluginLoadReason.Installer))
.ContinueWith(task =>
{
// There is no need to set as Complete for an individual plugin installation
this.installStatus = OperationStatus.Idle;
if (this.DisplayErrorContinuation(task, Locs.ErrorModal_InstallFail(manifest.Name)))
{
if (task.Result.State == PluginState.Loaded)
{
notifications.AddNotification(Locs.Notifications_PluginInstalled(manifest.Name), Locs.Notifications_PluginInstalledTitle, NotificationType.Success);
}
else
{
notifications.AddNotification(Locs.Notifications_PluginNotInstalled(manifest.Name), Locs.Notifications_PluginNotInstalledTitle, NotificationType.Error);
this.ShowErrorModal(Locs.ErrorModal_InstallFail(manifest.Name));
}
}
});
this.StartInstall(manifest, useTesting);
}
}
@ -1958,7 +2038,7 @@ internal class PluginInstallerWindow : Window, IDisposable
}
// Disabled
if (plugin.IsDisabled || !plugin.CheckPolicy())
if (!plugin.IsWantedByAnyProfile || !plugin.CheckPolicy())
{
label += Locs.PluginTitleMod_Disabled;
trouble = true;
@ -2118,7 +2198,7 @@ internal class PluginInstallerWindow : Window, IDisposable
this.DrawSendFeedbackButton(plugin.Manifest, plugin.IsTesting);
}
if (availablePluginUpdate != default)
if (availablePluginUpdate != default && !plugin.IsDev)
this.DrawUpdateSinglePluginButton(availablePluginUpdate);
ImGui.SameLine();
@ -2240,6 +2320,11 @@ internal class PluginInstallerWindow : Window, IDisposable
{
var notifications = Service<NotificationManager>.Get();
var pluginManager = Service<PluginManager>.Get();
var profileManager = Service<ProfileManager>.Get();
var config = Service<DalamudConfiguration>.Get();
var applicableForProfiles = plugin.Manifest.SupportsProfiles;
var isDefaultPlugin = profileManager.IsInDefaultProfile(plugin.Manifest.InternalName);
// Disable everything if the updater is running or another plugin is operating
var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress;
@ -2255,15 +2340,70 @@ internal class PluginInstallerWindow : Window, IDisposable
// Now handled by the first case below
// disabled = disabled || plugin.State == PluginState.LoadError || plugin.State == PluginState.DependencyResolutionFailed;
// Disable everything if we're working
// Disable everything if we're loading plugins
disabled = disabled || plugin.State == PluginState.Loading || plugin.State == PluginState.Unloading;
// Disable everything if we're applying profiles
disabled = disabled || profileManager.IsBusy;
var toggleId = plugin.Manifest.InternalName;
var isLoadedAndUnloadable = plugin.State == PluginState.Loaded ||
plugin.State == PluginState.DependencyResolutionFailed;
StyleModelV1.DalamudStandard.Push();
var profileChooserPopupName = $"###pluginProfileChooser{plugin.Manifest.InternalName}";
if (ImGui.BeginPopup(profileChooserPopupName))
{
var didAny = false;
foreach (var profile in profileManager.Profiles.Where(x => !x.IsDefaultProfile))
{
var inProfile = profile.WantsPlugin(plugin.Manifest.InternalName) != null;
if (ImGui.Checkbox($"###profilePick{profile.Guid}{plugin.Manifest.InternalName}", ref inProfile))
{
if (inProfile)
{
Task.Run(() => profile.AddOrUpdate(plugin.Manifest.InternalName, true))
.ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotAdd);
}
else
{
Task.Run(() => profile.Remove(plugin.Manifest.InternalName))
.ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotRemove);
}
}
ImGui.SameLine();
ImGui.TextUnformatted(profile.Name);
didAny = true;
}
if (!didAny)
ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.Profiles_None);
ImGui.Separator();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Times))
{
profileManager.DefaultProfile.AddOrUpdate(plugin.Manifest.InternalName, plugin.IsLoaded, false);
foreach (var profile in profileManager.Profiles.Where(x => !x.IsDefaultProfile && x.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName)))
{
profile.Remove(plugin.Manifest.InternalName, false);
}
// TODO error handling
Task.Run(() => profileManager.ApplyAllWantStates());
}
ImGui.SameLine();
ImGui.Text(Locs.Profiles_RemoveFromAll);
ImGui.EndPopup();
}
if (plugin.State is PluginState.UnloadError or PluginState.LoadError or PluginState.DependencyResolutionFailed && !plugin.IsDev)
{
ImGuiComponents.DisabledButton(FontAwesomeIcon.Frown);
@ -2271,14 +2411,18 @@ internal class PluginInstallerWindow : Window, IDisposable
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.PluginButtonToolTip_UnloadFailed);
}
else if (disabled)
else if (disabled || !isDefaultPlugin)
{
ImGuiComponents.DisabledToggleButton(toggleId, isLoadedAndUnloadable);
if (!isDefaultPlugin && ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.PluginButtonToolTip_NeedsToBeInDefault);
}
else
{
if (ImGuiComponents.ToggleButton(toggleId, ref isLoadedAndUnloadable))
{
// TODO: We can technically let profile manager take care of unloading/loading the plugin, but we should figure out error handling first.
if (!isLoadedAndUnloadable)
{
this.enableDisableStatus = OperationStatus.InProgress;
@ -2301,15 +2445,9 @@ internal class PluginInstallerWindow : Window, IDisposable
return;
}
var disableTask = Task.Run(() => plugin.Disable())
.ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_DisableFail(plugin.Name));
disableTask.Wait();
profileManager.DefaultProfile.AddOrUpdate(plugin.Manifest.InternalName, false, false);
this.enableDisableStatus = OperationStatus.Complete;
if (!disableTask.Result)
return;
notifications.AddNotification(Locs.Notifications_PluginDisabled(plugin.Manifest.Name), Locs.Notifications_PluginDisabledTitle, NotificationType.Success);
});
}
@ -2325,17 +2463,7 @@ internal class PluginInstallerWindow : Window, IDisposable
plugin.ReloadManifest();
}
var enableTask = Task.Run(plugin.Enable)
.ContinueWith(
this.DisplayErrorContinuation,
Locs.ErrorModal_EnableFail(plugin.Name));
enableTask.Wait();
if (!enableTask.Result)
{
this.enableDisableStatus = OperationStatus.Complete;
return;
}
profileManager.DefaultProfile.AddOrUpdate(plugin.Manifest.InternalName, true, false);
var loadTask = Task.Run(() => plugin.LoadAsync(PluginLoadReason.Installer))
.ContinueWith(
@ -2388,6 +2516,29 @@ internal class PluginInstallerWindow : Window, IDisposable
// Only if the plugin isn't broken.
this.DrawOpenPluginSettingsButton(plugin);
}
if (applicableForProfiles && config.ProfilesEnabled)
{
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Toolbox))
{
ImGui.OpenPopup(profileChooserPopupName);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.PluginButtonToolTip_PickProfiles);
}
else if (!applicableForProfiles && config.ProfilesEnabled)
{
ImGui.SameLine();
ImGui.BeginDisabled();
ImGuiComponents.IconButton(FontAwesomeIcon.Toolbox);
ImGui.EndDisabled();
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.PluginButtonToolTip_ProfilesNotSupported);
}
}
private async Task<bool> UpdateSinglePlugin(AvailablePluginUpdate update)
@ -2474,26 +2625,32 @@ internal class PluginInstallerWindow : Window, IDisposable
if (localPlugin is LocalDevPlugin plugin)
{
var isInDefaultProfile =
Service<ProfileManager>.Get().IsInDefaultProfile(localPlugin.Manifest.InternalName);
// https://colorswall.com/palette/2868/
var greenColor = new Vector4(0x5C, 0xB8, 0x5C, 0xFF) / 0xFF;
var redColor = new Vector4(0xD9, 0x53, 0x4F, 0xFF) / 0xFF;
// Load on boot
ImGui.PushStyleColor(ImGuiCol.Button, plugin.StartOnBoot ? greenColor : redColor);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, plugin.StartOnBoot ? greenColor : redColor);
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.PowerOff))
using (ImRaii.Disabled(!isInDefaultProfile))
{
plugin.StartOnBoot ^= true;
configuration.QueueSave();
}
ImGui.PushStyleColor(ImGuiCol.Button, plugin.StartOnBoot ? greenColor : redColor);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, plugin.StartOnBoot ? greenColor : redColor);
ImGui.PopStyleColor(2);
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.PowerOff))
{
plugin.StartOnBoot ^= true;
configuration.QueueSave();
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(Locs.PluginButtonToolTip_StartOnBoot);
ImGui.PopStyleColor(2);
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(isInDefaultProfile ? Locs.PluginButtonToolTip_StartOnBoot : Locs.PluginButtonToolTip_NeedsToBeInDefault);
}
}
// Automatic reload
@ -2800,46 +2957,6 @@ internal class PluginInstallerWindow : Window, IDisposable
private bool WasPluginSeen(string internalName) =>
Service<DalamudConfiguration>.Get().SeenPluginInternalName.Contains(internalName);
/// <summary>
/// A continuation task that displays any errors received into the error modal.
/// </summary>
/// <param name="task">The previous task.</param>
/// <param name="state">An error message to be displayed.</param>
/// <returns>A value indicating whether to continue with the next task.</returns>
private bool DisplayErrorContinuation(Task task, object state)
{
if (task.IsFaulted)
{
var errorModalMessage = state as string;
foreach (var ex in task.Exception.InnerExceptions)
{
if (ex is PluginException)
{
Log.Error(ex, "Plugin installer threw an error");
#if DEBUG
if (!string.IsNullOrEmpty(ex.Message))
errorModalMessage += $"\n\n{ex.Message}";
#endif
}
else
{
Log.Error(ex, "Plugin installer threw an unexpected error");
#if DEBUG
if (!string.IsNullOrEmpty(ex.Message))
errorModalMessage += $"\n\n{ex.Message}";
#endif
}
}
this.ShowErrorModal(errorModalMessage);
return false;
}
return true;
}
private Task ShowErrorModal(string message)
{
this.errorModalMessage = message;
@ -2890,7 +3007,7 @@ internal class PluginInstallerWindow : Window, IDisposable
#region Header
public static string Header_Hint => Loc.Localize("InstallerHint", "This window allows you to install and remove in-game plugins.\nThey are made by third-party developers.");
public static string Header_Hint => Loc.Localize("InstallerHint", "This window allows you to install and remove Dalamud plugins.\nThey are made by the community.");
public static string Header_SearchPlaceholder => Loc.Localize("InstallerSearch", "Search");
@ -3047,6 +3164,10 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string PluginButtonToolTip_OpenConfiguration => Loc.Localize("InstallerOpenConfig", "Open Configuration");
public static string PluginButtonToolTip_PickProfiles => Loc.Localize("InstallerPickProfiles", "Pick collections for this plugin");
public static string PluginButtonToolTip_ProfilesNotSupported => Loc.Localize("InstallerProfilesNotSupported", "This plugin does not support collections");
public static string PluginButtonToolTip_StartOnBoot => Loc.Localize("InstallerStartOnBoot", "Start on boot");
public static string PluginButtonToolTip_AutomaticReloading => Loc.Localize("InstallerAutomaticReloading", "Automatic reloading");
@ -3067,6 +3188,8 @@ internal class PluginInstallerWindow : Window, IDisposable
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.");
#endregion
#region Notifications
@ -3226,5 +3349,20 @@ internal class PluginInstallerWindow : Window, IDisposable
public static string SafeModeDisclaimer => Loc.Localize("SafeModeDisclaimer", "You enabled safe mode, no plugins will be loaded.\nYou may delete plugins from the \"Installed plugins\" tab.\nSimply restart your game to disable safe mode.");
#endregion
#region Profiles
public static string Profiles_CouldNotAdd =>
Loc.Localize("InstallerProfilesCouldNotAdd", "Couldn't add plugin to this collection.");
public static string Profiles_CouldNotRemove =>
Loc.Localize("InstallerProfilesCouldNotRemove", "Couldn't remove plugin from this collection.");
public static string Profiles_None => Loc.Localize("InstallerProfilesNone", "No collections! Go add some in \"Plugin Collections\"!");
public static string Profiles_RemoveFromAll =>
Loc.Localize("InstallerProfilesRemoveFromAll", "Remove from all collections");
#endregion
}
}

View file

@ -0,0 +1,506 @@
using System;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using CheapLoc;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Raii;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Utility;
using ImGuiNET;
using Serilog;
namespace Dalamud.Interface.Internal.Windows.PluginInstaller;
/// <summary>
/// ImGui widget used to manage profiles.
/// </summary>
internal class ProfileManagerWidget
{
private readonly PluginInstallerWindow installer;
private Mode mode = Mode.Overview;
private Guid? editingProfileGuid;
private string pickerSearch = string.Empty;
private string profileNameEdit = string.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="ProfileManagerWidget"/> class.
/// </summary>
/// <param name="installer">The plugin installer.</param>
public ProfileManagerWidget(PluginInstallerWindow installer)
{
this.installer = installer;
}
private enum Mode
{
Overview,
EditSingleProfile,
}
/// <summary>
/// Draw this widget's contents.
/// </summary>
public void Draw()
{
switch (this.mode)
{
case Mode.Overview:
this.DrawOverview();
break;
case Mode.EditSingleProfile:
this.DrawEdit();
break;
}
}
/// <summary>
/// Reset the widget.
/// </summary>
public void Reset()
{
this.mode = Mode.Overview;
this.editingProfileGuid = null;
this.pickerSearch = string.Empty;
}
private void DrawOverview()
{
var didAny = false;
var profman = Service<ProfileManager>.Get();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus))
profman.AddNewProfile();
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.AddProfile);
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(5);
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.FileImport))
{
try
{
profman.ImportProfile(ImGui.GetClipboardText());
Service<NotificationManager>.Get().AddNotification(Locs.NotificationImportSuccess, type: NotificationType.Success);
}
catch (Exception ex)
{
Log.Error(ex, "Could not import profile");
Service<NotificationManager>.Get().AddNotification(Locs.NotificationImportError, type: NotificationType.Error);
}
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.ImportProfileHint);
ImGui.Separator();
ImGuiHelpers.ScaledDummy(5);
var windowSize = ImGui.GetWindowSize();
if (ImGui.BeginChild("###profileChooserScrolling"))
{
Guid? toCloneGuid = null;
foreach (var profile in profman.Profiles)
{
if (profile.IsDefaultProfile)
continue;
var isEnabled = profile.IsEnabled;
if (ImGuiComponents.ToggleButton($"###toggleButton{profile.Guid}", ref isEnabled))
{
Task.Run(() => profile.SetState(isEnabled))
.ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState);
}
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(3);
ImGui.SameLine();
ImGui.Text(profile.Name);
ImGui.SameLine();
ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30));
if (ImGuiComponents.IconButton($"###editButton{profile.Guid}", FontAwesomeIcon.PencilAlt))
{
this.mode = Mode.EditSingleProfile;
this.editingProfileGuid = profile.Guid;
this.profileNameEdit = profile.Name;
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.EditProfileHint);
ImGui.SameLine();
ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * 2) - 5);
if (ImGuiComponents.IconButton($"###cloneButton{profile.Guid}", FontAwesomeIcon.Copy))
toCloneGuid = profile.Guid;
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.CloneProfileHint);
ImGui.SameLine();
ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * 3) - 5);
if (ImGuiComponents.IconButton($"###exportButton{profile.Guid}", FontAwesomeIcon.FileExport))
{
ImGui.SetClipboardText(profile.Model.Serialize());
Service<NotificationManager>.Get().AddNotification(Locs.CopyToClipboardNotification, type: NotificationType.Success);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.CopyToClipboardHint);
didAny = true;
ImGuiHelpers.ScaledDummy(2);
}
if (toCloneGuid != null)
{
profman.CloneProfile(profman.Profiles.First(x => x.Guid == toCloneGuid));
}
if (!didAny)
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
ImGuiHelpers.CenteredText(Locs.AddProfileHint);
ImGui.PopStyleColor();
}
ImGui.EndChild();
}
}
private void DrawEdit()
{
if (this.editingProfileGuid == null)
{
Log.Error("Editing profile guid was null");
this.Reset();
return;
}
var profman = Service<ProfileManager>.Get();
var pm = Service<PluginManager>.Get();
var pic = Service<PluginImageCache>.Get();
var profile = profman.Profiles.FirstOrDefault(x => x.Guid == this.editingProfileGuid);
if (profile == null)
{
Log.Error("Could not find profile {Guid} for edit", this.editingProfileGuid);
this.Reset();
return;
}
const string addPluginToProfilePopup = "###addPluginToProfile";
using (var popup = ImRaii.Popup(addPluginToProfilePopup))
{
if (popup.Success)
{
var width = ImGuiHelpers.GlobalScale * 300;
using var disabled = ImRaii.Disabled(profman.IsBusy);
ImGui.SetNextItemWidth(width);
ImGui.InputTextWithHint("###pluginPickerSearch", Locs.SearchHint, ref this.pickerSearch, 255);
if (ImGui.BeginListBox("###pluginPicker", new Vector2(width, width - 80)))
{
// TODO: Plugin searching should be abstracted... installer and this should use the same search
foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles &&
(this.pickerSearch.IsNullOrWhitespace() || x.Manifest.Name.ToLowerInvariant().Contains(this.pickerSearch.ToLowerInvariant()))))
{
using var disabled2 =
ImRaii.Disabled(profile.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName));
if (ImGui.Selectable($"{plugin.Manifest.Name}###selector{plugin.Manifest.InternalName}"))
{
// TODO this sucks
profile.AddOrUpdate(plugin.Manifest.InternalName, true, false);
Task.Run(() => profman.ApplyAllWantStates())
.ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState);
}
}
ImGui.EndListBox();
}
}
}
var didAny = false;
// ======== Top bar ========
var windowSize = ImGui.GetWindowSize();
if (ImGuiComponents.IconButton(FontAwesomeIcon.ArrowLeft))
this.Reset();
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.BackToOverview);
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(5);
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.FileExport))
{
ImGui.SetClipboardText(profile.Model.Serialize());
Service<NotificationManager>.Get().AddNotification(Locs.CopyToClipboardNotification, type: NotificationType.Success);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.CopyToClipboardHint);
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(5);
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash))
{
this.Reset();
// DeleteProfile() is sync, it doesn't apply and we are modifying the plugins collection. Will throw below when iterating
profman.DeleteProfile(profile);
Task.Run(() => profman.ApplyAllWantStates())
.ContinueWith(t =>
{
this.installer.DisplayErrorContinuation(t, Locs.ErrorCouldNotChangeState);
});
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.DeleteProfileHint);
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(5);
ImGui.SameLine();
ImGui.SetNextItemWidth(windowSize.X / 3);
if (ImGui.InputText("###profileNameInput", ref this.profileNameEdit, 255))
{
profile.Name = this.profileNameEdit;
}
ImGui.SameLine();
ImGui.SetCursorPosX(windowSize.X - (ImGui.GetFrameHeight() * 1.55f * ImGuiHelpers.GlobalScale));
var isEnabled = profile.IsEnabled;
if (ImGuiComponents.ToggleButton($"###toggleButton{profile.Guid}", ref isEnabled))
{
Task.Run(() => profile.SetState(isEnabled))
.ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.TooltipEnableDisable);
ImGui.Separator();
ImGuiHelpers.ScaledDummy(5);
var enableAtBoot = profile.AlwaysEnableAtBoot;
if (ImGui.Checkbox(Locs.AlwaysEnableAtBoot, ref enableAtBoot))
{
profile.AlwaysEnableAtBoot = enableAtBoot;
}
ImGuiHelpers.ScaledDummy(5);
ImGui.Separator();
var wantPluginAddPopup = false;
if (ImGui.BeginChild("###profileEditorPluginList"))
{
var pluginLineHeight = 32 * ImGuiHelpers.GlobalScale;
string? wantRemovePluginInternalName = null;
foreach (var plugin in profile.Plugins)
{
didAny = true;
var pmPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.InternalName == plugin.InternalName);
var btnOffset = 2;
if (pmPlugin != null)
{
pic.TryGetIcon(pmPlugin, pmPlugin.Manifest, pmPlugin.Manifest.IsThirdParty, out var icon);
icon ??= pic.DefaultIcon;
ImGui.Image(icon.ImGuiHandle, new Vector2(pluginLineHeight));
ImGui.SameLine();
var text = $"{pmPlugin.Name}";
var textHeight = ImGui.CalcTextSize(text);
var before = ImGui.GetCursorPos();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2));
ImGui.TextUnformatted(text);
ImGui.SetCursorPos(before);
}
else
{
ImGui.Image(pic.DefaultIcon.ImGuiHandle, new Vector2(pluginLineHeight));
ImGui.SameLine();
var text = Locs.NotInstalled(plugin.InternalName);
var textHeight = ImGui.CalcTextSize(text);
var before = ImGui.GetCursorPos();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2));
ImGui.TextUnformatted(text);
var available =
pm.AvailablePlugins.FirstOrDefault(
x => x.InternalName == plugin.InternalName && !x.SourceRepo.IsThirdParty);
if (available != null)
{
ImGui.SameLine();
ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * 2) - 2);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2));
btnOffset = 3;
if (ImGuiComponents.IconButton($"###installMissingPlugin{available.InternalName}", FontAwesomeIcon.Download))
{
this.installer.StartInstall(available, false);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.InstallPlugin);
}
ImGui.SetCursorPos(before);
}
ImGui.SameLine();
ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30));
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2));
var enabled = plugin.IsEnabled;
if (ImGui.Checkbox($"###{this.editingProfileGuid}-{plugin.InternalName}", ref enabled))
{
Task.Run(() => profile.AddOrUpdate(plugin.InternalName, enabled))
.ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState);
}
ImGui.SameLine();
ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * btnOffset) - 5);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2));
if (ImGuiComponents.IconButton($"###removePlugin{plugin.InternalName}", FontAwesomeIcon.Trash))
{
wantRemovePluginInternalName = plugin.InternalName;
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(Locs.RemovePlugin);
}
if (wantRemovePluginInternalName != null)
{
// TODO: handle error
profile.Remove(wantRemovePluginInternalName, false);
Task.Run(() => profman.ApplyAllWantStates())
.ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotRemove);
}
if (!didAny)
{
ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.NoPluginsInProfile);
}
ImGuiHelpers.ScaledDummy(10);
var addPluginsText = Locs.AddPlugin;
ImGuiHelpers.CenterCursorFor((int)(ImGui.CalcTextSize(addPluginsText).X + 30 + (ImGuiHelpers.GlobalScale * 5)));
if (ImGuiComponents.IconButton(FontAwesomeIcon.Plus))
wantPluginAddPopup = true;
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(5);
ImGui.SameLine();
ImGui.TextUnformatted(addPluginsText);
ImGuiHelpers.ScaledDummy(10);
ImGui.EndChild();
}
if (wantPluginAddPopup)
{
this.pickerSearch = string.Empty;
ImGui.OpenPopup(addPluginToProfilePopup);
}
}
private static class Locs
{
public static string TooltipEnableDisable =>
Loc.Localize("ProfileManagerEnableDisableHint", "Enable/Disable this collection");
public static string InstallPlugin => Loc.Localize("ProfileManagerInstall", "Install this plugin");
public static string RemovePlugin =>
Loc.Localize("ProfileManagerRemoveFromProfile", "Remove plugin from this collection");
public static string AddPlugin => Loc.Localize("ProfileManagerAddPlugin", "Add a plugin!");
public static string NoPluginsInProfile =>
Loc.Localize("ProfileManagerNoPluginsInProfile", "Collection has no plugins!");
public static string AlwaysEnableAtBoot =>
Loc.Localize("ProfileManagerAlwaysEnableAtBoot", "Always enable when game starts");
public static string DeleteProfileHint => Loc.Localize("ProfileManagerDeleteProfile", "Delete this collection");
public static string CopyToClipboardHint =>
Loc.Localize("ProfileManagerCopyToClipboard", "Copy collection to clipboard for sharing");
public static string CopyToClipboardNotification =>
Loc.Localize("ProfileManagerCopyToClipboardHint", "Copied to clipboard!");
public static string BackToOverview => Loc.Localize("ProfileManagerBackToOverview", "Back to overview");
public static string SearchHint => Loc.Localize("ProfileManagerSearchHint", "Search...");
public static string AddProfileHint => Loc.Localize("ProfileManagerAddProfileHint", "No collections! Add one!");
public static string CloneProfileHint => Loc.Localize("ProfileManagerCloneProfile", "Clone this collection");
public static string EditProfileHint => Loc.Localize("ProfileManagerEditProfile", "Edit this collection");
public static string ImportProfileHint =>
Loc.Localize("ProfileManagerImportProfile", "Import a shared collection from your clipboard");
public static string AddProfile => Loc.Localize("ProfileManagerAddProfile", "Add a new collection");
public static string NotificationImportSuccess =>
Loc.Localize("ProfileManagerNotificationImportSuccess", "Collection successfully imported!");
public static string NotificationImportError =>
Loc.Localize("ProfileManagerNotificationImportError", "Could not import collection.");
public static string ErrorCouldNotRemove =>
Loc.Localize("ProfileManagerCouldNotRemove", "Could not remove plugin.");
public static string ErrorCouldNotChangeState =>
Loc.Localize("ProfileManagerCouldNotChangeState", "Could not change plugin state.");
public static string NotInstalled(string name) =>
Loc.Localize("ProfileManagerNotInstalled", "{0} (Not Installed)").Format(name);
}
}

View file

@ -23,6 +23,8 @@ namespace Dalamud.Interface.Internal.Windows;
internal class PluginStatWindow : Window
{
private bool showDalamudHooks;
private string drawSearchText = string.Empty;
private string frameworkSearchText = string.Empty;
private string hookSearchText = string.Empty;
/// <summary>
@ -79,6 +81,12 @@ internal class PluginStatWindow : Window
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Collective Average", $"{(loadedPlugins.Any() ? totalAverage / loadedPlugins.Count() / 10000f : 0):F4}ms", "Average of all average draw times");
ImGui.InputTextWithHint(
"###PluginStatWindow_DrawSearch",
"Search",
ref this.drawSearchText,
500);
if (ImGui.BeginTable(
"##PluginStatsDrawTimes",
4,
@ -104,16 +112,22 @@ internal class PluginStatWindow : Window
? loadedPlugins.OrderBy(plugin => plugin.Name)
: loadedPlugins.OrderByDescending(plugin => plugin.Name),
2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.UiBuilder.MaxDrawTime)
: loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.UiBuilder.MaxDrawTime),
? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.UiBuilder.MaxDrawTime ?? 0)
: loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.UiBuilder.MaxDrawTime ?? 0),
3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.UiBuilder.DrawTimeHistory.Average())
: loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.UiBuilder.DrawTimeHistory.Average()),
? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.UiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0)
: loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.UiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0),
_ => loadedPlugins,
};
foreach (var plugin in loadedPlugins)
{
if (!this.drawSearchText.IsNullOrEmpty()
&& !plugin.Manifest.Name.Contains(this.drawSearchText, StringComparison.OrdinalIgnoreCase))
{
continue;
}
ImGui.TableNextRow();
ImGui.TableNextColumn();
@ -168,6 +182,12 @@ internal class PluginStatWindow : Window
ImGui.SameLine();
ImGuiComponents.TextWithLabel("Collective Average", $"{(statsHistory.Any() ? totalAverage / statsHistory.Length : 0):F4}ms", "Average of all average update times");
ImGui.InputTextWithHint(
"###PluginStatWindow_FrameworkSearch",
"Search",
ref this.frameworkSearchText,
500);
if (ImGui.BeginTable(
"##PluginStatsFrameworkTimes",
4,
@ -208,6 +228,13 @@ internal class PluginStatWindow : Window
continue;
}
if (!this.frameworkSearchText.IsNullOrEmpty()
&& handlerHistory.Key != null
&& !handlerHistory.Key.Contains(this.frameworkSearchText, StringComparison.OrdinalIgnoreCase))
{
continue;
}
ImGui.TableNextRow();
ImGui.TableNextColumn();

View file

@ -46,6 +46,14 @@ public class SettingsTabExperimental : SettingsTab
new GapSettingsEntry(5, true),
new ThirdRepoSettingsEntry(),
new GapSettingsEntry(5, true),
new SettingsEntry<bool>(
Loc.Localize("DalamudSettingsEnableProfiles", "Enable plugin collections"),
Loc.Localize("DalamudSettingsEnableProfilesHint", "Enables plugin collections, which lets you create toggleable lists of plugins."),
c => c.ProfilesEnabled,
(v, c) => c.ProfilesEnabled = v),
};
public override string Title => Loc.Localize("DalamudSettingsExperimental", "Experimental");

View file

@ -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);

View file

@ -23,8 +23,16 @@ internal sealed class SettingsEntry<T> : SettingsEntry
private object? valueBacking;
private object? fallbackValue;
public SettingsEntry(string name, string description, LoadSettingDelegate load, SaveSettingDelegate save, Action<T?>? change = null, Func<T?, string?>? warning = null, Func<T?, string?>? validity = null, Func<bool>? visibility = null,
object? fallbackValue = null)
public SettingsEntry(
string name,
string description,
LoadSettingDelegate load,
SaveSettingDelegate save,
Action<T?>? change = null,
Func<T?, string?>? warning = null,
Func<T?, string?>? validity = null,
Func<bool>? visibility = null,
object? fallbackValue = null)
{
this.load = load;
this.save = save;

View file

@ -11,6 +11,7 @@ using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Style;
using Dalamud.Interface.Windowing;
using Dalamud.Utility;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using Serilog;
@ -97,7 +98,7 @@ public class StyleEditorWindow : Window
this.SaveStyle();
var newStyle = StyleModelV1.DalamudStandard;
newStyle.Name = GetRandomName();
newStyle.Name = Util.GetRandomName();
config.SavedStyles.Add(newStyle);
this.currentSel = config.SavedStyles.Count - 1;
@ -167,11 +168,11 @@ public class StyleEditorWindow : Window
{
var newStyle = StyleModel.Deserialize(styleJson);
newStyle.Name ??= GetRandomName();
newStyle.Name ??= Util.GetRandomName();
if (config.SavedStyles.Any(x => x.Name == newStyle.Name))
{
newStyle.Name = $"{newStyle.Name} ({GetRandomName()} Mix)";
newStyle.Name = $"{newStyle.Name} ({Util.GetRandomName()} Mix)";
}
config.SavedStyles.Add(newStyle);
@ -375,15 +376,6 @@ public class StyleEditorWindow : Window
}
}
private static string GetRandomName()
{
var data = Service<DataManager>.Get();
var names = data.GetExcelSheet<BNpcName>(ClientLanguage.English);
var rng = new Random();
return names.ElementAt(rng.Next(0, names.Count() - 1)).Singular.RawString;
}
private void SaveStyle()
{
if (this.currentSel < 2)

View file

@ -254,7 +254,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable
var initialCursor = ImGui.GetCursorPos();
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)shadeEasing.Value))
{
ImGui.Image(this.shadeTexture.ImGuiHandle, new Vector2(this.shadeTexture.Width * scale, this.shadeTexture.Height * scale));

View file

@ -33,12 +33,12 @@ public sealed class UiBuilder : IDisposable
private readonly GameFontManager gameFontManager = Service<GameFontManager>.Get();
private readonly DragDropManager dragDropManager = Service<DragDropManager>.Get();
private bool hasErrorWindow = false;
private bool lastFrameUiHideState = false;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private bool hasErrorWindow = false;
private bool lastFrameUiHideState = false;
/// <summary>
/// Initializes a new instance of the <see cref="UiBuilder"/> class and registers it.
/// You do not have to call this manually.

View file

@ -0,0 +1,21 @@
using System;
namespace Dalamud.IoC.Internal;
/// <summary>
/// Indicates that an interface a service can implement can be used to resolve that service.
/// Take care: only one service can implement an interface with this attribute at a time.
/// </summary>
/// <typeparam name="T">The interface that can be used to resolve the service.</typeparam>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
internal class ResolveViaAttribute<T> : ResolveViaAttribute
{
}
/// <summary>
/// Helper class used for matching. Use the generic version.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
internal class ResolveViaAttribute : Attribute
{
}

View file

@ -1,12 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
namespace Dalamud.IoC.Internal;
@ -18,6 +18,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
private static readonly ModuleLog Log = new("SERVICECONTAINER");
private readonly Dictionary<Type, ObjectInstance> instances = new();
private readonly Dictionary<Type, Type> interfaceToTypeMap = new();
/// <summary>
/// Initializes a new instance of the <see cref="ServiceContainer"/> class.
@ -39,6 +40,20 @@ internal class ServiceContainer : IServiceProvider, IServiceType
}
this.instances[typeof(T)] = new(instance.ContinueWith(x => new WeakReference(x.Result)), typeof(T));
var resolveViaTypes = typeof(T)
.GetCustomAttributes()
.OfType<ResolveViaAttribute>()
.Select(x => x.GetType().GetGenericArguments().First());
foreach (var resolvableType in resolveViaTypes)
{
Log.Verbose("=> {InterfaceName} provides for {TName}", resolvableType.FullName ?? "???", typeof(T).FullName ?? "???");
Debug.Assert(!this.interfaceToTypeMap.ContainsKey(resolvableType), "A service already implements this interface, this is not allowed");
Debug.Assert(typeof(T).IsAssignableTo(resolvableType), "Service does not inherit from indicated ResolveVia type");
this.interfaceToTypeMap[resolvableType] = typeof(T);
}
}
/// <summary>
@ -233,6 +248,9 @@ internal class ServiceContainer : IServiceProvider, IServiceType
private async Task<object?> GetService(Type serviceType)
{
if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType))
serviceType = implementingType;
if (!this.instances.TryGetValue(serviceType, out var service))
return null;

View file

@ -277,6 +277,12 @@ public class PluginLog : IServiceType, IDisposable
public static void LogRaw(LogEventLevel level, Exception? exception, string messageTemplate, params object[] values)
=> WriteLog(Assembly.GetCallingAssembly().GetName().Name, level, messageTemplate, exception, values);
/// <inheritdoc/>
void IDisposable.Dispose()
{
// ignored
}
#region New instanced methods
/// <summary>
@ -290,12 +296,6 @@ public class PluginLog : IServiceType, IDisposable
#endregion
/// <inheritdoc/>
void IDisposable.Dispose()
{
// ignored
}
private static ILogger GetPluginLogger(string? pluginName)
{
return Serilog.Log.ForContext("SourceContext", pluginName ?? string.Empty);

View file

@ -1132,6 +1132,7 @@ internal static partial class NativeFunctions
/// <summary>
/// Native kernel32 functions.
/// </summary>
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1629:Documentation text should end with a period", Justification = "Stupid rule")]
internal static partial class NativeFunctions
{
/// <summary>
@ -1394,7 +1395,7 @@ internal static partial class NativeFunctions
}
/// <summary>
/// HEAP_* from heapapi
/// HEAP_* from heapapi.
/// </summary>
[Flags]
public enum HeapOptions

View file

@ -91,11 +91,7 @@ public class HappyEyeballsCallback : IDisposable
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;
}
token.ThrowIfCancellationRequested();
var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
{
@ -116,9 +112,9 @@ public class HappyEyeballsCallback : IDisposable
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
// 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.
// 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);

View file

@ -24,6 +24,7 @@ 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;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
@ -57,20 +58,10 @@ 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();
private readonly DirectoryInfo pluginDirectory;
private readonly DirectoryInfo devPluginDirectory;
private readonly BannedPlugin[]? bannedPlugins;
private readonly DalamudLinkPayload openInstallerWindowPluginChangelogsLink;
@ -81,6 +72,9 @@ Thanks and have fun!";
[ServiceManager.ServiceDependency]
private readonly DalamudStartInfo startInfo = Service<DalamudStartInfo>.Get();
[ServiceManager.ServiceDependency]
private readonly ProfileManager profileManager = Service<ProfileManager>.Get();
[ServiceManager.ServiceDependency]
private readonly HappyHttpClient happyHttpClient = Service<HappyHttpClient>.Get();
@ -88,18 +82,10 @@ Thanks and have fun!";
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
@ -394,9 +380,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())
{
@ -428,7 +411,7 @@ Thanks and have fun!";
try
{
pluginDefs.Add(versionsDefs.OrderByDescending(x => x.Manifest!.EffectiveVersion).First());
pluginDefs.Add(versionsDefs.MaxBy(x => x.Manifest!.EffectiveVersion));
}
catch (Exception ex)
{
@ -437,7 +420,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)
{
@ -660,11 +643,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)
{
@ -836,122 +816,6 @@ Thanks and have fun!";
return plugin;
}
/// <summary>
/// Load a plugin.
/// </summary>
/// <param name="dllFile">The <see cref="FileInfo"/> associated with the main assembly of this plugin.</param>
/// <param name="manifest">The already loaded definition, if available.</param>
/// <param name="reason">The reason this plugin was loaded.</param>
/// <param name="isDev">If this plugin should support development features.</param>
/// <param name="isBoot">If this plugin is being loaded at boot.</param>
/// <param name="doNotLoad">Don't load the plugin, just don't do it.</param>
/// <returns>The loaded plugin.</returns>
public async Task<LocalPlugin> LoadPluginAsync(FileInfo dllFile, LocalPluginManifest? manifest, PluginLoadReason reason, bool isDev = false, bool isBoot = false, bool doNotLoad = false)
{
var name = manifest?.Name ?? dllFile.Name;
var loadPlugin = !doNotLoad;
LocalPlugin plugin;
if (manifest != null && manifest.InternalName == null)
{
Log.Error("{FileName}: Your manifest has no internal name set! Can't load this.", dllFile.FullName);
throw new Exception("No internal name");
}
if (isDev)
{
Log.Information($"Loading dev plugin {name}");
var devPlugin = new LocalDevPlugin(dllFile, manifest);
loadPlugin &= !isBoot || devPlugin.StartOnBoot;
// If we're not loading it, make sure it's disabled
if (!loadPlugin && !devPlugin.IsDisabled)
devPlugin.Disable();
plugin = devPlugin;
}
else
{
Log.Information($"Loading plugin {name}");
plugin = new LocalPlugin(dllFile, manifest);
}
if (loadPlugin)
{
try
{
if (!plugin.IsDisabled && !plugin.IsOrphaned)
{
await plugin.LoadAsync(reason);
}
else
{
Log.Verbose($"{name} not loaded, disabled:{plugin.IsDisabled} orphaned:{plugin.IsOrphaned}");
}
}
catch (InvalidPluginException)
{
PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
throw;
}
catch (BannedPluginException)
{
// Out of date plugins get added so they can be updated.
Log.Information($"Plugin was banned, adding anyways: {dllFile.Name}");
}
catch (Exception ex)
{
if (plugin.IsDev)
{
// Dev plugins always get added to the list so they can be fiddled with in the UI
Log.Information(ex, $"Dev plugin failed to load, adding anyways: {dllFile.Name}");
// NOTE(goat): This can't work - plugins don't "unload" if they fail to load.
// plugin.Disable(); // Disable here, otherwise you can't enable+load later
}
else if (plugin.IsOutdated)
{
// Out of date plugins get added, so they can be updated.
Log.Information(ex, $"Plugin was outdated, adding anyways: {dllFile.Name}");
}
else if (plugin.IsOrphaned)
{
// Orphaned plugins get added, so that users aren't confused.
Log.Information(ex, $"Plugin was orphaned, adding anyways: {dllFile.Name}");
}
else if (isBoot)
{
// During boot load, plugins always get added to the list so they can be fiddled with in the UI
Log.Information(ex, $"Regular plugin failed to load, adding anyways: {dllFile.Name}");
// NOTE(goat): This can't work - plugins don't "unload" if they fail to load.
// plugin.Disable(); // Disable here, otherwise you can't enable+load later
}
else if (!plugin.CheckPolicy())
{
// During boot load, plugins always get added to the list so they can be fiddled with in the UI
Log.Information(ex, $"Plugin not loaded due to policy, adding anyways: {dllFile.Name}");
// NOTE(goat): This can't work - plugins don't "unload" if they fail to load.
// plugin.Disable(); // Disable here, otherwise you can't enable+load later
}
else
{
PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
throw;
}
}
}
lock (this.pluginListLock)
{
this.InstalledPlugins = this.InstalledPlugins.Add(plugin);
}
return plugin;
}
/// <summary>
/// Remove a plugin.
/// </summary>
@ -1081,7 +945,7 @@ Thanks and have fun!";
if (plugin.InstalledPlugin.IsDev)
continue;
if (plugin.InstalledPlugin.Manifest.Disabled && ignoreDisabled)
if (!plugin.InstalledPlugin.IsWantedByAnyProfile && ignoreDisabled)
continue;
if (plugin.InstalledPlugin.Manifest.ScheduledForDeletion)
@ -1143,40 +1007,28 @@ Thanks and have fun!";
if (plugin.IsDev)
{
try
{
plugin.DllFile.Delete();
lock (this.pluginListLock)
{
this.InstalledPlugins = this.InstalledPlugins.Remove(plugin);
}
}
catch (Exception ex)
{
Log.Error(ex, "Error during delete (update)");
updateStatus.WasUpdated = false;
return updateStatus;
}
throw new Exception("We should never update a dev plugin");
}
else
try
{
try
{
// TODO: Why were we ever doing this? We should never be loading the old version in the first place
/*
if (!plugin.IsDisabled)
plugin.Disable();
*/
lock (this.pluginListLock)
{
this.InstalledPlugins = this.InstalledPlugins.Remove(plugin);
}
}
catch (Exception ex)
lock (this.pluginListLock)
{
Log.Error(ex, "Error during disable (update)");
updateStatus.WasUpdated = false;
return updateStatus;
this.InstalledPlugins = this.InstalledPlugins.Remove(plugin);
}
}
catch (Exception ex)
{
Log.Error(ex, "Error during disable (update)");
updateStatus.WasUpdated = false;
return updateStatus;
}
// We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates.
var dtr = Service<DtrBar>.Get();
@ -1347,6 +1199,137 @@ Thanks and have fun!";
/// <returns>The calling plugin, or null.</returns>
public LocalPlugin? FindCallingPlugin() => this.FindCallingPlugin(new StackTrace());
/// <summary>
/// Load a plugin.
/// </summary>
/// <param name="dllFile">The <see cref="FileInfo"/> associated with the main assembly of this plugin.</param>
/// <param name="manifest">The already loaded definition, if available.</param>
/// <param name="reason">The reason this plugin was loaded.</param>
/// <param name="isDev">If this plugin should support development features.</param>
/// <param name="isBoot">If this plugin is being loaded at boot.</param>
/// <param name="doNotLoad">Don't load the plugin, just don't do it.</param>
/// <returns>The loaded plugin.</returns>
private async Task<LocalPlugin> LoadPluginAsync(FileInfo dllFile, LocalPluginManifest? manifest, PluginLoadReason reason, bool isDev = false, bool isBoot = false, bool doNotLoad = false)
{
var name = manifest?.Name ?? dllFile.Name;
var loadPlugin = !doNotLoad;
LocalPlugin plugin;
if (manifest != null && manifest.InternalName == null)
{
Log.Error("{FileName}: Your manifest has no internal name set! Can't load this.", dllFile.FullName);
throw new Exception("No internal name");
}
if (isDev)
{
Log.Information($"Loading dev plugin {name}");
var devPlugin = new LocalDevPlugin(dllFile, manifest);
loadPlugin &= !isBoot || devPlugin.StartOnBoot;
var probablyInternalNameForThisPurpose = manifest?.InternalName ?? dllFile.Name;
var wantsInDefaultProfile =
this.profileManager.DefaultProfile.WantsPlugin(probablyInternalNameForThisPurpose);
if (wantsInDefaultProfile == false && devPlugin.StartOnBoot)
{
this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, true, false);
}
else if (wantsInDefaultProfile == true && !devPlugin.StartOnBoot)
{
this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, false, false);
}
plugin = devPlugin;
}
else
{
Log.Information($"Loading plugin {name}");
plugin = new LocalPlugin(dllFile, manifest);
}
#pragma warning disable CS0618
var defaultState = manifest?.Disabled != true && loadPlugin;
#pragma warning restore CS0618
// Need to do this here, so plugins that don't load are still added to the default profile
var wantToLoad = this.profileManager.GetWantState(plugin.Manifest.InternalName, defaultState);
if (loadPlugin)
{
try
{
if (wantToLoad && !plugin.IsOrphaned)
{
await plugin.LoadAsync(reason);
}
else
{
Log.Verbose($"{name} not loaded, wantToLoad:{wantToLoad} orphaned:{plugin.IsOrphaned}");
}
}
catch (InvalidPluginException)
{
PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
throw;
}
catch (BannedPluginException)
{
// Out of date plugins get added so they can be updated.
Log.Information($"Plugin was banned, adding anyways: {dllFile.Name}");
}
catch (Exception ex)
{
if (plugin.IsDev)
{
// Dev plugins always get added to the list so they can be fiddled with in the UI
Log.Information(ex, $"Dev plugin failed to load, adding anyways: {dllFile.Name}");
// NOTE(goat): This can't work - plugins don't "unload" if they fail to load.
// plugin.Disable(); // Disable here, otherwise you can't enable+load later
}
else if (plugin.IsOutdated)
{
// Out of date plugins get added, so they can be updated.
Log.Information(ex, $"Plugin was outdated, adding anyways: {dllFile.Name}");
}
else if (plugin.IsOrphaned)
{
// Orphaned plugins get added, so that users aren't confused.
Log.Information(ex, $"Plugin was orphaned, adding anyways: {dllFile.Name}");
}
else if (isBoot)
{
// During boot load, plugins always get added to the list so they can be fiddled with in the UI
Log.Information(ex, $"Regular plugin failed to load, adding anyways: {dllFile.Name}");
// NOTE(goat): This can't work - plugins don't "unload" if they fail to load.
// plugin.Disable(); // Disable here, otherwise you can't enable+load later
}
else if (!plugin.CheckPolicy())
{
// During boot load, plugins always get added to the list so they can be fiddled with in the UI
Log.Information(ex, $"Plugin not loaded due to policy, adding anyways: {dllFile.Name}");
// NOTE(goat): This can't work - plugins don't "unload" if they fail to load.
// plugin.Disable(); // Disable here, otherwise you can't enable+load later
}
else
{
PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _);
throw;
}
}
}
lock (this.pluginListLock)
{
this.InstalledPlugins = this.InstalledPlugins.Add(plugin);
}
return plugin;
}
private void DetectAvailablePluginUpdates()
{
var updatablePlugins = new List<AvailablePluginUpdate>();

View file

@ -0,0 +1,214 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Dalamud.Configuration.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Utility;
namespace Dalamud.Plugin.Internal.Profiles;
/// <summary>
/// Class representing a single runtime profile.
/// </summary>
internal class Profile
{
private static readonly ModuleLog Log = new("PROFILE");
private readonly ProfileManager manager;
private readonly ProfileModelV1 modelV1;
/// <summary>
/// Initializes a new instance of the <see cref="Profile"/> class.
/// </summary>
/// <param name="manager">The manager this profile belongs to.</param>
/// <param name="model">The model this profile is tied to.</param>
/// <param name="isDefaultProfile">Whether or not this profile is the default profile.</param>
/// <param name="isBoot">Whether or not this profile was initialized during bootup.</param>
public Profile(ProfileManager manager, ProfileModel model, bool isDefaultProfile, bool isBoot)
{
this.manager = manager;
this.IsDefaultProfile = isDefaultProfile;
this.modelV1 = model as ProfileModelV1 ??
throw new ArgumentException("Model was null or unhandled version");
// We don't actually enable plugins here, PM will do it on bootup
if (isDefaultProfile)
{
// Default profile cannot be disabled
this.IsEnabled = this.modelV1.IsEnabled = true;
this.Name = this.modelV1.Name = "DEFAULT";
}
else if (this.modelV1.AlwaysEnableOnBoot && isBoot)
{
this.IsEnabled = true;
Log.Verbose("{Guid} set enabled because bootup", this.modelV1.Guid);
}
else if (this.modelV1.IsEnabled)
{
this.IsEnabled = true;
Log.Verbose("{Guid} set enabled because remember", this.modelV1.Guid);
}
else
{
Log.Verbose("{Guid} not enabled", this.modelV1.Guid);
}
}
/// <summary>
/// Gets or sets this profile's name.
/// </summary>
public string Name
{
get => this.modelV1.Name;
set
{
this.modelV1.Name = value;
Service<DalamudConfiguration>.Get().QueueSave();
}
}
/// <summary>
/// Gets or sets a value indicating whether this profile shall always be enabled at boot.
/// </summary>
public bool AlwaysEnableAtBoot
{
get => this.modelV1.AlwaysEnableOnBoot;
set
{
this.modelV1.AlwaysEnableOnBoot = value;
Service<DalamudConfiguration>.Get().QueueSave();
}
}
/// <summary>
/// Gets this profile's guid.
/// </summary>
public Guid Guid => this.modelV1.Guid;
/// <summary>
/// Gets a value indicating whether or not this profile is currently enabled.
/// </summary>
public bool IsEnabled { get; private set; }
/// <summary>
/// Gets a value indicating whether or not this profile is the default profile.
/// </summary>
public bool IsDefaultProfile { get; }
/// <summary>
/// Gets all plugins declared in this profile.
/// </summary>
public IEnumerable<ProfilePluginEntry> Plugins =>
this.modelV1.Plugins.Select(x => new ProfilePluginEntry(x.InternalName, x.IsEnabled));
/// <summary>
/// Gets this profile's underlying model.
/// </summary>
public ProfileModel Model => this.modelV1;
/// <summary>
/// Set this profile's state. This cannot be called for the default profile.
/// This will block until all states have been applied.
/// </summary>
/// <param name="enabled">Whether or not the profile is enabled.</param>
/// <param name="apply">Whether or not the current state should immediately be applied.</param>
/// <exception cref="InvalidOperationException">Thrown when an untoggleable profile is toggled.</exception>
public void SetState(bool enabled, bool apply = true)
{
if (this.IsDefaultProfile)
throw new InvalidOperationException("Cannot set state of default profile");
Debug.Assert(this.IsEnabled != enabled, "Trying to set state of a profile to the same state");
this.IsEnabled = this.modelV1.IsEnabled = enabled;
Log.Verbose("Set state {State} for {Guid}", enabled, this.modelV1.Guid);
Service<DalamudConfiguration>.Get().QueueSave();
if (apply)
this.manager.ApplyAllWantStates();
}
/// <summary>
/// Check if this profile contains a specific plugin, and if it is enabled.
/// </summary>
/// <param name="internalName">The internal name of the plugin.</param>
/// <returns>Null if this profile does not declare the plugin, true if the profile declares the plugin and wants it enabled, false if the profile declares the plugin and does not want it enabled.</returns>
public bool? WantsPlugin(string internalName)
{
var entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName);
return entry?.IsEnabled;
}
/// <summary>
/// Add a plugin to this profile with the desired state, or change the state of a plugin in this profile.
/// This will block until all states have been applied.
/// </summary>
/// <param name="internalName">The internal name of the plugin.</param>
/// <param name="state">Whether or not the plugin should be enabled.</param>
/// <param name="apply">Whether or not the current state should immediately be applied.</param>
public void AddOrUpdate(string internalName, bool state, bool apply = true)
{
Debug.Assert(!internalName.IsNullOrEmpty(), "!internalName.IsNullOrEmpty()");
var existing = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName);
if (existing != null)
{
existing.IsEnabled = state;
}
else
{
this.modelV1.Plugins.Add(new ProfileModelV1.ProfileModelV1Plugin
{
InternalName = internalName,
IsEnabled = state,
});
}
// We need to remove this plugin from the default profile, if it declares it.
if (!this.IsDefaultProfile && this.manager.DefaultProfile.WantsPlugin(internalName) != null)
{
this.manager.DefaultProfile.Remove(internalName, false);
}
Service<DalamudConfiguration>.Get().QueueSave();
if (apply)
this.manager.ApplyAllWantStates();
}
/// <summary>
/// Remove a plugin from this profile.
/// This will block until all states have been applied.
/// </summary>
/// <param name="internalName">The internal name of the plugin.</param>
/// <param name="apply">Whether or not the current state should immediately be applied.</param>
public void Remove(string internalName, bool apply = true)
{
var entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName);
if (entry == null)
throw new ArgumentException($"No plugin \"{internalName}\" in profile \"{this.Guid}\"");
if (!this.modelV1.Plugins.Remove(entry))
throw new Exception("Couldn't remove plugin from model collection");
// We need to add this plugin back to the default profile, if we were the last profile to have it.
if (!this.manager.IsInAnyProfile(internalName))
{
if (!this.IsDefaultProfile)
{
this.manager.DefaultProfile.AddOrUpdate(internalName, entry.IsEnabled, false);
}
else
{
throw new Exception("Removed plugin from default profile, but wasn't in any other profile");
}
}
Service<DalamudConfiguration>.Get().QueueSave();
if (apply)
this.manager.ApplyAllWantStates();
}
}

View file

@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CheapLoc;
using Dalamud.Game;
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.Utility;
using Serilog;
namespace Dalamud.Plugin.Internal.Profiles;
/// <summary>
/// Service responsible for profile-related chat commands.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class ProfileCommandHandler : IServiceType, IDisposable
{
private readonly CommandManager cmd;
private readonly ProfileManager profileManager;
private readonly ChatGui chat;
private readonly Framework framework;
private List<(string, ProfileOp)> queue = new();
/// <summary>
/// Initializes a new instance of the <see cref="ProfileCommandHandler"/> class.
/// </summary>
/// <param name="cmd">Command handler.</param>
/// <param name="profileManager">Profile manager.</param>
/// <param name="chat">Chat handler.</param>
/// <param name="framework">Framework.</param>
[ServiceManager.ServiceConstructor]
public ProfileCommandHandler(CommandManager cmd, ProfileManager profileManager, ChatGui chat, Framework framework)
{
this.cmd = cmd;
this.profileManager = profileManager;
this.chat = chat;
this.framework = framework;
this.cmd.AddHandler("/xlenableprofile", new CommandInfo(this.OnEnableProfile)
{
HelpMessage = Loc.Localize("ProfileCommandsEnableHint", "Enable a collection. Usage: /xlenablecollection \"Collection Name\""),
ShowInHelp = true,
});
this.cmd.AddHandler("/xldisableprofile", new CommandInfo(this.OnDisableProfile)
{
HelpMessage = Loc.Localize("ProfileCommandsDisableHint", "Disable a collection. Usage: /xldisablecollection \"Collection Name\""),
ShowInHelp = true,
});
this.cmd.AddHandler("/xltoggleprofile", new CommandInfo(this.OnToggleProfile)
{
HelpMessage = Loc.Localize("ProfileCommandsToggleHint", "Toggle a collection. Usage: /xltogglecollection \"Collection Name\""),
ShowInHelp = true,
});
this.framework.Update += this.FrameworkOnUpdate;
}
private enum ProfileOp
{
Enable,
Disable,
Toggle,
}
/// <inheritdoc/>
public void Dispose()
{
this.cmd.RemoveHandler("/xlenablecollection");
this.cmd.RemoveHandler("/xldisablecollection");
this.cmd.RemoveHandler("/xltogglecollection");
this.framework.Update += this.FrameworkOnUpdate;
}
private void FrameworkOnUpdate(Framework framework1)
{
if (this.profileManager.IsBusy)
return;
if (this.queue.Count > 0)
{
var op = this.queue[0];
this.queue.RemoveAt(0);
var profile = this.profileManager.Profiles.FirstOrDefault(x => x.Name == op.Item1);
if (profile == null || profile.IsDefaultProfile)
return;
switch (op.Item2)
{
case ProfileOp.Enable:
if (!profile.IsEnabled)
profile.SetState(true, false);
break;
case ProfileOp.Disable:
if (profile.IsEnabled)
profile.SetState(false, false);
break;
case ProfileOp.Toggle:
profile.SetState(!profile.IsEnabled, false);
break;
default:
throw new ArgumentOutOfRangeException();
}
if (profile.IsEnabled)
{
this.chat.Print(Loc.Localize("ProfileCommandsEnabling", "Enabling collection \"{0}\"...").Format(profile.Name));
}
else
{
this.chat.Print(Loc.Localize("ProfileCommandsDisabling", "Disabling collection \"{0}\"...").Format(profile.Name));
}
Task.Run(() => this.profileManager.ApplyAllWantStates()).ContinueWith(t =>
{
if (!t.IsCompletedSuccessfully && t.Exception != null)
{
Log.Error(t.Exception, "Could not apply profiles through commands");
this.chat.PrintError(Loc.Localize("ProfileCommandsApplyFailed", "Failed to apply your collections. Please check the console for errors."));
}
else
{
this.chat.Print(Loc.Localize("ProfileCommandsApplySuccess", "Collections applied."));
}
});
}
}
private void OnEnableProfile(string command, string arguments)
{
var name = this.ValidateName(arguments);
if (name == null)
return;
this.queue = this.queue.Where(x => x.Item1 != name).ToList();
this.queue.Add((name, ProfileOp.Enable));
}
private void OnDisableProfile(string command, string arguments)
{
var name = this.ValidateName(arguments);
if (name == null)
return;
this.queue = this.queue.Where(x => x.Item1 != name).ToList();
this.queue.Add((name, ProfileOp.Disable));
}
private void OnToggleProfile(string command, string arguments)
{
var name = this.ValidateName(arguments);
if (name == null)
return;
this.queue.Add((name, ProfileOp.Toggle));
}
private string? ValidateName(string arguments)
{
var name = arguments.Replace("\"", string.Empty);
if (this.profileManager.Profiles.All(x => x.Name != name))
{
this.chat.PrintError($"No collection like \"{name}\".");
return null;
}
return name;
}
}

View file

@ -0,0 +1,282 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CheapLoc;
using Dalamud.Configuration.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Utility;
namespace Dalamud.Plugin.Internal.Profiles;
/// <summary>
/// Class responsible for managing plugin profiles.
/// </summary>
[ServiceManager.BlockingEarlyLoadedService]
internal class ProfileManager : IServiceType
{
private static readonly ModuleLog Log = new("PROFMAN");
private readonly DalamudConfiguration config;
private readonly List<Profile> profiles = new();
private volatile bool isBusy = false;
/// <summary>
/// Initializes a new instance of the <see cref="ProfileManager"/> class.
/// </summary>
/// <param name="config">Dalamud config.</param>
[ServiceManager.ServiceConstructor]
public ProfileManager(DalamudConfiguration config)
{
this.config = config;
this.LoadProfilesFromConfigInitially();
}
/// <summary>
/// Gets the default profile.
/// </summary>
public Profile DefaultProfile => this.profiles.First(x => x.IsDefaultProfile);
/// <summary>
/// Gets all profiles, including the default profile.
/// </summary>
public IEnumerable<Profile> Profiles => this.profiles;
/// <summary>
/// Gets a value indicating whether or not the profile manager is busy enabling/disabling plugins.
/// </summary>
public bool IsBusy => this.isBusy;
/// <summary>
/// Check if any enabled profile wants a specific plugin enabled.
/// </summary>
/// <param name="internalName">The internal name of the plugin.</param>
/// <param name="defaultState">The state the plugin shall be in, if it needs to be added.</param>
/// <param name="addIfNotDeclared">Whether or not the plugin should be added to the default preset, if it's not present in any preset.</param>
/// <returns>Whether or not the plugin shall be enabled.</returns>
public bool GetWantState(string internalName, bool defaultState, bool addIfNotDeclared = true)
{
var want = false;
var wasInAnyProfile = false;
foreach (var profile in this.profiles)
{
var state = profile.WantsPlugin(internalName);
if (state.HasValue)
{
want = want || (profile.IsEnabled && state.Value);
wasInAnyProfile = true;
}
}
if (!wasInAnyProfile && addIfNotDeclared)
{
Log.Warning("{Name} was not in any profile, adding to default with {Default}", internalName, defaultState);
this.DefaultProfile.AddOrUpdate(internalName, defaultState, false);
return defaultState;
}
return want;
}
/// <summary>
/// Check whether a plugin is declared in any profile.
/// </summary>
/// <param name="internalName">The internal name of the plugin.</param>
/// <returns>Whether or not the plugin is in any profile.</returns>
public bool IsInAnyProfile(string internalName)
=> this.profiles.Any(x => x.WantsPlugin(internalName) != null);
/// <summary>
/// Check whether a plugin is only in the default profile.
/// A plugin can never be in the default profile if it is in any other profile.
/// </summary>
/// <param name="internalName">The internal name of the plugin.</param>
/// <returns>Whether or not the plugin is in the default profile.</returns>
public bool IsInDefaultProfile(string internalName)
=> this.DefaultProfile.WantsPlugin(internalName) != null;
/// <summary>
/// Add a new profile.
/// </summary>
/// <returns>The added profile.</returns>
public Profile AddNewProfile()
{
var model = new ProfileModelV1
{
Guid = Guid.NewGuid(),
Name = this.GenerateUniqueProfileName(Loc.Localize("PluginProfilesNewProfile", "New Collection")),
IsEnabled = false,
};
this.config.SavedProfiles!.Add(model);
this.config.QueueSave();
var profile = new Profile(this, model, false, false);
this.profiles.Add(profile);
return profile;
}
/// <summary>
/// Clone a specified profile.
/// </summary>
/// <param name="toClone">The profile to clone.</param>
/// <returns>The newly cloned profile.</returns>
public Profile CloneProfile(Profile toClone)
{
var newProfile = this.ImportProfile(toClone.Model.Serialize());
if (newProfile == null)
throw new Exception("New profile was null while cloning");
return newProfile;
}
/// <summary>
/// Import a profile with a sharing string.
/// </summary>
/// <param name="data">The sharing string to import.</param>
/// <returns>The imported profile, or null, if the string was invalid.</returns>
public Profile? ImportProfile(string data)
{
var newModel = ProfileModel.Deserialize(data);
if (newModel == null)
return null;
newModel.Guid = Guid.NewGuid();
newModel.Name = this.GenerateUniqueProfileName(newModel.Name.IsNullOrEmpty() ? "Unknown Collection" : newModel.Name);
if (newModel is ProfileModelV1 modelV1)
modelV1.IsEnabled = false;
this.config.SavedProfiles!.Add(newModel);
this.config.QueueSave();
var profile = new Profile(this, newModel, false, false);
this.profiles.Add(profile);
return profile;
}
/// <summary>
/// Go through all profiles and plugins, and enable/disable plugins they want active.
/// This will block until all plugins have been loaded/reloaded.
/// </summary>
public void ApplyAllWantStates()
{
this.isBusy = true;
Log.Information("Getting want states...");
var wantActive = this.profiles
.Where(x => x.IsEnabled)
.SelectMany(profile => profile.Plugins.Where(plugin => plugin.IsEnabled)
.Select(plugin => plugin.InternalName))
.Distinct().ToList();
foreach (var internalName in wantActive)
{
Log.Information("\t=> Want {Name}", internalName);
}
Log.Information("Applying want states...");
var pm = Service<PluginManager>.Get();
var tasks = new List<Task>();
foreach (var installedPlugin in pm.InstalledPlugins)
{
var wantThis = wantActive.Contains(installedPlugin.Manifest.InternalName);
switch (wantThis)
{
case true when !installedPlugin.IsLoaded:
if (installedPlugin.ApplicableForLoad)
{
Log.Information("\t=> Enabling {Name}", installedPlugin.Manifest.InternalName);
tasks.Add(installedPlugin.LoadAsync(PluginLoadReason.Installer));
}
else
{
Log.Warning("\t=> {Name} wanted active, but not applicable", installedPlugin.Manifest.InternalName);
}
break;
case false when installedPlugin.IsLoaded:
Log.Information("\t=> Disabling {Name}", installedPlugin.Manifest.InternalName);
tasks.Add(installedPlugin.UnloadAsync());
break;
}
}
// This is probably not ideal... Might need to rethink the error handling strategy for this.
try
{
Task.WaitAll(tasks.ToArray());
}
catch (Exception e)
{
Log.Error(e, "Couldn't apply state for one or more plugins");
}
Log.Information("Applied!");
this.isBusy = false;
}
/// <summary>
/// Delete a profile.
/// </summary>
/// <remarks>
/// You should definitely apply states after this. It doesn't do it for you.
/// </remarks>
/// <param name="profile">The profile to delete.</param>
public void DeleteProfile(Profile profile)
{
// We need to remove all plugins from the profile first, so that they are re-added to the default profile if needed
foreach (var plugin in profile.Plugins.ToArray())
{
profile.Remove(plugin.InternalName, false);
}
if (!this.config.SavedProfiles!.Remove(profile.Model))
throw new Exception("Couldn't remove profile from models");
if (!this.profiles.Remove(profile))
throw new Exception("Couldn't remove runtime profile");
this.config.QueueSave();
}
private string GenerateUniqueProfileName(string startingWith)
{
if (this.profiles.All(x => x.Name != startingWith))
return startingWith;
startingWith = Regex.Replace(startingWith, @" \(.* Mix\)", string.Empty);
while (true)
{
var newName = $"{startingWith} ({CultureInfo.InvariantCulture.TextInfo.ToTitleCase(Util.GetRandomName())} Mix)";
if (this.profiles.All(x => x.Name != newName))
return newName;
}
}
private void LoadProfilesFromConfigInitially()
{
this.config.DefaultProfile ??= new ProfileModelV1();
this.profiles.Add(new Profile(this, this.config.DefaultProfile, true, true));
this.config.SavedProfiles ??= new List<ProfileModel>();
foreach (var profileModel in this.config.SavedProfiles)
{
this.profiles.Add(new Profile(this, profileModel, false, true));
}
this.config.QueueSave();
}
}

View file

@ -0,0 +1,60 @@
using System;
using Dalamud.Utility;
using Newtonsoft.Json;
namespace Dalamud.Plugin.Internal.Profiles;
/// <summary>
/// Class representing a profile.
/// </summary>
public abstract class ProfileModel
{
/// <summary>
/// Gets or sets the ID of the profile.
/// </summary>
[JsonProperty("id")]
public Guid Guid { get; set; } = Guid.Empty;
/// <summary>
/// Gets or sets the name of the profile.
/// </summary>
[JsonProperty("n")]
public string Name { get; set; } = "New Collection";
/// <summary>
/// Deserialize a profile into a model.
/// </summary>
/// <param name="model">The string to decompress.</param>
/// <returns>The parsed model.</returns>
/// <exception cref="ArgumentException">Thrown when the parsed string is not a valid profile.</exception>
public static ProfileModel? Deserialize(string model)
{
var json = Util.DecompressString(Convert.FromBase64String(model.Substring(3)));
if (model.StartsWith(ProfileModelV1.SerializedPrefix))
return JsonConvert.DeserializeObject<ProfileModelV1>(json);
throw new ArgumentException("Was not a compressed profile.");
}
/// <summary>
/// Serialize this model into a string usable for sharing.
/// </summary>
/// <returns>The serialized representation of the model.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when an unsupported model is serialized.</exception>
public string Serialize()
{
string prefix;
switch (this)
{
case ProfileModelV1:
prefix = ProfileModelV1.SerializedPrefix;
break;
default:
throw new ArgumentOutOfRangeException();
}
return prefix + Convert.ToBase64String(Util.CompressString(JsonConvert.SerializeObject(this)));
}
}

View file

@ -0,0 +1,55 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Dalamud.Plugin.Internal.Profiles;
/// <summary>
/// Version 1 of the profile model.
/// </summary>
public class ProfileModelV1 : ProfileModel
{
/// <summary>
/// Gets the prefix of this version.
/// </summary>
public static string SerializedPrefix => "DP1";
/// <summary>
/// Gets or sets a value indicating whether or not this profile should always be enabled at boot.
/// </summary>
[JsonProperty("b")]
public bool AlwaysEnableOnBoot { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether or not this profile is currently enabled.
/// </summary>
[JsonProperty("e")]
public bool IsEnabled { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating this profile's color.
/// </summary>
[JsonProperty("c")]
public uint Color { get; set; }
/// <summary>
/// Gets or sets the list of plugins in this profile.
/// </summary>
public List<ProfileModelV1Plugin> Plugins { get; set; } = new();
/// <summary>
/// Class representing a single plugin in a profile.
/// </summary>
public class ProfileModelV1Plugin
{
/// <summary>
/// Gets or sets the internal name of the plugin.
/// </summary>
public string? InternalName { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not this entry is enabled.
/// </summary>
public bool IsEnabled { get; set; }
}
}

View file

@ -0,0 +1,28 @@
namespace Dalamud.Plugin.Internal.Profiles;
/// <summary>
/// Class representing a single plugin in a profile.
/// </summary>
internal class ProfilePluginEntry
{
/// <summary>
/// Initializes a new instance of the <see cref="ProfilePluginEntry"/> class.
/// </summary>
/// <param name="internalName">The internal name of the plugin.</param>
/// <param name="state">A value indicating whether or not this entry is enabled.</param>
public ProfilePluginEntry(string internalName, bool state)
{
this.InternalName = internalName;
this.IsEnabled = state;
}
/// <summary>
/// Gets the internal name of the plugin.
/// </summary>
public string InternalName { get; }
/// <summary>
/// Gets a value indicating whether or not this entry is enabled.
/// </summary>
public bool IsEnabled { get; }
}

View file

@ -15,6 +15,7 @@ using Dalamud.Logging;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Exceptions;
using Dalamud.Plugin.Internal.Loader;
using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Utility;
namespace Dalamud.Plugin.Internal.Types;
@ -135,7 +136,9 @@ internal class LocalPlugin : IDisposable
this.disabledFile = LocalPluginManifest.GetDisabledFile(this.DllFile);
if (this.disabledFile.Exists)
{
#pragma warning disable CS0618
this.Manifest.Disabled = true;
#pragma warning restore CS0618
this.disabledFile.Delete();
}
@ -206,9 +209,11 @@ internal class LocalPlugin : IDisposable
public bool IsLoaded => this.State == PluginState.Loaded;
/// <summary>
/// Gets a value indicating whether the plugin is disabled.
/// Gets a value indicating whether this plugin is wanted active by any profile.
/// INCLUDES the default profile.
/// </summary>
public bool IsDisabled => this.Manifest.Disabled;
public bool IsWantedByAnyProfile =>
Service<ProfileManager>.Get().GetWantState(this.Manifest.InternalName, false, false);
/// <summary>
/// Gets a value indicating whether this plugin's API level is out of date.
@ -244,6 +249,12 @@ internal class LocalPlugin : IDisposable
/// </summary>
public bool IsDev => this is LocalDevPlugin;
/// <summary>
/// Gets a value indicating whether this plugin should be allowed to load.
/// </summary>
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>
@ -289,7 +300,6 @@ internal class LocalPlugin : IDisposable
/// <returns>A task.</returns>
public async Task LoadAsync(PluginLoadReason reason, bool reloading = false)
{
var configuration = await Service<DalamudConfiguration>.GetAsync();
var framework = await Service<Framework>.GetAsync();
var ioc = await Service<ServiceContainer>.GetAsync();
var pluginManager = await Service<PluginManager>.GetAsync();
@ -311,6 +321,10 @@ internal class LocalPlugin : IDisposable
this.ReloadManifest();
}
// If we reload a plugin we don't want to delete it. Makes sense, right?
this.Manifest.ScheduledForDeletion = false;
this.SaveManifest();
switch (this.State)
{
case PluginState.Loaded:
@ -348,8 +362,9 @@ internal class LocalPlugin : IDisposable
if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !pluginManager.LoadAllApiLevels)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level");
if (this.Manifest.Disabled)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, disabled");
// We might want to throw here?
if (!this.IsWantedByAnyProfile)
Log.Warning("{Name} is loading, but isn't wanted by any profile", this.Name);
if (this.IsOrphaned)
throw new InvalidPluginOperationException($"Plugin {this.Name} had no associated repo.");
@ -575,42 +590,6 @@ internal class LocalPlugin : IDisposable
await this.LoadAsync(PluginLoadReason.Reload, true);
}
/// <summary>
/// Revert a disable. Must be unloaded first, does not load.
/// </summary>
public void Enable()
{
// Allowed: Unloaded, UnloadError
switch (this.State)
{
case PluginState.Loading:
case PluginState.Unloading:
case PluginState.Loaded:
case PluginState.LoadError:
if (!this.IsDev)
throw new InvalidPluginOperationException($"Unable to enable {this.Name}, still loaded");
break;
case PluginState.Unloaded:
break;
case PluginState.UnloadError:
break;
case PluginState.DependencyResolutionFailed:
throw new InvalidPluginOperationException($"Unable to enable {this.Name}, dependency resolution failed");
default:
throw new ArgumentOutOfRangeException(this.State.ToString());
}
// NOTE(goat): This is inconsequential, and we do have situations where a plugin can end up enabled but not loaded:
// Orphaned plugins can have their repo added back, but may not have been loaded at boot and may still be enabled.
// We don't want to disable orphaned plugins when they are orphaned so this is how it's going to be.
// if (!this.Manifest.Disabled)
// throw new InvalidPluginOperationException($"Unable to enable {this.Name}, not disabled");
this.Manifest.Disabled = false;
this.Manifest.ScheduledForDeletion = false;
this.SaveManifest();
}
/// <summary>
/// Check if anything forbids this plugin from loading.
/// </summary>
@ -632,36 +611,6 @@ internal class LocalPlugin : IDisposable
return true;
}
/// <summary>
/// Disable this plugin, must be unloaded first.
/// </summary>
public void Disable()
{
// Allowed: Unloaded, UnloadError
switch (this.State)
{
case PluginState.Loading:
case PluginState.Unloading:
case PluginState.Loaded:
case PluginState.LoadError:
throw new InvalidPluginOperationException($"Unable to disable {this.Name}, still loaded");
case PluginState.Unloaded:
break;
case PluginState.UnloadError:
break;
case PluginState.DependencyResolutionFailed:
return; // This is a no-op.
default:
throw new ArgumentOutOfRangeException(this.State.ToString());
}
if (this.Manifest.Disabled)
throw new InvalidPluginOperationException($"Unable to disable {this.Name}, already disabled");
this.Manifest.Disabled = true;
this.SaveManifest();
}
/// <summary>
/// Schedule the deletion of this plugin on next cleanup.
/// </summary>
@ -680,9 +629,9 @@ internal class LocalPlugin : IDisposable
var manifest = LocalPluginManifest.GetManifestFile(this.DllFile);
if (manifest.Exists)
{
var isDisabled = this.IsDisabled; // saving the internal state because it could have been deleted
// var isDisabled = this.IsDisabled; // saving the internal state because it could have been deleted
this.Manifest = LocalPluginManifest.Load(manifest);
this.Manifest.Disabled = isDisabled;
// this.Manifest.Disabled = isDisabled;
this.SaveManifest();
}

View file

@ -28,6 +28,7 @@ internal record LocalPluginManifest : PluginManifest
/// Gets or sets a value indicating whether the plugin is disabled and should not be loaded.
/// This value supersedes the ".disabled" file functionality and should not be included in the plugin master.
/// </summary>
[Obsolete("This is merely used for migrations now. Use the profile manager to check if a plugin shall be enabled.")]
public bool Disabled { get; set; }
/// <summary>

View file

@ -162,6 +162,12 @@ internal record PluginManifest
[JsonProperty]
public bool CanUnloadAsync { get; init; }
/// <summary>
/// Gets a value indicating whether the plugin supports profiles.
/// </summary>
[JsonProperty]
public bool SupportsProfiles { get; init; } = true;
/// <summary>
/// Gets a list of screenshot image URLs to show in the plugin installer.
/// </summary>

View file

@ -30,3 +30,5 @@ public enum PluginLoadReason
/// </summary>
Boot,
}
// TODO(api9): This should be a mask, so that we can combine Installer | ProfileLoaded

View file

@ -0,0 +1,23 @@
using System.Collections.Generic;
using Dalamud.Game.ClientState.Aetherytes;
namespace Dalamud.Plugin.Services;
/// <summary>
/// This collection represents the list of available Aetherytes in the Teleport window.
/// </summary>
public interface IAetheryteList : IReadOnlyCollection<AetheryteEntry>
{
/// <summary>
/// Gets the amount of Aetherytes the local player has unlocked.
/// </summary>
public int Length { get; }
/// <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 AetheryteEntry? this[int index] { get; }
}

View file

@ -16,18 +16,15 @@ using JetBrains.Annotations;
namespace Dalamud;
// TODO:
// - Unify dependency walking code(load/unload
// - Visualize/output .dot or imgui thing
/// <summary>
/// Class to initialize Service&lt;T&gt;s.
/// </summary>
internal static class ServiceManager
{
/**
* TODO:
* - Unify dependency walking code(load/unload
* - Visualize/output .dot or imgui thing
*/
/// <summary>
/// Static log facility for Service{T}, to avoid duplicate instances for different types.
/// </summary>

View file

@ -141,7 +141,6 @@ internal static class Service<T> where T : IServiceType
.OfType<InherentDependencyAttribute>()
.Select(x => x.GetType().GetGenericArguments().First()));
// HACK: PluginManager needs to depend on ALL plugin exposed services
if (typeof(T) == typeof(PluginManager))
{

View file

@ -1,6 +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;

View file

@ -68,7 +68,7 @@ public static class Troubleshooting
var payload = new TroubleshootingPayload
{
LoadedPlugins = pluginManager?.InstalledPlugins?.Select(x => x.Manifest)?.OrderByDescending(x => x.InternalName).ToArray(),
PluginStates = pluginManager?.InstalledPlugins?.ToDictionary(x => x.Manifest.InternalName, x => x.IsBanned ? "Banned" : x.State.ToString()),
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(),

View file

@ -19,7 +19,7 @@ public static class AsyncUtils
/// <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>
/// <returns>Returns the first task that completes, according to <see cref="Task.IsCompletedSuccessfully"/>.</returns>
public static Task<T> FirstSuccessfulTask<T>(ICollection<Task<T>> tasks)
{
var tcs = new TaskCompletionSource<T>();
@ -51,7 +51,7 @@ public static class AsyncUtils
{
try
{
await Task.Delay(millisecondsDelay, cancellationToken);
await Task.Delay(millisecondsDelay, cancellationToken).ConfigureAwait(false);
}
catch (TaskCanceledException)
{

View file

@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Client.Graphics;
namespace Dalamud.Utility.Numerics;

View file

@ -11,6 +11,7 @@ using System.Runtime.CompilerServices;
using System.Text;
using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Interface;
@ -18,6 +19,7 @@ using Dalamud.Interface.Colors;
using Dalamud.Logging.Internal;
using Dalamud.Networking.Http;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using Microsoft.Win32;
using Serilog;
@ -657,6 +659,19 @@ public static class Util
File.Move(tmpPath, path, true);
}
/// <summary>
/// Gets a random, inoffensive, human-friendly string.
/// </summary>
/// <returns>A random human-friendly name.</returns>
internal static string GetRandomName()
{
var data = Service<DataManager>.Get();
var names = data.GetExcelSheet<BNpcName>(ClientLanguage.English)!;
var rng = new Random();
return names.ElementAt(rng.Next(0, names.Count() - 1)).Singular.RawString;
}
private static unsafe void ShowValue(ulong addr, IEnumerable<string> path, Type type, object value)
{
if (type.IsPointer)

View file

@ -26,23 +26,22 @@ Thanks to Mino, whose work has made this possible!
These components are used in order to load Dalamud into a target process.
Dalamud can be loaded via DLL injection, or by rewriting a process' entrypoint.
| Name | Purpose |
|---|---|
| *Dalamud.Injector.Boot* (C++) | Loads the .NET Core runtime into a process via hostfxr and kicks off Dalamud.Injector |
| *Dalamud.Injector* (C#) | Performs DLL injection on the target process |
| *Dalamud.Boot* (C++) | Loads the .NET Core runtime into the active process and kicks off Dalamud, or rewrites a target process' entrypoint to do so |
| *Dalamud* (C#) | Core API, game bindings, plugin framework |
| *Dalamud.CorePlugin* (C#) | Testbed plugin that can access Dalamud internals, to prototype new Dalamud features |
| Name | Purpose |
|-------------------------------|------------------------------------------------------------------------------------------------------------------------------|
| *Dalamud.Injector.Boot* (C++) | Loads the .NET Core runtime into a process via hostfxr and kicks off Dalamud.Injector |
| *Dalamud.Injector* (C#) | Performs DLL injection on the target process |
| *Dalamud.Boot* (C++) | Loads the .NET Core runtime into the active process and kicks off Dalamud, or rewrites a target process' entrypoint to do so |
| *Dalamud* (C#) | Core API, game bindings, plugin framework |
| *Dalamud.CorePlugin* (C#) | Testbed plugin that can access Dalamud internals, to prototype new Dalamud features |
## Branches
We are currently working from the following branches.
| Name | Purpose | .NET Version | Track |
|---|---|---|---|
| *master* | Current release branch | .NET 6.0.3 (March 2022) | Release & Staging |
| *net7* | Upgrade to .NET 7 | .NET 7.0.0 (November 2022) | net7 |
| *api3* | Legacy version, no longer in active use | .NET Framework 4.7.2 (April 2017) | - |
| Name | API Level | Purpose | .NET Version | Track |
|----------|-----------|------------------------------------------------------------|----------------------------|-------------------|
| *master* | **8** | Current release branch | .NET 7.0.0 (November 2022) | Release & Staging |
| *v9* | **9** | Next major version, slated for release alongside Patch 6.5 | .NET 7.0.0 (November 2022) | v9 |
<br>

@ -1 +1 @@
Subproject commit 010e878febb631c8f3ff5ff90d656f318e35b1de
Subproject commit e61e4d8fa9e58e37b454314f0f9a9f305f173483

@ -1 +1 @@
Subproject commit 262d3b0668196fb236e2191c4a37e9be94e5a7a3
Subproject commit 2f37349ffd778561a1103a650683116c43edc86c