Merge branch 'net5'

This commit is contained in:
goaaats 2022-05-12 11:09:57 +02:00
commit 6dcddb1f29
No known key found for this signature in database
GPG key ID: 49E2AA8C6A76498B
30 changed files with 2600 additions and 2349 deletions

View file

@ -123,10 +123,12 @@ resharper_foreach_can_be_partly_converted_to_query_using_another_get_enumerator_
resharper_invert_if_highlighting = none
resharper_loop_can_be_converted_to_query_highlighting = none
resharper_method_has_async_overload_highlighting = none
resharper_private_field_can_be_converted_to_local_variable_highlighting = none
resharper_redundant_base_qualifier_highlighting = none
resharper_suggest_var_or_type_built_in_types_highlighting = hint
resharper_suggest_var_or_type_elsewhere_highlighting = hint
resharper_suggest_var_or_type_simple_types_highlighting = hint
resharper_unused_auto_property_accessor_global_highlighting = none
csharp_style_deconstructed_variable_declaration=true:silent
[*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}]

View file

@ -1,6 +1,5 @@
name: Tag Build
on: [push]
concurrency: build_dalamud
jobs:
tag:

View file

@ -37,9 +37,9 @@ namespace Dalamud.Injector
/// <param name="argvPtr">byte** string arguments.</param>
public static void Main(int argc, IntPtr argvPtr)
{
Init();
List<string> args = new(argc);
Init(args);
unsafe
{
var argv = (IntPtr*)argvPtr;
@ -59,7 +59,11 @@ namespace Dalamud.Injector
// No command defaults to inject
args.Add("inject");
args.Add("--all");
#if !DEBUG
args.Add("--warn");
#endif
}
else if (int.TryParse(args[1], out var _))
{
@ -92,15 +96,13 @@ namespace Dalamud.Injector
}
else
{
Console.WriteLine("Invalid command: {0}", mainCommand);
ProcessHelpCommand(args);
Environment.Exit(-1);
throw new CommandLineException($"\"{mainCommand}\" is not a valid command.");
}
}
private static void Init()
private static void Init(List<string> args)
{
InitUnhandledException();
InitUnhandledException(args);
InitLogging();
var cwd = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory;
@ -111,25 +113,31 @@ namespace Dalamud.Injector
}
}
private static void InitUnhandledException()
private static void InitUnhandledException(List<string> args)
{
AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
{
if (Log.Logger == null)
var exObj = eventArgs.ExceptionObject;
if (exObj is CommandLineException clex)
{
Console.WriteLine();
Console.WriteLine("Command line error: {0}", clex.Message);
Console.WriteLine();
ProcessHelpCommand(args);
Environment.Exit(-1);
}
else if (Log.Logger == null)
{
Console.WriteLine($"A fatal error has occurred: {eventArgs.ExceptionObject}");
}
else if (exObj is Exception ex)
{
Log.Error(ex, "A fatal error has occurred.");
}
else
{
var exObj = eventArgs.ExceptionObject;
if (exObj is Exception ex)
{
Log.Error(ex, "A fatal error has occurred.");
}
else
{
Log.Error($"A fatal error has occurred: {eventArgs.ExceptionObject}");
}
Log.Error($"A fatal error has occurred: {eventArgs.ExceptionObject}");
}
#if DEBUG
@ -146,7 +154,7 @@ namespace Dalamud.Injector
#endif
_ = MessageBoxW(IntPtr.Zero, message, caption, MessageBoxType.IconError | MessageBoxType.Ok);
Environment.Exit(0);
Environment.Exit(-1);
};
}
@ -225,6 +233,9 @@ namespace Dalamud.Injector
private static DalamudStartInfo ExtractAndInitializeStartInfoFromArguments(DalamudStartInfo? startInfo, List<string> args)
{
int len;
string key;
if (startInfo == null)
startInfo = new();
@ -234,10 +245,10 @@ namespace Dalamud.Injector
var defaultPluginDirectory = startInfo.DefaultPluginDirectory;
var assetDirectory = startInfo.AssetDirectory;
var delayInitializeMs = startInfo.DelayInitializeMs;
var languageStr = startInfo.Language.ToString().ToLowerInvariant();
for (var i = 2; i < args.Count; i++)
{
string key;
if (args[i].StartsWith(key = "--dalamud-working-directory="))
workingDirectory = args[i][key.Length..];
else if (args[i].StartsWith(key = "--dalamud-configuration-path="))
@ -250,6 +261,8 @@ namespace Dalamud.Injector
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
continue;
@ -266,6 +279,26 @@ namespace Dalamud.Injector
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 = "deutsche").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.");
return new()
{
WorkingDirectory = workingDirectory,
@ -273,7 +306,7 @@ namespace Dalamud.Injector
PluginDirectory = pluginDirectory,
DefaultPluginDirectory = defaultPluginDirectory,
AssetDirectory = assetDirectory,
Language = ClientLanguage.English,
Language = clientLanguage,
GameVersion = null,
DelayInitializeMs = delayInitializeMs,
};
@ -299,12 +332,14 @@ namespace Dalamud.Injector
Console.WriteLine("{0} [-g path/to/ffxiv_dx11.exe] [--game=path/to/ffxiv_dx11.exe]", exeSpaces);
Console.WriteLine("{0} [-m entrypoint|inject] [--mode=entrypoint|inject]", exeSpaces);
Console.WriteLine("{0} [--handle-owner=inherited-handle-value]", exeSpaces);
Console.WriteLine("{0} [--without-dalamud]", exeSpaces);
Console.WriteLine("{0} [-- game_arg1=value1 game_arg2=value2 ...]", exeSpaces);
}
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-asset-directory path] [--dalamud-delay-initialize 0(ms)]");
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-asset-directory=path] [--dalamud-delay-initialize=0(ms)]");
Console.WriteLine(" [--dalamud-client-language=0-3|j(apanese)|e(nglish)|d|g(erman)|f(rench)]");
return 0;
}
@ -349,8 +384,7 @@ namespace Dalamud.Injector
}
else
{
Log.Error("\"{0}\" is not a valid argument.", args[i]);
return -1;
throw new CommandLineException($"\"{args[i]}\" is not a command line argument.");
}
}
@ -362,8 +396,7 @@ namespace Dalamud.Injector
if (!targetProcessSpecified)
{
Log.Error("No target process has been specified.");
return -1;
throw new CommandLineException("No target process has been specified. Use -a(--all) option to inject to all ffxiv_dx11.exe processes.");
}
else if (!processes.Any())
{
@ -397,6 +430,7 @@ namespace Dalamud.Injector
var useFakeArguments = false;
var showHelp = args.Count <= 2;
var handleOwner = IntPtr.Zero;
var withoutDalamud = false;
var parsingGameArgument = false;
for (var i = 2; i < args.Count; i++)
@ -408,42 +442,25 @@ 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] == "-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="))
{
gamePath = args[i].Split('=', 2)[1];
}
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
{
Log.Error("No such command found: {0}", args[i]);
return -1;
}
throw new CommandLineException($"\"{args[i]}\" is not a command line argument.");
}
if (showHelp)
@ -463,8 +480,7 @@ namespace Dalamud.Injector
}
else
{
Log.Error("Invalid mode: {0}", mode);
return -1;
throw new CommandLineException($"\"{mode}\" is not a valid Dalamud load mode.");
}
if (gamePath == null)
@ -522,7 +538,7 @@ namespace Dalamud.Injector
"DEV.LobbyHost09=127.0.0.9",
"DEV.LobbyPort09=54994",
"SYS.Region=0",
"language=1",
$"language={(int)dalamudStartInfo.Language}",
$"ver={gameVersion}",
$"DEV.MaxEntitledExpansionID={maxEntitledExpansionId}",
"DEV.GMServerHost=127.0.0.100",
@ -533,7 +549,7 @@ namespace Dalamud.Injector
var gameArgumentString = string.Join(" ", gameArguments.Select(x => EncodeParameterArgument(x)));
var process = NativeAclFix.LaunchGame(Path.GetDirectoryName(gamePath), gamePath, gameArgumentString, (Process p) =>
{
if (mode == "entrypoint")
if (!withoutDalamud && mode == "entrypoint")
{
var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath);
Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo));
@ -545,7 +561,7 @@ namespace Dalamud.Injector
}
});
if (mode == "inject")
if (!withoutDalamud && mode == "inject")
{
var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath);
Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo));
@ -625,7 +641,7 @@ namespace Dalamud.Injector
PluginDirectory = startInfo.PluginDirectory,
DefaultPluginDirectory = startInfo.DefaultPluginDirectory,
AssetDirectory = startInfo.AssetDirectory,
Language = ClientLanguage.English,
Language = startInfo.Language,
GameVersion = gameVer,
DelayInitializeMs = startInfo.DelayInitializeMs,
};
@ -734,5 +750,13 @@ namespace Dalamud.Injector
return quoted.ToString();
}
private class CommandLineException : Exception
{
public CommandLineException(string cause)
: base(cause)
{
}
}
}
}

View file

@ -50,11 +50,15 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=bannedplugin/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=clientopcode/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Dalamud/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=FFXIV/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Flytext/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Gpose/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=LOCALPLUGIN/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=lumina/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Materia/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=PLUGINM/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=pluginmaster/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=PLUGINR/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Refilter/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=serveropcode/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Universalis/@EntryIndexedValue">True</s:Boolean>

View file

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

View file

@ -4,6 +4,7 @@ using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Game.Gui;
using Dalamud.Game.Gui.Toast;
@ -26,7 +27,9 @@ namespace Dalamud.Game
public sealed class Framework : IDisposable
{
private static Stopwatch statsStopwatch = new();
private Stopwatch updateStopwatch = new();
private readonly List<RunOnNextTickTaskBase> runOnNextTickTaskList = new();
private readonly Stopwatch updateStopwatch = new();
private bool tier2Initialized = false;
private bool tier3Initialized = false;
@ -36,6 +39,8 @@ namespace Dalamud.Game
private Hook<OnDestroyDetour> destroyHook;
private Hook<OnRealDestroyDelegate> realDestroyHook;
private Thread? frameworkUpdateThread;
/// <summary>
/// Initializes a new instance of the <see cref="Framework"/> class.
/// </summary>
@ -113,6 +118,11 @@ namespace Dalamud.Game
/// </summary>
public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero;
/// <summary>
/// Gets a value indicating whether currently executing code is running in the game's framework update thread.
/// </summary>
public bool IsInFrameworkUpdateThread => Thread.CurrentThread == this.frameworkUpdateThread;
/// <summary>
/// Gets or sets a value indicating whether to dispatch update events.
/// </summary>
@ -132,6 +142,84 @@ namespace Dalamud.Game
this.realDestroyHook.Enable();
}
/// <summary>
/// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call.
/// </summary>
/// <typeparam name="T">Return type.</typeparam>
/// <param name="func">Function to call.</param>
/// <returns>Task representing the pending or already completed function.</returns>
public Task<T> RunOnFrameworkThread<T>(Func<T> func) => this.IsInFrameworkUpdateThread ? Task.FromResult(func()) : this.RunOnTick(func);
/// <summary>
/// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call.
/// </summary>
/// <param name="action">Function to call.</param>
/// <returns>Task representing the pending or already completed function.</returns>
public Task RunOnFrameworkThread(Action action)
{
if (this.IsInFrameworkUpdateThread)
{
try
{
action();
return Task.CompletedTask;
}
catch (Exception ex)
{
return Task.FromException(ex);
}
}
else
{
return this.RunOnTick(action);
}
}
/// <summary>
/// Run given function in upcoming Framework.Tick call.
/// </summary>
/// <typeparam name="T">Return type.</typeparam>
/// <param name="func">Function to call.</param>
/// <param name="delay">Wait for given timespan before calling this function.</param>
/// <param name="delayTicks">Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter.</param>
/// <param name="cancellationToken">Cancellation token which will prevent the execution of this function if wait conditions are not met.</param>
/// <returns>Task representing the pending function.</returns>
public Task<T> RunOnTick<T>(Func<T> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
{
var tcs = new TaskCompletionSource<T>();
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc<T>()
{
RemainingTicks = delayTicks,
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
CancellationToken = cancellationToken,
TaskCompletionSource = tcs,
Func = func,
});
return tcs.Task;
}
/// <summary>
/// Run given function in upcoming Framework.Tick call.
/// </summary>
/// <param name="action">Function to call.</param>
/// <param name="delay">Wait for given timespan before calling this function.</param>
/// <param name="delayTicks">Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter.</param>
/// <param name="cancellationToken">Cancellation token which will prevent the execution of this function if wait conditions are not met.</param>
/// <returns>Task representing the pending function.</returns>
public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
{
var tcs = new TaskCompletionSource();
this.runOnNextTickTaskList.Add(new RunOnNextTickTaskAction()
{
RemainingTicks = delayTicks,
RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds),
CancellationToken = cancellationToken,
TaskCompletionSource = tcs,
Action = action,
});
return tcs.Task;
}
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
@ -179,6 +267,8 @@ namespace Dalamud.Game
if (this.tierInitError)
goto original;
this.frameworkUpdateThread ??= Thread.CurrentThread;
var dalamud = Service<Dalamud>.Get();
// If this is the first time we are running this loop, we need to init Dalamud subsystems synchronously
@ -223,6 +313,8 @@ namespace Dalamud.Game
try
{
this.runOnNextTickTaskList.RemoveAll(x => x.Run());
if (StatsEnabled && this.Update != null)
{
// Stat Tracking for Framework Updates
@ -312,5 +404,88 @@ namespace Dalamud.Game
// Return the original trampoline location to cleanly exit
return originalPtr;
}
private abstract class RunOnNextTickTaskBase
{
internal int RemainingTicks { get; set; }
internal long RunAfterTickCount { get; init; }
internal CancellationToken CancellationToken { get; init; }
internal bool Run()
{
if (this.CancellationToken.IsCancellationRequested)
{
this.CancelImpl();
return true;
}
if (this.RemainingTicks > 0)
this.RemainingTicks -= 1;
if (this.RemainingTicks > 0)
return false;
if (this.RunAfterTickCount > Environment.TickCount64)
return false;
this.RunImpl();
return true;
}
protected abstract void RunImpl();
protected abstract void CancelImpl();
}
private class RunOnNextTickTaskFunc<T> : RunOnNextTickTaskBase
{
internal TaskCompletionSource<T> TaskCompletionSource { get; init; }
internal Func<T> Func { get; init; }
protected override void RunImpl()
{
try
{
this.TaskCompletionSource.SetResult(this.Func());
}
catch (Exception ex)
{
this.TaskCompletionSource.SetException(ex);
}
}
protected override void CancelImpl()
{
this.TaskCompletionSource.SetCanceled();
}
}
private class RunOnNextTickTaskAction : RunOnNextTickTaskBase
{
internal TaskCompletionSource TaskCompletionSource { get; init; }
internal Action Action { get; init; }
protected override void RunImpl()
{
try
{
this.Action();
this.TaskCompletionSource.SetResult();
}
catch (Exception ex)
{
this.TaskCompletionSource.SetException(ex);
}
}
protected override void CancelImpl()
{
this.TaskCompletionSource.SetCanceled();
}
}
}
}

