diff --git a/.editorconfig b/.editorconfig index 8816a8511..2dbbff2dc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -24,6 +24,7 @@ csharp_preferred_modifier_order = public, private, protected, internal, new, abs csharp_style_var_elsewhere = true:suggestion csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion +dotnet_code_quality_unused_parameters = non_public dotnet_naming_rule.event_rule.severity = warning dotnet_naming_rule.event_rule.style = on_upper_camel_case_style dotnet_naming_rule.event_rule.symbols = event_symbols @@ -56,11 +57,13 @@ dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly -dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:suggestion -dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:suggestion -dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators =always_for_clarity:suggestion +dotnet_style_parentheses_in_other_binary_operators =always_for_clarity:suggestion +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion dotnet_style_predefined_type_for_member_access = true:suggestion dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion +dotnet_style_parentheses_in_other_operators=always_for_clarity:silent +dotnet_style_object_initializer = false # ReSharper properties resharper_align_linq_query = true @@ -105,8 +108,10 @@ 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 +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}] indent_style = space indent_size = 4 tab_width = 4 +dotnet_style_parentheses_in_other_operators=always_for_clarity:silent diff --git a/.github/workflows/delete-artifacts.yml b/.github/workflows/delete-artifacts.yml index 7eb2b001c..6191e6cba 100644 --- a/.github/workflows/delete-artifacts.yml +++ b/.github/workflows/delete-artifacts.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Remove old artifacts - uses: c-hive/gha-remove-artifacts@24dc23384a1fa6a058b79c73727ae0cb2200ca4c + uses: c-hive/gha-remove-artifacts@v1.2.0 with: age: '1 month' skip-tags: true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2293db95d..f19420022 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,15 +7,12 @@ jobs: name: Build on Windows runs-on: windows-2019 steps: - - uses: actions/checkout@v2 + - name: Checkout Dalamud + uses: actions/checkout@v2 with: submodules: recursive - - name: Setup Nuget - uses: nuget/setup-nuget@v1 - with: - nuget-version: latest - - name: Restore Nuget Packages - run: nuget restore Dalamud.sln + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v1.0.2 - name: Define VERSION run: | $env:COMMIT = $env:GITHUB_SHA.Substring(0, 7) @@ -25,18 +22,17 @@ jobs: ($env:REPO_NAME) >> VERSION ($env:BRANCH) >> VERSION ($env:COMMIT) >> VERSION - - name: Build DotNet4 - run: | - cd "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\" - .\MSBuild.exe $Env:GITHUB_WORKSPACE\Dalamud.sln /t:Build /p:Configuration=Release /p:DefineConstants=XL_NOAUTOUPDATE - - name: Run xUnit Tests - run: | - ${{github.workspace}}\packages\xunit.runner.console.2.4.1\tools\net472\xunit.console.exe ${{github.workspace}}\Dalamud.Test\bin\Release\Dalamud.Test.dll + - name: Build Dalamud + run: .\build.ps1 compile + - name: Test Dalamud + run: .\build.ps1 test + - name: Create hashlist + run: .\CreateHashList.ps1 .\bin\Release - name: Upload artifact uses: actions/upload-artifact@v2 with: name: dalamud-artifact - path: bin\ + path: bin\Release deploy_stg: name: Deploy dalamud-distrib staging diff --git a/.gitmodules b/.gitmodules index 57306103a..f063dd0b3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/FFXIVClientStructs"] path = lib/FFXIVClientStructs url = https://github.com/goatcorp/FFXIVClientStructs.git +[submodule "lib/SharpDX.Desktop"] + path = lib/SharpDX.Desktop + url = https://github.com/goatcorp/SharpDX.Desktop.git diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 000000000..65ec356a7 --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,112 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Build Schema", + "$ref": "#/definitions/build", + "definitions": { + "build": { + "type": "object", + "properties": { + "Configuration": { + "type": "string", + "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", + "enum": [ + "Debug", + "Release" + ] + }, + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "type": "string", + "description": "Host for execution. Default is 'automatic'", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI" + ] + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "CompileDalamud", + "CompileDalamudBoot", + "CompileInjector", + "CompileInjectorBoot", + "Restore", + "Test" + ] + } + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "CompileDalamud", + "CompileDalamudBoot", + "CompileInjector", + "CompileInjectorBoot", + "Restore", + "Test" + ] + } + }, + "Verbosity": { + "type": "string", + "description": "Logging verbosity during build execution. Default is 'Normal'", + "enum": [ + "Minimal", + "Normal", + "Quiet", + "Verbose" + ] + } + } + } + } +} \ No newline at end of file diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 000000000..34f14e26a --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Solution": "Dalamud.sln" +} \ No newline at end of file diff --git a/CreateHashList.ps1 b/CreateHashList.ps1 index 668c1c140..389c2adec 100644 --- a/CreateHashList.ps1 +++ b/CreateHashList.ps1 @@ -1,7 +1,11 @@ -$hashes = @{} +$hashes = [ordered]@{} -Get-ChildItem $args[0] -Exclude dalamud.txt,*.zip,*.pdb,*.ipdb | Foreach-Object { - $hashes.Add($_.Name, (Get-FileHash $_.FullName -Algorithm MD5).Hash) +Set-Location $args[0] + +Get-ChildItem -File -Recurse -Exclude dalamud.txt,*.zip,*.pdb,*.ipdb | Foreach-Object { + $key = ($_.FullName | Resolve-Path -Relative).TrimStart(".\\") + $val = (Get-FileHash $_.FullName -Algorithm MD5).Hash + $hashes.Add($key, $val) } -ConvertTo-Json $hashes | Out-File -FilePath (Join-Path $args[0] "hashes.json") \ No newline at end of file +$hashes | ConvertTo-Json | Out-File -FilePath "hashes.json" \ No newline at end of file diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj new file mode 100644 index 000000000..b1158d85a --- /dev/null +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj @@ -0,0 +1,105 @@ + + + + {55198DC3-A03D-408E-A8EB-2077780C8576} + Dalamud_Boot + Debug + x64 + + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + 10.0 + + + + DynamicLibrary + true + v142 + false + Unicode + ..\bin\$(Configuration)\ + obj\$(Configuration)\ + + + + + Level3 + true + true + stdcpplatest + MultiThreadedDebug + pch.h + ProgramDatabase + CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + + + Windows + true + false + ..\lib\CoreCLR;%(AdditionalLibraryDirectories) + + + + + true + false + _DEBUG;%(PreprocessorDefinitions) + + + false + false + + + + + true + true + NDEBUG;%(PreprocessorDefinitions) + + + true + true + + + + + + nethost.dll + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters new file mode 100644 index 000000000..afcc6e502 --- /dev/null +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters @@ -0,0 +1,62 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {18be40ac-9367-46ff-b848-4c528aa97a8d} + lib + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Library Files + + + Library Files + + + \ No newline at end of file diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp new file mode 100644 index 000000000..1abde0ed3 --- /dev/null +++ b/Dalamud.Boot/dllmain.cpp @@ -0,0 +1,66 @@ +#define WIN32_LEAN_AND_MEAN +#define DllExport extern "C" __declspec(dllexport) + +#include +#include +#include "..\lib\CoreCLR\CoreCLR.h" +#include "..\lib\CoreCLR\boot.h" + +HMODULE g_hModule; + +DllExport DWORD WINAPI Initialize(LPVOID lpParam) +{ + #if defined(_DEBUG) + ConsoleSetup(L"Dalamud Boot"); + #endif + + wchar_t _module_path[MAX_PATH]; + GetModuleFileNameW(g_hModule, _module_path, sizeof _module_path / 2); + std::filesystem::path fs_module_path(_module_path); + + std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.runtimeconfig.json").c_str()); + std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.dll").c_str()); + + // =========================================================================== // + + void* entrypoint_vfn; + int result = InitializeClrAndGetEntryPoint( + runtimeconfig_path, + module_path, + L"Dalamud.EntryPoint, Dalamud", + L"Initialize", + L"Dalamud.EntryPoint+InitDelegate, Dalamud", + &entrypoint_vfn); + + if (result != 0) + return result; + + typedef void (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(LPVOID); + custom_component_entry_point_fn entrypoint_fn = reinterpret_cast(entrypoint_vfn); + + printf("Initializing Dalamud... "); + entrypoint_fn(lpParam); + printf("Done!\n"); + + // =========================================================================== // + + #if defined(_DEBUG) + FreeConsole(); + #endif + + return 0; +} + +BOOL APIENTRY DllMain(const HMODULE hModule, const DWORD dwReason, LPVOID lpReserved) { + DisableThreadLibraryCalls(hModule); + + switch (dwReason) + { + case DLL_PROCESS_ATTACH: + g_hModule = hModule; + break; + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} diff --git a/Dalamud.Boot/main.cpp b/Dalamud.Boot/main.cpp new file mode 100644 index 000000000..0eb0f9055 --- /dev/null +++ b/Dalamud.Boot/main.cpp @@ -0,0 +1,49 @@ +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include "CoreCLR.h" +#include "boot.h" + +int wmain(int argc, char** argv) +{ + #if defined(_DEBUG) + ConsoleSetup(L"Dalamud Injector Boot"); + #endif + + wchar_t _module_path[MAX_PATH]; + GetModuleFileNameW(NULL, _module_path, sizeof _module_path / 2); + std::filesystem::path fs_module_path(_module_path); + + std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.runtimeconfig.json").c_str()); + std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.dll").c_str()); + + // =========================================================================== // + + void* entrypoint_vfn; + int result = InitializeClrAndGetEntryPoint( + runtimeconfig_path, + module_path, + L"Dalamud.Injector.EntryPoint, Dalamud.Injector", + L"Main", + L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector", + &entrypoint_vfn); + + if (result != 0) + return result; + + typedef void (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, char**); + custom_component_entry_point_fn entrypoint_fn = reinterpret_cast(entrypoint_vfn); + + printf("Running Dalamud Injector... "); + entrypoint_fn(argc, argv); + printf("Done!\n"); + + // =========================================================================== // + + #if defined(_DEBUG) + FreeConsole(); + #endif + + return 0; +} diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj new file mode 100644 index 000000000..8c714785c --- /dev/null +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -0,0 +1,61 @@ + + + Dalamud.CorePlugin + net5.0-windows + x64 + x64;AnyCPU + 9.0 + true + false + false + IDE0003 + + + + true + full + false + $(appData)\XIVLauncher\devPlugins\ + DEBUG;TRACE + prompt + 4 + + + + pdbonly + true + $(appData)\XIVLauncher\devPlugins\ + TRACE + prompt + 4 + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + false + + + false + + + false + + + false + + + diff --git a/Dalamud.CorePlugin/GlobalSuppressions.cs b/Dalamud.CorePlugin/GlobalSuppressions.cs new file mode 100644 index 000000000..9b5d219bb --- /dev/null +++ b/Dalamud.CorePlugin/GlobalSuppressions.cs @@ -0,0 +1,16 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +// General +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "Preventing long lines", Scope = "namespaceanddescendants", Target = "~N:Dalamud.CorePlugin")] +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "I like regions", Scope = "namespaceanddescendants", Target = "~N:Dalamud.CorePlugin")] +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1123:Do not place regions within elements", Justification = "I like regions in elements too", Scope = "namespaceanddescendants", Target = "~N:Dalamud.CorePlugin")] +[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying", Scope = "namespaceanddescendants", Target = "~N:Dalamud.CorePlugin")] +[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud.CorePlugin")] +[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud.CorePlugin")] +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1127:Generic type constraints should be on their own line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud.CorePlugin")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")] diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs new file mode 100644 index 000000000..d2f140d72 --- /dev/null +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -0,0 +1,96 @@ +using System; +using System.IO; + +using Dalamud.Interface.Windowing; +using Dalamud.Plugin; + +namespace Dalamud.CorePlugin +{ + /// + /// This class is a a plugin testbed for developing new Dalamud features with easy access to Dalamud itself. + /// Be careful to not commit anything extra. + /// + public sealed class PluginImpl : IDalamudPlugin + { + private readonly WindowSystem windowSystem = new("Dalamud.CorePlugin"); + private Localization localizationManager; + + /// + public string Name => "Dalamud.CorePlugin"; + + /// + /// Gets the plugin interface. + /// + internal DalamudPluginInterface Interface { get; private set; } + + /// + public void Initialize(DalamudPluginInterface pluginInterface) + { + try + { + this.InitLoc(); + + this.Interface = pluginInterface; + + // this.windowSystem.AddWindow(your_window); + + this.Interface.UiBuilder.OnBuildUi += this.OnDraw; + this.Interface.UiBuilder.OnOpenConfigUi += this.OnOpenConfigUi; + + this.Interface.CommandManager.AddHandler("/di", new(this.OnCommand) { HelpMessage = $"Access the {this.Name} plugin." }); + } + catch (Exception ex) + { + PluginLog.Error(ex, "kaboom"); + } + } + + /// + public void Dispose() + { + this.Interface.CommandManager.RemoveHandler("/di"); + + this.Interface.UiBuilder.OnBuildUi -= this.OnDraw; + + this.windowSystem.RemoveAllWindows(); + + this.Interface.Dispose(); + } + + private void InitLoc() + { + // CheapLoc needs to be reinitialized here because it tracks the setup by assembly name. New assembly, new setup. + this.localizationManager = new Localization(Path.Combine(Dalamud.Instance.AssetDirectory.FullName, "UIRes", "loc", "dalamud"), "dalamud_"); + if (!string.IsNullOrEmpty(Dalamud.Instance.Configuration.LanguageOverride)) + { + this.localizationManager.SetupWithLangCode(Dalamud.Instance.Configuration.LanguageOverride); + } + else + { + this.localizationManager.SetupWithUiCulture(); + } + } + + private void OnDraw() + { + try + { + this.windowSystem.Draw(); + } + catch (Exception ex) + { + PluginLog.Error(ex, "Boom"); + } + } + + private void OnCommand(string command, string args) + { + // this.window.IsOpen = true; + } + + private void OnOpenConfigUi(object sender, EventArgs e) + { + // this.window.IsOpen = true; + } + } +} diff --git a/Dalamud.CorePlugin/PluginWindow.cs b/Dalamud.CorePlugin/PluginWindow.cs new file mode 100644 index 000000000..e604389ba --- /dev/null +++ b/Dalamud.CorePlugin/PluginWindow.cs @@ -0,0 +1,47 @@ +using System; +using System.Numerics; + +using Dalamud.Interface.Windowing; +using ImGuiNET; + +namespace Dalamud.CorePlugin +{ + /// + /// Class responsible for drawing the plugin installer. + /// + internal class PluginWindow : Window, IDisposable + { + private static readonly ModuleLog Log = new("CorePlugin"); + + private readonly Dalamud dalamud; + + /// + /// Initializes a new instance of the class. + /// + /// The Dalamud instance. + public PluginWindow(Dalamud dalamud) + : base("CorePlugin", ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoScrollbar) + { + this.dalamud = dalamud; + this.IsOpen = true; + + this.Size = new Vector2(810, 520); + this.SizeCondition = ImGuiCond.Always; + } + + /// + public void Dispose() + { + } + + /// + public override void OnOpen() + { + } + + /// + public override void Draw() + { + } + } +} diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj new file mode 100644 index 000000000..8257d42aa --- /dev/null +++ b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj @@ -0,0 +1,113 @@ + + + + {8874326B-E755-4D13-90B4-59AB263A3E6B} + Dalamud_Injector_Boot + Debug + x64 + + + + Debug + x64 + + + Release + x64 + + + + 16.0 + Win32Proj + 10.0 + Dalamud.Injector + + + + Application + true + v142 + false + Unicode + ..\bin\$(Configuration)\ + obj\$(Configuration)\ + + + + + Level3 + true + true + stdcpplatest + MultiThreadedDebug + pch.h + ProgramDatabase + CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + + + Console + true + false + ..\lib\CoreCLR;%(AdditionalLibraryDirectories) + $(OutDir)$(TargetName).Boot.pdb + + + + + true + false + _DEBUG;%(PreprocessorDefinitions) + + + false + false + + + + + true + true + NDEBUG;%(PreprocessorDefinitions) + + + true + true + + + + + + nethost.dll + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters new file mode 100644 index 000000000..47f80ab3e --- /dev/null +++ b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters @@ -0,0 +1,75 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {4faac519-3a73-4b2b-96e7-fb597f02c0be} + ico;rc + + + {6aff1bed-6979-4bc9-94e8-ddafb626e6bf} + + + + + Resource Files + + + + + Resource Files + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Library Files + + + Library Files + + + \ No newline at end of file diff --git a/Dalamud.Injector/dalamud.ico b/Dalamud.Injector.Boot/dalamud.ico similarity index 99% rename from Dalamud.Injector/dalamud.ico rename to Dalamud.Injector.Boot/dalamud.ico index 98b9a9b9f..1cd63765d 100644 Binary files a/Dalamud.Injector/dalamud.ico and b/Dalamud.Injector.Boot/dalamud.ico differ diff --git a/Dalamud.Injector.Boot/main.cpp b/Dalamud.Injector.Boot/main.cpp new file mode 100644 index 000000000..545a68601 --- /dev/null +++ b/Dalamud.Injector.Boot/main.cpp @@ -0,0 +1,49 @@ +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include "..\lib\CoreCLR\CoreCLR.h" +#include "..\lib\CoreCLR\boot.h" + +int wmain(int argc, char** argv) +{ + #if defined(_DEBUG) + ConsoleSetup(L"Dalamud Injector Boot"); + #endif + + wchar_t _module_path[MAX_PATH]; + GetModuleFileNameW(NULL, _module_path, sizeof _module_path / 2); + std::filesystem::path fs_module_path(_module_path); + + std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.runtimeconfig.json").c_str()); + std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.dll").c_str()); + + // =========================================================================== // + + void* entrypoint_vfn; + int result = InitializeClrAndGetEntryPoint( + runtimeconfig_path, + module_path, + L"Dalamud.Injector.EntryPoint, Dalamud.Injector", + L"Main", + L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector", + &entrypoint_vfn); + + if (result != 0) + return result; + + typedef void (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, char**); + custom_component_entry_point_fn entrypoint_fn = reinterpret_cast(entrypoint_vfn); + + printf("Running Dalamud Injector... "); + entrypoint_fn(argc, argv); + printf("Done!\n"); + + // =========================================================================== // + + #if defined(_DEBUG) + FreeConsole(); + #endif + + return 0; +} diff --git a/Dalamud.Injector.Boot/resources.rc b/Dalamud.Injector.Boot/resources.rc new file mode 100644 index 000000000..8369e82a1 --- /dev/null +++ b/Dalamud.Injector.Boot/resources.rc @@ -0,0 +1 @@ +MAINICON ICON "dalamud.ico" diff --git a/Dalamud.Injector/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj index 5d76a11f6..eac9f1009 100644 --- a/Dalamud.Injector/Dalamud.Injector.csproj +++ b/Dalamud.Injector/Dalamud.Injector.csproj @@ -1,60 +1,89 @@ + - AnyCPU - net48 - 8.0 - AnyCPU;x64 - - - WinExe - $(SolutionDir)bin - false - true - Portable - IDE1006;CS1701;CS1702 - true - $(SolutionDir)\bin\Dalamud.Injector.xml + net5.0 + win-x64 + x64 + x64;AnyCPU + 9.0 + + 5.2.4.6 + XIV Launcher addon injector + $(InjectorVersion) + $(InjectorVersion) + $(InjectorVersion) + + + + Library + ..\bin\$(Configuration)\ + false + false + true + false + + + + + true + + + + true + true + portable + true + annotations true - 5.2.4.6 - 5.2.4.6 - XIVLauncher addon injection - 5.2.4.6 + + + + Debug + + + DEBUG;TRACE $(MSBuildProjectDirectory)\ $(AppOutputBase)=C:\goatsoft\companysecrets\injector\ - true - - false - - - dalamud.ico + + + IDE1006;CS1591;CS1701;CS1702 + + + + + - - - - - - - - + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + - - + + - + + + + + + + - - - diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs new file mode 100644 index 000000000..eff8b81a9 --- /dev/null +++ b/Dalamud.Injector/EntryPoint.cs @@ -0,0 +1,275 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; + +using Dalamud.Game; +using Dalamud.Interface.Internal; +using Newtonsoft.Json; +using Reloaded.Memory.Buffers; +using Serilog; +using Serilog.Core; +using Serilog.Events; + +using static Dalamud.Injector.NativeFunctions; + +namespace Dalamud.Injector +{ + /// + /// Entrypoint to the program. + /// + public sealed class EntryPoint + { + /// + /// A delegate used during initialization of the CLR from Dalamud.Injector.Boot. + /// + /// Count of arguments. + /// char** string arguments. + public delegate void MainDelegate(int argc, IntPtr argvPtr); + + /// + /// Start the Dalamud injector. + /// + /// Count of arguments. + /// byte** string arguments. + public static void Main(int argc, IntPtr argvPtr) + { + InitUnhandledException(); + InitLogging(); + + var args = new string[argc]; + + unsafe + { + var argv = (IntPtr*)argvPtr; + for (var i = 0; i < argc; i++) + { + args[i] = Marshal.PtrToStringUni(argv[i]); + } + } + + var cwd = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory; + if (cwd.FullName != Directory.GetCurrentDirectory()) + { + Log.Debug($"Changing cwd to {cwd}"); + Directory.SetCurrentDirectory(cwd.FullName); + } + + var process = GetProcess(args.ElementAtOrDefault(1)); + var startInfo = GetStartInfo(args.ElementAtOrDefault(2), process); + + startInfo.WorkingDirectory = Directory.GetCurrentDirectory(); + + // This seems to help with the STATUS_INTERNAL_ERROR condition + Thread.Sleep(1000); + + Inject(process, startInfo); + + Thread.Sleep(1000); + } + + private static void InitUnhandledException() + { + AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) => + { + if (Log.Logger == null) + { + Console.WriteLine($"A fatal error has occurred: {eventArgs.ExceptionObject}"); + } + 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}"); + } + } + +#if DEBUG + var caption = "Debug Error"; + var message = + $"Couldn't inject.\nMake sure that Dalamud was not injected into your target process " + + $"as a release build before and that the target process can be accessed with VM_WRITE permissions.\n\n" + + $"{eventArgs.ExceptionObject}"; +#else + var caption = "XIVLauncher Error"; + var message = + "Failed to inject the XIVLauncher in-game addon.\nPlease try restarting your game and your PC.\n" + + "If this keeps happening, please report this error."; +#endif + _ = MessageBoxW(IntPtr.Zero, message, caption, MessageBoxType.IconError | MessageBoxType.Ok); + + Environment.Exit(0); + }; + } + + private static void InitLogging() + { + var baseDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + +#if DEBUG + var logPath = Path.Combine(baseDirectory, "injector.log"); +#else + var logPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.injector.log"); +#endif + + var levelSwitch = new LoggingLevelSwitch(); + +#if DEBUG + levelSwitch.MinimumLevel = LogEventLevel.Verbose; +#else + levelSwitch.MinimumLevel = LogEventLevel.Information; +#endif + + Log.Logger = new LoggerConfiguration() + .WriteTo.Async(a => a.File(logPath)) + .WriteTo.Sink(SerilogEventSink.Instance) + .MinimumLevel.ControlledBy(levelSwitch) + .CreateLogger(); + } + + private static Process GetProcess(string arg) + { + Process process; + + var pid = -1; + if (arg != default) + { + pid = int.Parse(arg); + } + + switch (pid) + { + case -1: + process = Process.GetProcessesByName("ffxiv_dx11").FirstOrDefault(); + + if (process == default) + { + throw new Exception("Could not find process"); + } + + break; + case -2: + var exePath = "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn\\game\\ffxiv_dx11.exe"; + var exeArgs = new StringBuilder() + .Append("DEV.TestSID=0 DEV.UseSqPack=1 DEV.DataPathType=1 ") + .Append("DEV.LobbyHost01=127.0.0.1 DEV.LobbyPort01=54994 ") + .Append("DEV.LobbyHost02=127.0.0.1 DEV.LobbyPort02=54994 ") + .Append("DEV.LobbyHost03=127.0.0.1 DEV.LobbyPort03=54994 ") + .Append("DEV.LobbyHost04=127.0.0.1 DEV.LobbyPort04=54994 ") + .Append("DEV.LobbyHost05=127.0.0.1 DEV.LobbyPort05=54994 ") + .Append("DEV.LobbyHost06=127.0.0.1 DEV.LobbyPort06=54994 ") + .Append("DEV.LobbyHost07=127.0.0.1 DEV.LobbyPort07=54994 ") + .Append("DEV.LobbyHost08=127.0.0.1 DEV.LobbyPort08=54994 ") + .Append("SYS.Region=0 language=1 version=1.0.0.0 ") + .Append("DEV.MaxEntitledExpansionID=2 DEV.GMServerHost=127.0.0.1 DEV.GameQuitMessageBox=0").ToString(); + process = Process.Start(exePath, exeArgs); + Thread.Sleep(1000); + break; + default: + process = Process.GetProcessById(pid); + break; + } + + return process; + } + + private static DalamudStartInfo GetStartInfo(string arg, Process process) + { + DalamudStartInfo startInfo; + + if (arg != default) + { + startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(arg))); + } + else + { + var ffxivDir = Path.GetDirectoryName(process.MainModule.FileName); + var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher"); + + var gameVerStr = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver")); + var gameVer = GameVersion.Parse(gameVerStr); + + startInfo = new DalamudStartInfo + { + WorkingDirectory = null, + ConfigurationPath = Path.Combine(xivlauncherDir, "dalamudConfig.json"), + PluginDirectory = Path.Combine(xivlauncherDir, "installedPlugins"), + DefaultPluginDirectory = Path.Combine(xivlauncherDir, "devPlugins"), + AssetDirectory = Path.Combine(xivlauncherDir, "dalamudAssets"), + GameVersion = gameVer, + Language = ClientLanguage.English, + OptOutMbCollection = false, + }; + + Log.Debug( + "Creating a new StartInfo with:\n" + + $" WorkingDirectory: {startInfo.WorkingDirectory}\n" + + $" ConfigurationPath: {startInfo.ConfigurationPath}\n" + + $" PluginDirectory: {startInfo.PluginDirectory}\n" + + $" DefaultPluginDirectory: {startInfo.DefaultPluginDirectory}\n" + + $" AssetDirectory: {startInfo.AssetDirectory}\n" + + $" GameVersion: {startInfo.GameVersion}\n" + + $" Language: {startInfo.Language}\n" + + $" OptOutMbCollection: {startInfo.OptOutMbCollection}"); + + Log.Information("A Dalamud start info was not found in the program arguments. One has been generated for you."); + Log.Information("Copy the following contents into the program arguments:"); + + var startInfoJson = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(startInfo))); + Log.Information(startInfoJson); + } + + return startInfo; + } + + private static void Inject(Process process, DalamudStartInfo startInfo) + { + var nethostName = "nethost.dll"; + var bootName = "Dalamud.Boot.dll"; + + var nethostPath = Path.GetFullPath(nethostName); + var bootPath = Path.GetFullPath(bootName); + + // ====================================================== + + using var injector = new Injector(process); + + injector.LoadLibrary(nethostPath, out _); + injector.LoadLibrary(bootPath, out var bootModule); + + // ====================================================== + + var startInfoJson = JsonConvert.SerializeObject(startInfo); + var startInfoBytes = Encoding.UTF8.GetBytes(startInfoJson); + + using var startInfoBuffer = new MemoryBufferHelper(process).CreatePrivateMemoryBuffer(startInfoBytes.Length + 0x8); + var startInfoAddress = startInfoBuffer.Add(startInfoBytes); + + if (startInfoAddress == IntPtr.Zero) + throw new Exception("Unable to allocate start info JSON"); + + injector.GetFunctionAddress(bootModule, "Initialize", out var initAddress); + injector.CallRemoteFunction(initAddress, startInfoAddress, out var exitCode); + + // ====================================================== + + if (exitCode > 0) + { + Log.Error($"Dalamud.Boot::Initialize returned {exitCode}"); + return; + } + + Log.Information("Done"); + } + } +} diff --git a/Dalamud.Injector/GlobalSuppressions.cs b/Dalamud.Injector/GlobalSuppressions.cs index 3fca475cc..e1978fae4 100644 --- a/Dalamud.Injector/GlobalSuppressions.cs +++ b/Dalamud.Injector/GlobalSuppressions.cs @@ -6,14 +6,8 @@ using System.Diagnostics.CodeAnalysis; // General -[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "Preventing long lines", Scope = "namespaceanddescendants", Target = "~N:Dalamud")] -[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "I like regions", Scope = "namespaceanddescendants", Target = "~N:Dalamud")] -[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1123:Do not place regions within elements", Justification = "I like regions in elements too", Scope = "namespaceanddescendants", Target = "~N:Dalamud")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")] [assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying", Scope = "namespaceanddescendants", Target = "~N:Dalamud")] [assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")] [assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")] -[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1127:Generic type constraints should be on their own line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")] -[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")] - -// Program.cs -[assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Used during #if DEBUG", Scope = "member", Target = "~M:Dalamud.Injector.Program.NativeInject(System.Diagnostics.Process)")] +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "I'll make what I want static", Scope = "namespaceanddescendants", Target = "~N:Dalamud")] diff --git a/Dalamud.Injector/Injector.cs b/Dalamud.Injector/Injector.cs new file mode 100644 index 000000000..3f9c14046 --- /dev/null +++ b/Dalamud.Injector/Injector.cs @@ -0,0 +1,303 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +using Iced.Intel; +using PeNet; +using PeNet.Header.Pe; +using Reloaded.Memory.Buffers; +using Reloaded.Memory.Sources; +using Reloaded.Memory.Utilities; + +using static Dalamud.Injector.NativeFunctions; +using static Iced.Intel.AssemblerRegisters; + +namespace Dalamud.Injector +{ + /// + /// This class implements injecting into a remote process. It is a highly stripped down version of the + /// https://github.com/Reloaded-Project injector/assembler implementation due to issues with Lutris and + /// Wine. + /// + internal sealed class Injector : IDisposable + { + private readonly Process targetProcess; + private readonly ExternalMemory extMemory; + private readonly CircularBuffer circularBuffer; + private readonly PrivateMemoryBuffer privateBuffer; + + private IntPtr loadLibraryShellPtr; + private IntPtr loadLibraryRetPtr; + + private IntPtr getProcAddressShellPtr; + private IntPtr getProcAddressRetPtr; + + /// + /// Initializes a new instance of the class. + /// + /// Process to inject. + public Injector(Process targetProcess) + { + this.targetProcess = targetProcess; + + this.extMemory = new ExternalMemory(targetProcess); + this.circularBuffer = new CircularBuffer(4096, this.extMemory); + this.privateBuffer = new MemoryBufferHelper(targetProcess).CreatePrivateMemoryBuffer(4096); + + using var kernel32Module = this.GetProcessModule("KERNEL32.DLL"); + var kernel32PeFile = new PeFile(kernel32Module.FileName); + var kernel32Exports = kernel32PeFile.ExportedFunctions; + + this.SetupLoadLibrary(kernel32Module, kernel32Exports); + this.SetupGetProcAddress(kernel32Module, kernel32Exports); + } + + /// + /// Finalizes an instance of the class. + /// + ~Injector() => this.Dispose(); + + /// + public void Dispose() + { + GC.SuppressFinalize(this); + + this.targetProcess?.Dispose(); + this.circularBuffer?.Dispose(); + this.privateBuffer?.Dispose(); + } + + /// + /// Load a module by absolute file path. + /// + /// Absolute file path. + /// Address to the module. + public void LoadLibrary(string modulePath, out IntPtr address) + { + var lpParameter = this.WriteNullTerminatedUnicodeString(modulePath); + + if (lpParameter == IntPtr.Zero) + throw new Exception("Unable to allocate LoadLibraryW parameter"); + + var threadHandle = CreateRemoteThread( + this.targetProcess.Handle, + IntPtr.Zero, + UIntPtr.Zero, + this.loadLibraryShellPtr, + lpParameter, + CreateThreadFlags.RunImmediately, + out _); + + _ = WaitForSingleObject(threadHandle, uint.MaxValue); + + this.extMemory.Read(this.loadLibraryRetPtr, out address); + + if (address == IntPtr.Zero) + throw new Exception($"Error calling LoadLibraryW with {modulePath}"); + } + + /// + /// Get the address of an exported module function. + /// + /// Module address. + /// Name of the exported method. + /// Address to the function. + public void GetFunctionAddress(IntPtr module, string functionName, out IntPtr address) + { + var getProcAddressParams = new GetProcAddressParams(module, this.WriteNullTerminatedASCIIString(functionName)); + var lpParameter = this.circularBuffer.Add(ref getProcAddressParams); + + if (lpParameter == IntPtr.Zero) + throw new Exception("Unable to allocate GetProcAddress parameter ptr"); + + var threadHandle = CreateRemoteThread( + this.targetProcess.Handle, + IntPtr.Zero, + UIntPtr.Zero, + this.getProcAddressShellPtr, + lpParameter, + CreateThreadFlags.RunImmediately, + out _); + + _ = WaitForSingleObject(threadHandle, uint.MaxValue); + + this.extMemory.Read(this.getProcAddressRetPtr, out address); + + if (address == IntPtr.Zero) + throw new Exception($"Error calling GetProcAddress with {functionName}"); + } + + /// + /// Call a method in a remote process via CreateRemoteThread. + /// + /// Method address. + /// Parameter address. + /// Thread exit code. + public void CallRemoteFunction(IntPtr methodAddress, IntPtr parameterAddress, out uint exitCode) + { + // Create and initialize a thread at our address and parameter address. + var threadHandle = CreateRemoteThread( + this.targetProcess.Handle, + IntPtr.Zero, + UIntPtr.Zero, + methodAddress, + parameterAddress, + CreateThreadFlags.RunImmediately, + out _); + + _ = WaitForSingleObject(threadHandle, uint.MaxValue); + + GetExitCodeThread(threadHandle, out exitCode); + } + + private void SetupLoadLibrary(ProcessModule kernel32Module, ExportFunction[] kernel32Exports) + { + var offset = this.GetExportedFunctionOffset(kernel32Exports, "LoadLibraryW"); + var functionAddr = kernel32Module.BaseAddress + (int)offset; + var functionPtr = this.privateBuffer.Add(ref functionAddr); + + if (functionPtr == IntPtr.Zero) + throw new Exception("Unable to allocate LoadLibraryW function ptr"); + + var dummy = 0L; + this.loadLibraryRetPtr = this.privateBuffer.Add(ref dummy); + + if (this.loadLibraryRetPtr == IntPtr.Zero) + throw new Exception("Unable to allocate LoadLibraryW return value"); + + var func = functionPtr.ToInt64(); + var retVal = this.loadLibraryRetPtr.ToInt64(); + + var asm = new Assembler(64); + + asm.sub(rsp, 40); // sub rsp, 40 // Re-align stack to 16 byte boundary + shadow space. + asm.call(__qword_ptr[__qword_ptr[func]]); // call qword [qword func] // CreateRemoteThread lpParameter with string already in ECX. + asm.mov(__qword_ptr[__qword_ptr[retVal]], rax); // mov qword [qword retVal], rax // + asm.add(rsp, 40); // add rsp, 40 // Re-align stack to 16 byte boundary + shadow space. + asm.ret(); // ret // Restore stack ptr. (Callee cleanup) + + var bytes = this.Assemble(asm); + this.loadLibraryShellPtr = this.privateBuffer.Add(bytes); + + if (this.loadLibraryShellPtr == IntPtr.Zero) + throw new Exception("Unable to allocate LoadLibraryW shellcode"); + } + + private void SetupGetProcAddress(ProcessModule kernel32Module, ExportFunction[] kernel32Exports) + { + var offset = this.GetExportedFunctionOffset(kernel32Exports, "GetProcAddress"); + var functionAddr = kernel32Module.BaseAddress + (int)offset; + var functionPtr = this.privateBuffer.Add(ref functionAddr); + + if (functionPtr == IntPtr.Zero) + throw new Exception("Unable to allocate GetProcAddress function ptr"); + + var dummy = 0L; + this.getProcAddressRetPtr = this.privateBuffer.Add(ref dummy); + + if (this.getProcAddressRetPtr == IntPtr.Zero) + throw new Exception("Unable to allocate GetProcAddress return value"); + + var func = functionPtr.ToInt64(); + var retVal = this.getProcAddressRetPtr.ToInt64(); + + var asm = new Assembler(64); + + asm.sub(rsp, 40); // sub rsp, 40 // Re-align stack to 16 byte boundary +32 shadow space + asm.mov(rdx, __qword_ptr[__qword_ptr[rcx + 8]]); // mov rdx, qword [qword rcx + 8] // lpProcName + asm.mov(rcx, __qword_ptr[__qword_ptr[rcx + 0]]); // mov rcx, qword [qword rcx + 0] // hModule + asm.call(__qword_ptr[__qword_ptr[func]]); // call qword [qword func] // + asm.mov(__qword_ptr[__qword_ptr[retVal]], rax); // mov qword [qword retVal] // + asm.add(rsp, 40); // add rsp, 40 // Re-align stack to 16 byte boundary + shadow space. + asm.ret(); // ret // Restore stack ptr. (Callee cleanup) + + var bytes = this.Assemble(asm); + this.getProcAddressShellPtr = this.privateBuffer.Add(bytes); + + if (this.getProcAddressShellPtr == IntPtr.Zero) + throw new Exception("Unable to allocate GetProcAddress shellcode"); + } + + private byte[] Assemble(Assembler assembler) + { + using var stream = new MemoryStream(); + assembler.Assemble(new StreamCodeWriter(stream), 0); + + stream.Position = 0; + var reader = new StreamCodeReader(stream); + + int next; + var bytes = new byte[stream.Length]; + while ((next = reader.ReadByte()) >= 0) + { + bytes[stream.Position - 1] = (byte)next; + } + + return bytes; + } + + private ProcessModule GetProcessModule(string moduleName) + { + var modules = this.targetProcess.Modules; + for (var i = 0; i < modules.Count; i++) + { + var module = modules[i]; + if (module.ModuleName.Equals(moduleName, StringComparison.InvariantCultureIgnoreCase)) + { + return module; + } + } + + throw new Exception($"Failed to find {moduleName} in target process' modules"); + } + + private uint GetExportedFunctionOffset(ExportFunction[] exportFunctions, string functionName) + { + var exportFunction = exportFunctions.FirstOrDefault(func => func.Name == functionName); + + if (exportFunction == default) + throw new Exception($"Failed to find exported function {functionName} in target module's exports"); + + return exportFunction.Address; + } + + private IntPtr WriteNullTerminatedASCIIString(string libraryPath) + { + var libraryNameBytes = Encoding.ASCII.GetBytes(libraryPath + '\0'); + var value = this.circularBuffer.Add(libraryNameBytes); + + if (value == IntPtr.Zero) + throw new Exception("Unable to write ASCII string to buffer"); + + return value; + } + + private IntPtr WriteNullTerminatedUnicodeString(string libraryPath) + { + var libraryNameBytes = Encoding.Unicode.GetBytes(libraryPath + '\0'); + var value = this.circularBuffer.Add(libraryNameBytes); + + if (value == IntPtr.Zero) + throw new Exception("Unable to write Unicode string to buffer"); + + return value; + } + + [StructLayout(LayoutKind.Sequential)] + private struct GetProcAddressParams + { + public GetProcAddressParams(IntPtr hModule, IntPtr lPProcName) + { + this.HModule = hModule.ToInt64(); + this.LPProcName = lPProcName.ToInt64(); + } + + public long HModule { get; set; } + + public long LPProcName { get; set; } + } + } +} diff --git a/Dalamud.Injector/NativeFunctions.cs b/Dalamud.Injector/NativeFunctions.cs index 90e61a5fc..46da6728c 100644 --- a/Dalamud.Injector/NativeFunctions.cs +++ b/Dalamud.Injector/NativeFunctions.cs @@ -1,14 +1,234 @@ using System; -using System.Runtime.ConstrainedExecution; using System.Runtime.InteropServices; -using System.Security; namespace Dalamud.Injector { /// - /// Native functions. + /// Native user32 functions. /// - internal static class NativeFunctions + internal static partial class NativeFunctions + { + /// + /// MB_* from winuser. + /// + public enum MessageBoxType : uint + { + /// + /// The default value for any of the various subtypes. + /// + DefaultValue = 0x0, + + // To indicate the buttons displayed in the message box, specify one of the following values. + + /// + /// The message box contains three push buttons: Abort, Retry, and Ignore. + /// + AbortRetryIgnore = 0x2, + + /// + /// The message box contains three push buttons: Cancel, Try Again, Continue. Use this message box type instead + /// of MB_ABORTRETRYIGNORE. + /// + CancelTryContinue = 0x6, + + /// + /// Adds a Help button to the message box. When the user clicks the Help button or presses F1, the system sends + /// a WM_HELP message to the owner. + /// + Help = 0x4000, + + /// + /// The message box contains one push button: OK. This is the default. + /// + Ok = DefaultValue, + + /// + /// The message box contains two push buttons: OK and Cancel. + /// + OkCancel = 0x1, + + /// + /// The message box contains two push buttons: Retry and Cancel. + /// + RetryCancel = 0x5, + + /// + /// The message box contains two push buttons: Yes and No. + /// + YesNo = 0x4, + + /// + /// The message box contains three push buttons: Yes, No, and Cancel. + /// + YesNoCancel = 0x3, + + // To display an icon in the message box, specify one of the following values. + + /// + /// An exclamation-point icon appears in the message box. + /// + IconExclamation = 0x30, + + /// + /// An exclamation-point icon appears in the message box. + /// + IconWarning = IconExclamation, + + /// + /// An icon consisting of a lowercase letter i in a circle appears in the message box. + /// + IconInformation = 0x40, + + /// + /// An icon consisting of a lowercase letter i in a circle appears in the message box. + /// + IconAsterisk = IconInformation, + + /// + /// A question-mark icon appears in the message box. + /// The question-mark message icon is no longer recommended because it does not clearly represent a specific type + /// of message and because the phrasing of a message as a question could apply to any message type. In addition, + /// users can confuse the message symbol question mark with Help information. Therefore, do not use this question + /// mark message symbol in your message boxes. The system continues to support its inclusion only for backward + /// compatibility. + /// + IconQuestion = 0x20, + + /// + /// A stop-sign icon appears in the message box. + /// + IconStop = 0x10, + + /// + /// A stop-sign icon appears in the message box. + /// + IconError = IconStop, + + /// + /// A stop-sign icon appears in the message box. + /// + IconHand = IconStop, + + // To indicate the default button, specify one of the following values. + + /// + /// The first button is the default button. + /// MB_DEFBUTTON1 is the default unless MB_DEFBUTTON2, MB_DEFBUTTON3, or MB_DEFBUTTON4 is specified. + /// + DefButton1 = DefaultValue, + + /// + /// The second button is the default button. + /// + DefButton2 = 0x100, + + /// + /// The third button is the default button. + /// + DefButton3 = 0x200, + + /// + /// The fourth button is the default button. + /// + DefButton4 = 0x300, + + // To indicate the modality of the dialog box, specify one of the following values. + + /// + /// The user must respond to the message box before continuing work in the window identified by the hWnd parameter. + /// However, the user can move to the windows of other threads and work in those windows. Depending on the hierarchy + /// of windows in the application, the user may be able to move to other windows within the thread. All child windows + /// of the parent of the message box are automatically disabled, but pop-up windows are not. MB_APPLMODAL is the + /// default if neither MB_SYSTEMMODAL nor MB_TASKMODAL is specified. + /// + ApplModal = DefaultValue, + + /// + /// Same as MB_APPLMODAL except that the message box has the WS_EX_TOPMOST style. + /// Use system-modal message boxes to notify the user of serious, potentially damaging errors that require immediate + /// attention (for example, running out of memory). This flag has no effect on the user's ability to interact with + /// windows other than those associated with hWnd. + /// + SystemModal = 0x1000, + + /// + /// Same as MB_APPLMODAL except that all the top-level windows belonging to the current thread are disabled if the + /// hWnd parameter is NULL. Use this flag when the calling application or library does not have a window handle + /// available but still needs to prevent input to other windows in the calling thread without suspending other threads. + /// + TaskModal = 0x2000, + + // To specify other options, use one or more of the following values. + + /// + /// Same as desktop of the interactive window station. For more information, see Window Stations. If the current + /// input desktop is not the default desktop, MessageBox does not return until the user switches to the default + /// desktop. + /// + DefaultDesktopOnly = 0x20000, + + /// + /// The text is right-justified. + /// + Right = 0x80000, + + /// + /// Displays message and caption text using right-to-left reading order on Hebrew and Arabic systems. + /// + RtlReading = 0x100000, + + /// + /// The message box becomes the foreground window. Internally, the system calls the SetForegroundWindow function + /// for the message box. + /// + SetForeground = 0x10000, + + /// + /// The message box is created with the WS_EX_TOPMOST window style. + /// + Topmost = 0x40000, + + /// + /// The caller is a service notifying the user of an event. The function displays a message box on the current active + /// desktop, even if there is no user logged on to the computer. + /// + ServiceNotification = 0x200000, + } + + /// + /// Displays a modal dialog box that contains a system icon, a set of buttons, and a brief application-specific message, + /// such as status or error information. The message box returns an integer value that indicates which button the user + /// clicked. + /// + /// + /// A handle to the owner window of the message box to be created. If this parameter is NULL, the message box has no + /// owner window. + /// + /// + /// The message to be displayed. If the string consists of more than one line, you can separate the lines using a carriage + /// return and/or linefeed character between each line. + /// + /// + /// The dialog box title. If this parameter is NULL, the default title is Error. + /// + /// The contents and behavior of the dialog box. This parameter can be a combination of flags from the following groups + /// of flags. + /// + /// + /// If a message box has a Cancel button, the function returns the IDCANCEL value if either the ESC key is pressed or + /// the Cancel button is selected. If the message box has no Cancel button, pressing ESC will no effect - unless an + /// MB_OK button is present. If an MB_OK button is displayed and the user presses ESC, the return value will be IDOK. + /// If the function fails, the return value is zero.To get extended error information, call GetLastError. If the function + /// succeeds, the return value is one of the ID* enum values. + /// + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern int MessageBoxW(IntPtr hWnd, string text, string caption, MessageBoxType type); + } + + /// + /// Native kernel32 functions. + /// + internal static partial class NativeFunctions { /// /// MEM_* from memoryapi. @@ -20,14 +240,14 @@ namespace Dalamud.Injector /// To coalesce two adjacent placeholders, specify MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS. When you coalesce /// placeholders, lpAddress and dwSize must exactly match those of the placeholder. /// - CoalescePlaceholders = 0x00000001, + CoalescePlaceholders = 0x1, /// /// Frees an allocation back to a placeholder (after you've replaced a placeholder with a private allocation using /// VirtualAlloc2 or Virtual2AllocFromApp). To split a placeholder into two placeholders, specify /// MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER. /// - PreservePlaceholder = 0x00000002, + PreservePlaceholder = 0x2, /// /// Allocates memory charges (from the overall size of memory and the paging files on disk) for the specified reserved @@ -88,7 +308,7 @@ namespace Dalamud.Injector /// the specified address range is intact. If the function fails, at least some of the data in the address range /// has been replaced with zeroes. This value cannot be used with any other value. If MEM_RESET_UNDO is called on /// an address range which was not MEM_RESET earlier, the behavior is undefined. When you specify MEM_RESET, the - /// VirtualAllocEx function ignores the value of flProtect. However, you must still set flProtect to a valid + /// VirtualAllocEx function ignores the value of flProtect. However, you must still set flProtect to a valid /// protection value, such as PAGE_NOACCESS. /// ResetUndo = 0x1000000, @@ -122,6 +342,28 @@ namespace Dalamud.Injector LargePages = 0x20000000, } + /// + /// Unprefixed flags from CreateRemoteThread. + /// + [Flags] + public enum CreateThreadFlags + { + /// + /// The thread runs immediately after creation. + /// + RunImmediately = 0x0, + + /// + /// The thread is created in a suspended state, and does not run until the ResumeThread function is called. + /// + CreateSuspended = 0x4, + + /// + /// The dwStackSize parameter specifies the initial reserve size of the stack. If this flag is not specified, dwStackSize specifies the commit size. + /// + StackSizeParamIsReservation = 0x10000, + } + /// /// PAGE_* from memoryapi. /// @@ -198,7 +440,7 @@ namespace Dalamud.Injector /// The default behavior for VirtualProtect protection change to executable is to mark all locations as valid call /// targets for CFG. /// - TargetsNoUpdate = 0x40000000, + TargetsNoUpdate = TargetsInvalid, /// /// Pages in the region become guard pages. Any attempt to access a guard page causes the system to raise a @@ -312,23 +554,33 @@ namespace Dalamud.Injector } /// - /// Closes an open object handle. + /// WAIT_* from synchapi. /// - /// - /// A valid handle to an open object. - /// - /// - /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero.To get extended - /// error information, call GetLastError. If the application is running under a debugger, the function will throw an - /// exception if it receives either a handle value that is not valid or a pseudo-handle value. This can happen if you - /// close a handle twice, or if you call CloseHandle on a handle returned by the FindFirstFile function instead of calling - /// the FindClose function. - /// - [DllImport("kernel32.dll", SetLastError = true)] - [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] - [SuppressUnmanagedCodeSecurity] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool CloseHandle(IntPtr hObject); + public enum WaitResult + { + /// + /// The specified object is a mutex object that was not released by the thread that owned the mutex object + /// before the owning thread terminated.Ownership of the mutex object is granted to the calling thread and + /// the mutex state is set to nonsignaled. If the mutex was protecting persistent state information, you + /// should check it for consistency. + /// + Abandoned = 0x80, + + /// + /// The state of the specified object is signaled. + /// + Object0 = 0x0, + + /// + /// The time-out interval elapsed, and the object's state is nonsignaled. + /// + Timeout = 0x102, + + /// + /// The function has failed. To get extended error information, call GetLastError. + /// + WAIT_FAILED = 0xFFFFFFF, + } /// /// Creates a thread that runs in the virtual address space of another process. Use the CreateRemoteThreadEx function @@ -336,23 +588,23 @@ namespace Dalamud.Injector /// /// /// A handle to the process in which the thread is to be created. The handle must have the PROCESS_CREATE_THREAD, - /// PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ access rights, and may fail - /// without these rights on certain platforms. For more information, see Process Security and Access Rights. + /// PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ access rights, and may fail without + /// these rights on certain platforms. For more information, see Process Security and Access Rights. /// /// - /// A pointer to a SECURITY_ATTRIBUTES structure that specifies a security descriptor for the new thread and determines - /// whether child processes can inherit the returned handle. If lpThreadAttributes is NULL, the thread gets a default - /// security descriptor and the handle cannot be inherited. The access control lists (ACL) in the default security descriptor - /// for a thread come from the primary token of the creator. + /// A pointer to a SECURITY_ATTRIBUTES structure that specifies a security descriptor for the new thread and determines whether + /// child processes can inherit the returned handle. If lpThreadAttributes is NULL, the thread gets a default security descriptor + /// and the handle cannot be inherited. The access control lists (ACL) in the default security descriptor for a thread come from + /// the primary token of the creator. /// /// - /// The initial size of the stack, in bytes. The system rounds this value to the nearest page. If this parameter is - /// 0 (zero), the new thread uses the default size for the executable. For more information, see Thread Stack Size. + /// The initial size of the stack, in bytes. The system rounds this value to the nearest page. If this parameter is 0 (zero), the + /// new thread uses the default size for the executable. For more information, see Thread Stack Size. /// /// - /// A pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread and - /// represents the starting address of the thread in the remote process. The function must exist in the remote process. - /// For more information, see ThreadProc. + /// A pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread and represents the + /// starting address of the thread in the remote process. The function must exist in the remote process. For more information, + /// see ThreadProc. /// /// /// A pointer to a variable to be passed to the thread function. @@ -361,92 +613,43 @@ namespace Dalamud.Injector /// The flags that control the creation of the thread. /// /// - /// A pointer to a variable that receives the thread identifier. If this parameter is NULL, the thread identifier is - /// not returned. + /// A pointer to a variable that receives the thread identifier. If this parameter is NULL, the thread identifier is not returned. /// /// - /// If the function succeeds, the return value is a handle to the new thread. If the function fails, the return value - /// is NULL.To get extended error information, call GetLastError. Note that CreateRemoteThread may succeed even if - /// lpStartAddress points to data, code, or is not accessible. If the start address is invalid when the thread runs, - /// an exception occurs, and the thread terminates. Thread termination due to a invalid start address is handled as - /// an error exit for the thread's process. This behavior is similar to the asynchronous nature of CreateProcess, where - /// the process is created even if it refers to invalid or missing dynamic-link libraries (DLL). + /// If the function succeeds, the return value is a handle to the new thread. If the function fails, the return value is + /// NULL.To get extended error information, call GetLastError. Note that CreateRemoteThread may succeed even if lpStartAddress + /// points to data, code, or is not accessible. If the start address is invalid when the thread runs, an exception occurs, and + /// the thread terminates. Thread termination due to a invalid start address is handled as an error exit for the thread's process. + /// This behavior is similar to the asynchronous nature of CreateProcess, where the process is created even if it refers to + /// invalid or missing dynamic-link libraries (DLL). /// - [DllImport("kernel32.dll")] + [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr CreateRemoteThread( IntPtr hProcess, IntPtr lpThreadAttributes, - uint dwStackSize, + UIntPtr dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, - uint dwCreationFlags, - IntPtr lpThreadId); + CreateThreadFlags dwCreationFlags, + out uint lpThreadId); /// - /// See https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlew. - /// Retrieves a module handle for the specified module. The module must have been loaded by the calling process. To - /// avoid the race conditions described in the Remarks section, use the GetModuleHandleEx function. + /// Retrieves the termination status of the specified thread. /// - /// - /// The name of the loaded module (either a .dll or .exe file). If the file name extension is omitted, the default library - /// extension .dll is appended. The file name string can include a trailing point character (.) to indicate that the - /// module name has no extension. The string does not have to specify a path. When specifying a path, be sure to use - /// backslashes (\), not forward slashes (/). The name is compared (case independently) to the names of modules currently - /// mapped into the address space of the calling process. If this parameter is NULL, GetModuleHandle returns a handle - /// to the file used to create the calling process (.exe file). The GetModuleHandle function does not retrieve handles - /// for modules that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx. + /// + /// A handle to the thread. The handle must have the THREAD_QUERY_INFORMATION or THREAD_QUERY_LIMITED_INFORMATION + /// access right.For more information, see Thread Security and Access Rights. + /// + /// + /// A pointer to a variable to receive the thread termination status. /// /// - /// If the function succeeds, the return value is a handle to the specified module. If the function fails, the return - /// value is NULL.To get extended error information, call GetLastError. - /// - [DllImport("kernel32.dll", CharSet = CharSet.Auto)] - public static extern IntPtr GetModuleHandle(string lpModuleName); - - /// - /// Retrieves the address of an exported function or variable from the specified dynamic-link library (DLL). - /// - /// - /// A handle to the DLL module that contains the function or variable. The LoadLibrary, LoadLibraryEx, LoadPackagedLibrary, - /// or GetModuleHandle function returns this handle. The GetProcAddress function does not retrieve addresses from modules - /// that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx. - /// - /// - /// The function or variable name, or the function's ordinal value. If this parameter is an ordinal value, it must be - /// in the low-order word; the high-order word must be zero. - /// - /// - /// If the function succeeds, the return value is the address of the exported function or variable. If the function - /// fails, the return value is NULL.To get extended error information, call GetLastError. - /// - [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] - public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); - - /// - /// See https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess. - /// Opens an existing local process object. - /// - /// - /// The access to the process object. This access right is checked against the security descriptor for the process. - /// This parameter can be one or more of the process access rights. If the caller has enabled the SeDebugPrivilege - /// privilege, the requested access is granted regardless of the contents of the security descriptor. - /// - /// - /// If this value is TRUE, processes created by this process will inherit the handle. Otherwise, the processes do - /// not inherit this handle. - /// - /// - /// The identifier of the local process to be opened. - /// - /// - /// If the function succeeds, the return value is an open handle to the specified process. If the function fails, the - /// return value is NULL.To get extended error information, call GetLastError. + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get + /// extended error information, call GetLastError. /// [DllImport("kernel32.dll", SetLastError = true)] - public static extern IntPtr OpenProcess( - ProcessAccessFlags processAccess, - bool bInheritHandle, - int processId); + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetExitCodeThread(IntPtr hThread, out uint lpExitCode); /// /// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex. @@ -530,6 +733,27 @@ namespace Dalamud.Injector int dwSize, AllocationType dwFreeType); + /// + /// Waits until the specified object is in the signaled state or the time-out interval elapses. To enter an alertable wait + /// state, use the WaitForSingleObjectEx function.To wait for multiple objects, use WaitForMultipleObjects. + /// + /// + /// A handle to the object. For a list of the object types whose handles can be specified, see the following Remarks section. + /// If this handle is closed while the wait is still pending, the function's behavior is undefined. The handle must have the + /// SYNCHRONIZE access right. For more information, see Standard Access Rights. + /// + /// + /// The time-out interval, in milliseconds. If a nonzero value is specified, the function waits until the object is signaled + /// or the interval elapses. If dwMilliseconds is zero, the function does not enter a wait state if the object is not signaled; + /// it always returns immediately. If dwMilliseconds is INFINITE, the function will return only when the object is signaled. + /// + /// + /// If the function succeeds, the return value indicates the event that caused the function to return. + /// It can be one of the WaitResult values. + /// + [DllImport("kernel32.dll", SetLastError = true)] + public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); + /// /// Writes data to an area of memory in a specified process. The entire area to be written to must be accessible or /// the operation fails. diff --git a/Dalamud.Injector/Program.cs b/Dalamud.Injector/Program.cs deleted file mode 100644 index 74f3a1585..000000000 --- a/Dalamud.Injector/Program.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading; -using System.Windows.Forms; - -using EasyHook; -using Newtonsoft.Json; - -namespace Dalamud.Injector -{ - /// - /// Application entrypoint. - /// - internal static class Program - { - private static Process process = null; - - private static void Main(string[] args) - { - AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) => - { - File.WriteAllText("InjectorException.txt", eventArgs.ExceptionObject.ToString()); -#if !DEBUG - MessageBox.Show("Failed to inject the XIVLauncher in-game addon.\nPlease try restarting your game and your PC.\nIf this keeps happening, please report this error.", "XIVLauncher Error", MessageBoxButtons.OK, MessageBoxIcon.Error); -#else - MessageBox.Show("Couldn't inject.\nMake sure that Dalamud was not injected into your target process as a release build before and that the target process can be accessed with VM_WRITE permissions.\n\n" + eventArgs.ExceptionObject, "Debug Error", MessageBoxButtons.OK, MessageBoxIcon.Error); -#endif - Environment.Exit(0); - }; - - var pid = -1; - if (args.Length >= 1) - { - pid = int.Parse(args[0]); - } - - switch (pid) - { - case -1: - process = Process.GetProcessesByName("ffxiv_dx11")[0]; - break; - case -2: - process = Process.Start( - "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn\\game\\ffxiv_dx11.exe", - "DEV.TestSID=0 DEV.UseSqPack=1 DEV.DataPathType=1 DEV.LobbyHost01=127.0.0.1 DEV.LobbyPort01=54994 DEV.LobbyHost02=127.0.0.1 DEV.LobbyPort02=54994 DEV.LobbyHost03=127.0.0.1 DEV.LobbyPort03=54994 DEV.LobbyHost04=127.0.0.1 DEV.LobbyPort04=54994 DEV.LobbyHost05=127.0.0.1 DEV.LobbyPort05=54994 DEV.LobbyHost06=127.0.0.1 DEV.LobbyPort06=54994 DEV.LobbyHost07=127.0.0.1 DEV.LobbyPort07=54994 DEV.LobbyHost08=127.0.0.1 DEV.LobbyPort08=54994 SYS.Region=0 language=1 version=1.0.0.0 DEV.MaxEntitledExpansionID=2 DEV.GMServerHost=127.0.0.1 DEV.GameQuitMessageBox=0"); - Thread.Sleep(1000); - break; - default: - process = Process.GetProcessById(pid); - break; - } - - DalamudStartInfo startInfo; - if (args.Length <= 1) - { - startInfo = GetDefaultStartInfo(); - Console.WriteLine("\nA Dalamud start info was not found in the program arguments. One has been generated for you."); - Console.WriteLine("\nCopy the following contents into the program arguments:"); - Console.WriteLine(); - Console.WriteLine(Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(startInfo)))); - } - else - { - startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(args[1]))); - } - - startInfo.WorkingDirectory = Directory.GetCurrentDirectory(); - - // Seems to help with the STATUS_INTERNAL_ERROR condition - Thread.Sleep(1000); - - // Thread.Sleep(10000); - - // Inject to process - Inject(process, startInfo); - - Thread.Sleep(1000); - -#if DEBUG - // Inject exception handler - // NativeInject(process); -#endif - } - - private static void Inject(Process process, DalamudStartInfo info) - { - Console.WriteLine($"Injecting to {process.Id}"); - - // File check - var libPath = Path.GetFullPath("Dalamud.dll"); - if (!File.Exists(libPath)) - { - Console.WriteLine($"Can't find a dll on {libPath}"); - return; - } - - RemoteHooking.Inject(process.Id, InjectionOptions.DoNotRequireStrongName, libPath, libPath, info); - - Console.WriteLine("Injected"); - } - - private static void NativeInject(Process process) - { - var libPath = Path.GetFullPath("DalamudDebugStub.dll"); - - var pathBytes = Encoding.Unicode.GetBytes(libPath); - var len = pathBytes.Length + 1; - - Console.WriteLine($"Injecting {libPath}..."); - - var handle = NativeFunctions.OpenProcess( - NativeFunctions.ProcessAccessFlags.AllAccess, - false, - process.Id); - - if (handle == IntPtr.Zero) - { - throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not OpenProcess"); - } - - var dllMem = NativeFunctions.VirtualAllocEx( - handle, - IntPtr.Zero, - len, - NativeFunctions.AllocationType.Commit, - NativeFunctions.MemoryProtection.ReadWrite); - - if (dllMem == IntPtr.Zero) - { - throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not alloc memory {Marshal.GetLastWin32Error():X}"); - } - - Console.WriteLine($"dll path at {dllMem.ToInt64():X}"); - - if (!NativeFunctions.WriteProcessMemory( - handle, - dllMem, - pathBytes, - len, - out var bytesWritten)) - { - throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not write DLL"); - } - - Console.WriteLine($"Wrote {bytesWritten}"); - - var kernel32 = NativeFunctions.GetModuleHandle("Kernel32.dll"); - var loadLibA = NativeFunctions.GetProcAddress(kernel32, "LoadLibraryW"); - - var remoteThread = NativeFunctions.CreateRemoteThread( - handle, - IntPtr.Zero, - 0, - loadLibA, - dllMem, - 0, - IntPtr.Zero); - - if (remoteThread == IntPtr.Zero) - { - throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not CreateRemoteThread"); - } - - /* - TODO kill myself - VirtualFreeEx( - handle, - dllMem, - 0, - AllocationType.Release); - */ - - NativeFunctions.CloseHandle(remoteThread); - NativeFunctions.CloseHandle(handle); - } - - private static DalamudStartInfo GetDefaultStartInfo() - { - var ffxivDir = Path.GetDirectoryName(process.MainModule.FileName); - var startInfo = new DalamudStartInfo - { - WorkingDirectory = null, - ConfigurationPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XIVLauncher", "dalamudConfig.json"), - PluginDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XIVLauncher", "installedPlugins"), - DefaultPluginDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XIVLauncher", "devPlugins"), - AssetDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XIVLauncher", "dalamudAssets"), - - GameVersion = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver")), - Language = ClientLanguage.English, - }; - - Console.WriteLine("Creating a StartInfo with:\n" + - $"ConfigurationPath: {startInfo.ConfigurationPath}\n" + - $"PluginDirectory: {startInfo.PluginDirectory}\n" + - $"DefaultPluginDirectory: {startInfo.DefaultPluginDirectory}\n" + - $"Language: {startInfo.Language}\n" + - $"GameVersion: {startInfo.GameVersion}\n" + - $"OptOutMbCollection: {startInfo.OptOutMbCollection}\n" + - $"AssetDirectory: {startInfo.AssetDirectory}"); - - return startInfo; - } - } -} diff --git a/Dalamud.Injector/RemotePinnedData.cs b/Dalamud.Injector/RemotePinnedData.cs new file mode 100644 index 000000000..fc713639f --- /dev/null +++ b/Dalamud.Injector/RemotePinnedData.cs @@ -0,0 +1,79 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +using static Dalamud.Injector.NativeFunctions; + +namespace Dalamud.Injector +{ + /// + /// Pin an arbitrary string to a remote process. + /// + internal class RemotePinnedData : IDisposable + { + private readonly Process process; + private readonly byte[] data; + private readonly IntPtr allocAddr; + + /// + /// Initializes a new instance of the class. + /// + /// Process to write in. + /// Data to write. + public unsafe RemotePinnedData(Process process, byte[] data) + { + this.process = process; + this.data = data; + + this.allocAddr = VirtualAllocEx( + this.process.Handle, + IntPtr.Zero, + this.data.Length, + AllocationType.Commit, + MemoryProtection.ReadWrite); + + if (this.allocAddr == IntPtr.Zero || Marshal.GetLastWin32Error() != 0) + { + throw new Exception("Error allocating memory"); + } + + var result = WriteProcessMemory( + this.process.Handle, + this.allocAddr, + this.data, + this.data.Length, + out _); + + if (!result || Marshal.GetLastWin32Error() != 0) + { + throw new Exception("Error writing memory"); + } + } + + /// + /// Gets the address of the pinned data. + /// + public IntPtr Address => this.allocAddr; + + /// + public void Dispose() + { + if (this.allocAddr == IntPtr.Zero) + { + return; + } + + var result = VirtualFreeEx( + this.process.Handle, + this.allocAddr, + 0, + AllocationType.Release); + + if (!result || Marshal.GetLastWin32Error() != 0) + { + throw new Exception("Error freeing memory"); + } + } + } +} diff --git a/Dalamud.Test/Dalamud.Test.csproj b/Dalamud.Test/Dalamud.Test.csproj index bcbccb45a..7163336b1 100644 --- a/Dalamud.Test/Dalamud.Test.csproj +++ b/Dalamud.Test/Dalamud.Test.csproj @@ -1,96 +1,70 @@ - - - - - - - - Debug - AnyCPU - {C8004563-1806-4329-844F-0EF6274291FC} - {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Library - Properties - Dalamud.Test - Dalamud.Test - v4.7.2 - 512 - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - ..\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll - True - - - ..\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll - True - - - ..\packages\xunit.extensibility.core.2.4.1\lib\net452\xunit.core.dll - True - - - ..\packages\xunit.extensibility.execution.2.4.1\lib\net452\xunit.execution.desktop.dll - True - - - - - - - - - - - - - {b92dab43-2279-4a2c-96e3-d9d5910edbea} - Dalamud - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}. + + + + net5.0-windows + win-x64 + x64 + x64;AnyCPU + 9.0 - - - - - - - - \ No newline at end of file + + + Dalamud.Test + Dalamud.Test + Dalamud.Test + Dalamud.Test + Unit tests for Dalamud + goatcorp + Copyright © goatcorp 2021 + + + + Library + false + false + + + + Debug + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/Dalamud.Test/Game/GameVersionTests.cs b/Dalamud.Test/Game/GameVersionTests.cs new file mode 100644 index 000000000..44a5813c8 --- /dev/null +++ b/Dalamud.Test/Game/GameVersionTests.cs @@ -0,0 +1,58 @@ +using Dalamud.Game; +using Xunit; + +namespace Dalamud.Test.Game +{ + public class GameVersionTests + { + [Theory] + [InlineData("any", "any")] + [InlineData("2021.01.01.0000.0000", "2021.01.01.0000.0000")] + public void VersionEquality(string ver1, string ver2) + { + var v1 = GameVersion.Parse(ver1); + var v2 = GameVersion.Parse(ver2); + + Assert.Equal(v1, v2); + } + + [Theory] + [InlineData("2020.06.15.0000.0000", "any")] + [InlineData("2021.01.01.0000.0000", "2021.01.01.0000.0001")] + [InlineData("2021.01.01.0000.0000", "2021.01.01.0001.0000")] + [InlineData("2021.01.01.0000.0000", "2021.01.02.0000.0000")] + [InlineData("2021.01.01.0000.0000", "2021.02.01.0000.0000")] + [InlineData("2021.01.01.0000.0000", "2022.01.01.0000.0000")] + public void VersionComparison(string ver1, string ver2) + { + var v1 = GameVersion.Parse(ver1); + var v2 = GameVersion.Parse(ver2); + + Assert.True(v1.CompareTo(v2) < 0); + } + + [Theory] + [InlineData("2020.06.15.0000.0000")] + [InlineData("2021.01.01.0000")] + [InlineData("2021.01.01")] + [InlineData("2021.01")] + [InlineData("2021")] + public void VersionConstructor(string ver) + { + var v = GameVersion.Parse(ver); + + Assert.True(v != null); + } + + [Theory] + [InlineData("2020.06.15.0000.0000.0000")] + [InlineData("")] + public void VersionConstructorInvalid(string ver) + { + var result = GameVersion.TryParse(ver, out var v); + + Assert.False(result); + Assert.Null(v); + } + } +} diff --git a/Dalamud.Test/Game/Text/Sanitizer/SanitizerTests.cs b/Dalamud.Test/Game/Text/Sanitizer/SanitizerTests.cs index f2ce7a9c9..2d2a34df5 100644 --- a/Dalamud.Test/Game/Text/Sanitizer/SanitizerTests.cs +++ b/Dalamud.Test/Game/Text/Sanitizer/SanitizerTests.cs @@ -1,29 +1,29 @@ -// ReSharper disable StringLiteralTypo - using System.Collections.Generic; using System.Linq; using Xunit; -namespace Dalamud.Test.Game.Text.Sanitizer { - - public class SanitizerTests { +// ReSharper disable StringLiteralTypo +namespace Dalamud.Test.Game.Text.Sanitizer +{ + public class SanitizerTests + { private global::Dalamud.Game.Text.Sanitizer.Sanitizer sanitizer; [Theory] - [InlineData( ClientLanguage.English, "Pixie Cotton Hood of Healing", "Pixie Cotton Hood of Healing" )] - [InlineData( ClientLanguage.Japanese, "アラガントームストーン:真理", "アラガントームストーン:真理" )] - [InlineData( ClientLanguage.German, "Anemos-Pan\x02\x16\x01\x03zer\x02\x16\x01\x03hand\x02\x16\x01\x03schu\x02\x16\x01\x03he des Drachenbluts", "Anemos-Panzerhandschuhe des Drachenbluts" )] - [InlineData( ClientLanguage.German, "Bienen-Spatha †", "Bienen-Spatha" )] - [InlineData( ClientLanguage.French, "Le Diademe\x02\x1D\x01\x03: terrains de chasse|Le Diademe\x02\x1D\x01\x03: terrains de chasse", "Le Diademe: terrains de chasse|Le Diademe: terrains de chasse" )] - [InlineData( ClientLanguage.French, "Cuir de bœuf", "Cuir de boeuf" )] - public void StringsAreSanitizedCorrectly( - ClientLanguage clientLanguage, string unsanitizedString, string sanitizedString) + [InlineData(ClientLanguage.English, "Pixie Cotton Hood of Healing", "Pixie Cotton Hood of Healing")] + [InlineData(ClientLanguage.Japanese, "アラガントームストーン:真理", "アラガントームストーン:真理")] + [InlineData(ClientLanguage.German, "Anemos-Pan\x02\x16\x01\x03zer\x02\x16\x01\x03hand\x02\x16\x01\x03schu\x02\x16\x01\x03he des Drachenbluts", "Anemos-Panzerhandschuhe des Drachenbluts")] + [InlineData(ClientLanguage.German, "Bienen-Spatha †", "Bienen-Spatha")] + [InlineData(ClientLanguage.French, "Le Diademe\x02\x1D\x01\x03: terrains de chasse|Le Diademe\x02\x1D\x01\x03: terrains de chasse", "Le Diademe: terrains de chasse|Le Diademe: terrains de chasse")] + [InlineData(ClientLanguage.French, "Cuir de bœuf", "Cuir de boeuf")] + public void StringsAreSanitizedCorrectly(ClientLanguage clientLanguage, string unsanitizedString, string sanitizedString) { - var sanitizedStrings = new List {unsanitizedString}; + var sanitizedStrings = new List { unsanitizedString }; + sanitizer = new global::Dalamud.Game.Text.Sanitizer.Sanitizer(clientLanguage); Assert.Equal(sanitizedString, sanitizer.Sanitize(unsanitizedString)); Assert.Equal(sanitizedString, sanitizer.Sanitize(sanitizedStrings).First()); - + sanitizer = new global::Dalamud.Game.Text.Sanitizer.Sanitizer(ClientLanguage.English); Assert.Equal(sanitizedString, sanitizer.Sanitize(unsanitizedString, clientLanguage)); Assert.Equal(sanitizedString, sanitizer.Sanitize(sanitizedStrings, clientLanguage).First()); diff --git a/Dalamud.Test/LocalizationTests.cs b/Dalamud.Test/LocalizationTests.cs index 67977f805..093affc9a 100644 --- a/Dalamud.Test/LocalizationTests.cs +++ b/Dalamud.Test/LocalizationTests.cs @@ -1,20 +1,24 @@ -using System.IO; +using System.IO; using System.Reflection; using Xunit; -namespace Dalamud.Test { - public class LocalizationTests { +namespace Dalamud.Test +{ + public class LocalizationTests + { private readonly Localization localization; private string currentLangCode; - - public LocalizationTests() { + + public LocalizationTests() + { var workingDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); this.localization = new Localization(workingDir, "dalamud_"); this.localization.OnLocalizationChanged += code => this.currentLangCode = code; } [Fact] - public void SetupWithFallbacks_EventInvoked() { + public void SetupWithFallbacks_EventInvoked() + { this.localization.SetupWithFallbacks(); Assert.Equal("en", this.currentLangCode); } diff --git a/Dalamud.Test/Properties/AssemblyInfo.cs b/Dalamud.Test/Properties/AssemblyInfo.cs deleted file mode 100644 index 589173bb4..000000000 --- a/Dalamud.Test/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Dalamud.Test")] -[assembly: AssemblyDescription("Unit tests for Dalamud")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("goatcorp")] -[assembly: AssemblyProduct("Dalamud.Test")] -[assembly: AssemblyCopyright("Copyright © goatcorp 2021")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("C8004563-1806-4329-844F-0EF6274291FC")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Dalamud.Test/packages.config b/Dalamud.Test/packages.config deleted file mode 100644 index f2ae94af3..000000000 --- a/Dalamud.Test/packages.config +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/Dalamud.sln b/Dalamud.sln index 16ca2e3ac..5b58f00d4 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -1,137 +1,162 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29215.179 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31410.414 MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CEF7D22B-CB85-400E-BD64-349A30E3C097}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitignore = .gitignore + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "build", "build\build.csproj", "{94E5B016-02B1-459B-97D9-E783F28764B2}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud", "Dalamud\Dalamud.csproj", "{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Boot", "Dalamud.Boot\Dalamud.Boot.vcxproj", "{55198DC3-A03D-408E-A8EB-2077780C8576}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Injector", "Dalamud.Injector\Dalamud.Injector.csproj", "{5B832F73-5F54-4ADC-870F-D0095EF72C9A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGuiScene", "lib\ImGuiScene\ImGuiScene\ImGuiScene.csproj", "{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Injector.Boot", "Dalamud.Injector.Boot\Dalamud.Injector.Boot.vcxproj", "{8874326B-E755-4D13-90B4-59AB263A3E6B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SDL2-CS", "lib\ImGuiScene\deps\SDL2-CS\SDL2-CS.csproj", "{85480198-8711-4355-830E-72FD794AD3F6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGui.NET-472", "lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj", "{0483026E-C6CE-4B1A-AA68-46544C08140B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interface", "Interface", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DalamudDebugStub", "DalamudDebugStub\DalamudDebugStub.vcxproj", "{9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGui.NET-472", "lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj", "{0483026E-C6CE-4B1A-AA68-46544C08140B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs", "lib\FFXIVClientStructs\FFXIVClientStructs.csproj", "{3DBAEE68-9D94-4807-BCB1-E42EDD52B489}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGuiScene", "lib\ImGuiScene\ImGuiScene\ImGuiScene.csproj", "{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SDL2-CS", "lib\ImGuiScene\deps\SDL2-CS\SDL2-CS.csproj", "{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.CorePlugin", "Dalamud.CorePlugin\Dalamud.CorePlugin.csproj", "{4AFDB34A-7467-4D41-B067-53BC4101D9D0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs", "lib\FFXIVClientStructs\FFXIVClientStructs\FFXIVClientStructs.csproj", "{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs.Common", "lib\FFXIVClientStructs\FFXIVClientStructs.Common\FFXIVClientStructs.Common.csproj", "{F3F0CC3A-DE2E-403F-88B4-B47C62582477}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs.Generators", "lib\FFXIVClientStructs\FFXIVClientStructs.Generators\FFXIVClientStructs.Generators.csproj", "{05AB2F46-268B-4915-806F-DDF813E2D59D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 - Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x64.ActiveCfg = Debug|Any CPU + {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x64.Build.0 = Debug|Any CPU + {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.Build.0 = Release|Any CPU + {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x64.ActiveCfg = Release|Any CPU + {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x64.Build.0 = Release|Any CPU + {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.ActiveCfg = Debug|x64 + {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.Build.0 = Debug|x64 {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x64.ActiveCfg = Debug|x64 {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x64.Build.0 = Debug|x64 - {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x86.ActiveCfg = Debug|Any CPU - {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x86.Build.0 = Debug|Any CPU - {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.Build.0 = Release|Any CPU + {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.ActiveCfg = Release|x64 + {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.Build.0 = Release|x64 {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x64.ActiveCfg = Release|x64 {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x64.Build.0 = Release|x64 - {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x86.ActiveCfg = Release|Any CPU - {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x86.Build.0 = Release|Any CPU - {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|Any CPU.ActiveCfg = Debug|x64 + {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|Any CPU.Build.0 = Debug|x64 + {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x64.ActiveCfg = Debug|x64 + {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x64.Build.0 = Debug|x64 + {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|Any CPU.ActiveCfg = Release|x64 + {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|Any CPU.Build.0 = Release|x64 + {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x64.ActiveCfg = Release|x64 + {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x64.Build.0 = Release|x64 + {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.ActiveCfg = Debug|x64 + {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x64.ActiveCfg = Debug|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x64.Build.0 = Debug|x64 - {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x86.ActiveCfg = Debug|Any CPU - {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x86.Build.0 = Debug|Any CPU - {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|Any CPU + {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|x64 + {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x64.ActiveCfg = Release|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x64.Build.0 = Release|x64 - {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x86.ActiveCfg = Release|Any CPU - {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x86.Build.0 = Release|Any CPU - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.ActiveCfg = Debug|Any CPU - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.Build.0 = Debug|Any CPU - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x86.ActiveCfg = Debug|Any CPU - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x86.Build.0 = Debug|Any CPU - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.Build.0 = Release|Any CPU - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.ActiveCfg = Release|Any CPU - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.Build.0 = Release|Any CPU - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x86.ActiveCfg = Release|Any CPU - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x86.Build.0 = Release|Any CPU - {85480198-8711-4355-830E-72FD794AD3F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {85480198-8711-4355-830E-72FD794AD3F6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {85480198-8711-4355-830E-72FD794AD3F6}.Debug|x64.ActiveCfg = Debug|x64 - {85480198-8711-4355-830E-72FD794AD3F6}.Debug|x64.Build.0 = Debug|x64 - {85480198-8711-4355-830E-72FD794AD3F6}.Debug|x86.ActiveCfg = Debug|x86 - {85480198-8711-4355-830E-72FD794AD3F6}.Debug|x86.Build.0 = Debug|x86 - {85480198-8711-4355-830E-72FD794AD3F6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {85480198-8711-4355-830E-72FD794AD3F6}.Release|Any CPU.Build.0 = Release|Any CPU - {85480198-8711-4355-830E-72FD794AD3F6}.Release|x64.ActiveCfg = Release|x64 - {85480198-8711-4355-830E-72FD794AD3F6}.Release|x64.Build.0 = Release|x64 - {85480198-8711-4355-830E-72FD794AD3F6}.Release|x86.ActiveCfg = Release|x86 - {85480198-8711-4355-830E-72FD794AD3F6}.Release|x86.Build.0 = Release|x86 - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.ActiveCfg = Debug|Any CPU - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.Build.0 = Debug|Any CPU - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x86.ActiveCfg = Debug|Any CPU - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x86.Build.0 = Debug|Any CPU - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.Build.0 = Release|Any CPU - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.ActiveCfg = Release|Any CPU - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.Build.0 = Release|Any CPU - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x86.ActiveCfg = Release|Any CPU - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x86.Build.0 = Release|Any CPU - {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Debug|Any CPU.ActiveCfg = Debug|x64 - {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Debug|x64.ActiveCfg = Debug|x64 - {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Debug|x86.ActiveCfg = Debug|Win32 - {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|Any CPU.ActiveCfg = Release|x64 - {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|Any CPU.Build.0 = Release|x64 - {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|x64.ActiveCfg = Release|x64 - {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|x64.Build.0 = Release|x64 - {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|x86.ActiveCfg = Release|Win32 - {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|x86.Build.0 = Release|Win32 - {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|x64.ActiveCfg = Debug|Any CPU - {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|x64.Build.0 = Debug|Any CPU - {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|x86.ActiveCfg = Debug|Any CPU - {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|x86.Build.0 = Debug|Any CPU - {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|Any CPU.Build.0 = Release|Any CPU - {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|x64.ActiveCfg = Release|Any CPU - {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|x64.Build.0 = Release|Any CPU - {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|x86.ActiveCfg = Release|Any CPU - {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|x86.Build.0 = Release|Any CPU - {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.ActiveCfg = Debug|Any CPU - {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.Build.0 = Debug|Any CPU - {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x86.ActiveCfg = Debug|Any CPU - {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x86.Build.0 = Debug|Any CPU - {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.Build.0 = Release|Any CPU - {C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.ActiveCfg = Release|Any CPU - {C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.Build.0 = Release|Any CPU - {C8004563-1806-4329-844F-0EF6274291FC}.Release|x86.ActiveCfg = Release|Any CPU - {C8004563-1806-4329-844F-0EF6274291FC}.Release|x86.Build.0 = Release|Any CPU + {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.ActiveCfg = Debug|x64 + {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x64.ActiveCfg = Debug|x64 + {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x64.Build.0 = Debug|x64 + {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.ActiveCfg = Release|x64 + {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x64.ActiveCfg = Release|x64 + {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x64.Build.0 = Release|x64 + {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.ActiveCfg = Debug|x64 + {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|x64 + {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.ActiveCfg = Debug|x64 + {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.Build.0 = Debug|x64 + {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64 + {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.Build.0 = Release|x64 + {C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.ActiveCfg = Release|x64 + {C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.Build.0 = Release|x64 + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.ActiveCfg = Debug|x64 + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.Build.0 = Debug|x64 + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.ActiveCfg = Debug|x64 + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.Build.0 = Debug|x64 + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.ActiveCfg = Release|x64 + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.Build.0 = Release|x64 + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.ActiveCfg = Release|x64 + {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.Build.0 = Release|x64 + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.ActiveCfg = Debug|x64 + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.Build.0 = Debug|x64 + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.ActiveCfg = Debug|x64 + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.Build.0 = Debug|x64 + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.ActiveCfg = Release|x64 + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.Build.0 = Release|x64 + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.ActiveCfg = Release|x64 + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.Build.0 = Release|x64 + {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.ActiveCfg = Debug|x64 + {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.Build.0 = Debug|x64 + {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x64.ActiveCfg = Debug|x64 + {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x64.Build.0 = Debug|x64 + {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.ActiveCfg = Release|x64 + {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.Build.0 = Release|x64 + {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x64.ActiveCfg = Release|x64 + {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x64.Build.0 = Release|x64 + {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x64.ActiveCfg = Debug|Any CPU + {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x64.Build.0 = Debug|Any CPU + {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|Any CPU.Build.0 = Release|Any CPU + {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x64.ActiveCfg = Release|Any CPU + {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x64.Build.0 = Release|Any CPU + {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x64.ActiveCfg = Debug|Any CPU + {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x64.Build.0 = Debug|Any CPU + {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.Build.0 = Release|Any CPU + {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x64.ActiveCfg = Release|Any CPU + {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x64.Build.0 = Release|Any CPU + {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Debug|x64.ActiveCfg = Debug|Any CPU + {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Debug|x64.Build.0 = Debug|Any CPU + {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Release|Any CPU.Build.0 = Release|Any CPU + {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Release|x64.ActiveCfg = Release|Any CPU + {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Release|x64.Build.0 = Release|Any CPU + {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x64.ActiveCfg = Debug|Any CPU + {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x64.Build.0 = Debug|Any CPU + {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|Any CPU.Build.0 = Release|Any CPU + {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x64.ActiveCfg = Release|Any CPU + {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} - {85480198-8711-4355-830E-72FD794AD3F6} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} {0483026E-C6CE-4B1A-AA68-46544C08140B} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} + {C0E7E797-4FBF-4F46-BC57-463F3719BA7A} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} + {2F7FF0A8-B619-4572-86C7-71E46FE22FB8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} + {C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} + {F3F0CC3A-DE2E-403F-88B4-B47C62582477} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} + {05AB2F46-268B-4915-806F-DDF813E2D59D} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599} diff --git a/Dalamud/Configuration/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs similarity index 89% rename from Dalamud/Configuration/DalamudConfiguration.cs rename to Dalamud/Configuration/Internal/DalamudConfiguration.cs index 46c3381d0..678a68e78 100644 --- a/Dalamud/Configuration/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -7,19 +7,19 @@ using Newtonsoft.Json; using Serilog; using Serilog.Events; -namespace Dalamud.Configuration +namespace Dalamud.Configuration.Internal { /// /// Class containing Dalamud settings. /// [Serializable] - public class DalamudConfiguration + internal sealed class DalamudConfiguration { [JsonIgnore] private string configPath; /// - /// Delegate for the event that occurs when the dalamud configuration is saved. + /// Delegate for the event that occurs when the dalamud configuration is saved. /// /// The current dalamud configuration. public delegate void DalamudConfigurationSavedDelegate(DalamudConfiguration dalamudConfiguration); @@ -69,15 +69,25 @@ namespace Dalamud.Configuration /// public bool DoDalamudTest { get; set; } + /// + /// Gets or sets a value indicating whether or not XL should download the Dalamud .NET runtime. + /// + public bool DoDalamudRuntime { get; set; } + /// /// Gets or sets a list of custom repos. /// - public List ThirdRepoList { get; set; } = new List(); + public List ThirdRepoList { get; set; } = new(); /// /// Gets or sets a list of hidden plugins. /// - public List HiddenPluginInternalName { get; set; } = new List(); + public List HiddenPluginInternalName { get; set; } = new(); + + /// + /// Gets or sets a list of additional settings for devPlugins. + /// + public List DevPluginSettings { get; set; } = new(); /// /// Gets or sets the global UI scale. @@ -150,7 +160,7 @@ namespace Dalamud.Configuration public bool IsAntiAntiDebugEnabled { get; set; } = false; /// - /// Specifies the kind of beta to download when is set to true. + /// Gets or sets the kind of beta to download when is set to true. /// public string DalamudBetaKind { get; set; } diff --git a/Dalamud/Configuration/Internal/DevPluginSettings.cs b/Dalamud/Configuration/Internal/DevPluginSettings.cs new file mode 100644 index 000000000..fc6557da6 --- /dev/null +++ b/Dalamud/Configuration/Internal/DevPluginSettings.cs @@ -0,0 +1,33 @@ +namespace Dalamud.Configuration.Internal +{ + /// + /// Settings for DevPlugins. + /// + internal sealed class DevPluginSettings + { + /// + /// Initializes a new instance of the class. + /// + /// Filename of the DLL representing this plugin. + public DevPluginSettings(string dllFile) + { + this.DllFile = dllFile; + } + + /// + /// Gets or sets the path to a plugin DLL. This is automatically generated for any plugins in the devPlugins folder. However by + /// specifiying this value manually, you can add arbitrary files outside the normal file paths. + /// + public string DllFile { get; set; } + + /// + /// Gets or sets a value indicating whether this plugin should automatically start when Dalamud boots up. + /// + public bool StartOnBoot { get; set; } = true; + + /// + /// Gets or sets a value indicating whether this plugin should automatically reload on file change. + /// + public bool AutomaticReloading { get; set; } = false; + } +} diff --git a/Dalamud/Configuration/ThirdRepoSetting.cs b/Dalamud/Configuration/Internal/ThirdPartyRepoSettings.cs similarity index 56% rename from Dalamud/Configuration/ThirdRepoSetting.cs rename to Dalamud/Configuration/Internal/ThirdPartyRepoSettings.cs index b0fcb03b0..cafb96a47 100644 --- a/Dalamud/Configuration/ThirdRepoSetting.cs +++ b/Dalamud/Configuration/Internal/ThirdPartyRepoSettings.cs @@ -3,7 +3,7 @@ namespace Dalamud.Configuration /// /// Third party repository for dalamud plugins. /// - public class ThirdRepoSetting + internal sealed class ThirdPartyRepoSettings { /// /// Gets or sets the third party repo url. @@ -16,16 +16,14 @@ namespace Dalamud.Configuration public bool IsEnabled { get; set; } /// - /// Create new instance of third party repo object. + /// Gets or sets a short name for the repo url. /// - /// New instance of third party repo. - public ThirdRepoSetting Clone() - { - return new ThirdRepoSetting - { - Url = this.Url, - IsEnabled = this.IsEnabled, - }; - } + public string Name { get; set; } + + /// + /// Clone this object. + /// + /// A shallow copy of this object. + public ThirdPartyRepoSettings Clone() => this.MemberwiseClone() as ThirdPartyRepoSettings; } } diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs index 5fa1d54cf..1cadb2180 100644 --- a/Dalamud/Configuration/PluginConfigurations.cs +++ b/Dalamud/Configuration/PluginConfigurations.cs @@ -7,7 +7,7 @@ namespace Dalamud.Configuration /// /// Configuration to store settings for a dalamud plugin. /// - public class PluginConfigurations + public sealed class PluginConfigurations { private readonly DirectoryInfo configDirectory; diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 5453da2a6..4d5bcead9 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -1,10 +1,10 @@ using System; using System.Diagnostics; using System.IO; +using System.Runtime.CompilerServices; using System.Threading; -using System.Threading.Tasks; -using Dalamud.Configuration; +using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.Addon; @@ -13,17 +13,24 @@ using Dalamud.Game.Command; using Dalamud.Game.Internal; using Dalamud.Game.Network; using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Interface; -using Dalamud.Plugin; +using Dalamud.Hooking.Internal; +using Dalamud.Interface.Internal; +using Dalamud.Memory; +using Dalamud.Plugin.Internal; using Serilog; using Serilog.Core; +#if DEBUG +// This allows for rapid prototyping of Dalamud modules with access to internal objects. +[assembly: InternalsVisibleTo("Dalamud.CorePlugin")] +#endif + namespace Dalamud { /// /// The main Dalamud class containing all subsystems. /// - public sealed class Dalamud : IDisposable + internal sealed class Dalamud : IDisposable { #region Internals @@ -31,8 +38,6 @@ namespace Dalamud private readonly ManualResetEvent finishUnloadSignal; - private readonly string baseDirectory; - private bool hasDisposedPlugins = false; #endregion @@ -46,11 +51,14 @@ namespace Dalamud /// The Dalamud configuration. public Dalamud(DalamudStartInfo info, LoggingLevelSwitch loggingLevelSwitch, ManualResetEvent finishSignal, DalamudConfiguration configuration) { +#if DEBUG + Instance = this; +#endif this.StartInfo = info; this.LogLevelSwitch = loggingLevelSwitch; this.Configuration = configuration; - this.baseDirectory = info.WorkingDirectory; + // this.baseDirectory = info.WorkingDirectory; this.unloadSignal = new ManualResetEvent(false); this.unloadSignal.Reset(); @@ -59,6 +67,13 @@ namespace Dalamud this.finishUnloadSignal.Reset(); } +#if DEBUG + /// + /// Gets the Dalamud singleton instance. + /// + internal static Dalamud Instance { get; private set; } +#endif + #region Native Game Subsystems /// @@ -76,6 +91,11 @@ namespace Dalamud /// internal WinSockHandlers WinSock2 { get; private set; } + /// + /// Gets Hook management subsystem. + /// + internal HookManager HookManager { get; private set; } + /// /// Gets ImGui Interface subsystem. /// @@ -95,11 +115,6 @@ namespace Dalamud /// internal PluginManager PluginManager { get; private set; } - /// - /// Gets Plugin Repository subsystem. - /// - internal PluginRepository PluginRepository { get; private set; } - /// /// Gets Data provider subsystem. /// @@ -205,6 +220,7 @@ namespace Dalamud // Initialize the process information. this.TargetModule = Process.GetCurrentProcess().MainModule; this.SigScanner = new SigScanner(this.TargetModule, true); + this.HookManager = new HookManager(this); // Initialize game subsystem this.Framework = new Framework(this.SigScanner, this); @@ -289,6 +305,7 @@ namespace Dalamud Log.Information("[T2] Data OK!"); this.SeStringManager = new SeStringManager(this.Data); + MemoryHelper.Initialize(this); // For SeString handling Log.Information("[T2] SeString OK!"); @@ -327,28 +344,18 @@ namespace Dalamud { Log.Information("[T3] START!"); - this.PluginRepository = - new PluginRepository(this, this.StartInfo.PluginDirectory, this.StartInfo.GameVersion); - - Log.Information("[T3] PREPO OK!"); - if (!bool.Parse(Environment.GetEnvironmentVariable("DALAMUD_NOT_HAVE_PLUGINS") ?? "false")) { try { - this.PluginRepository.CleanupPlugins(); - - Log.Information("[T3] PRC OK!"); - - this.PluginManager = new PluginManager( - this, - this.StartInfo.PluginDirectory, - this.StartInfo.DefaultPluginDirectory); - this.PluginManager.LoadSynchronousPlugins(); - - Task.Run(() => this.PluginManager.LoadDeferredPlugins()); - + this.PluginManager = new PluginManager(this); Log.Information("[T3] PM OK!"); + + this.PluginManager.CleanupPlugins(); + Log.Information("[T3] PMC OK!"); + + this.PluginManager.LoadAllPlugins(); + Log.Information("[T3] PML OK!"); } catch (Exception ex) { @@ -357,8 +364,6 @@ namespace Dalamud } this.DalamudUi = new DalamudInterface(this); - this.InterfaceManager.OnDraw += this.DalamudUi.Draw; - Log.Information("[T3] DUI OK!"); Troubleshooting.LogTroubleshooting(this, this.InterfaceManager != null); @@ -410,16 +415,9 @@ namespace Dalamud // use any resources that it freed in its own Dispose method this.InterfaceManager?.Dispose(); - try - { - this.PluginManager.UnloadPlugins(); - } - catch (Exception ex) - { - Log.Error(ex, "Plugin unload failed."); - } - this.DalamudUi?.Dispose(); + + this.PluginManager?.Dispose(); } /// @@ -436,20 +434,23 @@ namespace Dalamud } this.Framework?.Dispose(); + this.ClientState?.Dispose(); this.unloadSignal?.Dispose(); this.WinSock2?.Dispose(); - this.SigScanner?.Dispose(); - this.Data?.Dispose(); this.AntiDebug?.Dispose(); this.SystemMenu?.Dispose(); + this.HookManager?.Dispose(); + + this.SigScanner?.Dispose(); + Log.Debug("Dalamud::Dispose() OK!"); } catch (Exception ex) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index f656e0cef..95ddc2ad5 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -1,105 +1,124 @@ + - AnyCPU - net472 + net5.0-windows + x64 + x64;AnyCPU 9.0 - AnyCPU;x64 - - - Library - - false - true - full - $(SolutionDir)\bin\Dalamud.xml + 5.2.7.0 - true + XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) - - - - - $(MSBuildProjectDirectory)\ - $(AppOutputBase)=C:\goatsoft\companysecrets\dalamud\ + + + Library + ..\bin\$(Configuration)\ + false + false + true + false + + + + $(OutputPath)Dalamud.xml + true + + + + true + true + true + portable true + annotations + true - - false + + + Debug - + DEBUG;TRACE - - IDE0017;IDE0044;IDE0047;IDE0048;IDE1006;CS1573;CS1591;CS1701;CS1702 - - - - - - + + $(MSBuildProjectDirectory)\ + $(AppOutputBase)=C:\goatsoft\companysecrets\dalamud\ + + + + IDE0003;IDE1006;CS1591;CS1701;CS1702 + + - - - - - - - - - - + + + + + - - - - - - - - + + + + + all + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + - + + + + + + + + + True True Resources.resx - - ResXFileCodeGenerator Resources.Designer.cs + - - - - - - - + PreserveNewest - Lumina.Generated.dll - + + + + + + + + diff --git a/Dalamud/DalamudStartInfo.cs b/Dalamud/DalamudStartInfo.cs index 78ceca446..7678cfb40 100644 --- a/Dalamud/DalamudStartInfo.cs +++ b/Dalamud/DalamudStartInfo.cs @@ -1,12 +1,15 @@ using System; +using Dalamud.Game; +using Newtonsoft.Json; + namespace Dalamud { /// - /// Class containing information needed to initialize Dalamud. + /// Struct containing information needed to initialize Dalamud. /// [Serializable] - public sealed class DalamudStartInfo + public struct DalamudStartInfo { /// /// The working directory of the XIVLauncher installations. @@ -41,7 +44,8 @@ namespace Dalamud /// /// The current game version code. /// - public string GameVersion; + [JsonConverter(typeof(GameVersionConverter))] + public GameVersion GameVersion; /// /// Whether or not market board information should be uploaded by default. diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index a6e4016e5..ebdf24c57 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -7,6 +7,7 @@ using System.Threading; using Dalamud.Data.LuminaExtensions; using Dalamud.Interface; +using Dalamud.Interface.Internal; using ImGuiScene; using JetBrains.Annotations; using Lumina; @@ -21,7 +22,7 @@ namespace Dalamud.Data /// /// This class provides data for Dalamud-internal features, but can also be used by plugins if needed. /// - public class DataManager : IDisposable + public sealed class DataManager : IDisposable { private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex"; private readonly InterfaceManager interfaceManager; @@ -32,6 +33,7 @@ namespace Dalamud.Data private GameData gameData; private Thread luminaResourceThread; + private CancellationTokenSource luminaCancellationTokenSource; /// /// Initializes a new instance of the class. @@ -41,8 +43,9 @@ namespace Dalamud.Data internal DataManager(ClientLanguage language, InterfaceManager interfaceManager) { this.interfaceManager = interfaceManager; + // Set up default values so plugins do not null-reference when data is being loaded. - this.ServerOpCodes = new ReadOnlyDictionary(new Dictionary()); + this.ClientOpCodes = this.ServerOpCodes = new ReadOnlyDictionary(new Dictionary()); this.Language = language; } @@ -99,7 +102,7 @@ namespace Dalamud.Data ClientLanguage.English => Lumina.Data.Language.English, ClientLanguage.German => Lumina.Data.Language.German, ClientLanguage.French => Lumina.Data.Language.French, - _ => throw new ArgumentOutOfRangeException(nameof(this.Language), $"Unknown Language: {this.Language}"), + _ => throw new ArgumentOutOfRangeException(nameof(language), $"Unknown Language: {language}"), }; return this.Excel.GetSheet(lang); } @@ -162,7 +165,7 @@ namespace Dalamud.Data ClientLanguage.English => "en/", ClientLanguage.German => "de/", ClientLanguage.French => "fr/", - _ => throw new ArgumentOutOfRangeException(nameof(this.Language), $"Unknown Language: {this.Language}"), + _ => throw new ArgumentOutOfRangeException(nameof(iconLanguage), $"Unknown Language: {iconLanguage}"), }; return this.GetIcon(type, iconId); @@ -232,7 +235,7 @@ namespace Dalamud.Data /// public void Dispose() { - this.luminaResourceThread.Abort(); + this.luminaCancellationTokenSource.Cancel(); } /// @@ -245,14 +248,14 @@ namespace Dalamud.Data { Log.Verbose("Starting data load..."); - var zoneOpCodeDict = - JsonConvert.DeserializeObject>(File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json"))); + var zoneOpCodeDict = JsonConvert.DeserializeObject>( + File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json"))); this.ServerOpCodes = new ReadOnlyDictionary(zoneOpCodeDict); Log.Verbose("Loaded {0} ServerOpCodes.", zoneOpCodeDict.Count); - var clientOpCodeDict = - JsonConvert.DeserializeObject>(File.ReadAllText(Path.Combine(baseDir, "UIRes", "clientopcode.json"))); + var clientOpCodeDict = JsonConvert.DeserializeObject>( + File.ReadAllText(Path.Combine(baseDir, "UIRes", "clientopcode.json"))); this.ClientOpCodes = new ReadOnlyDictionary(clientOpCodeDict); Log.Verbose("Loaded {0} ClientOpCodes.", clientOpCodeDict.Count); @@ -273,9 +276,7 @@ namespace Dalamud.Data ClientLanguage.English => Lumina.Data.Language.English, ClientLanguage.German => Lumina.Data.Language.German, ClientLanguage.French => Lumina.Data.Language.French, - _ => throw new ArgumentOutOfRangeException( - nameof(this.Language), - @"Unknown Language: " + this.Language), + _ => throw new ArgumentOutOfRangeException(nameof(this.Language), $"Unknown Language: {this.Language}"), }, }; @@ -289,9 +290,12 @@ namespace Dalamud.Data this.IsDataReady = true; - this.luminaResourceThread = new Thread(() => + this.luminaCancellationTokenSource = new(); + + var luminaCancellationToken = this.luminaCancellationTokenSource.Token; + this.luminaResourceThread = new(() => { - while (true) + while (!luminaCancellationToken.IsCancellationRequested) { if (this.gameData.FileHandleManager.HasPendingFileLoads) { @@ -302,8 +306,6 @@ namespace Dalamud.Data Thread.Sleep(5); } } - - // ReSharper disable once FunctionNeverReturns }); this.luminaResourceThread.Start(); } diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 9287ec033..d12624fbd 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -1,12 +1,13 @@ using System; using System.IO; using System.Net; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using Dalamud.Configuration; -using Dalamud.Interface; -using EasyHook; +using Dalamud.Configuration.Internal; +using Dalamud.Interface.Internal; +using Newtonsoft.Json; using Serilog; using Serilog.Core; using Serilog.Events; @@ -16,31 +17,41 @@ namespace Dalamud /// /// The main entrypoint for the Dalamud system. /// - public sealed class EntryPoint : IEntryPoint + public sealed class EntryPoint { /// - /// Initializes a new instance of the class. + /// A delegate used during initialization of the CLR from Dalamud.Boot. /// - /// The used to load the DLL. - /// The containing information needed to initialize Dalamud. - public EntryPoint(RemoteHooking.IContext ctx, DalamudStartInfo info) + /// Pointer to a serialized data. + public delegate void InitDelegate(IntPtr infoPtr); + + /// + /// Initialize Dalamud. + /// + /// Pointer to a serialized data. + public static void Initialize(IntPtr infoPtr) { - // Required by EasyHook + var infoStr = Marshal.PtrToStringAnsi(infoPtr); + var info = JsonConvert.DeserializeObject(infoStr); + + new Thread(() => RunThread(info)).Start(); } /// /// Initialize all Dalamud subsystems and start running on the main thread. /// - /// The used to load the DLL. /// The containing information needed to initialize Dalamud. - public void Run(RemoteHooking.IContext ctx, DalamudStartInfo info) + private static void RunThread(DalamudStartInfo info) { // Load configuration first to get some early persistent state, like log level var configuration = DalamudConfiguration.Load(info.ConfigurationPath); // Setup logger - var (logger, levelSwitch) = this.NewLogger(info.WorkingDirectory, configuration.LogLevel); - Log.Logger = logger; + var levelSwitch = InitLogging(info.WorkingDirectory, configuration); + + // Log any unhandled exception. + AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; var finishSignal = new ManualResetEvent(false); @@ -50,12 +61,7 @@ namespace Dalamud Log.Information("Initializing a session.."); // This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally - System.Net.ServicePointManager.SecurityProtocol = - SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls | SecurityProtocolType.Ssl3; - - // Log any unhandled exception. - AppDomain.CurrentDomain.UnhandledException += this.OnUnhandledException; - TaskScheduler.UnobservedTaskException += this.OnUnobservedTaskException; + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls; var dalamud = new Dalamud(info, levelSwitch, finishSignal, configuration); Log.Information("Starting a session.."); @@ -72,7 +78,8 @@ namespace Dalamud } finally { - AppDomain.CurrentDomain.UnhandledException -= this.OnUnhandledException; + TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException; + AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException; Log.Information("Session has ended."); Log.CloseAndFlush(); @@ -81,7 +88,7 @@ namespace Dalamud } } - private (Logger Logger, LoggingLevelSwitch LevelSwitch) NewLogger(string baseDirectory, LogEventLevel logLevel) + private static LoggingLevelSwitch InitLogging(string baseDirectory, DalamudConfiguration configuration) { #if DEBUG var logPath = Path.Combine(baseDirectory, "dalamud.log"); @@ -94,35 +101,34 @@ namespace Dalamud #if DEBUG levelSwitch.MinimumLevel = LogEventLevel.Verbose; #else - levelSwitch.MinimumLevel = logLevel; + levelSwitch.MinimumLevel = configuration.LogLevel; #endif - - var newLogger = new LoggerConfiguration() + Log.Logger = new LoggerConfiguration() .WriteTo.Async(a => a.File(logPath)) .WriteTo.Sink(SerilogEventSink.Instance) .MinimumLevel.ControlledBy(levelSwitch) .CreateLogger(); - return (newLogger, levelSwitch); + return levelSwitch; } - private void OnUnhandledException(object sender, UnhandledExceptionEventArgs arg) + private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs args) { - switch (arg.ExceptionObject) + switch (args.ExceptionObject) { case Exception ex: Log.Fatal(ex, "Unhandled exception on AppDomain"); break; default: - Log.Fatal("Unhandled SEH object on AppDomain: {Object}", arg.ExceptionObject); + Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject); break; } } - private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) + private static void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs args) { - if (!e.Observed) - Log.Error(e.Exception, "Unobserved exception in Task."); + if (!args.Observed) + Log.Error(args.Exception, "Unobserved exception in Task."); } } } diff --git a/Dalamud/FodyWeavers.xsd b/Dalamud/FodyWeavers.xsd index 2f1b8aae7..69dbe488c 100644 --- a/Dalamud/FodyWeavers.xsd +++ b/Dalamud/FodyWeavers.xsd @@ -11,6 +11,16 @@ Used to control if the On_PropertyName_Changed feature is enabled. + + + Used to control if the Dependent properties feature is enabled. + + + + + Used to control if the IsChanged property feature is enabled. + + Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form. @@ -31,6 +41,16 @@ Used to control if equality checks should use the static Equals method resolved from the base class. + + + Used to turn off build warnings from this weaver. + + + + + Used to turn off build warnings about mismatched On_PropertyName_Changed methods. + + diff --git a/Dalamud/Game/Addon/DalamudSystemMenu.cs b/Dalamud/Game/Addon/DalamudSystemMenu.cs index b783730d3..e735b6154 100644 --- a/Dalamud/Game/Addon/DalamudSystemMenu.cs +++ b/Dalamud/Game/Addon/DalamudSystemMenu.cs @@ -16,11 +16,11 @@ namespace Dalamud.Game.Addon internal sealed unsafe partial class DalamudSystemMenu { private readonly Dalamud dalamud; - private AtkValueChangeType atkValueChangeType; - private AtkValueSetString atkValueSetString; - private Hook hookAgentHudOpenSystemMenu; + private readonly AtkValueChangeType atkValueChangeType; + private readonly AtkValueSetString atkValueSetString; + private readonly Hook hookAgentHudOpenSystemMenu; // TODO: Make this into events in Framework.Gui - private Hook hookUiModuleRequestMainCommand; + private readonly Hook hookUiModuleRequestMainCommand; /// /// Initializes a new instance of the class. @@ -34,13 +34,10 @@ namespace Dalamud.Game.Addon this.hookAgentHudOpenSystemMenu = new Hook(openSystemMenuAddress, this.AgentHudOpenSystemMenuDetour); - var atkValueChangeTypeAddress = - this.dalamud.SigScanner.ScanText("E8 ?? ?? ?? ?? 45 84 F6 48 8D 4C 24 ??"); - this.atkValueChangeType = - Marshal.GetDelegateForFunctionPointer(atkValueChangeTypeAddress); + var atkValueChangeTypeAddress = this.dalamud.SigScanner.ScanText("E8 ?? ?? ?? ?? 45 84 F6 48 8D 4C 24 ??"); + this.atkValueChangeType = Marshal.GetDelegateForFunctionPointer(atkValueChangeTypeAddress); - var atkValueSetStringAddress = - this.dalamud.SigScanner.ScanText("E8 ?? ?? ?? ?? 41 03 ED"); + var atkValueSetStringAddress = this.dalamud.SigScanner.ScanText("E8 ?? ?? ?? ?? 41 03 ED"); this.atkValueSetString = Marshal.GetDelegateForFunctionPointer(atkValueSetStringAddress); var uiModuleRequestMainCommandAddress = this.dalamud.SigScanner.ScanText("40 53 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B 01 8B DA 48 8B F1 FF 90 ?? ?? ?? ??"); @@ -140,10 +137,10 @@ namespace Dalamud.Game.Addon switch (commandId) { case 69420: - this.dalamud.DalamudUi.TogglePluginInstaller(); + this.dalamud.DalamudUi.TogglePluginInstallerWindow(); break; case 69421: - this.dalamud.DalamudUi.ToggleSettings(); + this.dalamud.DalamudUi.ToggleSettingsWindow(); break; default: this.hookUiModuleRequestMainCommand.Original(thisPtr, commandId); diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 5beb4c5ed..9fa2411df 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -11,7 +11,7 @@ using CheapLoc; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; -using Dalamud.Interface; +using Dalamud.Interface.Internal.Windows; using Serilog; namespace Dalamud.Game @@ -21,39 +21,39 @@ namespace Dalamud.Game /// public class ChatHandlers { - private static readonly Dictionary UnicodeToDiscordEmojiDict = new() - { - { "", "<:ffxive071:585847382210642069>" }, - { "", "<:ffxive083:585848592699490329>" }, - }; + // private static readonly Dictionary UnicodeToDiscordEmojiDict = new() + // { + // { "", "<:ffxive071:585847382210642069>" }, + // { "", "<:ffxive083:585848592699490329>" }, + // }; - private readonly Dictionary handledChatTypeColors = new() - { - { XivChatType.CrossParty, Color.DodgerBlue }, - { XivChatType.Party, Color.DodgerBlue }, - { XivChatType.FreeCompany, Color.DeepSkyBlue }, - { XivChatType.CrossLinkShell1, Color.ForestGreen }, - { XivChatType.CrossLinkShell2, Color.ForestGreen }, - { XivChatType.CrossLinkShell3, Color.ForestGreen }, - { XivChatType.CrossLinkShell4, Color.ForestGreen }, - { XivChatType.CrossLinkShell5, Color.ForestGreen }, - { XivChatType.CrossLinkShell6, Color.ForestGreen }, - { XivChatType.CrossLinkShell7, Color.ForestGreen }, - { XivChatType.CrossLinkShell8, Color.ForestGreen }, - { XivChatType.Ls1, Color.ForestGreen }, - { XivChatType.Ls2, Color.ForestGreen }, - { XivChatType.Ls3, Color.ForestGreen }, - { XivChatType.Ls4, Color.ForestGreen }, - { XivChatType.Ls5, Color.ForestGreen }, - { XivChatType.Ls6, Color.ForestGreen }, - { XivChatType.Ls7, Color.ForestGreen }, - { XivChatType.Ls8, Color.ForestGreen }, - { XivChatType.TellIncoming, Color.HotPink }, - { XivChatType.PvPTeam, Color.SandyBrown }, - { XivChatType.Urgent, Color.DarkViolet }, - { XivChatType.NoviceNetwork, Color.SaddleBrown }, - { XivChatType.Echo, Color.Gray }, - }; + // private readonly Dictionary handledChatTypeColors = new() + // { + // { XivChatType.CrossParty, Color.DodgerBlue }, + // { XivChatType.Party, Color.DodgerBlue }, + // { XivChatType.FreeCompany, Color.DeepSkyBlue }, + // { XivChatType.CrossLinkShell1, Color.ForestGreen }, + // { XivChatType.CrossLinkShell2, Color.ForestGreen }, + // { XivChatType.CrossLinkShell3, Color.ForestGreen }, + // { XivChatType.CrossLinkShell4, Color.ForestGreen }, + // { XivChatType.CrossLinkShell5, Color.ForestGreen }, + // { XivChatType.CrossLinkShell6, Color.ForestGreen }, + // { XivChatType.CrossLinkShell7, Color.ForestGreen }, + // { XivChatType.CrossLinkShell8, Color.ForestGreen }, + // { XivChatType.Ls1, Color.ForestGreen }, + // { XivChatType.Ls2, Color.ForestGreen }, + // { XivChatType.Ls3, Color.ForestGreen }, + // { XivChatType.Ls4, Color.ForestGreen }, + // { XivChatType.Ls5, Color.ForestGreen }, + // { XivChatType.Ls6, Color.ForestGreen }, + // { XivChatType.Ls7, Color.ForestGreen }, + // { XivChatType.Ls8, Color.ForestGreen }, + // { XivChatType.TellIncoming, Color.HotPink }, + // { XivChatType.PvPTeam, Color.SandyBrown }, + // { XivChatType.Urgent, Color.DarkViolet }, + // { XivChatType.NoviceNetwork, Color.SaddleBrown }, + // { XivChatType.Echo, Color.Gray }, + // }; private readonly Regex rmtRegex = new( @"4KGOLD|We have sufficient stock|VPK\.OM|Gil for free|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%オ|Off Code( *):|offers Fantasia", @@ -96,14 +96,14 @@ namespace Dalamud.Game private readonly Regex urlRegex = new(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled); private readonly Dalamud dalamud; - private DalamudLinkPayload openInstallerWindowLink; + private readonly DalamudLinkPayload openInstallerWindowLink; private bool hasSeenLoadingMsg; /// /// Initializes a new instance of the class. /// /// Dalamud instance. - public ChatHandlers(Dalamud dalamud) + internal ChatHandlers(Dalamud dalamud) { this.dalamud = dalamud; @@ -121,24 +121,24 @@ namespace Dalamud.Game /// public string LastLink { get; private set; } - /// - /// Convert a string to SeString and wrap in italics payloads. - /// - /// Text to convert. - /// SeString payload of italicized text. - private static SeString MakeItalics(string text) - { - // TODO: when the code OnCharMessage is switched to SeString, this can be a straight insertion of the - // italics payloads only, and be a lot cleaner - var italicString = new SeString(new List(new Payload[] - { - EmphasisItalicPayload.ItalicsOn, - new TextPayload(text), - EmphasisItalicPayload.ItalicsOff, - })); - - return italicString; - } + // /// + // /// Convert a string to SeString and wrap in italics payloads. + // /// + // /// Text to convert. + // /// SeString payload of italicized text. + // private static SeString MakeItalics(string text) + // { + // // TODO: when the code OnCharMessage is switched to SeString, this can be a straight insertion of the + // // italics payloads only, and be a lot cleaner + // var italicString = new SeString(new List(new Payload[] + // { + // EmphasisItalicPayload.ItalicsOn, + // new TextPayload(text), + // EmphasisItalicPayload.ItalicsOff, + // })); + // + // return italicString; + // } private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled) { @@ -243,13 +243,13 @@ namespace Dalamud.Game var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString(); this.dalamud.Framework.Gui.Chat.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud vD{0} loaded."), assemblyVersion) - + string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), this.dalamud.PluginManager.Plugins.Count)); + + string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), this.dalamud.PluginManager.InstalledPlugins.Count)); if (this.dalamud.Configuration.PrintPluginsWelcomeMsg) { - foreach (var plugin in this.dalamud.PluginManager.Plugins.OrderBy(x => x.Plugin.Name)) + foreach (var plugin in this.dalamud.PluginManager.InstalledPlugins.OrderBy(plugin => plugin.Name)) { - this.dalamud.Framework.Gui.Chat.Print(string.Format(Loc.Localize("DalamudPluginLoaded", " 》 {0} v{1} loaded."), plugin.Plugin.Name, plugin.Definition.AssemblyVersion)); + this.dalamud.Framework.Gui.Chat.Print(string.Format(Loc.Localize("DalamudPluginLoaded", " 》 {0} v{1} loaded."), plugin.Name, plugin.Manifest.AssemblyVersion)); } } @@ -261,16 +261,15 @@ namespace Dalamud.Game Type = XivChatType.Notice, }); - if (DalamudChangelogWindow.WarrantsChangelog) -#pragma warning disable CS0162 // Unreachable code detected - this.dalamud.DalamudUi.OpenChangelog(); -#pragma warning restore CS0162 // Unreachable code detected + if (this.dalamud.DalamudUi.WarrantsChangelog) + this.dalamud.DalamudUi.OpenChangelogWindow(); this.dalamud.Configuration.LastVersion = assemblyVersion; this.dalamud.Configuration.Save(); } - Task.Run(() => this.dalamud.PluginRepository.UpdatePlugins(!this.dalamud.Configuration.AutoUpdatePlugins)).ContinueWith(t => + Task.Run(() => this.dalamud.PluginManager.UpdatePlugins(!this.dalamud.Configuration.AutoUpdatePlugins)) + .ContinueWith(t => { if (t.IsFaulted) { @@ -278,13 +277,13 @@ namespace Dalamud.Game } else { - var updatedPlugins = t.Result.UpdatedPlugins; + var updatedPlugins = t.Result; if (updatedPlugins != null && updatedPlugins.Any()) { if (this.dalamud.Configuration.AutoUpdatePlugins) { - this.dalamud.PluginRepository.PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:")); + this.dalamud.PluginManager.PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:")); } else { diff --git a/Dalamud/Game/ClientState/Actors/ActorTable.cs b/Dalamud/Game/ClientState/Actors/ActorTable.cs index 7f8ca7c35..f950418b3 100644 --- a/Dalamud/Game/ClientState/Actors/ActorTable.cs +++ b/Dalamud/Game/ClientState/Actors/ActorTable.cs @@ -34,14 +34,14 @@ namespace Dalamud.Game.ClientState.Actors /// /// The Dalamud instance. /// The ClientStateAddressResolver instance. - public ActorTable(Dalamud dalamud, ClientStateAddressResolver addressResolver) + internal ActorTable(Dalamud dalamud, ClientStateAddressResolver addressResolver) { this.address = addressResolver; this.dalamud = dalamud; dalamud.Framework.OnUpdateEvent += this.Framework_OnUpdateEvent; - Log.Verbose("Actor table address {ActorTable}", this.address.ActorTable); + Log.Verbose($"Actor table address 0x{this.address.ActorTable.ToInt64():X}"); } /// diff --git a/Dalamud/Game/ClientState/Actors/Resolvers/BaseResolver.cs b/Dalamud/Game/ClientState/Actors/Resolvers/BaseResolver.cs index c097b1111..728939255 100644 --- a/Dalamud/Game/ClientState/Actors/Resolvers/BaseResolver.cs +++ b/Dalamud/Game/ClientState/Actors/Resolvers/BaseResolver.cs @@ -11,7 +11,7 @@ namespace Dalamud.Game.ClientState.Actors.Resolvers /// Initializes a new instance of the class. /// /// The Dalamud instance. - public BaseResolver(Dalamud dalamud) + internal BaseResolver(Dalamud dalamud) { this.dalamud = dalamud; } @@ -19,6 +19,6 @@ namespace Dalamud.Game.ClientState.Actors.Resolvers /// /// Gets the Dalamud instance. /// - protected Dalamud Dalamud => this.dalamud; + internal Dalamud Dalamud => this.dalamud; } } diff --git a/Dalamud/Game/ClientState/Actors/Resolvers/ClassJob.cs b/Dalamud/Game/ClientState/Actors/Resolvers/ClassJob.cs index 57ae9fc48..19fea14a7 100644 --- a/Dalamud/Game/ClientState/Actors/Resolvers/ClassJob.cs +++ b/Dalamud/Game/ClientState/Actors/Resolvers/ClassJob.cs @@ -16,7 +16,7 @@ namespace Dalamud.Game.ClientState.Actors.Resolvers /// /// The ID of the classJob. /// The Dalamud instance. - public ClassJob(byte id, Dalamud dalamud) + internal ClassJob(byte id, Dalamud dalamud) : base(dalamud) { this.Id = id; diff --git a/Dalamud/Game/ClientState/Actors/Resolvers/World.cs b/Dalamud/Game/ClientState/Actors/Resolvers/World.cs index 536bfaf81..6a5d437f0 100644 --- a/Dalamud/Game/ClientState/Actors/Resolvers/World.cs +++ b/Dalamud/Game/ClientState/Actors/Resolvers/World.cs @@ -16,7 +16,7 @@ namespace Dalamud.Game.ClientState.Actors.Resolvers /// /// The ID of the world. /// The Dalamud instance. - public World(ushort id, Dalamud dalamud) + internal World(ushort id, Dalamud dalamud) : base(dalamud) { this.Id = id; diff --git a/Dalamud/Game/ClientState/Actors/Types/Actor.cs b/Dalamud/Game/ClientState/Actors/Types/Actor.cs index 9707f6674..a38daf5b0 100644 --- a/Dalamud/Game/ClientState/Actors/Types/Actor.cs +++ b/Dalamud/Game/ClientState/Actors/Types/Actor.cs @@ -1,7 +1,6 @@ using System; -using System.Text; + using Dalamud.Game.ClientState.Structs; -using Serilog; namespace Dalamud.Game.ClientState.Actors.Types { @@ -22,7 +21,7 @@ namespace Dalamud.Game.ClientState.Actors.Types /// The memory representation of the base actor. /// A dalamud reference needed to access game data in Resolvers. /// The address of this actor in memory. - public Actor(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud) + internal Actor(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud) { this.actorStruct = actorStruct; this.dalamud = dalamud; @@ -94,7 +93,7 @@ namespace Dalamud.Game.ClientState.Actors.Types /// /// Gets the backing instance. /// - protected Dalamud Dalamud => this.dalamud; + internal Dalamud Dalamud => this.dalamud; /// bool IEquatable.Equals(Actor other) => this.ActorId == other.ActorId; diff --git a/Dalamud/Game/ClientState/Actors/Types/Chara.cs b/Dalamud/Game/ClientState/Actors/Types/Chara.cs index 73875d80c..4977801e9 100644 --- a/Dalamud/Game/ClientState/Actors/Types/Chara.cs +++ b/Dalamud/Game/ClientState/Actors/Types/Chara.cs @@ -16,7 +16,7 @@ namespace Dalamud.Game.ClientState.Actors.Types /// The memory representation of the base actor. /// A dalamud reference needed to access game data in Resolvers. /// The address of this actor in memory. - protected Chara(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud) + internal Chara(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud) : base(address, actorStruct, dalamud) { } diff --git a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/BattleNpc.cs b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/BattleNpc.cs index 0bfd60c5b..bcc0ebe70 100644 --- a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/BattleNpc.cs +++ b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/BattleNpc.cs @@ -14,7 +14,7 @@ namespace Dalamud.Game.ClientState.Actors.Types.NonPlayer /// The memory representation of the base actor. /// A dalamud reference needed to access game data in Resolvers. /// The address of this actor in memory. - public BattleNpc(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud) + internal BattleNpc(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud) : base(address, actorStruct, dalamud) { } diff --git a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/EventObj.cs b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/EventObj.cs index e0ac5964d..fd112c247 100644 --- a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/EventObj.cs +++ b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/EventObj.cs @@ -14,7 +14,7 @@ namespace Dalamud.Game.ClientState.Actors.Types.NonPlayer /// The memory representation of the base actor. /// A dalamud reference needed to access game data in Resolvers. /// The address of this actor in memory. - public EventObj(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud) + internal EventObj(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud) : base(address, actorStruct, dalamud) { } diff --git a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/Npc.cs b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/Npc.cs index 7be029450..4571203c9 100644 --- a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/Npc.cs +++ b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/Npc.cs @@ -14,7 +14,7 @@ namespace Dalamud.Game.ClientState.Actors.Types.NonPlayer /// The memory representation of the base actor. /// A dalamud reference needed to access game data in Resolvers. /// The address of this actor in memory. - public Npc(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud) + internal Npc(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud) : base(address, actorStruct, dalamud) { } diff --git a/Dalamud/Game/ClientState/Actors/Types/PlayerCharacter.cs b/Dalamud/Game/ClientState/Actors/Types/PlayerCharacter.cs index 7cc932550..44d01f783 100644 --- a/Dalamud/Game/ClientState/Actors/Types/PlayerCharacter.cs +++ b/Dalamud/Game/ClientState/Actors/Types/PlayerCharacter.cs @@ -20,7 +20,7 @@ namespace Dalamud.Game.ClientState.Actors.Types /// The memory representation of the base actor. /// A dalamud reference needed to access game data in Resolvers. /// The address of this actor in memory. - public PlayerCharacter(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud) + internal PlayerCharacter(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud) : base(address, actorStruct, dalamud) { var companyTagBytes = new byte[5]; diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index f2057a607..bf5ed523f 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -15,7 +15,7 @@ namespace Dalamud.Game.ClientState /// /// This class represents the state of the game client at the time of access. /// - public class ClientState : INotifyPropertyChanged, IDisposable + public sealed class ClientState : INotifyPropertyChanged, IDisposable { /// /// The table of all present actors. @@ -80,7 +80,7 @@ namespace Dalamud.Game.ClientState /// Dalamud instance. /// StartInfo of the current Dalamud launch. /// Sig scanner. - public ClientState(Dalamud dalamud, DalamudStartInfo startInfo, SigScanner scanner) + internal ClientState(Dalamud dalamud, DalamudStartInfo startInfo, SigScanner scanner) { this.dalamud = dalamud; this.address = new ClientStateAddressResolver(); @@ -104,7 +104,7 @@ namespace Dalamud.Game.ClientState this.Targets = new Targets(dalamud, this.address); - Log.Verbose("SetupTerritoryType address {SetupTerritoryType}", this.address.SetupTerritoryType); + Log.Verbose($"SetupTerritoryType address 0x{this.address.SetupTerritoryType.ToInt64():X}"); this.setupTerritoryTypeHook = new Hook(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour); diff --git a/Dalamud/Game/ClientState/GamepadState.cs b/Dalamud/Game/ClientState/GamepadState.cs index e3ffc1cef..2d84fdb83 100644 --- a/Dalamud/Game/ClientState/GamepadState.cs +++ b/Dalamud/Game/ClientState/GamepadState.cs @@ -1,4 +1,4 @@ -using System; +using System; using Dalamud.Game.ClientState.Structs; using Dalamud.Hooking; @@ -12,7 +12,7 @@ namespace Dalamud.Game.ClientState /// /// Will block game's gamepad input if is set. /// - public unsafe class GamepadState + public unsafe class GamepadState : IDisposable { private readonly Hook gamepadPoll; @@ -29,12 +29,8 @@ namespace Dalamud.Game.ClientState /// Resolver knowing the pointer to the GamepadPoll function. public GamepadState(ClientStateAddressResolver resolver) { -#if DEBUG - Log.Verbose("GamepadPoll address {GamepadPoll}", resolver.GamepadPoll); -#endif - this.gamepadPoll = new Hook( - resolver.GamepadPoll, - (ControllerPoll)this.GamepadPollDetour); + Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}"); + this.gamepadPoll = new Hook(resolver.GamepadPoll, this.GamepadPollDetour); } /// diff --git a/Dalamud/Game/ClientState/JobGauges.cs b/Dalamud/Game/ClientState/JobGauges.cs index f25f5d8ab..e0e9c0f9d 100644 --- a/Dalamud/Game/ClientState/JobGauges.cs +++ b/Dalamud/Game/ClientState/JobGauges.cs @@ -17,7 +17,7 @@ namespace Dalamud.Game.ClientState { this.Address = addressResolver; - Log.Verbose("JobGaugeData address {JobGaugeData}", this.Address.JobGaugeData); + Log.Verbose($"JobGaugeData address 0x{this.Address.JobGaugeData.ToInt64():X}"); } private ClientStateAddressResolver Address { get; } diff --git a/Dalamud/Game/ClientState/KeyState.cs b/Dalamud/Game/ClientState/KeyState.cs index 8bc3c200f..e17d2cf50 100644 --- a/Dalamud/Game/ClientState/KeyState.cs +++ b/Dalamud/Game/ClientState/KeyState.cs @@ -25,7 +25,7 @@ namespace Dalamud.Game.ClientState { this.bufferBase = moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardState); - Log.Verbose($"Keyboard state buffer address {this.bufferBase}"); + Log.Verbose($"Keyboard state buffer address 0x{this.bufferBase.ToInt64():X}"); } /// diff --git a/Dalamud/Game/ClientState/PartyList.cs b/Dalamud/Game/ClientState/PartyList.cs index a1db673af..2aa4b95ed 100644 --- a/Dalamud/Game/ClientState/PartyList.cs +++ b/Dalamud/Game/ClientState/PartyList.cs @@ -25,7 +25,7 @@ namespace Dalamud.Game.ClientState /// /// The Dalamud instance. /// The ClientStateAddressResolver instance. - public PartyList(Dalamud dalamud, ClientStateAddressResolver addressResolver) + internal PartyList(Dalamud dalamud, ClientStateAddressResolver addressResolver) { this.address = addressResolver; this.dalamud = dalamud; diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/DNCGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/DNCGauge.cs index 2143aa99f..8002a2f19 100644 --- a/Dalamud/Game/ClientState/Structs/JobGauge/DNCGauge.cs +++ b/Dalamud/Game/ClientState/Structs/JobGauge/DNCGauge.cs @@ -32,6 +32,7 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge /// /// Gets the next step in the current dance. /// + /// The next dance step action ID. public ulong NextStep() => (ulong)(15999 + this.stepOrder[this.NumCompleteSteps] - 1); /// diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 9d7dac9cd..6c31e2444 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -27,7 +27,7 @@ namespace Dalamud.Game.Command /// /// The Dalamud instance. /// The client language requested. - public CommandManager(Dalamud dalamud, ClientLanguage language) + internal CommandManager(Dalamud dalamud, ClientLanguage language) { this.dalamud = dalamud; @@ -128,7 +128,8 @@ namespace Dalamud.Game.Command /// If adding was successful. public bool AddHandler(string command, CommandInfo info) { - if (info == null) throw new ArgumentNullException(nameof(info), "Command handler is null."); + if (info == null) + throw new ArgumentNullException(nameof(info), "Command handler is null."); try { diff --git a/Dalamud/Game/GameVersion.cs b/Dalamud/Game/GameVersion.cs new file mode 100644 index 000000000..8ef76eabe --- /dev/null +++ b/Dalamud/Game/GameVersion.cs @@ -0,0 +1,383 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text; + +using Newtonsoft.Json; + +namespace Dalamud.Game +{ + /// + /// A GameVersion object contains give hierarchical numeric components: year, month, + /// day, major and minor. All components may be unspecified, which is represented + /// internally as a -1. By definition, an unspecified component matches anything + /// (both unspecified and specified), and an unspecified component is "less than" any + /// specified component. It will also equal the string "any" if all components are + /// unspecified. The value can be retrieved from the ffxivgame.ver file in your game + /// installation directory. + /// + [Serializable] + public sealed class GameVersion : ICloneable, IComparable, IComparable, IEquatable + { + private static readonly GameVersion AnyVersion = new(); + + /// + /// Initializes a new instance of the class. + /// + /// Version string to parse. + [JsonConstructor] + public GameVersion(string version) + { + var ver = Parse(version); + this.Year = ver.Year; + this.Month = ver.Month; + this.Day = ver.Day; + this.Major = ver.Major; + this.Minor = ver.Minor; + } + + /// + /// Initializes a new instance of the class. + /// + /// The year. + /// The month. + /// The day. + /// The major version. + /// The minor version. + public GameVersion(int year, int month, int day, int major, int minor) + { + if ((this.Year = year) < 0) + throw new ArgumentOutOfRangeException(nameof(year)); + + if ((this.Month = month) < 0) + throw new ArgumentOutOfRangeException(nameof(month)); + + if ((this.Day = day) < 0) + throw new ArgumentOutOfRangeException(nameof(day)); + + if ((this.Major = major) < 0) + throw new ArgumentOutOfRangeException(nameof(major)); + + if ((this.Minor = minor) < 0) + throw new ArgumentOutOfRangeException(nameof(minor)); + } + + /// + /// Initializes a new instance of the class. + /// + /// The year. + /// The month. + /// The day. + /// The major version. + public GameVersion(int year, int month, int day, int major) + { + if ((this.Year = year) < 0) + throw new ArgumentOutOfRangeException(nameof(year)); + + if ((this.Month = month) < 0) + throw new ArgumentOutOfRangeException(nameof(month)); + + if ((this.Day = day) < 0) + throw new ArgumentOutOfRangeException(nameof(day)); + + if ((this.Major = major) < 0) + throw new ArgumentOutOfRangeException(nameof(major)); + } + + /// + /// Initializes a new instance of the class. + /// + /// The year. + /// The month. + /// The day. + public GameVersion(int year, int month, int day) + { + if ((this.Year = year) < 0) + throw new ArgumentOutOfRangeException(nameof(year)); + + if ((this.Month = month) < 0) + throw new ArgumentOutOfRangeException(nameof(month)); + + if ((this.Day = day) < 0) + throw new ArgumentOutOfRangeException(nameof(day)); + } + + /// + /// Initializes a new instance of the class. + /// + /// The year. + /// The month. + public GameVersion(int year, int month) + { + if ((this.Year = year) < 0) + throw new ArgumentOutOfRangeException(nameof(year)); + + if ((this.Month = month) < 0) + throw new ArgumentOutOfRangeException(nameof(month)); + } + + /// + /// Initializes a new instance of the class. + /// + /// The year. + public GameVersion(int year) + { + if ((this.Year = year) < 0) + throw new ArgumentOutOfRangeException(nameof(year)); + } + + /// + /// Initializes a new instance of the class. + /// + public GameVersion() + { + } + + /// + /// Gets the default "any" game version. + /// + public static GameVersion Any => AnyVersion; + + /// + /// Gets the year component. + /// + public int Year { get; } = -1; + + /// + /// Gets the month component. + /// + public int Month { get; } = -1; + + /// + /// Gets the day component. + /// + public int Day { get; } = -1; + + /// + /// Gets the major version component. + /// + public int Major { get; } = -1; + + /// + /// Gets the minor version component. + /// + public int Minor { get; } = -1; + + public static implicit operator GameVersion(string ver) + { + return Parse(ver); + } + + public static bool operator ==(GameVersion v1, GameVersion v2) + { + if (v1 is null) + { + return v2 is null; + } + + return v1.Equals(v2); + } + + public static bool operator !=(GameVersion v1, GameVersion v2) + { + return !(v1 == v2); + } + + public static bool operator <(GameVersion v1, GameVersion v2) + { + if (v1 is null) + throw new ArgumentNullException(nameof(v1)); + + return v1.CompareTo(v2) < 0; + } + + public static bool operator <=(GameVersion v1, GameVersion v2) + { + if (v1 is null) + throw new ArgumentNullException(nameof(v1)); + + return v1.CompareTo(v2) <= 0; + } + + public static bool operator >(GameVersion v1, GameVersion v2) + { + return v2 < v1; + } + + public static bool operator >=(GameVersion v1, GameVersion v2) + { + return v2 <= v1; + } + + /// + /// Parse a version string. YYYY.MM.DD.majr.minr or "any". + /// + /// Input to parse. + /// GameVersion object. + public static GameVersion Parse(string input) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + if (input.ToLower(CultureInfo.InvariantCulture) == "any") + return new GameVersion(); + + var parts = input.Split('.'); + var tplParts = parts.Select(p => + { + var result = int.TryParse(p, out var value); + return (result, value); + }).ToArray(); + + if (tplParts.Any(t => !t.result)) + throw new FormatException("Bad formatting"); + + var intParts = tplParts.Select(t => t.value).ToArray(); + var len = intParts.Length; + + if (len == 1) + return new GameVersion(intParts[0]); + else if (len == 2) + return new GameVersion(intParts[0], intParts[1]); + else if (len == 3) + return new GameVersion(intParts[0], intParts[1], intParts[2]); + else if (len == 4) + return new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3]); + else if (len == 5) + return new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3], intParts[4]); + else + throw new ArgumentException("Too many parts"); + } + + /// + /// Try to parse a version string. YYYY.MM.DD.majr.minr or "any". + /// + /// Input to parse. + /// GameVersion object. + /// Success or failure. + public static bool TryParse(string input, out GameVersion result) + { + try + { + result = Parse(input); + return true; + } + catch + { + result = null; + return false; + } + } + + /// + public object Clone() => new GameVersion(this.Year, this.Month, this.Day, this.Major, this.Minor); + + /// + public int CompareTo(object obj) + { + if (obj == null) + return 1; + + if (obj is GameVersion value) + { + return this.CompareTo(value); + } + else + { + throw new ArgumentException("Argument must be a GameVersion"); + } + } + + /// + public int CompareTo(GameVersion value) + { + if (value == null) + return 1; + + if (this == value) + return 0; + + if (this == AnyVersion) + return 1; + + if (value == AnyVersion) + return -1; + + if (this.Year != value.Year) + return this.Year > value.Year ? 1 : -1; + + if (this.Month != value.Month) + return this.Month > value.Month ? 1 : -1; + + if (this.Day != value.Day) + return this.Day > value.Day ? 1 : -1; + + if (this.Major != value.Major) + return this.Major > value.Major ? 1 : -1; + + if (this.Minor != value.Minor) + return this.Minor > value.Minor ? 1 : -1; + + return 0; + } + + /// + public override bool Equals(object obj) + { + if (obj is not GameVersion value) + return false; + + return this.Equals(value); + } + + /// + public bool Equals(GameVersion value) + { + if (value == null) + { + return false; + } + + return + (this.Year == value.Year) && + (this.Month == value.Month) && + (this.Day == value.Day) && + (this.Major == value.Major) && + (this.Minor == value.Minor); + } + + /// + public override int GetHashCode() + { + var accumulator = 0; + + // This might be horribly wrong, but it isn't used heavily. + accumulator |= this.Year.GetHashCode(); + accumulator |= this.Month.GetHashCode(); + accumulator |= this.Day.GetHashCode(); + accumulator |= this.Major.GetHashCode(); + accumulator |= this.Minor.GetHashCode(); + + return accumulator; + } + + /// + public override string ToString() + { + if (this.Year == -1 && + this.Month == -1 && + this.Day == -1 && + this.Major == -1 && + this.Minor == -1) + return "any"; + + return new StringBuilder() + .Append(string.Format("{0:D4}.", this.Year == -1 ? 0 : this.Year)) + .Append(string.Format("{0:D2}.", this.Month == -1 ? 0 : this.Month)) + .Append(string.Format("{0:D2}.", this.Day == -1 ? 0 : this.Day)) + .Append(string.Format("{0:D4}.", this.Major == -1 ? 0 : this.Major)) + .Append(string.Format("{0:D4}", this.Minor == -1 ? 0 : this.Minor)) + .ToString(); + } + } +} diff --git a/Dalamud/Game/GameVersionConverter.cs b/Dalamud/Game/GameVersionConverter.cs new file mode 100644 index 000000000..9058e8be9 --- /dev/null +++ b/Dalamud/Game/GameVersionConverter.cs @@ -0,0 +1,80 @@ +using System; + +using Newtonsoft.Json; + +namespace Dalamud.Game +{ + /// + /// Converts a to and from a string (e.g. "2010.01.01.1234.5678"). + /// + public sealed class GameVersionConverter : JsonConverter + { + /// + /// Writes the JSON representation of the object. + /// + /// The to write to. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + } + else if (value is GameVersion) + { + writer.WriteValue(value.ToString()); + } + else + { + throw new JsonSerializationException("Expected GameVersion object value"); + } + } + + /// + /// Reads the JSON representation of the object. + /// + /// The to read from. + /// Type of the object. + /// The existing property value of the JSON that is being converted. + /// The calling serializer. + /// The object value. + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + else + { + if (reader.TokenType == JsonToken.String) + { + try + { + return new GameVersion((string)reader.Value!); + } + catch (Exception ex) + { + throw new JsonSerializationException($"Error parsing GameVersion string: {reader.Value}", ex); + } + } + else + { + throw new JsonSerializationException($"Unexpected token or value when parsing GameVersion. Token: {reader.TokenType}, Value: {reader.Value}"); + } + } + } + + /// + /// Determines whether this instance can convert the specified object type. + /// + /// Type of the object. + /// + /// true if this instance can convert the specified object type; otherwise, false. + /// + public override bool CanConvert(Type objectType) + { + return objectType == typeof(GameVersion); + } + } +} diff --git a/Dalamud/Game/Internal/AntiDebug.cs b/Dalamud/Game/Internal/AntiDebug.cs index b7ca43b86..57c7fdeb4 100644 --- a/Dalamud/Game/Internal/AntiDebug.cs +++ b/Dalamud/Game/Internal/AntiDebug.cs @@ -29,7 +29,7 @@ namespace Dalamud.Game.Internal this.debugCheckAddress = IntPtr.Zero; } - Log.Verbose("DebugCheck address {DebugCheckAddress}", this.debugCheckAddress); + Log.Verbose($"Debug check address 0x{this.debugCheckAddress.ToInt64():X}"); } /// @@ -45,7 +45,7 @@ namespace Dalamud.Game.Internal this.original = new byte[this.nop.Length]; if (this.debugCheckAddress != IntPtr.Zero && !this.IsEnabled) { - Log.Information($"Overwriting debug check @ 0x{this.debugCheckAddress.ToInt64():X}"); + Log.Information($"Overwriting debug check at 0x{this.debugCheckAddress.ToInt64():X}"); SafeMemory.ReadBytes(this.debugCheckAddress, this.nop.Length, out this.original); SafeMemory.WriteBytes(this.debugCheckAddress, this.nop); } @@ -64,7 +64,7 @@ namespace Dalamud.Game.Internal { if (this.debugCheckAddress != IntPtr.Zero && this.original != null) { - Log.Information($"Reverting debug check @ 0x{this.debugCheckAddress.ToInt64():X}"); + Log.Information($"Reverting debug check at 0x{this.debugCheckAddress.ToInt64():X}"); SafeMemory.WriteBytes(this.debugCheckAddress, this.original); } else @@ -110,9 +110,11 @@ namespace Dalamud.Game.Internal { // If anti-debug is enabled and is being disposed, odds are either the game is exiting, or Dalamud is being reloaded. // If it is the latter, there's half a chance a debugger is currently attached. There's no real need to disable the - // check in either situation anyways. - // this.Disable(); + // check in either situation anyways. However if Dalamud is being reloaded, the sig may fail so may as well undo it. + this.Disable(); } + + this.disposed = true; } } } diff --git a/Dalamud/Game/Internal/DXGI/Definitions/ID3D11DeviceVtbl.cs b/Dalamud/Game/Internal/DXGI/Definitions/ID3D11DeviceVtbl.cs new file mode 100644 index 000000000..7703713b3 --- /dev/null +++ b/Dalamud/Game/Internal/DXGI/Definitions/ID3D11DeviceVtbl.cs @@ -0,0 +1,227 @@ +namespace Dalamud.Game.Internal.DXGI.Definitions +{ + /// + /// Contains a full list of ID3D11Device functions to be used as an indexer into the DirectX Virtual Function Table entries. + /// + internal enum ID3D11DeviceVtbl + { + // IUnknown + + /// + /// IUnknown::QueryInterface method (unknwn.h). + /// + QueryInterface = 0, + + /// + /// IUnknown::AddRef method (unknwn.h). + /// + AddRef = 1, + + /// + /// IUnknown::Release method (unknwn.h). + /// + Release = 2, + + // ID3D11Device + + /// + /// ID3D11Device::CreateBuffer method (d3d11.h). + /// + CreateBuffer = 3, + + /// + /// ID3D11Device::CreateTexture1D method (d3d11.h). + /// + CreateTexture1D = 4, + + /// + /// ID3D11Device::CreateTexture2D method (d3d11.h). + /// + CreateTexture2D = 5, + + /// + /// ID3D11Device::CreateTexture3D method (d3d11.h). + /// + CreateTexture3D = 6, + + /// + /// ID3D11Device::CreateShaderResourceView method (d3d11.h). + /// + CreateShaderResourceView = 7, + + /// + /// ID3D11Device::CreateUnorderedAccessView method (d3d11.h). + /// + CreateUnorderedAccessView = 8, + + /// + /// ID3D11Device::CreateRenderTargetView method (d3d11.h). + /// + CreateRenderTargetView = 9, + + /// + /// ID3D11Device::CreateDepthStencilView method (d3d11.h). + /// + CreateDepthStencilView = 10, + + /// + /// ID3D11Device::CreateInputLayout method (d3d11.h). + /// + CreateInputLayout = 11, + + /// + /// ID3D11Device::CreateVertexShader method (d3d11.h). + /// + CreateVertexShader = 12, + + /// + /// ID3D11Device::CreateGeometryShader method (d3d11.h). + /// + CreateGeometryShader = 13, + + /// + /// ID3D11Device::CreateGeometryShaderWithStreamOutput method (d3d11.h). + /// + CreateGeometryShaderWithStreamOutput = 14, + + /// + /// ID3D11Device::CreatePixelShader method (d3d11.h). + /// + CreatePixelShader = 15, + + /// + /// ID3D11Device::CreateHullShader method (d3d11.h). + /// + CreateHullShader = 16, + + /// + /// ID3D11Device::CreateDomainShader method (d3d11.h). + /// + CreateDomainShader = 17, + + /// + /// ID3D11Device::CreateComputeShader method (d3d11.h). + /// + CreateComputeShader = 18, + + /// + /// ID3D11Device::CreateClassLinkage method (d3d11.h). + /// + CreateClassLinkage = 19, + + /// + /// ID3D11Device::CreateBlendState method (d3d11.h). + /// + CreateBlendState = 20, + + /// + /// ID3D11Device::CreateDepthStencilState method (d3d11.h). + /// + CreateDepthStencilState = 21, + + /// + /// ID3D11Device::CreateRasterizerState method (d3d11.h). + /// + CreateRasterizerState = 22, + + /// + /// ID3D11Device::CreateSamplerState method (d3d11.h). + /// + CreateSamplerState = 23, + + /// + /// ID3D11Device::CreateQuery method (d3d11.h). + /// + CreateQuery = 24, + + /// + /// ID3D11Device::CreatePredicate method (d3d11.h). + /// + CreatePredicate = 25, + + /// + /// ID3D11Device::CreateCounter method (d3d11.h). + /// + CreateCounter = 26, + + /// + /// ID3D11Device::CreateDeferredContext method (d3d11.h). + /// + CreateDeferredContext = 27, + + /// + /// ID3D11Device::OpenSharedResource method (d3d11.h). + /// + OpenSharedResource = 28, + + /// + /// ID3D11Device::CheckFormatSupport method (d3d11.h). + /// + CheckFormatSupport = 29, + + /// + /// ID3D11Device::CheckMultisampleQualityLevels method (d3d11.h). + /// + CheckMultisampleQualityLevels = 30, + + /// + /// ID3D11Device::CheckCounterInfo method (d3d11.h). + /// + CheckCounterInfo = 31, + + /// + /// ID3D11Device::CheckCounter method (d3d11.h). + /// + CheckCounter = 32, + + /// + /// ID3D11Device::CheckFeatureSupport method (d3d11.h). + /// + CheckFeatureSupport = 33, + + /// + /// ID3D11Device::GetPrivateData method (d3d11.h). + /// + GetPrivateData = 34, + + /// + /// ID3D11Device::SetPrivateData method (d3d11.h). + /// + SetPrivateData = 35, + + /// + /// ID3D11Device::SetPrivateDataInterface method (d3d11.h). + /// + SetPrivateDataInterface = 36, + + /// + /// ID3D11Device::GetFeatureLevel method (d3d11.h). + /// + GetFeatureLevel = 37, + + /// + /// ID3D11Device::GetCreationFlags method (d3d11.h). + /// + GetCreationFlags = 38, + + /// + /// ID3D11Device::GetDeviceRemovedReason method (d3d11.h). + /// + GetDeviceRemovedReason = 39, + + /// + /// ID3D11Device::GetImmediateContext method (d3d11.h). + /// + GetImmediateContext = 40, + + /// + /// ID3D11Device::SetExceptionMode method (d3d11.h). + /// + SetExceptionMode = 41, + + /// + /// ID3D11Device::GetExceptionMode method (d3d11.h). + /// + GetExceptionMode = 42, + } +} diff --git a/Dalamud/Game/Internal/DXGI/Definitions/IDXGISwapChainVtbl.cs b/Dalamud/Game/Internal/DXGI/Definitions/IDXGISwapChainVtbl.cs new file mode 100644 index 000000000..e3d627ce3 --- /dev/null +++ b/Dalamud/Game/Internal/DXGI/Definitions/IDXGISwapChainVtbl.cs @@ -0,0 +1,107 @@ +namespace Dalamud.Game.Internal.DXGI.Definitions +{ + /// + /// Contains a full list of IDXGISwapChain functions to be used as an indexer into the SwapChain Virtual Function Table + /// entries. + /// + internal enum IDXGISwapChainVtbl + { + // IUnknown + + /// + /// IUnknown::QueryInterface method (unknwn.h). + /// + QueryInterface = 0, + + /// + /// IUnknown::AddRef method (unknwn.h). + /// + AddRef = 1, + + /// + /// IUnknown::Release method (unknwn.h). + /// + Release = 2, + + // IDXGIObject + + /// + /// IDXGIObject::SetPrivateData method (dxgi.h). + /// + SetPrivateData = 3, + + /// + /// IDXGIObject::SetPrivateDataInterface method (dxgi.h). + /// + SetPrivateDataInterface = 4, + + /// + /// IDXGIObject::GetPrivateData method (dxgi.h). + /// + GetPrivateData = 5, + + /// + /// IDXGIObject::GetParent method (dxgi.h). + /// + GetParent = 6, + + // IDXGIDeviceSubObject + + /// + /// IDXGIDeviceSubObject::GetDevice method (dxgi.h). + /// + GetDevice = 7, + + // IDXGISwapChain + + /// + /// IDXGISwapChain::Present method (dxgi.h). + /// + Present = 8, + + /// + /// IUnknIDXGISwapChainown::GetBuffer method (dxgi.h). + /// + GetBuffer = 9, + + /// + /// IDXGISwapChain::SetFullscreenState method (dxgi.h). + /// + SetFullscreenState = 10, + + /// + /// IDXGISwapChain::GetFullscreenState method (dxgi.h). + /// + GetFullscreenState = 11, + + /// + /// IDXGISwapChain::GetDesc method (dxgi.h). + /// + GetDesc = 12, + + /// + /// IDXGISwapChain::ResizeBuffers method (dxgi.h). + /// + ResizeBuffers = 13, + + /// + /// IDXGISwapChain::ResizeTarget method (dxgi.h). + /// + ResizeTarget = 14, + + /// + /// IDXGISwapChain::GetContainingOutput method (dxgi.h). + /// + GetContainingOutput = 15, + + /// + /// IDXGISwapChain::GetFrameStatistics method (dxgi.h). + /// + GetFrameStatistics = 16, + + /// + /// IDXGISwapChain::GetLastPresentCount method (dxgi.h). + /// + GetLastPresentCount = 17, + } +} diff --git a/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs b/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs index 701068ed3..e44883612 100644 --- a/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs +++ b/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs @@ -22,7 +22,7 @@ namespace Dalamud.Game.Internal.DXGI { var module = Process.GetCurrentProcess().Modules.Cast().First(m => m.ModuleName == "dxgi.dll"); - Log.Debug($"Found DXGI: {module.BaseAddress.ToInt64():X}"); + Log.Debug($"Found DXGI: 0x{module.BaseAddress.ToInt64():X}"); var scanner = new SigScanner(module); diff --git a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs index 6e69a17bd..b5c0d3225 100644 --- a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs +++ b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; -using System.Windows.Forms; +using Dalamud.Game.Internal.DXGI.Definitions; using SharpDX.Direct3D; using SharpDX.Direct3D11; using SharpDX.DXGI; @@ -19,9 +19,6 @@ namespace Dalamud.Game.Internal.DXGI /// public class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressResolver { - private const int DxgiSwapchainMethodCount = 18; - private const int D3D11DeviceMethodCount = 43; - private List d3d11VTblAddresses; private List dxgiSwapChainVTblAddresses; @@ -34,30 +31,33 @@ namespace Dalamud.Game.Internal.DXGI /// protected override void Setup64Bit(SigScanner sig) { + // Create temporary device + swapchain and determine method addresses if (this.d3d11VTblAddresses == null) { - // Create temporary device + swapchain and determine method addresses - var renderForm = new Form(); + // A renderable object isnt required, just a handle + var handle = Marshal.AllocHGlobal(Marshal.SizeOf()); Device.CreateWithSwapChain( DriverType.Hardware, DeviceCreationFlags.BgraSupport, - CreateSwapChainDescription(renderForm.Handle), + CreateSwapChainDescription(handle), out var device, out var swapChain); if (device != null && swapChain != null) { - this.d3d11VTblAddresses = this.GetVTblAddresses(device.NativePointer, D3D11DeviceMethodCount); - this.dxgiSwapChainVTblAddresses = this.GetVTblAddresses(swapChain.NativePointer, DxgiSwapchainMethodCount); + this.d3d11VTblAddresses = GetVTblAddresses(device.NativePointer, Enum.GetValues(typeof(ID3D11DeviceVtbl)).Length); + this.dxgiSwapChainVTblAddresses = GetVTblAddresses(swapChain.NativePointer, Enum.GetValues(typeof(IDXGISwapChainVtbl)).Length); } device?.Dispose(); swapChain?.Dispose(); + + Marshal.FreeHGlobal(handle); } - this.Present = this.dxgiSwapChainVTblAddresses[8]; - this.ResizeBuffers = this.dxgiSwapChainVTblAddresses[13]; + this.Present = this.dxgiSwapChainVTblAddresses[(int)IDXGISwapChainVtbl.Present]; + this.ResizeBuffers = this.dxgiSwapChainVTblAddresses[(int)IDXGISwapChainVtbl.ResizeBuffers]; } private static SwapChainDescription CreateSwapChainDescription(IntPtr renderForm) @@ -75,12 +75,12 @@ namespace Dalamud.Game.Internal.DXGI }; } - private List GetVTblAddresses(IntPtr pointer, int numberOfMethods) + private static List GetVTblAddresses(IntPtr pointer, int numberOfMethods) { - return this.GetVTblAddresses(pointer, 0, numberOfMethods); + return GetVTblAddresses(pointer, 0, numberOfMethods); } - private List GetVTblAddresses(IntPtr pointer, int startIndex, int numberOfMethods) + private static List GetVTblAddresses(IntPtr pointer, int startIndex, int numberOfMethods) { var vtblAddresses = new List(); var vTable = Marshal.ReadIntPtr(pointer); diff --git a/Dalamud/Game/Internal/Framework.cs b/Dalamud/Game/Internal/Framework.cs index 7d13318f6..abe3272e0 100644 --- a/Dalamud/Game/Internal/Framework.cs +++ b/Dalamud/Game/Internal/Framework.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; +using System.Threading; using Dalamud.Game.Internal.Gui; using Dalamud.Game.Internal.Libc; @@ -29,13 +30,13 @@ namespace Dalamud.Game.Internal /// /// The SigScanner instance. /// The Dalamud instance. - public Framework(SigScanner scanner, Dalamud dalamud) + internal Framework(SigScanner scanner, Dalamud dalamud) { this.dalamud = dalamud; this.Address = new FrameworkAddressResolver(); this.Address.Setup(scanner); - Log.Verbose("Framework address {FrameworkAddress}", this.Address.BaseAddress); + Log.Verbose($"Framework address 0x{this.Address.BaseAddress.ToInt64():X}"); if (this.Address.BaseAddress == IntPtr.Zero) { throw new InvalidOperationException("Framework is not initalized yet."); @@ -143,6 +144,11 @@ namespace Dalamud.Game.Internal this.Gui.Dispose(); this.Network.Dispose(); + this.updateHook.Disable(); + this.destroyHook.Disable(); + this.realDestroyHook.Disable(); + Thread.Sleep(500); + this.updateHook.Dispose(); this.destroyHook.Dispose(); this.realDestroyHook.Dispose(); diff --git a/Dalamud/Game/Internal/FrameworkAddressResolver.cs b/Dalamud/Game/Internal/FrameworkAddressResolver.cs index 551e779e5..1af2e9263 100644 --- a/Dalamud/Game/Internal/FrameworkAddressResolver.cs +++ b/Dalamud/Game/Internal/FrameworkAddressResolver.cs @@ -44,7 +44,7 @@ namespace Dalamud.Game.Internal // 00007FF701AD666A | 48 8D ?? ?? ?? 00 00 | LEA RCX,QWORD PTR DS:[RBX + 2C38] // 00007FF701AD6671 | E8 ?? ?? ?? ?? | CALL ffxiv_dx11.7FF701E2A7D0 // 00007FF701AD6676 | 48 8D ?? ?? ?? ?? ?? | LEA RAX,QWORD PTR DS:[7FF702C31F80 - var fwDtor = scanner.ScanText("48C705????????00000000 E8???????? 488D??????0000 E8???????? 488D"); + var fwDtor = scanner.ScanText("48 C7 05 ?? ?? ?? ?? 00 00 00 00 E8 ?? ?? ?? ?? 48 8D ?? ?? ?? 00 00 E8 ?? ?? ?? ?? 48 8D"); var fwOffset = Marshal.ReadInt32(fwDtor + 3); var pFramework = scanner.ResolveRelativeAddress(fwDtor + 11, fwOffset); diff --git a/Dalamud/Game/Internal/Gui/ChatGui.cs b/Dalamud/Game/Internal/Gui/ChatGui.cs index 38d7a2783..cd4220e0d 100644 --- a/Dalamud/Game/Internal/Gui/ChatGui.cs +++ b/Dalamud/Game/Internal/Gui/ChatGui.cs @@ -36,14 +36,14 @@ namespace Dalamud.Game.Internal.Gui /// The base address of the ChatManager. /// The SigScanner instance. /// The Dalamud instance. - public ChatGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud) + internal ChatGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud) { this.dalamud = dalamud; this.address = new ChatGuiAddressResolver(baseAddress); this.address.Setup(scanner); - Log.Verbose("Chat manager address {ChatManager}", this.address.BaseAddress); + Log.Verbose($"Chat manager address 0x{this.address.BaseAddress.ToInt64():X}"); this.printMessageHook = new Hook(this.address.PrintMessage, this.HandlePrintMessageDetour); this.populateItemLinkHook = new Hook(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour); @@ -252,7 +252,7 @@ namespace Dalamud.Game.Internal.Gui var senderRaw = Encoding.UTF8.GetBytes(chat.Name ?? string.Empty); using var senderOwned = framework.Libc.NewString(senderRaw); - var messageRaw = chat.MessageBytes ?? new byte[0]; + var messageRaw = chat.MessageBytes ?? Array.Empty(); using var messageOwned = framework.Libc.NewString(messageRaw); this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters); diff --git a/Dalamud/Game/Internal/Gui/ChatGuiAddressResolver.cs b/Dalamud/Game/Internal/Gui/ChatGuiAddressResolver.cs index dfc6ed0f3..067558e12 100644 --- a/Dalamud/Game/Internal/Gui/ChatGuiAddressResolver.cs +++ b/Dalamud/Game/Internal/Gui/ChatGuiAddressResolver.cs @@ -10,10 +10,10 @@ namespace Dalamud.Game.Internal.Gui /// /// Initializes a new instance of the class. /// - /// The base address of the native ChatManager class. - public ChatGuiAddressResolver(IntPtr baseAddres) + /// The base address of the native ChatManager class. + public ChatGuiAddressResolver(IntPtr baseAddress) { - this.BaseAddress = baseAddres; + this.BaseAddress = baseAddress; } /// diff --git a/Dalamud/Game/Internal/Gui/GameGui.cs b/Dalamud/Game/Internal/Gui/GameGui.cs index be018f1e1..f27f75662 100644 --- a/Dalamud/Game/Internal/Gui/GameGui.cs +++ b/Dalamud/Game/Internal/Gui/GameGui.cs @@ -47,7 +47,7 @@ namespace Dalamud.Game.Internal.Gui /// The base address of the native GuiManager class. /// The SigScanner instance. /// The Dalamud instance. - public GameGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud) + internal GameGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud) { this.dalamud = dalamud; @@ -56,12 +56,12 @@ namespace Dalamud.Game.Internal.Gui Log.Verbose("===== G A M E G U I ====="); - Log.Verbose("GameGuiManager address {Address:X}", this.address.BaseAddress.ToInt64()); - Log.Verbose("SetGlobalBgm address {Address:X}", this.address.SetGlobalBgm.ToInt64()); - Log.Verbose("HandleItemHover address {Address:X}", this.address.HandleItemHover.ToInt64()); - Log.Verbose("HandleItemOut address {Address:X}", this.address.HandleItemOut.ToInt64()); - Log.Verbose("GetUIObject address {Address:X}", this.address.GetUIObject.ToInt64()); - Log.Verbose("GetAgentModule address {Address:X}", this.address.GetAgentModule.ToInt64()); + Log.Verbose($"GameGuiManager address 0x{this.address.BaseAddress.ToInt64():X}"); + Log.Verbose($"SetGlobalBgm address 0x{this.address.SetGlobalBgm.ToInt64():X}"); + Log.Verbose($"HandleItemHover address 0x{this.address.HandleItemHover.ToInt64():X}"); + Log.Verbose($"HandleItemOut address 0x{this.address.HandleItemOut.ToInt64():X}"); + Log.Verbose($"GetUIObject address 0x{this.address.GetUIObject.ToInt64():X}"); + Log.Verbose($"GetAgentModule address 0x{this.address.GetAgentModule.ToInt64():X}"); this.Chat = new ChatGui(this.address.ChatManager, scanner, dalamud); this.PartyFinder = new PartyFinderGui(scanner, dalamud); diff --git a/Dalamud/Game/Internal/Gui/PartyFinderGui.cs b/Dalamud/Game/Internal/Gui/PartyFinderGui.cs index bdc5eee96..24079cf4e 100755 --- a/Dalamud/Game/Internal/Gui/PartyFinderGui.cs +++ b/Dalamud/Game/Internal/Gui/PartyFinderGui.cs @@ -23,7 +23,7 @@ namespace Dalamud.Game.Internal.Gui /// /// The SigScanner instance. /// The Dalamud instance. - public PartyFinderGui(SigScanner scanner, Dalamud dalamud) + internal PartyFinderGui(SigScanner scanner, Dalamud dalamud) { this.dalamud = dalamud; diff --git a/Dalamud/Game/Internal/Gui/Structs/Addon.cs b/Dalamud/Game/Internal/Gui/Structs/Addon.cs index be1f08f33..57595a2c7 100644 --- a/Dalamud/Game/Internal/Gui/Structs/Addon.cs +++ b/Dalamud/Game/Internal/Gui/Structs/Addon.cs @@ -1,5 +1,7 @@ using System.Runtime.InteropServices; +using FFXIVClientStructs.FFXIV.Component.GUI; + namespace Dalamud.Game.Internal.Gui.Structs { /// diff --git a/Dalamud/Game/Internal/Gui/Structs/AtkResNode.cs b/Dalamud/Game/Internal/Gui/Structs/AtkResNode.cs deleted file mode 100644 index 406a1262a..000000000 --- a/Dalamud/Game/Internal/Gui/Structs/AtkResNode.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Dalamud.Game.Internal.Gui.Structs -{ - /// - /// Native memory representation of a UI resource node. - /// - /// - /// This is copied from https://github.com/aers/FFXIVClientStructs/blob/main/Component/GUI/AtkResNode.cs. - /// If you need newer features, include FFXIVClientStructs and ILMerge the assembly. - /// - [StructLayout(LayoutKind.Explicit, Size = 0xA8)] - public unsafe struct AtkResNode - { - [FieldOffset(0x0)] - public IntPtr AtkEventTarget; - - [FieldOffset(0x8)] - public uint NodeID; - - [FieldOffset(0x20)] - public AtkResNode* ParentNode; - - [FieldOffset(0x28)] - public AtkResNode* PrevSiblingNode; - - [FieldOffset(0x30)] - public AtkResNode* NextSiblingNode; - - [FieldOffset(0x38)] - public AtkResNode* ChildNode; - - [FieldOffset(0x40)] - public ushort Type; - - [FieldOffset(0x42)] - public ushort ChildCount; - - [FieldOffset(0x44)] - public float X; - - [FieldOffset(0x48)] - public float Y; - - [FieldOffset(0x4C)] - public float ScaleX; - - [FieldOffset(0x50)] - public float ScaleY; - - [FieldOffset(0x54)] - public float Rotation; - - [FieldOffset(0x58)] - public fixed float UnkMatrix[3 * 2]; - - [FieldOffset(0x70)] - public uint Color; - - [FieldOffset(0x74)] - public float Depth; - - [FieldOffset(0x78)] - public float Depth_2; - - [FieldOffset(0x7C)] - public ushort AddRed; - - [FieldOffset(0x7E)] - public ushort AddGreen; - - [FieldOffset(0x80)] - public ushort AddBlue; - - [FieldOffset(0x82)] - public ushort AddRed_2; - - [FieldOffset(0x84)] - public ushort AddGreen_2; - - [FieldOffset(0x86)] - public ushort AddBlue_2; - - [FieldOffset(0x88)] - public byte MultiplyRed; - - [FieldOffset(0x89)] - public byte MultiplyGreen; - - [FieldOffset(0x8A)] - public byte MultiplyBlue; - - [FieldOffset(0x8B)] - public byte MultiplyRed_2; - - [FieldOffset(0x8C)] - public byte MultiplyGreen_2; - - [FieldOffset(0x8D)] - public byte MultiplyBlue_2; - - [FieldOffset(0x8E)] - public byte Alpha_2; - - [FieldOffset(0x8F)] - public byte UnkByte_1; - - [FieldOffset(0x90)] - public ushort Width; - - [FieldOffset(0x92)] - public ushort Height; - - [FieldOffset(0x94)] - public float OriginX; - - [FieldOffset(0x98)] - public float OriginY; - - [FieldOffset(0x9C)] - public ushort Priority; - - [FieldOffset(0x9E)] - public short Flags; - - [FieldOffset(0xA0)] - public uint Flags_2; - } -} diff --git a/Dalamud/Game/Internal/Gui/ToastGui.cs b/Dalamud/Game/Internal/Gui/ToastGui.cs index bb4883e7a..4c1ce497f 100755 --- a/Dalamud/Game/Internal/Gui/ToastGui.cs +++ b/Dalamud/Game/Internal/Gui/ToastGui.cs @@ -31,7 +31,7 @@ namespace Dalamud.Game.Internal.Gui /// /// The SigScanner instance. /// The Dalamud instance. - public ToastGui(SigScanner scanner, Dalamud dalamud) + internal ToastGui(SigScanner scanner, Dalamud dalamud) { this.dalamud = dalamud; diff --git a/Dalamud/Game/Internal/Network/GameNetwork.cs b/Dalamud/Game/Internal/Network/GameNetwork.cs index cb88c9dfc..947a4651c 100644 --- a/Dalamud/Game/Internal/Network/GameNetwork.cs +++ b/Dalamud/Game/Internal/Network/GameNetwork.cs @@ -34,8 +34,8 @@ namespace Dalamud.Game.Internal.Network this.address.Setup(scanner); Log.Verbose("===== G A M E N E T W O R K ====="); - Log.Verbose("ProcessZonePacketDown address {ProcessZonePacketDown}", this.address.ProcessZonePacketDown); - Log.Verbose("ProcessZonePacketUp address {ProcessZonePacketUp}", this.address.ProcessZonePacketUp); + Log.Verbose($"ProcessZonePacketDown address 0x{this.address.ProcessZonePacketDown.ToInt64():X}"); + Log.Verbose($"ProcessZonePacketUp address 0x{this.address.ProcessZonePacketUp.ToInt64():X}"); this.processZonePacketDownHook = new Hook(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour); this.processZonePacketUpHook = new Hook(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour); diff --git a/Dalamud/Game/Internal/Resource/ResourceManager.cs b/Dalamud/Game/Internal/Resource/ResourceManager.cs index 1e80475c1..7e3c2b045 100644 --- a/Dalamud/Game/Internal/Resource/ResourceManager.cs +++ b/Dalamud/Game/Internal/Resource/ResourceManager.cs @@ -25,7 +25,7 @@ namespace Dalamud.Game.Internal.File /// /// The Dalamud instance. /// The SigScanner instance. - public ResourceManager(Dalamud dalamud, SigScanner scanner) + internal ResourceManager(Dalamud dalamud, SigScanner scanner) { this.dalamud = dalamud; this.address = new ResourceManagerAddressResolver(); diff --git a/Dalamud/Game/Network/NetworkHandlers.cs b/Dalamud/Game/Network/NetworkHandlers.cs index 790ed57d5..d5fa25adb 100644 --- a/Dalamud/Game/Network/NetworkHandlers.cs +++ b/Dalamud/Game/Network/NetworkHandlers.cs @@ -31,7 +31,7 @@ namespace Dalamud.Game.Network /// /// The Dalamud instance. /// Whether the client should opt out of marketboard uploads. - public NetworkHandlers(Dalamud dalamud, bool optOutMbUploads) + internal NetworkHandlers(Dalamud dalamud, bool optOutMbUploads) { this.dalamud = dalamud; this.optOutMbUploads = optOutMbUploads; @@ -84,11 +84,11 @@ namespace Dalamud.Game.Network { var flashInfo = new NativeFunctions.FlashWindowInfo { - cbSize = (uint)Marshal.SizeOf(), - uCount = uint.MaxValue, - dwTimeout = 0, - dwFlags = NativeFunctions.FlashWindow.All | NativeFunctions.FlashWindow.TimerNoFG, - hwnd = Process.GetCurrentProcess().MainWindowHandle, + Size = (uint)Marshal.SizeOf(), + Count = uint.MaxValue, + Timeout = 0, + Flags = NativeFunctions.FlashWindow.All | NativeFunctions.FlashWindow.TimerNoFG, + Hwnd = Process.GetCurrentProcess().MainWindowHandle, }; NativeFunctions.FlashWindowEx(ref flashInfo); } diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index ec1608bec..56d0003e3 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -34,8 +34,8 @@ namespace Dalamud.Game if (this.IsCopy) this.SetupCopiedSegments(); - Log.Verbose("Module base: {Address}", this.TextSectionBase); - Log.Verbose("Module size: {Size}", this.TextSectionSize); + Log.Verbose($"Module base: 0x{this.TextSectionBase.ToInt64():X}"); + Log.Verbose($"Module size: 0x{this.TextSectionSize:X}"); } /// @@ -206,7 +206,7 @@ namespace Dalamud.Game var insnByte = Marshal.ReadByte(scanRet); if (insnByte == 0xE8 || insnByte == 0xE9) - return ReadCallSig(scanRet); + return ReadJmpCallSig(scanRet); return scanRet; } @@ -220,13 +220,13 @@ namespace Dalamud.Game } /// - /// Helper for ScanText to get the correct address for IDA sigs that mark the first CALL location. + /// Helper for ScanText to get the correct address for IDA sigs that mark the first JMP or CALL location. /// - /// The address the CALL sig resolved to. + /// The address the JMP or CALL sig resolved to. /// The real offset of the signature. - private static IntPtr ReadCallSig(IntPtr sigLocation) + private static IntPtr ReadJmpCallSig(IntPtr sigLocation) { - var jumpOffset = Marshal.ReadInt32(IntPtr.Add(sigLocation, 1)); + var jumpOffset = Marshal.ReadInt32(sigLocation, 1); return IntPtr.Add(sigLocation, 5 + jumpOffset); } @@ -235,6 +235,7 @@ namespace Dalamud.Game signature = signature.Replace(" ", string.Empty); if (signature.Length % 2 != 0) throw new ArgumentException("Signature without whitespaces must be divisible by two.", nameof(signature)); + var needleLength = signature.Length / 2; var needle = new byte[needleLength]; var mask = new bool[needleLength]; diff --git a/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs b/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs index 7b6f45e07..ad805f528 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs @@ -88,7 +88,7 @@ namespace Dalamud.Game.Text.SeStringHandling /// An SeString containing all the payloads necessary to display an item link in the chat log. public SeString CreateItemLink(Item item, bool isHQ, string displayNameOverride = null) { - return this.CreateItemLink((uint)item.RowId, isHQ, displayNameOverride ?? item.Name); + return this.CreateItemLink(item.RowId, isHQ, displayNameOverride ?? item.Name); } /// diff --git a/Dalamud/GlobalSuppressions.cs b/Dalamud/GlobalSuppressions.cs index e4f2f5e86..73bbf6329 100644 --- a/Dalamud/GlobalSuppressions.cs +++ b/Dalamud/GlobalSuppressions.cs @@ -18,26 +18,15 @@ using System.Diagnostics.CodeAnalysis; // Extensions [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group extensions with the relevant class", Scope = "type", Target = "~T:Dalamud.Interface.FontAwesomeExtensions")] [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Enum followed by an extension", Scope = "type", Target = "~T:Dalamud.Interface.FontAwesomeExtensions")] -[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group extensions with the relevant class", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.JobFlagsExtensions")] -[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Enum followed by an extension", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.JobFlagsExtensions")] +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group extensions with the relevant class", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.JobFlagsExt")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Enum followed by an extension", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.JobFlagsExt")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group extensions with the relevant class", Scope = "type", Target = "~T:Dalamud.Game.Text.XivChatTypeExtensions")] [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Enum followed by an extension", Scope = "type", Target = "~T:Dalamud.Game.Text.XivChatTypeExtensions")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group attributes with the relevant class", Scope = "type", Target = "~T:Dalamud.Game.Text.XivChatTypeInfoAttribute")] -// NativeFunctions.cs -[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Default Microsoft naming", Scope = "type", Target = "~T:Dalamud.NativeFunctions.FlashWindowInfo")] - -// EntryPoint.cs -[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Required by EasyHook", Scope = "member", Target = "~M:Dalamud.EntryPoint.#ctor(EasyHook.RemoteHooking.IContext,Dalamud.DalamudStartInfo)")] -[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Required by EasyHook", Scope = "member", Target = "~M:Dalamud.EntryPoint.Run(EasyHook.RemoteHooking.IContext,Dalamud.DalamudStartInfo)")] - // DalamudStartInfo.cs [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Interop", Scope = "type", Target = "~T:Dalamud.DalamudStartInfo")] -// AtkResNode.cs -[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Inherit documentation or lack thereof from FFXIVClientStructs", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.AtkResNode")] -[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Inherit naming from FFXIVClientStructs", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.AtkResNode")] - // PartyFinder [assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "Explicit struct layout", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.PartyFinder.Packet")] [assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "Explicit struct layout", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.PartyFinder.Listing")] @@ -86,9 +75,7 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.GamepadState")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.Condition")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.Targets")] -[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Interface.InterfaceManager.LastImGuiIoPtr")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "type", Target = "~T:Dalamud.Game.ClientState.Actors.Types.PartyMember")] -[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Interface.InterfaceManager.OnBuildFonts")] [assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.Actors.Types.Actor.Address")] [assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.Structs.JobGauge.BLMGauge.NumUmbralHearts")] [assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.Structs.JobGauge.DNCGauge.NumCompleteSteps")] @@ -97,10 +84,12 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Text.SeStringHandling.Payload.END_BYTE")] [assembly: SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "Unused, but eventually, maybe.", Scope = "member", Target = "~F:Dalamud.Game.ClientState.PartyList.address")] [assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "breaking api change", Scope = "member", Target = "~E:Dalamud.Game.ClientState.ClientState.CfPop")] -[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Framework.StatsHistory")] -[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Framework.StatsHistory")] [assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204:Static elements should appear before instance elements", Justification = "breaking api change, move to util", Scope = "type", Target = "~T:Dalamud.Game.Text.EnumExtensions")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "breaking api change, move to util", Scope = "type", Target = "~T:Dalamud.Game.Text.EnumExtensions")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Framework.StatsHistory")] +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Framework.StatsHistory")] +[assembly: SuppressMessage("Usage", "CA2211:Non-constant fields should not be visible", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Framework.StatsHistory")] +[assembly: SuppressMessage("Usage", "CA2208:Instantiate argument exceptions correctly", Justification = "Appears to be a bug, it is being used correctly", Scope = "member", Target = "~M:Dalamud.Data.DataManager.Initialize(System.String)")] // I mostly didnt care to do these. [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Network.GameNetwork.OnNetworkMessage")] diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index f709f2a63..90c611844 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -3,7 +3,8 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using EasyHook; +using CoreHook; +using Dalamud.Hooking.Internal; namespace Dalamud.Hooking { @@ -30,8 +31,8 @@ namespace Dalamud.Hooking { this.hookInfo = LocalHook.Create(address, detour, null); // Installs a hook here this.address = address; - this.original = Marshal.GetDelegateForFunctionPointer(this.hookInfo.HookBypassAddress); - HookInfo.TrackedHooks.Add(new HookInfo() { Delegate = detour, Hook = this, Assembly = Assembly.GetCallingAssembly() }); + this.original = Marshal.GetDelegateForFunctionPointer(this.hookInfo.OriginalAddress); + HookManager.TrackedHooks.Add(new HookInfo() { Delegate = detour, Hook = this, Assembly = Assembly.GetCallingAssembly() }); } /// @@ -130,9 +131,8 @@ namespace Dalamud.Hooking return; } - this.hookInfo.Dispose(); - this.IsDisposed = true; + this.hookInfo.Dispose(); } /// diff --git a/Dalamud/Hooking/HookInfo.cs b/Dalamud/Hooking/Internal/HookInfo.cs similarity index 89% rename from Dalamud/Hooking/HookInfo.cs rename to Dalamud/Hooking/Internal/HookInfo.cs index 85a815c0d..a702fed27 100644 --- a/Dalamud/Hooking/HookInfo.cs +++ b/Dalamud/Hooking/Internal/HookInfo.cs @@ -3,18 +3,13 @@ using System.Collections.Generic; using System.Diagnostics; using System.Reflection; -namespace Dalamud.Hooking +namespace Dalamud.Hooking.Internal { /// /// Class containing information about registered hooks. /// internal class HookInfo { - /// - /// Static list of tracked and registered hooks. - /// - internal static readonly List TrackedHooks = new(); - private ulong? inProcessMemory = 0; /// diff --git a/Dalamud/Hooking/Internal/HookManager.cs b/Dalamud/Hooking/Internal/HookManager.cs new file mode 100644 index 000000000..1651c696e --- /dev/null +++ b/Dalamud/Hooking/Internal/HookManager.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; + +namespace Dalamud.Hooking.Internal +{ + /// + /// This class manages the final disposition of hooks, cleaning up any that have not reverted their changes. + /// + internal class HookManager : IDisposable + { + // private readonly Dalamud dalamud; + + /// + /// Initializes a new instance of the class. + /// + /// Dalamud instance. + public HookManager(Dalamud dalamud) + { + _ = dalamud; + // this.dalamud = dalamud; + } + + /// + /// Gets a static list of tracked and registered hooks. + /// + internal static List TrackedHooks { get; } = new(); + + /// + public void Dispose() + { + } + } +} diff --git a/Dalamud/Hooking/IDalamudHook.cs b/Dalamud/Hooking/Internal/IDalamudHook.cs similarity index 94% rename from Dalamud/Hooking/IDalamudHook.cs rename to Dalamud/Hooking/Internal/IDalamudHook.cs index d33fa47b8..8fb95195f 100644 --- a/Dalamud/Hooking/IDalamudHook.cs +++ b/Dalamud/Hooking/Internal/IDalamudHook.cs @@ -1,6 +1,6 @@ using System; -namespace Dalamud.Hooking +namespace Dalamud.Hooking.Internal { /// /// Interface describing a generic hook. diff --git a/Dalamud/Interface/Colors/ColorDemoWindow.cs b/Dalamud/Interface/Colors/ColorDemoWindow.cs deleted file mode 100644 index 4dc4e30c9..000000000 --- a/Dalamud/Interface/Colors/ColorDemoWindow.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Numerics; - -using Dalamud.Interface.Windowing; -using ImGuiNET; - -namespace Dalamud.Interface.Colors -{ - /// - /// color Demo Window to view custom ImGui colors. - /// - internal class ColorDemoWindow : Window - { - private readonly List> colors; - - /// - /// Initializes a new instance of the class. - /// - public ColorDemoWindow() - : base("Dalamud Colors Demo") - { - this.Size = new Vector2(600, 500); - this.SizeCondition = ImGuiCond.FirstUseEver; - this.colors = new List> - { - Demo("White", ImGuiColors.White), - Demo("DalamudRed", ImGuiColors.DalamudRed), - Demo("DalamudGrey", ImGuiColors.DalamudGrey), - Demo("DalamudGrey2", ImGuiColors.DalamudGrey2), - Demo("DalamudGrey3", ImGuiColors.DalamudGrey3), - Demo("DalamudWhite", ImGuiColors.DalamudWhite), - Demo("DalamudWhite2", ImGuiColors.DalamudWhite2), - Demo("DalamudOrange", ImGuiColors.DalamudOrange), - Demo("TankBlue", ImGuiColors.TankBlue), - Demo("HealerGreen", ImGuiColors.HealerGreen), - Demo("DPSRed", ImGuiColors.DPSRed), - }; - this.colors = this.colors.OrderBy(colorDemo => colorDemo.Key).ToList(); - } - - /// - public override void Draw() - { - ImGui.BeginChild("color_scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.AlwaysVerticalScrollbar | ImGuiWindowFlags.HorizontalScrollbar); - ImGui.Text("This is a collection of UI colors you can use in your plugin."); - ImGui.Separator(); - foreach (var color in this.colors) - { - ImGui.TextColored(color.Value, color.Key); - } - - ImGui.EndChild(); - } - - private static KeyValuePair Demo(string name, Vector4 color) - { - return new KeyValuePair(name, color); - } - } -} diff --git a/Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs b/Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs new file mode 100644 index 000000000..225b171bb --- /dev/null +++ b/Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs @@ -0,0 +1,76 @@ +using System.Numerics; + +using ImGuiNET; + +namespace Dalamud.Interface.Components +{ + /// + /// Class containing various methods providing ImGui components. + /// + public static partial class ImGuiComponents + { + /// + /// Alpha modified IconButton component to use an icon as a button with alpha and color options. + /// + /// The icon for the button. + /// The ID of the button. + /// The default color of the button. + /// The color of the button when active. + /// The color of the button when hovered. + /// A multiplier for the current alpha levels. + /// Indicator if button is clicked. + public static bool DisabledButton(FontAwesomeIcon icon, int? id = null, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null, float alphaMult = .5f) + { + ImGui.PushFont(UiBuilder.IconFont); + + var text = icon.ToIconString(); + if (id.HasValue) + text = $"{text}{id}"; + + var button = DisabledButton(text, defaultColor, activeColor, hoveredColor, alphaMult); + + ImGui.PopFont(); + + return button; + } + + /// + /// Alpha modified Button component to use as a disabled button with alpha and color options. + /// + /// The button label with ID. + /// The default color of the button. + /// The color of the button when active. + /// The color of the button when hovered. + /// A multiplier for the current alpha levels. + /// Indicator if button is clicked. + public static bool DisabledButton(string labelWithId, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null, float alphaMult = .5f) + { + if (defaultColor.HasValue) + ImGui.PushStyleColor(ImGuiCol.Button, defaultColor.Value); + + if (activeColor.HasValue) + ImGui.PushStyleColor(ImGuiCol.ButtonActive, activeColor.Value); + + if (hoveredColor.HasValue) + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hoveredColor.Value); + + var style = ImGui.GetStyle(); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, style.Alpha * alphaMult); + + var button = ImGui.Button(labelWithId); + + ImGui.PopStyleVar(); + + if (defaultColor.HasValue) + ImGui.PopStyleColor(); + + if (activeColor.HasValue) + ImGui.PopStyleColor(); + + if (hoveredColor.HasValue) + ImGui.PopStyleColor(); + + return button; + } + } +} diff --git a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs index 3b6f8f213..3d65deb74 100644 --- a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs +++ b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs @@ -9,6 +9,14 @@ namespace Dalamud.Interface.Components /// public static partial class ImGuiComponents { + /// + /// IconButton component to use an icon as a button. + /// + /// The icon for the button. + /// Indicator if button is clicked. + public static bool IconButton(FontAwesomeIcon icon) + => IconButton(icon, null, null, null); + /// /// IconButton component to use an icon as a button. /// @@ -16,16 +24,26 @@ namespace Dalamud.Interface.Components /// The icon for the button. /// Indicator if button is clicked. public static bool IconButton(int id, FontAwesomeIcon icon) - { - ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero); - ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero); - ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero); - ImGui.PushFont(UiBuilder.IconFont); - var button = ImGui.Button($"{icon.ToIconString()}{id}"); - ImGui.PopFont(); - ImGui.PopStyleColor(3); - return button; - } + => IconButton(id, icon, null, null, null); + + /// + /// IconButton component to use an icon as a button. + /// + /// Text already containing the icon string. + /// Indicator if button is clicked. + public static bool IconButton(string iconText) + => IconButton(iconText, null, null, null); + + /// + /// IconButton component to use an icon as a button. + /// + /// The icon for the button. + /// The default color of the button. + /// The color of the button when active. + /// The color of the button when hovered. + /// Indicator if button is clicked. + public static bool IconButton(FontAwesomeIcon icon, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null) + => IconButton($"{icon.ToIconString()}", defaultColor, activeColor, hoveredColor); /// /// IconButton component to use an icon as a button with color options. @@ -36,15 +54,48 @@ namespace Dalamud.Interface.Components /// The color of the button when active. /// The color of the button when hovered. /// Indicator if button is clicked. - public static bool IconButton(int id, FontAwesomeIcon icon, Vector4 defaultColor, Vector4 activeColor, Vector4 hoveredColor) + public static bool IconButton(int id, FontAwesomeIcon icon, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null) + => IconButton($"{icon.ToIconString()}{id}", defaultColor, activeColor, hoveredColor); + + /// + /// IconButton component to use an icon as a button with color options. + /// + /// Text already containing the icon string. + /// The default color of the button. + /// The color of the button when active. + /// The color of the button when hovered. + /// Indicator if button is clicked. + public static bool IconButton(string iconText, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null) { - ImGui.PushStyleColor(ImGuiCol.Button, defaultColor); - ImGui.PushStyleColor(ImGuiCol.ButtonActive, activeColor); - ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hoveredColor); + var numColors = 0; + + if (defaultColor.HasValue) + { + ImGui.PushStyleColor(ImGuiCol.Button, defaultColor.Value); + numColors++; + } + + if (activeColor.HasValue) + { + ImGui.PushStyleColor(ImGuiCol.ButtonActive, activeColor.Value); + numColors++; + } + + if (hoveredColor.HasValue) + { + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hoveredColor.Value); + numColors++; + } + ImGui.PushFont(UiBuilder.IconFont); - var button = ImGui.Button($"{icon.ToIconString()}{id}"); + + var button = ImGui.Button(iconText); + ImGui.PopFont(); - ImGui.PopStyleColor(3); + + if (numColors > 0) + ImGui.PopStyleColor(numColors); + return button; } } diff --git a/Dalamud/Interface/Components/ImGuiComponents.TextWithLabel.cs b/Dalamud/Interface/Components/ImGuiComponents.TextWithLabel.cs index feb127d2a..991fefb3a 100644 --- a/Dalamud/Interface/Components/ImGuiComponents.TextWithLabel.cs +++ b/Dalamud/Interface/Components/ImGuiComponents.TextWithLabel.cs @@ -13,8 +13,7 @@ namespace Dalamud.Interface.Components /// The label for text. /// The text value. /// The hint to show on hover. - public static void TextWithLabel( - string label, string value, string hint = "") + public static void TextWithLabel(string label, string value, string hint = "") { ImGui.Text(label + ": "); ImGui.SameLine(); diff --git a/Dalamud/Interface/ImGuiHelpers.cs b/Dalamud/Interface/ImGuiHelpers.cs index d2ecda131..52c9802f9 100644 --- a/Dalamud/Interface/ImGuiHelpers.cs +++ b/Dalamud/Interface/ImGuiHelpers.cs @@ -20,6 +20,24 @@ namespace Dalamud.Interface /// public static float GlobalScale { get; private set; } + /// + /// Gets a that is pre-scaled with the multiplier. + /// + /// Vector2 X parameter. + /// Vector2 Y parameter. + /// A scaled Vector2. + public static Vector2 ScaledVector2(float x, float y) => new Vector2(x, y) * GlobalScale; + + /// + /// Gets a that is pre-scaled with the multiplier. + /// + /// Vector4 X parameter. + /// Vector4 Y parameter. + /// Vector4 Z parameter. + /// Vector4 W parameter. + /// A scaled Vector2. + public static Vector4 ScaledVector4(float x, float y, float z, float w) => new Vector4(x, y, z, w) * GlobalScale; + /// /// Force the next ImGui window to stay inside the main game window. /// @@ -29,7 +47,14 @@ namespace Dalamud.Interface /// Create a dummy scaled by the global Dalamud scale. /// /// The size of the dummy. - public static void ScaledDummy(float size) => ImGui.Dummy(new Vector2(size * GlobalScale, size * GlobalScale)); + public static void ScaledDummy(float size) => ScaledDummy(size, size); + + /// + /// Create a dummy scaled by the global Dalamud scale. + /// + /// Vector2 X parameter. + /// Vector2 Y parameter. + public static void ScaledDummy(float x, float y) => ScaledDummy(new Vector2(x, y)); /// /// Create a dummy scaled by the global Dalamud scale. diff --git a/Dalamud/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs similarity index 95% rename from Dalamud/DalamudCommands.cs rename to Dalamud/Interface/Internal/DalamudCommands.cs index 5b59f4a70..4353a3fee 100644 --- a/Dalamud/DalamudCommands.cs +++ b/Dalamud/Interface/Internal/DalamudCommands.cs @@ -7,7 +7,7 @@ using CheapLoc; using Dalamud.Game.Command; using Serilog; -namespace Dalamud +namespace Dalamud.Interface.Internal { /// /// Class handling Dalamud core commands. @@ -149,7 +149,7 @@ namespace Dalamud try { - this.dalamud.PluginManager.ReloadPlugins(); + this.dalamud.PluginManager.ReloadAllPlugins(); this.dalamud.Framework.Gui.Chat.Print("OK"); } @@ -226,20 +226,20 @@ namespace Dalamud private void OnDebugDrawDevMenu(string command, string arguments) { - this.dalamud.DalamudUi.IsDevMenu = !this.dalamud.DalamudUi.IsDevMenu; + this.dalamud.DalamudUi.ToggleDevMenu(); } private void OnDebugDrawDataMenu(string command, string arguments) { if (string.IsNullOrEmpty(arguments)) - this.dalamud.DalamudUi.ToggleData(); + this.dalamud.DalamudUi.ToggleDataWindow(); else - this.dalamud.DalamudUi.ToggleData(arguments); + this.dalamud.DalamudUi.ToggleDataWindow(arguments); } private void OnOpenLog(string command, string arguments) { - this.dalamud.DalamudUi.ToggleLog(); + this.dalamud.DalamudUi.ToggleLogWindow(); } private void OnDebugImInfoCommand(string command, string arguments) @@ -267,12 +267,12 @@ namespace Dalamud private void OnOpenInstallerCommand(string command, string arguments) { - this.dalamud.DalamudUi.TogglePluginInstaller(); + this.dalamud.DalamudUi.TogglePluginInstallerWindow(); } private void OnOpenCreditsCommand(string command, string arguments) { - this.dalamud.DalamudUi.ToggleCredits(); + this.dalamud.DalamudUi.ToggleCreditsWindow(); } private void OnSetLanguageCommand(string command, string arguments) @@ -299,7 +299,7 @@ namespace Dalamud private void OnOpenSettingsCommand(string command, string arguments) { - this.dalamud.DalamudUi.ToggleSettings(); + this.dalamud.DalamudUi.ToggleSettingsWindow(); } } } diff --git a/Dalamud/Interface/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs similarity index 60% rename from Dalamud/Interface/DalamudInterface.cs rename to Dalamud/Interface/Internal/DalamudInterface.cs index 09c15051a..16203edec 100644 --- a/Dalamud/Interface/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -1,152 +1,288 @@ using System; using System.Diagnostics; -using System.IO; using System.Linq; using System.Numerics; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using CheapLoc; -using Dalamud.Interface.Colors; -using Dalamud.Interface.Components; -using Dalamud.Interface.Scratchpad; +using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.Windowing; using Dalamud.Plugin; +using Dalamud.Plugin.Internal; using ImGuiNET; -using Serilog; using Serilog.Events; -namespace Dalamud.Interface +namespace Dalamud.Interface.Internal { /// - /// Class handling Dalamud core interface. + /// This plugin implements all of the Dalamud interface separately, to allow for reloading of the interface and rapid prototyping. /// internal class DalamudInterface : IDisposable { + private static readonly ModuleLog Log = new("DUI"); + private readonly Dalamud dalamud; + private readonly WindowSystem windowSystem; - private readonly DalamudLogWindow logWindow; - private readonly DalamudDataWindow dataWindow; - private readonly DalamudCreditsWindow creditsWindow; - private readonly DalamudSettingsWindow settingsWindow; - private readonly PluginInstallerWindow pluginWindow; - private readonly DalamudPluginStatWindow pluginStatWindow; - private readonly DalamudChangelogWindow changelogWindow; - private readonly ComponentDemoWindow componentDemoWindow; + private readonly ChangelogWindow changelogWindow; private readonly ColorDemoWindow colorDemoWindow; - private readonly ScratchpadWindow scratchpadWindow; + private readonly ComponentDemoWindow componentDemoWindow; + private readonly CreditsWindow creditsWindow; + private readonly DataWindow dataWindow; private readonly GamepadModeNotifierWindow gamepadModeNotifierWindow; - - private readonly WindowSystem windowSystem = new("DalamudCore"); + private readonly LogWindow logWindow; + private readonly PluginStatWindow pluginStatWindow; + private readonly PluginInstallerWindow pluginWindow; + private readonly ScratchpadWindow scratchpadWindow; + private readonly SettingsWindow settingsWindow; private ulong frameCount = 0; - private bool isImguiDrawDemoWindow = false; - #if DEBUG - private bool isImguiDrawDevMenu = true; + private bool isImGuiDrawDevMenu = true; #else - private bool isImguiDrawDevMenu = false; + private bool isImGuiDrawDevMenu = false; #endif + private bool isImGuiDrawDemoWindow = false; + /// /// Initializes a new instance of the class. /// - /// The Dalamud instance to register to. + /// The Dalamud instance. public DalamudInterface(Dalamud dalamud) { this.dalamud = dalamud; + this.windowSystem = new WindowSystem("DalamudCore"); - this.logWindow = new DalamudLogWindow(this.dalamud.CommandManager, this.dalamud.Configuration) - { - IsOpen = false, - }; - this.windowSystem.AddWindow(this.logWindow); + this.changelogWindow = new ChangelogWindow(dalamud) { IsOpen = false }; + this.colorDemoWindow = new ColorDemoWindow() { IsOpen = false }; + this.componentDemoWindow = new ComponentDemoWindow() { IsOpen = false }; + this.creditsWindow = new CreditsWindow(dalamud) { IsOpen = false }; + this.dataWindow = new DataWindow(dalamud) { IsOpen = false }; + this.gamepadModeNotifierWindow = new GamepadModeNotifierWindow(); + this.logWindow = new LogWindow(dalamud) { IsOpen = this.dalamud.Configuration.LogOpenAtStartup }; + this.pluginStatWindow = new PluginStatWindow(dalamud) { IsOpen = false }; + this.pluginWindow = new PluginInstallerWindow(dalamud) { IsOpen = false }; + this.scratchpadWindow = new ScratchpadWindow(dalamud) { IsOpen = false }; + this.settingsWindow = new SettingsWindow(dalamud) { IsOpen = false }; - this.dataWindow = new DalamudDataWindow(this.dalamud) - { - IsOpen = false, - }; - this.windowSystem.AddWindow(this.dataWindow); - - this.creditsWindow = new DalamudCreditsWindow(this.dalamud) - { - IsOpen = false, - }; + this.windowSystem.AddWindow(this.changelogWindow); + this.windowSystem.AddWindow(this.colorDemoWindow); + this.windowSystem.AddWindow(this.componentDemoWindow); this.windowSystem.AddWindow(this.creditsWindow); - - this.settingsWindow = new DalamudSettingsWindow(this.dalamud) - { - IsOpen = false, - }; + this.windowSystem.AddWindow(this.dataWindow); + this.windowSystem.AddWindow(this.gamepadModeNotifierWindow); + this.windowSystem.AddWindow(this.logWindow); + this.windowSystem.AddWindow(this.pluginStatWindow); + this.windowSystem.AddWindow(this.pluginWindow); + this.windowSystem.AddWindow(this.scratchpadWindow); this.windowSystem.AddWindow(this.settingsWindow); - this.pluginWindow = new PluginInstallerWindow(this.dalamud, this.dalamud.StartInfo.GameVersion) - { - IsOpen = false, - }; - this.windowSystem.AddWindow(this.pluginWindow); + this.dalamud.InterfaceManager.OnDraw += this.OnDraw; - this.pluginStatWindow = new DalamudPluginStatWindow(this.dalamud.PluginManager) - { - IsOpen = false, - }; - this.windowSystem.AddWindow(this.pluginStatWindow); - - this.changelogWindow = new DalamudChangelogWindow(this.dalamud) - { - IsOpen = false, - }; - this.windowSystem.AddWindow(this.changelogWindow); - - this.componentDemoWindow = new ComponentDemoWindow() - { - IsOpen = false, - }; - this.windowSystem.AddWindow(this.componentDemoWindow); - - this.colorDemoWindow = new ColorDemoWindow() - { - IsOpen = false, - }; - this.windowSystem.AddWindow(this.colorDemoWindow); - - this.scratchpadWindow = new ScratchpadWindow(this.dalamud) - { - IsOpen = false, - }; - this.windowSystem.AddWindow(this.scratchpadWindow); - - this.gamepadModeNotifierWindow = new GamepadModeNotifierWindow(); - this.windowSystem.AddWindow(this.gamepadModeNotifierWindow); - - Log.Information("[DUI] Windows added"); - - if (dalamud.Configuration.LogOpenAtStartup) - this.OpenLog(); + Log.Information("Windows added"); } /// - /// Gets or sets a value indicating whether the Dalamud dev menu is drawing. + /// Gets or sets a value indicating whether the /xldev menu is open. /// - public bool IsDevMenu + public bool IsDevMenuOpen { - get => this.isImguiDrawDevMenu; - set => this.isImguiDrawDevMenu = value; + get => this.isImGuiDrawDevMenu; + set => this.isImGuiDrawDevMenu = value; } /// - /// Draw the Dalamud core interface via ImGui. + /// Gets a value indicating whether the current Dalamud version warrants displaying the changelog. /// - public void Draw() + public bool WarrantsChangelog => ChangelogWindow.WarrantsChangelog; + + /// + public void Dispose() + { + this.dalamud.InterfaceManager.OnDraw -= this.OnDraw; + + this.windowSystem.RemoveAllWindows(); + + this.creditsWindow.Dispose(); + this.logWindow.Dispose(); + this.scratchpadWindow.Dispose(); + } + + #region Open + + /// + /// Opens the . + /// + public void OpenChangelogWindow() => this.changelogWindow.IsOpen = true; + + /// + /// Opens the . + /// + public void OpenColorsDemoWindow() => this.colorDemoWindow.IsOpen = true; + + /// + /// Opens the . + /// + public void OpenComponentDemoWindow() => this.componentDemoWindow.IsOpen = true; + + /// + /// Opens the . + /// + public void OpenCreditsWindow() => this.creditsWindow.IsOpen = true; + + /// + /// Opens the . + /// + /// The data kind to switch to after opening. + public void OpenDataWindow(string dataKind = null) + { + this.dataWindow.IsOpen = true; + if (dataKind != null && this.dataWindow.IsOpen) + { + this.dataWindow.SetDataKind(dataKind); + } + } + + /// + /// Opens the dev menu bar. + /// + public void OpenDevMenu() => this.isImGuiDrawDevMenu = true; + + /// + /// Opens the . + /// + public void OpenGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.IsOpen = true; + + /// + /// Opens the . + /// + public void OpenLogWindow() => this.logWindow.IsOpen = true; + + /// + /// Opens the . + /// + public void OpenPluginStats() => this.pluginStatWindow.IsOpen = true; + + /// + /// Opens the . + /// + public void OpenPluginInstaller() => this.pluginWindow.IsOpen = true; + + /// + /// Opens the . + /// + public void OpenScratchpadWindow() => this.scratchpadWindow.IsOpen = true; + + /// + /// Opens the . + /// + public void OpenSettings() => this.settingsWindow.IsOpen = true; + + #endregion + + #region Toggle + + /// + /// Toggles the . + /// + public void ToggleChangelogWindow() => this.changelogWindow.Toggle(); + + /// + /// Toggles the . + /// + public void ToggleColorsDemoWindow() => this.colorDemoWindow.Toggle(); + + /// + /// Toggles the . + /// + public void ToggleComponentDemoWindow() => this.componentDemoWindow.Toggle(); + + /// + /// Toggles the . + /// + public void ToggleCreditsWindow() => this.creditsWindow.Toggle(); + + /// + /// Toggles the . + /// + /// The data kind to switch to after opening. + public void ToggleDataWindow(string dataKind = null) + { + this.dataWindow.Toggle(); + if (dataKind != null && this.dataWindow.IsOpen) + { + this.dataWindow.SetDataKind(dataKind); + } + } + + /// + /// Toggles the dev menu bar. + /// + public void ToggleDevMenu() => this.isImGuiDrawDevMenu ^= true; + + /// + /// Toggles the . + /// + public void ToggleGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.Toggle(); + + /// + /// Toggles the . + /// + public void ToggleLogWindow() => this.logWindow.Toggle(); + + /// + /// Toggles the . + /// + public void TogglePluginStatsWindow() => this.pluginStatWindow.Toggle(); + + /// + /// Toggles the . + /// + public void TogglePluginInstallerWindow() => this.pluginWindow.Toggle(); + + /// + /// Toggles the . + /// + public void ToggleScratchpadWindow() => this.scratchpadWindow.Toggle(); + + /// + /// Toggles the . + /// + public void ToggleSettingsWindow() => this.settingsWindow.Toggle(); + + #endregion + + private void OnDraw() { this.frameCount++; - if (!this.IsDevMenu && !this.dalamud.ClientState.Condition.Any()) + try { - ImGui.PushStyleColor(ImGuiCol.Button, new Vector4(0, 0, 0, 0)); - ImGui.PushStyleColor(ImGuiCol.ButtonActive, new Vector4(0, 0, 0, 0)); - ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(0, 0, 0, 0)); + this.DrawHiddenDevMenuOpener(); + this.DrawDevMenu(); + + if (this.dalamud.Framework.Gui.GameUiHidden) + return; + + this.windowSystem.Draw(); + + if (this.isImGuiDrawDemoWindow) + ImGui.ShowDemoWindow(); + } + catch (Exception ex) + { + PluginLog.Error(ex, "Error during OnDraw"); + } + } + + private void DrawHiddenDevMenuOpener() + { + if (!this.isImGuiDrawDevMenu && !this.dalamud.ClientState.Condition.Any()) + { + ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero); ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0, 0, 0, 1)); ImGui.PushStyleColor(ImGuiCol.TextSelectedBg, new Vector4(0, 0, 0, 1)); ImGui.PushStyleColor(ImGuiCol.Border, new Vector4(0, 0, 0, 1)); @@ -160,27 +296,30 @@ namespace Dalamud.Interface if (ImGui.Begin("DevMenu Opener", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoSavedSettings)) { if (ImGui.Button("###devMenuOpener", new Vector2(40, 25))) - this.IsDevMenu = true; + this.isImGuiDrawDevMenu = true; ImGui.End(); } ImGui.PopStyleColor(8); } + } - if (this.IsDevMenu) + private void DrawDevMenu() + { + if (this.isImGuiDrawDevMenu) { if (ImGui.BeginMainMenuBar()) { if (ImGui.BeginMenu("Dalamud")) { - ImGui.MenuItem("Draw Dalamud dev menu", string.Empty, ref this.isImguiDrawDevMenu); + ImGui.MenuItem("Draw Dalamud dev menu", string.Empty, ref this.isImGuiDrawDevMenu); ImGui.Separator(); if (ImGui.MenuItem("Open Log window")) { - this.OpenLog(); + this.OpenLogWindow(); } if (ImGui.BeginMenu("Set log level...")) @@ -214,12 +353,12 @@ namespace Dalamud.Interface if (ImGui.MenuItem("Open Data window")) { - this.OpenData(); + this.OpenDataWindow(); } if (ImGui.MenuItem("Open Credits window")) { - this.OpenCredits(); + this.OpenCreditsWindow(); } if (ImGui.MenuItem("Open Settings window")) @@ -229,20 +368,20 @@ namespace Dalamud.Interface if (ImGui.MenuItem("Open Changelog window")) { - this.OpenChangelog(); + this.OpenChangelogWindow(); } if (ImGui.MenuItem("Open Components Demo")) { - this.OpenComponentDemo(); + this.OpenComponentDemoWindow(); } if (ImGui.MenuItem("Open Colors Demo")) { - this.OpenColorsDemo(); + this.OpenColorsDemoWindow(); } - ImGui.MenuItem("Draw ImGui demo", string.Empty, ref this.isImguiDrawDemoWindow); + ImGui.MenuItem("Draw ImGui demo", string.Empty, ref this.isImGuiDrawDemoWindow); ImGui.Separator(); @@ -264,12 +403,12 @@ namespace Dalamud.Interface ImGui.Separator(); if (ImGui.MenuItem("Enable Dalamud testing", string.Empty, this.dalamud.Configuration.DoDalamudTest)) { - this.dalamud.Configuration.DoDalamudTest = !this.dalamud.Configuration.DoDalamudTest; + this.dalamud.Configuration.DoDalamudTest ^= true; this.dalamud.Configuration.Save(); } ImGui.MenuItem(Util.AssemblyVersion, false); - ImGui.MenuItem(this.dalamud.StartInfo.GameVersion, false); + ImGui.MenuItem(this.dalamud.StartInfo.GameVersion.ToString(), false); ImGui.EndMenu(); } @@ -300,10 +439,10 @@ namespace Dalamud.Interface if (ImGui.MenuItem("Print plugin info")) { - foreach (var plugin in this.dalamud.PluginManager.Plugins) + foreach (var plugin in this.dalamud.PluginManager.InstalledPlugins) { // TODO: some more here, state maybe? - Log.Information($"{plugin.Plugin.Name}"); + PluginLog.Information($"{plugin.Name}"); } } @@ -311,18 +450,23 @@ namespace Dalamud.Interface { try { - this.dalamud.PluginManager.ReloadPlugins(); + this.dalamud.PluginManager.ReloadAllPlugins(); } catch (Exception ex) { this.dalamud.Framework.Gui.Chat.PrintError("Reload failed."); - Log.Error(ex, "Plugin reload failed."); + PluginLog.Error(ex, "Plugin reload failed."); } } + if (ImGui.MenuItem("Scan dev plugins")) + { + this.dalamud.PluginManager.ScanDevPlugins(); + } + ImGui.Separator(); ImGui.MenuItem("API Level:" + PluginManager.DalamudApiLevel, false); - ImGui.MenuItem("Loaded plugins:" + this.dalamud.PluginManager?.Plugins.Count, false); + ImGui.MenuItem("Loaded plugins:" + this.dalamud.PluginManager?.InstalledPlugins.Count, false); ImGui.EndMenu(); } @@ -384,197 +528,6 @@ namespace Dalamud.Interface ImGui.EndMainMenuBar(); } } - - if (this.dalamud.Framework.Gui.GameUiHidden) - return; - - this.windowSystem.Draw(); - - if (this.isImguiDrawDemoWindow) - ImGui.ShowDemoWindow(); - } - - /// - /// Dispose the window system and all windows that require it. - /// - public void Dispose() - { - this.scratchpadWindow.Dispose(); - this.windowSystem.RemoveAllWindows(); - - this.logWindow?.Dispose(); - this.creditsWindow?.Dispose(); - } - - /// - /// Open the Plugin Installer window. - /// - internal void OpenPluginInstaller() - { - this.pluginWindow.IsOpen = true; - } - - /// - /// Open the changelog window. - /// - internal void OpenChangelog() - { - this.changelogWindow.IsOpen = true; - } - - /// - /// Open the settings window. - /// - internal void OpenSettings() - { - this.settingsWindow.IsOpen = true; - } - - /// - /// Open the log window. - /// - internal void OpenLog() - { - this.logWindow.IsOpen = true; - } - - /// - /// Open the data window. - /// - internal void OpenData() - { - this.dataWindow.IsOpen = true; - } - - /// - /// Open the credits window. - /// - internal void OpenCredits() - { - this.creditsWindow.IsOpen = true; - } - - /// - /// Open the stats window. - /// - internal void OpenPluginStats() - { - this.pluginStatWindow.IsOpen = true; - } - - /// - /// Open the component test window. - /// - internal void OpenComponentDemo() - { - this.componentDemoWindow.IsOpen = true; - } - - /// - /// Open the colors test window. - /// - internal void OpenColorsDemo() - { - this.colorDemoWindow.IsOpen = true; - } - - /// - /// Open the colors test window. - /// - internal void OpenScratchpadWindow() - { - this.scratchpadWindow.IsOpen = true; - } - - /// - /// Toggle the Plugin Installer window. - /// - internal void TogglePluginInstaller() - { - this.pluginWindow.IsOpen ^= true; - } - - /// - /// Toggle the changelog window. - /// - internal void ToggleChangelog() - { - this.changelogWindow.IsOpen ^= true; - } - - /// - /// Toggle the settings window. - /// - internal void ToggleSettings() - { - this.settingsWindow.IsOpen ^= true; - } - - /// - /// Toggle the log window. - /// - internal void ToggleLog() - { - this.logWindow.IsOpen ^= true; - } - - /// - /// Toggle the data window. - /// - internal void ToggleData() - { - this.dataWindow.IsOpen ^= true; - } - - /// - /// Toggle the data window and preset the dropdown. - /// - /// The data kind to toggle. - internal void ToggleData(string dataKind) - { - this.dataWindow.IsOpen ^= true; - if (this.dataWindow.IsOpen) - this.dataWindow.SetDataKind(dataKind); - } - - /// - /// Toggle the credits window. - /// - internal void ToggleCredits() - { - this.creditsWindow.IsOpen ^= true; - } - - /// - /// Toggle the stats window. - /// - internal void TogglePluginStats() - { - this.pluginStatWindow.IsOpen ^= true; - } - - /// - /// Toggle the component test window. - /// - internal void ToggleComponentDemo() - { - this.componentDemoWindow.IsOpen ^= true; - } - - /// - /// Toggle the scratchpad window. - /// - internal void ToggleScratchpadWindow() - { - this.scratchpadWindow.IsOpen ^= true; - } - - /// - /// Toggle the gamepad notifier window window. - /// - internal void ToggleGamePadNotifierWindow() - { - this.gamepadModeNotifierWindow.IsOpen ^= true; } } } diff --git a/Dalamud/Interface/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs similarity index 94% rename from Dalamud/Interface/InterfaceManager.cs rename to Dalamud/Interface/Internal/InterfaceManager.cs index fec66aa19..e4745887f 100644 --- a/Dalamud/Interface/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -10,7 +10,6 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; -using EasyHook; using ImGuiNET; using ImGuiScene; using Serilog; @@ -27,35 +26,24 @@ using SharpDX.Direct3D11; * - Might eventually want to render to a separate target and composite, especially with reshade etc in the mix. */ -namespace Dalamud.Interface +namespace Dalamud.Interface.Internal { /// /// This class manages interaction with the ImGui interface. /// internal class InterfaceManager : IDisposable { - /// - /// Code that is exexuted when fonts are rebuilt. - /// - public Action OnBuildFonts; - - /// - /// The pointer to ImGui.IO(), when it last used.. - /// - public ImGuiIOPtr LastImGuiIoPtr; - private readonly Dalamud dalamud; + private readonly string rtssPath; private readonly Hook presentHook; private readonly Hook resizeBuffersHook; private readonly Hook setCursorHook; - private ManualResetEvent fontBuildSignal; - private ISwapChainAddressResolver address; + private readonly ManualResetEvent fontBuildSignal; + private readonly ISwapChainAddressResolver address; private RawDX11Scene scene; - private string rtssPath; - // can't access imgui IO before first present call private bool lastWantCapture = false; private bool isRebuildingFonts = false; @@ -95,12 +83,12 @@ namespace Dalamud.Interface try { - var rtss = NativeFunctions.GetModuleHandle("RTSSHooks64.dll"); + var rtss = NativeFunctions.GetModuleHandleW("RTSSHooks64.dll"); if (rtss != IntPtr.Zero) { var fileName = new StringBuilder(255); - NativeFunctions.GetModuleFileName(rtss, fileName, fileName.Capacity); + _ = NativeFunctions.GetModuleFileNameW(rtss, fileName, fileName.Capacity); this.rtssPath = fileName.ToString(); Log.Verbose("RTSS at {0}", this.rtssPath); @@ -113,17 +101,16 @@ namespace Dalamud.Interface Log.Error(e, "RTSS Free failed"); } - var setCursorAddr = LocalHook.GetProcAddress("user32.dll", "SetCursor"); + var user32 = NativeFunctions.GetModuleHandleW("user32.dll"); + var setCursorAddr = NativeFunctions.GetProcAddress(user32, "SetCursor"); Log.Verbose("===== S W A P C H A I N ====="); - Log.Verbose("SetCursor address {SetCursor}", setCursorAddr); - Log.Verbose("Present address {Present}", this.address.Present); - Log.Verbose("ResizeBuffers address {ResizeBuffers}", this.address.ResizeBuffers); + Log.Verbose($"SetCursor address 0x{setCursorAddr.ToInt64():X}"); + Log.Verbose($"Present address 0x{this.address.Present.ToInt64():X}"); + Log.Verbose($"ResizeBuffers address 0x{this.address.ResizeBuffers.ToInt64():X}"); this.setCursorHook = new Hook(setCursorAddr, this.SetCursorDetour); - this.presentHook = new Hook(this.address.Present, this.PresentDetour); - this.resizeBuffersHook = new Hook(this.address.ResizeBuffers, this.ResizeBuffersDetour); } @@ -153,6 +140,16 @@ namespace Dalamud.Interface /// public static ImFontPtr IconFont { get; private set; } + /// + /// Gets or sets an action that is exexuted when fonts are rebuilt. + /// + public Action OnBuildFonts { get; set; } + + /// + /// Gets or sets the pointer to ImGui.IO(), when it was last used. + /// + public ImGuiIOPtr LastImGuiIoPtr { get; set; } + /// /// Gets the D3D11 device instance. /// @@ -195,11 +192,11 @@ namespace Dalamud.Interface { if (!string.IsNullOrEmpty(this.rtssPath)) { - NativeFunctions.LoadLibrary(this.rtssPath); + NativeFunctions.LoadLibraryW(this.rtssPath); + var rtssModule = NativeFunctions.GetModuleHandleW("RTSSHooks64.dll"); + var installAddr = NativeFunctions.GetProcAddress(rtssModule, "InstallRTSSHook"); - var installAddr = LocalHook.GetProcAddress("RTSSHooks64.dll", "InstallRTSSHook"); - var installDele = Marshal.GetDelegateForFunctionPointer(installAddr); - installDele.Invoke(); + Marshal.GetDelegateForFunctionPointer(installAddr).Invoke(); } } catch (Exception ex) @@ -567,7 +564,7 @@ namespace Dalamud.Interface { ImGui.GetIO().ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad; this.dalamud.ClientState.GamepadState.NavEnableGamepad ^= true; - this.dalamud.DalamudUi.ToggleGamePadNotifierWindow(); + this.dalamud.DalamudUi.ToggleGamepadModeNotifierWindow(); } if (gamepadEnabled @@ -592,7 +589,7 @@ namespace Dalamud.Interface if (this.dalamud.ClientState.GamepadState.Pressed(GamepadButtons.R3) > 0) { - this.dalamud.DalamudUi.TogglePluginInstaller(); + this.dalamud.DalamudUi.TogglePluginInstallerWindow(); } } } diff --git a/Dalamud/Interface/Scratchpad/ScratchExecutionManager.cs b/Dalamud/Interface/Internal/Scratchpad/ScratchExecutionManager.cs similarity index 94% rename from Dalamud/Interface/Scratchpad/ScratchExecutionManager.cs rename to Dalamud/Interface/Internal/Scratchpad/ScratchExecutionManager.cs index 07010bc84..8d0386662 100644 --- a/Dalamud/Interface/Scratchpad/ScratchExecutionManager.cs +++ b/Dalamud/Interface/Internal/Scratchpad/ScratchExecutionManager.cs @@ -9,7 +9,7 @@ using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting; using Serilog; -namespace Dalamud.Interface.Scratchpad +namespace Dalamud.Interface.Internal.Scratchpad { /// /// This class manages the execution of classes. @@ -83,9 +83,9 @@ namespace Dalamud.Interface.Scratchpad { var script = CSharpScript.Create(code, options); - var pi = new DalamudPluginInterface(this.dalamud, "Scratch-" + doc.Id, null, PluginLoadReason.Installer); - var plugin = script.ContinueWith("return new ScratchPlugin() as IDalamudPlugin;").RunAsync().GetAwaiter().GetResult() - .ReturnValue; + var pi = new DalamudPluginInterface(this.dalamud, "Scratch-" + doc.Id, null); + var plugin = script.ContinueWith("return new ScratchPlugin() as IDalamudPlugin;") + .RunAsync().GetAwaiter().GetResult().ReturnValue; plugin.Initialize(pi); diff --git a/Dalamud/Interface/Scratchpad/ScratchFileWatcher.cs b/Dalamud/Interface/Internal/Scratchpad/ScratchFileWatcher.cs similarity index 96% rename from Dalamud/Interface/Scratchpad/ScratchFileWatcher.cs rename to Dalamud/Interface/Internal/Scratchpad/ScratchFileWatcher.cs index 805ce11fe..88212a3f0 100644 --- a/Dalamud/Interface/Scratchpad/ScratchFileWatcher.cs +++ b/Dalamud/Interface/Internal/Scratchpad/ScratchFileWatcher.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.IO; -namespace Dalamud.Interface.Scratchpad +namespace Dalamud.Interface.Internal.Scratchpad { /// /// A file watcher for classes. diff --git a/Dalamud/Interface/Scratchpad/ScratchLoadStatus.cs b/Dalamud/Interface/Internal/Scratchpad/ScratchLoadStatus.cs similarity index 91% rename from Dalamud/Interface/Scratchpad/ScratchLoadStatus.cs rename to Dalamud/Interface/Internal/Scratchpad/ScratchLoadStatus.cs index b9398c0f5..46ac0566e 100644 --- a/Dalamud/Interface/Scratchpad/ScratchLoadStatus.cs +++ b/Dalamud/Interface/Internal/Scratchpad/ScratchLoadStatus.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Interface.Scratchpad +namespace Dalamud.Interface.Internal.Scratchpad { /// /// The load status of a class. diff --git a/Dalamud/Interface/Scratchpad/ScratchMacroProcessor.cs b/Dalamud/Interface/Internal/Scratchpad/ScratchMacroProcessor.cs similarity index 99% rename from Dalamud/Interface/Scratchpad/ScratchMacroProcessor.cs rename to Dalamud/Interface/Internal/Scratchpad/ScratchMacroProcessor.cs index f47c2cab2..ad20b493e 100644 --- a/Dalamud/Interface/Scratchpad/ScratchMacroProcessor.cs +++ b/Dalamud/Interface/Internal/Scratchpad/ScratchMacroProcessor.cs @@ -5,7 +5,7 @@ using System.Text.RegularExpressions; using Dalamud.Plugin; -namespace Dalamud.Interface.Scratchpad +namespace Dalamud.Interface.Internal.Scratchpad { /// /// This class converts ScratchPad macros into runnable scripts. diff --git a/Dalamud/Interface/Scratchpad/ScratchpadDocument.cs b/Dalamud/Interface/Internal/Scratchpad/ScratchpadDocument.cs similarity index 96% rename from Dalamud/Interface/Scratchpad/ScratchpadDocument.cs rename to Dalamud/Interface/Internal/Scratchpad/ScratchpadDocument.cs index 22c1e6b51..cd76a0135 100644 --- a/Dalamud/Interface/Scratchpad/ScratchpadDocument.cs +++ b/Dalamud/Interface/Internal/Scratchpad/ScratchpadDocument.cs @@ -1,6 +1,6 @@ using System; -namespace Dalamud.Interface.Scratchpad +namespace Dalamud.Interface.Internal.Scratchpad { /// /// This class represents a single document in the ScratchPad. diff --git a/Dalamud/Interface/SerilogEventSink.cs b/Dalamud/Interface/Internal/SerilogEventSink.cs similarity index 95% rename from Dalamud/Interface/SerilogEventSink.cs rename to Dalamud/Interface/Internal/SerilogEventSink.cs index 4810f75b8..bf4a0e5d2 100644 --- a/Dalamud/Interface/SerilogEventSink.cs +++ b/Dalamud/Interface/Internal/SerilogEventSink.cs @@ -3,7 +3,7 @@ using System; using Serilog.Core; using Serilog.Events; -namespace Dalamud.Interface +namespace Dalamud.Interface.Internal { /// /// Serilog event sink. @@ -41,7 +41,9 @@ namespace Dalamud.Interface var message = $"[{DateTimeOffset.Now:HH:mm:ss.fff}][{logEvent.Level}] {logEvent.RenderMessage(this.formatProvider)}"; if (logEvent.Exception != null) + { message += "\n" + logEvent.Exception; + } this.OnLogLine?.Invoke(this, (message, logEvent.Level)); } diff --git a/Dalamud/Interface/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs similarity index 99% rename from Dalamud/Interface/UiDebug.cs rename to Dalamud/Interface/Internal/UiDebug.cs index 16e2a75fc..3e7310633 100644 --- a/Dalamud/Interface/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -14,7 +14,7 @@ using AlignmentType = FFXIVClientStructs.FFXIV.Component.GUI.AlignmentType; // Customised version of https://github.com/aers/FFXIVUIDebug -namespace Dalamud.Interface +namespace Dalamud.Interface.Internal { /// /// This class displays a debug window to inspect native addons. diff --git a/Dalamud/Interface/DalamudChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs similarity index 74% rename from Dalamud/Interface/DalamudChangelogWindow.cs rename to Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 3d01bef83..69c2ade06 100644 --- a/Dalamud/Interface/DalamudChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -1,15 +1,14 @@ using System.Diagnostics; -using System.Numerics; using Dalamud.Interface.Windowing; using ImGuiNET; -namespace Dalamud.Interface +namespace Dalamud.Interface.Internal.Windows { /// /// For major updates, an in-game Changelog window. /// - internal class DalamudChangelogWindow : Window + internal sealed class ChangelogWindow : Window { /// /// Whether the latest update warrants a changelog window. @@ -23,13 +22,13 @@ namespace Dalamud.Interface If you note any issues or need help, please make sure to ask on our discord server."; private readonly Dalamud dalamud; - private string assemblyVersion = Util.AssemblyVersion; + private readonly string assemblyVersion = Util.AssemblyVersion; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The Dalamud instance. - public DalamudChangelogWindow(Dalamud dalamud) + public ChangelogWindow(Dalamud dalamud) : base("What's new in XIVLauncher?", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoResize) { this.dalamud = dalamud; @@ -44,57 +43,63 @@ If you note any issues or need help, please make sure to ask on our discord serv { ImGui.Text($"The in-game addon has been updated to version D{this.assemblyVersion}."); - ImGui.Dummy(new Vector2(10, 10) * ImGui.GetIO().FontGlobalScale); + ImGuiHelpers.ScaledDummy(10); ImGui.Text("The following changes were introduced:"); ImGui.Text(ChangeLog); - ImGui.Dummy(new Vector2(10, 10) * ImGui.GetIO().FontGlobalScale); + ImGuiHelpers.ScaledDummy(10); ImGui.Text("Thank you for using our tools!"); - ImGui.Dummy(new Vector2(10, 10) * ImGui.GetIO().FontGlobalScale); + ImGuiHelpers.ScaledDummy(10); - ImGui.PushFont(InterfaceManager.IconFont); + ImGui.PushFont(UiBuilder.IconFont); if (ImGui.Button(FontAwesomeIcon.Download.ToIconString())) + { this.dalamud.DalamudUi.OpenPluginInstaller(); + } if (ImGui.IsItemHovered()) { ImGui.PopFont(); ImGui.SetTooltip("Open Plugin Installer"); - ImGui.PushFont(InterfaceManager.IconFont); + ImGui.PushFont(UiBuilder.IconFont); } ImGui.SameLine(); if (ImGui.Button(FontAwesomeIcon.LaughBeam.ToIconString())) + { Process.Start("https://discord.gg/3NMcUV5"); + } if (ImGui.IsItemHovered()) { ImGui.PopFont(); ImGui.SetTooltip("Join our Discord server"); - ImGui.PushFont(InterfaceManager.IconFont); + ImGui.PushFont(UiBuilder.IconFont); } ImGui.SameLine(); if (ImGui.Button(FontAwesomeIcon.Globe.ToIconString())) + { Process.Start("https://github.com/goatcorp/FFXIVQuickLauncher"); + } if (ImGui.IsItemHovered()) { ImGui.PopFont(); ImGui.SetTooltip("See our GitHub repository"); - ImGui.PushFont(InterfaceManager.IconFont); + ImGui.PushFont(UiBuilder.IconFont); } ImGui.PopFont(); ImGui.SameLine(); - ImGui.Dummy(new Vector2(20, 0) * ImGui.GetIO().FontGlobalScale); + ImGuiHelpers.ScaledDummy(20, 0); ImGui.SameLine(); if (ImGui.Button("Close")) diff --git a/Dalamud/Interface/Internal/Windows/ColorDemoWindow.cs b/Dalamud/Interface/Internal/Windows/ColorDemoWindow.cs new file mode 100644 index 000000000..a1c1c1866 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/ColorDemoWindow.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +using Dalamud.Interface.Colors; +using Dalamud.Interface.Windowing; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows +{ + /// + /// Color Demo Window to view custom ImGui colors. + /// + internal sealed class ColorDemoWindow : Window + { + private readonly List<(string Name, Vector4 Color)> colors; + + /// + /// Initializes a new instance of the class. + /// + public ColorDemoWindow() + : base("Dalamud Colors Demo") + { + this.Size = new Vector2(600, 500); + this.SizeCondition = ImGuiCond.FirstUseEver; + + this.colors = new List<(string Name, Vector4 Color)>() + { + ("White", ImGuiColors.White), + ("DalamudRed", ImGuiColors.DalamudRed), + ("DalamudGrey", ImGuiColors.DalamudGrey), + ("DalamudGrey2", ImGuiColors.DalamudGrey2), + ("DalamudGrey3", ImGuiColors.DalamudGrey3), + ("DalamudWhite", ImGuiColors.DalamudWhite), + ("DalamudWhite2", ImGuiColors.DalamudWhite2), + ("DalamudOrange", ImGuiColors.DalamudOrange), + ("TankBlue", ImGuiColors.TankBlue), + ("HealerGreen", ImGuiColors.HealerGreen), + ("DPSRed", ImGuiColors.DPSRed), + }.OrderBy(colorDemo => colorDemo.Name).ToList(); + } + + /// + public override void Draw() + { + ImGui.Text("This is a collection of UI colors you can use in your plugin."); + + ImGui.Separator(); + + foreach (var (name, color) in this.colors) + { + ImGui.TextColored(color, name); + } + } + } +} diff --git a/Dalamud/Interface/Components/ComponentDemoWindow.cs b/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs similarity index 68% rename from Dalamud/Interface/Components/ComponentDemoWindow.cs rename to Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs index bb62fba66..814c7abc6 100644 --- a/Dalamud/Interface/Components/ComponentDemoWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs @@ -3,17 +3,18 @@ using System.Collections.Generic; using System.Numerics; using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; using Dalamud.Interface.Windowing; using ImGuiNET; -namespace Dalamud.Interface.Components +namespace Dalamud.Interface.Internal.Windows { /// /// Component Demo Window to view custom ImGui components. /// - internal class ComponentDemoWindow : Window + internal sealed class ComponentDemoWindow : Window { - private readonly List> componentDemos; + private readonly List<(string Name, Action Demo)> componentDemos; private Vector4 defaultColor = ImGuiColors.DalamudOrange; /// @@ -24,33 +25,31 @@ namespace Dalamud.Interface.Components { this.Size = new Vector2(600, 500); this.SizeCondition = ImGuiCond.FirstUseEver; - this.componentDemos = new List> + + this.componentDemos = new() { - Demo("Test", ImGuiComponents.Test), - Demo("HelpMarker", HelpMarkerDemo), - Demo("IconButton", IconButtonDemo), - Demo("TextWithLabel", TextWithLabelDemo), - Demo("ColorPickerWithPalette", this.ColorPickerWithPaletteDemo), + ("Test", ImGuiComponents.Test), + ("HelpMarker", HelpMarkerDemo), + ("IconButton", IconButtonDemo), + ("TextWithLabel", TextWithLabelDemo), + ("ColorPickerWithPalette", this.ColorPickerWithPaletteDemo), }; } /// public override void Draw() { - ImGui.BeginChild("comp_scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.AlwaysVerticalScrollbar | ImGuiWindowFlags.HorizontalScrollbar); ImGui.Text("This is a collection of UI components you can use in your plugin."); for (var i = 0; i < this.componentDemos.Count; i++) { var componentDemo = this.componentDemos[i]; - if (ImGui.CollapsingHeader($"{componentDemo.Key}###comp{i}")) + if (ImGui.CollapsingHeader($"{componentDemo.Name}###comp{i}")) { - componentDemo.Value(); + componentDemo.Demo(); } } - - ImGui.EndChild(); } private static void HelpMarkerDemo() @@ -80,11 +79,6 @@ namespace Dalamud.Interface.Components ImGuiComponents.TextWithLabel("Label", "Hover to see more", "more"); } - private static KeyValuePair Demo(string name, Action func) - { - return new KeyValuePair(name, func); - } - private void ColorPickerWithPaletteDemo() { ImGui.Text("Click on the color button to use the picker."); diff --git a/Dalamud/Interface/DalamudCreditsWindow.cs b/Dalamud/Interface/Internal/Windows/CreditsWindow.cs similarity index 63% rename from Dalamud/Interface/DalamudCreditsWindow.cs rename to Dalamud/Interface/Internal/Windows/CreditsWindow.cs index a3dd358e7..412a7ffbb 100644 --- a/Dalamud/Interface/DalamudCreditsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/CreditsWindow.cs @@ -4,17 +4,16 @@ using System.IO; using System.Linq; using System.Numerics; -using Dalamud.Game.Internal; using Dalamud.Interface.Windowing; using ImGuiNET; using ImGuiScene; -namespace Dalamud.Interface +namespace Dalamud.Interface.Internal.Windows { /// /// A window documenting contributors to the project. /// - internal class DalamudCreditsWindow : Window, IDisposable + internal class CreditsWindow : Window, IDisposable { private const float CreditFPS = 60.0f; private const string CreditsTextTempl = @" @@ -105,24 +104,21 @@ Thank you for using XIVLauncher and Dalamud! "; private readonly Dalamud dalamud; - private TextureWrap logoTexture; - private Framework framework; + private readonly TextureWrap logoTexture; + private readonly Stopwatch creditsThrottler; private string creditsText; - private Stopwatch creditsThrottler; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The Dalamud instance. - public DalamudCreditsWindow(Dalamud dalamud) + public CreditsWindow(Dalamud dalamud) : base("Dalamud Credits", ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize, true) { this.dalamud = dalamud; - this.logoTexture = this.dalamud.InterfaceManager.LoadImage( - Path.Combine(this.dalamud.AssetDirectory.FullName, "UIRes", "logo.png")); - this.framework = dalamud.Framework; - this.creditsThrottler = new Stopwatch(); + this.logoTexture = this.dalamud.InterfaceManager.LoadImage(Path.Combine(this.dalamud.AssetDirectory.FullName, "UIRes", "logo.png")); + this.creditsThrottler = new(); this.Size = new Vector2(500, 400); this.SizeCondition = ImGuiCond.Always; @@ -135,14 +131,14 @@ Thank you for using XIVLauncher and Dalamud! /// public override void OnOpen() { - base.OnOpen(); + var pluginCredits = this.dalamud.PluginManager.InstalledPlugins + .Where(plugin => plugin.Manifest != null) + .Select(plugin => $"{plugin.Manifest.Name} by {plugin.Manifest.Author}\n") + .Aggregate(string.Empty, (current, next) => $"{current}{next}"); - var pluginCredits = this.dalamud.PluginManager.Plugins.Where(x => x.Definition != null).Aggregate(string.Empty, (current, plugin) => current + $"{plugin.Definition.Name} by {plugin.Definition.Author}\n"); + this.creditsText = string.Format(CreditsTextTempl, typeof(Dalamud).Assembly.GetName().Version, pluginCredits); - this.creditsText = - string.Format(CreditsTextTempl, typeof(Dalamud).Assembly.GetName().Version, pluginCredits); - - this.framework.Gui.SetBgm(132); + this.dalamud.Framework.Gui.SetBgm(132); this.creditsThrottler.Restart(); } @@ -150,9 +146,7 @@ Thank you for using XIVLauncher and Dalamud! public override void OnClose() { this.creditsThrottler.Reset(); - base.OnClose(); - - this.framework.Gui.SetBgm(9999); + this.dalamud.Framework.Gui.SetBgm(9999); } /// @@ -161,19 +155,19 @@ Thank you for using XIVLauncher and Dalamud! var screenSize = ImGui.GetMainViewport().Size; var windowSize = ImGui.GetWindowSize(); - this.Position = new Vector2((screenSize.X / 2) - (windowSize.X / 2), (screenSize.Y / 2) - (windowSize.Y / 2)); + this.Position = (screenSize - windowSize) / 2; - ImGui.BeginChild("scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.NoScrollbar); + ImGui.BeginChild("scrolling", Vector2.Zero, false, ImGuiWindowFlags.NoScrollbar); - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0)); + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - ImGui.Dummy(new Vector2(0, 340f) * ImGui.GetIO().FontGlobalScale); + ImGuiHelpers.ScaledDummy(0, 340f); ImGui.Text(string.Empty); ImGui.SameLine(150f); - ImGui.Image(this.logoTexture.ImGuiHandle, new Vector2(190f, 190f) * ImGui.GetIO().FontGlobalScale); + ImGui.Image(this.logoTexture.ImGuiHandle, ImGuiHelpers.ScaledVector2(190f, 190f)); - ImGui.Dummy(new Vector2(0, 20f) * ImGui.GetIO().FontGlobalScale); + ImGuiHelpers.ScaledDummy(0, 20f); var windowX = ImGui.GetWindowSize().X; @@ -188,12 +182,11 @@ Thank you for using XIVLauncher and Dalamud! ImGui.PopStyleVar(); - var curY = ImGui.GetScrollY(); - var maxY = ImGui.GetScrollMaxY(); - if (this.creditsThrottler.Elapsed.TotalMilliseconds > (1000.0f / CreditFPS)) { - this.creditsThrottler.Restart(); + var curY = ImGui.GetScrollY(); + var maxY = ImGui.GetScrollMaxY(); + if (curY < maxY - 1) { ImGui.SetScrollY(curY + 1); diff --git a/Dalamud/Interface/DalamudDataWindow.cs b/Dalamud/Interface/Internal/Windows/DataWindow.cs similarity index 95% rename from Dalamud/Interface/DalamudDataWindow.cs rename to Dalamud/Interface/Internal/Windows/DataWindow.cs index 45ba6a6ad..29042f4c2 100644 --- a/Dalamud/Interface/DalamudDataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/DataWindow.cs @@ -12,7 +12,6 @@ using Dalamud.Game.Internal; using Dalamud.Game.Internal.Gui.Addon; using Dalamud.Game.Internal.Gui.Toast; using Dalamud.Game.Text; -using Dalamud.Interface.Colors; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using ImGuiNET; @@ -20,25 +19,25 @@ using ImGuiScene; using Newtonsoft.Json; using Serilog; -namespace Dalamud.Interface +namespace Dalamud.Interface.Internal.Windows { /// /// Class responsible for drawing the data/debug window. /// - internal class DalamudDataWindow : Window + internal class DataWindow : Window { private readonly Dalamud dalamud; - private bool wasReady; - private string serverOpString; - - private int currentKind; - private string[] dataKinds = new[] + private readonly string[] dataKinds = new[] { "ServerOpCode", "Address", "Actor Table", "Font Test", "Party List", "Plugin IPC", "Condition", "Gauge", "Command", "Addon", "Addon Inspector", "StartInfo", "Target", "Toast", "ImGui", "Tex", "Gamepad", }; + private bool wasReady; + private string serverOpString; + private int currentKind; + private bool drawActors = false; private float maxActorDrawDistance = 20; @@ -73,10 +72,10 @@ namespace Dalamud.Interface private uint copyButtonIndex = 0; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The Dalamud instance to access data of. - public DalamudDataWindow(Dalamud dalamud) + public DataWindow(Dalamud dalamud) : base("Dalamud Data") { this.dalamud = dalamud; @@ -202,7 +201,7 @@ namespace Dalamud.Interface ImGui.Text(((int)fontAwesomeIcon.ToIconChar()).ToString("X") + " - "); ImGui.SameLine(); - ImGui.PushFont(InterfaceManager.IconFont); + ImGui.PushFont(UiBuilder.IconFont); ImGui.Text(fontAwesomeIcon.ToIconString()); ImGui.PopFont(); } @@ -248,7 +247,7 @@ namespace Dalamud.Interface // Condition case 6: #if DEBUG - ImGui.Text($"ptr: {this.dalamud.ClientState.Condition.ConditionArrayBase.ToString("X16")}"); + ImGui.Text($"ptr: 0x{this.dalamud.ClientState.Condition.ConditionArrayBase.ToInt64():X}"); #endif ImGui.Text("Current Conditions:"); @@ -413,9 +412,13 @@ namespace Dalamud.Interface $"R3 {resolve(GamepadButtons.R3)} "); }; #if DEBUG - ImGui.Text($"GamepadInput {this.dalamud.ClientState.GamepadState.GamepadInput.ToString("X")}"); - if (ImGui.IsItemHovered()) ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); - if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"{this.dalamud.ClientState.GamepadState.GamepadInput.ToString("X")}"); + ImGui.Text($"GamepadInput 0x{this.dalamud.ClientState.GamepadState.GamepadInput.ToInt64():X}"); + + if (ImGui.IsItemHovered()) + ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); + + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText($"0x{this.dalamud.ClientState.GamepadState.GamepadInput.ToInt64():X}"); #endif helper( @@ -504,8 +507,7 @@ namespace Dalamud.Interface // So, while WorldToScreen will return false if the point is off of game client screen, to // to avoid performance issues, we have to manually determine if creating a window would // produce a new viewport, and skip rendering it if so - var actorText = - $"{actor.Address.ToInt64():X}:{actor.ActorId:X}[{i}] - {actor.ObjectKind} - {actor.Name}"; + var actorText = $"{actor.Address.ToInt64():X}:{actor.ActorId:X}[{i}] - {actor.ObjectKind} - {actor.Name}"; var screenPos = ImGui.GetMainViewport().Pos; var screenSize = ImGui.GetMainViewport().Size; @@ -546,8 +548,8 @@ namespace Dalamud.Interface #pragma warning disable CS0618 // Type or member is obsolete private void DrawIpcDebug() { - var i1 = new DalamudPluginInterface(this.dalamud, "DalamudTestSub", null, PluginLoadReason.Boot); - var i2 = new DalamudPluginInterface(this.dalamud, "DalamudTestPub", null, PluginLoadReason.Boot); + var i1 = new DalamudPluginInterface(this.dalamud, "DalamudTestSub", null); + var i2 = new DalamudPluginInterface(this.dalamud, "DalamudTestPub", null); if (ImGui.Button("Add test sub")) { @@ -588,8 +590,8 @@ namespace Dalamud.Interface i2.SendMessage("DalamudTestSub", testMsg); } - foreach (var (sourcePluginName, subPluginName, subAction) in this.dalamud.PluginManager.IpcSubscriptions) - ImGui.Text($"Source:{sourcePluginName} Sub:{subPluginName}"); + foreach (var ipc in this.dalamud.PluginManager.IpcSubscriptions) + ImGui.Text($"Source:{ipc.SourcePluginName} Sub:{ipc.SubPluginName}"); } #pragma warning restore CS0618 // Type or member is obsolete diff --git a/Dalamud/Interface/GamepadModeNotifierWindow.cs b/Dalamud/Interface/Internal/Windows/GamepadModeNotifierWindow.cs similarity index 96% rename from Dalamud/Interface/GamepadModeNotifierWindow.cs rename to Dalamud/Interface/Internal/Windows/GamepadModeNotifierWindow.cs index 8794a0386..2dc174fde 100644 --- a/Dalamud/Interface/GamepadModeNotifierWindow.cs +++ b/Dalamud/Interface/Internal/Windows/GamepadModeNotifierWindow.cs @@ -1,10 +1,10 @@ -using System.Numerics; +using System.Numerics; using CheapLoc; using Dalamud.Interface.Windowing; using ImGuiNET; -namespace Dalamud.Interface +namespace Dalamud.Interface.Internal.Windows { /// /// Class responsible for drawing a notifier on screen that gamepad mode is active. diff --git a/Dalamud/Interface/DalamudLogWindow.cs b/Dalamud/Interface/Internal/Windows/LogWindow.cs similarity index 78% rename from Dalamud/Interface/DalamudLogWindow.cs rename to Dalamud/Interface/Internal/Windows/LogWindow.cs index 78cbaa17e..305ff1676 100644 --- a/Dalamud/Interface/DalamudLogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/LogWindow.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Numerics; -using Dalamud.Configuration; +using Dalamud.Configuration.Internal; using Dalamud.Game.Command; using Dalamud.Interface.Colors; using Dalamud.Interface.Windowing; @@ -10,12 +10,12 @@ using ImGuiNET; using Serilog; using Serilog.Events; -namespace Dalamud.Interface +namespace Dalamud.Interface.Internal.Windows { /// /// The window that displays the Dalamud log file in-game. /// - internal class DalamudLogWindow : Window, IDisposable + internal class LogWindow : Window, IDisposable { private readonly CommandManager commandManager; private readonly DalamudConfiguration configuration; @@ -27,18 +27,17 @@ namespace Dalamud.Interface private string commandText = string.Empty; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The CommandManager instance. - /// The DalamudConfiguration instance. - public DalamudLogWindow(CommandManager commandManager, DalamudConfiguration configuration) + /// The Dalamud instance. + public LogWindow(Dalamud dalamud) : base("Dalamud LOG") { - this.commandManager = commandManager; - this.configuration = configuration; - this.autoScroll = configuration.LogAutoScroll; - this.openAtStartup = configuration.LogOpenAtStartup; - SerilogEventSink.Instance.OnLogLine += this.Serilog_OnLogLine; + this.commandManager = dalamud.CommandManager; + this.configuration = dalamud.Configuration; + this.autoScroll = this.configuration.LogAutoScroll; + this.openAtStartup = this.configuration.LogOpenAtStartup; + SerilogEventSink.Instance.OnLogLine += this.OnLogLine; this.Size = new Vector2(500, 400); this.SizeCondition = ImGuiCond.FirstUseEver; @@ -49,7 +48,7 @@ namespace Dalamud.Interface /// public void Dispose() { - SerilogEventSink.Instance.OnLogLine -= this.Serilog_OnLogLine; + SerilogEventSink.Instance.OnLogLine -= this.OnLogLine; } /// @@ -100,15 +99,19 @@ namespace Dalamud.Interface // Main window if (ImGui.Button("Options")) ImGui.OpenPopup("Options"); + ImGui.SameLine(); var clear = ImGui.Button("Clear"); + ImGui.SameLine(); var copy = ImGui.Button("Copy"); ImGui.Text("Enter command: "); ImGui.SameLine(); + ImGui.InputText("##commandbox", ref this.commandText, 255); ImGui.SameLine(); + if (ImGui.Button("Send")) { if (this.commandManager.ProcessCommand(this.commandText)) @@ -117,18 +120,23 @@ namespace Dalamud.Interface } else { - Log.Information("Command {0} not registered.", this.commandText); + Log.Information($"Command {this.commandText} is not registered."); } } ImGui.BeginChild("scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.HorizontalScrollbar); if (clear) + { this.Clear(); - if (copy) - ImGui.LogToClipboard(); + } - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0)); + if (copy) + { + ImGui.LogToClipboard(); + } + + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); lock (this.renderLock) { @@ -141,12 +149,14 @@ namespace Dalamud.Interface ImGui.PopStyleVar(); if (this.autoScroll && ImGui.GetScrollY() >= ImGui.GetScrollMaxY()) + { ImGui.SetScrollHereY(1.0f); + } ImGui.EndChild(); } - private void Serilog_OnLogLine(object sender, (string Line, LogEventLevel Level) logEvent) + private void OnLogLine(object sender, (string Line, LogEventLevel Level) logEvent) { var color = logEvent.Level switch { @@ -156,7 +166,7 @@ namespace Dalamud.Interface LogEventLevel.Information => ImGuiColors.DalamudWhite, LogEventLevel.Warning => ImGuiColors.DalamudOrange, LogEventLevel.Fatal => ImGuiColors.DalamudRed, - _ => throw new ArgumentOutOfRangeException(), + _ => throw new ArgumentOutOfRangeException(logEvent.Level.ToString(), "Invalid LogEventLevel"), }; this.AddLog(logEvent.Line, color); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs new file mode 100644 index 000000000..75037704b --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs @@ -0,0 +1,1173 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; + +using CheapLoc; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; +using Dalamud.Interface.Windowing; +using Dalamud.Plugin.Internal; +using Dalamud.Plugin.Internal.Exceptions; +using Dalamud.Plugin.Internal.Types; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows +{ + /// + /// Class responsible for drawing the plugin installer. + /// + internal class PluginInstallerWindow : Window, IDisposable + { + private static readonly ModuleLog Log = new("PLUGINW"); + + private readonly Dalamud dalamud; + + private bool errorModalDrawing = true; + private bool errorModalOnNextFrame = false; + private string errorModalMessage = string.Empty; + + private int updatePluginCount = 0; + private List updatedPlugins; + + private List pluginListAvailable = new(); + private List pluginListInstalled = new(); + private List pluginListUpdatable = new(); + private bool hasDevPlugins = false; + + private string searchText = string.Empty; + + private PluginSortKind sortKind = PluginSortKind.Alphabetical; + private string filterText = Locs.SortBy_Alphabetical; + + private OperationStatus installStatus = OperationStatus.Idle; + private OperationStatus updateStatus = OperationStatus.Idle; + + /// + /// Initializes a new instance of the class. + /// + /// The Dalamud instance. + public PluginInstallerWindow(Dalamud dalamud) + : base( + Locs.WindowTitle + (dalamud.Configuration.DoPluginTest ? Locs.WindowTitleMod_Testing : string.Empty) + "###XlPluginInstaller", + ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoScrollbar) + { + this.dalamud = dalamud; + this.IsOpen = true; + + this.Size = new Vector2(810, 520); + this.SizeCondition = ImGuiCond.Always; + + // For debugging + if (this.dalamud.PluginManager.PluginsReady) + this.OnInstalledPluginsChanged(); + + this.dalamud.PluginManager.OnAvailablePluginsChanged += this.OnAvailablePluginsChanged; + this.dalamud.PluginManager.OnInstalledPluginsChanged += this.OnInstalledPluginsChanged; + } + + private enum OperationStatus + { + Idle, + InProgress, + Complete, + } + + private enum PluginSortKind + { + Alphabetical, + DownloadCount, + LastUpdate, + } + + /// + public void Dispose() + { + this.dalamud.PluginManager.OnAvailablePluginsChanged -= this.OnAvailablePluginsChanged; + this.dalamud.PluginManager.OnInstalledPluginsChanged -= this.OnInstalledPluginsChanged; + } + + /// + public override void OnOpen() + { + Task.Run(this.dalamud.PluginManager.ReloadPluginMasters); + + this.updatePluginCount = 0; + this.updatedPlugins = null; + + this.searchText = string.Empty; + this.sortKind = PluginSortKind.Alphabetical; + this.filterText = Locs.SortBy_Alphabetical; + } + + /// + public override void Draw() + { + this.DrawHeader(); + this.DrawPluginTabBar(); + this.DrawFooter(); + this.DrawErrorModal(); + } + + private static Vector2 GetButtonSize(string text) => ImGui.CalcTextSize(text) + (ImGui.GetStyle().FramePadding * 2); + + private void DrawHeader() + { + var style = ImGui.GetStyle(); + var windowSize = ImGui.GetWindowContentRegionMax(); + + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5 * ImGuiHelpers.GlobalScale)); + + var searchInputWidth = 240 * ImGuiHelpers.GlobalScale; + + var sortByText = Locs.SortBy_Label; + var sortByTextWidth = ImGui.CalcTextSize(sortByText).X; + var sortSelectables = new (string Localization, PluginSortKind SortKind)[] + { + (Locs.SortBy_Alphabetical, PluginSortKind.Alphabetical), + (Locs.SortBy_DownloadCounts, PluginSortKind.DownloadCount), + (Locs.SortBy_LastUpdate, PluginSortKind.LastUpdate), + }; + var longestSelectableWidth = sortSelectables.Select(t => ImGui.CalcTextSize(t.Localization).X).Max(); + var selectableWidth = longestSelectableWidth + (style.FramePadding.X * 2); // This does not include the label + var sortSelectWidth = selectableWidth + sortByTextWidth + style.ItemInnerSpacing.X; // Item spacing between the selectable and the label + + var headerText = Locs.Header_Hint; + var headerTextSize = ImGui.CalcTextSize(headerText); + ImGui.Text(headerText); + + ImGui.SameLine(); + + // Shift down a little to align with the middle of the header text + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (headerTextSize.Y / 4) - 2); + + ImGui.SetCursorPosX(windowSize.X - sortSelectWidth - style.ItemSpacing.X - searchInputWidth); + ImGui.SetNextItemWidth(searchInputWidth); + ImGui.InputTextWithHint("###XlPluginInstaller_Search", Locs.Header_SearchPlaceholder, ref this.searchText, 100); + + ImGui.SameLine(); + ImGui.SetCursorPosX(windowSize.X - sortSelectWidth); + ImGui.SetNextItemWidth(selectableWidth); + if (ImGui.BeginCombo(sortByText, this.filterText, ImGuiComboFlags.NoArrowButton)) + { + foreach (var selectable in sortSelectables) + { + if (ImGui.Selectable(selectable.Localization)) + { + this.sortKind = selectable.SortKind; + this.filterText = selectable.Localization; + + this.ResortPlugins(); + } + } + + ImGui.EndCombo(); + } + } + + private void DrawFooter() + { + var windowSize = ImGui.GetWindowContentRegionMax(); + var placeholderButtonSize = GetButtonSize("placeholder"); + + ImGui.Separator(); + + ImGui.SetCursorPosY(windowSize.Y - placeholderButtonSize.Y); + + this.DrawUpdatePluginsButton(); + + ImGui.SameLine(); + if (ImGui.Button(Locs.FooterButton_Settings)) + { + this.dalamud.DalamudUi.OpenSettings(); + } + + // If any dev plugins are installed, allow a shortcut for the /xldev menu item + if (this.hasDevPlugins) + { + ImGui.SameLine(); + if (ImGui.Button(Locs.FooterButton_ScanDevPlugins)) + { + this.dalamud.PluginManager.ScanDevPlugins(); + } + } + + var closeText = Locs.FooterButton_Close; + var closeButtonSize = GetButtonSize(closeText); + + ImGui.SameLine(windowSize.X - closeButtonSize.X); + if (ImGui.Button(closeText)) + { + this.IsOpen = false; + this.dalamud.Configuration.Save(); + } + } + + private void DrawUpdatePluginsButton() + { + var ready = this.dalamud.PluginManager.PluginsReady && this.dalamud.PluginManager.ReposReady; + + if (!ready || this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress) + { + ImGuiComponents.DisabledButton(Locs.FooterButton_UpdatePlugins); + } + else if (this.updateStatus == OperationStatus.Complete) + { + ImGui.Button(this.updatePluginCount > 0 + ? Locs.FooterButton_UpdateComplete(this.updatePluginCount) + : Locs.FooterButton_NoUpdates); + } + else + { + if (ImGui.Button(Locs.FooterButton_UpdatePlugins)) + { + this.updateStatus = OperationStatus.InProgress; + + Task.Run(() => this.dalamud.PluginManager.UpdatePlugins()) + .ContinueWith(task => + { + this.updateStatus = OperationStatus.Complete; + + if (task.IsFaulted) + { + this.updatePluginCount = 0; + this.updatedPlugins = null; + this.DisplayErrorContinuation(task, Locs.ErrorModal_UpdaterFatal); + } + else + { + this.updatedPlugins = task.Result.Where(res => res.WasUpdated).ToList(); + this.updatePluginCount = this.updatedPlugins.Count; + + var errorPlugins = task.Result.Where(res => !res.WasUpdated).ToList(); + var errorPluginCount = errorPlugins.Count; + + if (errorPluginCount > 0) + { + var errorMessage = this.updatePluginCount > 0 + ? Locs.ErrorModal_UpdaterFailPartial(this.updatePluginCount, errorPluginCount) + : Locs.ErrorModal_UpdaterFail(errorPluginCount); + + var hintInsert = errorPlugins + .Aggregate(string.Empty, (current, pluginUpdateStatus) => $"{current}* {pluginUpdateStatus.InternalName}\n") + .TrimEnd(); + errorMessage += Locs.ErrorModal_HintBlame(hintInsert); + + this.DisplayErrorContinuation(task, errorMessage); + } + + if (this.updatePluginCount > 0) + { + this.dalamud.PluginManager.PrintUpdatedPlugins(this.updatedPlugins, Locs.PluginUpdateHeader_Chatbox); + } + } + }); + } + } + } + + private void DrawErrorModal() + { + var modalTitle = Locs.ErrorModal_Title; + + if (ImGui.BeginPopupModal(modalTitle, ref this.errorModalDrawing, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar)) + { + ImGui.Text(this.errorModalMessage); + ImGui.Spacing(); + + var buttonWidth = 120f; + ImGui.SetCursorPosX((ImGui.GetWindowWidth() - buttonWidth) / 2); + + if (ImGui.Button(Locs.ErrorModalButton_Ok, new Vector2(buttonWidth, 40))) + { + ImGui.CloseCurrentPopup(); + } + + ImGui.EndPopup(); + } + + if (this.errorModalOnNextFrame) + { + ImGui.OpenPopup(modalTitle); + this.errorModalOnNextFrame = false; + } + } + + private void DrawPluginTabBar() + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5 * ImGuiHelpers.GlobalScale)); + + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, ImGuiHelpers.ScaledVector2(1, 3)); + + if (ImGui.BeginTabBar("PluginsTabBar", ImGuiTabBarFlags.NoTooltip)) + { + this.DrawPluginTab(Locs.TabTitle_AvailablePlugins, this.DrawAvailablePluginList); + this.DrawPluginTab(Locs.TabTitle_InstalledPlugins, this.DrawInstalledPluginList); + + if (this.hasDevPlugins) + { + this.DrawPluginTab(Locs.TabTitle_InstalledDevPlugins, this.DrawInstalledDevPluginList); + } + } + + ImGui.PopStyleVar(); + } + + private void DrawPluginTab(string title, Action drawPluginList) + { + if (ImGui.BeginTabItem(title)) + { + ImGui.BeginChild($"Scrolling{title}", ImGuiHelpers.ScaledVector2(0, 384), true, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoBackground); + + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 5); + + var ready = this.DrawPluginListLoading(); + + if (ready) + { + drawPluginList(); + } + + ImGui.EndChild(); + + ImGui.EndTabItem(); + } + } + + private void DrawAvailablePluginList() + { + var pluginList = this.pluginListAvailable; + + if (pluginList.Count == 0) + { + ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_SearchNoCompatible); + return; + } + + var filteredList = pluginList + .Where(rm => !this.IsManifestFiltered(rm)) + .ToList(); + + if (filteredList.Count == 0) + { + ImGui.TextColored(ImGuiColors.DalamudGrey2, Locs.TabBody_SearchNoMatching); + return; + } + + var i = 0; + foreach (var manifest in filteredList) + { + var (isInstalled, plugin) = this.IsManifestInstalled(manifest); + + ImGui.PushID($"{manifest.InternalName}{manifest.AssemblyVersion}"); + + if (isInstalled) + { + this.DrawInstalledPlugin(plugin, i++, true); + } + else + { + this.DrawAvailablePlugin(manifest, i++); + } + + ImGui.PopID(); + } + } + + private void DrawInstalledPluginList() + { + var pluginList = this.pluginListInstalled; + + if (pluginList.Count == 0) + { + ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_SearchNoInstalled); + return; + } + + var filteredList = pluginList + .Where(plugin => !this.IsManifestFiltered(plugin.Manifest)) + .ToList(); + + if (filteredList.Count == 0) + { + ImGui.TextColored(ImGuiColors.DalamudGrey2, Locs.TabBody_SearchNoMatching); + return; + } + + var i = 0; + foreach (var plugin in filteredList) + { + this.DrawInstalledPlugin(plugin, i++); + } + } + + private void DrawInstalledDevPluginList() + { + var pluginList = this.pluginListInstalled + .Where(plugin => plugin.IsDev) + .ToList(); + + if (pluginList.Count == 0) + { + ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_SearchNoInstalled); + return; + } + + var filteredList = pluginList + .Where(plugin => !this.IsManifestFiltered(plugin.Manifest)) + .ToList(); + + if (filteredList.Count == 0) + { + ImGui.TextColored(ImGuiColors.DalamudGrey2, Locs.TabBody_SearchNoMatching); + return; + } + + var i = 0; + foreach (var plugin in filteredList) + { + this.DrawInstalledPlugin(plugin, i++); + } + } + + private bool DrawPluginListLoading() + { + var ready = this.dalamud.PluginManager.PluginsReady && this.dalamud.PluginManager.ReposReady; + + if (!ready) + { + ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_LoadingPlugins); + } + + var failedRepos = this.dalamud.PluginManager.Repos + .Where(repo => repo.State == PluginRepositoryState.Fail) + .ToArray(); + + if (failedRepos.Length > 0) + { + var failText = Locs.TabBody_DownloadFailed; + var aggFailText = failedRepos + .Select(repo => $"{failText} ({repo.PluginMasterUrl})") + .Aggregate((s1, s2) => $"{s1}\n{s2}"); + + ImGui.TextColored(ImGuiColors.DalamudRed, aggFailText); + } + + return ready; + } + + private void DrawAvailablePlugin(RemotePluginManifest manifest, int index) + { + var useTesting = this.dalamud.PluginManager.UseTesting(manifest); + + // Check for valid versions + if ((useTesting && manifest.TestingAssemblyVersion == null) || manifest.AssemblyVersion == null) + { + // Without a valid version, quit + return; + } + + // Name + var label = manifest.Name; + + // Testing + if (useTesting) + { + label += Locs.PluginTitleMod_TestingVersion; + } + + if (ImGui.CollapsingHeader($"{label}###Header{index}{manifest.InternalName}")) + { + ImGui.Indent(); + + // Name + ImGui.Text(manifest.Name); + + // Download count + var downloadCountText = manifest.DownloadCount > 0 + ? Locs.PluginBody_AuthorWithDownloadCount(manifest.Author, manifest.DownloadCount) + : Locs.PluginBody_AuthorWithDownloadCountUnavailable(manifest.Author); + + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.DalamudGrey3, downloadCountText); + + // Installable from + if (manifest.SourceRepo.IsThirdParty) + { + var repoText = Locs.PluginBody_Plugin3rdPartyRepo(manifest.SourceRepo.PluginMasterUrl); + ImGui.TextColored(ImGuiColors.DalamudGrey3, repoText); + } + + // Description + if (!string.IsNullOrWhiteSpace(manifest.Description)) + { + ImGui.TextWrapped(manifest.Description); + } + + // Controls + var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress; + + var versionString = useTesting + ? $"{manifest.TestingAssemblyVersion}" + : $"{manifest.AssemblyVersion}"; + + if (disabled) + { + ImGuiComponents.DisabledButton(Locs.PluginButton_InstallVersion(versionString)); + } + else + { + if (ImGui.Button(Locs.PluginButton_InstallVersion(versionString))) + { + this.installStatus = OperationStatus.InProgress; + + Task.Run(() => this.dalamud.PluginManager.InstallPlugin(manifest, useTesting)) + .ContinueWith(task => + { + // There is no need to set as Complete for an individual plugin installation + this.installStatus = OperationStatus.Idle; + this.DisplayErrorContinuation(task, Locs.ErrorModal_InstallFail(manifest.Name)); + }); + } + } + + this.DrawVisitRepoUrlButton(manifest.RepoUrl); + + ImGui.Unindent(); + } + + if (ImGui.BeginPopupContextItem("ItemContextMenu")) + { + if (ImGui.Selectable(Locs.PluginContext_HidePlugin)) + { + Log.Debug($"Adding {manifest.InternalName} to hidden plugins"); + this.dalamud.Configuration.HiddenPluginInternalName.Add(manifest.InternalName); + this.dalamud.Configuration.Save(); + this.dalamud.PluginManager.RefilterPluginMasters(); + } + + ImGui.EndPopup(); + } + } + + private void DrawInstalledPlugin(LocalPlugin plugin, int index, bool showInstalled = false) + { + // Name + var label = plugin.Manifest.Name; + + // Testing + if (plugin.Manifest.Testing) + { + label += Locs.PluginTitleMod_TestingVersion; + } + + // Freshly installed + if (showInstalled) + { + label += Locs.PluginTitleMod_Installed; + } + + // Disabled + if (plugin.IsDisabled) + { + label += Locs.PluginTitleMod_Disabled; + } + + // Load error + if (plugin.State == PluginState.LoadError) + { + label += Locs.PluginTitleMod_LoadError; + } + + // Unload error + if (plugin.State == PluginState.UnloadError) + { + label += Locs.PluginTitleMod_UnloadError; + } + + // Update available + if (this.pluginListUpdatable.FirstOrDefault(up => up.InstalledPlugin == plugin) != default) + { + label += Locs.PluginTitleMod_HasUpdate; + } + + // Freshly updated + if (this.updatedPlugins != null && !plugin.IsDev) + { + var update = this.updatedPlugins.FirstOrDefault(update => update.InternalName == plugin.Manifest.InternalName); + if (update != default) + { + if (update.WasUpdated) + { + label += Locs.PluginTitleMod_Updated; + } + else + { + label += Locs.PluginTitleMod_UpdateFailed; + } + } + } + + if (ImGui.CollapsingHeader($"{label}###Header{index}{plugin.Manifest.InternalName}")) + { + var manifest = plugin.Manifest; + + ImGui.Indent(); + + // Name + ImGui.Text(manifest.Name); + + // Download count + var downloadText = manifest.DownloadCount > 0 + ? Locs.PluginBody_AuthorWithDownloadCount(manifest.Author, manifest.DownloadCount) + : Locs.PluginBody_AuthorWithDownloadCountUnavailable(manifest.Author); + + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.DalamudGrey3, downloadText); + + // Installed from + if (!string.IsNullOrEmpty(manifest.InstalledFromUrl)) + { + var repoText = Locs.PluginBody_Plugin3rdPartyRepo(manifest.InstalledFromUrl); + ImGui.TextColored(ImGuiColors.DalamudGrey3, repoText); + } + + // Description + if (!string.IsNullOrWhiteSpace(manifest.Description)) + { + ImGui.TextWrapped(manifest.Description); + } + + // Available commands (if loaded) + if (plugin.IsLoaded) + { + var commands = this.dalamud.CommandManager.Commands.Where(cInfo => cInfo.Value.ShowInHelp && cInfo.Value.LoaderAssemblyName == plugin.Manifest.InternalName); + if (commands.Any()) + { + ImGui.Dummy(ImGuiHelpers.ScaledVector2(10f, 10f)); + foreach (var command in commands) + { + ImGui.TextWrapped($"{command.Key} → {command.Value.HelpMessage}"); + } + } + } + + // Controls + this.DrawPluginControlButton(plugin); + this.DrawDevPluginButtons(plugin); + this.DrawVisitRepoUrlButton(plugin.Manifest.RepoUrl); + + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{plugin.Manifest.AssemblyVersion}"); + + if (plugin.IsDev) + { + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.DalamudRed, Locs.PluginBody_DeleteDevPlugin); + } + + ImGui.Unindent(); + } + } + + private void DrawPluginControlButton(LocalPlugin plugin) + { + // Disable everything if the updater is running or another plugin is operating + var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress; + + if (plugin.State == PluginState.InProgress) + { + ImGuiComponents.DisabledButton(Locs.PluginButton_Working); + } + else if (plugin.State == PluginState.Loaded || plugin.State == PluginState.LoadError) + { + if (disabled) + { + ImGuiComponents.DisabledButton(Locs.PluginButton_Disable); + } + else + { + if (ImGui.Button(Locs.PluginButton_Disable)) + { + Task.Run(() => + { + var unloadTask = Task.Run(() => plugin.Unload()) + .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_UnloadFail(plugin.Name)); + + unloadTask.Wait(); + if (!unloadTask.Result) + return; + + var disableTask = Task.Run(() => plugin.Disable()) + .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_DisableFail(plugin.Name)); + + disableTask.Wait(); + if (!disableTask.Result) + return; + + if (!plugin.IsDev) + { + this.dalamud.PluginManager.RemovePlugin(plugin); + } + }); + } + } + + if (plugin.State == PluginState.Loaded) + { + // Only if the plugin isn't broken. + this.DrawOpenPluginSettingsButton(plugin); + } + } + else if (plugin.State == PluginState.Unloaded) + { + if (disabled) + { + ImGuiComponents.DisabledButton(Locs.PluginButton_Load); + } + else + { + if (ImGui.Button(Locs.PluginButton_Load)) + { + Task.Run(() => + { + var enableTask = Task.Run(() => plugin.Enable()) + .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_EnableFail(plugin.Name)); + + enableTask.Wait(); + if (!enableTask.Result) + return; + + var loadTask = Task.Run(() => plugin.Load()) + .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_LoadFail(plugin.Name)); + + loadTask.Wait(); + if (!loadTask.Result) + return; + }); + } + } + } + else if (plugin.State == PluginState.UnloadError) + { + ImGuiComponents.DisabledButton(FontAwesomeIcon.Frown); + } + } + + private void DrawOpenPluginSettingsButton(LocalPlugin plugin) + { + if (plugin.DalamudInterface?.UiBuilder?.HasConfigUi ?? false) + { + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Cog)) + { + try + { + plugin.DalamudInterface.UiBuilder.OpenConfigUi(); + } + catch (Exception ex) + { + Log.Error(ex, $"Error during OpenConfigUi: {plugin.Name}"); + } + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip(Locs.PluginButtonToolTip_OpenConfiguration); + } + } + } + + private void DrawDevPluginButtons(LocalPlugin localPlugin) + { + if (localPlugin is LocalDevPlugin plugin) + { + // https://colorswall.com/palette/2868/ + var greenColor = new Vector4(0x5C, 0xB8, 0x5C, 0xFF) / 0xFF; + var redColor = new Vector4(0xD9, 0x53, 0x4F, 0xFF) / 0xFF; + + // Load on boot + ImGui.PushStyleColor(ImGuiCol.Button, plugin.StartOnBoot ? greenColor : redColor); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, plugin.StartOnBoot ? greenColor : redColor); + + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.PowerOff)) + { + plugin.StartOnBoot ^= true; + this.dalamud.Configuration.Save(); + } + + ImGui.PopStyleColor(2); + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip(Locs.PluginButtonToolTip_StartOnBoot); + } + + // Automatic reload + ImGui.PushStyleColor(ImGuiCol.Button, plugin.AutomaticReload ? greenColor : redColor); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, plugin.AutomaticReload ? greenColor : redColor); + + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.SyncAlt)) + { + plugin.AutomaticReload ^= true; + this.dalamud.Configuration.Save(); + } + + ImGui.PopStyleColor(2); + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip(Locs.PluginButtonToolTip_AutomaticReloading); + } + + // Delete + if (plugin.State == PluginState.Unloaded) + { + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.TrashAlt)) + { + try + { + plugin.DllFile.Delete(); + this.dalamud.PluginManager.RemovePlugin(plugin); + } + catch (Exception ex) + { + Log.Error(ex, $"Plugin installer threw an error during removal of {plugin.Name}"); + + this.errorModalMessage = Locs.ErrorModal_DeleteFail(plugin.Name); + this.errorModalDrawing = true; + this.errorModalOnNextFrame = true; + } + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip(Locs.PluginBody_DeleteDevPlugin); + } + } + } + } + + private void DrawVisitRepoUrlButton(string repoUrl) + { + if (!string.IsNullOrEmpty(repoUrl) && repoUrl.StartsWith("https://")) + { + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Globe)) + { + try + { + _ = Process.Start(new ProcessStartInfo() + { + FileName = repoUrl, + UseShellExecute = true, + }); + } + catch (Exception ex) + { + Log.Error(ex, $"Could not open repoUrl: {repoUrl}"); + } + } + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(Locs.PluginButtonToolTip_VisitPluginUrl); + } + } + + private bool IsManifestFiltered(PluginManifest manifest) + { + var searchString = this.searchText.ToLowerInvariant(); + var hasSearchString = !string.IsNullOrWhiteSpace(searchString); + + return hasSearchString && !( + manifest.Name.ToLowerInvariant().Contains(searchString) || + manifest.Author.Equals(this.searchText, StringComparison.InvariantCultureIgnoreCase) || + (manifest.Tags != null && manifest.Tags.Contains(searchString, StringComparer.InvariantCultureIgnoreCase))); + } + + private (bool IsInstalled, LocalPlugin Plugin) IsManifestInstalled(RemotePluginManifest manifest) + { + var plugin = this.pluginListInstalled.FirstOrDefault(plugin => plugin.Manifest.InternalName == manifest.InternalName); + var isInstalled = plugin != default; + + return (isInstalled, plugin); + } + + private void OnAvailablePluginsChanged() + { + // By removing installed plugins only when the available plugin list changes (basically when the window is + // opened), plugins that have been newly installed remain in the available plugin list as installed. + this.pluginListAvailable = this.dalamud.PluginManager.AvailablePlugins + .Where(manifest => !this.IsManifestInstalled(manifest).IsInstalled) + .ToList(); + this.pluginListUpdatable = this.dalamud.PluginManager.UpdatablePlugins.ToList(); + this.ResortPlugins(); + } + + private void OnInstalledPluginsChanged() + { + this.pluginListInstalled = this.dalamud.PluginManager.InstalledPlugins.ToList(); + this.pluginListUpdatable = this.dalamud.PluginManager.UpdatablePlugins.ToList(); + this.hasDevPlugins = this.pluginListInstalled.Any(plugin => plugin.IsDev); + this.ResortPlugins(); + } + + private void ResortPlugins() + { + switch (this.sortKind) + { + case PluginSortKind.Alphabetical: + this.pluginListAvailable.Sort((p1, p2) => p1.Name.CompareTo(p2.Name)); + this.pluginListInstalled.Sort((p1, p2) => p1.Manifest.Name.CompareTo(p2.Manifest.Name)); + break; + case PluginSortKind.DownloadCount: + this.pluginListAvailable.Sort((p1, p2) => p2.DownloadCount.CompareTo(p1.DownloadCount)); + this.pluginListInstalled.Sort((p1, p2) => p2.Manifest.DownloadCount.CompareTo(p1.Manifest.DownloadCount)); + break; + case PluginSortKind.LastUpdate: + this.pluginListAvailable.Sort((p1, p2) => p2.LastUpdate.CompareTo(p1.LastUpdate)); + this.pluginListInstalled.Sort((p1, p2) => p2.Manifest.LastUpdate.CompareTo(p1.Manifest.LastUpdate)); + break; + default: + throw new InvalidEnumArgumentException("Unknown plugin sort type."); + } + } + + /// + /// A continuation task that displays any errors received into the error modal. + /// + /// The previous task. + /// An error message to be displayed. + /// A value indicating whether to continue with the next task. + private bool DisplayErrorContinuation(Task task, object state) + { + if (task.IsFaulted) + { + this.errorModalMessage = state as string; + + foreach (var ex in task.Exception.InnerExceptions) + { + if (ex is PluginException) + { + Log.Error(ex, "Plugin installer threw an error"); +#if DEBUG + if (!string.IsNullOrEmpty(ex.Message)) + this.errorModalMessage += $"\n\n{ex.Message}"; +#endif + } + else + { + Log.Error(ex, "Plugin installer threw an unexpected error"); +#if DEBUG + if (!string.IsNullOrEmpty(ex.Message)) + this.errorModalMessage += $"\n\n{ex.Message}"; +#endif + } + } + + this.errorModalDrawing = true; + this.errorModalOnNextFrame = true; + + return false; + } + + return true; + } + + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Disregard here")] + private static class Locs + { + #region Window Title + + public static string WindowTitle => Loc.Localize("InstallerHeader", "Plugin Installer"); + + public static string WindowTitleMod_Testing => Loc.Localize("InstallerHeaderTesting", " (TESTING)"); + + #endregion + + #region Header + + public static string Header_Hint => Loc.Localize("InstallerHint", "This window allows you to install and remove in-game plugins.\nThey are made by third-party developers."); + + public static string Header_SearchPlaceholder => Loc.Localize("InstallerSearch", "Search"); + + #endregion + + #region SortBy + + public static string SortBy_Alphabetical => Loc.Localize("InstallerAlphabetical", "Alphabetical"); + + public static string SortBy_DownloadCounts => Loc.Localize("InstallerDownloadCount", "Download Count"); + + public static string SortBy_LastUpdate => Loc.Localize("InstallerLastUpdate", "Last Update"); + + public static string SortBy_Label => Loc.Localize("InstallerSortBy", "Sort By"); + + #endregion + + #region Tabs + + public static string TabTitle_AvailablePlugins => Loc.Localize("InstallerAvailablePlugins", "Available Plugins"); + + public static string TabTitle_InstalledPlugins => Loc.Localize("InstallerInstalledPlugins", "Installed Plugins"); + + public static string TabTitle_InstalledDevPlugins => Loc.Localize("InstallerInstalledDevPlugins", "Installed Dev Plugins"); + + #endregion + + #region Tab body + + public static string TabBody_LoadingPlugins => Loc.Localize("InstallerLoading", "Loading plugins..."); + + public static string TabBody_DownloadFailed => Loc.Localize("InstallerDownloadFailed", "Download failed."); + + #endregion + + #region Search text + + public static string TabBody_SearchNoMatching => Loc.Localize("InstallerNoMatching", "No plugins were found matching your search."); + + public static string TabBody_SearchNoCompatible => Loc.Localize("InstallerNoCompatible", "No compatible plugins were found :( Please restart your game and try again."); + + public static string TabBody_SearchNoInstalled => Loc.Localize("InstallerNoInstalled", "No plugins are currently installed. You can install them from the Available Plugins tab."); + + #endregion + + #region Plugin title text + + public static string PluginTitleMod_Installed => Loc.Localize("InstallerInstalled", " (installed)"); + + public static string PluginTitleMod_Disabled => Loc.Localize("InstallerDisabled", " (disabled)"); + + public static string PluginTitleMod_Unloaded => Loc.Localize("InstallerUnloaded", " (unloaded)"); + + public static string PluginTitleMod_HasUpdate => Loc.Localize("InstallerHasUpdate", " (has update)"); + + public static string PluginTitleMod_Updated => Loc.Localize("InstallerUpdated", " (updated)"); + + public static string PluginTitleMod_TestingVersion => Loc.Localize("InstallerTestingVersion", " (testing version)"); + + public static string PluginTitleMod_UpdateFailed => Loc.Localize("InstallerUpdateFailed", " (update failed)"); + + public static string PluginTitleMod_LoadError => Loc.Localize("InstallerLoadError", " (load error)"); + + public static string PluginTitleMod_UnloadError => Loc.Localize("InstallerUnloadError", " (unload error)"); + + #endregion + + #region Plugin context menu + + public static string PluginContext_HidePlugin => Loc.Localize("InstallerHidePlugin", "Hide from installer"); + + #endregion + + #region Plugin body + + public static string PluginBody_AuthorWithDownloadCount(string author, long count) => Loc.Localize("InstallerAuthorWithDownloadCount", " by {0}, {1} downloads").Format(author, count); + + public static string PluginBody_AuthorWithDownloadCountUnavailable(string author) => Loc.Localize("InstallerAuthorWithDownloadCountUnavailable", " by {0}, download count unavailable").Format(author); + + public static string PluginBody_Plugin3rdPartyRepo(string url) => Loc.Localize("InstallerPlugin3rdPartyRepo", "From custom plugin repository {0}").Format(url); + + public static string PluginBody_AvailableDevPlugin => Loc.Localize("InstallerDevPlugin", " This plugin is available in one of your repos, please remove it from the devPlugins folder."); + + public static string PluginBody_DeleteDevPlugin => Loc.Localize("InstallerDeleteDevPlugin ", " To delete this plugin, please remove it from the devPlugins folder."); + + #endregion + + #region Plugin buttons + + public static string PluginButton_InstallVersion(string version) => Loc.Localize("InstallerInstall", "Install v{0}").Format(version); + + public static string PluginButton_Working => Loc.Localize("InstallerWorking", "Working"); + + public static string PluginButton_Disable => Loc.Localize("InstallerDisable", "Disable"); + + public static string PluginButton_Load => Loc.Localize("InstallerLoad", "Load"); + + public static string PluginButton_Unload => Loc.Localize("InstallerUnload", "Unload"); + + #endregion + + #region Plugin button tooltips + + public static string PluginButtonToolTip_OpenConfiguration => Loc.Localize("InstallerOpenConfig", "Open Configuration"); + + public static string PluginButtonToolTip_StartOnBoot => Loc.Localize("InstallerStartOnBoot", "Start on boot"); + + public static string PluginButtonToolTip_AutomaticReloading => Loc.Localize("InstallerAutomaticReloading", "Automatic reloading"); + + public static string PluginButtonToolTip_DeletePlugin => Loc.Localize("InstallerDeletePlugin ", "Delete plugin"); + + public static string PluginButtonToolTip_VisitPluginUrl => Loc.Localize("InstallerVisitPluginUrl", "Visit plugin URL"); + + #endregion + + #region Footer + + public static string FooterButton_UpdatePlugins => Loc.Localize("InstallerUpdatePlugins", "Update plugins"); + + public static string FooterButton_InProgress => Loc.Localize("InstallerInProgress", "Install in progress..."); + + public static string FooterButton_NoUpdates => Loc.Localize("InstallerNoUpdates", "No updates found!"); + + public static string FooterButton_UpdateComplete(int count) => Loc.Localize("InstallerUpdateComplete", "{0} plugins updated!").Format(count); + + public static string FooterButton_Settings => Loc.Localize("InstallerSettings", "Settings"); + + public static string FooterButton_ScanDevPlugins => Loc.Localize("InstallerScanDevPlugins", "Scan Dev Plugins"); + + public static string FooterButton_Close => Loc.Localize("InstallerClose", "Close"); + + #endregion + + #region Error modal + + public static string ErrorModal_Title => Loc.Localize("InstallerError", "Installer Error"); + + public static string ErrorModal_InstallFail(string name) => Loc.Localize("InstallerInstallFail", "Failed to install plugin {0}.").Format(name); + + public static string ErrorModal_EnableFail(string name) => Loc.Localize("InstallerEnableFail", "Failed to enable plugin {0}.").Format(name); + + public static string ErrorModal_DisableFail(string name) => Loc.Localize("InstallerDisableFail", "Failed to disable plugin {0}.").Format(name); + + public static string ErrorModal_UnloadFail(string name) => Loc.Localize("InstallerUnloadFail", "Failed to unload plugin {0}.").Format(name); + + public static string ErrorModal_LoadFail(string name) => Loc.Localize("InstallerLoadFail", "Failed to load plugin {0}.").Format(name); + + public static string ErrorModal_DeleteFail(string name) => Loc.Localize("InstallerDeleteFail", "Failed to delete plugin {0}.").Format(name); + + public static string ErrorModal_UpdaterFatal => Loc.Localize("InstallerUpdaterFatal", "Failed to update plugins."); + + public static string ErrorModal_UpdaterFail(int failCount) => Loc.Localize("InstallerUpdaterFail", "Failed to update {0} plugins.").Format(failCount); + + public static string ErrorModal_UpdaterFailPartial(int successCount, int failCount) => Loc.Localize("InstallerUpdaterFailPartial", "Updated {0} plugins, failed to update {1}.").Format(successCount, failCount); + + public static string ErrorModal_HintBlame(string plugins) => Loc.Localize("InstallerErrorPluginInfo", "\n\nThe following plugins caused these issues:\n\n{0}\nYou may try removing these plugins manually and reinstalling them.").Format(plugins); + + // public static string ErrorModal_Hint => Loc.Localize("InstallerErrorHint", "The plugin installer ran into an issue or the plugin is incompatible.\nPlease restart the game and report this error on our discord."); + + #endregion + + #region Plugin Update chatbox + + public static string PluginUpdateHeader_Chatbox => Loc.Localize("DalamudPluginUpdates", "Updates:"); + + #endregion + + #region Error modal buttons + + public static string ErrorModalButton_Ok => Loc.Localize("OK", "OK"); + + #endregion + } + } +} diff --git a/Dalamud/Interface/DalamudPluginStatWindow.cs b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs similarity index 75% rename from Dalamud/Interface/DalamudPluginStatWindow.cs rename to Dalamud/Interface/Internal/Windows/PluginStatWindow.cs index fca07d31b..cdb60d9b0 100644 --- a/Dalamud/Interface/DalamudPluginStatWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs @@ -3,29 +3,30 @@ using System.Linq; using System.Reflection; using Dalamud.Game.Internal; -using Dalamud.Hooking; +using Dalamud.Hooking.Internal; using Dalamud.Interface.Windowing; -using Dalamud.Plugin; +using Dalamud.Plugin.Internal; +using Dalamud.Plugin.Internal.Types; using ImGuiNET; -namespace Dalamud.Interface +namespace Dalamud.Interface.Internal.Windows { /// /// This window displays plugin statistics for troubleshooting. /// - internal class DalamudPluginStatWindow : Window + internal class PluginStatWindow : Window { private readonly PluginManager pluginManager; private bool showDalamudHooks; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The PluginManager instance. - public DalamudPluginStatWindow(PluginManager pluginManager) + /// The Dalamud instance. + public PluginStatWindow(Dalamud dalamud) : base("Plugin Statistics###DalamudPluginStatWindow") { - this.pluginManager = pluginManager; + this.pluginManager = dalamud.PluginManager; } /// @@ -47,11 +48,11 @@ namespace Dalamud.Interface ImGui.SameLine(); if (ImGui.Button("Reset")) { - foreach (var a in this.pluginManager.Plugins) + foreach (var plugin in this.pluginManager.InstalledPlugins) { - a.PluginInterface.UiBuilder.LastDrawTime = -1; - a.PluginInterface.UiBuilder.MaxDrawTime = -1; - a.PluginInterface.UiBuilder.DrawTimeHistory.Clear(); + plugin.DalamudInterface.UiBuilder.LastDrawTime = -1; + plugin.DalamudInterface.UiBuilder.MaxDrawTime = -1; + plugin.DalamudInterface.UiBuilder.DrawTimeHistory.Clear(); } } @@ -60,26 +61,35 @@ namespace Dalamud.Interface ImGui.SetColumnWidth(1, 100f); ImGui.SetColumnWidth(2, 100f); ImGui.SetColumnWidth(3, 100f); + ImGui.Text("Plugin"); ImGui.NextColumn(); + ImGui.Text("Last"); ImGui.NextColumn(); + ImGui.Text("Longest"); ImGui.NextColumn(); + ImGui.Text("Average"); ImGui.NextColumn(); + ImGui.Separator(); - foreach (var a in this.pluginManager.Plugins) + + foreach (var plugin in this.pluginManager.InstalledPlugins.Where(plugin => plugin.State == PluginState.Loaded)) { - ImGui.Text(a.Definition.Name); + ImGui.Text(plugin.Manifest.Name); ImGui.NextColumn(); - ImGui.Text($"{a.PluginInterface.UiBuilder.LastDrawTime / 10000f:F4}ms"); + + ImGui.Text($"{plugin.DalamudInterface.UiBuilder.LastDrawTime / 10000f:F4}ms"); ImGui.NextColumn(); - ImGui.Text($"{a.PluginInterface.UiBuilder.MaxDrawTime / 10000f:F4}ms"); + + ImGui.Text($"{plugin.DalamudInterface.UiBuilder.MaxDrawTime / 10000f:F4}ms"); ImGui.NextColumn(); - if (a.PluginInterface.UiBuilder.DrawTimeHistory.Count > 0) + + if (plugin.DalamudInterface.UiBuilder.DrawTimeHistory.Count > 0) { - ImGui.Text($"{a.PluginInterface.UiBuilder.DrawTimeHistory.Average() / 10000f:F4}ms"); + ImGui.Text($"{plugin.DalamudInterface.UiBuilder.DrawTimeHistory.Average() / 10000f:F4}ms"); } else { @@ -113,33 +123,46 @@ namespace Dalamud.Interface } ImGui.Columns(4); + ImGui.SetColumnWidth(0, ImGui.GetWindowContentRegionWidth() - 300); ImGui.SetColumnWidth(1, 100f); ImGui.SetColumnWidth(2, 100f); ImGui.SetColumnWidth(3, 100f); + ImGui.Text("Method"); ImGui.NextColumn(); + ImGui.Text("Last"); ImGui.NextColumn(); + ImGui.Text("Longest"); ImGui.NextColumn(); + ImGui.Text("Average"); ImGui.NextColumn(); + ImGui.Separator(); ImGui.Separator(); foreach (var handlerHistory in Framework.StatsHistory) { - if (handlerHistory.Value.Count == 0) continue; + if (handlerHistory.Value.Count == 0) + continue; + ImGui.SameLine(); + ImGui.Text($"{handlerHistory.Key}"); ImGui.NextColumn(); + ImGui.Text($"{handlerHistory.Value.Last():F4}ms"); ImGui.NextColumn(); + ImGui.Text($"{handlerHistory.Value.Max():F4}ms"); ImGui.NextColumn(); + ImGui.Text($"{handlerHistory.Value.Average():F4}ms"); ImGui.NextColumn(); + ImGui.Separator(); } @@ -152,27 +175,35 @@ namespace Dalamud.Interface if (ImGui.BeginTabItem("Hooks")) { ImGui.Columns(3); + ImGui.SetColumnWidth(0, ImGui.GetWindowContentRegionWidth() - 280); ImGui.SetColumnWidth(1, 180f); ImGui.SetColumnWidth(2, 100f); + ImGui.Text("Detour Method"); ImGui.SameLine(); + ImGui.Text(" "); ImGui.SameLine(); + ImGui.Checkbox("Show Dalamud Hooks ###showDalamudHooksCheckbox", ref this.showDalamudHooks); ImGui.NextColumn(); + ImGui.Text("Address"); ImGui.NextColumn(); + ImGui.Text("Status"); ImGui.NextColumn(); + ImGui.Separator(); ImGui.Separator(); - foreach (var trackedHook in HookInfo.TrackedHooks) + foreach (var trackedHook in HookManager.TrackedHooks) { try { - if (!this.showDalamudHooks && trackedHook.Assembly == Assembly.GetExecutingAssembly()) continue; + if (!this.showDalamudHooks && trackedHook.Assembly == Assembly.GetExecutingAssembly()) + continue; ImGui.Text($"{trackedHook.Delegate.Target} :: {trackedHook.Delegate.Method.Name}"); ImGui.TextDisabled(trackedHook.Assembly.FullName); @@ -180,13 +211,19 @@ namespace Dalamud.Interface if (!trackedHook.Hook.IsDisposed) { ImGui.Text($"{trackedHook.Hook.Address.ToInt64():X}"); - if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"{trackedHook.Hook.Address.ToInt64():X}"); + if (ImGui.IsItemClicked()) + { + ImGui.SetClipboardText($"{trackedHook.Hook.Address.ToInt64():X}"); + } var processMemoryOffset = trackedHook.InProcessMemory; if (processMemoryOffset.HasValue) { ImGui.Text($"ffxiv_dx11.exe + {processMemoryOffset:X}"); - if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"ffxiv_dx11.exe+{processMemoryOffset:X}"); + if (ImGui.IsItemClicked()) + { + ImGui.SetClipboardText($"ffxiv_dx11.exe+{processMemoryOffset:X}"); + } } } @@ -218,7 +255,7 @@ namespace Dalamud.Interface if (ImGui.IsWindowAppearing()) { - HookInfo.TrackedHooks.RemoveAll(h => h.Hook.IsDisposed); + HookManager.TrackedHooks.RemoveAll(h => h.Hook.IsDisposed); } ImGui.EndTabBar(); diff --git a/Dalamud/Interface/Scratchpad/ScratchpadWindow.cs b/Dalamud/Interface/Internal/Windows/ScratchpadWindow.cs similarity index 95% rename from Dalamud/Interface/Scratchpad/ScratchpadWindow.cs rename to Dalamud/Interface/Internal/Windows/ScratchpadWindow.cs index 91324117c..198ac0cf6 100644 --- a/Dalamud/Interface/Scratchpad/ScratchpadWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ScratchpadWindow.cs @@ -4,11 +4,12 @@ using System.Linq; using System.Numerics; using Dalamud.Interface.Colors; +using Dalamud.Interface.Internal.Scratchpad; using Dalamud.Interface.Windowing; using ImGuiNET; using Serilog; -namespace Dalamud.Interface.Scratchpad +namespace Dalamud.Interface.Internal.Windows { /// /// This class facilitates interacting with the ScratchPad window. @@ -16,8 +17,8 @@ namespace Dalamud.Interface.Scratchpad internal class ScratchpadWindow : Window, IDisposable { private readonly Dalamud dalamud; - private List documents = new(); - private ScratchFileWatcher watcher = new(); + private readonly List documents = new(); + private readonly ScratchFileWatcher watcher = new(); private string pathInput = string.Empty; /// @@ -138,7 +139,7 @@ namespace Dalamud.Interface.Scratchpad if (ImGui.Button("Toggle Log")) { - this.dalamud.DalamudUi.ToggleLog(); + this.dalamud.DalamudUi.ToggleLogWindow(); } ImGui.SameLine(); diff --git a/Dalamud/Interface/DalamudSettingsWindow.cs b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs similarity index 89% rename from Dalamud/Interface/DalamudSettingsWindow.cs rename to Dalamud/Interface/Internal/Windows/SettingsWindow.cs index 544e14b67..4e6812328 100644 --- a/Dalamud/Interface/DalamudSettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs @@ -9,24 +9,24 @@ using CheapLoc; using Dalamud.Configuration; using Dalamud.Game.Text; using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; using Dalamud.Interface.Windowing; using ImGuiNET; -using Serilog; -namespace Dalamud.Interface +namespace Dalamud.Interface.Internal.Windows { /// /// The window that allows for general configuration of Dalamud itself. /// - internal class DalamudSettingsWindow : Window + internal class SettingsWindow : Window { private const float MinScale = 0.3f; private const float MaxScale = 2.0f; private readonly Dalamud dalamud; - private string[] languages; - private string[] locLanguages; + private readonly string[] languages; + private readonly string[] locLanguages; private int langIndex; private Vector4 hintTextColor = ImGuiColors.DalamudGrey; @@ -44,7 +44,7 @@ namespace Dalamud.Interface private bool doDocking; private bool doViewport; private bool doGamepad; - private List thirdRepoList; + private List thirdRepoList; private bool printPluginsWelcomeMsg; private bool autoUpdatePlugins; @@ -60,10 +60,10 @@ namespace Dalamud.Interface #endregion /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The Dalamud Instance. - public DalamudSettingsWindow(Dalamud dalamud) + public SettingsWindow(Dalamud dalamud) : base(Loc.Localize("DalamudSettingsHeader", "Dalamud Settings") + "###XlSettings2", ImGuiWindowFlags.NoCollapse) { this.dalamud = dalamud; @@ -142,31 +142,21 @@ namespace Dalamud.Interface /// public override void OnOpen() { - base.OnOpen(); - - Log.Information("OnOpen start"); - - Log.Information("OnOpen end"); } /// public override void OnClose() { - base.OnClose(); - - Log.Information("OnClose start"); - ImGui.GetIO().FontGlobalScale = this.dalamud.Configuration.GlobalUiScale; - this.thirdRepoList = this.dalamud.Configuration.ThirdRepoList.Select(x => x.Clone()).ToList(); - Log.Information("OnClose end"); + this.thirdRepoList = this.dalamud.Configuration.ThirdRepoList.Select(x => x.Clone()).ToList(); } /// public override void Draw() { var windowSize = ImGui.GetWindowSize(); - ImGui.BeginChild("scrolling", new Vector2(windowSize.X - 5 - (5 * ImGui.GetIO().FontGlobalScale), windowSize.Y - 35 - (35 * ImGui.GetIO().FontGlobalScale)), false, ImGuiWindowFlags.HorizontalScrollbar); + ImGui.BeginChild("scrolling", new Vector2(windowSize.X - 5 - (5 * ImGuiHelpers.GlobalScale), windowSize.Y - 35 - (35 * ImGuiHelpers.GlobalScale)), false, ImGuiWindowFlags.HorizontalScrollbar); if (ImGui.BeginTabBar("SetTabBar")) { @@ -176,7 +166,7 @@ namespace Dalamud.Interface ImGui.Combo("##XlLangCombo", ref this.langIndex, this.locLanguages, this.locLanguages.Length); ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsLanguageHint", "Select the language Dalamud will be displayed in.")); - ImGui.Dummy(new Vector2(5f, 5f) * ImGui.GetIO().FontGlobalScale); + ImGuiHelpers.ScaledDummy(5); ImGui.Text(Loc.Localize("DalamudSettingsChannel", "General Chat Channel")); if (ImGui.BeginCombo("##XlChatTypeCombo", this.dalamudMessagesChatType.ToString())) @@ -194,7 +184,7 @@ namespace Dalamud.Interface ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsChannelHint", "Select the chat channel that is to be used for general Dalamud messages.")); - ImGui.Dummy(new Vector2(5f, 5f) * ImGui.GetIO().FontGlobalScale); + ImGuiHelpers.ScaledDummy(5); ImGui.Checkbox(Loc.Localize("DalamudSettingsFlash", "Flash FFXIV window on duty pop"), ref this.doCfTaskBarFlash); ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsFlashHint", "Flash the FFXIV window in your task bar when a duty is ready.")); @@ -231,7 +221,7 @@ namespace Dalamud.Interface ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsGlobalUiScaleHint", "Scale all XIVLauncher UI elements - useful for 4K displays.")); - ImGui.Dummy(new Vector2(10f, 16f) * ImGui.GetIO().FontGlobalScale); + ImGuiHelpers.ScaledDummy(10, 16); ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingToggleUiHideOptOutNote", "Plugins may independently opt out of the settings below.")); @@ -244,7 +234,7 @@ namespace Dalamud.Interface ImGui.Checkbox(Loc.Localize("DalamudSettingToggleUiHideDuringGpose", "Hide plugin UI while gpose is active"), ref this.doToggleUiHideDuringGpose); ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingToggleUiHideDuringGposeHint", "Hide any open windows by plugins while gpose is active.")); - ImGui.Dummy(new Vector2(10f, 16f) * ImGui.GetIO().FontGlobalScale); + ImGuiHelpers.ScaledDummy(10, 16); ImGui.Checkbox(Loc.Localize("DalamudSettingToggleViewports", "Enable multi-monitor windows"), ref this.doViewport); ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingToggleViewportsHint", "This will allow you move plugin windows onto other monitors.\nWill only work in Borderless Window or Windowed mode.")); @@ -264,27 +254,31 @@ namespace Dalamud.Interface ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsPluginTestHint", "Receive testing prereleases for plugins.")); ImGui.TextColored(this.warnTextColor, Loc.Localize("DalamudSettingsPluginTestWarning", "Testing plugins may not have been vetted before being published. Please only enable this if you are aware of the risks.")); - ImGui.Dummy(new Vector2(12f, 12f) * ImGui.GetIO().FontGlobalScale); + ImGuiHelpers.ScaledDummy(12); if (ImGui.Button(Loc.Localize("DalamudSettingsClearHidden", "Clear hidden plugins"))) + { this.dalamud.Configuration.HiddenPluginInternalName.Clear(); + this.dalamud.PluginManager.RefilterPluginMasters(); + } + ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsClearHiddenHint", "Restore plugins you have previously hidden from the plugin installer.")); - ImGui.Dummy(new Vector2(12f, 12f) * ImGui.GetIO().FontGlobalScale); + ImGuiHelpers.ScaledDummy(12); - ImGui.Dummy(new Vector2(12f, 12f) * ImGui.GetIO().FontGlobalScale); + ImGuiHelpers.ScaledDummy(12); ImGui.Text(Loc.Localize("DalamudSettingsCustomRepo", "Custom Plugin Repositories")); ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingCustomRepoHint", "Add custom plugin repositories.")); ImGui.TextColored(this.warnTextColor, Loc.Localize("DalamudSettingCustomRepoWarning", "We cannot take any responsibility for third-party plugins and repositories.\nTake care when installing third-party plugins from untrusted sources.")); - ImGui.Dummy(new Vector2(5f, 5f) * ImGui.GetIO().FontGlobalScale); + ImGuiHelpers.ScaledDummy(5); ImGui.Columns(4); - ImGui.SetColumnWidth(0, 18 + (5 * ImGui.GetIO().FontGlobalScale)); - ImGui.SetColumnWidth(1, ImGui.GetWindowWidth() - (18 + 16 + 14) - ((5 + 45 + 26) * ImGui.GetIO().FontGlobalScale)); - ImGui.SetColumnWidth(2, 16 + (45 * ImGui.GetIO().FontGlobalScale)); - ImGui.SetColumnWidth(3, 14 + (26 * ImGui.GetIO().FontGlobalScale)); + ImGui.SetColumnWidth(0, 18 + (5 * ImGuiHelpers.GlobalScale)); + ImGui.SetColumnWidth(1, ImGui.GetWindowWidth() - (18 + 16 + 14) - ((5 + 45 + 26) * ImGuiHelpers.GlobalScale)); + ImGui.SetColumnWidth(2, 16 + (45 * ImGuiHelpers.GlobalScale)); + ImGui.SetColumnWidth(3, 14 + (26 * ImGuiHelpers.GlobalScale)); ImGui.Separator(); @@ -307,7 +301,7 @@ namespace Dalamud.Interface ImGui.NextColumn(); ImGui.Separator(); - ThirdRepoSetting toRemove = null; + ThirdPartyRepoSettings toRemove = null; var repoNumber = 1; foreach (var thirdRepoSetting in this.thirdRepoList) @@ -323,17 +317,15 @@ namespace Dalamud.Interface ImGui.TextWrapped(thirdRepoSetting.Url); ImGui.NextColumn(); - ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (ImGui.GetColumnWidth() / 2) - 7 - (12 * ImGui.GetIO().FontGlobalScale)); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (ImGui.GetColumnWidth() / 2) - 7 - (12 * ImGuiHelpers.GlobalScale)); ImGui.Checkbox("##thirdRepoCheck", ref isEnabled); ImGui.NextColumn(); - ImGui.PushFont(InterfaceManager.IconFont); - if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString())) + if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash)) { toRemove = thirdRepoSetting; } - ImGui.PopFont(); ImGui.NextColumn(); ImGui.Separator(); @@ -353,9 +345,9 @@ namespace Dalamud.Interface ImGui.SetNextItemWidth(-1); ImGui.InputText("##thirdRepoUrlInput", ref this.thirdRepoTempUrl, 300); ImGui.NextColumn(); + // Enabled button ImGui.NextColumn(); - ImGui.PushFont(InterfaceManager.IconFont); - if (!string.IsNullOrEmpty(this.thirdRepoTempUrl) && ImGui.Button(FontAwesomeIcon.Plus.ToIconString())) + if (!string.IsNullOrEmpty(this.thirdRepoTempUrl) && ImGuiComponents.IconButton(FontAwesomeIcon.Plus)) { if (this.thirdRepoList.Any(r => string.Equals(r.Url, this.thirdRepoTempUrl, StringComparison.InvariantCultureIgnoreCase))) { @@ -364,7 +356,7 @@ namespace Dalamud.Interface } else { - this.thirdRepoList.Add(new ThirdRepoSetting + this.thirdRepoList.Add(new ThirdPartyRepoSettings { Url = this.thirdRepoTempUrl, IsEnabled = true, @@ -374,7 +366,6 @@ namespace Dalamud.Interface } } - ImGui.PopFont(); ImGui.Columns(1); ImGui.EndTabItem(); @@ -456,7 +447,7 @@ namespace Dalamud.Interface this.dalamud.Configuration.Save(); - this.dalamud.PluginRepository.ReloadPluginMasterAsync(); + this.dalamud.PluginManager.ReloadPluginMasters(); } } } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 81067678b..59e4f75c4 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using Dalamud.Game.ClientState; +using Dalamud.Interface.Internal; using ImGuiNET; using ImGuiScene; using Serilog; @@ -13,11 +15,11 @@ namespace Dalamud.Interface /// This class represents the Dalamud UI that is drawn on top of the game. /// It can be used to draw custom windows and overlays. /// - public class UiBuilder : IDisposable + public sealed class UiBuilder : IDisposable { - private readonly string namespaceName; private readonly Dalamud dalamud; - private readonly System.Diagnostics.Stopwatch stopwatch; + private readonly Stopwatch stopwatch; + private readonly string namespaceName; private bool hasErrorWindow; @@ -29,11 +31,11 @@ namespace Dalamud.Interface /// The plugin namespace. internal UiBuilder(Dalamud dalamud, string namespaceName) { + this.dalamud = dalamud; + this.stopwatch = new Stopwatch(); this.namespaceName = namespaceName; - this.dalamud = dalamud; this.dalamud.InterfaceManager.OnDraw += this.OnDraw; - this.stopwatch = new System.Diagnostics.Stopwatch(); } /// @@ -217,8 +219,7 @@ namespace Dalamud.Interface if (this.hasErrorWindow && ImGui.Begin($"{this.namespaceName} Error", ref this.hasErrorWindow, ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize)) { - ImGui.Text( - $"The plugin {this.namespaceName} ran into an error.\nContact the plugin developer for support.\n\nPlease try restarting your game."); + ImGui.Text($"The plugin {this.namespaceName} ran into an error.\nContact the plugin developer for support.\n\nPlease try restarting your game."); ImGui.Spacing(); if (ImGui.Button("OK")) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index bd3c55bd5..6a15ef9d5 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -143,9 +143,6 @@ namespace Dalamud.Interface.Windowing /// internal void DrawInternal() { - // if (WindowName.Contains("Credits")) - // Log.Information($"Draw: {IsOpen} {this.internalIsOpen} {this.internalLastIsOpen}"); - if (!this.IsOpen) { if (this.internalIsOpen != this.internalLastIsOpen) diff --git a/Dalamud/Localization.cs b/Dalamud/Localization.cs index 3469dcb64..5c7255a95 100644 --- a/Dalamud/Localization.cs +++ b/Dalamud/Localization.cs @@ -112,6 +112,7 @@ namespace Dalamud } this.OnLocalizationChanged?.Invoke(langCode); + try { Loc.Setup(this.ReadLocData(langCode), this.assembly); @@ -135,9 +136,10 @@ namespace Dalamud { if (this.useEmbedded) { - var resourceStream = - this.assembly.GetManifestResourceStream($"{this.locResourceDirectory}{this.locResourcePrefix}{langCode}.json"); - if (resourceStream == null) return null; + var resourceStream = this.assembly.GetManifestResourceStream($"{this.locResourceDirectory}{this.locResourcePrefix}{langCode}.json"); + if (resourceStream == null) + return null; + using var reader = new StreamReader(resourceStream); return reader.ReadToEnd(); } diff --git a/Dalamud/Memory/Exceptions/MemoryAllocationException.cs b/Dalamud/Memory/Exceptions/MemoryAllocationException.cs new file mode 100644 index 000000000..e289c8782 --- /dev/null +++ b/Dalamud/Memory/Exceptions/MemoryAllocationException.cs @@ -0,0 +1,47 @@ +using System; +using System.Runtime.Serialization; + +namespace Dalamud.Memory.Exceptions +{ + /// + /// An exception thrown when VirtualAlloc fails. + /// + public class MemoryAllocationException : MemoryException + { + /// + /// Initializes a new instance of the class. + /// + public MemoryAllocationException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public MemoryAllocationException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + public MemoryAllocationException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The object that holds the serialized data about the exception being thrown. + /// The object that contains contextual information about the source or destination. + protected MemoryAllocationException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/Dalamud/Memory/Exceptions/MemoryException.cs b/Dalamud/Memory/Exceptions/MemoryException.cs new file mode 100644 index 000000000..810e76404 --- /dev/null +++ b/Dalamud/Memory/Exceptions/MemoryException.cs @@ -0,0 +1,47 @@ +using System; +using System.Runtime.Serialization; + +namespace Dalamud.Memory.Exceptions +{ + /// + /// The base exception when thrown from Dalamud.Memory. + /// + public abstract class MemoryException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public MemoryException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public MemoryException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + public MemoryException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The object that holds the serialized data about the exception being thrown. + /// The object that contains contextual information about the source or destination. + protected MemoryException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/Dalamud/Memory/Exceptions/MemoryPermissionException.cs b/Dalamud/Memory/Exceptions/MemoryPermissionException.cs new file mode 100644 index 000000000..e94ccc0ce --- /dev/null +++ b/Dalamud/Memory/Exceptions/MemoryPermissionException.cs @@ -0,0 +1,47 @@ +using System; +using System.Runtime.Serialization; + +namespace Dalamud.Memory.Exceptions +{ + /// + /// An exception thrown when VirtualProtect fails. + /// + public class MemoryPermissionException : MemoryException + { + /// + /// Initializes a new instance of the class. + /// + public MemoryPermissionException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public MemoryPermissionException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + public MemoryPermissionException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The object that holds the serialized data about the exception being thrown. + /// The object that contains contextual information about the source or destination. + protected MemoryPermissionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/Dalamud/Memory/Exceptions/MemoryReadException.cs b/Dalamud/Memory/Exceptions/MemoryReadException.cs new file mode 100644 index 000000000..7f3f4b1f2 --- /dev/null +++ b/Dalamud/Memory/Exceptions/MemoryReadException.cs @@ -0,0 +1,47 @@ +using System; +using System.Runtime.Serialization; + +namespace Dalamud.Memory.Exceptions +{ + /// + /// An exception thrown when ReadProcessMemory fails. + /// + public class MemoryReadException : MemoryException + { + /// + /// Initializes a new instance of the class. + /// + public MemoryReadException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public MemoryReadException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + public MemoryReadException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The object that holds the serialized data about the exception being thrown. + /// The object that contains contextual information about the source or destination. + protected MemoryReadException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/Dalamud/Memory/Exceptions/MemoryWriteException.cs b/Dalamud/Memory/Exceptions/MemoryWriteException.cs new file mode 100644 index 000000000..5aadcee53 --- /dev/null +++ b/Dalamud/Memory/Exceptions/MemoryWriteException.cs @@ -0,0 +1,47 @@ +using System; +using System.Runtime.Serialization; + +namespace Dalamud.Memory.Exceptions +{ + /// + /// An exception thrown when WriteProcessMemory fails. + /// + public class MemoryWriteException : MemoryException + { + /// + /// Initializes a new instance of the class. + /// + public MemoryWriteException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public MemoryWriteException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + public MemoryWriteException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The object that holds the serialized data about the exception being thrown. + /// The object that contains contextual information about the source or destination. + protected MemoryWriteException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/Dalamud/Memory/MemoryHelper.cs b/Dalamud/Memory/MemoryHelper.cs new file mode 100644 index 000000000..f47f48263 --- /dev/null +++ b/Dalamud/Memory/MemoryHelper.cs @@ -0,0 +1,610 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Memory.Exceptions; + +using static Dalamud.NativeFunctions; + +// Heavily inspired from Reloaded (https://github.com/Reloaded-Project/Reloaded.Memory) + +namespace Dalamud.Memory +{ + /// + /// A simple class that provides read/write access to arbitrary memory. + /// + public static unsafe class MemoryHelper + { + private static SeStringManager seStringManager; + private static IntPtr handle; + + #region Read + + /// + /// Reads a generic type from a specified memory address. + /// + /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. + /// The memory address to read from. + /// The read in struct. + public static T Read(IntPtr memoryAddress) where T : unmanaged + => Read(memoryAddress, false); + + /// + /// Reads a generic type from a specified memory address. + /// + /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. + /// The memory address to read from. + /// Set this to true to enable struct marshalling. + /// The read in struct. + public static T Read(IntPtr memoryAddress, bool marshal) + { + return marshal + ? Marshal.PtrToStructure(memoryAddress) + : Unsafe.Read((void*)memoryAddress); + } + + /// + /// Reads a generic type from a specified memory address. + /// + /// The memory address to read from. + /// The amount of bytes to read starting from the memoryAddress. + /// The read in struct. + public static byte[] ReadRaw(IntPtr memoryAddress, int length) + { + var value = new byte[length]; + Marshal.Copy(memoryAddress, value, 0, value.Length); + return value; + } + + /// + /// Reads a generic type array from a specified memory address. + /// + /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. + /// The memory address to read from. + /// The amount of array items to read. + /// The read in struct array. + public static T[] Read(IntPtr memoryAddress, int arrayLength) where T : unmanaged + => Read(memoryAddress, arrayLength, false); + + /// + /// Reads a generic type array from a specified memory address. + /// + /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. + /// The memory address to read from. + /// The amount of array items to read. + /// Set this to true to enable struct marshalling. + /// The read in struct array. + public static T[] Read(IntPtr memoryAddress, int arrayLength, bool marshal) + { + var structSize = SizeOf(); + var value = new T[arrayLength]; + + for (var i = 0; i < arrayLength; i++) + { + var address = memoryAddress + (structSize * i); + Read(address, out T result, marshal); + value[i] = result; + } + + return value; + } + + #endregion + + #region Read(out) + + /// + /// Reads a generic type from a specified memory address. + /// + /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. + /// The memory address to read from. + /// Local variable to receive the read in struct. + public static void Read(IntPtr memoryAddress, out T value) where T : unmanaged + => value = Read(memoryAddress); + + /// + /// Reads a generic type from a specified memory address. + /// + /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. + /// The memory address to read from. + /// Local variable to receive the read in struct. + /// Set this to true to enable struct marshalling. + public static void Read(IntPtr memoryAddress, out T value, bool marshal) + => value = Read(memoryAddress, marshal); + + /// + /// Reads raw data from a specified memory address. + /// + /// The memory address to read from. + /// The amount of bytes to read starting from the memoryAddress. + /// Local variable to receive the read in bytes. + public static void ReadRaw(IntPtr memoryAddress, int length, out byte[] value) + => value = ReadRaw(memoryAddress, length); + + /// + /// Reads a generic type array from a specified memory address. + /// + /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. + /// The memory address to read from. + /// The amount of array items to read. + /// The read in struct array. + public static void Read(IntPtr memoryAddress, int arrayLength, out T[] value) where T : unmanaged + => value = Read(memoryAddress, arrayLength); + + /// + /// Reads a generic type array from a specified memory address. + /// + /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. + /// The memory address to read from. + /// The amount of array items to read. + /// Set this to true to enable struct marshalling. + /// The read in struct array. + public static void Read(IntPtr memoryAddress, int arrayLength, bool marshal, out T[] value) + => value = Read(memoryAddress, arrayLength, marshal); + + #endregion + + #region ReadString + + /// + /// Read a UTF-8 encoded string from a specified memory address. + /// + /// + /// Attention! If this is an SeString, use the to decode or the applicable helper method. + /// + /// The memory address to read from. + /// The read in string. + public static string ReadString(IntPtr memoryAddress) + => ReadString(memoryAddress, 256); + + /// + /// Read a UTF-8 encoded string from a specified memory address. + /// + /// + /// Attention! If this is an SeString, use the to decode or the applicable helper method. + /// + /// The memory address to read from. + /// The maximum length of the string. + /// The read in string. + public static string ReadString(IntPtr memoryAddress, int maxLength) + => ReadString(memoryAddress, Encoding.UTF8, maxLength); + + /// + /// Read a string with the given encoding from a specified memory address. + /// + /// + /// Attention! If this is an SeString, use the to decode or the applicable helper method. + /// + /// The memory address to read from. + /// The encoding to use to decode the string. + /// The read in string. + public static string ReadString(IntPtr memoryAddress, Encoding encoding) + => ReadString(memoryAddress, encoding, 256); + + /// + /// Read a string with the given encoding from a specified memory address. + /// + /// + /// Attention! If this is an SeString, use the to decode or the applicable helper method. + /// + /// The memory address to read from. + /// The encoding to use to decode the string. + /// The maximum length of the string. + /// The read in string. + public static string ReadString(IntPtr memoryAddress, Encoding encoding, int maxLength) + { + if (maxLength <= 0) + return string.Empty; + + ReadRaw(memoryAddress, maxLength, out var buffer); + + var data = encoding.GetString(buffer); + var eosPos = data.IndexOf('\0'); + return eosPos >= 0 ? data.Substring(0, eosPos) : data; + } + + /// + /// Read an SeString from a specified memory address. + /// + /// The memory address to read from. + /// The read in string. + public static SeString ReadSeString(IntPtr memoryAddress) + => ReadSeString(memoryAddress, 256); + + /// + /// Read an SeString from a specified memory address. + /// + /// The memory address to read from. + /// The maximum length of the string. + /// The read in string. + public static SeString ReadSeString(IntPtr memoryAddress, int maxLength) + { + ReadRaw(memoryAddress, maxLength, out var buffer); + return seStringManager.Parse(buffer); + } + + #endregion + + #region ReadString(out) + + /// + /// Read a UTF-8 encoded string from a specified memory address. + /// + /// + /// Attention! If this is an SeString, use the to decode or the applicable helper method. + /// + /// The memory address to read from. + /// The read in string. + public static void ReadString(IntPtr memoryAddress, out string value) + => value = ReadString(memoryAddress); + + /// + /// Read a UTF-8 encoded string from a specified memory address. + /// + /// + /// Attention! If this is an SeString, use the to decode or the applicable helper method. + /// + /// The memory address to read from. + /// The read in string. + /// The maximum length of the string. + public static void ReadString(IntPtr memoryAddress, out string value, int maxLength) + => value = ReadString(memoryAddress, maxLength); + + /// + /// Read a string with the given encoding from a specified memory address. + /// + /// + /// Attention! If this is an SeString, use the to decode or the applicable helper method. + /// + /// The memory address to read from. + /// The encoding to use to decode the string. + /// The read in string. + public static void ReadString(IntPtr memoryAddress, Encoding encoding, out string value) + => value = ReadString(memoryAddress, encoding); + + /// + /// Read a string with the given encoding from a specified memory address. + /// + /// + /// Attention! If this is an SeString, use the to decode or the applicable helper method. + /// + /// The memory address to read from. + /// The encoding to use to decode the string. + /// The maximum length of the string. + /// The read in string. + public static void ReadString(IntPtr memoryAddress, Encoding encoding, int maxLength, out string value) + => value = ReadString(memoryAddress, encoding, maxLength); + + /// + /// Read an SeString from a specified memory address. + /// + /// The memory address to read from. + /// The read in SeString. + public static void ReadSeString(IntPtr memoryAddress, out SeString value) + => value = ReadSeString(memoryAddress); + + /// + /// Read an SeString from a specified memory address. + /// + /// The memory address to read from. + /// The maximum length of the string. + /// The read in SeString. + public static void ReadSeString(IntPtr memoryAddress, int maxLength, out SeString value) + => value = ReadSeString(memoryAddress, maxLength); + + #endregion + + #region Write + + /// + /// Writes a generic type to a specified memory address. + /// + /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. + /// The memory address to read from. + /// The item to write to the address. + public static void Write(IntPtr memoryAddress, T item) where T : unmanaged + => Write(memoryAddress, item); + + /// + /// Writes a generic type to a specified memory address. + /// + /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. + /// The memory address to read from. + /// The item to write to the address. + /// Set this to true to enable struct marshalling. + public static void Write(IntPtr memoryAddress, T item, bool marshal) + { + if (marshal) + Marshal.StructureToPtr(item, memoryAddress, false); + else + Unsafe.Write((void*)memoryAddress, item); + } + + /// + /// Writes raw data to a specified memory address. + /// + /// The memory address to read from. + /// The bytes to write to memoryAddress. + public static void WriteRaw(IntPtr memoryAddress, byte[] data) + { + Marshal.Copy(data, 0, memoryAddress, data.Length); + } + + /// + /// Writes a generic type array to a specified memory address. + /// + /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. + /// The memory address to write to. + /// The array of items to write to the address. + public static void Write(IntPtr memoryAddress, T[] items) where T : unmanaged + => Write(memoryAddress, items, false); + + /// + /// Writes a generic type array to a specified memory address. + /// + /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. + /// The memory address to write to. + /// The array of items to write to the address. + /// Set this to true to enable struct marshalling. + public static void Write(IntPtr memoryAddress, T[] items, bool marshal) + { + var structSize = SizeOf(marshal); + + for (var i = 0; i < items.Length; i++) + { + var address = memoryAddress + (structSize * i); + Write(address, items[i], marshal); + } + } + + #endregion + + #region WriteString + + /// + /// Write a UTF-8 encoded string to a specified memory address. + /// + /// + /// Attention! If this is an SeString, use the to encode or the applicable helper method. + /// + /// The memory address to write to. + /// The string to write. + public static void WriteString(IntPtr memoryAddress, string value) + => WriteString(memoryAddress, value, Encoding.UTF8); + + /// + /// Write a string with the given encoding to a specified memory address. + /// + /// + /// Attention! If this is an SeString, use the to encode or the applicable helper method. + /// + /// The memory address to write to. + /// The string to write. + /// The encoding to use. + public static void WriteString(IntPtr memoryAddress, string value, Encoding encoding) + { + if (string.IsNullOrEmpty(value)) + return; + + var bytes = encoding.GetBytes(value + '\0'); + + WriteRaw(memoryAddress, bytes); + } + + /// + /// Write an SeString to a specified memory address. + /// + /// The memory address to write to. + /// The SeString to write. + public static void WriteSeString(IntPtr memoryAddress, SeString value) + { + if (value is null) + return; + + WriteRaw(memoryAddress, value.Encode()); + } + + #endregion + + #region ApiWrappers + + /// + /// Allocates fixed size of memory inside the target memory source via Windows API calls. + /// Returns the address of newly allocated memory. + /// + /// Amount of bytes to be allocated. + /// Address to the newly allocated memory. + public static IntPtr Allocate(int length) + { + var address = VirtualAlloc( + IntPtr.Zero, + (UIntPtr)length, + AllocationType.Commit | AllocationType.Reserve, + MemoryProtection.ExecuteReadWrite); + + if (address == IntPtr.Zero) + throw new MemoryAllocationException($"Unable to allocate {length} bytes."); + + return address; + } + + /// + /// Allocates fixed size of memory inside the target memory source via Windows API calls. + /// Returns the address of newly allocated memory. + /// + /// Amount of bytes to be allocated. + /// Address to the newly allocated memory. + public static void Allocate(int length, out IntPtr memoryAddress) + => memoryAddress = Allocate(length); + + /// + /// Frees memory previously allocated with Allocate via Windows API calls. + /// + /// The address of the memory to free. + /// True if the operation is successful. + public static bool Free(IntPtr memoryAddress) + { + return VirtualFree(memoryAddress, UIntPtr.Zero, AllocationType.Release); + } + + /// + /// Changes the page permissions for a specified combination of address and length via Windows API calls. + /// + /// The memory address for which to change page permissions for. + /// The region size for which to change permissions for. + /// The new permissions to set. + /// The old page permissions. + public static MemoryProtection ChangePermission(IntPtr memoryAddress, int length, MemoryProtection newPermissions) + { + var result = VirtualProtect(memoryAddress, (UIntPtr)length, newPermissions, out var oldPermissions); + + if (!result) + throw new MemoryPermissionException($"Unable to change permissions at 0x{memoryAddress.ToInt64():X} of length {length} and permission {newPermissions} (result={result})"); + + var last = Marshal.GetLastWin32Error(); + if (last > 0) + throw new MemoryPermissionException($"Unable to change permissions at 0x{memoryAddress.ToInt64():X} of length {length} and permission {newPermissions} (error={last})"); + + return oldPermissions; + } + + /// + /// Changes the page permissions for a specified combination of address and length via Windows API calls. + /// + /// The memory address for which to change page permissions for. + /// The region size for which to change permissions for. + /// The new permissions to set. + /// The old page permissions. + public static void ChangePermission(IntPtr memoryAddress, int length, MemoryProtection newPermissions, out MemoryProtection oldPermissions) + => oldPermissions = ChangePermission(memoryAddress, length, newPermissions); + + /// + /// Changes the page permissions for a specified combination of address and element from which to deduce size via Windows API calls. + /// + /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. + /// The memory address for which to change page permissions for. + /// The struct element from which the region size to change permissions for will be calculated. + /// The new permissions to set. + /// Set to true to calculate the size of the struct after marshalling instead of before. + /// The old page permissions. + public static MemoryProtection ChangePermission(IntPtr memoryAddress, ref T baseElement, MemoryProtection newPermissions, bool marshal) + => ChangePermission(memoryAddress, SizeOf(marshal), newPermissions); + + /// + /// Reads raw data from a specified memory address via Windows API calls. + /// This is noticably slower than Unsafe or Marshal. + /// + /// The memory address to read from. + /// The amount of bytes to read starting from the memoryAddress. + /// The read in bytes. + public static byte[] ReadProcessMemory(IntPtr memoryAddress, int length) + { + var value = new byte[length]; + ReadProcessMemory(memoryAddress, ref value); + return value; + } + + /// + /// Reads raw data from a specified memory address via Windows API calls. + /// This is noticably slower than Unsafe or Marshal. + /// + /// The memory address to read from. + /// The amount of bytes to read starting from the memoryAddress. + /// The read in bytes. + public static void ReadProcessMemory(IntPtr memoryAddress, int length, out byte[] value) + => value = ReadProcessMemory(memoryAddress, length); + + /// + /// Reads raw data from a specified memory address via Windows API calls. + /// This is noticably slower than Unsafe or Marshal. + /// + /// The memory address to read from. + /// The read in bytes. + public static void ReadProcessMemory(IntPtr memoryAddress, ref byte[] value) + { + var length = value.Length; + var result = NativeFunctions.ReadProcessMemory(handle, memoryAddress, value, length, out _); + + if (!result) + throw new MemoryReadException($"Unable to read memory at 0x{memoryAddress.ToInt64():X} of length {length} (result={result})"); + + var last = Marshal.GetLastWin32Error(); + if (last > 0) + throw new MemoryReadException($"Unable to read memory at 0x{memoryAddress.ToInt64():X} of length {length} (error={last})"); + } + + /// + /// Writes raw data to a specified memory address via Windows API calls. + /// This is noticably slower than Unsafe or Marshal. + /// + /// The memory address to write to. + /// The bytes to write to memoryAddress. + public static void WriteProcessMemory(IntPtr memoryAddress, byte[] data) + { + var length = data.Length; + var result = NativeFunctions.WriteProcessMemory(handle, memoryAddress, data, length, out _); + + if (!result) + throw new MemoryWriteException($"Unable to write memory at 0x{memoryAddress.ToInt64():X} of length {length} (result={result})"); + + var last = Marshal.GetLastWin32Error(); + if (last > 0) + throw new MemoryWriteException($"Unable to write memory at 0x{memoryAddress.ToInt64():X} of length {length} (error={last})"); + } + + #endregion + + #region Sizing + + /// + /// Returns the size of a specific primitive or struct type. + /// + /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. + /// The size of the primitive or struct. + public static int SizeOf() + => SizeOf(false); + + /// + /// Returns the size of a specific primitive or struct type. + /// + /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. + /// If set to true; will return the size of an element after marshalling. + /// The size of the primitive or struct. + public static int SizeOf(bool marshal) + => marshal ? Marshal.SizeOf() : Unsafe.SizeOf(); + + /// + /// Returns the size of a specific primitive or struct type. + /// + /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. + /// The number of array elements present. + /// The size of the primitive or struct array. + public static int SizeOf(int elementCount) where T : unmanaged + => SizeOf() * elementCount; + + /// + /// Returns the size of a specific primitive or struct type. + /// + /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. + /// The number of array elements present. + /// If set to true; will return the size of an element after marshalling. + /// The size of the primitive or struct array. + public static int SizeOf(int elementCount, bool marshal) + => SizeOf(marshal) * elementCount; + + #endregion + + /// + /// Initialize with static access to Dalamud. + /// + /// The Dalamud instance. + internal static void Initialize(Dalamud dalamud) + { + seStringManager = dalamud.SeStringManager; + handle = Process.GetCurrentProcess().Handle; + } + } +} diff --git a/Dalamud/Memory/MemoryProtection.cs b/Dalamud/Memory/MemoryProtection.cs new file mode 100644 index 000000000..289c5024d --- /dev/null +++ b/Dalamud/Memory/MemoryProtection.cs @@ -0,0 +1,117 @@ +using System; + +// This is a copy from NativeFunctions.MemoryProtection + +namespace Dalamud.Memory +{ + /// + /// PAGE_* from memoryapi. + /// + [Flags] + public enum MemoryProtection + { + // Copied from NativeFunctions to expose to the user. + + /// + /// Enables execute access to the committed region of pages. An attempt to write to the committed region results + /// in an access violation. This flag is not supported by the CreateFileMapping function. + /// + Execute = 0x10, + + /// + /// Enables execute or read-only access to the committed region of pages. An attempt to write to the committed region + /// results in an access violation. + /// + ExecuteRead = 0x20, + + /// + /// Enables execute, read-only, or read/write access to the committed region of pages. + /// + ExecuteReadWrite = 0x40, + + /// + /// Enables execute, read-only, or copy-on-write access to a mapped view of a file mapping object. An attempt to + /// write to a committed copy-on-write page results in a private copy of the page being made for the process. The + /// private page is marked as PAGE_EXECUTE_READWRITE, and the change is written to the new page. This flag is not + /// supported by the VirtualAlloc or VirtualAllocEx functions. + /// + ExecuteWriteCopy = 0x80, + + /// + /// Disables all access to the committed region of pages. An attempt to read from, write to, or execute the committed + /// region results in an access violation. This flag is not supported by the CreateFileMapping function. + /// + NoAccess = 0x01, + + /// + /// Enables read-only access to the committed region of pages. An attempt to write to the committed region results + /// in an access violation. If Data Execution Prevention is enabled, an attempt to execute code in the committed + /// region results in an access violation. + /// + ReadOnly = 0x02, + + /// + /// Enables read-only or read/write access to the committed region of pages. If Data Execution Prevention is enabled, + /// attempting to execute code in the committed region results in an access violation. + /// + ReadWrite = 0x04, + + /// + /// Enables read-only or copy-on-write access to a mapped view of a file mapping object. An attempt to write to + /// a committed copy-on-write page results in a private copy of the page being made for the process. The private + /// page is marked as PAGE_READWRITE, and the change is written to the new page. If Data Execution Prevention is + /// enabled, attempting to execute code in the committed region results in an access violation. This flag is not + /// supported by the VirtualAlloc or VirtualAllocEx functions. + /// + WriteCopy = 0x08, + + /// + /// Sets all locations in the pages as invalid targets for CFG. Used along with any execute page protection like + /// PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY. Any indirect call to locations + /// in those pages will fail CFG checks and the process will be terminated. The default behavior for executable + /// pages allocated is to be marked valid call targets for CFG. This flag is not supported by the VirtualProtect + /// or CreateFileMapping functions. + /// + TargetsInvalid = 0x40000000, + + /// + /// Pages in the region will not have their CFG information updated while the protection changes for VirtualProtect. + /// For example, if the pages in the region was allocated using PAGE_TARGETS_INVALID, then the invalid information + /// will be maintained while the page protection changes. This flag is only valid when the protection changes to + /// an executable type like PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY. + /// The default behavior for VirtualProtect protection change to executable is to mark all locations as valid call + /// targets for CFG. + /// + TargetsNoUpdate = TargetsInvalid, + + /// + /// Pages in the region become guard pages. Any attempt to access a guard page causes the system to raise a + /// STATUS_GUARD_PAGE_VIOLATION exception and turn off the guard page status. Guard pages thus act as a one-time + /// access alarm. For more information, see Creating Guard Pages. When an access attempt leads the system to turn + /// off guard page status, the underlying page protection takes over. If a guard page exception occurs during a + /// system service, the service typically returns a failure status indicator. This value cannot be used with + /// PAGE_NOACCESS. This flag is not supported by the CreateFileMapping function. + /// + Guard = 0x100, + + /// + /// Sets all pages to be non-cachable. Applications should not use this attribute except when explicitly required + /// for a device. Using the interlocked functions with memory that is mapped with SEC_NOCACHE can result in an + /// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_NOCACHE flag cannot be used with the PAGE_GUARD, PAGE_NOACCESS, + /// or PAGE_WRITECOMBINE flags. The PAGE_NOCACHE flag can be used only when allocating private memory with the + /// VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable non-cached memory access for shared + /// memory, specify the SEC_NOCACHE flag when calling the CreateFileMapping function. + /// + NoCache = 0x200, + + /// + /// Sets all pages to be write-combined. Applications should not use this attribute except when explicitly required + /// for a device. Using the interlocked functions with memory that is mapped as write-combined can result in an + /// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_WRITECOMBINE flag cannot be specified with the PAGE_NOACCESS, + /// PAGE_GUARD, and PAGE_NOCACHE flags. The PAGE_WRITECOMBINE flag can be used only when allocating private memory + /// with the VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable write-combined memory access + /// for shared memory, specify the SEC_WRITECOMBINE flag when calling the CreateFileMapping function. + /// + WriteCombine = 0x400, + } +} diff --git a/Dalamud/ModuleLog.cs b/Dalamud/ModuleLog.cs new file mode 100644 index 000000000..b78430a92 --- /dev/null +++ b/Dalamud/ModuleLog.cs @@ -0,0 +1,124 @@ +using System; + +namespace Dalamud +{ + /// + /// Class offering various methods to allow for logging in Dalamud modules. + /// + internal class ModuleLog + { + private readonly string moduleName; + + /// + /// Initializes a new instance of the class. + /// This class can be used to prefix logging messages with a Dalamud module name prefix. For example, "[PLUGINR] ...". + /// + /// The module name. + public ModuleLog(string moduleName) + { + this.moduleName = moduleName; + } + + /// + /// Log a templated verbose message to the in-game debug log. + /// + /// The message template. + /// Values to log. + public void Verbose(string messageTemplate, params object[] values) + => Serilog.Log.Verbose($"[{this.moduleName}] {messageTemplate}", values); + + /// + /// Log a templated verbose message to the in-game debug log. + /// + /// The exception that caused the error. + /// The message template. + /// Values to log. + public void Verbose(Exception exception, string messageTemplate, params object[] values) + => Serilog.Log.Verbose(exception, $"[{this.moduleName}] {messageTemplate}", values); + + /// + /// Log a templated debug message to the in-game debug log. + /// + /// The message template. + /// Values to log. + public void Debug(string messageTemplate, params object[] values) + => Serilog.Log.Debug($"[{this.moduleName}] {messageTemplate}", values); + + /// + /// Log a templated debug message to the in-game debug log. + /// + /// The exception that caused the error. + /// The message template. + /// Values to log. + public void Debug(Exception exception, string messageTemplate, params object[] values) + => Serilog.Log.Debug(exception, $"[{this.moduleName}] {messageTemplate}", values); + + /// + /// Log a templated information message to the in-game debug log. + /// + /// The message template. + /// Values to log. + public void Information(string messageTemplate, params object[] values) + => Serilog.Log.Information($"[{this.moduleName}] {messageTemplate}", values); + + /// + /// Log a templated information message to the in-game debug log. + /// + /// The exception that caused the error. + /// The message template. + /// Values to log. + public void Information(Exception exception, string messageTemplate, params object[] values) + => Serilog.Log.Information(exception, $"[{this.moduleName}] {messageTemplate}", values); + + /// + /// Log a templated warning message to the in-game debug log. + /// + /// The message template. + /// Values to log. + public void Warning(string messageTemplate, params object[] values) + => Serilog.Log.Warning($"[{this.moduleName}] {messageTemplate}", values); + + /// + /// Log a templated warning message to the in-game debug log. + /// + /// The exception that caused the error. + /// The message template. + /// Values to log. + public void Warning(Exception exception, string messageTemplate, params object[] values) + => Serilog.Log.Warning(exception, $"[{this.moduleName}] {messageTemplate}", values); + + /// + /// Log a templated error message to the in-game debug log. + /// + /// The message template. + /// Values to log. + public void Error(string messageTemplate, params object[] values) + => Serilog.Log.Error($"[{this.moduleName}] {messageTemplate}", values); + + /// + /// Log a templated error message to the in-game debug log. + /// + /// The exception that caused the error. + /// The message template. + /// Values to log. + public void Error(Exception exception, string messageTemplate, params object[] values) + => Serilog.Log.Error(exception, $"[{this.moduleName}] {messageTemplate}", values); + + /// + /// Log a templated fatal message to the in-game debug log. + /// + /// The message template. + /// Values to log. + public void Fatal(string messageTemplate, params object[] values) + => Serilog.Log.Fatal($"[{this.moduleName}] {messageTemplate}", values); + + /// + /// Log a templated fatal message to the in-game debug log. + /// + /// The exception that caused the error. + /// The message template. + /// Values to log. + public void Fatal(Exception exception, string messageTemplate, params object[] values) + => Serilog.Log.Fatal(exception, $"[{this.moduleName}] {messageTemplate}", values); + } +} diff --git a/Dalamud/NativeFunctions.cs b/Dalamud/NativeFunctions.cs index d41d64985..0a4fbc5bd 100644 --- a/Dalamud/NativeFunctions.cs +++ b/Dalamud/NativeFunctions.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Net.Sockets; using System.Runtime.InteropServices; @@ -54,6 +53,11 @@ namespace Dalamud /// public enum MessageBoxType : uint { + /// + /// The default value for any of the various subtypes. + /// + DefaultValue = 0x0, + // To indicate the buttons displayed in the message box, specify one of the following values. /// @@ -76,7 +80,7 @@ namespace Dalamud /// /// The message box contains one push button: OK. This is the default. /// - Ok = 0x0, + Ok = DefaultValue, /// /// The message box contains two push buttons: OK and Cancel. @@ -108,7 +112,7 @@ namespace Dalamud /// /// An exclamation-point icon appears in the message box. /// - IconWarning = 0x30, + IconWarning = IconExclamation, /// /// An icon consisting of a lowercase letter i in a circle appears in the message box. @@ -118,7 +122,7 @@ namespace Dalamud /// /// An icon consisting of a lowercase letter i in a circle appears in the message box. /// - IconAsterisk = 0x40, + IconAsterisk = IconInformation, /// /// A question-mark icon appears in the message box. @@ -138,12 +142,12 @@ namespace Dalamud /// /// A stop-sign icon appears in the message box. /// - IconError = 0x10, + IconError = IconStop, /// /// A stop-sign icon appears in the message box. /// - IconHand = 0x10, + IconHand = IconStop, // To indicate the default button, specify one of the following values. @@ -151,7 +155,7 @@ namespace Dalamud /// The first button is the default button. /// MB_DEFBUTTON1 is the default unless MB_DEFBUTTON2, MB_DEFBUTTON3, or MB_DEFBUTTON4 is specified. /// - DefButton1 = 0x0, + DefButton1 = DefaultValue, /// /// The second button is the default button. @@ -177,7 +181,7 @@ namespace Dalamud /// of the parent of the message box are automatically disabled, but pop-up windows are not. MB_APPLMODAL is the /// default if neither MB_SYSTEMMODAL nor MB_TASKMODAL is specified. /// - ApplModal = 0x0, + ApplModal = DefaultValue, /// /// Same as MB_APPLMODAL except that the message box has the WS_EX_TOPMOST style. @@ -241,14 +245,13 @@ namespace Dalamud { var activatedHandle = GetForegroundWindow(); if (activatedHandle == IntPtr.Zero) - { return false; // No window is currently activated - } - var procId = Process.GetCurrentProcess().Id; - GetWindowThreadProcessId(activatedHandle, out var activeProcId); + _ = GetWindowThreadProcessId(activatedHandle, out var activeProcId); + if (Marshal.GetLastWin32Error() != 0) + return false; - return activeProcId == procId; + return activeProcId == Environment.ProcessId; } /// @@ -320,8 +323,8 @@ namespace Dalamud /// If the function fails, the return value is zero.To get extended error information, call GetLastError. If the function /// succeeds, the return value is one of the ID* enum values. /// - [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] - public static extern int MessageBox(IntPtr hWnd, string text, string caption, MessageBoxType type); + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern int MessageBoxW(IntPtr hWnd, string text, string caption, MessageBoxType type); /// /// See https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-flashwinfo. @@ -333,28 +336,28 @@ namespace Dalamud /// /// The size of the structure, in bytes. /// - public uint cbSize; + public uint Size; /// /// A handle to the window to be flashed. The window can be either opened or minimized. /// - public IntPtr hwnd; + public IntPtr Hwnd; /// /// The flash status. This parameter can be one or more of the FlashWindow enum values. /// - public FlashWindow dwFlags; + public FlashWindow Flags; /// /// The number of times to flash the window. /// - public uint uCount; + public uint Count; /// /// The rate at which the window is to be flashed, in milliseconds. If dwTimeout is zero, the function uses the /// default cursor blink rate. /// - public uint dwTimeout; + public uint Timeout; } } @@ -363,6 +366,118 @@ namespace Dalamud /// internal static partial class NativeFunctions { + /// + /// MEM_* from memoryapi. + /// + [Flags] + public enum AllocationType + { + /// + /// To coalesce two adjacent placeholders, specify MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS. When you coalesce + /// placeholders, lpAddress and dwSize must exactly match those of the placeholder. + /// + CoalescePlaceholders = 0x1, + + /// + /// Frees an allocation back to a placeholder (after you've replaced a placeholder with a private allocation using + /// VirtualAlloc2 or Virtual2AllocFromApp). To split a placeholder into two placeholders, specify + /// MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER. + /// + PreservePlaceholder = 0x2, + + /// + /// Allocates memory charges (from the overall size of memory and the paging files on disk) for the specified reserved + /// memory pages. The function also guarantees that when the caller later initially accesses the memory, the contents + /// will be zero. Actual physical pages are not allocated unless/until the virtual addresses are actually accessed. + /// To reserve and commit pages in one step, call VirtualAllocEx with MEM_COMMIT | MEM_RESERVE. Attempting to commit + /// a specific address range by specifying MEM_COMMIT without MEM_RESERVE and a non-NULL lpAddress fails unless the + /// entire range has already been reserved. The resulting error code is ERROR_INVALID_ADDRESS. An attempt to commit + /// a page that is already committed does not cause the function to fail. This means that you can commit pages without + /// first determining the current commitment state of each page. If lpAddress specifies an address within an enclave, + /// flAllocationType must be MEM_COMMIT. + /// + Commit = 0x1000, + + /// + /// Reserves a range of the process's virtual address space without allocating any actual physical storage in memory + /// or in the paging file on disk. You commit reserved pages by calling VirtualAllocEx again with MEM_COMMIT. To + /// reserve and commit pages in one step, call VirtualAllocEx with MEM_COMMIT | MEM_RESERVE. Other memory allocation + /// functions, such as malloc and LocalAlloc, cannot use reserved memory until it has been released. + /// + Reserve = 0x2000, + + /// + /// Decommits the specified region of committed pages. After the operation, the pages are in the reserved state. + /// The function does not fail if you attempt to decommit an uncommitted page. This means that you can decommit + /// a range of pages without first determining the current commitment state. The MEM_DECOMMIT value is not supported + /// when the lpAddress parameter provides the base address for an enclave. + /// + Decommit = 0x4000, + + /// + /// Releases the specified region of pages, or placeholder (for a placeholder, the address space is released and + /// available for other allocations). After this operation, the pages are in the free state. If you specify this + /// value, dwSize must be 0 (zero), and lpAddress must point to the base address returned by the VirtualAlloc function + /// when the region is reserved. The function fails if either of these conditions is not met. If any pages in the + /// region are committed currently, the function first decommits, and then releases them. The function does not + /// fail if you attempt to release pages that are in different states, some reserved and some committed. This means + /// that you can release a range of pages without first determining the current commitment state. + /// + Release = 0x8000, + + /// + /// Indicates that data in the memory range specified by lpAddress and dwSize is no longer of interest. The pages + /// should not be read from or written to the paging file. However, the memory block will be used again later, so + /// it should not be decommitted. This value cannot be used with any other value. Using this value does not guarantee + /// that the range operated on with MEM_RESET will contain zeros. If you want the range to contain zeros, decommit + /// the memory and then recommit it. When you use MEM_RESET, the VirtualAllocEx function ignores the value of fProtect. + /// However, you must still set fProtect to a valid protection value, such as PAGE_NOACCESS. VirtualAllocEx returns + /// an error if you use MEM_RESET and the range of memory is mapped to a file. A shared view is only acceptable + /// if it is mapped to a paging file. + /// + Reset = 0x80000, + + /// + /// MEM_RESET_UNDO should only be called on an address range to which MEM_RESET was successfully applied earlier. + /// It indicates that the data in the specified memory range specified by lpAddress and dwSize is of interest to + /// the caller and attempts to reverse the effects of MEM_RESET. If the function succeeds, that means all data in + /// the specified address range is intact. If the function fails, at least some of the data in the address range + /// has been replaced with zeroes. This value cannot be used with any other value. If MEM_RESET_UNDO is called on + /// an address range which was not MEM_RESET earlier, the behavior is undefined. When you specify MEM_RESET, the + /// VirtualAllocEx function ignores the value of flProtect. However, you must still set flProtect to a valid + /// protection value, such as PAGE_NOACCESS. + /// + ResetUndo = 0x1000000, + + /// + /// Reserves an address range that can be used to map Address Windowing Extensions (AWE) pages. This value must + /// be used with MEM_RESERVE and no other values. + /// + Physical = 0x400000, + + /// + /// Allocates memory at the highest possible address. This can be slower than regular allocations, especially when + /// there are many allocations. + /// + TopDown = 0x100000, + + /// + /// Causes the system to track pages that are written to in the allocated region. If you specify this value, you + /// must also specify MEM_RESERVE. To retrieve the addresses of the pages that have been written to since the region + /// was allocated or the write-tracking state was reset, call the GetWriteWatch function. To reset the write-tracking + /// state, call GetWriteWatch or ResetWriteWatch. The write-tracking feature remains enabled for the memory region + /// until the region is freed. + /// + WriteWatch = 0x200000, + + /// + /// Allocates memory using large page support. The size and alignment must be a multiple of the large-page minimum. + /// To obtain this value, use the GetLargePageMinimum function. If you specify this value, you must also specify + /// MEM_RESERVE and MEM_COMMIT. + /// + LargePages = 0x20000000, + } + /// /// SEM_* from errhandlingapi. /// @@ -402,20 +517,113 @@ namespace Dalamud } /// - /// See https://docs.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-debugactiveprocess. - /// Enables a debugger to attach to an active process and debug it. + /// PAGE_* from memoryapi. /// - /// - /// The identifier for the process to be debugged. The debugger is granted debugging access to the process as if it - /// created the process with the DEBUG_ONLY_THIS_PROCESS flag. For more information, see the Remarks section of this - /// topic. - /// - /// - /// If the function succeeds, the return value is nonzero. If the function fails, the return value is 0 (zero). To get - /// extended error information, call GetLastError. - /// - [DllImport("kernel32.dll", SetLastError = true)] - public static extern bool DebugActiveProcess(uint dwProcessId); + [Flags] + public enum MemoryProtection + { + /// + /// Enables execute access to the committed region of pages. An attempt to write to the committed region results + /// in an access violation. This flag is not supported by the CreateFileMapping function. + /// + Execute = 0x10, + + /// + /// Enables execute or read-only access to the committed region of pages. An attempt to write to the committed region + /// results in an access violation. + /// + ExecuteRead = 0x20, + + /// + /// Enables execute, read-only, or read/write access to the committed region of pages. + /// + ExecuteReadWrite = 0x40, + + /// + /// Enables execute, read-only, or copy-on-write access to a mapped view of a file mapping object. An attempt to + /// write to a committed copy-on-write page results in a private copy of the page being made for the process. The + /// private page is marked as PAGE_EXECUTE_READWRITE, and the change is written to the new page. This flag is not + /// supported by the VirtualAlloc or VirtualAllocEx functions. + /// + ExecuteWriteCopy = 0x80, + + /// + /// Disables all access to the committed region of pages. An attempt to read from, write to, or execute the committed + /// region results in an access violation. This flag is not supported by the CreateFileMapping function. + /// + NoAccess = 0x01, + + /// + /// Enables read-only access to the committed region of pages. An attempt to write to the committed region results + /// in an access violation. If Data Execution Prevention is enabled, an attempt to execute code in the committed + /// region results in an access violation. + /// + ReadOnly = 0x02, + + /// + /// Enables read-only or read/write access to the committed region of pages. If Data Execution Prevention is enabled, + /// attempting to execute code in the committed region results in an access violation. + /// + ReadWrite = 0x04, + + /// + /// Enables read-only or copy-on-write access to a mapped view of a file mapping object. An attempt to write to + /// a committed copy-on-write page results in a private copy of the page being made for the process. The private + /// page is marked as PAGE_READWRITE, and the change is written to the new page. If Data Execution Prevention is + /// enabled, attempting to execute code in the committed region results in an access violation. This flag is not + /// supported by the VirtualAlloc or VirtualAllocEx functions. + /// + WriteCopy = 0x08, + + /// + /// Sets all locations in the pages as invalid targets for CFG. Used along with any execute page protection like + /// PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY. Any indirect call to locations + /// in those pages will fail CFG checks and the process will be terminated. The default behavior for executable + /// pages allocated is to be marked valid call targets for CFG. This flag is not supported by the VirtualProtect + /// or CreateFileMapping functions. + /// + TargetsInvalid = 0x40000000, + + /// + /// Pages in the region will not have their CFG information updated while the protection changes for VirtualProtect. + /// For example, if the pages in the region was allocated using PAGE_TARGETS_INVALID, then the invalid information + /// will be maintained while the page protection changes. This flag is only valid when the protection changes to + /// an executable type like PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY. + /// The default behavior for VirtualProtect protection change to executable is to mark all locations as valid call + /// targets for CFG. + /// + TargetsNoUpdate = TargetsInvalid, + + /// + /// Pages in the region become guard pages. Any attempt to access a guard page causes the system to raise a + /// STATUS_GUARD_PAGE_VIOLATION exception and turn off the guard page status. Guard pages thus act as a one-time + /// access alarm. For more information, see Creating Guard Pages. When an access attempt leads the system to turn + /// off guard page status, the underlying page protection takes over. If a guard page exception occurs during a + /// system service, the service typically returns a failure status indicator. This value cannot be used with + /// PAGE_NOACCESS. This flag is not supported by the CreateFileMapping function. + /// + Guard = 0x100, + + /// + /// Sets all pages to be non-cachable. Applications should not use this attribute except when explicitly required + /// for a device. Using the interlocked functions with memory that is mapped with SEC_NOCACHE can result in an + /// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_NOCACHE flag cannot be used with the PAGE_GUARD, PAGE_NOACCESS, + /// or PAGE_WRITECOMBINE flags. The PAGE_NOCACHE flag can be used only when allocating private memory with the + /// VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable non-cached memory access for shared + /// memory, specify the SEC_NOCACHE flag when calling the CreateFileMapping function. + /// + NoCache = 0x200, + + /// + /// Sets all pages to be write-combined. Applications should not use this attribute except when explicitly required + /// for a device. Using the interlocked functions with memory that is mapped as write-combined can result in an + /// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_WRITECOMBINE flag cannot be specified with the PAGE_NOACCESS, + /// PAGE_GUARD, and PAGE_NOCACHE flags. The PAGE_WRITECOMBINE flag can be used only when allocating private memory + /// with the VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable write-combined memory access + /// for shared memory, specify the SEC_WRITECOMBINE flag when calling the CreateFileMapping function. + /// + WriteCombine = 0x400, + } /// /// See https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-freelibrary. @@ -463,9 +671,9 @@ namespace Dalamud /// code is ERROR_SUCCESS. If the function fails, the return value is 0 (zero). To get extended error information, call /// GetLastError. /// - [DllImport("kernel32.dll", SetLastError = true)] + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [PreserveSig] - public static extern uint GetModuleFileName( + public static extern uint GetModuleFileNameW( [In] IntPtr hModule, [Out] StringBuilder lpFilename, [In][MarshalAs(UnmanagedType.U4)] int nSize); @@ -488,8 +696,28 @@ namespace Dalamud /// If the function succeeds, the return value is a handle to the specified module. If the function fails, the return /// value is NULL.To get extended error information, call GetLastError. /// - [DllImport("kernel32.dll", CharSet = CharSet.Auto)] - public static extern IntPtr GetModuleHandle(string lpModuleName); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] + public static extern IntPtr GetModuleHandleW(string lpModuleName); + + /// + /// Retrieves the address of an exported function or variable from the specified dynamic-link library (DLL). + /// + /// + /// A handle to the DLL module that contains the function or variable. The LoadLibrary, LoadLibraryEx, LoadPackagedLibrary, + /// or GetModuleHandle function returns this handle. The GetProcAddress function does not retrieve addresses from modules + /// that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx. + /// + /// + /// The function or variable name, or the function's ordinal value. If this parameter is an ordinal value, it must be + /// in the low-order word; the high-order word must be zero. + /// + /// + /// If the function succeeds, the return value is the address of the exported function or variable. If the function + /// fails, the return value is NULL.To get extended error information, call GetLastError. + /// + [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] + [SuppressMessage("Globalization", "CA2101:Specify marshaling for P/Invoke string arguments", Justification = "Ansi only")] + public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); /// /// See https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryw. @@ -512,8 +740,8 @@ namespace Dalamud /// If the function succeeds, the return value is a handle to the module. If the function fails, the return value is /// NULL.To get extended error information, call GetLastError. /// - [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Ansi)] - public static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName); + [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern IntPtr LoadLibraryW([MarshalAs(UnmanagedType.LPWStr)] string lpFileName); /// /// ReadProcessMemory copies the data in the specified address range from the address space of the specified process @@ -551,6 +779,42 @@ namespace Dalamud int dwSize, out IntPtr lpNumberOfBytesRead); + /// + /// ReadProcessMemory copies the data in the specified address range from the address space of the specified process + /// into the specified buffer of the current process. Any process that has a handle with PROCESS_VM_READ access can + /// call the function. The entire area to be read must be accessible, and if it is not accessible, the function fails. + /// + /// + /// A handle to the process with memory that is being read. The handle must have PROCESS_VM_READ access to the process. + /// + /// + /// A pointer to the base address in the specified process from which to read. Before any data transfer occurs, the + /// system verifies that all data in the base address and memory of the specified size is accessible for read access, + /// and if it is not accessible the function fails. + /// + /// + /// A pointer to a buffer that receives the contents from the address space of the specified process. + /// + /// + /// The number of bytes to be read from the specified process. + /// + /// + /// A pointer to a variable that receives the number of bytes transferred into the specified buffer. If lpNumberOfBytesRead + /// is NULL, the parameter is ignored. + /// + /// + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is 0 (zero). To get + /// extended error information, call GetLastError. The function fails if the requested read operation crosses into an + /// area of the process that is inaccessible. + /// + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool ReadProcessMemory( + IntPtr hProcess, + IntPtr lpBaseAddress, + byte[] lpBuffer, + int dwSize, + out IntPtr lpNumberOfBytesRead); + /// /// See https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-seterrormode. /// Controls whether the system will handle the specified types of serious errors or whether the process will handle @@ -585,6 +849,166 @@ namespace Dalamud /// [DllImport("kernel32.dll")] public static extern IntPtr SetUnhandledExceptionFilter(IntPtr lpTopLevelExceptionFilter); + + /// + /// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc. + /// Reserves, commits, or changes the state of a region of pages in the virtual address space of the calling process. + /// Memory allocated by this function is automatically initialized to zero. To allocate memory in the address space + /// of another process, use the VirtualAllocEx function. + /// + /// + /// The starting address of the region to allocate. If the memory is being reserved, the specified address is rounded + /// down to the nearest multiple of the allocation granularity. If the memory is already reserved and is being committed, + /// the address is rounded down to the next page boundary. To determine the size of a page and the allocation granularity + /// on the host computer, use the GetSystemInfo function. If this parameter is NULL, the system determines where to + /// allocate the region. If this address is within an enclave that you have not initialized by calling InitializeEnclave, + /// VirtualAlloc allocates a page of zeros for the enclave at that address. The page must be previously uncommitted, + /// and will not be measured with the EEXTEND instruction of the Intel Software Guard Extensions programming model. + /// If the address in within an enclave that you initialized, then the allocation operation fails with the + /// ERROR_INVALID_ADDRESS error. + /// + /// + /// The size of the region, in bytes. If the lpAddress parameter is NULL, this value is rounded up to the next page + /// boundary. Otherwise, the allocated pages include all pages containing one or more bytes in the range from lpAddress + /// to lpAddress+dwSize. This means that a 2-byte range straddling a page boundary causes both pages to be included + /// in the allocated region. + /// + /// + /// The type of memory allocation. This parameter must contain one of the MEM_* enum values. + /// + /// + /// The memory protection for the region of pages to be allocated. If the pages are being committed, you can specify + /// any one of the memory protection constants. + /// + /// + /// If the function succeeds, the return value is the base address of the allocated region of pages. If the function + /// fails, the return value is NULL.To get extended error information, call GetLastError. + /// + [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] + public static extern IntPtr VirtualAlloc( + IntPtr lpAddress, + UIntPtr dwSize, + AllocationType flAllocationType, + MemoryProtection flProtect); + + /// + [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] + public static extern IntPtr VirtualAlloc( + IntPtr lpAddress, + UIntPtr dwSize, + AllocationType flAllocationType, + Memory.MemoryProtection flProtect); + + /// + /// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualfree. + /// Releases, decommits, or releases and decommits a region of pages within the virtual address space of the calling + /// process. + /// process. + /// + /// + /// A pointer to the base address of the region of pages to be freed. If the dwFreeType parameter is MEM_RELEASE, this + /// parameter must be the base address returned by the VirtualAlloc function when the region of pages is reserved. + /// + /// + /// The size of the region of memory to be freed, in bytes. If the dwFreeType parameter is MEM_RELEASE, this parameter + /// must be 0 (zero). The function frees the entire region that is reserved in the initial allocation call to VirtualAlloc. + /// If the dwFreeType parameter is MEM_DECOMMIT, the function decommits all memory pages that contain one or more bytes + /// in the range from the lpAddress parameter to (lpAddress+dwSize). This means, for example, that a 2-byte region of + /// memory that straddles a page boundary causes both pages to be decommitted.If lpAddress is the base address returned + /// by VirtualAlloc and dwSize is 0 (zero), the function decommits the entire region that is allocated by VirtualAlloc. + /// After that, the entire region is in the reserved state. + /// + /// + /// The type of free operation. This parameter must be one of the MEM_* enum values. + /// + /// + /// If the function succeeds, the return value is a nonzero value. If the function fails, the return value is 0 (zero). + /// To get extended error information, call GetLastError. + /// + [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] + public static extern bool VirtualFree( + IntPtr lpAddress, + UIntPtr dwSize, + AllocationType dwFreeType); + + /// + /// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect. + /// Changes the protection on a region of committed pages in the virtual address space of the calling process. + /// + /// + /// The address of the starting page of the region of pages whose access protection attributes are to be changed. All + /// pages in the specified region must be within the same reserved region allocated when calling the VirtualAlloc or + /// VirtualAllocEx function using MEM_RESERVE. The pages cannot span adjacent reserved regions that were allocated by + /// separate calls to VirtualAlloc or VirtualAllocEx using MEM_RESERVE. + /// + /// + /// The size of the region whose access protection attributes are to be changed, in bytes. The region of affected pages + /// includes all pages containing one or more bytes in the range from the lpAddress parameter to (lpAddress+dwSize). + /// This means that a 2-byte range straddling a page boundary causes the protection attributes of both pages to be changed. + /// + /// + /// The memory protection option. This parameter can be one of the memory protection constants. For mapped views, this + /// value must be compatible with the access protection specified when the view was mapped (see MapViewOfFile, + /// MapViewOfFileEx, and MapViewOfFileExNuma). + /// + /// + /// A pointer to a variable that receives the previous access protection value of the first page in the specified region + /// of pages. If this parameter is NULL or does not point to a valid variable, the function fails. + /// + /// + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. + /// To get extended error information, call GetLastError. + /// + [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] + public static extern bool VirtualProtect( + IntPtr lpAddress, + UIntPtr dwSize, + MemoryProtection flNewProtection, + out MemoryProtection lpflOldProtect); + + /// + [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] + public static extern bool VirtualProtect( + IntPtr lpAddress, + UIntPtr dwSize, + Memory.MemoryProtection flNewProtection, + out Memory.MemoryProtection lpflOldProtect); + + /// + /// Writes data to an area of memory in a specified process. The entire area to be written to must be accessible or + /// the operation fails. + /// + /// + /// A handle to the process memory to be modified. The handle must have PROCESS_VM_WRITE and PROCESS_VM_OPERATION access + /// to the process. + /// + /// + /// A pointer to the base address in the specified process to which data is written. Before data transfer occurs, the + /// system verifies that all data in the base address and memory of the specified size is accessible for write access, + /// and if it is not accessible, the function fails. + /// + /// + /// A pointer to the buffer that contains data to be written in the address space of the specified process. + /// + /// + /// The number of bytes to be written to the specified process. + /// + /// + /// A pointer to a variable that receives the number of bytes transferred into the specified process. This parameter + /// is optional. If lpNumberOfBytesWritten is NULL, the parameter is ignored. + /// + /// + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is 0 (zero). To get + /// extended error information, call GetLastError.The function fails if the requested write operation crosses into an + /// area of the process that is inaccessible. + /// + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool WriteProcessMemory( + IntPtr hProcess, + IntPtr lpBaseAddress, + byte[] lpBuffer, + int dwSize, + out IntPtr lpNumberOfBytesWritten); } /// @@ -593,6 +1017,7 @@ namespace Dalamud internal static partial class NativeFunctions { /// + /// See https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-setsockopt. /// The setsockopt function sets a socket option. /// /// diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index a9ed0dc4a..8dfced79a 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using Dalamud.Configuration; +using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; @@ -22,7 +23,7 @@ namespace Dalamud.Plugin /// /// This class acts as an interface to various objects needed to interact with Dalamud and the game. /// - public class DalamudPluginInterface : IDisposable + public sealed class DalamudPluginInterface : IDisposable { private readonly Dalamud dalamud; private readonly string pluginName; @@ -34,11 +35,9 @@ namespace Dalamud.Plugin /// /// The dalamud instance to expose. /// The internal name of the plugin. - /// The plugin configurations handler. - /// The reason this plugin was loaded. - internal DalamudPluginInterface(Dalamud dalamud, string pluginName, PluginConfigurations configs, PluginLoadReason reason) + /// The equivalent of what Assembly.GetExecutingAssembly().Location should return. + internal DalamudPluginInterface(Dalamud dalamud, string pluginName, string assemblyLocation) { - this.Reason = reason; this.CommandManager = dalamud.CommandManager; this.Framework = dalamud.Framework; this.ClientState = dalamud.ClientState; @@ -49,7 +48,8 @@ namespace Dalamud.Plugin this.dalamud = dalamud; this.pluginName = pluginName; - this.configs = configs; + this.configs = dalamud.PluginManager.PluginConfigs; + this.AssemblyLocation = assemblyLocation; this.GeneralChatType = this.dalamud.Configuration.GeneralChatType; this.Sanitizer = new Sanitizer(this.Data.Language); @@ -60,7 +60,7 @@ namespace Dalamud.Plugin else { var currentUiLang = CultureInfo.CurrentUICulture; - if (Localization.ApplicableLangCodes.Any(x => currentUiLang.TwoLetterISOLanguageName == x)) + if (Localization.ApplicableLangCodes.Any(langCode => currentUiLang.TwoLetterISOLanguageName == langCode)) this.UiLanguage = currentUiLang.TwoLetterISOLanguageName; else this.UiLanguage = "en"; @@ -82,9 +82,9 @@ namespace Dalamud.Plugin public event LanguageChangedDelegate OnLanguageChanged; /// - /// Gets the reason this plugin was loaded. + /// Gets the plugin assembly location. /// - public PluginLoadReason Reason { get; } + public string AssemblyLocation { get; private set; } /// /// Gets the directory Dalamud assets are stored in. @@ -142,7 +142,7 @@ namespace Dalamud.Plugin #if DEBUG public bool IsDebugging => true; #else - public bool IsDebugging => this.dalamud.DalamudUi.IsDevMenu; + public bool IsDebugging => this.dalamud.DalamudUi.IsDevMenuOpen; #endif /// @@ -192,7 +192,7 @@ namespace Dalamud.Plugin // This is here for now to support the current plugin API foreach (var type in Assembly.GetCallingAssembly().GetTypes()) { - if (type.GetInterface(typeof(IPluginConfiguration).FullName) != null) + if (type.IsAssignableTo(typeof(IPluginConfiguration))) { var mi = this.configs.GetType().GetMethod("LoadForType"); var fn = mi.MakeGenericMethod(type); @@ -273,7 +273,7 @@ namespace Dalamud.Plugin if (this.dalamud.PluginManager.IpcSubscriptions.Any(x => x.SourcePluginName == this.pluginName && x.SubPluginName == pluginName)) throw new InvalidOperationException("Can't add multiple subscriptions for the same plugin."); - this.dalamud.PluginManager.IpcSubscriptions.Add((this.pluginName, pluginName, action)); + this.dalamud.PluginManager.IpcSubscriptions.Add(new(this.pluginName, pluginName, action)); } /// @@ -325,12 +325,16 @@ namespace Dalamud.Plugin [Obsolete("The current IPC mechanism is obsolete and scheduled to be replaced after API level 3.")] public bool SendMessage(string pluginName, ExpandoObject message) { - var (_, _, pluginInterface, _) = this.dalamud.PluginManager.Plugins.FirstOrDefault(x => x.Definition.InternalName == pluginName); + var plugin = this.dalamud.PluginManager.InstalledPlugins.FirstOrDefault(x => x.Manifest.InternalName == pluginName); - if (pluginInterface?.AnyPluginIpcAction == null) + if (plugin == default) return false; - pluginInterface.AnyPluginIpcAction.Invoke(this.pluginName, message); + if (plugin.DalamudInterface?.AnyPluginIpcAction == null) + return false; + + plugin.DalamudInterface.AnyPluginIpcAction.Invoke(this.pluginName, message); + return true; } @@ -343,22 +347,16 @@ namespace Dalamud.Plugin /// /// The message template. /// Values to log. - [Obsolete] - public void Log(string messageTemplate, params object[] values) - { - Serilog.Log.Information(messageTemplate, values); - } + [Obsolete("Use PluginLog")] + public void Log(string messageTemplate, params object[] values) => Serilog.Log.Information(messageTemplate, values); /// /// Log a templated error message to the in-game debug log. /// /// The message template. /// Values to log. - [Obsolete] - public void LogError(string messageTemplate, params object[] values) - { - Serilog.Log.Error(messageTemplate, values); - } + [Obsolete("Use PluginLog")] + public void LogError(string messageTemplate, params object[] values) => Serilog.Log.Error(messageTemplate, values); /// /// Log a templated error message to the in-game debug log. @@ -366,11 +364,8 @@ namespace Dalamud.Plugin /// The exception that caused the error. /// The message template. /// Values to log. - [Obsolete] - public void LogError(Exception exception, string messageTemplate, params object[] values) - { - Serilog.Log.Error(exception, messageTemplate, values); - } + [Obsolete("Use PluginLog")] + public void LogError(Exception exception, string messageTemplate, params object[] values) => Serilog.Log.Error(exception, messageTemplate, values); #endregion diff --git a/Dalamud/Plugin/Internal/Exceptions/DuplicatePluginException.cs b/Dalamud/Plugin/Internal/Exceptions/DuplicatePluginException.cs new file mode 100644 index 000000000..093f97e69 --- /dev/null +++ b/Dalamud/Plugin/Internal/Exceptions/DuplicatePluginException.cs @@ -0,0 +1,26 @@ +namespace Dalamud.Plugin.Internal.Exceptions +{ + /// + /// This exception that is thrown when a plugin is instructed to load while another plugin with the same + /// assembly name is already present and loaded. + /// + internal class DuplicatePluginException : PluginException + { + /// + /// Initializes a new instance of the class. + /// + /// Name of the conflicting assembly. + public DuplicatePluginException(string assemblyName) + { + this.AssemblyName = assemblyName; + } + + /// + /// Gets the name of the conflicting assembly. + /// + public string AssemblyName { get; init; } + + /// + public override string Message => $"A plugin with the same assembly name of {this.AssemblyName} is already loaded"; + } +} diff --git a/Dalamud/Plugin/Internal/Exceptions/InvalidPluginException.cs b/Dalamud/Plugin/Internal/Exceptions/InvalidPluginException.cs new file mode 100644 index 000000000..6b5c8920a --- /dev/null +++ b/Dalamud/Plugin/Internal/Exceptions/InvalidPluginException.cs @@ -0,0 +1,25 @@ +using System; +using System.IO; + +namespace Dalamud.Plugin.Internal.Exceptions +{ + /// + /// This exception represents a file that does not implement IDalamudPlugin. + /// + internal class InvalidPluginException : PluginException + { + /// + /// Initializes a new instance of the class. + /// + /// The invalid file. + public InvalidPluginException(FileInfo dllFile) + { + this.DllFile = dllFile; + } + + /// + /// Gets the invalid file. + /// + public FileInfo DllFile { get; init; } + } +} diff --git a/Dalamud/Plugin/Internal/Exceptions/InvalidPluginOperationException.cs b/Dalamud/Plugin/Internal/Exceptions/InvalidPluginOperationException.cs new file mode 100644 index 000000000..a80d6d51d --- /dev/null +++ b/Dalamud/Plugin/Internal/Exceptions/InvalidPluginOperationException.cs @@ -0,0 +1,24 @@ +using System; + +namespace Dalamud.Plugin.Internal.Exceptions +{ + /// + /// This represents an invalid plugin operation. + /// + internal class InvalidPluginOperationException : PluginException + { + /// + /// Initializes a new instance of the class. + /// + /// The message describing the invalid operation. + public InvalidPluginOperationException(string message) + { + this.Message = message; + } + + /// + /// Gets the message describing the invalid operation. + /// + public override string Message { get; } + } +} diff --git a/Dalamud/Plugin/Internal/Exceptions/PluginException.cs b/Dalamud/Plugin/Internal/Exceptions/PluginException.cs new file mode 100644 index 000000000..e4b17b686 --- /dev/null +++ b/Dalamud/Plugin/Internal/Exceptions/PluginException.cs @@ -0,0 +1,11 @@ +using System; + +namespace Dalamud.Plugin.Internal.Exceptions +{ + /// + /// This represents the base Dalamud plugin exception. + /// + internal abstract class PluginException : Exception + { + } +} diff --git a/Dalamud/Plugin/Internal/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/LocalDevPlugin.cs new file mode 100644 index 000000000..58390270b --- /dev/null +++ b/Dalamud/Plugin/Internal/LocalDevPlugin.cs @@ -0,0 +1,157 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Configuration.Internal; +using Dalamud.Plugin.Internal.Types; + +namespace Dalamud.Plugin.Internal +{ + /// + /// This class represents a dev plugin and all facets of its lifecycle. + /// The DLL on disk, dependencies, loaded assembly, etc. + /// + 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; + private int reloadCounter; + + /// + /// Initializes a new instance of the class. + /// + /// Dalamud instance. + /// Path to the DLL file. + /// The plugin manifest. + public LocalDevPlugin(Dalamud dalamud, FileInfo dllFile, LocalPluginManifest manifest) + : base(dalamud, dllFile, manifest) + { + // base is called first, ensuring that this is a valid plugin assembly + var devSettings = dalamud.Configuration.DevPluginSettings.FirstOrDefault(cfg => cfg.DllFile == dllFile.FullName); + + if (devSettings == default) + { + devSettings = new DevPluginSettings(dllFile.FullName); + dalamud.Configuration.DevPluginSettings.Add(devSettings); + dalamud.Configuration.Save(); + } + + this.devSettings = devSettings; + + if (this.AutomaticReload) + { + this.EnableReloading(); + } + } + + /// + /// Gets or sets a value indicating whether this dev plugin should start on boot. + /// + public bool StartOnBoot + { + get => this.devSettings.StartOnBoot; + set => this.devSettings.StartOnBoot = value; + } + + /// + /// Gets or sets a value indicating whether this dev plugin should reload on change. + /// + public bool AutomaticReload + { + get => this.devSettings.AutomaticReloading; + set + { + this.devSettings.AutomaticReloading = value; + + if (this.devSettings.AutomaticReloading) + { + this.EnableReloading(); + } + else + { + this.DisableReloading(); + } + } + } + + /// + public new void Dispose() + { + if (this.fileWatcher != null) + { + this.fileWatcher.Changed -= this.OnFileChanged; + this.fileWatcherTokenSource.Cancel(); + this.fileWatcher.Dispose(); + } + + base.Dispose(); + } + + /// + /// Configure this plugin for automatic reloading and enable it. + /// + 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; + } + } + + /// + /// Disable automatic reloading for this plugin. + /// + 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( + task => + { + 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; + } + + this.Reload(); + }, + this.fileWatcherTokenSource.Token); + } + } +} diff --git a/Dalamud/Plugin/Internal/LocalPlugin.cs b/Dalamud/Plugin/Internal/LocalPlugin.cs new file mode 100644 index 000000000..45fbb144f --- /dev/null +++ b/Dalamud/Plugin/Internal/LocalPlugin.cs @@ -0,0 +1,415 @@ +using System; +using System.IO; +using System.Linq; +using System.Reflection; + +using Dalamud.Configuration.Internal; +using Dalamud.Game; +using Dalamud.Plugin.Internal.Exceptions; +using Dalamud.Plugin.Internal.Types; +using McMaster.NETCore.Plugins; + +namespace Dalamud.Plugin.Internal +{ + /// + /// This class represents a plugin and all facets of its lifecycle. + /// The DLL on disk, dependencies, loaded assembly, etc. + /// + internal class LocalPlugin : IDisposable + { + private static readonly ModuleLog Log = new("PLUGIN"); + + private readonly Dalamud dalamud; + private readonly FileInfo manifestFile; + private readonly FileInfo disabledFile; + private readonly FileInfo testingFile; + + private PluginLoader loader; + private Assembly pluginAssembly; + private Type pluginType; + private IDalamudPlugin instance; + + /// + /// Initializes a new instance of the class. + /// + /// Dalamud instance. + /// Path to the DLL file. + /// The plugin manifest. + public LocalPlugin(Dalamud dalamud, FileInfo dllFile, LocalPluginManifest manifest) + { + this.dalamud = dalamud; + this.DllFile = dllFile; + this.State = PluginState.Unloaded; + + this.loader ??= PluginLoader.CreateFromAssemblyFile( + this.DllFile.FullName, + config => + { + config.IsUnloadable = true; + config.LoadInMemory = true; + config.PreferSharedTypes = true; + }); + + Version assemblyVersion; + + try + { + // BadImageFormatException + this.pluginAssembly ??= this.loader.LoadDefaultAssembly(); + + // InvalidOperationException + this.pluginType = this.pluginAssembly.GetTypes().First(type => type.IsAssignableTo(typeof(IDalamudPlugin))); + + assemblyVersion = this.pluginAssembly.GetName().Version; + } + catch (Exception) + { + this.pluginAssembly = null; + this.pluginType = null; + this.loader.Dispose(); + + Log.Debug($"Not a plugin: {this.DllFile.Name}"); + throw new InvalidPluginException(this.DllFile); + } + + // Files that may or may not exist + this.manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile); + this.disabledFile = LocalPluginManifest.GetDisabledFile(this.DllFile); + this.testingFile = LocalPluginManifest.GetTestingFile(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. + var manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile); + this.Manifest.Save(manifestFile); + } + else + { + this.Manifest = manifest; + } + + // This bit converts from ".disabled" functionality to using the manifest. + if (this.disabledFile.Exists) + { + this.Manifest.Disabled = true; + this.disabledFile.Delete(); + } + + // This bit converts from ".testing" functionality to using the manifest. + if (this.testingFile.Exists) + { + this.Manifest.Testing = true; + this.testingFile.Delete(); + } + + this.SaveManifest(); + } + + /// + /// Gets the associated with this plugin. + /// + public DalamudPluginInterface DalamudInterface { get; private set; } + + /// + /// Gets the path to the plugin DLL. + /// + public FileInfo DllFile { get; } + + /// + /// Gets the plugin manifest, if one exists. + /// + public LocalPluginManifest Manifest { get; } + + /// + /// Gets or sets the current state of the plugin. + /// + public PluginState State { get; protected set; } = PluginState.Unloaded; + + /// + /// Gets the AssemblyName plugin, populated during . + /// + /// Plugin type. + public AssemblyName AssemblyName { get; private set; } = null; + + /// + /// Gets the plugin name, directly from the plugin or if it is not loaded from the manifest. + /// + public string Name => this.instance?.Name ?? this.Manifest.Name ?? this.DllFile.Name; + + /// + /// Gets a value indicating whether the plugin is loaded and running. + /// + public bool IsLoaded => this.State == PluginState.Loaded; + + /// + /// Gets a value indicating whether the plugin is disabled. + /// + public bool IsDisabled => this.Manifest.Disabled; + + /// + /// Gets a value indicating whether the plugin is for testing use only. + /// + public bool IsTesting => this.Manifest.IsTestingExclusive || this.Manifest.Testing; + + /// + /// Gets a value indicating whether this plugin is dev plugin. + /// + public bool IsDev => this is LocalDevPlugin; + + /// + public void Dispose() + { + this.instance?.Dispose(); + this.instance = null; + + this.DalamudInterface.Dispose(); + this.DalamudInterface = null; + + this.pluginType = null; + this.pluginAssembly = null; + + this.loader?.Dispose(); + } + + /// + /// Load this plugin. + /// + /// Load while reloading. + public void Load(bool reloading = false) + { + // 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 (this.Manifest.ApplicableVersion < this.dalamud.StartInfo.GameVersion) + throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version"); + + if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel) + 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}"); + + try + { + this.loader ??= PluginLoader.CreateFromAssemblyFile( + this.DllFile.FullName, + config => + { + config.IsUnloadable = true; + config.LoadInMemory = true; + config.PreferSharedTypes = true; + }); + + if (reloading) + { + this.loader.Reload(); + } + + // 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 this.dalamud.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); + + // Instantiate and initialize + this.instance = Activator.CreateInstance(this.pluginType) as IDalamudPlugin; + + // 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.DalamudInterface = new DalamudPluginInterface(this.dalamud, this.pluginAssembly.GetName().Name, this.DllFile.FullName); + + if (this.IsDev) + { + // Inherit LPL's AssemblyLocation functionality + try + { + var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + + this.instance.GetType() + ?.GetProperty("AssemblyLocation", bindingFlags) + ?.SetValue(this.instance, this.DllFile.FullName); + this.instance.GetType() + ?.GetMethod("SetLocation", bindingFlags) + ?.Invoke(this.instance, new object[] { this.DllFile.FullName }); + } + catch + { + // Ignored + } + } + + this.instance.Initialize(this.DalamudInterface); + + 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; + } + } + + /// + /// 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. + /// + /// Unload while reloading. + public void Unload(bool reloading = false) + { + // Allowed: Loaded + switch (this.State) + { + case PluginState.InProgress: + throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already working"); + case PluginState.LoadError: + throw new InvalidPluginOperationException($"Unable to unload {this.Name}, load previously faulted, unload first"); + 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?.Dispose(); + 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; + } + } + + /// + /// Reload this plugin. + /// + public void Reload() + { + this.Unload(true); + this.Load(true); + } + + /// + /// Revert a disable. Must be unloaded first, does not load. + /// + 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(); + } + + /// + /// Disable this plugin, must be unloaded first. + /// + 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 SaveManifest() => this.Manifest.Save(this.manifestFile); + } +} diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs new file mode 100644 index 000000000..2d57c4e2b --- /dev/null +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -0,0 +1,1023 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +using CheapLoc; +using Dalamud.Configuration; +using Dalamud.Game.Text; +using Dalamud.Plugin.Internal.Exceptions; +using Dalamud.Plugin.Internal.Types; +using HarmonyLib; +using Newtonsoft.Json; + +namespace Dalamud.Plugin.Internal +{ + /// + /// Class responsible for loading and unloading plugins. + /// + internal partial class PluginManager : IDisposable + { + /// + /// The current Dalamud API level, used to handle breaking changes. Only plugins with this level will be loaded. + /// + public const int DalamudApiLevel = 3; + + private static readonly ModuleLog Log = new("PLUGINM"); + + private readonly Dalamud dalamud; + private readonly DirectoryInfo pluginDirectory; + private readonly DirectoryInfo devPluginDirectory; + private readonly BannedPlugin[] bannedPlugins; + + private readonly List installedPlugins = new(); + private List availablePlugins = new(); + private List updatablePlugins = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The instance to load plugins with. + public PluginManager(Dalamud dalamud) + { + this.dalamud = dalamud; + this.pluginDirectory = new DirectoryInfo(dalamud.StartInfo.PluginDirectory); + this.devPluginDirectory = new DirectoryInfo(dalamud.StartInfo.DefaultPluginDirectory); + + if (!this.pluginDirectory.Exists) + this.pluginDirectory.Create(); + + if (!this.devPluginDirectory.Exists) + this.devPluginDirectory.Create(); + + this.PluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(dalamud.StartInfo.ConfigurationPath), "pluginConfigs")); + + var bannedPluginsJson = File.ReadAllText(Path.Combine(this.dalamud.StartInfo.AssetDirectory, "UIRes", "bannedplugin.json")); + this.bannedPlugins = JsonConvert.DeserializeObject(bannedPluginsJson); + + this.Repos.Add(PluginRepository.MainRepo); + this.Repos.AddRange(this.dalamud.Configuration.ThirdRepoList + .Select(repo => new PluginRepository(repo.Url, repo.IsEnabled))); + + this.ApplyPatches(); + } + + /// + /// An event that fires when the installed plugins have changed. + /// + public event Action OnInstalledPluginsChanged; + + /// + /// An event that fires when the available plugins have changed. + /// + public event Action OnAvailablePluginsChanged; + + /// + /// Gets a list of all loaded plugins. + /// + public ImmutableList InstalledPlugins { get; private set; } = ImmutableList.Create(); + + /// + /// Gets a list of all available plugins. + /// + public ImmutableList AvailablePlugins { get; private set; } = ImmutableList.Create(); + + /// + /// Gets a list of all plugins with an available update. + /// + public ImmutableList UpdatablePlugins { get; private set; } = ImmutableList.Create(); + + /// + /// Gets a list of all plugin repositories. The main repo should always be first. + /// + public List Repos { get; } = new(); + + /// + /// Gets a value indicating whether plugins are not still loading from boot. + /// + public bool PluginsReady { get; private set; } = false; + + /// + /// Gets a value indicating whether all added repos are not in progress. + /// + public bool ReposReady => this.Repos.All(repo => repo.State != PluginRepositoryState.InProgress); + + /// + /// Gets a list of all IPC subscriptions. + /// + public List IpcSubscriptions { get; } = new(); + + /// + /// Gets the object used when initializing plugins. + /// + public PluginConfigurations PluginConfigs { get; } + + /// + public void Dispose() + { + foreach (var plugin in this.installedPlugins.ToArray()) + { + try + { + plugin.Dispose(); + } + catch (Exception ex) + { + Log.Error(ex, $"Error disposing {plugin.Name}"); + } + } + } + + /// + /// Load all plugins, sorted by priority. Any plugins with no explicit definition file or a negative priority + /// are loaded asynchronously. Should only be called during Dalamud startup. + /// + public void LoadAllPlugins() + { + var pluginDefs = new List(); + var devPluginDefs = new List(); + + if (!this.pluginDirectory.Exists) + this.pluginDirectory.Create(); + + if (!this.devPluginDirectory.Exists) + this.devPluginDirectory.Create(); + + // Add installed plugins. These are expected to be in a specific format so we can look for exactly that. + foreach (var pluginDir in this.pluginDirectory.GetDirectories()) + { + foreach (var versionDir in pluginDir.GetDirectories()) + { + var dllFile = new FileInfo(Path.Combine(versionDir.FullName, $"{pluginDir.Name}.dll")); + var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); + + if (!manifestFile.Exists) + continue; + + var manifest = LocalPluginManifest.Load(manifestFile); + + pluginDefs.Add(new(dllFile, manifest, false)); + } + } + + // devPlugins are more freeform. Look for any dll and hope to get lucky. + var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories); + + foreach (var dllFile in devDllFiles) + { + // Manifests are not required for devPlugins. the Plugin type will handle any null manifests. + var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); + var manifest = manifestFile.Exists ? LocalPluginManifest.Load(manifestFile) : null; + devPluginDefs.Add(new(dllFile, manifest, true)); + } + + // Sort for load order - unloaded definitions have default priority of 0 + pluginDefs.Sort(PluginDef.Sorter); + devPluginDefs.Sort(PluginDef.Sorter); + + // Dev plugins should load first. + pluginDefs.InsertRange(0, devPluginDefs); + + void LoadPlugins(IEnumerable pluginDefs) + { + foreach (var pluginDef in pluginDefs) + { + try + { + this.LoadPlugin(pluginDef.DllFile, pluginDef.Manifest, pluginDef.IsDev, isBoot: true); + } + catch (InvalidPluginException) + { + // Not a plugin + } + catch (Exception ex) + { + Log.Error(ex, "During boot plugin load, an unexpected error occurred"); + } + } + } + + // Load sync plugins + var syncPlugins = pluginDefs.Where(def => def.Manifest?.LoadPriority > 0); + LoadPlugins(syncPlugins); + + var asyncPlugins = pluginDefs.Where(def => def.Manifest == null || def.Manifest.LoadPriority <= 0); + Task.Run(() => LoadPlugins(asyncPlugins)) + .ContinueWith(task => this.PluginsReady = true) + .ContinueWith(task => this.NotifyInstalledPluginsChanged()); + } + + /// + /// Reload all loaded plugins. + /// + public void ReloadAllPlugins() + { + var aggregate = new List(); + + for (var i = 0; i < this.installedPlugins.Count; i++) + { + var plugin = this.installedPlugins[i]; + + if (plugin.IsLoaded) + { + try + { + plugin.Unload(); + plugin.Load(); + } + catch (Exception ex) + { + Log.Error(ex, "Error during reload all"); + + aggregate.Add(ex); + } + } + } + + if (aggregate.Any()) + { + throw new AggregateException(aggregate); + } + } + + /// + /// Reload the PluginMaster for each repo, filter, and event that the list has updated. + /// + public void ReloadPluginMasters() + { + Task.WhenAll(this.Repos.Select(repo => repo.ReloadPluginMasterAsync())) + .ContinueWith(task => this.RefilterPluginMasters()) + .Wait(); + } + + /// + /// Apply visibility and eligibility filters to the available plugins, then event that the list has updated. + /// + public void RefilterPluginMasters() + { + this.availablePlugins = this.dalamud.PluginManager.Repos + .SelectMany(repo => repo.PluginMaster) + .Where(this.IsManifestEligible) + .Where(this.IsManifestVisible) + .ToList(); + + this.NotifyAvailablePluginsChanged(); + } + + /// + /// Scan the devPlugins folder for new DLL files that are not already loaded into the manager. They are not loaded, + /// only shown as disabled in the installed plugins window. This is a modified version of LoadAllPlugins that works + /// a little differently. + /// + public void ScanDevPlugins() + { + if (!this.devPluginDirectory.Exists) + this.devPluginDirectory.Create(); + + // devPlugins are more freeform. Look for any dll and hope to get lucky. + var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories); + + var listChanged = false; + + foreach (var dllFile in devDllFiles) + { + // This file is already known to us + if (this.InstalledPlugins.Any(lp => lp.DllFile.FullName == dllFile.FullName)) + continue; + + // Manifests are not required for devPlugins. the Plugin type will handle any null manifests. + var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); + var manifest = manifestFile.Exists ? LocalPluginManifest.Load(manifestFile) : null; + + try + { + // Add them to the list and let the user decide, nothing is auto-loaded. + this.LoadPlugin(dllFile, manifest, isDev: true, doNotLoad: true); + listChanged = true; + } + catch (InvalidPluginException) + { + // Not a plugin + } + catch (Exception ex) + { + Log.Error(ex, $"During devPlugin scan, an unexpected error occurred"); + } + } + + if (listChanged) + this.NotifyInstalledPluginsChanged(); + } + + /// + /// Install a plugin from a repository and load it. + /// + /// The plugin definition. + /// If the testing version should be used. + public void InstallPlugin(RemotePluginManifest repoManifest, bool useTesting) + { + Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting})"); + + var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall; + var version = useTesting ? repoManifest.TestingAssemblyVersion : repoManifest.AssemblyVersion; + + var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory.FullName, repoManifest.InternalName, version.ToString())); + + try + { + if (outputDir.Exists) + outputDir.Delete(true); + + outputDir.Create(); + } + catch + { + // ignored, since the plugin may be loaded already + } + + using var client = new WebClient(); + + var tempZip = new FileInfo(Path.GetTempFileName()); + + try + { + Log.Debug($"Downloading plugin to {tempZip} from {downloadUrl}"); + client.DownloadFile(downloadUrl, tempZip.FullName); + } + catch (WebException ex) + { + Log.Error(ex, $"Download of plugin {repoManifest.Name} failed unexpectedly."); + throw; + } + + Log.Debug($"Extracting to {outputDir}"); + // This throws an error, even with overwrite=false + // ZipFile.ExtractToDirectory(tempZip.FullName, outputDir.FullName, false); + using (var archive = ZipFile.OpenRead(tempZip.FullName)) + { + foreach (var zipFile in archive.Entries) + { + var completeFileName = Path.GetFullPath(Path.Combine(outputDir.FullName, zipFile.FullName)); + + if (!completeFileName.StartsWith(outputDir.FullName, StringComparison.OrdinalIgnoreCase)) + { + throw new IOException("Trying to extract file outside of destination directory. See this link for more info: https://snyk.io/research/zip-slip-vulnerability"); + } + + if (zipFile.Name == string.Empty) + { + // Assuming Empty for Directory + Directory.CreateDirectory(Path.GetDirectoryName(completeFileName)); + continue; + } + + try + { + zipFile.ExtractToFile(completeFileName, true); + } + catch (Exception ex) + { + Log.Information($"Could not overwrite {zipFile.Name}: {ex.Message}"); + } + } + } + + tempZip.Delete(); + + var dllFile = LocalPluginManifest.GetPluginFile(outputDir, repoManifest); + var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); + + // Reload as a local manifest, add some attributes, and save again. + var manifest = LocalPluginManifest.Load(manifestFile); + + if (useTesting) + { + manifest.Testing = true; + } + + if (repoManifest.SourceRepo.IsThirdParty) + { + // Only document the url if it came from a third party repo. + manifest.InstalledFromUrl = repoManifest.SourceRepo.PluginMasterUrl; + } + + manifest.Save(manifestFile); + + Log.Information($"Installed plugin {manifest.Name} (testing={useTesting})"); + + this.LoadPlugin(dllFile, manifest); + + this.NotifyInstalledPluginsChanged(); + } + + /// + /// Load a plugin. + /// + /// The associated with the main assembly of this plugin. + /// The already loaded definition, if available. + /// If this plugin should support development features. + /// If this plugin is being loaded at boot. + /// Don't load the plugin, just don't do it. + public void LoadPlugin(FileInfo dllFile, LocalPluginManifest manifest, bool isDev = false, bool isBoot = false, bool doNotLoad = false) + { + var name = manifest?.Name ?? dllFile.Name; + var loadPlugin = !doNotLoad; + + LocalPlugin plugin; + + if (isDev) + { + Log.Information($"Loading dev plugin {name}"); + var devPlugin = new LocalDevPlugin(this.dalamud, dllFile, manifest); + loadPlugin &= !isBoot || devPlugin.StartOnBoot; + + plugin = devPlugin; + } + else + { + Log.Information($"Loading plugin {name}"); + plugin = new LocalPlugin(this.dalamud, dllFile, manifest); + } + + if (loadPlugin) + { + try + { + if (plugin.IsDisabled) + plugin.Enable(); + + plugin.Load(); + } + catch (InvalidPluginException) + { + PluginLocations.Remove(plugin.AssemblyName.FullName); + throw; + } + catch (Exception ex) + { + // Dev plugins always get added to the list so they can be fiddled with in the UI + if (plugin.IsDev) + { + Log.Information(ex, $"Dev plugin failed to load, adding anyways: {dllFile.Name}"); + } + else + { + PluginLocations.Remove(plugin.AssemblyName.FullName); + throw; + } + } + } + + this.installedPlugins.Add(plugin); + } + + /// + /// Remove a plugin. + /// + /// Plugin to remove. + public void RemovePlugin(LocalPlugin plugin) + { + if (plugin.State != PluginState.Unloaded) + throw new InvalidPluginOperationException($"Unable to remove {plugin.Name}, not unloaded"); + + this.installedPlugins.Remove(plugin); + PluginLocations.Remove(plugin.AssemblyName.FullName); + + this.NotifyInstalledPluginsChanged(); + } + + /// + /// Cleanup disabled plugins. Does not target devPlugins. + /// + public void CleanupPlugins() + { + foreach (var pluginDir in this.pluginDirectory.GetDirectories()) + { + try + { + var versionDirs = pluginDir.GetDirectories(); + + versionDirs = versionDirs + .OrderByDescending(dir => + { + var isVersion = Version.TryParse(dir.Name, out var version); + + if (!isVersion) + { + Log.Debug($"Not a version, cleaning up {dir.FullName}"); + dir.Delete(); + } + + return version; + }) + .Where(version => version != null) + .ToArray(); + + if (versionDirs.Length == 0) + { + Log.Information($"No versions: cleaning up {pluginDir.FullName}"); + pluginDir.Delete(true); + continue; + } + else + { + foreach (var versionDir in versionDirs) + { + try + { + var dllFile = new FileInfo(Path.Combine(versionDir.FullName, $"{pluginDir.Name}.dll")); + if (!dllFile.Exists) + { + Log.Information($"Missing dll: cleaning up {versionDir.FullName}"); + versionDir.Delete(true); + continue; + } + + var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); + if (!manifestFile.Exists) + { + Log.Information($"Missing manifest: cleaning up {versionDir.FullName}"); + versionDir.Delete(true); + continue; + } + + var manifest = LocalPluginManifest.Load(manifestFile); + if (manifest.Disabled) + { + Log.Information($"Disabled: cleaning up {versionDir.FullName}"); + versionDir.Delete(true); + continue; + } + + if (manifest.DalamudApiLevel < DalamudApiLevel) + { + Log.Information($"Lower API: cleaning up {versionDir.FullName}"); + versionDir.Delete(true); + continue; + } + + if (manifest.ApplicableVersion < this.dalamud.StartInfo.GameVersion) + { + Log.Information($"Inapplicable version: cleaning up {versionDir.FullName}"); + versionDir.Delete(true); + continue; + } + } + catch (Exception ex) + { + Log.Error(ex, $"Could not clean up {versionDir.FullName}"); + } + } + } + } + catch (Exception ex) + { + Log.Error(ex, $"Could not clean up {pluginDir.FullName}"); + } + } + } + + /// + /// Update all plugins. + /// + /// Perform a dry run, don't install anything. + /// Success or failure and a list of updated plugin metadata. + public List UpdatePlugins(bool dryRun = false) + { + Log.Information("Starting plugin update"); + + var listChanged = false; + + var updatedList = new List(); + + // Prevent collection was modified errors + for (var i = 0; i < this.updatablePlugins.Count; i++) + { + var metadata = this.updatablePlugins[i]; + + var plugin = metadata.InstalledPlugin; + + // Can't update that! + if (plugin is LocalDevPlugin) + continue; + + var updateStatus = new PluginUpdateStatus() + { + InternalName = plugin.Manifest.InternalName, + Name = plugin.Manifest.Name, + Version = metadata.UseTesting + ? metadata.UpdateManifest.TestingAssemblyVersion + : metadata.UpdateManifest.AssemblyVersion, + }; + + if (dryRun) + { + updateStatus.WasUpdated = true; + updatedList.Add(updateStatus); + } + else + { + // Unload if loaded + if (plugin.State == PluginState.Loaded || plugin.State == PluginState.LoadError) + { + try + { + plugin.Unload(); + } + catch (Exception ex) + { + Log.Error(ex, "Error during unload (update)"); + continue; + } + } + + try + { + plugin.Disable(); + this.installedPlugins.Remove(plugin); + listChanged = true; + } + catch (Exception ex) + { + Log.Error(ex, "Error during disable (update)"); + continue; + } + + try + { + this.InstallPlugin(metadata.UpdateManifest, metadata.UseTesting); + listChanged = true; + } + catch (Exception ex) + { + Log.Error(ex, "Error during install (update)"); + continue; + } + } + } + + if (listChanged) + this.NotifyInstalledPluginsChanged(); + + Log.Information("Plugin update OK."); + + return updatedList; + } + + /// + /// Print to chat any plugin updates and whether they were successful. + /// + /// The list of updated plugin metadata. + /// The header text to send to chat prior to any update info. + public void PrintUpdatedPlugins(List updateMetadata, string header) + { + if (updateMetadata != null && updateMetadata.Count > 0) + { + this.dalamud.Framework.Gui.Chat.Print(header); + + foreach (var metadata in updateMetadata) + { + if (metadata.WasUpdated) + { + this.dalamud.Framework.Gui.Chat.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version)); + } + else + { + this.dalamud.Framework.Gui.Chat.PrintChat(new XivChatEntry + { + MessageBytes = Encoding.UTF8.GetBytes(Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version)), + Type = XivChatType.Urgent, + }); + } + } + } + } + + /// + /// For a given manifest, determine if the testing version should be used over the normal version. + /// The higher of the two versions is calculated after checking other settings. + /// + /// Manifest to check. + /// A value indicating whether testing should be used. + public bool UseTesting(PluginManifest manifest) + { + if (!this.dalamud.Configuration.DoPluginTest) + return false; + + if (manifest.IsTestingExclusive) + return true; + + var av = manifest.AssemblyVersion; + var tv = manifest.TestingAssemblyVersion; + var hasAv = av != null; + var hasTv = tv != null; + + if (hasAv && hasTv) + { + return tv > av; + } + else + { + return hasTv; + } + } + + /// + /// Gets a value indicating whether the given repo manifest should be visible to the user. + /// + /// Repo manifest. + /// If the manifest is visible. + public bool IsManifestVisible(RemotePluginManifest manifest) + { + // Hidden by user + if (this.dalamud.Configuration.HiddenPluginInternalName.Contains(manifest.InternalName)) + return false; + + // Hidden by manifest + if (manifest.IsHide) + return false; + + return true; + } + + /// + /// Gets a value indicating whether the given manifest is eligible for ANYTHING. These are hard + /// checks that should not allow installation or loading. + /// + /// Plugin manifest. + /// If the manifest is eligible. + public bool IsManifestEligible(PluginManifest manifest) + { + // Testing exclusive + if (manifest.IsTestingExclusive && !this.dalamud.Configuration.DoPluginTest) + return false; + + // Applicable version + if (manifest.ApplicableVersion < this.dalamud.StartInfo.GameVersion) + return false; + + // API level + if (manifest.DalamudApiLevel < DalamudApiLevel) + return false; + + // Banned + if (this.IsManifestBanned(manifest)) + return false; + + return true; + } + + private bool IsManifestBanned(PluginManifest manifest) + { + return this.bannedPlugins.Any(ban => ban.Name == manifest.InternalName && ban.AssemblyVersion == manifest.AssemblyVersion); + } + + private void DetectAvailablePluginUpdates() + { + var updatablePlugins = new List(); + + for (var i = 0; i < this.installedPlugins.Count; i++) + { + var plugin = this.installedPlugins[i]; + + var installedVersion = plugin.IsTesting + ? plugin.Manifest.TestingAssemblyVersion + : plugin.Manifest.AssemblyVersion; + + var updates = this.availablePlugins + .Where(remoteManifest => plugin.Manifest.InternalName == remoteManifest.InternalName) + .Select(remoteManifest => + { + var useTesting = this.UseTesting(remoteManifest); + var candidateVersion = useTesting + ? remoteManifest.TestingAssemblyVersion + : remoteManifest.AssemblyVersion; + var isUpdate = candidateVersion > installedVersion; + + return (isUpdate, useTesting, candidateVersion, remoteManifest); + }) + .Where(tpl => tpl.isUpdate) + .ToList(); + + if (updates.Count > 0) + { + var update = updates.Aggregate((t1, t2) => t1.candidateVersion > t2.candidateVersion ? t1 : t2); + updatablePlugins.Add(new(plugin, update.remoteManifest, update.useTesting)); + } + } + + this.updatablePlugins = updatablePlugins; + } + + private void NotifyAvailablePluginsChanged() + { + this.DetectAvailablePluginUpdates(); + + try + { + this.AvailablePlugins = ImmutableList.CreateRange(this.availablePlugins); + this.UpdatablePlugins = ImmutableList.CreateRange(this.updatablePlugins); + this.OnAvailablePluginsChanged.Invoke(); + } + catch (Exception ex) + { + Log.Error(ex, $"Error notifying {nameof(this.OnAvailablePluginsChanged)}"); + } + } + + private void NotifyInstalledPluginsChanged() + { + this.DetectAvailablePluginUpdates(); + + try + { + this.InstalledPlugins = ImmutableList.CreateRange(this.installedPlugins); + this.UpdatablePlugins = ImmutableList.CreateRange(this.updatablePlugins); + this.OnInstalledPluginsChanged.Invoke(); + } + catch (Exception ex) + { + Log.Error(ex, $"Error notifying {nameof(this.OnInstalledPluginsChanged)}"); + } + } + + private struct BannedPlugin + { + [JsonProperty] + public string Name { get; private set; } + + [JsonProperty] + public Version AssemblyVersion { get; private set; } + } + + private struct PluginDef + { + public PluginDef(FileInfo dllFile, LocalPluginManifest manifest, bool isDev) + { + this.DllFile = dllFile; + this.Manifest = manifest; + this.IsDev = isDev; + } + + public FileInfo DllFile { get; init; } + + public LocalPluginManifest Manifest { get; init; } + + public bool IsDev { get; init; } + + public static int Sorter(PluginDef def1, PluginDef def2) + { + var prio1 = def1.Manifest?.LoadPriority ?? 0; + var prio2 = def2.Manifest?.LoadPriority ?? 0; + return prio2.CompareTo(prio1); + } + } + + private static class Locs + { + public static string DalamudPluginUpdateSuccessful(string name, Version version) => Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}.").Format(name, version); + + public static string DalamudPluginUpdateFailed(string name, Version version) => Loc.Localize("DalamudPluginUpdateFailed", " 》 {0} update to v{1} failed.").Format(name, version); + } + } + + /// + /// Class responsible for loading and unloading plugins. + /// This contains the assembly patching functionality to resolve assembly locations. + /// + internal partial class PluginManager + { + /// + /// A mapping of plugin assembly name to patch data. Used to fill in missing data due to loading + /// plugins via byte[]. + /// + internal static readonly Dictionary PluginLocations = new(); + + /// + /// Patch method for internal class RuntimeAssembly.Location, also known as Assembly.Location. + /// This patch facilitates resolving the assembly location for plugins that are loaded via byte[]. + /// It should never be called manually. + /// + /// The equivalent of `this`. + /// The result from the original method. + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Enforced naming for special injected parameters")] + private static void AssemblyLocationPatch(Assembly __instance, ref string __result) + { + // Assembly.GetExecutingAssembly can return this. + // Check for it as a special case and find the plugin. + if (__result.EndsWith("System.Private.CoreLib.dll", StringComparison.InvariantCultureIgnoreCase)) + { + foreach (var assemblyName in GetStackFrameAssemblyNames()) + { + if (PluginLocations.TryGetValue(assemblyName, out var data)) + { + __result = data.Location; + return; + } + } + } + else if (string.IsNullOrEmpty(__result)) + { + if (PluginLocations.TryGetValue(__instance.FullName, out var data)) + { + __result = data.Location; + } + } + } + + /// + /// Patch method for internal class RuntimeAssembly.CodeBase, also known as Assembly.CodeBase. + /// This patch facilitates resolving the assembly location for plugins that are loaded via byte[]. + /// It should never be called manually. + /// + /// The equivalent of `this`. + /// The result from the original method. + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Enforced naming for special injected parameters")] + private static void AssemblyCodeBasePatch(Assembly __instance, ref string __result) + { + // Assembly.GetExecutingAssembly can return this. + // Check for it as a special case and find the plugin. + if (__result.EndsWith("System.Private.CoreLib.dll")) + { + foreach (var assemblyName in GetStackFrameAssemblyNames()) + { + if (PluginLocations.TryGetValue(assemblyName, out var data)) + { + __result = data.Location; + return; + } + } + } + else if (string.IsNullOrEmpty(__result)) + { + if (PluginLocations.TryGetValue(__instance.FullName, out var data)) + { + __result = data.Location; + } + } + } + + private static IEnumerable GetStackFrameAssemblyNames() + { + var stackTrace = new StackTrace(); + var stackFrames = stackTrace.GetFrames(); + + foreach (var stackFrame in stackFrames) + { + var methodBase = stackFrame.GetMethod(); + if (methodBase == null) + continue; + + yield return methodBase.Module.Assembly.FullName; + } + } + + private void ApplyPatches() + { + var harmony = new Harmony("goatcorp.dalamud.pluginmanager"); + + var targetType = typeof(PluginManager).Assembly.GetType(); + + var locationTarget = AccessTools.PropertyGetter(targetType, nameof(Assembly.Location)); + var locationPatch = AccessTools.Method(typeof(PluginManager), nameof(PluginManager.AssemblyLocationPatch)); + harmony.Patch(locationTarget, postfix: new(locationPatch)); + +#pragma warning disable SYSLIB0012 // Type or member is obsolete + var codebaseTarget = AccessTools.PropertyGetter(targetType, nameof(Assembly.CodeBase)); + var codebasePatch = AccessTools.Method(typeof(PluginManager), nameof(PluginManager.AssemblyCodeBasePatch)); + harmony.Patch(codebaseTarget, postfix: new(codebasePatch)); +#pragma warning restore SYSLIB0012 // Type or member is obsolete + } + + internal record PluginPatchData + { + /// + /// Initializes a new instance of the class. + /// + /// DLL file being loaded. + public PluginPatchData(FileInfo dllFile) + { + this.Location = dllFile.FullName; + this.CodeBase = new Uri(dllFile.FullName).AbsoluteUri; + } + + /// + /// Gets simulated Assembly.Location output. + /// + public string Location { get; } + + /// + /// Gets simulated Assembly.CodeBase output. + /// + public string CodeBase { get; } + } + } +} diff --git a/Dalamud/Plugin/Internal/PluginRepository.cs b/Dalamud/Plugin/Internal/PluginRepository.cs new file mode 100644 index 000000000..64769322d --- /dev/null +++ b/Dalamud/Plugin/Internal/PluginRepository.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +using Dalamud.Plugin.Internal.Types; +using Newtonsoft.Json; + +namespace Dalamud.Plugin.Internal +{ + /// + /// This class represents a single plugin repository. + /// + internal partial class PluginRepository + { + private const string DalamudPluginsMasterUrl = "https://raw.githubusercontent.com/goatcorp/DalamudPlugins/master/pluginmaster.json"; + + private static readonly ModuleLog Log = new("PLUGINR"); + + /// + /// Initializes a new instance of the class. + /// + /// The plugin master URL. + /// Whether the plugin repo is enabled. + public PluginRepository(string pluginMasterUrl, bool isEnabled) + { + this.PluginMasterUrl = pluginMasterUrl; + this.IsThirdParty = pluginMasterUrl != DalamudPluginsMasterUrl; + this.IsEnabled = isEnabled; + + // No need to wait for this + Task.Run(this.ReloadPluginMasterAsync); + } + + /// + /// Gets a new instance of the class for the main repo. + /// + public static PluginRepository MainRepo => new(DalamudPluginsMasterUrl, true); + + /// + /// Gets the pluginmaster.json URL. + /// + public string PluginMasterUrl { get; } + + /// + /// Gets a value indicating whether this plugin repository is from a third party. + /// + public bool IsThirdParty { get; } + + /// + /// Gets a value indicating whether this repo is enabled. + /// + public bool IsEnabled { get; } + + /// + /// Gets the plugin master list of available plugins. + /// + public ReadOnlyCollection PluginMaster { get; private set; } + + /// + /// Gets the initialization state of the plugin repository. + /// + public PluginRepositoryState State { get; private set; } + + /// + /// Reload the plugin master asynchronously in a task. + /// + /// The new state. + public Task ReloadPluginMasterAsync() + { + this.State = PluginRepositoryState.InProgress; + this.PluginMaster = new List().AsReadOnly(); + + return Task.Run(() => + { + using var client = new WebClient(); + + Log.Information($"Fetching repo: {this.PluginMasterUrl}"); + + var data = client.DownloadString(this.PluginMasterUrl); + + var pluginMaster = JsonConvert.DeserializeObject>(data); + 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(); + }).ContinueWith(task => + { + if (task.IsCompletedSuccessfully) + { + Log.Debug($"Successfully fetched repo: {this.PluginMasterUrl}"); + this.State = PluginRepositoryState.Success; + } + else + { + Log.Error(task.Exception, $"PluginMaster failed: {this.PluginMasterUrl}"); + this.State = PluginRepositoryState.Fail; + } + }); + } + } +} diff --git a/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs b/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs new file mode 100644 index 000000000..32dde337c --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs @@ -0,0 +1,36 @@ +namespace Dalamud.Plugin.Internal.Types +{ + /// + /// Information about an available plugin update. + /// + internal record AvailablePluginUpdate + { + /// + /// Initializes a new instance of the class. + /// + /// The installed plugin to update. + /// The manifest to use for the update. + /// If the testing version should be used for the update. + public AvailablePluginUpdate(LocalPlugin installedPlugin, RemotePluginManifest updateManifest, bool useTesting) + { + this.InstalledPlugin = installedPlugin; + this.UpdateManifest = updateManifest; + this.UseTesting = useTesting; + } + + /// + /// Gets the currently installed plugin. + /// + public LocalPlugin InstalledPlugin { get; init; } + + /// + /// Gets the available update manifest. + /// + public RemotePluginManifest UpdateManifest { get; init; } + + /// + /// Gets a value indicating whether the update should use the testing URL. + /// + public bool UseTesting { get; init; } + } +} diff --git a/Dalamud/Plugin/Internal/Types/IpcSubscription.cs b/Dalamud/Plugin/Internal/Types/IpcSubscription.cs new file mode 100644 index 000000000..3427a824a --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/IpcSubscription.cs @@ -0,0 +1,39 @@ +using System; +using System.Dynamic; + +namespace Dalamud.Plugin.Internal.Types +{ + /// + /// This class represents an IPC subscription between two plugins. + /// + internal record IpcSubscription + { + /// + /// Initializes a new instance of the class. + /// + /// The source plugin name. + /// The name of the plugin being subscribed to. + /// The subscription action. + public IpcSubscription(string sourcePluginName, string subPluginName, Action subAction) + { + this.SourcePluginName = sourcePluginName; + this.SubPluginName = subPluginName; + this.SubAction = subAction; + } + + /// + /// Gets the name of the plugin requesting the subscription. + /// + public string SourcePluginName { get; } + + /// + /// Gets the name of the plugin being subscribed to. + /// + public string SubPluginName { get; } + + /// + /// Gets the subscription action. + /// + public Action SubAction { get; } + } +} diff --git a/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs new file mode 100644 index 000000000..d0a3b7448 --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs @@ -0,0 +1,73 @@ +using System.IO; + +using Newtonsoft.Json; + +namespace Dalamud.Plugin.Internal.Types +{ + /// + /// 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. + /// + internal record LocalPluginManifest : PluginManifest + { + /// + /// 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. + /// + public bool Disabled { get; set; } = false; + + /// + /// 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. + /// + public bool Testing { get; set; } = false; + + /// + /// 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. + /// + public string InstalledFromUrl { get; set; } + + /// + /// Save a plugin manifest to file. + /// + /// Path to save at. + public void Save(FileInfo manifestFile) => File.WriteAllText(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented)); + + /// + /// Loads a plugin manifest from file. + /// + /// Path to the manifest. + /// A object. + public static LocalPluginManifest Load(FileInfo manifestFile) => JsonConvert.DeserializeObject(File.ReadAllText(manifestFile.FullName)); + + /// + /// A standardized way to get the plugin DLL name that should accompany a manifest file. May not exist. + /// + /// Manifest directory. + /// The manifest. + /// The file. + public static FileInfo GetPluginFile(DirectoryInfo dir, PluginManifest manifest) => new(Path.Combine(dir.FullName, $"{manifest.InternalName}.dll")); + + /// + /// A standardized way to get the manifest file that should accompany a plugin DLL. May not exist. + /// + /// The plugin DLL. + /// The file. + public static FileInfo GetManifestFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, Path.GetFileNameWithoutExtension(dllFile.Name) + ".json")); + + /// + /// A standardized way to get the obsolete .disabled file that should accompany a plugin DLL. May not exist. + /// + /// The plugin DLL. + /// The .disabled file. + public static FileInfo GetDisabledFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, ".disabled")); + + /// + /// A standardized way to get the obsolete .testing file that should accompany a plugin DLL. May not exist. + /// + /// The plugin DLL. + /// The .testing file. + public static FileInfo GetTestingFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, ".testing")); + } +} diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs new file mode 100644 index 000000000..3c937d710 --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; + +using Dalamud.Game; +using Newtonsoft.Json; + +namespace Dalamud.Plugin.Internal.Types +{ + /// + /// Information about a plugin, packaged in a json file with the DLL. + /// + internal record PluginManifest + { + /// + /// Gets the author/s of the plugin. + /// + [JsonProperty] + public string Author { get; init; } + + /// + /// Gets or sets the public name of the plugin. + /// + [JsonProperty] + public string Name { get; set; } + + /// + /// Gets a description of the plugins functions. + /// + [JsonProperty] + public string Description { get; init; } + + /// + /// Gets a list of tags defined on the plugin. + /// + [JsonProperty] + public List Tags { get; init; } + + /// + /// 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. + /// + [JsonProperty] + public bool IsHide { get; init; } + + /// + /// Gets the internal name of the plugin, which should match the assembly name of the plugin. + /// + [JsonProperty] + public string InternalName { get; init; } + + /// + /// Gets the current assembly version of the plugin. + /// + [JsonProperty] + public Version AssemblyVersion { get; init; } + + /// + /// Gets the current testing assembly version of the plugin. + /// + [JsonProperty] + public Version TestingAssemblyVersion { get; init; } + + /// + /// Gets a value indicating whether the is not null. + /// + [JsonIgnore] + public bool HasAssemblyVersion => this.AssemblyVersion != null; + + /// + /// Gets a value indicating whether the is not null. + /// + [JsonIgnore] + public bool HasTestingAssemblyVersion => this.TestingAssemblyVersion != null; + + /// + /// Gets a value indicating whether the plugin is only available for testing. + /// + [JsonProperty] + public bool IsTestingExclusive { get; init; } + + /// + /// Gets an URL to the website or source code of the plugin. + /// + [JsonProperty] + public string RepoUrl { get; init; } + + /// + /// Gets the version of the game this plugin works with. + /// + [JsonProperty] + [JsonConverter(typeof(GameVersionConverter))] + public GameVersion ApplicableVersion { get; init; } = GameVersion.Any; + + /// + /// Gets the API level of this plugin. For the current API level, please see + /// for the currently used API level. + /// + [JsonProperty] + public int DalamudApiLevel { get; init; } = PluginManager.DalamudApiLevel; + + /// + /// Gets the number of downloads this plugin has. + /// + [JsonProperty] + public long DownloadCount { get; init; } + + /// + /// Gets the last time this plugin was updated. + /// + [JsonProperty] + public long LastUpdate { get; init; } + + /// + /// Gets the download link used to install the plugin. + /// + [JsonProperty] + public string DownloadLinkInstall { get; init; } + + /// + /// Gets the download link used to update the plugin. + /// + [JsonProperty] + public string DownloadLinkUpdate { get; init; } + + /// + /// Gets the download link used to get testing versions of the plugin. + /// + [JsonProperty] + public string DownloadLinkTesting { get; init; } + + /// + /// Gets the load priority for this plugin. Higher values means higher priority. 0 is default priority. + /// + [JsonProperty] + public int LoadPriority { get; init; } + } +} diff --git a/Dalamud/Plugin/Internal/Types/PluginOperationResult.cs b/Dalamud/Plugin/Internal/Types/PluginOperationResult.cs new file mode 100644 index 000000000..9ac1db611 --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/PluginOperationResult.cs @@ -0,0 +1,57 @@ +using System; + +namespace Dalamud.Plugin.Internal.Types +{ + /// + /// This represents the result of a an operation taken against a plugin. + /// Loading, unloading, installation, etc. + /// + internal enum PluginOperationResult + { + /// + /// The result is unknown. Should not be used. + /// + [Obsolete("Do not use this", error: true)] + Unknown, + + /// + /// The result is pending. Take a seat and wait. + /// + Pending, + + /// + /// The operation was successful. + /// + Success, + + /// + /// During the plugin operation, an unexpected error occurred. + /// + UnknownError, + + /// + /// The plugin state was invalid for the attempted operation. + /// + InvalidState, + + /// + /// The plugin applicable version is not compativle with the currently running game. + /// + InvalidGameVersion, + + /// + /// The plugin API level is not compatible with the currently running Dalamud. + /// + InvalidApiLevel, + + /// + /// During loading, the current plugin was marked as disabled. + /// + InvalidStateDisabled, + + /// + /// During loading, another plugin was detected with the same internal name. + /// + InvalidStateDuplicate, + } +} diff --git a/Dalamud/Plugin/Internal/Types/PluginRepositoryState.cs b/Dalamud/Plugin/Internal/Types/PluginRepositoryState.cs new file mode 100644 index 000000000..46aa2c351 --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/PluginRepositoryState.cs @@ -0,0 +1,28 @@ +namespace Dalamud.Plugin.Internal.Types +{ + /// + /// Values representing plugin repository state. + /// + internal enum PluginRepositoryState + { + /// + /// State is unknown. + /// + Unknown, + + /// + /// Currently loading. + /// + InProgress, + + /// + /// Load was successful. + /// + Success, + + /// + /// Load failed. + /// + Fail, + } +} diff --git a/Dalamud/Plugin/Internal/Types/PluginState.cs b/Dalamud/Plugin/Internal/Types/PluginState.cs new file mode 100644 index 000000000..f32543b39 --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/PluginState.cs @@ -0,0 +1,33 @@ +namespace Dalamud.Plugin.Internal.Types +{ + /// + /// Values representing plugin load state. + /// + internal enum PluginState + { + /// + /// Plugin is defined, but unloaded. + /// + Unloaded, + + /// + /// Plugin has thrown an error during unload. + /// + UnloadError, + + /// + /// Currently loading. + /// + InProgress, + + /// + /// Load is successful. + /// + Loaded, + + /// + /// Plugin has thrown an error during loading. + /// + LoadError, + } +} diff --git a/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs b/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs new file mode 100644 index 000000000..f0394b9b7 --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs @@ -0,0 +1,30 @@ +using System; + +namespace Dalamud.Plugin.Internal.Types +{ + /// + /// Plugin update status. + /// + internal class PluginUpdateStatus + { + /// + /// Gets or sets the plugin internal name. + /// + public string InternalName { get; set; } + + /// + /// Gets or sets the plugin name. + /// + public string Name { get; set; } + + /// + /// Gets or sets the plugin version. + /// + public Version Version { get; set; } + + /// + /// Gets or sets a value indicating whether the plugin was updated. + /// + public bool WasUpdated { get; set; } + } +} diff --git a/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs b/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs new file mode 100644 index 000000000..cbb989159 --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Dalamud.Plugin.Internal.Types +{ + /// + /// 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. + /// + internal record RemotePluginManifest : PluginManifest + { + /// + /// 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. + /// + [JsonIgnore] + public PluginRepository SourceRepo { get; set; } = null; + } +} diff --git a/Dalamud/Plugin/PluginDefinition.cs b/Dalamud/Plugin/PluginDefinition.cs deleted file mode 100644 index 088688811..000000000 --- a/Dalamud/Plugin/PluginDefinition.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Dalamud.Plugin -{ - /// - /// Class containing information about a plugin. - /// - public class PluginDefinition - { - /// - /// Gets or sets the author/s of the plugin. - /// - public string Author { get; set; } - - /// - /// Gets or sets the public name of the plugin. - /// - public string Name { get; set; } - - /// - /// Gets or sets the internal name of the plugin, which should match the assembly name of the plugin. - /// - public string InternalName { get; set; } - - /// - /// Gets or sets the current assembly version of the plugin. - /// - public string AssemblyVersion { get; set; } - - /// - /// Gets or sets the current testing assembly version of the plugin. - /// - public string TestingAssemblyVersion { get; set; } - - /// - /// Gets or sets a value indicating whether the plugin is only available for testing. - /// - public bool IsTestingExclusive { get; set; } - - /// - /// Gets or sets a description of the plugins functions. - /// - public string Description { get; set; } - - /// - /// Gets or sets the version of the game this plugin works with. - /// - public string ApplicableVersion { get; set; } - - /// - /// Gets or sets an URL to the website or source code of the plugin. - /// - public string RepoUrl { get; set; } - - /// - /// Gets or sets a list of tags defined on the plugin. - /// - public List Tags { get; set; } - - /// - /// Gets or sets a value indicating whether or not the plugin is hidden in the plugin installer. - /// - public bool IsHide { get; set; } - - /// - /// Gets or sets the API level of this plugin. For the current API level, please see for the currently used API level. - /// - public int DalamudApiLevel { get; set; } - - /// - /// Gets or sets the number of downloads this plugin has. - /// - public long DownloadCount { get; set; } - - /// - /// Gets or sets the last time this plugin was updated. - /// - public long LastUpdate { get; set; } - - /// - /// Gets or sets the index of the third party repo. - /// - public int RepoNumber { get; set; } - - /// - /// Gets or sets the download link used to install the plugin. - /// - public string DownloadLinkInstall { get; set; } - - /// - /// Gets or sets the download link used to update the plugin. - /// - public string DownloadLinkUpdate { get; set; } - - /// - /// Gets or sets the download link used to get testing versions of the plugin. - /// - public string DownloadLinkTesting { get; set; } - - /// - /// Gets or sets the load priority for this plugin. Higher values means higher priority. 0 is default priority. - /// - public int LoadPriority { get; set; } - } -} diff --git a/Dalamud/Plugin/PluginInstallerWindow.cs b/Dalamud/Plugin/PluginInstallerWindow.cs deleted file mode 100644 index 3be2305e2..000000000 --- a/Dalamud/Plugin/PluginInstallerWindow.cs +++ /dev/null @@ -1,607 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Numerics; -using System.Threading.Tasks; - -using CheapLoc; -using Dalamud.Interface; -using Dalamud.Interface.Colors; -using Dalamud.Interface.Windowing; -using ImGuiNET; -using Serilog; - -namespace Dalamud.Plugin -{ - /// - /// Class responsible for drawing the plugin installer. - /// - internal class PluginInstallerWindow : Window - { - private readonly Dalamud dalamud; - - private string gameVersion; - - private bool errorModalDrawing = true; - private bool errorModalOnNextFrame = false; - - private bool updateComplete = false; - private int updatePluginCount = 0; - private List updatedPlugins; - - private List pluginListAvailable; - private List pluginListInstalled; - - private string searchText = string.Empty; - - private PluginSortKind sortKind = PluginSortKind.Alphabetical; - private string filterText = Loc.Localize("SortAlphabetical", "Alphabetical"); - - private PluginInstallStatus installStatus = PluginInstallStatus.None; - - /// - /// Initializes a new instance of the class. - /// - /// The relevant Dalamud instance. - /// The version of the game. - public PluginInstallerWindow(Dalamud dalamud, string gameVersion) - : base( - Loc.Localize("InstallerHeader", "Plugin Installer") + (dalamud.Configuration.DoPluginTest ? " (TESTING)" : string.Empty) + "###XlPluginInstaller", - ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoScrollbar) - { - this.dalamud = dalamud; - this.gameVersion = gameVersion; - - this.Size = new Vector2(810, 520); - this.SizeCondition = ImGuiCond.Always; - } - - private enum PluginInstallStatus - { - None, - InProgress, - Success, - Fail, - } - - private enum PluginSortKind - { - Alphabetical, - DownloadCount, - LastUpdate, - } - - /// - /// Code to be executed when the window is opened. - /// - public override void OnOpen() - { - base.OnOpen(); - - if (this.dalamud.PluginRepository.State != PluginRepository.InitializationState.InProgress) - this.dalamud.PluginRepository.ReloadPluginMasterAsync(); - - this.pluginListAvailable = null; - this.pluginListInstalled = null; - this.updateComplete = false; - this.updatePluginCount = 0; - this.updatedPlugins = null; - this.searchText = string.Empty; - this.sortKind = PluginSortKind.Alphabetical; - this.filterText = Loc.Localize("SortAlphabetical", "Alphabetical"); - } - - /// - /// Draw the plugin installer view ImGui. - /// - public override void Draw() - { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5 * ImGui.GetIO().FontGlobalScale)); - var descriptionText = Loc.Localize("InstallerHint", "This window allows you to install and remove in-game plugins.\nThey are made by third-party developers."); - ImGui.Text(descriptionText); - - var sortingTextSize = ImGui.CalcTextSize(Loc.Localize("SortDownloadCounts", "Download Count")) + ImGui.CalcTextSize(Loc.Localize("PluginSort", "Sort By")); - ImGui.SameLine(ImGui.GetWindowWidth() - sortingTextSize.X - ((250 + 20) * ImGui.GetIO().FontGlobalScale)); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (ImGui.CalcTextSize(descriptionText).Y / 4) - 2); - ImGui.SetCursorPosX(ImGui.GetWindowWidth() - sortingTextSize.X - ((250 + 20) * ImGui.GetIO().FontGlobalScale)); - - ImGui.SetNextItemWidth(240 * ImGui.GetIO().FontGlobalScale); - ImGui.InputTextWithHint("###XPlPluginInstaller_Search", Loc.Localize("InstallerSearch", "Search"), ref this.searchText, 100); - - ImGui.SameLine(); - ImGui.SetNextItemWidth((10 * ImGui.GetIO().FontGlobalScale) + ImGui.CalcTextSize(Loc.Localize("SortDownloadCounts", "Download Count")).X); - if (ImGui.BeginCombo(Loc.Localize("PluginSort", "Sort By"), this.filterText, ImGuiComboFlags.NoArrowButton)) - { - if (ImGui.Selectable(Loc.Localize("SortAlphabetical", "Alphabetical"))) - { - this.sortKind = PluginSortKind.Alphabetical; - this.filterText = Loc.Localize("SortAlphabetical", "Alphabetical"); - - this.ResortPlugins(); - } - - if (ImGui.Selectable(Loc.Localize("SortDownloadCounts", "Download Count"))) - { - this.sortKind = PluginSortKind.DownloadCount; - this.filterText = Loc.Localize("SortDownloadCounts", "Download Count"); - - this.ResortPlugins(); - } - - if (ImGui.Selectable(Loc.Localize("SortLastUpdate", "Last Update"))) - { - this.sortKind = PluginSortKind.LastUpdate; - this.filterText = Loc.Localize("SortLastUpdate", "Last Update"); - - this.ResortPlugins(); - } - - ImGui.EndCombo(); - } - - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5 * ImGui.GetIO().FontGlobalScale)); - - (string Text, Vector4 Color) initializationStatusText = (null, ImGuiColors.DalamudGrey); - if (this.dalamud.PluginRepository.State == PluginRepository.InitializationState.InProgress) - { - initializationStatusText.Text = Loc.Localize("InstallerLoading", "Loading plugins..."); - this.pluginListAvailable = null; - } - else if (this.dalamud.PluginRepository.State == PluginRepository.InitializationState.Fail) - { - initializationStatusText.Text = Loc.Localize("InstallerDownloadFailed", "Download failed."); - this.pluginListAvailable = null; - } - else - { - if (this.dalamud.PluginRepository.State == PluginRepository.InitializationState.FailThirdRepo) - { - initializationStatusText.Text = Loc.Localize("InstallerDownloadFailedThird", "One of your third party repos is unreachable or there is no internet connection."); - initializationStatusText.Color = ImGuiColors.DalamudRed; - } - - if (this.pluginListAvailable == null) - { - this.RefetchPlugins(); - } - } - - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, 3) * ImGui.GetIO().FontGlobalScale); - - if (ImGui.BeginTabBar("PluginsTabBar", ImGuiTabBarFlags.NoTooltip)) - { - this.DrawTab(false, initializationStatusText); - this.DrawTab(true, initializationStatusText); - - ImGui.EndTabBar(); - ImGui.Separator(); - } - - ImGui.PopStyleVar(); - - ImGui.Dummy(new Vector2(3f, 3f) * ImGui.GetIO().FontGlobalScale); - - if (this.installStatus == PluginInstallStatus.InProgress) - { - ImGui.Button(Loc.Localize("InstallerUpdating", "Updating...")); - } - else - { - if (this.updateComplete) - { - ImGui.Button(this.updatePluginCount == 0 - ? Loc.Localize("InstallerNoUpdates", "No updates found!") - : string.Format(Loc.Localize("InstallerUpdateComplete", "{0} plugins updated!"), this.updatePluginCount)); - } - else - { - if (ImGui.Button(Loc.Localize("InstallerUpdatePlugins", "Update plugins")) && - this.dalamud.PluginRepository.State == PluginRepository.InitializationState.Success) - { - this.installStatus = PluginInstallStatus.InProgress; - - Task.Run(() => this.dalamud.PluginRepository.UpdatePlugins()).ContinueWith(t => - { - this.installStatus = - t.Result.Success ? PluginInstallStatus.Success : PluginInstallStatus.Fail; - this.installStatus = - t.IsFaulted ? PluginInstallStatus.Fail : this.installStatus; - - if (this.installStatus == PluginInstallStatus.Success) - { - this.updateComplete = true; - } - - if (t.Result.UpdatedPlugins != null) - { - this.updatePluginCount = t.Result.UpdatedPlugins.Count; - this.updatedPlugins = t.Result.UpdatedPlugins; - } - - this.errorModalDrawing = this.installStatus == PluginInstallStatus.Fail; - this.errorModalOnNextFrame = this.installStatus == PluginInstallStatus.Fail; - - this.dalamud.PluginRepository.PrintUpdatedPlugins( - this.updatedPlugins, Loc.Localize("DalamudPluginUpdates", "Updates:")); - - this.RefetchPlugins(); - }); - } - } - } - - ImGui.SameLine(); - - if (ImGui.Button(Loc.Localize("SettingsInstaller", "Settings"))) - { - this.dalamud.DalamudUi.OpenSettings(); - } - - var closeText = Loc.Localize("Close", "Close"); - - ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(closeText).X - (16 * ImGui.GetIO().FontGlobalScale)); - if (ImGui.Button(closeText)) - { - this.IsOpen = false; - this.dalamud.Configuration.Save(); - } - - if (ImGui.BeginPopupModal(Loc.Localize("InstallerError", "Installer failed"), ref this.errorModalDrawing, ImGuiWindowFlags.AlwaysAutoResize)) - { - var message = Loc.Localize( - "InstallerErrorHint", - "The plugin installer ran into an issue or the plugin is incompatible.\nPlease restart the game and report this error on our discord."); - - if (this.updatedPlugins != null) - { - if (this.updatedPlugins.Any(x => x.WasUpdated == false)) - { - var extraInfoMessage = Loc.Localize( - "InstallerErrorPluginInfo", - "\n\nThe following plugins caused these issues:\n\n{0}\nYou may try removing these plugins manually and reinstalling them."); - - var insert = this.updatedPlugins.Where(x => x.WasUpdated == false) - .Aggregate( - string.Empty, - (current, pluginUpdateStatus) => - current + $"* {pluginUpdateStatus.InternalName}\n"); - extraInfoMessage = string.Format(extraInfoMessage, insert); - message += extraInfoMessage; - } - } - - ImGui.Text(message); - - ImGui.Spacing(); - - if (ImGui.Button(Loc.Localize("OK", "OK"), new Vector2(120, 40))) - { - ImGui.CloseCurrentPopup(); - } - - ImGui.EndPopup(); - } - - if (this.errorModalOnNextFrame) - { - ImGui.OpenPopup(Loc.Localize("InstallerError", "Installer failed")); - this.errorModalOnNextFrame = false; - } - } - - private void RefetchPlugins() - { - var hiddenPlugins = this.dalamud.PluginManager.Plugins.Where( - x => this.dalamud.PluginRepository.PluginMaster.All( - y => y.InternalName != x.Definition.InternalName || (y.InternalName == x.Definition.InternalName && y.IsHide))).Select(x => x.Definition).ToList(); - this.pluginListInstalled = this.dalamud.PluginRepository.PluginMaster - .Where(def => - { - return this.dalamud.PluginManager.Plugins.Where(x => x.Definition != null).Any( - x => x.Definition.InternalName == def.InternalName); - }) - .GroupBy(x => new { x.InternalName, x.AssemblyVersion }) - .Select(y => y.First()).ToList(); - this.pluginListInstalled.AddRange(hiddenPlugins); - this.pluginListInstalled.Sort((x, y) => x.Name.CompareTo(y.Name)); - - this.ResortPlugins(); - } - - private void ResortPlugins() - { - var availableDefs = this.dalamud.PluginRepository.PluginMaster.Where( - x => this.pluginListInstalled.All(y => x.InternalName != y.InternalName)) - .GroupBy(x => new { x.InternalName, x.AssemblyVersion }) - .Select(y => y.First()).ToList(); - - switch (this.sortKind) - { - case PluginSortKind.Alphabetical: - this.pluginListAvailable = availableDefs.OrderBy(x => x.Name).ToList(); - this.pluginListInstalled.Sort((x, y) => x.Name.CompareTo(y.Name)); - break; - case PluginSortKind.DownloadCount: - this.pluginListAvailable = availableDefs.OrderByDescending(x => x.DownloadCount).ToList(); - this.pluginListInstalled.Sort((x, y) => y.DownloadCount.CompareTo(x.DownloadCount)); - break; - case PluginSortKind.LastUpdate: - this.pluginListAvailable = availableDefs.OrderByDescending(x => x.LastUpdate).ToList(); - this.pluginListInstalled.Sort((x, y) => y.LastUpdate.CompareTo(x.LastUpdate)); - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - - private void DrawTab(bool installed, (string Text, Vector4 Color) statusText) - { - if (ImGui.BeginTabItem(installed ? Loc.Localize("InstallerInstalledPluginList", "Installed Plugins") - : Loc.Localize("InstallerAvailablePluginList", "Available Plugins"))) - { - ImGui.BeginChild( - "Scrolling" + (installed ? "Installed" : "Available"), - new Vector2(0, 384 * ImGui.GetIO().FontGlobalScale), - true, - ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoBackground); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 5); - - if (statusText.Text != null) - ImGui.TextColored(statusText.Color, statusText.Text); - var list = installed ? this.pluginListInstalled : this.pluginListAvailable; - if (list != null) - { - this.DrawPluginList(list, installed); - } - - ImGui.EndChild(); - ImGui.EndTabItem(); - } - } - - private void DrawPluginList(List pluginDefinitions, bool installed) - { - var didAny = false; - var didAnyWithSearch = false; - var hasSearchString = !string.IsNullOrWhiteSpace(this.searchText); - - for (var index = 0; index < pluginDefinitions.Count; index++) - { - var pluginDefinition = pluginDefinitions[index]; - - if (pluginDefinition.ApplicableVersion != this.gameVersion && - pluginDefinition.ApplicableVersion != "any") - continue; - - if (pluginDefinition.IsHide) - continue; - - if (pluginDefinition.DalamudApiLevel < PluginManager.DalamudApiLevel) - continue; - - if (this.dalamud.Configuration.HiddenPluginInternalName.Contains(pluginDefinition.InternalName)) - continue; - - didAny = true; - - if (hasSearchString && - !(pluginDefinition.Name.ToLowerInvariant().Contains(this.searchText.ToLowerInvariant()) || - string.Equals(pluginDefinition.Author, this.searchText, StringComparison.InvariantCultureIgnoreCase) || - (pluginDefinition.Tags != null && pluginDefinition.Tags.Contains( - this.searchText.ToLowerInvariant(), - StringComparer.InvariantCultureIgnoreCase)))) - continue; - - didAnyWithSearch = true; - - var isInstalled = this.dalamud.PluginManager.Plugins.Where(x => x.Definition != null).Any( - x => x.Definition.InternalName == pluginDefinition.InternalName); - - var isTestingAvailable = false; - if (Version.TryParse(pluginDefinition.AssemblyVersion, out var assemblyVersion) && - Version.TryParse(pluginDefinition.TestingAssemblyVersion, out var testingAssemblyVersion)) - { - isTestingAvailable = this.dalamud.Configuration.DoPluginTest && - testingAssemblyVersion > assemblyVersion; - } - - if (this.dalamud.Configuration.DoPluginTest && pluginDefinition.IsTestingExclusive) - isTestingAvailable = true; - else if (!installed && !this.dalamud.Configuration.DoPluginTest && pluginDefinition.IsTestingExclusive) continue; - - var label = string.Empty; - if (isInstalled && !installed) - { - label += Loc.Localize("InstallerInstalled", " (installed)"); - } - else if (!isInstalled && installed) - { - label += Loc.Localize("InstallerDisabled", " (disabled)"); - } - - if (this.updatedPlugins != null && - this.updatedPlugins.Any(x => x.InternalName == pluginDefinition.InternalName && x.WasUpdated)) - label += Loc.Localize("InstallerUpdated", " (updated)"); - else if (this.updatedPlugins != null && - this.updatedPlugins.Any(x => x.InternalName == pluginDefinition.InternalName && - x.WasUpdated == false)) - label += Loc.Localize("InstallerUpdateFailed", " (update failed)"); - - if (isTestingAvailable) - label += Loc.Localize("InstallerTestingVersion", " (testing version)"); - - ImGui.PushID(pluginDefinition.InternalName + pluginDefinition.AssemblyVersion + installed + index); - - if (ImGui.CollapsingHeader(pluginDefinition.Name + label + "###Header" + pluginDefinition.InternalName)) - { - ImGui.Indent(); - - ImGui.Text(pluginDefinition.Name); - - ImGui.SameLine(); - - var info = $" by {pluginDefinition.Author}"; - info += pluginDefinition.DownloadCount != 0 - ? $", {pluginDefinition.DownloadCount} downloads" - : ", download count unavailable"; - if (pluginDefinition.RepoNumber != 0) - info += $", from custom plugin repository #{pluginDefinition.RepoNumber}"; - ImGui.TextColored(ImGuiColors.DalamudGrey3, info); - - if (!string.IsNullOrWhiteSpace(pluginDefinition.Description)) - ImGui.TextWrapped(pluginDefinition.Description); - - if (!isInstalled) - { - if (this.installStatus == PluginInstallStatus.InProgress) - { - ImGui.Button(Loc.Localize("InstallerInProgress", "Install in progress...")); - } - else - { - var versionString = isTestingAvailable - ? pluginDefinition.TestingAssemblyVersion + " (testing version)" - : pluginDefinition.AssemblyVersion; - - if (ImGui.Button($"Install v{versionString}")) - { - this.installStatus = PluginInstallStatus.InProgress; - - Task.Run(() => this.dalamud.PluginRepository.InstallPlugin(pluginDefinition, true, false, isTestingAvailable)).ContinueWith(t => - { - this.installStatus = - t.Result ? PluginInstallStatus.Success : PluginInstallStatus.Fail; - this.installStatus = - t.IsFaulted ? PluginInstallStatus.Fail : this.installStatus; - - this.errorModalDrawing = this.installStatus == PluginInstallStatus.Fail; - this.errorModalOnNextFrame = this.installStatus == PluginInstallStatus.Fail; - }); - } - } - - if (!string.IsNullOrEmpty(pluginDefinition.RepoUrl)) - { - ImGui.PushFont(InterfaceManager.IconFont); - - ImGui.SameLine(); - if (ImGui.Button(FontAwesomeIcon.Globe.ToIconString()) && - pluginDefinition.RepoUrl.StartsWith("https://")) - Process.Start(pluginDefinition.RepoUrl); - - ImGui.PopFont(); - } - } - else - { - var installedPlugin = this.dalamud.PluginManager.Plugins.Where(x => x.Definition != null).First( - x => x.Definition.InternalName == - pluginDefinition.InternalName); - - var commands = this.dalamud.CommandManager.Commands.Where( - x => x.Value.LoaderAssemblyName == installedPlugin.Definition?.InternalName && - x.Value.ShowInHelp); - if (commands.Any()) - { - ImGui.Dummy(new Vector2(10f, 10f) * ImGui.GetIO().FontGlobalScale); - foreach (var command in commands) - ImGui.TextWrapped($"{command.Key} → {command.Value.HelpMessage}"); - } - - ImGui.NewLine(); - - if (!installedPlugin.IsRaw) - { - ImGui.SameLine(); - - if (ImGui.Button(Loc.Localize("InstallerDisable", "Disable"))) - { - try - { - this.dalamud.PluginManager.DisablePlugin(installedPlugin.Definition); - } - catch (Exception exception) - { - Log.Error(exception, "Could not disable plugin."); - this.errorModalDrawing = true; - this.errorModalOnNextFrame = true; - } - } - } - - if (installedPlugin.PluginInterface.UiBuilder.HasConfigUi) - { - ImGui.SameLine(); - - if (ImGui.Button(Loc.Localize("InstallerOpenConfig", "Open Configuration"))) - installedPlugin.PluginInterface.UiBuilder.OpenConfigUi(); - } - - if (!string.IsNullOrEmpty(installedPlugin.Definition.RepoUrl)) - { - ImGui.PushFont(InterfaceManager.IconFont); - - ImGui.SameLine(); - if (ImGui.Button(FontAwesomeIcon.Globe.ToIconString()) && - installedPlugin.Definition.RepoUrl.StartsWith("https://")) - Process.Start(installedPlugin.Definition.RepoUrl); - - ImGui.PopFont(); - } - - ImGui.SameLine(); - ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{installedPlugin.Definition.AssemblyVersion}"); - - if (installedPlugin.IsRaw) - { - ImGui.SameLine(); - ImGui.TextColored( - ImGuiColors.DalamudRed, - this.dalamud.PluginRepository.PluginMaster.Any(x => x.InternalName == installedPlugin.Definition.InternalName) - ? " This plugin is available in one of your repos, please remove it from the devPlugins folder." - : " To disable this plugin, please remove it from the devPlugins folder."); - } - } - - ImGui.Unindent(); - } - - if (ImGui.BeginPopupContextItem("item context menu")) - { - if (ImGui.Selectable("Hide from installer")) - this.dalamud.Configuration.HiddenPluginInternalName.Add(pluginDefinition.InternalName); - ImGui.EndPopup(); - } - - ImGui.PopID(); - } - - if (!didAny) - { - if (installed) - { - ImGui.TextColored( - ImGuiColors.DalamudGrey, - Loc.Localize( - "InstallerNoInstalled", - "No plugins are currently installed. You can install them from the Available Plugins tab.")); - } - else - { - ImGui.TextColored( - ImGuiColors.DalamudGrey, - Loc.Localize( - "InstallerNoCompatible", - "No compatible plugins were found :( Please restart your game and try again.")); - } - } - else if (!didAnyWithSearch) - { - ImGui.TextColored( - ImGuiColors.DalamudGrey2, - Loc.Localize("InstallNoMatching", "No plugins were found matching your search.")); - } - } - } -} diff --git a/Dalamud/Plugin/PluginLoadReason.cs b/Dalamud/Plugin/PluginLoadReason.cs deleted file mode 100644 index 789d4d094..000000000 --- a/Dalamud/Plugin/PluginLoadReason.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Dalamud.Plugin -{ - /// - /// This enum reflects reasons for loading a plugin. - /// - public enum PluginLoadReason - { - /// - /// We don't know why this plugin was loaded. - /// - Unknown, - - /// - /// This plugin was loaded because it was installed with the plugin installer. - /// - Installer, - - /// - /// This plugin was loaded because the game was started or Dalamud was reinjected. - /// - Boot, - } -} diff --git a/Dalamud/Plugin/PluginManager.cs b/Dalamud/Plugin/PluginManager.cs deleted file mode 100644 index f45b0fc91..000000000 --- a/Dalamud/Plugin/PluginManager.cs +++ /dev/null @@ -1,387 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Dynamic; -using System.IO; -using System.Linq; -using System.Reflection; - -using Dalamud.Configuration; -using Newtonsoft.Json; -using Serilog; - -namespace Dalamud.Plugin -{ - /// - /// Class responsible for loading and unloading plugins. - /// - internal class PluginManager - { - /// - /// The current Dalamud API level, used to handle breaking changes. Only plugins with this level will be loaded. - /// - public const int DalamudApiLevel = 3; - - private readonly Dalamud dalamud; - private readonly string pluginDirectory; - private readonly string devPluginDirectory; - - private readonly PluginConfigurations pluginConfigs; - - private readonly Type interfaceType = typeof(IDalamudPlugin); - - private readonly List bannedPlugins; - - private IEnumerable<(FileInfo DllFile, PluginDefinition Definition, bool IsRaw)> deferredPlugins; - - /// - /// Initializes a new instance of the class. - /// - /// The instance to load plugins with. - /// The directory for regular plugins. - /// The directory for dev plugins. - public PluginManager(Dalamud dalamud, string pluginDirectory, string devPluginDirectory) - { - this.dalamud = dalamud; - this.pluginDirectory = pluginDirectory; - this.devPluginDirectory = devPluginDirectory; - - this.Plugins = new List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface, bool IsRaw)>(); - this.IpcSubscriptions = new List<(string SourcePluginName, string SubPluginName, Action SubAction)>(); - - this.pluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(dalamud.StartInfo.ConfigurationPath), "pluginConfigs")); - - this.bannedPlugins = JsonConvert.DeserializeObject>( - File.ReadAllText(Path.Combine(this.dalamud.StartInfo.AssetDirectory, "UIRes", "bannedplugin.json"))); - - // Try to load missing assemblies from the local directory of the requesting assembly - // This would usually be implicit when using Assembly.Load(), but Assembly.LoadFile() doesn't do it... - // This handler should only be invoked on things that fail regular lookups, but it *is* global to this appdomain - AppDomain.CurrentDomain.AssemblyResolve += (object source, ResolveEventArgs e) => - { - try - { - Log.Debug($"Resolving missing assembly {e.Name}"); - - // This looks weird but I'm pretty sure it's actually correct. Pretty sure. Probably. - var assemblyPath = Path.Combine( - Path.GetDirectoryName(e.RequestingAssembly.Location), - new AssemblyName(e.Name).Name + ".dll"); - - if (!File.Exists(assemblyPath)) - { - Log.Error($"Assembly not found at {assemblyPath}"); - return null; - } - - return Assembly.LoadFrom(assemblyPath); - } - catch (Exception ex) - { - Log.Error(ex, "Could not load assembly " + e.Name); - return null; - } - }; - } - - /// - /// Gets a list of all loaded plugins. - /// - public List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface, bool IsRaw)> Plugins { get; private set; } - - /// - /// Gets a list of all IPC subscriptions. - /// - public List<(string SourcePluginName, string SubPluginName, Action SubAction)> IpcSubscriptions { get; private set; } - - /// - /// Unload all plugins. - /// - public void UnloadPlugins() - { - if (this.Plugins == null) - return; - - for (var i = 0; i < this.Plugins.Count; i++) - { - this.Plugins[i].Plugin.Dispose(); - } - - this.Plugins.Clear(); - } - - /// - /// Load plugins that need to be loaded synchronously and prepare plugins that can be loaded asynchronously. - /// - public void LoadSynchronousPlugins() - { - var loadDirectories = new List<(DirectoryInfo DirInfo, bool IsRaw)> - { - (new DirectoryInfo(this.pluginDirectory), false), - (new DirectoryInfo(this.devPluginDirectory), true), - }; - - var pluginDefs = new List<(FileInfo DllFile, PluginDefinition Definition, bool IsRaw)>(); - foreach (var (dirInfo, isRaw) in loadDirectories) - { - if (!dirInfo.Exists) continue; - - var pluginDlls = dirInfo.GetFiles("*.dll", SearchOption.AllDirectories).Where(x => x.Extension == ".dll"); - - // Preload definitions to be able to determine load order - foreach (var dllFile in pluginDlls) - { - var defJson = new FileInfo(Path.Combine(dllFile.Directory.FullName, $"{Path.GetFileNameWithoutExtension(dllFile.Name)}.json")); - PluginDefinition def = null; - if (defJson.Exists) - def = JsonConvert.DeserializeObject(File.ReadAllText(defJson.FullName)); - pluginDefs.Add((dllFile, def, isRaw)); - } - } - - // Sort for load order - unloaded definitions have default priority of 0 - pluginDefs.Sort( - (info1, info2) => - { - var prio1 = info1.Definition?.LoadPriority ?? 0; - var prio2 = info2.Definition?.LoadPriority ?? 0; - return prio2.CompareTo(prio1); - }); - - this.deferredPlugins = pluginDefs.Where(x => x.Definition == null || x.Definition.LoadPriority <= 0); - - // Pass preloaded definitions for "synchronous load" plugins to LoadPluginFromAssembly, because we already loaded them anyways - foreach (var (dllFile, definition, isRaw) in pluginDefs.Where(x => x.Definition?.LoadPriority > 0)) - { - try - { - this.LoadPluginFromAssembly(dllFile, isRaw, PluginLoadReason.Boot, definition); - } - catch (Exception ex) - { - Log.Error(ex, $"Plugin load for {dllFile.FullName} failed."); - if (ex is ReflectionTypeLoadException typeLoadException) - { - foreach (var exception in typeLoadException.LoaderExceptions) - { - Log.Error(exception, "LoaderException:"); - } - } - } - } - } - - /// - /// Load plugins that have been explicitly deferred. - /// - public void LoadDeferredPlugins() - { - if (this.deferredPlugins == null) - throw new Exception("Synchronous plugins need to be loaded before deferred plugins."); - - // Pass preloaded definitions for "deferred load" plugins to LoadPluginFromAssembly, because we already loaded them anyways - foreach (var (dllFile, definition, isRaw) in this.deferredPlugins) - { - try - { - this.LoadPluginFromAssembly(dllFile, isRaw, PluginLoadReason.Boot, definition); - } - catch (Exception ex) - { - Log.Error(ex, $"Plugin load for {dllFile.FullName} failed."); - if (ex is ReflectionTypeLoadException typeLoadException) - { - foreach (var exception in typeLoadException.LoaderExceptions) - { - Log.Error(exception, "LoaderException:"); - } - } - } - } - } - - /// - /// Disable/unload a single plugin. - /// - /// The plugin definition of the plugin to be disabled/unloaded. - public void DisablePlugin(PluginDefinition definition) - { - var thisPlugin = this.Plugins.Where(x => x.Definition != null) - .First(x => x.Definition.InternalName == definition.InternalName); - - var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory, definition.InternalName, definition.AssemblyVersion)); - - // Need to do it with Open so the file handle gets closed immediately - // TODO: Don't use the ".disabled" crap, do it in a config - try - { - File.Open(Path.Combine(outputDir.FullName, ".disabled"), FileMode.Create).Close(); - } - catch (Exception ex) - { - Log.Error(ex, "Could not create the .disabled file, disabling all versions..."); - foreach (var version in outputDir.Parent.GetDirectories()) - { - if (!File.Exists(Path.Combine(version.FullName, ".disabled"))) - File.Open(Path.Combine(version.FullName, ".disabled"), FileMode.Create).Close(); - } - } - - thisPlugin.Plugin.Dispose(); - - this.Plugins.Remove(thisPlugin); - } - - /// - /// Load a plugin from an assembly. - /// - /// The associated with the main assembly of this plugin. - /// Whether or not the plugin is a dev plugin. - /// The reason this plugin was loaded. - /// The already loaded definition, if available. - /// Whether or not the plugin was loaded successfully. - public bool LoadPluginFromAssembly(FileInfo dllFile, bool isRaw, PluginLoadReason reason, PluginDefinition pluginDef = null) - { - Log.Information("Loading plugin at {0}", dllFile.Directory.FullName); - - // If this entire folder has been marked as a disabled plugin, don't even try to load anything - var disabledFile = new FileInfo(Path.Combine(dllFile.Directory.FullName, ".disabled")); - - // should raw/dev plugins really not respect this? - if (disabledFile.Exists && !isRaw) - { - Log.Information("Plugin {0} is disabled.", dllFile.FullName); - return false; - } - - var testingFile = new FileInfo(Path.Combine(dllFile.Directory.FullName, ".testing")); - if (testingFile.Exists && !this.dalamud.Configuration.DoPluginTest) - { - Log.Information("Plugin {0} was testing, but testing is disabled.", dllFile.FullName); - return false; - } - - // Preloaded - if (pluginDef == null) - { - // read the plugin def if present - again, fail before actually trying to load the dll if there is a problem - var defJsonFile = new FileInfo(Path.Combine(dllFile.Directory.FullName, $"{Path.GetFileNameWithoutExtension(dllFile.Name)}.json")); - - // load the definition if it exists, even for raw/developer plugins - if (defJsonFile.Exists) - { - Log.Information("Loading definition for plugin DLL {0}", dllFile.FullName); - - pluginDef = - JsonConvert.DeserializeObject( - File.ReadAllText(defJsonFile.FullName)); - } - } - - // Perform checks - if (!isRaw) - { - if (pluginDef != null) - { - if (pluginDef.ApplicableVersion != this.dalamud.StartInfo.GameVersion && - pluginDef.ApplicableVersion != "any") - { - Log.Information("Plugin {0} has not applicable version.", dllFile.FullName); - return false; - } - - if (pluginDef.DalamudApiLevel < DalamudApiLevel) - { - Log.Error("Incompatible API level: {0}", dllFile.FullName); - return false; - } - - if (this.bannedPlugins.Any(x => x.Name == pluginDef.InternalName && - x.AssemblyVersion == pluginDef.AssemblyVersion)) - { - Log.Error($"[PLUGINM] Banned plugin {pluginDef.InternalName} {pluginDef.AssemblyVersion}"); - return false; - } - } - else - { - Log.Information("Plugin DLL {0} has no definition.", dllFile.FullName); - return false; - } - } - - // TODO: given that it exists, the pluginDef's InternalName should probably be used - // as the actual assembly to load - // But plugins should also probably be loaded by directory and not by looking for every dll - Log.Information("Loading assembly at {0}", dllFile); - - // Assembly.Load() by name here will not load multiple versions with the same name, in the case of updates - var pluginAssembly = Assembly.LoadFile(dllFile.FullName); - - Log.Information("Loading types for {0}", pluginAssembly.FullName); - var types = pluginAssembly.GetTypes(); - foreach (var type in types) - { - if (type.IsInterface || type.IsAbstract) - { - continue; - } - - if (type.GetInterface(this.interfaceType.FullName) != null) - { - if (this.Plugins.Any(x => x.Plugin.GetType().Assembly.GetName().Name == type.Assembly.GetName().Name)) - { - Log.Error("Duplicate plugin found: {0}", dllFile.FullName); - return false; - } - - Log.Verbose("Plugin CreateInstance..."); - - var plugin = (IDalamudPlugin)Activator.CreateInstance(type); - - // this happens for raw plugins that don't specify a PluginDefinition - just generate a dummy one to avoid crashes anywhere - pluginDef ??= new PluginDefinition - { - Author = "developer", - Name = plugin.Name, - InternalName = Path.GetFileNameWithoutExtension(dllFile.Name), - AssemblyVersion = plugin.GetType().Assembly.GetName().Version.ToString(), - Description = string.Empty, - ApplicableVersion = "any", - IsHide = false, - DalamudApiLevel = DalamudApiLevel, - }; - - Log.Verbose("Plugin Initialize..."); - - var dalamudInterface = new DalamudPluginInterface(this.dalamud, type.Assembly.GetName().Name, this.pluginConfigs, reason); - plugin.Initialize(dalamudInterface); - - Log.Information("Loaded plugin: {0}", plugin.Name); - this.Plugins.Add((plugin, pluginDef, dalamudInterface, isRaw)); - - return true; - } - } - - Log.Information("Plugin DLL {0} has no plugin interface.", dllFile.FullName); - - return false; - } - - /// - /// Unload and reload all plugins. - /// - public void ReloadPlugins() - { - this.UnloadPlugins(); - this.LoadSynchronousPlugins(); - } - - private class BannedPlugin - { - public string Name { get; set; } - - public string AssemblyVersion { get; set; } - } - } -} diff --git a/Dalamud/Plugin/PluginRepository.cs b/Dalamud/Plugin/PluginRepository.cs deleted file mode 100644 index fe936f94f..000000000 --- a/Dalamud/Plugin/PluginRepository.cs +++ /dev/null @@ -1,571 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; - -using CheapLoc; -using Dalamud.Game.Text; -using Newtonsoft.Json; -using Serilog; - -namespace Dalamud.Plugin -{ - /// - /// This class represents a single plugin repository. - /// - internal class PluginRepository - { - private const string PluginMasterUrl = "https://raw.githubusercontent.com/goatcorp/DalamudPlugins/master/pluginmaster.json"; - - private readonly Dalamud dalamud; - private string pluginDirectory; - - /// - /// Initializes a new instance of the class. - /// - /// The Dalamud instance. - /// The plugin directory path. - /// The current game version. - public PluginRepository(Dalamud dalamud, string pluginDirectory, string gameVersion) - { - this.dalamud = dalamud; - this.pluginDirectory = pluginDirectory; - - this.ReloadPluginMasterAsync(); - } - - /// - /// Values representing plugin initialization state. - /// - public enum InitializationState - { - /// - /// State is unknown. - /// - Unknown, - - /// - /// State is in progress. - /// - InProgress, - - /// - /// State is successful. - /// - Success, - - /// - /// State is failure. - /// - Fail, - - /// - /// State is failure, for a 3rd party repo plugin. - /// - FailThirdRepo, - } - - /// - /// Gets the plugin master list of available plugins. - /// - public ReadOnlyCollection PluginMaster { get; private set; } - - /// - /// Gets the initialization state of the plugin repository. - /// - public InitializationState State { get; private set; } - - /// - /// Reload the plugin master asynchronously in a task. - /// - public void ReloadPluginMasterAsync() - { - this.State = InitializationState.InProgress; - - Task.Run(() => - { - this.PluginMaster = null; - - var allPlugins = new List(); - - var repos = this.dalamud.Configuration.ThirdRepoList.Where(x => x.IsEnabled).Select(x => x.Url) - .Prepend(PluginMasterUrl).ToArray(); - - using var client = new WebClient(); - - var repoNumber = 0; - var anyError = false; - foreach (var repo in repos) - { - Log.Information("[PLUGINR] Fetching repo: {0}", repo); - - try - { - var data = client.DownloadString(repo); - - var unsortedPluginMaster = JsonConvert.DeserializeObject>(data); - - foreach (var pluginDefinition in unsortedPluginMaster) - { - pluginDefinition.RepoNumber = repoNumber; - } - - allPlugins.AddRange(unsortedPluginMaster); - } - catch (Exception ex) - { - Log.Error(ex, "Could not download PluginMaster"); - - this.State = repos.Length > 1 ? InitializationState.FailThirdRepo : InitializationState.Fail; - - anyError = true; - } - - repoNumber++; - } - - this.PluginMaster = allPlugins.AsReadOnly(); - if (!anyError) - { - this.State = InitializationState.Success; - } - }).ContinueWith(t => - { - if (t.IsFaulted) - this.State = InitializationState.Fail; - }); - } - - /// - /// Install a plugin. - /// - /// The plugin definition. - /// Whether the plugin should be immediately enabled. - /// Whether this install is an update. - /// Whether this install is flagged as testing. - /// Success or failure. - public bool InstallPlugin(PluginDefinition definition, bool enableAfterInstall = true, bool isUpdate = false, bool fromTesting = false) - { - try - { - using var client = new WebClient(); - - var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory, definition.InternalName, fromTesting ? definition.TestingAssemblyVersion : definition.AssemblyVersion)); - var dllFile = new FileInfo(Path.Combine(outputDir.FullName, $"{definition.InternalName}.dll")); - var disabledFile = new FileInfo(Path.Combine(outputDir.FullName, ".disabled")); - var testingFile = new FileInfo(Path.Combine(outputDir.FullName, ".testing")); - var wasDisabled = disabledFile.Exists; - - if (dllFile.Exists && enableAfterInstall) - { - if (disabledFile.Exists) - disabledFile.Delete(); - - return this.dalamud.PluginManager.LoadPluginFromAssembly(dllFile, false, PluginLoadReason.Installer); - } - - if (dllFile.Exists && !enableAfterInstall) - { - return true; - } - - try - { - if (outputDir.Exists) - outputDir.Delete(true); - outputDir.Create(); - } - catch - { - // ignored, since the plugin may be loaded already - } - - var path = Path.GetTempFileName(); - - var doTestingDownload = false; - if ((Version.TryParse(definition.TestingAssemblyVersion, out var testingAssemblyVer) || definition.IsTestingExclusive) - && fromTesting) - { - doTestingDownload = testingAssemblyVer > Version.Parse(definition.AssemblyVersion) || definition.IsTestingExclusive; - } - - var url = definition.DownloadLinkInstall; - if (doTestingDownload) - url = definition.DownloadLinkTesting; - else if (isUpdate) - url = definition.DownloadLinkUpdate; - - Log.Information("Downloading plugin to {0} from {1} doTestingDownload:{2} isTestingExclusive:{3}", path, url, doTestingDownload, definition.IsTestingExclusive); - - client.DownloadFile(url, path); - - Log.Information("Extracting to {0}", outputDir); - - ZipFile.ExtractToDirectory(path, outputDir.FullName); - - if (wasDisabled || !enableAfterInstall) - { - disabledFile.Create().Close(); - return true; - } - - if (doTestingDownload) - { - testingFile.Create().Close(); - } - else - { - if (testingFile.Exists) - testingFile.Delete(); - } - - return this.dalamud.PluginManager.LoadPluginFromAssembly(dllFile, false, PluginLoadReason.Installer); - } - catch (Exception ex) - { - Log.Error(ex, "Plugin download failed hard."); - if (ex is ReflectionTypeLoadException typeLoadException) - { - foreach (var exception in typeLoadException.LoaderExceptions) - { - Log.Error(exception, "LoaderException:"); - } - } - - return false; - } - } - - /// - /// Update all plugins. - /// - /// Perform a dry run of the update and skip the actual installation. - /// A tuple of whether the update was successful and the list of updated plugins. - public (bool Success, List UpdatedPlugins) UpdatePlugins(bool dryRun = false) - { - Log.Information("Starting plugin update... dry:{0}", dryRun); - - var updatedList = new List(); - var hasError = false; - - try - { - var pluginsDirectory = new DirectoryInfo(this.pluginDirectory); - foreach (var installed in pluginsDirectory.GetDirectories()) - { - try - { - var versions = installed.GetDirectories(); - - if (versions.Length == 0) - { - Log.Information("Has no versions: {0}", installed.FullName); - continue; - } - - var sortedVersions = versions.OrderBy(dirInfo => - { - var success = Version.TryParse(dirInfo.Name, out var version); - if (!success) - { - Log.Debug("Unparseable version: {0}", dirInfo.Name); - } - - return version; - }); - var latest = sortedVersions.Last(); - - var isEnabled = !File.Exists(Path.Combine(latest.FullName, ".disabled")); - if (!isEnabled && File.Exists(Path.Combine(latest.FullName, ".testing"))) - { - // In case testing is installed, but stable is enabled - foreach (var version in versions) - { - if (!File.Exists(Path.Combine(version.FullName, ".disabled"))) - { - isEnabled = true; - break; - } - } - } - - if (!isEnabled) - { - Log.Verbose("Is disabled: {0}", installed.FullName); - continue; - } - - var localInfoFile = new FileInfo(Path.Combine(latest.FullName, $"{installed.Name}.json")); - - if (!localInfoFile.Exists) - { - Log.Information("Has no definition: {0}", localInfoFile.FullName); - continue; - } - - var info = JsonConvert.DeserializeObject( - File.ReadAllText(localInfoFile.FullName)); - - var remoteInfo = this.PluginMaster.FirstOrDefault(x => x.InternalName == info.InternalName); - - if (remoteInfo == null) - { - Log.Information("Is not in pluginmaster: {0}", info.Name); - continue; - } - - if (remoteInfo.DalamudApiLevel < PluginManager.DalamudApiLevel) - { - Log.Information("Has not applicable API level: {0}", info.Name); - continue; - } - - Version.TryParse(remoteInfo.AssemblyVersion, out var remoteAssemblyVer); - Version.TryParse(info.AssemblyVersion, out var localAssemblyVer); - - var testingAvailable = false; - if (!string.IsNullOrEmpty(remoteInfo.TestingAssemblyVersion)) - { - Version.TryParse(remoteInfo.TestingAssemblyVersion, out var testingAssemblyVer); - testingAvailable = testingAssemblyVer > localAssemblyVer && this.dalamud.Configuration.DoPluginTest; - } - - if (remoteAssemblyVer > localAssemblyVer || testingAvailable) - { - Log.Information("Eligible for update: {0}", remoteInfo.InternalName); - - // DisablePlugin() below immediately creates a .disabled file anyway, but will fail - // with an exception if we try to do it twice in row like this - - if (!dryRun) - { - var wasLoaded = - this.dalamud.PluginManager.Plugins.Where(x => x.Definition != null).Any( - x => x.Definition.InternalName == info.InternalName); - - Log.Verbose("isEnabled: {0} / wasLoaded: {1}", isEnabled, wasLoaded); - - // Try to disable plugin if it is loaded - if (wasLoaded) - { - try - { - this.dalamud.PluginManager.DisablePlugin(info); - } - catch (Exception ex) - { - Log.Error(ex, "Plugin disable failed"); - // hasError = true; - } - } - - try - { - // Just to be safe - foreach (var sortedVersion in sortedVersions) - { - var disabledFile = - new FileInfo(Path.Combine(sortedVersion.FullName, ".disabled")); - if (!disabledFile.Exists) - disabledFile.Create().Close(); - } - } - catch (Exception ex) - { - Log.Error(ex, "Plugin disable old versions failed"); - } - - var installSuccess = this.InstallPlugin(remoteInfo, isEnabled, true, testingAvailable); - - if (!installSuccess) - { - Log.Error("InstallPlugin failed."); - hasError = true; - } - - updatedList.Add(new PluginUpdateStatus - { - InternalName = remoteInfo.InternalName, - Name = remoteInfo.Name, - Version = testingAvailable ? remoteInfo.TestingAssemblyVersion : remoteInfo.AssemblyVersion, - WasUpdated = installSuccess, - }); - } - else - { - updatedList.Add(new PluginUpdateStatus - { - InternalName = remoteInfo.InternalName, - Name = remoteInfo.Name, - Version = testingAvailable ? remoteInfo.TestingAssemblyVersion : remoteInfo.AssemblyVersion, - WasUpdated = true, - }); - } - } - else - { - Log.Information("Up to date: {0}", remoteInfo.InternalName); - } - } - catch (Exception ex) - { - Log.Error(ex, "Could not update plugin: {0}", installed.FullName); - } - } - } - catch (Exception ex) - { - Log.Error(ex, "Plugin update failed."); - hasError = true; - } - - Log.Information("Plugin update OK."); - - return (!hasError, updatedList); - } - - /// - /// Print to chat any plugin updates and whether they were successful. - /// - /// The list of updated plugins. - /// The header text to send to chat prior to any update info. - public void PrintUpdatedPlugins(List updatedPlugins, string header) - { - if (updatedPlugins != null && updatedPlugins.Any()) - { - this.dalamud.Framework.Gui.Chat.Print(header); - foreach (var plugin in updatedPlugins) - { - if (plugin.WasUpdated) - { - this.dalamud.Framework.Gui.Chat.Print(string.Format(Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}."), plugin.Name, plugin.Version)); - } - else - { - this.dalamud.Framework.Gui.Chat.PrintChat(new XivChatEntry - { - MessageBytes = Encoding.UTF8.GetBytes(string.Format(Loc.Localize("DalamudPluginUpdateFailed", " 》 {0} update to v{1} failed."), plugin.Name, plugin.Version)), - Type = XivChatType.Urgent, - }); - } - } - } - } - - /// - /// Cleanup disabled plugins. - /// - public void CleanupPlugins() - { - try - { - var pluginsDirectory = new DirectoryInfo(this.pluginDirectory); - foreach (var installed in pluginsDirectory.GetDirectories()) - { - var versions = installed.GetDirectories(); - - var sortedVersions = versions.OrderBy(dirInfo => - { - var success = Version.TryParse(dirInfo.Name, out var version); - if (!success) - { - Log.Debug("Unparseable version: {0}", dirInfo.Name); - } - - return version; - }).ToArray(); - - foreach (var version in sortedVersions) - { - try - { - var disabledFile = new FileInfo(Path.Combine(version.FullName, ".disabled")); - var definition = JsonConvert.DeserializeObject( - File.ReadAllText(Path.Combine(version.FullName, version.Parent.Name + ".json"))); - - if (disabledFile.Exists) - { - Log.Information("[PLUGINR] Disabled: cleaning up {0} at {1}", installed.Name, version.FullName); - try - { - version.Delete(true); - } - catch (Exception ex) - { - Log.Error(ex, $"[PLUGINR] Could not clean up {disabledFile.FullName}"); - } - } - - if (definition.DalamudApiLevel < PluginManager.DalamudApiLevel - 1) - { - Log.Information("[PLUGINR] Lower API: cleaning up {0} at {1}", installed.Name, version.FullName); - try - { - version.Delete(true); - } - catch (Exception ex) - { - Log.Error(ex, $"[PLUGINR] Could not clean up {disabledFile.FullName}"); - } - } - } - catch (Exception ex) - { - Log.Error(ex, $"[PLUGINR] Could not clean up {version.FullName}"); - } - - if (installed.GetDirectories().Length == 0) - { - Log.Information("[PLUGINR] Has no versions, cleaning up: {0}", installed.FullName); - - try - { - installed.Delete(); - } - catch (Exception ex) - { - Log.Error(ex, $"[PLUGINR] Could not clean up {installed.FullName}"); - } - } - } - } - } - catch (Exception ex) - { - Log.Error(ex, "[PLUGINR] Plugin cleanup failed."); - } - } - - /// - /// Plugin update status. - /// - internal class PluginUpdateStatus - { - /// - /// Gets or sets the plugin internal name. - /// - public string InternalName { get; set; } - - /// - /// Gets or sets the plugin name. - /// - public string Name { get; set; } - - /// - /// Gets or sets the plugin version. - /// - public string Version { get; set; } - - /// - /// Gets or sets a value indicating whether the plugin was updated. - /// - public bool WasUpdated { get; set; } - } - } -} diff --git a/Dalamud/Properties/Resources.Designer.cs b/Dalamud/Properties/Resources.Designer.cs index f8904c30a..aa3d29241 100644 --- a/Dalamud/Properties/Resources.Designer.cs +++ b/Dalamud/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace Dalamud.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { diff --git a/Dalamud/Resources/Lumina.Generated.dll b/Dalamud/Resources/Lumina.Generated.dll deleted file mode 100644 index 301694057..000000000 Binary files a/Dalamud/Resources/Lumina.Generated.dll and /dev/null differ diff --git a/Dalamud/SafeMemory.cs b/Dalamud/SafeMemory.cs index 458cc6824..362291506 100644 --- a/Dalamud/SafeMemory.cs +++ b/Dalamud/SafeMemory.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; @@ -17,12 +16,10 @@ namespace Dalamud public static class SafeMemory { private static readonly IntPtr Handle; - private static readonly IntPtr MainModule; static SafeMemory() { Handle = Imports.GetCurrentProcess(); - MainModule = Process.GetCurrentProcess().MainModule?.BaseAddress ?? IntPtr.Zero; } /// diff --git a/Dalamud/Troubleshooting.cs b/Dalamud/Troubleshooting.cs index d8137165a..53d5a4b96 100644 --- a/Dalamud/Troubleshooting.cs +++ b/Dalamud/Troubleshooting.cs @@ -1,14 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using Dalamud.Configuration; using Dalamud.Plugin; +using Dalamud.Plugin.Internal.Types; using Newtonsoft.Json; using Serilog; -using Encoding = System.Text.Encoding; - namespace Dalamud { /// @@ -27,9 +27,9 @@ namespace Dalamud { var payload = new TroubleshootingPayload { - LoadedPlugins = dalamud.PluginManager.Plugins.Select(x => x.Definition).ToArray(), + LoadedPlugins = dalamud.PluginManager.InstalledPlugins.Select(x => x.Manifest).ToArray(), DalamudVersion = Util.AssemblyVersion, - GameVersion = dalamud.StartInfo.GameVersion, + GameVersion = dalamud.StartInfo.GameVersion.ToString(), Language = dalamud.StartInfo.Language.ToString(), DoDalamudTest = dalamud.Configuration.DoDalamudTest, DoPluginTest = dalamud.Configuration.DoPluginTest, @@ -48,7 +48,7 @@ namespace Dalamud private class TroubleshootingPayload { - public PluginDefinition[] LoadedPlugins { get; set; } + public PluginManifest[] LoadedPlugins { get; set; } public string DalamudVersion { get; set; } @@ -62,7 +62,7 @@ namespace Dalamud public bool InterfaceLoaded { get; set; } - public List ThirdRepo { get; set; } + public List ThirdRepo { get; set; } } } } diff --git a/Dalamud/Util.cs b/Dalamud/Util.cs index 456c93689..3084e1bab 100644 --- a/Dalamud/Util.cs +++ b/Dalamud/Util.cs @@ -161,8 +161,8 @@ namespace Dalamud public static void Fatal(string message, string caption) { var flags = NativeFunctions.MessageBoxType.Ok | NativeFunctions.MessageBoxType.IconError; + _ = NativeFunctions.MessageBoxW(Process.GetCurrentProcess().MainWindowHandle, message, caption, flags); - NativeFunctions.MessageBox(Process.GetCurrentProcess().MainWindowHandle, message, caption, flags); Environment.Exit(-1); } @@ -196,5 +196,13 @@ namespace Dalamud // TODO: Someone implement GetUTF8String with some IntPtr overloads. // while(Marshal.ReadByte(0, sz) != 0) { sz++; } + + /// + /// An extension method to chain usage of string.Format. + /// + /// Format string. + /// Format arguments. + /// Formatted string. + public static string Format(this string format, params object[] args) => string.Format(format, args); } } diff --git a/Dalamud/corehook64.dll b/Dalamud/corehook64.dll new file mode 100644 index 000000000..9b21a40d1 Binary files /dev/null and b/Dalamud/corehook64.dll differ diff --git a/Dalamud/stylecop.json b/Dalamud/stylecop.json deleted file mode 100644 index 6881efc6d..000000000 --- a/Dalamud/stylecop.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", - "settings": { - "orderingRules": { - "systemUsingDirectivesFirst": true, - "usingDirectivesPlacement": "outsideNamespace", - "blankLinesBetweenUsingGroups": "require" - }, - "maintainabilityRules": { - "topLevelTypes": [ "class", "interface", "struct", "enum" ] - } - } -} diff --git a/Resources/EasyHook32.dll b/Resources/EasyHook32.dll deleted file mode 100644 index 135609b46..000000000 Binary files a/Resources/EasyHook32.dll and /dev/null differ diff --git a/Resources/EasyHook32Svc.exe b/Resources/EasyHook32Svc.exe deleted file mode 100644 index 0d470eca4..000000000 Binary files a/Resources/EasyHook32Svc.exe and /dev/null differ diff --git a/Resources/EasyHook64.dll b/Resources/EasyHook64.dll deleted file mode 100644 index 4fb1331aa..000000000 Binary files a/Resources/EasyHook64.dll and /dev/null differ diff --git a/Resources/EasyHook64Svc.exe b/Resources/EasyHook64Svc.exe deleted file mode 100644 index 770663386..000000000 Binary files a/Resources/EasyHook64Svc.exe and /dev/null differ diff --git a/Resources/EasyLoad32.dll b/Resources/EasyLoad32.dll deleted file mode 100644 index 5b30daaa7..000000000 Binary files a/Resources/EasyLoad32.dll and /dev/null differ diff --git a/Resources/EasyLoad64.dll b/Resources/EasyLoad64.dll deleted file mode 100644 index 1696334ff..000000000 Binary files a/Resources/EasyLoad64.dll and /dev/null differ diff --git a/build.cmd b/build.cmd new file mode 100644 index 000000000..b08cc590f --- /dev/null +++ b/build.cmd @@ -0,0 +1,7 @@ +:; set -eo pipefail +:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) +:; ${SCRIPT_DIR}/build.sh "$@" +:; exit $? + +@ECHO OFF +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 000000000..c8aea5cc8 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,69 @@ +[CmdletBinding()] +Param( + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$BuildArguments +) + +Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" + +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } +$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent + +########################################################################### +# CONFIGURATION +########################################################################### + +$BuildProjectFile = "$PSScriptRoot\build\build.csproj" +$TempDirectory = "$PSScriptRoot\\.nuke\temp" + +$DotNetGlobalFile = "$PSScriptRoot\\global.json" +$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" +$DotNetChannel = "Current" + +$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 +$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 +$env:DOTNET_MULTILEVEL_LOOKUP = 0 + +########################################################################### +# EXECUTION +########################################################################### + +function ExecSafe([scriptblock] $cmd) { + & $cmd + if ($LASTEXITCODE) { exit $LASTEXITCODE } +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` + $(dotnet --version) -and $LASTEXITCODE -eq 0) { + $env:DOTNET_EXE = (Get-Command "dotnet").Path +} +else { + # Download install script + $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" + New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) + + # If global.json exists, load expected version + if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version + } + } + + # Install by channel or version + $DotNetDirectory = "$TempDirectory\dotnet-win" + if (!(Test-Path variable:DotNetVersion)) { + ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + } else { + ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } + } + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" +} + +Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)" + +ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh new file mode 100644 index 000000000..eca8e308c --- /dev/null +++ b/build.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +bash --version 2>&1 | head -n 1 + +set -eo pipefail +SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) + +########################################################################### +# CONFIGURATION +########################################################################### + +BUILD_PROJECT_FILE="$SCRIPT_DIR/build/build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" + +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" +DOTNET_CHANNEL="Current" + +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 +export DOTNET_MULTILEVEL_LOOKUP=0 + +########################################################################### +# EXECUTION +########################################################################### + +function FirstJsonValue { + perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then + export DOTNET_EXE="$(command -v dotnet)" +else + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + + # If global.json exists, load expected version + if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then + DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") + if [[ "$DOTNET_VERSION" == "" ]]; then + unset DOTNET_VERSION + fi + fi + + # Install by channel or version + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + if [[ -z ${DOTNET_VERSION+x} ]]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path + fi + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" +fi + +echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" + +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/build/.editorconfig b/build/.editorconfig new file mode 100644 index 000000000..06b0d10a7 --- /dev/null +++ b/build/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*.cs] +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning +dotnet_style_require_accessibility_modifiers = never:warning +dotnet_diagnostic.CA1822.severity=silent +dotnet_diagnostic.CA1050.severity=silent + +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_accessors = true:warning diff --git a/build/Configuration.cs b/build/Configuration.cs new file mode 100644 index 000000000..f4f6b4a51 --- /dev/null +++ b/build/Configuration.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using Nuke.Common.Tooling; + +[TypeConverter(typeof(TypeConverter))] +public class Configuration : Enumeration +{ + public static readonly Configuration Debug = new() { Value = nameof(Debug) }; + public static readonly Configuration Release = new() { Value = nameof(Release) }; + + public static implicit operator string(Configuration configuration) + { + return configuration.Value; + } +} diff --git a/build/DalamudBuild.cs b/build/DalamudBuild.cs new file mode 100644 index 000000000..01d6d3038 --- /dev/null +++ b/build/DalamudBuild.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using System.IO; +using Nuke.Common; +using Nuke.Common.Execution; +using Nuke.Common.Git; +using Nuke.Common.IO; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tools.DotNet; +using Nuke.Common.Tools.MSBuild; + +[CheckBuildProjectConfigurations] +[UnsetVisualStudioEnvironmentVariables] +public class DalamudBuild : NukeBuild +{ + /// Support plugins are available for: + /// - Microsoft VisualStudio https://nuke.build/visualstudio + /// - JetBrains ReSharper https://nuke.build/resharper + /// - JetBrains Rider https://nuke.build/rider + /// - Microsoft VSCode https://nuke.build/vscode + + public static int Main() => Execute(x => x.Compile); + + [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] + readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; + + [Solution] Solution Solution; + [GitRepository] GitRepository GitRepository; + + AbsolutePath DalamudProjectDir => RootDirectory / "Dalamud"; + AbsolutePath DalamudProjectFile => DalamudProjectDir / "Dalamud.csproj"; + + AbsolutePath DalamudBootProjectDir => RootDirectory / "Dalamud.Boot"; + AbsolutePath DalamudBootProjectFile => DalamudBootProjectDir / "Dalamud.Boot.vcxproj"; + + AbsolutePath InjectorProjectDir => RootDirectory / "Dalamud.Injector"; + AbsolutePath InjectorProjectFile => InjectorProjectDir / "Dalamud.Injector.csproj"; + + AbsolutePath InjectorBootProjectDir => RootDirectory / "Dalamud.Injector.Boot"; + AbsolutePath InjectorBootProjectFile => InjectorBootProjectDir / "Dalamud.Injector.Boot.vcxproj"; + + AbsolutePath TestProjectDir => RootDirectory / "Dalamud.Test"; + AbsolutePath TestProjectFile => TestProjectDir / "Dalamud.Test.csproj"; + + AbsolutePath ArtifactsDirectory => RootDirectory / "bin" / Configuration; + + private static AbsolutePath LibraryDirectory => RootDirectory / "lib"; + + private static Dictionary EnvironmentVariables => new(EnvironmentInfo.Variables); + + Target Restore => _ => _ + .Executes(() => + { + DotNetTasks.DotNetRestore(s => s + .SetProjectFile(Solution)); + }); + + Target CompileDalamud => _ => _ + .DependsOn(Restore) + .Executes(() => + { + DotNetTasks.DotNetBuild(s => s + .SetProjectFile(DalamudProjectFile) + .SetConfiguration(Configuration) + .EnableNoRestore()); + }); + + Target CompileDalamudBoot => _ => _ + .Executes(() => + { + MSBuildTasks.MSBuild(s => s + .SetTargetPath(DalamudBootProjectFile) + .SetConfiguration(Configuration)); + }); + + Target CompileInjector => _ => _ + .DependsOn(Restore) + .Executes(() => + { + DotNetTasks.DotNetBuild(s => s + .SetProjectFile(InjectorProjectFile) + .SetConfiguration(Configuration) + .EnableNoRestore()); + }); + + Target CompileInjectorBoot => _ => _ + .Executes(() => + { + MSBuildTasks.MSBuild(s => s + .SetTargetPath(InjectorBootProjectFile) + .SetConfiguration(Configuration)); + }); + + Target Compile => _ => _ + .DependsOn(CompileDalamud) + .DependsOn(CompileDalamudBoot) + .DependsOn(CompileInjector) + .DependsOn(CompileInjectorBoot); + + Target Test => _ => _ + .DependsOn(Compile) + .Executes(() => + { + DotNetTasks.DotNetTest(s => s + .SetProjectFile(TestProjectFile) + .SetConfiguration(Configuration) + .EnableNoRestore()); + }); + + Target Clean => _ => _ + .Executes(() => + { + DotNetTasks.DotNetClean(s => s + .SetProject(DalamudProjectFile) + .SetConfiguration(Configuration)); + + MSBuildTasks.MSBuild(s => s + .SetProjectFile(DalamudBootProjectFile) + .SetConfiguration(Configuration) + .SetTargets("Clean")); + + DotNetTasks.DotNetClean(s => s + .SetProject(InjectorProjectFile) + .SetConfiguration(Configuration)); + + MSBuildTasks.MSBuild(s => s + .SetProjectFile(InjectorBootProjectFile) + .SetConfiguration(Configuration) + .SetTargets("Clean")); + + FileSystemTasks.DeleteDirectory(ArtifactsDirectory); + Directory.CreateDirectory(ArtifactsDirectory); + }); +} diff --git a/build/build.csproj b/build/build.csproj new file mode 100644 index 000000000..b5403f5ce --- /dev/null +++ b/build/build.csproj @@ -0,0 +1,14 @@ + + + Exe + net5.0 + disable + + IDE0002;IDE0051;IDE1006;CS0649;CS0169 + .. + .. + + + + + diff --git a/build/build.csproj.DotSettings b/build/build.csproj.DotSettings new file mode 100644 index 000000000..c8947fcec --- /dev/null +++ b/build/build.csproj.DotSettings @@ -0,0 +1,26 @@ + + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + Implicit + Implicit + ExpressionBody + 0 + NEXT_LINE + True + False + 120 + IF_OWNER_IS_SINGLE_LINE + WRAP_IF_LONG + False + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + True + True + True + True + True + True + True + True diff --git a/lib/CoreCLR/CoreCLR.cpp b/lib/CoreCLR/CoreCLR.cpp new file mode 100644 index 000000000..ec8e548cd --- /dev/null +++ b/lib/CoreCLR/CoreCLR.cpp @@ -0,0 +1,111 @@ +#define WIN32_LEAN_AND_MEAN + +#include "CoreCLR.h" +#include +#include +#include "nethost/nethost.h" + +#pragma comment(lib, "nethost/libnethost.lib") + +CoreCLR::CoreCLR() {} + +/* Core public functions */ +int CoreCLR::load_hostfxr() +{ + return CoreCLR::load_hostfxr(nullptr); +} + +int CoreCLR::load_hostfxr(const struct get_hostfxr_parameters* parameters) +{ + // Get the path to CoreCLR's hostfxr + wchar_t buffer[MAX_PATH]{}; + size_t buffer_size = sizeof buffer / sizeof(wchar_t); + if (int rc = get_hostfxr_path(buffer, &buffer_size, parameters); rc != 0) + return rc; + + // Load hostfxr and get desired exports + auto lib = reinterpret_cast(load_library(buffer)); + m_hostfxr_initialize_for_runtime_config_fptr = reinterpret_cast( + get_export(lib, "hostfxr_initialize_for_runtime_config")); + m_hostfxr_get_runtime_delegate_fptr = reinterpret_cast( + get_export(lib, "hostfxr_get_runtime_delegate")); + m_hostfxr_close_fptr = reinterpret_cast( + get_export(lib, "hostfxr_close")); + + return m_hostfxr_initialize_for_runtime_config_fptr + && m_hostfxr_get_runtime_delegate_fptr + && m_hostfxr_close_fptr ? 0 : -1; +} + +bool CoreCLR::load_runtime(const std::wstring& runtime_config_path) +{ + return CoreCLR::load_runtime(runtime_config_path, nullptr); +} + +bool CoreCLR::load_runtime(const std::wstring& runtime_config_path, const struct hostfxr_initialize_parameters* parameters) +{ + int result; + + // Load .NET Core + hostfxr_handle context = nullptr; + result = m_hostfxr_initialize_for_runtime_config_fptr( + runtime_config_path.c_str(), + parameters, + &context); + + // Success_HostAlreadyInitialized + if (result == 1) + { + printf("Success_HostAlreadyInitialized (0x1) "); + result = 0; + } + + if (result != 0 || context == nullptr) + { + m_hostfxr_close_fptr(context); + return result; + } + + // Get the load assembly function pointer + result = m_hostfxr_get_runtime_delegate_fptr( + context, + hdt_load_assembly_and_get_function_pointer, + reinterpret_cast(&m_load_assembly_and_get_function_pointer_fptr)); + + if (result != 0 || m_load_assembly_and_get_function_pointer_fptr == nullptr) + { + m_hostfxr_close_fptr(context); + return result; + } + + m_hostfxr_close_fptr(context); + + return 0; +} + +int CoreCLR::load_assembly_and_get_function_pointer( + const wchar_t* assembly_path, + const wchar_t* type_name, + const wchar_t* method_name, + const wchar_t* delegate_type_name, + void* reserved, + void** delegate) const +{ + int result = m_load_assembly_and_get_function_pointer_fptr(assembly_path, type_name, method_name, delegate_type_name, reserved, delegate); + + if (result != 0) + delegate = nullptr; + + return result; +}; + +/* Helpers */ +uint64_t CoreCLR::load_library(const wchar_t* path) +{ + return reinterpret_cast(LoadLibraryW(path)); +} + +uint64_t CoreCLR::get_export(void* h, const char* name) +{ + return reinterpret_cast(GetProcAddress(static_cast(h), name)); +} diff --git a/lib/CoreCLR/CoreCLR.h b/lib/CoreCLR/CoreCLR.h new file mode 100644 index 000000000..c2395d07d --- /dev/null +++ b/lib/CoreCLR/CoreCLR.h @@ -0,0 +1,38 @@ +#pragma once +#include +#include "core/hostfxr.h" +#include "core/coreclr_delegates.h" +#include "nethost/nethost.h" + +class CoreCLR { + public: + explicit CoreCLR(); + ~CoreCLR() = default; + + int load_hostfxr(); + int load_hostfxr(const get_hostfxr_parameters* parameters); + + bool load_runtime(const std::wstring& runtime_config_path); + bool load_runtime( + const std::wstring& runtime_config_path, + const struct hostfxr_initialize_parameters* parameters); + + int load_assembly_and_get_function_pointer( + const wchar_t* assembly_path, + const wchar_t* type_name, + const wchar_t* method_name, + const wchar_t* delegate_type_name, + void* reserved, + void** delegate) const; + + private: + /* HostFXR delegates. */ + hostfxr_initialize_for_runtime_config_fn m_hostfxr_initialize_for_runtime_config_fptr{}; + hostfxr_get_runtime_delegate_fn m_hostfxr_get_runtime_delegate_fptr{}; + hostfxr_close_fn m_hostfxr_close_fptr{}; + load_assembly_and_get_function_pointer_fn m_load_assembly_and_get_function_pointer_fptr = nullptr; + + /* Helper functions. */ + static uint64_t load_library(const wchar_t* path); + static uint64_t get_export(void* h, const char* name); +}; diff --git a/lib/CoreCLR/boot.cpp b/lib/CoreCLR/boot.cpp new file mode 100644 index 000000000..0ba4dc19f --- /dev/null +++ b/lib/CoreCLR/boot.cpp @@ -0,0 +1,110 @@ +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include +#include +#include "CoreCLR.h" + +FILE* g_CmdStream; +void ConsoleSetup(const std::wstring console_name) +{ + if (!AllocConsole()) + return; + + SetConsoleTitleW(console_name.c_str()); + freopen_s(&g_CmdStream, "CONOUT$", "w", stdout); + freopen_s(&g_CmdStream, "CONOUT$", "w", stderr); + freopen_s(&g_CmdStream, "CONIN$", "r", stdin); +} + +void ConsoleTeardown() +{ + FreeConsole(); +} + +int InitializeClrAndGetEntryPoint( + std::wstring runtimeconfig_path, + std::wstring module_path, + std::wstring entrypoint_assembly_name, + std::wstring entrypoint_method_name, + std::wstring entrypoint_delegate_type_name, + void** entrypoint_fn) +{ + int result; + CoreCLR clr; + SetEnvironmentVariable(L"DOTNET_MULTILEVEL_LOOKUP", L"0"); + + wchar_t* _appdata; + result = SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_DEFAULT, NULL, &_appdata); + if (result != 0) + { + printf("Error: Unable to get RoamingAppData path (err=%d)\n", result); + return result; + } + std::filesystem::path fs_app_data(_appdata); + wchar_t* dotnet_path = _wcsdup(fs_app_data.append("XIVLauncher").append("runtime").c_str()); + + // =========================================================================== // + + wprintf(L"with dotnet_path: %s\n", dotnet_path); + wprintf(L"with config_path: %s\n", runtimeconfig_path.c_str()); + wprintf(L"with module_path: %s\n", module_path.c_str()); + + if (!std::filesystem::exists(dotnet_path)) + { + printf("Error: Unable to find .NET runtime path\n"); + return 1; + } + + get_hostfxr_parameters init_parameters + { + sizeof(get_hostfxr_parameters), + nullptr, + dotnet_path, + }; + + printf("Loading hostfxr... "); + if ((result = clr.load_hostfxr(&init_parameters)) != 0) + { + printf("\nError: Failed to load the `hostfxr` library (err=%d)\n", result); + return result; + } + printf("Done!\n"); + + // =========================================================================== // + + hostfxr_initialize_parameters runtime_parameters + { + sizeof(hostfxr_initialize_parameters), + module_path.c_str(), + dotnet_path, + }; + + printf("Loading coreclr... ");; + if ((result = clr.load_runtime(runtimeconfig_path, &runtime_parameters)) != 0) + { + printf("\nError: Failed to load coreclr (err=%d)\n", result); + return result; + } + printf("Done!\n"); + + // =========================================================================== // + + printf("Loading module... "); + if ((result = clr.load_assembly_and_get_function_pointer( + module_path.c_str(), + entrypoint_assembly_name.c_str(), + entrypoint_method_name.c_str(), + entrypoint_delegate_type_name.c_str(), + nullptr, entrypoint_fn)) != 0) + { + printf("\nError: Failed to load module (err=%d)\n", result); + return result; + } + printf("Done!\n"); + + // =========================================================================== // + + return 0; +} diff --git a/lib/CoreCLR/boot.h b/lib/CoreCLR/boot.h new file mode 100644 index 000000000..8f58042a1 --- /dev/null +++ b/lib/CoreCLR/boot.h @@ -0,0 +1,10 @@ +void ConsoleSetup(const std::wstring console_name); +void ConsoleTeardown(); + +int InitializeClrAndGetEntryPoint( + std::wstring runtimeconfig_path, + std::wstring module_path, + std::wstring entrypoint_assembly_name, + std::wstring entrypoint_method_name, + std::wstring entrypoint_delegate_type_name, + void** entrypoint_fn); diff --git a/lib/CoreCLR/core/coreclr_delegates.h b/lib/CoreCLR/core/coreclr_delegates.h new file mode 100644 index 000000000..20c1221bd --- /dev/null +++ b/lib/CoreCLR/core/coreclr_delegates.h @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef __CORECLR_DELEGATES_H__ +#define __CORECLR_DELEGATES_H__ + +#include + +#if defined(_WIN32) + #define CORECLR_DELEGATE_CALLTYPE __stdcall + #ifdef _WCHAR_T_DEFINED + typedef wchar_t char_t; + #else + typedef unsigned short char_t; + #endif +#else + #define CORECLR_DELEGATE_CALLTYPE + typedef char char_t; +#endif + +#define UNMANAGEDCALLERSONLY_METHOD ((const char_t*)-1) + +// Signature of delegate returned by coreclr_delegate_type::load_assembly_and_get_function_pointer +typedef int (CORECLR_DELEGATE_CALLTYPE* load_assembly_and_get_function_pointer_fn)( + const char_t* assembly_path /* Fully qualified path to assembly */, + const char_t* type_name /* Assembly qualified type name */, + const char_t* method_name /* Public static method name compatible with delegateType */, + const char_t* delegate_type_name /* Assembly qualified delegate type name or null + or UNMANAGEDCALLERSONLY_METHOD if the method is marked with + the UnmanagedCallersOnlyAttribute. */, + void* reserved /* Extensibility parameter (currently unused and must be 0) */, + /*out*/ void** delegate /* Pointer where to store the function pointer result */); + +// Signature of delegate returned by load_assembly_and_get_function_pointer_fn when delegate_type_name == null (default) +typedef int (CORECLR_DELEGATE_CALLTYPE* component_entry_point_fn)(void* arg, int32_t arg_size_in_bytes); + +typedef int (CORECLR_DELEGATE_CALLTYPE* get_function_pointer_fn)( + const char_t* type_name /* Assembly qualified type name */, + const char_t* method_name /* Public static method name compatible with delegateType */, + const char_t* delegate_type_name /* Assembly qualified delegate type name or null, + or UNMANAGEDCALLERSONLY_METHOD if the method is marked with + the UnmanagedCallersOnlyAttribute. */, + void* load_context /* Extensibility parameter (currently unused and must be 0) */, + void* reserved /* Extensibility parameter (currently unused and must be 0) */, + /*out*/ void** delegate /* Pointer where to store the function pointer result */); + +#endif // __CORECLR_DELEGATES_H__ diff --git a/lib/CoreCLR/core/hostfxr.h b/lib/CoreCLR/core/hostfxr.h new file mode 100644 index 000000000..a0a526d90 --- /dev/null +++ b/lib/CoreCLR/core/hostfxr.h @@ -0,0 +1,288 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef __HOSTFXR_H__ +#define __HOSTFXR_H__ + +#include +#include + +#if defined(_WIN32) + #define HOSTFXR_CALLTYPE __cdecl + #ifdef _WCHAR_T_DEFINED + typedef wchar_t char_t; + #else + typedef unsigned short char_t; + #endif +#else + #define HOSTFXR_CALLTYPE + typedef char char_t; +#endif + +enum hostfxr_delegate_type +{ + hdt_com_activation, + hdt_load_in_memory_assembly, + hdt_winrt_activation, + hdt_com_register, + hdt_com_unregister, + hdt_load_assembly_and_get_function_pointer, + hdt_get_function_pointer, +}; + +typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_fn)(const int argc, const char_t** argv); +typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_startupinfo_fn)( + const int argc, + const char_t** argv, + const char_t* host_path, + const char_t* dotnet_root, + const char_t* app_path); +typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_bundle_startupinfo_fn)( + const int argc, + const char_t** argv, + const char_t* host_path, + const char_t* dotnet_root, + const char_t* app_path, + int64_t bundle_header_offset); + +typedef void(HOSTFXR_CALLTYPE* hostfxr_error_writer_fn)(const char_t* message); + +// +// Sets a callback which is to be used to write errors to. +// +// Parameters: +// error_writer +// A callback function which will be invoked every time an error is to be reported. +// Or nullptr to unregister previously registered callback and return to the default behavior. +// Return value: +// The previously registered callback (which is now unregistered), or nullptr if no previous callback +// was registered +// +// The error writer is registered per-thread, so the registration is thread-local. On each thread +// only one callback can be registered. Subsequent registrations overwrite the previous ones. +// +// By default no callback is registered in which case the errors are written to stderr. +// +// Each call to the error writer is sort of like writing a single line (the EOL character is omitted). +// Multiple calls to the error writer may occure for one failure. +// +// If the hostfxr invokes functions in hostpolicy as part of its operation, the error writer +// will be propagated to hostpolicy for the duration of the call. This means that errors from +// both hostfxr and hostpolicy will be reporter through the same error writer. +// +typedef hostfxr_error_writer_fn(HOSTFXR_CALLTYPE* hostfxr_set_error_writer_fn)(hostfxr_error_writer_fn error_writer); + +typedef void* hostfxr_handle; +struct hostfxr_initialize_parameters +{ + size_t size; + const char_t* host_path; + const char_t* dotnet_root; +}; + +// +// Initializes the hosting components for a dotnet command line running an application +// +// Parameters: +// argc +// Number of argv arguments +// argv +// Command-line arguments for running an application (as if through the dotnet executable). +// parameters +// Optional. Additional parameters for initialization +// host_context_handle +// On success, this will be populated with an opaque value representing the initialized host context +// +// Return value: +// Success - Hosting components were successfully initialized +// HostInvalidState - Hosting components are already initialized +// +// This function parses the specified command-line arguments to determine the application to run. It will +// then find the corresponding .runtimeconfig.json and .deps.json with which to resolve frameworks and +// dependencies and prepare everything needed to load the runtime. +// +// This function only supports arguments for running an application. It does not support SDK commands. +// +// This function does not load the runtime. +// +typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_initialize_for_dotnet_command_line_fn)( + int argc, + const char_t** argv, + const struct hostfxr_initialize_parameters* parameters, + /*out*/ hostfxr_handle* host_context_handle); + +// +// Initializes the hosting components using a .runtimeconfig.json file +// +// Parameters: +// runtime_config_path +// Path to the .runtimeconfig.json file +// parameters +// Optional. Additional parameters for initialization +// host_context_handle +// On success, this will be populated with an opaque value representing the initialized host context +// +// Return value: +// Success - Hosting components were successfully initialized +// Success_HostAlreadyInitialized - Config is compatible with already initialized hosting components +// Success_DifferentRuntimeProperties - Config has runtime properties that differ from already initialized hosting components +// CoreHostIncompatibleConfig - Config is incompatible with already initialized hosting components +// +// This function will process the .runtimeconfig.json to resolve frameworks and prepare everything needed +// to load the runtime. It will only process the .deps.json from frameworks (not any app/component that +// may be next to the .runtimeconfig.json). +// +// This function does not load the runtime. +// +// If called when the runtime has already been loaded, this function will check if the specified runtime +// config is compatible with the existing runtime. +// +// Both Success_HostAlreadyInitialized and Success_DifferentRuntimeProperties codes are considered successful +// initializations. In the case of Success_DifferentRuntimeProperties, it is left to the consumer to verify that +// the difference in properties is acceptable. +// +typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_initialize_for_runtime_config_fn)( + const char_t* runtime_config_path, + const struct hostfxr_initialize_parameters* parameters, + /*out*/ hostfxr_handle* host_context_handle); + +// +// Gets the runtime property value for an initialized host context +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// name +// Runtime property name +// value +// Out parameter. Pointer to a buffer with the property value. +// +// Return value: +// The error code result. +// +// The buffer pointed to by value is owned by the host context. The lifetime of the buffer is only +// guaranteed until any of the below occur: +// - a 'run' method is called for the host context +// - properties are changed via hostfxr_set_runtime_property_value +// - the host context is closed via 'hostfxr_close' +// +// If host_context_handle is nullptr and an active host context exists, this function will get the +// property value for the active host context. +// +typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_runtime_property_value_fn)( + const hostfxr_handle host_context_handle, + const char_t* name, + /*out*/ const char_t** value); + +// +// Sets the value of a runtime property for an initialized host context +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// name +// Runtime property name +// value +// Value to set +// +// Return value: +// The error code result. +// +// Setting properties is only supported for the first host context, before the runtime has been loaded. +// +// If the property already exists in the host context, it will be overwritten. If value is nullptr, the +// property will be removed. +// +typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_set_runtime_property_value_fn)( + const hostfxr_handle host_context_handle, + const char_t* name, + const char_t* value); + +// +// Gets all the runtime properties for an initialized host context +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// count +// [in] Size of the keys and values buffers +// [out] Number of properties returned (size of keys/values buffers used). If the input value is too +// small or keys/values is nullptr, this is populated with the number of available properties +// keys +// Array of pointers to buffers with runtime property keys +// values +// Array of pointers to buffers with runtime property values +// +// Return value: +// The error code result. +// +// The buffers pointed to by keys and values are owned by the host context. The lifetime of the buffers is only +// guaranteed until any of the below occur: +// - a 'run' method is called for the host context +// - properties are changed via hostfxr_set_runtime_property_value +// - the host context is closed via 'hostfxr_close' +// +// If host_context_handle is nullptr and an active host context exists, this function will get the +// properties for the active host context. +// +typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_runtime_properties_fn)( + const hostfxr_handle host_context_handle, + /*inout*/ size_t* count, + /*out*/ const char_t** keys, + /*out*/ const char_t** values); + +// +// Load CoreCLR and run the application for an initialized host context +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// +// Return value: +// If the app was successfully run, the exit code of the application. Otherwise, the error code result. +// +// The host_context_handle must have been initialized using hostfxr_initialize_for_dotnet_command_line. +// +// This function will not return until the managed application exits. +// +typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_run_app_fn)(const hostfxr_handle host_context_handle); + +// +// Gets a typed delegate from the currently loaded CoreCLR or from a newly created one. +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// type +// Type of runtime delegate requested +// delegate +// An out parameter that will be assigned the delegate. +// +// Return value: +// The error code result. +// +// If the host_context_handle was initialized using hostfxr_initialize_for_runtime_config, +// then all delegate types are supported. +// If the host_context_handle was initialized using hostfxr_initialize_for_dotnet_command_line, +// then only the following delegate types are currently supported: +// hdt_load_assembly_and_get_function_pointer +// hdt_get_function_pointer +// +typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_runtime_delegate_fn)( + const hostfxr_handle host_context_handle, + enum hostfxr_delegate_type type, + /*out*/ void** delegate); + +// +// Closes an initialized host context +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// +// Return value: +// The error code result. +// +typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_close_fn)(const hostfxr_handle host_context_handle); + +#endif //__HOSTFXR_H__ diff --git a/lib/CoreCLR/framework.h b/lib/CoreCLR/framework.h new file mode 100644 index 000000000..8bb54776a --- /dev/null +++ b/lib/CoreCLR/framework.h @@ -0,0 +1,7 @@ +#pragma once + +// Exclude rarely-used stuff from Windows headers +#define WIN32_LEAN_AND_MEAN + +// Windows Header Files +#include diff --git a/lib/CoreCLR/nethost/libnethost.lib b/lib/CoreCLR/nethost/libnethost.lib new file mode 100644 index 000000000..4291a5387 Binary files /dev/null and b/lib/CoreCLR/nethost/libnethost.lib differ diff --git a/lib/CoreCLR/nethost/nethost.dll b/lib/CoreCLR/nethost/nethost.dll new file mode 100644 index 000000000..6673b3079 Binary files /dev/null and b/lib/CoreCLR/nethost/nethost.dll differ diff --git a/lib/CoreCLR/nethost/nethost.h b/lib/CoreCLR/nethost/nethost.h new file mode 100644 index 000000000..31adde5e8 --- /dev/null +++ b/lib/CoreCLR/nethost/nethost.h @@ -0,0 +1,99 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef __NETHOST_H__ +#define __NETHOST_H__ + +#include + +#ifdef _WIN32 + #ifdef NETHOST_EXPORT + #define NETHOST_API __declspec(dllexport) + #else + // Consuming the nethost as a static library + // Shouldn't export attempt to dllimport. + #ifdef NETHOST_USE_AS_STATIC + #define NETHOST_API + #else + #define NETHOST_API __declspec(dllimport) + #endif + #endif + + #define NETHOST_CALLTYPE __stdcall + #ifdef _WCHAR_T_DEFINED + typedef wchar_t char_t; + #else + typedef unsigned short char_t; + #endif +#else + #ifdef NETHOST_EXPORT + #define NETHOST_API __attribute__((__visibility__("default"))) + #else + #define NETHOST_API + #endif + + #define NETHOST_CALLTYPE + typedef char char_t; +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +// Parameters for get_hostfxr_path +// +// Fields: +// size +// Size of the struct. This is used for versioning. +// +// assembly_path +// Path to the compenent's assembly. +// If specified, hostfxr is located as if the assembly_path is the apphost +// +// dotnet_root +// Path to directory containing the dotnet executable. +// If specified, hostfxr is located as if an application is started using +// 'dotnet app.dll', which means it will be searched for under the dotnet_root +// path and the assembly_path is ignored. +// +struct get_hostfxr_parameters { + size_t size; + const char_t *assembly_path; + const char_t *dotnet_root; +}; + +// +// Get the path to the hostfxr library +// +// Parameters: +// buffer +// Buffer that will be populated with the hostfxr path, including a null terminator. +// +// buffer_size +// [in] Size of buffer in char_t units. +// [out] Size of buffer used in char_t units. If the input value is too small +// or buffer is nullptr, this is populated with the minimum required size +// in char_t units for a buffer to hold the hostfxr path +// +// get_hostfxr_parameters +// Optional. Parameters that modify the behaviour for locating the hostfxr library. +// If nullptr, hostfxr is located using the enviroment variable or global registration +// +// Return value: +// 0 on success, otherwise failure +// 0x80008098 - buffer is too small (HostApiBufferTooSmall) +// +// Remarks: +// The full search for the hostfxr library is done on every call. To minimize the need +// to call this function multiple times, pass a large buffer (e.g. PATH_MAX). +// +NETHOST_API int NETHOST_CALLTYPE get_hostfxr_path( + char_t * buffer, + size_t * buffer_size, + const struct get_hostfxr_parameters *parameters); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // __NETHOST_H__ diff --git a/lib/CoreCLR/nethost/nethost.lib b/lib/CoreCLR/nethost/nethost.lib new file mode 100644 index 000000000..4cd6f0382 Binary files /dev/null and b/lib/CoreCLR/nethost/nethost.lib differ diff --git a/lib/CoreCLR/pch.cpp b/lib/CoreCLR/pch.cpp new file mode 100644 index 000000000..64b7eef6d --- /dev/null +++ b/lib/CoreCLR/pch.cpp @@ -0,0 +1,5 @@ +// pch.cpp: source file corresponding to the pre-compiled header + +#include "pch.h" + +// When you are using pre-compiled headers, this source file is necessary for compilation to succeed. diff --git a/lib/CoreCLR/pch.h b/lib/CoreCLR/pch.h new file mode 100644 index 000000000..885d5d62e --- /dev/null +++ b/lib/CoreCLR/pch.h @@ -0,0 +1,13 @@ +// pch.h: This is a precompiled header file. +// Files listed below are compiled only once, improving build performance for future builds. +// This also affects IntelliSense performance, including code completion and many code browsing features. +// However, files listed here are ALL re-compiled if any one of them is updated between builds. +// Do not add files here that you will be updating frequently as this negates the performance advantage. + +#ifndef PCH_H +#define PCH_H + +// add headers that you want to pre-compile here +#include "framework.h" + +#endif //PCH_H diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index aace50182..e3bec1189 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit aace501829dcc765eb06eab446c6cdc087d351ca +Subproject commit e3bec118909b0eafdd0ee7c3312c646ce0c3f3dd diff --git a/lib/ImGuiScene b/lib/ImGuiScene index 89f6b1b92..b326a22da 160000 --- a/lib/ImGuiScene +++ b/lib/ImGuiScene @@ -1 +1 @@ -Subproject commit 89f6b1b92547d5a54fc4e8aa81fe3e454da30318 +Subproject commit b326a22dad825fc036d63956bc9901165d825d86 diff --git a/lib/SharpDX.Desktop b/lib/SharpDX.Desktop new file mode 160000 index 000000000..7fc56bc0a --- /dev/null +++ b/lib/SharpDX.Desktop @@ -0,0 +1 @@ +Subproject commit 7fc56bc0a240030d4736e6b16da33b08c73c3ba4 diff --git a/Dalamud.Injector/stylecop.json b/stylecop.json similarity index 100% rename from Dalamud.Injector/stylecop.json rename to stylecop.json