diff --git a/.vscode/launch.json b/.vscode/launch.json index 20c18f27..8cd13cf4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,13 +5,41 @@ "name": "Debug Daybreak.Linux", "type": "dotnet", "request": "launch", - "projectPath": "${workspaceFolder}/Daybreak.Linux/Daybreak.Linux.csproj" + "projectPath": "${workspaceFolder}/Daybreak.Linux/Daybreak.Linux.csproj", + "console": "integratedTerminal", + "justMyCode": true, + "stopAtEntry": false, + "suppressJITOptimizations": false, + "symbolOptions": { + "searchMicrosoftSymbolServer": false, + "searchNuGetOrgSymbolServer": false + }, + "logging": { + "engineLogging": false, + "moduleLoad": false, + "exceptions": false, + "browserStdOut": false + } }, { "name": "Debug Daybreak.Windows", "type": "dotnet", "request": "launch", - "projectPath": "${workspaceFolder}/Daybreak.Windows/Daybreak.Windows.csproj" + "projectPath": "${workspaceFolder}/Daybreak.Windows/Daybreak.Windows.csproj", + "console": "integratedTerminal", + "justMyCode": true, + "stopAtEntry": false, + "suppressJITOptimizations": false, + "symbolOptions": { + "searchMicrosoftSymbolServer": false, + "searchNuGetOrgSymbolServer": false + }, + "logging": { + "engineLogging": false, + "moduleLoad": false, + "exceptions": false, + "browserStdOut": false + } } ] } diff --git a/Daybreak.API/EntryPoint.cs b/Daybreak.API/EntryPoint.cs index 225cf214..e685175b 100644 --- a/Daybreak.API/EntryPoint.cs +++ b/Daybreak.API/EntryPoint.cs @@ -182,24 +182,43 @@ public class EntryPoint private static void PreloadNativeDependencies(ScopedLogger logger) { + // First, check if gwca.dll is already loaded in the process (e.g. by GWToolbox). + // If so, reuse it via a ref-count bump on the existing path rather than loading + // a second copy from the Daybreak.API directory. Loading a second instance of + // gwca.dll would result in duplicate native modules in the process, which can + // lead to inconsistent state across mods that depend on it. + string? daybreakApiDir = default; foreach (ProcessModule module in Process.GetCurrentProcess().Modules) { - if (module.ModuleName?.Contains("Daybreak.API", StringComparison.OrdinalIgnoreCase) is true) + if (module.ModuleName?.Equals("gwca.dll", StringComparison.OrdinalIgnoreCase) is true) { - var moduleDir = Path.GetDirectoryName(module.FileName)!; - var gwcaPath = Path.Combine(moduleDir, "gwca.dll"); - if (File.Exists(gwcaPath)) - { - NativeLibrary.Load(gwcaPath); - logger.LogDebug($"Preloaded gwca.dll from {gwcaPath}"); - } - else - { - logger.LogError($"gwca.dll not found at {gwcaPath}"); - } - - break; + NativeLibrary.Load(module.FileName); + logger.LogDebug($"Reused already-loaded gwca.dll from {module.FileName}"); + return; } + + if (daybreakApiDir is null && + module.ModuleName?.Contains("Daybreak.API", StringComparison.OrdinalIgnoreCase) is true) + { + daybreakApiDir = Path.GetDirectoryName(module.FileName); + } + } + + if (daybreakApiDir is null) + { + logger.LogError("Could not locate Daybreak.API module directory to preload gwca.dll"); + return; + } + + var gwcaPath = Path.Combine(daybreakApiDir, "gwca.dll"); + if (File.Exists(gwcaPath)) + { + NativeLibrary.Load(gwcaPath); + logger.LogDebug($"Preloaded gwca.dll from {gwcaPath}"); + } + else + { + logger.LogError($"gwca.dll not found at {gwcaPath}"); } } } diff --git a/Daybreak.API/Interop/GWCA.cs b/Daybreak.API/Interop/GWCA.cs index da2d40ea..57ea5964 100644 --- a/Daybreak.API/Interop/GWCA.cs +++ b/Daybreak.API/Interop/GWCA.cs @@ -13,7 +13,7 @@ namespace Daybreak.API.Interop { /// -/// P/Invoke bindings for 527 C++ exports from gwca.dll (0 skipped). +/// P/Invoke bindings for 528 C++ exports from gwca.dll (0 skipped). /// Nested classes mirror the C++ namespace hierarchy (e.g. GW::Agents → GWCA.GW.Agents). /// Types annotated with [GWCAEquivalent] are used in signatures where available. /// @@ -59,7 +59,7 @@ public static unsafe partial class GWCA // [NAMESPACE] GWCA.GW.StoC popped at line 50 // [NAMESPACE] GWCA.GW.TargetFilter popped at line 112 // [NAMESPACE] GWCA.GW.Trade popped at line 22 - // [NAMESPACE] GWCA.GW.UI popped at line 774 + // [NAMESPACE] GWCA.GW.UI popped at line 31 // [NAMESPACE] GWCA.GW.UI.UIPacket popped at line 30 // [NAMESPACE] GWCA.GWCA popped at line 15 // GWCA.GW.AccountContext: 9 fields [OK] @@ -1759,6 +1759,11 @@ public static unsafe partial class GWCA [return: MarshalAs(UnmanagedType.U1)] public static partial bool SetHeroBehavior(uint value1, global::Daybreak.API.Interop.GWCA.GW.HeroBehavior heroBehavior2); + // GW::PartyMgr::SetHeroSkillDisabled + [LibraryImport(DllName, EntryPoint = "?SetHeroSkillDisabled@PartyMgr@GW@@YA_NII_N@Z")] + [return: MarshalAs(UnmanagedType.U1)] + public static partial bool SetHeroSkillDisabled(uint value1, uint value2, [MarshalAs(UnmanagedType.U1)] bool flag3); + // GW::PartyMgr::SetHeroTarget [LibraryImport(DllName, EntryPoint = "?SetHeroTarget@PartyMgr@GW@@YA_NII@Z")] [return: MarshalAs(UnmanagedType.U1)] @@ -8121,7 +8126,7 @@ public static unsafe partial class GWCA public static partial class Urgoz { internal const int HoppingVampire = 3796; - internal const int Urgoz_ = 3805; + internal const int Urgoz = 3805; public static partial class Deep { @@ -8255,7 +8260,7 @@ public static unsafe partial class GWCA internal const int LockedChest = 8192; // this is actually ->ExtraType internal const int MiniatureLegionnaire = 8035; - public static partial class Minipet_ + public static partial class Minipet { internal const int MiniatureConfessorDorian = 8344; internal const int MiniatureConfessorIsaiah = 8352; diff --git a/Daybreak.Core/Configuration/ProjectConfiguration.cs b/Daybreak.Core/Configuration/ProjectConfiguration.cs index 2fd1dcc7..9b162561 100644 --- a/Daybreak.Core/Configuration/ProjectConfiguration.cs +++ b/Daybreak.Core/Configuration/ProjectConfiguration.cs @@ -383,8 +383,11 @@ public class ProjectConfiguration : PluginConfigurationBase public override void RegisterMods(IModsProducer modsManager) { modsManager.RegisterMod(); - modsManager.RegisterMod(); + // Toolbox must be registered before Daybreak API so that Toolbox injects first + // and loads its gwca.dll. Daybreak API will then reuse the already-loaded gwca.dll + // instead of loading a second copy, avoiding duplicate native module loads. modsManager.RegisterMod(); + modsManager.RegisterMod(); modsManager.RegisterMod(); modsManager.RegisterMod(singleton: true); } diff --git a/Daybreak.Core/Services/Toolbox/ToolboxService.cs b/Daybreak.Core/Services/Toolbox/ToolboxService.cs index ac9e7d4f..fdf8bd29 100644 --- a/Daybreak.Core/Services/Toolbox/ToolboxService.cs +++ b/Daybreak.Core/Services/Toolbox/ToolboxService.cs @@ -366,6 +366,10 @@ internal sealed class ToolboxService( if (await this.processInjector.Inject(process, dll, cancellationToken)) { scopedLogger.LogDebug("Injected toolbox dll"); + // Give Toolbox a moment to load its bundled gwca.dll into the target process + // before any subsequent mod (e.g. Daybreak.API) tries to load its own copy. + // This ensures Daybreak.API's existing-module check sees gwca.dll and reuses it. + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); this.notificationService.NotifyInformation( title: "GWToolbox started", description: "GWToolbox has been injected"); diff --git a/Dependencies/GWCA/gwca.dll b/Dependencies/GWCA/gwca.dll index f53104dc..b4c27b91 100644 Binary files a/Dependencies/GWCA/gwca.dll and b/Dependencies/GWCA/gwca.dll differ