View file

@ -7,6 +7,7 @@ using System.Text;
using Dalamud.Data;
using Dalamud.Interface.Internal;
using Dalamud.Utility;
using ImGuiNET;
using Lumina.Data.Files;
using Serilog;
@ -38,6 +39,9 @@ namespace Dalamud.Interface.GameFonts
private readonly Dictionary<GameFontStyle, int> fontUseCounter = new();
private readonly Dictionary<GameFontStyle, Dictionary<char, Tuple<int, FdtReader.FontTableEntry>>> glyphRectIds = new();
private bool isBetweenBuildFontsAndAfterBuildFonts = false;
private bool isBuildingAsFallbackFontMode = false;
/// <summary>
/// Initializes a new instance of the <see cref="GameFontManager"/> class.
/// </summary>
@ -110,65 +114,6 @@ namespace Dalamud.Interface.GameFonts
};
}
/// <summary>
/// Fills missing glyphs in target font from source font, if both are not null.
/// </summary>
/// <param name="source">Source font.</param>
/// <param name="target">Target font.</param>
/// <param name="missingOnly">Whether to copy missing glyphs only.</param>
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
/// <param name="rangeLow">Low codepoint range to copy.</param>
/// <param name="rangeHigh">High codepoing range to copy.</param>
public static void CopyGlyphsAcrossFonts(ImFontPtr? source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable, int rangeLow = 32, int rangeHigh = 0xFFFE)
{
if (!source.HasValue || !target.HasValue)
return;
var scale = target.Value!.FontSize / source.Value!.FontSize;
unsafe
{
var glyphs = (ImFontGlyphReal*)source.Value!.Glyphs.Data;
for (int j = 0, j_ = source.Value!.Glyphs.Size; j < j_; j++)
{
var glyph = &glyphs[j];
if (glyph->Codepoint < rangeLow || glyph->Codepoint > rangeHigh)
continue;
var prevGlyphPtr = (ImFontGlyphReal*)target.Value!.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr;
if ((IntPtr)prevGlyphPtr == IntPtr.Zero)
{
target.Value!.AddGlyph(
target.Value!.ConfigData,
(ushort)glyph->Codepoint,
glyph->X0 * scale,
((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent,
glyph->X1 * scale,
((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent,
glyph->U0,
glyph->V0,
glyph->U1,
glyph->V1,
glyph->AdvanceX * scale);
}
else if (!missingOnly)
{
prevGlyphPtr->X0 = glyph->X0 * scale;
prevGlyphPtr->Y0 = ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent;
prevGlyphPtr->X1 = glyph->X1 * scale;
prevGlyphPtr->Y1 = ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent;
prevGlyphPtr->U0 = glyph->U0;
prevGlyphPtr->V0 = glyph->V0;
prevGlyphPtr->U1 = glyph->U1;
prevGlyphPtr->V1 = glyph->V1;
prevGlyphPtr->AdvanceX = glyph->AdvanceX * scale;
}
}
}
if (rebuildLookupTable)
target.Value!.BuildLookupTable();
}
/// <summary>
/// Unscales fonts after they have been rendered onto atlas.
/// </summary>
@ -191,7 +136,7 @@ namespace Dalamud.Interface.GameFonts
font->Descent /= fontScale;
if (font->ConfigData != null)
font->ConfigData->SizePixels /= fontScale;
var glyphs = (ImFontGlyphReal*)font->Glyphs.Data;
var glyphs = (ImGuiHelpers.ImFontGlyphReal*)font->Glyphs.Data;
for (int i = 0, i_ = font->Glyphs.Size; i < i_; i++)
{
var glyph = &glyphs[i];
@ -223,15 +168,22 @@ namespace Dalamud.Interface.GameFonts
lock (this.syncRoot)
{
var prevValue = this.fontUseCounter.GetValueOrDefault(style, 0);
var newValue = this.fontUseCounter[style] = prevValue + 1;
needRebuild = (prevValue == 0) != (newValue == 0) && !this.fonts.ContainsKey(style);
this.fontUseCounter[style] = this.fontUseCounter.GetValueOrDefault(style, 0) + 1;
}
needRebuild = !this.fonts.ContainsKey(style);
if (needRebuild)
{
Log.Information("[GameFontManager] Calling RebuildFonts because {0} has been requested.", style.ToString());
this.interfaceManager.RebuildFonts();
if (Service<InterfaceManager>.Get().IsBuildingFontsBeforeAtlasBuild && this.isBetweenBuildFontsAndAfterBuildFonts)
{
Log.Information("[GameFontManager] NewFontRef: Building {0} right now, as it is called while BuildFonts is already in progress yet atlas build has not been called yet.", style.ToString());
this.EnsureFont(style);
}
else
{
Log.Information("[GameFontManager] NewFontRef: Calling RebuildFonts because {0} has been requested.", style.ToString());
this.interfaceManager.RebuildFonts();
}
}
return new(this, style);
@ -260,7 +212,7 @@ namespace Dalamud.Interface.GameFonts
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable)
{
GameFontManager.CopyGlyphsAcrossFonts(source, this.fonts[target], missingOnly, rebuildLookupTable);
ImGuiHelpers.CopyGlyphsAcrossFonts(source, this.fonts[target], missingOnly, rebuildLookupTable);
}
/// <summary>
@ -272,7 +224,7 @@ namespace Dalamud.Interface.GameFonts
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
public void CopyGlyphsAcrossFonts(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable)
{
GameFontManager.CopyGlyphsAcrossFonts(this.fonts[source], target, missingOnly, rebuildLookupTable);
ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], target, missingOnly, rebuildLookupTable);
}
/// <summary>
@ -284,7 +236,7 @@ namespace Dalamud.Interface.GameFonts
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
public void CopyGlyphsAcrossFonts(GameFontStyle source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable)
{
GameFontManager.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable);
ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable);
}
/// <summary>
@ -293,57 +245,20 @@ namespace Dalamud.Interface.GameFonts
/// <param name="forceMinSize">Whether to load fonts in minimum sizes.</param>
public void BuildFonts(bool forceMinSize)
{
unsafe
{
ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig();
fontConfig.OversampleH = 1;
fontConfig.OversampleV = 1;
fontConfig.PixelSnapH = false;
this.isBuildingAsFallbackFontMode = forceMinSize;
this.isBetweenBuildFontsAndAfterBuildFonts = true;
var io = ImGui.GetIO();
this.glyphRectIds.Clear();
this.fonts.Clear();
this.glyphRectIds.Clear();
this.fonts.Clear();
foreach (var style in this.fontUseCounter.Keys)
{
var rectIds = this.glyphRectIds[style] = new();
var fdt = this.fdts[(int)(forceMinSize ? style.FamilyWithMinimumSize : style.FamilyAndSize)];
if (fdt == null)
continue;
var font = io.Fonts.AddFontDefault(fontConfig);
this.fonts[style] = font;
foreach (var glyph in fdt.Glyphs)
{
var c = glyph.Char;
if (c < 32 || c >= 0xFFFF)
continue;
var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph);
rectIds[c] = Tuple.Create(
io.Fonts.AddCustomRectFontGlyph(
font,
c,
glyph.BoundingWidth + widthAdjustment + 1,
glyph.BoundingHeight + 1,
glyph.AdvanceWidth,
new Vector2(0, glyph.CurrentOffsetY)),
glyph);
}
}
fontConfig.Destroy();
}
foreach (var style in this.fontUseCounter.Keys)
this.EnsureFont(style);
}
/// <summary>
/// Post-build fonts before plugins do something more. To be called from InterfaceManager.
/// </summary>
/// <param name="forceMinSize">Whether to load fonts in minimum sizes.</param>
public unsafe void AfterBuildFonts(bool forceMinSize)
public unsafe void AfterBuildFonts()
{
var ioFonts = ImGui.GetIO().Fonts;
ioFonts.GetTexDataAsRGBA32(out byte* pixels8, out var width, out var height);
@ -352,7 +267,7 @@ namespace Dalamud.Interface.GameFonts
foreach (var (style, font) in this.fonts)
{
var fdt = this.fdts[(int)(forceMinSize ? style.FamilyWithMinimumSize : style.FamilyAndSize)];
var fdt = this.fdts[(int)(this.isBuildingAsFallbackFontMode ? style.FamilyWithMinimumSize : style.FamilyAndSize)];
var scale = style.SizePt / fdt.FontHeader.Size;
var fontPtr = font.NativePtr;
fontPtr->FontSize = fdt.FontHeader.Size * 4 / 3;
@ -449,10 +364,12 @@ namespace Dalamud.Interface.GameFonts
}
}
CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, font, true, false);
ImGuiHelpers.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, font, true, false);
UnscaleFont(font, 1 / scale, false);
font.BuildLookupTable();
}
this.isBetweenBuildFontsAndAfterBuildFonts = false;
}
/// <summary>
@ -471,35 +388,41 @@ namespace Dalamud.Interface.GameFonts
}
}
private struct ImFontGlyphReal
private unsafe void EnsureFont(GameFontStyle style)
{
public uint ColoredVisibleCodepoint;
public float AdvanceX;
public float X0;
public float Y0;
public float X1;
public float Y1;
public float U0;
public float V0;
public float U1;
public float V1;
var rectIds = this.glyphRectIds[style] = new();
public bool Colored
{
get => ((this.ColoredVisibleCodepoint >> 0) & 1) != 0;
set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 0xFFFFFFFEu) | (value ? 1u : 0u);
}
var fdt = this.fdts[(int)(this.isBuildingAsFallbackFontMode ? style.FamilyWithMinimumSize : style.FamilyAndSize)];
if (fdt == null)
return;
public bool Visible
{
get => ((this.ColoredVisibleCodepoint >> 1) & 1) != 0;
set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 0xFFFFFFFDu) | (value ? 2u : 0u);
}
ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig();
fontConfig.OversampleH = 1;
fontConfig.OversampleV = 1;
fontConfig.PixelSnapH = false;
public int Codepoint
var io = ImGui.GetIO();
var font = io.Fonts.AddFontDefault(fontConfig);
fontConfig.Destroy();
this.fonts[style] = font;
foreach (var glyph in fdt.Glyphs)
{
get => (int)(this.ColoredVisibleCodepoint >> 2);
set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 3u) | ((uint)this.Codepoint << 2);
var c = glyph.Char;
if (c < 32 || c >= 0xFFFF)
continue;
var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph);
rectIds[c] = Tuple.Create(
io.Fonts.AddCustomRectFontGlyph(
font,
c,
glyph.BoundingWidth + widthAdjustment + 1,
glyph.BoundingHeight + 1,
glyph.AdvanceWidth,
new Vector2(0, glyph.CurrentOffsetY)),
glyph);
}
}
}

View file

