diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7ada48e50..8a4fdf2e3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,7 +42,48 @@ jobs: with: name: dalamud-artifact 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: name: Deploy dalamud-distrib staging if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }} diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index a870bee17..6b2f1300a 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.14 + 9.0.0.16 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 90a399d4c..836fb5ec8 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using CheapLoc; @@ -14,9 +15,9 @@ using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.Internal.Windows.PluginInstaller; +using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; using Dalamud.Utility; -using Serilog; namespace Dalamud.Game; @@ -60,6 +61,8 @@ internal class ChatHandlers : IServiceType // { XivChatType.Echo, Color.Gray }, // }; + private static readonly ModuleLog Log = new("CHATHANDLER"); + 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", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -110,6 +113,7 @@ internal class ChatHandlers : IServiceType private bool hasSeenLoadingMsg; private bool startedAutoUpdatingPlugins; + private CancellationTokenSource deferredAutoUpdateCts = new(); [ServiceManager.ServiceConstructor] private ChatHandlers(ChatGui chatGui) @@ -165,16 +169,19 @@ internal class ChatHandlers : IServiceType if (clientState == null) return; - if (type == XivChatType.Notice && !this.hasSeenLoadingMsg) - this.PrintWelcomeMessage(); + if (type == XivChatType.Notice) + { + if (!this.hasSeenLoadingMsg) + this.PrintWelcomeMessage(); + + if (!this.startedAutoUpdatingPlugins) + this.AutoUpdatePluginsWithRetry(); + } // For injections while logged in if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg) this.PrintWelcomeMessage(); - if (!this.startedAutoUpdatingPlugins) - this.AutoUpdatePlugins(); - #if !DEBUG && false if (!this.hasSeenLoadingMsg) return; @@ -264,24 +271,42 @@ internal class ChatHandlers : IServiceType 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.GetNullable(); var pluginManager = Service.GetNullable(); var notifications = Service.GetNullable(); 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()) { // Plugins aren't ready yet. // TODO: We should retry. This sucks, because it means we won't ever get here again until another notice. - return; + Log.Warning("Aborting auto-update because plugins weren't loaded or ready."); + return false; } this.startedAutoUpdatingPlugins = true; + Log.Debug("Beginning plugin auto-update process..."); Task.Run(() => pluginManager.UpdatePluginsAsync(true, !this.configuration.AutoUpdatePlugins, true)).ContinueWith(task => { this.IsAutoUpdateComplete = true; @@ -320,5 +345,7 @@ internal class ChatHandlers : IServiceType } } }); + + return true; } } diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 40aa72913..9f90ea1ad 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Numerics; @@ -273,7 +273,10 @@ internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITe this.fallbackTextureWrap?.Dispose(); 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) { diff --git a/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs b/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs index b7a2ffe2e..1a6830a3a 100644 --- a/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs +++ b/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs @@ -131,9 +131,16 @@ internal class AssemblyLoadContextBuilder /// or the default app context. /// /// The name of the assembly. + /// Pull assmeblies recursively. /// The builder. - 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(new[] { assemblyName }); while (names.TryDequeue(out var name)) diff --git a/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs b/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs index d3fcdc99e..0b2150069 100644 --- a/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs +++ b/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs @@ -46,7 +46,7 @@ internal class LoaderConfig /// Gets a list of assemblies which should be unified between the host and the plugin. /// /// what-are-shared-types - public ICollection SharedAssemblies { get; } = new List(); + public ICollection<(AssemblyName Name, bool Recursive)> SharedAssemblies { get; } = new List<(AssemblyName Name, bool Recursive)>(); /// /// Gets or sets a value indicating whether attempt to unify all types from a plugin with the host. diff --git a/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs b/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs index 4bb326ce4..e0629217a 100644 --- a/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs +++ b/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs @@ -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); } /// diff --git a/Dalamud/Plugin/Internal/Loader/PluginLoader.cs b/Dalamud/Plugin/Internal/Loader/PluginLoader.cs index 53aec60ef..63b47cf17 100644 --- a/Dalamud/Plugin/Internal/Loader/PluginLoader.cs +++ b/Dalamud/Plugin/Internal/Loader/PluginLoader.cs @@ -146,18 +146,14 @@ internal class PluginLoader : IDisposable 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 - // load would otherwise fail, allowing them to resolve assemblies not already loaded by Dalamud - // itself yet. - builder.AddProbingPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); - - // Also make sure that plugins do not load their own Dalamud assembly. - builder.PreferDefaultLoadContextAssembly(Assembly.GetExecutingAssembly().GetName()); + // Note: not adding Dalamud path here as a probing path. + // It will be dealt as the last resort from ManagedLoadContext.Load. + // See there for more details. return builder; } diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 0ef3d49f8..020abf437 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -958,7 +958,7 @@ internal partial class PluginManager : IDisposable, IServiceType autoUpdate ? PluginListInvalidationKind.AutoUpdate : PluginListInvalidationKind.Update, updatedList.Select(x => x.InternalName)); - Log.Information("Plugin update OK."); + Log.Information("Plugin update OK. {updateCount} plugins updated.", updatedList.Length); return updatedList; } @@ -1581,6 +1581,8 @@ internal partial class PluginManager : IDisposable, IServiceType private void DetectAvailablePluginUpdates() { + Log.Debug("Starting plugin update check..."); + lock (this.pluginListLock) { this.updatablePluginsList.Clear(); @@ -1615,10 +1617,12 @@ internal partial class PluginManager : IDisposable, IServiceType } } } + + Log.Debug("Update check found {updateCount} available updates.", this.updatablePluginsList.Count); } private void NotifyAvailablePluginsChanged() - { + { this.DetectAvailablePluginUpdates(); this.OnAvailablePluginsChanged?.InvokeSafely(); diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index aff9a8b43..0ddd4b23e 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -627,8 +627,18 @@ internal class LocalPlugin : IDisposable 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()); + + // 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() diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 97b814ca1..89e713c07 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 97b814ca15d147911cdac3059623185a57984e0a +Subproject commit 89e713c071dae13112550d3e754193704e230b03