Merge branch 'master' into feature/separate-atlas-per-plugin

This commit is contained in:
srkizer 2024-01-15 18:05:23 +09:00 committed by GitHub
commit 5caed2f3b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 129 additions and 30 deletions

View file

@ -43,6 +43,47 @@ jobs:
name: dalamud-artifact name: dalamud-artifact
path: bin\Release path: bin\Release
check_api_compat:
name: "Check API Compatibility"
if: ${{ github.event_name == 'pull_request' }}
needs: build
runs-on: windows-latest
steps:
- name: "Install .NET SDK"
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7
- name: "Install ApiCompat"
run: |
dotnet tool install -g Microsoft.DotNet.ApiCompat.Tool
- name: "Download Proposed Artifacts"
uses: actions/download-artifact@v2
with:
name: dalamud-artifact
path: .\right
- name: "Download Live (Stg) Artifacts"
run: |
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
Expand-Archive -Force latest.zip "left"
- name: "Verify Compatibility"
run: |
$FILES_TO_VALIDATE = "Dalamud.dll","FFXIVClientStructs.dll","Lumina.dll","Lumina.Excel.dll"
$retcode = 0
foreach ($file in $FILES_TO_VALIDATE) {
$testout = ""
Write-Output "::group::=== API COMPATIBILITY CHECK: ${file} ==="
apicompat -l "left\${file}" -r "right\${file}" | Tee-Object -Variable testout
Write-Output "::endgroup::"
if ($testout -ne "APICompat ran successfully without finding any breaking changes.") {
Write-Output "::error::${file} did not pass. Please review it for problems."
$retcode = 1
}
}
exit $retcode
deploy_stg: deploy_stg:
name: Deploy dalamud-distrib staging name: Deploy dalamud-distrib staging
if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }} if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }}

View file

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

View file

@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CheapLoc; using CheapLoc;
@ -14,9 +15,9 @@ using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.Internal.Windows;
using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.PluginInstaller;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal;
using Dalamud.Utility; using Dalamud.Utility;
using Serilog;
namespace Dalamud.Game; namespace Dalamud.Game;
@ -60,6 +61,8 @@ internal class ChatHandlers : IServiceType
// { XivChatType.Echo, Color.Gray }, // { XivChatType.Echo, Color.Gray },
// }; // };
private static readonly ModuleLog Log = new("CHATHANDLER");
private readonly Regex rmtRegex = new( private readonly Regex rmtRegex = new(
@"4KGOLD|We have sufficient stock|VPK\.OM|[Gg]il for free|[Gg]il [Cc]heap|5GOLD|www\.so9\.com|Fast & Convenient|Cheap & Safety Guarantee|【Code|A O A U E|igfans|4KGOLD\.COM|Cheapest Gil with|pvp and bank on google|Selling Cheap GIL|ff14mogstation\.com|Cheap Gil 1000k|gilsforyou|server 1000K =|gils_selling|E A S Y\.C O M|bonus code|mins delivery guarantee|Sell cheap|Salegm\.com|cheap Mog|Off Code:|FF14Mog.com|使用する5オ|[Oo][Ff][Ff] [Cc]ode( *)[:;]|offers Fantasia", @"4KGOLD|We have sufficient stock|VPK\.OM|[Gg]il for free|[Gg]il [Cc]heap|5GOLD|www\.so9\.com|Fast & Convenient|Cheap & Safety Guarantee|【Code|A O A U E|igfans|4KGOLD\.COM|Cheapest Gil with|pvp and bank on google|Selling Cheap GIL|ff14mogstation\.com|Cheap Gil 1000k|gilsforyou|server 1000K =|gils_selling|E A S Y\.C O M|bonus code|mins delivery guarantee|Sell cheap|Salegm\.com|cheap Mog|Off Code:|FF14Mog.com|使用する5オ|[Oo][Ff][Ff] [Cc]ode( *)[:;]|offers Fantasia",
RegexOptions.Compiled | RegexOptions.IgnoreCase); RegexOptions.Compiled | RegexOptions.IgnoreCase);
@ -110,6 +113,7 @@ internal class ChatHandlers : IServiceType
private bool hasSeenLoadingMsg; private bool hasSeenLoadingMsg;
private bool startedAutoUpdatingPlugins; private bool startedAutoUpdatingPlugins;
private CancellationTokenSource deferredAutoUpdateCts = new();
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private ChatHandlers(ChatGui chatGui) private ChatHandlers(ChatGui chatGui)
@ -165,16 +169,19 @@ internal class ChatHandlers : IServiceType
if (clientState == null) if (clientState == null)
return; return;
if (type == XivChatType.Notice && !this.hasSeenLoadingMsg) if (type == XivChatType.Notice)
this.PrintWelcomeMessage(); {
if (!this.hasSeenLoadingMsg)
this.PrintWelcomeMessage();
if (!this.startedAutoUpdatingPlugins)
this.AutoUpdatePluginsWithRetry();
}
// For injections while logged in // For injections while logged in
if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg) if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg)
this.PrintWelcomeMessage(); this.PrintWelcomeMessage();
if (!this.startedAutoUpdatingPlugins)
this.AutoUpdatePlugins();
#if !DEBUG && false #if !DEBUG && false
if (!this.hasSeenLoadingMsg) if (!this.hasSeenLoadingMsg)
return; return;
@ -264,24 +271,42 @@ internal class ChatHandlers : IServiceType
this.hasSeenLoadingMsg = true; this.hasSeenLoadingMsg = true;
} }
private void AutoUpdatePlugins() private void AutoUpdatePluginsWithRetry()
{
var firstAttempt = this.AutoUpdatePlugins();
if (!firstAttempt)
{
Task.Run(() =>
{
Task.Delay(30_000, this.deferredAutoUpdateCts.Token);
this.AutoUpdatePlugins();
});
}
}
private bool AutoUpdatePlugins()
{ {
var chatGui = Service<ChatGui>.GetNullable(); var chatGui = Service<ChatGui>.GetNullable();
var pluginManager = Service<PluginManager>.GetNullable(); var pluginManager = Service<PluginManager>.GetNullable();
var notifications = Service<NotificationManager>.GetNullable(); var notifications = Service<NotificationManager>.GetNullable();
if (chatGui == null || pluginManager == null || notifications == null) if (chatGui == null || pluginManager == null || notifications == null)
return; {
Log.Warning("Aborting auto-update because a required service was not loaded.");
return false;
}
if (!pluginManager.ReposReady || !pluginManager.InstalledPlugins.Any() || !pluginManager.AvailablePlugins.Any()) if (!pluginManager.ReposReady || !pluginManager.InstalledPlugins.Any() || !pluginManager.AvailablePlugins.Any())
{ {
// Plugins aren't ready yet. // Plugins aren't ready yet.
// TODO: We should retry. This sucks, because it means we won't ever get here again until another notice. // TODO: We should retry. This sucks, because it means we won't ever get here again until another notice.
return; Log.Warning("Aborting auto-update because plugins weren't loaded or ready.");
return false;
} }
this.startedAutoUpdatingPlugins = true; this.startedAutoUpdatingPlugins = true;
Log.Debug("Beginning plugin auto-update process...");
Task.Run(() => pluginManager.UpdatePluginsAsync(true, !this.configuration.AutoUpdatePlugins, true)).ContinueWith(task => Task.Run(() => pluginManager.UpdatePluginsAsync(true, !this.configuration.AutoUpdatePlugins, true)).ContinueWith(task =>
{ {
this.IsAutoUpdateComplete = true; this.IsAutoUpdateComplete = true;
@ -320,5 +345,7 @@ internal class ChatHandlers : IServiceType
} }
} }
}); });
return true;
} }
} }