@ -20,8 +20,7 @@ namespace Dalamud.Interface.ImGuiFileDialog
/// <param name="callback">The action to execute when the dialog is finished.</param>
public void OpenFolderDialog(string title, Action<bool, string> callback)
{
this.SetCallback(callback);
this.SetDialog("OpenFolderDialog", title, string.Empty, this.savedPath, ".", string.Empty, 1, false, ImGuiFileDialogFlags.SelectOnly);
this.SetDialog("OpenFolderDialog", title, string.Empty, this.savedPath, ".", string.Empty, 1, false, ImGuiFileDialogFlags.SelectOnly, callback);
}
/// <summary>
@ -33,8 +32,7 @@ namespace Dalamud.Interface.ImGuiFileDialog
/// <param name="isModal">Whether the dialog should be a modal popup.</param>
public void OpenFolderDialog(string title, Action<bool, string> callback, string? startPath, bool isModal = false)
{
this.SetCallback(callback);
this.SetDialog("OpenFolderDialog", title, string.Empty, startPath ?? this.savedPath, ".", string.Empty, 1, isModal, ImGuiFileDialogFlags.SelectOnly);
this.SetDialog("OpenFolderDialog", title, string.Empty, startPath ?? this.savedPath, ".", string.Empty, 1, isModal, ImGuiFileDialogFlags.SelectOnly, callback);
}
/// <summary>
@ -45,8 +43,7 @@ namespace Dalamud.Interface.ImGuiFileDialog
/// <param name="callback">The action to execute when the dialog is finished.</param>
public void SaveFolderDialog(string title, string defaultFolderName, Action<bool, string> callback)
{
this.SetCallback(callback);
this.SetDialog("SaveFolderDialog", title, string.Empty, this.savedPath, defaultFolderName, string.Empty, 1, false, ImGuiFileDialogFlags.None);
this.SetDialog("SaveFolderDialog", title, string.Empty, this.savedPath, defaultFolderName, string.Empty, 1, false, ImGuiFileDialogFlags.None, callback);
}
/// <summary>
@ -59,8 +56,7 @@ namespace Dalamud.Interface.ImGuiFileDialog
/// <param name="isModal">Whether the dialog should be a modal popup.</param>
public void SaveFolderDialog(string title, string defaultFolderName, Action<bool, string> callback, string? startPath, bool isModal = false)
{
this.SetCallback(callback);
this.SetDialog("SaveFolderDialog", title, string.Empty, startPath ?? this.savedPath, defaultFolderName, string.Empty, 1, isModal, ImGuiFileDialogFlags.None);
this.SetDialog("SaveFolderDialog", title, string.Empty, startPath ?? this.savedPath, defaultFolderName, string.Empty, 1, isModal, ImGuiFileDialogFlags.None, callback);
}
/// <summary>
@ -71,8 +67,7 @@ namespace Dalamud.Interface.ImGuiFileDialog
/// <param name="callback">The action to execute when the dialog is finished.</param>
public void OpenFileDialog(string title, string filters, Action<bool, string> callback)
{
this.SetCallback(callback);
this.SetDialog("OpenFileDialog", title, filters, this.savedPath, ".", string.Empty, 1, false, ImGuiFileDialogFlags.SelectOnly);
this.SetDialog("OpenFileDialog", title, filters, this.savedPath, ".", string.Empty, 1, false, ImGuiFileDialogFlags.SelectOnly, callback);
}
/// <summary>
@ -81,19 +76,18 @@ namespace Dalamud.Interface.ImGuiFileDialog
/// <param name="title">The header title of the dialog.</param>
/// <param name="filters">Which files to show in the dialog.</param>
/// <param name="callback">The action to execute when the dialog is finished.</param>
/// <param name="startPath">The directory which the dialog should start inside of. The last path this manager was in is used if this is null.</param>
/// <param name="selectionCountMax">The maximum amount of files or directories which can be selected. Set to 0 for an infinite number.</param>
/// <param name="startPath">The directory which the dialog should start inside of. The last path this manager was in is used if this is null.</param>
/// <param name="isModal">Whether the dialog should be a modal popup.</param>
public void OpenFileDialog(
string title,
string filters,
Action<bool, List<string>> callback,
int selectionCountMax,
string? startPath = null,
int selectionCountMax = 1,
bool isModal = false)
{
this.SetCallback(callback);
this.SetDialog("OpenFileDialog", title, filters, startPath ?? this.savedPath, ".", string.Empty, selectionCountMax, isModal, ImGuiFileDialogFlags.SelectOnly);
this.SetDialog("OpenFileDialog", title, filters, startPath ?? this.savedPath, ".", string.Empty, selectionCountMax, isModal, ImGuiFileDialogFlags.SelectOnly, callback);
}
/// <summary>
@ -111,8 +105,7 @@ namespace Dalamud.Interface.ImGuiFileDialog
string defaultExtension,
Action<bool, string> callback)
{
this.SetCallback(callback);
this.SetDialog("SaveFileDialog", title, filters, this.savedPath, defaultFileName, defaultExtension, 1, false, ImGuiFileDialogFlags.None);
this.SetDialog("SaveFileDialog", title, filters, this.savedPath, defaultFileName, defaultExtension, 1, false, ImGuiFileDialogFlags.None, callback);
}
/// <summary>
@ -134,8 +127,7 @@ namespace Dalamud.Interface.ImGuiFileDialog
string? startPath,
bool isModal = false)
{
this.SetCallback(callback);
this.SetDialog("SaveFileDialog", title, filters, startPath ?? this.savedPath, defaultFileName, defaultExtension, 1, isModal, ImGuiFileDialogFlags.None);
this.SetDialog("SaveFileDialog", title, filters, startPath ?? this.savedPath, defaultFileName, defaultExtension, 1, isModal, ImGuiFileDialogFlags.None, callback);
}
/// <summary>
@ -166,18 +158,6 @@ namespace Dalamud.Interface.ImGuiFileDialog
this.multiCallback = null;
}
private void SetCallback(Action<bool, string> action)
{
this.callback = action;
this.multiCallback = null;
}
private void SetCallback(Action<bool, List<string>> action)
{
this.multiCallback = action;
this.callback = null;
}
private void SetDialog(
string id,
string title,
@ -187,9 +167,19 @@ namespace Dalamud.Interface.ImGuiFileDialog
string defaultExtension,
int selectionCountMax,
bool isModal,
ImGuiFileDialogFlags flags)
ImGuiFileDialogFlags flags,
Delegate callback)
{
this.Reset();
if (callback is Action<bool, List<string>> multi)
{
this.multiCallback = multi;
}
else
{
this.callback = callback as Action<bool, string>;
}
this.dialog = new FileDialog(id, title, filters, path, defaultFileName, defaultExtension, selectionCountMax, isModal, flags);
this.dialog.Show();
}

View file

@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using ImGuiNET;
@ -136,6 +139,67 @@ namespace Dalamud.Interface
/// <param name="text">The text to write.</param>
public static void SafeTextWrapped(string text) => ImGui.TextWrapped(text.Replace("%", "%%"));
/// <summary>
/// Fills missing glyphs in target font from source font, if both are not null.
/// </summary>
/// <param name="source">Source font.</param>
/// <param name="target">Target font.</param>
/// <param name="missingOnly">Whether to copy missing glyphs only.</param>
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
/// <param name="rangeLow">Low codepoint range to copy.</param>
/// <param name="rangeHigh">High codepoing range to copy.</param>
public static void CopyGlyphsAcrossFonts(ImFontPtr? source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable, int rangeLow = 32, int rangeHigh = 0xFFFE)
{
if (!source.HasValue || !target.HasValue)
return;
var scale = target.Value!.FontSize / source.Value!.FontSize;
unsafe
{
var glyphs = (ImFontGlyphReal*)source.Value!.Glyphs.Data;
for (int j = 0, k = source.Value!.Glyphs.Size; j < k; j++)
{
Debug.Assert(glyphs != null, nameof(glyphs) + " != null");
var glyph = &glyphs[j];
if (glyph->Codepoint < rangeLow || glyph->Codepoint > rangeHigh)
continue;
var prevGlyphPtr = (ImFontGlyphReal*)target.Value!.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr;
if ((IntPtr)prevGlyphPtr == IntPtr.Zero)
{
target.Value!.AddGlyph(
target.Value!.ConfigData,
(ushort)glyph->Codepoint,
glyph->X0 * scale,
((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent,
glyph->X1 * scale,
((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent,
glyph->U0,
glyph->V0,
glyph->U1,
glyph->V1,
glyph->AdvanceX * scale);
}
else if (!missingOnly)
{
prevGlyphPtr->X0 = glyph->X0 * scale;
prevGlyphPtr->Y0 = ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent;
prevGlyphPtr->X1 = glyph->X1 * scale;
prevGlyphPtr->Y1 = ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent;
prevGlyphPtr->U0 = glyph->U0;
prevGlyphPtr->V0 = glyph->V0;
prevGlyphPtr->U1 = glyph->U1;
prevGlyphPtr->V1 = glyph->V1;
prevGlyphPtr->AdvanceX = glyph->AdvanceX * scale;
}
}
}
if (rebuildLookupTable)
target.Value!.BuildLookupTable();
}
/// <summary>
/// Get data needed for each new frame.
/// </summary>
@ -143,5 +207,41 @@ namespace Dalamud.Interface
{
GlobalScale = ImGui.GetIO().FontGlobalScale;
}
/// <summary>
/// ImFontGlyph the correct version.
/// </summary>
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "ImGui internals")]
public struct ImFontGlyphReal
{
public uint ColoredVisibleCodepoint;
public float AdvanceX;
public float X0;
public float Y0;
public float X1;
public float Y1;
public float U0;
public float V0;
public float U1;
public float V1;
public bool Colored
{
get => ((this.ColoredVisibleCodepoint >> 0) & 1) != 0;
set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 0xFFFFFFFEu) | (value ? 1u : 0u);
}
public bool Visible
{
get => ((this.ColoredVisibleCodepoint >> 1) & 1) != 0;
set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 0xFFFFFFFDu) | (value ? 2u : 0u);
}
public int Codepoint
{
get => (int)(this.ColoredVisibleCodepoint >> 2);
set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 3u) | ((uint)value << 2);
}
}
}
}

View file

