diff --git a/.editorconfig b/.editorconfig
index 8816a8511..2dbbff2dc 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -24,6 +24,7 @@ csharp_preferred_modifier_order = public, private, protected, internal, new, abs
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
+dotnet_code_quality_unused_parameters = non_public
dotnet_naming_rule.event_rule.severity = warning
dotnet_naming_rule.event_rule.style = on_upper_camel_case_style
dotnet_naming_rule.event_rule.symbols = event_symbols
@@ -56,11 +57,13 @@ dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly
-dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:suggestion
-dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:suggestion
-dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:suggestion
+dotnet_style_parentheses_in_arithmetic_binary_operators =always_for_clarity:suggestion
+dotnet_style_parentheses_in_other_binary_operators =always_for_clarity:suggestion
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
+dotnet_style_parentheses_in_other_operators=always_for_clarity:silent
+dotnet_style_object_initializer = false
# ReSharper properties
resharper_align_linq_query = true
@@ -105,8 +108,10 @@ resharper_redundant_base_qualifier_highlighting = none
resharper_suggest_var_or_type_built_in_types_highlighting = hint
resharper_suggest_var_or_type_elsewhere_highlighting = hint
resharper_suggest_var_or_type_simple_types_highlighting = hint
+csharp_style_deconstructed_variable_declaration=true:silent
[*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}]
indent_style = space
indent_size = 4
tab_width = 4
+dotnet_style_parentheses_in_other_operators=always_for_clarity:silent
diff --git a/.github/workflows/delete-artifacts.yml b/.github/workflows/delete-artifacts.yml
index 7eb2b001c..6191e6cba 100644
--- a/.github/workflows/delete-artifacts.yml
+++ b/.github/workflows/delete-artifacts.yml
@@ -12,7 +12,7 @@ jobs:
steps:
- name: Remove old artifacts
- uses: c-hive/gha-remove-artifacts@24dc23384a1fa6a058b79c73727ae0cb2200ca4c
+ uses: c-hive/gha-remove-artifacts@v1.2.0
with:
age: '1 month'
skip-tags: true
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 2293db95d..f19420022 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -7,15 +7,12 @@ jobs:
name: Build on Windows
runs-on: windows-2019
steps:
- - uses: actions/checkout@v2
+ - name: Checkout Dalamud
+ uses: actions/checkout@v2
with:
submodules: recursive
- - name: Setup Nuget
- uses: nuget/setup-nuget@v1
- with:
- nuget-version: latest
- - name: Restore Nuget Packages
- run: nuget restore Dalamud.sln
+ - name: Setup MSBuild
+ uses: microsoft/setup-msbuild@v1.0.2
- name: Define VERSION
run: |
$env:COMMIT = $env:GITHUB_SHA.Substring(0, 7)
@@ -25,18 +22,17 @@ jobs:
($env:REPO_NAME) >> VERSION
($env:BRANCH) >> VERSION
($env:COMMIT) >> VERSION
- - name: Build DotNet4
- run: |
- cd "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\"
- .\MSBuild.exe $Env:GITHUB_WORKSPACE\Dalamud.sln /t:Build /p:Configuration=Release /p:DefineConstants=XL_NOAUTOUPDATE
- - name: Run xUnit Tests
- run: |
- ${{github.workspace}}\packages\xunit.runner.console.2.4.1\tools\net472\xunit.console.exe ${{github.workspace}}\Dalamud.Test\bin\Release\Dalamud.Test.dll
+ - name: Build Dalamud
+ run: .\build.ps1 compile
+ - name: Test Dalamud
+ run: .\build.ps1 test
+ - name: Create hashlist
+ run: .\CreateHashList.ps1 .\bin\Release
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: dalamud-artifact
- path: bin\
+ path: bin\Release
deploy_stg:
name: Deploy dalamud-distrib staging
diff --git a/.gitmodules b/.gitmodules
index 57306103a..f063dd0b3 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,6 @@
[submodule "lib/FFXIVClientStructs"]
path = lib/FFXIVClientStructs
url = https://github.com/goatcorp/FFXIVClientStructs.git
+[submodule "lib/SharpDX.Desktop"]
+ path = lib/SharpDX.Desktop
+ url = https://github.com/goatcorp/SharpDX.Desktop.git
diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json
new file mode 100644
index 000000000..65ec356a7
--- /dev/null
+++ b/.nuke/build.schema.json
@@ -0,0 +1,112 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "Build Schema",
+ "$ref": "#/definitions/build",
+ "definitions": {
+ "build": {
+ "type": "object",
+ "properties": {
+ "Configuration": {
+ "type": "string",
+ "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
+ "enum": [
+ "Debug",
+ "Release"
+ ]
+ },
+ "Continue": {
+ "type": "boolean",
+ "description": "Indicates to continue a previously failed build attempt"
+ },
+ "Help": {
+ "type": "boolean",
+ "description": "Shows the help text for this build assembly"
+ },
+ "Host": {
+ "type": "string",
+ "description": "Host for execution. Default is 'automatic'",
+ "enum": [
+ "AppVeyor",
+ "AzurePipelines",
+ "Bamboo",
+ "Bitrise",
+ "GitHubActions",
+ "GitLab",
+ "Jenkins",
+ "SpaceAutomation",
+ "TeamCity",
+ "Terminal",
+ "TravisCI"
+ ]
+ },
+ "NoLogo": {
+ "type": "boolean",
+ "description": "Disables displaying the NUKE logo"
+ },
+ "Plan": {
+ "type": "boolean",
+ "description": "Shows the execution plan (HTML)"
+ },
+ "Profile": {
+ "type": "array",
+ "description": "Defines the profiles to load",
+ "items": {
+ "type": "string"
+ }
+ },
+ "Root": {
+ "type": "string",
+ "description": "Root directory during build execution"
+ },
+ "Skip": {
+ "type": "array",
+ "description": "List of targets to be skipped. Empty list skips all dependencies",
+ "items": {
+ "type": "string",
+ "enum": [
+ "Clean",
+ "Compile",
+ "CompileDalamud",
+ "CompileDalamudBoot",
+ "CompileInjector",
+ "CompileInjectorBoot",
+ "Restore",
+ "Test"
+ ]
+ }
+ },
+ "Solution": {
+ "type": "string",
+ "description": "Path to a solution file that is automatically loaded"
+ },
+ "Target": {
+ "type": "array",
+ "description": "List of targets to be invoked. Default is '{default_target}'",
+ "items": {
+ "type": "string",
+ "enum": [
+ "Clean",
+ "Compile",
+ "CompileDalamud",
+ "CompileDalamudBoot",
+ "CompileInjector",
+ "CompileInjectorBoot",
+ "Restore",
+ "Test"
+ ]
+ }
+ },
+ "Verbosity": {
+ "type": "string",
+ "description": "Logging verbosity during build execution. Default is 'Normal'",
+ "enum": [
+ "Minimal",
+ "Normal",
+ "Quiet",
+ "Verbose"
+ ]
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.nuke/parameters.json b/.nuke/parameters.json
new file mode 100644
index 000000000..34f14e26a
--- /dev/null
+++ b/.nuke/parameters.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "./build.schema.json",
+ "Solution": "Dalamud.sln"
+}
\ No newline at end of file
diff --git a/CreateHashList.ps1 b/CreateHashList.ps1
index 668c1c140..389c2adec 100644
--- a/CreateHashList.ps1
+++ b/CreateHashList.ps1
@@ -1,7 +1,11 @@
-$hashes = @{}
+$hashes = [ordered]@{}
-Get-ChildItem $args[0] -Exclude dalamud.txt,*.zip,*.pdb,*.ipdb | Foreach-Object {
- $hashes.Add($_.Name, (Get-FileHash $_.FullName -Algorithm MD5).Hash)
+Set-Location $args[0]
+
+Get-ChildItem -File -Recurse -Exclude dalamud.txt,*.zip,*.pdb,*.ipdb | Foreach-Object {
+ $key = ($_.FullName | Resolve-Path -Relative).TrimStart(".\\")
+ $val = (Get-FileHash $_.FullName -Algorithm MD5).Hash
+ $hashes.Add($key, $val)
}
-ConvertTo-Json $hashes | Out-File -FilePath (Join-Path $args[0] "hashes.json")
\ No newline at end of file
+$hashes | ConvertTo-Json | Out-File -FilePath "hashes.json"
\ No newline at end of file
diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj
new file mode 100644
index 000000000..b1158d85a
--- /dev/null
+++ b/Dalamud.Boot/Dalamud.Boot.vcxproj
@@ -0,0 +1,105 @@
+
+
+
+ {55198DC3-A03D-408E-A8EB-2077780C8576}
+ Dalamud_Boot
+ Debug
+ x64
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 16.0
+ Win32Proj
+ 10.0
+
+
+
+ DynamicLibrary
+ true
+ v142
+ false
+ Unicode
+ ..\bin\$(Configuration)\
+ obj\$(Configuration)\
+
+
+
+
+ Level3
+ true
+ true
+ stdcpplatest
+ MultiThreadedDebug
+ pch.h
+ ProgramDatabase
+ CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+
+
+ Windows
+ true
+ false
+ ..\lib\CoreCLR;%(AdditionalLibraryDirectories)
+
+
+
+
+ true
+ false
+ _DEBUG;%(PreprocessorDefinitions)
+
+
+ false
+ false
+
+
+
+
+ true
+ true
+ NDEBUG;%(PreprocessorDefinitions)
+
+
+ true
+ true
+
+
+
+
+
+ nethost.dll
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters
new file mode 100644
index 000000000..afcc6e502
--- /dev/null
+++ b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters
@@ -0,0 +1,62 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
+
+
+ {18be40ac-9367-46ff-b848-4c528aa97a8d}
+ lib
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+
+
+ Library Files
+
+
+ Library Files
+
+
+
\ No newline at end of file
diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp
new file mode 100644
index 000000000..1abde0ed3
--- /dev/null
+++ b/Dalamud.Boot/dllmain.cpp
@@ -0,0 +1,66 @@
+#define WIN32_LEAN_AND_MEAN
+#define DllExport extern "C" __declspec(dllexport)
+
+#include
+#include
+#include "..\lib\CoreCLR\CoreCLR.h"
+#include "..\lib\CoreCLR\boot.h"
+
+HMODULE g_hModule;
+
+DllExport DWORD WINAPI Initialize(LPVOID lpParam)
+{
+ #if defined(_DEBUG)
+ ConsoleSetup(L"Dalamud Boot");
+ #endif
+
+ wchar_t _module_path[MAX_PATH];
+ GetModuleFileNameW(g_hModule, _module_path, sizeof _module_path / 2);
+ std::filesystem::path fs_module_path(_module_path);
+
+ std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.runtimeconfig.json").c_str());
+ std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.dll").c_str());
+
+ // =========================================================================== //
+
+ void* entrypoint_vfn;
+ int result = InitializeClrAndGetEntryPoint(
+ runtimeconfig_path,
+ module_path,
+ L"Dalamud.EntryPoint, Dalamud",
+ L"Initialize",
+ L"Dalamud.EntryPoint+InitDelegate, Dalamud",
+ &entrypoint_vfn);
+
+ if (result != 0)
+ return result;
+
+ typedef void (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(LPVOID);
+ custom_component_entry_point_fn entrypoint_fn = reinterpret_cast(entrypoint_vfn);
+
+ printf("Initializing Dalamud... ");
+ entrypoint_fn(lpParam);
+ printf("Done!\n");
+
+ // =========================================================================== //
+
+ #if defined(_DEBUG)
+ FreeConsole();
+ #endif
+
+ return 0;
+}
+
+BOOL APIENTRY DllMain(const HMODULE hModule, const DWORD dwReason, LPVOID lpReserved) {
+ DisableThreadLibraryCalls(hModule);
+
+ switch (dwReason)
+ {
+ case DLL_PROCESS_ATTACH:
+ g_hModule = hModule;
+ break;
+ case DLL_PROCESS_DETACH:
+ break;
+ }
+ return TRUE;
+}
diff --git a/Dalamud.Boot/main.cpp b/Dalamud.Boot/main.cpp
new file mode 100644
index 000000000..0eb0f9055
--- /dev/null
+++ b/Dalamud.Boot/main.cpp
@@ -0,0 +1,49 @@
+#define WIN32_LEAN_AND_MEAN
+
+#include
+#include
+#include "CoreCLR.h"
+#include "boot.h"
+
+int wmain(int argc, char** argv)
+{
+ #if defined(_DEBUG)
+ ConsoleSetup(L"Dalamud Injector Boot");
+ #endif
+
+ wchar_t _module_path[MAX_PATH];
+ GetModuleFileNameW(NULL, _module_path, sizeof _module_path / 2);
+ std::filesystem::path fs_module_path(_module_path);
+
+ std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.runtimeconfig.json").c_str());
+ std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.dll").c_str());
+
+ // =========================================================================== //
+
+ void* entrypoint_vfn;
+ int result = InitializeClrAndGetEntryPoint(
+ runtimeconfig_path,
+ module_path,
+ L"Dalamud.Injector.EntryPoint, Dalamud.Injector",
+ L"Main",
+ L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector",
+ &entrypoint_vfn);
+
+ if (result != 0)
+ return result;
+
+ typedef void (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, char**);
+ custom_component_entry_point_fn entrypoint_fn = reinterpret_cast(entrypoint_vfn);
+
+ printf("Running Dalamud Injector... ");
+ entrypoint_fn(argc, argv);
+ printf("Done!\n");
+
+ // =========================================================================== //
+
+ #if defined(_DEBUG)
+ FreeConsole();
+ #endif
+
+ return 0;
+}
diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj
new file mode 100644
index 000000000..8c714785c
--- /dev/null
+++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj
@@ -0,0 +1,61 @@
+
+
+ Dalamud.CorePlugin
+ net5.0-windows
+ x64
+ x64;AnyCPU
+ 9.0
+ true
+ false
+ false
+ IDE0003
+
+
+
+ true
+ full
+ false
+ $(appData)\XIVLauncher\devPlugins\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+
+ pdbonly
+ true
+ $(appData)\XIVLauncher\devPlugins\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+
diff --git a/Dalamud.CorePlugin/GlobalSuppressions.cs b/Dalamud.CorePlugin/GlobalSuppressions.cs
new file mode 100644
index 000000000..9b5d219bb
--- /dev/null
+++ b/Dalamud.CorePlugin/GlobalSuppressions.cs
@@ -0,0 +1,16 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+// General
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "Preventing long lines", Scope = "namespaceanddescendants", Target = "~N:Dalamud.CorePlugin")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "I like regions", Scope = "namespaceanddescendants", Target = "~N:Dalamud.CorePlugin")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1123:Do not place regions within elements", Justification = "I like regions in elements too", Scope = "namespaceanddescendants", Target = "~N:Dalamud.CorePlugin")]
+[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying", Scope = "namespaceanddescendants", Target = "~N:Dalamud.CorePlugin")]
+[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud.CorePlugin")]
+[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud.CorePlugin")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1127:Generic type constraints should be on their own line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud.CorePlugin")]
+[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")]
diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs
new file mode 100644
index 000000000..d2f140d72
--- /dev/null
+++ b/Dalamud.CorePlugin/PluginImpl.cs
@@ -0,0 +1,96 @@
+using System;
+using System.IO;
+
+using Dalamud.Interface.Windowing;
+using Dalamud.Plugin;
+
+namespace Dalamud.CorePlugin
+{
+ ///
+ /// This class is a a plugin testbed for developing new Dalamud features with easy access to Dalamud itself.
+ /// Be careful to not commit anything extra.
+ ///
+ public sealed class PluginImpl : IDalamudPlugin
+ {
+ private readonly WindowSystem windowSystem = new("Dalamud.CorePlugin");
+ private Localization localizationManager;
+
+ ///
+ public string Name => "Dalamud.CorePlugin";
+
+ ///
+ /// Gets the plugin interface.
+ ///
+ internal DalamudPluginInterface Interface { get; private set; }
+
+ ///
+ public void Initialize(DalamudPluginInterface pluginInterface)
+ {
+ try
+ {
+ this.InitLoc();
+
+ this.Interface = pluginInterface;
+
+ // this.windowSystem.AddWindow(your_window);
+
+ this.Interface.UiBuilder.OnBuildUi += this.OnDraw;
+ this.Interface.UiBuilder.OnOpenConfigUi += this.OnOpenConfigUi;
+
+ this.Interface.CommandManager.AddHandler("/di", new(this.OnCommand) { HelpMessage = $"Access the {this.Name} plugin." });
+ }
+ catch (Exception ex)
+ {
+ PluginLog.Error(ex, "kaboom");
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.Interface.CommandManager.RemoveHandler("/di");
+
+ this.Interface.UiBuilder.OnBuildUi -= this.OnDraw;
+
+ this.windowSystem.RemoveAllWindows();
+
+ this.Interface.Dispose();
+ }
+
+ private void InitLoc()
+ {
+ // CheapLoc needs to be reinitialized here because it tracks the setup by assembly name. New assembly, new setup.
+ this.localizationManager = new Localization(Path.Combine(Dalamud.Instance.AssetDirectory.FullName, "UIRes", "loc", "dalamud"), "dalamud_");
+ if (!string.IsNullOrEmpty(Dalamud.Instance.Configuration.LanguageOverride))
+ {
+ this.localizationManager.SetupWithLangCode(Dalamud.Instance.Configuration.LanguageOverride);
+ }
+ else
+ {
+ this.localizationManager.SetupWithUiCulture();
+ }
+ }
+
+ private void OnDraw()
+ {
+ try
+ {
+ this.windowSystem.Draw();
+ }
+ catch (Exception ex)
+ {
+ PluginLog.Error(ex, "Boom");
+ }
+ }
+
+ private void OnCommand(string command, string args)
+ {
+ // this.window.IsOpen = true;
+ }
+
+ private void OnOpenConfigUi(object sender, EventArgs e)
+ {
+ // this.window.IsOpen = true;
+ }
+ }
+}
diff --git a/Dalamud.CorePlugin/PluginWindow.cs b/Dalamud.CorePlugin/PluginWindow.cs
new file mode 100644
index 000000000..e604389ba
--- /dev/null
+++ b/Dalamud.CorePlugin/PluginWindow.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Numerics;
+
+using Dalamud.Interface.Windowing;
+using ImGuiNET;
+
+namespace Dalamud.CorePlugin
+{
+ ///
+ /// Class responsible for drawing the plugin installer.
+ ///
+ internal class PluginWindow : Window, IDisposable
+ {
+ private static readonly ModuleLog Log = new("CorePlugin");
+
+ private readonly Dalamud dalamud;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Dalamud instance.
+ public PluginWindow(Dalamud dalamud)
+ : base("CorePlugin", ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoScrollbar)
+ {
+ this.dalamud = dalamud;
+ this.IsOpen = true;
+
+ this.Size = new Vector2(810, 520);
+ this.SizeCondition = ImGuiCond.Always;
+ }
+
+ ///
+ public void Dispose()
+ {
+ }
+
+ ///
+ public override void OnOpen()
+ {
+ }
+
+ ///
+ public override void Draw()
+ {
+ }
+ }
+}
diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj
new file mode 100644
index 000000000..8257d42aa
--- /dev/null
+++ b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj
@@ -0,0 +1,113 @@
+
+
+
+ {8874326B-E755-4D13-90B4-59AB263A3E6B}
+ Dalamud_Injector_Boot
+ Debug
+ x64
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 16.0
+ Win32Proj
+ 10.0
+ Dalamud.Injector
+
+
+
+ Application
+ true
+ v142
+ false
+ Unicode
+ ..\bin\$(Configuration)\
+ obj\$(Configuration)\
+
+
+
+
+ Level3
+ true
+ true
+ stdcpplatest
+ MultiThreadedDebug
+ pch.h
+ ProgramDatabase
+ CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+
+
+ Console
+ true
+ false
+ ..\lib\CoreCLR;%(AdditionalLibraryDirectories)
+ $(OutDir)$(TargetName).Boot.pdb
+
+
+
+
+ true
+ false
+ _DEBUG;%(PreprocessorDefinitions)
+
+
+ false
+ false
+
+
+
+
+ true
+ true
+ NDEBUG;%(PreprocessorDefinitions)
+
+
+ true
+ true
+
+
+
+
+
+ nethost.dll
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters
new file mode 100644
index 000000000..47f80ab3e
--- /dev/null
+++ b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters
@@ -0,0 +1,75 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
+
+
+ {4faac519-3a73-4b2b-96e7-fb597f02c0be}
+ ico;rc
+
+
+ {6aff1bed-6979-4bc9-94e8-ddafb626e6bf}
+
+
+
+
+ Resource Files
+
+
+
+
+ Resource Files
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+
+
+ Library Files
+
+
+ Library Files
+
+
+
\ No newline at end of file
diff --git a/Dalamud.Injector/dalamud.ico b/Dalamud.Injector.Boot/dalamud.ico
similarity index 99%
rename from Dalamud.Injector/dalamud.ico
rename to Dalamud.Injector.Boot/dalamud.ico
index 98b9a9b9f..1cd63765d 100644
Binary files a/Dalamud.Injector/dalamud.ico and b/Dalamud.Injector.Boot/dalamud.ico differ
diff --git a/Dalamud.Injector.Boot/main.cpp b/Dalamud.Injector.Boot/main.cpp
new file mode 100644
index 000000000..545a68601
--- /dev/null
+++ b/Dalamud.Injector.Boot/main.cpp
@@ -0,0 +1,49 @@
+#define WIN32_LEAN_AND_MEAN
+
+#include
+#include
+#include "..\lib\CoreCLR\CoreCLR.h"
+#include "..\lib\CoreCLR\boot.h"
+
+int wmain(int argc, char** argv)
+{
+ #if defined(_DEBUG)
+ ConsoleSetup(L"Dalamud Injector Boot");
+ #endif
+
+ wchar_t _module_path[MAX_PATH];
+ GetModuleFileNameW(NULL, _module_path, sizeof _module_path / 2);
+ std::filesystem::path fs_module_path(_module_path);
+
+ std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.runtimeconfig.json").c_str());
+ std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.dll").c_str());
+
+ // =========================================================================== //
+
+ void* entrypoint_vfn;
+ int result = InitializeClrAndGetEntryPoint(
+ runtimeconfig_path,
+ module_path,
+ L"Dalamud.Injector.EntryPoint, Dalamud.Injector",
+ L"Main",
+ L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector",
+ &entrypoint_vfn);
+
+ if (result != 0)
+ return result;
+
+ typedef void (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, char**);
+ custom_component_entry_point_fn entrypoint_fn = reinterpret_cast(entrypoint_vfn);
+
+ printf("Running Dalamud Injector... ");
+ entrypoint_fn(argc, argv);
+ printf("Done!\n");
+
+ // =========================================================================== //
+
+ #if defined(_DEBUG)
+ FreeConsole();
+ #endif
+
+ return 0;
+}
diff --git a/Dalamud.Injector.Boot/resources.rc b/Dalamud.Injector.Boot/resources.rc
new file mode 100644
index 000000000..8369e82a1
--- /dev/null
+++ b/Dalamud.Injector.Boot/resources.rc
@@ -0,0 +1 @@
+MAINICON ICON "dalamud.ico"
diff --git a/Dalamud.Injector/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj
index 5d76a11f6..eac9f1009 100644
--- a/Dalamud.Injector/Dalamud.Injector.csproj
+++ b/Dalamud.Injector/Dalamud.Injector.csproj
@@ -1,60 +1,89 @@
+
- AnyCPU
- net48
- 8.0
- AnyCPU;x64
-
-
- WinExe
- $(SolutionDir)bin
- false
- true
- Portable
- IDE1006;CS1701;CS1702
- true
- $(SolutionDir)\bin\Dalamud.Injector.xml
+ net5.0
+ win-x64
+ x64
+ x64;AnyCPU
+ 9.0
+
+ 5.2.4.6
+ XIV Launcher addon injector
+ $(InjectorVersion)
+ $(InjectorVersion)
+ $(InjectorVersion)
+
+
+
+ Library
+ ..\bin\$(Configuration)\
+ false
+ false
+ true
+ false
+
+
+
+
+ true
+
+
+
+ true
+ true
+ portable
+ true
+ annotations
true
- 5.2.4.6
- 5.2.4.6
- XIVLauncher addon injection
- 5.2.4.6
+
+
+
+ Debug
+
+
+ DEBUG;TRACE
$(MSBuildProjectDirectory)\
$(AppOutputBase)=C:\goatsoft\companysecrets\injector\
- true
-
- false
-
-
- dalamud.ico
+
+
+ IDE1006;CS1591;CS1701;CS1702
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
-
-
+
+
-
+
+
+
+
+
+
+
-
-
-
diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs
new file mode 100644
index 000000000..eff8b81a9
--- /dev/null
+++ b/Dalamud.Injector/EntryPoint.cs
@@ -0,0 +1,275 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+
+using Dalamud.Game;
+using Dalamud.Interface.Internal;
+using Newtonsoft.Json;
+using Reloaded.Memory.Buffers;
+using Serilog;
+using Serilog.Core;
+using Serilog.Events;
+
+using static Dalamud.Injector.NativeFunctions;
+
+namespace Dalamud.Injector
+{
+ ///
+ /// Entrypoint to the program.
+ ///
+ public sealed class EntryPoint
+ {
+ ///
+ /// A delegate used during initialization of the CLR from Dalamud.Injector.Boot.
+ ///
+ /// Count of arguments.
+ /// char** string arguments.
+ public delegate void MainDelegate(int argc, IntPtr argvPtr);
+
+ ///
+ /// Start the Dalamud injector.
+ ///
+ /// Count of arguments.
+ /// byte** string arguments.
+ public static void Main(int argc, IntPtr argvPtr)
+ {
+ InitUnhandledException();
+ InitLogging();
+
+ var args = new string[argc];
+
+ unsafe
+ {
+ var argv = (IntPtr*)argvPtr;
+ for (var i = 0; i < argc; i++)
+ {
+ args[i] = Marshal.PtrToStringUni(argv[i]);
+ }
+ }
+
+ var cwd = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory;
+ if (cwd.FullName != Directory.GetCurrentDirectory())
+ {
+ Log.Debug($"Changing cwd to {cwd}");
+ Directory.SetCurrentDirectory(cwd.FullName);
+ }
+
+ var process = GetProcess(args.ElementAtOrDefault(1));
+ var startInfo = GetStartInfo(args.ElementAtOrDefault(2), process);
+
+ startInfo.WorkingDirectory = Directory.GetCurrentDirectory();
+
+ // This seems to help with the STATUS_INTERNAL_ERROR condition
+ Thread.Sleep(1000);
+
+ Inject(process, startInfo);
+
+ Thread.Sleep(1000);
+ }
+
+ private static void InitUnhandledException()
+ {
+ AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
+ {
+ if (Log.Logger == null)
+ {
+ Console.WriteLine($"A fatal error has occurred: {eventArgs.ExceptionObject}");
+ }
+ else
+ {
+ var exObj = eventArgs.ExceptionObject;
+ if (exObj is Exception ex)
+ {
+ Log.Error(ex, "A fatal error has occurred.");
+ }
+ else
+ {
+ Log.Error($"A fatal error has occurred: {eventArgs.ExceptionObject}");
+ }
+ }
+
+#if DEBUG
+ var caption = "Debug Error";
+ var message =
+ $"Couldn't inject.\nMake sure that Dalamud was not injected into your target process " +
+ $"as a release build before and that the target process can be accessed with VM_WRITE permissions.\n\n" +
+ $"{eventArgs.ExceptionObject}";
+#else
+ var caption = "XIVLauncher Error";
+ var message =
+ "Failed to inject the XIVLauncher in-game addon.\nPlease try restarting your game and your PC.\n" +
+ "If this keeps happening, please report this error.";
+#endif
+ _ = MessageBoxW(IntPtr.Zero, message, caption, MessageBoxType.IconError | MessageBoxType.Ok);
+
+ Environment.Exit(0);
+ };
+ }
+
+ private static void InitLogging()
+ {
+ var baseDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+
+#if DEBUG
+ var logPath = Path.Combine(baseDirectory, "injector.log");
+#else
+ var logPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.injector.log");
+#endif
+
+ var levelSwitch = new LoggingLevelSwitch();
+
+#if DEBUG
+ levelSwitch.MinimumLevel = LogEventLevel.Verbose;
+#else
+ levelSwitch.MinimumLevel = LogEventLevel.Information;
+#endif
+
+ Log.Logger = new LoggerConfiguration()
+ .WriteTo.Async(a => a.File(logPath))
+ .WriteTo.Sink(SerilogEventSink.Instance)
+ .MinimumLevel.ControlledBy(levelSwitch)
+ .CreateLogger();
+ }
+
+ private static Process GetProcess(string arg)
+ {
+ Process process;
+
+ var pid = -1;
+ if (arg != default)
+ {
+ pid = int.Parse(arg);
+ }
+
+ switch (pid)
+ {
+ case -1:
+ process = Process.GetProcessesByName("ffxiv_dx11").FirstOrDefault();
+
+ if (process == default)
+ {
+ throw new Exception("Could not find process");
+ }
+
+ break;
+ case -2:
+ var exePath = "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn\\game\\ffxiv_dx11.exe";
+ var exeArgs = new StringBuilder()
+ .Append("DEV.TestSID=0 DEV.UseSqPack=1 DEV.DataPathType=1 ")
+ .Append("DEV.LobbyHost01=127.0.0.1 DEV.LobbyPort01=54994 ")
+ .Append("DEV.LobbyHost02=127.0.0.1 DEV.LobbyPort02=54994 ")
+ .Append("DEV.LobbyHost03=127.0.0.1 DEV.LobbyPort03=54994 ")
+ .Append("DEV.LobbyHost04=127.0.0.1 DEV.LobbyPort04=54994 ")
+ .Append("DEV.LobbyHost05=127.0.0.1 DEV.LobbyPort05=54994 ")
+ .Append("DEV.LobbyHost06=127.0.0.1 DEV.LobbyPort06=54994 ")
+ .Append("DEV.LobbyHost07=127.0.0.1 DEV.LobbyPort07=54994 ")
+ .Append("DEV.LobbyHost08=127.0.0.1 DEV.LobbyPort08=54994 ")
+ .Append("SYS.Region=0 language=1 version=1.0.0.0 ")
+ .Append("DEV.MaxEntitledExpansionID=2 DEV.GMServerHost=127.0.0.1 DEV.GameQuitMessageBox=0").ToString();
+ process = Process.Start(exePath, exeArgs);
+ Thread.Sleep(1000);
+ break;
+ default:
+ process = Process.GetProcessById(pid);
+ break;
+ }
+
+ return process;
+ }
+
+ private static DalamudStartInfo GetStartInfo(string arg, Process process)
+ {
+ DalamudStartInfo startInfo;
+
+ if (arg != default)
+ {
+ startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(arg)));
+ }
+ else
+ {
+ var ffxivDir = Path.GetDirectoryName(process.MainModule.FileName);
+ var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher");
+
+ var gameVerStr = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver"));
+ var gameVer = GameVersion.Parse(gameVerStr);
+
+ startInfo = new DalamudStartInfo
+ {
+ WorkingDirectory = null,
+ ConfigurationPath = Path.Combine(xivlauncherDir, "dalamudConfig.json"),
+ PluginDirectory = Path.Combine(xivlauncherDir, "installedPlugins"),
+ DefaultPluginDirectory = Path.Combine(xivlauncherDir, "devPlugins"),
+ AssetDirectory = Path.Combine(xivlauncherDir, "dalamudAssets"),
+ GameVersion = gameVer,
+ Language = ClientLanguage.English,
+ OptOutMbCollection = false,
+ };
+
+ Log.Debug(
+ "Creating a new StartInfo with:\n" +
+ $" WorkingDirectory: {startInfo.WorkingDirectory}\n" +
+ $" ConfigurationPath: {startInfo.ConfigurationPath}\n" +
+ $" PluginDirectory: {startInfo.PluginDirectory}\n" +
+ $" DefaultPluginDirectory: {startInfo.DefaultPluginDirectory}\n" +
+ $" AssetDirectory: {startInfo.AssetDirectory}\n" +
+ $" GameVersion: {startInfo.GameVersion}\n" +
+ $" Language: {startInfo.Language}\n" +
+ $" OptOutMbCollection: {startInfo.OptOutMbCollection}");
+
+ Log.Information("A Dalamud start info was not found in the program arguments. One has been generated for you.");
+ Log.Information("Copy the following contents into the program arguments:");
+
+ var startInfoJson = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(startInfo)));
+ Log.Information(startInfoJson);
+ }
+
+ return startInfo;
+ }
+
+ private static void Inject(Process process, DalamudStartInfo startInfo)
+ {
+ var nethostName = "nethost.dll";
+ var bootName = "Dalamud.Boot.dll";
+
+ var nethostPath = Path.GetFullPath(nethostName);
+ var bootPath = Path.GetFullPath(bootName);
+
+ // ======================================================
+
+ using var injector = new Injector(process);
+
+ injector.LoadLibrary(nethostPath, out _);
+ injector.LoadLibrary(bootPath, out var bootModule);
+
+ // ======================================================
+
+ var startInfoJson = JsonConvert.SerializeObject(startInfo);
+ var startInfoBytes = Encoding.UTF8.GetBytes(startInfoJson);
+
+ using var startInfoBuffer = new MemoryBufferHelper(process).CreatePrivateMemoryBuffer(startInfoBytes.Length + 0x8);
+ var startInfoAddress = startInfoBuffer.Add(startInfoBytes);
+
+ if (startInfoAddress == IntPtr.Zero)
+ throw new Exception("Unable to allocate start info JSON");
+
+ injector.GetFunctionAddress(bootModule, "Initialize", out var initAddress);
+ injector.CallRemoteFunction(initAddress, startInfoAddress, out var exitCode);
+
+ // ======================================================
+
+ if (exitCode > 0)
+ {
+ Log.Error($"Dalamud.Boot::Initialize returned {exitCode}");
+ return;
+ }
+
+ Log.Information("Done");
+ }
+ }
+}
diff --git a/Dalamud.Injector/GlobalSuppressions.cs b/Dalamud.Injector/GlobalSuppressions.cs
index 3fca475cc..e1978fae4 100644
--- a/Dalamud.Injector/GlobalSuppressions.cs
+++ b/Dalamud.Injector/GlobalSuppressions.cs
@@ -6,14 +6,8 @@
using System.Diagnostics.CodeAnalysis;
// General
-[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "Preventing long lines", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "I like regions", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1123:Do not place regions within elements", Justification = "I like regions in elements too", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
+[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1127:Generic type constraints should be on their own line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")]
-
-// Program.cs
-[assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Used during #if DEBUG", Scope = "member", Target = "~M:Dalamud.Injector.Program.NativeInject(System.Diagnostics.Process)")]
+[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "I'll make what I want static", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
diff --git a/Dalamud.Injector/Injector.cs b/Dalamud.Injector/Injector.cs
new file mode 100644
index 000000000..3f9c14046
--- /dev/null
+++ b/Dalamud.Injector/Injector.cs
@@ -0,0 +1,303 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using Iced.Intel;
+using PeNet;
+using PeNet.Header.Pe;
+using Reloaded.Memory.Buffers;
+using Reloaded.Memory.Sources;
+using Reloaded.Memory.Utilities;
+
+using static Dalamud.Injector.NativeFunctions;
+using static Iced.Intel.AssemblerRegisters;
+
+namespace Dalamud.Injector
+{
+ ///
+ /// This class implements injecting into a remote process. It is a highly stripped down version of the
+ /// https://github.com/Reloaded-Project injector/assembler implementation due to issues with Lutris and
+ /// Wine.
+ ///
+ internal sealed class Injector : IDisposable
+ {
+ private readonly Process targetProcess;
+ private readonly ExternalMemory extMemory;
+ private readonly CircularBuffer circularBuffer;
+ private readonly PrivateMemoryBuffer privateBuffer;
+
+ private IntPtr loadLibraryShellPtr;
+ private IntPtr loadLibraryRetPtr;
+
+ private IntPtr getProcAddressShellPtr;
+ private IntPtr getProcAddressRetPtr;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Process to inject.
+ public Injector(Process targetProcess)
+ {
+ this.targetProcess = targetProcess;
+
+ this.extMemory = new ExternalMemory(targetProcess);
+ this.circularBuffer = new CircularBuffer(4096, this.extMemory);
+ this.privateBuffer = new MemoryBufferHelper(targetProcess).CreatePrivateMemoryBuffer(4096);
+
+ using var kernel32Module = this.GetProcessModule("KERNEL32.DLL");
+ var kernel32PeFile = new PeFile(kernel32Module.FileName);
+ var kernel32Exports = kernel32PeFile.ExportedFunctions;
+
+ this.SetupLoadLibrary(kernel32Module, kernel32Exports);
+ this.SetupGetProcAddress(kernel32Module, kernel32Exports);
+ }
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~Injector() => this.Dispose();
+
+ ///
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+
+ this.targetProcess?.Dispose();
+ this.circularBuffer?.Dispose();
+ this.privateBuffer?.Dispose();
+ }
+
+ ///
+ /// Load a module by absolute file path.
+ ///
+ /// Absolute file path.
+ /// Address to the module.
+ public void LoadLibrary(string modulePath, out IntPtr address)
+ {
+ var lpParameter = this.WriteNullTerminatedUnicodeString(modulePath);
+
+ if (lpParameter == IntPtr.Zero)
+ throw new Exception("Unable to allocate LoadLibraryW parameter");
+
+ var threadHandle = CreateRemoteThread(
+ this.targetProcess.Handle,
+ IntPtr.Zero,
+ UIntPtr.Zero,
+ this.loadLibraryShellPtr,
+ lpParameter,
+ CreateThreadFlags.RunImmediately,
+ out _);
+
+ _ = WaitForSingleObject(threadHandle, uint.MaxValue);
+
+ this.extMemory.Read(this.loadLibraryRetPtr, out address);
+
+ if (address == IntPtr.Zero)
+ throw new Exception($"Error calling LoadLibraryW with {modulePath}");
+ }
+
+ ///
+ /// Get the address of an exported module function.
+ ///
+ /// Module address.
+ /// Name of the exported method.
+ /// Address to the function.
+ public void GetFunctionAddress(IntPtr module, string functionName, out IntPtr address)
+ {
+ var getProcAddressParams = new GetProcAddressParams(module, this.WriteNullTerminatedASCIIString(functionName));
+ var lpParameter = this.circularBuffer.Add(ref getProcAddressParams);
+
+ if (lpParameter == IntPtr.Zero)
+ throw new Exception("Unable to allocate GetProcAddress parameter ptr");
+
+ var threadHandle = CreateRemoteThread(
+ this.targetProcess.Handle,
+ IntPtr.Zero,
+ UIntPtr.Zero,
+ this.getProcAddressShellPtr,
+ lpParameter,
+ CreateThreadFlags.RunImmediately,
+ out _);
+
+ _ = WaitForSingleObject(threadHandle, uint.MaxValue);
+
+ this.extMemory.Read(this.getProcAddressRetPtr, out address);
+
+ if (address == IntPtr.Zero)
+ throw new Exception($"Error calling GetProcAddress with {functionName}");
+ }
+
+ ///
+ /// Call a method in a remote process via CreateRemoteThread.
+ ///
+ /// Method address.
+ /// Parameter address.
+ /// Thread exit code.
+ public void CallRemoteFunction(IntPtr methodAddress, IntPtr parameterAddress, out uint exitCode)
+ {
+ // Create and initialize a thread at our address and parameter address.
+ var threadHandle = CreateRemoteThread(
+ this.targetProcess.Handle,
+ IntPtr.Zero,
+ UIntPtr.Zero,
+ methodAddress,
+ parameterAddress,
+ CreateThreadFlags.RunImmediately,
+ out _);
+
+ _ = WaitForSingleObject(threadHandle, uint.MaxValue);
+
+ GetExitCodeThread(threadHandle, out exitCode);
+ }
+
+ private void SetupLoadLibrary(ProcessModule kernel32Module, ExportFunction[] kernel32Exports)
+ {
+ var offset = this.GetExportedFunctionOffset(kernel32Exports, "LoadLibraryW");
+ var functionAddr = kernel32Module.BaseAddress + (int)offset;
+ var functionPtr = this.privateBuffer.Add(ref functionAddr);
+
+ if (functionPtr == IntPtr.Zero)
+ throw new Exception("Unable to allocate LoadLibraryW function ptr");
+
+ var dummy = 0L;
+ this.loadLibraryRetPtr = this.privateBuffer.Add(ref dummy);
+
+ if (this.loadLibraryRetPtr == IntPtr.Zero)
+ throw new Exception("Unable to allocate LoadLibraryW return value");
+
+ var func = functionPtr.ToInt64();
+ var retVal = this.loadLibraryRetPtr.ToInt64();
+
+ var asm = new Assembler(64);
+
+ asm.sub(rsp, 40); // sub rsp, 40 // Re-align stack to 16 byte boundary + shadow space.
+ asm.call(__qword_ptr[__qword_ptr[func]]); // call qword [qword func] // CreateRemoteThread lpParameter with string already in ECX.
+ asm.mov(__qword_ptr[__qword_ptr[retVal]], rax); // mov qword [qword retVal], rax //
+ asm.add(rsp, 40); // add rsp, 40 // Re-align stack to 16 byte boundary + shadow space.
+ asm.ret(); // ret // Restore stack ptr. (Callee cleanup)
+
+ var bytes = this.Assemble(asm);
+ this.loadLibraryShellPtr = this.privateBuffer.Add(bytes);
+
+ if (this.loadLibraryShellPtr == IntPtr.Zero)
+ throw new Exception("Unable to allocate LoadLibraryW shellcode");
+ }
+
+ private void SetupGetProcAddress(ProcessModule kernel32Module, ExportFunction[] kernel32Exports)
+ {
+ var offset = this.GetExportedFunctionOffset(kernel32Exports, "GetProcAddress");
+ var functionAddr = kernel32Module.BaseAddress + (int)offset;
+ var functionPtr = this.privateBuffer.Add(ref functionAddr);
+
+ if (functionPtr == IntPtr.Zero)
+ throw new Exception("Unable to allocate GetProcAddress function ptr");
+
+ var dummy = 0L;
+ this.getProcAddressRetPtr = this.privateBuffer.Add(ref dummy);
+
+ if (this.getProcAddressRetPtr == IntPtr.Zero)
+ throw new Exception("Unable to allocate GetProcAddress return value");
+
+ var func = functionPtr.ToInt64();
+ var retVal = this.getProcAddressRetPtr.ToInt64();
+
+ var asm = new Assembler(64);
+
+ asm.sub(rsp, 40); // sub rsp, 40 // Re-align stack to 16 byte boundary +32 shadow space
+ asm.mov(rdx, __qword_ptr[__qword_ptr[rcx + 8]]); // mov rdx, qword [qword rcx + 8] // lpProcName
+ asm.mov(rcx, __qword_ptr[__qword_ptr[rcx + 0]]); // mov rcx, qword [qword rcx + 0] // hModule
+ asm.call(__qword_ptr[__qword_ptr[func]]); // call qword [qword func] //
+ asm.mov(__qword_ptr[__qword_ptr[retVal]], rax); // mov qword [qword retVal] //
+ asm.add(rsp, 40); // add rsp, 40 // Re-align stack to 16 byte boundary + shadow space.
+ asm.ret(); // ret // Restore stack ptr. (Callee cleanup)
+
+ var bytes = this.Assemble(asm);
+ this.getProcAddressShellPtr = this.privateBuffer.Add(bytes);
+
+ if (this.getProcAddressShellPtr == IntPtr.Zero)
+ throw new Exception("Unable to allocate GetProcAddress shellcode");
+ }
+
+ private byte[] Assemble(Assembler assembler)
+ {
+ using var stream = new MemoryStream();
+ assembler.Assemble(new StreamCodeWriter(stream), 0);
+
+ stream.Position = 0;
+ var reader = new StreamCodeReader(stream);
+
+ int next;
+ var bytes = new byte[stream.Length];
+ while ((next = reader.ReadByte()) >= 0)
+ {
+ bytes[stream.Position - 1] = (byte)next;
+ }
+
+ return bytes;
+ }
+
+ private ProcessModule GetProcessModule(string moduleName)
+ {
+ var modules = this.targetProcess.Modules;
+ for (var i = 0; i < modules.Count; i++)
+ {
+ var module = modules[i];
+ if (module.ModuleName.Equals(moduleName, StringComparison.InvariantCultureIgnoreCase))
+ {
+ return module;
+ }
+ }
+
+ throw new Exception($"Failed to find {moduleName} in target process' modules");
+ }
+
+ private uint GetExportedFunctionOffset(ExportFunction[] exportFunctions, string functionName)
+ {
+ var exportFunction = exportFunctions.FirstOrDefault(func => func.Name == functionName);
+
+ if (exportFunction == default)
+ throw new Exception($"Failed to find exported function {functionName} in target module's exports");
+
+ return exportFunction.Address;
+ }
+
+ private IntPtr WriteNullTerminatedASCIIString(string libraryPath)
+ {
+ var libraryNameBytes = Encoding.ASCII.GetBytes(libraryPath + '\0');
+ var value = this.circularBuffer.Add(libraryNameBytes);
+
+ if (value == IntPtr.Zero)
+ throw new Exception("Unable to write ASCII string to buffer");
+
+ return value;
+ }
+
+ private IntPtr WriteNullTerminatedUnicodeString(string libraryPath)
+ {
+ var libraryNameBytes = Encoding.Unicode.GetBytes(libraryPath + '\0');
+ var value = this.circularBuffer.Add(libraryNameBytes);
+
+ if (value == IntPtr.Zero)
+ throw new Exception("Unable to write Unicode string to buffer");
+
+ return value;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct GetProcAddressParams
+ {
+ public GetProcAddressParams(IntPtr hModule, IntPtr lPProcName)
+ {
+ this.HModule = hModule.ToInt64();
+ this.LPProcName = lPProcName.ToInt64();
+ }
+
+ public long HModule { get; set; }
+
+ public long LPProcName { get; set; }
+ }
+ }
+}
diff --git a/Dalamud.Injector/NativeFunctions.cs b/Dalamud.Injector/NativeFunctions.cs
index 90e61a5fc..46da6728c 100644
--- a/Dalamud.Injector/NativeFunctions.cs
+++ b/Dalamud.Injector/NativeFunctions.cs
@@ -1,14 +1,234 @@
using System;
-using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
-using System.Security;
namespace Dalamud.Injector
{
///
- /// Native functions.
+ /// Native user32 functions.
///
- internal static class NativeFunctions
+ internal static partial class NativeFunctions
+ {
+ ///
+ /// MB_* from winuser.
+ ///
+ public enum MessageBoxType : uint
+ {
+ ///
+ /// The default value for any of the various subtypes.
+ ///
+ DefaultValue = 0x0,
+
+ // To indicate the buttons displayed in the message box, specify one of the following values.
+
+ ///
+ /// The message box contains three push buttons: Abort, Retry, and Ignore.
+ ///
+ AbortRetryIgnore = 0x2,
+
+ ///
+ /// The message box contains three push buttons: Cancel, Try Again, Continue. Use this message box type instead
+ /// of MB_ABORTRETRYIGNORE.
+ ///
+ CancelTryContinue = 0x6,
+
+ ///
+ /// Adds a Help button to the message box. When the user clicks the Help button or presses F1, the system sends
+ /// a WM_HELP message to the owner.
+ ///
+ Help = 0x4000,
+
+ ///
+ /// The message box contains one push button: OK. This is the default.
+ ///
+ Ok = DefaultValue,
+
+ ///
+ /// The message box contains two push buttons: OK and Cancel.
+ ///
+ OkCancel = 0x1,
+
+ ///
+ /// The message box contains two push buttons: Retry and Cancel.
+ ///
+ RetryCancel = 0x5,
+
+ ///
+ /// The message box contains two push buttons: Yes and No.
+ ///
+ YesNo = 0x4,
+
+ ///
+ /// The message box contains three push buttons: Yes, No, and Cancel.
+ ///
+ YesNoCancel = 0x3,
+
+ // To display an icon in the message box, specify one of the following values.
+
+ ///
+ /// An exclamation-point icon appears in the message box.
+ ///
+ IconExclamation = 0x30,
+
+ ///
+ /// An exclamation-point icon appears in the message box.
+ ///
+ IconWarning = IconExclamation,
+
+ ///
+ /// An icon consisting of a lowercase letter i in a circle appears in the message box.
+ ///
+ IconInformation = 0x40,
+
+ ///
+ /// An icon consisting of a lowercase letter i in a circle appears in the message box.
+ ///
+ IconAsterisk = IconInformation,
+
+ ///
+ /// A question-mark icon appears in the message box.
+ /// The question-mark message icon is no longer recommended because it does not clearly represent a specific type
+ /// of message and because the phrasing of a message as a question could apply to any message type. In addition,
+ /// users can confuse the message symbol question mark with Help information. Therefore, do not use this question
+ /// mark message symbol in your message boxes. The system continues to support its inclusion only for backward
+ /// compatibility.
+ ///
+ IconQuestion = 0x20,
+
+ ///
+ /// A stop-sign icon appears in the message box.
+ ///
+ IconStop = 0x10,
+
+ ///
+ /// A stop-sign icon appears in the message box.
+ ///
+ IconError = IconStop,
+
+ ///
+ /// A stop-sign icon appears in the message box.
+ ///
+ IconHand = IconStop,
+
+ // To indicate the default button, specify one of the following values.
+
+ ///
+ /// The first button is the default button.
+ /// MB_DEFBUTTON1 is the default unless MB_DEFBUTTON2, MB_DEFBUTTON3, or MB_DEFBUTTON4 is specified.
+ ///
+ DefButton1 = DefaultValue,
+
+ ///
+ /// The second button is the default button.
+ ///
+ DefButton2 = 0x100,
+
+ ///
+ /// The third button is the default button.
+ ///
+ DefButton3 = 0x200,
+
+ ///
+ /// The fourth button is the default button.
+ ///
+ DefButton4 = 0x300,
+
+ // To indicate the modality of the dialog box, specify one of the following values.
+
+ ///
+ /// The user must respond to the message box before continuing work in the window identified by the hWnd parameter.
+ /// However, the user can move to the windows of other threads and work in those windows. Depending on the hierarchy
+ /// of windows in the application, the user may be able to move to other windows within the thread. All child windows
+ /// of the parent of the message box are automatically disabled, but pop-up windows are not. MB_APPLMODAL is the
+ /// default if neither MB_SYSTEMMODAL nor MB_TASKMODAL is specified.
+ ///
+ ApplModal = DefaultValue,
+
+ ///
+ /// Same as MB_APPLMODAL except that the message box has the WS_EX_TOPMOST style.
+ /// Use system-modal message boxes to notify the user of serious, potentially damaging errors that require immediate
+ /// attention (for example, running out of memory). This flag has no effect on the user's ability to interact with
+ /// windows other than those associated with hWnd.
+ ///
+ SystemModal = 0x1000,
+
+ ///
+ /// Same as MB_APPLMODAL except that all the top-level windows belonging to the current thread are disabled if the
+ /// hWnd parameter is NULL. Use this flag when the calling application or library does not have a window handle
+ /// available but still needs to prevent input to other windows in the calling thread without suspending other threads.
+ ///
+ TaskModal = 0x2000,
+
+ // To specify other options, use one or more of the following values.
+
+ ///
+ /// Same as desktop of the interactive window station. For more information, see Window Stations. If the current
+ /// input desktop is not the default desktop, MessageBox does not return until the user switches to the default
+ /// desktop.
+ ///
+ DefaultDesktopOnly = 0x20000,
+
+ ///
+ /// The text is right-justified.
+ ///
+ Right = 0x80000,
+
+ ///
+ /// Displays message and caption text using right-to-left reading order on Hebrew and Arabic systems.
+ ///
+ RtlReading = 0x100000,
+
+ ///
+ /// The message box becomes the foreground window. Internally, the system calls the SetForegroundWindow function
+ /// for the message box.
+ ///
+ SetForeground = 0x10000,
+
+ ///
+ /// The message box is created with the WS_EX_TOPMOST window style.
+ ///
+ Topmost = 0x40000,
+
+ ///
+ /// The caller is a service notifying the user of an event. The function displays a message box on the current active
+ /// desktop, even if there is no user logged on to the computer.
+ ///
+ ServiceNotification = 0x200000,
+ }
+
+ ///
+ /// Displays a modal dialog box that contains a system icon, a set of buttons, and a brief application-specific message,
+ /// such as status or error information. The message box returns an integer value that indicates which button the user
+ /// clicked.
+ ///
+ ///
+ /// A handle to the owner window of the message box to be created. If this parameter is NULL, the message box has no
+ /// owner window.
+ ///
+ ///
+ /// The message to be displayed. If the string consists of more than one line, you can separate the lines using a carriage
+ /// return and/or linefeed character between each line.
+ ///
+ ///
+ /// The dialog box title. If this parameter is NULL, the default title is Error.
+ ///
+ /// The contents and behavior of the dialog box. This parameter can be a combination of flags from the following groups
+ /// of flags.
+ ///
+ ///
+ /// If a message box has a Cancel button, the function returns the IDCANCEL value if either the ESC key is pressed or
+ /// the Cancel button is selected. If the message box has no Cancel button, pressing ESC will no effect - unless an
+ /// MB_OK button is present. If an MB_OK button is displayed and the user presses ESC, the return value will be IDOK.
+ /// If the function fails, the return value is zero.To get extended error information, call GetLastError. If the function
+ /// succeeds, the return value is one of the ID* enum values.
+ ///
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern int MessageBoxW(IntPtr hWnd, string text, string caption, MessageBoxType type);
+ }
+
+ ///
+ /// Native kernel32 functions.
+ ///
+ internal static partial class NativeFunctions
{
///
/// MEM_* from memoryapi.
@@ -20,14 +240,14 @@ namespace Dalamud.Injector
/// To coalesce two adjacent placeholders, specify MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS. When you coalesce
/// placeholders, lpAddress and dwSize must exactly match those of the placeholder.
///
- CoalescePlaceholders = 0x00000001,
+ CoalescePlaceholders = 0x1,
///
/// Frees an allocation back to a placeholder (after you've replaced a placeholder with a private allocation using
/// VirtualAlloc2 or Virtual2AllocFromApp). To split a placeholder into two placeholders, specify
/// MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER.
///
- PreservePlaceholder = 0x00000002,
+ PreservePlaceholder = 0x2,
///
/// Allocates memory charges (from the overall size of memory and the paging files on disk) for the specified reserved
@@ -88,7 +308,7 @@ namespace Dalamud.Injector
/// the specified address range is intact. If the function fails, at least some of the data in the address range
/// has been replaced with zeroes. This value cannot be used with any other value. If MEM_RESET_UNDO is called on
/// an address range which was not MEM_RESET earlier, the behavior is undefined. When you specify MEM_RESET, the
- /// VirtualAllocEx function ignores the value of flProtect. However, you must still set flProtect to a valid
+ /// VirtualAllocEx function ignores the value of flProtect. However, you must still set flProtect to a valid
/// protection value, such as PAGE_NOACCESS.
///
ResetUndo = 0x1000000,
@@ -122,6 +342,28 @@ namespace Dalamud.Injector
LargePages = 0x20000000,
}
+ ///
+ /// Unprefixed flags from CreateRemoteThread.
+ ///
+ [Flags]
+ public enum CreateThreadFlags
+ {
+ ///
+ /// The thread runs immediately after creation.
+ ///
+ RunImmediately = 0x0,
+
+ ///
+ /// The thread is created in a suspended state, and does not run until the ResumeThread function is called.
+ ///
+ CreateSuspended = 0x4,
+
+ ///
+ /// The dwStackSize parameter specifies the initial reserve size of the stack. If this flag is not specified, dwStackSize specifies the commit size.
+ ///
+ StackSizeParamIsReservation = 0x10000,
+ }
+
///
/// PAGE_* from memoryapi.
///
@@ -198,7 +440,7 @@ namespace Dalamud.Injector
/// The default behavior for VirtualProtect protection change to executable is to mark all locations as valid call
/// targets for CFG.
///
- TargetsNoUpdate = 0x40000000,
+ TargetsNoUpdate = TargetsInvalid,
///
/// Pages in the region become guard pages. Any attempt to access a guard page causes the system to raise a
@@ -312,23 +554,33 @@ namespace Dalamud.Injector
}
///
- /// Closes an open object handle.
+ /// WAIT_* from synchapi.
///
- ///
- /// A valid handle to an open object.
- ///
- ///
- /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero.To get extended
- /// error information, call GetLastError. If the application is running under a debugger, the function will throw an
- /// exception if it receives either a handle value that is not valid or a pseudo-handle value. This can happen if you
- /// close a handle twice, or if you call CloseHandle on a handle returned by the FindFirstFile function instead of calling
- /// the FindClose function.
- ///
- [DllImport("kernel32.dll", SetLastError = true)]
- [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
- [SuppressUnmanagedCodeSecurity]
- [return: MarshalAs(UnmanagedType.Bool)]
- public static extern bool CloseHandle(IntPtr hObject);
+ public enum WaitResult
+ {
+ ///
+ /// The specified object is a mutex object that was not released by the thread that owned the mutex object
+ /// before the owning thread terminated.Ownership of the mutex object is granted to the calling thread and
+ /// the mutex state is set to nonsignaled. If the mutex was protecting persistent state information, you
+ /// should check it for consistency.
+ ///
+ Abandoned = 0x80,
+
+ ///
+ /// The state of the specified object is signaled.
+ ///
+ Object0 = 0x0,
+
+ ///
+ /// The time-out interval elapsed, and the object's state is nonsignaled.
+ ///
+ Timeout = 0x102,
+
+ ///
+ /// The function has failed. To get extended error information, call GetLastError.
+ ///
+ WAIT_FAILED = 0xFFFFFFF,
+ }
///
/// Creates a thread that runs in the virtual address space of another process. Use the CreateRemoteThreadEx function
@@ -336,23 +588,23 @@ namespace Dalamud.Injector
///
///
/// A handle to the process in which the thread is to be created. The handle must have the PROCESS_CREATE_THREAD,
- /// PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ access rights, and may fail
- /// without these rights on certain platforms. For more information, see Process Security and Access Rights.
+ /// PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ access rights, and may fail without
+ /// these rights on certain platforms. For more information, see Process Security and Access Rights.
///
///
- /// A pointer to a SECURITY_ATTRIBUTES structure that specifies a security descriptor for the new thread and determines
- /// whether child processes can inherit the returned handle. If lpThreadAttributes is NULL, the thread gets a default
- /// security descriptor and the handle cannot be inherited. The access control lists (ACL) in the default security descriptor
- /// for a thread come from the primary token of the creator.
+ /// A pointer to a SECURITY_ATTRIBUTES structure that specifies a security descriptor for the new thread and determines whether
+ /// child processes can inherit the returned handle. If lpThreadAttributes is NULL, the thread gets a default security descriptor
+ /// and the handle cannot be inherited. The access control lists (ACL) in the default security descriptor for a thread come from
+ /// the primary token of the creator.
///
///
- /// The initial size of the stack, in bytes. The system rounds this value to the nearest page. If this parameter is
- /// 0 (zero), the new thread uses the default size for the executable. For more information, see Thread Stack Size.
+ /// The initial size of the stack, in bytes. The system rounds this value to the nearest page. If this parameter is 0 (zero), the
+ /// new thread uses the default size for the executable. For more information, see Thread Stack Size.
///
///
- /// A pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread and
- /// represents the starting address of the thread in the remote process. The function must exist in the remote process.
- /// For more information, see ThreadProc.
+ /// A pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread and represents the
+ /// starting address of the thread in the remote process. The function must exist in the remote process. For more information,
+ /// see ThreadProc.
///
///
/// A pointer to a variable to be passed to the thread function.
@@ -361,92 +613,43 @@ namespace Dalamud.Injector
/// The flags that control the creation of the thread.
///
///
- /// A pointer to a variable that receives the thread identifier. If this parameter is NULL, the thread identifier is
- /// not returned.
+ /// A pointer to a variable that receives the thread identifier. If this parameter is NULL, the thread identifier is not returned.
///
///
- /// If the function succeeds, the return value is a handle to the new thread. If the function fails, the return value
- /// is NULL.To get extended error information, call GetLastError. Note that CreateRemoteThread may succeed even if
- /// lpStartAddress points to data, code, or is not accessible. If the start address is invalid when the thread runs,
- /// an exception occurs, and the thread terminates. Thread termination due to a invalid start address is handled as
- /// an error exit for the thread's process. This behavior is similar to the asynchronous nature of CreateProcess, where
- /// the process is created even if it refers to invalid or missing dynamic-link libraries (DLL).
+ /// If the function succeeds, the return value is a handle to the new thread. If the function fails, the return value is
+ /// NULL.To get extended error information, call GetLastError. Note that CreateRemoteThread may succeed even if lpStartAddress
+ /// points to data, code, or is not accessible. If the start address is invalid when the thread runs, an exception occurs, and
+ /// the thread terminates. Thread termination due to a invalid start address is handled as an error exit for the thread's process.
+ /// This behavior is similar to the asynchronous nature of CreateProcess, where the process is created even if it refers to
+ /// invalid or missing dynamic-link libraries (DLL).
///
- [DllImport("kernel32.dll")]
+ [DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr CreateRemoteThread(
IntPtr hProcess,
IntPtr lpThreadAttributes,
- uint dwStackSize,
+ UIntPtr dwStackSize,
IntPtr lpStartAddress,
IntPtr lpParameter,
- uint dwCreationFlags,
- IntPtr lpThreadId);
+ CreateThreadFlags dwCreationFlags,
+ out uint lpThreadId);
///
- /// See https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlew.
- /// Retrieves a module handle for the specified module. The module must have been loaded by the calling process. To
- /// avoid the race conditions described in the Remarks section, use the GetModuleHandleEx function.
+ /// Retrieves the termination status of the specified thread.
///
- ///
- /// The name of the loaded module (either a .dll or .exe file). If the file name extension is omitted, the default library
- /// extension .dll is appended. The file name string can include a trailing point character (.) to indicate that the
- /// module name has no extension. The string does not have to specify a path. When specifying a path, be sure to use
- /// backslashes (\), not forward slashes (/). The name is compared (case independently) to the names of modules currently
- /// mapped into the address space of the calling process. If this parameter is NULL, GetModuleHandle returns a handle
- /// to the file used to create the calling process (.exe file). The GetModuleHandle function does not retrieve handles
- /// for modules that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx.
+ ///
+ /// A handle to the thread. The handle must have the THREAD_QUERY_INFORMATION or THREAD_QUERY_LIMITED_INFORMATION
+ /// access right.For more information, see Thread Security and Access Rights.
+ ///
+ ///
+ /// A pointer to a variable to receive the thread termination status.
///
///
- /// If the function succeeds, the return value is a handle to the specified module. If the function fails, the return
- /// value is NULL.To get extended error information, call GetLastError.
- ///
- [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
- public static extern IntPtr GetModuleHandle(string lpModuleName);
-
- ///
- /// Retrieves the address of an exported function or variable from the specified dynamic-link library (DLL).
- ///
- ///
- /// A handle to the DLL module that contains the function or variable. The LoadLibrary, LoadLibraryEx, LoadPackagedLibrary,
- /// or GetModuleHandle function returns this handle. The GetProcAddress function does not retrieve addresses from modules
- /// that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx.
- ///
- ///
- /// The function or variable name, or the function's ordinal value. If this parameter is an ordinal value, it must be
- /// in the low-order word; the high-order word must be zero.
- ///
- ///
- /// If the function succeeds, the return value is the address of the exported function or variable. If the function
- /// fails, the return value is NULL.To get extended error information, call GetLastError.
- ///
- [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
- public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
-
- ///
- /// See https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess.
- /// Opens an existing local process object.
- ///
- ///
- /// The access to the process object. This access right is checked against the security descriptor for the process.
- /// This parameter can be one or more of the process access rights. If the caller has enabled the SeDebugPrivilege
- /// privilege, the requested access is granted regardless of the contents of the security descriptor.
- ///
- ///
- /// If this value is TRUE, processes created by this process will inherit the handle. Otherwise, the processes do
- /// not inherit this handle.
- ///
- ///
- /// The identifier of the local process to be opened.
- ///
- ///
- /// If the function succeeds, the return value is an open handle to the specified process. If the function fails, the
- /// return value is NULL.To get extended error information, call GetLastError.
+ /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get
+ /// extended error information, call GetLastError.
///
[DllImport("kernel32.dll", SetLastError = true)]
- public static extern IntPtr OpenProcess(
- ProcessAccessFlags processAccess,
- bool bInheritHandle,
- int processId);
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool GetExitCodeThread(IntPtr hThread, out uint lpExitCode);
///
/// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex.
@@ -530,6 +733,27 @@ namespace Dalamud.Injector
int dwSize,
AllocationType dwFreeType);
+ ///
+ /// Waits until the specified object is in the signaled state or the time-out interval elapses. To enter an alertable wait
+ /// state, use the WaitForSingleObjectEx function.To wait for multiple objects, use WaitForMultipleObjects.
+ ///
+ ///
+ /// A handle to the object. For a list of the object types whose handles can be specified, see the following Remarks section.
+ /// If this handle is closed while the wait is still pending, the function's behavior is undefined. The handle must have the
+ /// SYNCHRONIZE access right. For more information, see Standard Access Rights.
+ ///
+ ///
+ /// The time-out interval, in milliseconds. If a nonzero value is specified, the function waits until the object is signaled
+ /// or the interval elapses. If dwMilliseconds is zero, the function does not enter a wait state if the object is not signaled;
+ /// it always returns immediately. If dwMilliseconds is INFINITE, the function will return only when the object is signaled.
+ ///
+ ///
+ /// If the function succeeds, the return value indicates the event that caused the function to return.
+ /// It can be one of the WaitResult values.
+ ///
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
+
///
/// Writes data to an area of memory in a specified process. The entire area to be written to must be accessible or
/// the operation fails.
diff --git a/Dalamud.Injector/Program.cs b/Dalamud.Injector/Program.cs
deleted file mode 100644
index 74f3a1585..000000000
--- a/Dalamud.Injector/Program.cs
+++ /dev/null
@@ -1,208 +0,0 @@
-using System;
-using System.ComponentModel;
-using System.Diagnostics;
-using System.IO;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Threading;
-using System.Windows.Forms;
-
-using EasyHook;
-using Newtonsoft.Json;
-
-namespace Dalamud.Injector
-{
- ///
- /// Application entrypoint.
- ///
- internal static class Program
- {
- private static Process process = null;
-
- private static void Main(string[] args)
- {
- AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
- {
- File.WriteAllText("InjectorException.txt", eventArgs.ExceptionObject.ToString());
-#if !DEBUG
- MessageBox.Show("Failed to inject the XIVLauncher in-game addon.\nPlease try restarting your game and your PC.\nIf this keeps happening, please report this error.", "XIVLauncher Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
-#else
- MessageBox.Show("Couldn't inject.\nMake sure that Dalamud was not injected into your target process as a release build before and that the target process can be accessed with VM_WRITE permissions.\n\n" + eventArgs.ExceptionObject, "Debug Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
-#endif
- Environment.Exit(0);
- };
-
- var pid = -1;
- if (args.Length >= 1)
- {
- pid = int.Parse(args[0]);
- }
-
- switch (pid)
- {
- case -1:
- process = Process.GetProcessesByName("ffxiv_dx11")[0];
- break;
- case -2:
- process = Process.Start(
- "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn\\game\\ffxiv_dx11.exe",
- "DEV.TestSID=0 DEV.UseSqPack=1 DEV.DataPathType=1 DEV.LobbyHost01=127.0.0.1 DEV.LobbyPort01=54994 DEV.LobbyHost02=127.0.0.1 DEV.LobbyPort02=54994 DEV.LobbyHost03=127.0.0.1 DEV.LobbyPort03=54994 DEV.LobbyHost04=127.0.0.1 DEV.LobbyPort04=54994 DEV.LobbyHost05=127.0.0.1 DEV.LobbyPort05=54994 DEV.LobbyHost06=127.0.0.1 DEV.LobbyPort06=54994 DEV.LobbyHost07=127.0.0.1 DEV.LobbyPort07=54994 DEV.LobbyHost08=127.0.0.1 DEV.LobbyPort08=54994 SYS.Region=0 language=1 version=1.0.0.0 DEV.MaxEntitledExpansionID=2 DEV.GMServerHost=127.0.0.1 DEV.GameQuitMessageBox=0");
- Thread.Sleep(1000);
- break;
- default:
- process = Process.GetProcessById(pid);
- break;
- }
-
- DalamudStartInfo startInfo;
- if (args.Length <= 1)
- {
- startInfo = GetDefaultStartInfo();
- Console.WriteLine("\nA Dalamud start info was not found in the program arguments. One has been generated for you.");
- Console.WriteLine("\nCopy the following contents into the program arguments:");
- Console.WriteLine();
- Console.WriteLine(Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(startInfo))));
- }
- else
- {
- startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(args[1])));
- }
-
- startInfo.WorkingDirectory = Directory.GetCurrentDirectory();
-
- // Seems to help with the STATUS_INTERNAL_ERROR condition
- Thread.Sleep(1000);
-
- // Thread.Sleep(10000);
-
- // Inject to process
- Inject(process, startInfo);
-
- Thread.Sleep(1000);
-
-#if DEBUG
- // Inject exception handler
- // NativeInject(process);
-#endif
- }
-
- private static void Inject(Process process, DalamudStartInfo info)
- {
- Console.WriteLine($"Injecting to {process.Id}");
-
- // File check
- var libPath = Path.GetFullPath("Dalamud.dll");
- if (!File.Exists(libPath))
- {
- Console.WriteLine($"Can't find a dll on {libPath}");
- return;
- }
-
- RemoteHooking.Inject(process.Id, InjectionOptions.DoNotRequireStrongName, libPath, libPath, info);
-
- Console.WriteLine("Injected");
- }
-
- private static void NativeInject(Process process)
- {
- var libPath = Path.GetFullPath("DalamudDebugStub.dll");
-
- var pathBytes = Encoding.Unicode.GetBytes(libPath);
- var len = pathBytes.Length + 1;
-
- Console.WriteLine($"Injecting {libPath}...");
-
- var handle = NativeFunctions.OpenProcess(
- NativeFunctions.ProcessAccessFlags.AllAccess,
- false,
- process.Id);
-
- if (handle == IntPtr.Zero)
- {
- throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not OpenProcess");
- }
-
- var dllMem = NativeFunctions.VirtualAllocEx(
- handle,
- IntPtr.Zero,
- len,
- NativeFunctions.AllocationType.Commit,
- NativeFunctions.MemoryProtection.ReadWrite);
-
- if (dllMem == IntPtr.Zero)
- {
- throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not alloc memory {Marshal.GetLastWin32Error():X}");
- }
-
- Console.WriteLine($"dll path at {dllMem.ToInt64():X}");
-
- if (!NativeFunctions.WriteProcessMemory(
- handle,
- dllMem,
- pathBytes,
- len,
- out var bytesWritten))
- {
- throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not write DLL");
- }
-
- Console.WriteLine($"Wrote {bytesWritten}");
-
- var kernel32 = NativeFunctions.GetModuleHandle("Kernel32.dll");
- var loadLibA = NativeFunctions.GetProcAddress(kernel32, "LoadLibraryW");
-
- var remoteThread = NativeFunctions.CreateRemoteThread(
- handle,
- IntPtr.Zero,
- 0,
- loadLibA,
- dllMem,
- 0,
- IntPtr.Zero);
-
- if (remoteThread == IntPtr.Zero)
- {
- throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not CreateRemoteThread");
- }
-
- /*
- TODO kill myself
- VirtualFreeEx(
- handle,
- dllMem,
- 0,
- AllocationType.Release);
- */
-
- NativeFunctions.CloseHandle(remoteThread);
- NativeFunctions.CloseHandle(handle);
- }
-
- private static DalamudStartInfo GetDefaultStartInfo()
- {
- var ffxivDir = Path.GetDirectoryName(process.MainModule.FileName);
- var startInfo = new DalamudStartInfo
- {
- WorkingDirectory = null,
- ConfigurationPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XIVLauncher", "dalamudConfig.json"),
- PluginDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XIVLauncher", "installedPlugins"),
- DefaultPluginDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XIVLauncher", "devPlugins"),
- AssetDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XIVLauncher", "dalamudAssets"),
-
- GameVersion = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver")),
- Language = ClientLanguage.English,
- };
-
- Console.WriteLine("Creating a StartInfo with:\n" +
- $"ConfigurationPath: {startInfo.ConfigurationPath}\n" +
- $"PluginDirectory: {startInfo.PluginDirectory}\n" +
- $"DefaultPluginDirectory: {startInfo.DefaultPluginDirectory}\n" +
- $"Language: {startInfo.Language}\n" +
- $"GameVersion: {startInfo.GameVersion}\n" +
- $"OptOutMbCollection: {startInfo.OptOutMbCollection}\n" +
- $"AssetDirectory: {startInfo.AssetDirectory}");
-
- return startInfo;
- }
- }
-}
diff --git a/Dalamud.Injector/RemotePinnedData.cs b/Dalamud.Injector/RemotePinnedData.cs
new file mode 100644
index 000000000..fc713639f
--- /dev/null
+++ b/Dalamud.Injector/RemotePinnedData.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using static Dalamud.Injector.NativeFunctions;
+
+namespace Dalamud.Injector
+{
+ ///
+ /// Pin an arbitrary string to a remote process.
+ ///
+ internal class RemotePinnedData : IDisposable
+ {
+ private readonly Process process;
+ private readonly byte[] data;
+ private readonly IntPtr allocAddr;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Process to write in.
+ /// Data to write.
+ public unsafe RemotePinnedData(Process process, byte[] data)
+ {
+ this.process = process;
+ this.data = data;
+
+ this.allocAddr = VirtualAllocEx(
+ this.process.Handle,
+ IntPtr.Zero,
+ this.data.Length,
+ AllocationType.Commit,
+ MemoryProtection.ReadWrite);
+
+ if (this.allocAddr == IntPtr.Zero || Marshal.GetLastWin32Error() != 0)
+ {
+ throw new Exception("Error allocating memory");
+ }
+
+ var result = WriteProcessMemory(
+ this.process.Handle,
+ this.allocAddr,
+ this.data,
+ this.data.Length,
+ out _);
+
+ if (!result || Marshal.GetLastWin32Error() != 0)
+ {
+ throw new Exception("Error writing memory");
+ }
+ }
+
+ ///
+ /// Gets the address of the pinned data.
+ ///
+ public IntPtr Address => this.allocAddr;
+
+ ///
+ public void Dispose()
+ {
+ if (this.allocAddr == IntPtr.Zero)
+ {
+ return;
+ }
+
+ var result = VirtualFreeEx(
+ this.process.Handle,
+ this.allocAddr,
+ 0,
+ AllocationType.Release);
+
+ if (!result || Marshal.GetLastWin32Error() != 0)
+ {
+ throw new Exception("Error freeing memory");
+ }
+ }
+ }
+}
diff --git a/Dalamud.Test/Dalamud.Test.csproj b/Dalamud.Test/Dalamud.Test.csproj
index bcbccb45a..7163336b1 100644
--- a/Dalamud.Test/Dalamud.Test.csproj
+++ b/Dalamud.Test/Dalamud.Test.csproj
@@ -1,96 +1,70 @@
-
-
-
-
-
-
-
- Debug
- AnyCPU
- {C8004563-1806-4329-844F-0EF6274291FC}
- {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- Library
- Properties
- Dalamud.Test
- Dalamud.Test
- v4.7.2
- 512
-
-
- AnyCPU
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
-
-
- AnyCPU
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
-
-
-
-
-
-
-
- ..\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll
- True
-
-
- ..\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll
- True
-
-
- ..\packages\xunit.extensibility.core.2.4.1\lib\net452\xunit.core.dll
- True
-
-
- ..\packages\xunit.extensibility.execution.2.4.1\lib\net452\xunit.execution.desktop.dll
- True
-
-
-
-
-
-
-
-
-
-
-
-
- {b92dab43-2279-4a2c-96e3-d9d5910edbea}
- Dalamud
-
-
-
-
-
-
-
-
-
- This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}.
+
+
+
+ net5.0-windows
+ win-x64
+ x64
+ x64;AnyCPU
+ 9.0
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+ Dalamud.Test
+ Dalamud.Test
+ Dalamud.Test
+ Dalamud.Test
+ Unit tests for Dalamud
+ goatcorp
+ Copyright © goatcorp 2021
+
+
+
+ Library
+ false
+ false
+
+
+
+ Debug
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
diff --git a/Dalamud.Test/Game/GameVersionTests.cs b/Dalamud.Test/Game/GameVersionTests.cs
new file mode 100644
index 000000000..44a5813c8
--- /dev/null
+++ b/Dalamud.Test/Game/GameVersionTests.cs
@@ -0,0 +1,58 @@
+using Dalamud.Game;
+using Xunit;
+
+namespace Dalamud.Test.Game
+{
+ public class GameVersionTests
+ {
+ [Theory]
+ [InlineData("any", "any")]
+ [InlineData("2021.01.01.0000.0000", "2021.01.01.0000.0000")]
+ public void VersionEquality(string ver1, string ver2)
+ {
+ var v1 = GameVersion.Parse(ver1);
+ var v2 = GameVersion.Parse(ver2);
+
+ Assert.Equal(v1, v2);
+ }
+
+ [Theory]
+ [InlineData("2020.06.15.0000.0000", "any")]
+ [InlineData("2021.01.01.0000.0000", "2021.01.01.0000.0001")]
+ [InlineData("2021.01.01.0000.0000", "2021.01.01.0001.0000")]
+ [InlineData("2021.01.01.0000.0000", "2021.01.02.0000.0000")]
+ [InlineData("2021.01.01.0000.0000", "2021.02.01.0000.0000")]
+ [InlineData("2021.01.01.0000.0000", "2022.01.01.0000.0000")]
+ public void VersionComparison(string ver1, string ver2)
+ {
+ var v1 = GameVersion.Parse(ver1);
+ var v2 = GameVersion.Parse(ver2);
+
+ Assert.True(v1.CompareTo(v2) < 0);
+ }
+
+ [Theory]
+ [InlineData("2020.06.15.0000.0000")]
+ [InlineData("2021.01.01.0000")]
+ [InlineData("2021.01.01")]
+ [InlineData("2021.01")]
+ [InlineData("2021")]
+ public void VersionConstructor(string ver)
+ {
+ var v = GameVersion.Parse(ver);
+
+ Assert.True(v != null);
+ }
+
+ [Theory]
+ [InlineData("2020.06.15.0000.0000.0000")]
+ [InlineData("")]
+ public void VersionConstructorInvalid(string ver)
+ {
+ var result = GameVersion.TryParse(ver, out var v);
+
+ Assert.False(result);
+ Assert.Null(v);
+ }
+ }
+}
diff --git a/Dalamud.Test/Game/Text/Sanitizer/SanitizerTests.cs b/Dalamud.Test/Game/Text/Sanitizer/SanitizerTests.cs
index f2ce7a9c9..2d2a34df5 100644
--- a/Dalamud.Test/Game/Text/Sanitizer/SanitizerTests.cs
+++ b/Dalamud.Test/Game/Text/Sanitizer/SanitizerTests.cs
@@ -1,29 +1,29 @@
-// ReSharper disable StringLiteralTypo
-
using System.Collections.Generic;
using System.Linq;
using Xunit;
-namespace Dalamud.Test.Game.Text.Sanitizer {
-
- public class SanitizerTests {
+// ReSharper disable StringLiteralTypo
+namespace Dalamud.Test.Game.Text.Sanitizer
+{
+ public class SanitizerTests
+ {
private global::Dalamud.Game.Text.Sanitizer.Sanitizer sanitizer;
[Theory]
- [InlineData( ClientLanguage.English, "Pixie Cotton Hood of Healing", "Pixie Cotton Hood of Healing" )]
- [InlineData( ClientLanguage.Japanese, "アラガントームストーン:真理", "アラガントームストーン:真理" )]
- [InlineData( ClientLanguage.German, "Anemos-Pan\x02\x16\x01\x03zer\x02\x16\x01\x03hand\x02\x16\x01\x03schu\x02\x16\x01\x03he des Drachenbluts", "Anemos-Panzerhandschuhe des Drachenbluts" )]
- [InlineData( ClientLanguage.German, "Bienen-Spatha †", "Bienen-Spatha" )]
- [InlineData( ClientLanguage.French, "Le Diademe\x02\x1D\x01\x03: terrains de chasse|Le Diademe\x02\x1D\x01\x03: terrains de chasse", "Le Diademe: terrains de chasse|Le Diademe: terrains de chasse" )]
- [InlineData( ClientLanguage.French, "Cuir de bœuf", "Cuir de boeuf" )]
- public void StringsAreSanitizedCorrectly(
- ClientLanguage clientLanguage, string unsanitizedString, string sanitizedString)
+ [InlineData(ClientLanguage.English, "Pixie Cotton Hood of Healing", "Pixie Cotton Hood of Healing")]
+ [InlineData(ClientLanguage.Japanese, "アラガントームストーン:真理", "アラガントームストーン:真理")]
+ [InlineData(ClientLanguage.German, "Anemos-Pan\x02\x16\x01\x03zer\x02\x16\x01\x03hand\x02\x16\x01\x03schu\x02\x16\x01\x03he des Drachenbluts", "Anemos-Panzerhandschuhe des Drachenbluts")]
+ [InlineData(ClientLanguage.German, "Bienen-Spatha †", "Bienen-Spatha")]
+ [InlineData(ClientLanguage.French, "Le Diademe\x02\x1D\x01\x03: terrains de chasse|Le Diademe\x02\x1D\x01\x03: terrains de chasse", "Le Diademe: terrains de chasse|Le Diademe: terrains de chasse")]
+ [InlineData(ClientLanguage.French, "Cuir de bœuf", "Cuir de boeuf")]
+ public void StringsAreSanitizedCorrectly(ClientLanguage clientLanguage, string unsanitizedString, string sanitizedString)
{
- var sanitizedStrings = new List {unsanitizedString};
+ var sanitizedStrings = new List { unsanitizedString };
+
sanitizer = new global::Dalamud.Game.Text.Sanitizer.Sanitizer(clientLanguage);
Assert.Equal(sanitizedString, sanitizer.Sanitize(unsanitizedString));
Assert.Equal(sanitizedString, sanitizer.Sanitize(sanitizedStrings).First());
-
+
sanitizer = new global::Dalamud.Game.Text.Sanitizer.Sanitizer(ClientLanguage.English);
Assert.Equal(sanitizedString, sanitizer.Sanitize(unsanitizedString, clientLanguage));
Assert.Equal(sanitizedString, sanitizer.Sanitize(sanitizedStrings, clientLanguage).First());
diff --git a/Dalamud.Test/LocalizationTests.cs b/Dalamud.Test/LocalizationTests.cs
index 67977f805..093affc9a 100644
--- a/Dalamud.Test/LocalizationTests.cs
+++ b/Dalamud.Test/LocalizationTests.cs
@@ -1,20 +1,24 @@
-using System.IO;
+using System.IO;
using System.Reflection;
using Xunit;
-namespace Dalamud.Test {
- public class LocalizationTests {
+namespace Dalamud.Test
+{
+ public class LocalizationTests
+ {
private readonly Localization localization;
private string currentLangCode;
-
- public LocalizationTests() {
+
+ public LocalizationTests()
+ {
var workingDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
this.localization = new Localization(workingDir, "dalamud_");
this.localization.OnLocalizationChanged += code => this.currentLangCode = code;
}
[Fact]
- public void SetupWithFallbacks_EventInvoked() {
+ public void SetupWithFallbacks_EventInvoked()
+ {
this.localization.SetupWithFallbacks();
Assert.Equal("en", this.currentLangCode);
}
diff --git a/Dalamud.Test/Properties/AssemblyInfo.cs b/Dalamud.Test/Properties/AssemblyInfo.cs
deleted file mode 100644
index 589173bb4..000000000
--- a/Dalamud.Test/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.Reflection;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("Dalamud.Test")]
-[assembly: AssemblyDescription("Unit tests for Dalamud")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("goatcorp")]
-[assembly: AssemblyProduct("Dalamud.Test")]
-[assembly: AssemblyCopyright("Copyright © goatcorp 2021")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("C8004563-1806-4329-844F-0EF6274291FC")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Dalamud.Test/packages.config b/Dalamud.Test/packages.config
deleted file mode 100644
index f2ae94af3..000000000
--- a/Dalamud.Test/packages.config
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Dalamud.sln b/Dalamud.sln
index 16ca2e3ac..5b58f00d4 100644
--- a/Dalamud.sln
+++ b/Dalamud.sln
@@ -1,137 +1,162 @@
-
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.29215.179
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31410.414
MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CEF7D22B-CB85-400E-BD64-349A30E3C097}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ .gitignore = .gitignore
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "build", "build\build.csproj", "{94E5B016-02B1-459B-97D9-E783F28764B2}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud", "Dalamud\Dalamud.csproj", "{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}"
EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Boot", "Dalamud.Boot\Dalamud.Boot.vcxproj", "{55198DC3-A03D-408E-A8EB-2077780C8576}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Injector", "Dalamud.Injector\Dalamud.Injector.csproj", "{5B832F73-5F54-4ADC-870F-D0095EF72C9A}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGuiScene", "lib\ImGuiScene\ImGuiScene\ImGuiScene.csproj", "{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}"
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Injector.Boot", "Dalamud.Injector.Boot\Dalamud.Injector.Boot.vcxproj", "{8874326B-E755-4D13-90B4-59AB263A3E6B}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SDL2-CS", "lib\ImGuiScene\deps\SDL2-CS\SDL2-CS.csproj", "{85480198-8711-4355-830E-72FD794AD3F6}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGui.NET-472", "lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj", "{0483026E-C6CE-4B1A-AA68-46544C08140B}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interface", "Interface", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}"
EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DalamudDebugStub", "DalamudDebugStub\DalamudDebugStub.vcxproj", "{9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGui.NET-472", "lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj", "{0483026E-C6CE-4B1A-AA68-46544C08140B}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs", "lib\FFXIVClientStructs\FFXIVClientStructs.csproj", "{3DBAEE68-9D94-4807-BCB1-E42EDD52B489}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGuiScene", "lib\ImGuiScene\ImGuiScene\ImGuiScene.csproj", "{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SDL2-CS", "lib\ImGuiScene\deps\SDL2-CS\SDL2-CS.csproj", "{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.CorePlugin", "Dalamud.CorePlugin\Dalamud.CorePlugin.csproj", "{4AFDB34A-7467-4D41-B067-53BC4101D9D0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs", "lib\FFXIVClientStructs\FFXIVClientStructs\FFXIVClientStructs.csproj", "{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs.Common", "lib\FFXIVClientStructs\FFXIVClientStructs.Common\FFXIVClientStructs.Common.csproj", "{F3F0CC3A-DE2E-403F-88B4-B47C62582477}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs.Generators", "lib\FFXIVClientStructs\FFXIVClientStructs.Generators\FFXIVClientStructs.Generators.csproj", "{05AB2F46-268B-4915-806F-DDF813E2D59D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
- Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
- Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x64.Build.0 = Debug|Any CPU
+ {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x64.ActiveCfg = Release|Any CPU
+ {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x64.Build.0 = Release|Any CPU
+ {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.Build.0 = Debug|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x64.ActiveCfg = Debug|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x64.Build.0 = Debug|x64
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x86.ActiveCfg = Debug|Any CPU
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x86.Build.0 = Debug|Any CPU
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.ActiveCfg = Release|x64
+ {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.Build.0 = Release|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x64.ActiveCfg = Release|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x64.Build.0 = Release|x64
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x86.ActiveCfg = Release|Any CPU
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x86.Build.0 = Release|Any CPU
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|Any CPU.Build.0 = Debug|x64
+ {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x64.ActiveCfg = Debug|x64
+ {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x64.Build.0 = Debug|x64
+ {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|Any CPU.ActiveCfg = Release|x64
+ {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|Any CPU.Build.0 = Release|x64
+ {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x64.ActiveCfg = Release|x64
+ {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x64.Build.0 = Release|x64
+ {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x64.ActiveCfg = Debug|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x64.Build.0 = Debug|x64
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x86.ActiveCfg = Debug|Any CPU
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x86.Build.0 = Debug|Any CPU
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|x64
+ {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x64.ActiveCfg = Release|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x64.Build.0 = Release|x64
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x86.ActiveCfg = Release|Any CPU
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x86.Build.0 = Release|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.ActiveCfg = Debug|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.Build.0 = Debug|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x86.ActiveCfg = Debug|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x86.Build.0 = Debug|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.Build.0 = Release|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.ActiveCfg = Release|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.Build.0 = Release|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x86.ActiveCfg = Release|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x86.Build.0 = Release|Any CPU
- {85480198-8711-4355-830E-72FD794AD3F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {85480198-8711-4355-830E-72FD794AD3F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {85480198-8711-4355-830E-72FD794AD3F6}.Debug|x64.ActiveCfg = Debug|x64
- {85480198-8711-4355-830E-72FD794AD3F6}.Debug|x64.Build.0 = Debug|x64
- {85480198-8711-4355-830E-72FD794AD3F6}.Debug|x86.ActiveCfg = Debug|x86
- {85480198-8711-4355-830E-72FD794AD3F6}.Debug|x86.Build.0 = Debug|x86
- {85480198-8711-4355-830E-72FD794AD3F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {85480198-8711-4355-830E-72FD794AD3F6}.Release|Any CPU.Build.0 = Release|Any CPU
- {85480198-8711-4355-830E-72FD794AD3F6}.Release|x64.ActiveCfg = Release|x64
- {85480198-8711-4355-830E-72FD794AD3F6}.Release|x64.Build.0 = Release|x64
- {85480198-8711-4355-830E-72FD794AD3F6}.Release|x86.ActiveCfg = Release|x86
- {85480198-8711-4355-830E-72FD794AD3F6}.Release|x86.Build.0 = Release|x86
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.ActiveCfg = Debug|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.Build.0 = Debug|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x86.ActiveCfg = Debug|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x86.Build.0 = Debug|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.Build.0 = Release|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.ActiveCfg = Release|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.Build.0 = Release|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x86.ActiveCfg = Release|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x86.Build.0 = Release|Any CPU
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Debug|Any CPU.ActiveCfg = Debug|x64
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Debug|x64.ActiveCfg = Debug|x64
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Debug|x86.ActiveCfg = Debug|Win32
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|Any CPU.ActiveCfg = Release|x64
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|Any CPU.Build.0 = Release|x64
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|x64.ActiveCfg = Release|x64
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|x64.Build.0 = Release|x64
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|x86.ActiveCfg = Release|Win32
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|x86.Build.0 = Release|Win32
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|x64.ActiveCfg = Debug|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|x64.Build.0 = Debug|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|x86.ActiveCfg = Debug|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|x86.Build.0 = Debug|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|Any CPU.Build.0 = Release|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|x64.ActiveCfg = Release|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|x64.Build.0 = Release|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|x86.ActiveCfg = Release|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|x86.Build.0 = Release|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.ActiveCfg = Debug|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.Build.0 = Debug|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x86.ActiveCfg = Debug|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x86.Build.0 = Debug|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.Build.0 = Release|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.ActiveCfg = Release|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.Build.0 = Release|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Release|x86.ActiveCfg = Release|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Release|x86.Build.0 = Release|Any CPU
+ {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x64.ActiveCfg = Debug|x64
+ {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x64.Build.0 = Debug|x64
+ {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.ActiveCfg = Release|x64
+ {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x64.ActiveCfg = Release|x64
+ {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x64.Build.0 = Release|x64
+ {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|x64
+ {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.ActiveCfg = Debug|x64
+ {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.Build.0 = Debug|x64
+ {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64
+ {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.Build.0 = Release|x64
+ {C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.ActiveCfg = Release|x64
+ {C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.Build.0 = Release|x64
+ {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.Build.0 = Debug|x64
+ {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.ActiveCfg = Debug|x64
+ {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.Build.0 = Debug|x64
+ {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.ActiveCfg = Release|x64
+ {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.Build.0 = Release|x64
+ {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.ActiveCfg = Release|x64
+ {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.Build.0 = Release|x64
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.Build.0 = Debug|x64
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.ActiveCfg = Debug|x64
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.Build.0 = Debug|x64
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.ActiveCfg = Release|x64
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.Build.0 = Release|x64
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.ActiveCfg = Release|x64
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.Build.0 = Release|x64
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.Build.0 = Debug|x64
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x64.ActiveCfg = Debug|x64
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x64.Build.0 = Debug|x64
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.ActiveCfg = Release|x64
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.Build.0 = Release|x64
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x64.ActiveCfg = Release|x64
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x64.Build.0 = Release|x64
+ {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x64.Build.0 = Debug|Any CPU
+ {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x64.ActiveCfg = Release|Any CPU
+ {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x64.Build.0 = Release|Any CPU
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x64.Build.0 = Debug|Any CPU
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x64.ActiveCfg = Release|Any CPU
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x64.Build.0 = Release|Any CPU
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Debug|x64.Build.0 = Debug|Any CPU
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Release|x64.ActiveCfg = Release|Any CPU
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Release|x64.Build.0 = Release|Any CPU
+ {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x64.Build.0 = Debug|Any CPU
+ {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x64.ActiveCfg = Release|Any CPU
+ {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
- {85480198-8711-4355-830E-72FD794AD3F6} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{0483026E-C6CE-4B1A-AA68-46544C08140B} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
+ {05AB2F46-268B-4915-806F-DDF813E2D59D} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599}
diff --git a/Dalamud/Configuration/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
similarity index 89%
rename from Dalamud/Configuration/DalamudConfiguration.cs
rename to Dalamud/Configuration/Internal/DalamudConfiguration.cs
index 46c3381d0..678a68e78 100644
--- a/Dalamud/Configuration/DalamudConfiguration.cs
+++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
@@ -7,19 +7,19 @@ using Newtonsoft.Json;
using Serilog;
using Serilog.Events;
-namespace Dalamud.Configuration
+namespace Dalamud.Configuration.Internal
{
///
/// Class containing Dalamud settings.
///
[Serializable]
- public class DalamudConfiguration
+ internal sealed class DalamudConfiguration
{
[JsonIgnore]
private string configPath;
///
- /// Delegate for the event that occurs when the dalamud configuration is saved.
+ /// Delegate for the event that occurs when the dalamud configuration is saved.
///
/// The current dalamud configuration.
public delegate void DalamudConfigurationSavedDelegate(DalamudConfiguration dalamudConfiguration);
@@ -69,15 +69,25 @@ namespace Dalamud.Configuration
///
public bool DoDalamudTest { get; set; }
+ ///
+ /// Gets or sets a value indicating whether or not XL should download the Dalamud .NET runtime.
+ ///
+ public bool DoDalamudRuntime { get; set; }
+
///
/// Gets or sets a list of custom repos.
///
- public List ThirdRepoList { get; set; } = new List();
+ public List ThirdRepoList { get; set; } = new();
///
/// Gets or sets a list of hidden plugins.
///
- public List HiddenPluginInternalName { get; set; } = new List();
+ public List HiddenPluginInternalName { get; set; } = new();
+
+ ///
+ /// Gets or sets a list of additional settings for devPlugins.
+ ///
+ public List DevPluginSettings { get; set; } = new();
///
/// Gets or sets the global UI scale.
@@ -150,7 +160,7 @@ namespace Dalamud.Configuration
public bool IsAntiAntiDebugEnabled { get; set; } = false;
///
- /// Specifies the kind of beta to download when is set to true.
+ /// Gets or sets the kind of beta to download when is set to true.
///
public string DalamudBetaKind { get; set; }
diff --git a/Dalamud/Configuration/Internal/DevPluginSettings.cs b/Dalamud/Configuration/Internal/DevPluginSettings.cs
new file mode 100644
index 000000000..fc6557da6
--- /dev/null
+++ b/Dalamud/Configuration/Internal/DevPluginSettings.cs
@@ -0,0 +1,33 @@
+namespace Dalamud.Configuration.Internal
+{
+ ///
+ /// Settings for DevPlugins.
+ ///
+ internal sealed class DevPluginSettings
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Filename of the DLL representing this plugin.
+ public DevPluginSettings(string dllFile)
+ {
+ this.DllFile = dllFile;
+ }
+
+ ///
+ /// Gets or sets the path to a plugin DLL. This is automatically generated for any plugins in the devPlugins folder. However by
+ /// specifiying this value manually, you can add arbitrary files outside the normal file paths.
+ ///
+ public string DllFile { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether this plugin should automatically start when Dalamud boots up.
+ ///
+ public bool StartOnBoot { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether this plugin should automatically reload on file change.
+ ///
+ public bool AutomaticReloading { get; set; } = false;
+ }
+}
diff --git a/Dalamud/Configuration/ThirdRepoSetting.cs b/Dalamud/Configuration/Internal/ThirdPartyRepoSettings.cs
similarity index 56%
rename from Dalamud/Configuration/ThirdRepoSetting.cs
rename to Dalamud/Configuration/Internal/ThirdPartyRepoSettings.cs
index b0fcb03b0..cafb96a47 100644
--- a/Dalamud/Configuration/ThirdRepoSetting.cs
+++ b/Dalamud/Configuration/Internal/ThirdPartyRepoSettings.cs
@@ -3,7 +3,7 @@ namespace Dalamud.Configuration
///
/// Third party repository for dalamud plugins.
///
- public class ThirdRepoSetting
+ internal sealed class ThirdPartyRepoSettings
{
///
/// Gets or sets the third party repo url.
@@ -16,16 +16,14 @@ namespace Dalamud.Configuration
public bool IsEnabled { get; set; }
///
- /// Create new instance of third party repo object.
+ /// Gets or sets a short name for the repo url.
///
- /// New instance of third party repo.
- public ThirdRepoSetting Clone()
- {
- return new ThirdRepoSetting
- {
- Url = this.Url,
- IsEnabled = this.IsEnabled,
- };
- }
+ public string Name { get; set; }
+
+ ///
+ /// Clone this object.
+ ///
+ /// A shallow copy of this object.
+ public ThirdPartyRepoSettings Clone() => this.MemberwiseClone() as ThirdPartyRepoSettings;
}
}
diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs
index 5fa1d54cf..1cadb2180 100644
--- a/Dalamud/Configuration/PluginConfigurations.cs
+++ b/Dalamud/Configuration/PluginConfigurations.cs
@@ -7,7 +7,7 @@ namespace Dalamud.Configuration
///
/// Configuration to store settings for a dalamud plugin.
///
- public class PluginConfigurations
+ public sealed class PluginConfigurations
{
private readonly DirectoryInfo configDirectory;
diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs
index 5453da2a6..4d5bcead9 100644
--- a/Dalamud/Dalamud.cs
+++ b/Dalamud/Dalamud.cs
@@ -1,10 +1,10 @@
using System;
using System.Diagnostics;
using System.IO;
+using System.Runtime.CompilerServices;
using System.Threading;
-using System.Threading.Tasks;
-using Dalamud.Configuration;
+using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.Addon;
@@ -13,17 +13,24 @@ using Dalamud.Game.Command;
using Dalamud.Game.Internal;
using Dalamud.Game.Network;
using Dalamud.Game.Text.SeStringHandling;
-using Dalamud.Interface;
-using Dalamud.Plugin;
+using Dalamud.Hooking.Internal;
+using Dalamud.Interface.Internal;
+using Dalamud.Memory;
+using Dalamud.Plugin.Internal;
using Serilog;
using Serilog.Core;
+#if DEBUG
+// This allows for rapid prototyping of Dalamud modules with access to internal objects.
+[assembly: InternalsVisibleTo("Dalamud.CorePlugin")]
+#endif
+
namespace Dalamud
{
///
/// The main Dalamud class containing all subsystems.
///
- public sealed class Dalamud : IDisposable
+ internal sealed class Dalamud : IDisposable
{
#region Internals
@@ -31,8 +38,6 @@ namespace Dalamud
private readonly ManualResetEvent finishUnloadSignal;
- private readonly string baseDirectory;
-
private bool hasDisposedPlugins = false;
#endregion
@@ -46,11 +51,14 @@ namespace Dalamud
/// The Dalamud configuration.
public Dalamud(DalamudStartInfo info, LoggingLevelSwitch loggingLevelSwitch, ManualResetEvent finishSignal, DalamudConfiguration configuration)
{
+#if DEBUG
+ Instance = this;
+#endif
this.StartInfo = info;
this.LogLevelSwitch = loggingLevelSwitch;
this.Configuration = configuration;
- this.baseDirectory = info.WorkingDirectory;
+ // this.baseDirectory = info.WorkingDirectory;
this.unloadSignal = new ManualResetEvent(false);
this.unloadSignal.Reset();
@@ -59,6 +67,13 @@ namespace Dalamud
this.finishUnloadSignal.Reset();
}
+#if DEBUG
+ ///
+ /// Gets the Dalamud singleton instance.
+ ///
+ internal static Dalamud Instance { get; private set; }
+#endif
+
#region Native Game Subsystems
///
@@ -76,6 +91,11 @@ namespace Dalamud
///
internal WinSockHandlers WinSock2 { get; private set; }
+ ///
+ /// Gets Hook management subsystem.
+ ///
+ internal HookManager HookManager { get; private set; }
+
///
/// Gets ImGui Interface subsystem.
///
@@ -95,11 +115,6 @@ namespace Dalamud
///
internal PluginManager PluginManager { get; private set; }
- ///
- /// Gets Plugin Repository subsystem.
- ///
- internal PluginRepository PluginRepository { get; private set; }
-
///
/// Gets Data provider subsystem.
///
@@ -205,6 +220,7 @@ namespace Dalamud
// Initialize the process information.
this.TargetModule = Process.GetCurrentProcess().MainModule;
this.SigScanner = new SigScanner(this.TargetModule, true);
+ this.HookManager = new HookManager(this);
// Initialize game subsystem
this.Framework = new Framework(this.SigScanner, this);
@@ -289,6 +305,7 @@ namespace Dalamud
Log.Information("[T2] Data OK!");
this.SeStringManager = new SeStringManager(this.Data);
+ MemoryHelper.Initialize(this); // For SeString handling
Log.Information("[T2] SeString OK!");
@@ -327,28 +344,18 @@ namespace Dalamud
{
Log.Information("[T3] START!");
- this.PluginRepository =
- new PluginRepository(this, this.StartInfo.PluginDirectory, this.StartInfo.GameVersion);
-
- Log.Information("[T3] PREPO OK!");
-
if (!bool.Parse(Environment.GetEnvironmentVariable("DALAMUD_NOT_HAVE_PLUGINS") ?? "false"))
{
try
{
- this.PluginRepository.CleanupPlugins();
-
- Log.Information("[T3] PRC OK!");
-
- this.PluginManager = new PluginManager(
- this,
- this.StartInfo.PluginDirectory,
- this.StartInfo.DefaultPluginDirectory);
- this.PluginManager.LoadSynchronousPlugins();
-
- Task.Run(() => this.PluginManager.LoadDeferredPlugins());
-
+ this.PluginManager = new PluginManager(this);
Log.Information("[T3] PM OK!");
+
+ this.PluginManager.CleanupPlugins();
+ Log.Information("[T3] PMC OK!");
+
+ this.PluginManager.LoadAllPlugins();
+ Log.Information("[T3] PML OK!");
}
catch (Exception ex)
{
@@ -357,8 +364,6 @@ namespace Dalamud
}
this.DalamudUi = new DalamudInterface(this);
- this.InterfaceManager.OnDraw += this.DalamudUi.Draw;
-
Log.Information("[T3] DUI OK!");
Troubleshooting.LogTroubleshooting(this, this.InterfaceManager != null);
@@ -410,16 +415,9 @@ namespace Dalamud
// use any resources that it freed in its own Dispose method
this.InterfaceManager?.Dispose();
- try
- {
- this.PluginManager.UnloadPlugins();
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Plugin unload failed.");
- }
-
this.DalamudUi?.Dispose();
+
+ this.PluginManager?.Dispose();
}
///
@@ -436,20 +434,23 @@ namespace Dalamud
}
this.Framework?.Dispose();
+
this.ClientState?.Dispose();
this.unloadSignal?.Dispose();
this.WinSock2?.Dispose();
- this.SigScanner?.Dispose();
-
this.Data?.Dispose();
this.AntiDebug?.Dispose();
this.SystemMenu?.Dispose();
+ this.HookManager?.Dispose();
+
+ this.SigScanner?.Dispose();
+
Log.Debug("Dalamud::Dispose() OK!");
}
catch (Exception ex)
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index f656e0cef..95ddc2ad5 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -1,105 +1,124 @@
+
- AnyCPU
- net472
+ net5.0-windows
+ x64
+ x64;AnyCPU
9.0
- AnyCPU;x64
-
-
- Library
-
- false
- true
- full
- $(SolutionDir)\bin\Dalamud.xml
+
5.2.7.0
- true
+ XIV Launcher addon framework
$(DalamudVersion)
$(DalamudVersion)
$(DalamudVersion)
-
-
-
-
- $(MSBuildProjectDirectory)\
- $(AppOutputBase)=C:\goatsoft\companysecrets\dalamud\
+
+
+ Library
+ ..\bin\$(Configuration)\
+ false
+ false
+ true
+ false
+
+
+
+ $(OutputPath)Dalamud.xml
+ true
+
+
+
+ true
+ true
+ true
+ portable
true
+ annotations
+ true
-
- false
+
+
+ Debug
-
+
DEBUG;TRACE
-
- IDE0017;IDE0044;IDE0047;IDE0048;IDE1006;CS1573;CS1591;CS1701;CS1702
-
-
-
-
-
-
+
+ $(MSBuildProjectDirectory)\
+ $(AppOutputBase)=C:\goatsoft\companysecrets\dalamud\
+
+
+
+ IDE0003;IDE1006;CS1591;CS1701;CS1702
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+ all
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
-
+
+
+
+
+
+
+
+
+
True
True
Resources.resx
-
-
ResXFileCodeGenerator
Resources.Designer.cs
+
-
-
-
-
-
-
-
+
PreserveNewest
- Lumina.Generated.dll
-
+
+
+
+
+
+
+
+
diff --git a/Dalamud/DalamudStartInfo.cs b/Dalamud/DalamudStartInfo.cs
index 78ceca446..7678cfb40 100644
--- a/Dalamud/DalamudStartInfo.cs
+++ b/Dalamud/DalamudStartInfo.cs
@@ -1,12 +1,15 @@
using System;
+using Dalamud.Game;
+using Newtonsoft.Json;
+
namespace Dalamud
{
///
- /// Class containing information needed to initialize Dalamud.
+ /// Struct containing information needed to initialize Dalamud.
///
[Serializable]
- public sealed class DalamudStartInfo
+ public struct DalamudStartInfo
{
///
/// The working directory of the XIVLauncher installations.
@@ -41,7 +44,8 @@ namespace Dalamud
///
/// The current game version code.
///
- public string GameVersion;
+ [JsonConverter(typeof(GameVersionConverter))]
+ public GameVersion GameVersion;
///
/// Whether or not market board information should be uploaded by default.
diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs
index a6e4016e5..ebdf24c57 100644
--- a/Dalamud/Data/DataManager.cs
+++ b/Dalamud/Data/DataManager.cs
@@ -7,6 +7,7 @@ using System.Threading;
using Dalamud.Data.LuminaExtensions;
using Dalamud.Interface;
+using Dalamud.Interface.Internal;
using ImGuiScene;
using JetBrains.Annotations;
using Lumina;
@@ -21,7 +22,7 @@ namespace Dalamud.Data
///
/// This class provides data for Dalamud-internal features, but can also be used by plugins if needed.
///
- public class DataManager : IDisposable
+ public sealed class DataManager : IDisposable
{
private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex";
private readonly InterfaceManager interfaceManager;
@@ -32,6 +33,7 @@ namespace Dalamud.Data
private GameData gameData;
private Thread luminaResourceThread;
+ private CancellationTokenSource luminaCancellationTokenSource;
///
/// Initializes a new instance of the class.
@@ -41,8 +43,9 @@ namespace Dalamud.Data
internal DataManager(ClientLanguage language, InterfaceManager interfaceManager)
{
this.interfaceManager = interfaceManager;
+
// Set up default values so plugins do not null-reference when data is being loaded.
- this.ServerOpCodes = new ReadOnlyDictionary(new Dictionary());
+ this.ClientOpCodes = this.ServerOpCodes = new ReadOnlyDictionary(new Dictionary());
this.Language = language;
}
@@ -99,7 +102,7 @@ namespace Dalamud.Data
ClientLanguage.English => Lumina.Data.Language.English,
ClientLanguage.German => Lumina.Data.Language.German,
ClientLanguage.French => Lumina.Data.Language.French,
- _ => throw new ArgumentOutOfRangeException(nameof(this.Language), $"Unknown Language: {this.Language}"),
+ _ => throw new ArgumentOutOfRangeException(nameof(language), $"Unknown Language: {language}"),
};
return this.Excel.GetSheet(lang);
}
@@ -162,7 +165,7 @@ namespace Dalamud.Data
ClientLanguage.English => "en/",
ClientLanguage.German => "de/",
ClientLanguage.French => "fr/",
- _ => throw new ArgumentOutOfRangeException(nameof(this.Language), $"Unknown Language: {this.Language}"),
+ _ => throw new ArgumentOutOfRangeException(nameof(iconLanguage), $"Unknown Language: {iconLanguage}"),
};
return this.GetIcon(type, iconId);
@@ -232,7 +235,7 @@ namespace Dalamud.Data
///
public void Dispose()
{
- this.luminaResourceThread.Abort();
+ this.luminaCancellationTokenSource.Cancel();
}
///
@@ -245,14 +248,14 @@ namespace Dalamud.Data
{
Log.Verbose("Starting data load...");
- var zoneOpCodeDict =
- JsonConvert.DeserializeObject>(File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json")));
+ var zoneOpCodeDict = JsonConvert.DeserializeObject>(
+ File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json")));
this.ServerOpCodes = new ReadOnlyDictionary(zoneOpCodeDict);
Log.Verbose("Loaded {0} ServerOpCodes.", zoneOpCodeDict.Count);
- var clientOpCodeDict =
- JsonConvert.DeserializeObject>(File.ReadAllText(Path.Combine(baseDir, "UIRes", "clientopcode.json")));
+ var clientOpCodeDict = JsonConvert.DeserializeObject>(
+ File.ReadAllText(Path.Combine(baseDir, "UIRes", "clientopcode.json")));
this.ClientOpCodes = new ReadOnlyDictionary(clientOpCodeDict);
Log.Verbose("Loaded {0} ClientOpCodes.", clientOpCodeDict.Count);
@@ -273,9 +276,7 @@ namespace Dalamud.Data
ClientLanguage.English => Lumina.Data.Language.English,
ClientLanguage.German => Lumina.Data.Language.German,
ClientLanguage.French => Lumina.Data.Language.French,
- _ => throw new ArgumentOutOfRangeException(
- nameof(this.Language),
- @"Unknown Language: " + this.Language),
+ _ => throw new ArgumentOutOfRangeException(nameof(this.Language), $"Unknown Language: {this.Language}"),
},
};
@@ -289,9 +290,12 @@ namespace Dalamud.Data
this.IsDataReady = true;
- this.luminaResourceThread = new Thread(() =>
+ this.luminaCancellationTokenSource = new();
+
+ var luminaCancellationToken = this.luminaCancellationTokenSource.Token;
+ this.luminaResourceThread = new(() =>
{
- while (true)
+ while (!luminaCancellationToken.IsCancellationRequested)
{
if (this.gameData.FileHandleManager.HasPendingFileLoads)
{
@@ -302,8 +306,6 @@ namespace Dalamud.Data
Thread.Sleep(5);
}
}
-
- // ReSharper disable once FunctionNeverReturns
});
this.luminaResourceThread.Start();
}
diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs
index 9287ec033..d12624fbd 100644
--- a/Dalamud/EntryPoint.cs
+++ b/Dalamud/EntryPoint.cs
@@ -1,12 +1,13 @@
using System;
using System.IO;
using System.Net;
+using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
-using Dalamud.Configuration;
-using Dalamud.Interface;
-using EasyHook;
+using Dalamud.Configuration.Internal;
+using Dalamud.Interface.Internal;
+using Newtonsoft.Json;
using Serilog;
using Serilog.Core;
using Serilog.Events;
@@ -16,31 +17,41 @@ namespace Dalamud
///
/// The main entrypoint for the Dalamud system.
///
- public sealed class EntryPoint : IEntryPoint
+ public sealed class EntryPoint
{
///
- /// Initializes a new instance of the class.
+ /// A delegate used during initialization of the CLR from Dalamud.Boot.
///
- /// The used to load the DLL.
- /// The containing information needed to initialize Dalamud.
- public EntryPoint(RemoteHooking.IContext ctx, DalamudStartInfo info)
+ /// Pointer to a serialized data.
+ public delegate void InitDelegate(IntPtr infoPtr);
+
+ ///
+ /// Initialize Dalamud.
+ ///
+ /// Pointer to a serialized data.
+ public static void Initialize(IntPtr infoPtr)
{
- // Required by EasyHook
+ var infoStr = Marshal.PtrToStringAnsi(infoPtr);
+ var info = JsonConvert.DeserializeObject(infoStr);
+
+ new Thread(() => RunThread(info)).Start();
}
///
/// Initialize all Dalamud subsystems and start running on the main thread.
///
- /// The used to load the DLL.
/// The containing information needed to initialize Dalamud.
- public void Run(RemoteHooking.IContext ctx, DalamudStartInfo info)
+ private static void RunThread(DalamudStartInfo info)
{
// Load configuration first to get some early persistent state, like log level
var configuration = DalamudConfiguration.Load(info.ConfigurationPath);
// Setup logger
- var (logger, levelSwitch) = this.NewLogger(info.WorkingDirectory, configuration.LogLevel);
- Log.Logger = logger;
+ var levelSwitch = InitLogging(info.WorkingDirectory, configuration);
+
+ // Log any unhandled exception.
+ AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
+ TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
var finishSignal = new ManualResetEvent(false);
@@ -50,12 +61,7 @@ namespace Dalamud
Log.Information("Initializing a session..");
// This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally
- System.Net.ServicePointManager.SecurityProtocol =
- SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls | SecurityProtocolType.Ssl3;
-
- // Log any unhandled exception.
- AppDomain.CurrentDomain.UnhandledException += this.OnUnhandledException;
- TaskScheduler.UnobservedTaskException += this.OnUnobservedTaskException;
+ ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls;
var dalamud = new Dalamud(info, levelSwitch, finishSignal, configuration);
Log.Information("Starting a session..");
@@ -72,7 +78,8 @@ namespace Dalamud
}
finally
{
- AppDomain.CurrentDomain.UnhandledException -= this.OnUnhandledException;
+ TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
+ AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;
Log.Information("Session has ended.");
Log.CloseAndFlush();
@@ -81,7 +88,7 @@ namespace Dalamud
}
}
- private (Logger Logger, LoggingLevelSwitch LevelSwitch) NewLogger(string baseDirectory, LogEventLevel logLevel)
+ private static LoggingLevelSwitch InitLogging(string baseDirectory, DalamudConfiguration configuration)
{
#if DEBUG
var logPath = Path.Combine(baseDirectory, "dalamud.log");
@@ -94,35 +101,34 @@ namespace Dalamud
#if DEBUG
levelSwitch.MinimumLevel = LogEventLevel.Verbose;
#else
- levelSwitch.MinimumLevel = logLevel;
+ levelSwitch.MinimumLevel = configuration.LogLevel;
#endif
-
- var newLogger = new LoggerConfiguration()
+ Log.Logger = new LoggerConfiguration()
.WriteTo.Async(a => a.File(logPath))
.WriteTo.Sink(SerilogEventSink.Instance)
.MinimumLevel.ControlledBy(levelSwitch)
.CreateLogger();
- return (newLogger, levelSwitch);
+ return levelSwitch;
}
- private void OnUnhandledException(object sender, UnhandledExceptionEventArgs arg)
+ private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs args)
{
- switch (arg.ExceptionObject)
+ switch (args.ExceptionObject)
{
case Exception ex:
Log.Fatal(ex, "Unhandled exception on AppDomain");
break;
default:
- Log.Fatal("Unhandled SEH object on AppDomain: {Object}", arg.ExceptionObject);
+ Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject);
break;
}
}
- private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
+ private static void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs args)
{
- if (!e.Observed)
- Log.Error(e.Exception, "Unobserved exception in Task.");
+ if (!args.Observed)
+ Log.Error(args.Exception, "Unobserved exception in Task.");
}
}
}
diff --git a/Dalamud/FodyWeavers.xsd b/Dalamud/FodyWeavers.xsd
index 2f1b8aae7..69dbe488c 100644
--- a/Dalamud/FodyWeavers.xsd
+++ b/Dalamud/FodyWeavers.xsd
@@ -11,6 +11,16 @@
Used to control if the On_PropertyName_Changed feature is enabled.
+
+
+ Used to control if the Dependent properties feature is enabled.
+
+
+
+
+ Used to control if the IsChanged property feature is enabled.
+
+
Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form.
@@ -31,6 +41,16 @@
Used to control if equality checks should use the static Equals method resolved from the base class.
+
+
+ Used to turn off build warnings from this weaver.
+
+
+
+
+ Used to turn off build warnings about mismatched On_PropertyName_Changed methods.
+
+
diff --git a/Dalamud/Game/Addon/DalamudSystemMenu.cs b/Dalamud/Game/Addon/DalamudSystemMenu.cs
index b783730d3..e735b6154 100644
--- a/Dalamud/Game/Addon/DalamudSystemMenu.cs
+++ b/Dalamud/Game/Addon/DalamudSystemMenu.cs
@@ -16,11 +16,11 @@ namespace Dalamud.Game.Addon
internal sealed unsafe partial class DalamudSystemMenu
{
private readonly Dalamud dalamud;
- private AtkValueChangeType atkValueChangeType;
- private AtkValueSetString atkValueSetString;
- private Hook hookAgentHudOpenSystemMenu;
+ private readonly AtkValueChangeType atkValueChangeType;
+ private readonly AtkValueSetString atkValueSetString;
+ private readonly Hook hookAgentHudOpenSystemMenu;
// TODO: Make this into events in Framework.Gui
- private Hook hookUiModuleRequestMainCommand;
+ private readonly Hook hookUiModuleRequestMainCommand;
///
/// Initializes a new instance of the class.
@@ -34,13 +34,10 @@ namespace Dalamud.Game.Addon
this.hookAgentHudOpenSystemMenu = new Hook(openSystemMenuAddress, this.AgentHudOpenSystemMenuDetour);
- var atkValueChangeTypeAddress =
- this.dalamud.SigScanner.ScanText("E8 ?? ?? ?? ?? 45 84 F6 48 8D 4C 24 ??");
- this.atkValueChangeType =
- Marshal.GetDelegateForFunctionPointer(atkValueChangeTypeAddress);
+ var atkValueChangeTypeAddress = this.dalamud.SigScanner.ScanText("E8 ?? ?? ?? ?? 45 84 F6 48 8D 4C 24 ??");
+ this.atkValueChangeType = Marshal.GetDelegateForFunctionPointer(atkValueChangeTypeAddress);
- var atkValueSetStringAddress =
- this.dalamud.SigScanner.ScanText("E8 ?? ?? ?? ?? 41 03 ED");
+ var atkValueSetStringAddress = this.dalamud.SigScanner.ScanText("E8 ?? ?? ?? ?? 41 03 ED");
this.atkValueSetString = Marshal.GetDelegateForFunctionPointer(atkValueSetStringAddress);
var uiModuleRequestMainCommandAddress = this.dalamud.SigScanner.ScanText("40 53 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B 01 8B DA 48 8B F1 FF 90 ?? ?? ?? ??");
@@ -140,10 +137,10 @@ namespace Dalamud.Game.Addon
switch (commandId)
{
case 69420:
- this.dalamud.DalamudUi.TogglePluginInstaller();
+ this.dalamud.DalamudUi.TogglePluginInstallerWindow();
break;
case 69421:
- this.dalamud.DalamudUi.ToggleSettings();
+ this.dalamud.DalamudUi.ToggleSettingsWindow();
break;
default:
this.hookUiModuleRequestMainCommand.Original(thisPtr, commandId);
diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs
index 5beb4c5ed..9fa2411df 100644
--- a/Dalamud/Game/ChatHandlers.cs
+++ b/Dalamud/Game/ChatHandlers.cs
@@ -11,7 +11,7 @@ using CheapLoc;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
-using Dalamud.Interface;
+using Dalamud.Interface.Internal.Windows;
using Serilog;
namespace Dalamud.Game
@@ -21,39 +21,39 @@ namespace Dalamud.Game
///
public class ChatHandlers
{
- private static readonly Dictionary UnicodeToDiscordEmojiDict = new()
- {
- { "", "<:ffxive071:585847382210642069>" },
- { "", "<:ffxive083:585848592699490329>" },
- };
+ // private static readonly Dictionary UnicodeToDiscordEmojiDict = new()
+ // {
+ // { "", "<:ffxive071:585847382210642069>" },
+ // { "", "<:ffxive083:585848592699490329>" },
+ // };
- private readonly Dictionary handledChatTypeColors = new()
- {
- { XivChatType.CrossParty, Color.DodgerBlue },
- { XivChatType.Party, Color.DodgerBlue },
- { XivChatType.FreeCompany, Color.DeepSkyBlue },
- { XivChatType.CrossLinkShell1, Color.ForestGreen },
- { XivChatType.CrossLinkShell2, Color.ForestGreen },
- { XivChatType.CrossLinkShell3, Color.ForestGreen },
- { XivChatType.CrossLinkShell4, Color.ForestGreen },
- { XivChatType.CrossLinkShell5, Color.ForestGreen },
- { XivChatType.CrossLinkShell6, Color.ForestGreen },
- { XivChatType.CrossLinkShell7, Color.ForestGreen },
- { XivChatType.CrossLinkShell8, Color.ForestGreen },
- { XivChatType.Ls1, Color.ForestGreen },
- { XivChatType.Ls2, Color.ForestGreen },
- { XivChatType.Ls3, Color.ForestGreen },
- { XivChatType.Ls4, Color.ForestGreen },
- { XivChatType.Ls5, Color.ForestGreen },
- { XivChatType.Ls6, Color.ForestGreen },
- { XivChatType.Ls7, Color.ForestGreen },
- { XivChatType.Ls8, Color.ForestGreen },
- { XivChatType.TellIncoming, Color.HotPink },
- { XivChatType.PvPTeam, Color.SandyBrown },
- { XivChatType.Urgent, Color.DarkViolet },
- { XivChatType.NoviceNetwork, Color.SaddleBrown },
- { XivChatType.Echo, Color.Gray },
- };
+ // private readonly Dictionary handledChatTypeColors = new()
+ // {
+ // { XivChatType.CrossParty, Color.DodgerBlue },
+ // { XivChatType.Party, Color.DodgerBlue },
+ // { XivChatType.FreeCompany, Color.DeepSkyBlue },
+ // { XivChatType.CrossLinkShell1, Color.ForestGreen },
+ // { XivChatType.CrossLinkShell2, Color.ForestGreen },
+ // { XivChatType.CrossLinkShell3, Color.ForestGreen },
+ // { XivChatType.CrossLinkShell4, Color.ForestGreen },
+ // { XivChatType.CrossLinkShell5, Color.ForestGreen },
+ // { XivChatType.CrossLinkShell6, Color.ForestGreen },
+ // { XivChatType.CrossLinkShell7, Color.ForestGreen },
+ // { XivChatType.CrossLinkShell8, Color.ForestGreen },
+ // { XivChatType.Ls1, Color.ForestGreen },
+ // { XivChatType.Ls2, Color.ForestGreen },
+ // { XivChatType.Ls3, Color.ForestGreen },
+ // { XivChatType.Ls4, Color.ForestGreen },
+ // { XivChatType.Ls5, Color.ForestGreen },
+ // { XivChatType.Ls6, Color.ForestGreen },
+ // { XivChatType.Ls7, Color.ForestGreen },
+ // { XivChatType.Ls8, Color.ForestGreen },
+ // { XivChatType.TellIncoming, Color.HotPink },
+ // { XivChatType.PvPTeam, Color.SandyBrown },
+ // { XivChatType.Urgent, Color.DarkViolet },
+ // { XivChatType.NoviceNetwork, Color.SaddleBrown },
+ // { XivChatType.Echo, Color.Gray },
+ // };
private readonly Regex rmtRegex = new(
@"4KGOLD|We have sufficient stock|VPK\.OM|Gil for free|www\.so9\.com|Fast & Convenient|Cheap & Safety Guarantee|【Code|A O A U E|igfans|4KGOLD\.COM|Cheapest Gil with|pvp and bank on google|Selling Cheap GIL|ff14mogstation\.com|Cheap Gil 1000k|gilsforyou|server 1000K =|gils_selling|E A S Y\.C O M|bonus code|mins delivery guarantee|Sell cheap|Salegm\.com|cheap Mog|Off Code:|FF14Mog.com|使用する5%オ|Off Code( *):|offers Fantasia",
@@ -96,14 +96,14 @@ namespace Dalamud.Game
private readonly Regex urlRegex = new(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled);
private readonly Dalamud dalamud;
- private DalamudLinkPayload openInstallerWindowLink;
+ private readonly DalamudLinkPayload openInstallerWindowLink;
private bool hasSeenLoadingMsg;
///
/// Initializes a new instance of the class.
///
/// Dalamud instance.
- public ChatHandlers(Dalamud dalamud)
+ internal ChatHandlers(Dalamud dalamud)
{
this.dalamud = dalamud;
@@ -121,24 +121,24 @@ namespace Dalamud.Game
///
public string LastLink { get; private set; }
- ///
- /// Convert a string to SeString and wrap in italics payloads.
- ///
- /// Text to convert.
- /// SeString payload of italicized text.
- private static SeString MakeItalics(string text)
- {
- // TODO: when the code OnCharMessage is switched to SeString, this can be a straight insertion of the
- // italics payloads only, and be a lot cleaner
- var italicString = new SeString(new List(new Payload[]
- {
- EmphasisItalicPayload.ItalicsOn,
- new TextPayload(text),
- EmphasisItalicPayload.ItalicsOff,
- }));
-
- return italicString;
- }
+ // ///
+ // /// Convert a string to SeString and wrap in italics payloads.
+ // ///
+ // /// Text to convert.
+ // /// SeString payload of italicized text.
+ // private static SeString MakeItalics(string text)
+ // {
+ // // TODO: when the code OnCharMessage is switched to SeString, this can be a straight insertion of the
+ // // italics payloads only, and be a lot cleaner
+ // var italicString = new SeString(new List(new Payload[]
+ // {
+ // EmphasisItalicPayload.ItalicsOn,
+ // new TextPayload(text),
+ // EmphasisItalicPayload.ItalicsOff,
+ // }));
+ //
+ // return italicString;
+ // }
private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled)
{
@@ -243,13 +243,13 @@ namespace Dalamud.Game
var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString();
this.dalamud.Framework.Gui.Chat.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud vD{0} loaded."), assemblyVersion)
- + string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), this.dalamud.PluginManager.Plugins.Count));
+ + string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), this.dalamud.PluginManager.InstalledPlugins.Count));
if (this.dalamud.Configuration.PrintPluginsWelcomeMsg)
{
- foreach (var plugin in this.dalamud.PluginManager.Plugins.OrderBy(x => x.Plugin.Name))
+ foreach (var plugin in this.dalamud.PluginManager.InstalledPlugins.OrderBy(plugin => plugin.Name))
{
- this.dalamud.Framework.Gui.Chat.Print(string.Format(Loc.Localize("DalamudPluginLoaded", " 》 {0} v{1} loaded."), plugin.Plugin.Name, plugin.Definition.AssemblyVersion));
+ this.dalamud.Framework.Gui.Chat.Print(string.Format(Loc.Localize("DalamudPluginLoaded", " 》 {0} v{1} loaded."), plugin.Name, plugin.Manifest.AssemblyVersion));
}
}
@@ -261,16 +261,15 @@ namespace Dalamud.Game
Type = XivChatType.Notice,
});
- if (DalamudChangelogWindow.WarrantsChangelog)
-#pragma warning disable CS0162 // Unreachable code detected
- this.dalamud.DalamudUi.OpenChangelog();
-#pragma warning restore CS0162 // Unreachable code detected
+ if (this.dalamud.DalamudUi.WarrantsChangelog)
+ this.dalamud.DalamudUi.OpenChangelogWindow();
this.dalamud.Configuration.LastVersion = assemblyVersion;
this.dalamud.Configuration.Save();
}
- Task.Run(() => this.dalamud.PluginRepository.UpdatePlugins(!this.dalamud.Configuration.AutoUpdatePlugins)).ContinueWith(t =>
+ Task.Run(() => this.dalamud.PluginManager.UpdatePlugins(!this.dalamud.Configuration.AutoUpdatePlugins))
+ .ContinueWith(t =>
{
if (t.IsFaulted)
{
@@ -278,13 +277,13 @@ namespace Dalamud.Game
}
else
{
- var updatedPlugins = t.Result.UpdatedPlugins;
+ var updatedPlugins = t.Result;
if (updatedPlugins != null && updatedPlugins.Any())
{
if (this.dalamud.Configuration.AutoUpdatePlugins)
{
- this.dalamud.PluginRepository.PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:"));
+ this.dalamud.PluginManager.PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:"));
}
else
{
diff --git a/Dalamud/Game/ClientState/Actors/ActorTable.cs b/Dalamud/Game/ClientState/Actors/ActorTable.cs
index 7f8ca7c35..f950418b3 100644
--- a/Dalamud/Game/ClientState/Actors/ActorTable.cs
+++ b/Dalamud/Game/ClientState/Actors/ActorTable.cs
@@ -34,14 +34,14 @@ namespace Dalamud.Game.ClientState.Actors
///
/// The Dalamud instance.
/// The ClientStateAddressResolver instance.
- public ActorTable(Dalamud dalamud, ClientStateAddressResolver addressResolver)
+ internal ActorTable(Dalamud dalamud, ClientStateAddressResolver addressResolver)
{
this.address = addressResolver;
this.dalamud = dalamud;
dalamud.Framework.OnUpdateEvent += this.Framework_OnUpdateEvent;
- Log.Verbose("Actor table address {ActorTable}", this.address.ActorTable);
+ Log.Verbose($"Actor table address 0x{this.address.ActorTable.ToInt64():X}");
}
///
diff --git a/Dalamud/Game/ClientState/Actors/Resolvers/BaseResolver.cs b/Dalamud/Game/ClientState/Actors/Resolvers/BaseResolver.cs
index c097b1111..728939255 100644
--- a/Dalamud/Game/ClientState/Actors/Resolvers/BaseResolver.cs
+++ b/Dalamud/Game/ClientState/Actors/Resolvers/BaseResolver.cs
@@ -11,7 +11,7 @@ namespace Dalamud.Game.ClientState.Actors.Resolvers
/// Initializes a new instance of the class.
///
/// The Dalamud instance.
- public BaseResolver(Dalamud dalamud)
+ internal BaseResolver(Dalamud dalamud)
{
this.dalamud = dalamud;
}
@@ -19,6 +19,6 @@ namespace Dalamud.Game.ClientState.Actors.Resolvers
///
/// Gets the Dalamud instance.
///
- protected Dalamud Dalamud => this.dalamud;
+ internal Dalamud Dalamud => this.dalamud;
}
}
diff --git a/Dalamud/Game/ClientState/Actors/Resolvers/ClassJob.cs b/Dalamud/Game/ClientState/Actors/Resolvers/ClassJob.cs
index 57ae9fc48..19fea14a7 100644
--- a/Dalamud/Game/ClientState/Actors/Resolvers/ClassJob.cs
+++ b/Dalamud/Game/ClientState/Actors/Resolvers/ClassJob.cs
@@ -16,7 +16,7 @@ namespace Dalamud.Game.ClientState.Actors.Resolvers
///
/// The ID of the classJob.
/// The Dalamud instance.
- public ClassJob(byte id, Dalamud dalamud)
+ internal ClassJob(byte id, Dalamud dalamud)
: base(dalamud)
{
this.Id = id;
diff --git a/Dalamud/Game/ClientState/Actors/Resolvers/World.cs b/Dalamud/Game/ClientState/Actors/Resolvers/World.cs
index 536bfaf81..6a5d437f0 100644
--- a/Dalamud/Game/ClientState/Actors/Resolvers/World.cs
+++ b/Dalamud/Game/ClientState/Actors/Resolvers/World.cs
@@ -16,7 +16,7 @@ namespace Dalamud.Game.ClientState.Actors.Resolvers
///
/// The ID of the world.
/// The Dalamud instance.
- public World(ushort id, Dalamud dalamud)
+ internal World(ushort id, Dalamud dalamud)
: base(dalamud)
{
this.Id = id;
diff --git a/Dalamud/Game/ClientState/Actors/Types/Actor.cs b/Dalamud/Game/ClientState/Actors/Types/Actor.cs
index 9707f6674..a38daf5b0 100644
--- a/Dalamud/Game/ClientState/Actors/Types/Actor.cs
+++ b/Dalamud/Game/ClientState/Actors/Types/Actor.cs
@@ -1,7 +1,6 @@
using System;
-using System.Text;
+
using Dalamud.Game.ClientState.Structs;
-using Serilog;
namespace Dalamud.Game.ClientState.Actors.Types
{
@@ -22,7 +21,7 @@ namespace Dalamud.Game.ClientState.Actors.Types
/// The memory representation of the base actor.
/// A dalamud reference needed to access game data in Resolvers.
/// The address of this actor in memory.
- public Actor(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
+ internal Actor(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
{
this.actorStruct = actorStruct;
this.dalamud = dalamud;
@@ -94,7 +93,7 @@ namespace Dalamud.Game.ClientState.Actors.Types
///
/// Gets the backing instance.
///
- protected Dalamud Dalamud => this.dalamud;
+ internal Dalamud Dalamud => this.dalamud;
///
bool IEquatable.Equals(Actor other) => this.ActorId == other.ActorId;
diff --git a/Dalamud/Game/ClientState/Actors/Types/Chara.cs b/Dalamud/Game/ClientState/Actors/Types/Chara.cs
index 73875d80c..4977801e9 100644
--- a/Dalamud/Game/ClientState/Actors/Types/Chara.cs
+++ b/Dalamud/Game/ClientState/Actors/Types/Chara.cs
@@ -16,7 +16,7 @@ namespace Dalamud.Game.ClientState.Actors.Types
/// The memory representation of the base actor.
/// A dalamud reference needed to access game data in Resolvers.
/// The address of this actor in memory.
- protected Chara(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
+ internal Chara(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
: base(address, actorStruct, dalamud)
{
}
diff --git a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/BattleNpc.cs b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/BattleNpc.cs
index 0bfd60c5b..bcc0ebe70 100644
--- a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/BattleNpc.cs
+++ b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/BattleNpc.cs
@@ -14,7 +14,7 @@ namespace Dalamud.Game.ClientState.Actors.Types.NonPlayer
/// The memory representation of the base actor.
/// A dalamud reference needed to access game data in Resolvers.
/// The address of this actor in memory.
- public BattleNpc(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
+ internal BattleNpc(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
: base(address, actorStruct, dalamud)
{
}
diff --git a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/EventObj.cs b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/EventObj.cs
index e0ac5964d..fd112c247 100644
--- a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/EventObj.cs
+++ b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/EventObj.cs
@@ -14,7 +14,7 @@ namespace Dalamud.Game.ClientState.Actors.Types.NonPlayer
/// The memory representation of the base actor.
/// A dalamud reference needed to access game data in Resolvers.
/// The address of this actor in memory.
- public EventObj(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
+ internal EventObj(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
: base(address, actorStruct, dalamud)
{
}
diff --git a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/Npc.cs b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/Npc.cs
index 7be029450..4571203c9 100644
--- a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/Npc.cs
+++ b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/Npc.cs
@@ -14,7 +14,7 @@ namespace Dalamud.Game.ClientState.Actors.Types.NonPlayer
/// The memory representation of the base actor.
/// A dalamud reference needed to access game data in Resolvers.
/// The address of this actor in memory.
- public Npc(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
+ internal Npc(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
: base(address, actorStruct, dalamud)
{
}
diff --git a/Dalamud/Game/ClientState/Actors/Types/PlayerCharacter.cs b/Dalamud/Game/ClientState/Actors/Types/PlayerCharacter.cs
index 7cc932550..44d01f783 100644
--- a/Dalamud/Game/ClientState/Actors/Types/PlayerCharacter.cs
+++ b/Dalamud/Game/ClientState/Actors/Types/PlayerCharacter.cs
@@ -20,7 +20,7 @@ namespace Dalamud.Game.ClientState.Actors.Types
/// The memory representation of the base actor.
/// A dalamud reference needed to access game data in Resolvers.
/// The address of this actor in memory.
- public PlayerCharacter(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
+ internal PlayerCharacter(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
: base(address, actorStruct, dalamud)
{
var companyTagBytes = new byte[5];
diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs
index f2057a607..bf5ed523f 100644
--- a/Dalamud/Game/ClientState/ClientState.cs
+++ b/Dalamud/Game/ClientState/ClientState.cs
@@ -15,7 +15,7 @@ namespace Dalamud.Game.ClientState
///
/// This class represents the state of the game client at the time of access.
///
- public class ClientState : INotifyPropertyChanged, IDisposable
+ public sealed class ClientState : INotifyPropertyChanged, IDisposable
{
///
/// The table of all present actors.
@@ -80,7 +80,7 @@ namespace Dalamud.Game.ClientState
/// Dalamud instance.
/// StartInfo of the current Dalamud launch.
/// Sig scanner.
- public ClientState(Dalamud dalamud, DalamudStartInfo startInfo, SigScanner scanner)
+ internal ClientState(Dalamud dalamud, DalamudStartInfo startInfo, SigScanner scanner)
{
this.dalamud = dalamud;
this.address = new ClientStateAddressResolver();
@@ -104,7 +104,7 @@ namespace Dalamud.Game.ClientState
this.Targets = new Targets(dalamud, this.address);
- Log.Verbose("SetupTerritoryType address {SetupTerritoryType}", this.address.SetupTerritoryType);
+ Log.Verbose($"SetupTerritoryType address 0x{this.address.SetupTerritoryType.ToInt64():X}");
this.setupTerritoryTypeHook = new Hook(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour);
diff --git a/Dalamud/Game/ClientState/GamepadState.cs b/Dalamud/Game/ClientState/GamepadState.cs
index e3ffc1cef..2d84fdb83 100644
--- a/Dalamud/Game/ClientState/GamepadState.cs
+++ b/Dalamud/Game/ClientState/GamepadState.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Dalamud.Game.ClientState.Structs;
using Dalamud.Hooking;
@@ -12,7 +12,7 @@ namespace Dalamud.Game.ClientState
///
/// Will block game's gamepad input if is set.
///
- public unsafe class GamepadState
+ public unsafe class GamepadState : IDisposable
{
private readonly Hook gamepadPoll;
@@ -29,12 +29,8 @@ namespace Dalamud.Game.ClientState
/// Resolver knowing the pointer to the GamepadPoll function.
public GamepadState(ClientStateAddressResolver resolver)
{
-#if DEBUG
- Log.Verbose("GamepadPoll address {GamepadPoll}", resolver.GamepadPoll);
-#endif
- this.gamepadPoll = new Hook(
- resolver.GamepadPoll,
- (ControllerPoll)this.GamepadPollDetour);
+ Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}");
+ this.gamepadPoll = new Hook(resolver.GamepadPoll, this.GamepadPollDetour);
}
///
diff --git a/Dalamud/Game/ClientState/JobGauges.cs b/Dalamud/Game/ClientState/JobGauges.cs
index f25f5d8ab..e0e9c0f9d 100644
--- a/Dalamud/Game/ClientState/JobGauges.cs
+++ b/Dalamud/Game/ClientState/JobGauges.cs
@@ -17,7 +17,7 @@ namespace Dalamud.Game.ClientState
{
this.Address = addressResolver;
- Log.Verbose("JobGaugeData address {JobGaugeData}", this.Address.JobGaugeData);
+ Log.Verbose($"JobGaugeData address 0x{this.Address.JobGaugeData.ToInt64():X}");
}
private ClientStateAddressResolver Address { get; }
diff --git a/Dalamud/Game/ClientState/KeyState.cs b/Dalamud/Game/ClientState/KeyState.cs
index 8bc3c200f..e17d2cf50 100644
--- a/Dalamud/Game/ClientState/KeyState.cs
+++ b/Dalamud/Game/ClientState/KeyState.cs
@@ -25,7 +25,7 @@ namespace Dalamud.Game.ClientState
{
this.bufferBase = moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardState);
- Log.Verbose($"Keyboard state buffer address {this.bufferBase}");
+ Log.Verbose($"Keyboard state buffer address 0x{this.bufferBase.ToInt64():X}");
}
///
diff --git a/Dalamud/Game/ClientState/PartyList.cs b/Dalamud/Game/ClientState/PartyList.cs
index a1db673af..2aa4b95ed 100644
--- a/Dalamud/Game/ClientState/PartyList.cs
+++ b/Dalamud/Game/ClientState/PartyList.cs
@@ -25,7 +25,7 @@ namespace Dalamud.Game.ClientState
///
/// The Dalamud instance.
/// The ClientStateAddressResolver instance.
- public PartyList(Dalamud dalamud, ClientStateAddressResolver addressResolver)
+ internal PartyList(Dalamud dalamud, ClientStateAddressResolver addressResolver)
{
this.address = addressResolver;
this.dalamud = dalamud;
diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/DNCGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/DNCGauge.cs
index 2143aa99f..8002a2f19 100644
--- a/Dalamud/Game/ClientState/Structs/JobGauge/DNCGauge.cs
+++ b/Dalamud/Game/ClientState/Structs/JobGauge/DNCGauge.cs
@@ -32,6 +32,7 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
///
/// Gets the next step in the current dance.
///
+ /// The next dance step action ID.
public ulong NextStep() => (ulong)(15999 + this.stepOrder[this.NumCompleteSteps] - 1);
///
diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs
index 9d7dac9cd..6c31e2444 100644
--- a/Dalamud/Game/Command/CommandManager.cs
+++ b/Dalamud/Game/Command/CommandManager.cs
@@ -27,7 +27,7 @@ namespace Dalamud.Game.Command
///
/// The Dalamud instance.
/// The client language requested.
- public CommandManager(Dalamud dalamud, ClientLanguage language)
+ internal CommandManager(Dalamud dalamud, ClientLanguage language)
{
this.dalamud = dalamud;
@@ -128,7 +128,8 @@ namespace Dalamud.Game.Command
/// If adding was successful.
public bool AddHandler(string command, CommandInfo info)
{
- if (info == null) throw new ArgumentNullException(nameof(info), "Command handler is null.");
+ if (info == null)
+ throw new ArgumentNullException(nameof(info), "Command handler is null.");
try
{
diff --git a/Dalamud/Game/GameVersion.cs b/Dalamud/Game/GameVersion.cs
new file mode 100644
index 000000000..8ef76eabe
--- /dev/null
+++ b/Dalamud/Game/GameVersion.cs
@@ -0,0 +1,383 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+using Newtonsoft.Json;
+
+namespace Dalamud.Game
+{
+ ///
+ /// A GameVersion object contains give hierarchical numeric components: year, month,
+ /// day, major and minor. All components may be unspecified, which is represented
+ /// internally as a -1. By definition, an unspecified component matches anything
+ /// (both unspecified and specified), and an unspecified component is "less than" any
+ /// specified component. It will also equal the string "any" if all components are
+ /// unspecified. The value can be retrieved from the ffxivgame.ver file in your game
+ /// installation directory.
+ ///
+ [Serializable]
+ public sealed class GameVersion : ICloneable, IComparable, IComparable, IEquatable
+ {
+ private static readonly GameVersion AnyVersion = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Version string to parse.
+ [JsonConstructor]
+ public GameVersion(string version)
+ {
+ var ver = Parse(version);
+ this.Year = ver.Year;
+ this.Month = ver.Month;
+ this.Day = ver.Day;
+ this.Major = ver.Major;
+ this.Minor = ver.Minor;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The year.
+ /// The month.
+ /// The day.
+ /// The major version.
+ /// The minor version.
+ public GameVersion(int year, int month, int day, int major, int minor)
+ {
+ if ((this.Year = year) < 0)
+ throw new ArgumentOutOfRangeException(nameof(year));
+
+ if ((this.Month = month) < 0)
+ throw new ArgumentOutOfRangeException(nameof(month));
+
+ if ((this.Day = day) < 0)
+ throw new ArgumentOutOfRangeException(nameof(day));
+
+ if ((this.Major = major) < 0)
+ throw new ArgumentOutOfRangeException(nameof(major));
+
+ if ((this.Minor = minor) < 0)
+ throw new ArgumentOutOfRangeException(nameof(minor));
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The year.
+ /// The month.
+ /// The day.
+ /// The major version.
+ public GameVersion(int year, int month, int day, int major)
+ {
+ if ((this.Year = year) < 0)
+ throw new ArgumentOutOfRangeException(nameof(year));
+
+ if ((this.Month = month) < 0)
+ throw new ArgumentOutOfRangeException(nameof(month));
+
+ if ((this.Day = day) < 0)
+ throw new ArgumentOutOfRangeException(nameof(day));
+
+ if ((this.Major = major) < 0)
+ throw new ArgumentOutOfRangeException(nameof(major));
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The year.
+ /// The month.
+ /// The day.
+ public GameVersion(int year, int month, int day)
+ {
+ if ((this.Year = year) < 0)
+ throw new ArgumentOutOfRangeException(nameof(year));
+
+ if ((this.Month = month) < 0)
+ throw new ArgumentOutOfRangeException(nameof(month));
+
+ if ((this.Day = day) < 0)
+ throw new ArgumentOutOfRangeException(nameof(day));
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The year.
+ /// The month.
+ public GameVersion(int year, int month)
+ {
+ if ((this.Year = year) < 0)
+ throw new ArgumentOutOfRangeException(nameof(year));
+
+ if ((this.Month = month) < 0)
+ throw new ArgumentOutOfRangeException(nameof(month));
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The year.
+ public GameVersion(int year)
+ {
+ if ((this.Year = year) < 0)
+ throw new ArgumentOutOfRangeException(nameof(year));
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public GameVersion()
+ {
+ }
+
+ ///
+ /// Gets the default "any" game version.
+ ///
+ public static GameVersion Any => AnyVersion;
+
+ ///
+ /// Gets the year component.
+ ///
+ public int Year { get; } = -1;
+
+ ///
+ /// Gets the month component.
+ ///
+ public int Month { get; } = -1;
+
+ ///
+ /// Gets the day component.
+ ///
+ public int Day { get; } = -1;
+
+ ///
+ /// Gets the major version component.
+ ///
+ public int Major { get; } = -1;
+
+ ///
+ /// Gets the minor version component.
+ ///
+ public int Minor { get; } = -1;
+
+ public static implicit operator GameVersion(string ver)
+ {
+ return Parse(ver);
+ }
+
+ public static bool operator ==(GameVersion v1, GameVersion v2)
+ {
+ if (v1 is null)
+ {
+ return v2 is null;
+ }
+
+ return v1.Equals(v2);
+ }
+
+ public static bool operator !=(GameVersion v1, GameVersion v2)
+ {
+ return !(v1 == v2);
+ }
+
+ public static bool operator <(GameVersion v1, GameVersion v2)
+ {
+ if (v1 is null)
+ throw new ArgumentNullException(nameof(v1));
+
+ return v1.CompareTo(v2) < 0;
+ }
+
+ public static bool operator <=(GameVersion v1, GameVersion v2)
+ {
+ if (v1 is null)
+ throw new ArgumentNullException(nameof(v1));
+
+ return v1.CompareTo(v2) <= 0;
+ }
+
+ public static bool operator >(GameVersion v1, GameVersion v2)
+ {
+ return v2 < v1;
+ }
+
+ public static bool operator >=(GameVersion v1, GameVersion v2)
+ {
+ return v2 <= v1;
+ }
+
+ ///
+ /// Parse a version string. YYYY.MM.DD.majr.minr or "any".
+ ///
+ /// Input to parse.
+ /// GameVersion object.
+ public static GameVersion Parse(string input)
+ {
+ if (input == null)
+ throw new ArgumentNullException(nameof(input));
+
+ if (input.ToLower(CultureInfo.InvariantCulture) == "any")
+ return new GameVersion();
+
+ var parts = input.Split('.');
+ var tplParts = parts.Select(p =>
+ {
+ var result = int.TryParse(p, out var value);
+ return (result, value);
+ }).ToArray();
+
+ if (tplParts.Any(t => !t.result))
+ throw new FormatException("Bad formatting");
+
+ var intParts = tplParts.Select(t => t.value).ToArray();
+ var len = intParts.Length;
+
+ if (len == 1)
+ return new GameVersion(intParts[0]);
+ else if (len == 2)
+ return new GameVersion(intParts[0], intParts[1]);
+ else if (len == 3)
+ return new GameVersion(intParts[0], intParts[1], intParts[2]);
+ else if (len == 4)
+ return new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3]);
+ else if (len == 5)
+ return new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3], intParts[4]);
+ else
+ throw new ArgumentException("Too many parts");
+ }
+
+ ///
+ /// Try to parse a version string. YYYY.MM.DD.majr.minr or "any".
+ ///
+ /// Input to parse.
+ /// GameVersion object.
+ /// Success or failure.
+ public static bool TryParse(string input, out GameVersion result)
+ {
+ try
+ {
+ result = Parse(input);
+ return true;
+ }
+ catch
+ {
+ result = null;
+ return false;
+ }
+ }
+
+ ///
+ public object Clone() => new GameVersion(this.Year, this.Month, this.Day, this.Major, this.Minor);
+
+ ///
+ public int CompareTo(object obj)
+ {
+ if (obj == null)
+ return 1;
+
+ if (obj is GameVersion value)
+ {
+ return this.CompareTo(value);
+ }
+ else
+ {
+ throw new ArgumentException("Argument must be a GameVersion");
+ }
+ }
+
+ ///
+ public int CompareTo(GameVersion value)
+ {
+ if (value == null)
+ return 1;
+
+ if (this == value)
+ return 0;
+
+ if (this == AnyVersion)
+ return 1;
+
+ if (value == AnyVersion)
+ return -1;
+
+ if (this.Year != value.Year)
+ return this.Year > value.Year ? 1 : -1;
+
+ if (this.Month != value.Month)
+ return this.Month > value.Month ? 1 : -1;
+
+ if (this.Day != value.Day)
+ return this.Day > value.Day ? 1 : -1;
+
+ if (this.Major != value.Major)
+ return this.Major > value.Major ? 1 : -1;
+
+ if (this.Minor != value.Minor)
+ return this.Minor > value.Minor ? 1 : -1;
+
+ return 0;
+ }
+
+ ///
+ public override bool Equals(object obj)
+ {
+ if (obj is not GameVersion value)
+ return false;
+
+ return this.Equals(value);
+ }
+
+ ///
+ public bool Equals(GameVersion value)
+ {
+ if (value == null)
+ {
+ return false;
+ }
+
+ return
+ (this.Year == value.Year) &&
+ (this.Month == value.Month) &&
+ (this.Day == value.Day) &&
+ (this.Major == value.Major) &&
+ (this.Minor == value.Minor);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ var accumulator = 0;
+
+ // This might be horribly wrong, but it isn't used heavily.
+ accumulator |= this.Year.GetHashCode();
+ accumulator |= this.Month.GetHashCode();
+ accumulator |= this.Day.GetHashCode();
+ accumulator |= this.Major.GetHashCode();
+ accumulator |= this.Minor.GetHashCode();
+
+ return accumulator;
+ }
+
+ ///
+ public override string ToString()
+ {
+ if (this.Year == -1 &&
+ this.Month == -1 &&
+ this.Day == -1 &&
+ this.Major == -1 &&
+ this.Minor == -1)
+ return "any";
+
+ return new StringBuilder()
+ .Append(string.Format("{0:D4}.", this.Year == -1 ? 0 : this.Year))
+ .Append(string.Format("{0:D2}.", this.Month == -1 ? 0 : this.Month))
+ .Append(string.Format("{0:D2}.", this.Day == -1 ? 0 : this.Day))
+ .Append(string.Format("{0:D4}.", this.Major == -1 ? 0 : this.Major))
+ .Append(string.Format("{0:D4}", this.Minor == -1 ? 0 : this.Minor))
+ .ToString();
+ }
+ }
+}
diff --git a/Dalamud/Game/GameVersionConverter.cs b/Dalamud/Game/GameVersionConverter.cs
new file mode 100644
index 000000000..9058e8be9
--- /dev/null
+++ b/Dalamud/Game/GameVersionConverter.cs
@@ -0,0 +1,80 @@
+using System;
+
+using Newtonsoft.Json;
+
+namespace Dalamud.Game
+{
+ ///
+ /// Converts a to and from a string (e.g. "2010.01.01.1234.5678").
+ ///
+ public sealed class GameVersionConverter : JsonConverter
+ {
+ ///
+ /// Writes the JSON representation of the object.
+ ///
+ /// The to write to.
+ /// The value.
+ /// The calling serializer.
+ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
+ {
+ if (value == null)
+ {
+ writer.WriteNull();
+ }
+ else if (value is GameVersion)
+ {
+ writer.WriteValue(value.ToString());
+ }
+ else
+ {
+ throw new JsonSerializationException("Expected GameVersion object value");
+ }
+ }
+
+ ///
+ /// Reads the JSON representation of the object.
+ ///
+ /// The to read from.
+ /// Type of the object.
+ /// The existing property value of the JSON that is being converted.
+ /// The calling serializer.
+ /// The object value.
+ public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
+ {
+ if (reader.TokenType == JsonToken.Null)
+ {
+ return null;
+ }
+ else
+ {
+ if (reader.TokenType == JsonToken.String)
+ {
+ try
+ {
+ return new GameVersion((string)reader.Value!);
+ }
+ catch (Exception ex)
+ {
+ throw new JsonSerializationException($"Error parsing GameVersion string: {reader.Value}", ex);
+ }
+ }
+ else
+ {
+ throw new JsonSerializationException($"Unexpected token or value when parsing GameVersion. Token: {reader.TokenType}, Value: {reader.Value}");
+ }
+ }
+ }
+
+ ///
+ /// Determines whether this instance can convert the specified object type.
+ ///
+ /// Type of the object.
+ ///
+ /// true if this instance can convert the specified object type; otherwise, false.
+ ///
+ public override bool CanConvert(Type objectType)
+ {
+ return objectType == typeof(GameVersion);
+ }
+ }
+}
diff --git a/Dalamud/Game/Internal/AntiDebug.cs b/Dalamud/Game/Internal/AntiDebug.cs
index b7ca43b86..57c7fdeb4 100644
--- a/Dalamud/Game/Internal/AntiDebug.cs
+++ b/Dalamud/Game/Internal/AntiDebug.cs
@@ -29,7 +29,7 @@ namespace Dalamud.Game.Internal
this.debugCheckAddress = IntPtr.Zero;
}
- Log.Verbose("DebugCheck address {DebugCheckAddress}", this.debugCheckAddress);
+ Log.Verbose($"Debug check address 0x{this.debugCheckAddress.ToInt64():X}");
}
///
@@ -45,7 +45,7 @@ namespace Dalamud.Game.Internal
this.original = new byte[this.nop.Length];
if (this.debugCheckAddress != IntPtr.Zero && !this.IsEnabled)
{
- Log.Information($"Overwriting debug check @ 0x{this.debugCheckAddress.ToInt64():X}");
+ Log.Information($"Overwriting debug check at 0x{this.debugCheckAddress.ToInt64():X}");
SafeMemory.ReadBytes(this.debugCheckAddress, this.nop.Length, out this.original);
SafeMemory.WriteBytes(this.debugCheckAddress, this.nop);
}
@@ -64,7 +64,7 @@ namespace Dalamud.Game.Internal
{
if (this.debugCheckAddress != IntPtr.Zero && this.original != null)
{
- Log.Information($"Reverting debug check @ 0x{this.debugCheckAddress.ToInt64():X}");
+ Log.Information($"Reverting debug check at 0x{this.debugCheckAddress.ToInt64():X}");
SafeMemory.WriteBytes(this.debugCheckAddress, this.original);
}
else
@@ -110,9 +110,11 @@ namespace Dalamud.Game.Internal
{
// If anti-debug is enabled and is being disposed, odds are either the game is exiting, or Dalamud is being reloaded.
// If it is the latter, there's half a chance a debugger is currently attached. There's no real need to disable the
- // check in either situation anyways.
- // this.Disable();
+ // check in either situation anyways. However if Dalamud is being reloaded, the sig may fail so may as well undo it.
+ this.Disable();
}
+
+ this.disposed = true;
}
}
}
diff --git a/Dalamud/Game/Internal/DXGI/Definitions/ID3D11DeviceVtbl.cs b/Dalamud/Game/Internal/DXGI/Definitions/ID3D11DeviceVtbl.cs
new file mode 100644
index 000000000..7703713b3
--- /dev/null
+++ b/Dalamud/Game/Internal/DXGI/Definitions/ID3D11DeviceVtbl.cs
@@ -0,0 +1,227 @@
+namespace Dalamud.Game.Internal.DXGI.Definitions
+{
+ ///
+ /// Contains a full list of ID3D11Device functions to be used as an indexer into the DirectX Virtual Function Table entries.
+ ///
+ internal enum ID3D11DeviceVtbl
+ {
+ // IUnknown
+
+ ///
+ /// IUnknown::QueryInterface method (unknwn.h).
+ ///
+ QueryInterface = 0,
+
+ ///
+ /// IUnknown::AddRef method (unknwn.h).
+ ///
+ AddRef = 1,
+
+ ///
+ /// IUnknown::Release method (unknwn.h).
+ ///
+ Release = 2,
+
+ // ID3D11Device
+
+ ///
+ /// ID3D11Device::CreateBuffer method (d3d11.h).
+ ///
+ CreateBuffer = 3,
+
+ ///
+ /// ID3D11Device::CreateTexture1D method (d3d11.h).
+ ///
+ CreateTexture1D = 4,
+
+ ///
+ /// ID3D11Device::CreateTexture2D method (d3d11.h).
+ ///
+ CreateTexture2D = 5,
+
+ ///
+ /// ID3D11Device::CreateTexture3D method (d3d11.h).
+ ///
+ CreateTexture3D = 6,
+
+ ///
+ /// ID3D11Device::CreateShaderResourceView method (d3d11.h).
+ ///
+ CreateShaderResourceView = 7,
+
+ ///
+ /// ID3D11Device::CreateUnorderedAccessView method (d3d11.h).
+ ///
+ CreateUnorderedAccessView = 8,
+
+ ///
+ /// ID3D11Device::CreateRenderTargetView method (d3d11.h).
+ ///
+ CreateRenderTargetView = 9,
+
+ ///
+ /// ID3D11Device::CreateDepthStencilView method (d3d11.h).
+ ///
+ CreateDepthStencilView = 10,
+
+ ///
+ /// ID3D11Device::CreateInputLayout method (d3d11.h).
+ ///
+ CreateInputLayout = 11,
+
+ ///
+ /// ID3D11Device::CreateVertexShader method (d3d11.h).
+ ///
+ CreateVertexShader = 12,
+
+ ///
+ /// ID3D11Device::CreateGeometryShader method (d3d11.h).
+ ///
+ CreateGeometryShader = 13,
+
+ ///
+ /// ID3D11Device::CreateGeometryShaderWithStreamOutput method (d3d11.h).
+ ///
+ CreateGeometryShaderWithStreamOutput = 14,
+
+ ///
+ /// ID3D11Device::CreatePixelShader method (d3d11.h).
+ ///
+ CreatePixelShader = 15,
+
+ ///
+ /// ID3D11Device::CreateHullShader method (d3d11.h).
+ ///
+ CreateHullShader = 16,
+
+ ///
+ /// ID3D11Device::CreateDomainShader method (d3d11.h).
+ ///
+ CreateDomainShader = 17,
+
+ ///
+ /// ID3D11Device::CreateComputeShader method (d3d11.h).
+ ///
+ CreateComputeShader = 18,
+
+ ///
+ /// ID3D11Device::CreateClassLinkage method (d3d11.h).
+ ///
+ CreateClassLinkage = 19,
+
+ ///
+ /// ID3D11Device::CreateBlendState method (d3d11.h).
+ ///
+ CreateBlendState = 20,
+
+ ///
+ /// ID3D11Device::CreateDepthStencilState method (d3d11.h).
+ ///
+ CreateDepthStencilState = 21,
+
+ ///
+ /// ID3D11Device::CreateRasterizerState method (d3d11.h).
+ ///
+ CreateRasterizerState = 22,
+
+ ///
+ /// ID3D11Device::CreateSamplerState method (d3d11.h).
+ ///
+ CreateSamplerState = 23,
+
+ ///
+ /// ID3D11Device::CreateQuery method (d3d11.h).
+ ///
+ CreateQuery = 24,
+
+ ///
+ /// ID3D11Device::CreatePredicate method (d3d11.h).
+ ///
+ CreatePredicate = 25,
+
+ ///
+ /// ID3D11Device::CreateCounter method (d3d11.h).
+ ///
+ CreateCounter = 26,
+
+ ///
+ /// ID3D11Device::CreateDeferredContext method (d3d11.h).
+ ///
+ CreateDeferredContext = 27,
+
+ ///
+ /// ID3D11Device::OpenSharedResource method (d3d11.h).
+ ///
+ OpenSharedResource = 28,
+
+ ///
+ /// ID3D11Device::CheckFormatSupport method (d3d11.h).
+ ///
+ CheckFormatSupport = 29,
+
+ ///
+ /// ID3D11Device::CheckMultisampleQualityLevels method (d3d11.h).
+ ///
+ CheckMultisampleQualityLevels = 30,
+
+ ///
+ /// ID3D11Device::CheckCounterInfo method (d3d11.h).
+ ///
+ CheckCounterInfo = 31,
+
+ ///
+ /// ID3D11Device::CheckCounter method (d3d11.h).
+ ///
+ CheckCounter = 32,
+
+ ///
+ /// ID3D11Device::CheckFeatureSupport method (d3d11.h).
+ ///
+ CheckFeatureSupport = 33,
+
+ ///
+ /// ID3D11Device::GetPrivateData method (d3d11.h).
+ ///
+ GetPrivateData = 34,
+
+ ///
+ /// ID3D11Device::SetPrivateData method (d3d11.h).
+ ///
+ SetPrivateData = 35,
+
+ ///
+ /// ID3D11Device::SetPrivateDataInterface method (d3d11.h).
+ ///
+ SetPrivateDataInterface = 36,
+
+ ///
+ /// ID3D11Device::GetFeatureLevel method (d3d11.h).
+ ///
+ GetFeatureLevel = 37,
+
+ ///
+ /// ID3D11Device::GetCreationFlags method (d3d11.h).
+ ///
+ GetCreationFlags = 38,
+
+ ///
+ /// ID3D11Device::GetDeviceRemovedReason method (d3d11.h).
+ ///
+ GetDeviceRemovedReason = 39,
+
+ ///
+ /// ID3D11Device::GetImmediateContext method (d3d11.h).
+ ///
+ GetImmediateContext = 40,
+
+ ///
+ /// ID3D11Device::SetExceptionMode method (d3d11.h).
+ ///
+ SetExceptionMode = 41,
+
+ ///
+ /// ID3D11Device::GetExceptionMode method (d3d11.h).
+ ///
+ GetExceptionMode = 42,
+ }
+}
diff --git a/Dalamud/Game/Internal/DXGI/Definitions/IDXGISwapChainVtbl.cs b/Dalamud/Game/Internal/DXGI/Definitions/IDXGISwapChainVtbl.cs
new file mode 100644
index 000000000..e3d627ce3
--- /dev/null
+++ b/Dalamud/Game/Internal/DXGI/Definitions/IDXGISwapChainVtbl.cs
@@ -0,0 +1,107 @@
+namespace Dalamud.Game.Internal.DXGI.Definitions
+{
+ ///
+ /// Contains a full list of IDXGISwapChain functions to be used as an indexer into the SwapChain Virtual Function Table
+ /// entries.
+ ///
+ internal enum IDXGISwapChainVtbl
+ {
+ // IUnknown
+
+ ///
+ /// IUnknown::QueryInterface method (unknwn.h).
+ ///
+ QueryInterface = 0,
+
+ ///
+ /// IUnknown::AddRef method (unknwn.h).
+ ///
+ AddRef = 1,
+
+ ///
+ /// IUnknown::Release method (unknwn.h).
+ ///
+ Release = 2,
+
+ // IDXGIObject
+
+ ///
+ /// IDXGIObject::SetPrivateData method (dxgi.h).
+ ///
+ SetPrivateData = 3,
+
+ ///
+ /// IDXGIObject::SetPrivateDataInterface method (dxgi.h).
+ ///
+ SetPrivateDataInterface = 4,
+
+ ///
+ /// IDXGIObject::GetPrivateData method (dxgi.h).
+ ///
+ GetPrivateData = 5,
+
+ ///
+ /// IDXGIObject::GetParent method (dxgi.h).
+ ///
+ GetParent = 6,
+
+ // IDXGIDeviceSubObject
+
+ ///
+ /// IDXGIDeviceSubObject::GetDevice method (dxgi.h).
+ ///
+ GetDevice = 7,
+
+ // IDXGISwapChain
+
+ ///
+ /// IDXGISwapChain::Present method (dxgi.h).
+ ///
+ Present = 8,
+
+ ///
+ /// IUnknIDXGISwapChainown::GetBuffer method (dxgi.h).
+ ///
+ GetBuffer = 9,
+
+ ///
+ /// IDXGISwapChain::SetFullscreenState method (dxgi.h).
+ ///
+ SetFullscreenState = 10,
+
+ ///
+ /// IDXGISwapChain::GetFullscreenState method (dxgi.h).
+ ///
+ GetFullscreenState = 11,
+
+ ///
+ /// IDXGISwapChain::GetDesc method (dxgi.h).
+ ///
+ GetDesc = 12,
+
+ ///
+ /// IDXGISwapChain::ResizeBuffers method (dxgi.h).
+ ///
+ ResizeBuffers = 13,
+
+ ///
+ /// IDXGISwapChain::ResizeTarget method (dxgi.h).
+ ///
+ ResizeTarget = 14,
+
+ ///
+ /// IDXGISwapChain::GetContainingOutput method (dxgi.h).
+ ///
+ GetContainingOutput = 15,
+
+ ///
+ /// IDXGISwapChain::GetFrameStatistics method (dxgi.h).
+ ///
+ GetFrameStatistics = 16,
+
+ ///
+ /// IDXGISwapChain::GetLastPresentCount method (dxgi.h).
+ ///
+ GetLastPresentCount = 17,
+ }
+}
diff --git a/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs b/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs
index 701068ed3..e44883612 100644
--- a/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs
+++ b/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs
@@ -22,7 +22,7 @@ namespace Dalamud.Game.Internal.DXGI
{
var module = Process.GetCurrentProcess().Modules.Cast().First(m => m.ModuleName == "dxgi.dll");
- Log.Debug($"Found DXGI: {module.BaseAddress.ToInt64():X}");
+ Log.Debug($"Found DXGI: 0x{module.BaseAddress.ToInt64():X}");
var scanner = new SigScanner(module);
diff --git a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs
index 6e69a17bd..b5c0d3225 100644
--- a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs
+++ b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
-using System.Windows.Forms;
+using Dalamud.Game.Internal.DXGI.Definitions;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
@@ -19,9 +19,6 @@ namespace Dalamud.Game.Internal.DXGI
///
public class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressResolver
{
- private const int DxgiSwapchainMethodCount = 18;
- private const int D3D11DeviceMethodCount = 43;
-
private List d3d11VTblAddresses;
private List dxgiSwapChainVTblAddresses;
@@ -34,30 +31,33 @@ namespace Dalamud.Game.Internal.DXGI
///
protected override void Setup64Bit(SigScanner sig)
{
+ // Create temporary device + swapchain and determine method addresses
if (this.d3d11VTblAddresses == null)
{
- // Create temporary device + swapchain and determine method addresses
- var renderForm = new Form();
+ // A renderable object isnt required, just a handle
+ var handle = Marshal.AllocHGlobal(Marshal.SizeOf());
Device.CreateWithSwapChain(
DriverType.Hardware,
DeviceCreationFlags.BgraSupport,
- CreateSwapChainDescription(renderForm.Handle),
+ CreateSwapChainDescription(handle),
out var device,
out var swapChain);
if (device != null && swapChain != null)
{
- this.d3d11VTblAddresses = this.GetVTblAddresses(device.NativePointer, D3D11DeviceMethodCount);
- this.dxgiSwapChainVTblAddresses = this.GetVTblAddresses(swapChain.NativePointer, DxgiSwapchainMethodCount);
+ this.d3d11VTblAddresses = GetVTblAddresses(device.NativePointer, Enum.GetValues(typeof(ID3D11DeviceVtbl)).Length);
+ this.dxgiSwapChainVTblAddresses = GetVTblAddresses(swapChain.NativePointer, Enum.GetValues(typeof(IDXGISwapChainVtbl)).Length);
}
device?.Dispose();
swapChain?.Dispose();
+
+ Marshal.FreeHGlobal(handle);
}
- this.Present = this.dxgiSwapChainVTblAddresses[8];
- this.ResizeBuffers = this.dxgiSwapChainVTblAddresses[13];
+ this.Present = this.dxgiSwapChainVTblAddresses[(int)IDXGISwapChainVtbl.Present];
+ this.ResizeBuffers = this.dxgiSwapChainVTblAddresses[(int)IDXGISwapChainVtbl.ResizeBuffers];
}
private static SwapChainDescription CreateSwapChainDescription(IntPtr renderForm)
@@ -75,12 +75,12 @@ namespace Dalamud.Game.Internal.DXGI
};
}
- private List GetVTblAddresses(IntPtr pointer, int numberOfMethods)
+ private static List GetVTblAddresses(IntPtr pointer, int numberOfMethods)
{
- return this.GetVTblAddresses(pointer, 0, numberOfMethods);
+ return GetVTblAddresses(pointer, 0, numberOfMethods);
}
- private List GetVTblAddresses(IntPtr pointer, int startIndex, int numberOfMethods)
+ private static List GetVTblAddresses(IntPtr pointer, int startIndex, int numberOfMethods)
{
var vtblAddresses = new List();
var vTable = Marshal.ReadIntPtr(pointer);
diff --git a/Dalamud/Game/Internal/Framework.cs b/Dalamud/Game/Internal/Framework.cs
index 7d13318f6..abe3272e0 100644
--- a/Dalamud/Game/Internal/Framework.cs
+++ b/Dalamud/Game/Internal/Framework.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
+using System.Threading;
using Dalamud.Game.Internal.Gui;
using Dalamud.Game.Internal.Libc;
@@ -29,13 +30,13 @@ namespace Dalamud.Game.Internal
///
/// The SigScanner instance.
/// The Dalamud instance.
- public Framework(SigScanner scanner, Dalamud dalamud)
+ internal Framework(SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
this.Address = new FrameworkAddressResolver();
this.Address.Setup(scanner);
- Log.Verbose("Framework address {FrameworkAddress}", this.Address.BaseAddress);
+ Log.Verbose($"Framework address 0x{this.Address.BaseAddress.ToInt64():X}");
if (this.Address.BaseAddress == IntPtr.Zero)
{
throw new InvalidOperationException("Framework is not initalized yet.");
@@ -143,6 +144,11 @@ namespace Dalamud.Game.Internal
this.Gui.Dispose();
this.Network.Dispose();
+ this.updateHook.Disable();
+ this.destroyHook.Disable();
+ this.realDestroyHook.Disable();
+ Thread.Sleep(500);
+
this.updateHook.Dispose();
this.destroyHook.Dispose();
this.realDestroyHook.Dispose();
diff --git a/Dalamud/Game/Internal/FrameworkAddressResolver.cs b/Dalamud/Game/Internal/FrameworkAddressResolver.cs
index 551e779e5..1af2e9263 100644
--- a/Dalamud/Game/Internal/FrameworkAddressResolver.cs
+++ b/Dalamud/Game/Internal/FrameworkAddressResolver.cs
@@ -44,7 +44,7 @@ namespace Dalamud.Game.Internal
// 00007FF701AD666A | 48 8D ?? ?? ?? 00 00 | LEA RCX,QWORD PTR DS:[RBX + 2C38]
// 00007FF701AD6671 | E8 ?? ?? ?? ?? | CALL ffxiv_dx11.7FF701E2A7D0
// 00007FF701AD6676 | 48 8D ?? ?? ?? ?? ?? | LEA RAX,QWORD PTR DS:[7FF702C31F80
- var fwDtor = scanner.ScanText("48C705????????00000000 E8???????? 488D??????0000 E8???????? 488D");
+ var fwDtor = scanner.ScanText("48 C7 05 ?? ?? ?? ?? 00 00 00 00 E8 ?? ?? ?? ?? 48 8D ?? ?? ?? 00 00 E8 ?? ?? ?? ?? 48 8D");
var fwOffset = Marshal.ReadInt32(fwDtor + 3);
var pFramework = scanner.ResolveRelativeAddress(fwDtor + 11, fwOffset);
diff --git a/Dalamud/Game/Internal/Gui/ChatGui.cs b/Dalamud/Game/Internal/Gui/ChatGui.cs
index 38d7a2783..cd4220e0d 100644
--- a/Dalamud/Game/Internal/Gui/ChatGui.cs
+++ b/Dalamud/Game/Internal/Gui/ChatGui.cs
@@ -36,14 +36,14 @@ namespace Dalamud.Game.Internal.Gui
/// The base address of the ChatManager.
/// The SigScanner instance.
/// The Dalamud instance.
- public ChatGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud)
+ internal ChatGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
this.address = new ChatGuiAddressResolver(baseAddress);
this.address.Setup(scanner);
- Log.Verbose("Chat manager address {ChatManager}", this.address.BaseAddress);
+ Log.Verbose($"Chat manager address 0x{this.address.BaseAddress.ToInt64():X}");
this.printMessageHook = new Hook(this.address.PrintMessage, this.HandlePrintMessageDetour);
this.populateItemLinkHook = new Hook(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
@@ -252,7 +252,7 @@ namespace Dalamud.Game.Internal.Gui
var senderRaw = Encoding.UTF8.GetBytes(chat.Name ?? string.Empty);
using var senderOwned = framework.Libc.NewString(senderRaw);
- var messageRaw = chat.MessageBytes ?? new byte[0];
+ var messageRaw = chat.MessageBytes ?? Array.Empty();
using var messageOwned = framework.Libc.NewString(messageRaw);
this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters);
diff --git a/Dalamud/Game/Internal/Gui/ChatGuiAddressResolver.cs b/Dalamud/Game/Internal/Gui/ChatGuiAddressResolver.cs
index dfc6ed0f3..067558e12 100644
--- a/Dalamud/Game/Internal/Gui/ChatGuiAddressResolver.cs
+++ b/Dalamud/Game/Internal/Gui/ChatGuiAddressResolver.cs
@@ -10,10 +10,10 @@ namespace Dalamud.Game.Internal.Gui
///
/// Initializes a new instance of the class.
///
- /// The base address of the native ChatManager class.
- public ChatGuiAddressResolver(IntPtr baseAddres)
+ /// The base address of the native ChatManager class.
+ public ChatGuiAddressResolver(IntPtr baseAddress)
{
- this.BaseAddress = baseAddres;
+ this.BaseAddress = baseAddress;
}
///
diff --git a/Dalamud/Game/Internal/Gui/GameGui.cs b/Dalamud/Game/Internal/Gui/GameGui.cs
index be018f1e1..f27f75662 100644
--- a/Dalamud/Game/Internal/Gui/GameGui.cs
+++ b/Dalamud/Game/Internal/Gui/GameGui.cs
@@ -47,7 +47,7 @@ namespace Dalamud.Game.Internal.Gui
/// The base address of the native GuiManager class.
/// The SigScanner instance.
/// The Dalamud instance.
- public GameGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud)
+ internal GameGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
@@ -56,12 +56,12 @@ namespace Dalamud.Game.Internal.Gui
Log.Verbose("===== G A M E G U I =====");
- Log.Verbose("GameGuiManager address {Address:X}", this.address.BaseAddress.ToInt64());
- Log.Verbose("SetGlobalBgm address {Address:X}", this.address.SetGlobalBgm.ToInt64());
- Log.Verbose("HandleItemHover address {Address:X}", this.address.HandleItemHover.ToInt64());
- Log.Verbose("HandleItemOut address {Address:X}", this.address.HandleItemOut.ToInt64());
- Log.Verbose("GetUIObject address {Address:X}", this.address.GetUIObject.ToInt64());
- Log.Verbose("GetAgentModule address {Address:X}", this.address.GetAgentModule.ToInt64());
+ Log.Verbose($"GameGuiManager address 0x{this.address.BaseAddress.ToInt64():X}");
+ Log.Verbose($"SetGlobalBgm address 0x{this.address.SetGlobalBgm.ToInt64():X}");
+ Log.Verbose($"HandleItemHover address 0x{this.address.HandleItemHover.ToInt64():X}");
+ Log.Verbose($"HandleItemOut address 0x{this.address.HandleItemOut.ToInt64():X}");
+ Log.Verbose($"GetUIObject address 0x{this.address.GetUIObject.ToInt64():X}");
+ Log.Verbose($"GetAgentModule address 0x{this.address.GetAgentModule.ToInt64():X}");
this.Chat = new ChatGui(this.address.ChatManager, scanner, dalamud);
this.PartyFinder = new PartyFinderGui(scanner, dalamud);
diff --git a/Dalamud/Game/Internal/Gui/PartyFinderGui.cs b/Dalamud/Game/Internal/Gui/PartyFinderGui.cs
index bdc5eee96..24079cf4e 100755
--- a/Dalamud/Game/Internal/Gui/PartyFinderGui.cs
+++ b/Dalamud/Game/Internal/Gui/PartyFinderGui.cs
@@ -23,7 +23,7 @@ namespace Dalamud.Game.Internal.Gui
///
/// The SigScanner instance.
/// The Dalamud instance.
- public PartyFinderGui(SigScanner scanner, Dalamud dalamud)
+ internal PartyFinderGui(SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
diff --git a/Dalamud/Game/Internal/Gui/Structs/Addon.cs b/Dalamud/Game/Internal/Gui/Structs/Addon.cs
index be1f08f33..57595a2c7 100644
--- a/Dalamud/Game/Internal/Gui/Structs/Addon.cs
+++ b/Dalamud/Game/Internal/Gui/Structs/Addon.cs
@@ -1,5 +1,7 @@
using System.Runtime.InteropServices;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
namespace Dalamud.Game.Internal.Gui.Structs
{
///
diff --git a/Dalamud/Game/Internal/Gui/Structs/AtkResNode.cs b/Dalamud/Game/Internal/Gui/Structs/AtkResNode.cs
deleted file mode 100644
index 406a1262a..000000000
--- a/Dalamud/Game/Internal/Gui/Structs/AtkResNode.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-
-namespace Dalamud.Game.Internal.Gui.Structs
-{
- ///
- /// Native memory representation of a UI resource node.
- ///
- ///
- /// This is copied from https://github.com/aers/FFXIVClientStructs/blob/main/Component/GUI/AtkResNode.cs.
- /// If you need newer features, include FFXIVClientStructs and ILMerge the assembly.
- ///
- [StructLayout(LayoutKind.Explicit, Size = 0xA8)]
- public unsafe struct AtkResNode
- {
- [FieldOffset(0x0)]
- public IntPtr AtkEventTarget;
-
- [FieldOffset(0x8)]
- public uint NodeID;
-
- [FieldOffset(0x20)]
- public AtkResNode* ParentNode;
-
- [FieldOffset(0x28)]
- public AtkResNode* PrevSiblingNode;
-
- [FieldOffset(0x30)]
- public AtkResNode* NextSiblingNode;
-
- [FieldOffset(0x38)]
- public AtkResNode* ChildNode;
-
- [FieldOffset(0x40)]
- public ushort Type;
-
- [FieldOffset(0x42)]
- public ushort ChildCount;
-
- [FieldOffset(0x44)]
- public float X;
-
- [FieldOffset(0x48)]
- public float Y;
-
- [FieldOffset(0x4C)]
- public float ScaleX;
-
- [FieldOffset(0x50)]
- public float ScaleY;
-
- [FieldOffset(0x54)]
- public float Rotation;
-
- [FieldOffset(0x58)]
- public fixed float UnkMatrix[3 * 2];
-
- [FieldOffset(0x70)]
- public uint Color;
-
- [FieldOffset(0x74)]
- public float Depth;
-
- [FieldOffset(0x78)]
- public float Depth_2;
-
- [FieldOffset(0x7C)]
- public ushort AddRed;
-
- [FieldOffset(0x7E)]
- public ushort AddGreen;
-
- [FieldOffset(0x80)]
- public ushort AddBlue;
-
- [FieldOffset(0x82)]
- public ushort AddRed_2;
-
- [FieldOffset(0x84)]
- public ushort AddGreen_2;
-
- [FieldOffset(0x86)]
- public ushort AddBlue_2;
-
- [FieldOffset(0x88)]
- public byte MultiplyRed;
-
- [FieldOffset(0x89)]
- public byte MultiplyGreen;
-
- [FieldOffset(0x8A)]
- public byte MultiplyBlue;
-
- [FieldOffset(0x8B)]
- public byte MultiplyRed_2;
-
- [FieldOffset(0x8C)]
- public byte MultiplyGreen_2;
-
- [FieldOffset(0x8D)]
- public byte MultiplyBlue_2;
-
- [FieldOffset(0x8E)]
- public byte Alpha_2;
-
- [FieldOffset(0x8F)]
- public byte UnkByte_1;
-
- [FieldOffset(0x90)]
- public ushort Width;
-
- [FieldOffset(0x92)]
- public ushort Height;
-
- [FieldOffset(0x94)]
- public float OriginX;
-
- [FieldOffset(0x98)]
- public float OriginY;
-
- [FieldOffset(0x9C)]
- public ushort Priority;
-
- [FieldOffset(0x9E)]
- public short Flags;
-
- [FieldOffset(0xA0)]
- public uint Flags_2;
- }
-}
diff --git a/Dalamud/Game/Internal/Gui/ToastGui.cs b/Dalamud/Game/Internal/Gui/ToastGui.cs
index bb4883e7a..4c1ce497f 100755
--- a/Dalamud/Game/Internal/Gui/ToastGui.cs
+++ b/Dalamud/Game/Internal/Gui/ToastGui.cs
@@ -31,7 +31,7 @@ namespace Dalamud.Game.Internal.Gui
///
/// The SigScanner instance.
/// The Dalamud instance.
- public ToastGui(SigScanner scanner, Dalamud dalamud)
+ internal ToastGui(SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
diff --git a/Dalamud/Game/Internal/Network/GameNetwork.cs b/Dalamud/Game/Internal/Network/GameNetwork.cs
index cb88c9dfc..947a4651c 100644
--- a/Dalamud/Game/Internal/Network/GameNetwork.cs
+++ b/Dalamud/Game/Internal/Network/GameNetwork.cs
@@ -34,8 +34,8 @@ namespace Dalamud.Game.Internal.Network
this.address.Setup(scanner);
Log.Verbose("===== G A M E N E T W O R K =====");
- Log.Verbose("ProcessZonePacketDown address {ProcessZonePacketDown}", this.address.ProcessZonePacketDown);
- Log.Verbose("ProcessZonePacketUp address {ProcessZonePacketUp}", this.address.ProcessZonePacketUp);
+ Log.Verbose($"ProcessZonePacketDown address 0x{this.address.ProcessZonePacketDown.ToInt64():X}");
+ Log.Verbose($"ProcessZonePacketUp address 0x{this.address.ProcessZonePacketUp.ToInt64():X}");
this.processZonePacketDownHook = new Hook(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour);
this.processZonePacketUpHook = new Hook(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour);
diff --git a/Dalamud/Game/Internal/Resource/ResourceManager.cs b/Dalamud/Game/Internal/Resource/ResourceManager.cs
index 1e80475c1..7e3c2b045 100644
--- a/Dalamud/Game/Internal/Resource/ResourceManager.cs
+++ b/Dalamud/Game/Internal/Resource/ResourceManager.cs
@@ -25,7 +25,7 @@ namespace Dalamud.Game.Internal.File
///
/// The Dalamud instance.
/// The SigScanner instance.
- public ResourceManager(Dalamud dalamud, SigScanner scanner)
+ internal ResourceManager(Dalamud dalamud, SigScanner scanner)
{
this.dalamud = dalamud;
this.address = new ResourceManagerAddressResolver();
diff --git a/Dalamud/Game/Network/NetworkHandlers.cs b/Dalamud/Game/Network/NetworkHandlers.cs
index 790ed57d5..d5fa25adb 100644
--- a/Dalamud/Game/Network/NetworkHandlers.cs
+++ b/Dalamud/Game/Network/NetworkHandlers.cs
@@ -31,7 +31,7 @@ namespace Dalamud.Game.Network
///
/// The Dalamud instance.
/// Whether the client should opt out of marketboard uploads.
- public NetworkHandlers(Dalamud dalamud, bool optOutMbUploads)
+ internal NetworkHandlers(Dalamud dalamud, bool optOutMbUploads)
{
this.dalamud = dalamud;
this.optOutMbUploads = optOutMbUploads;
@@ -84,11 +84,11 @@ namespace Dalamud.Game.Network
{
var flashInfo = new NativeFunctions.FlashWindowInfo
{
- cbSize = (uint)Marshal.SizeOf(),
- uCount = uint.MaxValue,
- dwTimeout = 0,
- dwFlags = NativeFunctions.FlashWindow.All | NativeFunctions.FlashWindow.TimerNoFG,
- hwnd = Process.GetCurrentProcess().MainWindowHandle,
+ Size = (uint)Marshal.SizeOf(),
+ Count = uint.MaxValue,
+ Timeout = 0,
+ Flags = NativeFunctions.FlashWindow.All | NativeFunctions.FlashWindow.TimerNoFG,
+ Hwnd = Process.GetCurrentProcess().MainWindowHandle,
};
NativeFunctions.FlashWindowEx(ref flashInfo);
}
diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs
index ec1608bec..56d0003e3 100644
--- a/Dalamud/Game/SigScanner.cs
+++ b/Dalamud/Game/SigScanner.cs
@@ -34,8 +34,8 @@ namespace Dalamud.Game
if (this.IsCopy)
this.SetupCopiedSegments();
- Log.Verbose("Module base: {Address}", this.TextSectionBase);
- Log.Verbose("Module size: {Size}", this.TextSectionSize);
+ Log.Verbose($"Module base: 0x{this.TextSectionBase.ToInt64():X}");
+ Log.Verbose($"Module size: 0x{this.TextSectionSize:X}");
}
///
@@ -206,7 +206,7 @@ namespace Dalamud.Game
var insnByte = Marshal.ReadByte(scanRet);
if (insnByte == 0xE8 || insnByte == 0xE9)
- return ReadCallSig(scanRet);
+ return ReadJmpCallSig(scanRet);
return scanRet;
}
@@ -220,13 +220,13 @@ namespace Dalamud.Game
}
///
- /// Helper for ScanText to get the correct address for IDA sigs that mark the first CALL location.
+ /// Helper for ScanText to get the correct address for IDA sigs that mark the first JMP or CALL location.
///
- /// The address the CALL sig resolved to.
+ /// The address the JMP or CALL sig resolved to.
/// The real offset of the signature.
- private static IntPtr ReadCallSig(IntPtr sigLocation)
+ private static IntPtr ReadJmpCallSig(IntPtr sigLocation)
{
- var jumpOffset = Marshal.ReadInt32(IntPtr.Add(sigLocation, 1));
+ var jumpOffset = Marshal.ReadInt32(sigLocation, 1);
return IntPtr.Add(sigLocation, 5 + jumpOffset);
}
@@ -235,6 +235,7 @@ namespace Dalamud.Game
signature = signature.Replace(" ", string.Empty);
if (signature.Length % 2 != 0)
throw new ArgumentException("Signature without whitespaces must be divisible by two.", nameof(signature));
+
var needleLength = signature.Length / 2;
var needle = new byte[needleLength];
var mask = new bool[needleLength];
diff --git a/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs b/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs
index 7b6f45e07..ad805f528 100644
--- a/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs
+++ b/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs
@@ -88,7 +88,7 @@ namespace Dalamud.Game.Text.SeStringHandling
/// An SeString containing all the payloads necessary to display an item link in the chat log.
public SeString CreateItemLink(Item item, bool isHQ, string displayNameOverride = null)
{
- return this.CreateItemLink((uint)item.RowId, isHQ, displayNameOverride ?? item.Name);
+ return this.CreateItemLink(item.RowId, isHQ, displayNameOverride ?? item.Name);
}
///
diff --git a/Dalamud/GlobalSuppressions.cs b/Dalamud/GlobalSuppressions.cs
index e4f2f5e86..73bbf6329 100644
--- a/Dalamud/GlobalSuppressions.cs
+++ b/Dalamud/GlobalSuppressions.cs
@@ -18,26 +18,15 @@ using System.Diagnostics.CodeAnalysis;
// Extensions
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group extensions with the relevant class", Scope = "type", Target = "~T:Dalamud.Interface.FontAwesomeExtensions")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Enum followed by an extension", Scope = "type", Target = "~T:Dalamud.Interface.FontAwesomeExtensions")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group extensions with the relevant class", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.JobFlagsExtensions")]
-[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Enum followed by an extension", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.JobFlagsExtensions")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group extensions with the relevant class", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.JobFlagsExt")]
+[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Enum followed by an extension", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.JobFlagsExt")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group extensions with the relevant class", Scope = "type", Target = "~T:Dalamud.Game.Text.XivChatTypeExtensions")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Enum followed by an extension", Scope = "type", Target = "~T:Dalamud.Game.Text.XivChatTypeExtensions")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group attributes with the relevant class", Scope = "type", Target = "~T:Dalamud.Game.Text.XivChatTypeInfoAttribute")]
-// NativeFunctions.cs
-[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Default Microsoft naming", Scope = "type", Target = "~T:Dalamud.NativeFunctions.FlashWindowInfo")]
-
-// EntryPoint.cs
-[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Required by EasyHook", Scope = "member", Target = "~M:Dalamud.EntryPoint.#ctor(EasyHook.RemoteHooking.IContext,Dalamud.DalamudStartInfo)")]
-[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Required by EasyHook", Scope = "member", Target = "~M:Dalamud.EntryPoint.Run(EasyHook.RemoteHooking.IContext,Dalamud.DalamudStartInfo)")]
-
// DalamudStartInfo.cs
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Interop", Scope = "type", Target = "~T:Dalamud.DalamudStartInfo")]
-// AtkResNode.cs
-[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Inherit documentation or lack thereof from FFXIVClientStructs", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.AtkResNode")]
-[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Inherit naming from FFXIVClientStructs", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.AtkResNode")]
-
// PartyFinder
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "Explicit struct layout", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.PartyFinder.Packet")]
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "Explicit struct layout", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.PartyFinder.Listing")]
@@ -86,9 +75,7 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.GamepadState")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.Condition")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.Targets")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Interface.InterfaceManager.LastImGuiIoPtr")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "type", Target = "~T:Dalamud.Game.ClientState.Actors.Types.PartyMember")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Interface.InterfaceManager.OnBuildFonts")]
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.Actors.Types.Actor.Address")]
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.Structs.JobGauge.BLMGauge.NumUmbralHearts")]
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.Structs.JobGauge.DNCGauge.NumCompleteSteps")]
@@ -97,10 +84,12 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Text.SeStringHandling.Payload.END_BYTE")]
[assembly: SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "Unused, but eventually, maybe.", Scope = "member", Target = "~F:Dalamud.Game.ClientState.PartyList.address")]
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "breaking api change", Scope = "member", Target = "~E:Dalamud.Game.ClientState.ClientState.CfPop")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Framework.StatsHistory")]
-[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Framework.StatsHistory")]
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204:Static elements should appear before instance elements", Justification = "breaking api change, move to util", Scope = "type", Target = "~T:Dalamud.Game.Text.EnumExtensions")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "breaking api change, move to util", Scope = "type", Target = "~T:Dalamud.Game.Text.EnumExtensions")]
+[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Framework.StatsHistory")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Framework.StatsHistory")]
+[assembly: SuppressMessage("Usage", "CA2211:Non-constant fields should not be visible", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Framework.StatsHistory")]
+[assembly: SuppressMessage("Usage", "CA2208:Instantiate argument exceptions correctly", Justification = "Appears to be a bug, it is being used correctly", Scope = "member", Target = "~M:Dalamud.Data.DataManager.Initialize(System.String)")]
// I mostly didnt care to do these.
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Network.GameNetwork.OnNetworkMessage")]
diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs
index f709f2a63..90c611844 100644
--- a/Dalamud/Hooking/Hook.cs
+++ b/Dalamud/Hooking/Hook.cs
@@ -3,7 +3,8 @@ using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-using EasyHook;
+using CoreHook;
+using Dalamud.Hooking.Internal;
namespace Dalamud.Hooking
{
@@ -30,8 +31,8 @@ namespace Dalamud.Hooking
{
this.hookInfo = LocalHook.Create(address, detour, null); // Installs a hook here
this.address = address;
- this.original = Marshal.GetDelegateForFunctionPointer(this.hookInfo.HookBypassAddress);
- HookInfo.TrackedHooks.Add(new HookInfo() { Delegate = detour, Hook = this, Assembly = Assembly.GetCallingAssembly() });
+ this.original = Marshal.GetDelegateForFunctionPointer(this.hookInfo.OriginalAddress);
+ HookManager.TrackedHooks.Add(new HookInfo() { Delegate = detour, Hook = this, Assembly = Assembly.GetCallingAssembly() });
}
///
@@ -130,9 +131,8 @@ namespace Dalamud.Hooking
return;
}
- this.hookInfo.Dispose();
-
this.IsDisposed = true;
+ this.hookInfo.Dispose();
}
///
diff --git a/Dalamud/Hooking/HookInfo.cs b/Dalamud/Hooking/Internal/HookInfo.cs
similarity index 89%
rename from Dalamud/Hooking/HookInfo.cs
rename to Dalamud/Hooking/Internal/HookInfo.cs
index 85a815c0d..a702fed27 100644
--- a/Dalamud/Hooking/HookInfo.cs
+++ b/Dalamud/Hooking/Internal/HookInfo.cs
@@ -3,18 +3,13 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
-namespace Dalamud.Hooking
+namespace Dalamud.Hooking.Internal
{
///
/// Class containing information about registered hooks.
///
internal class HookInfo
{
- ///
- /// Static list of tracked and registered hooks.
- ///
- internal static readonly List TrackedHooks = new();
-
private ulong? inProcessMemory = 0;
///
diff --git a/Dalamud/Hooking/Internal/HookManager.cs b/Dalamud/Hooking/Internal/HookManager.cs
new file mode 100644
index 000000000..1651c696e
--- /dev/null
+++ b/Dalamud/Hooking/Internal/HookManager.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+
+namespace Dalamud.Hooking.Internal
+{
+ ///
+ /// This class manages the final disposition of hooks, cleaning up any that have not reverted their changes.
+ ///
+ internal class HookManager : IDisposable
+ {
+ // private readonly Dalamud dalamud;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Dalamud instance.
+ public HookManager(Dalamud dalamud)
+ {
+ _ = dalamud;
+ // this.dalamud = dalamud;
+ }
+
+ ///
+ /// Gets a static list of tracked and registered hooks.
+ ///
+ internal static List TrackedHooks { get; } = new();
+
+ ///
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/Dalamud/Hooking/IDalamudHook.cs b/Dalamud/Hooking/Internal/IDalamudHook.cs
similarity index 94%
rename from Dalamud/Hooking/IDalamudHook.cs
rename to Dalamud/Hooking/Internal/IDalamudHook.cs
index d33fa47b8..8fb95195f 100644
--- a/Dalamud/Hooking/IDalamudHook.cs
+++ b/Dalamud/Hooking/Internal/IDalamudHook.cs
@@ -1,6 +1,6 @@
using System;
-namespace Dalamud.Hooking
+namespace Dalamud.Hooking.Internal
{
///
/// Interface describing a generic hook.
diff --git a/Dalamud/Interface/Colors/ColorDemoWindow.cs b/Dalamud/Interface/Colors/ColorDemoWindow.cs
deleted file mode 100644
index 4dc4e30c9..000000000
--- a/Dalamud/Interface/Colors/ColorDemoWindow.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Numerics;
-
-using Dalamud.Interface.Windowing;
-using ImGuiNET;
-
-namespace Dalamud.Interface.Colors
-{
- ///
- /// color Demo Window to view custom ImGui colors.
- ///
- internal class ColorDemoWindow : Window
- {
- private readonly List> colors;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public ColorDemoWindow()
- : base("Dalamud Colors Demo")
- {
- this.Size = new Vector2(600, 500);
- this.SizeCondition = ImGuiCond.FirstUseEver;
- this.colors = new List>
- {
- Demo("White", ImGuiColors.White),
- Demo("DalamudRed", ImGuiColors.DalamudRed),
- Demo("DalamudGrey", ImGuiColors.DalamudGrey),
- Demo("DalamudGrey2", ImGuiColors.DalamudGrey2),
- Demo("DalamudGrey3", ImGuiColors.DalamudGrey3),
- Demo("DalamudWhite", ImGuiColors.DalamudWhite),
- Demo("DalamudWhite2", ImGuiColors.DalamudWhite2),
- Demo("DalamudOrange", ImGuiColors.DalamudOrange),
- Demo("TankBlue", ImGuiColors.TankBlue),
- Demo("HealerGreen", ImGuiColors.HealerGreen),
- Demo("DPSRed", ImGuiColors.DPSRed),
- };
- this.colors = this.colors.OrderBy(colorDemo => colorDemo.Key).ToList();
- }
-
- ///
- public override void Draw()
- {
- ImGui.BeginChild("color_scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.AlwaysVerticalScrollbar | ImGuiWindowFlags.HorizontalScrollbar);
- ImGui.Text("This is a collection of UI colors you can use in your plugin.");
- ImGui.Separator();
- foreach (var color in this.colors)
- {
- ImGui.TextColored(color.Value, color.Key);
- }
-
- ImGui.EndChild();
- }
-
- private static KeyValuePair Demo(string name, Vector4 color)
- {
- return new KeyValuePair(name, color);
- }
- }
-}
diff --git a/Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs b/Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs
new file mode 100644
index 000000000..225b171bb
--- /dev/null
+++ b/Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs
@@ -0,0 +1,76 @@
+using System.Numerics;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.Components
+{
+ ///
+ /// Class containing various methods providing ImGui components.
+ ///
+ public static partial class ImGuiComponents
+ {
+ ///
+ /// Alpha modified IconButton component to use an icon as a button with alpha and color options.
+ ///
+ /// The icon for the button.
+ /// The ID of the button.
+ /// The default color of the button.
+ /// The color of the button when active.
+ /// The color of the button when hovered.
+ /// A multiplier for the current alpha levels.
+ /// Indicator if button is clicked.
+ public static bool DisabledButton(FontAwesomeIcon icon, int? id = null, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null, float alphaMult = .5f)
+ {
+ ImGui.PushFont(UiBuilder.IconFont);
+
+ var text = icon.ToIconString();
+ if (id.HasValue)
+ text = $"{text}{id}";
+
+ var button = DisabledButton(text, defaultColor, activeColor, hoveredColor, alphaMult);
+
+ ImGui.PopFont();
+
+ return button;
+ }
+
+ ///
+ /// Alpha modified Button component to use as a disabled button with alpha and color options.
+ ///
+ /// The button label with ID.
+ /// The default color of the button.
+ /// The color of the button when active.
+ /// The color of the button when hovered.
+ /// A multiplier for the current alpha levels.
+ /// Indicator if button is clicked.
+ public static bool DisabledButton(string labelWithId, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null, float alphaMult = .5f)
+ {
+ if (defaultColor.HasValue)
+ ImGui.PushStyleColor(ImGuiCol.Button, defaultColor.Value);
+
+ if (activeColor.HasValue)
+ ImGui.PushStyleColor(ImGuiCol.ButtonActive, activeColor.Value);
+
+ if (hoveredColor.HasValue)
+ ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hoveredColor.Value);
+
+ var style = ImGui.GetStyle();
+ ImGui.PushStyleVar(ImGuiStyleVar.Alpha, style.Alpha * alphaMult);
+
+ var button = ImGui.Button(labelWithId);
+
+ ImGui.PopStyleVar();
+
+ if (defaultColor.HasValue)
+ ImGui.PopStyleColor();
+
+ if (activeColor.HasValue)
+ ImGui.PopStyleColor();
+
+ if (hoveredColor.HasValue)
+ ImGui.PopStyleColor();
+
+ return button;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs
index 3b6f8f213..3d65deb74 100644
--- a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs
+++ b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs
@@ -9,6 +9,14 @@ namespace Dalamud.Interface.Components
///
public static partial class ImGuiComponents
{
+ ///
+ /// IconButton component to use an icon as a button.
+ ///
+ /// The icon for the button.
+ /// Indicator if button is clicked.
+ public static bool IconButton(FontAwesomeIcon icon)
+ => IconButton(icon, null, null, null);
+
///
/// IconButton component to use an icon as a button.
///
@@ -16,16 +24,26 @@ namespace Dalamud.Interface.Components
/// The icon for the button.
/// Indicator if button is clicked.
public static bool IconButton(int id, FontAwesomeIcon icon)
- {
- ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero);
- ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero);
- ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero);
- ImGui.PushFont(UiBuilder.IconFont);
- var button = ImGui.Button($"{icon.ToIconString()}{id}");
- ImGui.PopFont();
- ImGui.PopStyleColor(3);
- return button;
- }
+ => IconButton(id, icon, null, null, null);
+
+ ///
+ /// IconButton component to use an icon as a button.
+ ///
+ /// Text already containing the icon string.
+ /// Indicator if button is clicked.
+ public static bool IconButton(string iconText)
+ => IconButton(iconText, null, null, null);
+
+ ///
+ /// IconButton component to use an icon as a button.
+ ///
+ /// The icon for the button.
+ /// The default color of the button.
+ /// The color of the button when active.
+ /// The color of the button when hovered.
+ /// Indicator if button is clicked.
+ public static bool IconButton(FontAwesomeIcon icon, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null)
+ => IconButton($"{icon.ToIconString()}", defaultColor, activeColor, hoveredColor);
///
/// IconButton component to use an icon as a button with color options.
@@ -36,15 +54,48 @@ namespace Dalamud.Interface.Components
/// The color of the button when active.
/// The color of the button when hovered.
/// Indicator if button is clicked.
- public static bool IconButton(int id, FontAwesomeIcon icon, Vector4 defaultColor, Vector4 activeColor, Vector4 hoveredColor)
+ public static bool IconButton(int id, FontAwesomeIcon icon, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null)
+ => IconButton($"{icon.ToIconString()}{id}", defaultColor, activeColor, hoveredColor);
+
+ ///
+ /// IconButton component to use an icon as a button with color options.
+ ///
+ /// Text already containing the icon string.
+ /// The default color of the button.
+ /// The color of the button when active.
+ /// The color of the button when hovered.
+ /// Indicator if button is clicked.
+ public static bool IconButton(string iconText, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null)
{
- ImGui.PushStyleColor(ImGuiCol.Button, defaultColor);
- ImGui.PushStyleColor(ImGuiCol.ButtonActive, activeColor);
- ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hoveredColor);
+ var numColors = 0;
+
+ if (defaultColor.HasValue)
+ {
+ ImGui.PushStyleColor(ImGuiCol.Button, defaultColor.Value);
+ numColors++;
+ }
+
+ if (activeColor.HasValue)
+ {
+ ImGui.PushStyleColor(ImGuiCol.ButtonActive, activeColor.Value);
+ numColors++;
+ }
+
+ if (hoveredColor.HasValue)
+ {
+ ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hoveredColor.Value);
+ numColors++;
+ }
+
ImGui.PushFont(UiBuilder.IconFont);
- var button = ImGui.Button($"{icon.ToIconString()}{id}");
+
+ var button = ImGui.Button(iconText);
+
ImGui.PopFont();
- ImGui.PopStyleColor(3);
+
+ if (numColors > 0)
+ ImGui.PopStyleColor(numColors);
+
return button;
}
}
diff --git a/Dalamud/Interface/Components/ImGuiComponents.TextWithLabel.cs b/Dalamud/Interface/Components/ImGuiComponents.TextWithLabel.cs
index feb127d2a..991fefb3a 100644
--- a/Dalamud/Interface/Components/ImGuiComponents.TextWithLabel.cs
+++ b/Dalamud/Interface/Components/ImGuiComponents.TextWithLabel.cs
@@ -13,8 +13,7 @@ namespace Dalamud.Interface.Components
/// The label for text.
/// The text value.
/// The hint to show on hover.
- public static void TextWithLabel(
- string label, string value, string hint = "")
+ public static void TextWithLabel(string label, string value, string hint = "")
{
ImGui.Text(label + ": ");
ImGui.SameLine();
diff --git a/Dalamud/Interface/ImGuiHelpers.cs b/Dalamud/Interface/ImGuiHelpers.cs
index d2ecda131..52c9802f9 100644
--- a/Dalamud/Interface/ImGuiHelpers.cs
+++ b/Dalamud/Interface/ImGuiHelpers.cs
@@ -20,6 +20,24 @@ namespace Dalamud.Interface
///
public static float GlobalScale { get; private set; }
+ ///
+ /// Gets a that is pre-scaled with the multiplier.
+ ///
+ /// Vector2 X parameter.
+ /// Vector2 Y parameter.
+ /// A scaled Vector2.
+ public static Vector2 ScaledVector2(float x, float y) => new Vector2(x, y) * GlobalScale;
+
+ ///
+ /// Gets a that is pre-scaled with the multiplier.
+ ///
+ /// Vector4 X parameter.
+ /// Vector4 Y parameter.
+ /// Vector4 Z parameter.
+ /// Vector4 W parameter.
+ /// A scaled Vector2.
+ public static Vector4 ScaledVector4(float x, float y, float z, float w) => new Vector4(x, y, z, w) * GlobalScale;
+
///
/// Force the next ImGui window to stay inside the main game window.
///
@@ -29,7 +47,14 @@ namespace Dalamud.Interface
/// Create a dummy scaled by the global Dalamud scale.
///
/// The size of the dummy.
- public static void ScaledDummy(float size) => ImGui.Dummy(new Vector2(size * GlobalScale, size * GlobalScale));
+ public static void ScaledDummy(float size) => ScaledDummy(size, size);
+
+ ///
+ /// Create a dummy scaled by the global Dalamud scale.
+ ///
+ /// Vector2 X parameter.
+ /// Vector2 Y parameter.
+ public static void ScaledDummy(float x, float y) => ScaledDummy(new Vector2(x, y));
///
/// Create a dummy scaled by the global Dalamud scale.
diff --git a/Dalamud/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs
similarity index 95%
rename from Dalamud/DalamudCommands.cs
rename to Dalamud/Interface/Internal/DalamudCommands.cs
index 5b59f4a70..4353a3fee 100644
--- a/Dalamud/DalamudCommands.cs
+++ b/Dalamud/Interface/Internal/DalamudCommands.cs
@@ -7,7 +7,7 @@ using CheapLoc;
using Dalamud.Game.Command;
using Serilog;
-namespace Dalamud
+namespace Dalamud.Interface.Internal
{
///
/// Class handling Dalamud core commands.
@@ -149,7 +149,7 @@ namespace Dalamud
try
{
- this.dalamud.PluginManager.ReloadPlugins();
+ this.dalamud.PluginManager.ReloadAllPlugins();
this.dalamud.Framework.Gui.Chat.Print("OK");
}
@@ -226,20 +226,20 @@ namespace Dalamud
private void OnDebugDrawDevMenu(string command, string arguments)
{
- this.dalamud.DalamudUi.IsDevMenu = !this.dalamud.DalamudUi.IsDevMenu;
+ this.dalamud.DalamudUi.ToggleDevMenu();
}
private void OnDebugDrawDataMenu(string command, string arguments)
{
if (string.IsNullOrEmpty(arguments))
- this.dalamud.DalamudUi.ToggleData();
+ this.dalamud.DalamudUi.ToggleDataWindow();
else
- this.dalamud.DalamudUi.ToggleData(arguments);
+ this.dalamud.DalamudUi.ToggleDataWindow(arguments);
}
private void OnOpenLog(string command, string arguments)
{
- this.dalamud.DalamudUi.ToggleLog();
+ this.dalamud.DalamudUi.ToggleLogWindow();
}
private void OnDebugImInfoCommand(string command, string arguments)
@@ -267,12 +267,12 @@ namespace Dalamud
private void OnOpenInstallerCommand(string command, string arguments)
{
- this.dalamud.DalamudUi.TogglePluginInstaller();
+ this.dalamud.DalamudUi.TogglePluginInstallerWindow();
}
private void OnOpenCreditsCommand(string command, string arguments)
{
- this.dalamud.DalamudUi.ToggleCredits();
+ this.dalamud.DalamudUi.ToggleCreditsWindow();
}
private void OnSetLanguageCommand(string command, string arguments)
@@ -299,7 +299,7 @@ namespace Dalamud
private void OnOpenSettingsCommand(string command, string arguments)
{
- this.dalamud.DalamudUi.ToggleSettings();
+ this.dalamud.DalamudUi.ToggleSettingsWindow();
}
}
}
diff --git a/Dalamud/Interface/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs
similarity index 60%
rename from Dalamud/Interface/DalamudInterface.cs
rename to Dalamud/Interface/Internal/DalamudInterface.cs
index 09c15051a..16203edec 100644
--- a/Dalamud/Interface/DalamudInterface.cs
+++ b/Dalamud/Interface/Internal/DalamudInterface.cs
@@ -1,152 +1,288 @@
using System;
using System.Diagnostics;
-using System.IO;
using System.Linq;
using System.Numerics;
-using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-using CheapLoc;
-using Dalamud.Interface.Colors;
-using Dalamud.Interface.Components;
-using Dalamud.Interface.Scratchpad;
+using Dalamud.Interface.Internal.Windows;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
+using Dalamud.Plugin.Internal;
using ImGuiNET;
-using Serilog;
using Serilog.Events;
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal
{
///
- /// Class handling Dalamud core interface.
+ /// This plugin implements all of the Dalamud interface separately, to allow for reloading of the interface and rapid prototyping.
///
internal class DalamudInterface : IDisposable
{
+ private static readonly ModuleLog Log = new("DUI");
+
private readonly Dalamud dalamud;
+ private readonly WindowSystem windowSystem;
- private readonly DalamudLogWindow logWindow;
- private readonly DalamudDataWindow dataWindow;
- private readonly DalamudCreditsWindow creditsWindow;
- private readonly DalamudSettingsWindow settingsWindow;
- private readonly PluginInstallerWindow pluginWindow;
- private readonly DalamudPluginStatWindow pluginStatWindow;
- private readonly DalamudChangelogWindow changelogWindow;
- private readonly ComponentDemoWindow componentDemoWindow;
+ private readonly ChangelogWindow changelogWindow;
private readonly ColorDemoWindow colorDemoWindow;
- private readonly ScratchpadWindow scratchpadWindow;
+ private readonly ComponentDemoWindow componentDemoWindow;
+ private readonly CreditsWindow creditsWindow;
+ private readonly DataWindow dataWindow;
private readonly GamepadModeNotifierWindow gamepadModeNotifierWindow;
-
- private readonly WindowSystem windowSystem = new("DalamudCore");
+ private readonly LogWindow logWindow;
+ private readonly PluginStatWindow pluginStatWindow;
+ private readonly PluginInstallerWindow pluginWindow;
+ private readonly ScratchpadWindow scratchpadWindow;
+ private readonly SettingsWindow settingsWindow;
private ulong frameCount = 0;
- private bool isImguiDrawDemoWindow = false;
-
#if DEBUG
- private bool isImguiDrawDevMenu = true;
+ private bool isImGuiDrawDevMenu = true;
#else
- private bool isImguiDrawDevMenu = false;
+ private bool isImGuiDrawDevMenu = false;
#endif
+ private bool isImGuiDrawDemoWindow = false;
+
///
/// Initializes a new instance of the class.
///
- /// The Dalamud instance to register to.
+ /// The Dalamud instance.
public DalamudInterface(Dalamud dalamud)
{
this.dalamud = dalamud;
+ this.windowSystem = new WindowSystem("DalamudCore");
- this.logWindow = new DalamudLogWindow(this.dalamud.CommandManager, this.dalamud.Configuration)
- {
- IsOpen = false,
- };
- this.windowSystem.AddWindow(this.logWindow);
+ this.changelogWindow = new ChangelogWindow(dalamud) { IsOpen = false };
+ this.colorDemoWindow = new ColorDemoWindow() { IsOpen = false };
+ this.componentDemoWindow = new ComponentDemoWindow() { IsOpen = false };
+ this.creditsWindow = new CreditsWindow(dalamud) { IsOpen = false };
+ this.dataWindow = new DataWindow(dalamud) { IsOpen = false };
+ this.gamepadModeNotifierWindow = new GamepadModeNotifierWindow();
+ this.logWindow = new LogWindow(dalamud) { IsOpen = this.dalamud.Configuration.LogOpenAtStartup };
+ this.pluginStatWindow = new PluginStatWindow(dalamud) { IsOpen = false };
+ this.pluginWindow = new PluginInstallerWindow(dalamud) { IsOpen = false };
+ this.scratchpadWindow = new ScratchpadWindow(dalamud) { IsOpen = false };
+ this.settingsWindow = new SettingsWindow(dalamud) { IsOpen = false };
- this.dataWindow = new DalamudDataWindow(this.dalamud)
- {
- IsOpen = false,
- };
- this.windowSystem.AddWindow(this.dataWindow);
-
- this.creditsWindow = new DalamudCreditsWindow(this.dalamud)
- {
- IsOpen = false,
- };
+ this.windowSystem.AddWindow(this.changelogWindow);
+ this.windowSystem.AddWindow(this.colorDemoWindow);
+ this.windowSystem.AddWindow(this.componentDemoWindow);
this.windowSystem.AddWindow(this.creditsWindow);
-
- this.settingsWindow = new DalamudSettingsWindow(this.dalamud)
- {
- IsOpen = false,
- };
+ this.windowSystem.AddWindow(this.dataWindow);
+ this.windowSystem.AddWindow(this.gamepadModeNotifierWindow);
+ this.windowSystem.AddWindow(this.logWindow);
+ this.windowSystem.AddWindow(this.pluginStatWindow);
+ this.windowSystem.AddWindow(this.pluginWindow);
+ this.windowSystem.AddWindow(this.scratchpadWindow);
this.windowSystem.AddWindow(this.settingsWindow);
- this.pluginWindow = new PluginInstallerWindow(this.dalamud, this.dalamud.StartInfo.GameVersion)
- {
- IsOpen = false,
- };
- this.windowSystem.AddWindow(this.pluginWindow);
+ this.dalamud.InterfaceManager.OnDraw += this.OnDraw;
- this.pluginStatWindow = new DalamudPluginStatWindow(this.dalamud.PluginManager)
- {
- IsOpen = false,
- };
- this.windowSystem.AddWindow(this.pluginStatWindow);
-
- this.changelogWindow = new DalamudChangelogWindow(this.dalamud)
- {
- IsOpen = false,
- };
- this.windowSystem.AddWindow(this.changelogWindow);
-
- this.componentDemoWindow = new ComponentDemoWindow()
- {
- IsOpen = false,
- };
- this.windowSystem.AddWindow(this.componentDemoWindow);
-
- this.colorDemoWindow = new ColorDemoWindow()
- {
- IsOpen = false,
- };
- this.windowSystem.AddWindow(this.colorDemoWindow);
-
- this.scratchpadWindow = new ScratchpadWindow(this.dalamud)
- {
- IsOpen = false,
- };
- this.windowSystem.AddWindow(this.scratchpadWindow);
-
- this.gamepadModeNotifierWindow = new GamepadModeNotifierWindow();
- this.windowSystem.AddWindow(this.gamepadModeNotifierWindow);
-
- Log.Information("[DUI] Windows added");
-
- if (dalamud.Configuration.LogOpenAtStartup)
- this.OpenLog();
+ Log.Information("Windows added");
}
///
- /// Gets or sets a value indicating whether the Dalamud dev menu is drawing.
+ /// Gets or sets a value indicating whether the /xldev menu is open.
///
- public bool IsDevMenu
+ public bool IsDevMenuOpen
{
- get => this.isImguiDrawDevMenu;
- set => this.isImguiDrawDevMenu = value;
+ get => this.isImGuiDrawDevMenu;
+ set => this.isImGuiDrawDevMenu = value;
}
///
- /// Draw the Dalamud core interface via ImGui.
+ /// Gets a value indicating whether the current Dalamud version warrants displaying the changelog.
///
- public void Draw()
+ public bool WarrantsChangelog => ChangelogWindow.WarrantsChangelog;
+
+ ///
+ public void Dispose()
+ {
+ this.dalamud.InterfaceManager.OnDraw -= this.OnDraw;
+
+ this.windowSystem.RemoveAllWindows();
+
+ this.creditsWindow.Dispose();
+ this.logWindow.Dispose();
+ this.scratchpadWindow.Dispose();
+ }
+
+ #region Open
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenChangelogWindow() => this.changelogWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenColorsDemoWindow() => this.colorDemoWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenComponentDemoWindow() => this.componentDemoWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenCreditsWindow() => this.creditsWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ /// The data kind to switch to after opening.
+ public void OpenDataWindow(string dataKind = null)
+ {
+ this.dataWindow.IsOpen = true;
+ if (dataKind != null && this.dataWindow.IsOpen)
+ {
+ this.dataWindow.SetDataKind(dataKind);
+ }
+ }
+
+ ///
+ /// Opens the dev menu bar.
+ ///
+ public void OpenDevMenu() => this.isImGuiDrawDevMenu = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenLogWindow() => this.logWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenPluginStats() => this.pluginStatWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenPluginInstaller() => this.pluginWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenScratchpadWindow() => this.scratchpadWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenSettings() => this.settingsWindow.IsOpen = true;
+
+ #endregion
+
+ #region Toggle
+
+ ///
+ /// Toggles the .
+ ///
+ public void ToggleChangelogWindow() => this.changelogWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ public void ToggleColorsDemoWindow() => this.colorDemoWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ public void ToggleComponentDemoWindow() => this.componentDemoWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ public void ToggleCreditsWindow() => this.creditsWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ /// The data kind to switch to after opening.
+ public void ToggleDataWindow(string dataKind = null)
+ {
+ this.dataWindow.Toggle();
+ if (dataKind != null && this.dataWindow.IsOpen)
+ {
+ this.dataWindow.SetDataKind(dataKind);
+ }
+ }
+
+ ///
+ /// Toggles the dev menu bar.
+ ///
+ public void ToggleDevMenu() => this.isImGuiDrawDevMenu ^= true;
+
+ ///
+ /// Toggles the .
+ ///
+ public void ToggleGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ public void ToggleLogWindow() => this.logWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ public void TogglePluginStatsWindow() => this.pluginStatWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ public void TogglePluginInstallerWindow() => this.pluginWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ public void ToggleScratchpadWindow() => this.scratchpadWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ public void ToggleSettingsWindow() => this.settingsWindow.Toggle();
+
+ #endregion
+
+ private void OnDraw()
{
this.frameCount++;
- if (!this.IsDevMenu && !this.dalamud.ClientState.Condition.Any())
+ try
{
- ImGui.PushStyleColor(ImGuiCol.Button, new Vector4(0, 0, 0, 0));
- ImGui.PushStyleColor(ImGuiCol.ButtonActive, new Vector4(0, 0, 0, 0));
- ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(0, 0, 0, 0));
+ this.DrawHiddenDevMenuOpener();
+ this.DrawDevMenu();
+
+ if (this.dalamud.Framework.Gui.GameUiHidden)
+ return;
+
+ this.windowSystem.Draw();
+
+ if (this.isImGuiDrawDemoWindow)
+ ImGui.ShowDemoWindow();
+ }
+ catch (Exception ex)
+ {
+ PluginLog.Error(ex, "Error during OnDraw");
+ }
+ }
+
+ private void DrawHiddenDevMenuOpener()
+ {
+ if (!this.isImGuiDrawDevMenu && !this.dalamud.ClientState.Condition.Any())
+ {
+ ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero);
+ ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero);
+ ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero);
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0, 0, 0, 1));
ImGui.PushStyleColor(ImGuiCol.TextSelectedBg, new Vector4(0, 0, 0, 1));
ImGui.PushStyleColor(ImGuiCol.Border, new Vector4(0, 0, 0, 1));
@@ -160,27 +296,30 @@ namespace Dalamud.Interface
if (ImGui.Begin("DevMenu Opener", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoSavedSettings))
{
if (ImGui.Button("###devMenuOpener", new Vector2(40, 25)))
- this.IsDevMenu = true;
+ this.isImGuiDrawDevMenu = true;
ImGui.End();
}
ImGui.PopStyleColor(8);
}
+ }
- if (this.IsDevMenu)
+ private void DrawDevMenu()
+ {
+ if (this.isImGuiDrawDevMenu)
{
if (ImGui.BeginMainMenuBar())
{
if (ImGui.BeginMenu("Dalamud"))
{
- ImGui.MenuItem("Draw Dalamud dev menu", string.Empty, ref this.isImguiDrawDevMenu);
+ ImGui.MenuItem("Draw Dalamud dev menu", string.Empty, ref this.isImGuiDrawDevMenu);
ImGui.Separator();
if (ImGui.MenuItem("Open Log window"))
{
- this.OpenLog();
+ this.OpenLogWindow();
}
if (ImGui.BeginMenu("Set log level..."))
@@ -214,12 +353,12 @@ namespace Dalamud.Interface
if (ImGui.MenuItem("Open Data window"))
{
- this.OpenData();
+ this.OpenDataWindow();
}
if (ImGui.MenuItem("Open Credits window"))
{
- this.OpenCredits();
+ this.OpenCreditsWindow();
}
if (ImGui.MenuItem("Open Settings window"))
@@ -229,20 +368,20 @@ namespace Dalamud.Interface
if (ImGui.MenuItem("Open Changelog window"))
{
- this.OpenChangelog();
+ this.OpenChangelogWindow();
}
if (ImGui.MenuItem("Open Components Demo"))
{
- this.OpenComponentDemo();
+ this.OpenComponentDemoWindow();
}
if (ImGui.MenuItem("Open Colors Demo"))
{
- this.OpenColorsDemo();
+ this.OpenColorsDemoWindow();
}
- ImGui.MenuItem("Draw ImGui demo", string.Empty, ref this.isImguiDrawDemoWindow);
+ ImGui.MenuItem("Draw ImGui demo", string.Empty, ref this.isImGuiDrawDemoWindow);
ImGui.Separator();
@@ -264,12 +403,12 @@ namespace Dalamud.Interface
ImGui.Separator();
if (ImGui.MenuItem("Enable Dalamud testing", string.Empty, this.dalamud.Configuration.DoDalamudTest))
{
- this.dalamud.Configuration.DoDalamudTest = !this.dalamud.Configuration.DoDalamudTest;
+ this.dalamud.Configuration.DoDalamudTest ^= true;
this.dalamud.Configuration.Save();
}
ImGui.MenuItem(Util.AssemblyVersion, false);
- ImGui.MenuItem(this.dalamud.StartInfo.GameVersion, false);
+ ImGui.MenuItem(this.dalamud.StartInfo.GameVersion.ToString(), false);
ImGui.EndMenu();
}
@@ -300,10 +439,10 @@ namespace Dalamud.Interface
if (ImGui.MenuItem("Print plugin info"))
{
- foreach (var plugin in this.dalamud.PluginManager.Plugins)
+ foreach (var plugin in this.dalamud.PluginManager.InstalledPlugins)
{
// TODO: some more here, state maybe?
- Log.Information($"{plugin.Plugin.Name}");
+ PluginLog.Information($"{plugin.Name}");
}
}
@@ -311,18 +450,23 @@ namespace Dalamud.Interface
{
try
{
- this.dalamud.PluginManager.ReloadPlugins();
+ this.dalamud.PluginManager.ReloadAllPlugins();
}
catch (Exception ex)
{
this.dalamud.Framework.Gui.Chat.PrintError("Reload failed.");
- Log.Error(ex, "Plugin reload failed.");
+ PluginLog.Error(ex, "Plugin reload failed.");
}
}
+ if (ImGui.MenuItem("Scan dev plugins"))
+ {
+ this.dalamud.PluginManager.ScanDevPlugins();
+ }
+
ImGui.Separator();
ImGui.MenuItem("API Level:" + PluginManager.DalamudApiLevel, false);
- ImGui.MenuItem("Loaded plugins:" + this.dalamud.PluginManager?.Plugins.Count, false);
+ ImGui.MenuItem("Loaded plugins:" + this.dalamud.PluginManager?.InstalledPlugins.Count, false);
ImGui.EndMenu();
}
@@ -384,197 +528,6 @@ namespace Dalamud.Interface
ImGui.EndMainMenuBar();
}
}
-
- if (this.dalamud.Framework.Gui.GameUiHidden)
- return;
-
- this.windowSystem.Draw();
-
- if (this.isImguiDrawDemoWindow)
- ImGui.ShowDemoWindow();
- }
-
- ///
- /// Dispose the window system and all windows that require it.
- ///
- public void Dispose()
- {
- this.scratchpadWindow.Dispose();
- this.windowSystem.RemoveAllWindows();
-
- this.logWindow?.Dispose();
- this.creditsWindow?.Dispose();
- }
-
- ///
- /// Open the Plugin Installer window.
- ///
- internal void OpenPluginInstaller()
- {
- this.pluginWindow.IsOpen = true;
- }
-
- ///
- /// Open the changelog window.
- ///
- internal void OpenChangelog()
- {
- this.changelogWindow.IsOpen = true;
- }
-
- ///
- /// Open the settings window.
- ///
- internal void OpenSettings()
- {
- this.settingsWindow.IsOpen = true;
- }
-
- ///
- /// Open the log window.
- ///
- internal void OpenLog()
- {
- this.logWindow.IsOpen = true;
- }
-
- ///
- /// Open the data window.
- ///
- internal void OpenData()
- {
- this.dataWindow.IsOpen = true;
- }
-
- ///
- /// Open the credits window.
- ///
- internal void OpenCredits()
- {
- this.creditsWindow.IsOpen = true;
- }
-
- ///
- /// Open the stats window.
- ///
- internal void OpenPluginStats()
- {
- this.pluginStatWindow.IsOpen = true;
- }
-
- ///
- /// Open the component test window.
- ///
- internal void OpenComponentDemo()
- {
- this.componentDemoWindow.IsOpen = true;
- }
-
- ///
- /// Open the colors test window.
- ///
- internal void OpenColorsDemo()
- {
- this.colorDemoWindow.IsOpen = true;
- }
-
- ///
- /// Open the colors test window.
- ///
- internal void OpenScratchpadWindow()
- {
- this.scratchpadWindow.IsOpen = true;
- }
-
- ///
- /// Toggle the Plugin Installer window.
- ///
- internal void TogglePluginInstaller()
- {
- this.pluginWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the changelog window.
- ///
- internal void ToggleChangelog()
- {
- this.changelogWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the settings window.
- ///
- internal void ToggleSettings()
- {
- this.settingsWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the log window.
- ///
- internal void ToggleLog()
- {
- this.logWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the data window.
- ///
- internal void ToggleData()
- {
- this.dataWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the data window and preset the dropdown.
- ///
- /// The data kind to toggle.
- internal void ToggleData(string dataKind)
- {
- this.dataWindow.IsOpen ^= true;
- if (this.dataWindow.IsOpen)
- this.dataWindow.SetDataKind(dataKind);
- }
-
- ///
- /// Toggle the credits window.
- ///
- internal void ToggleCredits()
- {
- this.creditsWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the stats window.
- ///
- internal void TogglePluginStats()
- {
- this.pluginStatWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the component test window.
- ///
- internal void ToggleComponentDemo()
- {
- this.componentDemoWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the scratchpad window.
- ///
- internal void ToggleScratchpadWindow()
- {
- this.scratchpadWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the gamepad notifier window window.
- ///
- internal void ToggleGamePadNotifierWindow()
- {
- this.gamepadModeNotifierWindow.IsOpen ^= true;
}
}
}
diff --git a/Dalamud/Interface/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
similarity index 94%
rename from Dalamud/Interface/InterfaceManager.cs
rename to Dalamud/Interface/Internal/InterfaceManager.cs
index fec66aa19..e4745887f 100644
--- a/Dalamud/Interface/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -10,7 +10,6 @@ using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.Internal.DXGI;
using Dalamud.Hooking;
-using EasyHook;
using ImGuiNET;
using ImGuiScene;
using Serilog;
@@ -27,35 +26,24 @@ using SharpDX.Direct3D11;
* - Might eventually want to render to a separate target and composite, especially with reshade etc in the mix.
*/
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal
{
///
/// This class manages interaction with the ImGui interface.
///
internal class InterfaceManager : IDisposable
{
- ///
- /// Code that is exexuted when fonts are rebuilt.
- ///
- public Action OnBuildFonts;
-
- ///
- /// The pointer to ImGui.IO(), when it last used..
- ///
- public ImGuiIOPtr LastImGuiIoPtr;
-
private readonly Dalamud dalamud;
+ private readonly string rtssPath;
private readonly Hook presentHook;
private readonly Hook resizeBuffersHook;
private readonly Hook setCursorHook;
- private ManualResetEvent fontBuildSignal;
- private ISwapChainAddressResolver address;
+ private readonly ManualResetEvent fontBuildSignal;
+ private readonly ISwapChainAddressResolver address;
private RawDX11Scene scene;
- private string rtssPath;
-
// can't access imgui IO before first present call
private bool lastWantCapture = false;
private bool isRebuildingFonts = false;
@@ -95,12 +83,12 @@ namespace Dalamud.Interface
try
{
- var rtss = NativeFunctions.GetModuleHandle("RTSSHooks64.dll");
+ var rtss = NativeFunctions.GetModuleHandleW("RTSSHooks64.dll");
if (rtss != IntPtr.Zero)
{
var fileName = new StringBuilder(255);
- NativeFunctions.GetModuleFileName(rtss, fileName, fileName.Capacity);
+ _ = NativeFunctions.GetModuleFileNameW(rtss, fileName, fileName.Capacity);
this.rtssPath = fileName.ToString();
Log.Verbose("RTSS at {0}", this.rtssPath);
@@ -113,17 +101,16 @@ namespace Dalamud.Interface
Log.Error(e, "RTSS Free failed");
}
- var setCursorAddr = LocalHook.GetProcAddress("user32.dll", "SetCursor");
+ var user32 = NativeFunctions.GetModuleHandleW("user32.dll");
+ var setCursorAddr = NativeFunctions.GetProcAddress(user32, "SetCursor");
Log.Verbose("===== S W A P C H A I N =====");
- Log.Verbose("SetCursor address {SetCursor}", setCursorAddr);
- Log.Verbose("Present address {Present}", this.address.Present);
- Log.Verbose("ResizeBuffers address {ResizeBuffers}", this.address.ResizeBuffers);
+ Log.Verbose($"SetCursor address 0x{setCursorAddr.ToInt64():X}");
+ Log.Verbose($"Present address 0x{this.address.Present.ToInt64():X}");
+ Log.Verbose($"ResizeBuffers address 0x{this.address.ResizeBuffers.ToInt64():X}");
this.setCursorHook = new Hook(setCursorAddr, this.SetCursorDetour);
-
this.presentHook = new Hook(this.address.Present, this.PresentDetour);
-
this.resizeBuffersHook = new Hook(this.address.ResizeBuffers, this.ResizeBuffersDetour);
}
@@ -153,6 +140,16 @@ namespace Dalamud.Interface
///
public static ImFontPtr IconFont { get; private set; }
+ ///
+ /// Gets or sets an action that is exexuted when fonts are rebuilt.
+ ///
+ public Action OnBuildFonts { get; set; }
+
+ ///
+ /// Gets or sets the pointer to ImGui.IO(), when it was last used.
+ ///
+ public ImGuiIOPtr LastImGuiIoPtr { get; set; }
+
///
/// Gets the D3D11 device instance.
///
@@ -195,11 +192,11 @@ namespace Dalamud.Interface
{
if (!string.IsNullOrEmpty(this.rtssPath))
{
- NativeFunctions.LoadLibrary(this.rtssPath);
+ NativeFunctions.LoadLibraryW(this.rtssPath);
+ var rtssModule = NativeFunctions.GetModuleHandleW("RTSSHooks64.dll");
+ var installAddr = NativeFunctions.GetProcAddress(rtssModule, "InstallRTSSHook");
- var installAddr = LocalHook.GetProcAddress("RTSSHooks64.dll", "InstallRTSSHook");
- var installDele = Marshal.GetDelegateForFunctionPointer(installAddr);
- installDele.Invoke();
+ Marshal.GetDelegateForFunctionPointer(installAddr).Invoke();
}
}
catch (Exception ex)
@@ -567,7 +564,7 @@ namespace Dalamud.Interface
{
ImGui.GetIO().ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad;
this.dalamud.ClientState.GamepadState.NavEnableGamepad ^= true;
- this.dalamud.DalamudUi.ToggleGamePadNotifierWindow();
+ this.dalamud.DalamudUi.ToggleGamepadModeNotifierWindow();
}
if (gamepadEnabled
@@ -592,7 +589,7 @@ namespace Dalamud.Interface
if (this.dalamud.ClientState.GamepadState.Pressed(GamepadButtons.R3) > 0)
{
- this.dalamud.DalamudUi.TogglePluginInstaller();
+ this.dalamud.DalamudUi.TogglePluginInstallerWindow();
}
}
}
diff --git a/Dalamud/Interface/Scratchpad/ScratchExecutionManager.cs b/Dalamud/Interface/Internal/Scratchpad/ScratchExecutionManager.cs
similarity index 94%
rename from Dalamud/Interface/Scratchpad/ScratchExecutionManager.cs
rename to Dalamud/Interface/Internal/Scratchpad/ScratchExecutionManager.cs
index 07010bc84..8d0386662 100644
--- a/Dalamud/Interface/Scratchpad/ScratchExecutionManager.cs
+++ b/Dalamud/Interface/Internal/Scratchpad/ScratchExecutionManager.cs
@@ -9,7 +9,7 @@ using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Serilog;
-namespace Dalamud.Interface.Scratchpad
+namespace Dalamud.Interface.Internal.Scratchpad
{
///
/// This class manages the execution of classes.
@@ -83,9 +83,9 @@ namespace Dalamud.Interface.Scratchpad
{
var script = CSharpScript.Create(code, options);
- var pi = new DalamudPluginInterface(this.dalamud, "Scratch-" + doc.Id, null, PluginLoadReason.Installer);
- var plugin = script.ContinueWith("return new ScratchPlugin() as IDalamudPlugin;").RunAsync().GetAwaiter().GetResult()
- .ReturnValue;
+ var pi = new DalamudPluginInterface(this.dalamud, "Scratch-" + doc.Id, null);
+ var plugin = script.ContinueWith("return new ScratchPlugin() as IDalamudPlugin;")
+ .RunAsync().GetAwaiter().GetResult().ReturnValue;
plugin.Initialize(pi);
diff --git a/Dalamud/Interface/Scratchpad/ScratchFileWatcher.cs b/Dalamud/Interface/Internal/Scratchpad/ScratchFileWatcher.cs
similarity index 96%
rename from Dalamud/Interface/Scratchpad/ScratchFileWatcher.cs
rename to Dalamud/Interface/Internal/Scratchpad/ScratchFileWatcher.cs
index 805ce11fe..88212a3f0 100644
--- a/Dalamud/Interface/Scratchpad/ScratchFileWatcher.cs
+++ b/Dalamud/Interface/Internal/Scratchpad/ScratchFileWatcher.cs
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.IO;
-namespace Dalamud.Interface.Scratchpad
+namespace Dalamud.Interface.Internal.Scratchpad
{
///
/// A file watcher for classes.
diff --git a/Dalamud/Interface/Scratchpad/ScratchLoadStatus.cs b/Dalamud/Interface/Internal/Scratchpad/ScratchLoadStatus.cs
similarity index 91%
rename from Dalamud/Interface/Scratchpad/ScratchLoadStatus.cs
rename to Dalamud/Interface/Internal/Scratchpad/ScratchLoadStatus.cs
index b9398c0f5..46ac0566e 100644
--- a/Dalamud/Interface/Scratchpad/ScratchLoadStatus.cs
+++ b/Dalamud/Interface/Internal/Scratchpad/ScratchLoadStatus.cs
@@ -1,4 +1,4 @@
-namespace Dalamud.Interface.Scratchpad
+namespace Dalamud.Interface.Internal.Scratchpad
{
///
/// The load status of a class.
diff --git a/Dalamud/Interface/Scratchpad/ScratchMacroProcessor.cs b/Dalamud/Interface/Internal/Scratchpad/ScratchMacroProcessor.cs
similarity index 99%
rename from Dalamud/Interface/Scratchpad/ScratchMacroProcessor.cs
rename to Dalamud/Interface/Internal/Scratchpad/ScratchMacroProcessor.cs
index f47c2cab2..ad20b493e 100644
--- a/Dalamud/Interface/Scratchpad/ScratchMacroProcessor.cs
+++ b/Dalamud/Interface/Internal/Scratchpad/ScratchMacroProcessor.cs
@@ -5,7 +5,7 @@ using System.Text.RegularExpressions;
using Dalamud.Plugin;
-namespace Dalamud.Interface.Scratchpad
+namespace Dalamud.Interface.Internal.Scratchpad
{
///
/// This class converts ScratchPad macros into runnable scripts.
diff --git a/Dalamud/Interface/Scratchpad/ScratchpadDocument.cs b/Dalamud/Interface/Internal/Scratchpad/ScratchpadDocument.cs
similarity index 96%
rename from Dalamud/Interface/Scratchpad/ScratchpadDocument.cs
rename to Dalamud/Interface/Internal/Scratchpad/ScratchpadDocument.cs
index 22c1e6b51..cd76a0135 100644
--- a/Dalamud/Interface/Scratchpad/ScratchpadDocument.cs
+++ b/Dalamud/Interface/Internal/Scratchpad/ScratchpadDocument.cs
@@ -1,6 +1,6 @@
using System;
-namespace Dalamud.Interface.Scratchpad
+namespace Dalamud.Interface.Internal.Scratchpad
{
///
/// This class represents a single document in the ScratchPad.
diff --git a/Dalamud/Interface/SerilogEventSink.cs b/Dalamud/Interface/Internal/SerilogEventSink.cs
similarity index 95%
rename from Dalamud/Interface/SerilogEventSink.cs
rename to Dalamud/Interface/Internal/SerilogEventSink.cs
index 4810f75b8..bf4a0e5d2 100644
--- a/Dalamud/Interface/SerilogEventSink.cs
+++ b/Dalamud/Interface/Internal/SerilogEventSink.cs
@@ -3,7 +3,7 @@ using System;
using Serilog.Core;
using Serilog.Events;
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal
{
///
/// Serilog event sink.
@@ -41,7 +41,9 @@ namespace Dalamud.Interface
var message = $"[{DateTimeOffset.Now:HH:mm:ss.fff}][{logEvent.Level}] {logEvent.RenderMessage(this.formatProvider)}";
if (logEvent.Exception != null)
+ {
message += "\n" + logEvent.Exception;
+ }
this.OnLogLine?.Invoke(this, (message, logEvent.Level));
}
diff --git a/Dalamud/Interface/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs
similarity index 99%
rename from Dalamud/Interface/UiDebug.cs
rename to Dalamud/Interface/Internal/UiDebug.cs
index 16e2a75fc..3e7310633 100644
--- a/Dalamud/Interface/UiDebug.cs
+++ b/Dalamud/Interface/Internal/UiDebug.cs
@@ -14,7 +14,7 @@ using AlignmentType = FFXIVClientStructs.FFXIV.Component.GUI.AlignmentType;
// Customised version of https://github.com/aers/FFXIVUIDebug
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal
{
///
/// This class displays a debug window to inspect native addons.
diff --git a/Dalamud/Interface/DalamudChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs
similarity index 74%
rename from Dalamud/Interface/DalamudChangelogWindow.cs
rename to Dalamud/Interface/Internal/Windows/ChangelogWindow.cs
index 3d01bef83..69c2ade06 100644
--- a/Dalamud/Interface/DalamudChangelogWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs
@@ -1,15 +1,14 @@
using System.Diagnostics;
-using System.Numerics;
using Dalamud.Interface.Windowing;
using ImGuiNET;
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal.Windows
{
///
/// For major updates, an in-game Changelog window.
///
- internal class DalamudChangelogWindow : Window
+ internal sealed class ChangelogWindow : Window
{
///
/// Whether the latest update warrants a changelog window.
@@ -23,13 +22,13 @@ namespace Dalamud.Interface
If you note any issues or need help, please make sure to ask on our discord server.";
private readonly Dalamud dalamud;
- private string assemblyVersion = Util.AssemblyVersion;
+ private readonly string assemblyVersion = Util.AssemblyVersion;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The Dalamud instance.
- public DalamudChangelogWindow(Dalamud dalamud)
+ public ChangelogWindow(Dalamud dalamud)
: base("What's new in XIVLauncher?", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoResize)
{
this.dalamud = dalamud;
@@ -44,57 +43,63 @@ If you note any issues or need help, please make sure to ask on our discord serv
{
ImGui.Text($"The in-game addon has been updated to version D{this.assemblyVersion}.");
- ImGui.Dummy(new Vector2(10, 10) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(10);
ImGui.Text("The following changes were introduced:");
ImGui.Text(ChangeLog);
- ImGui.Dummy(new Vector2(10, 10) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(10);
ImGui.Text("Thank you for using our tools!");
- ImGui.Dummy(new Vector2(10, 10) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(10);
- ImGui.PushFont(InterfaceManager.IconFont);
+ ImGui.PushFont(UiBuilder.IconFont);
if (ImGui.Button(FontAwesomeIcon.Download.ToIconString()))
+ {
this.dalamud.DalamudUi.OpenPluginInstaller();
+ }
if (ImGui.IsItemHovered())
{
ImGui.PopFont();
ImGui.SetTooltip("Open Plugin Installer");
- ImGui.PushFont(InterfaceManager.IconFont);
+ ImGui.PushFont(UiBuilder.IconFont);
}
ImGui.SameLine();
if (ImGui.Button(FontAwesomeIcon.LaughBeam.ToIconString()))
+ {
Process.Start("https://discord.gg/3NMcUV5");
+ }
if (ImGui.IsItemHovered())
{
ImGui.PopFont();
ImGui.SetTooltip("Join our Discord server");
- ImGui.PushFont(InterfaceManager.IconFont);
+ ImGui.PushFont(UiBuilder.IconFont);
}
ImGui.SameLine();
if (ImGui.Button(FontAwesomeIcon.Globe.ToIconString()))
+ {
Process.Start("https://github.com/goatcorp/FFXIVQuickLauncher");
+ }
if (ImGui.IsItemHovered())
{
ImGui.PopFont();
ImGui.SetTooltip("See our GitHub repository");
- ImGui.PushFont(InterfaceManager.IconFont);
+ ImGui.PushFont(UiBuilder.IconFont);
}
ImGui.PopFont();
ImGui.SameLine();
- ImGui.Dummy(new Vector2(20, 0) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(20, 0);
ImGui.SameLine();
if (ImGui.Button("Close"))
diff --git a/Dalamud/Interface/Internal/Windows/ColorDemoWindow.cs b/Dalamud/Interface/Internal/Windows/ColorDemoWindow.cs
new file mode 100644
index 000000000..a1c1c1866
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/ColorDemoWindow.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Windowing;
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.Windows
+{
+ ///
+ /// Color Demo Window to view custom ImGui colors.
+ ///
+ internal sealed class ColorDemoWindow : Window
+ {
+ private readonly List<(string Name, Vector4 Color)> colors;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ColorDemoWindow()
+ : base("Dalamud Colors Demo")
+ {
+ this.Size = new Vector2(600, 500);
+ this.SizeCondition = ImGuiCond.FirstUseEver;
+
+ this.colors = new List<(string Name, Vector4 Color)>()
+ {
+ ("White", ImGuiColors.White),
+ ("DalamudRed", ImGuiColors.DalamudRed),
+ ("DalamudGrey", ImGuiColors.DalamudGrey),
+ ("DalamudGrey2", ImGuiColors.DalamudGrey2),
+ ("DalamudGrey3", ImGuiColors.DalamudGrey3),
+ ("DalamudWhite", ImGuiColors.DalamudWhite),
+ ("DalamudWhite2", ImGuiColors.DalamudWhite2),
+ ("DalamudOrange", ImGuiColors.DalamudOrange),
+ ("TankBlue", ImGuiColors.TankBlue),
+ ("HealerGreen", ImGuiColors.HealerGreen),
+ ("DPSRed", ImGuiColors.DPSRed),
+ }.OrderBy(colorDemo => colorDemo.Name).ToList();
+ }
+
+ ///
+ public override void Draw()
+ {
+ ImGui.Text("This is a collection of UI colors you can use in your plugin.");
+
+ ImGui.Separator();
+
+ foreach (var (name, color) in this.colors)
+ {
+ ImGui.TextColored(color, name);
+ }
+ }
+ }
+}
diff --git a/Dalamud/Interface/Components/ComponentDemoWindow.cs b/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs
similarity index 68%
rename from Dalamud/Interface/Components/ComponentDemoWindow.cs
rename to Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs
index bb62fba66..814c7abc6 100644
--- a/Dalamud/Interface/Components/ComponentDemoWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs
@@ -3,17 +3,18 @@ using System.Collections.Generic;
using System.Numerics;
using Dalamud.Interface.Colors;
+using Dalamud.Interface.Components;
using Dalamud.Interface.Windowing;
using ImGuiNET;
-namespace Dalamud.Interface.Components
+namespace Dalamud.Interface.Internal.Windows
{
///
/// Component Demo Window to view custom ImGui components.
///
- internal class ComponentDemoWindow : Window
+ internal sealed class ComponentDemoWindow : Window
{
- private readonly List> componentDemos;
+ private readonly List<(string Name, Action Demo)> componentDemos;
private Vector4 defaultColor = ImGuiColors.DalamudOrange;
///
@@ -24,33 +25,31 @@ namespace Dalamud.Interface.Components
{
this.Size = new Vector2(600, 500);
this.SizeCondition = ImGuiCond.FirstUseEver;
- this.componentDemos = new List>
+
+ this.componentDemos = new()
{
- Demo("Test", ImGuiComponents.Test),
- Demo("HelpMarker", HelpMarkerDemo),
- Demo("IconButton", IconButtonDemo),
- Demo("TextWithLabel", TextWithLabelDemo),
- Demo("ColorPickerWithPalette", this.ColorPickerWithPaletteDemo),
+ ("Test", ImGuiComponents.Test),
+ ("HelpMarker", HelpMarkerDemo),
+ ("IconButton", IconButtonDemo),
+ ("TextWithLabel", TextWithLabelDemo),
+ ("ColorPickerWithPalette", this.ColorPickerWithPaletteDemo),
};
}
///
public override void Draw()
{
- ImGui.BeginChild("comp_scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.AlwaysVerticalScrollbar | ImGuiWindowFlags.HorizontalScrollbar);
ImGui.Text("This is a collection of UI components you can use in your plugin.");
for (var i = 0; i < this.componentDemos.Count; i++)
{
var componentDemo = this.componentDemos[i];
- if (ImGui.CollapsingHeader($"{componentDemo.Key}###comp{i}"))
+ if (ImGui.CollapsingHeader($"{componentDemo.Name}###comp{i}"))
{
- componentDemo.Value();
+ componentDemo.Demo();
}
}
-
- ImGui.EndChild();
}
private static void HelpMarkerDemo()
@@ -80,11 +79,6 @@ namespace Dalamud.Interface.Components
ImGuiComponents.TextWithLabel("Label", "Hover to see more", "more");
}
- private static KeyValuePair Demo(string name, Action func)
- {
- return new KeyValuePair(name, func);
- }
-
private void ColorPickerWithPaletteDemo()
{
ImGui.Text("Click on the color button to use the picker.");
diff --git a/Dalamud/Interface/DalamudCreditsWindow.cs b/Dalamud/Interface/Internal/Windows/CreditsWindow.cs
similarity index 63%
rename from Dalamud/Interface/DalamudCreditsWindow.cs
rename to Dalamud/Interface/Internal/Windows/CreditsWindow.cs
index a3dd358e7..412a7ffbb 100644
--- a/Dalamud/Interface/DalamudCreditsWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/CreditsWindow.cs
@@ -4,17 +4,16 @@ using System.IO;
using System.Linq;
using System.Numerics;
-using Dalamud.Game.Internal;
using Dalamud.Interface.Windowing;
using ImGuiNET;
using ImGuiScene;
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal.Windows
{
///
/// A window documenting contributors to the project.
///
- internal class DalamudCreditsWindow : Window, IDisposable
+ internal class CreditsWindow : Window, IDisposable
{
private const float CreditFPS = 60.0f;
private const string CreditsTextTempl = @"
@@ -105,24 +104,21 @@ Thank you for using XIVLauncher and Dalamud!
";
private readonly Dalamud dalamud;
- private TextureWrap logoTexture;
- private Framework framework;
+ private readonly TextureWrap logoTexture;
+ private readonly Stopwatch creditsThrottler;
private string creditsText;
- private Stopwatch creditsThrottler;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The Dalamud instance.
- public DalamudCreditsWindow(Dalamud dalamud)
+ public CreditsWindow(Dalamud dalamud)
: base("Dalamud Credits", ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize, true)
{
this.dalamud = dalamud;
- this.logoTexture = this.dalamud.InterfaceManager.LoadImage(
- Path.Combine(this.dalamud.AssetDirectory.FullName, "UIRes", "logo.png"));
- this.framework = dalamud.Framework;
- this.creditsThrottler = new Stopwatch();
+ this.logoTexture = this.dalamud.InterfaceManager.LoadImage(Path.Combine(this.dalamud.AssetDirectory.FullName, "UIRes", "logo.png"));
+ this.creditsThrottler = new();
this.Size = new Vector2(500, 400);
this.SizeCondition = ImGuiCond.Always;
@@ -135,14 +131,14 @@ Thank you for using XIVLauncher and Dalamud!
///
public override void OnOpen()
{
- base.OnOpen();
+ var pluginCredits = this.dalamud.PluginManager.InstalledPlugins
+ .Where(plugin => plugin.Manifest != null)
+ .Select(plugin => $"{plugin.Manifest.Name} by {plugin.Manifest.Author}\n")
+ .Aggregate(string.Empty, (current, next) => $"{current}{next}");
- var pluginCredits = this.dalamud.PluginManager.Plugins.Where(x => x.Definition != null).Aggregate(string.Empty, (current, plugin) => current + $"{plugin.Definition.Name} by {plugin.Definition.Author}\n");
+ this.creditsText = string.Format(CreditsTextTempl, typeof(Dalamud).Assembly.GetName().Version, pluginCredits);
- this.creditsText =
- string.Format(CreditsTextTempl, typeof(Dalamud).Assembly.GetName().Version, pluginCredits);
-
- this.framework.Gui.SetBgm(132);
+ this.dalamud.Framework.Gui.SetBgm(132);
this.creditsThrottler.Restart();
}
@@ -150,9 +146,7 @@ Thank you for using XIVLauncher and Dalamud!
public override void OnClose()
{
this.creditsThrottler.Reset();
- base.OnClose();
-
- this.framework.Gui.SetBgm(9999);
+ this.dalamud.Framework.Gui.SetBgm(9999);
}
///
@@ -161,19 +155,19 @@ Thank you for using XIVLauncher and Dalamud!
var screenSize = ImGui.GetMainViewport().Size;
var windowSize = ImGui.GetWindowSize();
- this.Position = new Vector2((screenSize.X / 2) - (windowSize.X / 2), (screenSize.Y / 2) - (windowSize.Y / 2));
+ this.Position = (screenSize - windowSize) / 2;
- ImGui.BeginChild("scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.NoScrollbar);
+ ImGui.BeginChild("scrolling", Vector2.Zero, false, ImGuiWindowFlags.NoScrollbar);
- ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0));
+ ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
- ImGui.Dummy(new Vector2(0, 340f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(0, 340f);
ImGui.Text(string.Empty);
ImGui.SameLine(150f);
- ImGui.Image(this.logoTexture.ImGuiHandle, new Vector2(190f, 190f) * ImGui.GetIO().FontGlobalScale);
+ ImGui.Image(this.logoTexture.ImGuiHandle, ImGuiHelpers.ScaledVector2(190f, 190f));
- ImGui.Dummy(new Vector2(0, 20f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(0, 20f);
var windowX = ImGui.GetWindowSize().X;
@@ -188,12 +182,11 @@ Thank you for using XIVLauncher and Dalamud!
ImGui.PopStyleVar();
- var curY = ImGui.GetScrollY();
- var maxY = ImGui.GetScrollMaxY();
-
if (this.creditsThrottler.Elapsed.TotalMilliseconds > (1000.0f / CreditFPS))
{
- this.creditsThrottler.Restart();
+ var curY = ImGui.GetScrollY();
+ var maxY = ImGui.GetScrollMaxY();
+
if (curY < maxY - 1)
{
ImGui.SetScrollY(curY + 1);
diff --git a/Dalamud/Interface/DalamudDataWindow.cs b/Dalamud/Interface/Internal/Windows/DataWindow.cs
similarity index 95%
rename from Dalamud/Interface/DalamudDataWindow.cs
rename to Dalamud/Interface/Internal/Windows/DataWindow.cs
index 45ba6a6ad..29042f4c2 100644
--- a/Dalamud/Interface/DalamudDataWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/DataWindow.cs
@@ -12,7 +12,6 @@ using Dalamud.Game.Internal;
using Dalamud.Game.Internal.Gui.Addon;
using Dalamud.Game.Internal.Gui.Toast;
using Dalamud.Game.Text;
-using Dalamud.Interface.Colors;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using ImGuiNET;
@@ -20,25 +19,25 @@ using ImGuiScene;
using Newtonsoft.Json;
using Serilog;
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal.Windows
{
///
/// Class responsible for drawing the data/debug window.
///
- internal class DalamudDataWindow : Window
+ internal class DataWindow : Window
{
private readonly Dalamud dalamud;
- private bool wasReady;
- private string serverOpString;
-
- private int currentKind;
- private string[] dataKinds = new[]
+ private readonly string[] dataKinds = new[]
{
"ServerOpCode", "Address", "Actor Table", "Font Test", "Party List", "Plugin IPC", "Condition",
"Gauge", "Command", "Addon", "Addon Inspector", "StartInfo", "Target", "Toast", "ImGui", "Tex", "Gamepad",
};
+ private bool wasReady;
+ private string serverOpString;
+ private int currentKind;
+
private bool drawActors = false;
private float maxActorDrawDistance = 20;
@@ -73,10 +72,10 @@ namespace Dalamud.Interface
private uint copyButtonIndex = 0;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The Dalamud instance to access data of.
- public DalamudDataWindow(Dalamud dalamud)
+ public DataWindow(Dalamud dalamud)
: base("Dalamud Data")
{
this.dalamud = dalamud;
@@ -202,7 +201,7 @@ namespace Dalamud.Interface
ImGui.Text(((int)fontAwesomeIcon.ToIconChar()).ToString("X") + " - ");
ImGui.SameLine();
- ImGui.PushFont(InterfaceManager.IconFont);
+ ImGui.PushFont(UiBuilder.IconFont);
ImGui.Text(fontAwesomeIcon.ToIconString());
ImGui.PopFont();
}
@@ -248,7 +247,7 @@ namespace Dalamud.Interface
// Condition
case 6:
#if DEBUG
- ImGui.Text($"ptr: {this.dalamud.ClientState.Condition.ConditionArrayBase.ToString("X16")}");
+ ImGui.Text($"ptr: 0x{this.dalamud.ClientState.Condition.ConditionArrayBase.ToInt64():X}");
#endif
ImGui.Text("Current Conditions:");
@@ -413,9 +412,13 @@ namespace Dalamud.Interface
$"R3 {resolve(GamepadButtons.R3)} ");
};
#if DEBUG
- ImGui.Text($"GamepadInput {this.dalamud.ClientState.GamepadState.GamepadInput.ToString("X")}");
- if (ImGui.IsItemHovered()) ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
- if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"{this.dalamud.ClientState.GamepadState.GamepadInput.ToString("X")}");
+ ImGui.Text($"GamepadInput 0x{this.dalamud.ClientState.GamepadState.GamepadInput.ToInt64():X}");
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
+
+ if (ImGui.IsItemClicked())
+ ImGui.SetClipboardText($"0x{this.dalamud.ClientState.GamepadState.GamepadInput.ToInt64():X}");
#endif
helper(
@@ -504,8 +507,7 @@ namespace Dalamud.Interface
// So, while WorldToScreen will return false if the point is off of game client screen, to
// to avoid performance issues, we have to manually determine if creating a window would
// produce a new viewport, and skip rendering it if so
- var actorText =
- $"{actor.Address.ToInt64():X}:{actor.ActorId:X}[{i}] - {actor.ObjectKind} - {actor.Name}";
+ var actorText = $"{actor.Address.ToInt64():X}:{actor.ActorId:X}[{i}] - {actor.ObjectKind} - {actor.Name}";
var screenPos = ImGui.GetMainViewport().Pos;
var screenSize = ImGui.GetMainViewport().Size;
@@ -546,8 +548,8 @@ namespace Dalamud.Interface
#pragma warning disable CS0618 // Type or member is obsolete
private void DrawIpcDebug()
{
- var i1 = new DalamudPluginInterface(this.dalamud, "DalamudTestSub", null, PluginLoadReason.Boot);
- var i2 = new DalamudPluginInterface(this.dalamud, "DalamudTestPub", null, PluginLoadReason.Boot);
+ var i1 = new DalamudPluginInterface(this.dalamud, "DalamudTestSub", null);
+ var i2 = new DalamudPluginInterface(this.dalamud, "DalamudTestPub", null);
if (ImGui.Button("Add test sub"))
{
@@ -588,8 +590,8 @@ namespace Dalamud.Interface
i2.SendMessage("DalamudTestSub", testMsg);
}
- foreach (var (sourcePluginName, subPluginName, subAction) in this.dalamud.PluginManager.IpcSubscriptions)
- ImGui.Text($"Source:{sourcePluginName} Sub:{subPluginName}");
+ foreach (var ipc in this.dalamud.PluginManager.IpcSubscriptions)
+ ImGui.Text($"Source:{ipc.SourcePluginName} Sub:{ipc.SubPluginName}");
}
#pragma warning restore CS0618 // Type or member is obsolete
diff --git a/Dalamud/Interface/GamepadModeNotifierWindow.cs b/Dalamud/Interface/Internal/Windows/GamepadModeNotifierWindow.cs
similarity index 96%
rename from Dalamud/Interface/GamepadModeNotifierWindow.cs
rename to Dalamud/Interface/Internal/Windows/GamepadModeNotifierWindow.cs
index 8794a0386..2dc174fde 100644
--- a/Dalamud/Interface/GamepadModeNotifierWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/GamepadModeNotifierWindow.cs
@@ -1,10 +1,10 @@
-using System.Numerics;
+using System.Numerics;
using CheapLoc;
using Dalamud.Interface.Windowing;
using ImGuiNET;
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal.Windows
{
///
/// Class responsible for drawing a notifier on screen that gamepad mode is active.
diff --git a/Dalamud/Interface/DalamudLogWindow.cs b/Dalamud/Interface/Internal/Windows/LogWindow.cs
similarity index 78%
rename from Dalamud/Interface/DalamudLogWindow.cs
rename to Dalamud/Interface/Internal/Windows/LogWindow.cs
index 78cbaa17e..305ff1676 100644
--- a/Dalamud/Interface/DalamudLogWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/LogWindow.cs
@@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Numerics;
-using Dalamud.Configuration;
+using Dalamud.Configuration.Internal;
using Dalamud.Game.Command;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Windowing;
@@ -10,12 +10,12 @@ using ImGuiNET;
using Serilog;
using Serilog.Events;
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal.Windows
{
///
/// The window that displays the Dalamud log file in-game.
///
- internal class DalamudLogWindow : Window, IDisposable
+ internal class LogWindow : Window, IDisposable
{
private readonly CommandManager commandManager;
private readonly DalamudConfiguration configuration;
@@ -27,18 +27,17 @@ namespace Dalamud.Interface
private string commandText = string.Empty;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
- /// The CommandManager instance.
- /// The DalamudConfiguration instance.
- public DalamudLogWindow(CommandManager commandManager, DalamudConfiguration configuration)
+ /// The Dalamud instance.
+ public LogWindow(Dalamud dalamud)
: base("Dalamud LOG")
{
- this.commandManager = commandManager;
- this.configuration = configuration;
- this.autoScroll = configuration.LogAutoScroll;
- this.openAtStartup = configuration.LogOpenAtStartup;
- SerilogEventSink.Instance.OnLogLine += this.Serilog_OnLogLine;
+ this.commandManager = dalamud.CommandManager;
+ this.configuration = dalamud.Configuration;
+ this.autoScroll = this.configuration.LogAutoScroll;
+ this.openAtStartup = this.configuration.LogOpenAtStartup;
+ SerilogEventSink.Instance.OnLogLine += this.OnLogLine;
this.Size = new Vector2(500, 400);
this.SizeCondition = ImGuiCond.FirstUseEver;
@@ -49,7 +48,7 @@ namespace Dalamud.Interface
///
public void Dispose()
{
- SerilogEventSink.Instance.OnLogLine -= this.Serilog_OnLogLine;
+ SerilogEventSink.Instance.OnLogLine -= this.OnLogLine;
}
///
@@ -100,15 +99,19 @@ namespace Dalamud.Interface
// Main window
if (ImGui.Button("Options"))
ImGui.OpenPopup("Options");
+
ImGui.SameLine();
var clear = ImGui.Button("Clear");
+
ImGui.SameLine();
var copy = ImGui.Button("Copy");
ImGui.Text("Enter command: ");
ImGui.SameLine();
+
ImGui.InputText("##commandbox", ref this.commandText, 255);
ImGui.SameLine();
+
if (ImGui.Button("Send"))
{
if (this.commandManager.ProcessCommand(this.commandText))
@@ -117,18 +120,23 @@ namespace Dalamud.Interface
}
else
{
- Log.Information("Command {0} not registered.", this.commandText);
+ Log.Information($"Command {this.commandText} is not registered.");
}
}
ImGui.BeginChild("scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.HorizontalScrollbar);
if (clear)
+ {
this.Clear();
- if (copy)
- ImGui.LogToClipboard();
+ }
- ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0));
+ if (copy)
+ {
+ ImGui.LogToClipboard();
+ }
+
+ ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
lock (this.renderLock)
{
@@ -141,12 +149,14 @@ namespace Dalamud.Interface
ImGui.PopStyleVar();
if (this.autoScroll && ImGui.GetScrollY() >= ImGui.GetScrollMaxY())
+ {
ImGui.SetScrollHereY(1.0f);
+ }
ImGui.EndChild();
}
- private void Serilog_OnLogLine(object sender, (string Line, LogEventLevel Level) logEvent)
+ private void OnLogLine(object sender, (string Line, LogEventLevel Level) logEvent)
{
var color = logEvent.Level switch
{
@@ -156,7 +166,7 @@ namespace Dalamud.Interface
LogEventLevel.Information => ImGuiColors.DalamudWhite,
LogEventLevel.Warning => ImGuiColors.DalamudOrange,
LogEventLevel.Fatal => ImGuiColors.DalamudRed,
- _ => throw new ArgumentOutOfRangeException(),
+ _ => throw new ArgumentOutOfRangeException(logEvent.Level.ToString(), "Invalid LogEventLevel"),
};
this.AddLog(logEvent.Line, color);
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs
new file mode 100644
index 000000000..75037704b
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs
@@ -0,0 +1,1173 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Numerics;
+using System.Threading.Tasks;
+
+using CheapLoc;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Components;
+using Dalamud.Interface.Windowing;
+using Dalamud.Plugin.Internal;
+using Dalamud.Plugin.Internal.Exceptions;
+using Dalamud.Plugin.Internal.Types;
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.Windows
+{
+ ///
+ /// Class responsible for drawing the plugin installer.
+ ///
+ internal class PluginInstallerWindow : Window, IDisposable
+ {
+ private static readonly ModuleLog Log = new("PLUGINW");
+
+ private readonly Dalamud dalamud;
+
+ private bool errorModalDrawing = true;
+ private bool errorModalOnNextFrame = false;
+ private string errorModalMessage = string.Empty;
+
+ private int updatePluginCount = 0;
+ private List updatedPlugins;
+
+ private List pluginListAvailable = new();
+ private List pluginListInstalled = new();
+ private List pluginListUpdatable = new();
+ private bool hasDevPlugins = false;
+
+ private string searchText = string.Empty;
+
+ private PluginSortKind sortKind = PluginSortKind.Alphabetical;
+ private string filterText = Locs.SortBy_Alphabetical;
+
+ private OperationStatus installStatus = OperationStatus.Idle;
+ private OperationStatus updateStatus = OperationStatus.Idle;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Dalamud instance.
+ public PluginInstallerWindow(Dalamud dalamud)
+ : base(
+ Locs.WindowTitle + (dalamud.Configuration.DoPluginTest ? Locs.WindowTitleMod_Testing : string.Empty) + "###XlPluginInstaller",
+ ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoScrollbar)
+ {
+ this.dalamud = dalamud;
+ this.IsOpen = true;
+
+ this.Size = new Vector2(810, 520);
+ this.SizeCondition = ImGuiCond.Always;
+
+ // For debugging
+ if (this.dalamud.PluginManager.PluginsReady)
+ this.OnInstalledPluginsChanged();
+
+ this.dalamud.PluginManager.OnAvailablePluginsChanged += this.OnAvailablePluginsChanged;
+ this.dalamud.PluginManager.OnInstalledPluginsChanged += this.OnInstalledPluginsChanged;
+ }
+
+ private enum OperationStatus
+ {
+ Idle,
+ InProgress,
+ Complete,
+ }
+
+ private enum PluginSortKind
+ {
+ Alphabetical,
+ DownloadCount,
+ LastUpdate,
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.dalamud.PluginManager.OnAvailablePluginsChanged -= this.OnAvailablePluginsChanged;
+ this.dalamud.PluginManager.OnInstalledPluginsChanged -= this.OnInstalledPluginsChanged;
+ }
+
+ ///
+ public override void OnOpen()
+ {
+ Task.Run(this.dalamud.PluginManager.ReloadPluginMasters);
+
+ this.updatePluginCount = 0;
+ this.updatedPlugins = null;
+
+ this.searchText = string.Empty;
+ this.sortKind = PluginSortKind.Alphabetical;
+ this.filterText = Locs.SortBy_Alphabetical;
+ }
+
+ ///
+ public override void Draw()
+ {
+ this.DrawHeader();
+ this.DrawPluginTabBar();
+ this.DrawFooter();
+ this.DrawErrorModal();
+ }
+
+ private static Vector2 GetButtonSize(string text) => ImGui.CalcTextSize(text) + (ImGui.GetStyle().FramePadding * 2);
+
+ private void DrawHeader()
+ {
+ var style = ImGui.GetStyle();
+ var windowSize = ImGui.GetWindowContentRegionMax();
+
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5 * ImGuiHelpers.GlobalScale));
+
+ var searchInputWidth = 240 * ImGuiHelpers.GlobalScale;
+
+ var sortByText = Locs.SortBy_Label;
+ var sortByTextWidth = ImGui.CalcTextSize(sortByText).X;
+ var sortSelectables = new (string Localization, PluginSortKind SortKind)[]
+ {
+ (Locs.SortBy_Alphabetical, PluginSortKind.Alphabetical),
+ (Locs.SortBy_DownloadCounts, PluginSortKind.DownloadCount),
+ (Locs.SortBy_LastUpdate, PluginSortKind.LastUpdate),
+ };
+ var longestSelectableWidth = sortSelectables.Select(t => ImGui.CalcTextSize(t.Localization).X).Max();
+ var selectableWidth = longestSelectableWidth + (style.FramePadding.X * 2); // This does not include the label
+ var sortSelectWidth = selectableWidth + sortByTextWidth + style.ItemInnerSpacing.X; // Item spacing between the selectable and the label
+
+ var headerText = Locs.Header_Hint;
+ var headerTextSize = ImGui.CalcTextSize(headerText);
+ ImGui.Text(headerText);
+
+ ImGui.SameLine();
+
+ // Shift down a little to align with the middle of the header text
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (headerTextSize.Y / 4) - 2);
+
+ ImGui.SetCursorPosX(windowSize.X - sortSelectWidth - style.ItemSpacing.X - searchInputWidth);
+ ImGui.SetNextItemWidth(searchInputWidth);
+ ImGui.InputTextWithHint("###XlPluginInstaller_Search", Locs.Header_SearchPlaceholder, ref this.searchText, 100);
+
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(windowSize.X - sortSelectWidth);
+ ImGui.SetNextItemWidth(selectableWidth);
+ if (ImGui.BeginCombo(sortByText, this.filterText, ImGuiComboFlags.NoArrowButton))
+ {
+ foreach (var selectable in sortSelectables)
+ {
+ if (ImGui.Selectable(selectable.Localization))
+ {
+ this.sortKind = selectable.SortKind;
+ this.filterText = selectable.Localization;
+
+ this.ResortPlugins();
+ }
+ }
+
+ ImGui.EndCombo();
+ }
+ }
+
+ private void DrawFooter()
+ {
+ var windowSize = ImGui.GetWindowContentRegionMax();
+ var placeholderButtonSize = GetButtonSize("placeholder");
+
+ ImGui.Separator();
+
+ ImGui.SetCursorPosY(windowSize.Y - placeholderButtonSize.Y);
+
+ this.DrawUpdatePluginsButton();
+
+ ImGui.SameLine();
+ if (ImGui.Button(Locs.FooterButton_Settings))
+ {
+ this.dalamud.DalamudUi.OpenSettings();
+ }
+
+ // If any dev plugins are installed, allow a shortcut for the /xldev menu item
+ if (this.hasDevPlugins)
+ {
+ ImGui.SameLine();
+ if (ImGui.Button(Locs.FooterButton_ScanDevPlugins))
+ {
+ this.dalamud.PluginManager.ScanDevPlugins();
+ }
+ }
+
+ var closeText = Locs.FooterButton_Close;
+ var closeButtonSize = GetButtonSize(closeText);
+
+ ImGui.SameLine(windowSize.X - closeButtonSize.X);
+ if (ImGui.Button(closeText))
+ {
+ this.IsOpen = false;
+ this.dalamud.Configuration.Save();
+ }
+ }
+
+ private void DrawUpdatePluginsButton()
+ {
+ var ready = this.dalamud.PluginManager.PluginsReady && this.dalamud.PluginManager.ReposReady;
+
+ if (!ready || this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress)
+ {
+ ImGuiComponents.DisabledButton(Locs.FooterButton_UpdatePlugins);
+ }
+ else if (this.updateStatus == OperationStatus.Complete)
+ {
+ ImGui.Button(this.updatePluginCount > 0
+ ? Locs.FooterButton_UpdateComplete(this.updatePluginCount)
+ : Locs.FooterButton_NoUpdates);
+ }
+ else
+ {
+ if (ImGui.Button(Locs.FooterButton_UpdatePlugins))
+ {
+ this.updateStatus = OperationStatus.InProgress;
+
+ Task.Run(() => this.dalamud.PluginManager.UpdatePlugins())
+ .ContinueWith(task =>
+ {
+ this.updateStatus = OperationStatus.Complete;
+
+ if (task.IsFaulted)
+ {
+ this.updatePluginCount = 0;
+ this.updatedPlugins = null;
+ this.DisplayErrorContinuation(task, Locs.ErrorModal_UpdaterFatal);
+ }
+ else
+ {
+ this.updatedPlugins = task.Result.Where(res => res.WasUpdated).ToList();
+ this.updatePluginCount = this.updatedPlugins.Count;
+
+ var errorPlugins = task.Result.Where(res => !res.WasUpdated).ToList();
+ var errorPluginCount = errorPlugins.Count;
+
+ if (errorPluginCount > 0)
+ {
+ var errorMessage = this.updatePluginCount > 0
+ ? Locs.ErrorModal_UpdaterFailPartial(this.updatePluginCount, errorPluginCount)
+ : Locs.ErrorModal_UpdaterFail(errorPluginCount);
+
+ var hintInsert = errorPlugins
+ .Aggregate(string.Empty, (current, pluginUpdateStatus) => $"{current}* {pluginUpdateStatus.InternalName}\n")
+ .TrimEnd();
+ errorMessage += Locs.ErrorModal_HintBlame(hintInsert);
+
+ this.DisplayErrorContinuation(task, errorMessage);
+ }
+
+ if (this.updatePluginCount > 0)
+ {
+ this.dalamud.PluginManager.PrintUpdatedPlugins(this.updatedPlugins, Locs.PluginUpdateHeader_Chatbox);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ private void DrawErrorModal()
+ {
+ var modalTitle = Locs.ErrorModal_Title;
+
+ if (ImGui.BeginPopupModal(modalTitle, ref this.errorModalDrawing, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar))
+ {
+ ImGui.Text(this.errorModalMessage);
+ ImGui.Spacing();
+
+ var buttonWidth = 120f;
+ ImGui.SetCursorPosX((ImGui.GetWindowWidth() - buttonWidth) / 2);
+
+ if (ImGui.Button(Locs.ErrorModalButton_Ok, new Vector2(buttonWidth, 40)))
+ {
+ ImGui.CloseCurrentPopup();
+ }
+
+ ImGui.EndPopup();
+ }
+
+ if (this.errorModalOnNextFrame)
+ {
+ ImGui.OpenPopup(modalTitle);
+ this.errorModalOnNextFrame = false;
+ }
+ }
+
+ private void DrawPluginTabBar()
+ {
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5 * ImGuiHelpers.GlobalScale));
+
+ ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, ImGuiHelpers.ScaledVector2(1, 3));
+
+ if (ImGui.BeginTabBar("PluginsTabBar", ImGuiTabBarFlags.NoTooltip))
+ {
+ this.DrawPluginTab(Locs.TabTitle_AvailablePlugins, this.DrawAvailablePluginList);
+ this.DrawPluginTab(Locs.TabTitle_InstalledPlugins, this.DrawInstalledPluginList);
+
+ if (this.hasDevPlugins)
+ {
+ this.DrawPluginTab(Locs.TabTitle_InstalledDevPlugins, this.DrawInstalledDevPluginList);
+ }
+ }
+
+ ImGui.PopStyleVar();
+ }
+
+ private void DrawPluginTab(string title, Action drawPluginList)
+ {
+ if (ImGui.BeginTabItem(title))
+ {
+ ImGui.BeginChild($"Scrolling{title}", ImGuiHelpers.ScaledVector2(0, 384), true, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoBackground);
+
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 5);
+
+ var ready = this.DrawPluginListLoading();
+
+ if (ready)
+ {
+ drawPluginList();
+ }
+
+ ImGui.EndChild();
+
+ ImGui.EndTabItem();
+ }
+ }
+
+ private void DrawAvailablePluginList()
+ {
+ var pluginList = this.pluginListAvailable;
+
+ if (pluginList.Count == 0)
+ {
+ ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_SearchNoCompatible);
+ return;
+ }
+
+ var filteredList = pluginList
+ .Where(rm => !this.IsManifestFiltered(rm))
+ .ToList();
+
+ if (filteredList.Count == 0)
+ {
+ ImGui.TextColored(ImGuiColors.DalamudGrey2, Locs.TabBody_SearchNoMatching);
+ return;
+ }
+
+ var i = 0;
+ foreach (var manifest in filteredList)
+ {
+ var (isInstalled, plugin) = this.IsManifestInstalled(manifest);
+
+ ImGui.PushID($"{manifest.InternalName}{manifest.AssemblyVersion}");
+
+ if (isInstalled)
+ {
+ this.DrawInstalledPlugin(plugin, i++, true);
+ }
+ else
+ {
+ this.DrawAvailablePlugin(manifest, i++);
+ }
+
+ ImGui.PopID();
+ }
+ }
+
+ private void DrawInstalledPluginList()
+ {
+ var pluginList = this.pluginListInstalled;
+
+ if (pluginList.Count == 0)
+ {
+ ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_SearchNoInstalled);
+ return;
+ }
+
+ var filteredList = pluginList
+ .Where(plugin => !this.IsManifestFiltered(plugin.Manifest))
+ .ToList();
+
+ if (filteredList.Count == 0)
+ {
+ ImGui.TextColored(ImGuiColors.DalamudGrey2, Locs.TabBody_SearchNoMatching);
+ return;
+ }
+
+ var i = 0;
+ foreach (var plugin in filteredList)
+ {
+ this.DrawInstalledPlugin(plugin, i++);
+ }
+ }
+
+ private void DrawInstalledDevPluginList()
+ {
+ var pluginList = this.pluginListInstalled
+ .Where(plugin => plugin.IsDev)
+ .ToList();
+
+ if (pluginList.Count == 0)
+ {
+ ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_SearchNoInstalled);
+ return;
+ }
+
+ var filteredList = pluginList
+ .Where(plugin => !this.IsManifestFiltered(plugin.Manifest))
+ .ToList();
+
+ if (filteredList.Count == 0)
+ {
+ ImGui.TextColored(ImGuiColors.DalamudGrey2, Locs.TabBody_SearchNoMatching);
+ return;
+ }
+
+ var i = 0;
+ foreach (var plugin in filteredList)
+ {
+ this.DrawInstalledPlugin(plugin, i++);
+ }
+ }
+
+ private bool DrawPluginListLoading()
+ {
+ var ready = this.dalamud.PluginManager.PluginsReady && this.dalamud.PluginManager.ReposReady;
+
+ if (!ready)
+ {
+ ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_LoadingPlugins);
+ }
+
+ var failedRepos = this.dalamud.PluginManager.Repos
+ .Where(repo => repo.State == PluginRepositoryState.Fail)
+ .ToArray();
+
+ if (failedRepos.Length > 0)
+ {
+ var failText = Locs.TabBody_DownloadFailed;
+ var aggFailText = failedRepos
+ .Select(repo => $"{failText} ({repo.PluginMasterUrl})")
+ .Aggregate((s1, s2) => $"{s1}\n{s2}");
+
+ ImGui.TextColored(ImGuiColors.DalamudRed, aggFailText);
+ }
+
+ return ready;
+ }
+
+ private void DrawAvailablePlugin(RemotePluginManifest manifest, int index)
+ {
+ var useTesting = this.dalamud.PluginManager.UseTesting(manifest);
+
+ // Check for valid versions
+ if ((useTesting && manifest.TestingAssemblyVersion == null) || manifest.AssemblyVersion == null)
+ {
+ // Without a valid version, quit
+ return;
+ }
+
+ // Name
+ var label = manifest.Name;
+
+ // Testing
+ if (useTesting)
+ {
+ label += Locs.PluginTitleMod_TestingVersion;
+ }
+
+ if (ImGui.CollapsingHeader($"{label}###Header{index}{manifest.InternalName}"))
+ {
+ ImGui.Indent();
+
+ // Name
+ ImGui.Text(manifest.Name);
+
+ // Download count
+ var downloadCountText = manifest.DownloadCount > 0
+ ? Locs.PluginBody_AuthorWithDownloadCount(manifest.Author, manifest.DownloadCount)
+ : Locs.PluginBody_AuthorWithDownloadCountUnavailable(manifest.Author);
+
+ ImGui.SameLine();
+ ImGui.TextColored(ImGuiColors.DalamudGrey3, downloadCountText);
+
+ // Installable from
+ if (manifest.SourceRepo.IsThirdParty)
+ {
+ var repoText = Locs.PluginBody_Plugin3rdPartyRepo(manifest.SourceRepo.PluginMasterUrl);
+ ImGui.TextColored(ImGuiColors.DalamudGrey3, repoText);
+ }
+
+ // Description
+ if (!string.IsNullOrWhiteSpace(manifest.Description))
+ {
+ ImGui.TextWrapped(manifest.Description);
+ }
+
+ // Controls
+ var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress;
+
+ var versionString = useTesting
+ ? $"{manifest.TestingAssemblyVersion}"
+ : $"{manifest.AssemblyVersion}";
+
+ if (disabled)
+ {
+ ImGuiComponents.DisabledButton(Locs.PluginButton_InstallVersion(versionString));
+ }
+ else
+ {
+ if (ImGui.Button(Locs.PluginButton_InstallVersion(versionString)))
+ {
+ this.installStatus = OperationStatus.InProgress;
+
+ Task.Run(() => this.dalamud.PluginManager.InstallPlugin(manifest, useTesting))
+ .ContinueWith(task =>
+ {
+ // There is no need to set as Complete for an individual plugin installation
+ this.installStatus = OperationStatus.Idle;
+ this.DisplayErrorContinuation(task, Locs.ErrorModal_InstallFail(manifest.Name));
+ });
+ }
+ }
+
+ this.DrawVisitRepoUrlButton(manifest.RepoUrl);
+
+ ImGui.Unindent();
+ }
+
+ if (ImGui.BeginPopupContextItem("ItemContextMenu"))
+ {
+ if (ImGui.Selectable(Locs.PluginContext_HidePlugin))
+ {
+ Log.Debug($"Adding {manifest.InternalName} to hidden plugins");
+ this.dalamud.Configuration.HiddenPluginInternalName.Add(manifest.InternalName);
+ this.dalamud.Configuration.Save();
+ this.dalamud.PluginManager.RefilterPluginMasters();
+ }
+
+ ImGui.EndPopup();
+ }
+ }
+
+ private void DrawInstalledPlugin(LocalPlugin plugin, int index, bool showInstalled = false)
+ {
+ // Name
+ var label = plugin.Manifest.Name;
+
+ // Testing
+ if (plugin.Manifest.Testing)
+ {
+ label += Locs.PluginTitleMod_TestingVersion;
+ }
+
+ // Freshly installed
+ if (showInstalled)
+ {
+ label += Locs.PluginTitleMod_Installed;
+ }
+
+ // Disabled
+ if (plugin.IsDisabled)
+ {
+ label += Locs.PluginTitleMod_Disabled;
+ }
+
+ // Load error
+ if (plugin.State == PluginState.LoadError)
+ {
+ label += Locs.PluginTitleMod_LoadError;
+ }
+
+ // Unload error
+ if (plugin.State == PluginState.UnloadError)
+ {
+ label += Locs.PluginTitleMod_UnloadError;
+ }
+
+ // Update available
+ if (this.pluginListUpdatable.FirstOrDefault(up => up.InstalledPlugin == plugin) != default)
+ {
+ label += Locs.PluginTitleMod_HasUpdate;
+ }
+
+ // Freshly updated
+ if (this.updatedPlugins != null && !plugin.IsDev)
+ {
+ var update = this.updatedPlugins.FirstOrDefault(update => update.InternalName == plugin.Manifest.InternalName);
+ if (update != default)
+ {
+ if (update.WasUpdated)
+ {
+ label += Locs.PluginTitleMod_Updated;
+ }
+ else
+ {
+ label += Locs.PluginTitleMod_UpdateFailed;
+ }
+ }
+ }
+
+ if (ImGui.CollapsingHeader($"{label}###Header{index}{plugin.Manifest.InternalName}"))
+ {
+ var manifest = plugin.Manifest;
+
+ ImGui.Indent();
+
+ // Name
+ ImGui.Text(manifest.Name);
+
+ // Download count
+ var downloadText = manifest.DownloadCount > 0
+ ? Locs.PluginBody_AuthorWithDownloadCount(manifest.Author, manifest.DownloadCount)
+ : Locs.PluginBody_AuthorWithDownloadCountUnavailable(manifest.Author);
+
+ ImGui.SameLine();
+ ImGui.TextColored(ImGuiColors.DalamudGrey3, downloadText);
+
+ // Installed from
+ if (!string.IsNullOrEmpty(manifest.InstalledFromUrl))
+ {
+ var repoText = Locs.PluginBody_Plugin3rdPartyRepo(manifest.InstalledFromUrl);
+ ImGui.TextColored(ImGuiColors.DalamudGrey3, repoText);
+ }
+
+ // Description
+ if (!string.IsNullOrWhiteSpace(manifest.Description))
+ {
+ ImGui.TextWrapped(manifest.Description);
+ }
+
+ // Available commands (if loaded)
+ if (plugin.IsLoaded)
+ {
+ var commands = this.dalamud.CommandManager.Commands.Where(cInfo => cInfo.Value.ShowInHelp && cInfo.Value.LoaderAssemblyName == plugin.Manifest.InternalName);
+ if (commands.Any())
+ {
+ ImGui.Dummy(ImGuiHelpers.ScaledVector2(10f, 10f));
+ foreach (var command in commands)
+ {
+ ImGui.TextWrapped($"{command.Key} → {command.Value.HelpMessage}");
+ }
+ }
+ }
+
+ // Controls
+ this.DrawPluginControlButton(plugin);
+ this.DrawDevPluginButtons(plugin);
+ this.DrawVisitRepoUrlButton(plugin.Manifest.RepoUrl);
+
+ ImGui.SameLine();
+ ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{plugin.Manifest.AssemblyVersion}");
+
+ if (plugin.IsDev)
+ {
+ ImGui.SameLine();
+ ImGui.TextColored(ImGuiColors.DalamudRed, Locs.PluginBody_DeleteDevPlugin);
+ }
+
+ ImGui.Unindent();
+ }
+ }
+
+ private void DrawPluginControlButton(LocalPlugin plugin)
+ {
+ // Disable everything if the updater is running or another plugin is operating
+ var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress;
+
+ if (plugin.State == PluginState.InProgress)
+ {
+ ImGuiComponents.DisabledButton(Locs.PluginButton_Working);
+ }
+ else if (plugin.State == PluginState.Loaded || plugin.State == PluginState.LoadError)
+ {
+ if (disabled)
+ {
+ ImGuiComponents.DisabledButton(Locs.PluginButton_Disable);
+ }
+ else
+ {
+ if (ImGui.Button(Locs.PluginButton_Disable))
+ {
+ Task.Run(() =>
+ {
+ var unloadTask = Task.Run(() => plugin.Unload())
+ .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_UnloadFail(plugin.Name));
+
+ unloadTask.Wait();
+ if (!unloadTask.Result)
+ return;
+
+ var disableTask = Task.Run(() => plugin.Disable())
+ .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_DisableFail(plugin.Name));
+
+ disableTask.Wait();
+ if (!disableTask.Result)
+ return;
+
+ if (!plugin.IsDev)
+ {
+ this.dalamud.PluginManager.RemovePlugin(plugin);
+ }
+ });
+ }
+ }
+
+ if (plugin.State == PluginState.Loaded)
+ {
+ // Only if the plugin isn't broken.
+ this.DrawOpenPluginSettingsButton(plugin);
+ }
+ }
+ else if (plugin.State == PluginState.Unloaded)
+ {
+ if (disabled)
+ {
+ ImGuiComponents.DisabledButton(Locs.PluginButton_Load);
+ }
+ else
+ {
+ if (ImGui.Button(Locs.PluginButton_Load))
+ {
+ Task.Run(() =>
+ {
+ var enableTask = Task.Run(() => plugin.Enable())
+ .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_EnableFail(plugin.Name));
+
+ enableTask.Wait();
+ if (!enableTask.Result)
+ return;
+
+ var loadTask = Task.Run(() => plugin.Load())
+ .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_LoadFail(plugin.Name));
+
+ loadTask.Wait();
+ if (!loadTask.Result)
+ return;
+ });
+ }
+ }
+ }
+ else if (plugin.State == PluginState.UnloadError)
+ {
+ ImGuiComponents.DisabledButton(FontAwesomeIcon.Frown);
+ }
+ }
+
+ private void DrawOpenPluginSettingsButton(LocalPlugin plugin)
+ {
+ if (plugin.DalamudInterface?.UiBuilder?.HasConfigUi ?? false)
+ {
+ ImGui.SameLine();
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Cog))
+ {
+ try
+ {
+ plugin.DalamudInterface.UiBuilder.OpenConfigUi();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Error during OpenConfigUi: {plugin.Name}");
+ }
+ }
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip(Locs.PluginButtonToolTip_OpenConfiguration);
+ }
+ }
+ }
+
+ private void DrawDevPluginButtons(LocalPlugin localPlugin)
+ {
+ if (localPlugin is LocalDevPlugin plugin)
+ {
+ // https://colorswall.com/palette/2868/
+ var greenColor = new Vector4(0x5C, 0xB8, 0x5C, 0xFF) / 0xFF;
+ var redColor = new Vector4(0xD9, 0x53, 0x4F, 0xFF) / 0xFF;
+
+ // Load on boot
+ ImGui.PushStyleColor(ImGuiCol.Button, plugin.StartOnBoot ? greenColor : redColor);
+ ImGui.PushStyleColor(ImGuiCol.ButtonHovered, plugin.StartOnBoot ? greenColor : redColor);
+
+ ImGui.SameLine();
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.PowerOff))
+ {
+ plugin.StartOnBoot ^= true;
+ this.dalamud.Configuration.Save();
+ }
+
+ ImGui.PopStyleColor(2);
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip(Locs.PluginButtonToolTip_StartOnBoot);
+ }
+
+ // Automatic reload
+ ImGui.PushStyleColor(ImGuiCol.Button, plugin.AutomaticReload ? greenColor : redColor);
+ ImGui.PushStyleColor(ImGuiCol.ButtonHovered, plugin.AutomaticReload ? greenColor : redColor);
+
+ ImGui.SameLine();
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.SyncAlt))
+ {
+ plugin.AutomaticReload ^= true;
+ this.dalamud.Configuration.Save();
+ }
+
+ ImGui.PopStyleColor(2);
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip(Locs.PluginButtonToolTip_AutomaticReloading);
+ }
+
+ // Delete
+ if (plugin.State == PluginState.Unloaded)
+ {
+ ImGui.SameLine();
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.TrashAlt))
+ {
+ try
+ {
+ plugin.DllFile.Delete();
+ this.dalamud.PluginManager.RemovePlugin(plugin);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Plugin installer threw an error during removal of {plugin.Name}");
+
+ this.errorModalMessage = Locs.ErrorModal_DeleteFail(plugin.Name);
+ this.errorModalDrawing = true;
+ this.errorModalOnNextFrame = true;
+ }
+ }
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip(Locs.PluginBody_DeleteDevPlugin);
+ }
+ }
+ }
+ }
+
+ private void DrawVisitRepoUrlButton(string repoUrl)
+ {
+ if (!string.IsNullOrEmpty(repoUrl) && repoUrl.StartsWith("https://"))
+ {
+ ImGui.SameLine();
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Globe))
+ {
+ try
+ {
+ _ = Process.Start(new ProcessStartInfo()
+ {
+ FileName = repoUrl,
+ UseShellExecute = true,
+ });
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Could not open repoUrl: {repoUrl}");
+ }
+ }
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.PluginButtonToolTip_VisitPluginUrl);
+ }
+ }
+
+ private bool IsManifestFiltered(PluginManifest manifest)
+ {
+ var searchString = this.searchText.ToLowerInvariant();
+ var hasSearchString = !string.IsNullOrWhiteSpace(searchString);
+
+ return hasSearchString && !(
+ manifest.Name.ToLowerInvariant().Contains(searchString) ||
+ manifest.Author.Equals(this.searchText, StringComparison.InvariantCultureIgnoreCase) ||
+ (manifest.Tags != null && manifest.Tags.Contains(searchString, StringComparer.InvariantCultureIgnoreCase)));
+ }
+
+ private (bool IsInstalled, LocalPlugin Plugin) IsManifestInstalled(RemotePluginManifest manifest)
+ {
+ var plugin = this.pluginListInstalled.FirstOrDefault(plugin => plugin.Manifest.InternalName == manifest.InternalName);
+ var isInstalled = plugin != default;
+
+ return (isInstalled, plugin);
+ }
+
+ private void OnAvailablePluginsChanged()
+ {
+ // By removing installed plugins only when the available plugin list changes (basically when the window is
+ // opened), plugins that have been newly installed remain in the available plugin list as installed.
+ this.pluginListAvailable = this.dalamud.PluginManager.AvailablePlugins
+ .Where(manifest => !this.IsManifestInstalled(manifest).IsInstalled)
+ .ToList();
+ this.pluginListUpdatable = this.dalamud.PluginManager.UpdatablePlugins.ToList();
+ this.ResortPlugins();
+ }
+
+ private void OnInstalledPluginsChanged()
+ {
+ this.pluginListInstalled = this.dalamud.PluginManager.InstalledPlugins.ToList();
+ this.pluginListUpdatable = this.dalamud.PluginManager.UpdatablePlugins.ToList();
+ this.hasDevPlugins = this.pluginListInstalled.Any(plugin => plugin.IsDev);
+ this.ResortPlugins();
+ }
+
+ private void ResortPlugins()
+ {
+ switch (this.sortKind)
+ {
+ case PluginSortKind.Alphabetical:
+ this.pluginListAvailable.Sort((p1, p2) => p1.Name.CompareTo(p2.Name));
+ this.pluginListInstalled.Sort((p1, p2) => p1.Manifest.Name.CompareTo(p2.Manifest.Name));
+ break;
+ case PluginSortKind.DownloadCount:
+ this.pluginListAvailable.Sort((p1, p2) => p2.DownloadCount.CompareTo(p1.DownloadCount));
+ this.pluginListInstalled.Sort((p1, p2) => p2.Manifest.DownloadCount.CompareTo(p1.Manifest.DownloadCount));
+ break;
+ case PluginSortKind.LastUpdate:
+ this.pluginListAvailable.Sort((p1, p2) => p2.LastUpdate.CompareTo(p1.LastUpdate));
+ this.pluginListInstalled.Sort((p1, p2) => p2.Manifest.LastUpdate.CompareTo(p1.Manifest.LastUpdate));
+ break;
+ default:
+ throw new InvalidEnumArgumentException("Unknown plugin sort type.");
+ }
+ }
+
+ ///
+ /// A continuation task that displays any errors received into the error modal.
+ ///
+ /// The previous task.
+ /// An error message to be displayed.
+ /// A value indicating whether to continue with the next task.
+ private bool DisplayErrorContinuation(Task task, object state)
+ {
+ if (task.IsFaulted)
+ {
+ this.errorModalMessage = state as string;
+
+ foreach (var ex in task.Exception.InnerExceptions)
+ {
+ if (ex is PluginException)
+ {
+ Log.Error(ex, "Plugin installer threw an error");
+#if DEBUG
+ if (!string.IsNullOrEmpty(ex.Message))
+ this.errorModalMessage += $"\n\n{ex.Message}";
+#endif
+ }
+ else
+ {
+ Log.Error(ex, "Plugin installer threw an unexpected error");
+#if DEBUG
+ if (!string.IsNullOrEmpty(ex.Message))
+ this.errorModalMessage += $"\n\n{ex.Message}";
+#endif
+ }
+ }
+
+ this.errorModalDrawing = true;
+ this.errorModalOnNextFrame = true;
+
+ return false;
+ }
+
+ return true;
+ }
+
+ [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Disregard here")]
+ private static class Locs
+ {
+ #region Window Title
+
+ public static string WindowTitle => Loc.Localize("InstallerHeader", "Plugin Installer");
+
+ public static string WindowTitleMod_Testing => Loc.Localize("InstallerHeaderTesting", " (TESTING)");
+
+ #endregion
+
+ #region Header
+
+ public static string Header_Hint => Loc.Localize("InstallerHint", "This window allows you to install and remove in-game plugins.\nThey are made by third-party developers.");
+
+ public static string Header_SearchPlaceholder => Loc.Localize("InstallerSearch", "Search");
+
+ #endregion
+
+ #region SortBy
+
+ public static string SortBy_Alphabetical => Loc.Localize("InstallerAlphabetical", "Alphabetical");
+
+ public static string SortBy_DownloadCounts => Loc.Localize("InstallerDownloadCount", "Download Count");
+
+ public static string SortBy_LastUpdate => Loc.Localize("InstallerLastUpdate", "Last Update");
+
+ public static string SortBy_Label => Loc.Localize("InstallerSortBy", "Sort By");
+
+ #endregion
+
+ #region Tabs
+
+ public static string TabTitle_AvailablePlugins => Loc.Localize("InstallerAvailablePlugins", "Available Plugins");
+
+ public static string TabTitle_InstalledPlugins => Loc.Localize("InstallerInstalledPlugins", "Installed Plugins");
+
+ public static string TabTitle_InstalledDevPlugins => Loc.Localize("InstallerInstalledDevPlugins", "Installed Dev Plugins");
+
+ #endregion
+
+ #region Tab body
+
+ public static string TabBody_LoadingPlugins => Loc.Localize("InstallerLoading", "Loading plugins...");
+
+ public static string TabBody_DownloadFailed => Loc.Localize("InstallerDownloadFailed", "Download failed.");
+
+ #endregion
+
+ #region Search text
+
+ public static string TabBody_SearchNoMatching => Loc.Localize("InstallerNoMatching", "No plugins were found matching your search.");
+
+ public static string TabBody_SearchNoCompatible => Loc.Localize("InstallerNoCompatible", "No compatible plugins were found :( Please restart your game and try again.");
+
+ public static string TabBody_SearchNoInstalled => Loc.Localize("InstallerNoInstalled", "No plugins are currently installed. You can install them from the Available Plugins tab.");
+
+ #endregion
+
+ #region Plugin title text
+
+ public static string PluginTitleMod_Installed => Loc.Localize("InstallerInstalled", " (installed)");
+
+ public static string PluginTitleMod_Disabled => Loc.Localize("InstallerDisabled", " (disabled)");
+
+ public static string PluginTitleMod_Unloaded => Loc.Localize("InstallerUnloaded", " (unloaded)");
+
+ public static string PluginTitleMod_HasUpdate => Loc.Localize("InstallerHasUpdate", " (has update)");
+
+ public static string PluginTitleMod_Updated => Loc.Localize("InstallerUpdated", " (updated)");
+
+ public static string PluginTitleMod_TestingVersion => Loc.Localize("InstallerTestingVersion", " (testing version)");
+
+ public static string PluginTitleMod_UpdateFailed => Loc.Localize("InstallerUpdateFailed", " (update failed)");
+
+ public static string PluginTitleMod_LoadError => Loc.Localize("InstallerLoadError", " (load error)");
+
+ public static string PluginTitleMod_UnloadError => Loc.Localize("InstallerUnloadError", " (unload error)");
+
+ #endregion
+
+ #region Plugin context menu
+
+ public static string PluginContext_HidePlugin => Loc.Localize("InstallerHidePlugin", "Hide from installer");
+
+ #endregion
+
+ #region Plugin body
+
+ public static string PluginBody_AuthorWithDownloadCount(string author, long count) => Loc.Localize("InstallerAuthorWithDownloadCount", " by {0}, {1} downloads").Format(author, count);
+
+ public static string PluginBody_AuthorWithDownloadCountUnavailable(string author) => Loc.Localize("InstallerAuthorWithDownloadCountUnavailable", " by {0}, download count unavailable").Format(author);
+
+ public static string PluginBody_Plugin3rdPartyRepo(string url) => Loc.Localize("InstallerPlugin3rdPartyRepo", "From custom plugin repository {0}").Format(url);
+
+ public static string PluginBody_AvailableDevPlugin => Loc.Localize("InstallerDevPlugin", " This plugin is available in one of your repos, please remove it from the devPlugins folder.");
+
+ public static string PluginBody_DeleteDevPlugin => Loc.Localize("InstallerDeleteDevPlugin ", " To delete this plugin, please remove it from the devPlugins folder.");
+
+ #endregion
+
+ #region Plugin buttons
+
+ public static string PluginButton_InstallVersion(string version) => Loc.Localize("InstallerInstall", "Install v{0}").Format(version);
+
+ public static string PluginButton_Working => Loc.Localize("InstallerWorking", "Working");
+
+ public static string PluginButton_Disable => Loc.Localize("InstallerDisable", "Disable");
+
+ public static string PluginButton_Load => Loc.Localize("InstallerLoad", "Load");
+
+ public static string PluginButton_Unload => Loc.Localize("InstallerUnload", "Unload");
+
+ #endregion
+
+ #region Plugin button tooltips
+
+ public static string PluginButtonToolTip_OpenConfiguration => Loc.Localize("InstallerOpenConfig", "Open Configuration");
+
+ public static string PluginButtonToolTip_StartOnBoot => Loc.Localize("InstallerStartOnBoot", "Start on boot");
+
+ public static string PluginButtonToolTip_AutomaticReloading => Loc.Localize("InstallerAutomaticReloading", "Automatic reloading");
+
+ public static string PluginButtonToolTip_DeletePlugin => Loc.Localize("InstallerDeletePlugin ", "Delete plugin");
+
+ public static string PluginButtonToolTip_VisitPluginUrl => Loc.Localize("InstallerVisitPluginUrl", "Visit plugin URL");
+
+ #endregion
+
+ #region Footer
+
+ public static string FooterButton_UpdatePlugins => Loc.Localize("InstallerUpdatePlugins", "Update plugins");
+
+ public static string FooterButton_InProgress => Loc.Localize("InstallerInProgress", "Install in progress...");
+
+ public static string FooterButton_NoUpdates => Loc.Localize("InstallerNoUpdates", "No updates found!");
+
+ public static string FooterButton_UpdateComplete(int count) => Loc.Localize("InstallerUpdateComplete", "{0} plugins updated!").Format(count);
+
+ public static string FooterButton_Settings => Loc.Localize("InstallerSettings", "Settings");
+
+ public static string FooterButton_ScanDevPlugins => Loc.Localize("InstallerScanDevPlugins", "Scan Dev Plugins");
+
+ public static string FooterButton_Close => Loc.Localize("InstallerClose", "Close");
+
+ #endregion
+
+ #region Error modal
+
+ public static string ErrorModal_Title => Loc.Localize("InstallerError", "Installer Error");
+
+ public static string ErrorModal_InstallFail(string name) => Loc.Localize("InstallerInstallFail", "Failed to install plugin {0}.").Format(name);
+
+ public static string ErrorModal_EnableFail(string name) => Loc.Localize("InstallerEnableFail", "Failed to enable plugin {0}.").Format(name);
+
+ public static string ErrorModal_DisableFail(string name) => Loc.Localize("InstallerDisableFail", "Failed to disable plugin {0}.").Format(name);
+
+ public static string ErrorModal_UnloadFail(string name) => Loc.Localize("InstallerUnloadFail", "Failed to unload plugin {0}.").Format(name);
+
+ public static string ErrorModal_LoadFail(string name) => Loc.Localize("InstallerLoadFail", "Failed to load plugin {0}.").Format(name);
+
+ public static string ErrorModal_DeleteFail(string name) => Loc.Localize("InstallerDeleteFail", "Failed to delete plugin {0}.").Format(name);
+
+ public static string ErrorModal_UpdaterFatal => Loc.Localize("InstallerUpdaterFatal", "Failed to update plugins.");
+
+ public static string ErrorModal_UpdaterFail(int failCount) => Loc.Localize("InstallerUpdaterFail", "Failed to update {0} plugins.").Format(failCount);
+
+ public static string ErrorModal_UpdaterFailPartial(int successCount, int failCount) => Loc.Localize("InstallerUpdaterFailPartial", "Updated {0} plugins, failed to update {1}.").Format(successCount, failCount);
+
+ public static string ErrorModal_HintBlame(string plugins) => Loc.Localize("InstallerErrorPluginInfo", "\n\nThe following plugins caused these issues:\n\n{0}\nYou may try removing these plugins manually and reinstalling them.").Format(plugins);
+
+ // public static string ErrorModal_Hint => Loc.Localize("InstallerErrorHint", "The plugin installer ran into an issue or the plugin is incompatible.\nPlease restart the game and report this error on our discord.");
+
+ #endregion
+
+ #region Plugin Update chatbox
+
+ public static string PluginUpdateHeader_Chatbox => Loc.Localize("DalamudPluginUpdates", "Updates:");
+
+ #endregion
+
+ #region Error modal buttons
+
+ public static string ErrorModalButton_Ok => Loc.Localize("OK", "OK");
+
+ #endregion
+ }
+ }
+}
diff --git a/Dalamud/Interface/DalamudPluginStatWindow.cs b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs
similarity index 75%
rename from Dalamud/Interface/DalamudPluginStatWindow.cs
rename to Dalamud/Interface/Internal/Windows/PluginStatWindow.cs
index fca07d31b..cdb60d9b0 100644
--- a/Dalamud/Interface/DalamudPluginStatWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs
@@ -3,29 +3,30 @@ using System.Linq;
using System.Reflection;
using Dalamud.Game.Internal;
-using Dalamud.Hooking;
+using Dalamud.Hooking.Internal;
using Dalamud.Interface.Windowing;
-using Dalamud.Plugin;
+using Dalamud.Plugin.Internal;
+using Dalamud.Plugin.Internal.Types;
using ImGuiNET;
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal.Windows
{
///
/// This window displays plugin statistics for troubleshooting.
///
- internal class DalamudPluginStatWindow : Window
+ internal class PluginStatWindow : Window
{
private readonly PluginManager pluginManager;
private bool showDalamudHooks;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
- /// The PluginManager instance.
- public DalamudPluginStatWindow(PluginManager pluginManager)
+ /// The Dalamud instance.
+ public PluginStatWindow(Dalamud dalamud)
: base("Plugin Statistics###DalamudPluginStatWindow")
{
- this.pluginManager = pluginManager;
+ this.pluginManager = dalamud.PluginManager;
}
///
@@ -47,11 +48,11 @@ namespace Dalamud.Interface
ImGui.SameLine();
if (ImGui.Button("Reset"))
{
- foreach (var a in this.pluginManager.Plugins)
+ foreach (var plugin in this.pluginManager.InstalledPlugins)
{
- a.PluginInterface.UiBuilder.LastDrawTime = -1;
- a.PluginInterface.UiBuilder.MaxDrawTime = -1;
- a.PluginInterface.UiBuilder.DrawTimeHistory.Clear();
+ plugin.DalamudInterface.UiBuilder.LastDrawTime = -1;
+ plugin.DalamudInterface.UiBuilder.MaxDrawTime = -1;
+ plugin.DalamudInterface.UiBuilder.DrawTimeHistory.Clear();
}
}
@@ -60,26 +61,35 @@ namespace Dalamud.Interface
ImGui.SetColumnWidth(1, 100f);
ImGui.SetColumnWidth(2, 100f);
ImGui.SetColumnWidth(3, 100f);
+
ImGui.Text("Plugin");
ImGui.NextColumn();
+
ImGui.Text("Last");
ImGui.NextColumn();
+
ImGui.Text("Longest");
ImGui.NextColumn();
+
ImGui.Text("Average");
ImGui.NextColumn();
+
ImGui.Separator();
- foreach (var a in this.pluginManager.Plugins)
+
+ foreach (var plugin in this.pluginManager.InstalledPlugins.Where(plugin => plugin.State == PluginState.Loaded))
{
- ImGui.Text(a.Definition.Name);
+ ImGui.Text(plugin.Manifest.Name);
ImGui.NextColumn();
- ImGui.Text($"{a.PluginInterface.UiBuilder.LastDrawTime / 10000f:F4}ms");
+
+ ImGui.Text($"{plugin.DalamudInterface.UiBuilder.LastDrawTime / 10000f:F4}ms");
ImGui.NextColumn();
- ImGui.Text($"{a.PluginInterface.UiBuilder.MaxDrawTime / 10000f:F4}ms");
+
+ ImGui.Text($"{plugin.DalamudInterface.UiBuilder.MaxDrawTime / 10000f:F4}ms");
ImGui.NextColumn();
- if (a.PluginInterface.UiBuilder.DrawTimeHistory.Count > 0)
+
+ if (plugin.DalamudInterface.UiBuilder.DrawTimeHistory.Count > 0)
{
- ImGui.Text($"{a.PluginInterface.UiBuilder.DrawTimeHistory.Average() / 10000f:F4}ms");
+ ImGui.Text($"{plugin.DalamudInterface.UiBuilder.DrawTimeHistory.Average() / 10000f:F4}ms");
}
else
{
@@ -113,33 +123,46 @@ namespace Dalamud.Interface
}
ImGui.Columns(4);
+
ImGui.SetColumnWidth(0, ImGui.GetWindowContentRegionWidth() - 300);
ImGui.SetColumnWidth(1, 100f);
ImGui.SetColumnWidth(2, 100f);
ImGui.SetColumnWidth(3, 100f);
+
ImGui.Text("Method");
ImGui.NextColumn();
+
ImGui.Text("Last");
ImGui.NextColumn();
+
ImGui.Text("Longest");
ImGui.NextColumn();
+
ImGui.Text("Average");
ImGui.NextColumn();
+
ImGui.Separator();
ImGui.Separator();
foreach (var handlerHistory in Framework.StatsHistory)
{
- if (handlerHistory.Value.Count == 0) continue;
+ if (handlerHistory.Value.Count == 0)
+ continue;
+
ImGui.SameLine();
+
ImGui.Text($"{handlerHistory.Key}");
ImGui.NextColumn();
+
ImGui.Text($"{handlerHistory.Value.Last():F4}ms");
ImGui.NextColumn();
+
ImGui.Text($"{handlerHistory.Value.Max():F4}ms");
ImGui.NextColumn();
+
ImGui.Text($"{handlerHistory.Value.Average():F4}ms");
ImGui.NextColumn();
+
ImGui.Separator();
}
@@ -152,27 +175,35 @@ namespace Dalamud.Interface
if (ImGui.BeginTabItem("Hooks"))
{
ImGui.Columns(3);
+
ImGui.SetColumnWidth(0, ImGui.GetWindowContentRegionWidth() - 280);
ImGui.SetColumnWidth(1, 180f);
ImGui.SetColumnWidth(2, 100f);
+
ImGui.Text("Detour Method");
ImGui.SameLine();
+
ImGui.Text(" ");
ImGui.SameLine();
+
ImGui.Checkbox("Show Dalamud Hooks ###showDalamudHooksCheckbox", ref this.showDalamudHooks);
ImGui.NextColumn();
+
ImGui.Text("Address");
ImGui.NextColumn();
+
ImGui.Text("Status");
ImGui.NextColumn();
+
ImGui.Separator();
ImGui.Separator();
- foreach (var trackedHook in HookInfo.TrackedHooks)
+ foreach (var trackedHook in HookManager.TrackedHooks)
{
try
{
- if (!this.showDalamudHooks && trackedHook.Assembly == Assembly.GetExecutingAssembly()) continue;
+ if (!this.showDalamudHooks && trackedHook.Assembly == Assembly.GetExecutingAssembly())
+ continue;
ImGui.Text($"{trackedHook.Delegate.Target} :: {trackedHook.Delegate.Method.Name}");
ImGui.TextDisabled(trackedHook.Assembly.FullName);
@@ -180,13 +211,19 @@ namespace Dalamud.Interface
if (!trackedHook.Hook.IsDisposed)
{
ImGui.Text($"{trackedHook.Hook.Address.ToInt64():X}");
- if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"{trackedHook.Hook.Address.ToInt64():X}");
+ if (ImGui.IsItemClicked())
+ {
+ ImGui.SetClipboardText($"{trackedHook.Hook.Address.ToInt64():X}");
+ }
var processMemoryOffset = trackedHook.InProcessMemory;
if (processMemoryOffset.HasValue)
{
ImGui.Text($"ffxiv_dx11.exe + {processMemoryOffset:X}");
- if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"ffxiv_dx11.exe+{processMemoryOffset:X}");
+ if (ImGui.IsItemClicked())
+ {
+ ImGui.SetClipboardText($"ffxiv_dx11.exe+{processMemoryOffset:X}");
+ }
}
}
@@ -218,7 +255,7 @@ namespace Dalamud.Interface
if (ImGui.IsWindowAppearing())
{
- HookInfo.TrackedHooks.RemoveAll(h => h.Hook.IsDisposed);
+ HookManager.TrackedHooks.RemoveAll(h => h.Hook.IsDisposed);
}
ImGui.EndTabBar();
diff --git a/Dalamud/Interface/Scratchpad/ScratchpadWindow.cs b/Dalamud/Interface/Internal/Windows/ScratchpadWindow.cs
similarity index 95%
rename from Dalamud/Interface/Scratchpad/ScratchpadWindow.cs
rename to Dalamud/Interface/Internal/Windows/ScratchpadWindow.cs
index 91324117c..198ac0cf6 100644
--- a/Dalamud/Interface/Scratchpad/ScratchpadWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/ScratchpadWindow.cs
@@ -4,11 +4,12 @@ using System.Linq;
using System.Numerics;
using Dalamud.Interface.Colors;
+using Dalamud.Interface.Internal.Scratchpad;
using Dalamud.Interface.Windowing;
using ImGuiNET;
using Serilog;
-namespace Dalamud.Interface.Scratchpad
+namespace Dalamud.Interface.Internal.Windows
{
///
/// This class facilitates interacting with the ScratchPad window.
@@ -16,8 +17,8 @@ namespace Dalamud.Interface.Scratchpad
internal class ScratchpadWindow : Window, IDisposable
{
private readonly Dalamud dalamud;
- private List documents = new();
- private ScratchFileWatcher watcher = new();
+ private readonly List documents = new();
+ private readonly ScratchFileWatcher watcher = new();
private string pathInput = string.Empty;
///
@@ -138,7 +139,7 @@ namespace Dalamud.Interface.Scratchpad
if (ImGui.Button("Toggle Log"))
{
- this.dalamud.DalamudUi.ToggleLog();
+ this.dalamud.DalamudUi.ToggleLogWindow();
}
ImGui.SameLine();
diff --git a/Dalamud/Interface/DalamudSettingsWindow.cs b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs
similarity index 89%
rename from Dalamud/Interface/DalamudSettingsWindow.cs
rename to Dalamud/Interface/Internal/Windows/SettingsWindow.cs
index 544e14b67..4e6812328 100644
--- a/Dalamud/Interface/DalamudSettingsWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs
@@ -9,24 +9,24 @@ using CheapLoc;
using Dalamud.Configuration;
using Dalamud.Game.Text;
using Dalamud.Interface.Colors;
+using Dalamud.Interface.Components;
using Dalamud.Interface.Windowing;
using ImGuiNET;
-using Serilog;
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal.Windows
{
///
/// The window that allows for general configuration of Dalamud itself.
///
- internal class DalamudSettingsWindow : Window
+ internal class SettingsWindow : Window
{
private const float MinScale = 0.3f;
private const float MaxScale = 2.0f;
private readonly Dalamud dalamud;
- private string[] languages;
- private string[] locLanguages;
+ private readonly string[] languages;
+ private readonly string[] locLanguages;
private int langIndex;
private Vector4 hintTextColor = ImGuiColors.DalamudGrey;
@@ -44,7 +44,7 @@ namespace Dalamud.Interface
private bool doDocking;
private bool doViewport;
private bool doGamepad;
- private List thirdRepoList;
+ private List thirdRepoList;
private bool printPluginsWelcomeMsg;
private bool autoUpdatePlugins;
@@ -60,10 +60,10 @@ namespace Dalamud.Interface
#endregion
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The Dalamud Instance.
- public DalamudSettingsWindow(Dalamud dalamud)
+ public SettingsWindow(Dalamud dalamud)
: base(Loc.Localize("DalamudSettingsHeader", "Dalamud Settings") + "###XlSettings2", ImGuiWindowFlags.NoCollapse)
{
this.dalamud = dalamud;
@@ -142,31 +142,21 @@ namespace Dalamud.Interface
///
public override void OnOpen()
{
- base.OnOpen();
-
- Log.Information("OnOpen start");
-
- Log.Information("OnOpen end");
}
///
public override void OnClose()
{
- base.OnClose();
-
- Log.Information("OnClose start");
-
ImGui.GetIO().FontGlobalScale = this.dalamud.Configuration.GlobalUiScale;
- this.thirdRepoList = this.dalamud.Configuration.ThirdRepoList.Select(x => x.Clone()).ToList();
- Log.Information("OnClose end");
+ this.thirdRepoList = this.dalamud.Configuration.ThirdRepoList.Select(x => x.Clone()).ToList();
}
///
public override void Draw()
{
var windowSize = ImGui.GetWindowSize();
- ImGui.BeginChild("scrolling", new Vector2(windowSize.X - 5 - (5 * ImGui.GetIO().FontGlobalScale), windowSize.Y - 35 - (35 * ImGui.GetIO().FontGlobalScale)), false, ImGuiWindowFlags.HorizontalScrollbar);
+ ImGui.BeginChild("scrolling", new Vector2(windowSize.X - 5 - (5 * ImGuiHelpers.GlobalScale), windowSize.Y - 35 - (35 * ImGuiHelpers.GlobalScale)), false, ImGuiWindowFlags.HorizontalScrollbar);
if (ImGui.BeginTabBar("SetTabBar"))
{
@@ -176,7 +166,7 @@ namespace Dalamud.Interface
ImGui.Combo("##XlLangCombo", ref this.langIndex, this.locLanguages, this.locLanguages.Length);
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsLanguageHint", "Select the language Dalamud will be displayed in."));
- ImGui.Dummy(new Vector2(5f, 5f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(5);
ImGui.Text(Loc.Localize("DalamudSettingsChannel", "General Chat Channel"));
if (ImGui.BeginCombo("##XlChatTypeCombo", this.dalamudMessagesChatType.ToString()))
@@ -194,7 +184,7 @@ namespace Dalamud.Interface
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsChannelHint", "Select the chat channel that is to be used for general Dalamud messages."));
- ImGui.Dummy(new Vector2(5f, 5f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(5);
ImGui.Checkbox(Loc.Localize("DalamudSettingsFlash", "Flash FFXIV window on duty pop"), ref this.doCfTaskBarFlash);
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsFlashHint", "Flash the FFXIV window in your task bar when a duty is ready."));
@@ -231,7 +221,7 @@ namespace Dalamud.Interface
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsGlobalUiScaleHint", "Scale all XIVLauncher UI elements - useful for 4K displays."));
- ImGui.Dummy(new Vector2(10f, 16f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(10, 16);
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingToggleUiHideOptOutNote", "Plugins may independently opt out of the settings below."));
@@ -244,7 +234,7 @@ namespace Dalamud.Interface
ImGui.Checkbox(Loc.Localize("DalamudSettingToggleUiHideDuringGpose", "Hide plugin UI while gpose is active"), ref this.doToggleUiHideDuringGpose);
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingToggleUiHideDuringGposeHint", "Hide any open windows by plugins while gpose is active."));
- ImGui.Dummy(new Vector2(10f, 16f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(10, 16);
ImGui.Checkbox(Loc.Localize("DalamudSettingToggleViewports", "Enable multi-monitor windows"), ref this.doViewport);
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingToggleViewportsHint", "This will allow you move plugin windows onto other monitors.\nWill only work in Borderless Window or Windowed mode."));
@@ -264,27 +254,31 @@ namespace Dalamud.Interface
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsPluginTestHint", "Receive testing prereleases for plugins."));
ImGui.TextColored(this.warnTextColor, Loc.Localize("DalamudSettingsPluginTestWarning", "Testing plugins may not have been vetted before being published. Please only enable this if you are aware of the risks."));
- ImGui.Dummy(new Vector2(12f, 12f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(12);
if (ImGui.Button(Loc.Localize("DalamudSettingsClearHidden", "Clear hidden plugins")))
+ {
this.dalamud.Configuration.HiddenPluginInternalName.Clear();
+ this.dalamud.PluginManager.RefilterPluginMasters();
+ }
+
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsClearHiddenHint", "Restore plugins you have previously hidden from the plugin installer."));
- ImGui.Dummy(new Vector2(12f, 12f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(12);
- ImGui.Dummy(new Vector2(12f, 12f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(12);
ImGui.Text(Loc.Localize("DalamudSettingsCustomRepo", "Custom Plugin Repositories"));
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingCustomRepoHint", "Add custom plugin repositories."));
ImGui.TextColored(this.warnTextColor, Loc.Localize("DalamudSettingCustomRepoWarning", "We cannot take any responsibility for third-party plugins and repositories.\nTake care when installing third-party plugins from untrusted sources."));
- ImGui.Dummy(new Vector2(5f, 5f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(5);
ImGui.Columns(4);
- ImGui.SetColumnWidth(0, 18 + (5 * ImGui.GetIO().FontGlobalScale));
- ImGui.SetColumnWidth(1, ImGui.GetWindowWidth() - (18 + 16 + 14) - ((5 + 45 + 26) * ImGui.GetIO().FontGlobalScale));
- ImGui.SetColumnWidth(2, 16 + (45 * ImGui.GetIO().FontGlobalScale));
- ImGui.SetColumnWidth(3, 14 + (26 * ImGui.GetIO().FontGlobalScale));
+ ImGui.SetColumnWidth(0, 18 + (5 * ImGuiHelpers.GlobalScale));
+ ImGui.SetColumnWidth(1, ImGui.GetWindowWidth() - (18 + 16 + 14) - ((5 + 45 + 26) * ImGuiHelpers.GlobalScale));
+ ImGui.SetColumnWidth(2, 16 + (45 * ImGuiHelpers.GlobalScale));
+ ImGui.SetColumnWidth(3, 14 + (26 * ImGuiHelpers.GlobalScale));
ImGui.Separator();
@@ -307,7 +301,7 @@ namespace Dalamud.Interface
ImGui.NextColumn();
ImGui.Separator();
- ThirdRepoSetting toRemove = null;
+ ThirdPartyRepoSettings toRemove = null;
var repoNumber = 1;
foreach (var thirdRepoSetting in this.thirdRepoList)
@@ -323,17 +317,15 @@ namespace Dalamud.Interface
ImGui.TextWrapped(thirdRepoSetting.Url);
ImGui.NextColumn();
- ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (ImGui.GetColumnWidth() / 2) - 7 - (12 * ImGui.GetIO().FontGlobalScale));
+ ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (ImGui.GetColumnWidth() / 2) - 7 - (12 * ImGuiHelpers.GlobalScale));
ImGui.Checkbox("##thirdRepoCheck", ref isEnabled);
ImGui.NextColumn();
- ImGui.PushFont(InterfaceManager.IconFont);
- if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString()))
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash))
{
toRemove = thirdRepoSetting;
}
- ImGui.PopFont();
ImGui.NextColumn();
ImGui.Separator();
@@ -353,9 +345,9 @@ namespace Dalamud.Interface
ImGui.SetNextItemWidth(-1);
ImGui.InputText("##thirdRepoUrlInput", ref this.thirdRepoTempUrl, 300);
ImGui.NextColumn();
+ // Enabled button
ImGui.NextColumn();
- ImGui.PushFont(InterfaceManager.IconFont);
- if (!string.IsNullOrEmpty(this.thirdRepoTempUrl) && ImGui.Button(FontAwesomeIcon.Plus.ToIconString()))
+ if (!string.IsNullOrEmpty(this.thirdRepoTempUrl) && ImGuiComponents.IconButton(FontAwesomeIcon.Plus))
{
if (this.thirdRepoList.Any(r => string.Equals(r.Url, this.thirdRepoTempUrl, StringComparison.InvariantCultureIgnoreCase)))
{
@@ -364,7 +356,7 @@ namespace Dalamud.Interface
}
else
{
- this.thirdRepoList.Add(new ThirdRepoSetting
+ this.thirdRepoList.Add(new ThirdPartyRepoSettings
{
Url = this.thirdRepoTempUrl,
IsEnabled = true,
@@ -374,7 +366,6 @@ namespace Dalamud.Interface
}
}
- ImGui.PopFont();
ImGui.Columns(1);
ImGui.EndTabItem();
@@ -456,7 +447,7 @@ namespace Dalamud.Interface
this.dalamud.Configuration.Save();
- this.dalamud.PluginRepository.ReloadPluginMasterAsync();
+ this.dalamud.PluginManager.ReloadPluginMasters();
}
}
}
diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs
index 81067678b..59e4f75c4 100644
--- a/Dalamud/Interface/UiBuilder.cs
+++ b/Dalamud/Interface/UiBuilder.cs
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using Dalamud.Game.ClientState;
+using Dalamud.Interface.Internal;
using ImGuiNET;
using ImGuiScene;
using Serilog;
@@ -13,11 +15,11 @@ namespace Dalamud.Interface
/// This class represents the Dalamud UI that is drawn on top of the game.
/// It can be used to draw custom windows and overlays.
///
- public class UiBuilder : IDisposable
+ public sealed class UiBuilder : IDisposable
{
- private readonly string namespaceName;
private readonly Dalamud dalamud;
- private readonly System.Diagnostics.Stopwatch stopwatch;
+ private readonly Stopwatch stopwatch;
+ private readonly string namespaceName;
private bool hasErrorWindow;
@@ -29,11 +31,11 @@ namespace Dalamud.Interface
/// The plugin namespace.
internal UiBuilder(Dalamud dalamud, string namespaceName)
{
+ this.dalamud = dalamud;
+ this.stopwatch = new Stopwatch();
this.namespaceName = namespaceName;
- this.dalamud = dalamud;
this.dalamud.InterfaceManager.OnDraw += this.OnDraw;
- this.stopwatch = new System.Diagnostics.Stopwatch();
}
///
@@ -217,8 +219,7 @@ namespace Dalamud.Interface
if (this.hasErrorWindow && ImGui.Begin($"{this.namespaceName} Error", ref this.hasErrorWindow, ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize))
{
- ImGui.Text(
- $"The plugin {this.namespaceName} ran into an error.\nContact the plugin developer for support.\n\nPlease try restarting your game.");
+ ImGui.Text($"The plugin {this.namespaceName} ran into an error.\nContact the plugin developer for support.\n\nPlease try restarting your game.");
ImGui.Spacing();
if (ImGui.Button("OK"))
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index bd3c55bd5..6a15ef9d5 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -143,9 +143,6 @@ namespace Dalamud.Interface.Windowing
///
internal void DrawInternal()
{
- // if (WindowName.Contains("Credits"))
- // Log.Information($"Draw: {IsOpen} {this.internalIsOpen} {this.internalLastIsOpen}");
-
if (!this.IsOpen)
{
if (this.internalIsOpen != this.internalLastIsOpen)
diff --git a/Dalamud/Localization.cs b/Dalamud/Localization.cs
index 3469dcb64..5c7255a95 100644
--- a/Dalamud/Localization.cs
+++ b/Dalamud/Localization.cs
@@ -112,6 +112,7 @@ namespace Dalamud
}
this.OnLocalizationChanged?.Invoke(langCode);
+
try
{
Loc.Setup(this.ReadLocData(langCode), this.assembly);
@@ -135,9 +136,10 @@ namespace Dalamud
{
if (this.useEmbedded)
{
- var resourceStream =
- this.assembly.GetManifestResourceStream($"{this.locResourceDirectory}{this.locResourcePrefix}{langCode}.json");
- if (resourceStream == null) return null;
+ var resourceStream = this.assembly.GetManifestResourceStream($"{this.locResourceDirectory}{this.locResourcePrefix}{langCode}.json");
+ if (resourceStream == null)
+ return null;
+
using var reader = new StreamReader(resourceStream);
return reader.ReadToEnd();
}
diff --git a/Dalamud/Memory/Exceptions/MemoryAllocationException.cs b/Dalamud/Memory/Exceptions/MemoryAllocationException.cs
new file mode 100644
index 000000000..e289c8782
--- /dev/null
+++ b/Dalamud/Memory/Exceptions/MemoryAllocationException.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Dalamud.Memory.Exceptions
+{
+ ///
+ /// An exception thrown when VirtualAlloc fails.
+ ///
+ public class MemoryAllocationException : MemoryException
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MemoryAllocationException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public MemoryAllocationException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception.
+ public MemoryAllocationException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The object that holds the serialized data about the exception being thrown.
+ /// The object that contains contextual information about the source or destination.
+ protected MemoryAllocationException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
diff --git a/Dalamud/Memory/Exceptions/MemoryException.cs b/Dalamud/Memory/Exceptions/MemoryException.cs
new file mode 100644
index 000000000..810e76404
--- /dev/null
+++ b/Dalamud/Memory/Exceptions/MemoryException.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Dalamud.Memory.Exceptions
+{
+ ///
+ /// The base exception when thrown from Dalamud.Memory.
+ ///
+ public abstract class MemoryException : Exception
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MemoryException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public MemoryException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception.
+ public MemoryException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The object that holds the serialized data about the exception being thrown.
+ /// The object that contains contextual information about the source or destination.
+ protected MemoryException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
diff --git a/Dalamud/Memory/Exceptions/MemoryPermissionException.cs b/Dalamud/Memory/Exceptions/MemoryPermissionException.cs
new file mode 100644
index 000000000..e94ccc0ce
--- /dev/null
+++ b/Dalamud/Memory/Exceptions/MemoryPermissionException.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Dalamud.Memory.Exceptions
+{
+ ///
+ /// An exception thrown when VirtualProtect fails.
+ ///
+ public class MemoryPermissionException : MemoryException
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MemoryPermissionException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public MemoryPermissionException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception.
+ public MemoryPermissionException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The object that holds the serialized data about the exception being thrown.
+ /// The object that contains contextual information about the source or destination.
+ protected MemoryPermissionException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
diff --git a/Dalamud/Memory/Exceptions/MemoryReadException.cs b/Dalamud/Memory/Exceptions/MemoryReadException.cs
new file mode 100644
index 000000000..7f3f4b1f2
--- /dev/null
+++ b/Dalamud/Memory/Exceptions/MemoryReadException.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Dalamud.Memory.Exceptions
+{
+ ///
+ /// An exception thrown when ReadProcessMemory fails.
+ ///
+ public class MemoryReadException : MemoryException
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MemoryReadException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public MemoryReadException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception.
+ public MemoryReadException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The object that holds the serialized data about the exception being thrown.
+ /// The object that contains contextual information about the source or destination.
+ protected MemoryReadException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
diff --git a/Dalamud/Memory/Exceptions/MemoryWriteException.cs b/Dalamud/Memory/Exceptions/MemoryWriteException.cs
new file mode 100644
index 000000000..5aadcee53
--- /dev/null
+++ b/Dalamud/Memory/Exceptions/MemoryWriteException.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Dalamud.Memory.Exceptions
+{
+ ///
+ /// An exception thrown when WriteProcessMemory fails.
+ ///
+ public class MemoryWriteException : MemoryException
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MemoryWriteException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public MemoryWriteException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception.
+ public MemoryWriteException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The object that holds the serialized data about the exception being thrown.
+ /// The object that contains contextual information about the source or destination.
+ protected MemoryWriteException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
diff --git a/Dalamud/Memory/MemoryHelper.cs b/Dalamud/Memory/MemoryHelper.cs
new file mode 100644
index 000000000..f47f48263
--- /dev/null
+++ b/Dalamud/Memory/MemoryHelper.cs
@@ -0,0 +1,610 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using Dalamud.Game.Text.SeStringHandling;
+using Dalamud.Memory.Exceptions;
+
+using static Dalamud.NativeFunctions;
+
+// Heavily inspired from Reloaded (https://github.com/Reloaded-Project/Reloaded.Memory)
+
+namespace Dalamud.Memory
+{
+ ///
+ /// A simple class that provides read/write access to arbitrary memory.
+ ///
+ public static unsafe class MemoryHelper
+ {
+ private static SeStringManager seStringManager;
+ private static IntPtr handle;
+
+ #region Read
+
+ ///
+ /// Reads a generic type from a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// The read in struct.
+ public static T Read(IntPtr memoryAddress) where T : unmanaged
+ => Read(memoryAddress, false);
+
+ ///
+ /// Reads a generic type from a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// Set this to true to enable struct marshalling.
+ /// The read in struct.
+ public static T Read(IntPtr memoryAddress, bool marshal)
+ {
+ return marshal
+ ? Marshal.PtrToStructure(memoryAddress)
+ : Unsafe.Read((void*)memoryAddress);
+ }
+
+ ///
+ /// Reads a generic type from a specified memory address.
+ ///
+ /// The memory address to read from.
+ /// The amount of bytes to read starting from the memoryAddress.
+ /// The read in struct.
+ public static byte[] ReadRaw(IntPtr memoryAddress, int length)
+ {
+ var value = new byte[length];
+ Marshal.Copy(memoryAddress, value, 0, value.Length);
+ return value;
+ }
+
+ ///
+ /// Reads a generic type array from a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// The amount of array items to read.
+ /// The read in struct array.
+ public static T[] Read(IntPtr memoryAddress, int arrayLength) where T : unmanaged
+ => Read(memoryAddress, arrayLength, false);
+
+ ///
+ /// Reads a generic type array from a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// The amount of array items to read.
+ /// Set this to true to enable struct marshalling.
+ /// The read in struct array.
+ public static T[] Read(IntPtr memoryAddress, int arrayLength, bool marshal)
+ {
+ var structSize = SizeOf();
+ var value = new T[arrayLength];
+
+ for (var i = 0; i < arrayLength; i++)
+ {
+ var address = memoryAddress + (structSize * i);
+ Read(address, out T result, marshal);
+ value[i] = result;
+ }
+
+ return value;
+ }
+
+ #endregion
+
+ #region Read(out)
+
+ ///
+ /// Reads a generic type from a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// Local variable to receive the read in struct.
+ public static void Read(IntPtr memoryAddress, out T value) where T : unmanaged
+ => value = Read(memoryAddress);
+
+ ///
+ /// Reads a generic type from a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// Local variable to receive the read in struct.
+ /// Set this to true to enable struct marshalling.
+ public static void Read(IntPtr memoryAddress, out T value, bool marshal)
+ => value = Read(memoryAddress, marshal);
+
+ ///
+ /// Reads raw data from a specified memory address.
+ ///
+ /// The memory address to read from.
+ /// The amount of bytes to read starting from the memoryAddress.
+ /// Local variable to receive the read in bytes.
+ public static void ReadRaw(IntPtr memoryAddress, int length, out byte[] value)
+ => value = ReadRaw(memoryAddress, length);
+
+ ///
+ /// Reads a generic type array from a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// The amount of array items to read.
+ /// The read in struct array.
+ public static void Read(IntPtr memoryAddress, int arrayLength, out T[] value) where T : unmanaged
+ => value = Read(memoryAddress, arrayLength);
+
+ ///
+ /// Reads a generic type array from a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// The amount of array items to read.
+ /// Set this to true to enable struct marshalling.
+ /// The read in struct array.
+ public static void Read(IntPtr memoryAddress, int arrayLength, bool marshal, out T[] value)
+ => value = Read(memoryAddress, arrayLength, marshal);
+
+ #endregion
+
+ #region ReadString
+
+ ///
+ /// Read a UTF-8 encoded string from a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to decode or the applicable helper method.
+ ///
+ /// The memory address to read from.
+ /// The read in string.
+ public static string ReadString(IntPtr memoryAddress)
+ => ReadString(memoryAddress, 256);
+
+ ///
+ /// Read a UTF-8 encoded string from a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to decode or the applicable helper method.
+ ///
+ /// The memory address to read from.
+ /// The maximum length of the string.
+ /// The read in string.
+ public static string ReadString(IntPtr memoryAddress, int maxLength)
+ => ReadString(memoryAddress, Encoding.UTF8, maxLength);
+
+ ///
+ /// Read a string with the given encoding from a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to decode or the applicable helper method.
+ ///
+ /// The memory address to read from.
+ /// The encoding to use to decode the string.
+ /// The read in string.
+ public static string ReadString(IntPtr memoryAddress, Encoding encoding)
+ => ReadString(memoryAddress, encoding, 256);
+
+ ///
+ /// Read a string with the given encoding from a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to decode or the applicable helper method.
+ ///
+ /// The memory address to read from.
+ /// The encoding to use to decode the string.
+ /// The maximum length of the string.
+ /// The read in string.
+ public static string ReadString(IntPtr memoryAddress, Encoding encoding, int maxLength)
+ {
+ if (maxLength <= 0)
+ return string.Empty;
+
+ ReadRaw(memoryAddress, maxLength, out var buffer);
+
+ var data = encoding.GetString(buffer);
+ var eosPos = data.IndexOf('\0');
+ return eosPos >= 0 ? data.Substring(0, eosPos) : data;
+ }
+
+ ///
+ /// Read an SeString from a specified memory address.
+ ///
+ /// The memory address to read from.
+ /// The read in string.
+ public static SeString ReadSeString(IntPtr memoryAddress)
+ => ReadSeString(memoryAddress, 256);
+
+ ///
+ /// Read an SeString from a specified memory address.
+ ///
+ /// The memory address to read from.
+ /// The maximum length of the string.
+ /// The read in string.
+ public static SeString ReadSeString(IntPtr memoryAddress, int maxLength)
+ {
+ ReadRaw(memoryAddress, maxLength, out var buffer);
+ return seStringManager.Parse(buffer);
+ }
+
+ #endregion
+
+ #region ReadString(out)
+
+ ///
+ /// Read a UTF-8 encoded string from a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to decode or the applicable helper method.
+ ///
+ /// The memory address to read from.
+ /// The read in string.
+ public static void ReadString(IntPtr memoryAddress, out string value)
+ => value = ReadString(memoryAddress);
+
+ ///
+ /// Read a UTF-8 encoded string from a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to decode or the applicable helper method.
+ ///
+ /// The memory address to read from.
+ /// The read in string.
+ /// The maximum length of the string.
+ public static void ReadString(IntPtr memoryAddress, out string value, int maxLength)
+ => value = ReadString(memoryAddress, maxLength);
+
+ ///
+ /// Read a string with the given encoding from a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to decode or the applicable helper method.
+ ///
+ /// The memory address to read from.
+ /// The encoding to use to decode the string.
+ /// The read in string.
+ public static void ReadString(IntPtr memoryAddress, Encoding encoding, out string value)
+ => value = ReadString(memoryAddress, encoding);
+
+ ///
+ /// Read a string with the given encoding from a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to decode or the applicable helper method.
+ ///
+ /// The memory address to read from.
+ /// The encoding to use to decode the string.
+ /// The maximum length of the string.
+ /// The read in string.
+ public static void ReadString(IntPtr memoryAddress, Encoding encoding, int maxLength, out string value)
+ => value = ReadString(memoryAddress, encoding, maxLength);
+
+ ///
+ /// Read an SeString from a specified memory address.
+ ///
+ /// The memory address to read from.
+ /// The read in SeString.
+ public static void ReadSeString(IntPtr memoryAddress, out SeString value)
+ => value = ReadSeString(memoryAddress);
+
+ ///
+ /// Read an SeString from a specified memory address.
+ ///
+ /// The memory address to read from.
+ /// The maximum length of the string.
+ /// The read in SeString.
+ public static void ReadSeString(IntPtr memoryAddress, int maxLength, out SeString value)
+ => value = ReadSeString(memoryAddress, maxLength);
+
+ #endregion
+
+ #region Write
+
+ ///
+ /// Writes a generic type to a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// The item to write to the address.
+ public static void Write(IntPtr memoryAddress, T item) where T : unmanaged
+ => Write(memoryAddress, item);
+
+ ///
+ /// Writes a generic type to a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// The item to write to the address.
+ /// Set this to true to enable struct marshalling.
+ public static void Write(IntPtr memoryAddress, T item, bool marshal)
+ {
+ if (marshal)
+ Marshal.StructureToPtr(item, memoryAddress, false);
+ else
+ Unsafe.Write((void*)memoryAddress, item);
+ }
+
+ ///
+ /// Writes raw data to a specified memory address.
+ ///
+ /// The memory address to read from.
+ /// The bytes to write to memoryAddress.
+ public static void WriteRaw(IntPtr memoryAddress, byte[] data)
+ {
+ Marshal.Copy(data, 0, memoryAddress, data.Length);
+ }
+
+ ///
+ /// Writes a generic type array to a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to write to.
+ /// The array of items to write to the address.
+ public static void Write(IntPtr memoryAddress, T[] items) where T : unmanaged
+ => Write(memoryAddress, items, false);
+
+ ///
+ /// Writes a generic type array to a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to write to.
+ /// The array of items to write to the address.
+ /// Set this to true to enable struct marshalling.
+ public static void Write(IntPtr memoryAddress, T[] items, bool marshal)
+ {
+ var structSize = SizeOf(marshal);
+
+ for (var i = 0; i < items.Length; i++)
+ {
+ var address = memoryAddress + (structSize * i);
+ Write(address, items[i], marshal);
+ }
+ }
+
+ #endregion
+
+ #region WriteString
+
+ ///
+ /// Write a UTF-8 encoded string to a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to encode or the applicable helper method.
+ ///
+ /// The memory address to write to.
+ /// The string to write.
+ public static void WriteString(IntPtr memoryAddress, string value)
+ => WriteString(memoryAddress, value, Encoding.UTF8);
+
+ ///
+ /// Write a string with the given encoding to a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to encode or the applicable helper method.
+ ///
+ /// The memory address to write to.
+ /// The string to write.
+ /// The encoding to use.
+ public static void WriteString(IntPtr memoryAddress, string value, Encoding encoding)
+ {
+ if (string.IsNullOrEmpty(value))
+ return;
+
+ var bytes = encoding.GetBytes(value + '\0');
+
+ WriteRaw(memoryAddress, bytes);
+ }
+
+ ///
+ /// Write an SeString to a specified memory address.
+ ///
+ /// The memory address to write to.
+ /// The SeString to write.
+ public static void WriteSeString(IntPtr memoryAddress, SeString value)
+ {
+ if (value is null)
+ return;
+
+ WriteRaw(memoryAddress, value.Encode());
+ }
+
+ #endregion
+
+ #region ApiWrappers
+
+ ///
+ /// Allocates fixed size of memory inside the target memory source via Windows API calls.
+ /// Returns the address of newly allocated memory.
+ ///
+ /// Amount of bytes to be allocated.
+ /// Address to the newly allocated memory.
+ public static IntPtr Allocate(int length)
+ {
+ var address = VirtualAlloc(
+ IntPtr.Zero,
+ (UIntPtr)length,
+ AllocationType.Commit | AllocationType.Reserve,
+ MemoryProtection.ExecuteReadWrite);
+
+ if (address == IntPtr.Zero)
+ throw new MemoryAllocationException($"Unable to allocate {length} bytes.");
+
+ return address;
+ }
+
+ ///
+ /// Allocates fixed size of memory inside the target memory source via Windows API calls.
+ /// Returns the address of newly allocated memory.
+ ///
+ /// Amount of bytes to be allocated.
+ /// Address to the newly allocated memory.
+ public static void Allocate(int length, out IntPtr memoryAddress)
+ => memoryAddress = Allocate(length);
+
+ ///
+ /// Frees memory previously allocated with Allocate via Windows API calls.
+ ///
+ /// The address of the memory to free.
+ /// True if the operation is successful.
+ public static bool Free(IntPtr memoryAddress)
+ {
+ return VirtualFree(memoryAddress, UIntPtr.Zero, AllocationType.Release);
+ }
+
+ ///
+ /// Changes the page permissions for a specified combination of address and length via Windows API calls.
+ ///
+ /// The memory address for which to change page permissions for.
+ /// The region size for which to change permissions for.
+ /// The new permissions to set.
+ /// The old page permissions.
+ public static MemoryProtection ChangePermission(IntPtr memoryAddress, int length, MemoryProtection newPermissions)
+ {
+ var result = VirtualProtect(memoryAddress, (UIntPtr)length, newPermissions, out var oldPermissions);
+
+ if (!result)
+ throw new MemoryPermissionException($"Unable to change permissions at 0x{memoryAddress.ToInt64():X} of length {length} and permission {newPermissions} (result={result})");
+
+ var last = Marshal.GetLastWin32Error();
+ if (last > 0)
+ throw new MemoryPermissionException($"Unable to change permissions at 0x{memoryAddress.ToInt64():X} of length {length} and permission {newPermissions} (error={last})");
+
+ return oldPermissions;
+ }
+
+ ///
+ /// Changes the page permissions for a specified combination of address and length via Windows API calls.
+ ///
+ /// The memory address for which to change page permissions for.
+ /// The region size for which to change permissions for.
+ /// The new permissions to set.
+ /// The old page permissions.
+ public static void ChangePermission(IntPtr memoryAddress, int length, MemoryProtection newPermissions, out MemoryProtection oldPermissions)
+ => oldPermissions = ChangePermission(memoryAddress, length, newPermissions);
+
+ ///
+ /// Changes the page permissions for a specified combination of address and element from which to deduce size via Windows API calls.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address for which to change page permissions for.
+ /// The struct element from which the region size to change permissions for will be calculated.
+ /// The new permissions to set.
+ /// Set to true to calculate the size of the struct after marshalling instead of before.
+ /// The old page permissions.
+ public static MemoryProtection ChangePermission(IntPtr memoryAddress, ref T baseElement, MemoryProtection newPermissions, bool marshal)
+ => ChangePermission(memoryAddress, SizeOf(marshal), newPermissions);
+
+ ///
+ /// Reads raw data from a specified memory address via Windows API calls.
+ /// This is noticably slower than Unsafe or Marshal.
+ ///
+ /// The memory address to read from.
+ /// The amount of bytes to read starting from the memoryAddress.
+ /// The read in bytes.
+ public static byte[] ReadProcessMemory(IntPtr memoryAddress, int length)
+ {
+ var value = new byte[length];
+ ReadProcessMemory(memoryAddress, ref value);
+ return value;
+ }
+
+ ///
+ /// Reads raw data from a specified memory address via Windows API calls.
+ /// This is noticably slower than Unsafe or Marshal.
+ ///
+ /// The memory address to read from.
+ /// The amount of bytes to read starting from the memoryAddress.
+ /// The read in bytes.
+ public static void ReadProcessMemory(IntPtr memoryAddress, int length, out byte[] value)
+ => value = ReadProcessMemory(memoryAddress, length);
+
+ ///
+ /// Reads raw data from a specified memory address via Windows API calls.
+ /// This is noticably slower than Unsafe or Marshal.
+ ///
+ /// The memory address to read from.
+ /// The read in bytes.
+ public static void ReadProcessMemory(IntPtr memoryAddress, ref byte[] value)
+ {
+ var length = value.Length;
+ var result = NativeFunctions.ReadProcessMemory(handle, memoryAddress, value, length, out _);
+
+ if (!result)
+ throw new MemoryReadException($"Unable to read memory at 0x{memoryAddress.ToInt64():X} of length {length} (result={result})");
+
+ var last = Marshal.GetLastWin32Error();
+ if (last > 0)
+ throw new MemoryReadException($"Unable to read memory at 0x{memoryAddress.ToInt64():X} of length {length} (error={last})");
+ }
+
+ ///
+ /// Writes raw data to a specified memory address via Windows API calls.
+ /// This is noticably slower than Unsafe or Marshal.
+ ///
+ /// The memory address to write to.
+ /// The bytes to write to memoryAddress.
+ public static void WriteProcessMemory(IntPtr memoryAddress, byte[] data)
+ {
+ var length = data.Length;
+ var result = NativeFunctions.WriteProcessMemory(handle, memoryAddress, data, length, out _);
+
+ if (!result)
+ throw new MemoryWriteException($"Unable to write memory at 0x{memoryAddress.ToInt64():X} of length {length} (result={result})");
+
+ var last = Marshal.GetLastWin32Error();
+ if (last > 0)
+ throw new MemoryWriteException($"Unable to write memory at 0x{memoryAddress.ToInt64():X} of length {length} (error={last})");
+ }
+
+ #endregion
+
+ #region Sizing
+
+ ///
+ /// Returns the size of a specific primitive or struct type.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The size of the primitive or struct.
+ public static int SizeOf()
+ => SizeOf(false);
+
+ ///
+ /// Returns the size of a specific primitive or struct type.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// If set to true; will return the size of an element after marshalling.
+ /// The size of the primitive or struct.
+ public static int SizeOf(bool marshal)
+ => marshal ? Marshal.SizeOf() : Unsafe.SizeOf();
+
+ ///
+ /// Returns the size of a specific primitive or struct type.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The number of array elements present.
+ /// The size of the primitive or struct array.
+ public static int SizeOf(int elementCount) where T : unmanaged
+ => SizeOf() * elementCount;
+
+ ///
+ /// Returns the size of a specific primitive or struct type.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The number of array elements present.
+ /// If set to true; will return the size of an element after marshalling.
+ /// The size of the primitive or struct array.
+ public static int SizeOf(int elementCount, bool marshal)
+ => SizeOf(marshal) * elementCount;
+
+ #endregion
+
+ ///
+ /// Initialize with static access to Dalamud.
+ ///
+ /// The Dalamud instance.
+ internal static void Initialize(Dalamud dalamud)
+ {
+ seStringManager = dalamud.SeStringManager;
+ handle = Process.GetCurrentProcess().Handle;
+ }
+ }
+}
diff --git a/Dalamud/Memory/MemoryProtection.cs b/Dalamud/Memory/MemoryProtection.cs
new file mode 100644
index 000000000..289c5024d
--- /dev/null
+++ b/Dalamud/Memory/MemoryProtection.cs
@@ -0,0 +1,117 @@
+using System;
+
+// This is a copy from NativeFunctions.MemoryProtection
+
+namespace Dalamud.Memory
+{
+ ///
+ /// PAGE_* from memoryapi.
+ ///
+ [Flags]
+ public enum MemoryProtection
+ {
+ // Copied from NativeFunctions to expose to the user.
+
+ ///
+ /// Enables execute access to the committed region of pages. An attempt to write to the committed region results
+ /// in an access violation. This flag is not supported by the CreateFileMapping function.
+ ///
+ Execute = 0x10,
+
+ ///
+ /// Enables execute or read-only access to the committed region of pages. An attempt to write to the committed region
+ /// results in an access violation.
+ ///
+ ExecuteRead = 0x20,
+
+ ///
+ /// Enables execute, read-only, or read/write access to the committed region of pages.
+ ///
+ ExecuteReadWrite = 0x40,
+
+ ///
+ /// Enables execute, read-only, or copy-on-write access to a mapped view of a file mapping object. An attempt to
+ /// write to a committed copy-on-write page results in a private copy of the page being made for the process. The
+ /// private page is marked as PAGE_EXECUTE_READWRITE, and the change is written to the new page. This flag is not
+ /// supported by the VirtualAlloc or VirtualAllocEx functions.
+ ///
+ ExecuteWriteCopy = 0x80,
+
+ ///
+ /// Disables all access to the committed region of pages. An attempt to read from, write to, or execute the committed
+ /// region results in an access violation. This flag is not supported by the CreateFileMapping function.
+ ///
+ NoAccess = 0x01,
+
+ ///
+ /// Enables read-only access to the committed region of pages. An attempt to write to the committed region results
+ /// in an access violation. If Data Execution Prevention is enabled, an attempt to execute code in the committed
+ /// region results in an access violation.
+ ///
+ ReadOnly = 0x02,
+
+ ///
+ /// Enables read-only or read/write access to the committed region of pages. If Data Execution Prevention is enabled,
+ /// attempting to execute code in the committed region results in an access violation.
+ ///
+ ReadWrite = 0x04,
+
+ ///
+ /// Enables read-only or copy-on-write access to a mapped view of a file mapping object. An attempt to write to
+ /// a committed copy-on-write page results in a private copy of the page being made for the process. The private
+ /// page is marked as PAGE_READWRITE, and the change is written to the new page. If Data Execution Prevention is
+ /// enabled, attempting to execute code in the committed region results in an access violation. This flag is not
+ /// supported by the VirtualAlloc or VirtualAllocEx functions.
+ ///
+ WriteCopy = 0x08,
+
+ ///
+ /// Sets all locations in the pages as invalid targets for CFG. Used along with any execute page protection like
+ /// PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY. Any indirect call to locations
+ /// in those pages will fail CFG checks and the process will be terminated. The default behavior for executable
+ /// pages allocated is to be marked valid call targets for CFG. This flag is not supported by the VirtualProtect
+ /// or CreateFileMapping functions.
+ ///
+ TargetsInvalid = 0x40000000,
+
+ ///
+ /// Pages in the region will not have their CFG information updated while the protection changes for VirtualProtect.
+ /// For example, if the pages in the region was allocated using PAGE_TARGETS_INVALID, then the invalid information
+ /// will be maintained while the page protection changes. This flag is only valid when the protection changes to
+ /// an executable type like PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY.
+ /// The default behavior for VirtualProtect protection change to executable is to mark all locations as valid call
+ /// targets for CFG.
+ ///
+ TargetsNoUpdate = TargetsInvalid,
+
+ ///
+ /// Pages in the region become guard pages. Any attempt to access a guard page causes the system to raise a
+ /// STATUS_GUARD_PAGE_VIOLATION exception and turn off the guard page status. Guard pages thus act as a one-time
+ /// access alarm. For more information, see Creating Guard Pages. When an access attempt leads the system to turn
+ /// off guard page status, the underlying page protection takes over. If a guard page exception occurs during a
+ /// system service, the service typically returns a failure status indicator. This value cannot be used with
+ /// PAGE_NOACCESS. This flag is not supported by the CreateFileMapping function.
+ ///
+ Guard = 0x100,
+
+ ///
+ /// Sets all pages to be non-cachable. Applications should not use this attribute except when explicitly required
+ /// for a device. Using the interlocked functions with memory that is mapped with SEC_NOCACHE can result in an
+ /// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_NOCACHE flag cannot be used with the PAGE_GUARD, PAGE_NOACCESS,
+ /// or PAGE_WRITECOMBINE flags. The PAGE_NOCACHE flag can be used only when allocating private memory with the
+ /// VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable non-cached memory access for shared
+ /// memory, specify the SEC_NOCACHE flag when calling the CreateFileMapping function.
+ ///
+ NoCache = 0x200,
+
+ ///
+ /// Sets all pages to be write-combined. Applications should not use this attribute except when explicitly required
+ /// for a device. Using the interlocked functions with memory that is mapped as write-combined can result in an
+ /// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_WRITECOMBINE flag cannot be specified with the PAGE_NOACCESS,
+ /// PAGE_GUARD, and PAGE_NOCACHE flags. The PAGE_WRITECOMBINE flag can be used only when allocating private memory
+ /// with the VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable write-combined memory access
+ /// for shared memory, specify the SEC_WRITECOMBINE flag when calling the CreateFileMapping function.
+ ///
+ WriteCombine = 0x400,
+ }
+}
diff --git a/Dalamud/ModuleLog.cs b/Dalamud/ModuleLog.cs
new file mode 100644
index 000000000..b78430a92
--- /dev/null
+++ b/Dalamud/ModuleLog.cs
@@ -0,0 +1,124 @@
+using System;
+
+namespace Dalamud
+{
+ ///
+ /// Class offering various methods to allow for logging in Dalamud modules.
+ ///
+ internal class ModuleLog
+ {
+ private readonly string moduleName;
+
+ ///
+ /// Initializes a new instance of the class.
+ /// This class can be used to prefix logging messages with a Dalamud module name prefix. For example, "[PLUGINR] ...".
+ ///
+ /// The module name.
+ public ModuleLog(string moduleName)
+ {
+ this.moduleName = moduleName;
+ }
+
+ ///
+ /// Log a templated verbose message to the in-game debug log.
+ ///
+ /// The message template.
+ /// Values to log.
+ public void Verbose(string messageTemplate, params object[] values)
+ => Serilog.Log.Verbose($"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated verbose message to the in-game debug log.
+ ///
+ /// The exception that caused the error.
+ /// The message template.
+ /// Values to log.
+ public void Verbose(Exception exception, string messageTemplate, params object[] values)
+ => Serilog.Log.Verbose(exception, $"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated debug message to the in-game debug log.
+ ///
+ /// The message template.
+ /// Values to log.
+ public void Debug(string messageTemplate, params object[] values)
+ => Serilog.Log.Debug($"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated debug message to the in-game debug log.
+ ///
+ /// The exception that caused the error.
+ /// The message template.
+ /// Values to log.
+ public void Debug(Exception exception, string messageTemplate, params object[] values)
+ => Serilog.Log.Debug(exception, $"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated information message to the in-game debug log.
+ ///
+ /// The message template.
+ /// Values to log.
+ public void Information(string messageTemplate, params object[] values)
+ => Serilog.Log.Information($"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated information message to the in-game debug log.
+ ///
+ /// The exception that caused the error.
+ /// The message template.
+ /// Values to log.
+ public void Information(Exception exception, string messageTemplate, params object[] values)
+ => Serilog.Log.Information(exception, $"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated warning message to the in-game debug log.
+ ///
+ /// The message template.
+ /// Values to log.
+ public void Warning(string messageTemplate, params object[] values)
+ => Serilog.Log.Warning($"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated warning message to the in-game debug log.
+ ///
+ /// The exception that caused the error.
+ /// The message template.
+ /// Values to log.
+ public void Warning(Exception exception, string messageTemplate, params object[] values)
+ => Serilog.Log.Warning(exception, $"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated error message to the in-game debug log.
+ ///
+ /// The message template.
+ /// Values to log.
+ public void Error(string messageTemplate, params object[] values)
+ => Serilog.Log.Error($"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated error message to the in-game debug log.
+ ///
+ /// The exception that caused the error.
+ /// The message template.
+ /// Values to log.
+ public void Error(Exception exception, string messageTemplate, params object[] values)
+ => Serilog.Log.Error(exception, $"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated fatal message to the in-game debug log.
+ ///
+ /// The message template.
+ /// Values to log.
+ public void Fatal(string messageTemplate, params object[] values)
+ => Serilog.Log.Fatal($"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated fatal message to the in-game debug log.
+ ///
+ /// The exception that caused the error.
+ /// The message template.
+ /// Values to log.
+ public void Fatal(Exception exception, string messageTemplate, params object[] values)
+ => Serilog.Log.Fatal(exception, $"[{this.moduleName}] {messageTemplate}", values);
+ }
+}
diff --git a/Dalamud/NativeFunctions.cs b/Dalamud/NativeFunctions.cs
index d41d64985..0a4fbc5bd 100644
--- a/Dalamud/NativeFunctions.cs
+++ b/Dalamud/NativeFunctions.cs
@@ -1,5 +1,4 @@
using System;
-using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net.Sockets;
using System.Runtime.InteropServices;
@@ -54,6 +53,11 @@ namespace Dalamud
///
public enum MessageBoxType : uint
{
+ ///
+ /// The default value for any of the various subtypes.
+ ///
+ DefaultValue = 0x0,
+
// To indicate the buttons displayed in the message box, specify one of the following values.
///
@@ -76,7 +80,7 @@ namespace Dalamud
///
/// The message box contains one push button: OK. This is the default.
///
- Ok = 0x0,
+ Ok = DefaultValue,
///
/// The message box contains two push buttons: OK and Cancel.
@@ -108,7 +112,7 @@ namespace Dalamud
///
/// An exclamation-point icon appears in the message box.
///
- IconWarning = 0x30,
+ IconWarning = IconExclamation,
///
/// An icon consisting of a lowercase letter i in a circle appears in the message box.
@@ -118,7 +122,7 @@ namespace Dalamud
///
/// An icon consisting of a lowercase letter i in a circle appears in the message box.
///
- IconAsterisk = 0x40,
+ IconAsterisk = IconInformation,
///
/// A question-mark icon appears in the message box.
@@ -138,12 +142,12 @@ namespace Dalamud
///
/// A stop-sign icon appears in the message box.
///
- IconError = 0x10,
+ IconError = IconStop,
///
/// A stop-sign icon appears in the message box.
///
- IconHand = 0x10,
+ IconHand = IconStop,
// To indicate the default button, specify one of the following values.
@@ -151,7 +155,7 @@ namespace Dalamud
/// The first button is the default button.
/// MB_DEFBUTTON1 is the default unless MB_DEFBUTTON2, MB_DEFBUTTON3, or MB_DEFBUTTON4 is specified.
///
- DefButton1 = 0x0,
+ DefButton1 = DefaultValue,
///
/// The second button is the default button.
@@ -177,7 +181,7 @@ namespace Dalamud
/// of the parent of the message box are automatically disabled, but pop-up windows are not. MB_APPLMODAL is the
/// default if neither MB_SYSTEMMODAL nor MB_TASKMODAL is specified.
///
- ApplModal = 0x0,
+ ApplModal = DefaultValue,
///
/// Same as MB_APPLMODAL except that the message box has the WS_EX_TOPMOST style.
@@ -241,14 +245,13 @@ namespace Dalamud
{
var activatedHandle = GetForegroundWindow();
if (activatedHandle == IntPtr.Zero)
- {
return false; // No window is currently activated
- }
- var procId = Process.GetCurrentProcess().Id;
- GetWindowThreadProcessId(activatedHandle, out var activeProcId);
+ _ = GetWindowThreadProcessId(activatedHandle, out var activeProcId);
+ if (Marshal.GetLastWin32Error() != 0)
+ return false;
- return activeProcId == procId;
+ return activeProcId == Environment.ProcessId;
}
///
@@ -320,8 +323,8 @@ namespace Dalamud
/// If the function fails, the return value is zero.To get extended error information, call GetLastError. If the function
/// succeeds, the return value is one of the ID* enum values.
///
- [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
- public static extern int MessageBox(IntPtr hWnd, string text, string caption, MessageBoxType type);
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern int MessageBoxW(IntPtr hWnd, string text, string caption, MessageBoxType type);
///
/// See https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-flashwinfo.
@@ -333,28 +336,28 @@ namespace Dalamud
///
/// The size of the structure, in bytes.
///
- public uint cbSize;
+ public uint Size;
///
/// A handle to the window to be flashed. The window can be either opened or minimized.
///
- public IntPtr hwnd;
+ public IntPtr Hwnd;
///
/// The flash status. This parameter can be one or more of the FlashWindow enum values.
///
- public FlashWindow dwFlags;
+ public FlashWindow Flags;
///
/// The number of times to flash the window.
///
- public uint uCount;
+ public uint Count;
///
/// The rate at which the window is to be flashed, in milliseconds. If dwTimeout is zero, the function uses the
/// default cursor blink rate.
///
- public uint dwTimeout;
+ public uint Timeout;
}
}
@@ -363,6 +366,118 @@ namespace Dalamud
///
internal static partial class NativeFunctions
{
+ ///
+ /// MEM_* from memoryapi.
+ ///
+ [Flags]
+ public enum AllocationType
+ {
+ ///
+ /// To coalesce two adjacent placeholders, specify MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS. When you coalesce
+ /// placeholders, lpAddress and dwSize must exactly match those of the placeholder.
+ ///
+ CoalescePlaceholders = 0x1,
+
+ ///
+ /// Frees an allocation back to a placeholder (after you've replaced a placeholder with a private allocation using
+ /// VirtualAlloc2 or Virtual2AllocFromApp). To split a placeholder into two placeholders, specify
+ /// MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER.
+ ///
+ PreservePlaceholder = 0x2,
+
+ ///
+ /// Allocates memory charges (from the overall size of memory and the paging files on disk) for the specified reserved
+ /// memory pages. The function also guarantees that when the caller later initially accesses the memory, the contents
+ /// will be zero. Actual physical pages are not allocated unless/until the virtual addresses are actually accessed.
+ /// To reserve and commit pages in one step, call VirtualAllocEx with MEM_COMMIT | MEM_RESERVE. Attempting to commit
+ /// a specific address range by specifying MEM_COMMIT without MEM_RESERVE and a non-NULL lpAddress fails unless the
+ /// entire range has already been reserved. The resulting error code is ERROR_INVALID_ADDRESS. An attempt to commit
+ /// a page that is already committed does not cause the function to fail. This means that you can commit pages without
+ /// first determining the current commitment state of each page. If lpAddress specifies an address within an enclave,
+ /// flAllocationType must be MEM_COMMIT.
+ ///
+ Commit = 0x1000,
+
+ ///
+ /// Reserves a range of the process's virtual address space without allocating any actual physical storage in memory
+ /// or in the paging file on disk. You commit reserved pages by calling VirtualAllocEx again with MEM_COMMIT. To
+ /// reserve and commit pages in one step, call VirtualAllocEx with MEM_COMMIT | MEM_RESERVE. Other memory allocation
+ /// functions, such as malloc and LocalAlloc, cannot use reserved memory until it has been released.
+ ///
+ Reserve = 0x2000,
+
+ ///
+ /// Decommits the specified region of committed pages. After the operation, the pages are in the reserved state.
+ /// The function does not fail if you attempt to decommit an uncommitted page. This means that you can decommit
+ /// a range of pages without first determining the current commitment state. The MEM_DECOMMIT value is not supported
+ /// when the lpAddress parameter provides the base address for an enclave.
+ ///
+ Decommit = 0x4000,
+
+ ///
+ /// Releases the specified region of pages, or placeholder (for a placeholder, the address space is released and
+ /// available for other allocations). After this operation, the pages are in the free state. If you specify this
+ /// value, dwSize must be 0 (zero), and lpAddress must point to the base address returned by the VirtualAlloc function
+ /// when the region is reserved. The function fails if either of these conditions is not met. If any pages in the
+ /// region are committed currently, the function first decommits, and then releases them. The function does not
+ /// fail if you attempt to release pages that are in different states, some reserved and some committed. This means
+ /// that you can release a range of pages without first determining the current commitment state.
+ ///
+ Release = 0x8000,
+
+ ///
+ /// Indicates that data in the memory range specified by lpAddress and dwSize is no longer of interest. The pages
+ /// should not be read from or written to the paging file. However, the memory block will be used again later, so
+ /// it should not be decommitted. This value cannot be used with any other value. Using this value does not guarantee
+ /// that the range operated on with MEM_RESET will contain zeros. If you want the range to contain zeros, decommit
+ /// the memory and then recommit it. When you use MEM_RESET, the VirtualAllocEx function ignores the value of fProtect.
+ /// However, you must still set fProtect to a valid protection value, such as PAGE_NOACCESS. VirtualAllocEx returns
+ /// an error if you use MEM_RESET and the range of memory is mapped to a file. A shared view is only acceptable
+ /// if it is mapped to a paging file.
+ ///
+ Reset = 0x80000,
+
+ ///
+ /// MEM_RESET_UNDO should only be called on an address range to which MEM_RESET was successfully applied earlier.
+ /// It indicates that the data in the specified memory range specified by lpAddress and dwSize is of interest to
+ /// the caller and attempts to reverse the effects of MEM_RESET. If the function succeeds, that means all data in
+ /// the specified address range is intact. If the function fails, at least some of the data in the address range
+ /// has been replaced with zeroes. This value cannot be used with any other value. If MEM_RESET_UNDO is called on
+ /// an address range which was not MEM_RESET earlier, the behavior is undefined. When you specify MEM_RESET, the
+ /// VirtualAllocEx function ignores the value of flProtect. However, you must still set flProtect to a valid
+ /// protection value, such as PAGE_NOACCESS.
+ ///
+ ResetUndo = 0x1000000,
+
+ ///
+ /// Reserves an address range that can be used to map Address Windowing Extensions (AWE) pages. This value must
+ /// be used with MEM_RESERVE and no other values.
+ ///
+ Physical = 0x400000,
+
+ ///
+ /// Allocates memory at the highest possible address. This can be slower than regular allocations, especially when
+ /// there are many allocations.
+ ///
+ TopDown = 0x100000,
+
+ ///
+ /// Causes the system to track pages that are written to in the allocated region. If you specify this value, you
+ /// must also specify MEM_RESERVE. To retrieve the addresses of the pages that have been written to since the region
+ /// was allocated or the write-tracking state was reset, call the GetWriteWatch function. To reset the write-tracking
+ /// state, call GetWriteWatch or ResetWriteWatch. The write-tracking feature remains enabled for the memory region
+ /// until the region is freed.
+ ///
+ WriteWatch = 0x200000,
+
+ ///
+ /// Allocates memory using large page support. The size and alignment must be a multiple of the large-page minimum.
+ /// To obtain this value, use the GetLargePageMinimum function. If you specify this value, you must also specify
+ /// MEM_RESERVE and MEM_COMMIT.
+ ///
+ LargePages = 0x20000000,
+ }
+
///
/// SEM_* from errhandlingapi.
///
@@ -402,20 +517,113 @@ namespace Dalamud
}
///
- /// See https://docs.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-debugactiveprocess.
- /// Enables a debugger to attach to an active process and debug it.
+ /// PAGE_* from memoryapi.
///
- ///
- /// The identifier for the process to be debugged. The debugger is granted debugging access to the process as if it
- /// created the process with the DEBUG_ONLY_THIS_PROCESS flag. For more information, see the Remarks section of this
- /// topic.
- ///
- ///
- /// If the function succeeds, the return value is nonzero. If the function fails, the return value is 0 (zero). To get
- /// extended error information, call GetLastError.
- ///
- [DllImport("kernel32.dll", SetLastError = true)]
- public static extern bool DebugActiveProcess(uint dwProcessId);
+ [Flags]
+ public enum MemoryProtection
+ {
+ ///
+ /// Enables execute access to the committed region of pages. An attempt to write to the committed region results
+ /// in an access violation. This flag is not supported by the CreateFileMapping function.
+ ///
+ Execute = 0x10,
+
+ ///
+ /// Enables execute or read-only access to the committed region of pages. An attempt to write to the committed region
+ /// results in an access violation.
+ ///
+ ExecuteRead = 0x20,
+
+ ///
+ /// Enables execute, read-only, or read/write access to the committed region of pages.
+ ///
+ ExecuteReadWrite = 0x40,
+
+ ///
+ /// Enables execute, read-only, or copy-on-write access to a mapped view of a file mapping object. An attempt to
+ /// write to a committed copy-on-write page results in a private copy of the page being made for the process. The
+ /// private page is marked as PAGE_EXECUTE_READWRITE, and the change is written to the new page. This flag is not
+ /// supported by the VirtualAlloc or VirtualAllocEx functions.
+ ///
+ ExecuteWriteCopy = 0x80,
+
+ ///
+ /// Disables all access to the committed region of pages. An attempt to read from, write to, or execute the committed
+ /// region results in an access violation. This flag is not supported by the CreateFileMapping function.
+ ///
+ NoAccess = 0x01,
+
+ ///
+ /// Enables read-only access to the committed region of pages. An attempt to write to the committed region results
+ /// in an access violation. If Data Execution Prevention is enabled, an attempt to execute code in the committed
+ /// region results in an access violation.
+ ///
+ ReadOnly = 0x02,
+
+ ///
+ /// Enables read-only or read/write access to the committed region of pages. If Data Execution Prevention is enabled,
+ /// attempting to execute code in the committed region results in an access violation.
+ ///
+ ReadWrite = 0x04,
+
+ ///
+ /// Enables read-only or copy-on-write access to a mapped view of a file mapping object. An attempt to write to
+ /// a committed copy-on-write page results in a private copy of the page being made for the process. The private
+ /// page is marked as PAGE_READWRITE, and the change is written to the new page. If Data Execution Prevention is
+ /// enabled, attempting to execute code in the committed region results in an access violation. This flag is not
+ /// supported by the VirtualAlloc or VirtualAllocEx functions.
+ ///
+ WriteCopy = 0x08,
+
+ ///
+ /// Sets all locations in the pages as invalid targets for CFG. Used along with any execute page protection like
+ /// PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY. Any indirect call to locations
+ /// in those pages will fail CFG checks and the process will be terminated. The default behavior for executable
+ /// pages allocated is to be marked valid call targets for CFG. This flag is not supported by the VirtualProtect
+ /// or CreateFileMapping functions.
+ ///
+ TargetsInvalid = 0x40000000,
+
+ ///
+ /// Pages in the region will not have their CFG information updated while the protection changes for VirtualProtect.
+ /// For example, if the pages in the region was allocated using PAGE_TARGETS_INVALID, then the invalid information
+ /// will be maintained while the page protection changes. This flag is only valid when the protection changes to
+ /// an executable type like PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY.
+ /// The default behavior for VirtualProtect protection change to executable is to mark all locations as valid call
+ /// targets for CFG.
+ ///
+ TargetsNoUpdate = TargetsInvalid,
+
+ ///
+ /// Pages in the region become guard pages. Any attempt to access a guard page causes the system to raise a
+ /// STATUS_GUARD_PAGE_VIOLATION exception and turn off the guard page status. Guard pages thus act as a one-time
+ /// access alarm. For more information, see Creating Guard Pages. When an access attempt leads the system to turn
+ /// off guard page status, the underlying page protection takes over. If a guard page exception occurs during a
+ /// system service, the service typically returns a failure status indicator. This value cannot be used with
+ /// PAGE_NOACCESS. This flag is not supported by the CreateFileMapping function.
+ ///
+ Guard = 0x100,
+
+ ///
+ /// Sets all pages to be non-cachable. Applications should not use this attribute except when explicitly required
+ /// for a device. Using the interlocked functions with memory that is mapped with SEC_NOCACHE can result in an
+ /// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_NOCACHE flag cannot be used with the PAGE_GUARD, PAGE_NOACCESS,
+ /// or PAGE_WRITECOMBINE flags. The PAGE_NOCACHE flag can be used only when allocating private memory with the
+ /// VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable non-cached memory access for shared
+ /// memory, specify the SEC_NOCACHE flag when calling the CreateFileMapping function.
+ ///
+ NoCache = 0x200,
+
+ ///
+ /// Sets all pages to be write-combined. Applications should not use this attribute except when explicitly required
+ /// for a device. Using the interlocked functions with memory that is mapped as write-combined can result in an
+ /// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_WRITECOMBINE flag cannot be specified with the PAGE_NOACCESS,
+ /// PAGE_GUARD, and PAGE_NOCACHE flags. The PAGE_WRITECOMBINE flag can be used only when allocating private memory
+ /// with the VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable write-combined memory access
+ /// for shared memory, specify the SEC_WRITECOMBINE flag when calling the CreateFileMapping function.
+ ///
+ WriteCombine = 0x400,
+ }
///
/// See https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-freelibrary.
@@ -463,9 +671,9 @@ namespace Dalamud
/// code is ERROR_SUCCESS. If the function fails, the return value is 0 (zero). To get extended error information, call
/// GetLastError.
///
- [DllImport("kernel32.dll", SetLastError = true)]
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
[PreserveSig]
- public static extern uint GetModuleFileName(
+ public static extern uint GetModuleFileNameW(
[In] IntPtr hModule,
[Out] StringBuilder lpFilename,
[In][MarshalAs(UnmanagedType.U4)] int nSize);
@@ -488,8 +696,28 @@ namespace Dalamud
/// If the function succeeds, the return value is a handle to the specified module. If the function fails, the return
/// value is NULL.To get extended error information, call GetLastError.
///
- [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
- public static extern IntPtr GetModuleHandle(string lpModuleName);
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
+ public static extern IntPtr GetModuleHandleW(string lpModuleName);
+
+ ///
+ /// Retrieves the address of an exported function or variable from the specified dynamic-link library (DLL).
+ ///
+ ///
+ /// A handle to the DLL module that contains the function or variable. The LoadLibrary, LoadLibraryEx, LoadPackagedLibrary,
+ /// or GetModuleHandle function returns this handle. The GetProcAddress function does not retrieve addresses from modules
+ /// that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx.
+ ///
+ ///
+ /// The function or variable name, or the function's ordinal value. If this parameter is an ordinal value, it must be
+ /// in the low-order word; the high-order word must be zero.
+ ///
+ ///
+ /// If the function succeeds, the return value is the address of the exported function or variable. If the function
+ /// fails, the return value is NULL.To get extended error information, call GetLastError.
+ ///
+ [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
+ [SuppressMessage("Globalization", "CA2101:Specify marshaling for P/Invoke string arguments", Justification = "Ansi only")]
+ public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
///
/// See https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryw.
@@ -512,8 +740,8 @@ namespace Dalamud
/// If the function succeeds, the return value is a handle to the module. If the function fails, the return value is
/// NULL.To get extended error information, call GetLastError.
///
- [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Ansi)]
- public static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName);
+ [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern IntPtr LoadLibraryW([MarshalAs(UnmanagedType.LPWStr)] string lpFileName);
///
/// ReadProcessMemory copies the data in the specified address range from the address space of the specified process
@@ -551,6 +779,42 @@ namespace Dalamud
int dwSize,
out IntPtr lpNumberOfBytesRead);
+ ///
+ /// ReadProcessMemory copies the data in the specified address range from the address space of the specified process
+ /// into the specified buffer of the current process. Any process that has a handle with PROCESS_VM_READ access can
+ /// call the function. The entire area to be read must be accessible, and if it is not accessible, the function fails.
+ ///
+ ///
+ /// A handle to the process with memory that is being read. The handle must have PROCESS_VM_READ access to the process.
+ ///
+ ///
+ /// A pointer to the base address in the specified process from which to read. Before any data transfer occurs, the
+ /// system verifies that all data in the base address and memory of the specified size is accessible for read access,
+ /// and if it is not accessible the function fails.
+ ///
+ ///
+ /// A pointer to a buffer that receives the contents from the address space of the specified process.
+ ///
+ ///
+ /// The number of bytes to be read from the specified process.
+ ///
+ ///
+ /// A pointer to a variable that receives the number of bytes transferred into the specified buffer. If lpNumberOfBytesRead
+ /// is NULL, the parameter is ignored.
+ ///
+ ///
+ /// If the function succeeds, the return value is nonzero. If the function fails, the return value is 0 (zero). To get
+ /// extended error information, call GetLastError. The function fails if the requested read operation crosses into an
+ /// area of the process that is inaccessible.
+ ///
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern bool ReadProcessMemory(
+ IntPtr hProcess,
+ IntPtr lpBaseAddress,
+ byte[] lpBuffer,
+ int dwSize,
+ out IntPtr lpNumberOfBytesRead);
+
///
/// See https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-seterrormode.
/// Controls whether the system will handle the specified types of serious errors or whether the process will handle
@@ -585,6 +849,166 @@ namespace Dalamud
///
[DllImport("kernel32.dll")]
public static extern IntPtr SetUnhandledExceptionFilter(IntPtr lpTopLevelExceptionFilter);
+
+ ///
+ /// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc.
+ /// Reserves, commits, or changes the state of a region of pages in the virtual address space of the calling process.
+ /// Memory allocated by this function is automatically initialized to zero. To allocate memory in the address space
+ /// of another process, use the VirtualAllocEx function.
+ ///
+ ///
+ /// The starting address of the region to allocate. If the memory is being reserved, the specified address is rounded
+ /// down to the nearest multiple of the allocation granularity. If the memory is already reserved and is being committed,
+ /// the address is rounded down to the next page boundary. To determine the size of a page and the allocation granularity
+ /// on the host computer, use the GetSystemInfo function. If this parameter is NULL, the system determines where to
+ /// allocate the region. If this address is within an enclave that you have not initialized by calling InitializeEnclave,
+ /// VirtualAlloc allocates a page of zeros for the enclave at that address. The page must be previously uncommitted,
+ /// and will not be measured with the EEXTEND instruction of the Intel Software Guard Extensions programming model.
+ /// If the address in within an enclave that you initialized, then the allocation operation fails with the
+ /// ERROR_INVALID_ADDRESS error.
+ ///
+ ///
+ /// The size of the region, in bytes. If the lpAddress parameter is NULL, this value is rounded up to the next page
+ /// boundary. Otherwise, the allocated pages include all pages containing one or more bytes in the range from lpAddress
+ /// to lpAddress+dwSize. This means that a 2-byte range straddling a page boundary causes both pages to be included
+ /// in the allocated region.
+ ///
+ ///
+ /// The type of memory allocation. This parameter must contain one of the MEM_* enum values.
+ ///
+ ///
+ /// The memory protection for the region of pages to be allocated. If the pages are being committed, you can specify
+ /// any one of the memory protection constants.
+ ///
+ ///
+ /// If the function succeeds, the return value is the base address of the allocated region of pages. If the function
+ /// fails, the return value is NULL.To get extended error information, call GetLastError.
+ ///
+ [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
+ public static extern IntPtr VirtualAlloc(
+ IntPtr lpAddress,
+ UIntPtr dwSize,
+ AllocationType flAllocationType,
+ MemoryProtection flProtect);
+
+ ///
+ [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
+ public static extern IntPtr VirtualAlloc(
+ IntPtr lpAddress,
+ UIntPtr dwSize,
+ AllocationType flAllocationType,
+ Memory.MemoryProtection flProtect);
+
+ ///
+ /// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualfree.
+ /// Releases, decommits, or releases and decommits a region of pages within the virtual address space of the calling
+ /// process.
+ /// process.
+ ///
+ ///
+ /// A pointer to the base address of the region of pages to be freed. If the dwFreeType parameter is MEM_RELEASE, this
+ /// parameter must be the base address returned by the VirtualAlloc function when the region of pages is reserved.
+ ///
+ ///
+ /// The size of the region of memory to be freed, in bytes. If the dwFreeType parameter is MEM_RELEASE, this parameter
+ /// must be 0 (zero). The function frees the entire region that is reserved in the initial allocation call to VirtualAlloc.
+ /// If the dwFreeType parameter is MEM_DECOMMIT, the function decommits all memory pages that contain one or more bytes
+ /// in the range from the lpAddress parameter to (lpAddress+dwSize). This means, for example, that a 2-byte region of
+ /// memory that straddles a page boundary causes both pages to be decommitted.If lpAddress is the base address returned
+ /// by VirtualAlloc and dwSize is 0 (zero), the function decommits the entire region that is allocated by VirtualAlloc.
+ /// After that, the entire region is in the reserved state.
+ ///
+ ///
+ /// The type of free operation. This parameter must be one of the MEM_* enum values.
+ ///
+ ///
+ /// If the function succeeds, the return value is a nonzero value. If the function fails, the return value is 0 (zero).
+ /// To get extended error information, call GetLastError.
+ ///
+ [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
+ public static extern bool VirtualFree(
+ IntPtr lpAddress,
+ UIntPtr dwSize,
+ AllocationType dwFreeType);
+
+ ///
+ /// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect.
+ /// Changes the protection on a region of committed pages in the virtual address space of the calling process.
+ ///
+ ///
+ /// The address of the starting page of the region of pages whose access protection attributes are to be changed. All
+ /// pages in the specified region must be within the same reserved region allocated when calling the VirtualAlloc or
+ /// VirtualAllocEx function using MEM_RESERVE. The pages cannot span adjacent reserved regions that were allocated by
+ /// separate calls to VirtualAlloc or VirtualAllocEx using MEM_RESERVE.
+ ///
+ ///
+ /// The size of the region whose access protection attributes are to be changed, in bytes. The region of affected pages
+ /// includes all pages containing one or more bytes in the range from the lpAddress parameter to (lpAddress+dwSize).
+ /// This means that a 2-byte range straddling a page boundary causes the protection attributes of both pages to be changed.
+ ///
+ ///
+ /// The memory protection option. This parameter can be one of the memory protection constants. For mapped views, this
+ /// value must be compatible with the access protection specified when the view was mapped (see MapViewOfFile,
+ /// MapViewOfFileEx, and MapViewOfFileExNuma).
+ ///
+ ///
+ /// A pointer to a variable that receives the previous access protection value of the first page in the specified region
+ /// of pages. If this parameter is NULL or does not point to a valid variable, the function fails.
+ ///
+ ///
+ /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero.
+ /// To get extended error information, call GetLastError.
+ ///
+ [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
+ public static extern bool VirtualProtect(
+ IntPtr lpAddress,
+ UIntPtr dwSize,
+ MemoryProtection flNewProtection,
+ out MemoryProtection lpflOldProtect);
+
+ ///
+ [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
+ public static extern bool VirtualProtect(
+ IntPtr lpAddress,
+ UIntPtr dwSize,
+ Memory.MemoryProtection flNewProtection,
+ out Memory.MemoryProtection lpflOldProtect);
+
+ ///
+ /// Writes data to an area of memory in a specified process. The entire area to be written to must be accessible or
+ /// the operation fails.
+ ///
+ ///
+ /// A handle to the process memory to be modified. The handle must have PROCESS_VM_WRITE and PROCESS_VM_OPERATION access
+ /// to the process.
+ ///
+ ///
+ /// A pointer to the base address in the specified process to which data is written. Before data transfer occurs, the
+ /// system verifies that all data in the base address and memory of the specified size is accessible for write access,
+ /// and if it is not accessible, the function fails.
+ ///
+ ///
+ /// A pointer to the buffer that contains data to be written in the address space of the specified process.
+ ///
+ ///
+ /// The number of bytes to be written to the specified process.
+ ///
+ ///
+ /// A pointer to a variable that receives the number of bytes transferred into the specified process. This parameter
+ /// is optional. If lpNumberOfBytesWritten is NULL, the parameter is ignored.
+ ///
+ ///
+ /// If the function succeeds, the return value is nonzero. If the function fails, the return value is 0 (zero). To get
+ /// extended error information, call GetLastError.The function fails if the requested write operation crosses into an
+ /// area of the process that is inaccessible.
+ ///
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern bool WriteProcessMemory(
+ IntPtr hProcess,
+ IntPtr lpBaseAddress,
+ byte[] lpBuffer,
+ int dwSize,
+ out IntPtr lpNumberOfBytesWritten);
}
///
@@ -593,6 +1017,7 @@ namespace Dalamud
internal static partial class NativeFunctions
{
///
+ /// See https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-setsockopt.
/// The setsockopt function sets a socket option.
///
///
diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs
index a9ed0dc4a..8dfced79a 100644
--- a/Dalamud/Plugin/DalamudPluginInterface.cs
+++ b/Dalamud/Plugin/DalamudPluginInterface.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Reflection;
using Dalamud.Configuration;
+using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState;
@@ -22,7 +23,7 @@ namespace Dalamud.Plugin
///
/// This class acts as an interface to various objects needed to interact with Dalamud and the game.
///
- public class DalamudPluginInterface : IDisposable
+ public sealed class DalamudPluginInterface : IDisposable
{
private readonly Dalamud dalamud;
private readonly string pluginName;
@@ -34,11 +35,9 @@ namespace Dalamud.Plugin
///
/// The dalamud instance to expose.
/// The internal name of the plugin.
- /// The plugin configurations handler.
- /// The reason this plugin was loaded.
- internal DalamudPluginInterface(Dalamud dalamud, string pluginName, PluginConfigurations configs, PluginLoadReason reason)
+ /// The equivalent of what Assembly.GetExecutingAssembly().Location should return.
+ internal DalamudPluginInterface(Dalamud dalamud, string pluginName, string assemblyLocation)
{
- this.Reason = reason;
this.CommandManager = dalamud.CommandManager;
this.Framework = dalamud.Framework;
this.ClientState = dalamud.ClientState;
@@ -49,7 +48,8 @@ namespace Dalamud.Plugin
this.dalamud = dalamud;
this.pluginName = pluginName;
- this.configs = configs;
+ this.configs = dalamud.PluginManager.PluginConfigs;
+ this.AssemblyLocation = assemblyLocation;
this.GeneralChatType = this.dalamud.Configuration.GeneralChatType;
this.Sanitizer = new Sanitizer(this.Data.Language);
@@ -60,7 +60,7 @@ namespace Dalamud.Plugin
else
{
var currentUiLang = CultureInfo.CurrentUICulture;
- if (Localization.ApplicableLangCodes.Any(x => currentUiLang.TwoLetterISOLanguageName == x))
+ if (Localization.ApplicableLangCodes.Any(langCode => currentUiLang.TwoLetterISOLanguageName == langCode))
this.UiLanguage = currentUiLang.TwoLetterISOLanguageName;
else
this.UiLanguage = "en";
@@ -82,9 +82,9 @@ namespace Dalamud.Plugin
public event LanguageChangedDelegate OnLanguageChanged;
///
- /// Gets the reason this plugin was loaded.
+ /// Gets the plugin assembly location.
///
- public PluginLoadReason Reason { get; }
+ public string AssemblyLocation { get; private set; }
///
/// Gets the directory Dalamud assets are stored in.
@@ -142,7 +142,7 @@ namespace Dalamud.Plugin
#if DEBUG
public bool IsDebugging => true;
#else
- public bool IsDebugging => this.dalamud.DalamudUi.IsDevMenu;
+ public bool IsDebugging => this.dalamud.DalamudUi.IsDevMenuOpen;
#endif
///
@@ -192,7 +192,7 @@ namespace Dalamud.Plugin
// This is here for now to support the current plugin API
foreach (var type in Assembly.GetCallingAssembly().GetTypes())
{
- if (type.GetInterface(typeof(IPluginConfiguration).FullName) != null)
+ if (type.IsAssignableTo(typeof(IPluginConfiguration)))
{
var mi = this.configs.GetType().GetMethod("LoadForType");
var fn = mi.MakeGenericMethod(type);
@@ -273,7 +273,7 @@ namespace Dalamud.Plugin
if (this.dalamud.PluginManager.IpcSubscriptions.Any(x => x.SourcePluginName == this.pluginName && x.SubPluginName == pluginName))
throw new InvalidOperationException("Can't add multiple subscriptions for the same plugin.");
- this.dalamud.PluginManager.IpcSubscriptions.Add((this.pluginName, pluginName, action));
+ this.dalamud.PluginManager.IpcSubscriptions.Add(new(this.pluginName, pluginName, action));
}
///
@@ -325,12 +325,16 @@ namespace Dalamud.Plugin
[Obsolete("The current IPC mechanism is obsolete and scheduled to be replaced after API level 3.")]
public bool SendMessage(string pluginName, ExpandoObject message)
{
- var (_, _, pluginInterface, _) = this.dalamud.PluginManager.Plugins.FirstOrDefault(x => x.Definition.InternalName == pluginName);
+ var plugin = this.dalamud.PluginManager.InstalledPlugins.FirstOrDefault(x => x.Manifest.InternalName == pluginName);
- if (pluginInterface?.AnyPluginIpcAction == null)
+ if (plugin == default)
return false;
- pluginInterface.AnyPluginIpcAction.Invoke(this.pluginName, message);
+ if (plugin.DalamudInterface?.AnyPluginIpcAction == null)
+ return false;
+
+ plugin.DalamudInterface.AnyPluginIpcAction.Invoke(this.pluginName, message);
+
return true;
}
@@ -343,22 +347,16 @@ namespace Dalamud.Plugin
///
/// The message template.
/// Values to log.
- [Obsolete]
- public void Log(string messageTemplate, params object[] values)
- {
- Serilog.Log.Information(messageTemplate, values);
- }
+ [Obsolete("Use PluginLog")]
+ public void Log(string messageTemplate, params object[] values) => Serilog.Log.Information(messageTemplate, values);
///
/// Log a templated error message to the in-game debug log.
///
/// The message template.
/// Values to log.
- [Obsolete]
- public void LogError(string messageTemplate, params object[] values)
- {
- Serilog.Log.Error(messageTemplate, values);
- }
+ [Obsolete("Use PluginLog")]
+ public void LogError(string messageTemplate, params object[] values) => Serilog.Log.Error(messageTemplate, values);
///
/// Log a templated error message to the in-game debug log.
@@ -366,11 +364,8 @@ namespace Dalamud.Plugin
/// The exception that caused the error.
/// The message template.
/// Values to log.
- [Obsolete]
- public void LogError(Exception exception, string messageTemplate, params object[] values)
- {
- Serilog.Log.Error(exception, messageTemplate, values);
- }
+ [Obsolete("Use PluginLog")]
+ public void LogError(Exception exception, string messageTemplate, params object[] values) => Serilog.Log.Error(exception, messageTemplate, values);
#endregion
diff --git a/Dalamud/Plugin/Internal/Exceptions/DuplicatePluginException.cs b/Dalamud/Plugin/Internal/Exceptions/DuplicatePluginException.cs
new file mode 100644
index 000000000..093f97e69
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Exceptions/DuplicatePluginException.cs
@@ -0,0 +1,26 @@
+namespace Dalamud.Plugin.Internal.Exceptions
+{
+ ///
+ /// This exception that is thrown when a plugin is instructed to load while another plugin with the same
+ /// assembly name is already present and loaded.
+ ///
+ internal class DuplicatePluginException : PluginException
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Name of the conflicting assembly.
+ public DuplicatePluginException(string assemblyName)
+ {
+ this.AssemblyName = assemblyName;
+ }
+
+ ///
+ /// Gets the name of the conflicting assembly.
+ ///
+ public string AssemblyName { get; init; }
+
+ ///
+ public override string Message => $"A plugin with the same assembly name of {this.AssemblyName} is already loaded";
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Exceptions/InvalidPluginException.cs b/Dalamud/Plugin/Internal/Exceptions/InvalidPluginException.cs
new file mode 100644
index 000000000..6b5c8920a
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Exceptions/InvalidPluginException.cs
@@ -0,0 +1,25 @@
+using System;
+using System.IO;
+
+namespace Dalamud.Plugin.Internal.Exceptions
+{
+ ///
+ /// This exception represents a file that does not implement IDalamudPlugin.
+ ///
+ internal class InvalidPluginException : PluginException
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The invalid file.
+ public InvalidPluginException(FileInfo dllFile)
+ {
+ this.DllFile = dllFile;
+ }
+
+ ///
+ /// Gets the invalid file.
+ ///
+ public FileInfo DllFile { get; init; }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Exceptions/InvalidPluginOperationException.cs b/Dalamud/Plugin/Internal/Exceptions/InvalidPluginOperationException.cs
new file mode 100644
index 000000000..a80d6d51d
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Exceptions/InvalidPluginOperationException.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace Dalamud.Plugin.Internal.Exceptions
+{
+ ///
+ /// This represents an invalid plugin operation.
+ ///
+ internal class InvalidPluginOperationException : PluginException
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message describing the invalid operation.
+ public InvalidPluginOperationException(string message)
+ {
+ this.Message = message;
+ }
+
+ ///
+ /// Gets the message describing the invalid operation.
+ ///
+ public override string Message { get; }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Exceptions/PluginException.cs b/Dalamud/Plugin/Internal/Exceptions/PluginException.cs
new file mode 100644
index 000000000..e4b17b686
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Exceptions/PluginException.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Dalamud.Plugin.Internal.Exceptions
+{
+ ///
+ /// This represents the base Dalamud plugin exception.
+ ///
+ internal abstract class PluginException : Exception
+ {
+ }
+}
diff --git a/Dalamud/Plugin/Internal/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/LocalDevPlugin.cs
new file mode 100644
index 000000000..58390270b
--- /dev/null
+++ b/Dalamud/Plugin/Internal/LocalDevPlugin.cs
@@ -0,0 +1,157 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Dalamud.Configuration.Internal;
+using Dalamud.Plugin.Internal.Types;
+
+namespace Dalamud.Plugin.Internal
+{
+ ///
+ /// This class represents a dev plugin and all facets of its lifecycle.
+ /// The DLL on disk, dependencies, loaded assembly, etc.
+ ///
+ internal class LocalDevPlugin : LocalPlugin, IDisposable
+ {
+ private static readonly ModuleLog Log = new("PLUGIN");
+
+ // Ref to Dalamud.Configuration.DevPluginSettings
+ private readonly DevPluginSettings devSettings;
+
+ private FileSystemWatcher fileWatcher;
+ private CancellationTokenSource fileWatcherTokenSource;
+ private int reloadCounter;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Dalamud instance.
+ /// Path to the DLL file.
+ /// The plugin manifest.
+ public LocalDevPlugin(Dalamud dalamud, FileInfo dllFile, LocalPluginManifest manifest)
+ : base(dalamud, dllFile, manifest)
+ {
+ // base is called first, ensuring that this is a valid plugin assembly
+ var devSettings = dalamud.Configuration.DevPluginSettings.FirstOrDefault(cfg => cfg.DllFile == dllFile.FullName);
+
+ if (devSettings == default)
+ {
+ devSettings = new DevPluginSettings(dllFile.FullName);
+ dalamud.Configuration.DevPluginSettings.Add(devSettings);
+ dalamud.Configuration.Save();
+ }
+
+ this.devSettings = devSettings;
+
+ if (this.AutomaticReload)
+ {
+ this.EnableReloading();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this dev plugin should start on boot.
+ ///
+ public bool StartOnBoot
+ {
+ get => this.devSettings.StartOnBoot;
+ set => this.devSettings.StartOnBoot = value;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this dev plugin should reload on change.
+ ///
+ public bool AutomaticReload
+ {
+ get => this.devSettings.AutomaticReloading;
+ set
+ {
+ this.devSettings.AutomaticReloading = value;
+
+ if (this.devSettings.AutomaticReloading)
+ {
+ this.EnableReloading();
+ }
+ else
+ {
+ this.DisableReloading();
+ }
+ }
+ }
+
+ ///
+ public new void Dispose()
+ {
+ if (this.fileWatcher != null)
+ {
+ this.fileWatcher.Changed -= this.OnFileChanged;
+ this.fileWatcherTokenSource.Cancel();
+ this.fileWatcher.Dispose();
+ }
+
+ base.Dispose();
+ }
+
+ ///
+ /// Configure this plugin for automatic reloading and enable it.
+ ///
+ public void EnableReloading()
+ {
+ if (this.fileWatcher == null)
+ {
+ this.fileWatcherTokenSource = new();
+ this.fileWatcher = new FileSystemWatcher(this.DllFile.DirectoryName);
+ this.fileWatcher.Changed += this.OnFileChanged;
+ this.fileWatcher.Filter = this.DllFile.Name;
+ this.fileWatcher.NotifyFilter = NotifyFilters.LastWrite;
+ this.fileWatcher.EnableRaisingEvents = true;
+ }
+ }
+
+ ///
+ /// Disable automatic reloading for this plugin.
+ ///
+ public void DisableReloading()
+ {
+ if (this.fileWatcher != null)
+ {
+ this.fileWatcherTokenSource.Cancel();
+ this.fileWatcher.Changed -= this.OnFileChanged;
+ this.fileWatcher.Dispose();
+ this.fileWatcher = null;
+ }
+ }
+
+ private void OnFileChanged(object sender, FileSystemEventArgs args)
+ {
+ var current = Interlocked.Increment(ref this.reloadCounter);
+
+ Task.Delay(500).ContinueWith(
+ task =>
+ {
+ if (this.fileWatcherTokenSource.IsCancellationRequested)
+ {
+ Log.Debug($"Skipping reload of {this.Name}, file watcher was cancelled.");
+ return;
+ }
+
+ if (current != this.reloadCounter)
+ {
+ Log.Debug($"Skipping reload of {this.Name}, file has changed again.");
+ return;
+ }
+
+ if (this.State != PluginState.Loaded)
+ {
+ Log.Debug($"Skipping reload of {this.Name}, state ({this.State}) is not {PluginState.Loaded}.");
+ return;
+ }
+
+ this.Reload();
+ },
+ this.fileWatcherTokenSource.Token);
+ }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/LocalPlugin.cs b/Dalamud/Plugin/Internal/LocalPlugin.cs
new file mode 100644
index 000000000..45fbb144f
--- /dev/null
+++ b/Dalamud/Plugin/Internal/LocalPlugin.cs
@@ -0,0 +1,415 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+
+using Dalamud.Configuration.Internal;
+using Dalamud.Game;
+using Dalamud.Plugin.Internal.Exceptions;
+using Dalamud.Plugin.Internal.Types;
+using McMaster.NETCore.Plugins;
+
+namespace Dalamud.Plugin.Internal
+{
+ ///
+ /// This class represents a plugin and all facets of its lifecycle.
+ /// The DLL on disk, dependencies, loaded assembly, etc.
+ ///
+ internal class LocalPlugin : IDisposable
+ {
+ private static readonly ModuleLog Log = new("PLUGIN");
+
+ private readonly Dalamud dalamud;
+ private readonly FileInfo manifestFile;
+ private readonly FileInfo disabledFile;
+ private readonly FileInfo testingFile;
+
+ private PluginLoader loader;
+ private Assembly pluginAssembly;
+ private Type pluginType;
+ private IDalamudPlugin instance;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Dalamud instance.
+ /// Path to the DLL file.
+ /// The plugin manifest.
+ public LocalPlugin(Dalamud dalamud, FileInfo dllFile, LocalPluginManifest manifest)
+ {
+ this.dalamud = dalamud;
+ this.DllFile = dllFile;
+ this.State = PluginState.Unloaded;
+
+ this.loader ??= PluginLoader.CreateFromAssemblyFile(
+ this.DllFile.FullName,
+ config =>
+ {
+ config.IsUnloadable = true;
+ config.LoadInMemory = true;
+ config.PreferSharedTypes = true;
+ });
+
+ Version assemblyVersion;
+
+ try
+ {
+ // BadImageFormatException
+ this.pluginAssembly ??= this.loader.LoadDefaultAssembly();
+
+ // InvalidOperationException
+ this.pluginType = this.pluginAssembly.GetTypes().First(type => type.IsAssignableTo(typeof(IDalamudPlugin)));
+
+ assemblyVersion = this.pluginAssembly.GetName().Version;
+ }
+ catch (Exception)
+ {
+ this.pluginAssembly = null;
+ this.pluginType = null;
+ this.loader.Dispose();
+
+ Log.Debug($"Not a plugin: {this.DllFile.Name}");
+ throw new InvalidPluginException(this.DllFile);
+ }
+
+ // Files that may or may not exist
+ this.manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile);
+ this.disabledFile = LocalPluginManifest.GetDisabledFile(this.DllFile);
+ this.testingFile = LocalPluginManifest.GetTestingFile(this.DllFile);
+
+ // If the parameter manifest was null
+ if (manifest == null)
+ {
+ this.Manifest = new LocalPluginManifest()
+ {
+ Author = "developer",
+ Name = Path.GetFileNameWithoutExtension(this.DllFile.Name),
+ InternalName = Path.GetFileNameWithoutExtension(this.DllFile.Name),
+ AssemblyVersion = assemblyVersion,
+ Description = string.Empty,
+ ApplicableVersion = GameVersion.Any,
+ DalamudApiLevel = PluginManager.DalamudApiLevel,
+ IsHide = false,
+ };
+
+ // Save the manifest to disk so there won't be any problems later.
+ // We'll update the name property after it can be retrieved from the instance.
+ var manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile);
+ this.Manifest.Save(manifestFile);
+ }
+ else
+ {
+ this.Manifest = manifest;
+ }
+
+ // This bit converts from ".disabled" functionality to using the manifest.
+ if (this.disabledFile.Exists)
+ {
+ this.Manifest.Disabled = true;
+ this.disabledFile.Delete();
+ }
+
+ // This bit converts from ".testing" functionality to using the manifest.
+ if (this.testingFile.Exists)
+ {
+ this.Manifest.Testing = true;
+ this.testingFile.Delete();
+ }
+
+ this.SaveManifest();
+ }
+
+ ///
+ /// Gets the associated with this plugin.
+ ///
+ public DalamudPluginInterface DalamudInterface { get; private set; }
+
+ ///
+ /// Gets the path to the plugin DLL.
+ ///
+ public FileInfo DllFile { get; }
+
+ ///
+ /// Gets the plugin manifest, if one exists.
+ ///
+ public LocalPluginManifest Manifest { get; }
+
+ ///
+ /// Gets or sets the current state of the plugin.
+ ///
+ public PluginState State { get; protected set; } = PluginState.Unloaded;
+
+ ///
+ /// Gets the AssemblyName plugin, populated during .
+ ///
+ /// Plugin type.
+ public AssemblyName AssemblyName { get; private set; } = null;
+
+ ///
+ /// Gets the plugin name, directly from the plugin or if it is not loaded from the manifest.
+ ///
+ public string Name => this.instance?.Name ?? this.Manifest.Name ?? this.DllFile.Name;
+
+ ///
+ /// Gets a value indicating whether the plugin is loaded and running.
+ ///
+ public bool IsLoaded => this.State == PluginState.Loaded;
+
+ ///
+ /// Gets a value indicating whether the plugin is disabled.
+ ///
+ public bool IsDisabled => this.Manifest.Disabled;
+
+ ///
+ /// Gets a value indicating whether the plugin is for testing use only.
+ ///
+ public bool IsTesting => this.Manifest.IsTestingExclusive || this.Manifest.Testing;
+
+ ///
+ /// Gets a value indicating whether this plugin is dev plugin.
+ ///
+ public bool IsDev => this is LocalDevPlugin;
+
+ ///
+ public void Dispose()
+ {
+ this.instance?.Dispose();
+ this.instance = null;
+
+ this.DalamudInterface.Dispose();
+ this.DalamudInterface = null;
+
+ this.pluginType = null;
+ this.pluginAssembly = null;
+
+ this.loader?.Dispose();
+ }
+
+ ///
+ /// Load this plugin.
+ ///
+ /// Load while reloading.
+ public void Load(bool reloading = false)
+ {
+ // Allowed: Unloaded
+ switch (this.State)
+ {
+ case PluginState.InProgress:
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, already working");
+ case PluginState.Loaded:
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, already loaded");
+ case PluginState.LoadError:
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, load previously faulted, unload first");
+ case PluginState.UnloadError:
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, unload previously faulted, restart Dalamud");
+ }
+
+ if (this.Manifest.ApplicableVersion < this.dalamud.StartInfo.GameVersion)
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version");
+
+ if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel)
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level");
+
+ if (this.Manifest.Disabled)
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, disabled");
+
+ this.State = PluginState.InProgress;
+ Log.Information($"Loading {this.DllFile.Name}");
+
+ try
+ {
+ this.loader ??= PluginLoader.CreateFromAssemblyFile(
+ this.DllFile.FullName,
+ config =>
+ {
+ config.IsUnloadable = true;
+ config.LoadInMemory = true;
+ config.PreferSharedTypes = true;
+ });
+
+ if (reloading)
+ {
+ this.loader.Reload();
+ }
+
+ // Load the assembly
+ this.pluginAssembly ??= this.loader.LoadDefaultAssembly();
+
+ this.AssemblyName = this.pluginAssembly.GetName();
+
+ // Find the plugin interface implementation. It is guaranteed to exist after checking in the ctor.
+ this.pluginType ??= this.pluginAssembly.GetTypes().First(type => type.IsAssignableTo(typeof(IDalamudPlugin)));
+
+ // Check for any loaded plugins with the same assembly name
+ var assemblyName = this.pluginAssembly.GetName().Name;
+ foreach (var otherPlugin in this.dalamud.PluginManager.InstalledPlugins)
+ {
+ // During hot-reloading, this plugin will be in the plugin list, and the instance will have been disposed
+ if (otherPlugin == this || otherPlugin.instance == null)
+ continue;
+
+ var otherPluginAssemblyName = otherPlugin.instance.GetType().Assembly.GetName().Name;
+ if (otherPluginAssemblyName == assemblyName)
+ {
+ this.State = PluginState.Unloaded;
+ Log.Debug($"Duplicate assembly: {this.Name}");
+
+ throw new DuplicatePluginException(assemblyName);
+ }
+ }
+
+ // Update the location for the Location and CodeBase patches
+ PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new(this.DllFile);
+
+ // Instantiate and initialize
+ this.instance = Activator.CreateInstance(this.pluginType) as IDalamudPlugin;
+
+ // In-case the manifest name was a placeholder. Can occur when no manifest was included.
+ if (this.instance.Name != this.Manifest.Name)
+ {
+ this.Manifest.Name = this.instance.Name;
+ this.Manifest.Save(this.manifestFile);
+ }
+
+ this.DalamudInterface = new DalamudPluginInterface(this.dalamud, this.pluginAssembly.GetName().Name, this.DllFile.FullName);
+
+ if (this.IsDev)
+ {
+ // Inherit LPL's AssemblyLocation functionality
+ try
+ {
+ var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
+
+ this.instance.GetType()
+ ?.GetProperty("AssemblyLocation", bindingFlags)
+ ?.SetValue(this.instance, this.DllFile.FullName);
+ this.instance.GetType()
+ ?.GetMethod("SetLocation", bindingFlags)
+ ?.Invoke(this.instance, new object[] { this.DllFile.FullName });
+ }
+ catch
+ {
+ // Ignored
+ }
+ }
+
+ this.instance.Initialize(this.DalamudInterface);
+
+ this.State = PluginState.Loaded;
+ Log.Information($"Finished loading {this.DllFile.Name}");
+ }
+ catch (Exception ex)
+ {
+ this.State = PluginState.LoadError;
+ Log.Error(ex, $"Error while loading {this.Name}");
+
+ throw;
+ }
+ }
+
+ ///
+ /// Unload this plugin. This is the same as dispose, but without the "disposed" connotations. This object should stay
+ /// in the plugin list until it has been actually disposed.
+ ///
+ /// Unload while reloading.
+ public void Unload(bool reloading = false)
+ {
+ // Allowed: Loaded
+ switch (this.State)
+ {
+ case PluginState.InProgress:
+ throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already working");
+ case PluginState.LoadError:
+ throw new InvalidPluginOperationException($"Unable to unload {this.Name}, load previously faulted, unload first");
+ case PluginState.Unloaded:
+ throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already unloaded");
+ case PluginState.UnloadError:
+ throw new InvalidPluginOperationException($"Unable to unload {this.Name}, unload previously faulted, restart Dalamud");
+ }
+
+ try
+ {
+ this.State = PluginState.InProgress;
+ Log.Information($"Unloading {this.DllFile.Name}");
+
+ this.instance?.Dispose();
+ this.instance = null;
+
+ this.DalamudInterface?.Dispose();
+ this.DalamudInterface = null;
+
+ this.pluginType = null;
+ this.pluginAssembly = null;
+
+ if (!reloading)
+ {
+ this.loader?.Dispose();
+ this.loader = null;
+ }
+
+ this.State = PluginState.Unloaded;
+ Log.Information($"Finished unloading {this.DllFile.Name}");
+ }
+ catch (Exception ex)
+ {
+ this.State = PluginState.UnloadError;
+ Log.Error(ex, $"Error while unloading {this.Name}");
+
+ throw;
+ }
+ }
+
+ ///
+ /// Reload this plugin.
+ ///
+ public void Reload()
+ {
+ this.Unload(true);
+ this.Load(true);
+ }
+
+ ///
+ /// Revert a disable. Must be unloaded first, does not load.
+ ///
+ public void Enable()
+ {
+ // Allowed: Unloaded, UnloadError
+ switch (this.State)
+ {
+ case PluginState.InProgress:
+ case PluginState.Loaded:
+ case PluginState.LoadError:
+ throw new InvalidPluginOperationException($"Unable to enable {this.Name}, still loaded");
+ }
+
+ if (!this.Manifest.Disabled)
+ throw new InvalidPluginOperationException($"Unable to enable {this.Name}, not disabled");
+
+ this.Manifest.Disabled = false;
+ this.SaveManifest();
+ }
+
+ ///
+ /// Disable this plugin, must be unloaded first.
+ ///
+ public void Disable()
+ {
+ // Allowed: Unloaded, UnloadError
+ switch (this.State)
+ {
+ case PluginState.InProgress:
+ case PluginState.Loaded:
+ case PluginState.LoadError:
+ throw new InvalidPluginOperationException($"Unable to disable {this.Name}, still loaded");
+ }
+
+ if (this.Manifest.Disabled)
+ throw new InvalidPluginOperationException($"Unable to disable {this.Name}, already disabled");
+
+ this.Manifest.Disabled = true;
+ this.SaveManifest();
+ }
+
+ private void SaveManifest() => this.Manifest.Save(this.manifestFile);
+ }
+}
diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs
new file mode 100644
index 000000000..2d57c4e2b
--- /dev/null
+++ b/Dalamud/Plugin/Internal/PluginManager.cs
@@ -0,0 +1,1023 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Net;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+
+using CheapLoc;
+using Dalamud.Configuration;
+using Dalamud.Game.Text;
+using Dalamud.Plugin.Internal.Exceptions;
+using Dalamud.Plugin.Internal.Types;
+using HarmonyLib;
+using Newtonsoft.Json;
+
+namespace Dalamud.Plugin.Internal
+{
+ ///
+ /// Class responsible for loading and unloading plugins.
+ ///
+ internal partial class PluginManager : IDisposable
+ {
+ ///
+ /// The current Dalamud API level, used to handle breaking changes. Only plugins with this level will be loaded.
+ ///
+ public const int DalamudApiLevel = 3;
+
+ private static readonly ModuleLog Log = new("PLUGINM");
+
+ private readonly Dalamud dalamud;
+ private readonly DirectoryInfo pluginDirectory;
+ private readonly DirectoryInfo devPluginDirectory;
+ private readonly BannedPlugin[] bannedPlugins;
+
+ private readonly List installedPlugins = new();
+ private List availablePlugins = new();
+ private List updatablePlugins = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The instance to load plugins with.
+ public PluginManager(Dalamud dalamud)
+ {
+ this.dalamud = dalamud;
+ this.pluginDirectory = new DirectoryInfo(dalamud.StartInfo.PluginDirectory);
+ this.devPluginDirectory = new DirectoryInfo(dalamud.StartInfo.DefaultPluginDirectory);
+
+ if (!this.pluginDirectory.Exists)
+ this.pluginDirectory.Create();
+
+ if (!this.devPluginDirectory.Exists)
+ this.devPluginDirectory.Create();
+
+ this.PluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(dalamud.StartInfo.ConfigurationPath), "pluginConfigs"));
+
+ var bannedPluginsJson = File.ReadAllText(Path.Combine(this.dalamud.StartInfo.AssetDirectory, "UIRes", "bannedplugin.json"));
+ this.bannedPlugins = JsonConvert.DeserializeObject(bannedPluginsJson);
+
+ this.Repos.Add(PluginRepository.MainRepo);
+ this.Repos.AddRange(this.dalamud.Configuration.ThirdRepoList
+ .Select(repo => new PluginRepository(repo.Url, repo.IsEnabled)));
+
+ this.ApplyPatches();
+ }
+
+ ///
+ /// An event that fires when the installed plugins have changed.
+ ///
+ public event Action OnInstalledPluginsChanged;
+
+ ///
+ /// An event that fires when the available plugins have changed.
+ ///
+ public event Action OnAvailablePluginsChanged;
+
+ ///
+ /// Gets a list of all loaded plugins.
+ ///
+ public ImmutableList InstalledPlugins { get; private set; } = ImmutableList.Create();
+
+ ///
+ /// Gets a list of all available plugins.
+ ///
+ public ImmutableList AvailablePlugins { get; private set; } = ImmutableList.Create();
+
+ ///
+ /// Gets a list of all plugins with an available update.
+ ///
+ public ImmutableList UpdatablePlugins { get; private set; } = ImmutableList.Create();
+
+ ///
+ /// Gets a list of all plugin repositories. The main repo should always be first.
+ ///
+ public List Repos { get; } = new();
+
+ ///
+ /// Gets a value indicating whether plugins are not still loading from boot.
+ ///
+ public bool PluginsReady { get; private set; } = false;
+
+ ///
+ /// Gets a value indicating whether all added repos are not in progress.
+ ///
+ public bool ReposReady => this.Repos.All(repo => repo.State != PluginRepositoryState.InProgress);
+
+ ///
+ /// Gets a list of all IPC subscriptions.
+ ///
+ public List IpcSubscriptions { get; } = new();
+
+ ///
+ /// Gets the object used when initializing plugins.
+ ///
+ public PluginConfigurations PluginConfigs { get; }
+
+ ///
+ public void Dispose()
+ {
+ foreach (var plugin in this.installedPlugins.ToArray())
+ {
+ try
+ {
+ plugin.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Error disposing {plugin.Name}");
+ }
+ }
+ }
+
+ ///
+ /// Load all plugins, sorted by priority. Any plugins with no explicit definition file or a negative priority
+ /// are loaded asynchronously. Should only be called during Dalamud startup.
+ ///
+ public void LoadAllPlugins()
+ {
+ var pluginDefs = new List();
+ var devPluginDefs = new List();
+
+ if (!this.pluginDirectory.Exists)
+ this.pluginDirectory.Create();
+
+ if (!this.devPluginDirectory.Exists)
+ this.devPluginDirectory.Create();
+
+ // Add installed plugins. These are expected to be in a specific format so we can look for exactly that.
+ foreach (var pluginDir in this.pluginDirectory.GetDirectories())
+ {
+ foreach (var versionDir in pluginDir.GetDirectories())
+ {
+ var dllFile = new FileInfo(Path.Combine(versionDir.FullName, $"{pluginDir.Name}.dll"));
+ var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
+
+ if (!manifestFile.Exists)
+ continue;
+
+ var manifest = LocalPluginManifest.Load(manifestFile);
+
+ pluginDefs.Add(new(dllFile, manifest, false));
+ }
+ }
+
+ // devPlugins are more freeform. Look for any dll and hope to get lucky.
+ var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories);
+
+ foreach (var dllFile in devDllFiles)
+ {
+ // Manifests are not required for devPlugins. the Plugin type will handle any null manifests.
+ var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
+ var manifest = manifestFile.Exists ? LocalPluginManifest.Load(manifestFile) : null;
+ devPluginDefs.Add(new(dllFile, manifest, true));
+ }
+
+ // Sort for load order - unloaded definitions have default priority of 0
+ pluginDefs.Sort(PluginDef.Sorter);
+ devPluginDefs.Sort(PluginDef.Sorter);
+
+ // Dev plugins should load first.
+ pluginDefs.InsertRange(0, devPluginDefs);
+
+ void LoadPlugins(IEnumerable pluginDefs)
+ {
+ foreach (var pluginDef in pluginDefs)
+ {
+ try
+ {
+ this.LoadPlugin(pluginDef.DllFile, pluginDef.Manifest, pluginDef.IsDev, isBoot: true);
+ }
+ catch (InvalidPluginException)
+ {
+ // Not a plugin
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "During boot plugin load, an unexpected error occurred");
+ }
+ }
+ }
+
+ // Load sync plugins
+ var syncPlugins = pluginDefs.Where(def => def.Manifest?.LoadPriority > 0);
+ LoadPlugins(syncPlugins);
+
+ var asyncPlugins = pluginDefs.Where(def => def.Manifest == null || def.Manifest.LoadPriority <= 0);
+ Task.Run(() => LoadPlugins(asyncPlugins))
+ .ContinueWith(task => this.PluginsReady = true)
+ .ContinueWith(task => this.NotifyInstalledPluginsChanged());
+ }
+
+ ///
+ /// Reload all loaded plugins.
+ ///
+ public void ReloadAllPlugins()
+ {
+ var aggregate = new List();
+
+ for (var i = 0; i < this.installedPlugins.Count; i++)
+ {
+ var plugin = this.installedPlugins[i];
+
+ if (plugin.IsLoaded)
+ {
+ try
+ {
+ plugin.Unload();
+ plugin.Load();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error during reload all");
+
+ aggregate.Add(ex);
+ }
+ }
+ }
+
+ if (aggregate.Any())
+ {
+ throw new AggregateException(aggregate);
+ }
+ }
+
+ ///
+ /// Reload the PluginMaster for each repo, filter, and event that the list has updated.
+ ///
+ public void ReloadPluginMasters()
+ {
+ Task.WhenAll(this.Repos.Select(repo => repo.ReloadPluginMasterAsync()))
+ .ContinueWith(task => this.RefilterPluginMasters())
+ .Wait();
+ }
+
+ ///
+ /// Apply visibility and eligibility filters to the available plugins, then event that the list has updated.
+ ///
+ public void RefilterPluginMasters()
+ {
+ this.availablePlugins = this.dalamud.PluginManager.Repos
+ .SelectMany(repo => repo.PluginMaster)
+ .Where(this.IsManifestEligible)
+ .Where(this.IsManifestVisible)
+ .ToList();
+
+ this.NotifyAvailablePluginsChanged();
+ }
+
+ ///
+ /// Scan the devPlugins folder for new DLL files that are not already loaded into the manager. They are not loaded,
+ /// only shown as disabled in the installed plugins window. This is a modified version of LoadAllPlugins that works
+ /// a little differently.
+ ///
+ public void ScanDevPlugins()
+ {
+ if (!this.devPluginDirectory.Exists)
+ this.devPluginDirectory.Create();
+
+ // devPlugins are more freeform. Look for any dll and hope to get lucky.
+ var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories);
+
+ var listChanged = false;
+
+ foreach (var dllFile in devDllFiles)
+ {
+ // This file is already known to us
+ if (this.InstalledPlugins.Any(lp => lp.DllFile.FullName == dllFile.FullName))
+ continue;
+
+ // Manifests are not required for devPlugins. the Plugin type will handle any null manifests.
+ var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
+ var manifest = manifestFile.Exists ? LocalPluginManifest.Load(manifestFile) : null;
+
+ try
+ {
+ // Add them to the list and let the user decide, nothing is auto-loaded.
+ this.LoadPlugin(dllFile, manifest, isDev: true, doNotLoad: true);
+ listChanged = true;
+ }
+ catch (InvalidPluginException)
+ {
+ // Not a plugin
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"During devPlugin scan, an unexpected error occurred");
+ }
+ }
+
+ if (listChanged)
+ this.NotifyInstalledPluginsChanged();
+ }
+
+ ///
+ /// Install a plugin from a repository and load it.
+ ///
+ /// The plugin definition.
+ /// If the testing version should be used.
+ public void InstallPlugin(RemotePluginManifest repoManifest, bool useTesting)
+ {
+ Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting})");
+
+ var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall;
+ var version = useTesting ? repoManifest.TestingAssemblyVersion : repoManifest.AssemblyVersion;
+
+ var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory.FullName, repoManifest.InternalName, version.ToString()));
+
+ try
+ {
+ if (outputDir.Exists)
+ outputDir.Delete(true);
+
+ outputDir.Create();
+ }
+ catch
+ {
+ // ignored, since the plugin may be loaded already
+ }
+
+ using var client = new WebClient();
+
+ var tempZip = new FileInfo(Path.GetTempFileName());
+
+ try
+ {
+ Log.Debug($"Downloading plugin to {tempZip} from {downloadUrl}");
+ client.DownloadFile(downloadUrl, tempZip.FullName);
+ }
+ catch (WebException ex)
+ {
+ Log.Error(ex, $"Download of plugin {repoManifest.Name} failed unexpectedly.");
+ throw;
+ }
+
+ Log.Debug($"Extracting to {outputDir}");
+ // This throws an error, even with overwrite=false
+ // ZipFile.ExtractToDirectory(tempZip.FullName, outputDir.FullName, false);
+ using (var archive = ZipFile.OpenRead(tempZip.FullName))
+ {
+ foreach (var zipFile in archive.Entries)
+ {
+ var completeFileName = Path.GetFullPath(Path.Combine(outputDir.FullName, zipFile.FullName));
+
+ if (!completeFileName.StartsWith(outputDir.FullName, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new IOException("Trying to extract file outside of destination directory. See this link for more info: https://snyk.io/research/zip-slip-vulnerability");
+ }
+
+ if (zipFile.Name == string.Empty)
+ {
+ // Assuming Empty for Directory
+ Directory.CreateDirectory(Path.GetDirectoryName(completeFileName));
+ continue;
+ }
+
+ try
+ {
+ zipFile.ExtractToFile(completeFileName, true);
+ }
+ catch (Exception ex)
+ {
+ Log.Information($"Could not overwrite {zipFile.Name}: {ex.Message}");
+ }
+ }
+ }
+
+ tempZip.Delete();
+
+ var dllFile = LocalPluginManifest.GetPluginFile(outputDir, repoManifest);
+ var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
+
+ // Reload as a local manifest, add some attributes, and save again.
+ var manifest = LocalPluginManifest.Load(manifestFile);
+
+ if (useTesting)
+ {
+ manifest.Testing = true;
+ }
+
+ if (repoManifest.SourceRepo.IsThirdParty)
+ {
+ // Only document the url if it came from a third party repo.
+ manifest.InstalledFromUrl = repoManifest.SourceRepo.PluginMasterUrl;
+ }
+
+ manifest.Save(manifestFile);
+
+ Log.Information($"Installed plugin {manifest.Name} (testing={useTesting})");
+
+ this.LoadPlugin(dllFile, manifest);
+
+ this.NotifyInstalledPluginsChanged();
+ }
+
+ ///
+ /// Load a plugin.
+ ///
+ /// The associated with the main assembly of this plugin.
+ /// The already loaded definition, if available.
+ /// If this plugin should support development features.
+ /// If this plugin is being loaded at boot.
+ /// Don't load the plugin, just don't do it.
+ public void LoadPlugin(FileInfo dllFile, LocalPluginManifest manifest, bool isDev = false, bool isBoot = false, bool doNotLoad = false)
+ {
+ var name = manifest?.Name ?? dllFile.Name;
+ var loadPlugin = !doNotLoad;
+
+ LocalPlugin plugin;
+
+ if (isDev)
+ {
+ Log.Information($"Loading dev plugin {name}");
+ var devPlugin = new LocalDevPlugin(this.dalamud, dllFile, manifest);
+ loadPlugin &= !isBoot || devPlugin.StartOnBoot;
+
+ plugin = devPlugin;
+ }
+ else
+ {
+ Log.Information($"Loading plugin {name}");
+ plugin = new LocalPlugin(this.dalamud, dllFile, manifest);
+ }
+
+ if (loadPlugin)
+ {
+ try
+ {
+ if (plugin.IsDisabled)
+ plugin.Enable();
+
+ plugin.Load();
+ }
+ catch (InvalidPluginException)
+ {
+ PluginLocations.Remove(plugin.AssemblyName.FullName);
+ throw;
+ }
+ catch (Exception ex)
+ {
+ // Dev plugins always get added to the list so they can be fiddled with in the UI
+ if (plugin.IsDev)
+ {
+ Log.Information(ex, $"Dev plugin failed to load, adding anyways: {dllFile.Name}");
+ }
+ else
+ {
+ PluginLocations.Remove(plugin.AssemblyName.FullName);
+ throw;
+ }
+ }
+ }
+
+ this.installedPlugins.Add(plugin);
+ }
+
+ ///
+ /// Remove a plugin.
+ ///
+ /// Plugin to remove.
+ public void RemovePlugin(LocalPlugin plugin)
+ {
+ if (plugin.State != PluginState.Unloaded)
+ throw new InvalidPluginOperationException($"Unable to remove {plugin.Name}, not unloaded");
+
+ this.installedPlugins.Remove(plugin);
+ PluginLocations.Remove(plugin.AssemblyName.FullName);
+
+ this.NotifyInstalledPluginsChanged();
+ }
+
+ ///
+ /// Cleanup disabled plugins. Does not target devPlugins.
+ ///
+ public void CleanupPlugins()
+ {
+ foreach (var pluginDir in this.pluginDirectory.GetDirectories())
+ {
+ try
+ {
+ var versionDirs = pluginDir.GetDirectories();
+
+ versionDirs = versionDirs
+ .OrderByDescending(dir =>
+ {
+ var isVersion = Version.TryParse(dir.Name, out var version);
+
+ if (!isVersion)
+ {
+ Log.Debug($"Not a version, cleaning up {dir.FullName}");
+ dir.Delete();
+ }
+
+ return version;
+ })
+ .Where(version => version != null)
+ .ToArray();
+
+ if (versionDirs.Length == 0)
+ {
+ Log.Information($"No versions: cleaning up {pluginDir.FullName}");
+ pluginDir.Delete(true);
+ continue;
+ }
+ else
+ {
+ foreach (var versionDir in versionDirs)
+ {
+ try
+ {
+ var dllFile = new FileInfo(Path.Combine(versionDir.FullName, $"{pluginDir.Name}.dll"));
+ if (!dllFile.Exists)
+ {
+ Log.Information($"Missing dll: cleaning up {versionDir.FullName}");
+ versionDir.Delete(true);
+ continue;
+ }
+
+ var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
+ if (!manifestFile.Exists)
+ {
+ Log.Information($"Missing manifest: cleaning up {versionDir.FullName}");
+ versionDir.Delete(true);
+ continue;
+ }
+
+ var manifest = LocalPluginManifest.Load(manifestFile);
+ if (manifest.Disabled)
+ {
+ Log.Information($"Disabled: cleaning up {versionDir.FullName}");
+ versionDir.Delete(true);
+ continue;
+ }
+
+ if (manifest.DalamudApiLevel < DalamudApiLevel)
+ {
+ Log.Information($"Lower API: cleaning up {versionDir.FullName}");
+ versionDir.Delete(true);
+ continue;
+ }
+
+ if (manifest.ApplicableVersion < this.dalamud.StartInfo.GameVersion)
+ {
+ Log.Information($"Inapplicable version: cleaning up {versionDir.FullName}");
+ versionDir.Delete(true);
+ continue;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Could not clean up {versionDir.FullName}");
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Could not clean up {pluginDir.FullName}");
+ }
+ }
+ }
+
+ ///
+ /// Update all plugins.
+ ///
+ /// Perform a dry run, don't install anything.
+ /// Success or failure and a list of updated plugin metadata.
+ public List UpdatePlugins(bool dryRun = false)
+ {
+ Log.Information("Starting plugin update");
+
+ var listChanged = false;
+
+ var updatedList = new List();
+
+ // Prevent collection was modified errors
+ for (var i = 0; i < this.updatablePlugins.Count; i++)
+ {
+ var metadata = this.updatablePlugins[i];
+
+ var plugin = metadata.InstalledPlugin;
+
+ // Can't update that!
+ if (plugin is LocalDevPlugin)
+ continue;
+
+ var updateStatus = new PluginUpdateStatus()
+ {
+ InternalName = plugin.Manifest.InternalName,
+ Name = plugin.Manifest.Name,
+ Version = metadata.UseTesting
+ ? metadata.UpdateManifest.TestingAssemblyVersion
+ : metadata.UpdateManifest.AssemblyVersion,
+ };
+
+ if (dryRun)
+ {
+ updateStatus.WasUpdated = true;
+ updatedList.Add(updateStatus);
+ }
+ else
+ {
+ // Unload if loaded
+ if (plugin.State == PluginState.Loaded || plugin.State == PluginState.LoadError)
+ {
+ try
+ {
+ plugin.Unload();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error during unload (update)");
+ continue;
+ }
+ }
+
+ try
+ {
+ plugin.Disable();
+ this.installedPlugins.Remove(plugin);
+ listChanged = true;
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error during disable (update)");
+ continue;
+ }
+
+ try
+ {
+ this.InstallPlugin(metadata.UpdateManifest, metadata.UseTesting);
+ listChanged = true;
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error during install (update)");
+ continue;
+ }
+ }
+ }
+
+ if (listChanged)
+ this.NotifyInstalledPluginsChanged();
+
+ Log.Information("Plugin update OK.");
+
+ return updatedList;
+ }
+
+ ///
+ /// Print to chat any plugin updates and whether they were successful.
+ ///
+ /// The list of updated plugin metadata.
+ /// The header text to send to chat prior to any update info.
+ public void PrintUpdatedPlugins(List updateMetadata, string header)
+ {
+ if (updateMetadata != null && updateMetadata.Count > 0)
+ {
+ this.dalamud.Framework.Gui.Chat.Print(header);
+
+ foreach (var metadata in updateMetadata)
+ {
+ if (metadata.WasUpdated)
+ {
+ this.dalamud.Framework.Gui.Chat.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version));
+ }
+ else
+ {
+ this.dalamud.Framework.Gui.Chat.PrintChat(new XivChatEntry
+ {
+ MessageBytes = Encoding.UTF8.GetBytes(Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version)),
+ Type = XivChatType.Urgent,
+ });
+ }
+ }
+ }
+ }
+
+ ///
+ /// For a given manifest, determine if the testing version should be used over the normal version.
+ /// The higher of the two versions is calculated after checking other settings.
+ ///
+ /// Manifest to check.
+ /// A value indicating whether testing should be used.
+ public bool UseTesting(PluginManifest manifest)
+ {
+ if (!this.dalamud.Configuration.DoPluginTest)
+ return false;
+
+ if (manifest.IsTestingExclusive)
+ return true;
+
+ var av = manifest.AssemblyVersion;
+ var tv = manifest.TestingAssemblyVersion;
+ var hasAv = av != null;
+ var hasTv = tv != null;
+
+ if (hasAv && hasTv)
+ {
+ return tv > av;
+ }
+ else
+ {
+ return hasTv;
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether the given repo manifest should be visible to the user.
+ ///
+ /// Repo manifest.
+ /// If the manifest is visible.
+ public bool IsManifestVisible(RemotePluginManifest manifest)
+ {
+ // Hidden by user
+ if (this.dalamud.Configuration.HiddenPluginInternalName.Contains(manifest.InternalName))
+ return false;
+
+ // Hidden by manifest
+ if (manifest.IsHide)
+ return false;
+
+ return true;
+ }
+
+ ///
+ /// Gets a value indicating whether the given manifest is eligible for ANYTHING. These are hard
+ /// checks that should not allow installation or loading.
+ ///
+ /// Plugin manifest.
+ /// If the manifest is eligible.
+ public bool IsManifestEligible(PluginManifest manifest)
+ {
+ // Testing exclusive
+ if (manifest.IsTestingExclusive && !this.dalamud.Configuration.DoPluginTest)
+ return false;
+
+ // Applicable version
+ if (manifest.ApplicableVersion < this.dalamud.StartInfo.GameVersion)
+ return false;
+
+ // API level
+ if (manifest.DalamudApiLevel < DalamudApiLevel)
+ return false;
+
+ // Banned
+ if (this.IsManifestBanned(manifest))
+ return false;
+
+ return true;
+ }
+
+ private bool IsManifestBanned(PluginManifest manifest)
+ {
+ return this.bannedPlugins.Any(ban => ban.Name == manifest.InternalName && ban.AssemblyVersion == manifest.AssemblyVersion);
+ }
+
+ private void DetectAvailablePluginUpdates()
+ {
+ var updatablePlugins = new List();
+
+ for (var i = 0; i < this.installedPlugins.Count; i++)
+ {
+ var plugin = this.installedPlugins[i];
+
+ var installedVersion = plugin.IsTesting
+ ? plugin.Manifest.TestingAssemblyVersion
+ : plugin.Manifest.AssemblyVersion;
+
+ var updates = this.availablePlugins
+ .Where(remoteManifest => plugin.Manifest.InternalName == remoteManifest.InternalName)
+ .Select(remoteManifest =>
+ {
+ var useTesting = this.UseTesting(remoteManifest);
+ var candidateVersion = useTesting
+ ? remoteManifest.TestingAssemblyVersion
+ : remoteManifest.AssemblyVersion;
+ var isUpdate = candidateVersion > installedVersion;
+
+ return (isUpdate, useTesting, candidateVersion, remoteManifest);
+ })
+ .Where(tpl => tpl.isUpdate)
+ .ToList();
+
+ if (updates.Count > 0)
+ {
+ var update = updates.Aggregate((t1, t2) => t1.candidateVersion > t2.candidateVersion ? t1 : t2);
+ updatablePlugins.Add(new(plugin, update.remoteManifest, update.useTesting));
+ }
+ }
+
+ this.updatablePlugins = updatablePlugins;
+ }
+
+ private void NotifyAvailablePluginsChanged()
+ {
+ this.DetectAvailablePluginUpdates();
+
+ try
+ {
+ this.AvailablePlugins = ImmutableList.CreateRange(this.availablePlugins);
+ this.UpdatablePlugins = ImmutableList.CreateRange(this.updatablePlugins);
+ this.OnAvailablePluginsChanged.Invoke();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Error notifying {nameof(this.OnAvailablePluginsChanged)}");
+ }
+ }
+
+ private void NotifyInstalledPluginsChanged()
+ {
+ this.DetectAvailablePluginUpdates();
+
+ try
+ {
+ this.InstalledPlugins = ImmutableList.CreateRange(this.installedPlugins);
+ this.UpdatablePlugins = ImmutableList.CreateRange(this.updatablePlugins);
+ this.OnInstalledPluginsChanged.Invoke();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Error notifying {nameof(this.OnInstalledPluginsChanged)}");
+ }
+ }
+
+ private struct BannedPlugin
+ {
+ [JsonProperty]
+ public string Name { get; private set; }
+
+ [JsonProperty]
+ public Version AssemblyVersion { get; private set; }
+ }
+
+ private struct PluginDef
+ {
+ public PluginDef(FileInfo dllFile, LocalPluginManifest manifest, bool isDev)
+ {
+ this.DllFile = dllFile;
+ this.Manifest = manifest;
+ this.IsDev = isDev;
+ }
+
+ public FileInfo DllFile { get; init; }
+
+ public LocalPluginManifest Manifest { get; init; }
+
+ public bool IsDev { get; init; }
+
+ public static int Sorter(PluginDef def1, PluginDef def2)
+ {
+ var prio1 = def1.Manifest?.LoadPriority ?? 0;
+ var prio2 = def2.Manifest?.LoadPriority ?? 0;
+ return prio2.CompareTo(prio1);
+ }
+ }
+
+ private static class Locs
+ {
+ public static string DalamudPluginUpdateSuccessful(string name, Version version) => Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}.").Format(name, version);
+
+ public static string DalamudPluginUpdateFailed(string name, Version version) => Loc.Localize("DalamudPluginUpdateFailed", " 》 {0} update to v{1} failed.").Format(name, version);
+ }
+ }
+
+ ///
+ /// Class responsible for loading and unloading plugins.
+ /// This contains the assembly patching functionality to resolve assembly locations.
+ ///
+ internal partial class PluginManager
+ {
+ ///
+ /// A mapping of plugin assembly name to patch data. Used to fill in missing data due to loading
+ /// plugins via byte[].
+ ///
+ internal static readonly Dictionary PluginLocations = new();
+
+ ///
+ /// Patch method for internal class RuntimeAssembly.Location, also known as Assembly.Location.
+ /// This patch facilitates resolving the assembly location for plugins that are loaded via byte[].
+ /// It should never be called manually.
+ ///
+ /// The equivalent of `this`.
+ /// The result from the original method.
+ [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Enforced naming for special injected parameters")]
+ private static void AssemblyLocationPatch(Assembly __instance, ref string __result)
+ {
+ // Assembly.GetExecutingAssembly can return this.
+ // Check for it as a special case and find the plugin.
+ if (__result.EndsWith("System.Private.CoreLib.dll", StringComparison.InvariantCultureIgnoreCase))
+ {
+ foreach (var assemblyName in GetStackFrameAssemblyNames())
+ {
+ if (PluginLocations.TryGetValue(assemblyName, out var data))
+ {
+ __result = data.Location;
+ return;
+ }
+ }
+ }
+ else if (string.IsNullOrEmpty(__result))
+ {
+ if (PluginLocations.TryGetValue(__instance.FullName, out var data))
+ {
+ __result = data.Location;
+ }
+ }
+ }
+
+ ///
+ /// Patch method for internal class RuntimeAssembly.CodeBase, also known as Assembly.CodeBase.
+ /// This patch facilitates resolving the assembly location for plugins that are loaded via byte[].
+ /// It should never be called manually.
+ ///
+ /// The equivalent of `this`.
+ /// The result from the original method.
+ [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Enforced naming for special injected parameters")]
+ private static void AssemblyCodeBasePatch(Assembly __instance, ref string __result)
+ {
+ // Assembly.GetExecutingAssembly can return this.
+ // Check for it as a special case and find the plugin.
+ if (__result.EndsWith("System.Private.CoreLib.dll"))
+ {
+ foreach (var assemblyName in GetStackFrameAssemblyNames())
+ {
+ if (PluginLocations.TryGetValue(assemblyName, out var data))
+ {
+ __result = data.Location;
+ return;
+ }
+ }
+ }
+ else if (string.IsNullOrEmpty(__result))
+ {
+ if (PluginLocations.TryGetValue(__instance.FullName, out var data))
+ {
+ __result = data.Location;
+ }
+ }
+ }
+
+ private static IEnumerable GetStackFrameAssemblyNames()
+ {
+ var stackTrace = new StackTrace();
+ var stackFrames = stackTrace.GetFrames();
+
+ foreach (var stackFrame in stackFrames)
+ {
+ var methodBase = stackFrame.GetMethod();
+ if (methodBase == null)
+ continue;
+
+ yield return methodBase.Module.Assembly.FullName;
+ }
+ }
+
+ private void ApplyPatches()
+ {
+ var harmony = new Harmony("goatcorp.dalamud.pluginmanager");
+
+ var targetType = typeof(PluginManager).Assembly.GetType();
+
+ var locationTarget = AccessTools.PropertyGetter(targetType, nameof(Assembly.Location));
+ var locationPatch = AccessTools.Method(typeof(PluginManager), nameof(PluginManager.AssemblyLocationPatch));
+ harmony.Patch(locationTarget, postfix: new(locationPatch));
+
+#pragma warning disable SYSLIB0012 // Type or member is obsolete
+ var codebaseTarget = AccessTools.PropertyGetter(targetType, nameof(Assembly.CodeBase));
+ var codebasePatch = AccessTools.Method(typeof(PluginManager), nameof(PluginManager.AssemblyCodeBasePatch));
+ harmony.Patch(codebaseTarget, postfix: new(codebasePatch));
+#pragma warning restore SYSLIB0012 // Type or member is obsolete
+ }
+
+ internal record PluginPatchData
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DLL file being loaded.
+ public PluginPatchData(FileInfo dllFile)
+ {
+ this.Location = dllFile.FullName;
+ this.CodeBase = new Uri(dllFile.FullName).AbsoluteUri;
+ }
+
+ ///
+ /// Gets simulated Assembly.Location output.
+ ///
+ public string Location { get; }
+
+ ///
+ /// Gets simulated Assembly.CodeBase output.
+ ///
+ public string CodeBase { get; }
+ }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/PluginRepository.cs b/Dalamud/Plugin/Internal/PluginRepository.cs
new file mode 100644
index 000000000..64769322d
--- /dev/null
+++ b/Dalamud/Plugin/Internal/PluginRepository.cs
@@ -0,0 +1,108 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+using Dalamud.Plugin.Internal.Types;
+using Newtonsoft.Json;
+
+namespace Dalamud.Plugin.Internal
+{
+ ///
+ /// This class represents a single plugin repository.
+ ///
+ internal partial class PluginRepository
+ {
+ private const string DalamudPluginsMasterUrl = "https://raw.githubusercontent.com/goatcorp/DalamudPlugins/master/pluginmaster.json";
+
+ private static readonly ModuleLog Log = new("PLUGINR");
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The plugin master URL.
+ /// Whether the plugin repo is enabled.
+ public PluginRepository(string pluginMasterUrl, bool isEnabled)
+ {
+ this.PluginMasterUrl = pluginMasterUrl;
+ this.IsThirdParty = pluginMasterUrl != DalamudPluginsMasterUrl;
+ this.IsEnabled = isEnabled;
+
+ // No need to wait for this
+ Task.Run(this.ReloadPluginMasterAsync);
+ }
+
+ ///
+ /// Gets a new instance of the class for the main repo.
+ ///
+ public static PluginRepository MainRepo => new(DalamudPluginsMasterUrl, true);
+
+ ///
+ /// Gets the pluginmaster.json URL.
+ ///
+ public string PluginMasterUrl { get; }
+
+ ///
+ /// Gets a value indicating whether this plugin repository is from a third party.
+ ///
+ public bool IsThirdParty { get; }
+
+ ///
+ /// Gets a value indicating whether this repo is enabled.
+ ///
+ public bool IsEnabled { get; }
+
+ ///
+ /// Gets the plugin master list of available plugins.
+ ///
+ public ReadOnlyCollection PluginMaster { get; private set; }
+
+ ///
+ /// Gets the initialization state of the plugin repository.
+ ///
+ public PluginRepositoryState State { get; private set; }
+
+ ///
+ /// Reload the plugin master asynchronously in a task.
+ ///
+ /// The new state.
+ public Task ReloadPluginMasterAsync()
+ {
+ this.State = PluginRepositoryState.InProgress;
+ this.PluginMaster = new List().AsReadOnly();
+
+ return Task.Run(() =>
+ {
+ using var client = new WebClient();
+
+ Log.Information($"Fetching repo: {this.PluginMasterUrl}");
+
+ var data = client.DownloadString(this.PluginMasterUrl);
+
+ var pluginMaster = JsonConvert.DeserializeObject>(data);
+ pluginMaster.Sort((pm1, pm2) => pm1.Name.CompareTo(pm2.Name));
+
+ // Set the source for each remote manifest. Allows for checking if is 3rd party.
+ foreach (var manifest in pluginMaster)
+ {
+ manifest.SourceRepo = this;
+ }
+
+ this.PluginMaster = pluginMaster.AsReadOnly();
+ }).ContinueWith(task =>
+ {
+ if (task.IsCompletedSuccessfully)
+ {
+ Log.Debug($"Successfully fetched repo: {this.PluginMasterUrl}");
+ this.State = PluginRepositoryState.Success;
+ }
+ else
+ {
+ Log.Error(task.Exception, $"PluginMaster failed: {this.PluginMasterUrl}");
+ this.State = PluginRepositoryState.Fail;
+ }
+ });
+ }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs b/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs
new file mode 100644
index 000000000..32dde337c
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs
@@ -0,0 +1,36 @@
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// Information about an available plugin update.
+ ///
+ internal record AvailablePluginUpdate
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The installed plugin to update.
+ /// The manifest to use for the update.
+ /// If the testing version should be used for the update.
+ public AvailablePluginUpdate(LocalPlugin installedPlugin, RemotePluginManifest updateManifest, bool useTesting)
+ {
+ this.InstalledPlugin = installedPlugin;
+ this.UpdateManifest = updateManifest;
+ this.UseTesting = useTesting;
+ }
+
+ ///
+ /// Gets the currently installed plugin.
+ ///
+ public LocalPlugin InstalledPlugin { get; init; }
+
+ ///
+ /// Gets the available update manifest.
+ ///
+ public RemotePluginManifest UpdateManifest { get; init; }
+
+ ///
+ /// Gets a value indicating whether the update should use the testing URL.
+ ///
+ public bool UseTesting { get; init; }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/IpcSubscription.cs b/Dalamud/Plugin/Internal/Types/IpcSubscription.cs
new file mode 100644
index 000000000..3427a824a
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/IpcSubscription.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Dynamic;
+
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// This class represents an IPC subscription between two plugins.
+ ///
+ internal record IpcSubscription
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The source plugin name.
+ /// The name of the plugin being subscribed to.
+ /// The subscription action.
+ public IpcSubscription(string sourcePluginName, string subPluginName, Action subAction)
+ {
+ this.SourcePluginName = sourcePluginName;
+ this.SubPluginName = subPluginName;
+ this.SubAction = subAction;
+ }
+
+ ///
+ /// Gets the name of the plugin requesting the subscription.
+ ///
+ public string SourcePluginName { get; }
+
+ ///
+ /// Gets the name of the plugin being subscribed to.
+ ///
+ public string SubPluginName { get; }
+
+ ///
+ /// Gets the subscription action.
+ ///
+ public Action SubAction { get; }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs
new file mode 100644
index 000000000..d0a3b7448
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs
@@ -0,0 +1,73 @@
+using System.IO;
+
+using Newtonsoft.Json;
+
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as
+ /// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk.
+ ///
+ internal record LocalPluginManifest : PluginManifest
+ {
+ ///
+ /// Gets or sets a value indicating whether the plugin is disabled and should not be loaded.
+ /// This value supercedes the ".disabled" file functionality and should not be included in the plugin master.
+ ///
+ public bool Disabled { get; set; } = false;
+
+ ///
+ /// Gets or sets a value indicating whether the plugin should only be loaded when testing is enabled.
+ /// This value supercedes the ".testing" file functionality and should not be included in the plugin master.
+ ///
+ public bool Testing { get; set; } = false;
+
+ ///
+ /// Gets or sets the 3rd party repo URL that this plugin was installed from. Used to display where the plugin was
+ /// sourced from on the installed plugin view. This should not be included in the plugin master.
+ ///
+ public string InstalledFromUrl { get; set; }
+
+ ///
+ /// Save a plugin manifest to file.
+ ///
+ /// Path to save at.
+ public void Save(FileInfo manifestFile) => File.WriteAllText(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented));
+
+ ///
+ /// Loads a plugin manifest from file.
+ ///
+ /// Path to the manifest.
+ /// A object.
+ public static LocalPluginManifest Load(FileInfo manifestFile) => JsonConvert.DeserializeObject(File.ReadAllText(manifestFile.FullName));
+
+ ///
+ /// A standardized way to get the plugin DLL name that should accompany a manifest file. May not exist.
+ ///
+ /// Manifest directory.
+ /// The manifest.
+ /// The file.
+ public static FileInfo GetPluginFile(DirectoryInfo dir, PluginManifest manifest) => new(Path.Combine(dir.FullName, $"{manifest.InternalName}.dll"));
+
+ ///
+ /// A standardized way to get the manifest file that should accompany a plugin DLL. May not exist.
+ ///
+ /// The plugin DLL.
+ /// The file.
+ public static FileInfo GetManifestFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, Path.GetFileNameWithoutExtension(dllFile.Name) + ".json"));
+
+ ///
+ /// A standardized way to get the obsolete .disabled file that should accompany a plugin DLL. May not exist.
+ ///
+ /// The plugin DLL.
+ /// The .disabled file.
+ public static FileInfo GetDisabledFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, ".disabled"));
+
+ ///
+ /// A standardized way to get the obsolete .testing file that should accompany a plugin DLL. May not exist.
+ ///
+ /// The plugin DLL.
+ /// The .testing file.
+ public static FileInfo GetTestingFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, ".testing"));
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs
new file mode 100644
index 000000000..3c937d710
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Collections.Generic;
+
+using Dalamud.Game;
+using Newtonsoft.Json;
+
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// Information about a plugin, packaged in a json file with the DLL.
+ ///
+ internal record PluginManifest
+ {
+ ///
+ /// Gets the author/s of the plugin.
+ ///
+ [JsonProperty]
+ public string Author { get; init; }
+
+ ///
+ /// Gets or sets the public name of the plugin.
+ ///
+ [JsonProperty]
+ public string Name { get; set; }
+
+ ///
+ /// Gets a description of the plugins functions.
+ ///
+ [JsonProperty]
+ public string Description { get; init; }
+
+ ///
+ /// Gets a list of tags defined on the plugin.
+ ///
+ [JsonProperty]
+ public List Tags { get; init; }
+
+ ///
+ /// Gets a value indicating whether or not the plugin is hidden in the plugin installer.
+ /// This value comes from the plugin master and is in addition to the list of hidden names kept by Dalamud.
+ ///
+ [JsonProperty]
+ public bool IsHide { get; init; }
+
+ ///
+ /// Gets the internal name of the plugin, which should match the assembly name of the plugin.
+ ///
+ [JsonProperty]
+ public string InternalName { get; init; }
+
+ ///
+ /// Gets the current assembly version of the plugin.
+ ///
+ [JsonProperty]
+ public Version AssemblyVersion { get; init; }
+
+ ///
+ /// Gets the current testing assembly version of the plugin.
+ ///
+ [JsonProperty]
+ public Version TestingAssemblyVersion { get; init; }
+
+ ///
+ /// Gets a value indicating whether the is not null.
+ ///
+ [JsonIgnore]
+ public bool HasAssemblyVersion => this.AssemblyVersion != null;
+
+ ///
+ /// Gets a value indicating whether the is not null.
+ ///
+ [JsonIgnore]
+ public bool HasTestingAssemblyVersion => this.TestingAssemblyVersion != null;
+
+ ///
+ /// Gets a value indicating whether the plugin is only available for testing.
+ ///
+ [JsonProperty]
+ public bool IsTestingExclusive { get; init; }
+
+ ///
+ /// Gets an URL to the website or source code of the plugin.
+ ///
+ [JsonProperty]
+ public string RepoUrl { get; init; }
+
+ ///
+ /// Gets the version of the game this plugin works with.
+ ///
+ [JsonProperty]
+ [JsonConverter(typeof(GameVersionConverter))]
+ public GameVersion ApplicableVersion { get; init; } = GameVersion.Any;
+
+ ///
+ /// Gets the API level of this plugin. For the current API level, please see
+ /// for the currently used API level.
+ ///
+ [JsonProperty]
+ public int DalamudApiLevel { get; init; } = PluginManager.DalamudApiLevel;
+
+ ///
+ /// Gets the number of downloads this plugin has.
+ ///
+ [JsonProperty]
+ public long DownloadCount { get; init; }
+
+ ///
+ /// Gets the last time this plugin was updated.
+ ///
+ [JsonProperty]
+ public long LastUpdate { get; init; }
+
+ ///
+ /// Gets the download link used to install the plugin.
+ ///
+ [JsonProperty]
+ public string DownloadLinkInstall { get; init; }
+
+ ///
+ /// Gets the download link used to update the plugin.
+ ///
+ [JsonProperty]
+ public string DownloadLinkUpdate { get; init; }
+
+ ///
+ /// Gets the download link used to get testing versions of the plugin.
+ ///
+ [JsonProperty]
+ public string DownloadLinkTesting { get; init; }
+
+ ///
+ /// Gets the load priority for this plugin. Higher values means higher priority. 0 is default priority.
+ ///
+ [JsonProperty]
+ public int LoadPriority { get; init; }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/PluginOperationResult.cs b/Dalamud/Plugin/Internal/Types/PluginOperationResult.cs
new file mode 100644
index 000000000..9ac1db611
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/PluginOperationResult.cs
@@ -0,0 +1,57 @@
+using System;
+
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// This represents the result of a an operation taken against a plugin.
+ /// Loading, unloading, installation, etc.
+ ///
+ internal enum PluginOperationResult
+ {
+ ///
+ /// The result is unknown. Should not be used.
+ ///
+ [Obsolete("Do not use this", error: true)]
+ Unknown,
+
+ ///
+ /// The result is pending. Take a seat and wait.
+ ///
+ Pending,
+
+ ///
+ /// The operation was successful.
+ ///
+ Success,
+
+ ///
+ /// During the plugin operation, an unexpected error occurred.
+ ///
+ UnknownError,
+
+ ///
+ /// The plugin state was invalid for the attempted operation.
+ ///
+ InvalidState,
+
+ ///
+ /// The plugin applicable version is not compativle with the currently running game.
+ ///
+ InvalidGameVersion,
+
+ ///
+ /// The plugin API level is not compatible with the currently running Dalamud.
+ ///
+ InvalidApiLevel,
+
+ ///
+ /// During loading, the current plugin was marked as disabled.
+ ///
+ InvalidStateDisabled,
+
+ ///
+ /// During loading, another plugin was detected with the same internal name.
+ ///
+ InvalidStateDuplicate,
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/PluginRepositoryState.cs b/Dalamud/Plugin/Internal/Types/PluginRepositoryState.cs
new file mode 100644
index 000000000..46aa2c351
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/PluginRepositoryState.cs
@@ -0,0 +1,28 @@
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// Values representing plugin repository state.
+ ///
+ internal enum PluginRepositoryState
+ {
+ ///
+ /// State is unknown.
+ ///
+ Unknown,
+
+ ///
+ /// Currently loading.
+ ///
+ InProgress,
+
+ ///
+ /// Load was successful.
+ ///
+ Success,
+
+ ///
+ /// Load failed.
+ ///
+ Fail,
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/PluginState.cs b/Dalamud/Plugin/Internal/Types/PluginState.cs
new file mode 100644
index 000000000..f32543b39
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/PluginState.cs
@@ -0,0 +1,33 @@
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// Values representing plugin load state.
+ ///
+ internal enum PluginState
+ {
+ ///
+ /// Plugin is defined, but unloaded.
+ ///
+ Unloaded,
+
+ ///
+ /// Plugin has thrown an error during unload.
+ ///
+ UnloadError,
+
+ ///
+ /// Currently loading.
+ ///
+ InProgress,
+
+ ///
+ /// Load is successful.
+ ///
+ Loaded,
+
+ ///
+ /// Plugin has thrown an error during loading.
+ ///
+ LoadError,
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs b/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs
new file mode 100644
index 000000000..f0394b9b7
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// Plugin update status.
+ ///
+ internal class PluginUpdateStatus
+ {
+ ///
+ /// Gets or sets the plugin internal name.
+ ///
+ public string InternalName { get; set; }
+
+ ///
+ /// Gets or sets the plugin name.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets the plugin version.
+ ///
+ public Version Version { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the plugin was updated.
+ ///
+ public bool WasUpdated { get; set; }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs b/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs
new file mode 100644
index 000000000..cbb989159
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs
@@ -0,0 +1,18 @@
+using Newtonsoft.Json;
+
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as
+ /// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk.
+ ///
+ internal record RemotePluginManifest : PluginManifest
+ {
+ ///
+ /// Gets or sets the plugin repository this manifest came from. Used in reporting which third party repo a manifest
+ /// may have come from in the plugins available view. This functionality should not be included in the plugin master.
+ ///
+ [JsonIgnore]
+ public PluginRepository SourceRepo { get; set; } = null;
+ }
+}
diff --git a/Dalamud/Plugin/PluginDefinition.cs b/Dalamud/Plugin/PluginDefinition.cs
deleted file mode 100644
index 088688811..000000000
--- a/Dalamud/Plugin/PluginDefinition.cs
+++ /dev/null
@@ -1,109 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Dalamud.Plugin
-{
- ///
- /// Class containing information about a plugin.
- ///
- public class PluginDefinition
- {
- ///
- /// Gets or sets the author/s of the plugin.
- ///
- public string Author { get; set; }
-
- ///
- /// Gets or sets the public name of the plugin.
- ///
- public string Name { get; set; }
-
- ///
- /// Gets or sets the internal name of the plugin, which should match the assembly name of the plugin.
- ///
- public string InternalName { get; set; }
-
- ///
- /// Gets or sets the current assembly version of the plugin.
- ///
- public string AssemblyVersion { get; set; }
-
- ///
- /// Gets or sets the current testing assembly version of the plugin.
- ///
- public string TestingAssemblyVersion { get; set; }
-
- ///
- /// Gets or sets a value indicating whether the plugin is only available for testing.
- ///
- public bool IsTestingExclusive { get; set; }
-
- ///
- /// Gets or sets a description of the plugins functions.
- ///
- public string Description { get; set; }
-
- ///
- /// Gets or sets the version of the game this plugin works with.
- ///
- public string ApplicableVersion { get; set; }
-
- ///
- /// Gets or sets an URL to the website or source code of the plugin.
- ///
- public string RepoUrl { get; set; }
-
- ///
- /// Gets or sets a list of tags defined on the plugin.
- ///
- public List Tags { get; set; }
-
- ///
- /// Gets or sets a value indicating whether or not the plugin is hidden in the plugin installer.
- ///
- public bool IsHide { get; set; }
-
- ///
- /// Gets or sets the API level of this plugin. For the current API level, please see for the currently used API level.
- ///
- public int DalamudApiLevel { get; set; }
-
- ///
- /// Gets or sets the number of downloads this plugin has.
- ///
- public long DownloadCount { get; set; }
-
- ///
- /// Gets or sets the last time this plugin was updated.
- ///
- public long LastUpdate { get; set; }
-
- ///
- /// Gets or sets the index of the third party repo.
- ///
- public int RepoNumber { get; set; }
-
- ///
- /// Gets or sets the download link used to install the plugin.
- ///
- public string DownloadLinkInstall { get; set; }
-
- ///
- /// Gets or sets the download link used to update the plugin.
- ///
- public string DownloadLinkUpdate { get; set; }
-
- ///
- /// Gets or sets the download link used to get testing versions of the plugin.
- ///
- public string DownloadLinkTesting { get; set; }
-
- ///
- /// Gets or sets the load priority for this plugin. Higher values means higher priority. 0 is default priority.
- ///
- public int LoadPriority { get; set; }
- }
-}
diff --git a/Dalamud/Plugin/PluginInstallerWindow.cs b/Dalamud/Plugin/PluginInstallerWindow.cs
deleted file mode 100644
index 3be2305e2..000000000
--- a/Dalamud/Plugin/PluginInstallerWindow.cs
+++ /dev/null
@@ -1,607 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Numerics;
-using System.Threading.Tasks;
-
-using CheapLoc;
-using Dalamud.Interface;
-using Dalamud.Interface.Colors;
-using Dalamud.Interface.Windowing;
-using ImGuiNET;
-using Serilog;
-
-namespace Dalamud.Plugin
-{
- ///
- /// Class responsible for drawing the plugin installer.
- ///
- internal class PluginInstallerWindow : Window
- {
- private readonly Dalamud dalamud;
-
- private string gameVersion;
-
- private bool errorModalDrawing = true;
- private bool errorModalOnNextFrame = false;
-
- private bool updateComplete = false;
- private int updatePluginCount = 0;
- private List updatedPlugins;
-
- private List pluginListAvailable;
- private List pluginListInstalled;
-
- private string searchText = string.Empty;
-
- private PluginSortKind sortKind = PluginSortKind.Alphabetical;
- private string filterText = Loc.Localize("SortAlphabetical", "Alphabetical");
-
- private PluginInstallStatus installStatus = PluginInstallStatus.None;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The relevant Dalamud instance.
- /// The version of the game.
- public PluginInstallerWindow(Dalamud dalamud, string gameVersion)
- : base(
- Loc.Localize("InstallerHeader", "Plugin Installer") + (dalamud.Configuration.DoPluginTest ? " (TESTING)" : string.Empty) + "###XlPluginInstaller",
- ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoScrollbar)
- {
- this.dalamud = dalamud;
- this.gameVersion = gameVersion;
-
- this.Size = new Vector2(810, 520);
- this.SizeCondition = ImGuiCond.Always;
- }
-
- private enum PluginInstallStatus
- {
- None,
- InProgress,
- Success,
- Fail,
- }
-
- private enum PluginSortKind
- {
- Alphabetical,
- DownloadCount,
- LastUpdate,
- }
-
- ///
- /// Code to be executed when the window is opened.
- ///
- public override void OnOpen()
- {
- base.OnOpen();
-
- if (this.dalamud.PluginRepository.State != PluginRepository.InitializationState.InProgress)
- this.dalamud.PluginRepository.ReloadPluginMasterAsync();
-
- this.pluginListAvailable = null;
- this.pluginListInstalled = null;
- this.updateComplete = false;
- this.updatePluginCount = 0;
- this.updatedPlugins = null;
- this.searchText = string.Empty;
- this.sortKind = PluginSortKind.Alphabetical;
- this.filterText = Loc.Localize("SortAlphabetical", "Alphabetical");
- }
-
- ///
- /// Draw the plugin installer view ImGui.
- ///
- public override void Draw()
- {
- ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5 * ImGui.GetIO().FontGlobalScale));
- var descriptionText = Loc.Localize("InstallerHint", "This window allows you to install and remove in-game plugins.\nThey are made by third-party developers.");
- ImGui.Text(descriptionText);
-
- var sortingTextSize = ImGui.CalcTextSize(Loc.Localize("SortDownloadCounts", "Download Count")) + ImGui.CalcTextSize(Loc.Localize("PluginSort", "Sort By"));
- ImGui.SameLine(ImGui.GetWindowWidth() - sortingTextSize.X - ((250 + 20) * ImGui.GetIO().FontGlobalScale));
- ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (ImGui.CalcTextSize(descriptionText).Y / 4) - 2);
- ImGui.SetCursorPosX(ImGui.GetWindowWidth() - sortingTextSize.X - ((250 + 20) * ImGui.GetIO().FontGlobalScale));
-
- ImGui.SetNextItemWidth(240 * ImGui.GetIO().FontGlobalScale);
- ImGui.InputTextWithHint("###XPlPluginInstaller_Search", Loc.Localize("InstallerSearch", "Search"), ref this.searchText, 100);
-
- ImGui.SameLine();
- ImGui.SetNextItemWidth((10 * ImGui.GetIO().FontGlobalScale) + ImGui.CalcTextSize(Loc.Localize("SortDownloadCounts", "Download Count")).X);
- if (ImGui.BeginCombo(Loc.Localize("PluginSort", "Sort By"), this.filterText, ImGuiComboFlags.NoArrowButton))
- {
- if (ImGui.Selectable(Loc.Localize("SortAlphabetical", "Alphabetical")))
- {
- this.sortKind = PluginSortKind.Alphabetical;
- this.filterText = Loc.Localize("SortAlphabetical", "Alphabetical");
-
- this.ResortPlugins();
- }
-
- if (ImGui.Selectable(Loc.Localize("SortDownloadCounts", "Download Count")))
- {
- this.sortKind = PluginSortKind.DownloadCount;
- this.filterText = Loc.Localize("SortDownloadCounts", "Download Count");
-
- this.ResortPlugins();
- }
-
- if (ImGui.Selectable(Loc.Localize("SortLastUpdate", "Last Update")))
- {
- this.sortKind = PluginSortKind.LastUpdate;
- this.filterText = Loc.Localize("SortLastUpdate", "Last Update");
-
- this.ResortPlugins();
- }
-
- ImGui.EndCombo();
- }
-
- ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5 * ImGui.GetIO().FontGlobalScale));
-
- (string Text, Vector4 Color) initializationStatusText = (null, ImGuiColors.DalamudGrey);
- if (this.dalamud.PluginRepository.State == PluginRepository.InitializationState.InProgress)
- {
- initializationStatusText.Text = Loc.Localize("InstallerLoading", "Loading plugins...");
- this.pluginListAvailable = null;
- }
- else if (this.dalamud.PluginRepository.State == PluginRepository.InitializationState.Fail)
- {
- initializationStatusText.Text = Loc.Localize("InstallerDownloadFailed", "Download failed.");
- this.pluginListAvailable = null;
- }
- else
- {
- if (this.dalamud.PluginRepository.State == PluginRepository.InitializationState.FailThirdRepo)
- {
- initializationStatusText.Text = Loc.Localize("InstallerDownloadFailedThird", "One of your third party repos is unreachable or there is no internet connection.");
- initializationStatusText.Color = ImGuiColors.DalamudRed;
- }
-
- if (this.pluginListAvailable == null)
- {
- this.RefetchPlugins();
- }
- }
-
- ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, 3) * ImGui.GetIO().FontGlobalScale);
-
- if (ImGui.BeginTabBar("PluginsTabBar", ImGuiTabBarFlags.NoTooltip))
- {
- this.DrawTab(false, initializationStatusText);
- this.DrawTab(true, initializationStatusText);
-
- ImGui.EndTabBar();
- ImGui.Separator();
- }
-
- ImGui.PopStyleVar();
-
- ImGui.Dummy(new Vector2(3f, 3f) * ImGui.GetIO().FontGlobalScale);
-
- if (this.installStatus == PluginInstallStatus.InProgress)
- {
- ImGui.Button(Loc.Localize("InstallerUpdating", "Updating..."));
- }
- else
- {
- if (this.updateComplete)
- {
- ImGui.Button(this.updatePluginCount == 0
- ? Loc.Localize("InstallerNoUpdates", "No updates found!")
- : string.Format(Loc.Localize("InstallerUpdateComplete", "{0} plugins updated!"), this.updatePluginCount));
- }
- else
- {
- if (ImGui.Button(Loc.Localize("InstallerUpdatePlugins", "Update plugins")) &&
- this.dalamud.PluginRepository.State == PluginRepository.InitializationState.Success)
- {
- this.installStatus = PluginInstallStatus.InProgress;
-
- Task.Run(() => this.dalamud.PluginRepository.UpdatePlugins()).ContinueWith(t =>
- {
- this.installStatus =
- t.Result.Success ? PluginInstallStatus.Success : PluginInstallStatus.Fail;
- this.installStatus =
- t.IsFaulted ? PluginInstallStatus.Fail : this.installStatus;
-
- if (this.installStatus == PluginInstallStatus.Success)
- {
- this.updateComplete = true;
- }
-
- if (t.Result.UpdatedPlugins != null)
- {
- this.updatePluginCount = t.Result.UpdatedPlugins.Count;
- this.updatedPlugins = t.Result.UpdatedPlugins;
- }
-
- this.errorModalDrawing = this.installStatus == PluginInstallStatus.Fail;
- this.errorModalOnNextFrame = this.installStatus == PluginInstallStatus.Fail;
-
- this.dalamud.PluginRepository.PrintUpdatedPlugins(
- this.updatedPlugins, Loc.Localize("DalamudPluginUpdates", "Updates:"));
-
- this.RefetchPlugins();
- });
- }
- }
- }
-
- ImGui.SameLine();
-
- if (ImGui.Button(Loc.Localize("SettingsInstaller", "Settings")))
- {
- this.dalamud.DalamudUi.OpenSettings();
- }
-
- var closeText = Loc.Localize("Close", "Close");
-
- ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(closeText).X - (16 * ImGui.GetIO().FontGlobalScale));
- if (ImGui.Button(closeText))
- {
- this.IsOpen = false;
- this.dalamud.Configuration.Save();
- }
-
- if (ImGui.BeginPopupModal(Loc.Localize("InstallerError", "Installer failed"), ref this.errorModalDrawing, ImGuiWindowFlags.AlwaysAutoResize))
- {
- var message = Loc.Localize(
- "InstallerErrorHint",
- "The plugin installer ran into an issue or the plugin is incompatible.\nPlease restart the game and report this error on our discord.");
-
- if (this.updatedPlugins != null)
- {
- if (this.updatedPlugins.Any(x => x.WasUpdated == false))
- {
- var extraInfoMessage = Loc.Localize(
- "InstallerErrorPluginInfo",
- "\n\nThe following plugins caused these issues:\n\n{0}\nYou may try removing these plugins manually and reinstalling them.");
-
- var insert = this.updatedPlugins.Where(x => x.WasUpdated == false)
- .Aggregate(
- string.Empty,
- (current, pluginUpdateStatus) =>
- current + $"* {pluginUpdateStatus.InternalName}\n");
- extraInfoMessage = string.Format(extraInfoMessage, insert);
- message += extraInfoMessage;
- }
- }
-
- ImGui.Text(message);
-
- ImGui.Spacing();
-
- if (ImGui.Button(Loc.Localize("OK", "OK"), new Vector2(120, 40)))
- {
- ImGui.CloseCurrentPopup();
- }
-
- ImGui.EndPopup();
- }
-
- if (this.errorModalOnNextFrame)
- {
- ImGui.OpenPopup(Loc.Localize("InstallerError", "Installer failed"));
- this.errorModalOnNextFrame = false;
- }
- }
-
- private void RefetchPlugins()
- {
- var hiddenPlugins = this.dalamud.PluginManager.Plugins.Where(
- x => this.dalamud.PluginRepository.PluginMaster.All(
- y => y.InternalName != x.Definition.InternalName || (y.InternalName == x.Definition.InternalName && y.IsHide))).Select(x => x.Definition).ToList();
- this.pluginListInstalled = this.dalamud.PluginRepository.PluginMaster
- .Where(def =>
- {
- return this.dalamud.PluginManager.Plugins.Where(x => x.Definition != null).Any(
- x => x.Definition.InternalName == def.InternalName);
- })
- .GroupBy(x => new { x.InternalName, x.AssemblyVersion })
- .Select(y => y.First()).ToList();
- this.pluginListInstalled.AddRange(hiddenPlugins);
- this.pluginListInstalled.Sort((x, y) => x.Name.CompareTo(y.Name));
-
- this.ResortPlugins();
- }
-
- private void ResortPlugins()
- {
- var availableDefs = this.dalamud.PluginRepository.PluginMaster.Where(
- x => this.pluginListInstalled.All(y => x.InternalName != y.InternalName))
- .GroupBy(x => new { x.InternalName, x.AssemblyVersion })
- .Select(y => y.First()).ToList();
-
- switch (this.sortKind)
- {
- case PluginSortKind.Alphabetical:
- this.pluginListAvailable = availableDefs.OrderBy(x => x.Name).ToList();
- this.pluginListInstalled.Sort((x, y) => x.Name.CompareTo(y.Name));
- break;
- case PluginSortKind.DownloadCount:
- this.pluginListAvailable = availableDefs.OrderByDescending(x => x.DownloadCount).ToList();
- this.pluginListInstalled.Sort((x, y) => y.DownloadCount.CompareTo(x.DownloadCount));
- break;
- case PluginSortKind.LastUpdate:
- this.pluginListAvailable = availableDefs.OrderByDescending(x => x.LastUpdate).ToList();
- this.pluginListInstalled.Sort((x, y) => y.LastUpdate.CompareTo(x.LastUpdate));
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
-
- private void DrawTab(bool installed, (string Text, Vector4 Color) statusText)
- {
- if (ImGui.BeginTabItem(installed ? Loc.Localize("InstallerInstalledPluginList", "Installed Plugins")
- : Loc.Localize("InstallerAvailablePluginList", "Available Plugins")))
- {
- ImGui.BeginChild(
- "Scrolling" + (installed ? "Installed" : "Available"),
- new Vector2(0, 384 * ImGui.GetIO().FontGlobalScale),
- true,
- ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoBackground);
- ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 5);
-
- if (statusText.Text != null)
- ImGui.TextColored(statusText.Color, statusText.Text);
- var list = installed ? this.pluginListInstalled : this.pluginListAvailable;
- if (list != null)
- {
- this.DrawPluginList(list, installed);
- }
-
- ImGui.EndChild();
- ImGui.EndTabItem();
- }
- }
-
- private void DrawPluginList(List pluginDefinitions, bool installed)
- {
- var didAny = false;
- var didAnyWithSearch = false;
- var hasSearchString = !string.IsNullOrWhiteSpace(this.searchText);
-
- for (var index = 0; index < pluginDefinitions.Count; index++)
- {
- var pluginDefinition = pluginDefinitions[index];
-
- if (pluginDefinition.ApplicableVersion != this.gameVersion &&
- pluginDefinition.ApplicableVersion != "any")
- continue;
-
- if (pluginDefinition.IsHide)
- continue;
-
- if (pluginDefinition.DalamudApiLevel < PluginManager.DalamudApiLevel)
- continue;
-
- if (this.dalamud.Configuration.HiddenPluginInternalName.Contains(pluginDefinition.InternalName))
- continue;
-
- didAny = true;
-
- if (hasSearchString &&
- !(pluginDefinition.Name.ToLowerInvariant().Contains(this.searchText.ToLowerInvariant()) ||
- string.Equals(pluginDefinition.Author, this.searchText, StringComparison.InvariantCultureIgnoreCase) ||
- (pluginDefinition.Tags != null && pluginDefinition.Tags.Contains(
- this.searchText.ToLowerInvariant(),
- StringComparer.InvariantCultureIgnoreCase))))
- continue;
-
- didAnyWithSearch = true;
-
- var isInstalled = this.dalamud.PluginManager.Plugins.Where(x => x.Definition != null).Any(
- x => x.Definition.InternalName == pluginDefinition.InternalName);
-
- var isTestingAvailable = false;
- if (Version.TryParse(pluginDefinition.AssemblyVersion, out var assemblyVersion) &&
- Version.TryParse(pluginDefinition.TestingAssemblyVersion, out var testingAssemblyVersion))
- {
- isTestingAvailable = this.dalamud.Configuration.DoPluginTest &&
- testingAssemblyVersion > assemblyVersion;
- }
-
- if (this.dalamud.Configuration.DoPluginTest && pluginDefinition.IsTestingExclusive)
- isTestingAvailable = true;
- else if (!installed && !this.dalamud.Configuration.DoPluginTest && pluginDefinition.IsTestingExclusive) continue;
-
- var label = string.Empty;
- if (isInstalled && !installed)
- {
- label += Loc.Localize("InstallerInstalled", " (installed)");
- }
- else if (!isInstalled && installed)
- {
- label += Loc.Localize("InstallerDisabled", " (disabled)");
- }
-
- if (this.updatedPlugins != null &&
- this.updatedPlugins.Any(x => x.InternalName == pluginDefinition.InternalName && x.WasUpdated))
- label += Loc.Localize("InstallerUpdated", " (updated)");
- else if (this.updatedPlugins != null &&
- this.updatedPlugins.Any(x => x.InternalName == pluginDefinition.InternalName &&
- x.WasUpdated == false))
- label += Loc.Localize("InstallerUpdateFailed", " (update failed)");
-
- if (isTestingAvailable)
- label += Loc.Localize("InstallerTestingVersion", " (testing version)");
-
- ImGui.PushID(pluginDefinition.InternalName + pluginDefinition.AssemblyVersion + installed + index);
-
- if (ImGui.CollapsingHeader(pluginDefinition.Name + label + "###Header" + pluginDefinition.InternalName))
- {
- ImGui.Indent();
-
- ImGui.Text(pluginDefinition.Name);
-
- ImGui.SameLine();
-
- var info = $" by {pluginDefinition.Author}";
- info += pluginDefinition.DownloadCount != 0
- ? $", {pluginDefinition.DownloadCount} downloads"
- : ", download count unavailable";
- if (pluginDefinition.RepoNumber != 0)
- info += $", from custom plugin repository #{pluginDefinition.RepoNumber}";
- ImGui.TextColored(ImGuiColors.DalamudGrey3, info);
-
- if (!string.IsNullOrWhiteSpace(pluginDefinition.Description))
- ImGui.TextWrapped(pluginDefinition.Description);
-
- if (!isInstalled)
- {
- if (this.installStatus == PluginInstallStatus.InProgress)
- {
- ImGui.Button(Loc.Localize("InstallerInProgress", "Install in progress..."));
- }
- else
- {
- var versionString = isTestingAvailable
- ? pluginDefinition.TestingAssemblyVersion + " (testing version)"
- : pluginDefinition.AssemblyVersion;
-
- if (ImGui.Button($"Install v{versionString}"))
- {
- this.installStatus = PluginInstallStatus.InProgress;
-
- Task.Run(() => this.dalamud.PluginRepository.InstallPlugin(pluginDefinition, true, false, isTestingAvailable)).ContinueWith(t =>
- {
- this.installStatus =
- t.Result ? PluginInstallStatus.Success : PluginInstallStatus.Fail;
- this.installStatus =
- t.IsFaulted ? PluginInstallStatus.Fail : this.installStatus;
-
- this.errorModalDrawing = this.installStatus == PluginInstallStatus.Fail;
- this.errorModalOnNextFrame = this.installStatus == PluginInstallStatus.Fail;
- });
- }
- }
-
- if (!string.IsNullOrEmpty(pluginDefinition.RepoUrl))
- {
- ImGui.PushFont(InterfaceManager.IconFont);
-
- ImGui.SameLine();
- if (ImGui.Button(FontAwesomeIcon.Globe.ToIconString()) &&
- pluginDefinition.RepoUrl.StartsWith("https://"))
- Process.Start(pluginDefinition.RepoUrl);
-
- ImGui.PopFont();
- }
- }
- else
- {
- var installedPlugin = this.dalamud.PluginManager.Plugins.Where(x => x.Definition != null).First(
- x => x.Definition.InternalName ==
- pluginDefinition.InternalName);
-
- var commands = this.dalamud.CommandManager.Commands.Where(
- x => x.Value.LoaderAssemblyName == installedPlugin.Definition?.InternalName &&
- x.Value.ShowInHelp);
- if (commands.Any())
- {
- ImGui.Dummy(new Vector2(10f, 10f) * ImGui.GetIO().FontGlobalScale);
- foreach (var command in commands)
- ImGui.TextWrapped($"{command.Key} → {command.Value.HelpMessage}");
- }
-
- ImGui.NewLine();
-
- if (!installedPlugin.IsRaw)
- {
- ImGui.SameLine();
-
- if (ImGui.Button(Loc.Localize("InstallerDisable", "Disable")))
- {
- try
- {
- this.dalamud.PluginManager.DisablePlugin(installedPlugin.Definition);
- }
- catch (Exception exception)
- {
- Log.Error(exception, "Could not disable plugin.");
- this.errorModalDrawing = true;
- this.errorModalOnNextFrame = true;
- }
- }
- }
-
- if (installedPlugin.PluginInterface.UiBuilder.HasConfigUi)
- {
- ImGui.SameLine();
-
- if (ImGui.Button(Loc.Localize("InstallerOpenConfig", "Open Configuration")))
- installedPlugin.PluginInterface.UiBuilder.OpenConfigUi();
- }
-
- if (!string.IsNullOrEmpty(installedPlugin.Definition.RepoUrl))
- {
- ImGui.PushFont(InterfaceManager.IconFont);
-
- ImGui.SameLine();
- if (ImGui.Button(FontAwesomeIcon.Globe.ToIconString()) &&
- installedPlugin.Definition.RepoUrl.StartsWith("https://"))
- Process.Start(installedPlugin.Definition.RepoUrl);
-
- ImGui.PopFont();
- }
-
- ImGui.SameLine();
- ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{installedPlugin.Definition.AssemblyVersion}");
-
- if (installedPlugin.IsRaw)
- {
- ImGui.SameLine();
- ImGui.TextColored(
- ImGuiColors.DalamudRed,
- this.dalamud.PluginRepository.PluginMaster.Any(x => x.InternalName == installedPlugin.Definition.InternalName)
- ? " This plugin is available in one of your repos, please remove it from the devPlugins folder."
- : " To disable this plugin, please remove it from the devPlugins folder.");
- }
- }
-
- ImGui.Unindent();
- }
-
- if (ImGui.BeginPopupContextItem("item context menu"))
- {
- if (ImGui.Selectable("Hide from installer"))
- this.dalamud.Configuration.HiddenPluginInternalName.Add(pluginDefinition.InternalName);
- ImGui.EndPopup();
- }
-
- ImGui.PopID();
- }
-
- if (!didAny)
- {
- if (installed)
- {
- ImGui.TextColored(
- ImGuiColors.DalamudGrey,
- Loc.Localize(
- "InstallerNoInstalled",
- "No plugins are currently installed. You can install them from the Available Plugins tab."));
- }
- else
- {
- ImGui.TextColored(
- ImGuiColors.DalamudGrey,
- Loc.Localize(
- "InstallerNoCompatible",
- "No compatible plugins were found :( Please restart your game and try again."));
- }
- }
- else if (!didAnyWithSearch)
- {
- ImGui.TextColored(
- ImGuiColors.DalamudGrey2,
- Loc.Localize("InstallNoMatching", "No plugins were found matching your search."));
- }
- }
- }
-}
diff --git a/Dalamud/Plugin/PluginLoadReason.cs b/Dalamud/Plugin/PluginLoadReason.cs
deleted file mode 100644
index 789d4d094..000000000
--- a/Dalamud/Plugin/PluginLoadReason.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace Dalamud.Plugin
-{
- ///
- /// This enum reflects reasons for loading a plugin.
- ///
- public enum PluginLoadReason
- {
- ///
- /// We don't know why this plugin was loaded.
- ///
- Unknown,
-
- ///
- /// This plugin was loaded because it was installed with the plugin installer.
- ///
- Installer,
-
- ///
- /// This plugin was loaded because the game was started or Dalamud was reinjected.
- ///
- Boot,
- }
-}
diff --git a/Dalamud/Plugin/PluginManager.cs b/Dalamud/Plugin/PluginManager.cs
deleted file mode 100644
index f45b0fc91..000000000
--- a/Dalamud/Plugin/PluginManager.cs
+++ /dev/null
@@ -1,387 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Dynamic;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-
-using Dalamud.Configuration;
-using Newtonsoft.Json;
-using Serilog;
-
-namespace Dalamud.Plugin
-{
- ///
- /// Class responsible for loading and unloading plugins.
- ///
- internal class PluginManager
- {
- ///
- /// The current Dalamud API level, used to handle breaking changes. Only plugins with this level will be loaded.
- ///
- public const int DalamudApiLevel = 3;
-
- private readonly Dalamud dalamud;
- private readonly string pluginDirectory;
- private readonly string devPluginDirectory;
-
- private readonly PluginConfigurations pluginConfigs;
-
- private readonly Type interfaceType = typeof(IDalamudPlugin);
-
- private readonly List bannedPlugins;
-
- private IEnumerable<(FileInfo DllFile, PluginDefinition Definition, bool IsRaw)> deferredPlugins;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The instance to load plugins with.
- /// The directory for regular plugins.
- /// The directory for dev plugins.
- public PluginManager(Dalamud dalamud, string pluginDirectory, string devPluginDirectory)
- {
- this.dalamud = dalamud;
- this.pluginDirectory = pluginDirectory;
- this.devPluginDirectory = devPluginDirectory;
-
- this.Plugins = new List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface, bool IsRaw)>();
- this.IpcSubscriptions = new List<(string SourcePluginName, string SubPluginName, Action SubAction)>();
-
- this.pluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(dalamud.StartInfo.ConfigurationPath), "pluginConfigs"));
-
- this.bannedPlugins = JsonConvert.DeserializeObject>(
- File.ReadAllText(Path.Combine(this.dalamud.StartInfo.AssetDirectory, "UIRes", "bannedplugin.json")));
-
- // Try to load missing assemblies from the local directory of the requesting assembly
- // This would usually be implicit when using Assembly.Load(), but Assembly.LoadFile() doesn't do it...
- // This handler should only be invoked on things that fail regular lookups, but it *is* global to this appdomain
- AppDomain.CurrentDomain.AssemblyResolve += (object source, ResolveEventArgs e) =>
- {
- try
- {
- Log.Debug($"Resolving missing assembly {e.Name}");
-
- // This looks weird but I'm pretty sure it's actually correct. Pretty sure. Probably.
- var assemblyPath = Path.Combine(
- Path.GetDirectoryName(e.RequestingAssembly.Location),
- new AssemblyName(e.Name).Name + ".dll");
-
- if (!File.Exists(assemblyPath))
- {
- Log.Error($"Assembly not found at {assemblyPath}");
- return null;
- }
-
- return Assembly.LoadFrom(assemblyPath);
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Could not load assembly " + e.Name);
- return null;
- }
- };
- }
-
- ///
- /// Gets a list of all loaded plugins.
- ///
- public List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface, bool IsRaw)> Plugins { get; private set; }
-
- ///
- /// Gets a list of all IPC subscriptions.
- ///
- public List<(string SourcePluginName, string SubPluginName, Action SubAction)> IpcSubscriptions { get; private set; }
-
- ///
- /// Unload all plugins.
- ///
- public void UnloadPlugins()
- {
- if (this.Plugins == null)
- return;
-
- for (var i = 0; i < this.Plugins.Count; i++)
- {
- this.Plugins[i].Plugin.Dispose();
- }
-
- this.Plugins.Clear();
- }
-
- ///
- /// Load plugins that need to be loaded synchronously and prepare plugins that can be loaded asynchronously.
- ///
- public void LoadSynchronousPlugins()
- {
- var loadDirectories = new List<(DirectoryInfo DirInfo, bool IsRaw)>
- {
- (new DirectoryInfo(this.pluginDirectory), false),
- (new DirectoryInfo(this.devPluginDirectory), true),
- };
-
- var pluginDefs = new List<(FileInfo DllFile, PluginDefinition Definition, bool IsRaw)>();
- foreach (var (dirInfo, isRaw) in loadDirectories)
- {
- if (!dirInfo.Exists) continue;
-
- var pluginDlls = dirInfo.GetFiles("*.dll", SearchOption.AllDirectories).Where(x => x.Extension == ".dll");
-
- // Preload definitions to be able to determine load order
- foreach (var dllFile in pluginDlls)
- {
- var defJson = new FileInfo(Path.Combine(dllFile.Directory.FullName, $"{Path.GetFileNameWithoutExtension(dllFile.Name)}.json"));
- PluginDefinition def = null;
- if (defJson.Exists)
- def = JsonConvert.DeserializeObject(File.ReadAllText(defJson.FullName));
- pluginDefs.Add((dllFile, def, isRaw));
- }
- }
-
- // Sort for load order - unloaded definitions have default priority of 0
- pluginDefs.Sort(
- (info1, info2) =>
- {
- var prio1 = info1.Definition?.LoadPriority ?? 0;
- var prio2 = info2.Definition?.LoadPriority ?? 0;
- return prio2.CompareTo(prio1);
- });
-
- this.deferredPlugins = pluginDefs.Where(x => x.Definition == null || x.Definition.LoadPriority <= 0);
-
- // Pass preloaded definitions for "synchronous load" plugins to LoadPluginFromAssembly, because we already loaded them anyways
- foreach (var (dllFile, definition, isRaw) in pluginDefs.Where(x => x.Definition?.LoadPriority > 0))
- {
- try
- {
- this.LoadPluginFromAssembly(dllFile, isRaw, PluginLoadReason.Boot, definition);
- }
- catch (Exception ex)
- {
- Log.Error(ex, $"Plugin load for {dllFile.FullName} failed.");
- if (ex is ReflectionTypeLoadException typeLoadException)
- {
- foreach (var exception in typeLoadException.LoaderExceptions)
- {
- Log.Error(exception, "LoaderException:");
- }
- }
- }
- }
- }
-
- ///
- /// Load plugins that have been explicitly deferred.
- ///
- public void LoadDeferredPlugins()
- {
- if (this.deferredPlugins == null)
- throw new Exception("Synchronous plugins need to be loaded before deferred plugins.");
-
- // Pass preloaded definitions for "deferred load" plugins to LoadPluginFromAssembly, because we already loaded them anyways
- foreach (var (dllFile, definition, isRaw) in this.deferredPlugins)
- {
- try
- {
- this.LoadPluginFromAssembly(dllFile, isRaw, PluginLoadReason.Boot, definition);
- }
- catch (Exception ex)
- {
- Log.Error(ex, $"Plugin load for {dllFile.FullName} failed.");
- if (ex is ReflectionTypeLoadException typeLoadException)
- {
- foreach (var exception in typeLoadException.LoaderExceptions)
- {
- Log.Error(exception, "LoaderException:");
- }
- }
- }
- }
- }
-
- ///
- /// Disable/unload a single plugin.
- ///
- /// The plugin definition of the plugin to be disabled/unloaded.
- public void DisablePlugin(PluginDefinition definition)
- {
- var thisPlugin = this.Plugins.Where(x => x.Definition != null)
- .First(x => x.Definition.InternalName == definition.InternalName);
-
- var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory, definition.InternalName, definition.AssemblyVersion));
-
- // Need to do it with Open so the file handle gets closed immediately
- // TODO: Don't use the ".disabled" crap, do it in a config
- try
- {
- File.Open(Path.Combine(outputDir.FullName, ".disabled"), FileMode.Create).Close();
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Could not create the .disabled file, disabling all versions...");
- foreach (var version in outputDir.Parent.GetDirectories())
- {
- if (!File.Exists(Path.Combine(version.FullName, ".disabled")))
- File.Open(Path.Combine(version.FullName, ".disabled"), FileMode.Create).Close();
- }
- }
-
- thisPlugin.Plugin.Dispose();
-
- this.Plugins.Remove(thisPlugin);
- }
-
- ///
- /// Load a plugin from an assembly.
- ///
- /// The associated with the main assembly of this plugin.
- /// Whether or not the plugin is a dev plugin.
- /// The reason this plugin was loaded.
- /// The already loaded definition, if available.
- /// Whether or not the plugin was loaded successfully.
- public bool LoadPluginFromAssembly(FileInfo dllFile, bool isRaw, PluginLoadReason reason, PluginDefinition pluginDef = null)
- {
- Log.Information("Loading plugin at {0}", dllFile.Directory.FullName);
-
- // If this entire folder has been marked as a disabled plugin, don't even try to load anything
- var disabledFile = new FileInfo(Path.Combine(dllFile.Directory.FullName, ".disabled"));
-
- // should raw/dev plugins really not respect this?
- if (disabledFile.Exists && !isRaw)
- {
- Log.Information("Plugin {0} is disabled.", dllFile.FullName);
- return false;
- }
-
- var testingFile = new FileInfo(Path.Combine(dllFile.Directory.FullName, ".testing"));
- if (testingFile.Exists && !this.dalamud.Configuration.DoPluginTest)
- {
- Log.Information("Plugin {0} was testing, but testing is disabled.", dllFile.FullName);
- return false;
- }
-
- // Preloaded
- if (pluginDef == null)
- {
- // read the plugin def if present - again, fail before actually trying to load the dll if there is a problem
- var defJsonFile = new FileInfo(Path.Combine(dllFile.Directory.FullName, $"{Path.GetFileNameWithoutExtension(dllFile.Name)}.json"));
-
- // load the definition if it exists, even for raw/developer plugins
- if (defJsonFile.Exists)
- {
- Log.Information("Loading definition for plugin DLL {0}", dllFile.FullName);
-
- pluginDef =
- JsonConvert.DeserializeObject(
- File.ReadAllText(defJsonFile.FullName));
- }
- }
-
- // Perform checks
- if (!isRaw)
- {
- if (pluginDef != null)
- {
- if (pluginDef.ApplicableVersion != this.dalamud.StartInfo.GameVersion &&
- pluginDef.ApplicableVersion != "any")
- {
- Log.Information("Plugin {0} has not applicable version.", dllFile.FullName);
- return false;
- }
-
- if (pluginDef.DalamudApiLevel < DalamudApiLevel)
- {
- Log.Error("Incompatible API level: {0}", dllFile.FullName);
- return false;
- }
-
- if (this.bannedPlugins.Any(x => x.Name == pluginDef.InternalName &&
- x.AssemblyVersion == pluginDef.AssemblyVersion))
- {
- Log.Error($"[PLUGINM] Banned plugin {pluginDef.InternalName} {pluginDef.AssemblyVersion}");
- return false;
- }
- }
- else
- {
- Log.Information("Plugin DLL {0} has no definition.", dllFile.FullName);
- return false;
- }
- }
-
- // TODO: given that it exists, the pluginDef's InternalName should probably be used
- // as the actual assembly to load
- // But plugins should also probably be loaded by directory and not by looking for every dll
- Log.Information("Loading assembly at {0}", dllFile);
-
- // Assembly.Load() by name here will not load multiple versions with the same name, in the case of updates
- var pluginAssembly = Assembly.LoadFile(dllFile.FullName);
-
- Log.Information("Loading types for {0}", pluginAssembly.FullName);
- var types = pluginAssembly.GetTypes();
- foreach (var type in types)
- {
- if (type.IsInterface || type.IsAbstract)
- {
- continue;
- }
-
- if (type.GetInterface(this.interfaceType.FullName) != null)
- {
- if (this.Plugins.Any(x => x.Plugin.GetType().Assembly.GetName().Name == type.Assembly.GetName().Name))
- {
- Log.Error("Duplicate plugin found: {0}", dllFile.FullName);
- return false;
- }
-
- Log.Verbose("Plugin CreateInstance...");
-
- var plugin = (IDalamudPlugin)Activator.CreateInstance(type);
-
- // this happens for raw plugins that don't specify a PluginDefinition - just generate a dummy one to avoid crashes anywhere
- pluginDef ??= new PluginDefinition
- {
- Author = "developer",
- Name = plugin.Name,
- InternalName = Path.GetFileNameWithoutExtension(dllFile.Name),
- AssemblyVersion = plugin.GetType().Assembly.GetName().Version.ToString(),
- Description = string.Empty,
- ApplicableVersion = "any",
- IsHide = false,
- DalamudApiLevel = DalamudApiLevel,
- };
-
- Log.Verbose("Plugin Initialize...");
-
- var dalamudInterface = new DalamudPluginInterface(this.dalamud, type.Assembly.GetName().Name, this.pluginConfigs, reason);
- plugin.Initialize(dalamudInterface);
-
- Log.Information("Loaded plugin: {0}", plugin.Name);
- this.Plugins.Add((plugin, pluginDef, dalamudInterface, isRaw));
-
- return true;
- }
- }
-
- Log.Information("Plugin DLL {0} has no plugin interface.", dllFile.FullName);
-
- return false;
- }
-
- ///
- /// Unload and reload all plugins.
- ///
- public void ReloadPlugins()
- {
- this.UnloadPlugins();
- this.LoadSynchronousPlugins();
- }
-
- private class BannedPlugin
- {
- public string Name { get; set; }
-
- public string AssemblyVersion { get; set; }
- }
- }
-}
diff --git a/Dalamud/Plugin/PluginRepository.cs b/Dalamud/Plugin/PluginRepository.cs
deleted file mode 100644
index fe936f94f..000000000
--- a/Dalamud/Plugin/PluginRepository.cs
+++ /dev/null
@@ -1,571 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.IO;
-using System.IO.Compression;
-using System.Linq;
-using System.Net;
-using System.Reflection;
-using System.Text;
-using System.Threading.Tasks;
-
-using CheapLoc;
-using Dalamud.Game.Text;
-using Newtonsoft.Json;
-using Serilog;
-
-namespace Dalamud.Plugin
-{
- ///
- /// This class represents a single plugin repository.
- ///
- internal class PluginRepository
- {
- private const string PluginMasterUrl = "https://raw.githubusercontent.com/goatcorp/DalamudPlugins/master/pluginmaster.json";
-
- private readonly Dalamud dalamud;
- private string pluginDirectory;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The Dalamud instance.
- /// The plugin directory path.
- /// The current game version.
- public PluginRepository(Dalamud dalamud, string pluginDirectory, string gameVersion)
- {
- this.dalamud = dalamud;
- this.pluginDirectory = pluginDirectory;
-
- this.ReloadPluginMasterAsync();
- }
-
- ///
- /// Values representing plugin initialization state.
- ///
- public enum InitializationState
- {
- ///
- /// State is unknown.
- ///
- Unknown,
-
- ///
- /// State is in progress.
- ///
- InProgress,
-
- ///
- /// State is successful.
- ///
- Success,
-
- ///
- /// State is failure.
- ///
- Fail,
-
- ///
- /// State is failure, for a 3rd party repo plugin.
- ///
- FailThirdRepo,
- }
-
- ///
- /// Gets the plugin master list of available plugins.
- ///
- public ReadOnlyCollection PluginMaster { get; private set; }
-
- ///
- /// Gets the initialization state of the plugin repository.
- ///
- public InitializationState State { get; private set; }
-
- ///
- /// Reload the plugin master asynchronously in a task.
- ///
- public void ReloadPluginMasterAsync()
- {
- this.State = InitializationState.InProgress;
-
- Task.Run(() =>
- {
- this.PluginMaster = null;
-
- var allPlugins = new List();
-
- var repos = this.dalamud.Configuration.ThirdRepoList.Where(x => x.IsEnabled).Select(x => x.Url)
- .Prepend(PluginMasterUrl).ToArray();
-
- using var client = new WebClient();
-
- var repoNumber = 0;
- var anyError = false;
- foreach (var repo in repos)
- {
- Log.Information("[PLUGINR] Fetching repo: {0}", repo);
-
- try
- {
- var data = client.DownloadString(repo);
-
- var unsortedPluginMaster = JsonConvert.DeserializeObject>(data);
-
- foreach (var pluginDefinition in unsortedPluginMaster)
- {
- pluginDefinition.RepoNumber = repoNumber;
- }
-
- allPlugins.AddRange(unsortedPluginMaster);
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Could not download PluginMaster");
-
- this.State = repos.Length > 1 ? InitializationState.FailThirdRepo : InitializationState.Fail;
-
- anyError = true;
- }
-
- repoNumber++;
- }
-
- this.PluginMaster = allPlugins.AsReadOnly();
- if (!anyError)
- {
- this.State = InitializationState.Success;
- }
- }).ContinueWith(t =>
- {
- if (t.IsFaulted)
- this.State = InitializationState.Fail;
- });
- }
-
- ///
- /// Install a plugin.
- ///
- /// The plugin definition.
- /// Whether the plugin should be immediately enabled.
- /// Whether this install is an update.
- /// Whether this install is flagged as testing.
- /// Success or failure.
- public bool InstallPlugin(PluginDefinition definition, bool enableAfterInstall = true, bool isUpdate = false, bool fromTesting = false)
- {
- try
- {
- using var client = new WebClient();
-
- var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory, definition.InternalName, fromTesting ? definition.TestingAssemblyVersion : definition.AssemblyVersion));
- var dllFile = new FileInfo(Path.Combine(outputDir.FullName, $"{definition.InternalName}.dll"));
- var disabledFile = new FileInfo(Path.Combine(outputDir.FullName, ".disabled"));
- var testingFile = new FileInfo(Path.Combine(outputDir.FullName, ".testing"));
- var wasDisabled = disabledFile.Exists;
-
- if (dllFile.Exists && enableAfterInstall)
- {
- if (disabledFile.Exists)
- disabledFile.Delete();
-
- return this.dalamud.PluginManager.LoadPluginFromAssembly(dllFile, false, PluginLoadReason.Installer);
- }
-
- if (dllFile.Exists && !enableAfterInstall)
- {
- return true;
- }
-
- try
- {
- if (outputDir.Exists)
- outputDir.Delete(true);
- outputDir.Create();
- }
- catch
- {
- // ignored, since the plugin may be loaded already
- }
-
- var path = Path.GetTempFileName();
-
- var doTestingDownload = false;
- if ((Version.TryParse(definition.TestingAssemblyVersion, out var testingAssemblyVer) || definition.IsTestingExclusive)
- && fromTesting)
- {
- doTestingDownload = testingAssemblyVer > Version.Parse(definition.AssemblyVersion) || definition.IsTestingExclusive;
- }
-
- var url = definition.DownloadLinkInstall;
- if (doTestingDownload)
- url = definition.DownloadLinkTesting;
- else if (isUpdate)
- url = definition.DownloadLinkUpdate;
-
- Log.Information("Downloading plugin to {0} from {1} doTestingDownload:{2} isTestingExclusive:{3}", path, url, doTestingDownload, definition.IsTestingExclusive);
-
- client.DownloadFile(url, path);
-
- Log.Information("Extracting to {0}", outputDir);
-
- ZipFile.ExtractToDirectory(path, outputDir.FullName);
-
- if (wasDisabled || !enableAfterInstall)
- {
- disabledFile.Create().Close();
- return true;
- }
-
- if (doTestingDownload)
- {
- testingFile.Create().Close();
- }
- else
- {
- if (testingFile.Exists)
- testingFile.Delete();
- }
-
- return this.dalamud.PluginManager.LoadPluginFromAssembly(dllFile, false, PluginLoadReason.Installer);
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Plugin download failed hard.");
- if (ex is ReflectionTypeLoadException typeLoadException)
- {
- foreach (var exception in typeLoadException.LoaderExceptions)
- {
- Log.Error(exception, "LoaderException:");
- }
- }
-
- return false;
- }
- }
-
- ///
- /// Update all plugins.
- ///
- /// Perform a dry run of the update and skip the actual installation.
- /// A tuple of whether the update was successful and the list of updated plugins.
- public (bool Success, List UpdatedPlugins) UpdatePlugins(bool dryRun = false)
- {
- Log.Information("Starting plugin update... dry:{0}", dryRun);
-
- var updatedList = new List();
- var hasError = false;
-
- try
- {
- var pluginsDirectory = new DirectoryInfo(this.pluginDirectory);
- foreach (var installed in pluginsDirectory.GetDirectories())
- {
- try
- {
- var versions = installed.GetDirectories();
-
- if (versions.Length == 0)
- {
- Log.Information("Has no versions: {0}", installed.FullName);
- continue;
- }
-
- var sortedVersions = versions.OrderBy(dirInfo =>
- {
- var success = Version.TryParse(dirInfo.Name, out var version);
- if (!success)
- {
- Log.Debug("Unparseable version: {0}", dirInfo.Name);
- }
-
- return version;
- });
- var latest = sortedVersions.Last();
-
- var isEnabled = !File.Exists(Path.Combine(latest.FullName, ".disabled"));
- if (!isEnabled && File.Exists(Path.Combine(latest.FullName, ".testing")))
- {
- // In case testing is installed, but stable is enabled
- foreach (var version in versions)
- {
- if (!File.Exists(Path.Combine(version.FullName, ".disabled")))
- {
- isEnabled = true;
- break;
- }
- }
- }
-
- if (!isEnabled)
- {
- Log.Verbose("Is disabled: {0}", installed.FullName);
- continue;
- }
-
- var localInfoFile = new FileInfo(Path.Combine(latest.FullName, $"{installed.Name}.json"));
-
- if (!localInfoFile.Exists)
- {
- Log.Information("Has no definition: {0}", localInfoFile.FullName);
- continue;
- }
-
- var info = JsonConvert.DeserializeObject(
- File.ReadAllText(localInfoFile.FullName));
-
- var remoteInfo = this.PluginMaster.FirstOrDefault(x => x.InternalName == info.InternalName);
-
- if (remoteInfo == null)
- {
- Log.Information("Is not in pluginmaster: {0}", info.Name);
- continue;
- }
-
- if (remoteInfo.DalamudApiLevel < PluginManager.DalamudApiLevel)
- {
- Log.Information("Has not applicable API level: {0}", info.Name);
- continue;
- }
-
- Version.TryParse(remoteInfo.AssemblyVersion, out var remoteAssemblyVer);
- Version.TryParse(info.AssemblyVersion, out var localAssemblyVer);
-
- var testingAvailable = false;
- if (!string.IsNullOrEmpty(remoteInfo.TestingAssemblyVersion))
- {
- Version.TryParse(remoteInfo.TestingAssemblyVersion, out var testingAssemblyVer);
- testingAvailable = testingAssemblyVer > localAssemblyVer && this.dalamud.Configuration.DoPluginTest;
- }
-
- if (remoteAssemblyVer > localAssemblyVer || testingAvailable)
- {
- Log.Information("Eligible for update: {0}", remoteInfo.InternalName);
-
- // DisablePlugin() below immediately creates a .disabled file anyway, but will fail
- // with an exception if we try to do it twice in row like this
-
- if (!dryRun)
- {
- var wasLoaded =
- this.dalamud.PluginManager.Plugins.Where(x => x.Definition != null).Any(
- x => x.Definition.InternalName == info.InternalName);
-
- Log.Verbose("isEnabled: {0} / wasLoaded: {1}", isEnabled, wasLoaded);
-
- // Try to disable plugin if it is loaded
- if (wasLoaded)
- {
- try
- {
- this.dalamud.PluginManager.DisablePlugin(info);
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Plugin disable failed");
- // hasError = true;
- }
- }
-
- try
- {
- // Just to be safe
- foreach (var sortedVersion in sortedVersions)
- {
- var disabledFile =
- new FileInfo(Path.Combine(sortedVersion.FullName, ".disabled"));
- if (!disabledFile.Exists)
- disabledFile.Create().Close();
- }
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Plugin disable old versions failed");
- }
-
- var installSuccess = this.InstallPlugin(remoteInfo, isEnabled, true, testingAvailable);
-
- if (!installSuccess)
- {
- Log.Error("InstallPlugin failed.");
- hasError = true;
- }
-
- updatedList.Add(new PluginUpdateStatus
- {
- InternalName = remoteInfo.InternalName,
- Name = remoteInfo.Name,
- Version = testingAvailable ? remoteInfo.TestingAssemblyVersion : remoteInfo.AssemblyVersion,
- WasUpdated = installSuccess,
- });
- }
- else
- {
- updatedList.Add(new PluginUpdateStatus
- {
- InternalName = remoteInfo.InternalName,
- Name = remoteInfo.Name,
- Version = testingAvailable ? remoteInfo.TestingAssemblyVersion : remoteInfo.AssemblyVersion,
- WasUpdated = true,
- });
- }
- }
- else
- {
- Log.Information("Up to date: {0}", remoteInfo.InternalName);
- }
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Could not update plugin: {0}", installed.FullName);
- }
- }
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Plugin update failed.");
- hasError = true;
- }
-
- Log.Information("Plugin update OK.");
-
- return (!hasError, updatedList);
- }
-
- ///
- /// Print to chat any plugin updates and whether they were successful.
- ///
- /// The list of updated plugins.
- /// The header text to send to chat prior to any update info.
- public void PrintUpdatedPlugins(List updatedPlugins, string header)
- {
- if (updatedPlugins != null && updatedPlugins.Any())
- {
- this.dalamud.Framework.Gui.Chat.Print(header);
- foreach (var plugin in updatedPlugins)
- {
- if (plugin.WasUpdated)
- {
- this.dalamud.Framework.Gui.Chat.Print(string.Format(Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}."), plugin.Name, plugin.Version));
- }
- else
- {
- this.dalamud.Framework.Gui.Chat.PrintChat(new XivChatEntry
- {
- MessageBytes = Encoding.UTF8.GetBytes(string.Format(Loc.Localize("DalamudPluginUpdateFailed", " 》 {0} update to v{1} failed."), plugin.Name, plugin.Version)),
- Type = XivChatType.Urgent,
- });
- }
- }
- }
- }
-
- ///
- /// Cleanup disabled plugins.
- ///
- public void CleanupPlugins()
- {
- try
- {
- var pluginsDirectory = new DirectoryInfo(this.pluginDirectory);
- foreach (var installed in pluginsDirectory.GetDirectories())
- {
- var versions = installed.GetDirectories();
-
- var sortedVersions = versions.OrderBy(dirInfo =>
- {
- var success = Version.TryParse(dirInfo.Name, out var version);
- if (!success)
- {
- Log.Debug("Unparseable version: {0}", dirInfo.Name);
- }
-
- return version;
- }).ToArray();
-
- foreach (var version in sortedVersions)
- {
- try
- {
- var disabledFile = new FileInfo(Path.Combine(version.FullName, ".disabled"));
- var definition = JsonConvert.DeserializeObject(
- File.ReadAllText(Path.Combine(version.FullName, version.Parent.Name + ".json")));
-
- if (disabledFile.Exists)
- {
- Log.Information("[PLUGINR] Disabled: cleaning up {0} at {1}", installed.Name, version.FullName);
- try
- {
- version.Delete(true);
- }
- catch (Exception ex)
- {
- Log.Error(ex, $"[PLUGINR] Could not clean up {disabledFile.FullName}");
- }
- }
-
- if (definition.DalamudApiLevel < PluginManager.DalamudApiLevel - 1)
- {
- Log.Information("[PLUGINR] Lower API: cleaning up {0} at {1}", installed.Name, version.FullName);
- try
- {
- version.Delete(true);
- }
- catch (Exception ex)
- {
- Log.Error(ex, $"[PLUGINR] Could not clean up {disabledFile.FullName}");
- }
- }
- }
- catch (Exception ex)
- {
- Log.Error(ex, $"[PLUGINR] Could not clean up {version.FullName}");
- }
-
- if (installed.GetDirectories().Length == 0)
- {
- Log.Information("[PLUGINR] Has no versions, cleaning up: {0}", installed.FullName);
-
- try
- {
- installed.Delete();
- }
- catch (Exception ex)
- {
- Log.Error(ex, $"[PLUGINR] Could not clean up {installed.FullName}");
- }
- }
- }
- }
- }
- catch (Exception ex)
- {
- Log.Error(ex, "[PLUGINR] Plugin cleanup failed.");
- }
- }
-
- ///
- /// Plugin update status.
- ///
- internal class PluginUpdateStatus
- {
- ///
- /// Gets or sets the plugin internal name.
- ///
- public string InternalName { get; set; }
-
- ///
- /// Gets or sets the plugin name.
- ///
- public string Name { get; set; }
-
- ///
- /// Gets or sets the plugin version.
- ///
- public string Version { get; set; }
-
- ///
- /// Gets or sets a value indicating whether the plugin was updated.
- ///
- public bool WasUpdated { get; set; }
- }
- }
-}
diff --git a/Dalamud/Properties/Resources.Designer.cs b/Dalamud/Properties/Resources.Designer.cs
index f8904c30a..aa3d29241 100644
--- a/Dalamud/Properties/Resources.Designer.cs
+++ b/Dalamud/Properties/Resources.Designer.cs
@@ -19,7 +19,7 @@ namespace Dalamud.Properties {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
diff --git a/Dalamud/Resources/Lumina.Generated.dll b/Dalamud/Resources/Lumina.Generated.dll
deleted file mode 100644
index 301694057..000000000
Binary files a/Dalamud/Resources/Lumina.Generated.dll and /dev/null differ
diff --git a/Dalamud/SafeMemory.cs b/Dalamud/SafeMemory.cs
index 458cc6824..362291506 100644
--- a/Dalamud/SafeMemory.cs
+++ b/Dalamud/SafeMemory.cs
@@ -1,5 +1,4 @@
using System;
-using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
@@ -17,12 +16,10 @@ namespace Dalamud
public static class SafeMemory
{
private static readonly IntPtr Handle;
- private static readonly IntPtr MainModule;
static SafeMemory()
{
Handle = Imports.GetCurrentProcess();
- MainModule = Process.GetCurrentProcess().MainModule?.BaseAddress ?? IntPtr.Zero;
}
///
diff --git a/Dalamud/Troubleshooting.cs b/Dalamud/Troubleshooting.cs
index d8137165a..53d5a4b96 100644
--- a/Dalamud/Troubleshooting.cs
+++ b/Dalamud/Troubleshooting.cs
@@ -1,14 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
using Dalamud.Configuration;
using Dalamud.Plugin;
+using Dalamud.Plugin.Internal.Types;
using Newtonsoft.Json;
using Serilog;
-using Encoding = System.Text.Encoding;
-
namespace Dalamud
{
///
@@ -27,9 +27,9 @@ namespace Dalamud
{
var payload = new TroubleshootingPayload
{
- LoadedPlugins = dalamud.PluginManager.Plugins.Select(x => x.Definition).ToArray(),
+ LoadedPlugins = dalamud.PluginManager.InstalledPlugins.Select(x => x.Manifest).ToArray(),
DalamudVersion = Util.AssemblyVersion,
- GameVersion = dalamud.StartInfo.GameVersion,
+ GameVersion = dalamud.StartInfo.GameVersion.ToString(),
Language = dalamud.StartInfo.Language.ToString(),
DoDalamudTest = dalamud.Configuration.DoDalamudTest,
DoPluginTest = dalamud.Configuration.DoPluginTest,
@@ -48,7 +48,7 @@ namespace Dalamud
private class TroubleshootingPayload
{
- public PluginDefinition[] LoadedPlugins { get; set; }
+ public PluginManifest[] LoadedPlugins { get; set; }
public string DalamudVersion { get; set; }
@@ -62,7 +62,7 @@ namespace Dalamud
public bool InterfaceLoaded { get; set; }
- public List ThirdRepo { get; set; }
+ public List ThirdRepo { get; set; }
}
}
}
diff --git a/Dalamud/Util.cs b/Dalamud/Util.cs
index 456c93689..3084e1bab 100644
--- a/Dalamud/Util.cs
+++ b/Dalamud/Util.cs
@@ -161,8 +161,8 @@ namespace Dalamud
public static void Fatal(string message, string caption)
{
var flags = NativeFunctions.MessageBoxType.Ok | NativeFunctions.MessageBoxType.IconError;
+ _ = NativeFunctions.MessageBoxW(Process.GetCurrentProcess().MainWindowHandle, message, caption, flags);
- NativeFunctions.MessageBox(Process.GetCurrentProcess().MainWindowHandle, message, caption, flags);
Environment.Exit(-1);
}
@@ -196,5 +196,13 @@ namespace Dalamud
// TODO: Someone implement GetUTF8String with some IntPtr overloads.
// while(Marshal.ReadByte(0, sz) != 0) { sz++; }
+
+ ///
+ /// An extension method to chain usage of string.Format.
+ ///
+ /// Format string.
+ /// Format arguments.
+ /// Formatted string.
+ public static string Format(this string format, params object[] args) => string.Format(format, args);
}
}
diff --git a/Dalamud/corehook64.dll b/Dalamud/corehook64.dll
new file mode 100644
index 000000000..9b21a40d1
Binary files /dev/null and b/Dalamud/corehook64.dll differ
diff --git a/Dalamud/stylecop.json b/Dalamud/stylecop.json
deleted file mode 100644
index 6881efc6d..000000000
--- a/Dalamud/stylecop.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
- "settings": {
- "orderingRules": {
- "systemUsingDirectivesFirst": true,
- "usingDirectivesPlacement": "outsideNamespace",
- "blankLinesBetweenUsingGroups": "require"
- },
- "maintainabilityRules": {
- "topLevelTypes": [ "class", "interface", "struct", "enum" ]
- }
- }
-}
diff --git a/Resources/EasyHook32.dll b/Resources/EasyHook32.dll
deleted file mode 100644
index 135609b46..000000000
Binary files a/Resources/EasyHook32.dll and /dev/null differ
diff --git a/Resources/EasyHook32Svc.exe b/Resources/EasyHook32Svc.exe
deleted file mode 100644
index 0d470eca4..000000000
Binary files a/Resources/EasyHook32Svc.exe and /dev/null differ
diff --git a/Resources/EasyHook64.dll b/Resources/EasyHook64.dll
deleted file mode 100644
index 4fb1331aa..000000000
Binary files a/Resources/EasyHook64.dll and /dev/null differ
diff --git a/Resources/EasyHook64Svc.exe b/Resources/EasyHook64Svc.exe
deleted file mode 100644
index 770663386..000000000
Binary files a/Resources/EasyHook64Svc.exe and /dev/null differ
diff --git a/Resources/EasyLoad32.dll b/Resources/EasyLoad32.dll
deleted file mode 100644
index 5b30daaa7..000000000
Binary files a/Resources/EasyLoad32.dll and /dev/null differ
diff --git a/Resources/EasyLoad64.dll b/Resources/EasyLoad64.dll
deleted file mode 100644
index 1696334ff..000000000
Binary files a/Resources/EasyLoad64.dll and /dev/null differ
diff --git a/build.cmd b/build.cmd
new file mode 100644
index 000000000..b08cc590f
--- /dev/null
+++ b/build.cmd
@@ -0,0 +1,7 @@
+:; set -eo pipefail
+:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
+:; ${SCRIPT_DIR}/build.sh "$@"
+:; exit $?
+
+@ECHO OFF
+powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %*
diff --git a/build.ps1 b/build.ps1
new file mode 100644
index 000000000..c8aea5cc8
--- /dev/null
+++ b/build.ps1
@@ -0,0 +1,69 @@
+[CmdletBinding()]
+Param(
+ [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
+ [string[]]$BuildArguments
+)
+
+Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)"
+
+Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 }
+$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
+
+###########################################################################
+# CONFIGURATION
+###########################################################################
+
+$BuildProjectFile = "$PSScriptRoot\build\build.csproj"
+$TempDirectory = "$PSScriptRoot\\.nuke\temp"
+
+$DotNetGlobalFile = "$PSScriptRoot\\global.json"
+$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1"
+$DotNetChannel = "Current"
+
+$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1
+$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1
+$env:DOTNET_MULTILEVEL_LOOKUP = 0
+
+###########################################################################
+# EXECUTION
+###########################################################################
+
+function ExecSafe([scriptblock] $cmd) {
+ & $cmd
+ if ($LASTEXITCODE) { exit $LASTEXITCODE }
+}
+
+# If dotnet CLI is installed globally and it matches requested version, use for execution
+if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and `
+ $(dotnet --version) -and $LASTEXITCODE -eq 0) {
+ $env:DOTNET_EXE = (Get-Command "dotnet").Path
+}
+else {
+ # Download install script
+ $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1"
+ New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile)
+
+ # If global.json exists, load expected version
+ if (Test-Path $DotNetGlobalFile) {
+ $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json)
+ if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) {
+ $DotNetVersion = $DotNetGlobal.sdk.version
+ }
+ }
+
+ # Install by channel or version
+ $DotNetDirectory = "$TempDirectory\dotnet-win"
+ if (!(Test-Path variable:DotNetVersion)) {
+ ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
+ } else {
+ ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
+ }
+ $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe"
+}
+
+Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)"
+
+ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet }
+ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments }
diff --git a/build.sh b/build.sh
new file mode 100644
index 000000000..eca8e308c
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,62 @@
+#!/usr/bin/env bash
+
+bash --version 2>&1 | head -n 1
+
+set -eo pipefail
+SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
+
+###########################################################################
+# CONFIGURATION
+###########################################################################
+
+BUILD_PROJECT_FILE="$SCRIPT_DIR/build/build.csproj"
+TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp"
+
+DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json"
+DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh"
+DOTNET_CHANNEL="Current"
+
+export DOTNET_CLI_TELEMETRY_OPTOUT=1
+export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
+export DOTNET_MULTILEVEL_LOOKUP=0
+
+###########################################################################
+# EXECUTION
+###########################################################################
+
+function FirstJsonValue {
+ perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}"
+}
+
+# If dotnet CLI is installed globally and it matches requested version, use for execution
+if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then
+ export DOTNET_EXE="$(command -v dotnet)"
+else
+ # Download install script
+ DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh"
+ mkdir -p "$TEMP_DIRECTORY"
+ curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL"
+ chmod +x "$DOTNET_INSTALL_FILE"
+
+ # If global.json exists, load expected version
+ if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then
+ DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")")
+ if [[ "$DOTNET_VERSION" == "" ]]; then
+ unset DOTNET_VERSION
+ fi
+ fi
+
+ # Install by channel or version
+ DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
+ if [[ -z ${DOTNET_VERSION+x} ]]; then
+ "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path
+ else
+ "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path
+ fi
+ export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet"
+fi
+
+echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)"
+
+"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet
+"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"
diff --git a/build/.editorconfig b/build/.editorconfig
new file mode 100644
index 000000000..06b0d10a7
--- /dev/null
+++ b/build/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*.cs]
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+dotnet_style_require_accessibility_modifiers = never:warning
+dotnet_diagnostic.CA1822.severity=silent
+dotnet_diagnostic.CA1050.severity=silent
+
+csharp_style_expression_bodied_methods = true:silent
+csharp_style_expression_bodied_properties = true:warning
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_accessors = true:warning
diff --git a/build/Configuration.cs b/build/Configuration.cs
new file mode 100644
index 000000000..f4f6b4a51
--- /dev/null
+++ b/build/Configuration.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+using Nuke.Common.Tooling;
+
+[TypeConverter(typeof(TypeConverter))]
+public class Configuration : Enumeration
+{
+ public static readonly Configuration Debug = new() { Value = nameof(Debug) };
+ public static readonly Configuration Release = new() { Value = nameof(Release) };
+
+ public static implicit operator string(Configuration configuration)
+ {
+ return configuration.Value;
+ }
+}
diff --git a/build/DalamudBuild.cs b/build/DalamudBuild.cs
new file mode 100644
index 000000000..01d6d3038
--- /dev/null
+++ b/build/DalamudBuild.cs
@@ -0,0 +1,133 @@
+using System.Collections.Generic;
+using System.IO;
+using Nuke.Common;
+using Nuke.Common.Execution;
+using Nuke.Common.Git;
+using Nuke.Common.IO;
+using Nuke.Common.ProjectModel;
+using Nuke.Common.Tools.DotNet;
+using Nuke.Common.Tools.MSBuild;
+
+[CheckBuildProjectConfigurations]
+[UnsetVisualStudioEnvironmentVariables]
+public class DalamudBuild : NukeBuild
+{
+ /// Support plugins are available for:
+ /// - Microsoft VisualStudio https://nuke.build/visualstudio
+ /// - JetBrains ReSharper https://nuke.build/resharper
+ /// - JetBrains Rider https://nuke.build/rider
+ /// - Microsoft VSCode https://nuke.build/vscode
+
+ public static int Main() => Execute(x => x.Compile);
+
+ [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")]
+ readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;
+
+ [Solution] Solution Solution;
+ [GitRepository] GitRepository GitRepository;
+
+ AbsolutePath DalamudProjectDir => RootDirectory / "Dalamud";
+ AbsolutePath DalamudProjectFile => DalamudProjectDir / "Dalamud.csproj";
+
+ AbsolutePath DalamudBootProjectDir => RootDirectory / "Dalamud.Boot";
+ AbsolutePath DalamudBootProjectFile => DalamudBootProjectDir / "Dalamud.Boot.vcxproj";
+
+ AbsolutePath InjectorProjectDir => RootDirectory / "Dalamud.Injector";
+ AbsolutePath InjectorProjectFile => InjectorProjectDir / "Dalamud.Injector.csproj";
+
+ AbsolutePath InjectorBootProjectDir => RootDirectory / "Dalamud.Injector.Boot";
+ AbsolutePath InjectorBootProjectFile => InjectorBootProjectDir / "Dalamud.Injector.Boot.vcxproj";
+
+ AbsolutePath TestProjectDir => RootDirectory / "Dalamud.Test";
+ AbsolutePath TestProjectFile => TestProjectDir / "Dalamud.Test.csproj";
+
+ AbsolutePath ArtifactsDirectory => RootDirectory / "bin" / Configuration;
+
+ private static AbsolutePath LibraryDirectory => RootDirectory / "lib";
+
+ private static Dictionary EnvironmentVariables => new(EnvironmentInfo.Variables);
+
+ Target Restore => _ => _
+ .Executes(() =>
+ {
+ DotNetTasks.DotNetRestore(s => s
+ .SetProjectFile(Solution));
+ });
+
+ Target CompileDalamud => _ => _
+ .DependsOn(Restore)
+ .Executes(() =>
+ {
+ DotNetTasks.DotNetBuild(s => s
+ .SetProjectFile(DalamudProjectFile)
+ .SetConfiguration(Configuration)
+ .EnableNoRestore());
+ });
+
+ Target CompileDalamudBoot => _ => _
+ .Executes(() =>
+ {
+ MSBuildTasks.MSBuild(s => s
+ .SetTargetPath(DalamudBootProjectFile)
+ .SetConfiguration(Configuration));
+ });
+
+ Target CompileInjector => _ => _
+ .DependsOn(Restore)
+ .Executes(() =>
+ {
+ DotNetTasks.DotNetBuild(s => s
+ .SetProjectFile(InjectorProjectFile)
+ .SetConfiguration(Configuration)
+ .EnableNoRestore());
+ });
+
+ Target CompileInjectorBoot => _ => _
+ .Executes(() =>
+ {
+ MSBuildTasks.MSBuild(s => s
+ .SetTargetPath(InjectorBootProjectFile)
+ .SetConfiguration(Configuration));
+ });
+
+ Target Compile => _ => _
+ .DependsOn(CompileDalamud)
+ .DependsOn(CompileDalamudBoot)
+ .DependsOn(CompileInjector)
+ .DependsOn(CompileInjectorBoot);
+
+ Target Test => _ => _
+ .DependsOn(Compile)
+ .Executes(() =>
+ {
+ DotNetTasks.DotNetTest(s => s
+ .SetProjectFile(TestProjectFile)
+ .SetConfiguration(Configuration)
+ .EnableNoRestore());
+ });
+
+ Target Clean => _ => _
+ .Executes(() =>
+ {
+ DotNetTasks.DotNetClean(s => s
+ .SetProject(DalamudProjectFile)
+ .SetConfiguration(Configuration));
+
+ MSBuildTasks.MSBuild(s => s
+ .SetProjectFile(DalamudBootProjectFile)
+ .SetConfiguration(Configuration)
+ .SetTargets("Clean"));
+
+ DotNetTasks.DotNetClean(s => s
+ .SetProject(InjectorProjectFile)
+ .SetConfiguration(Configuration));
+
+ MSBuildTasks.MSBuild(s => s
+ .SetProjectFile(InjectorBootProjectFile)
+ .SetConfiguration(Configuration)
+ .SetTargets("Clean"));
+
+ FileSystemTasks.DeleteDirectory(ArtifactsDirectory);
+ Directory.CreateDirectory(ArtifactsDirectory);
+ });
+}
diff --git a/build/build.csproj b/build/build.csproj
new file mode 100644
index 000000000..b5403f5ce
--- /dev/null
+++ b/build/build.csproj
@@ -0,0 +1,14 @@
+
+
+ Exe
+ net5.0
+ disable
+
+ IDE0002;IDE0051;IDE1006;CS0649;CS0169
+ ..
+ ..
+
+
+
+
+
diff --git a/build/build.csproj.DotSettings b/build/build.csproj.DotSettings
new file mode 100644
index 000000000..c8947fcec
--- /dev/null
+++ b/build/build.csproj.DotSettings
@@ -0,0 +1,26 @@
+
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ Implicit
+ Implicit
+ ExpressionBody
+ 0
+ NEXT_LINE
+ True
+ False
+ 120
+ IF_OWNER_IS_SINGLE_LINE
+ WRAP_IF_LONG
+ False
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
diff --git a/lib/CoreCLR/CoreCLR.cpp b/lib/CoreCLR/CoreCLR.cpp
new file mode 100644
index 000000000..ec8e548cd
--- /dev/null
+++ b/lib/CoreCLR/CoreCLR.cpp
@@ -0,0 +1,111 @@
+#define WIN32_LEAN_AND_MEAN
+
+#include "CoreCLR.h"
+#include
+#include
+#include "nethost/nethost.h"
+
+#pragma comment(lib, "nethost/libnethost.lib")
+
+CoreCLR::CoreCLR() {}
+
+/* Core public functions */
+int CoreCLR::load_hostfxr()
+{
+ return CoreCLR::load_hostfxr(nullptr);
+}
+
+int CoreCLR::load_hostfxr(const struct get_hostfxr_parameters* parameters)
+{
+ // Get the path to CoreCLR's hostfxr
+ wchar_t buffer[MAX_PATH]{};
+ size_t buffer_size = sizeof buffer / sizeof(wchar_t);
+ if (int rc = get_hostfxr_path(buffer, &buffer_size, parameters); rc != 0)
+ return rc;
+
+ // Load hostfxr and get desired exports
+ auto lib = reinterpret_cast(load_library(buffer));
+ m_hostfxr_initialize_for_runtime_config_fptr = reinterpret_cast(
+ get_export(lib, "hostfxr_initialize_for_runtime_config"));
+ m_hostfxr_get_runtime_delegate_fptr = reinterpret_cast(
+ get_export(lib, "hostfxr_get_runtime_delegate"));
+ m_hostfxr_close_fptr = reinterpret_cast(
+ get_export(lib, "hostfxr_close"));
+
+ return m_hostfxr_initialize_for_runtime_config_fptr
+ && m_hostfxr_get_runtime_delegate_fptr
+ && m_hostfxr_close_fptr ? 0 : -1;
+}
+
+bool CoreCLR::load_runtime(const std::wstring& runtime_config_path)
+{
+ return CoreCLR::load_runtime(runtime_config_path, nullptr);
+}
+
+bool CoreCLR::load_runtime(const std::wstring& runtime_config_path, const struct hostfxr_initialize_parameters* parameters)
+{
+ int result;
+
+ // Load .NET Core
+ hostfxr_handle context = nullptr;
+ result = m_hostfxr_initialize_for_runtime_config_fptr(
+ runtime_config_path.c_str(),
+ parameters,
+ &context);
+
+ // Success_HostAlreadyInitialized
+ if (result == 1)
+ {
+ printf("Success_HostAlreadyInitialized (0x1) ");
+ result = 0;
+ }
+
+ if (result != 0 || context == nullptr)
+ {
+ m_hostfxr_close_fptr(context);
+ return result;
+ }
+
+ // Get the load assembly function pointer
+ result = m_hostfxr_get_runtime_delegate_fptr(
+ context,
+ hdt_load_assembly_and_get_function_pointer,
+ reinterpret_cast(&m_load_assembly_and_get_function_pointer_fptr));
+
+ if (result != 0 || m_load_assembly_and_get_function_pointer_fptr == nullptr)
+ {
+ m_hostfxr_close_fptr(context);
+ return result;
+ }
+
+ m_hostfxr_close_fptr(context);
+
+ return 0;
+}
+
+int CoreCLR::load_assembly_and_get_function_pointer(
+ const wchar_t* assembly_path,
+ const wchar_t* type_name,
+ const wchar_t* method_name,
+ const wchar_t* delegate_type_name,
+ void* reserved,
+ void** delegate) const
+{
+ int result = m_load_assembly_and_get_function_pointer_fptr(assembly_path, type_name, method_name, delegate_type_name, reserved, delegate);
+
+ if (result != 0)
+ delegate = nullptr;
+
+ return result;
+};
+
+/* Helpers */
+uint64_t CoreCLR::load_library(const wchar_t* path)
+{
+ return reinterpret_cast(LoadLibraryW(path));
+}
+
+uint64_t CoreCLR::get_export(void* h, const char* name)
+{
+ return reinterpret_cast(GetProcAddress(static_cast(h), name));
+}
diff --git a/lib/CoreCLR/CoreCLR.h b/lib/CoreCLR/CoreCLR.h
new file mode 100644
index 000000000..c2395d07d
--- /dev/null
+++ b/lib/CoreCLR/CoreCLR.h
@@ -0,0 +1,38 @@
+#pragma once
+#include
+#include "core/hostfxr.h"
+#include "core/coreclr_delegates.h"
+#include "nethost/nethost.h"
+
+class CoreCLR {
+ public:
+ explicit CoreCLR();
+ ~CoreCLR() = default;
+
+ int load_hostfxr();
+ int load_hostfxr(const get_hostfxr_parameters* parameters);
+
+ bool load_runtime(const std::wstring& runtime_config_path);
+ bool load_runtime(
+ const std::wstring& runtime_config_path,
+ const struct hostfxr_initialize_parameters* parameters);
+
+ int load_assembly_and_get_function_pointer(
+ const wchar_t* assembly_path,
+ const wchar_t* type_name,
+ const wchar_t* method_name,
+ const wchar_t* delegate_type_name,
+ void* reserved,
+ void** delegate) const;
+
+ private:
+ /* HostFXR delegates. */
+ hostfxr_initialize_for_runtime_config_fn m_hostfxr_initialize_for_runtime_config_fptr{};
+ hostfxr_get_runtime_delegate_fn m_hostfxr_get_runtime_delegate_fptr{};
+ hostfxr_close_fn m_hostfxr_close_fptr{};
+ load_assembly_and_get_function_pointer_fn m_load_assembly_and_get_function_pointer_fptr = nullptr;
+
+ /* Helper functions. */
+ static uint64_t load_library(const wchar_t* path);
+ static uint64_t get_export(void* h, const char* name);
+};
diff --git a/lib/CoreCLR/boot.cpp b/lib/CoreCLR/boot.cpp
new file mode 100644
index 000000000..0ba4dc19f
--- /dev/null
+++ b/lib/CoreCLR/boot.cpp
@@ -0,0 +1,110 @@
+#define WIN32_LEAN_AND_MEAN
+
+#include
+#include
+#include
+#include
+#include "CoreCLR.h"
+
+FILE* g_CmdStream;
+void ConsoleSetup(const std::wstring console_name)
+{
+ if (!AllocConsole())
+ return;
+
+ SetConsoleTitleW(console_name.c_str());
+ freopen_s(&g_CmdStream, "CONOUT$", "w", stdout);
+ freopen_s(&g_CmdStream, "CONOUT$", "w", stderr);
+ freopen_s(&g_CmdStream, "CONIN$", "r", stdin);
+}
+
+void ConsoleTeardown()
+{
+ FreeConsole();
+}
+
+int InitializeClrAndGetEntryPoint(
+ std::wstring runtimeconfig_path,
+ std::wstring module_path,
+ std::wstring entrypoint_assembly_name,
+ std::wstring entrypoint_method_name,
+ std::wstring entrypoint_delegate_type_name,
+ void** entrypoint_fn)
+{
+ int result;
+ CoreCLR clr;
+ SetEnvironmentVariable(L"DOTNET_MULTILEVEL_LOOKUP", L"0");
+
+ wchar_t* _appdata;
+ result = SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_DEFAULT, NULL, &_appdata);
+ if (result != 0)
+ {
+ printf("Error: Unable to get RoamingAppData path (err=%d)\n", result);
+ return result;
+ }
+ std::filesystem::path fs_app_data(_appdata);
+ wchar_t* dotnet_path = _wcsdup(fs_app_data.append("XIVLauncher").append("runtime").c_str());
+
+ // =========================================================================== //
+
+ wprintf(L"with dotnet_path: %s\n", dotnet_path);
+ wprintf(L"with config_path: %s\n", runtimeconfig_path.c_str());
+ wprintf(L"with module_path: %s\n", module_path.c_str());
+
+ if (!std::filesystem::exists(dotnet_path))
+ {
+ printf("Error: Unable to find .NET runtime path\n");
+ return 1;
+ }
+
+ get_hostfxr_parameters init_parameters
+ {
+ sizeof(get_hostfxr_parameters),
+ nullptr,
+ dotnet_path,
+ };
+
+ printf("Loading hostfxr... ");
+ if ((result = clr.load_hostfxr(&init_parameters)) != 0)
+ {
+ printf("\nError: Failed to load the `hostfxr` library (err=%d)\n", result);
+ return result;
+ }
+ printf("Done!\n");
+
+ // =========================================================================== //
+
+ hostfxr_initialize_parameters runtime_parameters
+ {
+ sizeof(hostfxr_initialize_parameters),
+ module_path.c_str(),
+ dotnet_path,
+ };
+
+ printf("Loading coreclr... ");;
+ if ((result = clr.load_runtime(runtimeconfig_path, &runtime_parameters)) != 0)
+ {
+ printf("\nError: Failed to load coreclr (err=%d)\n", result);
+ return result;
+ }
+ printf("Done!\n");
+
+ // =========================================================================== //
+
+ printf("Loading module... ");
+ if ((result = clr.load_assembly_and_get_function_pointer(
+ module_path.c_str(),
+ entrypoint_assembly_name.c_str(),
+ entrypoint_method_name.c_str(),
+ entrypoint_delegate_type_name.c_str(),
+ nullptr, entrypoint_fn)) != 0)
+ {
+ printf("\nError: Failed to load module (err=%d)\n", result);
+ return result;
+ }
+ printf("Done!\n");
+
+ // =========================================================================== //
+
+ return 0;
+}
diff --git a/lib/CoreCLR/boot.h b/lib/CoreCLR/boot.h
new file mode 100644
index 000000000..8f58042a1
--- /dev/null
+++ b/lib/CoreCLR/boot.h
@@ -0,0 +1,10 @@
+void ConsoleSetup(const std::wstring console_name);
+void ConsoleTeardown();
+
+int InitializeClrAndGetEntryPoint(
+ std::wstring runtimeconfig_path,
+ std::wstring module_path,
+ std::wstring entrypoint_assembly_name,
+ std::wstring entrypoint_method_name,
+ std::wstring entrypoint_delegate_type_name,
+ void** entrypoint_fn);
diff --git a/lib/CoreCLR/core/coreclr_delegates.h b/lib/CoreCLR/core/coreclr_delegates.h
new file mode 100644
index 000000000..20c1221bd
--- /dev/null
+++ b/lib/CoreCLR/core/coreclr_delegates.h
@@ -0,0 +1,47 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#ifndef __CORECLR_DELEGATES_H__
+#define __CORECLR_DELEGATES_H__
+
+#include
+
+#if defined(_WIN32)
+ #define CORECLR_DELEGATE_CALLTYPE __stdcall
+ #ifdef _WCHAR_T_DEFINED
+ typedef wchar_t char_t;
+ #else
+ typedef unsigned short char_t;
+ #endif
+#else
+ #define CORECLR_DELEGATE_CALLTYPE
+ typedef char char_t;
+#endif
+
+#define UNMANAGEDCALLERSONLY_METHOD ((const char_t*)-1)
+
+// Signature of delegate returned by coreclr_delegate_type::load_assembly_and_get_function_pointer
+typedef int (CORECLR_DELEGATE_CALLTYPE* load_assembly_and_get_function_pointer_fn)(
+ const char_t* assembly_path /* Fully qualified path to assembly */,
+ const char_t* type_name /* Assembly qualified type name */,
+ const char_t* method_name /* Public static method name compatible with delegateType */,
+ const char_t* delegate_type_name /* Assembly qualified delegate type name or null
+ or UNMANAGEDCALLERSONLY_METHOD if the method is marked with
+ the UnmanagedCallersOnlyAttribute. */,
+ void* reserved /* Extensibility parameter (currently unused and must be 0) */,
+ /*out*/ void** delegate /* Pointer where to store the function pointer result */);
+
+// Signature of delegate returned by load_assembly_and_get_function_pointer_fn when delegate_type_name == null (default)
+typedef int (CORECLR_DELEGATE_CALLTYPE* component_entry_point_fn)(void* arg, int32_t arg_size_in_bytes);
+
+typedef int (CORECLR_DELEGATE_CALLTYPE* get_function_pointer_fn)(
+ const char_t* type_name /* Assembly qualified type name */,
+ const char_t* method_name /* Public static method name compatible with delegateType */,
+ const char_t* delegate_type_name /* Assembly qualified delegate type name or null,
+ or UNMANAGEDCALLERSONLY_METHOD if the method is marked with
+ the UnmanagedCallersOnlyAttribute. */,
+ void* load_context /* Extensibility parameter (currently unused and must be 0) */,
+ void* reserved /* Extensibility parameter (currently unused and must be 0) */,
+ /*out*/ void** delegate /* Pointer where to store the function pointer result */);
+
+#endif // __CORECLR_DELEGATES_H__
diff --git a/lib/CoreCLR/core/hostfxr.h b/lib/CoreCLR/core/hostfxr.h
new file mode 100644
index 000000000..a0a526d90
--- /dev/null
+++ b/lib/CoreCLR/core/hostfxr.h
@@ -0,0 +1,288 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#ifndef __HOSTFXR_H__
+#define __HOSTFXR_H__
+
+#include
+#include
+
+#if defined(_WIN32)
+ #define HOSTFXR_CALLTYPE __cdecl
+ #ifdef _WCHAR_T_DEFINED
+ typedef wchar_t char_t;
+ #else
+ typedef unsigned short char_t;
+ #endif
+#else
+ #define HOSTFXR_CALLTYPE
+ typedef char char_t;
+#endif
+
+enum hostfxr_delegate_type
+{
+ hdt_com_activation,
+ hdt_load_in_memory_assembly,
+ hdt_winrt_activation,
+ hdt_com_register,
+ hdt_com_unregister,
+ hdt_load_assembly_and_get_function_pointer,
+ hdt_get_function_pointer,
+};
+
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_fn)(const int argc, const char_t** argv);
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_startupinfo_fn)(
+ const int argc,
+ const char_t** argv,
+ const char_t* host_path,
+ const char_t* dotnet_root,
+ const char_t* app_path);
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_bundle_startupinfo_fn)(
+ const int argc,
+ const char_t** argv,
+ const char_t* host_path,
+ const char_t* dotnet_root,
+ const char_t* app_path,
+ int64_t bundle_header_offset);
+
+typedef void(HOSTFXR_CALLTYPE* hostfxr_error_writer_fn)(const char_t* message);
+
+//
+// Sets a callback which is to be used to write errors to.
+//
+// Parameters:
+// error_writer
+// A callback function which will be invoked every time an error is to be reported.
+// Or nullptr to unregister previously registered callback and return to the default behavior.
+// Return value:
+// The previously registered callback (which is now unregistered), or nullptr if no previous callback
+// was registered
+//
+// The error writer is registered per-thread, so the registration is thread-local. On each thread
+// only one callback can be registered. Subsequent registrations overwrite the previous ones.
+//
+// By default no callback is registered in which case the errors are written to stderr.
+//
+// Each call to the error writer is sort of like writing a single line (the EOL character is omitted).
+// Multiple calls to the error writer may occure for one failure.
+//
+// If the hostfxr invokes functions in hostpolicy as part of its operation, the error writer
+// will be propagated to hostpolicy for the duration of the call. This means that errors from
+// both hostfxr and hostpolicy will be reporter through the same error writer.
+//
+typedef hostfxr_error_writer_fn(HOSTFXR_CALLTYPE* hostfxr_set_error_writer_fn)(hostfxr_error_writer_fn error_writer);
+
+typedef void* hostfxr_handle;
+struct hostfxr_initialize_parameters
+{
+ size_t size;
+ const char_t* host_path;
+ const char_t* dotnet_root;
+};
+
+//
+// Initializes the hosting components for a dotnet command line running an application
+//
+// Parameters:
+// argc
+// Number of argv arguments
+// argv
+// Command-line arguments for running an application (as if through the dotnet executable).
+// parameters
+// Optional. Additional parameters for initialization
+// host_context_handle
+// On success, this will be populated with an opaque value representing the initialized host context
+//
+// Return value:
+// Success - Hosting components were successfully initialized
+// HostInvalidState - Hosting components are already initialized
+//
+// This function parses the specified command-line arguments to determine the application to run. It will
+// then find the corresponding .runtimeconfig.json and .deps.json with which to resolve frameworks and
+// dependencies and prepare everything needed to load the runtime.
+//
+// This function only supports arguments for running an application. It does not support SDK commands.
+//
+// This function does not load the runtime.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_initialize_for_dotnet_command_line_fn)(
+ int argc,
+ const char_t** argv,
+ const struct hostfxr_initialize_parameters* parameters,
+ /*out*/ hostfxr_handle* host_context_handle);
+
+//
+// Initializes the hosting components using a .runtimeconfig.json file
+//
+// Parameters:
+// runtime_config_path
+// Path to the .runtimeconfig.json file
+// parameters
+// Optional. Additional parameters for initialization
+// host_context_handle
+// On success, this will be populated with an opaque value representing the initialized host context
+//
+// Return value:
+// Success - Hosting components were successfully initialized
+// Success_HostAlreadyInitialized - Config is compatible with already initialized hosting components
+// Success_DifferentRuntimeProperties - Config has runtime properties that differ from already initialized hosting components
+// CoreHostIncompatibleConfig - Config is incompatible with already initialized hosting components
+//
+// This function will process the .runtimeconfig.json to resolve frameworks and prepare everything needed
+// to load the runtime. It will only process the .deps.json from frameworks (not any app/component that
+// may be next to the .runtimeconfig.json).
+//
+// This function does not load the runtime.
+//
+// If called when the runtime has already been loaded, this function will check if the specified runtime
+// config is compatible with the existing runtime.
+//
+// Both Success_HostAlreadyInitialized and Success_DifferentRuntimeProperties codes are considered successful
+// initializations. In the case of Success_DifferentRuntimeProperties, it is left to the consumer to verify that
+// the difference in properties is acceptable.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_initialize_for_runtime_config_fn)(
+ const char_t* runtime_config_path,
+ const struct hostfxr_initialize_parameters* parameters,
+ /*out*/ hostfxr_handle* host_context_handle);
+
+//
+// Gets the runtime property value for an initialized host context
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+// name
+// Runtime property name
+// value
+// Out parameter. Pointer to a buffer with the property value.
+//
+// Return value:
+// The error code result.
+//
+// The buffer pointed to by value is owned by the host context. The lifetime of the buffer is only
+// guaranteed until any of the below occur:
+// - a 'run' method is called for the host context
+// - properties are changed via hostfxr_set_runtime_property_value
+// - the host context is closed via 'hostfxr_close'
+//
+// If host_context_handle is nullptr and an active host context exists, this function will get the
+// property value for the active host context.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_runtime_property_value_fn)(
+ const hostfxr_handle host_context_handle,
+ const char_t* name,
+ /*out*/ const char_t** value);
+
+//
+// Sets the value of a runtime property for an initialized host context
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+// name
+// Runtime property name
+// value
+// Value to set
+//
+// Return value:
+// The error code result.
+//
+// Setting properties is only supported for the first host context, before the runtime has been loaded.
+//
+// If the property already exists in the host context, it will be overwritten. If value is nullptr, the
+// property will be removed.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_set_runtime_property_value_fn)(
+ const hostfxr_handle host_context_handle,
+ const char_t* name,
+ const char_t* value);
+
+//
+// Gets all the runtime properties for an initialized host context
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+// count
+// [in] Size of the keys and values buffers
+// [out] Number of properties returned (size of keys/values buffers used). If the input value is too
+// small or keys/values is nullptr, this is populated with the number of available properties
+// keys
+// Array of pointers to buffers with runtime property keys
+// values
+// Array of pointers to buffers with runtime property values
+//
+// Return value:
+// The error code result.
+//
+// The buffers pointed to by keys and values are owned by the host context. The lifetime of the buffers is only
+// guaranteed until any of the below occur:
+// - a 'run' method is called for the host context
+// - properties are changed via hostfxr_set_runtime_property_value
+// - the host context is closed via 'hostfxr_close'
+//
+// If host_context_handle is nullptr and an active host context exists, this function will get the
+// properties for the active host context.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_runtime_properties_fn)(
+ const hostfxr_handle host_context_handle,
+ /*inout*/ size_t* count,
+ /*out*/ const char_t** keys,
+ /*out*/ const char_t** values);
+
+//
+// Load CoreCLR and run the application for an initialized host context
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+//
+// Return value:
+// If the app was successfully run, the exit code of the application. Otherwise, the error code result.
+//
+// The host_context_handle must have been initialized using hostfxr_initialize_for_dotnet_command_line.
+//
+// This function will not return until the managed application exits.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_run_app_fn)(const hostfxr_handle host_context_handle);
+
+//
+// Gets a typed delegate from the currently loaded CoreCLR or from a newly created one.
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+// type
+// Type of runtime delegate requested
+// delegate
+// An out parameter that will be assigned the delegate.
+//
+// Return value:
+// The error code result.
+//
+// If the host_context_handle was initialized using hostfxr_initialize_for_runtime_config,
+// then all delegate types are supported.
+// If the host_context_handle was initialized using hostfxr_initialize_for_dotnet_command_line,
+// then only the following delegate types are currently supported:
+// hdt_load_assembly_and_get_function_pointer
+// hdt_get_function_pointer
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_runtime_delegate_fn)(
+ const hostfxr_handle host_context_handle,
+ enum hostfxr_delegate_type type,
+ /*out*/ void** delegate);
+
+//
+// Closes an initialized host context
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+//
+// Return value:
+// The error code result.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_close_fn)(const hostfxr_handle host_context_handle);
+
+#endif //__HOSTFXR_H__
diff --git a/lib/CoreCLR/framework.h b/lib/CoreCLR/framework.h
new file mode 100644
index 000000000..8bb54776a
--- /dev/null
+++ b/lib/CoreCLR/framework.h
@@ -0,0 +1,7 @@
+#pragma once
+
+// Exclude rarely-used stuff from Windows headers
+#define WIN32_LEAN_AND_MEAN
+
+// Windows Header Files
+#include
diff --git a/lib/CoreCLR/nethost/libnethost.lib b/lib/CoreCLR/nethost/libnethost.lib
new file mode 100644
index 000000000..4291a5387
Binary files /dev/null and b/lib/CoreCLR/nethost/libnethost.lib differ
diff --git a/lib/CoreCLR/nethost/nethost.dll b/lib/CoreCLR/nethost/nethost.dll
new file mode 100644
index 000000000..6673b3079
Binary files /dev/null and b/lib/CoreCLR/nethost/nethost.dll differ
diff --git a/lib/CoreCLR/nethost/nethost.h b/lib/CoreCLR/nethost/nethost.h
new file mode 100644
index 000000000..31adde5e8
--- /dev/null
+++ b/lib/CoreCLR/nethost/nethost.h
@@ -0,0 +1,99 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#ifndef __NETHOST_H__
+#define __NETHOST_H__
+
+#include
+
+#ifdef _WIN32
+ #ifdef NETHOST_EXPORT
+ #define NETHOST_API __declspec(dllexport)
+ #else
+ // Consuming the nethost as a static library
+ // Shouldn't export attempt to dllimport.
+ #ifdef NETHOST_USE_AS_STATIC
+ #define NETHOST_API
+ #else
+ #define NETHOST_API __declspec(dllimport)
+ #endif
+ #endif
+
+ #define NETHOST_CALLTYPE __stdcall
+ #ifdef _WCHAR_T_DEFINED
+ typedef wchar_t char_t;
+ #else
+ typedef unsigned short char_t;
+ #endif
+#else
+ #ifdef NETHOST_EXPORT
+ #define NETHOST_API __attribute__((__visibility__("default")))
+ #else
+ #define NETHOST_API
+ #endif
+
+ #define NETHOST_CALLTYPE
+ typedef char char_t;
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Parameters for get_hostfxr_path
+//
+// Fields:
+// size
+// Size of the struct. This is used for versioning.
+//
+// assembly_path
+// Path to the compenent's assembly.
+// If specified, hostfxr is located as if the assembly_path is the apphost
+//
+// dotnet_root
+// Path to directory containing the dotnet executable.
+// If specified, hostfxr is located as if an application is started using
+// 'dotnet app.dll', which means it will be searched for under the dotnet_root
+// path and the assembly_path is ignored.
+//
+struct get_hostfxr_parameters {
+ size_t size;
+ const char_t *assembly_path;
+ const char_t *dotnet_root;
+};
+
+//
+// Get the path to the hostfxr library
+//
+// Parameters:
+// buffer
+// Buffer that will be populated with the hostfxr path, including a null terminator.
+//
+// buffer_size
+// [in] Size of buffer in char_t units.
+// [out] Size of buffer used in char_t units. If the input value is too small
+// or buffer is nullptr, this is populated with the minimum required size
+// in char_t units for a buffer to hold the hostfxr path
+//
+// get_hostfxr_parameters
+// Optional. Parameters that modify the behaviour for locating the hostfxr library.
+// If nullptr, hostfxr is located using the enviroment variable or global registration
+//
+// Return value:
+// 0 on success, otherwise failure
+// 0x80008098 - buffer is too small (HostApiBufferTooSmall)
+//
+// Remarks:
+// The full search for the hostfxr library is done on every call. To minimize the need
+// to call this function multiple times, pass a large buffer (e.g. PATH_MAX).
+//
+NETHOST_API int NETHOST_CALLTYPE get_hostfxr_path(
+ char_t * buffer,
+ size_t * buffer_size,
+ const struct get_hostfxr_parameters *parameters);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // __NETHOST_H__
diff --git a/lib/CoreCLR/nethost/nethost.lib b/lib/CoreCLR/nethost/nethost.lib
new file mode 100644
index 000000000..4cd6f0382
Binary files /dev/null and b/lib/CoreCLR/nethost/nethost.lib differ
diff --git a/lib/CoreCLR/pch.cpp b/lib/CoreCLR/pch.cpp
new file mode 100644
index 000000000..64b7eef6d
--- /dev/null
+++ b/lib/CoreCLR/pch.cpp
@@ -0,0 +1,5 @@
+// pch.cpp: source file corresponding to the pre-compiled header
+
+#include "pch.h"
+
+// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.
diff --git a/lib/CoreCLR/pch.h b/lib/CoreCLR/pch.h
new file mode 100644
index 000000000..885d5d62e
--- /dev/null
+++ b/lib/CoreCLR/pch.h
@@ -0,0 +1,13 @@
+// pch.h: This is a precompiled header file.
+// Files listed below are compiled only once, improving build performance for future builds.
+// This also affects IntelliSense performance, including code completion and many code browsing features.
+// However, files listed here are ALL re-compiled if any one of them is updated between builds.
+// Do not add files here that you will be updating frequently as this negates the performance advantage.
+
+#ifndef PCH_H
+#define PCH_H
+
+// add headers that you want to pre-compile here
+#include "framework.h"
+
+#endif //PCH_H
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index aace50182..e3bec1189 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit aace501829dcc765eb06eab446c6cdc087d351ca
+Subproject commit e3bec118909b0eafdd0ee7c3312c646ce0c3f3dd
diff --git a/lib/ImGuiScene b/lib/ImGuiScene
index 89f6b1b92..b326a22da 160000
--- a/lib/ImGuiScene
+++ b/lib/ImGuiScene
@@ -1 +1 @@
-Subproject commit 89f6b1b92547d5a54fc4e8aa81fe3e454da30318
+Subproject commit b326a22dad825fc036d63956bc9901165d825d86
diff --git a/lib/SharpDX.Desktop b/lib/SharpDX.Desktop
new file mode 160000
index 000000000..7fc56bc0a
--- /dev/null
+++ b/lib/SharpDX.Desktop
@@ -0,0 +1 @@
+Subproject commit 7fc56bc0a240030d4736e6b16da33b08c73c3ba4
diff --git a/Dalamud.Injector/stylecop.json b/stylecop.json
similarity index 100%
rename from Dalamud.Injector/stylecop.json
rename to stylecop.json