View file

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Numerics; using System.Numerics;
@ -273,7 +273,10 @@ internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITe
this.fallbackTextureWrap?.Dispose(); this.fallbackTextureWrap?.Dispose();
this.framework.Update -= this.FrameworkOnUpdate; this.framework.Update -= this.FrameworkOnUpdate;
Log.Verbose("Disposing {Num} left behind textures."); if (this.activeTextures.Count == 0)
return;
Log.Verbose("Disposing {Num} left behind textures.", this.activeTextures.Count);
foreach (var activeTexture in this.activeTextures) foreach (var activeTexture in this.activeTextures)
{ {

View file

@ -131,9 +131,16 @@ internal class AssemblyLoadContextBuilder
/// or the default app context. /// or the default app context.
/// </summary> /// </summary>
/// <param name="assemblyName">The name of the assembly.</param> /// <param name="assemblyName">The name of the assembly.</param>
/// <param name="recursive">Pull assmeblies recursively.</param>
/// <returns>The builder.</returns> /// <returns>The builder.</returns>
public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName) public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName, bool recursive)
{ {
if (!recursive)
{
this.defaultAssemblies.Add(assemblyName.Name);
return this;
}
var names = new Queue<AssemblyName>(new[] { assemblyName }); var names = new Queue<AssemblyName>(new[] { assemblyName });
while (names.TryDequeue(out var name)) while (names.TryDequeue(out var name))

View file

@ -46,7 +46,7 @@ internal class LoaderConfig
/// Gets a list of assemblies which should be unified between the host and the plugin. /// Gets a list of assemblies which should be unified between the host and the plugin.
/// </summary> /// </summary>
/// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md">what-are-shared-types</seealso> /// <seealso href="https://github.com/natemcmaster/DotNetCorePlugins/blob/main/docs/what-are-shared-types.md">what-are-shared-types</seealso>
public ICollection<AssemblyName> SharedAssemblies { get; } = new List<AssemblyName>(); public ICollection<(AssemblyName Name, bool Recursive)> SharedAssemblies { get; } = new List<(AssemblyName Name, bool Recursive)>();
/// <summary> /// <summary>
/// Gets or sets a value indicating whether attempt to unify all types from a plugin with the host. /// Gets or sets a value indicating whether attempt to unify all types from a plugin with the host.

View file

@ -194,7 +194,18 @@ internal class ManagedLoadContext : AssemblyLoadContext
} }
} }
return null; // https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/loading-managed#algorithm
// > These assemblies are loaded (load-by-name) as needed by the runtime.
// For load-by-name assembiles, the following will happen in order:
// (1) this.Load will be called.
// (2) AssemblyLoadContext.Default's cache will be referred for lookup.
// (3) Default probing will be done from PLATFORM_RESOURCE_ROOTS and APP_PATHS.
// https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/default-probing#managed-assembly-default-probing
// > TRUSTED_PLATFORM_ASSEMBLIES: List of platform and application assembly file paths.
// > APP_PATHS: is not populated by default and is omitted for most applications.
// If we return null here, if the assembly has not been already loaded, the resolution will fail.
// Therefore as the final attempt, we try loading from the default load context.
return this.defaultLoadContext.LoadFromAssemblyName(assemblyName);
} }
/// <summary> /// <summary>