@ -248,6 +248,11 @@ namespace Dalamud.Interface.Internal
/// </summary>
public int FontResolutionLevel => this.FontResolutionLevelOverride ?? Service<DalamudConfiguration>.Get().FontResolutionLevel;
/// <summary>
/// Gets a value indicating whether we're building fonts but haven't generated atlas yet.
/// </summary>
public bool IsBuildingFontsBeforeAtlasBuild => this.isRebuildingFonts && !this.fontBuildSignal.WaitOne(0);
/// <summary>
/// Enable this module.
/// </summary>
@ -900,7 +905,7 @@ namespace Dalamud.Interface.Internal
texPixels[i] = (byte)(Math.Pow(texPixels[i] / 255.0f, 1.0f / fontGamma) * 255.0f);
}
gameFontManager.AfterBuildFonts(disableBigFonts);
gameFontManager.AfterBuildFonts();
foreach (var (font, mod) in this.loadedFontInfo)
{
@ -929,14 +934,14 @@ namespace Dalamud.Interface.Internal
font.Descent = mod.SourceAxis.ImFont.Descent;
font.FallbackChar = mod.SourceAxis.ImFont.FallbackChar;
font.EllipsisChar = mod.SourceAxis.ImFont.EllipsisChar;
GameFontManager.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, false, false);
ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, false, false);
}
else if (mod.Axis == TargetFontModification.AxisMode.GameGlyphsOnly)
{
Log.Verbose("[FONT] {0}: Overwrite game specific glyphs from AXIS of size {1}px", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize);
if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr)
mod.SourceAxis.ImFont.FontSize -= 1;
GameFontManager.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, true, false, 0xE020, 0xE0DB);
ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, true, false, 0xE020, 0xE0DB);
if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr)
mod.SourceAxis.ImFont.FontSize += 1;
}
@ -946,7 +951,7 @@ namespace Dalamud.Interface.Internal
}
// Fill missing glyphs in MonoFont from DefaultFont
GameFontManager.CopyGlyphsAcrossFonts(DefaultFont, MonoFont, true, false);
ImGuiHelpers.CopyGlyphsAcrossFonts(DefaultFont, MonoFont, true, false);
for (int i = 0, i_ = ioFonts.Fonts.Size; i < i_; i++)
{

View file

@ -114,6 +114,9 @@ namespace Dalamud.Interface.Internal.Windows
private DtrBarEntry? dtrTest2;
private DtrBarEntry? dtrTest3;
// Task Scheduler
private CancellationTokenSource taskSchedCancelSource = new();
private uint copyButtonIndex = 0;
/// <summary>
@ -1359,6 +1362,15 @@ namespace Dalamud.Interface.Internal.Windows
ImGuiHelpers.ScaledDummy(10);
ImGui.SameLine();
if (ImGui.Button("Cancel using CancellationTokenSource"))
{
this.taskSchedCancelSource.Cancel();
this.taskSchedCancelSource = new();
}
ImGui.Text("Run in any thread: ");
ImGui.SameLine();
if (ImGui.Button("Short Task.Run"))
{
Task.Run(() => { Thread.Sleep(500); });
@ -1368,7 +1380,8 @@ namespace Dalamud.Interface.Internal.Windows
if (ImGui.Button("Task in task(Delay)"))
{
Task.Run(async () => await this.TestTaskInTaskDelay());
var token = this.taskSchedCancelSource.Token;
Task.Run(async () => await this.TestTaskInTaskDelay(token));
}
ImGui.SameLine();
@ -1391,29 +1404,72 @@ namespace Dalamud.Interface.Internal.Windows
});
}
ImGui.Text("Run in Framework.Update: ");
ImGui.SameLine();
if (ImGui.Button("ASAP"))
{
Task.Run(async () => await Service<Framework>.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedCancelSource.Token));
}
ImGui.SameLine();
if (ImGui.Button("In 1s"))
{
Task.Run(async () => await Service<Framework>.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedCancelSource.Token, delay: TimeSpan.FromSeconds(1)));
}
ImGui.SameLine();
if (ImGui.Button("In 60f"))
{
Task.Run(async () => await Service<Framework>.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedCancelSource.Token, delayTicks: 60));
}
ImGui.SameLine();
if (ImGui.Button("Error in 1s"))
{
Task.Run(async () => await Service<Framework>.Get().RunOnTick(() => throw new Exception("Test Exception"), cancellationToken: this.taskSchedCancelSource.Token, delay: TimeSpan.FromSeconds(1)));
}
ImGui.SameLine();
if (ImGui.Button("As long as it's in Framework Thread"))
{
Task.Run(async () => await Service<Framework>.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from non-framework.update thread"); }));
Service<Framework>.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from framework.update thread"); }).Wait();
}
if (ImGui.Button("Drown in tasks"))
{
var token = this.taskSchedCancelSource.Token;
Task.Run(() =>
{
for (var i = 0; i < 100; i++)
{
token.ThrowIfCancellationRequested();
Task.Run(() =>
{
for (var i = 0; i < 100; i++)
{
token.ThrowIfCancellationRequested();
Task.Run(() =>
{
for (var i = 0; i < 100; i++)
{
token.ThrowIfCancellationRequested();
Task.Run(() =>
{
for (var i = 0; i < 100; i++)
{
Task.Run(() =>
token.ThrowIfCancellationRequested();
Task.Run(async () =>
{
for (var i = 0; i < 100; i++)
{
Thread.Sleep(1);
token.ThrowIfCancellationRequested();
await Task.Delay(1);
}
});
}
@ -1652,9 +1708,9 @@ namespace Dalamud.Interface.Internal.Windows
}
}
private async Task TestTaskInTaskDelay()
private async Task TestTaskInTaskDelay(CancellationToken token)
{
await Task.Delay(5000);
await Task.Delay(5000, token);
}
#pragma warning disable 1998

View file

@ -1,6 +1,7 @@
using System;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility;
namespace Dalamud.Interface.Internal.Windows.PluginInstaller

View file

@ -2013,6 +2013,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
return hasSearchString && !(
manifest.Name.ToLowerInvariant().Contains(searchString) ||
(!manifest.Author.IsNullOrEmpty() && manifest.Author.Equals(this.searchText, StringComparison.InvariantCultureIgnoreCase)) ||
(!manifest.Punchline.IsNullOrEmpty() && manifest.Punchline.ToLowerInvariant().Contains(searchString)) ||
(manifest.Tags != null && manifest.Tags.Contains(searchString, StringComparer.InvariantCultureIgnoreCase)));
}
@ -2253,7 +2254,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
public static string PluginBody_AuthorWithoutDownloadCount(string author) => Loc.Localize("InstallerAuthorWithoutDownloadCount", " by {0}").Format(author);
public static string PluginBody_AuthorWithDownloadCount(string author, long count) => Loc.Localize("InstallerAuthorWithDownloadCount", " by {0}, {1} downloads").Format(author, count);
public static string PluginBody_AuthorWithDownloadCount(string author, long count) => Loc.Localize("InstallerAuthorWithDownloadCount", " by {0} ({1} downloads)").Format(author, count.ToString("N0"));
public static string PluginBody_AuthorWithDownloadCountUnavailable(string author) => Loc.Localize("InstallerAuthorWithDownloadCountUnavailable", " by {0}, download count unavailable").Format(author);

View file

@ -24,9 +24,6 @@ namespace Dalamud.Interface.Internal.Windows
/// </summary>
internal class SettingsWindow : Window
{
private const float MinScale = 0.3f;
private const float MaxScale = 3.0f;
private readonly string[] languages;
private readonly string[] locLanguages;
@ -179,7 +176,8 @@ namespace Dalamud.Interface.Internal.Windows
var configuration = Service<DalamudConfiguration>.Get();
var interfaceManager = Service<InterfaceManager>.Get();
var rebuildFont = interfaceManager.FontGamma != configuration.FontGammaLevel
var rebuildFont = ImGui.GetIO().FontGlobalScale != configuration.GlobalUiScale
|| interfaceManager.FontGamma != configuration.FontGammaLevel
|| interfaceManager.FontResolutionLevel != configuration.FontResolutionLevel
|| interfaceManager.UseAxis != configuration.UseAxisFontsFromGame;
@ -298,7 +296,7 @@ namespace Dalamud.Interface.Internal.Windows
ImGui.Text(Loc.Localize("DalamudSettingsGlobalUiScale", "Global Font Scale"));
ImGui.SameLine();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 3);
if (ImGui.Button(Loc.Localize("DalamudSettingsUiScalePreset6", "9.6pt") + "##DalamudSettingsGlobalUiScaleReset96"))
if (ImGui.Button("9.6pt##DalamudSettingsGlobalUiScaleReset96"))
{
this.globalUiScale = 9.6f / 12.0f;
ImGui.GetIO().FontGlobalScale = this.globalUiScale;
@ -306,7 +304,7 @@ namespace Dalamud.Interface.Internal.Windows
}
ImGui.SameLine();
if (ImGui.Button(Loc.Localize("DalamudSettingsUiScalePreset12", "Reset (12pt)") + "##DalamudSettingsGlobalUiScaleReset12"))
if (ImGui.Button("12pt##DalamudSettingsGlobalUiScaleReset12"))
{
this.globalUiScale = 1.0f;
ImGui.GetIO().FontGlobalScale = this.globalUiScale;
@ -314,7 +312,7 @@ namespace Dalamud.Interface.Internal.Windows
}
ImGui.SameLine();
if (ImGui.Button(Loc.Localize("DalamudSettingsUiScalePreset14", "14pt") + "##DalamudSettingsGlobalUiScaleReset14"))
if (ImGui.Button("14pt##DalamudSettingsGlobalUiScaleReset14"))
{
this.globalUiScale = 14.0f / 12.0f;
ImGui.GetIO().FontGlobalScale = this.globalUiScale;
@ -322,7 +320,7 @@ namespace Dalamud.Interface.Internal.Windows
}
ImGui.SameLine();
if (ImGui.Button(Loc.Localize("DalamudSettingsUiScalePreset18", "18pt") + "##DalamudSettingsGlobalUiScaleReset18"))
if (ImGui.Button("18pt##DalamudSettingsGlobalUiScaleReset18"))
{
this.globalUiScale = 18.0f / 12.0f;
ImGui.GetIO().FontGlobalScale = this.globalUiScale;
@ -330,15 +328,17 @@ namespace Dalamud.Interface.Internal.Windows
}
ImGui.SameLine();
if (ImGui.Button(Loc.Localize("DalamudSettingsUiScalePreset36", "36pt") + "##DalamudSettingsGlobalUiScaleReset36"))
if (ImGui.Button("36pt##DalamudSettingsGlobalUiScaleReset36"))
{
this.globalUiScale = 36.0f / 12.0f;
ImGui.GetIO().FontGlobalScale = this.globalUiScale;
interfaceManager.RebuildFonts();
}
if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref this.globalUiScale, 0.005f, MinScale, MaxScale, "%.2f", ImGuiSliderFlags.AlwaysClamp))
var globalUiScaleInPt = 12f * this.globalUiScale;
if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPt, 0.1f, 9.6f, 36f, "%.1fpt", ImGuiSliderFlags.AlwaysClamp))
{
this.globalUiScale = globalUiScaleInPt / 12f;
ImGui.GetIO().FontGlobalScale = this.globalUiScale;
interfaceManager.RebuildFonts();
}
@ -436,7 +436,7 @@ namespace Dalamud.Interface.Internal.Windows
interfaceManager.RebuildFonts();
}
if (ImGui.DragFloat("##DalamudSettingsFontGammaDrag", ref this.fontGamma, 0.005f, MinScale, MaxScale, "%.2f", ImGuiSliderFlags.AlwaysClamp))
if (ImGui.DragFloat("##DalamudSettingsFontGammaDrag", ref this.fontGamma, 0.005f, 0.3f, 3f, "%.2f", ImGuiSliderFlags.AlwaysClamp))
{
interfaceManager.FontGammaOverride = this.fontGamma;
interfaceManager.RebuildFonts();

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
@ -138,11 +139,7 @@ namespace Dalamud.Plugin
/// <summary>
/// Gets a value indicating whether Dalamud is running in Debug mode or the /xldev menu is open. This can occur on release builds.
/// </summary>
#if DEBUG
public bool IsDebugging => true;
#else
public bool IsDebugging => Service<DalamudInterface>.GetNullable() is {IsDevMenuOpen: true}; // Can be null during boot
#endif
public bool IsDebugging => Debugger.IsAttached;
/// <summary>
/// Gets the current UI language in two-letter iso format.

View file

@ -1,164 +0,0 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
namespace Dalamud.Plugin.Internal
{
/// <summary>
/// This class represents a dev plugin and all facets of its lifecycle.
/// The DLL on disk, dependencies, loaded assembly, etc.
/// </summary>
internal class LocalDevPlugin : LocalPlugin, IDisposable
{
private static readonly ModuleLog Log = new("PLUGIN");
// Ref to Dalamud.Configuration.DevPluginSettings
private readonly DevPluginSettings devSettings;
private FileSystemWatcher? fileWatcher;
private CancellationTokenSource fileWatcherTokenSource = new();
private int reloadCounter;
/// <summary>
/// Initializes a new instance of the <see cref="LocalDevPlugin"/> class.
/// </summary>
/// <param name="dllFile">Path to the DLL file.</param>
/// <param name="manifest">The plugin manifest.</param>
public LocalDevPlugin(FileInfo dllFile, LocalPluginManifest? manifest)
: base(dllFile, manifest)
{
var configuration = Service<DalamudConfiguration>.Get();
if (!configuration.DevPluginSettings.TryGetValue(dllFile.FullName, out this.devSettings))
{
configuration.DevPluginSettings[dllFile.FullName] = this.devSettings = new DevPluginSettings();
configuration.Save();
}
if (this.AutomaticReload)
{
this.EnableReloading();
}
}
/// <summary>
/// Gets or sets a value indicating whether this dev plugin should start on boot.
/// </summary>
public bool StartOnBoot
{
get => this.devSettings.StartOnBoot;
set => this.devSettings.StartOnBoot = value;
}
/// <summary>
/// Gets or sets a value indicating whether this dev plugin should reload on change.
/// </summary>
public bool AutomaticReload
{
get => this.devSettings.AutomaticReloading;
set
{
this.devSettings.AutomaticReloading = value;
if (this.devSettings.AutomaticReloading)
{
this.EnableReloading();
}
else
{
this.DisableReloading();
}
}
}
/// <inheritdoc/>
public new void Dispose()
{
if (this.fileWatcher != null)
{
this.fileWatcher.Changed -= this.OnFileChanged;
this.fileWatcherTokenSource.Cancel();
this.fileWatcher.Dispose();
}
base.Dispose();
}
/// <summary>
/// Configure this plugin for automatic reloading and enable it.
/// </summary>
public void EnableReloading()
{
if (this.fileWatcher == null)
{
this.fileWatcherTokenSource = new();
this.fileWatcher = new FileSystemWatcher(this.DllFile.DirectoryName);
this.fileWatcher.Changed += this.OnFileChanged;
this.fileWatcher.Filter = this.DllFile.Name;
this.fileWatcher.NotifyFilter = NotifyFilters.LastWrite;
this.fileWatcher.EnableRaisingEvents = true;
}
}
/// <summary>
/// Disable automatic reloading for this plugin.
/// </summary>
public void DisableReloading()
{
if (this.fileWatcher != null)
{
this.fileWatcherTokenSource.Cancel();
this.fileWatcher.Changed -= this.OnFileChanged;
this.fileWatcher.Dispose();
this.fileWatcher = null;
}
}
private void OnFileChanged(object sender, FileSystemEventArgs args)
{
var current = Interlocked.Increment(ref this.reloadCounter);
Task.Delay(500).ContinueWith(
_ =>
{
if (this.fileWatcherTokenSource.IsCancellationRequested)
{
Log.Debug($"Skipping reload of {this.Name}, file watcher was cancelled.");
return;
}
if (current != this.reloadCounter)
{
Log.Debug($"Skipping reload of {this.Name}, file has changed again.");
return;
}
if (this.State != PluginState.Loaded)
{
Log.Debug($"Skipping reload of {this.Name}, state ({this.State}) is not {PluginState.Loaded}.");
return;
}
var notificationManager = Service<NotificationManager>.Get();
try
{
this.Reload();
notificationManager.AddNotification($"The DevPlugin '{this.Name} was reloaded successfully.", "Plugin reloaded!", NotificationType.Success);
}
catch (Exception ex)
{
Log.Error(ex, "DevPlugin reload failed.");
notificationManager.AddNotification($"The DevPlugin '{this.Name} could not be reloaded.", "Plugin reload failed!", NotificationType.Error);
}
},
this.fileWatcherTokenSource.Token);
}
}
}

View file

@ -1,481 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Game.Gui.Dtr;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Exceptions;
using Dalamud.Plugin.Internal.Loader;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility;
using Dalamud.Utility.Signatures;
namespace Dalamud.Plugin.Internal
{
/// <summary>
/// This class represents a plugin and all facets of its lifecycle.
/// The DLL on disk, dependencies, loaded assembly, etc.
/// </summary>
internal class LocalPlugin : IDisposable
{
private static readonly ModuleLog Log = new("LOCALPLUGIN");
private readonly FileInfo manifestFile;
private readonly FileInfo disabledFile;
private readonly FileInfo testingFile;
private PluginLoader? loader;
private Assembly? pluginAssembly;
private Type? pluginType;
private IDalamudPlugin? instance;
/// <summary>
/// Initializes a new instance of the <see cref="LocalPlugin"/> class.
/// </summary>
/// <param name="dllFile">Path to the DLL file.</param>
/// <param name="manifest">The plugin manifest.</param>
public LocalPlugin(FileInfo dllFile, LocalPluginManifest? manifest)
{
if (dllFile.Name == "FFXIVClientStructs.Generators.dll")
{
// Could this be done another way? Sure. It is an extremely common source
// of errors in the log through, and should never be loaded as a plugin.
Log.Error($"Not a plugin: {dllFile.FullName}");
throw new InvalidPluginException(dllFile);
}
this.DllFile = dllFile;
this.State = PluginState.Unloaded;
this.loader = PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, this.SetupLoaderConfig);
try
{
this.pluginAssembly = this.loader.LoadDefaultAssembly();
}
catch (Exception ex)
{
this.pluginAssembly = null;
this.pluginType = null;
this.loader.Dispose();
Log.Error(ex, $"Not a plugin: {this.DllFile.FullName}");
throw new InvalidPluginException(this.DllFile);
}
try
{
this.pluginType = this.pluginAssembly.GetTypes().FirstOrDefault(type => type.IsAssignableTo(typeof(IDalamudPlugin)));
}
catch (ReflectionTypeLoadException ex)
{
Log.Error(ex, $"Could not load one or more types when searching for IDalamudPlugin: {this.DllFile.FullName}");
// Something blew up when parsing types, but we still want to look for IDalamudPlugin. Let Load() handle the error.
this.pluginType = ex.Types.FirstOrDefault(type => type.IsAssignableTo(typeof(IDalamudPlugin)));
}
if (this.pluginType == default)
{
this.pluginAssembly = null;
this.pluginType = null;
this.loader.Dispose();
Log.Error($"Nothing inherits from IDalamudPlugin: {this.DllFile.FullName}");
throw new InvalidPluginException(this.DllFile);
}
var assemblyVersion = this.pluginAssembly.GetName().Version;
// Although it is conditionally used here, we need to set the initial value regardless.
this.manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile);
// If the parameter manifest was null
if (manifest == null)
{
this.Manifest = new LocalPluginManifest()
{
Author = "developer",
Name = Path.GetFileNameWithoutExtension(this.DllFile.Name),
InternalName = Path.GetFileNameWithoutExtension(this.DllFile.Name),
AssemblyVersion = assemblyVersion,
Description = string.Empty,
ApplicableVersion = GameVersion.Any,
DalamudApiLevel = PluginManager.DalamudApiLevel,
IsHide = false,
};
// Save the manifest to disk so there won't be any problems later.
// We'll update the name property after it can be retrieved from the instance.
this.Manifest.Save(this.manifestFile);
}
else
{
this.Manifest = manifest;
}
// This converts from the ".disabled" file feature to the manifest instead.
this.disabledFile = LocalPluginManifest.GetDisabledFile(this.DllFile);
if (this.disabledFile.Exists)
{
this.Manifest.Disabled = true;
this.disabledFile.Delete();
}
// This converts from the ".testing" file feature to the manifest instead.
this.testingFile = LocalPluginManifest.GetTestingFile(this.DllFile);
if (this.testingFile.Exists)
{
this.Manifest.Testing = true;
this.testingFile.Delete();
}
var pluginManager = Service<PluginManager>.Get();
this.IsBanned = pluginManager.IsManifestBanned(this.Manifest);
this.BanReason = pluginManager.GetBanReason(this.Manifest);
this.SaveManifest();
}
/// <summary>
/// Gets the <see cref="DalamudPluginInterface"/> associated with this plugin.
/// </summary>
public DalamudPluginInterface? DalamudInterface { get; private set; }
/// <summary>
/// Gets the path to the plugin DLL.
/// </summary>
public FileInfo DllFile { get; }
/// <summary>
/// Gets the plugin manifest, if one exists.
/// </summary>
public LocalPluginManifest Manifest { get; private set; }
/// <summary>
/// Gets or sets the current state of the plugin.
/// </summary>
public PluginState State { get; protected set; }
/// <summary>
/// Gets the AssemblyName plugin, populated during <see cref="Load(PluginLoadReason, bool)"/>.
/// </summary>
/// <returns>Plugin type.</returns>
public AssemblyName? AssemblyName { get; private set; }
/// <summary>
/// Gets the plugin name, directly from the plugin or if it is not loaded from the manifest.
/// </summary>
public string Name => this.instance?.Name ?? this.Manifest.Name;
/// <summary>
/// Gets an optional reason, if the plugin is banned.
/// </summary>
public string BanReason { get; }
/// <summary>
/// Gets a value indicating whether the plugin is loaded and running.
/// </summary>
public bool IsLoaded => this.State == PluginState.Loaded;
/// <summary>
/// Gets a value indicating whether the plugin is disabled.
/// </summary>
public bool IsDisabled => this.Manifest.Disabled;
/// <summary>
/// Gets a value indicating whether this plugin's API level is out of date.
/// </summary>
public bool IsOutdated => this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel;
/// <summary>
/// Gets a value indicating whether the plugin is for testing use only.
/// </summary>
public bool IsTesting => this.Manifest.IsTestingExclusive || this.Manifest.Testing;
/// <summary>
/// Gets a value indicating whether this plugin has been banned.
/// </summary>
public bool IsBanned { get; }
/// <summary>
/// Gets a value indicating whether this plugin is dev plugin.
/// </summary>
public bool IsDev => this is LocalDevPlugin;
/// <inheritdoc/>
public void Dispose()
{
this.instance?.Dispose();
this.instance = null;
this.DalamudInterface?.ExplicitDispose();
this.DalamudInterface = null;
this.pluginType = null;
this.pluginAssembly = null;
this.loader?.Dispose();
}
/// <summary>
/// Load this plugin.
/// </summary>
/// <param name="reason">The reason why this plugin is being loaded.</param>
/// <param name="reloading">Load while reloading.</param>
public void Load(PluginLoadReason reason, bool reloading = false)
{
var startInfo = Service<DalamudStartInfo>.Get();
var configuration = Service<DalamudConfiguration>.Get();
var pluginManager = Service<PluginManager>.Get();
// Allowed: Unloaded
switch (this.State)
{
case PluginState.InProgress:
throw new InvalidPluginOperationException($"Unable to load {this.Name}, already working");
case PluginState.Loaded:
throw new InvalidPluginOperationException($"Unable to load {this.Name}, already loaded");
case PluginState.LoadError:
throw new InvalidPluginOperationException($"Unable to load {this.Name}, load previously faulted, unload first");
case PluginState.UnloadError:
throw new InvalidPluginOperationException($"Unable to load {this.Name}, unload previously faulted, restart Dalamud");
}
if (pluginManager.IsManifestBanned(this.Manifest))
throw new BannedPluginException($"Unable to load {this.Name}, banned");
if (this.Manifest.ApplicableVersion < startInfo.GameVersion)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version");
if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !configuration.LoadAllApiLevels)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level");
if (this.Manifest.Disabled)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, disabled");
this.State = PluginState.InProgress;
Log.Information($"Loading {this.DllFile.Name}");
if (this.DllFile.DirectoryName != null && File.Exists(Path.Combine(this.DllFile.DirectoryName, "Dalamud.dll")))
{
Log.Error("==== IMPORTANT MESSAGE TO {0}, THE DEVELOPER OF {1} ====", this.Manifest.Author!, this.Manifest.InternalName);
Log.Error("YOU ARE INCLUDING DALAMUD DEPENDENCIES IN YOUR BUILDS!!!");
Log.Error("You may not be able to load your plugin. \"<Private>False</Private>\" needs to be set in your csproj.");
Log.Error("If you are using ILMerge, do not merge anything other than your direct dependencies.");
Log.Error("Do not merge FFXIVClientStructs.Generators.dll.");
Log.Error("Please refer to https://github.com/goatcorp/Dalamud/discussions/603 for more information.");
}
try
{
this.loader ??= PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, this.SetupLoaderConfig);
if (reloading || this.IsDev)
{
if (this.IsDev)
{
// If a dev plugin is set to not autoload on boot, but we want to reload it at the arbitrary load
// time, we need to essentially "Unload" the plugin, but we can't call plugin.Unload because of the
// load state checks. Null any references to the assembly and types, then proceed with regular reload
// operations.
this.pluginAssembly = null;
this.pluginType = null;
}
this.loader.Reload();
if (this.IsDev)
{
// Reload the manifest in-case there were changes here too.
var manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile);
if (manifestFile.Exists)
{
this.Manifest = LocalPluginManifest.Load(manifestFile);
}
}
}
// Load the assembly
this.pluginAssembly ??= this.loader.LoadDefaultAssembly();
this.AssemblyName = this.pluginAssembly.GetName();
// Find the plugin interface implementation. It is guaranteed to exist after checking in the ctor.
this.pluginType ??= this.pluginAssembly.GetTypes().First(type => type.IsAssignableTo(typeof(IDalamudPlugin)));
// Check for any loaded plugins with the same assembly name
var assemblyName = this.pluginAssembly.GetName().Name;
foreach (var otherPlugin in pluginManager.InstalledPlugins)
{
// During hot-reloading, this plugin will be in the plugin list, and the instance will have been disposed
if (otherPlugin == this || otherPlugin.instance == null)
continue;
var otherPluginAssemblyName = otherPlugin.instance.GetType().Assembly.GetName().Name;
if (otherPluginAssemblyName == assemblyName)
{
this.State = PluginState.Unloaded;
Log.Debug($"Duplicate assembly: {this.Name}");
throw new DuplicatePluginException(assemblyName);
}
}
// Update the location for the Location and CodeBase patches
PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new(this.DllFile);
this.DalamudInterface = new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, this.DllFile, reason, this.IsDev);
var ioc = Service<ServiceContainer>.Get();
this.instance = ioc.Create(this.pluginType, this.DalamudInterface) as IDalamudPlugin;
if (this.instance == null)
{
this.State = PluginState.LoadError;
this.DalamudInterface.ExplicitDispose();
Log.Error($"Error while loading {this.Name}, failed to bind and call the plugin constructor");
return;
}
SignatureHelper.Initialise(this.instance);
// In-case the manifest name was a placeholder. Can occur when no manifest was included.
if (this.instance.Name != this.Manifest.Name)
{
this.Manifest.Name = this.instance.Name;
this.Manifest.Save(this.manifestFile);
}
this.State = PluginState.Loaded;
Log.Information($"Finished loading {this.DllFile.Name}");
}
catch (Exception ex)
{
this.State = PluginState.LoadError;
Log.Error(ex, $"Error while loading {this.Name}");
throw;
}
}
/// <summary>
/// Unload this plugin. This is the same as dispose, but without the "disposed" connotations. This object should stay
/// in the plugin list until it has been actually disposed.
/// </summary>
/// <param name="reloading">Unload while reloading.</param>
public void Unload(bool reloading = false)
{
// Allowed: Loaded, LoadError(we are cleaning this up while we're at it)
switch (this.State)
{
case PluginState.InProgress:
throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already working");
case PluginState.Unloaded:
throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already unloaded");
case PluginState.UnloadError:
throw new InvalidPluginOperationException($"Unable to unload {this.Name}, unload previously faulted, restart Dalamud");
}
try
{
this.State = PluginState.InProgress;
Log.Information($"Unloading {this.DllFile.Name}");
this.instance?.Dispose();
this.instance = null;
this.DalamudInterface?.ExplicitDispose();
this.DalamudInterface = null;
this.pluginType = null;
this.pluginAssembly = null;
if (!reloading)
{
this.loader?.Dispose();
this.loader = null;
}
this.State = PluginState.Unloaded;
Log.Information($"Finished unloading {this.DllFile.Name}");
}
catch (Exception ex)
{
this.State = PluginState.UnloadError;
Log.Error(ex, $"Error while unloading {this.Name}");
throw;
}
}
/// <summary>
/// Reload this plugin.
/// </summary>
public void Reload()
{
this.Unload(true);
// We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates.
var dtr = Service<DtrBar>.Get();
dtr.HandleRemovedNodes();
this.Load(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.InProgress:
case PluginState.Loaded:
case PluginState.LoadError:
throw new InvalidPluginOperationException($"Unable to enable {this.Name}, still loaded");
}
if (!this.Manifest.Disabled)
throw new InvalidPluginOperationException($"Unable to enable {this.Name}, not disabled");
this.Manifest.Disabled = false;
this.SaveManifest();
}
/// <summary>
/// Disable this plugin, must be unloaded first.
/// </summary>
public void Disable()
{
// Allowed: Unloaded, UnloadError
switch (this.State)
{
case PluginState.InProgress:
case PluginState.Loaded:
case PluginState.LoadError:
throw new InvalidPluginOperationException($"Unable to disable {this.Name}, still loaded");
}
if (this.Manifest.Disabled)
throw new InvalidPluginOperationException($"Unable to disable {this.Name}, already disabled");
this.Manifest.Disabled = true;
this.SaveManifest();
}
private void SetupLoaderConfig(LoaderConfig config)
{
config.IsUnloadable = true;
config.LoadInMemory = true;
config.PreferSharedTypes = false;
config.SharedAssemblies.Add(typeof(Lumina.GameData).Assembly.GetName());
config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName());
}
private void SaveManifest() => this.Manifest.Save(this.manifestFile);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,120 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
using Newtonsoft.Json;
namespace Dalamud.Plugin.Internal
{
/// <summary>
/// This class represents a single plugin repository.
/// </summary>
internal class PluginRepository
{
private const string DalamudPluginsMasterUrl = "https://kamori.goats.dev/Plugin/PluginMaster";
private static readonly ModuleLog Log = new("PLUGINR");
private static readonly HttpClient HttpClient = new()
{
DefaultRequestHeaders =
{
CacheControl = new CacheControlHeaderValue
{
NoCache = true,
},
},
};
/// <summary>
/// Initializes a new instance of the <see cref="PluginRepository"/> class.
/// </summary>
/// <param name="pluginMasterUrl">The plugin master URL.</param>
/// <param name="isEnabled">Whether the plugin repo is enabled.</param>
public PluginRepository(string pluginMasterUrl, bool isEnabled)
{
this.PluginMasterUrl = pluginMasterUrl;
this.IsThirdParty = pluginMasterUrl != DalamudPluginsMasterUrl;
this.IsEnabled = isEnabled;
}
/// <summary>
/// Gets a new instance of the <see cref="PluginRepository"/> class for the main repo.
/// </summary>
public static PluginRepository MainRepo => new(DalamudPluginsMasterUrl, true);
/// <summary>
/// Gets the pluginmaster.json URL.
/// </summary>
public string PluginMasterUrl { get; }
/// <summary>
/// Gets a value indicating whether this plugin repository is from a third party.
/// </summary>
public bool IsThirdParty { get; }
/// <summary>
/// Gets a value indicating whether this repo is enabled.
/// </summary>
public bool IsEnabled { get; }
/// <summary>
/// Gets the plugin master list of available plugins.
/// </summary>
public ReadOnlyCollection<RemotePluginManifest>? PluginMaster { get; private set; }
/// <summary>
/// Gets the initialization state of the plugin repository.
/// </summary>
public PluginRepositoryState State { get; private set; }
/// <summary>
/// Reload the plugin master asynchronously in a task.
/// </summary>
/// <returns>The new state.</returns>
public async Task ReloadPluginMasterAsync()
{
this.State = PluginRepositoryState.InProgress;
this.PluginMaster = new List<RemotePluginManifest>().AsReadOnly();
try
{
Log.Information($"Fetching repo: {this.PluginMasterUrl}");
using var response = await HttpClient.GetAsync(this.PluginMasterUrl);
response.EnsureSuccessStatusCode();
var data = await response.Content.ReadAsStringAsync();
var pluginMaster = JsonConvert.DeserializeObject<List<RemotePluginManifest>>(data);
if (pluginMaster == null)
{
throw new Exception("Deserialized PluginMaster was null.");
}
pluginMaster.Sort((pm1, pm2) => pm1.Name.CompareTo(pm2.Name));
// Set the source for each remote manifest. Allows for checking if is 3rd party.
foreach (var manifest in pluginMaster)
{
manifest.SourceRepo = this;
}
this.PluginMaster = pluginMaster.AsReadOnly();
Log.Debug($"Successfully fetched repo: {this.PluginMasterUrl}");
this.State = PluginRepositoryState.Success;
}
catch (Exception ex)
{
Log.Error(ex, $"PluginMaster failed: {this.PluginMasterUrl}");
this.State = PluginRepositoryState.Fail;
}
}
}
}

View file

@ -1,36 +1,35 @@
namespace Dalamud.Plugin.Internal.Types
namespace Dalamud.Plugin.Internal.Types;
/// <summary>
/// Information about an available plugin update.
/// </summary>
internal record AvailablePluginUpdate
{
/// <summary>
/// Information about an available plugin update.
/// Initializes a new instance of the <see cref="AvailablePluginUpdate"/> class.
/// </summary>
internal record AvailablePluginUpdate
/// <param name="installedPlugin">The installed plugin to update.</param>
/// <param name="updateManifest">The manifest to use for the update.</param>
/// <param name="useTesting">If the testing version should be used for the update.</param>
public AvailablePluginUpdate(LocalPlugin installedPlugin, RemotePluginManifest updateManifest, bool useTesting)
{
/// <summary>
/// Initializes a new instance of the <see cref="AvailablePluginUpdate"/> class.
/// </summary>
/// <param name="installedPlugin">The installed plugin to update.</param>
/// <param name="updateManifest">The manifest to use for the update.</param>
/// <param name="useTesting">If the testing version should be used for the update.</param>
public AvailablePluginUpdate(LocalPlugin installedPlugin, RemotePluginManifest updateManifest, bool useTesting)
{
this.InstalledPlugin = installedPlugin;
this.UpdateManifest = updateManifest;
this.UseTesting = useTesting;
}
/// <summary>
/// Gets the currently installed plugin.
/// </summary>
public LocalPlugin InstalledPlugin { get; init; }
/// <summary>
/// Gets the available update manifest.
/// </summary>
public RemotePluginManifest UpdateManifest { get; init; }
/// <summary>
/// Gets a value indicating whether the update should use the testing URL.
/// </summary>
public bool UseTesting { get; init; }
this.InstalledPlugin = installedPlugin;
this.UpdateManifest = updateManifest;
this.UseTesting = useTesting;
}
/// <summary>
/// Gets the currently installed plugin.
/// </summary>
public LocalPlugin InstalledPlugin { get; init; }
/// <summary>
/// Gets the available update manifest.
/// </summary>
public RemotePluginManifest UpdateManifest { get; init; }
/// <summary>
/// Gets a value indicating whether the update should use the testing URL.
/// </summary>
public bool UseTesting { get; init; }
}

View file

@ -0,0 +1,162 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Logging.Internal;
namespace Dalamud.Plugin.Internal.Types;
/// <summary>
/// This class represents a dev plugin and all facets of its lifecycle.
/// The DLL on disk, dependencies, loaded assembly, etc.
/// </summary>
internal class LocalDevPlugin : LocalPlugin, IDisposable
{
private static readonly ModuleLog Log = new("PLUGIN");
// Ref to Dalamud.Configuration.DevPluginSettings
private readonly DevPluginSettings devSettings;
private FileSystemWatcher? fileWatcher;
private CancellationTokenSource fileWatcherTokenSource = new();
private int reloadCounter;
/// <summary>
/// Initializes a new instance of the <see cref="LocalDevPlugin"/> class.
/// </summary>
/// <param name="dllFile">Path to the DLL file.</param>
/// <param name="manifest">The plugin manifest.</param>
public LocalDevPlugin(FileInfo dllFile, LocalPluginManifest? manifest)
: base(dllFile, manifest)
{
var configuration = Service<DalamudConfiguration>.Get();
if (!configuration.DevPluginSettings.TryGetValue(dllFile.FullName, out this.devSettings))
{
configuration.DevPluginSettings[dllFile.FullName] = this.devSettings = new DevPluginSettings();
configuration.Save();
}
if (this.AutomaticReload)
{
this.EnableReloading();
}
}
/// <summary>
/// Gets or sets a value indicating whether this dev plugin should start on boot.
/// </summary>
public bool StartOnBoot
{
get => this.devSettings.StartOnBoot;
set => this.devSettings.StartOnBoot = value;
}
/// <summary>
/// Gets or sets a value indicating whether this dev plugin should reload on change.
/// </summary>
public bool AutomaticReload
{
get => this.devSettings.AutomaticReloading;
set
{
this.devSettings.AutomaticReloading = value;
if (this.devSettings.AutomaticReloading)
{
this.EnableReloading();
}
else
{
this.DisableReloading();
}
}
}
/// <inheritdoc/>
public new void Dispose()
{
if (this.fileWatcher != null)
{
this.fileWatcher.Changed -= this.OnFileChanged;
this.fileWatcherTokenSource.Cancel();
this.fileWatcher.Dispose();
}
base.Dispose();
}
/// <summary>
/// Configure this plugin for automatic reloading and enable it.
/// </summary>
public void EnableReloading()
{
if (this.fileWatcher == null && this.DllFile.DirectoryName != null)
{
this.fileWatcherTokenSource = new CancellationTokenSource();
this.fileWatcher = new FileSystemWatcher(this.DllFile.DirectoryName);
this.fileWatcher.Changed += this.OnFileChanged;
this.fileWatcher.Filter = this.DllFile.Name;
this.fileWatcher.NotifyFilter = NotifyFilters.LastWrite;
this.fileWatcher.EnableRaisingEvents = true;
}
}
/// <summary>
/// Disable automatic reloading for this plugin.
/// </summary>
public void DisableReloading()
{
if (this.fileWatcher != null)
{
this.fileWatcherTokenSource.Cancel();
this.fileWatcher.Changed -= this.OnFileChanged;
this.fileWatcher.Dispose();
this.fileWatcher = null;
}
}
private void OnFileChanged(object sender, FileSystemEventArgs args)
{
var current = Interlocked.Increment(ref this.reloadCounter);
Task.Delay(500).ContinueWith(
_ =>
{
if (this.fileWatcherTokenSource.IsCancellationRequested)
{
Log.Debug($"Skipping reload of {this.Name}, file watcher was cancelled.");
return;
}
if (current != this.reloadCounter)
{
Log.Debug($"Skipping reload of {this.Name}, file has changed again.");
return;
}
if (this.State != PluginState.Loaded)
{
Log.Debug($"Skipping reload of {this.Name}, state ({this.State}) is not {PluginState.Loaded}.");
return;
}
var notificationManager = Service<NotificationManager>.Get();
try
{
this.Reload();
notificationManager.AddNotification($"The DevPlugin '{this.Name} was reloaded successfully.", "Plugin reloaded!", NotificationType.Success);
}
catch (Exception ex)
{
Log.Error(ex, "DevPlugin reload failed.");
notificationManager.AddNotification($"The DevPlugin '{this.Name} could not be reloaded.", "Plugin reload failed!", NotificationType.Error);
}
},
this.fileWatcherTokenSource.Token);
}
}

View file

@ -0,0 +1,501 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Game.Gui.Dtr;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Exceptions;
using Dalamud.Plugin.Internal.Loader;
using Dalamud.Utility;
using Dalamud.Utility.Signatures;
namespace Dalamud.Plugin.Internal.Types;
/// <summary>
/// This class represents a plugin and all facets of its lifecycle.
/// The DLL on disk, dependencies, loaded assembly, etc.
/// </summary>
internal class LocalPlugin : IDisposable
{
private static readonly ModuleLog Log = new("LOCALPLUGIN");
private readonly FileInfo manifestFile;
private readonly FileInfo disabledFile;
private readonly FileInfo testingFile;
private PluginLoader? loader;
private Assembly? pluginAssembly;
private Type? pluginType;
private IDalamudPlugin? instance;
/// <summary>
/// Initializes a new instance of the <see cref="LocalPlugin"/> class.
/// </summary>
/// <param name="dllFile">Path to the DLL file.</param>
/// <param name="manifest">The plugin manifest.</param>
public LocalPlugin(FileInfo dllFile, LocalPluginManifest? manifest)
{
if (dllFile.Name == "FFXIVClientStructs.Generators.dll")
{
// Could this be done another way? Sure. It is an extremely common source
// of errors in the log through, and should never be loaded as a plugin.
Log.Error($"Not a plugin: {dllFile.FullName}");
throw new InvalidPluginException(dllFile);
}
this.DllFile = dllFile;
this.State = PluginState.Unloaded;
this.loader = PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig);
try
{
this.pluginAssembly = this.loader.LoadDefaultAssembly();
}
catch (Exception ex)
{
this.pluginAssembly = null;
this.pluginType = null;
this.loader.Dispose();
Log.Error(ex, $"Not a plugin: {this.DllFile.FullName}");
throw new InvalidPluginException(this.DllFile);
}
try
{
this.pluginType = this.pluginAssembly.GetTypes().FirstOrDefault(type => type.IsAssignableTo(typeof(IDalamudPlugin)));
}
catch (ReflectionTypeLoadException ex)
{
Log.Error(ex, $"Could not load one or more types when searching for IDalamudPlugin: {this.DllFile.FullName}");
// Something blew up when parsing types, but we still want to look for IDalamudPlugin. Let Load() handle the error.
this.pluginType = ex.Types.FirstOrDefault(type => type != null && type.IsAssignableTo(typeof(IDalamudPlugin)));
}
if (this.pluginType == default)
{
this.pluginAssembly = null;
this.pluginType = null;
this.loader.Dispose();
Log.Error($"Nothing inherits from IDalamudPlugin: {this.DllFile.FullName}");
throw new InvalidPluginException(this.DllFile);
}
var assemblyVersion = this.pluginAssembly.GetName().Version;
// Although it is conditionally used here, we need to set the initial value regardless.
this.manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile);
// If the parameter manifest was null
if (manifest == null)
{
this.Manifest = new LocalPluginManifest()
{
Author = "developer",
Name = Path.GetFileNameWithoutExtension(this.DllFile.Name),
InternalName = Path.GetFileNameWithoutExtension(this.DllFile.Name),
AssemblyVersion = assemblyVersion ?? new Version("1.0.0.0"),
Description = string.Empty,
ApplicableVersion = GameVersion.Any,
DalamudApiLevel = PluginManager.DalamudApiLevel,
IsHide = false,
};
// Save the manifest to disk so there won't be any problems later.
// We'll update the name property after it can be retrieved from the instance.
this.Manifest.Save(this.manifestFile);
}
else
{
this.Manifest = manifest;
}
// This converts from the ".disabled" file feature to the manifest instead.
this.disabledFile = LocalPluginManifest.GetDisabledFile(this.DllFile);
if (this.disabledFile.Exists)
{
this.Manifest.Disabled = true;
this.disabledFile.Delete();
}
// This converts from the ".testing" file feature to the manifest instead.
this.testingFile = LocalPluginManifest.GetTestingFile(this.DllFile);
if (this.testingFile.Exists)
{
this.Manifest.Testing = true;
this.testingFile.Delete();
}
var pluginManager = Service<PluginManager>.Get();
this.IsBanned = pluginManager.IsManifestBanned(this.Manifest);
this.BanReason = pluginManager.GetBanReason(this.Manifest);
this.SaveManifest();
}
/// <summary>
/// Gets the <see cref="DalamudPluginInterface"/> associated with this plugin.
/// </summary>
public DalamudPluginInterface? DalamudInterface { get; private set; }
/// <summary>
/// Gets the path to the plugin DLL.
/// </summary>
public FileInfo DllFile { get; }
/// <summary>
/// Gets the plugin manifest, if one exists.
/// </summary>
public LocalPluginManifest Manifest { get; private set; }
/// <summary>
/// Gets or sets the current state of the plugin.
/// </summary>
public PluginState State { get; protected set; }
/// <summary>
/// Gets the AssemblyName plugin, populated during <see cref="Load(PluginLoadReason, bool)"/>.
/// </summary>
/// <returns>Plugin type.</returns>
public AssemblyName? AssemblyName { get; private set; }
/// <summary>
/// Gets the plugin name, directly from the plugin or if it is not loaded from the manifest.
/// </summary>
public string Name => this.instance?.Name ?? this.Manifest.Name;
/// <summary>
/// Gets an optional reason, if the plugin is banned.
/// </summary>
public string BanReason { get; }
/// <summary>
/// Gets a value indicating whether the plugin is loaded and running.
/// </summary>
public bool IsLoaded => this.State == PluginState.Loaded;
/// <summary>
/// Gets a value indicating whether the plugin is disabled.
/// </summary>
public bool IsDisabled => this.Manifest.Disabled;
/// <summary>
/// Gets a value indicating whether this plugin's API level is out of date.
/// </summary>
public bool IsOutdated => this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel;
/// <summary>
/// Gets a value indicating whether the plugin is for testing use only.
/// </summary>
public bool IsTesting => this.Manifest.IsTestingExclusive || this.Manifest.Testing;
/// <summary>
/// Gets a value indicating whether this plugin has been banned.
/// </summary>
public bool IsBanned { get; }
/// <summary>
/// Gets a value indicating whether this plugin is dev plugin.
/// </summary>
public bool IsDev => this is LocalDevPlugin;
/// <inheritdoc/>
public void Dispose()
{
this.instance?.Dispose();
this.instance = null;
this.DalamudInterface?.ExplicitDispose();
this.DalamudInterface = null;
this.pluginType = null;
this.pluginAssembly = null;
this.loader?.Dispose();
}
/// <summary>
/// Load this plugin.
/// </summary>
/// <param name="reason">The reason why this plugin is being loaded.</param>
/// <param name="reloading">Load while reloading.</param>
public void Load(PluginLoadReason reason, bool reloading = false)
{
var startInfo = Service<DalamudStartInfo>.Get();
var configuration = Service<DalamudConfiguration>.Get();
var pluginManager = Service<PluginManager>.Get();
// Allowed: Unloaded
switch (this.State)
{
case PluginState.InProgress:
throw new InvalidPluginOperationException($"Unable to load {this.Name}, already working");
case PluginState.Loaded:
throw new InvalidPluginOperationException($"Unable to load {this.Name}, already loaded");
case PluginState.LoadError:
throw new InvalidPluginOperationException($"Unable to load {this.Name}, load previously faulted, unload first");
case PluginState.UnloadError:
throw new InvalidPluginOperationException($"Unable to load {this.Name}, unload previously faulted, restart Dalamud");
case PluginState.Unloaded:
break;
default:
throw new ArgumentOutOfRangeException(this.State.ToString());
}
if (pluginManager.IsManifestBanned(this.Manifest))
throw new BannedPluginException($"Unable to load {this.Name}, banned");
if (this.Manifest.ApplicableVersion < startInfo.GameVersion)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version");
if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !configuration.LoadAllApiLevels)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level");
if (this.Manifest.Disabled)
throw new InvalidPluginOperationException($"Unable to load {this.Name}, disabled");
this.State = PluginState.InProgress;
Log.Information($"Loading {this.DllFile.Name}");
if (this.DllFile.DirectoryName != null && File.Exists(Path.Combine(this.DllFile.DirectoryName, "Dalamud.dll")))
{
Log.Error("==== IMPORTANT MESSAGE TO {0}, THE DEVELOPER OF {1} ====", this.Manifest.Author!, this.Manifest.InternalName);
Log.Error("YOU ARE INCLUDING DALAMUD DEPENDENCIES IN YOUR BUILDS!!!");
Log.Error("You may not be able to load your plugin. \"<Private>False</Private>\" needs to be set in your csproj.");
Log.Error("If you are using ILMerge, do not merge anything other than your direct dependencies.");
Log.Error("Do not merge FFXIVClientStructs.Generators.dll.");
Log.Error("Please refer to https://github.com/goatcorp/Dalamud/discussions/603 for more information.");
}
try
{
this.loader ??= PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig);
if (reloading || this.IsDev)
{
if (this.IsDev)
{
// If a dev plugin is set to not autoload on boot, but we want to reload it at the arbitrary load
// time, we need to essentially "Unload" the plugin, but we can't call plugin.Unload because of the
// load state checks. Null any references to the assembly and types, then proceed with regular reload
// operations.
this.pluginAssembly = null;
this.pluginType = null;
}
this.loader.Reload();
if (this.IsDev)
{
// Reload the manifest in-case there were changes here too.
var manifestDevFile = LocalPluginManifest.GetManifestFile(this.DllFile);
if (manifestDevFile.Exists)
{
this.Manifest = LocalPluginManifest.Load(manifestDevFile);
}
}
}
// Load the assembly
this.pluginAssembly ??= this.loader.LoadDefaultAssembly();
this.AssemblyName = this.pluginAssembly.GetName();
// Find the plugin interface implementation. It is guaranteed to exist after checking in the ctor.
this.pluginType ??= this.pluginAssembly.GetTypes().First(type => type.IsAssignableTo(typeof(IDalamudPlugin)));
// Check for any loaded plugins with the same assembly name
var assemblyName = this.pluginAssembly.GetName().Name;
foreach (var otherPlugin in pluginManager.InstalledPlugins)
{
// During hot-reloading, this plugin will be in the plugin list, and the instance will have been disposed
if (otherPlugin == this || otherPlugin.instance == null)
continue;
var otherPluginAssemblyName = otherPlugin.instance.GetType().Assembly.GetName().Name;
if (otherPluginAssemblyName == assemblyName && otherPluginAssemblyName != null)
{
this.State = PluginState.Unloaded;
Log.Debug($"Duplicate assembly: {this.Name}");
throw new DuplicatePluginException(assemblyName);
}
}
// Update the location for the Location and CodeBase patches
PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new PluginPatchData(this.DllFile);
this.DalamudInterface = new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, this.DllFile, reason, this.IsDev);
var ioc = Service<ServiceContainer>.Get();
this.instance = ioc.Create(this.pluginType, this.DalamudInterface) as IDalamudPlugin;
if (this.instance == null)
{
this.State = PluginState.LoadError;
this.DalamudInterface.ExplicitDispose();
Log.Error($"Error while loading {this.Name}, failed to bind and call the plugin constructor");
return;
}
SignatureHelper.Initialise(this.instance);
// In-case the manifest name was a placeholder. Can occur when no manifest was included.
if (this.instance.Name != this.Manifest.Name)
{
this.Manifest.Name = this.instance.Name;
this.Manifest.Save(this.manifestFile);
}
this.State = PluginState.Loaded;
Log.Information($"Finished loading {this.DllFile.Name}");
}
catch (Exception ex)
{
this.State = PluginState.LoadError;
Log.Error(ex, $"Error while loading {this.Name}");
throw;
}
}
/// <summary>
/// Unload this plugin. This is the same as dispose, but without the "disposed" connotations. This object should stay
/// in the plugin list until it has been actually disposed.
/// </summary>
/// <param name="reloading">Unload while reloading.</param>
public void Unload(bool reloading = false)
{
// Allowed: Loaded, LoadError(we are cleaning this up while we're at it)
switch (this.State)
{
case PluginState.InProgress:
throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already working");
case PluginState.Unloaded:
throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already unloaded");
case PluginState.UnloadError:
throw new InvalidPluginOperationException($"Unable to unload {this.Name}, unload previously faulted, restart Dalamud");
case PluginState.Loaded:
break;
case PluginState.LoadError:
break;
default:
throw new ArgumentOutOfRangeException(this.State.ToString());
}
try
{
this.State = PluginState.InProgress;
Log.Information($"Unloading {this.DllFile.Name}");
this.instance?.Dispose();
this.instance = null;
this.DalamudInterface?.ExplicitDispose();
this.DalamudInterface = null;
this.pluginType = null;
this.pluginAssembly = null;
if (!reloading)
{
this.loader?.Dispose();
this.loader = null;
}
this.State = PluginState.Unloaded;
Log.Information($"Finished unloading {this.DllFile.Name}");
}
catch (Exception ex)
{
this.State = PluginState.UnloadError;
Log.Error(ex, $"Error while unloading {this.Name}");
throw;
}
}
/// <summary>
/// Reload this plugin.
/// </summary>
public void Reload()
{
this.Unload(true);
// We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates.
var dtr = Service<DtrBar>.Get();
dtr.HandleRemovedNodes();
this.Load(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.InProgress:
case PluginState.Loaded:
case PluginState.LoadError:
throw new InvalidPluginOperationException($"Unable to enable {this.Name}, still loaded");
case PluginState.Unloaded:
break;
case PluginState.UnloadError:
break;
default:
throw new ArgumentOutOfRangeException(this.State.ToString());
}
if (!this.Manifest.Disabled)
throw new InvalidPluginOperationException($"Unable to enable {this.Name}, not disabled");
this.Manifest.Disabled = false;
this.SaveManifest();
}
/// <summary>
/// Disable this plugin, must be unloaded first.
/// </summary>
public void Disable()
{
// Allowed: Unloaded, UnloadError
switch (this.State)
{
case PluginState.InProgress:
case PluginState.Loaded:
case PluginState.LoadError:
throw new InvalidPluginOperationException($"Unable to disable {this.Name}, still loaded");
case PluginState.Unloaded:
break;
case PluginState.UnloadError:
break;
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();
}
private static void SetupLoaderConfig(LoaderConfig config)
{
config.IsUnloadable = true;
config.LoadInMemory = true;
config.PreferSharedTypes = false;
config.SharedAssemblies.Add(typeof(Lumina.GameData).Assembly.GetName());
config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName());
}
private void SaveManifest() => this.Manifest.Save(this.manifestFile);
}

