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..d9c1af8f4 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 @@ -54,7 +50,7 @@ jobs: path: .\scratch - name: Generate dalamud-distrib version file - shell: powershell + shell: pwsh run: | Compress-Archive .\scratch\* .\canary.zip # Recreate the release zip 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..a454b4431 --- /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(NDEBUG) + 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(NDEBUG) + 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.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..bfaba20d0 --- /dev/null +++ b/Dalamud.CorePlugin/GlobalSuppressions.cs @@ -0,0 +1,17 @@ +// 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")] +[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.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.LayoutRules", "SA1503:Braces should not be omitted", 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")] diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs new file mode 100644 index 000000000..4d635eeb5 --- /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(new PluginWindow(Dalamud.Instance)); + + 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..2b752deb4 --- /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(NDEBUG) + 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(NDEBUG) + 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/Game/Text/SeStringHandling/SeStringManagerTests.cs b/Dalamud.Test/Game/Text/SeStringHandling/SeStringManagerTests.cs new file mode 100644 index 000000000..95c1ec4d3 --- /dev/null +++ b/Dalamud.Test/Game/Text/SeStringHandling/SeStringManagerTests.cs @@ -0,0 +1,20 @@ +using System.Linq; +using Dalamud.Game.Text.SeStringHandling; +using Xunit; + +namespace Dalamud.Test.Game.Text.SeStringHandling +{ + public class SeStringManagerTests + { + [Fact] + public void TestNewLinePayload() + { + var manager = new SeStringManager(null); + var newLinePayloadBytes = new byte[] {0x02, 0x10, 0x01, 0x03}; + + var seString = manager.Parse(newLinePayloadBytes); + + Assert.True(newLinePayloadBytes.SequenceEqual(seString.Encode())); + } + } +} 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..370b210bf 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -1,137 +1,163 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 -VisualStudioVersion = 16.0.29215.179 +VisualStudioVersion = 16.0.31424.327 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|Any CPU.Build.0 = 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 76% rename from Dalamud/Configuration/DalamudConfiguration.cs rename to Dalamud/Configuration/Internal/DalamudConfiguration.cs index cb2ffa1ce..ed10572e4 100644 --- a/Dalamud/Configuration/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -5,20 +5,21 @@ using System.IO; using Dalamud.Game.Text; using Newtonsoft.Json; using Serilog; +using Serilog.Events; -namespace Dalamud.Configuration +namespace Dalamud.Configuration.Internal { /// /// Class containing Dalamud settings. /// [Serializable] - internal 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); @@ -68,15 +69,28 @@ 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. The key is the absolute path + /// to the 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 Dictionary DevPluginSettings { get; set; } = new(); /// /// Gets or sets the global UI scale. @@ -113,6 +127,11 @@ namespace Dalamud.Configuration /// public bool DoButtonsSystemMenu { get; set; } = true; + /// + /// Gets or sets the default Dalamud debug log level on startup. + /// + public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information; + /// /// Gets or sets a value indicating whether or not the debug log should scroll automatically. /// @@ -138,6 +157,21 @@ namespace Dalamud.Configuration /// public bool IsGamepadNavigationEnabled { get; set; } = true; + /// + /// Gets or sets a value indicating whether or not the anti-anti-debug check is enabled on startup. + /// + public bool IsAntiAntiDebugEnabled { get; set; } = false; + + /// + /// Gets or sets the kind of beta to download when is set to true. + /// + public string DalamudBetaKind { get; set; } + + /// + /// Gets or sets a value indicating whether or not all plugins, regardless of API level, should be loaded. + /// + public bool LoadAllApiLevels { get; set; } + /// /// Load a configuration from the provided path. /// diff --git a/Dalamud/Configuration/Internal/DevPluginSettings.cs b/Dalamud/Configuration/Internal/DevPluginSettings.cs new file mode 100644 index 000000000..17350cba0 --- /dev/null +++ b/Dalamud/Configuration/Internal/DevPluginSettings.cs @@ -0,0 +1,18 @@ +namespace Dalamud.Configuration.Internal +{ + /// + /// Settings for DevPlugins. + /// + internal sealed class DevPluginSettings + { + /// + /// 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 63ab9f333..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. /// - internal 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..95f25b04a 100644 --- a/Dalamud/Configuration/PluginConfigurations.cs +++ b/Dalamud/Configuration/PluginConfigurations.cs @@ -1,5 +1,6 @@ using System.IO; +using JetBrains.Annotations; using Newtonsoft.Json; namespace Dalamud.Configuration @@ -7,7 +8,7 @@ namespace Dalamud.Configuration /// /// Configuration to store settings for a dalamud plugin. /// - public class PluginConfigurations + public sealed class PluginConfigurations { private readonly DirectoryInfo configDirectory; @@ -44,6 +45,7 @@ namespace Dalamud.Configuration /// /// Plugin name. /// Plugin configuration. + [CanBeNull] public IPluginConfiguration Load(string pluginName) { var path = this.GetConfigFile(pluginName); @@ -60,6 +62,22 @@ namespace Dalamud.Configuration }); } + /// + /// Delete the configuration file and folder for the specified plugin. + /// This will throw an if the plugin did not correctly close its handles. + /// + /// The name of the plugin. + public void Delete(string pluginName) + { + var directory = this.GetDirectoryPath(pluginName); + if (directory.Exists) + directory.Delete(true); + + var file = this.GetConfigFile(pluginName); + if (file.Exists) + file.Delete(); + } + /// /// Get plugin directory. /// diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 7c98e6a8e..170a64082 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 @@ -43,12 +48,17 @@ namespace Dalamud /// DalamudStartInfo instance. /// LoggingLevelSwitch to control Serilog level. /// Signal signalling shutdown. - public Dalamud(DalamudStartInfo info, LoggingLevelSwitch loggingLevelSwitch, ManualResetEvent finishSignal) + /// 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(); @@ -57,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 /// @@ -74,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. /// @@ -93,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. /// @@ -203,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); @@ -211,6 +229,10 @@ namespace Dalamud this.Framework.Enable(); Log.Information("[T1] Framework ENABLE!"); + + // Initialize FFXIVClientStructs function resolver + FFXIVClientStructs.Resolver.Initialize(); + Log.Information("[T1] FFXIVClientStructs initialized!"); } catch (Exception ex) { @@ -226,11 +248,12 @@ namespace Dalamud { try { - this.Configuration = DalamudConfiguration.Load(this.StartInfo.ConfigurationPath); - this.AntiDebug = new AntiDebug(this.SigScanner); + if (this.Configuration.IsAntiAntiDebugEnabled) + this.AntiDebug.Enable(); #if DEBUG - this.AntiDebug.Enable(); + if (!this.AntiDebug.IsEnabled) + this.AntiDebug.Enable(); #endif Log.Information("[T2] AntiDebug OK!"); @@ -286,6 +309,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!"); @@ -324,28 +348,21 @@ 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); + this.PluginManager.OnInstalledPluginsChanged += () => + Troubleshooting.LogTroubleshooting(this, this.InterfaceManager.IsReady); Log.Information("[T3] PM OK!"); + + this.PluginManager.CleanupPlugins(); + Log.Information("[T3] PMC OK!"); + + this.PluginManager.LoadAllPlugins(); + Log.Information("[T3] PML OK!"); } catch (Exception ex) { @@ -354,11 +371,9 @@ namespace Dalamud } this.DalamudUi = new DalamudInterface(this); - this.InterfaceManager.OnDraw += this.DalamudUi.Draw; - Log.Information("[T3] DUI OK!"); - Troubleshooting.LogTroubleshooting(this, this.InterfaceManager != null); + Troubleshooting.LogTroubleshooting(this, this.InterfaceManager.IsReady); Log.Information("Dalamud is ready."); } @@ -407,16 +422,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(); } /// @@ -433,20 +441,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 501c36869..28892907e 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.6.1 - true + 6.0.0.0 + 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;IDE0044;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 39fe1e38b..7cfe03443 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -1,11 +1,13 @@ using System; using System.IO; using System.Net; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using Dalamud.Interface; -using EasyHook; +using Dalamud.Configuration.Internal; +using Dalamud.Interface.Internal; +using Newtonsoft.Json; using Serilog; using Serilog.Core; using Serilog.Events; @@ -15,28 +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); - 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); @@ -46,14 +61,9 @@ 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; + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls; - // Log any unhandled exception. - AppDomain.CurrentDomain.UnhandledException += this.OnUnhandledException; - TaskScheduler.UnobservedTaskException += this.OnUnobservedTaskException; - - var dalamud = new Dalamud(info, levelSwitch, finishSignal); + var dalamud = new Dalamud(info, levelSwitch, finishSignal, configuration); Log.Information("Starting a session.."); // Run session @@ -68,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(); @@ -77,7 +88,7 @@ namespace Dalamud } } - private (Logger Logger, LoggingLevelSwitch LevelSwitch) NewLogger(string baseDirectory) + private static LoggingLevelSwitch InitLogging(string baseDirectory, DalamudConfiguration configuration) { #if DEBUG var logPath = Path.Combine(baseDirectory, "dalamud.log"); @@ -90,35 +101,34 @@ namespace Dalamud #if DEBUG levelSwitch.MinimumLevel = LogEventLevel.Verbose; #else - levelSwitch.MinimumLevel = LogEventLevel.Information; + levelSwitch.MinimumLevel = configuration.LogLevel; #endif - - var newLogger = new LoggerConfiguration() - .WriteTo.Async(a => a.File(logPath)) + Log.Logger = new LoggerConfiguration() + .WriteTo.Async(a => a.File(logPath, fileSizeLimitBytes: 5 * 1024 * 1024, rollOnFileSizeLimit: true)) .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..58701a572 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Drawing; using System.Linq; using System.Reflection; using System.Text; @@ -11,7 +10,6 @@ using CheapLoc; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; -using Dalamud.Interface; using Serilog; namespace Dalamud.Game @@ -21,39 +19,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 +94,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; @@ -122,23 +120,20 @@ namespace Dalamud.Game public string LastLink { get; private set; } /// - /// Convert a string to SeString and wrap in italics payloads. + /// Convert a TextPayload 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, - })); + public static SeString MakeItalics(string text) + => MakeItalics(new TextPayload(text)); - return italicString; - } + /// + /// Convert a TextPayload to SeString and wrap in italics payloads. + /// + /// Text to convert. + /// SeString payload of italicized text. + public static SeString MakeItalics(TextPayload text) + => new(EmphasisItalicPayload.ItalicsOn, text, EmphasisItalicPayload.ItalicsOff); private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled) { @@ -243,13 +238,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)); } } @@ -257,20 +252,19 @@ namespace Dalamud.Game { this.dalamud.Framework.Gui.Chat.PrintChat(new XivChatEntry { - MessageBytes = Encoding.UTF8.GetBytes(Loc.Localize("DalamudUpdated", "The In-Game addon has been updated or was reinstalled successfully! Please check the discord for a full changelog.")), + Message = Loc.Localize("DalamudUpdated", "The In-Game addon has been updated or was reinstalled successfully! Please check the discord for a full changelog."), 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,19 +272,19 @@ 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 { this.dalamud.Framework.Gui.Chat.PrintChat(new XivChatEntry { - MessageBytes = new SeString(new List() + Message = new SeString(new List() { new TextPayload(Loc.Localize("DalamudPluginUpdateRequired", "One or more of your plugins needs to be updated. Please use the /xlplugins command in-game to update them!")), new TextPayload(" ["), @@ -300,7 +294,7 @@ namespace Dalamud.Game RawPayload.LinkTerminator, new UIForegroundPayload(this.dalamud.Data, 0), new TextPayload("]"), - }).Encode(), + }), Type = XivChatType.Urgent, }); } diff --git a/Dalamud/Game/ClientState/Actors/ActorTable.cs b/Dalamud/Game/ClientState/Actors/ActorTable.cs index 7f8ca7c35..a139f8b48 100644 --- a/Dalamud/Game/ClientState/Actors/ActorTable.cs +++ b/Dalamud/Game/ClientState/Actors/ActorTable.cs @@ -1,8 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Actors.Types; using Dalamud.Game.ClientState.Actors.Types.NonPlayer; @@ -14,204 +12,131 @@ namespace Dalamud.Game.ClientState.Actors /// /// This collection represents the currently spawned FFXIV actors. /// - public sealed partial class ActorTable : IReadOnlyCollection, ICollection, IDisposable + public sealed partial class ActorTable { private const int ActorTableLength = 424; - #region ReadProcessMemory Hack - private static readonly int ActorMemSize = Marshal.SizeOf(typeof(Structs.Actor)); - private static readonly IntPtr ActorMem = Marshal.AllocHGlobal(ActorMemSize); - private static readonly IntPtr CurrentProcessHandle = new(-1); - #endregion - - private Dalamud dalamud; - private ClientStateAddressResolver address; - private List actorsCache; + private readonly Dalamud dalamud; + private readonly ClientStateAddressResolver address; /// /// Initializes a new instance of the class. - /// Set up the actor table collection. /// - /// The Dalamud instance. - /// The ClientStateAddressResolver instance. - public ActorTable(Dalamud dalamud, ClientStateAddressResolver addressResolver) + /// The instance. + /// Client state address resolver. + internal ActorTable(Dalamud dalamud, ClientStateAddressResolver addressResolver) { - this.address = addressResolver; this.dalamud = dalamud; + this.address = addressResolver; - 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}"); } /// /// Gets the amount of currently spawned actors. /// - public int Length => this.ActorsCache.Count; + public int Length + { + get + { + var count = 0; + for (var i = 0; i < ActorTableLength; i++) + { + var ptr = this.GetActorAddress(i); + if (ptr != IntPtr.Zero) + { + count++; + } + } - private List ActorsCache => this.actorsCache ??= this.GetActorTable(); + return count; + } + } /// /// Get an actor at the specified spawn index. /// /// Spawn index. - /// at the specified spawn index. + /// An at the specified spawn index. [CanBeNull] - public Actor this[int index] => this.ActorsCache[index]; + public Actor this[int index] + { + get + { + var address = this.GetActorAddress(index); + return this.CreateActorReference(address); + } + } /// - /// Read an actor struct from memory and create the appropriate type of actor. + /// Gets the address of the actor at the specified index of the actor table. /// - /// Offset of the actor in the actor table. - /// An instantiated actor. - internal Actor ReadActorFromMemory(IntPtr offset) + /// The index of the actor. + /// The memory address of the actor. + public unsafe IntPtr GetActorAddress(int index) { - try - { - // FIXME: hack workaround for trying to access the player on logout, after the main object has been deleted - if (!NativeFunctions.ReadProcessMemory(CurrentProcessHandle, offset, ActorMem, ActorMemSize, out _)) - { - Log.Debug("ActorTable - ReadProcessMemory failed: likely player deletion during logout"); - return null; - } + if (index >= ActorTableLength) + return IntPtr.Zero; - var actorStruct = Marshal.PtrToStructure(ActorMem); + return *(IntPtr*)(this.address.ActorTable + (8 * index)); + } - return actorStruct.ObjectKind switch - { - ObjectKind.Player => new PlayerCharacter(offset, actorStruct, this.dalamud), - ObjectKind.BattleNpc => new BattleNpc(offset, actorStruct, this.dalamud), - ObjectKind.EventObj => new EventObj(offset, actorStruct, this.dalamud), - ObjectKind.Companion => new Npc(offset, actorStruct, this.dalamud), - _ => new Actor(offset, actorStruct, this.dalamud), - }; - } - catch (Exception e) - { - Log.Error(e, "Could not read actor from memory."); + /// + /// Create a reference to a FFXIV actor. + /// + /// The address of the actor in memory. + /// object or inheritor containing requested data. + [CanBeNull] + public unsafe Actor CreateActorReference(IntPtr address) + { + if (this.dalamud.ClientState.LocalContentId == 0) return null; - } - } - private void ResetCache() => this.actorsCache = null; + if (address == IntPtr.Zero) + return null; - private void Framework_OnUpdateEvent(Internal.Framework framework) - { - this.ResetCache(); - } - - private IntPtr[] GetPointerTable() - { - var ret = new IntPtr[ActorTableLength]; - Marshal.Copy(this.address.ActorTable, ret, 0, ActorTableLength); - return ret; - } - - private List GetActorTable() - { - var actors = new List(); - var ptrTable = this.GetPointerTable(); - for (var i = 0; i < ActorTableLength; i++) + var objKind = *(ObjectKind*)(address + ActorOffsets.ObjectKind); + return objKind switch { - actors.Add(ptrTable[i] != IntPtr.Zero ? this.ReadActorFromMemory(ptrTable[i]) : null); - } - - return actors; + ObjectKind.Player => new PlayerCharacter(address, this.dalamud), + ObjectKind.BattleNpc => new BattleNpc(address, this.dalamud), + ObjectKind.EventObj => new EventObj(address, this.dalamud), + ObjectKind.Companion => new Npc(address, this.dalamud), + _ => new Actor(address, this.dalamud), + }; } } /// - /// Implementing IDisposable. + /// This collection represents the currently spawned FFXIV actors. /// - public sealed partial class ActorTable : IDisposable + public sealed partial class ActorTable : IReadOnlyCollection, ICollection { - private bool disposed = false; - - /// - /// Finalizes an instance of the class. - /// - ~ActorTable() => this.Dispose(false); - - /// - /// Disposes of managed and unmanaged resources. - /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (this.disposed) - return; - - if (disposing) - { - this.dalamud.Framework.OnUpdateEvent -= this.Framework_OnUpdateEvent; - Marshal.FreeHGlobal(ActorMem); - } - - this.disposed = true; - } - } - - /// - /// Implementing IReadOnlyCollection, IEnumerable, and Enumerable. - /// - public sealed partial class ActorTable : IReadOnlyCollection - { - /// - /// Gets the number of elements in the collection. - /// - /// The number of elements in the collection. + /// int IReadOnlyCollection.Count => this.Length; - /// - /// Gets an enumerator capable of iterating through the actor table. - /// - /// An actor enumerable. - public IEnumerator GetEnumerator() => this.ActorsCache.Where(a => a != null).GetEnumerator(); - - /// - /// Gets an enumerator capable of iterating through the actor table. - /// - /// An actor enumerable. - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - } - - /// - /// Implementing ICollection. - /// - public sealed partial class ActorTable : ICollection - { - /// - /// Gets the number of elements in the collection. - /// - /// The number of elements in the collection. + /// int ICollection.Count => this.Length; - /// - /// Gets a value indicating whether access to the collection is synchronized (thread safe). - /// - /// Whether access is synchronized (thread safe) or not. + /// bool ICollection.IsSynchronized => false; - /// - /// Gets an object that can be used to synchronize access to the collection. - /// - /// An object that can be used to synchronize access to the collection. + /// object ICollection.SyncRoot => this; - /// - /// Copies the elements of the collection to an array, starting at a particular index. - /// - /// - /// The one-dimensional array that is the destination of the elements copied from the collection. The array must have zero-based indexing. - /// - /// - /// The zero-based index in array at which copying begins. - /// + /// + public IEnumerator GetEnumerator() + { + for (var i = 0; i < ActorTableLength; i++) + { + yield return this[i]; + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + /// void ICollection.CopyTo(Array array, int index) { for (var i = 0; i < this.Length; i++) diff --git a/Dalamud/Game/ClientState/Actors/Resolvers/BaseResolver.cs b/Dalamud/Game/ClientState/Actors/Resolvers/BaseResolver.cs deleted file mode 100644 index c097b1111..000000000 --- a/Dalamud/Game/ClientState/Actors/Resolvers/BaseResolver.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Dalamud.Game.ClientState.Actors.Resolvers -{ - /// - /// Base object resolver. - /// - public abstract class BaseResolver - { - private Dalamud dalamud; - - /// - /// Initializes a new instance of the class. - /// - /// The Dalamud instance. - public BaseResolver(Dalamud dalamud) - { - this.dalamud = dalamud; - } - - /// - /// Gets the Dalamud instance. - /// - protected Dalamud Dalamud => this.dalamud; - } -} diff --git a/Dalamud/Game/ClientState/Actors/Resolvers/ClassJob.cs b/Dalamud/Game/ClientState/Actors/Resolvers/ClassJob.cs deleted file mode 100644 index 57ae9fc48..000000000 --- a/Dalamud/Game/ClientState/Actors/Resolvers/ClassJob.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Dalamud.Game.ClientState.Actors.Resolvers -{ - /// - /// This object represents a class or job. - /// - public class ClassJob : BaseResolver - { - /// - /// ID of the ClassJob. - /// - public readonly uint Id; - - /// - /// Initializes a new instance of the class. - /// Set up the ClassJob resolver with the provided ID. - /// - /// The ID of the classJob. - /// The Dalamud instance. - public ClassJob(byte id, Dalamud dalamud) - : base(dalamud) - { - this.Id = id; - } - - /// - /// Gets GameData linked to this ClassJob. - /// - public Lumina.Excel.GeneratedSheets.ClassJob GameData => - this.Dalamud.Data.GetExcelSheet().GetRow(this.Id); - } -} diff --git a/Dalamud/Game/ClientState/Actors/Resolvers/World.cs b/Dalamud/Game/ClientState/Actors/Resolvers/World.cs deleted file mode 100644 index 536bfaf81..000000000 --- a/Dalamud/Game/ClientState/Actors/Resolvers/World.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Dalamud.Game.ClientState.Actors.Resolvers -{ - /// - /// This object represents a world a character can reside on. - /// - public class World : BaseResolver - { - /// - /// ID of the world. - /// - public readonly uint Id; - - /// - /// Initializes a new instance of the class. - /// Set up the world resolver with the provided ID. - /// - /// The ID of the world. - /// The Dalamud instance. - public World(ushort id, Dalamud dalamud) - : base(dalamud) - { - this.Id = id; - } - - /// - /// Gets GameData linked to this world. - /// - public Lumina.Excel.GeneratedSheets.World GameData => - this.Dalamud.Data.GetExcelSheet().GetRow(this.Id); - } -} diff --git a/Dalamud/Game/ClientState/Actors/Targets.cs b/Dalamud/Game/ClientState/Actors/Targets.cs index c9e8a5129..154aa446d 100644 --- a/Dalamud/Game/ClientState/Actors/Targets.cs +++ b/Dalamud/Game/ClientState/Actors/Targets.cs @@ -2,6 +2,7 @@ using System; using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Actors.Types; +using JetBrains.Annotations; namespace Dalamud.Game.ClientState.Actors { @@ -10,8 +11,8 @@ namespace Dalamud.Game.ClientState.Actors /// public sealed class Targets { - private Dalamud dalamud; - private ClientStateAddressResolver address; + private readonly Dalamud dalamud; + private readonly ClientStateAddressResolver address; /// /// Initializes a new instance of the class. @@ -27,26 +28,31 @@ namespace Dalamud.Game.ClientState.Actors /// /// Gets the current target. /// + [CanBeNull] public Actor CurrentTarget => this.GetActorByOffset(TargetOffsets.CurrentTarget); /// /// Gets the mouseover target. /// + [CanBeNull] public Actor MouseOverTarget => this.GetActorByOffset(TargetOffsets.MouseOverTarget); /// /// Gets the focus target. /// + [CanBeNull] public Actor FocusTarget => this.GetActorByOffset(TargetOffsets.FocusTarget); /// /// Gets the previous target. /// + [CanBeNull] public Actor PreviousTarget => this.GetActorByOffset(TargetOffsets.PreviousTarget); /// /// Gets the soft target. /// + [CanBeNull] public Actor SoftTarget => this.GetActorByOffset(TargetOffsets.SoftTarget); /// @@ -91,6 +97,7 @@ namespace Dalamud.Game.ClientState.Actors Marshal.WriteIntPtr(this.address.TargetManager, offset, actorAddress); } + [CanBeNull] private Actor GetActorByOffset(int offset) { if (this.address.TargetManager == IntPtr.Zero) @@ -100,7 +107,7 @@ namespace Dalamud.Game.ClientState.Actors if (actorAddress == IntPtr.Zero) return null; - return this.dalamud.ClientState.Actors.ReadActorFromMemory(actorAddress); + return this.dalamud.ClientState.Actors.CreateActorReference(actorAddress); } } diff --git a/Dalamud/Game/ClientState/Actors/Types/Actor.cs b/Dalamud/Game/ClientState/Actors/Types/Actor.cs index 9707f6674..f98d8932e 100644 --- a/Dalamud/Game/ClientState/Actors/Types/Actor.cs +++ b/Dalamud/Game/ClientState/Actors/Types/Actor.cs @@ -1,102 +1,165 @@ using System; -using System.Text; +using System.Runtime.InteropServices; + using Dalamud.Game.ClientState.Structs; -using Serilog; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Memory; namespace Dalamud.Game.ClientState.Actors.Types { /// - /// This class represents a basic FFXIV actor. + /// This class represents a basic actor (GameObject) in FFXIV. /// - public class Actor : IEquatable + public unsafe partial class Actor : IEquatable { - private readonly Structs.Actor actorStruct; - private readonly Dalamud dalamud; - - private string name; - /// /// Initializes a new instance of the class. - /// This represents a basic FFXIV actor. /// - /// 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) + /// A dalamud reference needed to access game data in Resolvers. + internal Actor(IntPtr address, Dalamud dalamud) { - this.actorStruct = actorStruct; - this.dalamud = dalamud; + this.Dalamud = dalamud; this.Address = address; } /// - /// Gets the position of this . + /// Gets the address of the actor in memory. /// - public Position3 Position => this.ActorStruct.Position; + public IntPtr Address { get; } /// - /// Gets the rotation of this . - /// This ranges from -pi to pi radians. + /// Gets Dalamud itself. /// - public float Rotation => this.ActorStruct.Rotation; + private protected Dalamud Dalamud { get; } + /// + /// This allows you to if (actor) {...} to check for validity. + /// + /// The actor to check. + /// True or false. + public static implicit operator bool(Actor actor) => IsValid(actor); + + public static bool operator ==(Actor actor1, Actor actor2) + { + if (actor1 is null || actor2 is null) + return Equals(actor1, actor2); + + return actor1.Equals(actor2); + } + + public static bool operator !=(Actor actor1, Actor actor2) => !(actor1 == actor2); + + /// + /// Gets a value indicating whether this actor is still valid in memory. + /// + /// The actor to check. + /// True or false. + public static bool IsValid(Actor actor) + { + if (actor == null) + return false; + + if (actor.Dalamud.ClientState.LocalContentId == 0) + return false; + + return true; + } + + /// + /// Gets a value indicating whether this actor is still valid in memory. + /// + /// True or false. + public bool IsValid() => IsValid(this); + + /// + bool IEquatable.Equals(Actor other) => this.ActorId == other?.ActorId; + + /// + public override bool Equals(object obj) => ((IEquatable)this).Equals(obj as Actor); + + /// + public override int GetHashCode() => this.ActorId.GetHashCode(); + } + + /// + /// This class represents a basic actor (GameObject) in FFXIV. + /// + public unsafe partial class Actor + { /// /// Gets the displayname of this . /// - public string Name => this.name ??= Util.GetUTF8String(this.actorStruct.Name); + public SeString Name => MemoryHelper.ReadSeString(this.Address + ActorOffsets.Name, 32); /// /// Gets the actor ID of this . /// - public int ActorId => this.ActorStruct.ActorId; + public uint ActorId => *(uint*)(this.Address + ActorOffsets.ActorId); /// - /// Gets the hitbox radius of this . + /// Gets the data ID for linking to other respective game data. /// - public float HitboxRadius => this.ActorStruct.HitboxRadius; + public uint DataId => *(uint*)(this.Address + ActorOffsets.DataId); + + /// + /// Gets the ID of this GameObject's owner. + /// + public uint OwnerId => *(uint*)(this.Address + ActorOffsets.OwnerId); /// /// Gets the entity kind of this . /// See the ObjectKind enum for possible values. /// - public ObjectKind ObjectKind => this.ActorStruct.ObjectKind; + public ObjectKind ObjectKind => *(ObjectKind*)(this.Address + ActorOffsets.ObjectKind); + + /// + /// Gets the sub kind of this Actor. + /// + public byte SubKind => *(byte*)(this.Address + ActorOffsets.SubKind); + + /// + /// Gets a value indicating whether the actor is friendly. + /// + public bool IsFriendly => *(int*)(this.Address + ActorOffsets.IsFriendly) > 0; /// /// Gets the X distance from the local player in yalms. /// - public byte YalmDistanceX => this.ActorStruct.YalmDistanceFromPlayerX; + public byte YalmDistanceX => *(byte*)(this.Address + ActorOffsets.YalmDistanceFromObjectX); + + /// + /// Gets the target status. + /// + /// + /// This is some kind of enum. It may be . + /// + public byte TargetStatus => *(byte*)(this.Address + ActorOffsets.TargetStatus); /// /// Gets the Y distance from the local player in yalms. /// - public byte YalmDistanceY => this.ActorStruct.YalmDistanceFromPlayerY; + public byte YalmDistanceY => *(byte*)(this.Address + ActorOffsets.YalmDistanceFromObjectY); /// - /// Gets the target of the actor. + /// Gets the position of this . /// - public virtual int TargetActorID => 0; + public Position3 Position => *(Position3*)(this.Address + ActorOffsets.Position); /// - /// Gets status Effects. + /// Gets the rotation of this . + /// This ranges from -pi to pi radians. /// - public StatusEffect[] StatusEffects => this.ActorStruct.UIStatusEffects; + public float Rotation => *(float*)(this.Address + ActorOffsets.Rotation); /// - /// Gets the address of this actor in memory. + /// Gets the hitbox radius of this . /// - public readonly IntPtr Address; + public float HitboxRadius => *(float*)(this.Address + ActorOffsets.HitboxRadius); /// - /// Gets the memory representation of the base actor. + /// Gets the current target of the Actor. /// - internal Structs.Actor ActorStruct => this.actorStruct; - - /// - /// Gets the backing instance. - /// - protected Dalamud Dalamud => this.dalamud; - - /// - bool IEquatable.Equals(Actor other) => this.ActorId == other.ActorId; + public virtual uint TargetActorID => 0; } } diff --git a/Dalamud/Game/ClientState/Actors/Types/ActorOffsets.cs b/Dalamud/Game/ClientState/Actors/Types/ActorOffsets.cs new file mode 100644 index 000000000..e3ef26a5c --- /dev/null +++ b/Dalamud/Game/ClientState/Actors/Types/ActorOffsets.cs @@ -0,0 +1,61 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Dalamud.Game.ClientState.Actors.Types +{ + /// + /// Memory offsets for the type and all that inherit from it. + /// + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the offset usage instead.")] + public static class ActorOffsets + { + // GameObject(Actor) + // GameObject :: Character + // GameObject :: Character :: BattleChara + // GameObject :: Character :: Companion + + public const int Name = 0x30; + public const int ActorId = 0x74; + public const int DataId = 0x80; + public const int OwnerId = 0x84; + public const int ObjectKind = 0x8C; + public const int SubKind = 0x8D; + public const int IsFriendly = 0x8E; + public const int YalmDistanceFromObjectX = 0x90; + public const int TargetStatus = 0x91; + public const int YalmDistanceFromObjectY = 0x92; + public const int Position = 0xA0; + public const int Rotation = 0xB0; + public const int HitboxRadius = 0xC0; + // End GameObject 0x1A0 + + public const int CurrentHp = 0x1C4; + public const int MaxHp = 0x1C8; + public const int CurrentMp = 0x1CC; + public const int MaxMp = 0x1D0; + public const int CurrentGp = 0x1D4; + public const int MaxGp = 0x1D6; + public const int CurrentCp = 0x1D8; + public const int MaxCp = 0x1DA; + public const int ClassJob = 0x1E2; + public const int Level = 0x1E3; + public const int PlayerCharacterTargetActorId = 0x230; + public const int Customize = 0x1898; + public const int CompanyTag = 0x18B2; + public const int BattleNpcTargetActorId = 0x18D8; + public const int NameId = 0x1940; + public const int CurrentWorld = 0x195C; + public const int HomeWorld = 0x195E; + public const int StatusFlags = 0x19A0; + // End Character 0x19B0 + // End Companion 0x19C0 + + public const int UIStatusEffects = 0x19F8; + public const int IsCasting = 0x1B80; + public const int IsCasting2 = 0x1B82; + public const int CurrentCastSpellActionId = 0x1B84; + public const int CurrentCastTargetActorId = 0x1B90; + public const int CurrentCastTime = 0x1BB4; + public const int TotalCastTime = 0x1BB8; + // End BattleChara 0x2C00 + } +} diff --git a/Dalamud/Game/ClientState/Actors/Types/Chara.cs b/Dalamud/Game/ClientState/Actors/Types/Chara.cs index 01e4ba243..792ca17c0 100644 --- a/Dalamud/Game/ClientState/Actors/Types/Chara.cs +++ b/Dalamud/Game/ClientState/Actors/Types/Chara.cs @@ -1,80 +1,124 @@ using System; -using Dalamud.Game.ClientState.Actors.Resolvers; +using Dalamud.Game.ClientState.Resolvers; +using Dalamud.Game.ClientState.Structs; +using Dalamud.Memory; namespace Dalamud.Game.ClientState.Actors.Types { /// /// This class represents the base for non-static entities. /// - public class Chara : Actor + public unsafe class Chara : Actor { /// /// Initializes a new instance of the class. /// This represents a non-static entity. /// - /// 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) - : base(address, actorStruct, dalamud) + /// A dalamud reference needed to access game data in Resolvers. + internal Chara(IntPtr address, Dalamud dalamud) + : base(address, dalamud) { } - /// - /// Gets the level of this Chara. - /// - public byte Level => this.ActorStruct.Level; - - /// - /// Gets the ClassJob of this Chara. - /// - public ClassJob ClassJob => new(this.ActorStruct.ClassJob, this.Dalamud); - /// /// Gets the current HP of this Chara. /// - public int CurrentHp => this.ActorStruct.CurrentHp; + public uint CurrentHp => *(uint*)(this.Address + ActorOffsets.CurrentHp); /// /// Gets the maximum HP of this Chara. /// - public int MaxHp => this.ActorStruct.MaxHp; + public uint MaxHp => *(uint*)(this.Address + ActorOffsets.MaxHp); /// /// Gets the current MP of this Chara. /// - public int CurrentMp => this.ActorStruct.CurrentMp; + public uint CurrentMp => *(uint*)(this.Address + ActorOffsets.CurrentMp); /// /// Gets the maximum MP of this Chara. /// - public int MaxMp => this.ActorStruct.MaxMp; + public uint MaxMp => *(uint*)(this.Address + ActorOffsets.MaxMp); /// /// Gets the current GP of this Chara. /// - public int CurrentGp => this.ActorStruct.CurrentGp; + public uint CurrentGp => *(uint*)(this.Address + ActorOffsets.CurrentGp); /// /// Gets the maximum GP of this Chara. /// - public int MaxGp => this.ActorStruct.MaxGp; + public uint MaxGp => *(uint*)(this.Address + ActorOffsets.MaxGp); /// /// Gets the current CP of this Chara. /// - public int CurrentCp => this.ActorStruct.CurrentCp; + public uint CurrentCp => *(uint*)(this.Address + ActorOffsets.CurrentCp); /// /// Gets the maximum CP of this Chara. /// - public int MaxCp => this.ActorStruct.MaxCp; + public uint MaxCp => *(uint*)(this.Address + ActorOffsets.MaxCp); + + /// + /// Gets the ClassJob of this Chara. + /// + public ClassJobResolver ClassJob => new(*(byte*)(this.Address + ActorOffsets.ClassJob), this.Dalamud); + + /// + /// Gets the level of this Chara. + /// + public byte Level => *(byte*)(this.Address + ActorOffsets.Level); /// /// Gets a byte array describing the visual appearance of this Chara. /// Indexed by . /// - public byte[] Customize => this.ActorStruct.Customize; + public byte[] Customize => MemoryHelper.Read(this.Address + ActorOffsets.Customize, 28); + + /// + /// Gets the status flags. + /// + public StatusFlags StatusFlags => *(StatusFlags*)(this.Address + ActorOffsets.StatusFlags); + + /// + /// Gets the current status effects. + /// + /// + /// This copies every time it is invoked, so make sure to only grab it once. + /// + public StatusEffect[] StatusEffects => MemoryHelper.Read(this.Address + ActorOffsets.UIStatusEffects, 30, true); + + /// + /// Gets a value indicating whether the actor is currently casting. + /// + public bool IsCasting => *(int*)(this.Address + ActorOffsets.IsCasting) > 0; + + /// + /// Gets a value indicating whether the actor is currently casting (again?). + /// + public bool IsCasting2 => *(int*)(this.Address + ActorOffsets.IsCasting2) > 0; + + /// + /// Gets the spell action ID currently being cast by the actor. + /// + public uint CurrentCastSpellActionId => *(uint*)(this.Address + ActorOffsets.CurrentCastSpellActionId); + + /// + /// Gets the actor ID of the target currently being cast at by the actor. + /// + public uint CurrentCastTargetActorId => *(uint*)(this.Address + ActorOffsets.CurrentCastTargetActorId); + + /// + /// Gets the current casting time of the spell being cast by the actor. + /// + public float CurrentCastTime => *(float*)(this.Address + ActorOffsets.CurrentCastTime); + + /// + /// Gets the total casting time of the spell being cast by the actor. + /// + public float TotalCastTime => *(float*)(this.Address + ActorOffsets.TotalCastTime); } } diff --git a/Dalamud/Game/ClientState/Actors/CustomizeIndex.cs b/Dalamud/Game/ClientState/Actors/Types/CustomizeIndex.cs similarity index 98% rename from Dalamud/Game/ClientState/Actors/CustomizeIndex.cs rename to Dalamud/Game/ClientState/Actors/Types/CustomizeIndex.cs index 2461a028a..23618ae24 100644 --- a/Dalamud/Game/ClientState/Actors/CustomizeIndex.cs +++ b/Dalamud/Game/ClientState/Actors/Types/CustomizeIndex.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.ClientState.Actors +namespace Dalamud.Game.ClientState.Actors.Types { /// /// This enum describes the indices of the Customize array. diff --git a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/BattleNpc.cs b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/BattleNpc.cs index 0bfd60c5b..3fa8f0c28 100644 --- a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/BattleNpc.cs +++ b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/BattleNpc.cs @@ -5,33 +5,27 @@ namespace Dalamud.Game.ClientState.Actors.Types.NonPlayer /// /// This class represents a battle NPC. /// - public class BattleNpc : Npc + public unsafe class BattleNpc : Npc { /// /// Initializes a new instance of the class. /// Set up a new BattleNpc with the provided memory representation. /// - /// 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) - : base(address, actorStruct, dalamud) + /// A dalamud reference needed to access game data in Resolvers. + internal BattleNpc(IntPtr address, Dalamud dalamud) + : base(address, dalamud) { } /// /// Gets the BattleNpc of this BattleNpc. /// - public BattleNpcSubKind BattleNpcKind => (BattleNpcSubKind)this.ActorStruct.SubKind; + public BattleNpcSubKind BattleNpcKind => *(BattleNpcSubKind*)(this.Address + ActorOffsets.SubKind); /// - /// Gets the ID of this BattleNpc's owner. + /// Gets the target of the Battle NPC. /// - public int OwnerId => this.ActorStruct.OwnerId; - - /// - /// Gets target of the Battle NPC. - /// - public override int TargetActorID => this.ActorStruct.BattleNpcTargetActorId; + public override uint TargetActorID => *(uint*)(this.Address + ActorOffsets.BattleNpcTargetActorId); } } diff --git a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/EventObj.cs b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/EventObj.cs index e0ac5964d..7d9dce443 100644 --- a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/EventObj.cs +++ b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/EventObj.cs @@ -5,23 +5,22 @@ namespace Dalamud.Game.ClientState.Actors.Types.NonPlayer /// /// This class represents an EventObj. /// - public class EventObj : Actor + public unsafe class EventObj : Actor { /// /// Initializes a new instance of the class. - /// This represents an Event Object. + /// Set up a new EventObj with the provided memory representation. /// - /// 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) - : base(address, actorStruct, dalamud) + /// A dalamud reference needed to access game data in Resolvers. + internal EventObj(IntPtr address, Dalamud dalamud) + : base(address, dalamud) { } /// - /// Gets the data ID of the NPC linking to their respective game data. + /// Gets the event object ID of the linking to their respective game data. /// - public int DataId => this.ActorStruct.DataId; + public uint EventObjectId => *(uint*)(this.Address + ActorOffsets.DataId); } } diff --git a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/Npc.cs b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/Npc.cs index 7be029450..a3597570c 100644 --- a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/Npc.cs +++ b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/Npc.cs @@ -5,28 +5,27 @@ namespace Dalamud.Game.ClientState.Actors.Types.NonPlayer /// /// This class represents a NPC. /// - public class Npc : Chara + public unsafe class Npc : Chara { /// /// Initializes a new instance of the class. - /// This represents a Non-playable Character. + /// Set up a new NPC with the provided memory representation. /// - /// 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) - : base(address, actorStruct, dalamud) + /// A dalamud reference needed to access game data in Resolvers. + internal Npc(IntPtr address, Dalamud dalamud) + : base(address, dalamud) { } /// - /// Gets the data ID of the NPC linking to their respective game data. + /// Gets the data ID of the NPC linking to their assoicated BNpcBase data. /// - public int DataId => this.ActorStruct.DataId; + public uint BaseId => *(uint*)(this.Address + ActorOffsets.DataId); /// /// Gets the name ID of the NPC linking to their respective game data. /// - public int NameId => this.ActorStruct.NameId; + public uint NameId => *(uint*)(this.Address + ActorOffsets.NameId); } } diff --git a/Dalamud/Game/ClientState/Actors/ObjectKind.cs b/Dalamud/Game/ClientState/Actors/Types/ObjectKind.cs similarity index 97% rename from Dalamud/Game/ClientState/Actors/ObjectKind.cs rename to Dalamud/Game/ClientState/Actors/Types/ObjectKind.cs index 4e62d812d..b79adb784 100644 --- a/Dalamud/Game/ClientState/Actors/ObjectKind.cs +++ b/Dalamud/Game/ClientState/Actors/Types/ObjectKind.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.ClientState.Actors +namespace Dalamud.Game.ClientState.Actors.Types { /// /// Enum describing possible entity kinds. diff --git a/Dalamud/Game/ClientState/Actors/Types/PartyMember.cs b/Dalamud/Game/ClientState/Actors/Types/PartyMember.cs index 74cf9c97c..2e2d67f54 100644 --- a/Dalamud/Game/ClientState/Actors/Types/PartyMember.cs +++ b/Dalamud/Game/ClientState/Actors/Types/PartyMember.cs @@ -1,4 +1,5 @@ -using System.Runtime.InteropServices; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Memory; namespace Dalamud.Game.ClientState.Actors.Types { @@ -7,26 +8,6 @@ namespace Dalamud.Game.ClientState.Actors.Types /// public class PartyMember { - /// - /// The name of the character. - /// - public string CharacterName; - - /// - /// Unknown. - /// - public long Unknown; - - /// - /// The actor object that corresponds to this party member. - /// - public Actor Actor; - - /// - /// The kind or type of actor. - /// - public ObjectKind ObjectKind; - /// /// Initializes a new instance of the class. /// @@ -34,9 +15,10 @@ namespace Dalamud.Game.ClientState.Actors.Types /// The interop data struct. public PartyMember(ActorTable table, Structs.PartyMember rawData) { - this.CharacterName = Marshal.PtrToStringAnsi(rawData.namePtr); + this.CharacterName = MemoryHelper.ReadSeString(rawData.namePtr); this.Unknown = rawData.unknown; this.Actor = null; + for (var i = 0; i < table.Length; i++) { if (table[i] != null && table[i].ActorId == rawData.actorId) @@ -48,5 +30,25 @@ namespace Dalamud.Game.ClientState.Actors.Types this.ObjectKind = rawData.objectKind; } + + /// + /// Gets the name of the character. + /// + public SeString CharacterName { get; } + + /// + /// Gets something unknown. + /// + public long Unknown { get; } + + /// + /// Gets the actor object that corresponds to this party member. + /// + public Actor Actor { get; } + + /// + /// Gets the kind or type of actor. + /// + public ObjectKind ObjectKind { get; } } } diff --git a/Dalamud/Game/ClientState/Actors/Types/PlayerCharacter.cs b/Dalamud/Game/ClientState/Actors/Types/PlayerCharacter.cs index 7cc932550..a1ebe1ab0 100644 --- a/Dalamud/Game/ClientState/Actors/Types/PlayerCharacter.cs +++ b/Dalamud/Game/ClientState/Actors/Types/PlayerCharacter.cs @@ -1,51 +1,45 @@ using System; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using Dalamud.Game.ClientState.Actors.Resolvers; -using Dalamud.Game.ClientState.Structs; +using Dalamud.Game.ClientState.Resolvers; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Memory; namespace Dalamud.Game.ClientState.Actors.Types { /// /// This class represents a player character. /// - public class PlayerCharacter : Chara + public unsafe class PlayerCharacter : Chara { /// /// Initializes a new instance of the class. /// This represents a player character. /// - /// 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) - : base(address, actorStruct, dalamud) + /// A dalamud reference needed to access game data in Resolvers. + internal PlayerCharacter(IntPtr address, Dalamud dalamud) + : base(address, dalamud) { - var companyTagBytes = new byte[5]; - Marshal.Copy(this.Address + ActorOffsets.CompanyTag, companyTagBytes, 0, companyTagBytes.Length); - this.CompanyTag = Encoding.UTF8.GetString(companyTagBytes.TakeWhile(c => c != 0x0).ToArray()); } /// - /// Gets the current world of the character. + /// Gets the current world of the character. /// - public World CurrentWorld => new(this.ActorStruct.CurrentWorld, this.Dalamud); + public WorldResolver CurrentWorld => new(*(ushort*)(this.Address + ActorOffsets.CurrentWorld), this.Dalamud); /// - /// Gets the home world of the character. + /// Gets the home world of the character. /// - public World HomeWorld => new(this.ActorStruct.HomeWorld, this.Dalamud); + public WorldResolver HomeWorld => new(*(ushort*)(this.Address + ActorOffsets.HomeWorld), this.Dalamud); /// /// Gets the Free Company tag of this player. /// - public string CompanyTag { get; private set; } + public SeString CompanyTag => MemoryHelper.ReadSeString(this.Address + ActorOffsets.CompanyTag, 6); /// - /// Gets the target of the PlayerCharacter. + /// Gets the target actor ID of the PlayerCharacter. /// - public override int TargetActorID => this.ActorStruct.PlayerCharacterTargetActorId; + public override uint TargetActorID => *(uint*)(this.Address + ActorOffsets.PlayerCharacterTargetActorId); } } diff --git a/Dalamud/Game/ClientState/Actors/Types/StatusFlags.cs b/Dalamud/Game/ClientState/Actors/Types/StatusFlags.cs new file mode 100644 index 000000000..a1101ceca --- /dev/null +++ b/Dalamud/Game/ClientState/Actors/Types/StatusFlags.cs @@ -0,0 +1,56 @@ +using System; + +namespace Dalamud.Game.ClientState.Actors.Types +{ + /// + /// Enum describing possible status flags. + /// + [Flags] + public enum StatusFlags : byte + { + /// + /// No status flags set. + /// + None = 0, + + /// + /// Hostile actor. + /// + Hostile = 1, + + /// + /// Actor in combat. + /// + InCombat = 2, + + /// + /// Actor weapon is out. + /// + WeaponOut = 4, + + /// + /// Actor offhand is out. + /// + OffhandOut = 8, + + /// + /// Actor is a party member. + /// + PartyMember = 16, + + /// + /// Actor is a alliance member. + /// + AllianceMember = 32, + + /// + /// Actor is in friend list. + /// + Friend = 64, + + /// + /// Actor is casting. + /// + IsCasting = 128, + } +} diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index f2057a607..b9e40ccd7 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Actors; using Dalamud.Game.ClientState.Actors.Types; +using Dalamud.Game.ClientState.Fates; using Dalamud.Game.Internal; using Dalamud.Hooking; using JetBrains.Annotations; @@ -15,58 +16,8 @@ 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. - /// - public readonly ActorTable Actors; - - /// - /// Gets the language of the client. - /// - public readonly ClientLanguage ClientLanguage; - - /// - /// The current Territory the player resides in. - /// - public ushort TerritoryType; - - /// - /// The class facilitating Job Gauge data access. - /// - public JobGauges JobGauges; - - /// - /// The class facilitating party list data access. - /// - public PartyList PartyList; - - /// - /// Provides access to the keypress state of keyboard keys in game. - /// - public KeyState KeyState; - - /// - /// Provides access to the button state of gamepad buttons in game. - /// - public GamepadState GamepadState; - - /// - /// Provides access to client conditions/player state. Allows you to check if a player is in a duty, mounted, etc. - /// - public Condition Condition; - - /// - /// The class facilitating target data access. - /// - public Targets Targets; - - /// - /// Event that gets fired when the current Territory changes. - /// - public EventHandler TerritoryChanged; - private readonly Dalamud dalamud; private readonly ClientStateAddressResolver address; private readonly Hook setupTerritoryTypeHook; @@ -80,7 +31,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(); @@ -92,6 +43,8 @@ namespace Dalamud.Game.ClientState this.Actors = new ActorTable(dalamud, this.address); + this.Fates = new FateTable(dalamud, this.address); + this.PartyList = new PartyList(dalamud, this.address); this.JobGauges = new JobGauges(this.address); @@ -104,7 +57,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); @@ -122,6 +75,11 @@ namespace Dalamud.Game.ClientState public event PropertyChangedEventHandler PropertyChanged; #pragma warning restore + /// + /// Event that gets fired when the current Territory changes. + /// + public event EventHandler TerritoryChanged; + /// /// Event that fires when a character is logging in. /// @@ -137,22 +95,61 @@ namespace Dalamud.Game.ClientState /// public event EventHandler CfPop; + /// + /// Gets the table of all present actors. + /// + public ActorTable Actors { get; } + + /// + /// Gets the table of all present fates. + /// + public FateTable Fates { get; } + + /// + /// Gets the language of the client. + /// + public ClientLanguage ClientLanguage { get; } + + /// + /// Gets the class facilitating Job Gauge data access. + /// + public JobGauges JobGauges { get; } + + /// + /// Gets the class facilitating party list data access. + /// + public PartyList PartyList { get; } + + /// + /// Gets access to the keypress state of keyboard keys in game. + /// + public KeyState KeyState { get; } + + /// + /// Gets access to the button state of gamepad buttons in game. + /// + public GamepadState GamepadState { get; } + + /// + /// Gets access to client conditions/player state. Allows you to check if a player is in a duty, mounted, etc. + /// + public Condition Condition { get; } + + /// + /// Gets the class facilitating target data access. + /// + public Targets Targets { get; } + + /// + /// Gets the current Territory the player resides in. + /// + public ushort TerritoryType { get; private set; } + /// /// Gets the local player character, if one is present. /// [CanBeNull] - public PlayerCharacter LocalPlayer - { - get - { - var actor = this.Actors[0]; - - if (actor is PlayerCharacter pc) - return pc; - - return null; - } - } + public PlayerCharacter LocalPlayer => this.Actors[0] as PlayerCharacter; /// /// Gets the content ID of the local character. @@ -181,7 +178,6 @@ namespace Dalamud.Game.ClientState { this.PartyList.Dispose(); this.setupTerritoryTypeHook.Dispose(); - this.Actors.Dispose(); this.GamepadState.Dispose(); this.dalamud.Framework.OnUpdateEvent -= this.FrameworkOnOnUpdateEvent; diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs index ed87f06d5..a9937b40c 100644 --- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs +++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using Dalamud.Game.Internal; @@ -16,6 +17,14 @@ namespace Dalamud.Game.ClientState /// public IntPtr ActorTable { get; private set; } + /// + /// Gets the address of the fate table pointer. + /// + /// + /// This is a static address to a pointer, not the address of the table itself. + /// + public IntPtr FateTablePtr { get; private set; } + // public IntPtr ViewportActorTable { get; private set; } /// @@ -50,9 +59,6 @@ namespace Dalamud.Game.ClientState /// public IntPtr SetupTerritoryType { get; private set; } - // public IntPtr SomeActorTableAccess { get; private set; } - // public IntPtr PartyListUpdate { get; private set; } - /// /// Gets the address of the method which polls the gamepads for data. /// Called every frame, even when `Enable Gamepad` is off in the settings. @@ -68,14 +74,18 @@ namespace Dalamud.Game.ClientState // We don't need those anymore, but maybe someone else will - let's leave them here for good measure // ViewportActorTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? 85 ED", 0) + 0x148; // SomeActorTableAccess = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 55 A0 48 8D 8E ?? ?? ?? ??"); + this.ActorTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 83"); + this.FateTablePtr = sig.GetStaticAddressFromSig("48 8B 15 ?? ?? ?? ?? 48 8B F9 44 0F B7 41 ??"); + this.LocalContentId = sig.GetStaticAddressFromSig("48 0F 44 05 ?? ?? ?? ?? 48 39 07"); this.JobGaugeData = sig.GetStaticAddressFromSig("E8 ?? ?? ?? ?? FF C6 48 8D 5B 0C", 0xB9) + 0x10; this.SetupTerritoryType = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B F9 66 89 91 ?? ?? ?? ??"); - // This resolves to a fixed offset only, without the base address added in, so GetStaticAddressFromSig() can't be used + // This resolves to a fixed offset only, without the base address added in, + // so GetStaticAddressFromSig() can't be used. lea rcx, ds:1DB9F74h[rax*4] this.KeyboardState = sig.ScanText("48 8D 0C 85 ?? ?? ?? ?? 8B 04 31 85 C2 0F 85") + 0x4; // PartyListUpdate = sig.ScanText("E8 ?? ?? ?? ?? 49 8B D7 4C 8D 86 ?? ?? ?? ??"); diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs new file mode 100644 index 000000000..07158820e --- /dev/null +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +using Dalamud.Game.ClientState.Fates.Types; +using JetBrains.Annotations; +using Serilog; + +namespace Dalamud.Game.ClientState.Fates +{ + /// + /// This collection represents the currently available Fate events. + /// + public sealed partial class FateTable + { + // If the pointer at this offset is 0, do not scan the table + private const int CheckPtrOffset = 0x80; + private const int FirstPtrOffset = 0x90; + private const int LastPtrOffset = 0x98; + + private readonly Dalamud dalamud; + private readonly ClientStateAddressResolver address; + + /// + /// Initializes a new instance of the class. + /// + /// The instance. + /// Client state address resolver. + internal FateTable(Dalamud dalamud, ClientStateAddressResolver addressResolver) + { + this.address = addressResolver; + this.dalamud = dalamud; + + Log.Verbose($"Fate table address 0x{this.address.FateTablePtr.ToInt64():X}"); + } + + /// + /// Gets the amount of currently active Fates. + /// + public unsafe int Length + { + get + { + var fateTable = this.FateTableAddress; + if (fateTable == IntPtr.Zero) + return 0; + + var check = *(long*)(fateTable + CheckPtrOffset); + if (check == 0) + return 0; + + var start = *(long*)(fateTable + FirstPtrOffset); + var end = *(long*)(fateTable + LastPtrOffset); + if (start == 0 || end == 0) + return 0; + + return (int)((end - start) / 8); + } + } + + private unsafe IntPtr FateTableAddress + { + get + { + if (this.address.FateTablePtr == IntPtr.Zero) + return IntPtr.Zero; + + return *(IntPtr*)this.address.FateTablePtr; + } + } + + /// + /// Get an actor at the specified spawn index. + /// + /// Spawn index. + /// A at the specified spawn index. + [CanBeNull] + public Fate this[int index] + { + get + { + var address = this.GetFateAddress(index); + return this[address]; + } + } + + /// + /// Get a Fate at the specified address. + /// + /// The Fate address. + /// A at the specified address. + public Fate this[IntPtr address] + { + get + { + if (address == IntPtr.Zero) + return null; + + return this.CreateFateReference(address); + } + } + + /// + /// Gets the address of the Fate at the specified index of the fate table. + /// + /// The index of the Fate. + /// The memory address of the Fate. + public unsafe IntPtr GetFateAddress(int index) + { + if (index >= this.Length) + return IntPtr.Zero; + + var fateTable = this.FateTableAddress; + if (fateTable == IntPtr.Zero) + return IntPtr.Zero; + + var firstFate = *(IntPtr*)(fateTable + FirstPtrOffset); + return *(IntPtr*)(firstFate + (8 * index)); + } + + /// + /// Create a reference to a FFXIV actor. + /// + /// The offset of the actor in memory. + /// object containing requested data. + [CanBeNull] + internal unsafe Fate CreateFateReference(IntPtr offset) + { + if (this.dalamud.ClientState.LocalContentId == 0) + return null; + + if (offset == IntPtr.Zero) + return null; + + return new Fate(offset, this.dalamud); + } + } + + /// + /// This collection represents the currently available Fate events. + /// + public sealed partial class FateTable : IReadOnlyCollection, ICollection + { + /// + int IReadOnlyCollection.Count => this.Length; + + /// + int ICollection.Count => this.Length; + + /// + bool ICollection.IsSynchronized => false; + + /// + object ICollection.SyncRoot => this; + + /// + public IEnumerator GetEnumerator() + { + for (var i = 0; i < this.Length; i++) + { + yield return this[i]; + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + /// + void ICollection.CopyTo(Array array, int index) + { + for (var i = 0; i < this.Length; i++) + { + array.SetValue(this[i], index); + index++; + } + } + } +} diff --git a/Dalamud/Game/ClientState/Fates/Types/Fate.cs b/Dalamud/Game/ClientState/Fates/Types/Fate.cs new file mode 100644 index 000000000..a42e3f925 --- /dev/null +++ b/Dalamud/Game/ClientState/Fates/Types/Fate.cs @@ -0,0 +1,138 @@ +using System; + +using Dalamud.Game.ClientState.Resolvers; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Client.System.String; + +namespace Dalamud.Game.ClientState.Fates.Types +{ + /// + /// This class represents an FFXIV Fate. + /// + public unsafe partial class Fate : IEquatable + { + /// + /// Initializes a new instance of the class. + /// + /// The address of this fate in memory. + /// Dalamud instance. + internal Fate(IntPtr address, Dalamud dalamud) + { + this.Address = address; + this.Dalamud = dalamud; + } + + /// + /// Gets the address of this Fate in memory. + /// + public IntPtr Address { get; } + + /// + /// Gets Dalamud itself. + /// + private protected Dalamud Dalamud { get; } + + public static bool operator ==(Fate fate1, Fate fate2) + { + if (fate1 is null || fate2 is null) + return Equals(fate1, fate2); + + return fate1.Equals(fate2); + } + + public static bool operator !=(Fate fate1, Fate fate2) => !(fate1 == fate2); + + /// + /// Gets a value indicating whether this Fate is still valid in memory. + /// + /// The fate to check. + /// True or false. + public static bool IsValid(Fate fate) + { + if (fate == null) + return false; + + if (fate.Dalamud.ClientState.LocalContentId == 0) + return false; + + return true; + } + + /// + /// Gets a value indicating whether this actor is still valid in memory. + /// + /// True or false. + public bool IsValid() => IsValid(this); + + /// + bool IEquatable.Equals(Fate other) => this.FateId == other?.FateId; + + /// + public override bool Equals(object obj) => ((IEquatable)this).Equals(obj as Fate); + + /// + public override int GetHashCode() => this.FateId.GetHashCode(); + } + + /// + /// This class represents an FFXIV Fate. + /// + public unsafe partial class Fate + { + /// + /// Gets the Fate ID of this . + /// + public ushort FateId => *(ushort*)(this.Address + FateOffsets.FateId); + + /// + /// Gets game data linked to this Fate. + /// + public Lumina.Excel.GeneratedSheets.Fate GameData => this.Dalamud.Data.GetExcelSheet().GetRow(this.FateId); + + /// + /// Gets the time this started. + /// + public int StartTimeEpoch => *(int*)(this.Address + FateOffsets.StartTimeEpoch); + + /// + /// Gets how long this will run. + /// + public short Duration => *(short*)(this.Address + FateOffsets.Duration); + + /// + /// Gets the remaining time in seconds for this . + /// + public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds(); + + /// + /// Gets the displayname of this . + /// + public SeString Name => MemoryHelper.ReadSeString((Utf8String*)(this.Address + FateOffsets.Name)); + + /// + /// Gets the state of this (Running, Ended, Failed, Preparation, WaitingForEnd). + /// + public FateState State => *(FateState*)(this.Address + FateOffsets.State); + + /// + /// Gets the progress amount of this . + /// + public byte Progress => *(byte*)(this.Address + FateOffsets.Progress); + + /// + /// Gets the level of this . + /// + public byte Level => *(byte*)(this.Address + FateOffsets.Level); + + /// + /// Gets the position of this . + /// + public Position3 Position => *(Position3*)(this.Address + FateOffsets.Position); + + /// + /// Gets the territory this is located in. + /// + public TerritoryTypeResolver TerritoryType => new(*(ushort*)(this.Address + FateOffsets.Territory), this.Dalamud); + } +} diff --git a/Dalamud/Game/ClientState/Fates/Types/FateOffsets.cs b/Dalamud/Game/ClientState/Fates/Types/FateOffsets.cs new file mode 100644 index 000000000..73bc7a702 --- /dev/null +++ b/Dalamud/Game/ClientState/Fates/Types/FateOffsets.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Dalamud.Game.ClientState.Fates.Types +{ + /// + /// Memory offsets for the type. + /// + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the offset usage instead.")] + public static class FateOffsets + { + public const int FateId = 0x18; + public const int StartTimeEpoch = 0x20; + public const int Duration = 0x28; + public const int Name = 0xC0; + public const int State = 0x3AC; + public const int Progress = 0x3B8; + public const int Level = 0x3F9; + public const int Position = 0x450; + public const int Territory = 0x74E; + } +} diff --git a/Dalamud/Game/ClientState/Fates/Types/FateState.cs b/Dalamud/Game/ClientState/Fates/Types/FateState.cs new file mode 100644 index 000000000..94eb00717 --- /dev/null +++ b/Dalamud/Game/ClientState/Fates/Types/FateState.cs @@ -0,0 +1,33 @@ +namespace Dalamud.Game.ClientState.Fates.Types +{ + /// + /// This represents the state of a single Fate. + /// + public enum FateState : byte + { + /// + /// The Fate is active. + /// + Running = 0x02, + + /// + /// The Fate has ended. + /// + Ended = 0x04, + + /// + /// The player failed the Fate. + /// + Failed = 0x05, + + /// + /// The Fate is preparing to run. + /// + Preparation = 0x07, + + /// + /// The Fate is preparing to end. + /// + WaitingForEnd = 0x08, + } +} 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/Resolvers/BaseResolver{T}.cs b/Dalamud/Game/ClientState/Resolvers/BaseResolver{T}.cs new file mode 100644 index 000000000..2ded995d7 --- /dev/null +++ b/Dalamud/Game/ClientState/Resolvers/BaseResolver{T}.cs @@ -0,0 +1,34 @@ +using Lumina.Excel; + +namespace Dalamud.Game.ClientState.Resolvers +{ + /// + /// This object represents a class or job. + /// + /// The type of Lumina sheet to resolve. + public class BaseResolver where T : ExcelRow + { + private readonly Dalamud dalamud; + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the classJob. + /// The Dalamud instance. + internal BaseResolver(uint id, Dalamud dalamud) + { + this.dalamud = dalamud; + this.Id = id; + } + + /// + /// Gets the ID to be resolved. + /// + public uint Id { get; } + + /// + /// Gets GameData linked to this excel row. + /// + public T GameData => this.dalamud.Data.GetExcelSheet().GetRow(this.Id); + } +} diff --git a/Dalamud/Game/ClientState/Resolvers/ClassJobResolver.cs b/Dalamud/Game/ClientState/Resolvers/ClassJobResolver.cs new file mode 100644 index 000000000..b9603838d --- /dev/null +++ b/Dalamud/Game/ClientState/Resolvers/ClassJobResolver.cs @@ -0,0 +1,19 @@ +namespace Dalamud.Game.ClientState.Resolvers +{ + /// + /// This object represents a class or job. + /// + public class ClassJobResolver : BaseResolver + { + /// + /// Initializes a new instance of the class. + /// Set up the ClassJob resolver with the provided ID. + /// + /// The ID of the classJob. + /// The Dalamud instance. + internal ClassJobResolver(ushort id, Dalamud dalamud) + : base(id, dalamud) + { + } + } +} diff --git a/Dalamud/Game/ClientState/Resolvers/FateResolver.cs b/Dalamud/Game/ClientState/Resolvers/FateResolver.cs new file mode 100644 index 000000000..15f2fce0d --- /dev/null +++ b/Dalamud/Game/ClientState/Resolvers/FateResolver.cs @@ -0,0 +1,19 @@ +namespace Dalamud.Game.ClientState.Resolvers +{ + /// + /// This object represents a Fate a character can participate in. + /// + public class FateResolver : BaseResolver + { + /// + /// Initializes a new instance of the class. + /// Set up the Fate resolver with the provided ID. + /// + /// The ID of the Fate. + /// The Dalamud instance. + internal FateResolver(ushort id, Dalamud dalamud) + : base(id, dalamud) + { + } + } +} diff --git a/Dalamud/Game/ClientState/Resolvers/TerritoryTypeResolver.cs b/Dalamud/Game/ClientState/Resolvers/TerritoryTypeResolver.cs new file mode 100644 index 000000000..248bf94bb --- /dev/null +++ b/Dalamud/Game/ClientState/Resolvers/TerritoryTypeResolver.cs @@ -0,0 +1,19 @@ +namespace Dalamud.Game.ClientState.Resolvers +{ + /// + /// This object represents a territory a character can be in. + /// + public class TerritoryTypeResolver : BaseResolver + { + /// + /// Initializes a new instance of the class. + /// Set up the territory type resolver with the provided ID. + /// + /// The ID of the territory type. + /// The Dalamud instance. + internal TerritoryTypeResolver(ushort id, Dalamud dalamud) + : base(id, dalamud) + { + } + } +} diff --git a/Dalamud/Game/ClientState/Resolvers/WorldResolver.cs b/Dalamud/Game/ClientState/Resolvers/WorldResolver.cs new file mode 100644 index 000000000..0d37e3549 --- /dev/null +++ b/Dalamud/Game/ClientState/Resolvers/WorldResolver.cs @@ -0,0 +1,19 @@ +namespace Dalamud.Game.ClientState.Resolvers +{ + /// + /// This object represents a world a character can reside on. + /// + public class WorldResolver : BaseResolver + { + /// + /// Initializes a new instance of the class. + /// Set up the world resolver with the provided ID. + /// + /// The ID of the world. + /// The Dalamud instance. + internal WorldResolver(ushort id, Dalamud dalamud) + : base(id, dalamud) + { + } + } +} diff --git a/Dalamud/Game/ClientState/Structs/Actor.cs b/Dalamud/Game/ClientState/Structs/Actor.cs deleted file mode 100644 index e765a8c4c..000000000 --- a/Dalamud/Game/ClientState/Structs/Actor.cs +++ /dev/null @@ -1,289 +0,0 @@ -using System.Runtime.InteropServices; - -using Dalamud.Game.ClientState.Actors; - -namespace Dalamud.Game.ClientState.Structs -{ - /// - /// Native memory representation of an FFXIV actor. - /// - [StructLayout(LayoutKind.Explicit, Pack = 2)] - public struct Actor - { - /// - /// The actor name. - /// - [FieldOffset(ActorOffsets.Name)] - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 30)] - public byte[] Name; - - /// - /// The actor's internal id. - /// - [FieldOffset(ActorOffsets.ActorId)] - public int ActorId; - - /// - /// The actor's data id. - /// - [FieldOffset(ActorOffsets.DataId)] - public int DataId; - - /// - /// The actor's owner id. This is useful for pets, summons, and the like. - /// - [FieldOffset(ActorOffsets.OwnerId)] - public int OwnerId; - - /// - /// The type or kind of actor. - /// - [FieldOffset(ActorOffsets.ObjectKind)] - public ObjectKind ObjectKind; - - /// - /// The sub-type or sub-kind of actor. - /// - [FieldOffset(ActorOffsets.SubKind)] - public byte SubKind; - - /// - /// Whether the actor is friendly. - /// - [FieldOffset(ActorOffsets.IsFriendly)] - public bool IsFriendly; - - /// - /// The horizontal distance in game units from the player. - /// - [FieldOffset(ActorOffsets.YalmDistanceFromPlayerX)] - public byte YalmDistanceFromPlayerX; - - /// - /// The player target status. - /// - /// - /// This is some kind of enum. - /// - [FieldOffset(ActorOffsets.PlayerTargetStatus)] - public byte PlayerTargetStatus; - - /// - /// The vertical distance in game units from the player. - /// - [FieldOffset(ActorOffsets.YalmDistanceFromPlayerY)] - public byte YalmDistanceFromPlayerY; - - /// - /// The (X,Z,Y) position of the actor. - /// - [FieldOffset(ActorOffsets.Position)] - public Position3 Position; - - /// - /// The rotation of the actor. - /// - /// - /// The rotation is around the vertical axis (yaw), from -pi to pi radians. - /// - [FieldOffset(ActorOffsets.Rotation)] - public float Rotation; - - /// - /// The hitbox radius of the actor. - /// - [FieldOffset(ActorOffsets.HitboxRadius)] - public float HitboxRadius; - - /// - /// The current HP of the actor. - /// - [FieldOffset(ActorOffsets.CurrentHp)] - public int CurrentHp; - - /// - /// The max HP of the actor. - /// - [FieldOffset(ActorOffsets.MaxHp)] - public int MaxHp; - - /// - /// The current MP of the actor. - /// - [FieldOffset(ActorOffsets.CurrentMp)] - public int CurrentMp; - - /// - /// The max MP of the actor. - /// - [FieldOffset(ActorOffsets.MaxMp)] - public short MaxMp; - - /// - /// The current GP of the actor. - /// - [FieldOffset(ActorOffsets.CurrentGp)] - public short CurrentGp; - - /// - /// The max GP of the actor. - /// - [FieldOffset(ActorOffsets.MaxGp)] - public short MaxGp; - - /// - /// The current CP of the actor. - /// - [FieldOffset(ActorOffsets.CurrentCp)] - public short CurrentCp; - - /// - /// The max CP of the actor. - /// - [FieldOffset(ActorOffsets.MaxCp)] - public short MaxCp; - - /// - /// The class-job of the actor. - /// - [FieldOffset(ActorOffsets.ClassJob)] - public byte ClassJob; - - /// - /// The level of the actor. - /// - [FieldOffset(ActorOffsets.Level)] - public byte Level; - - /// - /// The (player character) actor ID being targeted by the actor. - /// - [FieldOffset(ActorOffsets.PlayerCharacterTargetActorId)] - public int PlayerCharacterTargetActorId; - - /// - /// The customization byte/bitfield of the actor. - /// - [FieldOffset(ActorOffsets.Customize)] - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)] - public byte[] Customize; - - // Normally pack=2 should work, but ByTVal or Injection breaks this. - // [FieldOffset(ActorOffsets.CompanyTag)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] public string CompanyTag; - - /// - /// The (battle npc) actor ID being targeted by the actor. - /// - [FieldOffset(ActorOffsets.BattleNpcTargetActorId)] - public int BattleNpcTargetActorId; - - /// - /// The name ID of the actor. - /// - [FieldOffset(ActorOffsets.NameId)] - public int NameId; - - /// - /// The current world ID of the actor. - /// - [FieldOffset(ActorOffsets.CurrentWorld)] - public ushort CurrentWorld; - - /// - /// The home world ID of the actor. - /// - [FieldOffset(ActorOffsets.HomeWorld)] - public ushort HomeWorld; - - /// - /// Whether the actor is currently casting. - /// - [FieldOffset(ActorOffsets.IsCasting)] - public bool IsCasting; - - /// - /// Whether the actor is currently casting (dup?). - /// - [FieldOffset(ActorOffsets.IsCasting2)] - public bool IsCasting2; - - /// - /// The spell action ID currently being cast by the actor. - /// - [FieldOffset(ActorOffsets.CurrentCastSpellActionId)] - public uint CurrentCastSpellActionId; - - /// - /// The actor ID of the target currently being cast at by the actor. - /// - [FieldOffset(ActorOffsets.CurrentCastTargetActorId)] - public uint CurrentCastTargetActorId; - - /// - /// The current casting time of the spell being cast by the actor. - /// - [FieldOffset(ActorOffsets.CurrentCastTime)] - public float CurrentCastTime; - - /// - /// The total casting time of the spell being cast by the actor. - /// - [FieldOffset(ActorOffsets.TotalCastTime)] - public float TotalCastTime; - - /// - /// The array of status effects that the actor is currently affected by. - /// - [FieldOffset(ActorOffsets.UIStatusEffects)] - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 30)] - public StatusEffect[] UIStatusEffects; - } - - /// - /// Memory offsets for the type. - /// - public static class ActorOffsets - { - // Reference https://github.com/FFXIVAPP/sharlayan-resources/blob/master/structures/5.4/x64.json for more - public const int Name = 48; // 0x0030 - public const int ActorId = 116; // 0x0074 - // public const int ??? = 120; // 0x0078 NPCID1 - public const int DataId = 128; // 0x0080 NPCID2 - public const int OwnerId = 132; // 0x0084 - public const int ObjectKind = 140; // 0x008C Type - public const int SubKind = 141; // 0x008D - public const int IsFriendly = 142; // 0x008E - public const int YalmDistanceFromPlayerX = 144; // 0x0090 - public const int PlayerTargetStatus = 145; // 0x0091 - public const int YalmDistanceFromPlayerY = 146; // 0x0092 Distance - public const int Position = 160; // 0x00A0 (X,Z,Y) - public const int Rotation = 176; // 0x00B0 Heading - public const int HitboxRadius = 192; // 0x00C0 - public const int CurrentHp = 452; // 0x01C4 HPCurrent - public const int MaxHp = 456; // 0x01C8 HPMax - public const int CurrentMp = 460; // 0x01CC MPCurrent - public const int MaxMp = 464; // 0x01D0 MPMax - public const int CurrentGp = 468; // 0x01D4 GPCurrent - public const int MaxGp = 470; // 0x01D6 GPMax - public const int CurrentCp = 472; // 0x01D8 CPCurrent - public const int MaxCp = 474; // 0x01DA CPMax - public const int ClassJob = 482; // 0x01E2 Job - public const int Level = 483; // 0x01E3 Level - public const int PlayerCharacterTargetActorId = 560; // 0x01F0 TargetID - - public const int Customize = 0x1898; // Needs verification - public const int CompanyTag = 0x18B2; - public const int BattleNpcTargetActorId = 0x18D8; // Needs verification - public const int NameId = 0x1940; // Needs verification - public const int CurrentWorld = 0x195C; - public const int HomeWorld = 0x195E; - - public const int IsCasting = 0x1B80; - public const int IsCasting2 = 0x1B82; - public const int CurrentCastSpellActionId = 0x1B84; - public const int CurrentCastTargetActorId = 0x1B90; - public const int CurrentCastTime = 0x1BB4; - public const int TotalCastTime = 0x1BB8; - public const int UIStatusEffects = 0x19F8; - } -} diff --git a/Dalamud/Game/ClientState/Structs/GamepadInput.cs b/Dalamud/Game/ClientState/Structs/GamepadInput.cs index ce7440b61..df80b0ef4 100644 --- a/Dalamud/Game/ClientState/Structs/GamepadInput.cs +++ b/Dalamud/Game/ClientState/Structs/GamepadInput.cs @@ -1,4 +1,4 @@ -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; namespace Dalamud.Game.ClientState.Structs { @@ -40,25 +40,37 @@ namespace Dalamud.Game.ClientState.Structs /// /// Raw input, set the whole time while a button is held. See for the mapping. /// + /// + /// This is a bitfield. + /// [FieldOffset(0x98)] - public ushort ButtonsRaw; // bitfield + public ushort ButtonsRaw; /// /// Button pressed, set once when the button is pressed. See for the mapping. /// + /// + /// This is a bitfield. + /// [FieldOffset(0x9C)] - public ushort ButtonsPressed; // bitfield + public ushort ButtonsPressed; /// /// Button released input, set once right after the button is not hold anymore. See for the mapping. /// + /// + /// This is a bitfield. + /// [FieldOffset(0xA0)] - public ushort ButtonsReleased; // bitfield + public ushort ButtonsReleased; /// /// Repeatedly emits the held button input in fixed intervals. See for the mapping. /// + /// + /// This is a bitfield. + /// [FieldOffset(0xA4)] - public ushort ButtonsRepeat; // bitfield + public ushort ButtonsRepeat; } } diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/BLMGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/BLMGauge.cs index 694a0d981..e4125745b 100644 --- a/Dalamud/Game/ClientState/Structs/JobGauge/BLMGauge.cs +++ b/Dalamud/Game/ClientState/Structs/JobGauge/BLMGauge.cs @@ -8,35 +8,43 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge [StructLayout(LayoutKind.Explicit)] public struct BLMGauge { - /// - /// Gets the time until the next Polyglot stack in milliseconds. - /// [FieldOffset(0)] - public short TimeUntilNextPolyglot; // enochian timer + private short timeUntilNextPolyglot; // enochian timer - /// - /// Gets the time remaining for Astral Fire or Umbral Ice in milliseconds. - /// [FieldOffset(2)] - public short ElementTimeRemaining; // umbral ice and astral fire timer + private short elementTimeRemaining; // umbral ice and astral fire timer [FieldOffset(4)] private byte elementStance; // umbral ice or astral fire - /// - /// Gets the number of Umbral Hearts remaining. - /// [FieldOffset(5)] - public byte NumUmbralHearts; + private byte numUmbralHearts; + + [FieldOffset(6)] + private byte numPolyglotStacks; + + [FieldOffset(7)] + private byte enochianState; + + /// + /// Gets the time until the next Polyglot stack in milliseconds. + /// + public short TimeUntilNextPolyglot => this.timeUntilNextPolyglot; + + /// + /// Gets the time remaining for Astral Fire or Umbral Ice in milliseconds. + /// + public short ElementTimeRemaining => this.elementTimeRemaining; /// /// Gets the number of Polyglot stacks remaining. /// - [FieldOffset(6)] - public byte NumPolyglotStacks; + public byte NumPolyglotStacks => this.numPolyglotStacks; - [FieldOffset(7)] - private byte enochianState; + /// + /// Gets the number of Umbral Hearts remaining. + /// + public byte NumUmbralHearts => this.numUmbralHearts; /// /// Gets if the player is in Umbral Ice. diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/BRDGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/BRDGauge.cs index 3fe2b5dee..98590805c 100644 --- a/Dalamud/Game/ClientState/Structs/JobGauge/BRDGauge.cs +++ b/Dalamud/Game/ClientState/Structs/JobGauge/BRDGauge.cs @@ -8,28 +8,36 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge [StructLayout(LayoutKind.Explicit)] public struct BRDGauge { + [FieldOffset(0)] + private short songTimer; + + [FieldOffset(2)] + private byte numSongStacks; + + [FieldOffset(3)] + private byte soulVoiceValue; + + [FieldOffset(4)] + private CurrentSong activeSong; + /// /// Gets the current song timer in milliseconds. /// - [FieldOffset(0)] - public short SongTimer; + public short SongTimer => this.songTimer; /// /// Gets the number of stacks for the current song. /// - [FieldOffset(2)] - public byte NumSongStacks; + public byte NumSongStacks => this.numSongStacks; /// /// Gets the amount of Soul Voice accumulated. /// - [FieldOffset(3)] - public byte SoulVoiceValue; + public byte SoulVoiceValue => this.soulVoiceValue; /// /// Gets the type of song that is active. /// - [FieldOffset(4)] - public CurrentSong ActiveSong; + public CurrentSong ActiveSong => this.activeSong; } } diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/DNCGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/DNCGauge.cs index 2143aa99f..e2f4f6544 100644 --- a/Dalamud/Game/ClientState/Structs/JobGauge/DNCGauge.cs +++ b/Dalamud/Game/ClientState/Structs/JobGauge/DNCGauge.cs @@ -8,30 +8,37 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge [StructLayout(LayoutKind.Explicit)] public unsafe struct DNCGauge { - /// - /// Gets the number of feathers available. - /// [FieldOffset(0)] - public byte NumFeathers; + private byte numFeathers; - /// - /// Gets the amount of Espirit available. - /// [FieldOffset(1)] - public byte Esprit; + private byte esprit; [FieldOffset(2)] private fixed byte stepOrder[4]; + [FieldOffset(6)] + private byte numCompleteSteps; + + /// + /// Gets the number of feathers available. + /// + public byte NumFeathers => this.numFeathers; + + /// + /// Gets the amount of Espirit available. + /// + public byte Esprit => this.esprit; + /// /// Gets the number of steps completed for the current dance. /// - [FieldOffset(6)] - public byte NumCompleteSteps; + public byte NumCompleteSteps => this.numCompleteSteps; /// /// 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/ClientState/Structs/JobGauge/DRGGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/DRGGauge.cs index 101c483b0..2f4c4ef16 100644 --- a/Dalamud/Game/ClientState/Structs/JobGauge/DRGGauge.cs +++ b/Dalamud/Game/ClientState/Structs/JobGauge/DRGGauge.cs @@ -8,22 +8,28 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge [StructLayout(LayoutKind.Explicit)] public struct DRGGauge { + [FieldOffset(0)] + private short botdTimer; + + [FieldOffset(2)] + private BOTDState botdState; + + [FieldOffset(3)] + private byte eyeCount; + /// /// Gets the time remaining for Blood of the Dragon in milliseconds. /// - [FieldOffset(0)] - public short BOTDTimer; + public short BOTDTimer => this.botdTimer; /// /// Gets the current state of Blood of the Dragon. /// - [FieldOffset(2)] - public BOTDState BOTDState; + public BOTDState BOTDState => this.botdState; /// /// Gets the count of eyes opened during Blood of the Dragon. /// - [FieldOffset(3)] - public byte EyeCount; + public byte EyeCount => this.eyeCount; } } diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/DRKGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/DRKGauge.cs index 9c5306764..97ab73161 100644 --- a/Dalamud/Game/ClientState/Structs/JobGauge/DRKGauge.cs +++ b/Dalamud/Game/ClientState/Structs/JobGauge/DRKGauge.cs @@ -8,26 +8,32 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge [StructLayout(LayoutKind.Explicit)] public struct DRKGauge { - /// - /// Gets the amount of blood accumulated. - /// [FieldOffset(0)] - public byte Blood; + private byte blood; - /// - /// Gets the Darkside time remaining in milliseconds. - /// [FieldOffset(2)] - public ushort DarksideTimeRemaining; + private ushort darksideTimeRemaining; [FieldOffset(4)] private byte darkArtsState; + [FieldOffset(6)] + private ushort shadowTimeRemaining; + + /// + /// Gets the amount of blood accumulated. + /// + public byte Blood => this.blood; + + /// + /// Gets the Darkside time remaining in milliseconds. + /// + public ushort DarksideTimeRemaining => this.darksideTimeRemaining; + /// /// Gets the Shadow time remaining in milliseconds. /// - [FieldOffset(6)] - public ushort ShadowTimeRemaining; + public ushort ShadowTimeRemaining => this.shadowTimeRemaining; /// /// Gets if the player has Dark Arts or not. diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/GNBGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/GNBGauge.cs index c1f7e496b..d42bd9fa1 100644 --- a/Dalamud/Game/ClientState/Structs/JobGauge/GNBGauge.cs +++ b/Dalamud/Game/ClientState/Structs/JobGauge/GNBGauge.cs @@ -8,22 +8,28 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge [StructLayout(LayoutKind.Explicit)] public struct GNBGauge { + [FieldOffset(0)] + private byte numAmmo; + + [FieldOffset(2)] + private short maxTimerDuration; + + [FieldOffset(4)] + private byte ammoComboStepNumber; + /// /// Gets the amount of ammo available. /// - [FieldOffset(0)] - public byte NumAmmo; + public byte NumAmmo => this.numAmmo; /// /// Gets the max combo time of the Gnashing Fang combo. /// - [FieldOffset(2)] - public short MaxTimerDuration; + public short MaxTimerDuration => this.maxTimerDuration; /// /// Gets the current step of the Gnashing Fang combo. /// - [FieldOffset(4)] - public byte AmmoComboStepNumber; + public byte AmmoComboStepNumber => this.ammoComboStepNumber; } } diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/MCHGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/MCHGauge.cs index 18f684c21..1444beac0 100644 --- a/Dalamud/Game/ClientState/Structs/JobGauge/MCHGauge.cs +++ b/Dalamud/Game/ClientState/Structs/JobGauge/MCHGauge.cs @@ -8,38 +8,48 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge [StructLayout(LayoutKind.Explicit)] public struct MCHGauge { + [FieldOffset(0)] + private short overheatTimeRemaining; + + [FieldOffset(2)] + private short robotTimeRemaining; + + [FieldOffset(4)] + private byte heat; + + [FieldOffset(5)] + private byte battery; + + [FieldOffset(6)] + private byte lastRobotBatteryPower; + + [FieldOffset(7)] + private byte timerActive; + /// /// Gets the time time remaining for Overheat in milliseconds. /// - [FieldOffset(0)] - public short OverheatTimeRemaining; + public short OverheatTimeRemaining => this.overheatTimeRemaining; /// /// Gets the time remaining for the Rook or Queen in milliseconds. /// - [FieldOffset(2)] - public short RobotTimeRemaining; + public short RobotTimeRemaining => this.robotTimeRemaining; /// /// Gets the current Heat level. /// - [FieldOffset(4)] - public byte Heat; + public byte Heat => this.heat; /// /// Gets the current Battery level. /// - [FieldOffset(5)] - public byte Battery; + public byte Battery => this.battery; /// /// Gets the battery level of the last Robot. /// - [FieldOffset(6)] - public byte LastRobotBatteryPower; - - [FieldOffset(7)] - private byte timerActive; + public byte LastRobotBatteryPower => this.lastRobotBatteryPower; /// /// Gets if the player is currently Overheated. diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/MNKGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/MNKGauge.cs index 177b077fc..ef1520d9e 100644 --- a/Dalamud/Game/ClientState/Structs/JobGauge/MNKGauge.cs +++ b/Dalamud/Game/ClientState/Structs/JobGauge/MNKGauge.cs @@ -1,4 +1,3 @@ -using System; using System.Runtime.InteropServices; namespace Dalamud.Game.ClientState.Structs.JobGauge @@ -9,35 +8,12 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge [StructLayout(LayoutKind.Explicit)] public struct MNKGauge { + [FieldOffset(0)] + private byte numChakra; + /// /// Gets the number of Chakra available. /// - [FieldOffset(0)] - public byte NumChakra; - - /// - /// Gets the Greased Lightning timer in milliseconds. - /// - [Obsolete("GL has been removed from the game")] - [FieldOffset(0)] - public byte GLTimer; - - /// - /// Gets the amount of Greased Lightning stacks. - /// - [Obsolete("GL has been removed from the game")] - [FieldOffset(2)] - public byte NumGLStacks; - - [Obsolete("GL has been removed from the game")] - [FieldOffset(4)] - private byte glTimerFreezeState; - - /// - /// Gets if the Greased Lightning timer has been frozen. - /// - /// >true or false. - [Obsolete("GL has been removed from the game")] - public bool IsGLTimerFroze() => false; + public byte NumChakra => this.numChakra; } } diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/NINGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/NINGauge.cs index 13eaec09d..f2019caf8 100644 --- a/Dalamud/Game/ClientState/Structs/JobGauge/NINGauge.cs +++ b/Dalamud/Game/ClientState/Structs/JobGauge/NINGauge.cs @@ -9,30 +9,28 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge [StructLayout(LayoutKind.Explicit)] public struct NINGauge { + [FieldOffset(0)] + private int hutonTimeLeft; + + [FieldOffset(4)] + private byte ninki; + + [FieldOffset(5)] + private byte numHutonManualCasts; + /// /// Gets the time left on Huton in milliseconds. /// - // TODO: Probably a short, confirm. - [FieldOffset(0)] - public int HutonTimeLeft; + public int HutonTimeLeft => this.hutonTimeLeft; /// /// Gets the amount of Ninki available. /// - [FieldOffset(4)] - public byte Ninki; - - /// - /// Obsolete. - /// - [Obsolete("Does not appear to be used")] - [FieldOffset(4)] - public byte TCJMudrasUsed; + public byte Ninki => this.ninki; /// /// Gets the number of times Huton has been cast manually. /// - [FieldOffset(5)] - public byte NumHutonManualCasts; + public byte NumHutonManualCasts => this.numHutonManualCasts; } } diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/PLDGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/PLDGauge.cs index d3eae81f3..545ae695a 100644 --- a/Dalamud/Game/ClientState/Structs/JobGauge/PLDGauge.cs +++ b/Dalamud/Game/ClientState/Structs/JobGauge/PLDGauge.cs @@ -8,10 +8,12 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge [StructLayout(LayoutKind.Explicit)] public struct PLDGauge { + [FieldOffset(0)] + private byte gaugeAmount; + /// /// Gets the current level of the Oath gauge. /// - [FieldOffset(0)] - public byte GaugeAmount; + public byte GaugeAmount => this.gaugeAmount; } } diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/RDMGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/RDMGauge.cs index f72d61d13..8d0ec38e1 100644 --- a/Dalamud/Game/ClientState/Structs/JobGauge/RDMGauge.cs +++ b/Dalamud/Game/ClientState/Structs/JobGauge/RDMGauge.cs @@ -8,16 +8,20 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge [StructLayout(LayoutKind.Explicit)] public struct RDMGauge { + [FieldOffset(0)] + private byte whiteGauge; + + [FieldOffset(1)] + private byte blackGauge; + /// /// Gets the level of the White gauge. /// - [FieldOffset(0)] - public byte WhiteGauge; + public byte WhiteGauge => this.whiteGauge; /// /// Gets the level of the Black gauge. /// - [FieldOffset(1)] - public byte BlackGauge; + public byte BlackGauge => this.blackGauge; } } diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/SAMGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/SAMGauge.cs index d0796e5c9..139a93265 100644 --- a/Dalamud/Game/ClientState/Structs/JobGauge/SAMGauge.cs +++ b/Dalamud/Game/ClientState/Structs/JobGauge/SAMGauge.cs @@ -8,23 +8,29 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge [StructLayout(LayoutKind.Explicit)] public struct SAMGauge { + [FieldOffset(3)] + private byte kenki; + + [FieldOffset(4)] + private byte meditationStacks; + + [FieldOffset(5)] + private Sen sen; + /// /// Gets the current amount of Kenki available. /// - [FieldOffset(3)] - public byte Kenki; + public byte Kenki => this.kenki; /// /// Gets the amount of Meditation stacks. /// - [FieldOffset(4)] - public byte MeditationStacks; + public byte MeditationStacks => this.meditationStacks; /// /// Gets the active Sen. /// - [FieldOffset(5)] - public Sen Sen; + public Sen Sen => this.sen; /// /// Gets if the Setsu Sen is active. diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/SCHGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/SCHGauge.cs index 5dc99cf15..66347b62e 100644 --- a/Dalamud/Game/ClientState/Structs/JobGauge/SCHGauge.cs +++ b/Dalamud/Game/ClientState/Structs/JobGauge/SCHGauge.cs @@ -8,28 +8,36 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge [StructLayout(LayoutKind.Explicit)] public struct SCHGauge { + [FieldOffset(2)] + private byte numAetherflowStacks; + + [FieldOffset(3)] + private byte fairyGaugeAmount; + + [FieldOffset(4)] + private short seraphTimer; + + [FieldOffset(6)] + private DismissedFairy dismissedFairy; + /// /// Gets the amount of Aetherflow stacks available. /// - [FieldOffset(2)] - public byte NumAetherflowStacks; + public byte NumAetherflowStacks => this.numAetherflowStacks; /// /// Gets the current level of the Fairy Gauge. /// - [FieldOffset(3)] - public byte FairyGaugeAmount; + public byte FairyGaugeAmount => this.fairyGaugeAmount; /// /// Gets the Seraph time remaining in milliseconds. /// - [FieldOffset(4)] - public short SeraphTimer; + public short SeraphTimer => this.seraphTimer; /// /// Gets the last dismissed fairy. /// - [FieldOffset(6)] - public DismissedFairy DismissedFairy; + public DismissedFairy DismissedFairy => this.dismissedFairy; } } diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/SMNGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/SMNGauge.cs index a5e9cc219..92cbcc19c 100644 --- a/Dalamud/Game/ClientState/Structs/JobGauge/SMNGauge.cs +++ b/Dalamud/Game/ClientState/Structs/JobGauge/SMNGauge.cs @@ -8,30 +8,38 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge [StructLayout(LayoutKind.Explicit)] public struct SMNGauge { + [FieldOffset(0)] + private short timerRemaining; + + [FieldOffset(2)] + private SummonPet returnSummon; + + [FieldOffset(3)] + private PetGlam returnSummonGlam; + + [FieldOffset(4)] + private byte numStacks; + /// /// Gets the time remaining for the current summon. /// - [FieldOffset(0)] - public short TimerRemaining; + public short TimerRemaining => this.timerRemaining; /// /// Gets the summon that will return after the current summon expires. /// - [FieldOffset(2)] - public SummonPet ReturnSummon; + public SummonPet ReturnSummon => this.returnSummon; /// /// Gets the summon glam for the . /// - [FieldOffset(3)] - public PetGlam ReturnSummonGlam; + public PetGlam ReturnSummonGlam => this.returnSummonGlam; /// /// Gets the current stacks. /// Use the summon accessors instead. /// - [FieldOffset(4)] - public byte NumStacks; + public byte NumStacks => this.numStacks; /// /// Gets if Phoenix is ready to be summoned. diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/WARGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/WARGauge.cs index 7747b9cea..0d070d68e 100644 --- a/Dalamud/Game/ClientState/Structs/JobGauge/WARGauge.cs +++ b/Dalamud/Game/ClientState/Structs/JobGauge/WARGauge.cs @@ -8,10 +8,12 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge [StructLayout(LayoutKind.Explicit)] public struct WARGauge { + [FieldOffset(0)] + private byte beastGaugeAmount; + /// /// Gets the amount of wrath in the Beast gauge. /// - [FieldOffset(0)] - public byte BeastGaugeAmount; + public byte BeastGaugeAmount => this.beastGaugeAmount; } } diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/WHMGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/WHMGauge.cs index 0ea51470e..496a39831 100644 --- a/Dalamud/Game/ClientState/Structs/JobGauge/WHMGauge.cs +++ b/Dalamud/Game/ClientState/Structs/JobGauge/WHMGauge.cs @@ -8,22 +8,28 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge [StructLayout(LayoutKind.Explicit)] public struct WHMGauge { + [FieldOffset(2)] + private short lilyTimer; + + [FieldOffset(4)] + private byte numLilies; + + [FieldOffset(5)] + private byte numBloodLily; + /// /// Gets the time to next lily in milliseconds. /// - [FieldOffset(2)] - public short LilyTimer; + public short LilyTimer => this.lilyTimer; /// /// Gets the number of Lilies. /// - [FieldOffset(4)] - public byte NumLilies; + public byte NumLilies => this.numLilies; /// /// Gets the number of times the blood lily has been nourished. /// - [FieldOffset(5)] - public byte NumBloodLily; + public byte NumBloodLily => this.numBloodLily; } } diff --git a/Dalamud/Game/ClientState/Structs/PartyMember.cs b/Dalamud/Game/ClientState/Structs/PartyMember.cs index 6acff52e7..926730e54 100644 --- a/Dalamud/Game/ClientState/Structs/PartyMember.cs +++ b/Dalamud/Game/ClientState/Structs/PartyMember.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.InteropServices; -using Dalamud.Game.ClientState.Actors; +using Dalamud.Game.ClientState.Actors.Types; namespace Dalamud.Game.ClientState.Structs { 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 04b107458..57c7fdeb4 100644 --- a/Dalamud/Game/Internal/AntiDebug.cs +++ b/Dalamud/Game/Internal/AntiDebug.cs @@ -29,13 +29,13 @@ 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}"); } /// /// Gets a value indicating whether the anti-debugging is enabled. /// - public bool IsEnabled { get; private set; } + public bool IsEnabled { get; private set; } = false; /// /// Enables the anti-debugging by overwriting code in memory. @@ -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..09aa234eb 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); @@ -184,7 +184,7 @@ namespace Dalamud.Game.Internal.Gui Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message); this.PrintChat(new XivChatEntry { - MessageBytes = Encoding.UTF8.GetBytes(message), + Message = message, Type = this.dalamud.Configuration.GeneralChatType, }); } @@ -199,7 +199,7 @@ namespace Dalamud.Game.Internal.Gui Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue); this.PrintChat(new XivChatEntry { - MessageBytes = message.Encode(), + Message = message, Type = this.dalamud.Configuration.GeneralChatType, }); } @@ -214,7 +214,7 @@ namespace Dalamud.Game.Internal.Gui Log.Verbose("[CHATGUI PRINT REGULAR ERROR]{0}", message); this.PrintChat(new XivChatEntry { - MessageBytes = Encoding.UTF8.GetBytes(message), + Message = message, Type = XivChatType.Urgent, }); } @@ -229,7 +229,7 @@ namespace Dalamud.Game.Internal.Gui Log.Verbose("[CHATGUI PRINT SESTRING ERROR]{0}", message.TextValue); this.PrintChat(new XivChatEntry { - MessageBytes = message.Encode(), + Message = message, Type = XivChatType.Urgent, }); } @@ -249,10 +249,10 @@ namespace Dalamud.Game.Internal.Gui continue; } - var senderRaw = Encoding.UTF8.GetBytes(chat.Name ?? string.Empty); + var senderRaw = (chat.Name ?? string.Empty).Encode(); using var senderOwned = framework.Libc.NewString(senderRaw); - var messageRaw = chat.MessageBytes ?? new byte[0]; + var messageRaw = (chat.Message ?? string.Empty).Encode(); 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 945627b83..fe54249df 100644 --- a/Dalamud/Game/Network/NetworkHandlers.cs +++ b/Dalamud/Game/Network/NetworkHandlers.cs @@ -96,11 +96,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/ClientState/Actors/Position3.cs b/Dalamud/Game/Position3.cs similarity index 96% rename from Dalamud/Game/ClientState/Actors/Position3.cs rename to Dalamud/Game/Position3.cs index df8b0c471..3812376df 100644 --- a/Dalamud/Game/ClientState/Actors/Position3.cs +++ b/Dalamud/Game/Position3.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Dalamud.Game.ClientState.Actors +namespace Dalamud.Game { /// /// A game native equivalent of a Vector3. 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/Payload.cs b/Dalamud/Game/Text/SeStringHandling/Payload.cs index 5d0132815..ebf689646 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payload.cs @@ -131,6 +131,10 @@ namespace Dalamud.Game.Text.SeStringHandling payload = new EmphasisItalicPayload(); break; + case SeStringChunkType.NewLine: + payload = NewLinePayload.Payload; + break; + case SeStringChunkType.SeHyphen: payload = SeHyphenPayload.Payload; break; @@ -295,6 +299,11 @@ namespace Dalamud.Game.Text.SeStringHandling /// EmphasisItalic = 0x1A, + /// + /// See the . + /// + NewLine = 0x10, + /// /// See the class. /// diff --git a/Dalamud/Game/Text/SeStringHandling/PayloadType.cs b/Dalamud/Game/Text/SeStringHandling/PayloadType.cs index 8e6c5ffbd..93bcc7e3e 100644 --- a/Dalamud/Game/Text/SeStringHandling/PayloadType.cs +++ b/Dalamud/Game/Text/SeStringHandling/PayloadType.cs @@ -5,6 +5,11 @@ namespace Dalamud.Game.Text.SeStringHandling /// public enum PayloadType { + /// + /// An unknown SeString. + /// + Unknown, + /// /// An SeString payload representing a player link. /// @@ -66,9 +71,9 @@ namespace Dalamud.Game.Text.SeStringHandling DalamudLink, /// - /// An SeString payload representing any data we don't handle. + /// An SeString payload representing a newline character. /// - Unknown, + NewLine, /// /// An SeString payload representing a doublewide SE hypen. diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/NewLinePayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/NewLinePayload.cs new file mode 100644 index 000000000..13aba8077 --- /dev/null +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/NewLinePayload.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; + +namespace Dalamud.Game.Text.SeStringHandling.Payloads +{ + /// + /// A wrapped newline character. + /// + public class NewLinePayload : Payload, ITextProvider + { + private readonly byte[] bytes = { START_BYTE, (byte)SeStringChunkType.NewLine, 0x01, END_BYTE }; + + /// + /// Gets an instance of NewLinePayload. + /// + public static NewLinePayload Payload => new(); + + /// + /// Gets the text of this payload, evaluates to Environment.NewLine. + /// + public string Text => Environment.NewLine; + + /// + public override PayloadType Type => PayloadType.NewLine; + + /// + protected override byte[] EncodeImpl() => this.bytes; + + /// + protected override void DecodeImpl(BinaryReader reader, long endOfStream) + { + } + } +} diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index 43ae7600e..0e8fe1ff3 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -13,6 +13,15 @@ namespace Dalamud.Game.Text.SeStringHandling /// public class SeString { + /// + /// Initializes a new instance of the class. + /// Creates a new SeString from an ordered list of payloads. + /// + public SeString() + { + this.Payloads = new List(); + } + /// /// Initializes a new instance of the class. /// Creates a new SeString from an ordered list of payloads. @@ -29,7 +38,7 @@ namespace Dalamud.Game.Text.SeStringHandling /// Creates a new SeString from an ordered list of payloads. /// /// The Payload objects to make up this string. - public SeString(Payload[] payloads) + public SeString(params Payload[] payloads) { this.Payloads = new List(payloads); } @@ -61,7 +70,7 @@ namespace Dalamud.Game.Text.SeStringHandling /// /// string to convert. /// Equivalent SeString. - public static implicit operator SeString(string str) => new(new Payload[] { new TextPayload(str) }); + public static implicit operator SeString(string str) => new(new TextPayload(str)); /// /// Creates a SeString from a json. (For testing - not recommended for production use.) 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/Game/Text/XivChatEntry.cs b/Dalamud/Game/Text/XivChatEntry.cs index dc8005f6b..a5a11766e 100644 --- a/Dalamud/Game/Text/XivChatEntry.cs +++ b/Dalamud/Game/Text/XivChatEntry.cs @@ -1,5 +1,7 @@ using System; +using Dalamud.Game.Text.SeStringHandling; + namespace Dalamud.Game.Text { /// @@ -20,12 +22,12 @@ namespace Dalamud.Game.Text /// /// Gets or sets the sender name. /// - public string Name { get; set; } = string.Empty; + public SeString Name { get; set; } = string.Empty; /// - /// Gets or sets the message bytes. + /// Gets or sets the message. /// - public byte[] MessageBytes { get; set; } + public SeString Message { get; set; } = string.Empty; /// /// Gets or sets the message parameters. diff --git a/Dalamud/GlobalSuppressions.cs b/Dalamud/GlobalSuppressions.cs index e4f2f5e86..15bce83e0 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")] @@ -48,10 +37,8 @@ using System.Diagnostics.CodeAnalysis; // Offsets.cs [assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204:Static elements should appear before instance elements", Justification = "Offset classes goto the end of file.", Scope = "type", Target = "~T:Dalamud.Game.ClientState.Actors.TargetOffsets")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group offset classes with the relevant class.", Scope = "type", Target = "~T:Dalamud.Game.ClientState.Actors.TargetOffsets")] -[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group offset classes with the relevant class.", Scope = "type", Target = "~T:Dalamud.Game.ClientState.Structs.ActorOffsets")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group offset classes with the relevant class.", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.AddonOffsets")] [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the offset usage instead.", Scope = "type", Target = "~T:Dalamud.Game.ClientState.Actors.TargetOffsets")] -[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the offset usage instead.", Scope = "type", Target = "~T:Dalamud.Game.ClientState.Structs.ActorOffsets")] [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the offset usage instead.", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.AddonOffsets")] // Breaking api changes: these should be split into a PartyFinder subdirectory @@ -68,39 +55,21 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "breaking api change", Scope = "member", Target = "~E:Dalamud.Game.Internal.Gui.PartyFinderGui.ReceiveListing")] // Breaking api changes -[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.Actors.Resolvers.ClassJob.Id")] -[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.Actors.Resolvers.World.Id")] -[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.Actors.Types.Actor.Address")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.BaseAddressResolver.DebugScannedValues")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Gui.Addon.Addon.Address")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Gui.Addon.Addon.addonStruct")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Gui.GameGui.GetBaseUIObject")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Text.SeStringHandling.Payload.DataResolver")] -[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.Actors")] -[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.TerritoryType")] -[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.TerritoryChanged")] -[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.ClientLanguage")] -[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.JobGauges")] -[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.PartyList")] -[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.KeyState")] -[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")] -[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.DRKGauge.ShadowTimeRemaining")] [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.START_BYTE")] [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/Animation/AnimUtil.cs b/Dalamud/Interface/Animation/AnimUtil.cs new file mode 100644 index 000000000..cff36b21c --- /dev/null +++ b/Dalamud/Interface/Animation/AnimUtil.cs @@ -0,0 +1,36 @@ +using System.Numerics; + +namespace Dalamud.Interface.Animation +{ + /// + /// Class providing helper functions when facilitating animations. + /// + public static class AnimUtil + { + /// + /// Lerp between two floats. + /// + /// The first float. + /// The second float. + /// The point to lerp to. + /// The lerped value. + public static float Lerp(float firstFloat, float secondFloat, float by) + { + return (firstFloat * (1 - @by)) + (secondFloat * by); + } + + /// + /// Lerp between two vectors. + /// + /// The first vector. + /// The second float. + /// The point to lerp to. + /// The lerped vector. + public static Vector2 Lerp(Vector2 firstVector, Vector2 secondVector, float by) + { + var retX = Lerp(firstVector.X, secondVector.X, by); + var retY = Lerp(firstVector.Y, secondVector.Y, by); + return new Vector2(retX, retY); + } + } +} diff --git a/Dalamud/Interface/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs new file mode 100644 index 000000000..441108e6f --- /dev/null +++ b/Dalamud/Interface/Animation/Easing.cs @@ -0,0 +1,105 @@ +using System; +using System.Diagnostics; +using System.Numerics; + +namespace Dalamud.Interface.Animation +{ + /// + /// Base class facilitating the implementation of easing functions. + /// + public abstract class Easing + { + // TODO: Use game delta time here instead + private readonly Stopwatch animationTimer = new(); + + private double valueInternal; + + /// + /// Initializes a new instance of the class with the specified duration. + /// + /// The animation duration. + protected Easing(TimeSpan duration) + { + this.Duration = duration; + } + + /// + /// Gets or sets the origin point of the animation. + /// + public Vector2? Point1 { get; set; } + + /// + /// Gets or sets the destination point of the animation. + /// + public Vector2? Point2 { get; set; } + + /// + /// Gets the resulting point at the current timestep. + /// + public Vector2 EasedPoint { get; private set; } + + /// + /// Gets or sets the current value of the animation, from 0 to 1. + /// + public double Value + { + get => this.valueInternal; + protected set + { + this.valueInternal = value; + + if (this.Point1.HasValue && this.Point2.HasValue) + this.EasedPoint = AnimUtil.Lerp(this.Point1.Value, this.Point2.Value, (float)this.valueInternal); + } + } + + /// + /// Gets or sets the duration of the animation. + /// + public TimeSpan Duration { get; set; } + + /// + /// Gets a value indicating whether or not the animation is running. + /// + public bool IsRunning => this.animationTimer.IsRunning; + + /// + /// Gets a value indicating whether or not the animation is done. + /// + public bool IsDone => this.animationTimer.ElapsedMilliseconds > this.Duration.TotalMilliseconds; + + /// + /// Gets the progress of the animation, from 0 to 1. + /// + protected double Progress => this.animationTimer.ElapsedMilliseconds / this.Duration.TotalMilliseconds; + + /// + /// Starts the animation from where it was last stopped, or from the start if it was never started before. + /// + public void Start() + { + this.animationTimer.Start(); + } + + /// + /// Stops the animation at the current point. + /// + public void Stop() + { + this.animationTimer.Stop(); + } + + /// + /// Restarts the animation. + /// + public void Restart() + { + this.animationTimer.Restart(); + } + + /// + /// Updates the animation. + /// + public abstract void Update(); + } +} diff --git a/Dalamud/Interface/Animation/EasingFunctions/InCirc.cs b/Dalamud/Interface/Animation/EasingFunctions/InCirc.cs new file mode 100644 index 000000000..952e3539d --- /dev/null +++ b/Dalamud/Interface/Animation/EasingFunctions/InCirc.cs @@ -0,0 +1,27 @@ +using System; + +namespace Dalamud.Interface.Animation.EasingFunctions +{ + /// + /// Class providing an "InCirc" easing animation. + /// + public class InCirc : Easing + { + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public InCirc(TimeSpan duration) + : base(duration) + { + // ignored + } + + /// + public override void Update() + { + var p = this.Progress; + this.Value = 1 - Math.Sqrt(1 - Math.Pow(p, 2)); + } + } +} diff --git a/Dalamud/Interface/Animation/EasingFunctions/InCubic.cs b/Dalamud/Interface/Animation/EasingFunctions/InCubic.cs new file mode 100644 index 000000000..ebca3203d --- /dev/null +++ b/Dalamud/Interface/Animation/EasingFunctions/InCubic.cs @@ -0,0 +1,27 @@ +using System; + +namespace Dalamud.Interface.Animation.EasingFunctions +{ + /// + /// Class providing an "InCubic" easing animation. + /// + public class InCubic : Easing + { + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public InCubic(TimeSpan duration) + : base(duration) + { + // ignored + } + + /// + public override void Update() + { + var p = this.Progress; + this.Value = p * p * p; + } + } +} diff --git a/Dalamud/Interface/Animation/EasingFunctions/InElastic.cs b/Dalamud/Interface/Animation/EasingFunctions/InElastic.cs new file mode 100644 index 000000000..97159df47 --- /dev/null +++ b/Dalamud/Interface/Animation/EasingFunctions/InElastic.cs @@ -0,0 +1,33 @@ +using System; + +namespace Dalamud.Interface.Animation.EasingFunctions +{ + /// + /// Class providing an "InElastic" easing animation. + /// + public class InElastic : Easing + { + private const double Constant = (2 * Math.PI) / 3; + + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public InElastic(TimeSpan duration) + : base(duration) + { + // ignored + } + + /// + public override void Update() + { + var p = this.Progress; + this.Value = p == 0 + ? 0 + : p == 1 + ? 1 + : -Math.Pow(2, (10 * p) - 10) * Math.Sin(((p * 10) - 10.75) * Constant); + } + } +} diff --git a/Dalamud/Interface/Animation/EasingFunctions/InOutCirc.cs b/Dalamud/Interface/Animation/EasingFunctions/InOutCirc.cs new file mode 100644 index 000000000..97fd1a2d2 --- /dev/null +++ b/Dalamud/Interface/Animation/EasingFunctions/InOutCirc.cs @@ -0,0 +1,29 @@ +using System; + +namespace Dalamud.Interface.Animation.EasingFunctions +{ + /// + /// Class providing an "InOutCirc" easing animation. + /// + public class InOutCirc : Easing + { + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public InOutCirc(TimeSpan duration) + : base(duration) + { + // ignored + } + + /// + public override void Update() + { + var p = this.Progress; + this.Value = p < 0.5 + ? (1 - Math.Sqrt(1 - Math.Pow(2 * p, 2))) / 2 + : (Math.Sqrt(1 - Math.Pow((-2 * p) + 2, 2)) + 1) / 2; + } + } +} diff --git a/Dalamud/Interface/Animation/EasingFunctions/InOutCubic.cs b/Dalamud/Interface/Animation/EasingFunctions/InOutCubic.cs new file mode 100644 index 000000000..c075da538 --- /dev/null +++ b/Dalamud/Interface/Animation/EasingFunctions/InOutCubic.cs @@ -0,0 +1,27 @@ +using System; + +namespace Dalamud.Interface.Animation.EasingFunctions +{ + /// + /// Class providing an "InOutCubic" easing animation. + /// + public class InOutCubic : Easing + { + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public InOutCubic(TimeSpan duration) + : base(duration) + { + // ignored + } + + /// + public override void Update() + { + var p = this.Progress; + this.Value = p < 0.5 ? 4 * p * p * p : 1 - (Math.Pow((-2 * p) + 2, 3) / 2); + } + } +} diff --git a/Dalamud/Interface/Animation/EasingFunctions/InOutElastic.cs b/Dalamud/Interface/Animation/EasingFunctions/InOutElastic.cs new file mode 100644 index 000000000..7bb36dd74 --- /dev/null +++ b/Dalamud/Interface/Animation/EasingFunctions/InOutElastic.cs @@ -0,0 +1,35 @@ +using System; + +namespace Dalamud.Interface.Animation.EasingFunctions +{ + /// + /// Class providing an "InOutCirc" easing animation. + /// + public class InOutElastic : Easing + { + private const double Constant = (2 * Math.PI) / 4.5; + + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public InOutElastic(TimeSpan duration) + : base(duration) + { + // ignored + } + + /// + public override void Update() + { + var p = this.Progress; + this.Value = p == 0 + ? 0 + : p == 1 + ? 1 + : p < 0.5 + ? -(Math.Pow(2, (20 * p) - 10) * Math.Sin(((20 * p) - 11.125) * Constant)) / 2 + : (Math.Pow(2, (-20 * p) + 10) * Math.Sin(((20 * p) - 11.125) * Constant) / 2) + 1; + } + } +} diff --git a/Dalamud/Interface/Animation/EasingFunctions/InOutQuint.cs b/Dalamud/Interface/Animation/EasingFunctions/InOutQuint.cs new file mode 100644 index 000000000..70f3123aa --- /dev/null +++ b/Dalamud/Interface/Animation/EasingFunctions/InOutQuint.cs @@ -0,0 +1,27 @@ +using System; + +namespace Dalamud.Interface.Animation.EasingFunctions +{ + /// + /// Class providing an "InOutQuint" easing animation. + /// + public class InOutQuint : Easing + { + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public InOutQuint(TimeSpan duration) + : base(duration) + { + // ignored + } + + /// + public override void Update() + { + var p = this.Progress; + this.Value = p < 0.5 ? 16 * p * p * p * p * p : 1 - (Math.Pow((-2 * p) + 2, 5) / 2); + } + } +} diff --git a/Dalamud/Interface/Animation/EasingFunctions/InOutSine.cs b/Dalamud/Interface/Animation/EasingFunctions/InOutSine.cs new file mode 100644 index 000000000..4808079d3 --- /dev/null +++ b/Dalamud/Interface/Animation/EasingFunctions/InOutSine.cs @@ -0,0 +1,27 @@ +using System; + +namespace Dalamud.Interface.Animation.EasingFunctions +{ + /// + /// Class providing an "InOutSine" easing animation. + /// + public class InOutSine : Easing + { + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public InOutSine(TimeSpan duration) + : base(duration) + { + // ignored + } + + /// + public override void Update() + { + var p = this.Progress; + this.Value = -(Math.Cos(Math.PI * p) - 1) / 2; + } + } +} diff --git a/Dalamud/Interface/Animation/EasingFunctions/InQuint.cs b/Dalamud/Interface/Animation/EasingFunctions/InQuint.cs new file mode 100644 index 000000000..42604f136 --- /dev/null +++ b/Dalamud/Interface/Animation/EasingFunctions/InQuint.cs @@ -0,0 +1,27 @@ +using System; + +namespace Dalamud.Interface.Animation.EasingFunctions +{ + /// + /// Class providing an "InQuint" easing animation. + /// + public class InQuint : Easing + { + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public InQuint(TimeSpan duration) + : base(duration) + { + // ignored + } + + /// + public override void Update() + { + var p = this.Progress; + this.Value = p * p * p * p * p; + } + } +} diff --git a/Dalamud/Interface/Animation/EasingFunctions/InSine.cs b/Dalamud/Interface/Animation/EasingFunctions/InSine.cs new file mode 100644 index 000000000..aaa19aa40 --- /dev/null +++ b/Dalamud/Interface/Animation/EasingFunctions/InSine.cs @@ -0,0 +1,27 @@ +using System; + +namespace Dalamud.Interface.Animation.EasingFunctions +{ + /// + /// Class providing an "InSine" easing animation. + /// + public class InSine : Easing + { + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public InSine(TimeSpan duration) + : base(duration) + { + // ignored + } + + /// + public override void Update() + { + var p = this.Progress; + this.Value = 1 - Math.Cos((p * Math.PI) / 2); + } + } +} diff --git a/Dalamud/Interface/Animation/EasingFunctions/OutCirc.cs b/Dalamud/Interface/Animation/EasingFunctions/OutCirc.cs new file mode 100644 index 000000000..da7b0029a --- /dev/null +++ b/Dalamud/Interface/Animation/EasingFunctions/OutCirc.cs @@ -0,0 +1,27 @@ +using System; + +namespace Dalamud.Interface.Animation.EasingFunctions +{ + /// + /// Class providing an "OutCirc" easing animation. + /// + public class OutCirc : Easing + { + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public OutCirc(TimeSpan duration) + : base(duration) + { + // ignored + } + + /// + public override void Update() + { + var p = this.Progress; + this.Value = Math.Sqrt(1 - Math.Pow(p - 1, 2)); + } + } +} diff --git a/Dalamud/Interface/Animation/EasingFunctions/OutCubic.cs b/Dalamud/Interface/Animation/EasingFunctions/OutCubic.cs new file mode 100644 index 000000000..e527b228f --- /dev/null +++ b/Dalamud/Interface/Animation/EasingFunctions/OutCubic.cs @@ -0,0 +1,27 @@ +using System; + +namespace Dalamud.Interface.Animation.EasingFunctions +{ + /// + /// Class providing an "OutCubic" easing animation. + /// + public class OutCubic : Easing + { + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public OutCubic(TimeSpan duration) + : base(duration) + { + // ignored + } + + /// + public override void Update() + { + var p = this.Progress; + this.Value = 1 - Math.Pow(1 - p, 3); + } + } +} diff --git a/Dalamud/Interface/Animation/EasingFunctions/OutElastic.cs b/Dalamud/Interface/Animation/EasingFunctions/OutElastic.cs new file mode 100644 index 000000000..3475c4a72 --- /dev/null +++ b/Dalamud/Interface/Animation/EasingFunctions/OutElastic.cs @@ -0,0 +1,33 @@ +using System; + +namespace Dalamud.Interface.Animation.EasingFunctions +{ + /// + /// Class providing an "OutElastic" easing animation. + /// + public class OutElastic : Easing + { + private const double Constant = (2 * Math.PI) / 3; + + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public OutElastic(TimeSpan duration) + : base(duration) + { + // ignored + } + + /// + public override void Update() + { + var p = this.Progress; + this.Value = p == 0 + ? 0 + : p == 1 + ? 1 + : (Math.Pow(2, -10 * p) * Math.Sin(((p * 10) - 0.75) * Constant)) + 1; + } + } +} diff --git a/Dalamud/Interface/Animation/EasingFunctions/OutQuint.cs b/Dalamud/Interface/Animation/EasingFunctions/OutQuint.cs new file mode 100644 index 000000000..c99c77a57 --- /dev/null +++ b/Dalamud/Interface/Animation/EasingFunctions/OutQuint.cs @@ -0,0 +1,27 @@ +using System; + +namespace Dalamud.Interface.Animation.EasingFunctions +{ + /// + /// Class providing an "OutQuint" easing animation. + /// + public class OutQuint : Easing + { + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public OutQuint(TimeSpan duration) + : base(duration) + { + // ignored + } + + /// + public override void Update() + { + var p = this.Progress; + this.Value = 1 - Math.Pow(1 - p, 5); + } + } +} diff --git a/Dalamud/Interface/Animation/EasingFunctions/OutSine.cs b/Dalamud/Interface/Animation/EasingFunctions/OutSine.cs new file mode 100644 index 000000000..c1becf81c --- /dev/null +++ b/Dalamud/Interface/Animation/EasingFunctions/OutSine.cs @@ -0,0 +1,27 @@ +using System; + +namespace Dalamud.Interface.Animation.EasingFunctions +{ + /// + /// Class providing an "OutSine" easing animation. + /// + public class OutSine : Easing + { + /// + /// Initializes a new instance of the class. + /// + /// The duration of the animation. + public OutSine(TimeSpan duration) + : base(duration) + { + // ignored + } + + /// + public override void Update() + { + var p = this.Progress; + this.Value = Math.Sin((p * Math.PI) / 2); + } + } +} 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/ComponentDemoWindow.cs b/Dalamud/Interface/Components/ComponentDemoWindow.cs deleted file mode 100644 index bb62fba66..000000000 --- a/Dalamud/Interface/Components/ComponentDemoWindow.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Numerics; - -using Dalamud.Interface.Colors; -using Dalamud.Interface.Windowing; -using ImGuiNET; - -namespace Dalamud.Interface.Components -{ - /// - /// Component Demo Window to view custom ImGui components. - /// - internal class ComponentDemoWindow : Window - { - private readonly List> componentDemos; - private Vector4 defaultColor = ImGuiColors.DalamudOrange; - - /// - /// Initializes a new instance of the class. - /// - public ComponentDemoWindow() - : base("Dalamud Components Demo") - { - this.Size = new Vector2(600, 500); - this.SizeCondition = ImGuiCond.FirstUseEver; - this.componentDemos = new List> - { - Demo("Test", ImGuiComponents.Test), - Demo("HelpMarker", HelpMarkerDemo), - Demo("IconButton", IconButtonDemo), - Demo("TextWithLabel", TextWithLabelDemo), - Demo("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}")) - { - componentDemo.Value(); - } - } - - ImGui.EndChild(); - } - - private static void HelpMarkerDemo() - { - ImGui.Text("Hover over the icon to learn more."); - ImGuiComponents.HelpMarker("help me!"); - } - - private static void IconButtonDemo() - { - ImGui.Text("Click on the icon to use as a button."); - ImGui.SameLine(); - if (ImGuiComponents.IconButton(1, FontAwesomeIcon.Carrot)) - { - ImGui.OpenPopup("IconButtonDemoPopup"); - } - - if (ImGui.BeginPopup("IconButtonDemoPopup")) - { - ImGui.Text("You clicked!"); - ImGui.EndPopup(); - } - } - - private static void TextWithLabelDemo() - { - 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."); - ImGui.SameLine(); - this.defaultColor = ImGuiComponents.ColorPickerWithPalette(1, "ColorPickerWithPalette Demo", this.defaultColor); - } - } -} 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/DalamudDataWindow.cs b/Dalamud/Interface/DalamudDataWindow.cs deleted file mode 100644 index e34e68dae..000000000 --- a/Dalamud/Interface/DalamudDataWindow.cs +++ /dev/null @@ -1,716 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Dynamic; -using System.Linq; -using System.Numerics; - -using Dalamud.Game.ClientState; -using Dalamud.Game.ClientState.Actors.Types; -using Dalamud.Game.ClientState.Actors.Types.NonPlayer; -using Dalamud.Game.ClientState.Structs.JobGauge; -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; -using ImGuiScene; -using Newtonsoft.Json; -using Serilog; - -namespace Dalamud.Interface -{ - /// - /// Class responsible for drawing the data/debug window. - /// - internal class DalamudDataWindow : Window - { - private readonly Dalamud dalamud; - - private bool wasReady; - private string serverOpString; - - private int currentKind; - private 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 drawActors = false; - private float maxActorDrawDistance = 20; - - private string inputSig = string.Empty; - private IntPtr sigResult = IntPtr.Zero; - - private string inputAddonName = string.Empty; - private int inputAddonIndex; - private Addon resultAddon; - - private IntPtr findAgentInterfacePtr; - - private bool resolveGameData = false; - - private UIDebug addonInspector = null; - - private string inputTextToast = string.Empty; - private int toastPosition = 0; - private int toastSpeed = 0; - private int questToastPosition = 0; - private bool questToastSound = false; - private int questToastIconId = 0; - private bool questToastCheckmark = false; - - private string inputTexPath = string.Empty; - private TextureWrap debugTex = null; - private Vector2 inputTexUv0 = Vector2.Zero; - private Vector2 inputTexUv1 = Vector2.One; - private Vector4 inputTintCol = Vector4.One; - private Vector2 inputTexScale = Vector2.Zero; - - private uint copyButtonIndex = 0; - - /// - /// Initializes a new instance of the class. - /// - /// The Dalamud instance to access data of. - public DalamudDataWindow(Dalamud dalamud) - : base("Dalamud Data") - { - this.dalamud = dalamud; - - this.Size = new Vector2(500, 500); - this.SizeCondition = ImGuiCond.FirstUseEver; - - this.Load(); - } - - /// - /// Set the DataKind dropdown menu. - /// - /// Data kind name, can be lower and/or without spaces. - public void SetDataKind(string dataKind) - { - if (string.IsNullOrEmpty(dataKind)) - return; - - if (dataKind == "ai") - dataKind = "Addon Inspector"; - - int index; - dataKind = dataKind.Replace(" ", string.Empty).ToLower(); - var dataKinds = this.dataKinds.Select(k => k.Replace(" ", string.Empty).ToLower()).ToList(); - if ((index = dataKinds.IndexOf(dataKind)) != -1) - { - this.currentKind = index; - } - else - { - this.dalamud.Framework.Gui.Chat.PrintError("/xldata: Invalid Data Type"); - } - } - - /// - /// Draw the window via ImGui. - /// - public override void Draw() - { - this.copyButtonIndex = 0; - - // Main window - if (ImGui.Button("Force Reload")) - this.Load(); - ImGui.SameLine(); - var copy = ImGui.Button("Copy all"); - ImGui.SameLine(); - - ImGui.Combo("Data kind", ref this.currentKind, this.dataKinds, this.dataKinds.Length); - - ImGui.Checkbox("Resolve GameData", ref this.resolveGameData); - - ImGui.BeginChild("scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.HorizontalScrollbar); - - if (copy) - ImGui.LogToClipboard(); - - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0)); - - try - { - if (this.wasReady) - { - switch (this.currentKind) - { - case 0: - ImGui.TextUnformatted(this.serverOpString); - break; - case 1: - - ImGui.InputText(".text sig", ref this.inputSig, 400); - if (ImGui.Button("Resolve")) - { - try - { - this.sigResult = this.dalamud.SigScanner.ScanText(this.inputSig); - } - catch (KeyNotFoundException) - { - this.sigResult = new IntPtr(-1); - } - } - - ImGui.Text($"Result: {this.sigResult.ToInt64():X}"); - ImGui.SameLine(); - if (ImGui.Button($"C{this.copyButtonIndex++}")) - ImGui.SetClipboardText(this.sigResult.ToInt64().ToString("x")); - - foreach (var debugScannedValue in BaseAddressResolver.DebugScannedValues) - { - ImGui.TextUnformatted($"{debugScannedValue.Key}"); - foreach (var valueTuple in debugScannedValue.Value) - { - ImGui.TextUnformatted( - $" {valueTuple.Item1} - 0x{valueTuple.Item2.ToInt64():x}"); - ImGui.SameLine(); - - if (ImGui.Button($"C##copyAddress{this.copyButtonIndex++}")) - ImGui.SetClipboardText(valueTuple.Item2.ToInt64().ToString("x")); - } - } - - break; - - // AT - case 2: - this.DrawActorTable(); - - break; - - // Font - case 3: - var specialChars = string.Empty; - for (var i = 0xE020; i <= 0xE0DB; i++) - specialChars += $"0x{i:X} - {(SeIconChar)i} - {(char)i}\n"; - - ImGui.TextUnformatted(specialChars); - - foreach (var fontAwesomeIcon in Enum.GetValues(typeof(FontAwesomeIcon)) - .Cast()) - { - ImGui.Text(((int)fontAwesomeIcon.ToIconChar()).ToString("X") + " - "); - ImGui.SameLine(); - - ImGui.PushFont(InterfaceManager.IconFont); - ImGui.Text(fontAwesomeIcon.ToIconString()); - ImGui.PopFont(); - } - - break; - - // Party - case 4: - var partyString = string.Empty; - - if (this.dalamud.ClientState.PartyList.Length == 0) - { - ImGui.TextUnformatted("Data not ready."); - } - else - { - partyString += $"{this.dalamud.ClientState.PartyList.Count} Members\n"; - for (var i = 0; i < this.dalamud.ClientState.PartyList.Count; i++) - { - var member = this.dalamud.ClientState.PartyList[i]; - if (member == null) - { - partyString += - $"[{i}] was null\n"; - continue; - } - - partyString += - $"[{i}] {member.CharacterName} - {member.ObjectKind} - {member.Actor.ActorId}\n"; - } - - ImGui.TextUnformatted(partyString); - } - - break; - - // Subscriptions - case 5: - this.DrawIpcDebug(); - - break; - - // Condition - case 6: -#if DEBUG - ImGui.Text($"ptr: {this.dalamud.ClientState.Condition.ConditionArrayBase.ToString("X16")}"); -#endif - - ImGui.Text("Current Conditions:"); - ImGui.Separator(); - - var didAny = false; - - for (var i = 0; i < Condition.MaxConditionEntries; i++) - { - var typedCondition = (ConditionFlag)i; - var cond = this.dalamud.ClientState.Condition[typedCondition]; - - if (!cond) continue; - - didAny = true; - - ImGui.Text($"ID: {i} Enum: {typedCondition}"); - } - - if (!didAny) - ImGui.Text("None. Talk to a shop NPC or visit a market board to find out more!!!!!!!"); - - break; - - // Gauge - case 7: - var gauge = this.dalamud.ClientState.JobGauges.Get(); - ImGui.Text($"Moon: {gauge.ContainsSeal(SealType.MOON)} Drawn: {gauge.DrawnCard()}"); - - break; - - // Command - case 8: - foreach (var command in this.dalamud.CommandManager.Commands) - ImGui.Text($"{command.Key}\n -> {command.Value.HelpMessage}\n -> In help: {command.Value.ShowInHelp}\n\n"); - - break; - - // Addon - case 9: - this.DrawAddonDebug(); - break; - - // Addon Inspector - case 10: - this.addonInspector ??= new UIDebug(this.dalamud); - this.addonInspector.Draw(); - break; - - // StartInfo - case 11: - ImGui.Text(JsonConvert.SerializeObject(this.dalamud.StartInfo, Formatting.Indented)); - break; - - // Target - case 12: - this.DrawTargetDebug(); - break; - - // Toast - case 13: - ImGui.InputText("Toast text", ref this.inputTextToast, 200); - - ImGui.Combo("Toast Position", ref this.toastPosition, new[] { "Bottom", "Top", }, 2); - ImGui.Combo("Toast Speed", ref this.toastSpeed, new[] { "Slow", "Fast", }, 2); - ImGui.Combo("Quest Toast Position", ref this.questToastPosition, new[] { "Centre", "Right", "Left" }, 3); - ImGui.Checkbox("Quest Checkmark", ref this.questToastCheckmark); - ImGui.Checkbox("Quest Play Sound", ref this.questToastSound); - ImGui.InputInt("Quest Icon ID", ref this.questToastIconId); - - ImGuiHelpers.ScaledDummy(new Vector2(10, 10)); - - if (ImGui.Button("Show toast")) - { - this.dalamud.Framework.Gui.Toast.ShowNormal(this.inputTextToast, new ToastOptions - { - Position = (ToastPosition)this.toastPosition, - Speed = (ToastSpeed)this.toastSpeed, - }); - } - - if (ImGui.Button("Show Quest toast")) - { - this.dalamud.Framework.Gui.Toast.ShowQuest(this.inputTextToast, new QuestToastOptions - { - Position = (QuestToastPosition)this.questToastPosition, - DisplayCheckmark = this.questToastCheckmark, - IconId = (uint)this.questToastIconId, - PlaySound = this.questToastSound, - }); - } - - if (ImGui.Button("Show Error toast")) - { - this.dalamud.Framework.Gui.Toast.ShowError(this.inputTextToast); - } - - break; - - // ImGui - case 14: - ImGui.Text("Monitor count: " + ImGui.GetPlatformIO().Monitors.Size); - ImGui.Text("OverrideGameCursor: " + this.dalamud.InterfaceManager.OverrideGameCursor); - - ImGui.Button("THIS IS A BUTTON###hoverTestButton"); - this.dalamud.InterfaceManager.OverrideGameCursor = !ImGui.IsItemHovered(); - - break; - - // Tex - case 15: - ImGui.InputText("Tex Path", ref this.inputTexPath, 255); - ImGui.InputFloat2("UV0", ref this.inputTexUv0); - ImGui.InputFloat2("UV1", ref this.inputTexUv1); - ImGui.InputFloat4("Tint", ref this.inputTintCol); - ImGui.InputFloat2("Scale", ref this.inputTexScale); - - if (ImGui.Button("Load Tex")) - { - try - { - this.debugTex = this.dalamud.Data.GetImGuiTexture(this.inputTexPath); - this.inputTexScale = new Vector2(this.debugTex.Width, this.debugTex.Height); - } - catch (Exception ex) - { - Log.Error(ex, "Could not load tex."); - } - } - - ImGuiHelpers.ScaledDummy(10); - - if (this.debugTex != null) - { - ImGui.Image(this.debugTex.ImGuiHandle, this.inputTexScale, this.inputTexUv0, this.inputTexUv1, this.inputTintCol); - ImGuiHelpers.ScaledDummy(5); - Util.ShowObject(this.debugTex); - } - - break; - - // Gamepad - case 16: - Action> helper = (text, mask, resolve) => - { - ImGui.Text($"{text} {mask:X4}"); - ImGui.Text($"DPadLeft {resolve(GamepadButtons.DpadLeft)} " + - $"DPadUp {resolve(GamepadButtons.DpadUp)} " + - $"DPadRight {resolve(GamepadButtons.DpadRight)} " + - $"DPadDown {resolve(GamepadButtons.DpadDown)} "); - ImGui.Text($"West {resolve(GamepadButtons.West)} " + - $"North {resolve(GamepadButtons.North)} " + - $"East {resolve(GamepadButtons.East)} " + - $"South {resolve(GamepadButtons.South)} "); - ImGui.Text($"L1 {resolve(GamepadButtons.L1)} " + - $"L2 {resolve(GamepadButtons.L2)} " + - $"R1 {resolve(GamepadButtons.R1)} " + - $"R2 {resolve(GamepadButtons.R2)} "); - ImGui.Text($"Select {resolve(GamepadButtons.Select)} " + - $"Start {resolve(GamepadButtons.Start)} " + - $"L3 {resolve(GamepadButtons.L3)} " + - $"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")}"); -#endif - - helper( - "Buttons Raw", - this.dalamud.ClientState.GamepadState.ButtonsRaw, - this.dalamud.ClientState.GamepadState.Raw); - helper( - "Buttons Pressed", - this.dalamud.ClientState.GamepadState.ButtonsPressed, - this.dalamud.ClientState.GamepadState.Pressed); - helper( - "Buttons Repeat", - this.dalamud.ClientState.GamepadState.ButtonsRepeat, - this.dalamud.ClientState.GamepadState.Repeat); - helper( - "Buttons Released", - this.dalamud.ClientState.GamepadState.ButtonsReleased, - this.dalamud.ClientState.GamepadState.Released); - ImGui.Text($"LeftStickLeft {this.dalamud.ClientState.GamepadState.LeftStickLeft:0.00} " + - $"LeftStickUp {this.dalamud.ClientState.GamepadState.LeftStickUp:0.00} " + - $"LeftStickRight {this.dalamud.ClientState.GamepadState.LeftStickRight:0.00} " + - $"LeftStickDown {this.dalamud.ClientState.GamepadState.LeftStickDown:0.00} "); - ImGui.Text($"RightStickLeft {this.dalamud.ClientState.GamepadState.RightStickLeft:0.00} " + - $"RightStickUp {this.dalamud.ClientState.GamepadState.RightStickUp:0.00} " + - $"RightStickRight {this.dalamud.ClientState.GamepadState.RightStickRight:0.00} " + - $"RightStickDown {this.dalamud.ClientState.GamepadState.RightStickDown:0.00} "); - break; - } - } - else - { - ImGui.TextUnformatted("Data not ready."); - } - } - catch (Exception ex) - { - ImGui.TextUnformatted(ex.ToString()); - } - - ImGui.PopStyleVar(); - - ImGui.EndChild(); - } - - private void DrawActorTable() - { - var stateString = string.Empty; - - // LocalPlayer is null in a number of situations (at least with the current visible-actors list) - // which would crash here. - if (this.dalamud.ClientState.Actors.Length == 0) - { - ImGui.TextUnformatted("Data not ready."); - } - else if (this.dalamud.ClientState.LocalPlayer == null) - { - ImGui.TextUnformatted("LocalPlayer null."); - } - else - { - stateString += $"FrameworkBase: {this.dalamud.Framework.Address.BaseAddress.ToInt64():X}\n"; - stateString += $"ActorTableLen: {this.dalamud.ClientState.Actors.Length}\n"; - stateString += $"LocalPlayerName: {this.dalamud.ClientState.LocalPlayer.Name}\n"; - stateString += $"CurrentWorldName: {(this.resolveGameData ? this.dalamud.ClientState.LocalPlayer.CurrentWorld.GameData.Name : this.dalamud.ClientState.LocalPlayer.CurrentWorld.Id.ToString())}\n"; - stateString += $"HomeWorldName: {(this.resolveGameData ? this.dalamud.ClientState.LocalPlayer.HomeWorld.GameData.Name : this.dalamud.ClientState.LocalPlayer.HomeWorld.Id.ToString())}\n"; - stateString += $"LocalCID: {this.dalamud.ClientState.LocalContentId:X}\n"; - stateString += $"LastLinkedItem: {this.dalamud.Framework.Gui.Chat.LastLinkedItemId}\n"; - stateString += $"TerritoryType: {this.dalamud.ClientState.TerritoryType}\n\n"; - - ImGui.TextUnformatted(stateString); - - ImGui.Checkbox("Draw actors on screen", ref this.drawActors); - ImGui.SliderFloat("Draw Distance", ref this.maxActorDrawDistance, 2f, 40f); - - for (var i = 0; i < this.dalamud.ClientState.Actors.Length; i++) - { - var actor = this.dalamud.ClientState.Actors[i]; - - if (actor == null) - continue; - - this.PrintActor(actor, i.ToString()); - - if (this.drawActors && this.dalamud.Framework.Gui.WorldToScreen(actor.Position, out var screenCoords)) - { - // 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 screenPos = ImGui.GetMainViewport().Pos; - var screenSize = ImGui.GetMainViewport().Size; - - var windowSize = ImGui.CalcTextSize(actorText); - - // Add some extra safety padding - windowSize.X += ImGui.GetStyle().WindowPadding.X + 10; - windowSize.Y += ImGui.GetStyle().WindowPadding.Y + 10; - - if (screenCoords.X + windowSize.X > screenPos.X + screenSize.X || - screenCoords.Y + windowSize.Y > screenPos.Y + screenSize.Y) - continue; - - if (actor.YalmDistanceX > this.maxActorDrawDistance) - continue; - - ImGui.SetNextWindowPos(new Vector2(screenCoords.X, screenCoords.Y)); - - ImGui.SetNextWindowBgAlpha(Math.Max(1f - (actor.YalmDistanceX / this.maxActorDrawDistance), 0.2f)); - if (ImGui.Begin( - $"Actor{i}##ActorWindow{i}", - ImGuiWindowFlags.NoDecoration | - ImGuiWindowFlags.AlwaysAutoResize | - ImGuiWindowFlags.NoSavedSettings | - ImGuiWindowFlags.NoMove | - ImGuiWindowFlags.NoMouseInputs | - ImGuiWindowFlags.NoDocking | - ImGuiWindowFlags.NoFocusOnAppearing | - ImGuiWindowFlags.NoNav)) - ImGui.Text(actorText); - ImGui.End(); - } - } - } - } - -#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); - - if (ImGui.Button("Add test sub")) - { - i1.Subscribe("DalamudTestPub", o => - { - dynamic msg = o; - Log.Debug(msg.Expand); - }); - } - - if (ImGui.Button("Add test sub any")) - { - i1.SubscribeAny((o, a) => - { - dynamic msg = a; - Log.Debug($"From {o}: {msg.Expand}"); - }); - } - - if (ImGui.Button("Remove test sub")) - i1.Unsubscribe("DalamudTestPub"); - - if (ImGui.Button("Remove test sub any")) - i1.UnsubscribeAny(); - - if (ImGui.Button("Send test message")) - { - dynamic testMsg = new ExpandoObject(); - testMsg.Expand = "dong"; - i2.SendMessage(testMsg); - } - - // This doesn't actually work, so don't mind it - impl relies on plugins being registered in PluginManager - if (ImGui.Button("Send test message any")) - { - dynamic testMsg = new ExpandoObject(); - testMsg.Expand = "dong"; - i2.SendMessage("DalamudTestSub", testMsg); - } - - foreach (var (sourcePluginName, subPluginName, subAction) in this.dalamud.PluginManager.IpcSubscriptions) - ImGui.Text($"Source:{sourcePluginName} Sub:{subPluginName}"); - } -#pragma warning restore CS0618 // Type or member is obsolete - - private void DrawAddonDebug() - { - ImGui.InputText("Addon name", ref this.inputAddonName, 256); - ImGui.InputInt("Addon Index", ref this.inputAddonIndex); - - if (ImGui.Button("Get Addon")) - { - this.resultAddon = - this.dalamud.Framework.Gui.GetAddonByName( - this.inputAddonName, this.inputAddonIndex); - } - - if (ImGui.Button("Find Agent")) - this.findAgentInterfacePtr = this.dalamud.Framework.Gui.FindAgentInterface(this.inputAddonName); - - if (this.resultAddon != null) - { - ImGui.TextUnformatted( - $"{this.resultAddon.Name} - 0x{this.resultAddon.Address.ToInt64():x}\n v:{this.resultAddon.Visible} x:{this.resultAddon.X} y:{this.resultAddon.Y} s:{this.resultAddon.Scale}, w:{this.resultAddon.Width}, h:{this.resultAddon.Height}"); - } - - if (this.findAgentInterfacePtr != IntPtr.Zero) - { - ImGui.TextUnformatted( - $"Agent: 0x{this.findAgentInterfacePtr.ToInt64():x}"); - ImGui.SameLine(); - - if (ImGui.Button("C")) - ImGui.SetClipboardText(this.findAgentInterfacePtr.ToInt64().ToString("x")); - } - - if (ImGui.Button("Get Base UI object")) - { - var addr = this.dalamud.Framework.Gui.GetBaseUIObject().ToInt64().ToString("x"); - Log.Information("{0}", addr); - ImGui.SetClipboardText(addr); - } - } - - private void DrawTargetDebug() - { - var targetMgr = this.dalamud.ClientState.Targets; - - if (targetMgr.CurrentTarget != null) - { - this.PrintActor(targetMgr.CurrentTarget, "CurrentTarget"); - Util.ShowObject(targetMgr.CurrentTarget); - } - - if (targetMgr.FocusTarget != null) - this.PrintActor(targetMgr.FocusTarget, "FocusTarget"); - - if (targetMgr.MouseOverTarget != null) - this.PrintActor(targetMgr.MouseOverTarget, "MouseOverTarget"); - - if (targetMgr.PreviousTarget != null) - this.PrintActor(targetMgr.PreviousTarget, "PreviousTarget"); - - if (targetMgr.SoftTarget != null) - this.PrintActor(targetMgr.SoftTarget, "SoftTarget"); - - if (ImGui.Button("Clear CT")) - targetMgr.ClearCurrentTarget(); - - if (ImGui.Button("Clear FT")) - targetMgr.ClearFocusTarget(); - - var localPlayer = this.dalamud.ClientState.LocalPlayer; - - if (localPlayer != null) - { - if (ImGui.Button("Set CT")) - targetMgr.SetCurrentTarget(localPlayer); - - if (ImGui.Button("Set FT")) - targetMgr.SetFocusTarget(localPlayer); - } - else - { - ImGui.Text("LocalPlayer is null."); - } - } - - private void Load() - { - if (this.dalamud.Data.IsDataReady) - { - this.serverOpString = JsonConvert.SerializeObject(this.dalamud.Data.ServerOpCodes, Formatting.Indented); - this.wasReady = true; - } - } - - private void PrintActor(Actor actor, string tag) - { - var actorString = - $"{actor.Address.ToInt64():X}:{actor.ActorId:X}[{tag}] - {actor.ObjectKind} - {actor.Name} - X{actor.Position.X} Y{actor.Position.Y} Z{actor.Position.Z} D{actor.YalmDistanceX} R{actor.Rotation} - Target: {actor.TargetActorID:X}\n"; - - if (actor is Npc npc) - actorString += $" DataId: {npc.DataId} NameId:{npc.NameId}\n"; - - if (actor is Chara chara) - { - actorString += - $" Level: {chara.Level} ClassJob: {(this.resolveGameData ? chara.ClassJob.GameData.Name : chara.ClassJob.Id.ToString())} CHP: {chara.CurrentHp} MHP: {chara.MaxHp} CMP: {chara.CurrentMp} MMP: {chara.MaxMp}\n Customize: {BitConverter.ToString(chara.Customize).Replace("-", " ")}\n"; - } - - if (actor is PlayerCharacter pc) - { - actorString += - $" HomeWorld: {(this.resolveGameData ? pc.HomeWorld.GameData.Name : pc.HomeWorld.Id.ToString())} CurrentWorld: {(this.resolveGameData ? pc.CurrentWorld.GameData.Name : pc.CurrentWorld.Id.ToString())} FC: {pc.CompanyTag}\n"; - } - - ImGui.TextUnformatted(actorString); - ImGui.SameLine(); - if (ImGui.Button($"C##{this.copyButtonIndex++}")) - { - ImGui.SetClipboardText(actor.Address.ToInt64().ToString("X")); - } - } - } -} diff --git a/Dalamud/Interface/DalamudLogWindow.cs b/Dalamud/Interface/DalamudLogWindow.cs deleted file mode 100644 index 78cbaa17e..000000000 --- a/Dalamud/Interface/DalamudLogWindow.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Numerics; - -using Dalamud.Configuration; -using Dalamud.Game.Command; -using Dalamud.Interface.Colors; -using Dalamud.Interface.Windowing; -using ImGuiNET; -using Serilog; -using Serilog.Events; - -namespace Dalamud.Interface -{ - /// - /// The window that displays the Dalamud log file in-game. - /// - internal class DalamudLogWindow : Window, IDisposable - { - private readonly CommandManager commandManager; - private readonly DalamudConfiguration configuration; - private readonly List<(string Line, Vector4 Color)> logText = new(); - private readonly object renderLock = new(); - private bool autoScroll; - private bool openAtStartup; - - private string commandText = string.Empty; - - /// - /// Initializes a new instance of the class. - /// - /// The CommandManager instance. - /// The DalamudConfiguration instance. - public DalamudLogWindow(CommandManager commandManager, DalamudConfiguration configuration) - : base("Dalamud LOG") - { - this.commandManager = commandManager; - this.configuration = configuration; - this.autoScroll = configuration.LogAutoScroll; - this.openAtStartup = configuration.LogOpenAtStartup; - SerilogEventSink.Instance.OnLogLine += this.Serilog_OnLogLine; - - this.Size = new Vector2(500, 400); - this.SizeCondition = ImGuiCond.FirstUseEver; - } - - /// - /// Dispose of managed and unmanaged resources. - /// - public void Dispose() - { - SerilogEventSink.Instance.OnLogLine -= this.Serilog_OnLogLine; - } - - /// - /// Clear the window of all log entries. - /// - public void Clear() - { - lock (this.renderLock) - { - this.logText.Clear(); - } - } - - /// - /// Add a single log line to the display. - /// - /// The line to add. - /// The line coloring. - public void AddLog(string line, Vector4 color) - { - lock (this.renderLock) - { - this.logText.Add((line, color)); - } - } - - /// - public override void Draw() - { - // Options menu - if (ImGui.BeginPopup("Options")) - { - if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll)) - { - this.configuration.LogAutoScroll = this.autoScroll; - this.configuration.Save(); - } - - if (ImGui.Checkbox("Open at startup", ref this.openAtStartup)) - { - this.configuration.LogOpenAtStartup = this.openAtStartup; - this.configuration.Save(); - } - - ImGui.EndPopup(); - } - - // 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)) - { - Log.Information("Command was dispatched."); - } - else - { - Log.Information("Command {0} not registered.", this.commandText); - } - } - - 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)); - - lock (this.renderLock) - { - foreach (var (line, color) in this.logText) - { - ImGui.TextColored(color, line); - } - } - - 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) - { - var color = logEvent.Level switch - { - LogEventLevel.Error => ImGuiColors.DalamudRed, - LogEventLevel.Verbose => ImGuiColors.DalamudWhite, - LogEventLevel.Debug => ImGuiColors.DalamudWhite2, - LogEventLevel.Information => ImGuiColors.DalamudWhite, - LogEventLevel.Warning => ImGuiColors.DalamudOrange, - LogEventLevel.Fatal => ImGuiColors.DalamudRed, - _ => throw new ArgumentOutOfRangeException(), - }; - - this.AddLog(logEvent.Line, color); - } - } -} 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 93% rename from Dalamud/DalamudCommands.cs rename to Dalamud/Interface/Internal/DalamudCommands.cs index 5b59f4a70..6e0f5e7a6 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"); } @@ -221,25 +221,33 @@ namespace Dalamud private void OnBgmSetCommand(string command, string arguments) { - this.dalamud.Framework.Gui.SetBgm(ushort.Parse(arguments)); + if (ushort.TryParse(arguments, out var value)) + { + this.dalamud.Framework.Gui.SetBgm(value); + } + else + { + // Revert to the original BGM by specifying an invalid one + this.dalamud.Framework.Gui.SetBgm(9999); + } } 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 +275,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 +307,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 56% rename from Dalamud/Interface/DalamudInterface.cs rename to Dalamud/Interface/Internal/DalamudInterface.cs index 30f1534f5..47225e815 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 ConsoleWindow consoleWindow; + 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.consoleWindow = new ConsoleWindow(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.consoleWindow); + 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.consoleWindow.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.consoleWindow.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.consoleWindow.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...")) @@ -190,6 +329,8 @@ namespace Dalamud.Interface if (ImGui.MenuItem(logLevel + "##logLevelSwitch", string.Empty, this.dalamud.LogLevelSwitch.MinimumLevel == logLevel)) { this.dalamud.LogLevelSwitch.MinimumLevel = logLevel; + this.dalamud.Configuration.LogLevel = logLevel; + this.dalamud.Configuration.Save(); } } @@ -198,19 +339,26 @@ namespace Dalamud.Interface if (ImGui.MenuItem("Enable AntiDebug", null, this.dalamud.AntiDebug.IsEnabled)) { - this.dalamud.AntiDebug.Enable(); + var newEnabled = !this.dalamud.AntiDebug.IsEnabled; + if (newEnabled) + this.dalamud.AntiDebug.Enable(); + else + this.dalamud.AntiDebug.Disable(); + + this.dalamud.Configuration.IsAntiAntiDebugEnabled = newEnabled; + this.dalamud.Configuration.Save(); } ImGui.Separator(); 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")) @@ -220,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(); @@ -255,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(); } @@ -291,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}"); } } @@ -302,18 +450,31 @@ 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(); + + if (ImGui.MenuItem("Load all API levels", null, this.dalamud.Configuration.LoadAllApiLevels)) + { + this.dalamud.Configuration.LoadAllApiLevels = !this.dalamud.Configuration.LoadAllApiLevels; + this.dalamud.Configuration.Save(); + } + 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(); } @@ -375,197 +536,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 92% rename from Dalamud/Interface/InterfaceManager.cs rename to Dalamud/Interface/Internal/InterfaceManager.cs index fec66aa19..b4c51d275 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,21 @@ namespace Dalamud.Interface /// public static ImFontPtr IconFont { get; private set; } + /// + /// Gets an included monospaced font. + /// + public static ImFontPtr MonoFont { 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 +197,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) @@ -467,6 +469,13 @@ namespace Dalamud.Interface GCHandleType.Pinned); IconFont = ImGui.GetIO().Fonts.AddFontFromFileTTF(fontPathIcon, 17.0f, null, iconRangeHandle.AddrOfPinnedObject()); + var fontPathMono = Path.Combine(this.dalamud.AssetDirectory.FullName, "UIRes", "Inconsolata-Regular.ttf"); + + if (!File.Exists(fontPathMono)) + ShowFontError(fontPathMono); + + MonoFont = ImGui.GetIO().Fonts.AddFontFromFileTTF(fontPathMono, 16.0f); + Log.Verbose("[FONT] Invoke OnBuildFonts"); this.OnBuildFonts?.Invoke(); Log.Verbose("[FONT] OnBuildFonts OK!"); @@ -567,7 +576,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 +601,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..729827b1e 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, PluginLoadReason.Unknown); + 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 86% rename from Dalamud/Interface/SerilogEventSink.cs rename to Dalamud/Interface/Internal/SerilogEventSink.cs index 4810f75b8..ac2c522ba 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. @@ -25,7 +25,7 @@ namespace Dalamud.Interface /// /// Event on a log line being emitted. /// - public event EventHandler<(string Line, LogEventLevel Level)> OnLogLine; + public event EventHandler<(string Line, LogEventLevel Level, DateTimeOffset TimeStamp)> OnLogLine; /// /// Gets the default instance. @@ -38,12 +38,14 @@ namespace Dalamud.Interface /// Log event to be emitted. public void Emit(LogEvent logEvent) { - var message = $"[{DateTimeOffset.Now:HH:mm:ss.fff}][{logEvent.Level}] {logEvent.RenderMessage(this.formatProvider)}"; + var message = logEvent.RenderMessage(this.formatProvider); if (logEvent.Exception != null) + { message += "\n" + logEvent.Exception; + } - this.OnLogLine?.Invoke(this, (message, logEvent.Level)); + this.OnLogLine?.Invoke(this, (message, logEvent.Level, logEvent.Timestamp)); } } } 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/Internal/Windows/ComponentDemoWindow.cs b/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs new file mode 100644 index 000000000..860a2ba15 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Numerics; + +using Dalamud.Interface.Animation; +using Dalamud.Interface.Animation.EasingFunctions; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; +using Dalamud.Interface.Windowing; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows +{ + /// + /// Component Demo Window to view custom ImGui components. + /// + internal sealed class ComponentDemoWindow : Window + { + private static readonly TimeSpan DefaultEasingTime = new(0, 0, 0, 1700); + + private readonly List<(string Name, Action Demo)> componentDemos; + private readonly IReadOnlyList easings = new Easing[] + { + new InSine(DefaultEasingTime), new OutSine(DefaultEasingTime), new InOutSine(DefaultEasingTime), + new InCubic(DefaultEasingTime), new OutCubic(DefaultEasingTime), new InOutCubic(DefaultEasingTime), + new InQuint(DefaultEasingTime), new OutQuint(DefaultEasingTime), new InOutQuint(DefaultEasingTime), + new InCirc(DefaultEasingTime), new OutCirc(DefaultEasingTime), new InOutCirc(DefaultEasingTime), + new InElastic(DefaultEasingTime), new OutElastic(DefaultEasingTime), new InOutElastic(DefaultEasingTime), + }; + + private int animationTimeMs = (int)DefaultEasingTime.TotalMilliseconds; + private Vector4 defaultColor = ImGuiColors.DalamudOrange; + + /// + /// Initializes a new instance of the class. + /// + public ComponentDemoWindow() + : base("Dalamud Components Demo") + { + this.Size = new Vector2(600, 500); + this.SizeCondition = ImGuiCond.FirstUseEver; + + this.componentDemos = new() + { + ("Test", ImGuiComponents.Test), + ("HelpMarker", HelpMarkerDemo), + ("IconButton", IconButtonDemo), + ("TextWithLabel", TextWithLabelDemo), + ("ColorPickerWithPalette", this.ColorPickerWithPaletteDemo), + }; + } + + /// + public override void OnOpen() + { + foreach (var easing in this.easings) + { + easing.Restart(); + } + } + + /// + public override void OnClose() + { + foreach (var easing in this.easings) + { + easing.Stop(); + } + } + + /// + public override void Draw() + { + 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.Name}###comp{i}")) + { + componentDemo.Demo(); + } + } + + if (ImGui.CollapsingHeader("Easing animations")) + { + this.EasingsDemo(); + } + } + + private static void HelpMarkerDemo() + { + ImGui.Text("Hover over the icon to learn more."); + ImGuiComponents.HelpMarker("help me!"); + } + + private static void IconButtonDemo() + { + ImGui.Text("Click on the icon to use as a button."); + ImGui.SameLine(); + if (ImGuiComponents.IconButton(1, FontAwesomeIcon.Carrot)) + { + ImGui.OpenPopup("IconButtonDemoPopup"); + } + + if (ImGui.BeginPopup("IconButtonDemoPopup")) + { + ImGui.Text("You clicked!"); + ImGui.EndPopup(); + } + } + + private static void TextWithLabelDemo() + { + ImGuiComponents.TextWithLabel("Label", "Hover to see more", "more"); + } + + private void EasingsDemo() + { + ImGui.SliderInt("Speed in MS", ref this.animationTimeMs, 200, 5000); + + foreach (var easing in this.easings) + { + easing.Duration = new TimeSpan(0, 0, 0, 0, this.animationTimeMs); + + if (!easing.IsRunning) + { + easing.Start(); + } + + var cursor = ImGui.GetCursorPos(); + var p1 = new Vector2(cursor.X + 5, cursor.Y); + var p2 = p1 + new Vector2(45, 0); + easing.Point1 = p1; + easing.Point2 = p2; + easing.Update(); + + if (easing.IsDone) + { + easing.Restart(); + } + + ImGui.SetCursorPos(easing.EasedPoint); + ImGui.Bullet(); + + ImGui.SetCursorPos(cursor + new Vector2(0, 10)); + ImGui.Text($"{easing.GetType().Name} ({easing.Value})"); + ImGuiHelpers.ScaledDummy(5); + } + } + + private void ColorPickerWithPaletteDemo() + { + ImGui.Text("Click on the color button to use the picker."); + ImGui.SameLine(); + this.defaultColor = ImGuiComponents.ColorPickerWithPalette(1, "ColorPickerWithPalette Demo", this.defaultColor); + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs new file mode 100644 index 000000000..92aa1a0c9 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -0,0 +1,479 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; + +using Dalamud.Configuration.Internal; +using Dalamud.Game.Command; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Windowing; +using ImGuiNET; +using Serilog; +using Serilog.Events; + +namespace Dalamud.Interface.Internal.Windows +{ + /// + /// The window that displays the Dalamud log file in-game. + /// + internal class ConsoleWindow : Window, IDisposable + { + private readonly Dalamud dalamud; + + private readonly List logText = new(); + private readonly object renderLock = new(); + + private readonly string[] logLevelStrings = new[] { "None", "Verbose", "Debug", "Information", "Warning", "Error", "Fatal" }; + + private List filteredLogText = new(); + private bool autoScroll; + private bool openAtStartup; + + private bool? lastCmdSuccess; + + private string commandText = string.Empty; + + private string textFilter = string.Empty; + private LogEventLevel? levelFilter = null; + private bool isFiltered = false; + + private int historyPos; + private List history = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The Dalamud instance. + public ConsoleWindow(Dalamud dalamud) + : base("Dalamud Console") + { + this.dalamud = dalamud; + + this.autoScroll = this.dalamud.Configuration.LogAutoScroll; + this.openAtStartup = this.dalamud.Configuration.LogOpenAtStartup; + SerilogEventSink.Instance.OnLogLine += this.OnLogLine; + + this.Size = new Vector2(500, 400); + this.SizeCondition = ImGuiCond.FirstUseEver; + } + + private List LogEntries => this.isFiltered ? this.filteredLogText : this.logText; + + /// + /// Dispose of managed and unmanaged resources. + /// + public void Dispose() + { + SerilogEventSink.Instance.OnLogLine -= this.OnLogLine; + } + + /// + /// Clear the window of all log entries. + /// + public void Clear() + { + lock (this.renderLock) + { + this.logText.Clear(); + this.filteredLogText.Clear(); + } + } + + /// + /// Add a single log line to the display. + /// + /// The line to add. + /// The level of the event. + /// The of the event. + public void HandleLogLine(string line, LogEventLevel level, DateTimeOffset offset) + { + if (line.IndexOfAny(new[] { '\n', '\r' }) != -1) + { + var subLines = line.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); + + this.AddAndFilter(subLines[0], level, offset, false); + + for (var i = 1; i < subLines.Length; i++) + { + this.AddAndFilter(subLines[i], level, offset, true); + } + } + else + { + this.AddAndFilter(line, level, offset, false); + } + } + + /// + public override void Draw() + { + // Options menu + if (ImGui.BeginPopup("Options")) + { + if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll)) + { + this.dalamud.Configuration.LogAutoScroll = this.autoScroll; + this.dalamud.Configuration.Save(); + } + + if (ImGui.Checkbox("Open at startup", ref this.openAtStartup)) + { + this.dalamud.Configuration.LogOpenAtStartup = this.openAtStartup; + this.dalamud.Configuration.Save(); + } + + var prevLevel = (int)this.dalamud.LogLevelSwitch.MinimumLevel; + if (ImGui.Combo("Log Level", ref prevLevel, Enum.GetValues(typeof(LogEventLevel)).Cast().Select(x => x.ToString()).ToArray(), 6)) + { + this.dalamud.LogLevelSwitch.MinimumLevel = (LogEventLevel)prevLevel; + this.dalamud.Configuration.LogLevel = (LogEventLevel)prevLevel; + this.dalamud.Configuration.Save(); + } + + ImGui.EndPopup(); + } + + // Filter menu + if (ImGui.BeginPopup("Filters")) + { + ImGui.Checkbox("Enabled", ref this.isFiltered); + + if (ImGui.InputTextWithHint("##filterText", "Text Filter", ref this.textFilter, 255, ImGuiInputTextFlags.EnterReturnsTrue)) + { + this.Refilter(); + } + + ImGui.TextColored(ImGuiColors.DalamudGrey, "Enter to confirm."); + + var filterVal = this.levelFilter.HasValue ? (int)this.levelFilter.Value + 1 : 0; + if (ImGui.Combo("Level", ref filterVal, this.logLevelStrings, 7)) + { + this.levelFilter = (LogEventLevel)(filterVal - 1); + this.Refilter(); + } + + ImGui.EndPopup(); + } + + ImGui.SameLine(); + ImGui.PushFont(InterfaceManager.IconFont); + + if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString())) + ImGui.OpenPopup("Options"); + + ImGui.SameLine(); + if (ImGui.Button(FontAwesomeIcon.Search.ToIconString())) + ImGui.OpenPopup("Filters"); + + ImGui.SameLine(); + var clear = ImGui.Button(FontAwesomeIcon.Trash.ToIconString()); + + ImGui.SameLine(); + var copy = ImGui.Button(FontAwesomeIcon.Copy.ToIconString()); + + ImGui.SameLine(); + if (ImGui.Button(FontAwesomeIcon.Skull.ToIconString())) + Process.GetCurrentProcess().Kill(); + + ImGui.PopFont(); + + ImGui.BeginChild("scrolling", new Vector2(0, ImGui.GetFrameHeightWithSpacing() - 55), false, ImGuiWindowFlags.HorizontalScrollbar); + + if (clear) + { + this.Clear(); + } + + if (copy) + { + ImGui.LogToClipboard(); + } + + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + + ImGuiListClipperPtr clipper; + unsafe + { + clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); + } + + ImGui.PushFont(InterfaceManager.MonoFont); + + var childPos = ImGui.GetWindowPos(); + var childDrawList = ImGui.GetWindowDrawList(); + var childSize = ImGui.GetWindowSize(); + + lock (this.renderLock) + { + clipper.Begin(this.LogEntries.Count); + while (clipper.Step()) + { + for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + { + var line = this.LogEntries[i]; + + if (!line.IsMultiline) + ImGui.Separator(); + + ImGui.PushStyleColor(ImGuiCol.Header, this.GetColorForLogEventLevel(line.Level)); + ImGui.PushStyleColor(ImGuiCol.HeaderActive, this.GetColorForLogEventLevel(line.Level)); + ImGui.PushStyleColor(ImGuiCol.HeaderHovered, this.GetColorForLogEventLevel(line.Level)); + + ImGui.Selectable("###consolenull", true, ImGuiSelectableFlags.AllowItemOverlap | ImGuiSelectableFlags.SpanAllColumns); + ImGui.SameLine(); + + ImGui.PopStyleColor(3); + + if (!line.IsMultiline) + { + ImGui.TextUnformatted(line.TimeStamp.ToString("HH:mm:ss.fff")); + ImGui.SameLine(); + ImGui.SetCursorPosX(92); + ImGui.TextUnformatted("|"); + ImGui.SameLine(); + ImGui.SetCursorPosX(100); + ImGui.TextUnformatted(this.GetTextForLogEventLevel(line.Level)); + ImGui.SameLine(); + } + + ImGui.SetCursorPosX(135); + ImGui.TextUnformatted(line.Line); + } + } + + clipper.End(); + } + + ImGui.PopFont(); + + ImGui.PopStyleVar(); + + if (this.autoScroll && ImGui.GetScrollY() >= ImGui.GetScrollMaxY()) + { + ImGui.SetScrollHereY(1.0f); + } + + // Draw dividing line + childDrawList.AddLine(new Vector2(childPos.X + 127, childPos.Y), new Vector2(childPos.X + 127, childPos.Y + childSize.Y), 0x4FFFFFFF, 1.0f); + + ImGui.EndChild(); + + var hadColor = false; + if (this.lastCmdSuccess.HasValue) + { + hadColor = true; + if (this.lastCmdSuccess.Value) + { + ImGui.PushStyleColor(ImGuiCol.FrameBg, ImGuiColors.HealerGreen - new Vector4(0, 0, 0, 0.7f)); + } + else + { + ImGui.PushStyleColor(ImGuiCol.FrameBg, ImGuiColors.DalamudRed - new Vector4(0, 0, 0, 0.7f)); + } + } + + ImGui.SetNextItemWidth(ImGui.GetWindowSize().X - 80); + + var getFocus = false; + unsafe + { + if (ImGui.InputText("##commandbox", ref this.commandText, 255, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackCompletion | ImGuiInputTextFlags.CallbackHistory, this.CommandInputCallback)) + { + this.ProcessCommand(); + getFocus = true; + } + + ImGui.SameLine(); + } + + ImGui.SetItemDefaultFocus(); + if (getFocus) + ImGui.SetKeyboardFocusHere(-1); // Auto focus previous widget + + if (hadColor) + ImGui.PopStyleColor(); + + if (ImGui.Button("Send")) + { + this.ProcessCommand(); + } + } + + private void ProcessCommand() + { + try + { + this.historyPos = -1; + for (int i = this.history.Count - 1; i >= 0; i--) + { + if (this.history[i] == this.commandText) + { + this.history.RemoveAt(i); + break; + } + } + + this.history.Add(this.commandText); + + if (this.commandText == "clear" || this.commandText == "cls") + { + this.Clear(); + return; + } + + this.lastCmdSuccess = this.dalamud.CommandManager.ProcessCommand("/" + this.commandText); + this.commandText = string.Empty; + + // TODO: Force scroll to bottom + } + catch (Exception ex) + { + Log.Error(ex, "Error during command dispatch"); + this.lastCmdSuccess = false; + } + } + + private unsafe int CommandInputCallback(ImGuiInputTextCallbackData* data) + { + var ptr = new ImGuiInputTextCallbackDataPtr(data); + + switch (data->EventFlag) + { + case ImGuiInputTextFlags.CallbackCompletion: + var textBytes = new byte[data->BufTextLen]; + Marshal.Copy((IntPtr)data->Buf, textBytes, 0, data->BufTextLen); + var text = Encoding.UTF8.GetString(textBytes); + + var words = text.Split(); + + // We can't do any completion for parameters at the moment since it just calls into CommandHandler + if (words.Length > 1) + return 0; + + // TODO: Improve this, add partial completion + // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L6443-L6484 + var candidates = this.dalamud.CommandManager.Commands.Where(x => x.Key.Contains("/" + words[0])).ToList(); + if (candidates.Count > 0) + { + ptr.DeleteChars(0, ptr.BufTextLen); + ptr.InsertChars(0, candidates[0].Key.Replace("/", string.Empty)); + } + + break; + case ImGuiInputTextFlags.CallbackHistory: + var prevPos = this.historyPos; + + if (ptr.EventKey == ImGuiKey.UpArrow) + { + if (this.historyPos == -1) + this.historyPos = this.history.Count - 1; + else if (this.historyPos > 0) + this.historyPos--; + } + else if (data->EventKey == ImGuiKey.DownArrow) + { + if (this.historyPos != -1) + { + if (++this.historyPos >= this.history.Count) + { + this.historyPos = -1; + } + } + } + + if (prevPos != this.historyPos) + { + var historyStr = this.historyPos >= 0 ? this.history[this.historyPos] : string.Empty; + + ptr.DeleteChars(0, ptr.BufTextLen); + ptr.InsertChars(0, historyStr); + } + + break; + } + + return 0; + } + + private void AddAndFilter(string line, LogEventLevel level, DateTimeOffset offset, bool isMultiline) + { + var entry = new LogEntry + { + IsMultiline = isMultiline, + Level = level, + Line = line, + TimeStamp = offset, + }; + + this.logText.Add(entry); + + if (!this.isFiltered) + return; + + if (this.IsFilterApplicable(entry)) + this.filteredLogText.Add(entry); + } + + private bool IsFilterApplicable(LogEntry entry) + { + if (this.levelFilter.HasValue) + { + return entry.Level == this.levelFilter.Value; + } + + if (!string.IsNullOrEmpty(this.textFilter)) + return entry.Line.Contains(this.textFilter); + + return true; + } + + private void Refilter() + { + lock (this.renderLock) + { + this.filteredLogText = this.logText.Where(this.IsFilterApplicable).ToList(); + } + } + + private string GetTextForLogEventLevel(LogEventLevel level) => level switch + { + LogEventLevel.Error => "ERR", + LogEventLevel.Verbose => "VRB", + LogEventLevel.Debug => "DBG", + LogEventLevel.Information => "INF", + LogEventLevel.Warning => "WRN", + LogEventLevel.Fatal => "FTL", + _ => throw new ArgumentOutOfRangeException(level.ToString(), "Invalid LogEventLevel"), + }; + + private uint GetColorForLogEventLevel(LogEventLevel level) => level switch + { + LogEventLevel.Error => 0x800000EE, + LogEventLevel.Verbose => 0x00000000, + LogEventLevel.Debug => 0x00000000, + LogEventLevel.Information => 0x00000000, + LogEventLevel.Warning => 0x8A0070EE, + LogEventLevel.Fatal => 0xFF00000A, + _ => throw new ArgumentOutOfRangeException(level.ToString(), "Invalid LogEventLevel"), + }; + + private void OnLogLine(object sender, (string Line, LogEventLevel Level, DateTimeOffset Offset) logEvent) + { + this.HandleLogLine(logEvent.Line, logEvent.Level, logEvent.Offset); + } + + private class LogEntry + { + public string Line { get; set; } + + public LogEventLevel Level { get; set; } + + public DateTimeOffset TimeStamp { get; set; } + + public bool IsMultiline { get; set; } + } + } +} diff --git a/Dalamud/Interface/DalamudCreditsWindow.cs b/Dalamud/Interface/Internal/Windows/CreditsWindow.cs similarity index 57% rename from Dalamud/Interface/DalamudCreditsWindow.cs rename to Dalamud/Interface/Internal/Windows/CreditsWindow.cs index d15c64909..a307d0161 100644 --- a/Dalamud/Interface/DalamudCreditsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/CreditsWindow.cs @@ -1,20 +1,21 @@ using System; +using System.Diagnostics; 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 = @" Dalamud A FFXIV Hooking Framework @@ -89,13 +90,25 @@ Kubera Truci Haplo Franz +aers + + +We use these awesome +FFXIV C# libraries: + +Lumina by Adam +FFXIVClientStructs by aers + + + +Thanks to everyone in the XIVLauncher +Discord server -Everyone in the XIVLauncher Discord server Join us at: https://discord.gg/3NMcUV5 -Licensed under AGPL +Licensed under AGPL V3 or later Contribute at: https://github.com/goatsoft/Dalamud @@ -103,22 +116,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; /// - /// 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.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; @@ -131,22 +143,22 @@ 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(); } /// public override void OnClose() { - base.OnClose(); - - this.framework.Gui.SetBgm(9999); + this.creditsThrottler.Reset(); + this.dalamud.Framework.Gui.SetBgm(9999); } /// @@ -155,19 +167,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; @@ -182,12 +194,15 @@ Thank you for using XIVLauncher and Dalamud! ImGui.PopStyleVar(); - var curY = ImGui.GetScrollY(); - var maxY = ImGui.GetScrollMaxY(); - - if (curY < maxY - 1) + if (this.creditsThrottler.Elapsed.TotalMilliseconds > (1000.0f / CreditFPS)) { - ImGui.SetScrollY(curY + 1); + var curY = ImGui.GetScrollY(); + var maxY = ImGui.GetScrollMaxY(); + + if (curY < maxY - 1) + { + ImGui.SetScrollY(curY + 1); + } } ImGui.EndChild(); diff --git a/Dalamud/Interface/Internal/Windows/DataWindow.cs b/Dalamud/Interface/Internal/Windows/DataWindow.cs new file mode 100644 index 000000000..6b723098f --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/DataWindow.cs @@ -0,0 +1,825 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Numerics; + +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Actors.Types; +using Dalamud.Game.ClientState.Actors.Types.NonPlayer; +using Dalamud.Game.ClientState.Structs.JobGauge; +using Dalamud.Game.Internal; +using Dalamud.Game.Internal.Gui.Addon; +using Dalamud.Game.Internal.Gui.Toast; +using Dalamud.Game.Text; +using Dalamud.Interface.Windowing; +using Dalamud.Plugin; +using ImGuiNET; +using ImGuiScene; +using Newtonsoft.Json; +using Serilog; + +namespace Dalamud.Interface.Internal.Windows +{ + /// + /// Class responsible for drawing the data/debug window. + /// + internal class DataWindow : Window + { + private readonly Dalamud dalamud; + private readonly string[] dataKindNames = Enum.GetNames(typeof(DataKind)).Select(k => k.Replace("_", " ")).ToArray(); + + private bool wasReady; + private string serverOpString; + private DataKind currentKind; + + private bool drawActors = false; + private float maxActorDrawDistance = 20; + + private string inputSig = string.Empty; + private IntPtr sigResult = IntPtr.Zero; + + private string inputAddonName = string.Empty; + private int inputAddonIndex; + private Addon resultAddon; + + private IntPtr findAgentInterfacePtr; + + private bool resolveGameData = false; + + private UIDebug addonInspector = null; + + private string inputTextToast = string.Empty; + private int toastPosition = 0; + private int toastSpeed = 0; + private int questToastPosition = 0; + private bool questToastSound = false; + private int questToastIconId = 0; + private bool questToastCheckmark = false; + + private string inputTexPath = string.Empty; + private TextureWrap debugTex = null; + private Vector2 inputTexUv0 = Vector2.Zero; + private Vector2 inputTexUv1 = Vector2.One; + private Vector4 inputTintCol = Vector4.One; + private Vector2 inputTexScale = Vector2.Zero; + + private uint copyButtonIndex = 0; + + /// + /// Initializes a new instance of the class. + /// + /// The Dalamud instance to access data of. + public DataWindow(Dalamud dalamud) + : base("Dalamud Data") + { + this.dalamud = dalamud; + + this.Size = new Vector2(500, 500); + this.SizeCondition = ImGuiCond.FirstUseEver; + + this.Load(); + } + + private enum DataKind + { + Server_OpCode, + Address, + Actor_Table, + Fate_Table, + Font_Test, + Party_List, + Plugin_IPC, + Condition, + Gauge, + Command, + Addon, + Addon_Inspector, + StartInfo, + Target, + Toast, + ImGui, + Tex, + Gamepad, + } + + /// + /// Set the DataKind dropdown menu. + /// + /// Data kind name, can be lower and/or without spaces. + public void SetDataKind(string dataKind) + { + if (string.IsNullOrEmpty(dataKind)) + return; + + if (dataKind == "ai") + dataKind = "Addon Inspector"; + + dataKind = dataKind.Replace(" ", string.Empty).ToLower(); + var dataKinds = Enum.GetValues(typeof(DataKind)) + .Cast() + .Where(k => nameof(k).Replace("_", string.Empty).ToLower() == dataKind) + .ToList(); + + if (dataKinds.Count > 0) + { + this.currentKind = dataKinds.First(); + } + else + { + this.dalamud.Framework.Gui.Chat.PrintError("/xldata: Invalid Data Type"); + } + } + + /// + /// Draw the window via ImGui. + /// + public override void Draw() + { + this.copyButtonIndex = 0; + + // Main window + if (ImGui.Button("Force Reload")) + this.Load(); + ImGui.SameLine(); + var copy = ImGui.Button("Copy all"); + ImGui.SameLine(); + + var currentKindIndex = (int)this.currentKind; + if (ImGui.Combo("Data kind", ref currentKindIndex, this.dataKindNames, this.dataKindNames.Length)) + { + this.currentKind = (DataKind)currentKindIndex; + } + + ImGui.Checkbox("Resolve GameData", ref this.resolveGameData); + + ImGui.BeginChild("scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.HorizontalScrollbar); + + if (copy) + ImGui.LogToClipboard(); + + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0)); + + try + { + if (this.wasReady) + { + switch (this.currentKind) + { + case DataKind.Server_OpCode: + this.DrawServerOpCode(); + break; + + case DataKind.Address: + this.DrawAddress(); + break; + + case DataKind.Actor_Table: + this.DrawActorTable(); + break; + + case DataKind.Fate_Table: + this.DrawFateTable(); + break; + + case DataKind.Font_Test: + this.DrawFontTest(); + break; + + case DataKind.Party_List: + this.DrawPartyList(); + break; + + case DataKind.Plugin_IPC: + this.DrawPluginIPC(); + break; + + case DataKind.Condition: + this.DrawCondition(); + break; + + case DataKind.Gauge: + this.DrawGauge(); + break; + + case DataKind.Command: + this.DrawCommand(); + break; + + case DataKind.Addon: + this.DrawAddon(); + break; + + case DataKind.Addon_Inspector: + this.DrawAddonInspector(); + break; + + case DataKind.StartInfo: + this.DrawStartInfo(); + break; + + case DataKind.Target: + this.DrawTarget(); + break; + + case DataKind.Toast: + this.DrawToast(); + break; + + case DataKind.ImGui: + this.DrawImGui(); + break; + + case DataKind.Tex: + this.DrawTex(); + break; + + case DataKind.Gamepad: + this.DrawGamepad(); + break; + } + } + else + { + ImGui.TextUnformatted("Data not ready."); + } + } + catch (Exception ex) + { + ImGui.TextUnformatted(ex.ToString()); + } + + ImGui.PopStyleVar(); + + ImGui.EndChild(); + } + + private void DrawServerOpCode() + { + ImGui.TextUnformatted(this.serverOpString); + } + + private void DrawAddress() + { + ImGui.InputText(".text sig", ref this.inputSig, 400); + if (ImGui.Button("Resolve")) + { + try + { + this.sigResult = this.dalamud.SigScanner.ScanText(this.inputSig); + } + catch (KeyNotFoundException) + { + this.sigResult = new IntPtr(-1); + } + } + + ImGui.Text($"Result: {this.sigResult.ToInt64():X}"); + ImGui.SameLine(); + if (ImGui.Button($"C{this.copyButtonIndex++}")) + ImGui.SetClipboardText(this.sigResult.ToInt64().ToString("x")); + + foreach (var debugScannedValue in BaseAddressResolver.DebugScannedValues) + { + ImGui.TextUnformatted($"{debugScannedValue.Key}"); + foreach (var valueTuple in debugScannedValue.Value) + { + ImGui.TextUnformatted( + $" {valueTuple.Item1} - 0x{valueTuple.Item2.ToInt64():x}"); + ImGui.SameLine(); + + if (ImGui.Button($"C##copyAddress{this.copyButtonIndex++}")) + ImGui.SetClipboardText(valueTuple.Item2.ToInt64().ToString("x")); + } + } + } + + private void DrawActorTable() + { + var stateString = string.Empty; + + // LocalPlayer is null in a number of situations (at least with the current visible-actors list) + // which would crash here. + if (this.dalamud.ClientState.Actors.Length == 0) + { + ImGui.TextUnformatted("Data not ready."); + } + else if (this.dalamud.ClientState.LocalPlayer == null) + { + ImGui.TextUnformatted("LocalPlayer null."); + } + else + { + stateString += $"FrameworkBase: {this.dalamud.Framework.Address.BaseAddress.ToInt64():X}\n"; + stateString += $"ActorTableLen: {this.dalamud.ClientState.Actors.Length}\n"; + stateString += $"LocalPlayerName: {this.dalamud.ClientState.LocalPlayer.Name}\n"; + stateString += $"CurrentWorldName: {(this.resolveGameData ? this.dalamud.ClientState.LocalPlayer.CurrentWorld.GameData.Name : this.dalamud.ClientState.LocalPlayer.CurrentWorld.Id.ToString())}\n"; + stateString += $"HomeWorldName: {(this.resolveGameData ? this.dalamud.ClientState.LocalPlayer.HomeWorld.GameData.Name : this.dalamud.ClientState.LocalPlayer.HomeWorld.Id.ToString())}\n"; + stateString += $"LocalCID: {this.dalamud.ClientState.LocalContentId:X}\n"; + stateString += $"LastLinkedItem: {this.dalamud.Framework.Gui.Chat.LastLinkedItemId}\n"; + stateString += $"TerritoryType: {this.dalamud.ClientState.TerritoryType}\n\n"; + + ImGui.TextUnformatted(stateString); + + ImGui.Checkbox("Draw actors on screen", ref this.drawActors); + ImGui.SliderFloat("Draw Distance", ref this.maxActorDrawDistance, 2f, 40f); + + for (var i = 0; i < this.dalamud.ClientState.Actors.Length; i++) + { + var actor = this.dalamud.ClientState.Actors[i]; + + if (actor == null) + continue; + + this.PrintActor(actor, i.ToString()); + + if (this.drawActors && this.dalamud.Framework.Gui.WorldToScreen(actor.Position, out var screenCoords)) + { + // 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 screenPos = ImGui.GetMainViewport().Pos; + var screenSize = ImGui.GetMainViewport().Size; + + var windowSize = ImGui.CalcTextSize(actorText); + + // Add some extra safety padding + windowSize.X += ImGui.GetStyle().WindowPadding.X + 10; + windowSize.Y += ImGui.GetStyle().WindowPadding.Y + 10; + + if (screenCoords.X + windowSize.X > screenPos.X + screenSize.X || + screenCoords.Y + windowSize.Y > screenPos.Y + screenSize.Y) + continue; + + if (actor.YalmDistanceX > this.maxActorDrawDistance) + continue; + + ImGui.SetNextWindowPos(new Vector2(screenCoords.X, screenCoords.Y)); + + ImGui.SetNextWindowBgAlpha(Math.Max(1f - (actor.YalmDistanceX / this.maxActorDrawDistance), 0.2f)); + if (ImGui.Begin( + $"Actor{i}##ActorWindow{i}", + ImGuiWindowFlags.NoDecoration | + ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoSavedSettings | + ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoMouseInputs | + ImGuiWindowFlags.NoDocking | + ImGuiWindowFlags.NoFocusOnAppearing | + ImGuiWindowFlags.NoNav)) + ImGui.Text(actorText); + ImGui.End(); + } + } + } + } + + private void DrawFateTable() + { + var stateString = string.Empty; + if (this.dalamud.ClientState.Fates.Length == 0) + { + ImGui.TextUnformatted("No fates or data not ready."); + } + else + { + stateString += $"FrameworkBase: {this.dalamud.Framework.Address.BaseAddress.ToInt64():X}\n"; + stateString += $"FateTableLen: {this.dalamud.ClientState.Fates.Length}\n"; + + ImGui.TextUnformatted(stateString); + + for (var i = 0; i < this.dalamud.ClientState.Fates.Length; i++) + { + var fate = this.dalamud.ClientState.Fates[i]; + if (fate == null) + continue; + + var fateString = $"{fate.Address.ToInt64():X}:[{i}]" + + $" - Lv.{fate.Level} {fate.Name} ({fate.Progress}%)" + + $" - X{fate.Position.X} Y{fate.Position.Y} Z{fate.Position.Z}" + + $" - Territory {(this.resolveGameData ? (fate.TerritoryType.GameData?.Name ?? fate.TerritoryType.Id.ToString()) : fate.TerritoryType.Id.ToString())}\n"; + + fateString += $" StartTimeEpoch: {fate.StartTimeEpoch}" + + $" - Duration: {fate.Duration}" + + $" - State: {fate.State}" + + $" - GameData name: {(this.resolveGameData ? (fate.GameData?.Name ?? fate.FateId.ToString()) : fate.FateId.ToString())}"; + + ImGui.TextUnformatted(fateString); + ImGui.SameLine(); + if (ImGui.Button("C")) + { + ImGui.SetClipboardText(fate.Address.ToString("X")); + } + } + } + } + + private void DrawFontTest() + { + var specialChars = string.Empty; + + for (var i = 0xE020; i <= 0xE0DB; i++) + specialChars += $"0x{i:X} - {(SeIconChar)i} - {(char)i}\n"; + + ImGui.TextUnformatted(specialChars); + + foreach (var fontAwesomeIcon in Enum.GetValues(typeof(FontAwesomeIcon)).Cast()) + { + ImGui.Text(((int)fontAwesomeIcon.ToIconChar()).ToString("X") + " - "); + ImGui.SameLine(); + + ImGui.PushFont(UiBuilder.IconFont); + ImGui.Text(fontAwesomeIcon.ToIconString()); + ImGui.PopFont(); + } + } + + private void DrawPartyList() + { + var partyString = string.Empty; + + if (this.dalamud.ClientState.PartyList.Length == 0) + { + ImGui.TextUnformatted("Data not ready."); + } + else + { + partyString += $"{this.dalamud.ClientState.PartyList.Count} Members\n"; + for (var i = 0; i < this.dalamud.ClientState.PartyList.Count; i++) + { + var member = this.dalamud.ClientState.PartyList[i]; + if (member == null) + { + partyString += + $"[{i}] was null\n"; + continue; + } + + partyString += + $"[{i}] {member.CharacterName} - {member.ObjectKind} - {member.Actor.ActorId}\n"; + } + + ImGui.TextUnformatted(partyString); + } + } + + private void DrawPluginIPC() + { +#pragma warning disable CS0618 // Type or member is obsolete + var i1 = new DalamudPluginInterface(this.dalamud, "DalamudTestSub", null, PluginLoadReason.Unknown); + var i2 = new DalamudPluginInterface(this.dalamud, "DalamudTestPub", null, PluginLoadReason.Unknown); + + if (ImGui.Button("Add test sub")) + { + i1.Subscribe("DalamudTestPub", o => + { + dynamic msg = o; + Log.Debug(msg.Expand); + }); + } + + if (ImGui.Button("Add test sub any")) + { + i1.SubscribeAny((o, a) => + { + dynamic msg = a; + Log.Debug($"From {o}: {msg.Expand}"); + }); + } + + if (ImGui.Button("Remove test sub")) + i1.Unsubscribe("DalamudTestPub"); + + if (ImGui.Button("Remove test sub any")) + i1.UnsubscribeAny(); + + if (ImGui.Button("Send test message")) + { + dynamic testMsg = new ExpandoObject(); + testMsg.Expand = "dong"; + i2.SendMessage(testMsg); + } + + // This doesn't actually work, so don't mind it - impl relies on plugins being registered in PluginManager + if (ImGui.Button("Send test message any")) + { + dynamic testMsg = new ExpandoObject(); + testMsg.Expand = "dong"; + i2.SendMessage("DalamudTestSub", testMsg); + } + + 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 + } + + private void DrawCondition() + { +#if DEBUG + ImGui.Text($"ptr: 0x{this.dalamud.ClientState.Condition.ConditionArrayBase.ToInt64():X}"); +#endif + + ImGui.Text("Current Conditions:"); + ImGui.Separator(); + + var didAny = false; + + for (var i = 0; i < Condition.MaxConditionEntries; i++) + { + var typedCondition = (ConditionFlag)i; + var cond = this.dalamud.ClientState.Condition[typedCondition]; + + if (!cond) continue; + + didAny = true; + + ImGui.Text($"ID: {i} Enum: {typedCondition}"); + } + + if (!didAny) + ImGui.Text("None. Talk to a shop NPC or visit a market board to find out more!!!!!!!"); + } + + private void DrawGauge() + { + var gauge = this.dalamud.ClientState.JobGauges.Get(); + ImGui.Text($"Moon: {gauge.ContainsSeal(SealType.MOON)} Drawn: {gauge.DrawnCard()}"); + } + + private void DrawCommand() + { + foreach (var command in this.dalamud.CommandManager.Commands) + ImGui.Text($"{command.Key}\n -> {command.Value.HelpMessage}\n -> In help: {command.Value.ShowInHelp}\n\n"); + } + + private void DrawAddon() + { + ImGui.InputText("Addon name", ref this.inputAddonName, 256); + ImGui.InputInt("Addon Index", ref this.inputAddonIndex); + + if (ImGui.Button("Get Addon")) + { + this.resultAddon = + this.dalamud.Framework.Gui.GetAddonByName( + this.inputAddonName, this.inputAddonIndex); + } + + if (ImGui.Button("Find Agent")) + this.findAgentInterfacePtr = this.dalamud.Framework.Gui.FindAgentInterface(this.inputAddonName); + + if (this.resultAddon != null) + { + ImGui.TextUnformatted( + $"{this.resultAddon.Name} - 0x{this.resultAddon.Address.ToInt64():x}\n v:{this.resultAddon.Visible} x:{this.resultAddon.X} y:{this.resultAddon.Y} s:{this.resultAddon.Scale}, w:{this.resultAddon.Width}, h:{this.resultAddon.Height}"); + } + + if (this.findAgentInterfacePtr != IntPtr.Zero) + { + ImGui.TextUnformatted( + $"Agent: 0x{this.findAgentInterfacePtr.ToInt64():x}"); + ImGui.SameLine(); + + if (ImGui.Button("C")) + ImGui.SetClipboardText(this.findAgentInterfacePtr.ToInt64().ToString("x")); + } + + if (ImGui.Button("Get Base UI object")) + { + var addr = this.dalamud.Framework.Gui.GetBaseUIObject().ToInt64().ToString("x"); + Log.Information("{0}", addr); + ImGui.SetClipboardText(addr); + } + } + + private void DrawAddonInspector() + { + this.addonInspector ??= new UIDebug(this.dalamud); + this.addonInspector.Draw(); + } + + private void DrawStartInfo() + { + ImGui.Text(JsonConvert.SerializeObject(this.dalamud.StartInfo, Formatting.Indented)); + } + + private void DrawTarget() + { + var targetMgr = this.dalamud.ClientState.Targets; + + if (targetMgr.CurrentTarget != null) + { + this.PrintActor(targetMgr.CurrentTarget, "CurrentTarget"); + Util.ShowObject(targetMgr.CurrentTarget); + } + + if (targetMgr.FocusTarget != null) + this.PrintActor(targetMgr.FocusTarget, "FocusTarget"); + + if (targetMgr.MouseOverTarget != null) + this.PrintActor(targetMgr.MouseOverTarget, "MouseOverTarget"); + + if (targetMgr.PreviousTarget != null) + this.PrintActor(targetMgr.PreviousTarget, "PreviousTarget"); + + if (targetMgr.SoftTarget != null) + this.PrintActor(targetMgr.SoftTarget, "SoftTarget"); + + if (ImGui.Button("Clear CT")) + targetMgr.ClearCurrentTarget(); + + if (ImGui.Button("Clear FT")) + targetMgr.ClearFocusTarget(); + + var localPlayer = this.dalamud.ClientState.LocalPlayer; + + if (localPlayer != null) + { + if (ImGui.Button("Set CT")) + targetMgr.SetCurrentTarget(localPlayer); + + if (ImGui.Button("Set FT")) + targetMgr.SetFocusTarget(localPlayer); + } + else + { + ImGui.Text("LocalPlayer is null."); + } + } + + private void DrawToast() + { + ImGui.InputText("Toast text", ref this.inputTextToast, 200); + + ImGui.Combo("Toast Position", ref this.toastPosition, new[] { "Bottom", "Top", }, 2); + ImGui.Combo("Toast Speed", ref this.toastSpeed, new[] { "Slow", "Fast", }, 2); + ImGui.Combo("Quest Toast Position", ref this.questToastPosition, new[] { "Centre", "Right", "Left" }, 3); + ImGui.Checkbox("Quest Checkmark", ref this.questToastCheckmark); + ImGui.Checkbox("Quest Play Sound", ref this.questToastSound); + ImGui.InputInt("Quest Icon ID", ref this.questToastIconId); + + ImGuiHelpers.ScaledDummy(new Vector2(10, 10)); + + if (ImGui.Button("Show toast")) + { + this.dalamud.Framework.Gui.Toast.ShowNormal(this.inputTextToast, new ToastOptions + { + Position = (ToastPosition)this.toastPosition, + Speed = (ToastSpeed)this.toastSpeed, + }); + } + + if (ImGui.Button("Show Quest toast")) + { + this.dalamud.Framework.Gui.Toast.ShowQuest(this.inputTextToast, new QuestToastOptions + { + Position = (QuestToastPosition)this.questToastPosition, + DisplayCheckmark = this.questToastCheckmark, + IconId = (uint)this.questToastIconId, + PlaySound = this.questToastSound, + }); + } + + if (ImGui.Button("Show Error toast")) + { + this.dalamud.Framework.Gui.Toast.ShowError(this.inputTextToast); + } + } + + private void DrawImGui() + { + ImGui.Text("Monitor count: " + ImGui.GetPlatformIO().Monitors.Size); + ImGui.Text("OverrideGameCursor: " + this.dalamud.InterfaceManager.OverrideGameCursor); + + ImGui.Button("THIS IS A BUTTON###hoverTestButton"); + this.dalamud.InterfaceManager.OverrideGameCursor = !ImGui.IsItemHovered(); + } + + private void DrawTex() + { + ImGui.InputText("Tex Path", ref this.inputTexPath, 255); + ImGui.InputFloat2("UV0", ref this.inputTexUv0); + ImGui.InputFloat2("UV1", ref this.inputTexUv1); + ImGui.InputFloat4("Tint", ref this.inputTintCol); + ImGui.InputFloat2("Scale", ref this.inputTexScale); + + if (ImGui.Button("Load Tex")) + { + try + { + this.debugTex = this.dalamud.Data.GetImGuiTexture(this.inputTexPath); + this.inputTexScale = new Vector2(this.debugTex.Width, this.debugTex.Height); + } + catch (Exception ex) + { + Log.Error(ex, "Could not load tex."); + } + } + + ImGuiHelpers.ScaledDummy(10); + + if (this.debugTex != null) + { + ImGui.Image(this.debugTex.ImGuiHandle, this.inputTexScale, this.inputTexUv0, this.inputTexUv1, this.inputTintCol); + ImGuiHelpers.ScaledDummy(5); + Util.ShowObject(this.debugTex); + } + } + + private void DrawGamepad() + { + static void DrawHelper(string text, uint mask, Func resolve) + { + ImGui.Text($"{text} {mask:X4}"); + ImGui.Text($"DPadLeft {resolve(GamepadButtons.DpadLeft)} " + + $"DPadUp {resolve(GamepadButtons.DpadUp)} " + + $"DPadRight {resolve(GamepadButtons.DpadRight)} " + + $"DPadDown {resolve(GamepadButtons.DpadDown)} "); + ImGui.Text($"West {resolve(GamepadButtons.West)} " + + $"North {resolve(GamepadButtons.North)} " + + $"East {resolve(GamepadButtons.East)} " + + $"South {resolve(GamepadButtons.South)} "); + ImGui.Text($"L1 {resolve(GamepadButtons.L1)} " + + $"L2 {resolve(GamepadButtons.L2)} " + + $"R1 {resolve(GamepadButtons.R1)} " + + $"R2 {resolve(GamepadButtons.R2)} "); + ImGui.Text($"Select {resolve(GamepadButtons.Select)} " + + $"Start {resolve(GamepadButtons.Start)} " + + $"L3 {resolve(GamepadButtons.L3)} " + + $"R3 {resolve(GamepadButtons.R3)} "); + } +#if DEBUG + 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 + + DrawHelper( + "Buttons Raw", + this.dalamud.ClientState.GamepadState.ButtonsRaw, + this.dalamud.ClientState.GamepadState.Raw); + DrawHelper( + "Buttons Pressed", + this.dalamud.ClientState.GamepadState.ButtonsPressed, + this.dalamud.ClientState.GamepadState.Pressed); + DrawHelper( + "Buttons Repeat", + this.dalamud.ClientState.GamepadState.ButtonsRepeat, + this.dalamud.ClientState.GamepadState.Repeat); + DrawHelper( + "Buttons Released", + this.dalamud.ClientState.GamepadState.ButtonsReleased, + this.dalamud.ClientState.GamepadState.Released); + ImGui.Text($"LeftStickLeft {this.dalamud.ClientState.GamepadState.LeftStickLeft:0.00} " + + $"LeftStickUp {this.dalamud.ClientState.GamepadState.LeftStickUp:0.00} " + + $"LeftStickRight {this.dalamud.ClientState.GamepadState.LeftStickRight:0.00} " + + $"LeftStickDown {this.dalamud.ClientState.GamepadState.LeftStickDown:0.00} "); + ImGui.Text($"RightStickLeft {this.dalamud.ClientState.GamepadState.RightStickLeft:0.00} " + + $"RightStickUp {this.dalamud.ClientState.GamepadState.RightStickUp:0.00} " + + $"RightStickRight {this.dalamud.ClientState.GamepadState.RightStickRight:0.00} " + + $"RightStickDown {this.dalamud.ClientState.GamepadState.RightStickDown:0.00} "); + } + + private void Load() + { + if (this.dalamud.Data.IsDataReady) + { + this.serverOpString = JsonConvert.SerializeObject(this.dalamud.Data.ServerOpCodes, Formatting.Indented); + this.wasReady = true; + } + } + + private void PrintActor(Actor actor, string tag) + { + var actorString = + $"{actor.Address.ToInt64():X}:{actor.ActorId:X}[{tag}] - {actor.ObjectKind} - {actor.Name} - X{actor.Position.X} Y{actor.Position.Y} Z{actor.Position.Z} D{actor.YalmDistanceX} R{actor.Rotation} - Target: {actor.TargetActorID:X}\n"; + + if (actor is Npc npc) + actorString += $" DataId: {npc.BaseId} NameId:{npc.NameId}\n"; + + if (actor is Chara chara) + { + actorString += + $" Level: {chara.Level} ClassJob: {(this.resolveGameData ? chara.ClassJob.GameData.Name : chara.ClassJob.Id.ToString())} CHP: {chara.CurrentHp} MHP: {chara.MaxHp} CMP: {chara.CurrentMp} MMP: {chara.MaxMp}\n Customize: {BitConverter.ToString(chara.Customize).Replace("-", " ")} StatusFlags: {chara.StatusFlags}\n"; + } + + if (actor is PlayerCharacter pc) + { + actorString += + $" HomeWorld: {(this.resolveGameData ? pc.HomeWorld.GameData.Name : pc.HomeWorld.Id.ToString())} CurrentWorld: {(this.resolveGameData ? pc.CurrentWorld.GameData.Name : pc.CurrentWorld.Id.ToString())} FC: {pc.CompanyTag}\n"; + } + + ImGui.TextUnformatted(actorString); + ImGui.SameLine(); + if (ImGui.Button($"C##{this.copyButtonIndex++}")) + { + ImGui.SetClipboardText(actor.Address.ToInt64().ToString("X")); + } + } + } +} 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/Internal/Windows/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs new file mode 100644 index 000000000..e3f867211 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs @@ -0,0 +1,1286 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +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; +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 + { + var buttonText = Locs.PluginButton_InstallVersion(versionString); + if (ImGui.Button($"{buttonText}##{buttonText}{index}")) + { + this.installStatus = OperationStatus.InProgress; + + Task.Run(() => this.dalamud.PluginManager.InstallPlugin(manifest, useTesting, PluginLoadReason.Installer)) + .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(); + } + + if (ImGui.Selectable(Locs.PluginContext_DeletePluginConfig)) + { + Log.Debug($"Deleting config for {manifest.InternalName}"); + + this.installStatus = OperationStatus.InProgress; + + Task.Run(() => + { + this.dalamud.PluginManager.PluginConfigs.Delete(manifest.InternalName); + + var path = Path.Combine(this.dalamud.StartInfo.PluginDirectory, manifest.InternalName); + if (Directory.Exists(path)) + Directory.Delete(path, true); + }) + .ContinueWith(task => + { + this.installStatus = OperationStatus.Idle; + + this.DisplayErrorContinuation(task, Locs.ErrorModal_DeleteConfigFail(manifest.InternalName)); + }); + } + + 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; + } + + var availablePluginUpdate = this.pluginListUpdatable.FirstOrDefault(up => up.InstalledPlugin == plugin); + // Update available + if (availablePluginUpdate != 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; + } + } + } + + // Outdated API level + if (plugin.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel) + { + label += Locs.PluginTitleMod_OutdatedError; + } + + 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); + } + + if (plugin.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel) + { + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + ImGui.TextWrapped(Locs.PluginBody_Outdated); + ImGui.PopStyleColor(); + } + + // 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); + + if (availablePluginUpdate != default) + this.DrawUpdateSinglePluginButton(availablePluginUpdate); + + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{plugin.Manifest.AssemblyVersion}"); + + if (plugin.IsDev) + { + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.DalamudRed, Locs.PluginBody_DeleteDevPlugin); + } + + ImGui.Unindent(); + } + + if (ImGui.BeginPopupContextItem("InstalledItemContextMenu")) + { + if (ImGui.Selectable(Locs.PluginContext_DeletePluginConfigReload)) + { + Log.Debug($"Deleting config for {plugin.Manifest.InternalName}"); + + this.installStatus = OperationStatus.InProgress; + + Task.Run(() => this.dalamud.PluginManager.DeleteConfiguration(plugin)) + .ContinueWith(task => + { + this.installStatus = OperationStatus.Idle; + + this.DisplayErrorContinuation(task, Locs.ErrorModal_DeleteConfigFail(plugin.Name)); + }); + } + + ImGui.EndPopup(); + } + } + + 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; + + // Disable everything if the plugin is outdated + disabled = disabled || (plugin.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !this.dalamud.Configuration.LoadAllApiLevels); + + 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(PluginLoadReason.Installer)) + .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 DrawUpdateSinglePluginButton(AvailablePluginUpdate update) + { + ImGui.SameLine(); + + if (ImGuiComponents.IconButton(FontAwesomeIcon.Download)) + { + this.installStatus = OperationStatus.InProgress; + + Task.Run(() => this.dalamud.PluginManager.UpdateSinglePlugin(update, true, false)) + .ContinueWith(task => + { + // There is no need to set as Complete for an individual plugin installation + this.installStatus = OperationStatus.Idle; + + var errorMessage = Locs.ErrorModal_SingleUpdateFail(update.UpdateManifest.Name); + this.DisplayErrorContinuation(task, errorMessage); + + if (!task.Result.WasUpdated) + { + this.ShowErrorModal(errorMessage); + } + }); + } + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(Locs.PluginButtonToolTip_UpdateSingle(update.UpdateManifest.AssemblyVersion.ToString())); + } + + 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) + { + var 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)) + errorModalMessage += $"\n\n{ex.Message}"; +#endif + } + else + { + Log.Error(ex, "Plugin installer threw an unexpected error"); +#if DEBUG + if (!string.IsNullOrEmpty(ex.Message)) + errorModalMessage += $"\n\n{ex.Message}"; +#endif + } + } + + this.ShowErrorModal(errorModalMessage); + + return false; + } + + return true; + } + + private void ShowErrorModal(string message) + { + this.errorModalMessage = message; + this.errorModalDrawing = true; + this.errorModalOnNextFrame = 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)"); + + public static string PluginTitleMod_OutdatedError => Loc.Localize("InstallerOutdatedError", " (outdated)"); + + #endregion + + #region Plugin context menu + + public static string PluginContext_HidePlugin => Loc.Localize("InstallerHidePlugin", "Hide from installer"); + + public static string PluginContext_DeletePluginConfig => Loc.Localize("InstallerDeletePluginConfig", "Reset plugin"); + + public static string PluginContext_DeletePluginConfigReload => Loc.Localize("InstallerDeletePluginConfig", "Reset plugin settings & reload"); + + #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."); + + public static string PluginBody_Outdated => Loc.Localize("InstallerOutdatedPluginBody ", "This plugin is outdated and incompatible at the moment. Please wait for it to be updated by its author."); + + #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"); + + public static string PluginButtonToolTip_UpdateSingle(string version) => Loc.Localize("InstallerUpdateSingle", "Update to {0}").Format(version); + + #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_SingleUpdateFail(string name) => Loc.Localize("InstallerSingleUpdateFail", "Failed to update plugin {0}.").Format(name); + + public static string ErrorModal_DeleteConfigFail(string name) => Loc.Localize("InstallerDeleteConfigFail", "Failed to reset the plugin {0}.\n\nThe plugin may not support this action. You can try deleting the configuration manually while the game is shut down - please see the FAQ.").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 88% rename from Dalamud/Interface/DalamudSettingsWindow.cs rename to Dalamud/Interface/Internal/Windows/SettingsWindow.cs index 544e14b67..f5c13dd0a 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,8 @@ namespace Dalamud.Interface private bool doDocking; private bool doViewport; private bool doGamepad; - private List thirdRepoList; + private List thirdRepoList; + private bool thirdRepoListChanged; private bool printPluginsWelcomeMsg; private bool autoUpdatePlugins; @@ -60,10 +61,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 +143,22 @@ namespace Dalamud.Interface /// public override void OnOpen() { - base.OnOpen(); - - Log.Information("OnOpen start"); - - Log.Information("OnOpen end"); + this.thirdRepoListChanged = false; } /// 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 +168,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 +186,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 +223,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 +236,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 +256,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 +303,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 +319,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(); @@ -345,6 +339,7 @@ namespace Dalamud.Interface if (toRemove != null) { this.thirdRepoList.Remove(toRemove); + this.thirdRepoListChanged = true; } ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (ImGui.GetColumnWidth() / 2) - 8 - (ImGui.CalcTextSize(repoNumber.ToString()).X / 2)); @@ -353,9 +348,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,17 +359,16 @@ namespace Dalamud.Interface } else { - this.thirdRepoList.Add(new ThirdRepoSetting + this.thirdRepoList.Add(new ThirdPartyRepoSettings { Url = this.thirdRepoTempUrl, IsEnabled = true, }); - + this.thirdRepoListChanged = true; this.thirdRepoTempUrl = string.Empty; } } - ImGui.PopFont(); ImGui.Columns(1); ImGui.EndTabItem(); @@ -393,6 +387,12 @@ namespace Dalamud.Interface if (ImGui.Button(Loc.Localize("Save", "Save"))) { this.Save(); + + if (this.thirdRepoListChanged) + { + this.dalamud.PluginManager.SetPluginReposFromConfig(true); + this.thirdRepoListChanged = false; + } } ImGui.SameLine(); @@ -400,6 +400,13 @@ namespace Dalamud.Interface if (ImGui.Button(Loc.Localize("SaveAndClose", "Save and Close"))) { this.Save(); + + if (this.thirdRepoListChanged) + { + this.dalamud.PluginManager.SetPluginReposFromConfig(true); + this.thirdRepoListChanged = false; + } + this.IsOpen = false; } } @@ -456,7 +463,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..e24ab8bc1 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(); } /// @@ -57,6 +59,11 @@ namespace Dalamud.Interface /// public static ImFontPtr IconFont => InterfaceManager.IconFont; + /// + /// Gets the default Dalamud monospaced font based on Inconsolata Regular in 16pt. + /// + public static ImFontPtr MonoFont => InterfaceManager.MonoFont; + /// /// Gets the game's active Direct3D device. /// @@ -217,8 +224,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..0e72955af --- /dev/null +++ b/Dalamud/Memory/MemoryHelper.cs @@ -0,0 +1,649 @@ +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 FFXIVClientStructs.FFXIV.Client.System.String; + +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(marshal); + 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); + + var eos = Array.IndexOf(buffer, (byte)0); + if (eos < 0) + { + return seStringManager.Parse(buffer); + } + else + { + var newBuffer = new byte[eos]; + Buffer.BlockCopy(buffer, 0, newBuffer, 0, eos); + return seStringManager.Parse(newBuffer); + } + } + + /// + /// Read an SeString from a specified Utf8String structure. + /// + /// The memory address to read from. + /// The read in string. + public static unsafe SeString ReadSeString(Utf8String* utf8String) + { + if (utf8String == null) + return string.Empty; + + var ptr = utf8String->StringPtr; + if (ptr == null) + return string.Empty; + + var len = Math.Max(utf8String->BufUsed, utf8String->StringLength); + + return ReadSeString((IntPtr)ptr, (int)len); + } + + #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); + + /// + /// Read an SeString from a specified Utf8String structure. + /// + /// The memory address to read from. + /// The read in string. + public static unsafe void ReadSeString(Utf8String* utf8String, out SeString value) + => value = ReadSeString(utf8String); + + #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..9ab55d24c 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,10 @@ 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. + /// The reason the plugin was loaded. + internal DalamudPluginInterface(Dalamud dalamud, string pluginName, string assemblyLocation, PluginLoadReason reason) { - this.Reason = reason; this.CommandManager = dalamud.CommandManager; this.Framework = dalamud.Framework; this.ClientState = dalamud.ClientState; @@ -49,7 +49,9 @@ namespace Dalamud.Plugin this.dalamud = dalamud; this.pluginName = pluginName; - this.configs = configs; + this.configs = dalamud.PluginManager.PluginConfigs; + this.AssemblyLocation = assemblyLocation; + this.Reason = reason; this.GeneralChatType = this.dalamud.Configuration.GeneralChatType; this.Sanitizer = new Sanitizer(this.Data.Language); @@ -60,7 +62,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"; @@ -86,6 +88,11 @@ namespace Dalamud.Plugin /// public PluginLoadReason Reason { get; } + /// + /// Gets the plugin assembly location. + /// + public string AssemblyLocation { get; private set; } + /// /// Gets the directory Dalamud assets are stored in. /// @@ -142,7 +149,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 +199,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 +280,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 +332,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 +354,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 +371,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..77b3be253 --- /dev/null +++ b/Dalamud/Plugin/Internal/LocalDevPlugin.cs @@ -0,0 +1,151 @@ +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) + { + if (!dalamud.Configuration.DevPluginSettings.TryGetValue(dllFile.FullName, out this.devSettings)) + { + dalamud.Configuration.DevPluginSettings[dllFile.FullName] = this.devSettings = new DevPluginSettings(); + dalamud.Configuration.Save(); + } + + 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..f7bbe9615 --- /dev/null +++ b/Dalamud/Plugin/Internal/LocalPlugin.cs @@ -0,0 +1,414 @@ +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. + /// + /// The reason why this plugin is being loaded. + /// Load while reloading. + public void Load(PluginLoadReason reason, 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 && !this.dalamud.Configuration.LoadAllApiLevels) + throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level"); + + if (this.Manifest.Disabled) + throw new InvalidPluginOperationException($"Unable to load {this.Name}, disabled"); + + this.State = PluginState.InProgress; + Log.Information($"Loading {this.DllFile.Name}"); + + 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, reason); + + 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, LoadError(we are cleaning this up while we're at it) + switch (this.State) + { + case PluginState.InProgress: + throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already working"); + case PluginState.Unloaded: + throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already unloaded"); + case PluginState.UnloadError: + throw new InvalidPluginOperationException($"Unable to unload {this.Name}, unload previously faulted, restart Dalamud"); + } + + try + { + this.State = PluginState.InProgress; + Log.Information($"Unloading {this.DllFile.Name}"); + + this.instance?.Dispose(); + this.instance = null; + + this.DalamudInterface?.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(PluginLoadReason.Reload, 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..5f1165742 --- /dev/null +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -0,0 +1,1088 @@ +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.Threading; +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 JetBrains.Annotations; +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 = 4; + + 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.SetPluginReposFromConfig(false); + + 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; private set; } = 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}"); + } + } + } + + /// + /// Set the list of repositories to use. Should be called when the Settings window has been updated or at instantiation. + /// + /// Whether the available plugins changed should be evented after. + public void SetPluginReposFromConfig(bool notify) + { + var repos = new List() { PluginRepository.MainRepo }; + repos.AddRange(this.dalamud.Configuration.ThirdRepoList + .Select(repo => new PluginRepository(repo.Url, repo.IsEnabled))); + + this.Repos = repos; + + if (notify) + this.NotifyAvailablePluginsChanged(); + } + + /// + /// 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, PluginLoadReason.Boot, pluginDef.IsDev, isBoot: true); + } + catch (InvalidPluginException) + { + // Not a plugin + } + catch (Exception) + { + Log.Error("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.Reload(); + } + 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, PluginLoadReason.Installer, 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. + /// The reason this plugin was loaded. + public void InstallPlugin(RemotePluginManifest repoManifest, bool useTesting, PluginLoadReason reason) + { + 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, reason); + + this.NotifyInstalledPluginsChanged(); + } + + /// + /// Load a plugin. + /// + /// The associated with the main assembly of this plugin. + /// The already loaded definition, if available. + /// The reason this plugin was loaded. + /// 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, PluginLoadReason reason, 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; + + // If we're not loading it, make sure it's disabled + if (!loadPlugin && !devPlugin.IsDisabled) + devPlugin.Disable(); + + 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(reason); + } + catch (InvalidPluginException) + { + PluginLocations.Remove(plugin.AssemblyName.FullName); + throw; + } + catch (Exception ex) + { + if (plugin.IsDev) + { + // Dev plugins always get added to the list so they can be fiddled with in the UI + Log.Information(ex, $"Dev plugin failed to load, adding anyways: {dllFile.Name}"); + plugin.Disable(); // Disable here, otherwise you can't enable+load later + } + else if (plugin.Manifest.DalamudApiLevel < DalamudApiLevel) + { + // Out of date plugins get added so they can be updated. + Log.Information(ex, $"Plugin was outdated, adding anyways: {dllFile.Name}"); + // plugin.Disable(); // Don't disable, or it gets deleted next boot. + } + 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.NotifyAvailablePluginsChanged(); + 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(true); + } + + 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 - 1 && !this.dalamud.Configuration.LoadAllApiLevels) + { + 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 updatedList = new List(); + + // Prevent collection was modified errors + for (var i = 0; i < this.updatablePlugins.Count; i++) + { + updatedList.Add(this.UpdateSinglePlugin(this.updatablePlugins[i], false, dryRun)); + } + + this.NotifyInstalledPluginsChanged(); + + Log.Information("Plugin update OK."); + + return updatedList; + } + + /// + /// Update a single plugin, provided a valid . + /// + /// The available plugin update. + /// Whether to notify that installed plugins have changed afterwards. + /// Whether or not to actually perform the update, or just indicate success. + /// The status of the update. + [CanBeNull] + public PluginUpdateStatus UpdateSinglePlugin(AvailablePluginUpdate metadata, bool notify, bool dryRun) + { + var plugin = metadata.InstalledPlugin; + + // Can't update that! + if (plugin is LocalDevPlugin) + return null; + + 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; + } + else + { + updateStatus.WasUpdated = true; + + // 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)"); + updateStatus.WasUpdated = false; + return updateStatus; + } + } + + try + { + plugin.Disable(); + this.installedPlugins.Remove(plugin); + } + catch (Exception ex) + { + Log.Error(ex, "Error during disable (update)"); + updateStatus.WasUpdated = false; + return updateStatus; + } + + try + { + this.InstallPlugin(metadata.UpdateManifest, metadata.UseTesting, PluginLoadReason.Update); + } + catch (Exception ex) + { + Log.Error(ex, "Error during install (update)"); + updateStatus.WasUpdated = false; + return updateStatus; + } + } + + if (notify && updateStatus.WasUpdated) + this.NotifyInstalledPluginsChanged(); + + return updateStatus; + } + + /// + /// Unload the plugin, delete its configuration, and reload it. + /// + /// The plugin. + /// Throws if the plugin is still loading/unloading. + public void DeleteConfiguration(LocalPlugin plugin) + { + if (plugin.State == PluginState.InProgress) + throw new Exception("Cannot delete configuration for a loading/unloading plugin"); + + if (plugin.IsLoaded) + plugin.Unload(); + + // Let's wait so any handles on files in plugin configurations can be closed + Thread.Sleep(500); + + this.PluginConfigs.Delete(plugin.Name); + + Thread.Sleep(500); + + // Let's indicate "installer" here since this is supposed to be a fresh install + plugin.Load(PluginLoadReason.Installer); + } + + /// + /// 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 + { + Message = 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 && !this.dalamud.Configuration.LoadAllApiLevels) + 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 index 789d4d094..846525b0f 100644 --- a/Dalamud/Plugin/PluginLoadReason.cs +++ b/Dalamud/Plugin/PluginLoadReason.cs @@ -15,6 +15,16 @@ namespace Dalamud.Plugin /// Installer, + /// + /// This plugin was loaded because it was just updated. + /// + Update, + + /// + /// This plugin was loaded because it was told to reload. + /// + Reload, + /// /// This plugin was loaded because the game was started or Dalamud was reinjected. /// 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..69b0df23e 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,10 @@ 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, + DalamudGitHash = Util.GetGitHash(), + GameVersion = dalamud.StartInfo.GameVersion.ToString(), Language = dalamud.StartInfo.Language.ToString(), DoDalamudTest = dalamud.Configuration.DoDalamudTest, DoPluginTest = dalamud.Configuration.DoPluginTest, @@ -48,10 +49,12 @@ namespace Dalamud private class TroubleshootingPayload { - public PluginDefinition[] LoadedPlugins { get; set; } + public PluginManifest[] LoadedPlugins { get; set; } public string DalamudVersion { get; set; } + public string DalamudGitHash { get; set; } + public string GameVersion { get; set; } public string Language { get; set; } @@ -62,7 +65,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/README.md b/README.md index f85b9cc30..7069facfa 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,15 @@ If you need any support regarding the API or usage of Dalamud, please [join our Thanks to Mino, whose work has made this possible! +## Branches + +We are currently working from the following branches. + +| Name | Purpose | .NET Version | +|---|---|---| +| *master* | .NET Core rework, in-development, will replace stable | .NET 5.0.6 (May 2021) | +| *api3* | Legacy, current stable version, to be replaced in November | .NET 4.7.5 (April 2017) | +
##### Final Fantasy XIV © 2010-2021 SQUARE ENIX CO., LTD. All Rights Reserved. We are not affiliated with SQUARE ENIX CO., LTD. in any way. 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