View file

@ -146,18 +146,14 @@ internal class PluginLoader : IDisposable
builder.ShadowCopyNativeLibraries(); builder.ShadowCopyNativeLibraries();
} }
foreach (var assemblyName in config.SharedAssemblies) foreach (var (assemblyName, recursive) in config.SharedAssemblies)
{ {
builder.PreferDefaultLoadContextAssembly(assemblyName); builder.PreferDefaultLoadContextAssembly(assemblyName, recursive);
} }
// This allows plugins to search for dependencies in the Dalamud directory when their assembly // Note: not adding Dalamud path here as a probing path.
// load would otherwise fail, allowing them to resolve assemblies not already loaded by Dalamud // It will be dealt as the last resort from ManagedLoadContext.Load.
// itself yet. // See there for more details.
builder.AddProbingPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
// Also make sure that plugins do not load their own Dalamud assembly.
builder.PreferDefaultLoadContextAssembly(Assembly.GetExecutingAssembly().GetName());
return builder; return builder;
} }

View file

@ -958,7 +958,7 @@ internal partial class PluginManager : IDisposable, IServiceType
autoUpdate ? PluginListInvalidationKind.AutoUpdate : PluginListInvalidationKind.Update, autoUpdate ? PluginListInvalidationKind.AutoUpdate : PluginListInvalidationKind.Update,
updatedList.Select(x => x.InternalName)); updatedList.Select(x => x.InternalName));
Log.Information("Plugin update OK."); Log.Information("Plugin update OK. {updateCount} plugins updated.", updatedList.Length);
return updatedList; return updatedList;
} }
@ -1581,6 +1581,8 @@ internal partial class PluginManager : IDisposable, IServiceType
private void DetectAvailablePluginUpdates() private void DetectAvailablePluginUpdates()
{ {
Log.Debug("Starting plugin update check...");
lock (this.pluginListLock) lock (this.pluginListLock)
{ {
this.updatablePluginsList.Clear(); this.updatablePluginsList.Clear();
@ -1615,6 +1617,8 @@ internal partial class PluginManager : IDisposable, IServiceType
} }
} }
} }
Log.Debug("Update check found {updateCount} available updates.", this.updatablePluginsList.Count);
} }
private void NotifyAvailablePluginsChanged() private void NotifyAvailablePluginsChanged()

View file

@ -627,8 +627,18 @@ internal class LocalPlugin : IDisposable
config.IsUnloadable = true; config.IsUnloadable = true;
config.LoadInMemory = true; config.LoadInMemory = true;
config.PreferSharedTypes = false; config.PreferSharedTypes = false;
config.SharedAssemblies.Add(typeof(Lumina.GameData).Assembly.GetName());
config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName()); // Pin Lumina and its dependencies recursively (compatibility behavior).
// It currently only pulls in System.* anyway.
// TODO(api10): Remove this. We don't want to pin Lumina anymore, plugins should be able to provide their own.
config.SharedAssemblies.Add((typeof(Lumina.GameData).Assembly.GetName(), true));
config.SharedAssemblies.Add((typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName(), true));
// Make sure that plugins do not load their own Dalamud assembly.
// We do not pin this recursively; if a plugin loads its own assembly of Dalamud, it is always wrong,
// but plugins may load other versions of assemblies that Dalamud depends on.
config.SharedAssemblies.Add((typeof(EntryPoint).Assembly.GetName(), false));
config.SharedAssemblies.Add((typeof(Common.DalamudStartInfo).Assembly.GetName(), false));
} }
private void EnsureLoader() private void EnsureLoader()

@ -1 +1 @@
Subproject commit 97b814ca15d147911cdac3059623185a57984e0a Subproject commit 89e713c071dae13112550d3e754193704e230b03