View file

@ -1,99 +1,79 @@
using System.IO;
using Dalamud.Utility;
using Newtonsoft.Json;
namespace Dalamud.Plugin.Internal.Types
namespace Dalamud.Plugin.Internal.Types;
/// <summary>
/// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as
/// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk.
/// </summary>
internal record LocalPluginManifest : PluginManifest
{
/// <summary>
/// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as
/// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk.
/// 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>
internal record LocalPluginManifest : PluginManifest
{
/// <summary>
/// Gets or sets a value indicating whether the plugin is disabled and should not be loaded.
/// This value supercedes the ".disabled" file functionality and should not be included in the plugin master.
/// </summary>
public bool Disabled { get; set; } = false;
public bool Disabled { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the plugin should only be loaded when testing is enabled.
/// This value supercedes the ".testing" file functionality and should not be included in the plugin master.
/// </summary>
public bool Testing { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether the plugin should only be loaded when testing is enabled.
/// This value supersedes the ".testing" file functionality and should not be included in the plugin master.
/// </summary>
public bool Testing { get; set; }
/// <summary>
/// Gets or sets the 3rd party repo URL that this plugin was installed from. Used to display where the plugin was
/// sourced from on the installed plugin view. This should not be included in the plugin master. This value is null
/// when installed from the main repo.
/// </summary>
public string InstalledFromUrl { get; set; }
/// <summary>
/// Gets or sets the 3rd party repo URL that this plugin was installed from. Used to display where the plugin was
/// sourced from on the installed plugin view. This should not be included in the plugin master. This value is null
/// when installed from the main repo.
/// </summary>
public string InstalledFromUrl { get; set; } = string.Empty;
/// <summary>
/// Gets a value indicating whether this manifest is associated with a plugin that was installed from a third party
/// repo. Unless the manifest has been manually modified, this is determined by the InstalledFromUrl being null.
/// </summary>
public bool IsThirdParty => !string.IsNullOrEmpty(this.InstalledFromUrl);
/// <summary>
/// Gets a value indicating whether this manifest is associated with a plugin that was installed from a third party
/// repo. Unless the manifest has been manually modified, this is determined by the InstalledFromUrl being null.
/// </summary>
public bool IsThirdParty => !string.IsNullOrEmpty(this.InstalledFromUrl);
/// <summary>
/// Save a plugin manifest to file.
/// </summary>
/// <param name="manifestFile">Path to save at.</param>
public void Save(FileInfo manifestFile) => File.WriteAllText(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented));
/// <summary>
/// Save a plugin manifest to file.
/// </summary>
/// <param name="manifestFile">Path to save at.</param>
public void Save(FileInfo manifestFile) => File.WriteAllText(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented));
/// <summary>
/// Loads a plugin manifest from file.
/// </summary>
/// <param name="manifestFile">Path to the manifest.</param>
/// <returns>A <see cref="PluginManifest"/> object.</returns>
public static LocalPluginManifest Load(FileInfo manifestFile) => JsonConvert.DeserializeObject<LocalPluginManifest>(File.ReadAllText(manifestFile.FullName));
/// <summary>
/// Loads a plugin manifest from file.
/// </summary>
/// <param name="manifestFile">Path to the manifest.</param>
/// <returns>A <see cref="PluginManifest"/> object.</returns>
public static LocalPluginManifest Load(FileInfo manifestFile) => JsonConvert.DeserializeObject<LocalPluginManifest>(File.ReadAllText(manifestFile.FullName))!;
/// <summary>
/// A standardized way to get the plugin DLL name that should accompany a manifest file. May not exist.
/// </summary>
/// <param name="dir">Manifest directory.</param>
/// <param name="manifest">The manifest.</param>
/// <returns>The <see cref="LocalPlugin"/> file.</returns>
public static FileInfo GetPluginFile(DirectoryInfo dir, PluginManifest manifest) => new(Path.Combine(dir.FullName, $"{manifest.InternalName}.dll"));
/// <summary>
/// A standardized way to get the plugin DLL name that should accompany a manifest file. May not exist.
/// </summary>
/// <param name="dir">Manifest directory.</param>
/// <param name="manifest">The manifest.</param>
/// <returns>The <see cref="LocalPlugin"/> file.</returns>
public static FileInfo GetPluginFile(DirectoryInfo dir, PluginManifest manifest) => new(Path.Combine(dir.FullName, $"{manifest.InternalName}.dll"));
/// <summary>
/// A standardized way to get the manifest file that should accompany a plugin DLL. May not exist.
/// </summary>
/// <param name="dllFile">The plugin DLL.</param>
/// <returns>The <see cref="PluginManifest"/> file.</returns>
public static FileInfo GetManifestFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, Path.GetFileNameWithoutExtension(dllFile.Name) + ".json"));
/// <summary>
/// A standardized way to get the manifest file that should accompany a plugin DLL. May not exist.
/// </summary>
/// <param name="dllFile">The plugin DLL.</param>
/// <returns>The <see cref="PluginManifest"/> file.</returns>
public static FileInfo GetManifestFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName!, Path.GetFileNameWithoutExtension(dllFile.Name) + ".json"));
/// <summary>
/// A standardized way to get the obsolete .disabled file that should accompany a plugin DLL. May not exist.
/// </summary>
/// <param name="dllFile">The plugin DLL.</param>
/// <returns>The <see cref="PluginManifest"/> .disabled file.</returns>
public static FileInfo GetDisabledFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, ".disabled"));
/// <summary>
/// A standardized way to get the obsolete .disabled file that should accompany a plugin DLL. May not exist.
/// </summary>
/// <param name="dllFile">The plugin DLL.</param>
/// <returns>The <see cref="PluginManifest"/> .disabled file.</returns>
public static FileInfo GetDisabledFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName!, ".disabled"));
/// <summary>
/// A standardized way to get the obsolete .testing file that should accompany a plugin DLL. May not exist.
/// </summary>
/// <param name="dllFile">The plugin DLL.</param>
/// <returns>The <see cref="PluginManifest"/> .testing file.</returns>
public static FileInfo GetTestingFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, ".testing"));
/// <summary>
/// Check if this manifest is valid.
/// </summary>
/// <returns>Whether or not this manifest is valid.</returns>
public bool CheckSanity()
{
if (this.InternalName.IsNullOrEmpty())
return false;
if (this.Name.IsNullOrEmpty())
return false;
if (this.DalamudApiLevel == 0)
return false;
return true;
}
}
/// <summary>
/// A standardized way to get the obsolete .testing file that should accompany a plugin DLL. May not exist.
/// </summary>
/// <param name="dllFile">The plugin DLL.</param>
/// <returns>The <see cref="PluginManifest"/> .testing file.</returns>
public static FileInfo GetTestingFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName!, ".testing"));
}

View file

@ -4,177 +4,159 @@ using System.Collections.Generic;
using Dalamud.Game;
using Newtonsoft.Json;
namespace Dalamud.Plugin.Internal.Types
namespace Dalamud.Plugin.Internal.Types;
/// <summary>
/// Information about a plugin, packaged in a json file with the DLL.
/// </summary>
internal record PluginManifest
{
/// <summary>
/// Information about a plugin, packaged in a json file with the DLL.
/// Gets the author/s of the plugin.
/// </summary>
internal record PluginManifest
{
/// <summary>
/// Gets the author/s of the plugin.
/// </summary>
[JsonProperty]
public string? Author { get; init; }
[JsonProperty]
public string? Author { get; init; }
/// <summary>
/// Gets or sets the public name of the plugin.
/// </summary>
[JsonProperty]
public string Name { get; set; }
/// <summary>
/// Gets or sets the public name of the plugin.
/// </summary>
[JsonProperty]
public string Name { get; set; } = null!;
/// <summary>
/// Gets a punchline of the plugins functions.
/// </summary>
[JsonProperty]
public string? Punchline { get; init; }
/// <summary>
/// Gets a punchline of the plugins functions.
/// </summary>
[JsonProperty]
public string? Punchline { get; init; }
/// <summary>
/// Gets a description of the plugins functions.
/// </summary>
[JsonProperty]
public string? Description { get; init; }
/// <summary>
/// Gets a description of the plugins functions.
/// </summary>
[JsonProperty]
public string? Description { get; init; }
/// <summary>
/// Gets a changelog.
/// </summary>
[JsonProperty]
public string? Changelog { get; init; }
/// <summary>
/// Gets a changelog.
/// </summary>
[JsonProperty]
public string? Changelog { get; init; }
/// <summary>
/// Gets a list of tags defined on the plugin.
/// </summary>
[JsonProperty]
public List<string>? Tags { get; init; }
/// <summary>
/// Gets a list of tags defined on the plugin.
/// </summary>
[JsonProperty]
public List<string>? Tags { get; init; }
/// <summary>
/// Gets a list of category tags defined on the plugin.
/// </summary>
[JsonProperty]
public List<string>? CategoryTags { get; init; }
/// <summary>
/// Gets a list of category tags defined on the plugin.
/// </summary>
[JsonProperty]
public List<string>? CategoryTags { get; init; }
/// <summary>
/// Gets a value indicating whether or not the plugin is hidden in the plugin installer.
/// This value comes from the plugin master and is in addition to the list of hidden names kept by Dalamud.
/// </summary>
[JsonProperty]
public bool IsHide { get; init; }
/// <summary>
/// Gets a value indicating whether or not the plugin is hidden in the plugin installer.
/// This value comes from the plugin master and is in addition to the list of hidden names kept by Dalamud.
/// </summary>
[JsonProperty]
public bool IsHide { get; init; }
/// <summary>
/// Gets the internal name of the plugin, which should match the assembly name of the plugin.
/// </summary>
[JsonProperty]
public string InternalName { get; init; }
/// <summary>
/// Gets the internal name of the plugin, which should match the assembly name of the plugin.
/// </summary>
[JsonProperty]
public string InternalName { get; init; } = null!;
/// <summary>
/// Gets the current assembly version of the plugin.
/// </summary>
[JsonProperty]
public Version AssemblyVersion { get; init; }
/// <summary>
/// Gets the current assembly version of the plugin.
/// </summary>
[JsonProperty]
public Version AssemblyVersion { get; init; } = null!;
/// <summary>
/// Gets the current testing assembly version of the plugin.
/// </summary>
[JsonProperty]
public Version? TestingAssemblyVersion { get; init; }
/// <summary>
/// Gets the current testing assembly version of the plugin.
/// </summary>
[JsonProperty]
public Version? TestingAssemblyVersion { get; init; }
/// <summary>
/// Gets a value indicating whether the <see cref="AssemblyVersion"/> is not null.
/// </summary>
[JsonIgnore]
public bool HasAssemblyVersion => this.AssemblyVersion != null;
/// <summary>
/// Gets a value indicating whether the plugin is only available for testing.
/// </summary>
[JsonProperty]
public bool IsTestingExclusive { get; init; }
/// <summary>
/// Gets a value indicating whether the <see cref="TestingAssemblyVersion"/> is not null.
/// </summary>
[JsonIgnore]
public bool HasTestingAssemblyVersion => this.TestingAssemblyVersion != null;
/// <summary>
/// Gets an URL to the website or source code of the plugin.
/// </summary>
[JsonProperty]
public string? RepoUrl { get; init; }
/// <summary>
/// Gets a value indicating whether the plugin is only available for testing.
/// </summary>
[JsonProperty]
public bool IsTestingExclusive { get; init; }
/// <summary>
/// Gets the version of the game this plugin works with.
/// </summary>
[JsonProperty]
[JsonConverter(typeof(GameVersionConverter))]
public GameVersion? ApplicableVersion { get; init; } = GameVersion.Any;
/// <summary>
/// Gets an URL to the website or source code of the plugin.
/// </summary>
[JsonProperty]
public string? RepoUrl { get; init; }
/// <summary>
/// Gets the API level of this plugin. For the current API level, please see <see cref="PluginManager.DalamudApiLevel"/>
/// for the currently used API level.
/// </summary>
[JsonProperty]
public int DalamudApiLevel { get; init; } = PluginManager.DalamudApiLevel;
/// <summary>
/// Gets the version of the game this plugin works with.
/// </summary>
[JsonProperty]
[JsonConverter(typeof(GameVersionConverter))]
public GameVersion? ApplicableVersion { get; init; } = GameVersion.Any;
/// <summary>
/// Gets the number of downloads this plugin has.
/// </summary>
[JsonProperty]
public long DownloadCount { get; init; }
/// <summary>
/// Gets the API level of this plugin. For the current API level, please see <see cref="PluginManager.DalamudApiLevel"/>
/// for the currently used API level.
/// </summary>
[JsonProperty]
public int DalamudApiLevel { get; init; } = PluginManager.DalamudApiLevel;
/// <summary>
/// Gets the last time this plugin was updated.
/// </summary>
[JsonProperty]
public long LastUpdate { get; init; }
/// <summary>
/// Gets the number of downloads this plugin has.
/// </summary>
[JsonProperty]
public long DownloadCount { get; init; }
/// <summary>
/// Gets the download link used to install the plugin.
/// </summary>
[JsonProperty]
public string DownloadLinkInstall { get; init; } = null!;
/// <summary>
/// Gets the last time this plugin was updated.
/// </summary>
[JsonProperty]
public long LastUpdate { get; init; }
/// <summary>
/// Gets the download link used to update the plugin.
/// </summary>
[JsonProperty]
public string DownloadLinkUpdate { get; init; } = null!;
/// <summary>
/// Gets the download link used to install the plugin.
/// </summary>
[JsonProperty]
public string DownloadLinkInstall { get; init; }
/// <summary>
/// Gets the download link used to get testing versions of the plugin.
/// </summary>
[JsonProperty]
public string DownloadLinkTesting { get; init; } = null!;
/// <summary>
/// Gets the download link used to update the plugin.
/// </summary>
[JsonProperty]
public string DownloadLinkUpdate { get; init; }
/// <summary>
/// Gets the load priority for this plugin. Higher values means higher priority. 0 is default priority.
/// </summary>
[JsonProperty]
public int LoadPriority { get; init; }
/// <summary>
/// Gets the download link used to get testing versions of the plugin.
/// </summary>
[JsonProperty]
public string DownloadLinkTesting { get; init; }
/// <summary>
/// Gets a list of screenshot image URLs to show in the plugin installer.
/// </summary>
public List<string>? ImageUrls { get; init; }
/// <summary>
/// Gets the load priority for this plugin. Higher values means higher priority. 0 is default priority.
/// </summary>
[JsonProperty]
public int LoadPriority { get; init; }
/// <summary>
/// Gets an URL for the plugin's icon.
/// </summary>
public string? IconUrl { get; init; }
/// <summary>
/// Gets a list of screenshot image URLs to show in the plugin installer.
/// </summary>
public List<string>? ImageUrls { get; init; }
/// <summary>
/// Gets a value indicating whether this plugin accepts feedback.
/// </summary>
public bool AcceptsFeedback { get; init; } = true;
/// <summary>
/// Gets an URL for the plugin's icon.
/// </summary>
public string? IconUrl { get; init; }
/// <summary>
/// Gets a value indicating whether this plugin accepts feedback.
/// </summary>
public bool AcceptsFeedback { get; init; } = true;
/// <summary>
/// Gets a message that is shown to users when sending feedback.
/// </summary>
public string? FeedbackMessage { get; init; }
/// <summary>
/// Gets a value indicating the webhook URL feedback is sent to.
/// </summary>
public string? FeedbackWebhook { get; init; }
}
/// <summary>
/// Gets a message that is shown to users when sending feedback.
/// </summary>
public string? FeedbackMessage { get; init; }
}

View file

@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Dalamud.Logging.Internal;
using Newtonsoft.Json;
namespace Dalamud.Plugin.Internal.Types;
/// <summary>
/// This class represents a single plugin repository.
/// </summary>
internal class PluginRepository
{
private const string DalamudPluginsMasterUrl = "https://kamori.goats.dev/Plugin/PluginMaster";
private static readonly ModuleLog Log = new("PLUGINR");
private static readonly HttpClient HttpClient = new()
{
DefaultRequestHeaders =
{
CacheControl = new CacheControlHeaderValue
{
NoCache = true,
},
},
};
/// <summary>
/// Initializes a new instance of the <see cref="PluginRepository"/> class.
/// </summary>
/// <param name="pluginMasterUrl">The plugin master URL.</param>
/// <param name="isEnabled">Whether the plugin repo is enabled.</param>
public PluginRepository(string pluginMasterUrl, bool isEnabled)
{
this.PluginMasterUrl = pluginMasterUrl;
this.IsThirdParty = pluginMasterUrl != DalamudPluginsMasterUrl;
this.IsEnabled = isEnabled;
}
/// <summary>
/// Gets a new instance of the <see cref="PluginRepository"/> class for the main repo.
/// </summary>
public static PluginRepository MainRepo => new(DalamudPluginsMasterUrl, true);
/// <summary>
/// Gets the pluginmaster.json URL.
/// </summary>
public string PluginMasterUrl { get; }
/// <summary>
/// Gets a value indicating whether this plugin repository is from a third party.
/// </summary>
public bool IsThirdParty { get; }
/// <summary>
/// Gets a value indicating whether this repo is enabled.
/// </summary>
public bool IsEnabled { get; }
/// <summary>
/// Gets the plugin master list of available plugins.
/// </summary>
public ReadOnlyCollection<RemotePluginManifest>? PluginMaster { get; private set; }
/// <summary>
/// Gets the initialization state of the plugin repository.
/// </summary>
public PluginRepositoryState State { get; private set; }
/// <summary>
/// Reload the plugin master asynchronously in a task.
/// </summary>
/// <returns>The new state.</returns>
public async Task ReloadPluginMasterAsync()
{
this.State = PluginRepositoryState.InProgress;
this.PluginMaster = new List<RemotePluginManifest>().AsReadOnly();
try
{
Log.Information($"Fetching repo: {this.PluginMasterUrl}");
using var response = await HttpClient.GetAsync(this.PluginMasterUrl);
response.EnsureSuccessStatusCode();
var data = await response.Content.ReadAsStringAsync();
var pluginMaster = JsonConvert.DeserializeObject<List<RemotePluginManifest>>(data);
if (pluginMaster == null)
{
throw new Exception("Deserialized PluginMaster was null.");
}
pluginMaster.Sort((pm1, pm2) => string.Compare(pm1.Name, pm2.Name, StringComparison.Ordinal));
// Set the source for each remote manifest. Allows for checking if is 3rd party.
foreach (var manifest in pluginMaster)
{
manifest.SourceRepo = this;
}
this.PluginMaster = pluginMaster.AsReadOnly();
Log.Debug($"Successfully fetched repo: {this.PluginMasterUrl}");
this.State = PluginRepositoryState.Success;
}
catch (Exception ex)
{
Log.Error(ex, $"PluginMaster failed: {this.PluginMasterUrl}");
this.State = PluginRepositoryState.Fail;
}
}
}

View file

@ -1,28 +1,27 @@
namespace Dalamud.Plugin.Internal.Types
namespace Dalamud.Plugin.Internal.Types;
/// <summary>
/// Values representing plugin repository state.
/// </summary>
internal enum PluginRepositoryState
{
/// <summary>
/// Values representing plugin repository state.
/// State is unknown.
/// </summary>
internal enum PluginRepositoryState
{
/// <summary>
/// State is unknown.
/// </summary>
Unknown,
Unknown,
/// <summary>
/// Currently loading.
/// </summary>
InProgress,
/// <summary>
/// Currently loading.
/// </summary>
InProgress,
/// <summary>
/// Load was successful.
/// </summary>
Success,
/// <summary>
/// Load was successful.
/// </summary>
Success,
/// <summary>
/// Load failed.
/// </summary>
Fail,
}
/// <summary>
/// Load failed.
/// </summary>
Fail,
}

View file

@ -1,33 +1,32 @@
namespace Dalamud.Plugin.Internal.Types
namespace Dalamud.Plugin.Internal.Types;
/// <summary>
/// Values representing plugin load state.
/// </summary>
internal enum PluginState
{
/// <summary>
/// Values representing plugin load state.
/// Plugin is defined, but unloaded.
/// </summary>
internal enum PluginState
{
/// <summary>
/// Plugin is defined, but unloaded.
/// </summary>
Unloaded,
Unloaded,
/// <summary>
/// Plugin has thrown an error during unload.
/// </summary>
UnloadError,
/// <summary>
/// Plugin has thrown an error during unload.
/// </summary>
UnloadError,
/// <summary>
/// Currently loading.
/// </summary>
InProgress,
/// <summary>
/// Currently loading.
/// </summary>
InProgress,
/// <summary>
/// Load is successful.
/// </summary>
Loaded,
/// <summary>
/// Load is successful.
/// </summary>
Loaded,
/// <summary>
/// Plugin has thrown an error during loading.
/// </summary>
LoadError,
}
/// <summary>
/// Plugin has thrown an error during loading.
/// </summary>
LoadError,
}

View file

@ -1,30 +1,29 @@
using System;
namespace Dalamud.Plugin.Internal.Types
namespace Dalamud.Plugin.Internal.Types;
/// <summary>
/// Plugin update status.
/// </summary>
internal class PluginUpdateStatus
{
/// <summary>
/// Plugin update status.
/// Gets the plugin internal name.
/// </summary>
internal class PluginUpdateStatus
{
/// <summary>
/// Gets or sets the plugin internal name.
/// </summary>
public string InternalName { get; set; }
public string InternalName { get; init; } = null!;
/// <summary>
/// Gets or sets the plugin name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets the plugin name.
/// </summary>
public string Name { get; init; } = null!;
/// <summary>
/// Gets or sets the plugin version.
/// </summary>
public Version Version { get; set; }
/// <summary>
/// Gets the plugin version.
/// </summary>
public Version Version { get; init; } = null!;
/// <summary>
/// Gets or sets a value indicating whether the plugin was updated.
/// </summary>
public bool WasUpdated { get; set; }
}
/// <summary>
/// Gets or sets a value indicating whether the plugin was updated.
/// </summary>
public bool WasUpdated { get; set; }
}

View file

@ -1,18 +1,19 @@
using JetBrains.Annotations;
using Newtonsoft.Json;
namespace Dalamud.Plugin.Internal.Types
namespace Dalamud.Plugin.Internal.Types;
/// <summary>
/// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as
/// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk.
/// </summary>
[UsedImplicitly]
internal record RemotePluginManifest : PluginManifest
{
/// <summary>
/// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as
/// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk.
/// Gets or sets the plugin repository this manifest came from. Used in reporting which third party repo a manifest
/// may have come from in the plugins available view. This functionality should not be included in the plugin master.
/// </summary>
internal record RemotePluginManifest : PluginManifest
{
/// <summary>
/// Gets or sets the plugin repository this manifest came from. Used in reporting which third party repo a manifest
/// may have come from in the plugins available view. This functionality should not be included in the plugin master.
/// </summary>
[JsonIgnore]
public PluginRepository SourceRepo { get; set; } = null;
}
[JsonIgnore]
public PluginRepository SourceRepo { get; set; } = null!;
}

@ -1 +1 @@
Subproject commit 7d95dce097dce7aa6712e4ef499a9d4ad8fafed7
Subproject commit a2972adbd333d0ad9c127fff1cfc288d1cecf6b4