diff --git a/.editorconfig b/.editorconfig
index 8816a8511..2dbbff2dc 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -24,6 +24,7 @@ csharp_preferred_modifier_order = public, private, protected, internal, new, abs
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
+dotnet_code_quality_unused_parameters = non_public
dotnet_naming_rule.event_rule.severity = warning
dotnet_naming_rule.event_rule.style = on_upper_camel_case_style
dotnet_naming_rule.event_rule.symbols = event_symbols
@@ -56,11 +57,13 @@ dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly
-dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:suggestion
-dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:suggestion
-dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:suggestion
+dotnet_style_parentheses_in_arithmetic_binary_operators =always_for_clarity:suggestion
+dotnet_style_parentheses_in_other_binary_operators =always_for_clarity:suggestion
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
+dotnet_style_parentheses_in_other_operators=always_for_clarity:silent
+dotnet_style_object_initializer = false
# ReSharper properties
resharper_align_linq_query = true
@@ -105,8 +108,10 @@ resharper_redundant_base_qualifier_highlighting = none
resharper_suggest_var_or_type_built_in_types_highlighting = hint
resharper_suggest_var_or_type_elsewhere_highlighting = hint
resharper_suggest_var_or_type_simple_types_highlighting = hint
+csharp_style_deconstructed_variable_declaration=true:silent
[*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}]
indent_style = space
indent_size = 4
tab_width = 4
+dotnet_style_parentheses_in_other_operators=always_for_clarity:silent
diff --git a/.github/workflows/delete-artifacts.yml b/.github/workflows/delete-artifacts.yml
index 7eb2b001c..6191e6cba 100644
--- a/.github/workflows/delete-artifacts.yml
+++ b/.github/workflows/delete-artifacts.yml
@@ -12,7 +12,7 @@ jobs:
steps:
- name: Remove old artifacts
- uses: c-hive/gha-remove-artifacts@24dc23384a1fa6a058b79c73727ae0cb2200ca4c
+ uses: c-hive/gha-remove-artifacts@v1.2.0
with:
age: '1 month'
skip-tags: true
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 2293db95d..d9c1af8f4 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -7,15 +7,12 @@ jobs:
name: Build on Windows
runs-on: windows-2019
steps:
- - uses: actions/checkout@v2
+ - name: Checkout Dalamud
+ uses: actions/checkout@v2
with:
submodules: recursive
- - name: Setup Nuget
- uses: nuget/setup-nuget@v1
- with:
- nuget-version: latest
- - name: Restore Nuget Packages
- run: nuget restore Dalamud.sln
+ - name: Setup MSBuild
+ uses: microsoft/setup-msbuild@v1.0.2
- name: Define VERSION
run: |
$env:COMMIT = $env:GITHUB_SHA.Substring(0, 7)
@@ -25,18 +22,17 @@ jobs:
($env:REPO_NAME) >> VERSION
($env:BRANCH) >> VERSION
($env:COMMIT) >> VERSION
- - name: Build DotNet4
- run: |
- cd "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\"
- .\MSBuild.exe $Env:GITHUB_WORKSPACE\Dalamud.sln /t:Build /p:Configuration=Release /p:DefineConstants=XL_NOAUTOUPDATE
- - name: Run xUnit Tests
- run: |
- ${{github.workspace}}\packages\xunit.runner.console.2.4.1\tools\net472\xunit.console.exe ${{github.workspace}}\Dalamud.Test\bin\Release\Dalamud.Test.dll
+ - name: Build Dalamud
+ run: .\build.ps1 compile
+ - name: Test Dalamud
+ run: .\build.ps1 test
+ - name: Create hashlist
+ run: .\CreateHashList.ps1 .\bin\Release
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: dalamud-artifact
- path: bin\
+ path: bin\Release
deploy_stg:
name: Deploy dalamud-distrib staging
@@ -54,7 +50,7 @@ jobs:
path: .\scratch
- name: Generate dalamud-distrib version file
- shell: powershell
+ shell: pwsh
run: |
Compress-Archive .\scratch\* .\canary.zip # Recreate the release zip
diff --git a/.gitmodules b/.gitmodules
index 57306103a..f063dd0b3 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -4,3 +4,6 @@
[submodule "lib/FFXIVClientStructs"]
path = lib/FFXIVClientStructs
url = https://github.com/goatcorp/FFXIVClientStructs.git
+[submodule "lib/SharpDX.Desktop"]
+ path = lib/SharpDX.Desktop
+ url = https://github.com/goatcorp/SharpDX.Desktop.git
diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json
new file mode 100644
index 000000000..65ec356a7
--- /dev/null
+++ b/.nuke/build.schema.json
@@ -0,0 +1,112 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "Build Schema",
+ "$ref": "#/definitions/build",
+ "definitions": {
+ "build": {
+ "type": "object",
+ "properties": {
+ "Configuration": {
+ "type": "string",
+ "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
+ "enum": [
+ "Debug",
+ "Release"
+ ]
+ },
+ "Continue": {
+ "type": "boolean",
+ "description": "Indicates to continue a previously failed build attempt"
+ },
+ "Help": {
+ "type": "boolean",
+ "description": "Shows the help text for this build assembly"
+ },
+ "Host": {
+ "type": "string",
+ "description": "Host for execution. Default is 'automatic'",
+ "enum": [
+ "AppVeyor",
+ "AzurePipelines",
+ "Bamboo",
+ "Bitrise",
+ "GitHubActions",
+ "GitLab",
+ "Jenkins",
+ "SpaceAutomation",
+ "TeamCity",
+ "Terminal",
+ "TravisCI"
+ ]
+ },
+ "NoLogo": {
+ "type": "boolean",
+ "description": "Disables displaying the NUKE logo"
+ },
+ "Plan": {
+ "type": "boolean",
+ "description": "Shows the execution plan (HTML)"
+ },
+ "Profile": {
+ "type": "array",
+ "description": "Defines the profiles to load",
+ "items": {
+ "type": "string"
+ }
+ },
+ "Root": {
+ "type": "string",
+ "description": "Root directory during build execution"
+ },
+ "Skip": {
+ "type": "array",
+ "description": "List of targets to be skipped. Empty list skips all dependencies",
+ "items": {
+ "type": "string",
+ "enum": [
+ "Clean",
+ "Compile",
+ "CompileDalamud",
+ "CompileDalamudBoot",
+ "CompileInjector",
+ "CompileInjectorBoot",
+ "Restore",
+ "Test"
+ ]
+ }
+ },
+ "Solution": {
+ "type": "string",
+ "description": "Path to a solution file that is automatically loaded"
+ },
+ "Target": {
+ "type": "array",
+ "description": "List of targets to be invoked. Default is '{default_target}'",
+ "items": {
+ "type": "string",
+ "enum": [
+ "Clean",
+ "Compile",
+ "CompileDalamud",
+ "CompileDalamudBoot",
+ "CompileInjector",
+ "CompileInjectorBoot",
+ "Restore",
+ "Test"
+ ]
+ }
+ },
+ "Verbosity": {
+ "type": "string",
+ "description": "Logging verbosity during build execution. Default is 'Normal'",
+ "enum": [
+ "Minimal",
+ "Normal",
+ "Quiet",
+ "Verbose"
+ ]
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.nuke/parameters.json b/.nuke/parameters.json
new file mode 100644
index 000000000..34f14e26a
--- /dev/null
+++ b/.nuke/parameters.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "./build.schema.json",
+ "Solution": "Dalamud.sln"
+}
\ No newline at end of file
diff --git a/CreateHashList.ps1 b/CreateHashList.ps1
index 668c1c140..389c2adec 100644
--- a/CreateHashList.ps1
+++ b/CreateHashList.ps1
@@ -1,7 +1,11 @@
-$hashes = @{}
+$hashes = [ordered]@{}
-Get-ChildItem $args[0] -Exclude dalamud.txt,*.zip,*.pdb,*.ipdb | Foreach-Object {
- $hashes.Add($_.Name, (Get-FileHash $_.FullName -Algorithm MD5).Hash)
+Set-Location $args[0]
+
+Get-ChildItem -File -Recurse -Exclude dalamud.txt,*.zip,*.pdb,*.ipdb | Foreach-Object {
+ $key = ($_.FullName | Resolve-Path -Relative).TrimStart(".\\")
+ $val = (Get-FileHash $_.FullName -Algorithm MD5).Hash
+ $hashes.Add($key, $val)
}
-ConvertTo-Json $hashes | Out-File -FilePath (Join-Path $args[0] "hashes.json")
\ No newline at end of file
+$hashes | ConvertTo-Json | Out-File -FilePath "hashes.json"
\ No newline at end of file
diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj
new file mode 100644
index 000000000..b1158d85a
--- /dev/null
+++ b/Dalamud.Boot/Dalamud.Boot.vcxproj
@@ -0,0 +1,105 @@
+
+
+
+ {55198DC3-A03D-408E-A8EB-2077780C8576}
+ Dalamud_Boot
+ Debug
+ x64
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 16.0
+ Win32Proj
+ 10.0
+
+
+
+ DynamicLibrary
+ true
+ v142
+ false
+ Unicode
+ ..\bin\$(Configuration)\
+ obj\$(Configuration)\
+
+
+
+
+ Level3
+ true
+ true
+ stdcpplatest
+ MultiThreadedDebug
+ pch.h
+ ProgramDatabase
+ CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+
+
+ Windows
+ true
+ false
+ ..\lib\CoreCLR;%(AdditionalLibraryDirectories)
+
+
+
+
+ true
+ false
+ _DEBUG;%(PreprocessorDefinitions)
+
+
+ false
+ false
+
+
+
+
+ true
+ true
+ NDEBUG;%(PreprocessorDefinitions)
+
+
+ true
+ true
+
+
+
+
+
+ nethost.dll
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters
new file mode 100644
index 000000000..afcc6e502
--- /dev/null
+++ b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters
@@ -0,0 +1,62 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
+
+
+ {18be40ac-9367-46ff-b848-4c528aa97a8d}
+ lib
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+
+
+ Library Files
+
+
+ Library Files
+
+
+
\ No newline at end of file
diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp
new file mode 100644
index 000000000..a454b4431
--- /dev/null
+++ b/Dalamud.Boot/dllmain.cpp
@@ -0,0 +1,66 @@
+#define WIN32_LEAN_AND_MEAN
+#define DllExport extern "C" __declspec(dllexport)
+
+#include
+#include
+#include "..\lib\CoreCLR\CoreCLR.h"
+#include "..\lib\CoreCLR\boot.h"
+
+HMODULE g_hModule;
+
+DllExport DWORD WINAPI Initialize(LPVOID lpParam)
+{
+ #if !defined(NDEBUG)
+ ConsoleSetup(L"Dalamud Boot");
+ #endif
+
+ wchar_t _module_path[MAX_PATH];
+ GetModuleFileNameW(g_hModule, _module_path, sizeof _module_path / 2);
+ std::filesystem::path fs_module_path(_module_path);
+
+ std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.runtimeconfig.json").c_str());
+ std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.dll").c_str());
+
+ // =========================================================================== //
+
+ void* entrypoint_vfn;
+ int result = InitializeClrAndGetEntryPoint(
+ runtimeconfig_path,
+ module_path,
+ L"Dalamud.EntryPoint, Dalamud",
+ L"Initialize",
+ L"Dalamud.EntryPoint+InitDelegate, Dalamud",
+ &entrypoint_vfn);
+
+ if (result != 0)
+ return result;
+
+ typedef void (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(LPVOID);
+ custom_component_entry_point_fn entrypoint_fn = reinterpret_cast(entrypoint_vfn);
+
+ printf("Initializing Dalamud... ");
+ entrypoint_fn(lpParam);
+ printf("Done!\n");
+
+ // =========================================================================== //
+
+ #if !defined(NDEBUG)
+ FreeConsole();
+ #endif
+
+ return 0;
+}
+
+BOOL APIENTRY DllMain(const HMODULE hModule, const DWORD dwReason, LPVOID lpReserved) {
+ DisableThreadLibraryCalls(hModule);
+
+ switch (dwReason)
+ {
+ case DLL_PROCESS_ATTACH:
+ g_hModule = hModule;
+ break;
+ case DLL_PROCESS_DETACH:
+ break;
+ }
+ return TRUE;
+}
diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj
new file mode 100644
index 000000000..8c714785c
--- /dev/null
+++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj
@@ -0,0 +1,61 @@
+
+
+ Dalamud.CorePlugin
+ net5.0-windows
+ x64
+ x64;AnyCPU
+ 9.0
+ true
+ false
+ false
+ IDE0003
+
+
+
+ true
+ full
+ false
+ $(appData)\XIVLauncher\devPlugins\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+
+ pdbonly
+ true
+ $(appData)\XIVLauncher\devPlugins\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+
diff --git a/Dalamud.CorePlugin/GlobalSuppressions.cs b/Dalamud.CorePlugin/GlobalSuppressions.cs
new file mode 100644
index 000000000..bfaba20d0
--- /dev/null
+++ b/Dalamud.CorePlugin/GlobalSuppressions.cs
@@ -0,0 +1,17 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+// General
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "Preventing long lines", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "I like regions", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1123:Do not place regions within elements", Justification = "I like regions in elements too", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
+[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
+[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
+[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
+[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1127:Generic type constraints should be on their own line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
+[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
+[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")]
diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs
new file mode 100644
index 000000000..4d635eeb5
--- /dev/null
+++ b/Dalamud.CorePlugin/PluginImpl.cs
@@ -0,0 +1,96 @@
+using System;
+using System.IO;
+
+using Dalamud.Interface.Windowing;
+using Dalamud.Plugin;
+
+namespace Dalamud.CorePlugin
+{
+ ///
+ /// This class is a a plugin testbed for developing new Dalamud features with easy access to Dalamud itself.
+ /// Be careful to not commit anything extra.
+ ///
+ public sealed class PluginImpl : IDalamudPlugin
+ {
+ private readonly WindowSystem windowSystem = new("Dalamud.CorePlugin");
+ private Localization localizationManager;
+
+ ///
+ public string Name => "Dalamud.CorePlugin";
+
+ ///
+ /// Gets the plugin interface.
+ ///
+ internal DalamudPluginInterface Interface { get; private set; }
+
+ ///
+ public void Initialize(DalamudPluginInterface pluginInterface)
+ {
+ try
+ {
+ this.InitLoc();
+
+ this.Interface = pluginInterface;
+
+ this.windowSystem.AddWindow(new PluginWindow(Dalamud.Instance));
+
+ this.Interface.UiBuilder.OnBuildUi += this.OnDraw;
+ this.Interface.UiBuilder.OnOpenConfigUi += this.OnOpenConfigUi;
+
+ this.Interface.CommandManager.AddHandler("/di", new(this.OnCommand) { HelpMessage = $"Access the {this.Name} plugin." });
+ }
+ catch (Exception ex)
+ {
+ PluginLog.Error(ex, "kaboom");
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.Interface.CommandManager.RemoveHandler("/di");
+
+ this.Interface.UiBuilder.OnBuildUi -= this.OnDraw;
+
+ this.windowSystem.RemoveAllWindows();
+
+ this.Interface.Dispose();
+ }
+
+ private void InitLoc()
+ {
+ // CheapLoc needs to be reinitialized here because it tracks the setup by assembly name. New assembly, new setup.
+ this.localizationManager = new Localization(Path.Combine(Dalamud.Instance.AssetDirectory.FullName, "UIRes", "loc", "dalamud"), "dalamud_");
+ if (!string.IsNullOrEmpty(Dalamud.Instance.Configuration.LanguageOverride))
+ {
+ this.localizationManager.SetupWithLangCode(Dalamud.Instance.Configuration.LanguageOverride);
+ }
+ else
+ {
+ this.localizationManager.SetupWithUiCulture();
+ }
+ }
+
+ private void OnDraw()
+ {
+ try
+ {
+ this.windowSystem.Draw();
+ }
+ catch (Exception ex)
+ {
+ PluginLog.Error(ex, "Boom");
+ }
+ }
+
+ private void OnCommand(string command, string args)
+ {
+ // this.window.IsOpen = true;
+ }
+
+ private void OnOpenConfigUi(object sender, EventArgs e)
+ {
+ // this.window.IsOpen = true;
+ }
+ }
+}
diff --git a/Dalamud.CorePlugin/PluginWindow.cs b/Dalamud.CorePlugin/PluginWindow.cs
new file mode 100644
index 000000000..e604389ba
--- /dev/null
+++ b/Dalamud.CorePlugin/PluginWindow.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Numerics;
+
+using Dalamud.Interface.Windowing;
+using ImGuiNET;
+
+namespace Dalamud.CorePlugin
+{
+ ///
+ /// Class responsible for drawing the plugin installer.
+ ///
+ internal class PluginWindow : Window, IDisposable
+ {
+ private static readonly ModuleLog Log = new("CorePlugin");
+
+ private readonly Dalamud dalamud;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Dalamud instance.
+ public PluginWindow(Dalamud dalamud)
+ : base("CorePlugin", ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoScrollbar)
+ {
+ this.dalamud = dalamud;
+ this.IsOpen = true;
+
+ this.Size = new Vector2(810, 520);
+ this.SizeCondition = ImGuiCond.Always;
+ }
+
+ ///
+ public void Dispose()
+ {
+ }
+
+ ///
+ public override void OnOpen()
+ {
+ }
+
+ ///
+ public override void Draw()
+ {
+ }
+ }
+}
diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj
new file mode 100644
index 000000000..8257d42aa
--- /dev/null
+++ b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj
@@ -0,0 +1,113 @@
+
+
+
+ {8874326B-E755-4D13-90B4-59AB263A3E6B}
+ Dalamud_Injector_Boot
+ Debug
+ x64
+
+
+
+ Debug
+ x64
+
+
+ Release
+ x64
+
+
+
+ 16.0
+ Win32Proj
+ 10.0
+ Dalamud.Injector
+
+
+
+ Application
+ true
+ v142
+ false
+ Unicode
+ ..\bin\$(Configuration)\
+ obj\$(Configuration)\
+
+
+
+
+ Level3
+ true
+ true
+ stdcpplatest
+ MultiThreadedDebug
+ pch.h
+ ProgramDatabase
+ CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+
+
+ Console
+ true
+ false
+ ..\lib\CoreCLR;%(AdditionalLibraryDirectories)
+ $(OutDir)$(TargetName).Boot.pdb
+
+
+
+
+ true
+ false
+ _DEBUG;%(PreprocessorDefinitions)
+
+
+ false
+ false
+
+
+
+
+ true
+ true
+ NDEBUG;%(PreprocessorDefinitions)
+
+
+ true
+ true
+
+
+
+
+
+ nethost.dll
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters
new file mode 100644
index 000000000..47f80ab3e
--- /dev/null
+++ b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters
@@ -0,0 +1,75 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
+
+
+ {4faac519-3a73-4b2b-96e7-fb597f02c0be}
+ ico;rc
+
+
+ {6aff1bed-6979-4bc9-94e8-ddafb626e6bf}
+
+
+
+
+ Resource Files
+
+
+
+
+ Resource Files
+
+
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+ Source Files
+
+
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+ Header Files
+
+
+
+
+ Library Files
+
+
+ Library Files
+
+
+
\ No newline at end of file
diff --git a/Dalamud.Injector/dalamud.ico b/Dalamud.Injector.Boot/dalamud.ico
similarity index 99%
rename from Dalamud.Injector/dalamud.ico
rename to Dalamud.Injector.Boot/dalamud.ico
index 98b9a9b9f..1cd63765d 100644
Binary files a/Dalamud.Injector/dalamud.ico and b/Dalamud.Injector.Boot/dalamud.ico differ
diff --git a/Dalamud.Injector.Boot/main.cpp b/Dalamud.Injector.Boot/main.cpp
new file mode 100644
index 000000000..2b752deb4
--- /dev/null
+++ b/Dalamud.Injector.Boot/main.cpp
@@ -0,0 +1,49 @@
+#define WIN32_LEAN_AND_MEAN
+
+#include
+#include
+#include "..\lib\CoreCLR\CoreCLR.h"
+#include "..\lib\CoreCLR\boot.h"
+
+int wmain(int argc, char** argv)
+{
+ #if !defined(NDEBUG)
+ ConsoleSetup(L"Dalamud Injector Boot");
+ #endif
+
+ wchar_t _module_path[MAX_PATH];
+ GetModuleFileNameW(NULL, _module_path, sizeof _module_path / 2);
+ std::filesystem::path fs_module_path(_module_path);
+
+ std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.runtimeconfig.json").c_str());
+ std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.dll").c_str());
+
+ // =========================================================================== //
+
+ void* entrypoint_vfn;
+ int result = InitializeClrAndGetEntryPoint(
+ runtimeconfig_path,
+ module_path,
+ L"Dalamud.Injector.EntryPoint, Dalamud.Injector",
+ L"Main",
+ L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector",
+ &entrypoint_vfn);
+
+ if (result != 0)
+ return result;
+
+ typedef void (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, char**);
+ custom_component_entry_point_fn entrypoint_fn = reinterpret_cast(entrypoint_vfn);
+
+ printf("Running Dalamud Injector... ");
+ entrypoint_fn(argc, argv);
+ printf("Done!\n");
+
+ // =========================================================================== //
+
+ #if !defined(NDEBUG)
+ FreeConsole();
+ #endif
+
+ return 0;
+}
diff --git a/Dalamud.Injector.Boot/resources.rc b/Dalamud.Injector.Boot/resources.rc
new file mode 100644
index 000000000..8369e82a1
--- /dev/null
+++ b/Dalamud.Injector.Boot/resources.rc
@@ -0,0 +1 @@
+MAINICON ICON "dalamud.ico"
diff --git a/Dalamud.Injector/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj
index 5d76a11f6..eac9f1009 100644
--- a/Dalamud.Injector/Dalamud.Injector.csproj
+++ b/Dalamud.Injector/Dalamud.Injector.csproj
@@ -1,60 +1,89 @@
+
- AnyCPU
- net48
- 8.0
- AnyCPU;x64
-
-
- WinExe
- $(SolutionDir)bin
- false
- true
- Portable
- IDE1006;CS1701;CS1702
- true
- $(SolutionDir)\bin\Dalamud.Injector.xml
+ net5.0
+ win-x64
+ x64
+ x64;AnyCPU
+ 9.0
+
+ 5.2.4.6
+ XIV Launcher addon injector
+ $(InjectorVersion)
+ $(InjectorVersion)
+ $(InjectorVersion)
+
+
+
+ Library
+ ..\bin\$(Configuration)\
+ false
+ false
+ true
+ false
+
+
+
+
+ true
+
+
+
+ true
+ true
+ portable
+ true
+ annotations
true
- 5.2.4.6
- 5.2.4.6
- XIVLauncher addon injection
- 5.2.4.6
+
+
+
+ Debug
+
+
+ DEBUG;TRACE
$(MSBuildProjectDirectory)\
$(AppOutputBase)=C:\goatsoft\companysecrets\injector\
- true
-
- false
-
-
- dalamud.ico
+
+
+ IDE1006;CS1591;CS1701;CS1702
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
-
-
+
+
-
+
+
+
+
+
+
+
-
-
-
diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs
new file mode 100644
index 000000000..eff8b81a9
--- /dev/null
+++ b/Dalamud.Injector/EntryPoint.cs
@@ -0,0 +1,275 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+
+using Dalamud.Game;
+using Dalamud.Interface.Internal;
+using Newtonsoft.Json;
+using Reloaded.Memory.Buffers;
+using Serilog;
+using Serilog.Core;
+using Serilog.Events;
+
+using static Dalamud.Injector.NativeFunctions;
+
+namespace Dalamud.Injector
+{
+ ///
+ /// Entrypoint to the program.
+ ///
+ public sealed class EntryPoint
+ {
+ ///
+ /// A delegate used during initialization of the CLR from Dalamud.Injector.Boot.
+ ///
+ /// Count of arguments.
+ /// char** string arguments.
+ public delegate void MainDelegate(int argc, IntPtr argvPtr);
+
+ ///
+ /// Start the Dalamud injector.
+ ///
+ /// Count of arguments.
+ /// byte** string arguments.
+ public static void Main(int argc, IntPtr argvPtr)
+ {
+ InitUnhandledException();
+ InitLogging();
+
+ var args = new string[argc];
+
+ unsafe
+ {
+ var argv = (IntPtr*)argvPtr;
+ for (var i = 0; i < argc; i++)
+ {
+ args[i] = Marshal.PtrToStringUni(argv[i]);
+ }
+ }
+
+ var cwd = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory;
+ if (cwd.FullName != Directory.GetCurrentDirectory())
+ {
+ Log.Debug($"Changing cwd to {cwd}");
+ Directory.SetCurrentDirectory(cwd.FullName);
+ }
+
+ var process = GetProcess(args.ElementAtOrDefault(1));
+ var startInfo = GetStartInfo(args.ElementAtOrDefault(2), process);
+
+ startInfo.WorkingDirectory = Directory.GetCurrentDirectory();
+
+ // This seems to help with the STATUS_INTERNAL_ERROR condition
+ Thread.Sleep(1000);
+
+ Inject(process, startInfo);
+
+ Thread.Sleep(1000);
+ }
+
+ private static void InitUnhandledException()
+ {
+ AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
+ {
+ if (Log.Logger == null)
+ {
+ Console.WriteLine($"A fatal error has occurred: {eventArgs.ExceptionObject}");
+ }
+ else
+ {
+ var exObj = eventArgs.ExceptionObject;
+ if (exObj is Exception ex)
+ {
+ Log.Error(ex, "A fatal error has occurred.");
+ }
+ else
+ {
+ Log.Error($"A fatal error has occurred: {eventArgs.ExceptionObject}");
+ }
+ }
+
+#if DEBUG
+ var caption = "Debug Error";
+ var message =
+ $"Couldn't inject.\nMake sure that Dalamud was not injected into your target process " +
+ $"as a release build before and that the target process can be accessed with VM_WRITE permissions.\n\n" +
+ $"{eventArgs.ExceptionObject}";
+#else
+ var caption = "XIVLauncher Error";
+ var message =
+ "Failed to inject the XIVLauncher in-game addon.\nPlease try restarting your game and your PC.\n" +
+ "If this keeps happening, please report this error.";
+#endif
+ _ = MessageBoxW(IntPtr.Zero, message, caption, MessageBoxType.IconError | MessageBoxType.Ok);
+
+ Environment.Exit(0);
+ };
+ }
+
+ private static void InitLogging()
+ {
+ var baseDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+
+#if DEBUG
+ var logPath = Path.Combine(baseDirectory, "injector.log");
+#else
+ var logPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.injector.log");
+#endif
+
+ var levelSwitch = new LoggingLevelSwitch();
+
+#if DEBUG
+ levelSwitch.MinimumLevel = LogEventLevel.Verbose;
+#else
+ levelSwitch.MinimumLevel = LogEventLevel.Information;
+#endif
+
+ Log.Logger = new LoggerConfiguration()
+ .WriteTo.Async(a => a.File(logPath))
+ .WriteTo.Sink(SerilogEventSink.Instance)
+ .MinimumLevel.ControlledBy(levelSwitch)
+ .CreateLogger();
+ }
+
+ private static Process GetProcess(string arg)
+ {
+ Process process;
+
+ var pid = -1;
+ if (arg != default)
+ {
+ pid = int.Parse(arg);
+ }
+
+ switch (pid)
+ {
+ case -1:
+ process = Process.GetProcessesByName("ffxiv_dx11").FirstOrDefault();
+
+ if (process == default)
+ {
+ throw new Exception("Could not find process");
+ }
+
+ break;
+ case -2:
+ var exePath = "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn\\game\\ffxiv_dx11.exe";
+ var exeArgs = new StringBuilder()
+ .Append("DEV.TestSID=0 DEV.UseSqPack=1 DEV.DataPathType=1 ")
+ .Append("DEV.LobbyHost01=127.0.0.1 DEV.LobbyPort01=54994 ")
+ .Append("DEV.LobbyHost02=127.0.0.1 DEV.LobbyPort02=54994 ")
+ .Append("DEV.LobbyHost03=127.0.0.1 DEV.LobbyPort03=54994 ")
+ .Append("DEV.LobbyHost04=127.0.0.1 DEV.LobbyPort04=54994 ")
+ .Append("DEV.LobbyHost05=127.0.0.1 DEV.LobbyPort05=54994 ")
+ .Append("DEV.LobbyHost06=127.0.0.1 DEV.LobbyPort06=54994 ")
+ .Append("DEV.LobbyHost07=127.0.0.1 DEV.LobbyPort07=54994 ")
+ .Append("DEV.LobbyHost08=127.0.0.1 DEV.LobbyPort08=54994 ")
+ .Append("SYS.Region=0 language=1 version=1.0.0.0 ")
+ .Append("DEV.MaxEntitledExpansionID=2 DEV.GMServerHost=127.0.0.1 DEV.GameQuitMessageBox=0").ToString();
+ process = Process.Start(exePath, exeArgs);
+ Thread.Sleep(1000);
+ break;
+ default:
+ process = Process.GetProcessById(pid);
+ break;
+ }
+
+ return process;
+ }
+
+ private static DalamudStartInfo GetStartInfo(string arg, Process process)
+ {
+ DalamudStartInfo startInfo;
+
+ if (arg != default)
+ {
+ startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(arg)));
+ }
+ else
+ {
+ var ffxivDir = Path.GetDirectoryName(process.MainModule.FileName);
+ var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher");
+
+ var gameVerStr = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver"));
+ var gameVer = GameVersion.Parse(gameVerStr);
+
+ startInfo = new DalamudStartInfo
+ {
+ WorkingDirectory = null,
+ ConfigurationPath = Path.Combine(xivlauncherDir, "dalamudConfig.json"),
+ PluginDirectory = Path.Combine(xivlauncherDir, "installedPlugins"),
+ DefaultPluginDirectory = Path.Combine(xivlauncherDir, "devPlugins"),
+ AssetDirectory = Path.Combine(xivlauncherDir, "dalamudAssets"),
+ GameVersion = gameVer,
+ Language = ClientLanguage.English,
+ OptOutMbCollection = false,
+ };
+
+ Log.Debug(
+ "Creating a new StartInfo with:\n" +
+ $" WorkingDirectory: {startInfo.WorkingDirectory}\n" +
+ $" ConfigurationPath: {startInfo.ConfigurationPath}\n" +
+ $" PluginDirectory: {startInfo.PluginDirectory}\n" +
+ $" DefaultPluginDirectory: {startInfo.DefaultPluginDirectory}\n" +
+ $" AssetDirectory: {startInfo.AssetDirectory}\n" +
+ $" GameVersion: {startInfo.GameVersion}\n" +
+ $" Language: {startInfo.Language}\n" +
+ $" OptOutMbCollection: {startInfo.OptOutMbCollection}");
+
+ Log.Information("A Dalamud start info was not found in the program arguments. One has been generated for you.");
+ Log.Information("Copy the following contents into the program arguments:");
+
+ var startInfoJson = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(startInfo)));
+ Log.Information(startInfoJson);
+ }
+
+ return startInfo;
+ }
+
+ private static void Inject(Process process, DalamudStartInfo startInfo)
+ {
+ var nethostName = "nethost.dll";
+ var bootName = "Dalamud.Boot.dll";
+
+ var nethostPath = Path.GetFullPath(nethostName);
+ var bootPath = Path.GetFullPath(bootName);
+
+ // ======================================================
+
+ using var injector = new Injector(process);
+
+ injector.LoadLibrary(nethostPath, out _);
+ injector.LoadLibrary(bootPath, out var bootModule);
+
+ // ======================================================
+
+ var startInfoJson = JsonConvert.SerializeObject(startInfo);
+ var startInfoBytes = Encoding.UTF8.GetBytes(startInfoJson);
+
+ using var startInfoBuffer = new MemoryBufferHelper(process).CreatePrivateMemoryBuffer(startInfoBytes.Length + 0x8);
+ var startInfoAddress = startInfoBuffer.Add(startInfoBytes);
+
+ if (startInfoAddress == IntPtr.Zero)
+ throw new Exception("Unable to allocate start info JSON");
+
+ injector.GetFunctionAddress(bootModule, "Initialize", out var initAddress);
+ injector.CallRemoteFunction(initAddress, startInfoAddress, out var exitCode);
+
+ // ======================================================
+
+ if (exitCode > 0)
+ {
+ Log.Error($"Dalamud.Boot::Initialize returned {exitCode}");
+ return;
+ }
+
+ Log.Information("Done");
+ }
+ }
+}
diff --git a/Dalamud.Injector/GlobalSuppressions.cs b/Dalamud.Injector/GlobalSuppressions.cs
index 3fca475cc..e1978fae4 100644
--- a/Dalamud.Injector/GlobalSuppressions.cs
+++ b/Dalamud.Injector/GlobalSuppressions.cs
@@ -6,14 +6,8 @@
using System.Diagnostics.CodeAnalysis;
// General
-[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "Preventing long lines", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "I like regions", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1123:Do not place regions within elements", Justification = "I like regions in elements too", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
+[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "This is annoying", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1512:Single-line comments should not be followed by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1515:Single-line comment should be preceded by blank line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1127:Generic type constraints should be on their own line", Justification = "I like this better", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
-[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")]
-
-// Program.cs
-[assembly: SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "Used during #if DEBUG", Scope = "member", Target = "~M:Dalamud.Injector.Program.NativeInject(System.Diagnostics.Process)")]
+[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "I'll make what I want static", Scope = "namespaceanddescendants", Target = "~N:Dalamud")]
diff --git a/Dalamud.Injector/Injector.cs b/Dalamud.Injector/Injector.cs
new file mode 100644
index 000000000..3f9c14046
--- /dev/null
+++ b/Dalamud.Injector/Injector.cs
@@ -0,0 +1,303 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using Iced.Intel;
+using PeNet;
+using PeNet.Header.Pe;
+using Reloaded.Memory.Buffers;
+using Reloaded.Memory.Sources;
+using Reloaded.Memory.Utilities;
+
+using static Dalamud.Injector.NativeFunctions;
+using static Iced.Intel.AssemblerRegisters;
+
+namespace Dalamud.Injector
+{
+ ///
+ /// This class implements injecting into a remote process. It is a highly stripped down version of the
+ /// https://github.com/Reloaded-Project injector/assembler implementation due to issues with Lutris and
+ /// Wine.
+ ///
+ internal sealed class Injector : IDisposable
+ {
+ private readonly Process targetProcess;
+ private readonly ExternalMemory extMemory;
+ private readonly CircularBuffer circularBuffer;
+ private readonly PrivateMemoryBuffer privateBuffer;
+
+ private IntPtr loadLibraryShellPtr;
+ private IntPtr loadLibraryRetPtr;
+
+ private IntPtr getProcAddressShellPtr;
+ private IntPtr getProcAddressRetPtr;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Process to inject.
+ public Injector(Process targetProcess)
+ {
+ this.targetProcess = targetProcess;
+
+ this.extMemory = new ExternalMemory(targetProcess);
+ this.circularBuffer = new CircularBuffer(4096, this.extMemory);
+ this.privateBuffer = new MemoryBufferHelper(targetProcess).CreatePrivateMemoryBuffer(4096);
+
+ using var kernel32Module = this.GetProcessModule("KERNEL32.DLL");
+ var kernel32PeFile = new PeFile(kernel32Module.FileName);
+ var kernel32Exports = kernel32PeFile.ExportedFunctions;
+
+ this.SetupLoadLibrary(kernel32Module, kernel32Exports);
+ this.SetupGetProcAddress(kernel32Module, kernel32Exports);
+ }
+
+ ///
+ /// Finalizes an instance of the class.
+ ///
+ ~Injector() => this.Dispose();
+
+ ///
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+
+ this.targetProcess?.Dispose();
+ this.circularBuffer?.Dispose();
+ this.privateBuffer?.Dispose();
+ }
+
+ ///
+ /// Load a module by absolute file path.
+ ///
+ /// Absolute file path.
+ /// Address to the module.
+ public void LoadLibrary(string modulePath, out IntPtr address)
+ {
+ var lpParameter = this.WriteNullTerminatedUnicodeString(modulePath);
+
+ if (lpParameter == IntPtr.Zero)
+ throw new Exception("Unable to allocate LoadLibraryW parameter");
+
+ var threadHandle = CreateRemoteThread(
+ this.targetProcess.Handle,
+ IntPtr.Zero,
+ UIntPtr.Zero,
+ this.loadLibraryShellPtr,
+ lpParameter,
+ CreateThreadFlags.RunImmediately,
+ out _);
+
+ _ = WaitForSingleObject(threadHandle, uint.MaxValue);
+
+ this.extMemory.Read(this.loadLibraryRetPtr, out address);
+
+ if (address == IntPtr.Zero)
+ throw new Exception($"Error calling LoadLibraryW with {modulePath}");
+ }
+
+ ///
+ /// Get the address of an exported module function.
+ ///
+ /// Module address.
+ /// Name of the exported method.
+ /// Address to the function.
+ public void GetFunctionAddress(IntPtr module, string functionName, out IntPtr address)
+ {
+ var getProcAddressParams = new GetProcAddressParams(module, this.WriteNullTerminatedASCIIString(functionName));
+ var lpParameter = this.circularBuffer.Add(ref getProcAddressParams);
+
+ if (lpParameter == IntPtr.Zero)
+ throw new Exception("Unable to allocate GetProcAddress parameter ptr");
+
+ var threadHandle = CreateRemoteThread(
+ this.targetProcess.Handle,
+ IntPtr.Zero,
+ UIntPtr.Zero,
+ this.getProcAddressShellPtr,
+ lpParameter,
+ CreateThreadFlags.RunImmediately,
+ out _);
+
+ _ = WaitForSingleObject(threadHandle, uint.MaxValue);
+
+ this.extMemory.Read(this.getProcAddressRetPtr, out address);
+
+ if (address == IntPtr.Zero)
+ throw new Exception($"Error calling GetProcAddress with {functionName}");
+ }
+
+ ///
+ /// Call a method in a remote process via CreateRemoteThread.
+ ///
+ /// Method address.
+ /// Parameter address.
+ /// Thread exit code.
+ public void CallRemoteFunction(IntPtr methodAddress, IntPtr parameterAddress, out uint exitCode)
+ {
+ // Create and initialize a thread at our address and parameter address.
+ var threadHandle = CreateRemoteThread(
+ this.targetProcess.Handle,
+ IntPtr.Zero,
+ UIntPtr.Zero,
+ methodAddress,
+ parameterAddress,
+ CreateThreadFlags.RunImmediately,
+ out _);
+
+ _ = WaitForSingleObject(threadHandle, uint.MaxValue);
+
+ GetExitCodeThread(threadHandle, out exitCode);
+ }
+
+ private void SetupLoadLibrary(ProcessModule kernel32Module, ExportFunction[] kernel32Exports)
+ {
+ var offset = this.GetExportedFunctionOffset(kernel32Exports, "LoadLibraryW");
+ var functionAddr = kernel32Module.BaseAddress + (int)offset;
+ var functionPtr = this.privateBuffer.Add(ref functionAddr);
+
+ if (functionPtr == IntPtr.Zero)
+ throw new Exception("Unable to allocate LoadLibraryW function ptr");
+
+ var dummy = 0L;
+ this.loadLibraryRetPtr = this.privateBuffer.Add(ref dummy);
+
+ if (this.loadLibraryRetPtr == IntPtr.Zero)
+ throw new Exception("Unable to allocate LoadLibraryW return value");
+
+ var func = functionPtr.ToInt64();
+ var retVal = this.loadLibraryRetPtr.ToInt64();
+
+ var asm = new Assembler(64);
+
+ asm.sub(rsp, 40); // sub rsp, 40 // Re-align stack to 16 byte boundary + shadow space.
+ asm.call(__qword_ptr[__qword_ptr[func]]); // call qword [qword func] // CreateRemoteThread lpParameter with string already in ECX.
+ asm.mov(__qword_ptr[__qword_ptr[retVal]], rax); // mov qword [qword retVal], rax //
+ asm.add(rsp, 40); // add rsp, 40 // Re-align stack to 16 byte boundary + shadow space.
+ asm.ret(); // ret // Restore stack ptr. (Callee cleanup)
+
+ var bytes = this.Assemble(asm);
+ this.loadLibraryShellPtr = this.privateBuffer.Add(bytes);
+
+ if (this.loadLibraryShellPtr == IntPtr.Zero)
+ throw new Exception("Unable to allocate LoadLibraryW shellcode");
+ }
+
+ private void SetupGetProcAddress(ProcessModule kernel32Module, ExportFunction[] kernel32Exports)
+ {
+ var offset = this.GetExportedFunctionOffset(kernel32Exports, "GetProcAddress");
+ var functionAddr = kernel32Module.BaseAddress + (int)offset;
+ var functionPtr = this.privateBuffer.Add(ref functionAddr);
+
+ if (functionPtr == IntPtr.Zero)
+ throw new Exception("Unable to allocate GetProcAddress function ptr");
+
+ var dummy = 0L;
+ this.getProcAddressRetPtr = this.privateBuffer.Add(ref dummy);
+
+ if (this.getProcAddressRetPtr == IntPtr.Zero)
+ throw new Exception("Unable to allocate GetProcAddress return value");
+
+ var func = functionPtr.ToInt64();
+ var retVal = this.getProcAddressRetPtr.ToInt64();
+
+ var asm = new Assembler(64);
+
+ asm.sub(rsp, 40); // sub rsp, 40 // Re-align stack to 16 byte boundary +32 shadow space
+ asm.mov(rdx, __qword_ptr[__qword_ptr[rcx + 8]]); // mov rdx, qword [qword rcx + 8] // lpProcName
+ asm.mov(rcx, __qword_ptr[__qword_ptr[rcx + 0]]); // mov rcx, qword [qword rcx + 0] // hModule
+ asm.call(__qword_ptr[__qword_ptr[func]]); // call qword [qword func] //
+ asm.mov(__qword_ptr[__qword_ptr[retVal]], rax); // mov qword [qword retVal] //
+ asm.add(rsp, 40); // add rsp, 40 // Re-align stack to 16 byte boundary + shadow space.
+ asm.ret(); // ret // Restore stack ptr. (Callee cleanup)
+
+ var bytes = this.Assemble(asm);
+ this.getProcAddressShellPtr = this.privateBuffer.Add(bytes);
+
+ if (this.getProcAddressShellPtr == IntPtr.Zero)
+ throw new Exception("Unable to allocate GetProcAddress shellcode");
+ }
+
+ private byte[] Assemble(Assembler assembler)
+ {
+ using var stream = new MemoryStream();
+ assembler.Assemble(new StreamCodeWriter(stream), 0);
+
+ stream.Position = 0;
+ var reader = new StreamCodeReader(stream);
+
+ int next;
+ var bytes = new byte[stream.Length];
+ while ((next = reader.ReadByte()) >= 0)
+ {
+ bytes[stream.Position - 1] = (byte)next;
+ }
+
+ return bytes;
+ }
+
+ private ProcessModule GetProcessModule(string moduleName)
+ {
+ var modules = this.targetProcess.Modules;
+ for (var i = 0; i < modules.Count; i++)
+ {
+ var module = modules[i];
+ if (module.ModuleName.Equals(moduleName, StringComparison.InvariantCultureIgnoreCase))
+ {
+ return module;
+ }
+ }
+
+ throw new Exception($"Failed to find {moduleName} in target process' modules");
+ }
+
+ private uint GetExportedFunctionOffset(ExportFunction[] exportFunctions, string functionName)
+ {
+ var exportFunction = exportFunctions.FirstOrDefault(func => func.Name == functionName);
+
+ if (exportFunction == default)
+ throw new Exception($"Failed to find exported function {functionName} in target module's exports");
+
+ return exportFunction.Address;
+ }
+
+ private IntPtr WriteNullTerminatedASCIIString(string libraryPath)
+ {
+ var libraryNameBytes = Encoding.ASCII.GetBytes(libraryPath + '\0');
+ var value = this.circularBuffer.Add(libraryNameBytes);
+
+ if (value == IntPtr.Zero)
+ throw new Exception("Unable to write ASCII string to buffer");
+
+ return value;
+ }
+
+ private IntPtr WriteNullTerminatedUnicodeString(string libraryPath)
+ {
+ var libraryNameBytes = Encoding.Unicode.GetBytes(libraryPath + '\0');
+ var value = this.circularBuffer.Add(libraryNameBytes);
+
+ if (value == IntPtr.Zero)
+ throw new Exception("Unable to write Unicode string to buffer");
+
+ return value;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct GetProcAddressParams
+ {
+ public GetProcAddressParams(IntPtr hModule, IntPtr lPProcName)
+ {
+ this.HModule = hModule.ToInt64();
+ this.LPProcName = lPProcName.ToInt64();
+ }
+
+ public long HModule { get; set; }
+
+ public long LPProcName { get; set; }
+ }
+ }
+}
diff --git a/Dalamud.Injector/NativeFunctions.cs b/Dalamud.Injector/NativeFunctions.cs
index 90e61a5fc..46da6728c 100644
--- a/Dalamud.Injector/NativeFunctions.cs
+++ b/Dalamud.Injector/NativeFunctions.cs
@@ -1,14 +1,234 @@
using System;
-using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
-using System.Security;
namespace Dalamud.Injector
{
///
- /// Native functions.
+ /// Native user32 functions.
///
- internal static class NativeFunctions
+ internal static partial class NativeFunctions
+ {
+ ///
+ /// MB_* from winuser.
+ ///
+ public enum MessageBoxType : uint
+ {
+ ///
+ /// The default value for any of the various subtypes.
+ ///
+ DefaultValue = 0x0,
+
+ // To indicate the buttons displayed in the message box, specify one of the following values.
+
+ ///
+ /// The message box contains three push buttons: Abort, Retry, and Ignore.
+ ///
+ AbortRetryIgnore = 0x2,
+
+ ///
+ /// The message box contains three push buttons: Cancel, Try Again, Continue. Use this message box type instead
+ /// of MB_ABORTRETRYIGNORE.
+ ///
+ CancelTryContinue = 0x6,
+
+ ///
+ /// Adds a Help button to the message box. When the user clicks the Help button or presses F1, the system sends
+ /// a WM_HELP message to the owner.
+ ///
+ Help = 0x4000,
+
+ ///
+ /// The message box contains one push button: OK. This is the default.
+ ///
+ Ok = DefaultValue,
+
+ ///
+ /// The message box contains two push buttons: OK and Cancel.
+ ///
+ OkCancel = 0x1,
+
+ ///
+ /// The message box contains two push buttons: Retry and Cancel.
+ ///
+ RetryCancel = 0x5,
+
+ ///
+ /// The message box contains two push buttons: Yes and No.
+ ///
+ YesNo = 0x4,
+
+ ///
+ /// The message box contains three push buttons: Yes, No, and Cancel.
+ ///
+ YesNoCancel = 0x3,
+
+ // To display an icon in the message box, specify one of the following values.
+
+ ///
+ /// An exclamation-point icon appears in the message box.
+ ///
+ IconExclamation = 0x30,
+
+ ///
+ /// An exclamation-point icon appears in the message box.
+ ///
+ IconWarning = IconExclamation,
+
+ ///
+ /// An icon consisting of a lowercase letter i in a circle appears in the message box.
+ ///
+ IconInformation = 0x40,
+
+ ///
+ /// An icon consisting of a lowercase letter i in a circle appears in the message box.
+ ///
+ IconAsterisk = IconInformation,
+
+ ///
+ /// A question-mark icon appears in the message box.
+ /// The question-mark message icon is no longer recommended because it does not clearly represent a specific type
+ /// of message and because the phrasing of a message as a question could apply to any message type. In addition,
+ /// users can confuse the message symbol question mark with Help information. Therefore, do not use this question
+ /// mark message symbol in your message boxes. The system continues to support its inclusion only for backward
+ /// compatibility.
+ ///
+ IconQuestion = 0x20,
+
+ ///
+ /// A stop-sign icon appears in the message box.
+ ///
+ IconStop = 0x10,
+
+ ///
+ /// A stop-sign icon appears in the message box.
+ ///
+ IconError = IconStop,
+
+ ///
+ /// A stop-sign icon appears in the message box.
+ ///
+ IconHand = IconStop,
+
+ // To indicate the default button, specify one of the following values.
+
+ ///
+ /// The first button is the default button.
+ /// MB_DEFBUTTON1 is the default unless MB_DEFBUTTON2, MB_DEFBUTTON3, or MB_DEFBUTTON4 is specified.
+ ///
+ DefButton1 = DefaultValue,
+
+ ///
+ /// The second button is the default button.
+ ///
+ DefButton2 = 0x100,
+
+ ///
+ /// The third button is the default button.
+ ///
+ DefButton3 = 0x200,
+
+ ///
+ /// The fourth button is the default button.
+ ///
+ DefButton4 = 0x300,
+
+ // To indicate the modality of the dialog box, specify one of the following values.
+
+ ///
+ /// The user must respond to the message box before continuing work in the window identified by the hWnd parameter.
+ /// However, the user can move to the windows of other threads and work in those windows. Depending on the hierarchy
+ /// of windows in the application, the user may be able to move to other windows within the thread. All child windows
+ /// of the parent of the message box are automatically disabled, but pop-up windows are not. MB_APPLMODAL is the
+ /// default if neither MB_SYSTEMMODAL nor MB_TASKMODAL is specified.
+ ///
+ ApplModal = DefaultValue,
+
+ ///
+ /// Same as MB_APPLMODAL except that the message box has the WS_EX_TOPMOST style.
+ /// Use system-modal message boxes to notify the user of serious, potentially damaging errors that require immediate
+ /// attention (for example, running out of memory). This flag has no effect on the user's ability to interact with
+ /// windows other than those associated with hWnd.
+ ///
+ SystemModal = 0x1000,
+
+ ///
+ /// Same as MB_APPLMODAL except that all the top-level windows belonging to the current thread are disabled if the
+ /// hWnd parameter is NULL. Use this flag when the calling application or library does not have a window handle
+ /// available but still needs to prevent input to other windows in the calling thread without suspending other threads.
+ ///
+ TaskModal = 0x2000,
+
+ // To specify other options, use one or more of the following values.
+
+ ///
+ /// Same as desktop of the interactive window station. For more information, see Window Stations. If the current
+ /// input desktop is not the default desktop, MessageBox does not return until the user switches to the default
+ /// desktop.
+ ///
+ DefaultDesktopOnly = 0x20000,
+
+ ///
+ /// The text is right-justified.
+ ///
+ Right = 0x80000,
+
+ ///
+ /// Displays message and caption text using right-to-left reading order on Hebrew and Arabic systems.
+ ///
+ RtlReading = 0x100000,
+
+ ///
+ /// The message box becomes the foreground window. Internally, the system calls the SetForegroundWindow function
+ /// for the message box.
+ ///
+ SetForeground = 0x10000,
+
+ ///
+ /// The message box is created with the WS_EX_TOPMOST window style.
+ ///
+ Topmost = 0x40000,
+
+ ///
+ /// The caller is a service notifying the user of an event. The function displays a message box on the current active
+ /// desktop, even if there is no user logged on to the computer.
+ ///
+ ServiceNotification = 0x200000,
+ }
+
+ ///
+ /// Displays a modal dialog box that contains a system icon, a set of buttons, and a brief application-specific message,
+ /// such as status or error information. The message box returns an integer value that indicates which button the user
+ /// clicked.
+ ///
+ ///
+ /// A handle to the owner window of the message box to be created. If this parameter is NULL, the message box has no
+ /// owner window.
+ ///
+ ///
+ /// The message to be displayed. If the string consists of more than one line, you can separate the lines using a carriage
+ /// return and/or linefeed character between each line.
+ ///
+ ///
+ /// The dialog box title. If this parameter is NULL, the default title is Error.
+ ///
+ /// The contents and behavior of the dialog box. This parameter can be a combination of flags from the following groups
+ /// of flags.
+ ///
+ ///
+ /// If a message box has a Cancel button, the function returns the IDCANCEL value if either the ESC key is pressed or
+ /// the Cancel button is selected. If the message box has no Cancel button, pressing ESC will no effect - unless an
+ /// MB_OK button is present. If an MB_OK button is displayed and the user presses ESC, the return value will be IDOK.
+ /// If the function fails, the return value is zero.To get extended error information, call GetLastError. If the function
+ /// succeeds, the return value is one of the ID* enum values.
+ ///
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern int MessageBoxW(IntPtr hWnd, string text, string caption, MessageBoxType type);
+ }
+
+ ///
+ /// Native kernel32 functions.
+ ///
+ internal static partial class NativeFunctions
{
///
/// MEM_* from memoryapi.
@@ -20,14 +240,14 @@ namespace Dalamud.Injector
/// To coalesce two adjacent placeholders, specify MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS. When you coalesce
/// placeholders, lpAddress and dwSize must exactly match those of the placeholder.
///
- CoalescePlaceholders = 0x00000001,
+ CoalescePlaceholders = 0x1,
///
/// Frees an allocation back to a placeholder (after you've replaced a placeholder with a private allocation using
/// VirtualAlloc2 or Virtual2AllocFromApp). To split a placeholder into two placeholders, specify
/// MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER.
///
- PreservePlaceholder = 0x00000002,
+ PreservePlaceholder = 0x2,
///
/// Allocates memory charges (from the overall size of memory and the paging files on disk) for the specified reserved
@@ -88,7 +308,7 @@ namespace Dalamud.Injector
/// the specified address range is intact. If the function fails, at least some of the data in the address range
/// has been replaced with zeroes. This value cannot be used with any other value. If MEM_RESET_UNDO is called on
/// an address range which was not MEM_RESET earlier, the behavior is undefined. When you specify MEM_RESET, the
- /// VirtualAllocEx function ignores the value of flProtect. However, you must still set flProtect to a valid
+ /// VirtualAllocEx function ignores the value of flProtect. However, you must still set flProtect to a valid
/// protection value, such as PAGE_NOACCESS.
///
ResetUndo = 0x1000000,
@@ -122,6 +342,28 @@ namespace Dalamud.Injector
LargePages = 0x20000000,
}
+ ///
+ /// Unprefixed flags from CreateRemoteThread.
+ ///
+ [Flags]
+ public enum CreateThreadFlags
+ {
+ ///
+ /// The thread runs immediately after creation.
+ ///
+ RunImmediately = 0x0,
+
+ ///
+ /// The thread is created in a suspended state, and does not run until the ResumeThread function is called.
+ ///
+ CreateSuspended = 0x4,
+
+ ///
+ /// The dwStackSize parameter specifies the initial reserve size of the stack. If this flag is not specified, dwStackSize specifies the commit size.
+ ///
+ StackSizeParamIsReservation = 0x10000,
+ }
+
///
/// PAGE_* from memoryapi.
///
@@ -198,7 +440,7 @@ namespace Dalamud.Injector
/// The default behavior for VirtualProtect protection change to executable is to mark all locations as valid call
/// targets for CFG.
///
- TargetsNoUpdate = 0x40000000,
+ TargetsNoUpdate = TargetsInvalid,
///
/// Pages in the region become guard pages. Any attempt to access a guard page causes the system to raise a
@@ -312,23 +554,33 @@ namespace Dalamud.Injector
}
///
- /// Closes an open object handle.
+ /// WAIT_* from synchapi.
///
- ///
- /// A valid handle to an open object.
- ///
- ///
- /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero.To get extended
- /// error information, call GetLastError. If the application is running under a debugger, the function will throw an
- /// exception if it receives either a handle value that is not valid or a pseudo-handle value. This can happen if you
- /// close a handle twice, or if you call CloseHandle on a handle returned by the FindFirstFile function instead of calling
- /// the FindClose function.
- ///
- [DllImport("kernel32.dll", SetLastError = true)]
- [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
- [SuppressUnmanagedCodeSecurity]
- [return: MarshalAs(UnmanagedType.Bool)]
- public static extern bool CloseHandle(IntPtr hObject);
+ public enum WaitResult
+ {
+ ///
+ /// The specified object is a mutex object that was not released by the thread that owned the mutex object
+ /// before the owning thread terminated.Ownership of the mutex object is granted to the calling thread and
+ /// the mutex state is set to nonsignaled. If the mutex was protecting persistent state information, you
+ /// should check it for consistency.
+ ///
+ Abandoned = 0x80,
+
+ ///
+ /// The state of the specified object is signaled.
+ ///
+ Object0 = 0x0,
+
+ ///
+ /// The time-out interval elapsed, and the object's state is nonsignaled.
+ ///
+ Timeout = 0x102,
+
+ ///
+ /// The function has failed. To get extended error information, call GetLastError.
+ ///
+ WAIT_FAILED = 0xFFFFFFF,
+ }
///
/// Creates a thread that runs in the virtual address space of another process. Use the CreateRemoteThreadEx function
@@ -336,23 +588,23 @@ namespace Dalamud.Injector
///
///
/// A handle to the process in which the thread is to be created. The handle must have the PROCESS_CREATE_THREAD,
- /// PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ access rights, and may fail
- /// without these rights on certain platforms. For more information, see Process Security and Access Rights.
+ /// PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ access rights, and may fail without
+ /// these rights on certain platforms. For more information, see Process Security and Access Rights.
///
///
- /// A pointer to a SECURITY_ATTRIBUTES structure that specifies a security descriptor for the new thread and determines
- /// whether child processes can inherit the returned handle. If lpThreadAttributes is NULL, the thread gets a default
- /// security descriptor and the handle cannot be inherited. The access control lists (ACL) in the default security descriptor
- /// for a thread come from the primary token of the creator.
+ /// A pointer to a SECURITY_ATTRIBUTES structure that specifies a security descriptor for the new thread and determines whether
+ /// child processes can inherit the returned handle. If lpThreadAttributes is NULL, the thread gets a default security descriptor
+ /// and the handle cannot be inherited. The access control lists (ACL) in the default security descriptor for a thread come from
+ /// the primary token of the creator.
///
///
- /// The initial size of the stack, in bytes. The system rounds this value to the nearest page. If this parameter is
- /// 0 (zero), the new thread uses the default size for the executable. For more information, see Thread Stack Size.
+ /// The initial size of the stack, in bytes. The system rounds this value to the nearest page. If this parameter is 0 (zero), the
+ /// new thread uses the default size for the executable. For more information, see Thread Stack Size.
///
///
- /// A pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread and
- /// represents the starting address of the thread in the remote process. The function must exist in the remote process.
- /// For more information, see ThreadProc.
+ /// A pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread and represents the
+ /// starting address of the thread in the remote process. The function must exist in the remote process. For more information,
+ /// see ThreadProc.
///
///
/// A pointer to a variable to be passed to the thread function.
@@ -361,92 +613,43 @@ namespace Dalamud.Injector
/// The flags that control the creation of the thread.
///
///
- /// A pointer to a variable that receives the thread identifier. If this parameter is NULL, the thread identifier is
- /// not returned.
+ /// A pointer to a variable that receives the thread identifier. If this parameter is NULL, the thread identifier is not returned.
///
///
- /// If the function succeeds, the return value is a handle to the new thread. If the function fails, the return value
- /// is NULL.To get extended error information, call GetLastError. Note that CreateRemoteThread may succeed even if
- /// lpStartAddress points to data, code, or is not accessible. If the start address is invalid when the thread runs,
- /// an exception occurs, and the thread terminates. Thread termination due to a invalid start address is handled as
- /// an error exit for the thread's process. This behavior is similar to the asynchronous nature of CreateProcess, where
- /// the process is created even if it refers to invalid or missing dynamic-link libraries (DLL).
+ /// If the function succeeds, the return value is a handle to the new thread. If the function fails, the return value is
+ /// NULL.To get extended error information, call GetLastError. Note that CreateRemoteThread may succeed even if lpStartAddress
+ /// points to data, code, or is not accessible. If the start address is invalid when the thread runs, an exception occurs, and
+ /// the thread terminates. Thread termination due to a invalid start address is handled as an error exit for the thread's process.
+ /// This behavior is similar to the asynchronous nature of CreateProcess, where the process is created even if it refers to
+ /// invalid or missing dynamic-link libraries (DLL).
///
- [DllImport("kernel32.dll")]
+ [DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr CreateRemoteThread(
IntPtr hProcess,
IntPtr lpThreadAttributes,
- uint dwStackSize,
+ UIntPtr dwStackSize,
IntPtr lpStartAddress,
IntPtr lpParameter,
- uint dwCreationFlags,
- IntPtr lpThreadId);
+ CreateThreadFlags dwCreationFlags,
+ out uint lpThreadId);
///
- /// See https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlew.
- /// Retrieves a module handle for the specified module. The module must have been loaded by the calling process. To
- /// avoid the race conditions described in the Remarks section, use the GetModuleHandleEx function.
+ /// Retrieves the termination status of the specified thread.
///
- ///
- /// The name of the loaded module (either a .dll or .exe file). If the file name extension is omitted, the default library
- /// extension .dll is appended. The file name string can include a trailing point character (.) to indicate that the
- /// module name has no extension. The string does not have to specify a path. When specifying a path, be sure to use
- /// backslashes (\), not forward slashes (/). The name is compared (case independently) to the names of modules currently
- /// mapped into the address space of the calling process. If this parameter is NULL, GetModuleHandle returns a handle
- /// to the file used to create the calling process (.exe file). The GetModuleHandle function does not retrieve handles
- /// for modules that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx.
+ ///
+ /// A handle to the thread. The handle must have the THREAD_QUERY_INFORMATION or THREAD_QUERY_LIMITED_INFORMATION
+ /// access right.For more information, see Thread Security and Access Rights.
+ ///
+ ///
+ /// A pointer to a variable to receive the thread termination status.
///
///
- /// If the function succeeds, the return value is a handle to the specified module. If the function fails, the return
- /// value is NULL.To get extended error information, call GetLastError.
- ///
- [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
- public static extern IntPtr GetModuleHandle(string lpModuleName);
-
- ///
- /// Retrieves the address of an exported function or variable from the specified dynamic-link library (DLL).
- ///
- ///
- /// A handle to the DLL module that contains the function or variable. The LoadLibrary, LoadLibraryEx, LoadPackagedLibrary,
- /// or GetModuleHandle function returns this handle. The GetProcAddress function does not retrieve addresses from modules
- /// that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx.
- ///
- ///
- /// The function or variable name, or the function's ordinal value. If this parameter is an ordinal value, it must be
- /// in the low-order word; the high-order word must be zero.
- ///
- ///
- /// If the function succeeds, the return value is the address of the exported function or variable. If the function
- /// fails, the return value is NULL.To get extended error information, call GetLastError.
- ///
- [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
- public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
-
- ///
- /// See https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess.
- /// Opens an existing local process object.
- ///
- ///
- /// The access to the process object. This access right is checked against the security descriptor for the process.
- /// This parameter can be one or more of the process access rights. If the caller has enabled the SeDebugPrivilege
- /// privilege, the requested access is granted regardless of the contents of the security descriptor.
- ///
- ///
- /// If this value is TRUE, processes created by this process will inherit the handle. Otherwise, the processes do
- /// not inherit this handle.
- ///
- ///
- /// The identifier of the local process to be opened.
- ///
- ///
- /// If the function succeeds, the return value is an open handle to the specified process. If the function fails, the
- /// return value is NULL.To get extended error information, call GetLastError.
+ /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get
+ /// extended error information, call GetLastError.
///
[DllImport("kernel32.dll", SetLastError = true)]
- public static extern IntPtr OpenProcess(
- ProcessAccessFlags processAccess,
- bool bInheritHandle,
- int processId);
+ [return: MarshalAs(UnmanagedType.Bool)]
+ public static extern bool GetExitCodeThread(IntPtr hThread, out uint lpExitCode);
///
/// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex.
@@ -530,6 +733,27 @@ namespace Dalamud.Injector
int dwSize,
AllocationType dwFreeType);
+ ///
+ /// Waits until the specified object is in the signaled state or the time-out interval elapses. To enter an alertable wait
+ /// state, use the WaitForSingleObjectEx function.To wait for multiple objects, use WaitForMultipleObjects.
+ ///
+ ///
+ /// A handle to the object. For a list of the object types whose handles can be specified, see the following Remarks section.
+ /// If this handle is closed while the wait is still pending, the function's behavior is undefined. The handle must have the
+ /// SYNCHRONIZE access right. For more information, see Standard Access Rights.
+ ///
+ ///
+ /// The time-out interval, in milliseconds. If a nonzero value is specified, the function waits until the object is signaled
+ /// or the interval elapses. If dwMilliseconds is zero, the function does not enter a wait state if the object is not signaled;
+ /// it always returns immediately. If dwMilliseconds is INFINITE, the function will return only when the object is signaled.
+ ///
+ ///
+ /// If the function succeeds, the return value indicates the event that caused the function to return.
+ /// It can be one of the WaitResult values.
+ ///
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
+
///
/// Writes data to an area of memory in a specified process. The entire area to be written to must be accessible or
/// the operation fails.
diff --git a/Dalamud.Injector/Program.cs b/Dalamud.Injector/Program.cs
deleted file mode 100644
index 74f3a1585..000000000
--- a/Dalamud.Injector/Program.cs
+++ /dev/null
@@ -1,208 +0,0 @@
-using System;
-using System.ComponentModel;
-using System.Diagnostics;
-using System.IO;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Threading;
-using System.Windows.Forms;
-
-using EasyHook;
-using Newtonsoft.Json;
-
-namespace Dalamud.Injector
-{
- ///
- /// Application entrypoint.
- ///
- internal static class Program
- {
- private static Process process = null;
-
- private static void Main(string[] args)
- {
- AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
- {
- File.WriteAllText("InjectorException.txt", eventArgs.ExceptionObject.ToString());
-#if !DEBUG
- MessageBox.Show("Failed to inject the XIVLauncher in-game addon.\nPlease try restarting your game and your PC.\nIf this keeps happening, please report this error.", "XIVLauncher Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
-#else
- MessageBox.Show("Couldn't inject.\nMake sure that Dalamud was not injected into your target process as a release build before and that the target process can be accessed with VM_WRITE permissions.\n\n" + eventArgs.ExceptionObject, "Debug Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
-#endif
- Environment.Exit(0);
- };
-
- var pid = -1;
- if (args.Length >= 1)
- {
- pid = int.Parse(args[0]);
- }
-
- switch (pid)
- {
- case -1:
- process = Process.GetProcessesByName("ffxiv_dx11")[0];
- break;
- case -2:
- process = Process.Start(
- "C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn\\game\\ffxiv_dx11.exe",
- "DEV.TestSID=0 DEV.UseSqPack=1 DEV.DataPathType=1 DEV.LobbyHost01=127.0.0.1 DEV.LobbyPort01=54994 DEV.LobbyHost02=127.0.0.1 DEV.LobbyPort02=54994 DEV.LobbyHost03=127.0.0.1 DEV.LobbyPort03=54994 DEV.LobbyHost04=127.0.0.1 DEV.LobbyPort04=54994 DEV.LobbyHost05=127.0.0.1 DEV.LobbyPort05=54994 DEV.LobbyHost06=127.0.0.1 DEV.LobbyPort06=54994 DEV.LobbyHost07=127.0.0.1 DEV.LobbyPort07=54994 DEV.LobbyHost08=127.0.0.1 DEV.LobbyPort08=54994 SYS.Region=0 language=1 version=1.0.0.0 DEV.MaxEntitledExpansionID=2 DEV.GMServerHost=127.0.0.1 DEV.GameQuitMessageBox=0");
- Thread.Sleep(1000);
- break;
- default:
- process = Process.GetProcessById(pid);
- break;
- }
-
- DalamudStartInfo startInfo;
- if (args.Length <= 1)
- {
- startInfo = GetDefaultStartInfo();
- Console.WriteLine("\nA Dalamud start info was not found in the program arguments. One has been generated for you.");
- Console.WriteLine("\nCopy the following contents into the program arguments:");
- Console.WriteLine();
- Console.WriteLine(Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(startInfo))));
- }
- else
- {
- startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(args[1])));
- }
-
- startInfo.WorkingDirectory = Directory.GetCurrentDirectory();
-
- // Seems to help with the STATUS_INTERNAL_ERROR condition
- Thread.Sleep(1000);
-
- // Thread.Sleep(10000);
-
- // Inject to process
- Inject(process, startInfo);
-
- Thread.Sleep(1000);
-
-#if DEBUG
- // Inject exception handler
- // NativeInject(process);
-#endif
- }
-
- private static void Inject(Process process, DalamudStartInfo info)
- {
- Console.WriteLine($"Injecting to {process.Id}");
-
- // File check
- var libPath = Path.GetFullPath("Dalamud.dll");
- if (!File.Exists(libPath))
- {
- Console.WriteLine($"Can't find a dll on {libPath}");
- return;
- }
-
- RemoteHooking.Inject(process.Id, InjectionOptions.DoNotRequireStrongName, libPath, libPath, info);
-
- Console.WriteLine("Injected");
- }
-
- private static void NativeInject(Process process)
- {
- var libPath = Path.GetFullPath("DalamudDebugStub.dll");
-
- var pathBytes = Encoding.Unicode.GetBytes(libPath);
- var len = pathBytes.Length + 1;
-
- Console.WriteLine($"Injecting {libPath}...");
-
- var handle = NativeFunctions.OpenProcess(
- NativeFunctions.ProcessAccessFlags.AllAccess,
- false,
- process.Id);
-
- if (handle == IntPtr.Zero)
- {
- throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not OpenProcess");
- }
-
- var dllMem = NativeFunctions.VirtualAllocEx(
- handle,
- IntPtr.Zero,
- len,
- NativeFunctions.AllocationType.Commit,
- NativeFunctions.MemoryProtection.ReadWrite);
-
- if (dllMem == IntPtr.Zero)
- {
- throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not alloc memory {Marshal.GetLastWin32Error():X}");
- }
-
- Console.WriteLine($"dll path at {dllMem.ToInt64():X}");
-
- if (!NativeFunctions.WriteProcessMemory(
- handle,
- dllMem,
- pathBytes,
- len,
- out var bytesWritten))
- {
- throw new Win32Exception(Marshal.GetLastWin32Error(), "Could not write DLL");
- }
-
- Console.WriteLine($"Wrote {bytesWritten}");
-
- var kernel32 = NativeFunctions.GetModuleHandle("Kernel32.dll");
- var loadLibA = NativeFunctions.GetProcAddress(kernel32, "LoadLibraryW");
-
- var remoteThread = NativeFunctions.CreateRemoteThread(
- handle,
- IntPtr.Zero,
- 0,
- loadLibA,
- dllMem,
- 0,
- IntPtr.Zero);
-
- if (remoteThread == IntPtr.Zero)
- {
- throw new Win32Exception(Marshal.GetLastWin32Error(), $"Could not CreateRemoteThread");
- }
-
- /*
- TODO kill myself
- VirtualFreeEx(
- handle,
- dllMem,
- 0,
- AllocationType.Release);
- */
-
- NativeFunctions.CloseHandle(remoteThread);
- NativeFunctions.CloseHandle(handle);
- }
-
- private static DalamudStartInfo GetDefaultStartInfo()
- {
- var ffxivDir = Path.GetDirectoryName(process.MainModule.FileName);
- var startInfo = new DalamudStartInfo
- {
- WorkingDirectory = null,
- ConfigurationPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XIVLauncher", "dalamudConfig.json"),
- PluginDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XIVLauncher", "installedPlugins"),
- DefaultPluginDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XIVLauncher", "devPlugins"),
- AssetDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XIVLauncher", "dalamudAssets"),
-
- GameVersion = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver")),
- Language = ClientLanguage.English,
- };
-
- Console.WriteLine("Creating a StartInfo with:\n" +
- $"ConfigurationPath: {startInfo.ConfigurationPath}\n" +
- $"PluginDirectory: {startInfo.PluginDirectory}\n" +
- $"DefaultPluginDirectory: {startInfo.DefaultPluginDirectory}\n" +
- $"Language: {startInfo.Language}\n" +
- $"GameVersion: {startInfo.GameVersion}\n" +
- $"OptOutMbCollection: {startInfo.OptOutMbCollection}\n" +
- $"AssetDirectory: {startInfo.AssetDirectory}");
-
- return startInfo;
- }
- }
-}
diff --git a/Dalamud.Injector/RemotePinnedData.cs b/Dalamud.Injector/RemotePinnedData.cs
new file mode 100644
index 000000000..fc713639f
--- /dev/null
+++ b/Dalamud.Injector/RemotePinnedData.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using static Dalamud.Injector.NativeFunctions;
+
+namespace Dalamud.Injector
+{
+ ///
+ /// Pin an arbitrary string to a remote process.
+ ///
+ internal class RemotePinnedData : IDisposable
+ {
+ private readonly Process process;
+ private readonly byte[] data;
+ private readonly IntPtr allocAddr;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Process to write in.
+ /// Data to write.
+ public unsafe RemotePinnedData(Process process, byte[] data)
+ {
+ this.process = process;
+ this.data = data;
+
+ this.allocAddr = VirtualAllocEx(
+ this.process.Handle,
+ IntPtr.Zero,
+ this.data.Length,
+ AllocationType.Commit,
+ MemoryProtection.ReadWrite);
+
+ if (this.allocAddr == IntPtr.Zero || Marshal.GetLastWin32Error() != 0)
+ {
+ throw new Exception("Error allocating memory");
+ }
+
+ var result = WriteProcessMemory(
+ this.process.Handle,
+ this.allocAddr,
+ this.data,
+ this.data.Length,
+ out _);
+
+ if (!result || Marshal.GetLastWin32Error() != 0)
+ {
+ throw new Exception("Error writing memory");
+ }
+ }
+
+ ///
+ /// Gets the address of the pinned data.
+ ///
+ public IntPtr Address => this.allocAddr;
+
+ ///
+ public void Dispose()
+ {
+ if (this.allocAddr == IntPtr.Zero)
+ {
+ return;
+ }
+
+ var result = VirtualFreeEx(
+ this.process.Handle,
+ this.allocAddr,
+ 0,
+ AllocationType.Release);
+
+ if (!result || Marshal.GetLastWin32Error() != 0)
+ {
+ throw new Exception("Error freeing memory");
+ }
+ }
+ }
+}
diff --git a/Dalamud.Test/Dalamud.Test.csproj b/Dalamud.Test/Dalamud.Test.csproj
index bcbccb45a..7163336b1 100644
--- a/Dalamud.Test/Dalamud.Test.csproj
+++ b/Dalamud.Test/Dalamud.Test.csproj
@@ -1,96 +1,70 @@
-
-
-
-
-
-
-
- Debug
- AnyCPU
- {C8004563-1806-4329-844F-0EF6274291FC}
- {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- Library
- Properties
- Dalamud.Test
- Dalamud.Test
- v4.7.2
- 512
-
-
- AnyCPU
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
-
-
- AnyCPU
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
-
-
-
-
-
-
-
- ..\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll
- True
-
-
- ..\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll
- True
-
-
- ..\packages\xunit.extensibility.core.2.4.1\lib\net452\xunit.core.dll
- True
-
-
- ..\packages\xunit.extensibility.execution.2.4.1\lib\net452\xunit.execution.desktop.dll
- True
-
-
-
-
-
-
-
-
-
-
-
-
- {b92dab43-2279-4a2c-96e3-d9d5910edbea}
- Dalamud
-
-
-
-
-
-
-
-
-
- This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105.The missing file is {0}.
+
+
+
+ net5.0-windows
+ win-x64
+ x64
+ x64;AnyCPU
+ 9.0
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+ Dalamud.Test
+ Dalamud.Test
+ Dalamud.Test
+ Dalamud.Test
+ Unit tests for Dalamud
+ goatcorp
+ Copyright © goatcorp 2021
+
+
+
+ Library
+ false
+ false
+
+
+
+ Debug
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
diff --git a/Dalamud.Test/Game/GameVersionTests.cs b/Dalamud.Test/Game/GameVersionTests.cs
new file mode 100644
index 000000000..44a5813c8
--- /dev/null
+++ b/Dalamud.Test/Game/GameVersionTests.cs
@@ -0,0 +1,58 @@
+using Dalamud.Game;
+using Xunit;
+
+namespace Dalamud.Test.Game
+{
+ public class GameVersionTests
+ {
+ [Theory]
+ [InlineData("any", "any")]
+ [InlineData("2021.01.01.0000.0000", "2021.01.01.0000.0000")]
+ public void VersionEquality(string ver1, string ver2)
+ {
+ var v1 = GameVersion.Parse(ver1);
+ var v2 = GameVersion.Parse(ver2);
+
+ Assert.Equal(v1, v2);
+ }
+
+ [Theory]
+ [InlineData("2020.06.15.0000.0000", "any")]
+ [InlineData("2021.01.01.0000.0000", "2021.01.01.0000.0001")]
+ [InlineData("2021.01.01.0000.0000", "2021.01.01.0001.0000")]
+ [InlineData("2021.01.01.0000.0000", "2021.01.02.0000.0000")]
+ [InlineData("2021.01.01.0000.0000", "2021.02.01.0000.0000")]
+ [InlineData("2021.01.01.0000.0000", "2022.01.01.0000.0000")]
+ public void VersionComparison(string ver1, string ver2)
+ {
+ var v1 = GameVersion.Parse(ver1);
+ var v2 = GameVersion.Parse(ver2);
+
+ Assert.True(v1.CompareTo(v2) < 0);
+ }
+
+ [Theory]
+ [InlineData("2020.06.15.0000.0000")]
+ [InlineData("2021.01.01.0000")]
+ [InlineData("2021.01.01")]
+ [InlineData("2021.01")]
+ [InlineData("2021")]
+ public void VersionConstructor(string ver)
+ {
+ var v = GameVersion.Parse(ver);
+
+ Assert.True(v != null);
+ }
+
+ [Theory]
+ [InlineData("2020.06.15.0000.0000.0000")]
+ [InlineData("")]
+ public void VersionConstructorInvalid(string ver)
+ {
+ var result = GameVersion.TryParse(ver, out var v);
+
+ Assert.False(result);
+ Assert.Null(v);
+ }
+ }
+}
diff --git a/Dalamud.Test/Game/Text/Sanitizer/SanitizerTests.cs b/Dalamud.Test/Game/Text/Sanitizer/SanitizerTests.cs
index f2ce7a9c9..2d2a34df5 100644
--- a/Dalamud.Test/Game/Text/Sanitizer/SanitizerTests.cs
+++ b/Dalamud.Test/Game/Text/Sanitizer/SanitizerTests.cs
@@ -1,29 +1,29 @@
-// ReSharper disable StringLiteralTypo
-
using System.Collections.Generic;
using System.Linq;
using Xunit;
-namespace Dalamud.Test.Game.Text.Sanitizer {
-
- public class SanitizerTests {
+// ReSharper disable StringLiteralTypo
+namespace Dalamud.Test.Game.Text.Sanitizer
+{
+ public class SanitizerTests
+ {
private global::Dalamud.Game.Text.Sanitizer.Sanitizer sanitizer;
[Theory]
- [InlineData( ClientLanguage.English, "Pixie Cotton Hood of Healing", "Pixie Cotton Hood of Healing" )]
- [InlineData( ClientLanguage.Japanese, "アラガントームストーン:真理", "アラガントームストーン:真理" )]
- [InlineData( ClientLanguage.German, "Anemos-Pan\x02\x16\x01\x03zer\x02\x16\x01\x03hand\x02\x16\x01\x03schu\x02\x16\x01\x03he des Drachenbluts", "Anemos-Panzerhandschuhe des Drachenbluts" )]
- [InlineData( ClientLanguage.German, "Bienen-Spatha †", "Bienen-Spatha" )]
- [InlineData( ClientLanguage.French, "Le Diademe\x02\x1D\x01\x03: terrains de chasse|Le Diademe\x02\x1D\x01\x03: terrains de chasse", "Le Diademe: terrains de chasse|Le Diademe: terrains de chasse" )]
- [InlineData( ClientLanguage.French, "Cuir de bœuf", "Cuir de boeuf" )]
- public void StringsAreSanitizedCorrectly(
- ClientLanguage clientLanguage, string unsanitizedString, string sanitizedString)
+ [InlineData(ClientLanguage.English, "Pixie Cotton Hood of Healing", "Pixie Cotton Hood of Healing")]
+ [InlineData(ClientLanguage.Japanese, "アラガントームストーン:真理", "アラガントームストーン:真理")]
+ [InlineData(ClientLanguage.German, "Anemos-Pan\x02\x16\x01\x03zer\x02\x16\x01\x03hand\x02\x16\x01\x03schu\x02\x16\x01\x03he des Drachenbluts", "Anemos-Panzerhandschuhe des Drachenbluts")]
+ [InlineData(ClientLanguage.German, "Bienen-Spatha †", "Bienen-Spatha")]
+ [InlineData(ClientLanguage.French, "Le Diademe\x02\x1D\x01\x03: terrains de chasse|Le Diademe\x02\x1D\x01\x03: terrains de chasse", "Le Diademe: terrains de chasse|Le Diademe: terrains de chasse")]
+ [InlineData(ClientLanguage.French, "Cuir de bœuf", "Cuir de boeuf")]
+ public void StringsAreSanitizedCorrectly(ClientLanguage clientLanguage, string unsanitizedString, string sanitizedString)
{
- var sanitizedStrings = new List {unsanitizedString};
+ var sanitizedStrings = new List { unsanitizedString };
+
sanitizer = new global::Dalamud.Game.Text.Sanitizer.Sanitizer(clientLanguage);
Assert.Equal(sanitizedString, sanitizer.Sanitize(unsanitizedString));
Assert.Equal(sanitizedString, sanitizer.Sanitize(sanitizedStrings).First());
-
+
sanitizer = new global::Dalamud.Game.Text.Sanitizer.Sanitizer(ClientLanguage.English);
Assert.Equal(sanitizedString, sanitizer.Sanitize(unsanitizedString, clientLanguage));
Assert.Equal(sanitizedString, sanitizer.Sanitize(sanitizedStrings, clientLanguage).First());
diff --git a/Dalamud.Test/Game/Text/SeStringHandling/SeStringManagerTests.cs b/Dalamud.Test/Game/Text/SeStringHandling/SeStringManagerTests.cs
new file mode 100644
index 000000000..95c1ec4d3
--- /dev/null
+++ b/Dalamud.Test/Game/Text/SeStringHandling/SeStringManagerTests.cs
@@ -0,0 +1,20 @@
+using System.Linq;
+using Dalamud.Game.Text.SeStringHandling;
+using Xunit;
+
+namespace Dalamud.Test.Game.Text.SeStringHandling
+{
+ public class SeStringManagerTests
+ {
+ [Fact]
+ public void TestNewLinePayload()
+ {
+ var manager = new SeStringManager(null);
+ var newLinePayloadBytes = new byte[] {0x02, 0x10, 0x01, 0x03};
+
+ var seString = manager.Parse(newLinePayloadBytes);
+
+ Assert.True(newLinePayloadBytes.SequenceEqual(seString.Encode()));
+ }
+ }
+}
diff --git a/Dalamud.Test/LocalizationTests.cs b/Dalamud.Test/LocalizationTests.cs
index 67977f805..093affc9a 100644
--- a/Dalamud.Test/LocalizationTests.cs
+++ b/Dalamud.Test/LocalizationTests.cs
@@ -1,20 +1,24 @@
-using System.IO;
+using System.IO;
using System.Reflection;
using Xunit;
-namespace Dalamud.Test {
- public class LocalizationTests {
+namespace Dalamud.Test
+{
+ public class LocalizationTests
+ {
private readonly Localization localization;
private string currentLangCode;
-
- public LocalizationTests() {
+
+ public LocalizationTests()
+ {
var workingDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
this.localization = new Localization(workingDir, "dalamud_");
this.localization.OnLocalizationChanged += code => this.currentLangCode = code;
}
[Fact]
- public void SetupWithFallbacks_EventInvoked() {
+ public void SetupWithFallbacks_EventInvoked()
+ {
this.localization.SetupWithFallbacks();
Assert.Equal("en", this.currentLangCode);
}
diff --git a/Dalamud.Test/Properties/AssemblyInfo.cs b/Dalamud.Test/Properties/AssemblyInfo.cs
deleted file mode 100644
index 589173bb4..000000000
--- a/Dalamud.Test/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.Reflection;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("Dalamud.Test")]
-[assembly: AssemblyDescription("Unit tests for Dalamud")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("goatcorp")]
-[assembly: AssemblyProduct("Dalamud.Test")]
-[assembly: AssemblyCopyright("Copyright © goatcorp 2021")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("C8004563-1806-4329-844F-0EF6274291FC")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Dalamud.Test/packages.config b/Dalamud.Test/packages.config
deleted file mode 100644
index f2ae94af3..000000000
--- a/Dalamud.Test/packages.config
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Dalamud.sln b/Dalamud.sln
index 16ca2e3ac..370b210bf 100644
--- a/Dalamud.sln
+++ b/Dalamud.sln
@@ -1,137 +1,163 @@
-
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
-VisualStudioVersion = 16.0.29215.179
+VisualStudioVersion = 16.0.31424.327
MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CEF7D22B-CB85-400E-BD64-349A30E3C097}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ .gitignore = .gitignore
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "build", "build\build.csproj", "{94E5B016-02B1-459B-97D9-E783F28764B2}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud", "Dalamud\Dalamud.csproj", "{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}"
EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Boot", "Dalamud.Boot\Dalamud.Boot.vcxproj", "{55198DC3-A03D-408E-A8EB-2077780C8576}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Injector", "Dalamud.Injector\Dalamud.Injector.csproj", "{5B832F73-5F54-4ADC-870F-D0095EF72C9A}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGuiScene", "lib\ImGuiScene\ImGuiScene\ImGuiScene.csproj", "{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}"
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Injector.Boot", "Dalamud.Injector.Boot\Dalamud.Injector.Boot.vcxproj", "{8874326B-E755-4D13-90B4-59AB263A3E6B}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SDL2-CS", "lib\ImGuiScene\deps\SDL2-CS\SDL2-CS.csproj", "{85480198-8711-4355-830E-72FD794AD3F6}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImGui.NET-472", "lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj", "{0483026E-C6CE-4B1A-AA68-46544C08140B}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interface", "Interface", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}"
EndProject
-Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DalamudDebugStub", "DalamudDebugStub\DalamudDebugStub.vcxproj", "{9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGui.NET-472", "lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj", "{0483026E-C6CE-4B1A-AA68-46544C08140B}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs", "lib\FFXIVClientStructs\FFXIVClientStructs.csproj", "{3DBAEE68-9D94-4807-BCB1-E42EDD52B489}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImGuiScene", "lib\ImGuiScene\ImGuiScene\ImGuiScene.csproj", "{C0E7E797-4FBF-4F46-BC57-463F3719BA7A}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SDL2-CS", "lib\ImGuiScene\deps\SDL2-CS\SDL2-CS.csproj", "{2F7FF0A8-B619-4572-86C7-71E46FE22FB8}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.CorePlugin", "Dalamud.CorePlugin\Dalamud.CorePlugin.csproj", "{4AFDB34A-7467-4D41-B067-53BC4101D9D0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs", "lib\FFXIVClientStructs\FFXIVClientStructs\FFXIVClientStructs.csproj", "{C9B87BD7-AF49-41C3-91F1-D550ADEB7833}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs.Common", "lib\FFXIVClientStructs\FFXIVClientStructs.Common\FFXIVClientStructs.Common.csproj", "{F3F0CC3A-DE2E-403F-88B4-B47C62582477}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs.Generators", "lib\FFXIVClientStructs\FFXIVClientStructs.Generators\FFXIVClientStructs.Generators.csproj", "{05AB2F46-268B-4915-806F-DDF813E2D59D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
- Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
- Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x64.Build.0 = Debug|Any CPU
+ {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x64.ActiveCfg = Release|Any CPU
+ {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x64.Build.0 = Release|Any CPU
+ {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.Build.0 = Debug|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x64.ActiveCfg = Debug|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x64.Build.0 = Debug|x64
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x86.ActiveCfg = Debug|Any CPU
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x86.Build.0 = Debug|Any CPU
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.ActiveCfg = Release|x64
+ {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.Build.0 = Release|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x64.ActiveCfg = Release|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x64.Build.0 = Release|x64
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x86.ActiveCfg = Release|Any CPU
- {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x86.Build.0 = Release|Any CPU
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|Any CPU.Build.0 = Debug|x64
+ {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x64.ActiveCfg = Debug|x64
+ {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x64.Build.0 = Debug|x64
+ {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|Any CPU.ActiveCfg = Release|x64
+ {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|Any CPU.Build.0 = Release|x64
+ {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x64.ActiveCfg = Release|x64
+ {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x64.Build.0 = Release|x64
+ {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x64.ActiveCfg = Debug|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x64.Build.0 = Debug|x64
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x86.ActiveCfg = Debug|Any CPU
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x86.Build.0 = Debug|Any CPU
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|x64
+ {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x64.ActiveCfg = Release|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x64.Build.0 = Release|x64
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x86.ActiveCfg = Release|Any CPU
- {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x86.Build.0 = Release|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.ActiveCfg = Debug|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.Build.0 = Debug|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x86.ActiveCfg = Debug|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x86.Build.0 = Debug|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.Build.0 = Release|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.ActiveCfg = Release|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.Build.0 = Release|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x86.ActiveCfg = Release|Any CPU
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x86.Build.0 = Release|Any CPU
- {85480198-8711-4355-830E-72FD794AD3F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {85480198-8711-4355-830E-72FD794AD3F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {85480198-8711-4355-830E-72FD794AD3F6}.Debug|x64.ActiveCfg = Debug|x64
- {85480198-8711-4355-830E-72FD794AD3F6}.Debug|x64.Build.0 = Debug|x64
- {85480198-8711-4355-830E-72FD794AD3F6}.Debug|x86.ActiveCfg = Debug|x86
- {85480198-8711-4355-830E-72FD794AD3F6}.Debug|x86.Build.0 = Debug|x86
- {85480198-8711-4355-830E-72FD794AD3F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {85480198-8711-4355-830E-72FD794AD3F6}.Release|Any CPU.Build.0 = Release|Any CPU
- {85480198-8711-4355-830E-72FD794AD3F6}.Release|x64.ActiveCfg = Release|x64
- {85480198-8711-4355-830E-72FD794AD3F6}.Release|x64.Build.0 = Release|x64
- {85480198-8711-4355-830E-72FD794AD3F6}.Release|x86.ActiveCfg = Release|x86
- {85480198-8711-4355-830E-72FD794AD3F6}.Release|x86.Build.0 = Release|x86
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.ActiveCfg = Debug|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.Build.0 = Debug|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x86.ActiveCfg = Debug|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x86.Build.0 = Debug|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.Build.0 = Release|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.ActiveCfg = Release|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.Build.0 = Release|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x86.ActiveCfg = Release|Any CPU
- {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x86.Build.0 = Release|Any CPU
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Debug|Any CPU.ActiveCfg = Debug|x64
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Debug|x64.ActiveCfg = Debug|x64
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Debug|x86.ActiveCfg = Debug|Win32
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|Any CPU.ActiveCfg = Release|x64
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|Any CPU.Build.0 = Release|x64
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|x64.ActiveCfg = Release|x64
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|x64.Build.0 = Release|x64
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|x86.ActiveCfg = Release|Win32
- {9FDA9A5C-50C6-4333-8DCE-DFEB89363F2A}.Release|x86.Build.0 = Release|Win32
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|x64.ActiveCfg = Debug|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|x64.Build.0 = Debug|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|x86.ActiveCfg = Debug|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Debug|x86.Build.0 = Debug|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|Any CPU.Build.0 = Release|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|x64.ActiveCfg = Release|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|x64.Build.0 = Release|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|x86.ActiveCfg = Release|Any CPU
- {3DBAEE68-9D94-4807-BCB1-E42EDD52B489}.Release|x86.Build.0 = Release|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.ActiveCfg = Debug|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.Build.0 = Debug|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x86.ActiveCfg = Debug|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x86.Build.0 = Debug|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.Build.0 = Release|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.ActiveCfg = Release|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.Build.0 = Release|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Release|x86.ActiveCfg = Release|Any CPU
- {C8004563-1806-4329-844F-0EF6274291FC}.Release|x86.Build.0 = Release|Any CPU
+ {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.Build.0 = Debug|x64
+ {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x64.ActiveCfg = Debug|x64
+ {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x64.Build.0 = Debug|x64
+ {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.ActiveCfg = Release|x64
+ {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x64.ActiveCfg = Release|x64
+ {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x64.Build.0 = Release|x64
+ {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|x64
+ {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.ActiveCfg = Debug|x64
+ {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.Build.0 = Debug|x64
+ {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64
+ {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.Build.0 = Release|x64
+ {C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.ActiveCfg = Release|x64
+ {C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.Build.0 = Release|x64
+ {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.Build.0 = Debug|x64
+ {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.ActiveCfg = Debug|x64
+ {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.Build.0 = Debug|x64
+ {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.ActiveCfg = Release|x64
+ {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.Build.0 = Release|x64
+ {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.ActiveCfg = Release|x64
+ {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.Build.0 = Release|x64
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.Build.0 = Debug|x64
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.ActiveCfg = Debug|x64
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.Build.0 = Debug|x64
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.ActiveCfg = Release|x64
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.Build.0 = Release|x64
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.ActiveCfg = Release|x64
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.Build.0 = Release|x64
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.Build.0 = Debug|x64
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x64.ActiveCfg = Debug|x64
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x64.Build.0 = Debug|x64
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.ActiveCfg = Release|x64
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.Build.0 = Release|x64
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x64.ActiveCfg = Release|x64
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x64.Build.0 = Release|x64
+ {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x64.Build.0 = Debug|Any CPU
+ {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x64.ActiveCfg = Release|Any CPU
+ {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x64.Build.0 = Release|Any CPU
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x64.Build.0 = Debug|Any CPU
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x64.ActiveCfg = Release|Any CPU
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x64.Build.0 = Release|Any CPU
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Debug|x64.Build.0 = Debug|Any CPU
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Release|x64.ActiveCfg = Release|Any CPU
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477}.Release|x64.Build.0 = Release|Any CPU
+ {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x64.Build.0 = Debug|Any CPU
+ {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x64.ActiveCfg = Release|Any CPU
+ {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
- {C0E7E797-4FBF-4F46-BC57-463F3719BA7A} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
- {85480198-8711-4355-830E-72FD794AD3F6} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{0483026E-C6CE-4B1A-AA68-46544C08140B} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
+ {C0E7E797-4FBF-4F46-BC57-463F3719BA7A} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
+ {2F7FF0A8-B619-4572-86C7-71E46FE22FB8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
+ {C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
+ {F3F0CC3A-DE2E-403F-88B4-B47C62582477} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
+ {05AB2F46-268B-4915-806F-DDF813E2D59D} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599}
diff --git a/Dalamud/Configuration/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
similarity index 76%
rename from Dalamud/Configuration/DalamudConfiguration.cs
rename to Dalamud/Configuration/Internal/DalamudConfiguration.cs
index cb2ffa1ce..ed10572e4 100644
--- a/Dalamud/Configuration/DalamudConfiguration.cs
+++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
@@ -5,20 +5,21 @@ using System.IO;
using Dalamud.Game.Text;
using Newtonsoft.Json;
using Serilog;
+using Serilog.Events;
-namespace Dalamud.Configuration
+namespace Dalamud.Configuration.Internal
{
///
/// Class containing Dalamud settings.
///
[Serializable]
- internal class DalamudConfiguration
+ internal sealed class DalamudConfiguration
{
[JsonIgnore]
private string configPath;
///
- /// Delegate for the event that occurs when the dalamud configuration is saved.
+ /// Delegate for the event that occurs when the dalamud configuration is saved.
///
/// The current dalamud configuration.
public delegate void DalamudConfigurationSavedDelegate(DalamudConfiguration dalamudConfiguration);
@@ -68,15 +69,28 @@ namespace Dalamud.Configuration
///
public bool DoDalamudTest { get; set; }
+ ///
+ /// Gets or sets a value indicating whether or not XL should download the Dalamud .NET runtime.
+ ///
+ public bool DoDalamudRuntime { get; set; }
+
///
/// Gets or sets a list of custom repos.
///
- public List ThirdRepoList { get; set; } = new List();
+ public List ThirdRepoList { get; set; } = new();
///
/// Gets or sets a list of hidden plugins.
///
- public List HiddenPluginInternalName { get; set; } = new List();
+ public List HiddenPluginInternalName { get; set; } = new();
+
+ ///
+ /// Gets or sets a list of additional settings for devPlugins. The key is the absolute path
+ /// to the plugin DLL. This is automatically generated for any plugins in the devPlugins folder.
+ /// However by specifiying this value manually, you can add arbitrary files outside the normal
+ /// file paths.
+ ///
+ public Dictionary DevPluginSettings { get; set; } = new();
///
/// Gets or sets the global UI scale.
@@ -113,6 +127,11 @@ namespace Dalamud.Configuration
///
public bool DoButtonsSystemMenu { get; set; } = true;
+ ///
+ /// Gets or sets the default Dalamud debug log level on startup.
+ ///
+ public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information;
+
///
/// Gets or sets a value indicating whether or not the debug log should scroll automatically.
///
@@ -138,6 +157,21 @@ namespace Dalamud.Configuration
///
public bool IsGamepadNavigationEnabled { get; set; } = true;
+ ///
+ /// Gets or sets a value indicating whether or not the anti-anti-debug check is enabled on startup.
+ ///
+ public bool IsAntiAntiDebugEnabled { get; set; } = false;
+
+ ///
+ /// Gets or sets the kind of beta to download when is set to true.
+ ///
+ public string DalamudBetaKind { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether or not all plugins, regardless of API level, should be loaded.
+ ///
+ public bool LoadAllApiLevels { get; set; }
+
///
/// Load a configuration from the provided path.
///
diff --git a/Dalamud/Configuration/Internal/DevPluginSettings.cs b/Dalamud/Configuration/Internal/DevPluginSettings.cs
new file mode 100644
index 000000000..17350cba0
--- /dev/null
+++ b/Dalamud/Configuration/Internal/DevPluginSettings.cs
@@ -0,0 +1,18 @@
+namespace Dalamud.Configuration.Internal
+{
+ ///
+ /// Settings for DevPlugins.
+ ///
+ internal sealed class DevPluginSettings
+ {
+ ///
+ /// Gets or sets a value indicating whether this plugin should automatically start when Dalamud boots up.
+ ///
+ public bool StartOnBoot { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether this plugin should automatically reload on file change.
+ ///
+ public bool AutomaticReloading { get; set; } = false;
+ }
+}
diff --git a/Dalamud/Configuration/ThirdRepoSetting.cs b/Dalamud/Configuration/Internal/ThirdPartyRepoSettings.cs
similarity index 56%
rename from Dalamud/Configuration/ThirdRepoSetting.cs
rename to Dalamud/Configuration/Internal/ThirdPartyRepoSettings.cs
index 63ab9f333..cafb96a47 100644
--- a/Dalamud/Configuration/ThirdRepoSetting.cs
+++ b/Dalamud/Configuration/Internal/ThirdPartyRepoSettings.cs
@@ -3,7 +3,7 @@ namespace Dalamud.Configuration
///
/// Third party repository for dalamud plugins.
///
- internal class ThirdRepoSetting
+ internal sealed class ThirdPartyRepoSettings
{
///
/// Gets or sets the third party repo url.
@@ -16,16 +16,14 @@ namespace Dalamud.Configuration
public bool IsEnabled { get; set; }
///
- /// Create new instance of third party repo object.
+ /// Gets or sets a short name for the repo url.
///
- /// New instance of third party repo.
- public ThirdRepoSetting Clone()
- {
- return new ThirdRepoSetting
- {
- Url = this.Url,
- IsEnabled = this.IsEnabled,
- };
- }
+ public string Name { get; set; }
+
+ ///
+ /// Clone this object.
+ ///
+ /// A shallow copy of this object.
+ public ThirdPartyRepoSettings Clone() => this.MemberwiseClone() as ThirdPartyRepoSettings;
}
}
diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs
index 5fa1d54cf..95f25b04a 100644
--- a/Dalamud/Configuration/PluginConfigurations.cs
+++ b/Dalamud/Configuration/PluginConfigurations.cs
@@ -1,5 +1,6 @@
using System.IO;
+using JetBrains.Annotations;
using Newtonsoft.Json;
namespace Dalamud.Configuration
@@ -7,7 +8,7 @@ namespace Dalamud.Configuration
///
/// Configuration to store settings for a dalamud plugin.
///
- public class PluginConfigurations
+ public sealed class PluginConfigurations
{
private readonly DirectoryInfo configDirectory;
@@ -44,6 +45,7 @@ namespace Dalamud.Configuration
///
/// Plugin name.
/// Plugin configuration.
+ [CanBeNull]
public IPluginConfiguration Load(string pluginName)
{
var path = this.GetConfigFile(pluginName);
@@ -60,6 +62,22 @@ namespace Dalamud.Configuration
});
}
+ ///
+ /// Delete the configuration file and folder for the specified plugin.
+ /// This will throw an if the plugin did not correctly close its handles.
+ ///
+ /// The name of the plugin.
+ public void Delete(string pluginName)
+ {
+ var directory = this.GetDirectoryPath(pluginName);
+ if (directory.Exists)
+ directory.Delete(true);
+
+ var file = this.GetConfigFile(pluginName);
+ if (file.Exists)
+ file.Delete();
+ }
+
///
/// Get plugin directory.
///
diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs
index 7c98e6a8e..170a64082 100644
--- a/Dalamud/Dalamud.cs
+++ b/Dalamud/Dalamud.cs
@@ -1,10 +1,10 @@
using System;
using System.Diagnostics;
using System.IO;
+using System.Runtime.CompilerServices;
using System.Threading;
-using System.Threading.Tasks;
-using Dalamud.Configuration;
+using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.Addon;
@@ -13,17 +13,24 @@ using Dalamud.Game.Command;
using Dalamud.Game.Internal;
using Dalamud.Game.Network;
using Dalamud.Game.Text.SeStringHandling;
-using Dalamud.Interface;
-using Dalamud.Plugin;
+using Dalamud.Hooking.Internal;
+using Dalamud.Interface.Internal;
+using Dalamud.Memory;
+using Dalamud.Plugin.Internal;
using Serilog;
using Serilog.Core;
+#if DEBUG
+// This allows for rapid prototyping of Dalamud modules with access to internal objects.
+[assembly: InternalsVisibleTo("Dalamud.CorePlugin")]
+#endif
+
namespace Dalamud
{
///
/// The main Dalamud class containing all subsystems.
///
- public sealed class Dalamud : IDisposable
+ internal sealed class Dalamud : IDisposable
{
#region Internals
@@ -31,8 +38,6 @@ namespace Dalamud
private readonly ManualResetEvent finishUnloadSignal;
- private readonly string baseDirectory;
-
private bool hasDisposedPlugins = false;
#endregion
@@ -43,12 +48,17 @@ namespace Dalamud
/// DalamudStartInfo instance.
/// LoggingLevelSwitch to control Serilog level.
/// Signal signalling shutdown.
- public Dalamud(DalamudStartInfo info, LoggingLevelSwitch loggingLevelSwitch, ManualResetEvent finishSignal)
+ /// The Dalamud configuration.
+ public Dalamud(DalamudStartInfo info, LoggingLevelSwitch loggingLevelSwitch, ManualResetEvent finishSignal, DalamudConfiguration configuration)
{
+#if DEBUG
+ Instance = this;
+#endif
this.StartInfo = info;
this.LogLevelSwitch = loggingLevelSwitch;
+ this.Configuration = configuration;
- this.baseDirectory = info.WorkingDirectory;
+ // this.baseDirectory = info.WorkingDirectory;
this.unloadSignal = new ManualResetEvent(false);
this.unloadSignal.Reset();
@@ -57,6 +67,13 @@ namespace Dalamud
this.finishUnloadSignal.Reset();
}
+#if DEBUG
+ ///
+ /// Gets the Dalamud singleton instance.
+ ///
+ internal static Dalamud Instance { get; private set; }
+#endif
+
#region Native Game Subsystems
///
@@ -74,6 +91,11 @@ namespace Dalamud
///
internal WinSockHandlers WinSock2 { get; private set; }
+ ///
+ /// Gets Hook management subsystem.
+ ///
+ internal HookManager HookManager { get; private set; }
+
///
/// Gets ImGui Interface subsystem.
///
@@ -93,11 +115,6 @@ namespace Dalamud
///
internal PluginManager PluginManager { get; private set; }
- ///
- /// Gets Plugin Repository subsystem.
- ///
- internal PluginRepository PluginRepository { get; private set; }
-
///
/// Gets Data provider subsystem.
///
@@ -203,6 +220,7 @@ namespace Dalamud
// Initialize the process information.
this.TargetModule = Process.GetCurrentProcess().MainModule;
this.SigScanner = new SigScanner(this.TargetModule, true);
+ this.HookManager = new HookManager(this);
// Initialize game subsystem
this.Framework = new Framework(this.SigScanner, this);
@@ -211,6 +229,10 @@ namespace Dalamud
this.Framework.Enable();
Log.Information("[T1] Framework ENABLE!");
+
+ // Initialize FFXIVClientStructs function resolver
+ FFXIVClientStructs.Resolver.Initialize();
+ Log.Information("[T1] FFXIVClientStructs initialized!");
}
catch (Exception ex)
{
@@ -226,11 +248,12 @@ namespace Dalamud
{
try
{
- this.Configuration = DalamudConfiguration.Load(this.StartInfo.ConfigurationPath);
-
this.AntiDebug = new AntiDebug(this.SigScanner);
+ if (this.Configuration.IsAntiAntiDebugEnabled)
+ this.AntiDebug.Enable();
#if DEBUG
- this.AntiDebug.Enable();
+ if (!this.AntiDebug.IsEnabled)
+ this.AntiDebug.Enable();
#endif
Log.Information("[T2] AntiDebug OK!");
@@ -286,6 +309,7 @@ namespace Dalamud
Log.Information("[T2] Data OK!");
this.SeStringManager = new SeStringManager(this.Data);
+ MemoryHelper.Initialize(this); // For SeString handling
Log.Information("[T2] SeString OK!");
@@ -324,28 +348,21 @@ namespace Dalamud
{
Log.Information("[T3] START!");
- this.PluginRepository =
- new PluginRepository(this, this.StartInfo.PluginDirectory, this.StartInfo.GameVersion);
-
- Log.Information("[T3] PREPO OK!");
-
if (!bool.Parse(Environment.GetEnvironmentVariable("DALAMUD_NOT_HAVE_PLUGINS") ?? "false"))
{
try
{
- this.PluginRepository.CleanupPlugins();
-
- Log.Information("[T3] PRC OK!");
-
- this.PluginManager = new PluginManager(
- this,
- this.StartInfo.PluginDirectory,
- this.StartInfo.DefaultPluginDirectory);
- this.PluginManager.LoadSynchronousPlugins();
-
- Task.Run(() => this.PluginManager.LoadDeferredPlugins());
+ this.PluginManager = new PluginManager(this);
+ this.PluginManager.OnInstalledPluginsChanged += () =>
+ Troubleshooting.LogTroubleshooting(this, this.InterfaceManager.IsReady);
Log.Information("[T3] PM OK!");
+
+ this.PluginManager.CleanupPlugins();
+ Log.Information("[T3] PMC OK!");
+
+ this.PluginManager.LoadAllPlugins();
+ Log.Information("[T3] PML OK!");
}
catch (Exception ex)
{
@@ -354,11 +371,9 @@ namespace Dalamud
}
this.DalamudUi = new DalamudInterface(this);
- this.InterfaceManager.OnDraw += this.DalamudUi.Draw;
-
Log.Information("[T3] DUI OK!");
- Troubleshooting.LogTroubleshooting(this, this.InterfaceManager != null);
+ Troubleshooting.LogTroubleshooting(this, this.InterfaceManager.IsReady);
Log.Information("Dalamud is ready.");
}
@@ -407,16 +422,9 @@ namespace Dalamud
// use any resources that it freed in its own Dispose method
this.InterfaceManager?.Dispose();
- try
- {
- this.PluginManager.UnloadPlugins();
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Plugin unload failed.");
- }
-
this.DalamudUi?.Dispose();
+
+ this.PluginManager?.Dispose();
}
///
@@ -433,20 +441,23 @@ namespace Dalamud
}
this.Framework?.Dispose();
+
this.ClientState?.Dispose();
this.unloadSignal?.Dispose();
this.WinSock2?.Dispose();
- this.SigScanner?.Dispose();
-
this.Data?.Dispose();
this.AntiDebug?.Dispose();
this.SystemMenu?.Dispose();
+ this.HookManager?.Dispose();
+
+ this.SigScanner?.Dispose();
+
Log.Debug("Dalamud::Dispose() OK!");
}
catch (Exception ex)
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index 501c36869..28892907e 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -1,105 +1,124 @@
+
- AnyCPU
- net472
+ net5.0-windows
+ x64
+ x64;AnyCPU
9.0
- AnyCPU;x64
-
-
- Library
-
- false
- true
- full
- $(SolutionDir)\bin\Dalamud.xml
+
- 5.2.6.1
- true
+ 6.0.0.0
+ XIV Launcher addon framework
$(DalamudVersion)
$(DalamudVersion)
$(DalamudVersion)
-
-
-
-
- $(MSBuildProjectDirectory)\
- $(AppOutputBase)=C:\goatsoft\companysecrets\dalamud\
+
+
+ Library
+ ..\bin\$(Configuration)\
+ false
+ false
+ true
+ false
+
+
+
+ $(OutputPath)Dalamud.xml
+ true
+
+
+
+ true
+ true
+ true
+ portable
true
+ annotations
+ true
-
- false
+
+
+ Debug
-
+
DEBUG;TRACE
-
- IDE0017;IDE0044;IDE0047;IDE0048;IDE1006;CS1573;CS1591;CS1701;CS1702
-
-
-
-
-
-
+
+ $(MSBuildProjectDirectory)\
+ $(AppOutputBase)=C:\goatsoft\companysecrets\dalamud\
+
+
+
+ IDE0003;IDE0044;IDE1006;CS1591;CS1701;CS1702
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+ all
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
-
+
+
+
+
+
+
+
+
+
True
True
Resources.resx
-
-
ResXFileCodeGenerator
Resources.Designer.cs
+
-
-
-
-
-
-
-
+
PreserveNewest
- Lumina.Generated.dll
-
+
+
+
+
+
+
+
+
diff --git a/Dalamud/DalamudStartInfo.cs b/Dalamud/DalamudStartInfo.cs
index 78ceca446..7678cfb40 100644
--- a/Dalamud/DalamudStartInfo.cs
+++ b/Dalamud/DalamudStartInfo.cs
@@ -1,12 +1,15 @@
using System;
+using Dalamud.Game;
+using Newtonsoft.Json;
+
namespace Dalamud
{
///
- /// Class containing information needed to initialize Dalamud.
+ /// Struct containing information needed to initialize Dalamud.
///
[Serializable]
- public sealed class DalamudStartInfo
+ public struct DalamudStartInfo
{
///
/// The working directory of the XIVLauncher installations.
@@ -41,7 +44,8 @@ namespace Dalamud
///
/// The current game version code.
///
- public string GameVersion;
+ [JsonConverter(typeof(GameVersionConverter))]
+ public GameVersion GameVersion;
///
/// Whether or not market board information should be uploaded by default.
diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs
index a6e4016e5..ebdf24c57 100644
--- a/Dalamud/Data/DataManager.cs
+++ b/Dalamud/Data/DataManager.cs
@@ -7,6 +7,7 @@ using System.Threading;
using Dalamud.Data.LuminaExtensions;
using Dalamud.Interface;
+using Dalamud.Interface.Internal;
using ImGuiScene;
using JetBrains.Annotations;
using Lumina;
@@ -21,7 +22,7 @@ namespace Dalamud.Data
///
/// This class provides data for Dalamud-internal features, but can also be used by plugins if needed.
///
- public class DataManager : IDisposable
+ public sealed class DataManager : IDisposable
{
private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex";
private readonly InterfaceManager interfaceManager;
@@ -32,6 +33,7 @@ namespace Dalamud.Data
private GameData gameData;
private Thread luminaResourceThread;
+ private CancellationTokenSource luminaCancellationTokenSource;
///
/// Initializes a new instance of the class.
@@ -41,8 +43,9 @@ namespace Dalamud.Data
internal DataManager(ClientLanguage language, InterfaceManager interfaceManager)
{
this.interfaceManager = interfaceManager;
+
// Set up default values so plugins do not null-reference when data is being loaded.
- this.ServerOpCodes = new ReadOnlyDictionary(new Dictionary());
+ this.ClientOpCodes = this.ServerOpCodes = new ReadOnlyDictionary(new Dictionary());
this.Language = language;
}
@@ -99,7 +102,7 @@ namespace Dalamud.Data
ClientLanguage.English => Lumina.Data.Language.English,
ClientLanguage.German => Lumina.Data.Language.German,
ClientLanguage.French => Lumina.Data.Language.French,
- _ => throw new ArgumentOutOfRangeException(nameof(this.Language), $"Unknown Language: {this.Language}"),
+ _ => throw new ArgumentOutOfRangeException(nameof(language), $"Unknown Language: {language}"),
};
return this.Excel.GetSheet(lang);
}
@@ -162,7 +165,7 @@ namespace Dalamud.Data
ClientLanguage.English => "en/",
ClientLanguage.German => "de/",
ClientLanguage.French => "fr/",
- _ => throw new ArgumentOutOfRangeException(nameof(this.Language), $"Unknown Language: {this.Language}"),
+ _ => throw new ArgumentOutOfRangeException(nameof(iconLanguage), $"Unknown Language: {iconLanguage}"),
};
return this.GetIcon(type, iconId);
@@ -232,7 +235,7 @@ namespace Dalamud.Data
///
public void Dispose()
{
- this.luminaResourceThread.Abort();
+ this.luminaCancellationTokenSource.Cancel();
}
///
@@ -245,14 +248,14 @@ namespace Dalamud.Data
{
Log.Verbose("Starting data load...");
- var zoneOpCodeDict =
- JsonConvert.DeserializeObject>(File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json")));
+ var zoneOpCodeDict = JsonConvert.DeserializeObject>(
+ File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json")));
this.ServerOpCodes = new ReadOnlyDictionary(zoneOpCodeDict);
Log.Verbose("Loaded {0} ServerOpCodes.", zoneOpCodeDict.Count);
- var clientOpCodeDict =
- JsonConvert.DeserializeObject>(File.ReadAllText(Path.Combine(baseDir, "UIRes", "clientopcode.json")));
+ var clientOpCodeDict = JsonConvert.DeserializeObject>(
+ File.ReadAllText(Path.Combine(baseDir, "UIRes", "clientopcode.json")));
this.ClientOpCodes = new ReadOnlyDictionary(clientOpCodeDict);
Log.Verbose("Loaded {0} ClientOpCodes.", clientOpCodeDict.Count);
@@ -273,9 +276,7 @@ namespace Dalamud.Data
ClientLanguage.English => Lumina.Data.Language.English,
ClientLanguage.German => Lumina.Data.Language.German,
ClientLanguage.French => Lumina.Data.Language.French,
- _ => throw new ArgumentOutOfRangeException(
- nameof(this.Language),
- @"Unknown Language: " + this.Language),
+ _ => throw new ArgumentOutOfRangeException(nameof(this.Language), $"Unknown Language: {this.Language}"),
},
};
@@ -289,9 +290,12 @@ namespace Dalamud.Data
this.IsDataReady = true;
- this.luminaResourceThread = new Thread(() =>
+ this.luminaCancellationTokenSource = new();
+
+ var luminaCancellationToken = this.luminaCancellationTokenSource.Token;
+ this.luminaResourceThread = new(() =>
{
- while (true)
+ while (!luminaCancellationToken.IsCancellationRequested)
{
if (this.gameData.FileHandleManager.HasPendingFileLoads)
{
@@ -302,8 +306,6 @@ namespace Dalamud.Data
Thread.Sleep(5);
}
}
-
- // ReSharper disable once FunctionNeverReturns
});
this.luminaResourceThread.Start();
}
diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs
index 39fe1e38b..7cfe03443 100644
--- a/Dalamud/EntryPoint.cs
+++ b/Dalamud/EntryPoint.cs
@@ -1,11 +1,13 @@
using System;
using System.IO;
using System.Net;
+using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
-using Dalamud.Interface;
-using EasyHook;
+using Dalamud.Configuration.Internal;
+using Dalamud.Interface.Internal;
+using Newtonsoft.Json;
using Serilog;
using Serilog.Core;
using Serilog.Events;
@@ -15,28 +17,41 @@ namespace Dalamud
///
/// The main entrypoint for the Dalamud system.
///
- public sealed class EntryPoint : IEntryPoint
+ public sealed class EntryPoint
{
///
- /// Initializes a new instance of the class.
+ /// A delegate used during initialization of the CLR from Dalamud.Boot.
///
- /// The used to load the DLL.
- /// The containing information needed to initialize Dalamud.
- public EntryPoint(RemoteHooking.IContext ctx, DalamudStartInfo info)
+ /// Pointer to a serialized data.
+ public delegate void InitDelegate(IntPtr infoPtr);
+
+ ///
+ /// Initialize Dalamud.
+ ///
+ /// Pointer to a serialized data.
+ public static void Initialize(IntPtr infoPtr)
{
- // Required by EasyHook
+ var infoStr = Marshal.PtrToStringAnsi(infoPtr);
+ var info = JsonConvert.DeserializeObject(infoStr);
+
+ new Thread(() => RunThread(info)).Start();
}
///
/// Initialize all Dalamud subsystems and start running on the main thread.
///
- /// The used to load the DLL.
/// The containing information needed to initialize Dalamud.
- public void Run(RemoteHooking.IContext ctx, DalamudStartInfo info)
+ private static void RunThread(DalamudStartInfo info)
{
+ // Load configuration first to get some early persistent state, like log level
+ var configuration = DalamudConfiguration.Load(info.ConfigurationPath);
+
// Setup logger
- var (logger, levelSwitch) = this.NewLogger(info.WorkingDirectory);
- Log.Logger = logger;
+ var levelSwitch = InitLogging(info.WorkingDirectory, configuration);
+
+ // Log any unhandled exception.
+ AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
+ TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
var finishSignal = new ManualResetEvent(false);
@@ -46,14 +61,9 @@ namespace Dalamud
Log.Information("Initializing a session..");
// This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally
- System.Net.ServicePointManager.SecurityProtocol =
- SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls | SecurityProtocolType.Ssl3;
+ ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls;
- // Log any unhandled exception.
- AppDomain.CurrentDomain.UnhandledException += this.OnUnhandledException;
- TaskScheduler.UnobservedTaskException += this.OnUnobservedTaskException;
-
- var dalamud = new Dalamud(info, levelSwitch, finishSignal);
+ var dalamud = new Dalamud(info, levelSwitch, finishSignal, configuration);
Log.Information("Starting a session..");
// Run session
@@ -68,7 +78,8 @@ namespace Dalamud
}
finally
{
- AppDomain.CurrentDomain.UnhandledException -= this.OnUnhandledException;
+ TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
+ AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;
Log.Information("Session has ended.");
Log.CloseAndFlush();
@@ -77,7 +88,7 @@ namespace Dalamud
}
}
- private (Logger Logger, LoggingLevelSwitch LevelSwitch) NewLogger(string baseDirectory)
+ private static LoggingLevelSwitch InitLogging(string baseDirectory, DalamudConfiguration configuration)
{
#if DEBUG
var logPath = Path.Combine(baseDirectory, "dalamud.log");
@@ -90,35 +101,34 @@ namespace Dalamud
#if DEBUG
levelSwitch.MinimumLevel = LogEventLevel.Verbose;
#else
- levelSwitch.MinimumLevel = LogEventLevel.Information;
+ levelSwitch.MinimumLevel = configuration.LogLevel;
#endif
-
- var newLogger = new LoggerConfiguration()
- .WriteTo.Async(a => a.File(logPath))
+ Log.Logger = new LoggerConfiguration()
+ .WriteTo.Async(a => a.File(logPath, fileSizeLimitBytes: 5 * 1024 * 1024, rollOnFileSizeLimit: true))
.WriteTo.Sink(SerilogEventSink.Instance)
.MinimumLevel.ControlledBy(levelSwitch)
.CreateLogger();
- return (newLogger, levelSwitch);
+ return levelSwitch;
}
- private void OnUnhandledException(object sender, UnhandledExceptionEventArgs arg)
+ private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs args)
{
- switch (arg.ExceptionObject)
+ switch (args.ExceptionObject)
{
case Exception ex:
Log.Fatal(ex, "Unhandled exception on AppDomain");
break;
default:
- Log.Fatal("Unhandled SEH object on AppDomain: {Object}", arg.ExceptionObject);
+ Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject);
break;
}
}
- private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
+ private static void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs args)
{
- if (!e.Observed)
- Log.Error(e.Exception, "Unobserved exception in Task.");
+ if (!args.Observed)
+ Log.Error(args.Exception, "Unobserved exception in Task.");
}
}
}
diff --git a/Dalamud/FodyWeavers.xsd b/Dalamud/FodyWeavers.xsd
index 2f1b8aae7..69dbe488c 100644
--- a/Dalamud/FodyWeavers.xsd
+++ b/Dalamud/FodyWeavers.xsd
@@ -11,6 +11,16 @@
Used to control if the On_PropertyName_Changed feature is enabled.
+
+
+ Used to control if the Dependent properties feature is enabled.
+
+
+
+
+ Used to control if the IsChanged property feature is enabled.
+
+
Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form.
@@ -31,6 +41,16 @@
Used to control if equality checks should use the static Equals method resolved from the base class.
+
+
+ Used to turn off build warnings from this weaver.
+
+
+
+
+ Used to turn off build warnings about mismatched On_PropertyName_Changed methods.
+
+
diff --git a/Dalamud/Game/Addon/DalamudSystemMenu.cs b/Dalamud/Game/Addon/DalamudSystemMenu.cs
index b783730d3..e735b6154 100644
--- a/Dalamud/Game/Addon/DalamudSystemMenu.cs
+++ b/Dalamud/Game/Addon/DalamudSystemMenu.cs
@@ -16,11 +16,11 @@ namespace Dalamud.Game.Addon
internal sealed unsafe partial class DalamudSystemMenu
{
private readonly Dalamud dalamud;
- private AtkValueChangeType atkValueChangeType;
- private AtkValueSetString atkValueSetString;
- private Hook hookAgentHudOpenSystemMenu;
+ private readonly AtkValueChangeType atkValueChangeType;
+ private readonly AtkValueSetString atkValueSetString;
+ private readonly Hook hookAgentHudOpenSystemMenu;
// TODO: Make this into events in Framework.Gui
- private Hook hookUiModuleRequestMainCommand;
+ private readonly Hook hookUiModuleRequestMainCommand;
///
/// Initializes a new instance of the class.
@@ -34,13 +34,10 @@ namespace Dalamud.Game.Addon
this.hookAgentHudOpenSystemMenu = new Hook(openSystemMenuAddress, this.AgentHudOpenSystemMenuDetour);
- var atkValueChangeTypeAddress =
- this.dalamud.SigScanner.ScanText("E8 ?? ?? ?? ?? 45 84 F6 48 8D 4C 24 ??");
- this.atkValueChangeType =
- Marshal.GetDelegateForFunctionPointer(atkValueChangeTypeAddress);
+ var atkValueChangeTypeAddress = this.dalamud.SigScanner.ScanText("E8 ?? ?? ?? ?? 45 84 F6 48 8D 4C 24 ??");
+ this.atkValueChangeType = Marshal.GetDelegateForFunctionPointer(atkValueChangeTypeAddress);
- var atkValueSetStringAddress =
- this.dalamud.SigScanner.ScanText("E8 ?? ?? ?? ?? 41 03 ED");
+ var atkValueSetStringAddress = this.dalamud.SigScanner.ScanText("E8 ?? ?? ?? ?? 41 03 ED");
this.atkValueSetString = Marshal.GetDelegateForFunctionPointer(atkValueSetStringAddress);
var uiModuleRequestMainCommandAddress = this.dalamud.SigScanner.ScanText("40 53 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B 01 8B DA 48 8B F1 FF 90 ?? ?? ?? ??");
@@ -140,10 +137,10 @@ namespace Dalamud.Game.Addon
switch (commandId)
{
case 69420:
- this.dalamud.DalamudUi.TogglePluginInstaller();
+ this.dalamud.DalamudUi.TogglePluginInstallerWindow();
break;
case 69421:
- this.dalamud.DalamudUi.ToggleSettings();
+ this.dalamud.DalamudUi.ToggleSettingsWindow();
break;
default:
this.hookUiModuleRequestMainCommand.Original(thisPtr, commandId);
diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs
index 5beb4c5ed..58701a572 100644
--- a/Dalamud/Game/ChatHandlers.cs
+++ b/Dalamud/Game/ChatHandlers.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Drawing;
using System.Linq;
using System.Reflection;
using System.Text;
@@ -11,7 +10,6 @@ using CheapLoc;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
-using Dalamud.Interface;
using Serilog;
namespace Dalamud.Game
@@ -21,39 +19,39 @@ namespace Dalamud.Game
///
public class ChatHandlers
{
- private static readonly Dictionary UnicodeToDiscordEmojiDict = new()
- {
- { "", "<:ffxive071:585847382210642069>" },
- { "", "<:ffxive083:585848592699490329>" },
- };
+ // private static readonly Dictionary UnicodeToDiscordEmojiDict = new()
+ // {
+ // { "", "<:ffxive071:585847382210642069>" },
+ // { "", "<:ffxive083:585848592699490329>" },
+ // };
- private readonly Dictionary handledChatTypeColors = new()
- {
- { XivChatType.CrossParty, Color.DodgerBlue },
- { XivChatType.Party, Color.DodgerBlue },
- { XivChatType.FreeCompany, Color.DeepSkyBlue },
- { XivChatType.CrossLinkShell1, Color.ForestGreen },
- { XivChatType.CrossLinkShell2, Color.ForestGreen },
- { XivChatType.CrossLinkShell3, Color.ForestGreen },
- { XivChatType.CrossLinkShell4, Color.ForestGreen },
- { XivChatType.CrossLinkShell5, Color.ForestGreen },
- { XivChatType.CrossLinkShell6, Color.ForestGreen },
- { XivChatType.CrossLinkShell7, Color.ForestGreen },
- { XivChatType.CrossLinkShell8, Color.ForestGreen },
- { XivChatType.Ls1, Color.ForestGreen },
- { XivChatType.Ls2, Color.ForestGreen },
- { XivChatType.Ls3, Color.ForestGreen },
- { XivChatType.Ls4, Color.ForestGreen },
- { XivChatType.Ls5, Color.ForestGreen },
- { XivChatType.Ls6, Color.ForestGreen },
- { XivChatType.Ls7, Color.ForestGreen },
- { XivChatType.Ls8, Color.ForestGreen },
- { XivChatType.TellIncoming, Color.HotPink },
- { XivChatType.PvPTeam, Color.SandyBrown },
- { XivChatType.Urgent, Color.DarkViolet },
- { XivChatType.NoviceNetwork, Color.SaddleBrown },
- { XivChatType.Echo, Color.Gray },
- };
+ // private readonly Dictionary handledChatTypeColors = new()
+ // {
+ // { XivChatType.CrossParty, Color.DodgerBlue },
+ // { XivChatType.Party, Color.DodgerBlue },
+ // { XivChatType.FreeCompany, Color.DeepSkyBlue },
+ // { XivChatType.CrossLinkShell1, Color.ForestGreen },
+ // { XivChatType.CrossLinkShell2, Color.ForestGreen },
+ // { XivChatType.CrossLinkShell3, Color.ForestGreen },
+ // { XivChatType.CrossLinkShell4, Color.ForestGreen },
+ // { XivChatType.CrossLinkShell5, Color.ForestGreen },
+ // { XivChatType.CrossLinkShell6, Color.ForestGreen },
+ // { XivChatType.CrossLinkShell7, Color.ForestGreen },
+ // { XivChatType.CrossLinkShell8, Color.ForestGreen },
+ // { XivChatType.Ls1, Color.ForestGreen },
+ // { XivChatType.Ls2, Color.ForestGreen },
+ // { XivChatType.Ls3, Color.ForestGreen },
+ // { XivChatType.Ls4, Color.ForestGreen },
+ // { XivChatType.Ls5, Color.ForestGreen },
+ // { XivChatType.Ls6, Color.ForestGreen },
+ // { XivChatType.Ls7, Color.ForestGreen },
+ // { XivChatType.Ls8, Color.ForestGreen },
+ // { XivChatType.TellIncoming, Color.HotPink },
+ // { XivChatType.PvPTeam, Color.SandyBrown },
+ // { XivChatType.Urgent, Color.DarkViolet },
+ // { XivChatType.NoviceNetwork, Color.SaddleBrown },
+ // { XivChatType.Echo, Color.Gray },
+ // };
private readonly Regex rmtRegex = new(
@"4KGOLD|We have sufficient stock|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 +94,14 @@ namespace Dalamud.Game
private readonly Regex urlRegex = new(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled);
private readonly Dalamud dalamud;
- private DalamudLinkPayload openInstallerWindowLink;
+ private readonly DalamudLinkPayload openInstallerWindowLink;
private bool hasSeenLoadingMsg;
///
/// Initializes a new instance of the class.
///
/// Dalamud instance.
- public ChatHandlers(Dalamud dalamud)
+ internal ChatHandlers(Dalamud dalamud)
{
this.dalamud = dalamud;
@@ -122,23 +120,20 @@ namespace Dalamud.Game
public string LastLink { get; private set; }
///
- /// Convert a string to SeString and wrap in italics payloads.
+ /// Convert a TextPayload to SeString and wrap in italics payloads.
///
/// Text to convert.
/// SeString payload of italicized text.
- private static SeString MakeItalics(string text)
- {
- // TODO: when the code OnCharMessage is switched to SeString, this can be a straight insertion of the
- // italics payloads only, and be a lot cleaner
- var italicString = new SeString(new List(new Payload[]
- {
- EmphasisItalicPayload.ItalicsOn,
- new TextPayload(text),
- EmphasisItalicPayload.ItalicsOff,
- }));
+ public static SeString MakeItalics(string text)
+ => MakeItalics(new TextPayload(text));
- return italicString;
- }
+ ///
+ /// Convert a TextPayload to SeString and wrap in italics payloads.
+ ///
+ /// Text to convert.
+ /// SeString payload of italicized text.
+ public static SeString MakeItalics(TextPayload text)
+ => new(EmphasisItalicPayload.ItalicsOn, text, EmphasisItalicPayload.ItalicsOff);
private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled)
{
@@ -243,13 +238,13 @@ namespace Dalamud.Game
var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString();
this.dalamud.Framework.Gui.Chat.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud vD{0} loaded."), assemblyVersion)
- + string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), this.dalamud.PluginManager.Plugins.Count));
+ + string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), this.dalamud.PluginManager.InstalledPlugins.Count));
if (this.dalamud.Configuration.PrintPluginsWelcomeMsg)
{
- foreach (var plugin in this.dalamud.PluginManager.Plugins.OrderBy(x => x.Plugin.Name))
+ foreach (var plugin in this.dalamud.PluginManager.InstalledPlugins.OrderBy(plugin => plugin.Name))
{
- this.dalamud.Framework.Gui.Chat.Print(string.Format(Loc.Localize("DalamudPluginLoaded", " 》 {0} v{1} loaded."), plugin.Plugin.Name, plugin.Definition.AssemblyVersion));
+ this.dalamud.Framework.Gui.Chat.Print(string.Format(Loc.Localize("DalamudPluginLoaded", " 》 {0} v{1} loaded."), plugin.Name, plugin.Manifest.AssemblyVersion));
}
}
@@ -257,20 +252,19 @@ namespace Dalamud.Game
{
this.dalamud.Framework.Gui.Chat.PrintChat(new XivChatEntry
{
- MessageBytes = Encoding.UTF8.GetBytes(Loc.Localize("DalamudUpdated", "The In-Game addon has been updated or was reinstalled successfully! Please check the discord for a full changelog.")),
+ Message = Loc.Localize("DalamudUpdated", "The In-Game addon has been updated or was reinstalled successfully! Please check the discord for a full changelog."),
Type = XivChatType.Notice,
});
- if (DalamudChangelogWindow.WarrantsChangelog)
-#pragma warning disable CS0162 // Unreachable code detected
- this.dalamud.DalamudUi.OpenChangelog();
-#pragma warning restore CS0162 // Unreachable code detected
+ if (this.dalamud.DalamudUi.WarrantsChangelog)
+ this.dalamud.DalamudUi.OpenChangelogWindow();
this.dalamud.Configuration.LastVersion = assemblyVersion;
this.dalamud.Configuration.Save();
}
- Task.Run(() => this.dalamud.PluginRepository.UpdatePlugins(!this.dalamud.Configuration.AutoUpdatePlugins)).ContinueWith(t =>
+ Task.Run(() => this.dalamud.PluginManager.UpdatePlugins(!this.dalamud.Configuration.AutoUpdatePlugins))
+ .ContinueWith(t =>
{
if (t.IsFaulted)
{
@@ -278,19 +272,19 @@ namespace Dalamud.Game
}
else
{
- var updatedPlugins = t.Result.UpdatedPlugins;
+ var updatedPlugins = t.Result;
if (updatedPlugins != null && updatedPlugins.Any())
{
if (this.dalamud.Configuration.AutoUpdatePlugins)
{
- this.dalamud.PluginRepository.PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:"));
+ this.dalamud.PluginManager.PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:"));
}
else
{
this.dalamud.Framework.Gui.Chat.PrintChat(new XivChatEntry
{
- MessageBytes = new SeString(new List()
+ Message = new SeString(new List()
{
new TextPayload(Loc.Localize("DalamudPluginUpdateRequired", "One or more of your plugins needs to be updated. Please use the /xlplugins command in-game to update them!")),
new TextPayload(" ["),
@@ -300,7 +294,7 @@ namespace Dalamud.Game
RawPayload.LinkTerminator,
new UIForegroundPayload(this.dalamud.Data, 0),
new TextPayload("]"),
- }).Encode(),
+ }),
Type = XivChatType.Urgent,
});
}
diff --git a/Dalamud/Game/ClientState/Actors/ActorTable.cs b/Dalamud/Game/ClientState/Actors/ActorTable.cs
index 7f8ca7c35..a139f8b48 100644
--- a/Dalamud/Game/ClientState/Actors/ActorTable.cs
+++ b/Dalamud/Game/ClientState/Actors/ActorTable.cs
@@ -1,8 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
-using System.Linq;
-using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Actors.Types;
using Dalamud.Game.ClientState.Actors.Types.NonPlayer;
@@ -14,204 +12,131 @@ namespace Dalamud.Game.ClientState.Actors
///
/// This collection represents the currently spawned FFXIV actors.
///
- public sealed partial class ActorTable : IReadOnlyCollection, ICollection, IDisposable
+ public sealed partial class ActorTable
{
private const int ActorTableLength = 424;
- #region ReadProcessMemory Hack
- private static readonly int ActorMemSize = Marshal.SizeOf(typeof(Structs.Actor));
- private static readonly IntPtr ActorMem = Marshal.AllocHGlobal(ActorMemSize);
- private static readonly IntPtr CurrentProcessHandle = new(-1);
- #endregion
-
- private Dalamud dalamud;
- private ClientStateAddressResolver address;
- private List actorsCache;
+ private readonly Dalamud dalamud;
+ private readonly ClientStateAddressResolver address;
///
/// Initializes a new instance of the class.
- /// Set up the actor table collection.
///
- /// The Dalamud instance.
- /// The ClientStateAddressResolver instance.
- public ActorTable(Dalamud dalamud, ClientStateAddressResolver addressResolver)
+ /// The instance.
+ /// Client state address resolver.
+ internal ActorTable(Dalamud dalamud, ClientStateAddressResolver addressResolver)
{
- this.address = addressResolver;
this.dalamud = dalamud;
+ this.address = addressResolver;
- dalamud.Framework.OnUpdateEvent += this.Framework_OnUpdateEvent;
-
- Log.Verbose("Actor table address {ActorTable}", this.address.ActorTable);
+ Log.Verbose($"Actor table address 0x{this.address.ActorTable.ToInt64():X}");
}
///
/// Gets the amount of currently spawned actors.
///
- public int Length => this.ActorsCache.Count;
+ public int Length
+ {
+ get
+ {
+ var count = 0;
+ for (var i = 0; i < ActorTableLength; i++)
+ {
+ var ptr = this.GetActorAddress(i);
+ if (ptr != IntPtr.Zero)
+ {
+ count++;
+ }
+ }
- private List ActorsCache => this.actorsCache ??= this.GetActorTable();
+ return count;
+ }
+ }
///
/// Get an actor at the specified spawn index.
///
/// Spawn index.
- /// at the specified spawn index.
+ /// An at the specified spawn index.
[CanBeNull]
- public Actor this[int index] => this.ActorsCache[index];
+ public Actor this[int index]
+ {
+ get
+ {
+ var address = this.GetActorAddress(index);
+ return this.CreateActorReference(address);
+ }
+ }
///
- /// Read an actor struct from memory and create the appropriate type of actor.
+ /// Gets the address of the actor at the specified index of the actor table.
///
- /// Offset of the actor in the actor table.
- /// An instantiated actor.
- internal Actor ReadActorFromMemory(IntPtr offset)
+ /// The index of the actor.
+ /// The memory address of the actor.
+ public unsafe IntPtr GetActorAddress(int index)
{
- try
- {
- // FIXME: hack workaround for trying to access the player on logout, after the main object has been deleted
- if (!NativeFunctions.ReadProcessMemory(CurrentProcessHandle, offset, ActorMem, ActorMemSize, out _))
- {
- Log.Debug("ActorTable - ReadProcessMemory failed: likely player deletion during logout");
- return null;
- }
+ if (index >= ActorTableLength)
+ return IntPtr.Zero;
- var actorStruct = Marshal.PtrToStructure(ActorMem);
+ return *(IntPtr*)(this.address.ActorTable + (8 * index));
+ }
- return actorStruct.ObjectKind switch
- {
- ObjectKind.Player => new PlayerCharacter(offset, actorStruct, this.dalamud),
- ObjectKind.BattleNpc => new BattleNpc(offset, actorStruct, this.dalamud),
- ObjectKind.EventObj => new EventObj(offset, actorStruct, this.dalamud),
- ObjectKind.Companion => new Npc(offset, actorStruct, this.dalamud),
- _ => new Actor(offset, actorStruct, this.dalamud),
- };
- }
- catch (Exception e)
- {
- Log.Error(e, "Could not read actor from memory.");
+ ///
+ /// Create a reference to a FFXIV actor.
+ ///
+ /// The address of the actor in memory.
+ /// object or inheritor containing requested data.
+ [CanBeNull]
+ public unsafe Actor CreateActorReference(IntPtr address)
+ {
+ if (this.dalamud.ClientState.LocalContentId == 0)
return null;
- }
- }
- private void ResetCache() => this.actorsCache = null;
+ if (address == IntPtr.Zero)
+ return null;
- private void Framework_OnUpdateEvent(Internal.Framework framework)
- {
- this.ResetCache();
- }
-
- private IntPtr[] GetPointerTable()
- {
- var ret = new IntPtr[ActorTableLength];
- Marshal.Copy(this.address.ActorTable, ret, 0, ActorTableLength);
- return ret;
- }
-
- private List GetActorTable()
- {
- var actors = new List();
- var ptrTable = this.GetPointerTable();
- for (var i = 0; i < ActorTableLength; i++)
+ var objKind = *(ObjectKind*)(address + ActorOffsets.ObjectKind);
+ return objKind switch
{
- actors.Add(ptrTable[i] != IntPtr.Zero ? this.ReadActorFromMemory(ptrTable[i]) : null);
- }
-
- return actors;
+ ObjectKind.Player => new PlayerCharacter(address, this.dalamud),
+ ObjectKind.BattleNpc => new BattleNpc(address, this.dalamud),
+ ObjectKind.EventObj => new EventObj(address, this.dalamud),
+ ObjectKind.Companion => new Npc(address, this.dalamud),
+ _ => new Actor(address, this.dalamud),
+ };
}
}
///
- /// Implementing IDisposable.
+ /// This collection represents the currently spawned FFXIV actors.
///
- public sealed partial class ActorTable : IDisposable
+ public sealed partial class ActorTable : IReadOnlyCollection, ICollection
{
- private bool disposed = false;
-
- ///
- /// Finalizes an instance of the class.
- ///
- ~ActorTable() => this.Dispose(false);
-
- ///
- /// Disposes of managed and unmanaged resources.
- ///
- public void Dispose()
- {
- this.Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- private void Dispose(bool disposing)
- {
- if (this.disposed)
- return;
-
- if (disposing)
- {
- this.dalamud.Framework.OnUpdateEvent -= this.Framework_OnUpdateEvent;
- Marshal.FreeHGlobal(ActorMem);
- }
-
- this.disposed = true;
- }
- }
-
- ///
- /// Implementing IReadOnlyCollection, IEnumerable, and Enumerable.
- ///
- public sealed partial class ActorTable : IReadOnlyCollection
- {
- ///
- /// Gets the number of elements in the collection.
- ///
- /// The number of elements in the collection.
+ ///
int IReadOnlyCollection.Count => this.Length;
- ///
- /// Gets an enumerator capable of iterating through the actor table.
- ///
- /// An actor enumerable.
- public IEnumerator GetEnumerator() => this.ActorsCache.Where(a => a != null).GetEnumerator();
-
- ///
- /// Gets an enumerator capable of iterating through the actor table.
- ///
- /// An actor enumerable.
- IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
- }
-
- ///
- /// Implementing ICollection.
- ///
- public sealed partial class ActorTable : ICollection
- {
- ///
- /// Gets the number of elements in the collection.
- ///
- /// The number of elements in the collection.
+ ///
int ICollection.Count => this.Length;
- ///
- /// Gets a value indicating whether access to the collection is synchronized (thread safe).
- ///
- /// Whether access is synchronized (thread safe) or not.
+ ///
bool ICollection.IsSynchronized => false;
- ///
- /// Gets an object that can be used to synchronize access to the collection.
- ///
- /// An object that can be used to synchronize access to the collection.
+ ///
object ICollection.SyncRoot => this;
- ///
- /// Copies the elements of the collection to an array, starting at a particular index.
- ///
- ///
- /// The one-dimensional array that is the destination of the elements copied from the collection. The array must have zero-based indexing.
- ///
- ///
- /// The zero-based index in array at which copying begins.
- ///
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ for (var i = 0; i < ActorTableLength; i++)
+ {
+ yield return this[i];
+ }
+ }
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
+
+ ///
void ICollection.CopyTo(Array array, int index)
{
for (var i = 0; i < this.Length; i++)
diff --git a/Dalamud/Game/ClientState/Actors/Resolvers/BaseResolver.cs b/Dalamud/Game/ClientState/Actors/Resolvers/BaseResolver.cs
deleted file mode 100644
index c097b1111..000000000
--- a/Dalamud/Game/ClientState/Actors/Resolvers/BaseResolver.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-namespace Dalamud.Game.ClientState.Actors.Resolvers
-{
- ///
- /// Base object resolver.
- ///
- public abstract class BaseResolver
- {
- private Dalamud dalamud;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The Dalamud instance.
- public BaseResolver(Dalamud dalamud)
- {
- this.dalamud = dalamud;
- }
-
- ///
- /// Gets the Dalamud instance.
- ///
- protected Dalamud Dalamud => this.dalamud;
- }
-}
diff --git a/Dalamud/Game/ClientState/Actors/Resolvers/ClassJob.cs b/Dalamud/Game/ClientState/Actors/Resolvers/ClassJob.cs
deleted file mode 100644
index 57ae9fc48..000000000
--- a/Dalamud/Game/ClientState/Actors/Resolvers/ClassJob.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-namespace Dalamud.Game.ClientState.Actors.Resolvers
-{
- ///
- /// This object represents a class or job.
- ///
- public class ClassJob : BaseResolver
- {
- ///
- /// ID of the ClassJob.
- ///
- public readonly uint Id;
-
- ///
- /// Initializes a new instance of the class.
- /// Set up the ClassJob resolver with the provided ID.
- ///
- /// The ID of the classJob.
- /// The Dalamud instance.
- public ClassJob(byte id, Dalamud dalamud)
- : base(dalamud)
- {
- this.Id = id;
- }
-
- ///
- /// Gets GameData linked to this ClassJob.
- ///
- public Lumina.Excel.GeneratedSheets.ClassJob GameData =>
- this.Dalamud.Data.GetExcelSheet().GetRow(this.Id);
- }
-}
diff --git a/Dalamud/Game/ClientState/Actors/Resolvers/World.cs b/Dalamud/Game/ClientState/Actors/Resolvers/World.cs
deleted file mode 100644
index 536bfaf81..000000000
--- a/Dalamud/Game/ClientState/Actors/Resolvers/World.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-namespace Dalamud.Game.ClientState.Actors.Resolvers
-{
- ///
- /// This object represents a world a character can reside on.
- ///
- public class World : BaseResolver
- {
- ///
- /// ID of the world.
- ///
- public readonly uint Id;
-
- ///
- /// Initializes a new instance of the class.
- /// Set up the world resolver with the provided ID.
- ///
- /// The ID of the world.
- /// The Dalamud instance.
- public World(ushort id, Dalamud dalamud)
- : base(dalamud)
- {
- this.Id = id;
- }
-
- ///
- /// Gets GameData linked to this world.
- ///
- public Lumina.Excel.GeneratedSheets.World GameData =>
- this.Dalamud.Data.GetExcelSheet().GetRow(this.Id);
- }
-}
diff --git a/Dalamud/Game/ClientState/Actors/Targets.cs b/Dalamud/Game/ClientState/Actors/Targets.cs
index c9e8a5129..154aa446d 100644
--- a/Dalamud/Game/ClientState/Actors/Targets.cs
+++ b/Dalamud/Game/ClientState/Actors/Targets.cs
@@ -2,6 +2,7 @@ using System;
using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Actors.Types;
+using JetBrains.Annotations;
namespace Dalamud.Game.ClientState.Actors
{
@@ -10,8 +11,8 @@ namespace Dalamud.Game.ClientState.Actors
///
public sealed class Targets
{
- private Dalamud dalamud;
- private ClientStateAddressResolver address;
+ private readonly Dalamud dalamud;
+ private readonly ClientStateAddressResolver address;
///
/// Initializes a new instance of the class.
@@ -27,26 +28,31 @@ namespace Dalamud.Game.ClientState.Actors
///
/// Gets the current target.
///
+ [CanBeNull]
public Actor CurrentTarget => this.GetActorByOffset(TargetOffsets.CurrentTarget);
///
/// Gets the mouseover target.
///
+ [CanBeNull]
public Actor MouseOverTarget => this.GetActorByOffset(TargetOffsets.MouseOverTarget);
///
/// Gets the focus target.
///
+ [CanBeNull]
public Actor FocusTarget => this.GetActorByOffset(TargetOffsets.FocusTarget);
///
/// Gets the previous target.
///
+ [CanBeNull]
public Actor PreviousTarget => this.GetActorByOffset(TargetOffsets.PreviousTarget);
///
/// Gets the soft target.
///
+ [CanBeNull]
public Actor SoftTarget => this.GetActorByOffset(TargetOffsets.SoftTarget);
///
@@ -91,6 +97,7 @@ namespace Dalamud.Game.ClientState.Actors
Marshal.WriteIntPtr(this.address.TargetManager, offset, actorAddress);
}
+ [CanBeNull]
private Actor GetActorByOffset(int offset)
{
if (this.address.TargetManager == IntPtr.Zero)
@@ -100,7 +107,7 @@ namespace Dalamud.Game.ClientState.Actors
if (actorAddress == IntPtr.Zero)
return null;
- return this.dalamud.ClientState.Actors.ReadActorFromMemory(actorAddress);
+ return this.dalamud.ClientState.Actors.CreateActorReference(actorAddress);
}
}
diff --git a/Dalamud/Game/ClientState/Actors/Types/Actor.cs b/Dalamud/Game/ClientState/Actors/Types/Actor.cs
index 9707f6674..f98d8932e 100644
--- a/Dalamud/Game/ClientState/Actors/Types/Actor.cs
+++ b/Dalamud/Game/ClientState/Actors/Types/Actor.cs
@@ -1,102 +1,165 @@
using System;
-using System.Text;
+using System.Runtime.InteropServices;
+
using Dalamud.Game.ClientState.Structs;
-using Serilog;
+using Dalamud.Game.Text.SeStringHandling;
+using Dalamud.Memory;
namespace Dalamud.Game.ClientState.Actors.Types
{
///
- /// This class represents a basic FFXIV actor.
+ /// This class represents a basic actor (GameObject) in FFXIV.
///
- public class Actor : IEquatable
+ public unsafe partial class Actor : IEquatable
{
- private readonly Structs.Actor actorStruct;
- private readonly Dalamud dalamud;
-
- private string name;
-
///
/// Initializes a new instance of the class.
- /// This represents a basic FFXIV actor.
///
- /// The memory representation of the base actor.
- /// A dalamud reference needed to access game data in Resolvers.
/// The address of this actor in memory.
- public Actor(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
+ /// A dalamud reference needed to access game data in Resolvers.
+ internal Actor(IntPtr address, Dalamud dalamud)
{
- this.actorStruct = actorStruct;
- this.dalamud = dalamud;
+ this.Dalamud = dalamud;
this.Address = address;
}
///
- /// Gets the position of this .
+ /// Gets the address of the actor in memory.
///
- public Position3 Position => this.ActorStruct.Position;
+ public IntPtr Address { get; }
///
- /// Gets the rotation of this .
- /// This ranges from -pi to pi radians.
+ /// Gets Dalamud itself.
///
- public float Rotation => this.ActorStruct.Rotation;
+ private protected Dalamud Dalamud { get; }
+ ///
+ /// This allows you to if (actor) {...} to check for validity.
+ ///
+ /// The actor to check.
+ /// True or false.
+ public static implicit operator bool(Actor actor) => IsValid(actor);
+
+ public static bool operator ==(Actor actor1, Actor actor2)
+ {
+ if (actor1 is null || actor2 is null)
+ return Equals(actor1, actor2);
+
+ return actor1.Equals(actor2);
+ }
+
+ public static bool operator !=(Actor actor1, Actor actor2) => !(actor1 == actor2);
+
+ ///
+ /// Gets a value indicating whether this actor is still valid in memory.
+ ///
+ /// The actor to check.
+ /// True or false.
+ public static bool IsValid(Actor actor)
+ {
+ if (actor == null)
+ return false;
+
+ if (actor.Dalamud.ClientState.LocalContentId == 0)
+ return false;
+
+ return true;
+ }
+
+ ///
+ /// Gets a value indicating whether this actor is still valid in memory.
+ ///
+ /// True or false.
+ public bool IsValid() => IsValid(this);
+
+ ///
+ bool IEquatable.Equals(Actor other) => this.ActorId == other?.ActorId;
+
+ ///
+ public override bool Equals(object obj) => ((IEquatable)this).Equals(obj as Actor);
+
+ ///
+ public override int GetHashCode() => this.ActorId.GetHashCode();
+ }
+
+ ///
+ /// This class represents a basic actor (GameObject) in FFXIV.
+ ///
+ public unsafe partial class Actor
+ {
///
/// Gets the displayname of this .
///
- public string Name => this.name ??= Util.GetUTF8String(this.actorStruct.Name);
+ public SeString Name => MemoryHelper.ReadSeString(this.Address + ActorOffsets.Name, 32);
///
/// Gets the actor ID of this .
///
- public int ActorId => this.ActorStruct.ActorId;
+ public uint ActorId => *(uint*)(this.Address + ActorOffsets.ActorId);
///
- /// Gets the hitbox radius of this .
+ /// Gets the data ID for linking to other respective game data.
///
- public float HitboxRadius => this.ActorStruct.HitboxRadius;
+ public uint DataId => *(uint*)(this.Address + ActorOffsets.DataId);
+
+ ///
+ /// Gets the ID of this GameObject's owner.
+ ///
+ public uint OwnerId => *(uint*)(this.Address + ActorOffsets.OwnerId);
///
/// Gets the entity kind of this .
/// See the ObjectKind enum for possible values.
///
- public ObjectKind ObjectKind => this.ActorStruct.ObjectKind;
+ public ObjectKind ObjectKind => *(ObjectKind*)(this.Address + ActorOffsets.ObjectKind);
+
+ ///
+ /// Gets the sub kind of this Actor.
+ ///
+ public byte SubKind => *(byte*)(this.Address + ActorOffsets.SubKind);
+
+ ///
+ /// Gets a value indicating whether the actor is friendly.
+ ///
+ public bool IsFriendly => *(int*)(this.Address + ActorOffsets.IsFriendly) > 0;
///
/// Gets the X distance from the local player in yalms.
///
- public byte YalmDistanceX => this.ActorStruct.YalmDistanceFromPlayerX;
+ public byte YalmDistanceX => *(byte*)(this.Address + ActorOffsets.YalmDistanceFromObjectX);
+
+ ///
+ /// Gets the target status.
+ ///
+ ///
+ /// This is some kind of enum. It may be .
+ ///
+ public byte TargetStatus => *(byte*)(this.Address + ActorOffsets.TargetStatus);
///
/// Gets the Y distance from the local player in yalms.
///
- public byte YalmDistanceY => this.ActorStruct.YalmDistanceFromPlayerY;
+ public byte YalmDistanceY => *(byte*)(this.Address + ActorOffsets.YalmDistanceFromObjectY);
///
- /// Gets the target of the actor.
+ /// Gets the position of this .
///
- public virtual int TargetActorID => 0;
+ public Position3 Position => *(Position3*)(this.Address + ActorOffsets.Position);
///
- /// Gets status Effects.
+ /// Gets the rotation of this .
+ /// This ranges from -pi to pi radians.
///
- public StatusEffect[] StatusEffects => this.ActorStruct.UIStatusEffects;
+ public float Rotation => *(float*)(this.Address + ActorOffsets.Rotation);
///
- /// Gets the address of this actor in memory.
+ /// Gets the hitbox radius of this .
///
- public readonly IntPtr Address;
+ public float HitboxRadius => *(float*)(this.Address + ActorOffsets.HitboxRadius);
///
- /// Gets the memory representation of the base actor.
+ /// Gets the current target of the Actor.
///
- internal Structs.Actor ActorStruct => this.actorStruct;
-
- ///
- /// Gets the backing instance.
- ///
- protected Dalamud Dalamud => this.dalamud;
-
- ///
- bool IEquatable.Equals(Actor other) => this.ActorId == other.ActorId;
+ public virtual uint TargetActorID => 0;
}
}
diff --git a/Dalamud/Game/ClientState/Actors/Types/ActorOffsets.cs b/Dalamud/Game/ClientState/Actors/Types/ActorOffsets.cs
new file mode 100644
index 000000000..e3ef26a5c
--- /dev/null
+++ b/Dalamud/Game/ClientState/Actors/Types/ActorOffsets.cs
@@ -0,0 +1,61 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Dalamud.Game.ClientState.Actors.Types
+{
+ ///
+ /// Memory offsets for the type and all that inherit from it.
+ ///
+ [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the offset usage instead.")]
+ public static class ActorOffsets
+ {
+ // GameObject(Actor)
+ // GameObject :: Character
+ // GameObject :: Character :: BattleChara
+ // GameObject :: Character :: Companion
+
+ public const int Name = 0x30;
+ public const int ActorId = 0x74;
+ public const int DataId = 0x80;
+ public const int OwnerId = 0x84;
+ public const int ObjectKind = 0x8C;
+ public const int SubKind = 0x8D;
+ public const int IsFriendly = 0x8E;
+ public const int YalmDistanceFromObjectX = 0x90;
+ public const int TargetStatus = 0x91;
+ public const int YalmDistanceFromObjectY = 0x92;
+ public const int Position = 0xA0;
+ public const int Rotation = 0xB0;
+ public const int HitboxRadius = 0xC0;
+ // End GameObject 0x1A0
+
+ public const int CurrentHp = 0x1C4;
+ public const int MaxHp = 0x1C8;
+ public const int CurrentMp = 0x1CC;
+ public const int MaxMp = 0x1D0;
+ public const int CurrentGp = 0x1D4;
+ public const int MaxGp = 0x1D6;
+ public const int CurrentCp = 0x1D8;
+ public const int MaxCp = 0x1DA;
+ public const int ClassJob = 0x1E2;
+ public const int Level = 0x1E3;
+ public const int PlayerCharacterTargetActorId = 0x230;
+ public const int Customize = 0x1898;
+ public const int CompanyTag = 0x18B2;
+ public const int BattleNpcTargetActorId = 0x18D8;
+ public const int NameId = 0x1940;
+ public const int CurrentWorld = 0x195C;
+ public const int HomeWorld = 0x195E;
+ public const int StatusFlags = 0x19A0;
+ // End Character 0x19B0
+ // End Companion 0x19C0
+
+ public const int UIStatusEffects = 0x19F8;
+ public const int IsCasting = 0x1B80;
+ public const int IsCasting2 = 0x1B82;
+ public const int CurrentCastSpellActionId = 0x1B84;
+ public const int CurrentCastTargetActorId = 0x1B90;
+ public const int CurrentCastTime = 0x1BB4;
+ public const int TotalCastTime = 0x1BB8;
+ // End BattleChara 0x2C00
+ }
+}
diff --git a/Dalamud/Game/ClientState/Actors/Types/Chara.cs b/Dalamud/Game/ClientState/Actors/Types/Chara.cs
index 01e4ba243..792ca17c0 100644
--- a/Dalamud/Game/ClientState/Actors/Types/Chara.cs
+++ b/Dalamud/Game/ClientState/Actors/Types/Chara.cs
@@ -1,80 +1,124 @@
using System;
-using Dalamud.Game.ClientState.Actors.Resolvers;
+using Dalamud.Game.ClientState.Resolvers;
+using Dalamud.Game.ClientState.Structs;
+using Dalamud.Memory;
namespace Dalamud.Game.ClientState.Actors.Types
{
///
/// This class represents the base for non-static entities.
///
- public class Chara : Actor
+ public unsafe class Chara : Actor
{
///
/// Initializes a new instance of the class.
/// This represents a non-static entity.
///
- /// The memory representation of the base actor.
- /// A dalamud reference needed to access game data in Resolvers.
/// The address of this actor in memory.
- protected Chara(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
- : base(address, actorStruct, dalamud)
+ /// A dalamud reference needed to access game data in Resolvers.
+ internal Chara(IntPtr address, Dalamud dalamud)
+ : base(address, dalamud)
{
}
- ///
- /// Gets the level of this Chara.
- ///
- public byte Level => this.ActorStruct.Level;
-
- ///
- /// Gets the ClassJob of this Chara.
- ///
- public ClassJob ClassJob => new(this.ActorStruct.ClassJob, this.Dalamud);
-
///
/// Gets the current HP of this Chara.
///
- public int CurrentHp => this.ActorStruct.CurrentHp;
+ public uint CurrentHp => *(uint*)(this.Address + ActorOffsets.CurrentHp);
///
/// Gets the maximum HP of this Chara.
///
- public int MaxHp => this.ActorStruct.MaxHp;
+ public uint MaxHp => *(uint*)(this.Address + ActorOffsets.MaxHp);
///
/// Gets the current MP of this Chara.
///
- public int CurrentMp => this.ActorStruct.CurrentMp;
+ public uint CurrentMp => *(uint*)(this.Address + ActorOffsets.CurrentMp);
///
/// Gets the maximum MP of this Chara.
///
- public int MaxMp => this.ActorStruct.MaxMp;
+ public uint MaxMp => *(uint*)(this.Address + ActorOffsets.MaxMp);
///
/// Gets the current GP of this Chara.
///
- public int CurrentGp => this.ActorStruct.CurrentGp;
+ public uint CurrentGp => *(uint*)(this.Address + ActorOffsets.CurrentGp);
///
/// Gets the maximum GP of this Chara.
///
- public int MaxGp => this.ActorStruct.MaxGp;
+ public uint MaxGp => *(uint*)(this.Address + ActorOffsets.MaxGp);
///
/// Gets the current CP of this Chara.
///
- public int CurrentCp => this.ActorStruct.CurrentCp;
+ public uint CurrentCp => *(uint*)(this.Address + ActorOffsets.CurrentCp);
///
/// Gets the maximum CP of this Chara.
///
- public int MaxCp => this.ActorStruct.MaxCp;
+ public uint MaxCp => *(uint*)(this.Address + ActorOffsets.MaxCp);
+
+ ///
+ /// Gets the ClassJob of this Chara.
+ ///
+ public ClassJobResolver ClassJob => new(*(byte*)(this.Address + ActorOffsets.ClassJob), this.Dalamud);
+
+ ///
+ /// Gets the level of this Chara.
+ ///
+ public byte Level => *(byte*)(this.Address + ActorOffsets.Level);
///
/// Gets a byte array describing the visual appearance of this Chara.
/// Indexed by .
///
- public byte[] Customize => this.ActorStruct.Customize;
+ public byte[] Customize => MemoryHelper.Read(this.Address + ActorOffsets.Customize, 28);
+
+ ///
+ /// Gets the status flags.
+ ///
+ public StatusFlags StatusFlags => *(StatusFlags*)(this.Address + ActorOffsets.StatusFlags);
+
+ ///
+ /// Gets the current status effects.
+ ///
+ ///
+ /// This copies every time it is invoked, so make sure to only grab it once.
+ ///
+ public StatusEffect[] StatusEffects => MemoryHelper.Read(this.Address + ActorOffsets.UIStatusEffects, 30, true);
+
+ ///
+ /// Gets a value indicating whether the actor is currently casting.
+ ///
+ public bool IsCasting => *(int*)(this.Address + ActorOffsets.IsCasting) > 0;
+
+ ///
+ /// Gets a value indicating whether the actor is currently casting (again?).
+ ///
+ public bool IsCasting2 => *(int*)(this.Address + ActorOffsets.IsCasting2) > 0;
+
+ ///
+ /// Gets the spell action ID currently being cast by the actor.
+ ///
+ public uint CurrentCastSpellActionId => *(uint*)(this.Address + ActorOffsets.CurrentCastSpellActionId);
+
+ ///
+ /// Gets the actor ID of the target currently being cast at by the actor.
+ ///
+ public uint CurrentCastTargetActorId => *(uint*)(this.Address + ActorOffsets.CurrentCastTargetActorId);
+
+ ///
+ /// Gets the current casting time of the spell being cast by the actor.
+ ///
+ public float CurrentCastTime => *(float*)(this.Address + ActorOffsets.CurrentCastTime);
+
+ ///
+ /// Gets the total casting time of the spell being cast by the actor.
+ ///
+ public float TotalCastTime => *(float*)(this.Address + ActorOffsets.TotalCastTime);
}
}
diff --git a/Dalamud/Game/ClientState/Actors/CustomizeIndex.cs b/Dalamud/Game/ClientState/Actors/Types/CustomizeIndex.cs
similarity index 98%
rename from Dalamud/Game/ClientState/Actors/CustomizeIndex.cs
rename to Dalamud/Game/ClientState/Actors/Types/CustomizeIndex.cs
index 2461a028a..23618ae24 100644
--- a/Dalamud/Game/ClientState/Actors/CustomizeIndex.cs
+++ b/Dalamud/Game/ClientState/Actors/Types/CustomizeIndex.cs
@@ -1,4 +1,4 @@
-namespace Dalamud.Game.ClientState.Actors
+namespace Dalamud.Game.ClientState.Actors.Types
{
///
/// This enum describes the indices of the Customize array.
diff --git a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/BattleNpc.cs b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/BattleNpc.cs
index 0bfd60c5b..3fa8f0c28 100644
--- a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/BattleNpc.cs
+++ b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/BattleNpc.cs
@@ -5,33 +5,27 @@ namespace Dalamud.Game.ClientState.Actors.Types.NonPlayer
///
/// This class represents a battle NPC.
///
- public class BattleNpc : Npc
+ public unsafe class BattleNpc : Npc
{
///
/// Initializes a new instance of the class.
/// Set up a new BattleNpc with the provided memory representation.
///
- /// The memory representation of the base actor.
- /// A dalamud reference needed to access game data in Resolvers.
/// The address of this actor in memory.
- public BattleNpc(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
- : base(address, actorStruct, dalamud)
+ /// A dalamud reference needed to access game data in Resolvers.
+ internal BattleNpc(IntPtr address, Dalamud dalamud)
+ : base(address, dalamud)
{
}
///
/// Gets the BattleNpc of this BattleNpc.
///
- public BattleNpcSubKind BattleNpcKind => (BattleNpcSubKind)this.ActorStruct.SubKind;
+ public BattleNpcSubKind BattleNpcKind => *(BattleNpcSubKind*)(this.Address + ActorOffsets.SubKind);
///
- /// Gets the ID of this BattleNpc's owner.
+ /// Gets the target of the Battle NPC.
///
- public int OwnerId => this.ActorStruct.OwnerId;
-
- ///
- /// Gets target of the Battle NPC.
- ///
- public override int TargetActorID => this.ActorStruct.BattleNpcTargetActorId;
+ public override uint TargetActorID => *(uint*)(this.Address + ActorOffsets.BattleNpcTargetActorId);
}
}
diff --git a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/EventObj.cs b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/EventObj.cs
index e0ac5964d..7d9dce443 100644
--- a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/EventObj.cs
+++ b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/EventObj.cs
@@ -5,23 +5,22 @@ namespace Dalamud.Game.ClientState.Actors.Types.NonPlayer
///
/// This class represents an EventObj.
///
- public class EventObj : Actor
+ public unsafe class EventObj : Actor
{
///
/// Initializes a new instance of the class.
- /// This represents an Event Object.
+ /// Set up a new EventObj with the provided memory representation.
///
- /// The memory representation of the base actor.
- /// A dalamud reference needed to access game data in Resolvers.
/// The address of this actor in memory.
- public EventObj(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
- : base(address, actorStruct, dalamud)
+ /// A dalamud reference needed to access game data in Resolvers.
+ internal EventObj(IntPtr address, Dalamud dalamud)
+ : base(address, dalamud)
{
}
///
- /// Gets the data ID of the NPC linking to their respective game data.
+ /// Gets the event object ID of the linking to their respective game data.
///
- public int DataId => this.ActorStruct.DataId;
+ public uint EventObjectId => *(uint*)(this.Address + ActorOffsets.DataId);
}
}
diff --git a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/Npc.cs b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/Npc.cs
index 7be029450..a3597570c 100644
--- a/Dalamud/Game/ClientState/Actors/Types/NonPlayer/Npc.cs
+++ b/Dalamud/Game/ClientState/Actors/Types/NonPlayer/Npc.cs
@@ -5,28 +5,27 @@ namespace Dalamud.Game.ClientState.Actors.Types.NonPlayer
///
/// This class represents a NPC.
///
- public class Npc : Chara
+ public unsafe class Npc : Chara
{
///
/// Initializes a new instance of the class.
- /// This represents a Non-playable Character.
+ /// Set up a new NPC with the provided memory representation.
///
- /// The memory representation of the base actor.
- /// A dalamud reference needed to access game data in Resolvers.
/// The address of this actor in memory.
- public Npc(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
- : base(address, actorStruct, dalamud)
+ /// A dalamud reference needed to access game data in Resolvers.
+ internal Npc(IntPtr address, Dalamud dalamud)
+ : base(address, dalamud)
{
}
///
- /// Gets the data ID of the NPC linking to their respective game data.
+ /// Gets the data ID of the NPC linking to their assoicated BNpcBase data.
///
- public int DataId => this.ActorStruct.DataId;
+ public uint BaseId => *(uint*)(this.Address + ActorOffsets.DataId);
///
/// Gets the name ID of the NPC linking to their respective game data.
///
- public int NameId => this.ActorStruct.NameId;
+ public uint NameId => *(uint*)(this.Address + ActorOffsets.NameId);
}
}
diff --git a/Dalamud/Game/ClientState/Actors/ObjectKind.cs b/Dalamud/Game/ClientState/Actors/Types/ObjectKind.cs
similarity index 97%
rename from Dalamud/Game/ClientState/Actors/ObjectKind.cs
rename to Dalamud/Game/ClientState/Actors/Types/ObjectKind.cs
index 4e62d812d..b79adb784 100644
--- a/Dalamud/Game/ClientState/Actors/ObjectKind.cs
+++ b/Dalamud/Game/ClientState/Actors/Types/ObjectKind.cs
@@ -1,4 +1,4 @@
-namespace Dalamud.Game.ClientState.Actors
+namespace Dalamud.Game.ClientState.Actors.Types
{
///
/// Enum describing possible entity kinds.
diff --git a/Dalamud/Game/ClientState/Actors/Types/PartyMember.cs b/Dalamud/Game/ClientState/Actors/Types/PartyMember.cs
index 74cf9c97c..2e2d67f54 100644
--- a/Dalamud/Game/ClientState/Actors/Types/PartyMember.cs
+++ b/Dalamud/Game/ClientState/Actors/Types/PartyMember.cs
@@ -1,4 +1,5 @@
-using System.Runtime.InteropServices;
+using Dalamud.Game.Text.SeStringHandling;
+using Dalamud.Memory;
namespace Dalamud.Game.ClientState.Actors.Types
{
@@ -7,26 +8,6 @@ namespace Dalamud.Game.ClientState.Actors.Types
///
public class PartyMember
{
- ///
- /// The name of the character.
- ///
- public string CharacterName;
-
- ///
- /// Unknown.
- ///
- public long Unknown;
-
- ///
- /// The actor object that corresponds to this party member.
- ///
- public Actor Actor;
-
- ///
- /// The kind or type of actor.
- ///
- public ObjectKind ObjectKind;
-
///
/// Initializes a new instance of the class.
///
@@ -34,9 +15,10 @@ namespace Dalamud.Game.ClientState.Actors.Types
/// The interop data struct.
public PartyMember(ActorTable table, Structs.PartyMember rawData)
{
- this.CharacterName = Marshal.PtrToStringAnsi(rawData.namePtr);
+ this.CharacterName = MemoryHelper.ReadSeString(rawData.namePtr);
this.Unknown = rawData.unknown;
this.Actor = null;
+
for (var i = 0; i < table.Length; i++)
{
if (table[i] != null && table[i].ActorId == rawData.actorId)
@@ -48,5 +30,25 @@ namespace Dalamud.Game.ClientState.Actors.Types
this.ObjectKind = rawData.objectKind;
}
+
+ ///
+ /// Gets the name of the character.
+ ///
+ public SeString CharacterName { get; }
+
+ ///
+ /// Gets something unknown.
+ ///
+ public long Unknown { get; }
+
+ ///
+ /// Gets the actor object that corresponds to this party member.
+ ///
+ public Actor Actor { get; }
+
+ ///
+ /// Gets the kind or type of actor.
+ ///
+ public ObjectKind ObjectKind { get; }
}
}
diff --git a/Dalamud/Game/ClientState/Actors/Types/PlayerCharacter.cs b/Dalamud/Game/ClientState/Actors/Types/PlayerCharacter.cs
index 7cc932550..a1ebe1ab0 100644
--- a/Dalamud/Game/ClientState/Actors/Types/PlayerCharacter.cs
+++ b/Dalamud/Game/ClientState/Actors/Types/PlayerCharacter.cs
@@ -1,51 +1,45 @@
using System;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Text;
-using Dalamud.Game.ClientState.Actors.Resolvers;
-using Dalamud.Game.ClientState.Structs;
+using Dalamud.Game.ClientState.Resolvers;
+using Dalamud.Game.Text.SeStringHandling;
+using Dalamud.Memory;
namespace Dalamud.Game.ClientState.Actors.Types
{
///
/// This class represents a player character.
///
- public class PlayerCharacter : Chara
+ public unsafe class PlayerCharacter : Chara
{
///
/// Initializes a new instance of the class.
/// This represents a player character.
///
- /// The memory representation of the base actor.
- /// A dalamud reference needed to access game data in Resolvers.
/// The address of this actor in memory.
- public PlayerCharacter(IntPtr address, Structs.Actor actorStruct, Dalamud dalamud)
- : base(address, actorStruct, dalamud)
+ /// A dalamud reference needed to access game data in Resolvers.
+ internal PlayerCharacter(IntPtr address, Dalamud dalamud)
+ : base(address, dalamud)
{
- var companyTagBytes = new byte[5];
- Marshal.Copy(this.Address + ActorOffsets.CompanyTag, companyTagBytes, 0, companyTagBytes.Length);
- this.CompanyTag = Encoding.UTF8.GetString(companyTagBytes.TakeWhile(c => c != 0x0).ToArray());
}
///
- /// Gets the current world of the character.
+ /// Gets the current world of the character.
///
- public World CurrentWorld => new(this.ActorStruct.CurrentWorld, this.Dalamud);
+ public WorldResolver CurrentWorld => new(*(ushort*)(this.Address + ActorOffsets.CurrentWorld), this.Dalamud);
///
- /// Gets the home world of the character.
+ /// Gets the home world of the character.
///
- public World HomeWorld => new(this.ActorStruct.HomeWorld, this.Dalamud);
+ public WorldResolver HomeWorld => new(*(ushort*)(this.Address + ActorOffsets.HomeWorld), this.Dalamud);
///
/// Gets the Free Company tag of this player.
///
- public string CompanyTag { get; private set; }
+ public SeString CompanyTag => MemoryHelper.ReadSeString(this.Address + ActorOffsets.CompanyTag, 6);
///
- /// Gets the target of the PlayerCharacter.
+ /// Gets the target actor ID of the PlayerCharacter.
///
- public override int TargetActorID => this.ActorStruct.PlayerCharacterTargetActorId;
+ public override uint TargetActorID => *(uint*)(this.Address + ActorOffsets.PlayerCharacterTargetActorId);
}
}
diff --git a/Dalamud/Game/ClientState/Actors/Types/StatusFlags.cs b/Dalamud/Game/ClientState/Actors/Types/StatusFlags.cs
new file mode 100644
index 000000000..a1101ceca
--- /dev/null
+++ b/Dalamud/Game/ClientState/Actors/Types/StatusFlags.cs
@@ -0,0 +1,56 @@
+using System;
+
+namespace Dalamud.Game.ClientState.Actors.Types
+{
+ ///
+ /// Enum describing possible status flags.
+ ///
+ [Flags]
+ public enum StatusFlags : byte
+ {
+ ///
+ /// No status flags set.
+ ///
+ None = 0,
+
+ ///
+ /// Hostile actor.
+ ///
+ Hostile = 1,
+
+ ///
+ /// Actor in combat.
+ ///
+ InCombat = 2,
+
+ ///
+ /// Actor weapon is out.
+ ///
+ WeaponOut = 4,
+
+ ///
+ /// Actor offhand is out.
+ ///
+ OffhandOut = 8,
+
+ ///
+ /// Actor is a party member.
+ ///
+ PartyMember = 16,
+
+ ///
+ /// Actor is a alliance member.
+ ///
+ AllianceMember = 32,
+
+ ///
+ /// Actor is in friend list.
+ ///
+ Friend = 64,
+
+ ///
+ /// Actor is casting.
+ ///
+ IsCasting = 128,
+ }
+}
diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs
index f2057a607..b9e40ccd7 100644
--- a/Dalamud/Game/ClientState/ClientState.cs
+++ b/Dalamud/Game/ClientState/ClientState.cs
@@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Actors;
using Dalamud.Game.ClientState.Actors.Types;
+using Dalamud.Game.ClientState.Fates;
using Dalamud.Game.Internal;
using Dalamud.Hooking;
using JetBrains.Annotations;
@@ -15,58 +16,8 @@ namespace Dalamud.Game.ClientState
///
/// This class represents the state of the game client at the time of access.
///
- public class ClientState : INotifyPropertyChanged, IDisposable
+ public sealed class ClientState : INotifyPropertyChanged, IDisposable
{
- ///
- /// The table of all present actors.
- ///
- public readonly ActorTable Actors;
-
- ///
- /// Gets the language of the client.
- ///
- public readonly ClientLanguage ClientLanguage;
-
- ///
- /// The current Territory the player resides in.
- ///
- public ushort TerritoryType;
-
- ///
- /// The class facilitating Job Gauge data access.
- ///
- public JobGauges JobGauges;
-
- ///
- /// The class facilitating party list data access.
- ///
- public PartyList PartyList;
-
- ///
- /// Provides access to the keypress state of keyboard keys in game.
- ///
- public KeyState KeyState;
-
- ///
- /// Provides access to the button state of gamepad buttons in game.
- ///
- public GamepadState GamepadState;
-
- ///
- /// Provides access to client conditions/player state. Allows you to check if a player is in a duty, mounted, etc.
- ///
- public Condition Condition;
-
- ///
- /// The class facilitating target data access.
- ///
- public Targets Targets;
-
- ///
- /// Event that gets fired when the current Territory changes.
- ///
- public EventHandler TerritoryChanged;
-
private readonly Dalamud dalamud;
private readonly ClientStateAddressResolver address;
private readonly Hook setupTerritoryTypeHook;
@@ -80,7 +31,7 @@ namespace Dalamud.Game.ClientState
/// Dalamud instance.
/// StartInfo of the current Dalamud launch.
/// Sig scanner.
- public ClientState(Dalamud dalamud, DalamudStartInfo startInfo, SigScanner scanner)
+ internal ClientState(Dalamud dalamud, DalamudStartInfo startInfo, SigScanner scanner)
{
this.dalamud = dalamud;
this.address = new ClientStateAddressResolver();
@@ -92,6 +43,8 @@ namespace Dalamud.Game.ClientState
this.Actors = new ActorTable(dalamud, this.address);
+ this.Fates = new FateTable(dalamud, this.address);
+
this.PartyList = new PartyList(dalamud, this.address);
this.JobGauges = new JobGauges(this.address);
@@ -104,7 +57,7 @@ namespace Dalamud.Game.ClientState
this.Targets = new Targets(dalamud, this.address);
- Log.Verbose("SetupTerritoryType address {SetupTerritoryType}", this.address.SetupTerritoryType);
+ Log.Verbose($"SetupTerritoryType address 0x{this.address.SetupTerritoryType.ToInt64():X}");
this.setupTerritoryTypeHook = new Hook(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour);
@@ -122,6 +75,11 @@ namespace Dalamud.Game.ClientState
public event PropertyChangedEventHandler PropertyChanged;
#pragma warning restore
+ ///
+ /// Event that gets fired when the current Territory changes.
+ ///
+ public event EventHandler TerritoryChanged;
+
///
/// Event that fires when a character is logging in.
///
@@ -137,22 +95,61 @@ namespace Dalamud.Game.ClientState
///
public event EventHandler CfPop;
+ ///
+ /// Gets the table of all present actors.
+ ///
+ public ActorTable Actors { get; }
+
+ ///
+ /// Gets the table of all present fates.
+ ///
+ public FateTable Fates { get; }
+
+ ///
+ /// Gets the language of the client.
+ ///
+ public ClientLanguage ClientLanguage { get; }
+
+ ///
+ /// Gets the class facilitating Job Gauge data access.
+ ///
+ public JobGauges JobGauges { get; }
+
+ ///
+ /// Gets the class facilitating party list data access.
+ ///
+ public PartyList PartyList { get; }
+
+ ///
+ /// Gets access to the keypress state of keyboard keys in game.
+ ///
+ public KeyState KeyState { get; }
+
+ ///
+ /// Gets access to the button state of gamepad buttons in game.
+ ///
+ public GamepadState GamepadState { get; }
+
+ ///
+ /// Gets access to client conditions/player state. Allows you to check if a player is in a duty, mounted, etc.
+ ///
+ public Condition Condition { get; }
+
+ ///
+ /// Gets the class facilitating target data access.
+ ///
+ public Targets Targets { get; }
+
+ ///
+ /// Gets the current Territory the player resides in.
+ ///
+ public ushort TerritoryType { get; private set; }
+
///
/// Gets the local player character, if one is present.
///
[CanBeNull]
- public PlayerCharacter LocalPlayer
- {
- get
- {
- var actor = this.Actors[0];
-
- if (actor is PlayerCharacter pc)
- return pc;
-
- return null;
- }
- }
+ public PlayerCharacter LocalPlayer => this.Actors[0] as PlayerCharacter;
///
/// Gets the content ID of the local character.
@@ -181,7 +178,6 @@ namespace Dalamud.Game.ClientState
{
this.PartyList.Dispose();
this.setupTerritoryTypeHook.Dispose();
- this.Actors.Dispose();
this.GamepadState.Dispose();
this.dalamud.Framework.OnUpdateEvent -= this.FrameworkOnOnUpdateEvent;
diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs
index ed87f06d5..a9937b40c 100644
--- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs
+++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs
@@ -1,4 +1,5 @@
using System;
+using System.Runtime.InteropServices;
using Dalamud.Game.Internal;
@@ -16,6 +17,14 @@ namespace Dalamud.Game.ClientState
///
public IntPtr ActorTable { get; private set; }
+ ///
+ /// Gets the address of the fate table pointer.
+ ///
+ ///
+ /// This is a static address to a pointer, not the address of the table itself.
+ ///
+ public IntPtr FateTablePtr { get; private set; }
+
// public IntPtr ViewportActorTable { get; private set; }
///
@@ -50,9 +59,6 @@ namespace Dalamud.Game.ClientState
///
public IntPtr SetupTerritoryType { get; private set; }
- // public IntPtr SomeActorTableAccess { get; private set; }
- // public IntPtr PartyListUpdate { get; private set; }
-
///
/// Gets the address of the method which polls the gamepads for data.
/// Called every frame, even when `Enable Gamepad` is off in the settings.
@@ -68,14 +74,18 @@ namespace Dalamud.Game.ClientState
// We don't need those anymore, but maybe someone else will - let's leave them here for good measure
// ViewportActorTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? 85 ED", 0) + 0x148;
// SomeActorTableAccess = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 55 A0 48 8D 8E ?? ?? ?? ??");
+
this.ActorTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 83");
+ this.FateTablePtr = sig.GetStaticAddressFromSig("48 8B 15 ?? ?? ?? ?? 48 8B F9 44 0F B7 41 ??");
+
this.LocalContentId = sig.GetStaticAddressFromSig("48 0F 44 05 ?? ?? ?? ?? 48 39 07");
this.JobGaugeData = sig.GetStaticAddressFromSig("E8 ?? ?? ?? ?? FF C6 48 8D 5B 0C", 0xB9) + 0x10;
this.SetupTerritoryType = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B F9 66 89 91 ?? ?? ?? ??");
- // This resolves to a fixed offset only, without the base address added in, so GetStaticAddressFromSig() can't be used
+ // This resolves to a fixed offset only, without the base address added in,
+ // so GetStaticAddressFromSig() can't be used. lea rcx, ds:1DB9F74h[rax*4]
this.KeyboardState = sig.ScanText("48 8D 0C 85 ?? ?? ?? ?? 8B 04 31 85 C2 0F 85") + 0x4;
// PartyListUpdate = sig.ScanText("E8 ?? ?? ?? ?? 49 8B D7 4C 8D 86 ?? ?? ?? ??");
diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs
new file mode 100644
index 000000000..07158820e
--- /dev/null
+++ b/Dalamud/Game/ClientState/Fates/FateTable.cs
@@ -0,0 +1,178 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+using Dalamud.Game.ClientState.Fates.Types;
+using JetBrains.Annotations;
+using Serilog;
+
+namespace Dalamud.Game.ClientState.Fates
+{
+ ///
+ /// This collection represents the currently available Fate events.
+ ///
+ public sealed partial class FateTable
+ {
+ // If the pointer at this offset is 0, do not scan the table
+ private const int CheckPtrOffset = 0x80;
+ private const int FirstPtrOffset = 0x90;
+ private const int LastPtrOffset = 0x98;
+
+ private readonly Dalamud dalamud;
+ private readonly ClientStateAddressResolver address;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The instance.
+ /// Client state address resolver.
+ internal FateTable(Dalamud dalamud, ClientStateAddressResolver addressResolver)
+ {
+ this.address = addressResolver;
+ this.dalamud = dalamud;
+
+ Log.Verbose($"Fate table address 0x{this.address.FateTablePtr.ToInt64():X}");
+ }
+
+ ///
+ /// Gets the amount of currently active Fates.
+ ///
+ public unsafe int Length
+ {
+ get
+ {
+ var fateTable = this.FateTableAddress;
+ if (fateTable == IntPtr.Zero)
+ return 0;
+
+ var check = *(long*)(fateTable + CheckPtrOffset);
+ if (check == 0)
+ return 0;
+
+ var start = *(long*)(fateTable + FirstPtrOffset);
+ var end = *(long*)(fateTable + LastPtrOffset);
+ if (start == 0 || end == 0)
+ return 0;
+
+ return (int)((end - start) / 8);
+ }
+ }
+
+ private unsafe IntPtr FateTableAddress
+ {
+ get
+ {
+ if (this.address.FateTablePtr == IntPtr.Zero)
+ return IntPtr.Zero;
+
+ return *(IntPtr*)this.address.FateTablePtr;
+ }
+ }
+
+ ///
+ /// Get an actor at the specified spawn index.
+ ///
+ /// Spawn index.
+ /// A at the specified spawn index.
+ [CanBeNull]
+ public Fate this[int index]
+ {
+ get
+ {
+ var address = this.GetFateAddress(index);
+ return this[address];
+ }
+ }
+
+ ///
+ /// Get a Fate at the specified address.
+ ///
+ /// The Fate address.
+ /// A at the specified address.
+ public Fate this[IntPtr address]
+ {
+ get
+ {
+ if (address == IntPtr.Zero)
+ return null;
+
+ return this.CreateFateReference(address);
+ }
+ }
+
+ ///
+ /// Gets the address of the Fate at the specified index of the fate table.
+ ///
+ /// The index of the Fate.
+ /// The memory address of the Fate.
+ public unsafe IntPtr GetFateAddress(int index)
+ {
+ if (index >= this.Length)
+ return IntPtr.Zero;
+
+ var fateTable = this.FateTableAddress;
+ if (fateTable == IntPtr.Zero)
+ return IntPtr.Zero;
+
+ var firstFate = *(IntPtr*)(fateTable + FirstPtrOffset);
+ return *(IntPtr*)(firstFate + (8 * index));
+ }
+
+ ///
+ /// Create a reference to a FFXIV actor.
+ ///
+ /// The offset of the actor in memory.
+ /// object containing requested data.
+ [CanBeNull]
+ internal unsafe Fate CreateFateReference(IntPtr offset)
+ {
+ if (this.dalamud.ClientState.LocalContentId == 0)
+ return null;
+
+ if (offset == IntPtr.Zero)
+ return null;
+
+ return new Fate(offset, this.dalamud);
+ }
+ }
+
+ ///
+ /// This collection represents the currently available Fate events.
+ ///
+ public sealed partial class FateTable : IReadOnlyCollection, ICollection
+ {
+ ///
+ int IReadOnlyCollection.Count => this.Length;
+
+ ///
+ int ICollection.Count => this.Length;
+
+ ///
+ bool ICollection.IsSynchronized => false;
+
+ ///
+ object ICollection.SyncRoot => this;
+
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ for (var i = 0; i < this.Length; i++)
+ {
+ yield return this[i];
+ }
+ }
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
+
+ ///
+ void ICollection.CopyTo(Array array, int index)
+ {
+ for (var i = 0; i < this.Length; i++)
+ {
+ array.SetValue(this[i], index);
+ index++;
+ }
+ }
+ }
+}
diff --git a/Dalamud/Game/ClientState/Fates/Types/Fate.cs b/Dalamud/Game/ClientState/Fates/Types/Fate.cs
new file mode 100644
index 000000000..a42e3f925
--- /dev/null
+++ b/Dalamud/Game/ClientState/Fates/Types/Fate.cs
@@ -0,0 +1,138 @@
+using System;
+
+using Dalamud.Game.ClientState.Resolvers;
+using Dalamud.Game.Text.SeStringHandling;
+using Dalamud.Memory;
+using FFXIVClientStructs.FFXIV.Client.System.String;
+
+namespace Dalamud.Game.ClientState.Fates.Types
+{
+ ///
+ /// This class represents an FFXIV Fate.
+ ///
+ public unsafe partial class Fate : IEquatable
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The address of this fate in memory.
+ /// Dalamud instance.
+ internal Fate(IntPtr address, Dalamud dalamud)
+ {
+ this.Address = address;
+ this.Dalamud = dalamud;
+ }
+
+ ///
+ /// Gets the address of this Fate in memory.
+ ///
+ public IntPtr Address { get; }
+
+ ///
+ /// Gets Dalamud itself.
+ ///
+ private protected Dalamud Dalamud { get; }
+
+ public static bool operator ==(Fate fate1, Fate fate2)
+ {
+ if (fate1 is null || fate2 is null)
+ return Equals(fate1, fate2);
+
+ return fate1.Equals(fate2);
+ }
+
+ public static bool operator !=(Fate fate1, Fate fate2) => !(fate1 == fate2);
+
+ ///
+ /// Gets a value indicating whether this Fate is still valid in memory.
+ ///
+ /// The fate to check.
+ /// True or false.
+ public static bool IsValid(Fate fate)
+ {
+ if (fate == null)
+ return false;
+
+ if (fate.Dalamud.ClientState.LocalContentId == 0)
+ return false;
+
+ return true;
+ }
+
+ ///
+ /// Gets a value indicating whether this actor is still valid in memory.
+ ///
+ /// True or false.
+ public bool IsValid() => IsValid(this);
+
+ ///
+ bool IEquatable.Equals(Fate other) => this.FateId == other?.FateId;
+
+ ///
+ public override bool Equals(object obj) => ((IEquatable)this).Equals(obj as Fate);
+
+ ///
+ public override int GetHashCode() => this.FateId.GetHashCode();
+ }
+
+ ///
+ /// This class represents an FFXIV Fate.
+ ///
+ public unsafe partial class Fate
+ {
+ ///
+ /// Gets the Fate ID of this .
+ ///
+ public ushort FateId => *(ushort*)(this.Address + FateOffsets.FateId);
+
+ ///
+ /// Gets game data linked to this Fate.
+ ///
+ public Lumina.Excel.GeneratedSheets.Fate GameData => this.Dalamud.Data.GetExcelSheet().GetRow(this.FateId);
+
+ ///
+ /// Gets the time this started.
+ ///
+ public int StartTimeEpoch => *(int*)(this.Address + FateOffsets.StartTimeEpoch);
+
+ ///
+ /// Gets how long this will run.
+ ///
+ public short Duration => *(short*)(this.Address + FateOffsets.Duration);
+
+ ///
+ /// Gets the remaining time in seconds for this .
+ ///
+ public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds();
+
+ ///
+ /// Gets the displayname of this .
+ ///
+ public SeString Name => MemoryHelper.ReadSeString((Utf8String*)(this.Address + FateOffsets.Name));
+
+ ///
+ /// Gets the state of this (Running, Ended, Failed, Preparation, WaitingForEnd).
+ ///
+ public FateState State => *(FateState*)(this.Address + FateOffsets.State);
+
+ ///
+ /// Gets the progress amount of this .
+ ///
+ public byte Progress => *(byte*)(this.Address + FateOffsets.Progress);
+
+ ///
+ /// Gets the level of this .
+ ///
+ public byte Level => *(byte*)(this.Address + FateOffsets.Level);
+
+ ///
+ /// Gets the position of this .
+ ///
+ public Position3 Position => *(Position3*)(this.Address + FateOffsets.Position);
+
+ ///
+ /// Gets the territory this is located in.
+ ///
+ public TerritoryTypeResolver TerritoryType => new(*(ushort*)(this.Address + FateOffsets.Territory), this.Dalamud);
+ }
+}
diff --git a/Dalamud/Game/ClientState/Fates/Types/FateOffsets.cs b/Dalamud/Game/ClientState/Fates/Types/FateOffsets.cs
new file mode 100644
index 000000000..73bc7a702
--- /dev/null
+++ b/Dalamud/Game/ClientState/Fates/Types/FateOffsets.cs
@@ -0,0 +1,21 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Dalamud.Game.ClientState.Fates.Types
+{
+ ///
+ /// Memory offsets for the type.
+ ///
+ [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the offset usage instead.")]
+ public static class FateOffsets
+ {
+ public const int FateId = 0x18;
+ public const int StartTimeEpoch = 0x20;
+ public const int Duration = 0x28;
+ public const int Name = 0xC0;
+ public const int State = 0x3AC;
+ public const int Progress = 0x3B8;
+ public const int Level = 0x3F9;
+ public const int Position = 0x450;
+ public const int Territory = 0x74E;
+ }
+}
diff --git a/Dalamud/Game/ClientState/Fates/Types/FateState.cs b/Dalamud/Game/ClientState/Fates/Types/FateState.cs
new file mode 100644
index 000000000..94eb00717
--- /dev/null
+++ b/Dalamud/Game/ClientState/Fates/Types/FateState.cs
@@ -0,0 +1,33 @@
+namespace Dalamud.Game.ClientState.Fates.Types
+{
+ ///
+ /// This represents the state of a single Fate.
+ ///
+ public enum FateState : byte
+ {
+ ///
+ /// The Fate is active.
+ ///
+ Running = 0x02,
+
+ ///
+ /// The Fate has ended.
+ ///
+ Ended = 0x04,
+
+ ///
+ /// The player failed the Fate.
+ ///
+ Failed = 0x05,
+
+ ///
+ /// The Fate is preparing to run.
+ ///
+ Preparation = 0x07,
+
+ ///
+ /// The Fate is preparing to end.
+ ///
+ WaitingForEnd = 0x08,
+ }
+}
diff --git a/Dalamud/Game/ClientState/GamepadState.cs b/Dalamud/Game/ClientState/GamepadState.cs
index e3ffc1cef..2d84fdb83 100644
--- a/Dalamud/Game/ClientState/GamepadState.cs
+++ b/Dalamud/Game/ClientState/GamepadState.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Dalamud.Game.ClientState.Structs;
using Dalamud.Hooking;
@@ -12,7 +12,7 @@ namespace Dalamud.Game.ClientState
///
/// Will block game's gamepad input if is set.
///
- public unsafe class GamepadState
+ public unsafe class GamepadState : IDisposable
{
private readonly Hook gamepadPoll;
@@ -29,12 +29,8 @@ namespace Dalamud.Game.ClientState
/// Resolver knowing the pointer to the GamepadPoll function.
public GamepadState(ClientStateAddressResolver resolver)
{
-#if DEBUG
- Log.Verbose("GamepadPoll address {GamepadPoll}", resolver.GamepadPoll);
-#endif
- this.gamepadPoll = new Hook(
- resolver.GamepadPoll,
- (ControllerPoll)this.GamepadPollDetour);
+ Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}");
+ this.gamepadPoll = new Hook(resolver.GamepadPoll, this.GamepadPollDetour);
}
///
diff --git a/Dalamud/Game/ClientState/JobGauges.cs b/Dalamud/Game/ClientState/JobGauges.cs
index f25f5d8ab..e0e9c0f9d 100644
--- a/Dalamud/Game/ClientState/JobGauges.cs
+++ b/Dalamud/Game/ClientState/JobGauges.cs
@@ -17,7 +17,7 @@ namespace Dalamud.Game.ClientState
{
this.Address = addressResolver;
- Log.Verbose("JobGaugeData address {JobGaugeData}", this.Address.JobGaugeData);
+ Log.Verbose($"JobGaugeData address 0x{this.Address.JobGaugeData.ToInt64():X}");
}
private ClientStateAddressResolver Address { get; }
diff --git a/Dalamud/Game/ClientState/KeyState.cs b/Dalamud/Game/ClientState/KeyState.cs
index 8bc3c200f..e17d2cf50 100644
--- a/Dalamud/Game/ClientState/KeyState.cs
+++ b/Dalamud/Game/ClientState/KeyState.cs
@@ -25,7 +25,7 @@ namespace Dalamud.Game.ClientState
{
this.bufferBase = moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardState);
- Log.Verbose($"Keyboard state buffer address {this.bufferBase}");
+ Log.Verbose($"Keyboard state buffer address 0x{this.bufferBase.ToInt64():X}");
}
///
diff --git a/Dalamud/Game/ClientState/PartyList.cs b/Dalamud/Game/ClientState/PartyList.cs
index a1db673af..2aa4b95ed 100644
--- a/Dalamud/Game/ClientState/PartyList.cs
+++ b/Dalamud/Game/ClientState/PartyList.cs
@@ -25,7 +25,7 @@ namespace Dalamud.Game.ClientState
///
/// The Dalamud instance.
/// The ClientStateAddressResolver instance.
- public PartyList(Dalamud dalamud, ClientStateAddressResolver addressResolver)
+ internal PartyList(Dalamud dalamud, ClientStateAddressResolver addressResolver)
{
this.address = addressResolver;
this.dalamud = dalamud;
diff --git a/Dalamud/Game/ClientState/Resolvers/BaseResolver{T}.cs b/Dalamud/Game/ClientState/Resolvers/BaseResolver{T}.cs
new file mode 100644
index 000000000..2ded995d7
--- /dev/null
+++ b/Dalamud/Game/ClientState/Resolvers/BaseResolver{T}.cs
@@ -0,0 +1,34 @@
+using Lumina.Excel;
+
+namespace Dalamud.Game.ClientState.Resolvers
+{
+ ///
+ /// This object represents a class or job.
+ ///
+ /// The type of Lumina sheet to resolve.
+ public class BaseResolver where T : ExcelRow
+ {
+ private readonly Dalamud dalamud;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The ID of the classJob.
+ /// The Dalamud instance.
+ internal BaseResolver(uint id, Dalamud dalamud)
+ {
+ this.dalamud = dalamud;
+ this.Id = id;
+ }
+
+ ///
+ /// Gets the ID to be resolved.
+ ///
+ public uint Id { get; }
+
+ ///
+ /// Gets GameData linked to this excel row.
+ ///
+ public T GameData => this.dalamud.Data.GetExcelSheet().GetRow(this.Id);
+ }
+}
diff --git a/Dalamud/Game/ClientState/Resolvers/ClassJobResolver.cs b/Dalamud/Game/ClientState/Resolvers/ClassJobResolver.cs
new file mode 100644
index 000000000..b9603838d
--- /dev/null
+++ b/Dalamud/Game/ClientState/Resolvers/ClassJobResolver.cs
@@ -0,0 +1,19 @@
+namespace Dalamud.Game.ClientState.Resolvers
+{
+ ///
+ /// This object represents a class or job.
+ ///
+ public class ClassJobResolver : BaseResolver
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// Set up the ClassJob resolver with the provided ID.
+ ///
+ /// The ID of the classJob.
+ /// The Dalamud instance.
+ internal ClassJobResolver(ushort id, Dalamud dalamud)
+ : base(id, dalamud)
+ {
+ }
+ }
+}
diff --git a/Dalamud/Game/ClientState/Resolvers/FateResolver.cs b/Dalamud/Game/ClientState/Resolvers/FateResolver.cs
new file mode 100644
index 000000000..15f2fce0d
--- /dev/null
+++ b/Dalamud/Game/ClientState/Resolvers/FateResolver.cs
@@ -0,0 +1,19 @@
+namespace Dalamud.Game.ClientState.Resolvers
+{
+ ///
+ /// This object represents a Fate a character can participate in.
+ ///
+ public class FateResolver : BaseResolver
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// Set up the Fate resolver with the provided ID.
+ ///
+ /// The ID of the Fate.
+ /// The Dalamud instance.
+ internal FateResolver(ushort id, Dalamud dalamud)
+ : base(id, dalamud)
+ {
+ }
+ }
+}
diff --git a/Dalamud/Game/ClientState/Resolvers/TerritoryTypeResolver.cs b/Dalamud/Game/ClientState/Resolvers/TerritoryTypeResolver.cs
new file mode 100644
index 000000000..248bf94bb
--- /dev/null
+++ b/Dalamud/Game/ClientState/Resolvers/TerritoryTypeResolver.cs
@@ -0,0 +1,19 @@
+namespace Dalamud.Game.ClientState.Resolvers
+{
+ ///
+ /// This object represents a territory a character can be in.
+ ///
+ public class TerritoryTypeResolver : BaseResolver
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// Set up the territory type resolver with the provided ID.
+ ///
+ /// The ID of the territory type.
+ /// The Dalamud instance.
+ internal TerritoryTypeResolver(ushort id, Dalamud dalamud)
+ : base(id, dalamud)
+ {
+ }
+ }
+}
diff --git a/Dalamud/Game/ClientState/Resolvers/WorldResolver.cs b/Dalamud/Game/ClientState/Resolvers/WorldResolver.cs
new file mode 100644
index 000000000..0d37e3549
--- /dev/null
+++ b/Dalamud/Game/ClientState/Resolvers/WorldResolver.cs
@@ -0,0 +1,19 @@
+namespace Dalamud.Game.ClientState.Resolvers
+{
+ ///
+ /// This object represents a world a character can reside on.
+ ///
+ public class WorldResolver : BaseResolver
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// Set up the world resolver with the provided ID.
+ ///
+ /// The ID of the world.
+ /// The Dalamud instance.
+ internal WorldResolver(ushort id, Dalamud dalamud)
+ : base(id, dalamud)
+ {
+ }
+ }
+}
diff --git a/Dalamud/Game/ClientState/Structs/Actor.cs b/Dalamud/Game/ClientState/Structs/Actor.cs
deleted file mode 100644
index e765a8c4c..000000000
--- a/Dalamud/Game/ClientState/Structs/Actor.cs
+++ /dev/null
@@ -1,289 +0,0 @@
-using System.Runtime.InteropServices;
-
-using Dalamud.Game.ClientState.Actors;
-
-namespace Dalamud.Game.ClientState.Structs
-{
- ///
- /// Native memory representation of an FFXIV actor.
- ///
- [StructLayout(LayoutKind.Explicit, Pack = 2)]
- public struct Actor
- {
- ///
- /// The actor name.
- ///
- [FieldOffset(ActorOffsets.Name)]
- [MarshalAs(UnmanagedType.ByValArray, SizeConst = 30)]
- public byte[] Name;
-
- ///
- /// The actor's internal id.
- ///
- [FieldOffset(ActorOffsets.ActorId)]
- public int ActorId;
-
- ///
- /// The actor's data id.
- ///
- [FieldOffset(ActorOffsets.DataId)]
- public int DataId;
-
- ///
- /// The actor's owner id. This is useful for pets, summons, and the like.
- ///
- [FieldOffset(ActorOffsets.OwnerId)]
- public int OwnerId;
-
- ///
- /// The type or kind of actor.
- ///
- [FieldOffset(ActorOffsets.ObjectKind)]
- public ObjectKind ObjectKind;
-
- ///
- /// The sub-type or sub-kind of actor.
- ///
- [FieldOffset(ActorOffsets.SubKind)]
- public byte SubKind;
-
- ///
- /// Whether the actor is friendly.
- ///
- [FieldOffset(ActorOffsets.IsFriendly)]
- public bool IsFriendly;
-
- ///
- /// The horizontal distance in game units from the player.
- ///
- [FieldOffset(ActorOffsets.YalmDistanceFromPlayerX)]
- public byte YalmDistanceFromPlayerX;
-
- ///
- /// The player target status.
- ///
- ///
- /// This is some kind of enum.
- ///
- [FieldOffset(ActorOffsets.PlayerTargetStatus)]
- public byte PlayerTargetStatus;
-
- ///
- /// The vertical distance in game units from the player.
- ///
- [FieldOffset(ActorOffsets.YalmDistanceFromPlayerY)]
- public byte YalmDistanceFromPlayerY;
-
- ///
- /// The (X,Z,Y) position of the actor.
- ///
- [FieldOffset(ActorOffsets.Position)]
- public Position3 Position;
-
- ///
- /// The rotation of the actor.
- ///
- ///
- /// The rotation is around the vertical axis (yaw), from -pi to pi radians.
- ///
- [FieldOffset(ActorOffsets.Rotation)]
- public float Rotation;
-
- ///
- /// The hitbox radius of the actor.
- ///
- [FieldOffset(ActorOffsets.HitboxRadius)]
- public float HitboxRadius;
-
- ///
- /// The current HP of the actor.
- ///
- [FieldOffset(ActorOffsets.CurrentHp)]
- public int CurrentHp;
-
- ///
- /// The max HP of the actor.
- ///
- [FieldOffset(ActorOffsets.MaxHp)]
- public int MaxHp;
-
- ///
- /// The current MP of the actor.
- ///
- [FieldOffset(ActorOffsets.CurrentMp)]
- public int CurrentMp;
-
- ///
- /// The max MP of the actor.
- ///
- [FieldOffset(ActorOffsets.MaxMp)]
- public short MaxMp;
-
- ///
- /// The current GP of the actor.
- ///
- [FieldOffset(ActorOffsets.CurrentGp)]
- public short CurrentGp;
-
- ///
- /// The max GP of the actor.
- ///
- [FieldOffset(ActorOffsets.MaxGp)]
- public short MaxGp;
-
- ///
- /// The current CP of the actor.
- ///
- [FieldOffset(ActorOffsets.CurrentCp)]
- public short CurrentCp;
-
- ///
- /// The max CP of the actor.
- ///
- [FieldOffset(ActorOffsets.MaxCp)]
- public short MaxCp;
-
- ///
- /// The class-job of the actor.
- ///
- [FieldOffset(ActorOffsets.ClassJob)]
- public byte ClassJob;
-
- ///
- /// The level of the actor.
- ///
- [FieldOffset(ActorOffsets.Level)]
- public byte Level;
-
- ///
- /// The (player character) actor ID being targeted by the actor.
- ///
- [FieldOffset(ActorOffsets.PlayerCharacterTargetActorId)]
- public int PlayerCharacterTargetActorId;
-
- ///
- /// The customization byte/bitfield of the actor.
- ///
- [FieldOffset(ActorOffsets.Customize)]
- [MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)]
- public byte[] Customize;
-
- // Normally pack=2 should work, but ByTVal or Injection breaks this.
- // [FieldOffset(ActorOffsets.CompanyTag)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] public string CompanyTag;
-
- ///
- /// The (battle npc) actor ID being targeted by the actor.
- ///
- [FieldOffset(ActorOffsets.BattleNpcTargetActorId)]
- public int BattleNpcTargetActorId;
-
- ///
- /// The name ID of the actor.
- ///
- [FieldOffset(ActorOffsets.NameId)]
- public int NameId;
-
- ///
- /// The current world ID of the actor.
- ///
- [FieldOffset(ActorOffsets.CurrentWorld)]
- public ushort CurrentWorld;
-
- ///
- /// The home world ID of the actor.
- ///
- [FieldOffset(ActorOffsets.HomeWorld)]
- public ushort HomeWorld;
-
- ///
- /// Whether the actor is currently casting.
- ///
- [FieldOffset(ActorOffsets.IsCasting)]
- public bool IsCasting;
-
- ///
- /// Whether the actor is currently casting (dup?).
- ///
- [FieldOffset(ActorOffsets.IsCasting2)]
- public bool IsCasting2;
-
- ///
- /// The spell action ID currently being cast by the actor.
- ///
- [FieldOffset(ActorOffsets.CurrentCastSpellActionId)]
- public uint CurrentCastSpellActionId;
-
- ///
- /// The actor ID of the target currently being cast at by the actor.
- ///
- [FieldOffset(ActorOffsets.CurrentCastTargetActorId)]
- public uint CurrentCastTargetActorId;
-
- ///
- /// The current casting time of the spell being cast by the actor.
- ///
- [FieldOffset(ActorOffsets.CurrentCastTime)]
- public float CurrentCastTime;
-
- ///
- /// The total casting time of the spell being cast by the actor.
- ///
- [FieldOffset(ActorOffsets.TotalCastTime)]
- public float TotalCastTime;
-
- ///
- /// The array of status effects that the actor is currently affected by.
- ///
- [FieldOffset(ActorOffsets.UIStatusEffects)]
- [MarshalAs(UnmanagedType.ByValArray, SizeConst = 30)]
- public StatusEffect[] UIStatusEffects;
- }
-
- ///
- /// Memory offsets for the type.
- ///
- public static class ActorOffsets
- {
- // Reference https://github.com/FFXIVAPP/sharlayan-resources/blob/master/structures/5.4/x64.json for more
- public const int Name = 48; // 0x0030
- public const int ActorId = 116; // 0x0074
- // public const int ??? = 120; // 0x0078 NPCID1
- public const int DataId = 128; // 0x0080 NPCID2
- public const int OwnerId = 132; // 0x0084
- public const int ObjectKind = 140; // 0x008C Type
- public const int SubKind = 141; // 0x008D
- public const int IsFriendly = 142; // 0x008E
- public const int YalmDistanceFromPlayerX = 144; // 0x0090
- public const int PlayerTargetStatus = 145; // 0x0091
- public const int YalmDistanceFromPlayerY = 146; // 0x0092 Distance
- public const int Position = 160; // 0x00A0 (X,Z,Y)
- public const int Rotation = 176; // 0x00B0 Heading
- public const int HitboxRadius = 192; // 0x00C0
- public const int CurrentHp = 452; // 0x01C4 HPCurrent
- public const int MaxHp = 456; // 0x01C8 HPMax
- public const int CurrentMp = 460; // 0x01CC MPCurrent
- public const int MaxMp = 464; // 0x01D0 MPMax
- public const int CurrentGp = 468; // 0x01D4 GPCurrent
- public const int MaxGp = 470; // 0x01D6 GPMax
- public const int CurrentCp = 472; // 0x01D8 CPCurrent
- public const int MaxCp = 474; // 0x01DA CPMax
- public const int ClassJob = 482; // 0x01E2 Job
- public const int Level = 483; // 0x01E3 Level
- public const int PlayerCharacterTargetActorId = 560; // 0x01F0 TargetID
-
- public const int Customize = 0x1898; // Needs verification
- public const int CompanyTag = 0x18B2;
- public const int BattleNpcTargetActorId = 0x18D8; // Needs verification
- public const int NameId = 0x1940; // Needs verification
- public const int CurrentWorld = 0x195C;
- public const int HomeWorld = 0x195E;
-
- public const int IsCasting = 0x1B80;
- public const int IsCasting2 = 0x1B82;
- public const int CurrentCastSpellActionId = 0x1B84;
- public const int CurrentCastTargetActorId = 0x1B90;
- public const int CurrentCastTime = 0x1BB4;
- public const int TotalCastTime = 0x1BB8;
- public const int UIStatusEffects = 0x19F8;
- }
-}
diff --git a/Dalamud/Game/ClientState/Structs/GamepadInput.cs b/Dalamud/Game/ClientState/Structs/GamepadInput.cs
index ce7440b61..df80b0ef4 100644
--- a/Dalamud/Game/ClientState/Structs/GamepadInput.cs
+++ b/Dalamud/Game/ClientState/Structs/GamepadInput.cs
@@ -1,4 +1,4 @@
-using System.Runtime.InteropServices;
+using System.Runtime.InteropServices;
namespace Dalamud.Game.ClientState.Structs
{
@@ -40,25 +40,37 @@ namespace Dalamud.Game.ClientState.Structs
///
/// Raw input, set the whole time while a button is held. See for the mapping.
///
+ ///
+ /// This is a bitfield.
+ ///
[FieldOffset(0x98)]
- public ushort ButtonsRaw; // bitfield
+ public ushort ButtonsRaw;
///
/// Button pressed, set once when the button is pressed. See for the mapping.
///
+ ///
+ /// This is a bitfield.
+ ///
[FieldOffset(0x9C)]
- public ushort ButtonsPressed; // bitfield
+ public ushort ButtonsPressed;
///
/// Button released input, set once right after the button is not hold anymore. See for the mapping.
///
+ ///
+ /// This is a bitfield.
+ ///
[FieldOffset(0xA0)]
- public ushort ButtonsReleased; // bitfield
+ public ushort ButtonsReleased;
///
/// Repeatedly emits the held button input in fixed intervals. See for the mapping.
///
+ ///
+ /// This is a bitfield.
+ ///
[FieldOffset(0xA4)]
- public ushort ButtonsRepeat; // bitfield
+ public ushort ButtonsRepeat;
}
}
diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/BLMGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/BLMGauge.cs
index 694a0d981..e4125745b 100644
--- a/Dalamud/Game/ClientState/Structs/JobGauge/BLMGauge.cs
+++ b/Dalamud/Game/ClientState/Structs/JobGauge/BLMGauge.cs
@@ -8,35 +8,43 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
[StructLayout(LayoutKind.Explicit)]
public struct BLMGauge
{
- ///
- /// Gets the time until the next Polyglot stack in milliseconds.
- ///
[FieldOffset(0)]
- public short TimeUntilNextPolyglot; // enochian timer
+ private short timeUntilNextPolyglot; // enochian timer
- ///
- /// Gets the time remaining for Astral Fire or Umbral Ice in milliseconds.
- ///
[FieldOffset(2)]
- public short ElementTimeRemaining; // umbral ice and astral fire timer
+ private short elementTimeRemaining; // umbral ice and astral fire timer
[FieldOffset(4)]
private byte elementStance; // umbral ice or astral fire
- ///
- /// Gets the number of Umbral Hearts remaining.
- ///
[FieldOffset(5)]
- public byte NumUmbralHearts;
+ private byte numUmbralHearts;
+
+ [FieldOffset(6)]
+ private byte numPolyglotStacks;
+
+ [FieldOffset(7)]
+ private byte enochianState;
+
+ ///
+ /// Gets the time until the next Polyglot stack in milliseconds.
+ ///
+ public short TimeUntilNextPolyglot => this.timeUntilNextPolyglot;
+
+ ///
+ /// Gets the time remaining for Astral Fire or Umbral Ice in milliseconds.
+ ///
+ public short ElementTimeRemaining => this.elementTimeRemaining;
///
/// Gets the number of Polyglot stacks remaining.
///
- [FieldOffset(6)]
- public byte NumPolyglotStacks;
+ public byte NumPolyglotStacks => this.numPolyglotStacks;
- [FieldOffset(7)]
- private byte enochianState;
+ ///
+ /// Gets the number of Umbral Hearts remaining.
+ ///
+ public byte NumUmbralHearts => this.numUmbralHearts;
///
/// Gets if the player is in Umbral Ice.
diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/BRDGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/BRDGauge.cs
index 3fe2b5dee..98590805c 100644
--- a/Dalamud/Game/ClientState/Structs/JobGauge/BRDGauge.cs
+++ b/Dalamud/Game/ClientState/Structs/JobGauge/BRDGauge.cs
@@ -8,28 +8,36 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
[StructLayout(LayoutKind.Explicit)]
public struct BRDGauge
{
+ [FieldOffset(0)]
+ private short songTimer;
+
+ [FieldOffset(2)]
+ private byte numSongStacks;
+
+ [FieldOffset(3)]
+ private byte soulVoiceValue;
+
+ [FieldOffset(4)]
+ private CurrentSong activeSong;
+
///
/// Gets the current song timer in milliseconds.
///
- [FieldOffset(0)]
- public short SongTimer;
+ public short SongTimer => this.songTimer;
///
/// Gets the number of stacks for the current song.
///
- [FieldOffset(2)]
- public byte NumSongStacks;
+ public byte NumSongStacks => this.numSongStacks;
///
/// Gets the amount of Soul Voice accumulated.
///
- [FieldOffset(3)]
- public byte SoulVoiceValue;
+ public byte SoulVoiceValue => this.soulVoiceValue;
///
/// Gets the type of song that is active.
///
- [FieldOffset(4)]
- public CurrentSong ActiveSong;
+ public CurrentSong ActiveSong => this.activeSong;
}
}
diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/DNCGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/DNCGauge.cs
index 2143aa99f..e2f4f6544 100644
--- a/Dalamud/Game/ClientState/Structs/JobGauge/DNCGauge.cs
+++ b/Dalamud/Game/ClientState/Structs/JobGauge/DNCGauge.cs
@@ -8,30 +8,37 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
[StructLayout(LayoutKind.Explicit)]
public unsafe struct DNCGauge
{
- ///
- /// Gets the number of feathers available.
- ///
[FieldOffset(0)]
- public byte NumFeathers;
+ private byte numFeathers;
- ///
- /// Gets the amount of Espirit available.
- ///
[FieldOffset(1)]
- public byte Esprit;
+ private byte esprit;
[FieldOffset(2)]
private fixed byte stepOrder[4];
+ [FieldOffset(6)]
+ private byte numCompleteSteps;
+
+ ///
+ /// Gets the number of feathers available.
+ ///
+ public byte NumFeathers => this.numFeathers;
+
+ ///
+ /// Gets the amount of Espirit available.
+ ///
+ public byte Esprit => this.esprit;
+
///
/// Gets the number of steps completed for the current dance.
///
- [FieldOffset(6)]
- public byte NumCompleteSteps;
+ public byte NumCompleteSteps => this.numCompleteSteps;
///
/// Gets the next step in the current dance.
///
+ /// The next dance step action ID.
public ulong NextStep() => (ulong)(15999 + this.stepOrder[this.NumCompleteSteps] - 1);
///
diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/DRGGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/DRGGauge.cs
index 101c483b0..2f4c4ef16 100644
--- a/Dalamud/Game/ClientState/Structs/JobGauge/DRGGauge.cs
+++ b/Dalamud/Game/ClientState/Structs/JobGauge/DRGGauge.cs
@@ -8,22 +8,28 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
[StructLayout(LayoutKind.Explicit)]
public struct DRGGauge
{
+ [FieldOffset(0)]
+ private short botdTimer;
+
+ [FieldOffset(2)]
+ private BOTDState botdState;
+
+ [FieldOffset(3)]
+ private byte eyeCount;
+
///
/// Gets the time remaining for Blood of the Dragon in milliseconds.
///
- [FieldOffset(0)]
- public short BOTDTimer;
+ public short BOTDTimer => this.botdTimer;
///
/// Gets the current state of Blood of the Dragon.
///
- [FieldOffset(2)]
- public BOTDState BOTDState;
+ public BOTDState BOTDState => this.botdState;
///
/// Gets the count of eyes opened during Blood of the Dragon.
///
- [FieldOffset(3)]
- public byte EyeCount;
+ public byte EyeCount => this.eyeCount;
}
}
diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/DRKGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/DRKGauge.cs
index 9c5306764..97ab73161 100644
--- a/Dalamud/Game/ClientState/Structs/JobGauge/DRKGauge.cs
+++ b/Dalamud/Game/ClientState/Structs/JobGauge/DRKGauge.cs
@@ -8,26 +8,32 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
[StructLayout(LayoutKind.Explicit)]
public struct DRKGauge
{
- ///
- /// Gets the amount of blood accumulated.
- ///
[FieldOffset(0)]
- public byte Blood;
+ private byte blood;
- ///
- /// Gets the Darkside time remaining in milliseconds.
- ///
[FieldOffset(2)]
- public ushort DarksideTimeRemaining;
+ private ushort darksideTimeRemaining;
[FieldOffset(4)]
private byte darkArtsState;
+ [FieldOffset(6)]
+ private ushort shadowTimeRemaining;
+
+ ///
+ /// Gets the amount of blood accumulated.
+ ///
+ public byte Blood => this.blood;
+
+ ///
+ /// Gets the Darkside time remaining in milliseconds.
+ ///
+ public ushort DarksideTimeRemaining => this.darksideTimeRemaining;
+
///
/// Gets the Shadow time remaining in milliseconds.
///
- [FieldOffset(6)]
- public ushort ShadowTimeRemaining;
+ public ushort ShadowTimeRemaining => this.shadowTimeRemaining;
///
/// Gets if the player has Dark Arts or not.
diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/GNBGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/GNBGauge.cs
index c1f7e496b..d42bd9fa1 100644
--- a/Dalamud/Game/ClientState/Structs/JobGauge/GNBGauge.cs
+++ b/Dalamud/Game/ClientState/Structs/JobGauge/GNBGauge.cs
@@ -8,22 +8,28 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
[StructLayout(LayoutKind.Explicit)]
public struct GNBGauge
{
+ [FieldOffset(0)]
+ private byte numAmmo;
+
+ [FieldOffset(2)]
+ private short maxTimerDuration;
+
+ [FieldOffset(4)]
+ private byte ammoComboStepNumber;
+
///
/// Gets the amount of ammo available.
///
- [FieldOffset(0)]
- public byte NumAmmo;
+ public byte NumAmmo => this.numAmmo;
///
/// Gets the max combo time of the Gnashing Fang combo.
///
- [FieldOffset(2)]
- public short MaxTimerDuration;
+ public short MaxTimerDuration => this.maxTimerDuration;
///
/// Gets the current step of the Gnashing Fang combo.
///
- [FieldOffset(4)]
- public byte AmmoComboStepNumber;
+ public byte AmmoComboStepNumber => this.ammoComboStepNumber;
}
}
diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/MCHGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/MCHGauge.cs
index 18f684c21..1444beac0 100644
--- a/Dalamud/Game/ClientState/Structs/JobGauge/MCHGauge.cs
+++ b/Dalamud/Game/ClientState/Structs/JobGauge/MCHGauge.cs
@@ -8,38 +8,48 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
[StructLayout(LayoutKind.Explicit)]
public struct MCHGauge
{
+ [FieldOffset(0)]
+ private short overheatTimeRemaining;
+
+ [FieldOffset(2)]
+ private short robotTimeRemaining;
+
+ [FieldOffset(4)]
+ private byte heat;
+
+ [FieldOffset(5)]
+ private byte battery;
+
+ [FieldOffset(6)]
+ private byte lastRobotBatteryPower;
+
+ [FieldOffset(7)]
+ private byte timerActive;
+
///
/// Gets the time time remaining for Overheat in milliseconds.
///
- [FieldOffset(0)]
- public short OverheatTimeRemaining;
+ public short OverheatTimeRemaining => this.overheatTimeRemaining;
///
/// Gets the time remaining for the Rook or Queen in milliseconds.
///
- [FieldOffset(2)]
- public short RobotTimeRemaining;
+ public short RobotTimeRemaining => this.robotTimeRemaining;
///
/// Gets the current Heat level.
///
- [FieldOffset(4)]
- public byte Heat;
+ public byte Heat => this.heat;
///
/// Gets the current Battery level.
///
- [FieldOffset(5)]
- public byte Battery;
+ public byte Battery => this.battery;
///
/// Gets the battery level of the last Robot.
///
- [FieldOffset(6)]
- public byte LastRobotBatteryPower;
-
- [FieldOffset(7)]
- private byte timerActive;
+ public byte LastRobotBatteryPower => this.lastRobotBatteryPower;
///
/// Gets if the player is currently Overheated.
diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/MNKGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/MNKGauge.cs
index 177b077fc..ef1520d9e 100644
--- a/Dalamud/Game/ClientState/Structs/JobGauge/MNKGauge.cs
+++ b/Dalamud/Game/ClientState/Structs/JobGauge/MNKGauge.cs
@@ -1,4 +1,3 @@
-using System;
using System.Runtime.InteropServices;
namespace Dalamud.Game.ClientState.Structs.JobGauge
@@ -9,35 +8,12 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
[StructLayout(LayoutKind.Explicit)]
public struct MNKGauge
{
+ [FieldOffset(0)]
+ private byte numChakra;
+
///
/// Gets the number of Chakra available.
///
- [FieldOffset(0)]
- public byte NumChakra;
-
- ///
- /// Gets the Greased Lightning timer in milliseconds.
- ///
- [Obsolete("GL has been removed from the game")]
- [FieldOffset(0)]
- public byte GLTimer;
-
- ///
- /// Gets the amount of Greased Lightning stacks.
- ///
- [Obsolete("GL has been removed from the game")]
- [FieldOffset(2)]
- public byte NumGLStacks;
-
- [Obsolete("GL has been removed from the game")]
- [FieldOffset(4)]
- private byte glTimerFreezeState;
-
- ///
- /// Gets if the Greased Lightning timer has been frozen.
- ///
- /// >true or false.
- [Obsolete("GL has been removed from the game")]
- public bool IsGLTimerFroze() => false;
+ public byte NumChakra => this.numChakra;
}
}
diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/NINGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/NINGauge.cs
index 13eaec09d..f2019caf8 100644
--- a/Dalamud/Game/ClientState/Structs/JobGauge/NINGauge.cs
+++ b/Dalamud/Game/ClientState/Structs/JobGauge/NINGauge.cs
@@ -9,30 +9,28 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
[StructLayout(LayoutKind.Explicit)]
public struct NINGauge
{
+ [FieldOffset(0)]
+ private int hutonTimeLeft;
+
+ [FieldOffset(4)]
+ private byte ninki;
+
+ [FieldOffset(5)]
+ private byte numHutonManualCasts;
+
///
/// Gets the time left on Huton in milliseconds.
///
- // TODO: Probably a short, confirm.
- [FieldOffset(0)]
- public int HutonTimeLeft;
+ public int HutonTimeLeft => this.hutonTimeLeft;
///
/// Gets the amount of Ninki available.
///
- [FieldOffset(4)]
- public byte Ninki;
-
- ///
- /// Obsolete.
- ///
- [Obsolete("Does not appear to be used")]
- [FieldOffset(4)]
- public byte TCJMudrasUsed;
+ public byte Ninki => this.ninki;
///
/// Gets the number of times Huton has been cast manually.
///
- [FieldOffset(5)]
- public byte NumHutonManualCasts;
+ public byte NumHutonManualCasts => this.numHutonManualCasts;
}
}
diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/PLDGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/PLDGauge.cs
index d3eae81f3..545ae695a 100644
--- a/Dalamud/Game/ClientState/Structs/JobGauge/PLDGauge.cs
+++ b/Dalamud/Game/ClientState/Structs/JobGauge/PLDGauge.cs
@@ -8,10 +8,12 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
[StructLayout(LayoutKind.Explicit)]
public struct PLDGauge
{
+ [FieldOffset(0)]
+ private byte gaugeAmount;
+
///
/// Gets the current level of the Oath gauge.
///
- [FieldOffset(0)]
- public byte GaugeAmount;
+ public byte GaugeAmount => this.gaugeAmount;
}
}
diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/RDMGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/RDMGauge.cs
index f72d61d13..8d0ec38e1 100644
--- a/Dalamud/Game/ClientState/Structs/JobGauge/RDMGauge.cs
+++ b/Dalamud/Game/ClientState/Structs/JobGauge/RDMGauge.cs
@@ -8,16 +8,20 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
[StructLayout(LayoutKind.Explicit)]
public struct RDMGauge
{
+ [FieldOffset(0)]
+ private byte whiteGauge;
+
+ [FieldOffset(1)]
+ private byte blackGauge;
+
///
/// Gets the level of the White gauge.
///
- [FieldOffset(0)]
- public byte WhiteGauge;
+ public byte WhiteGauge => this.whiteGauge;
///
/// Gets the level of the Black gauge.
///
- [FieldOffset(1)]
- public byte BlackGauge;
+ public byte BlackGauge => this.blackGauge;
}
}
diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/SAMGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/SAMGauge.cs
index d0796e5c9..139a93265 100644
--- a/Dalamud/Game/ClientState/Structs/JobGauge/SAMGauge.cs
+++ b/Dalamud/Game/ClientState/Structs/JobGauge/SAMGauge.cs
@@ -8,23 +8,29 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
[StructLayout(LayoutKind.Explicit)]
public struct SAMGauge
{
+ [FieldOffset(3)]
+ private byte kenki;
+
+ [FieldOffset(4)]
+ private byte meditationStacks;
+
+ [FieldOffset(5)]
+ private Sen sen;
+
///
/// Gets the current amount of Kenki available.
///
- [FieldOffset(3)]
- public byte Kenki;
+ public byte Kenki => this.kenki;
///
/// Gets the amount of Meditation stacks.
///
- [FieldOffset(4)]
- public byte MeditationStacks;
+ public byte MeditationStacks => this.meditationStacks;
///
/// Gets the active Sen.
///
- [FieldOffset(5)]
- public Sen Sen;
+ public Sen Sen => this.sen;
///
/// Gets if the Setsu Sen is active.
diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/SCHGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/SCHGauge.cs
index 5dc99cf15..66347b62e 100644
--- a/Dalamud/Game/ClientState/Structs/JobGauge/SCHGauge.cs
+++ b/Dalamud/Game/ClientState/Structs/JobGauge/SCHGauge.cs
@@ -8,28 +8,36 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
[StructLayout(LayoutKind.Explicit)]
public struct SCHGauge
{
+ [FieldOffset(2)]
+ private byte numAetherflowStacks;
+
+ [FieldOffset(3)]
+ private byte fairyGaugeAmount;
+
+ [FieldOffset(4)]
+ private short seraphTimer;
+
+ [FieldOffset(6)]
+ private DismissedFairy dismissedFairy;
+
///
/// Gets the amount of Aetherflow stacks available.
///
- [FieldOffset(2)]
- public byte NumAetherflowStacks;
+ public byte NumAetherflowStacks => this.numAetherflowStacks;
///
/// Gets the current level of the Fairy Gauge.
///
- [FieldOffset(3)]
- public byte FairyGaugeAmount;
+ public byte FairyGaugeAmount => this.fairyGaugeAmount;
///
/// Gets the Seraph time remaining in milliseconds.
///
- [FieldOffset(4)]
- public short SeraphTimer;
+ public short SeraphTimer => this.seraphTimer;
///
/// Gets the last dismissed fairy.
///
- [FieldOffset(6)]
- public DismissedFairy DismissedFairy;
+ public DismissedFairy DismissedFairy => this.dismissedFairy;
}
}
diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/SMNGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/SMNGauge.cs
index a5e9cc219..92cbcc19c 100644
--- a/Dalamud/Game/ClientState/Structs/JobGauge/SMNGauge.cs
+++ b/Dalamud/Game/ClientState/Structs/JobGauge/SMNGauge.cs
@@ -8,30 +8,38 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
[StructLayout(LayoutKind.Explicit)]
public struct SMNGauge
{
+ [FieldOffset(0)]
+ private short timerRemaining;
+
+ [FieldOffset(2)]
+ private SummonPet returnSummon;
+
+ [FieldOffset(3)]
+ private PetGlam returnSummonGlam;
+
+ [FieldOffset(4)]
+ private byte numStacks;
+
///
/// Gets the time remaining for the current summon.
///
- [FieldOffset(0)]
- public short TimerRemaining;
+ public short TimerRemaining => this.timerRemaining;
///
/// Gets the summon that will return after the current summon expires.
///
- [FieldOffset(2)]
- public SummonPet ReturnSummon;
+ public SummonPet ReturnSummon => this.returnSummon;
///
/// Gets the summon glam for the .
///
- [FieldOffset(3)]
- public PetGlam ReturnSummonGlam;
+ public PetGlam ReturnSummonGlam => this.returnSummonGlam;
///
/// Gets the current stacks.
/// Use the summon accessors instead.
///
- [FieldOffset(4)]
- public byte NumStacks;
+ public byte NumStacks => this.numStacks;
///
/// Gets if Phoenix is ready to be summoned.
diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/WARGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/WARGauge.cs
index 7747b9cea..0d070d68e 100644
--- a/Dalamud/Game/ClientState/Structs/JobGauge/WARGauge.cs
+++ b/Dalamud/Game/ClientState/Structs/JobGauge/WARGauge.cs
@@ -8,10 +8,12 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
[StructLayout(LayoutKind.Explicit)]
public struct WARGauge
{
+ [FieldOffset(0)]
+ private byte beastGaugeAmount;
+
///
/// Gets the amount of wrath in the Beast gauge.
///
- [FieldOffset(0)]
- public byte BeastGaugeAmount;
+ public byte BeastGaugeAmount => this.beastGaugeAmount;
}
}
diff --git a/Dalamud/Game/ClientState/Structs/JobGauge/WHMGauge.cs b/Dalamud/Game/ClientState/Structs/JobGauge/WHMGauge.cs
index 0ea51470e..496a39831 100644
--- a/Dalamud/Game/ClientState/Structs/JobGauge/WHMGauge.cs
+++ b/Dalamud/Game/ClientState/Structs/JobGauge/WHMGauge.cs
@@ -8,22 +8,28 @@ namespace Dalamud.Game.ClientState.Structs.JobGauge
[StructLayout(LayoutKind.Explicit)]
public struct WHMGauge
{
+ [FieldOffset(2)]
+ private short lilyTimer;
+
+ [FieldOffset(4)]
+ private byte numLilies;
+
+ [FieldOffset(5)]
+ private byte numBloodLily;
+
///
/// Gets the time to next lily in milliseconds.
///
- [FieldOffset(2)]
- public short LilyTimer;
+ public short LilyTimer => this.lilyTimer;
///
/// Gets the number of Lilies.
///
- [FieldOffset(4)]
- public byte NumLilies;
+ public byte NumLilies => this.numLilies;
///
/// Gets the number of times the blood lily has been nourished.
///
- [FieldOffset(5)]
- public byte NumBloodLily;
+ public byte NumBloodLily => this.numBloodLily;
}
}
diff --git a/Dalamud/Game/ClientState/Structs/PartyMember.cs b/Dalamud/Game/ClientState/Structs/PartyMember.cs
index 6acff52e7..926730e54 100644
--- a/Dalamud/Game/ClientState/Structs/PartyMember.cs
+++ b/Dalamud/Game/ClientState/Structs/PartyMember.cs
@@ -1,7 +1,7 @@
using System;
using System.Runtime.InteropServices;
-using Dalamud.Game.ClientState.Actors;
+using Dalamud.Game.ClientState.Actors.Types;
namespace Dalamud.Game.ClientState.Structs
{
diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs
index 9d7dac9cd..6c31e2444 100644
--- a/Dalamud/Game/Command/CommandManager.cs
+++ b/Dalamud/Game/Command/CommandManager.cs
@@ -27,7 +27,7 @@ namespace Dalamud.Game.Command
///
/// The Dalamud instance.
/// The client language requested.
- public CommandManager(Dalamud dalamud, ClientLanguage language)
+ internal CommandManager(Dalamud dalamud, ClientLanguage language)
{
this.dalamud = dalamud;
@@ -128,7 +128,8 @@ namespace Dalamud.Game.Command
/// If adding was successful.
public bool AddHandler(string command, CommandInfo info)
{
- if (info == null) throw new ArgumentNullException(nameof(info), "Command handler is null.");
+ if (info == null)
+ throw new ArgumentNullException(nameof(info), "Command handler is null.");
try
{
diff --git a/Dalamud/Game/GameVersion.cs b/Dalamud/Game/GameVersion.cs
new file mode 100644
index 000000000..8ef76eabe
--- /dev/null
+++ b/Dalamud/Game/GameVersion.cs
@@ -0,0 +1,383 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+using Newtonsoft.Json;
+
+namespace Dalamud.Game
+{
+ ///
+ /// A GameVersion object contains give hierarchical numeric components: year, month,
+ /// day, major and minor. All components may be unspecified, which is represented
+ /// internally as a -1. By definition, an unspecified component matches anything
+ /// (both unspecified and specified), and an unspecified component is "less than" any
+ /// specified component. It will also equal the string "any" if all components are
+ /// unspecified. The value can be retrieved from the ffxivgame.ver file in your game
+ /// installation directory.
+ ///
+ [Serializable]
+ public sealed class GameVersion : ICloneable, IComparable, IComparable, IEquatable
+ {
+ private static readonly GameVersion AnyVersion = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Version string to parse.
+ [JsonConstructor]
+ public GameVersion(string version)
+ {
+ var ver = Parse(version);
+ this.Year = ver.Year;
+ this.Month = ver.Month;
+ this.Day = ver.Day;
+ this.Major = ver.Major;
+ this.Minor = ver.Minor;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The year.
+ /// The month.
+ /// The day.
+ /// The major version.
+ /// The minor version.
+ public GameVersion(int year, int month, int day, int major, int minor)
+ {
+ if ((this.Year = year) < 0)
+ throw new ArgumentOutOfRangeException(nameof(year));
+
+ if ((this.Month = month) < 0)
+ throw new ArgumentOutOfRangeException(nameof(month));
+
+ if ((this.Day = day) < 0)
+ throw new ArgumentOutOfRangeException(nameof(day));
+
+ if ((this.Major = major) < 0)
+ throw new ArgumentOutOfRangeException(nameof(major));
+
+ if ((this.Minor = minor) < 0)
+ throw new ArgumentOutOfRangeException(nameof(minor));
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The year.
+ /// The month.
+ /// The day.
+ /// The major version.
+ public GameVersion(int year, int month, int day, int major)
+ {
+ if ((this.Year = year) < 0)
+ throw new ArgumentOutOfRangeException(nameof(year));
+
+ if ((this.Month = month) < 0)
+ throw new ArgumentOutOfRangeException(nameof(month));
+
+ if ((this.Day = day) < 0)
+ throw new ArgumentOutOfRangeException(nameof(day));
+
+ if ((this.Major = major) < 0)
+ throw new ArgumentOutOfRangeException(nameof(major));
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The year.
+ /// The month.
+ /// The day.
+ public GameVersion(int year, int month, int day)
+ {
+ if ((this.Year = year) < 0)
+ throw new ArgumentOutOfRangeException(nameof(year));
+
+ if ((this.Month = month) < 0)
+ throw new ArgumentOutOfRangeException(nameof(month));
+
+ if ((this.Day = day) < 0)
+ throw new ArgumentOutOfRangeException(nameof(day));
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The year.
+ /// The month.
+ public GameVersion(int year, int month)
+ {
+ if ((this.Year = year) < 0)
+ throw new ArgumentOutOfRangeException(nameof(year));
+
+ if ((this.Month = month) < 0)
+ throw new ArgumentOutOfRangeException(nameof(month));
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The year.
+ public GameVersion(int year)
+ {
+ if ((this.Year = year) < 0)
+ throw new ArgumentOutOfRangeException(nameof(year));
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public GameVersion()
+ {
+ }
+
+ ///
+ /// Gets the default "any" game version.
+ ///
+ public static GameVersion Any => AnyVersion;
+
+ ///
+ /// Gets the year component.
+ ///
+ public int Year { get; } = -1;
+
+ ///
+ /// Gets the month component.
+ ///
+ public int Month { get; } = -1;
+
+ ///
+ /// Gets the day component.
+ ///
+ public int Day { get; } = -1;
+
+ ///
+ /// Gets the major version component.
+ ///
+ public int Major { get; } = -1;
+
+ ///
+ /// Gets the minor version component.
+ ///
+ public int Minor { get; } = -1;
+
+ public static implicit operator GameVersion(string ver)
+ {
+ return Parse(ver);
+ }
+
+ public static bool operator ==(GameVersion v1, GameVersion v2)
+ {
+ if (v1 is null)
+ {
+ return v2 is null;
+ }
+
+ return v1.Equals(v2);
+ }
+
+ public static bool operator !=(GameVersion v1, GameVersion v2)
+ {
+ return !(v1 == v2);
+ }
+
+ public static bool operator <(GameVersion v1, GameVersion v2)
+ {
+ if (v1 is null)
+ throw new ArgumentNullException(nameof(v1));
+
+ return v1.CompareTo(v2) < 0;
+ }
+
+ public static bool operator <=(GameVersion v1, GameVersion v2)
+ {
+ if (v1 is null)
+ throw new ArgumentNullException(nameof(v1));
+
+ return v1.CompareTo(v2) <= 0;
+ }
+
+ public static bool operator >(GameVersion v1, GameVersion v2)
+ {
+ return v2 < v1;
+ }
+
+ public static bool operator >=(GameVersion v1, GameVersion v2)
+ {
+ return v2 <= v1;
+ }
+
+ ///
+ /// Parse a version string. YYYY.MM.DD.majr.minr or "any".
+ ///
+ /// Input to parse.
+ /// GameVersion object.
+ public static GameVersion Parse(string input)
+ {
+ if (input == null)
+ throw new ArgumentNullException(nameof(input));
+
+ if (input.ToLower(CultureInfo.InvariantCulture) == "any")
+ return new GameVersion();
+
+ var parts = input.Split('.');
+ var tplParts = parts.Select(p =>
+ {
+ var result = int.TryParse(p, out var value);
+ return (result, value);
+ }).ToArray();
+
+ if (tplParts.Any(t => !t.result))
+ throw new FormatException("Bad formatting");
+
+ var intParts = tplParts.Select(t => t.value).ToArray();
+ var len = intParts.Length;
+
+ if (len == 1)
+ return new GameVersion(intParts[0]);
+ else if (len == 2)
+ return new GameVersion(intParts[0], intParts[1]);
+ else if (len == 3)
+ return new GameVersion(intParts[0], intParts[1], intParts[2]);
+ else if (len == 4)
+ return new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3]);
+ else if (len == 5)
+ return new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3], intParts[4]);
+ else
+ throw new ArgumentException("Too many parts");
+ }
+
+ ///
+ /// Try to parse a version string. YYYY.MM.DD.majr.minr or "any".
+ ///
+ /// Input to parse.
+ /// GameVersion object.
+ /// Success or failure.
+ public static bool TryParse(string input, out GameVersion result)
+ {
+ try
+ {
+ result = Parse(input);
+ return true;
+ }
+ catch
+ {
+ result = null;
+ return false;
+ }
+ }
+
+ ///
+ public object Clone() => new GameVersion(this.Year, this.Month, this.Day, this.Major, this.Minor);
+
+ ///
+ public int CompareTo(object obj)
+ {
+ if (obj == null)
+ return 1;
+
+ if (obj is GameVersion value)
+ {
+ return this.CompareTo(value);
+ }
+ else
+ {
+ throw new ArgumentException("Argument must be a GameVersion");
+ }
+ }
+
+ ///
+ public int CompareTo(GameVersion value)
+ {
+ if (value == null)
+ return 1;
+
+ if (this == value)
+ return 0;
+
+ if (this == AnyVersion)
+ return 1;
+
+ if (value == AnyVersion)
+ return -1;
+
+ if (this.Year != value.Year)
+ return this.Year > value.Year ? 1 : -1;
+
+ if (this.Month != value.Month)
+ return this.Month > value.Month ? 1 : -1;
+
+ if (this.Day != value.Day)
+ return this.Day > value.Day ? 1 : -1;
+
+ if (this.Major != value.Major)
+ return this.Major > value.Major ? 1 : -1;
+
+ if (this.Minor != value.Minor)
+ return this.Minor > value.Minor ? 1 : -1;
+
+ return 0;
+ }
+
+ ///
+ public override bool Equals(object obj)
+ {
+ if (obj is not GameVersion value)
+ return false;
+
+ return this.Equals(value);
+ }
+
+ ///
+ public bool Equals(GameVersion value)
+ {
+ if (value == null)
+ {
+ return false;
+ }
+
+ return
+ (this.Year == value.Year) &&
+ (this.Month == value.Month) &&
+ (this.Day == value.Day) &&
+ (this.Major == value.Major) &&
+ (this.Minor == value.Minor);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ var accumulator = 0;
+
+ // This might be horribly wrong, but it isn't used heavily.
+ accumulator |= this.Year.GetHashCode();
+ accumulator |= this.Month.GetHashCode();
+ accumulator |= this.Day.GetHashCode();
+ accumulator |= this.Major.GetHashCode();
+ accumulator |= this.Minor.GetHashCode();
+
+ return accumulator;
+ }
+
+ ///
+ public override string ToString()
+ {
+ if (this.Year == -1 &&
+ this.Month == -1 &&
+ this.Day == -1 &&
+ this.Major == -1 &&
+ this.Minor == -1)
+ return "any";
+
+ return new StringBuilder()
+ .Append(string.Format("{0:D4}.", this.Year == -1 ? 0 : this.Year))
+ .Append(string.Format("{0:D2}.", this.Month == -1 ? 0 : this.Month))
+ .Append(string.Format("{0:D2}.", this.Day == -1 ? 0 : this.Day))
+ .Append(string.Format("{0:D4}.", this.Major == -1 ? 0 : this.Major))
+ .Append(string.Format("{0:D4}", this.Minor == -1 ? 0 : this.Minor))
+ .ToString();
+ }
+ }
+}
diff --git a/Dalamud/Game/GameVersionConverter.cs b/Dalamud/Game/GameVersionConverter.cs
new file mode 100644
index 000000000..9058e8be9
--- /dev/null
+++ b/Dalamud/Game/GameVersionConverter.cs
@@ -0,0 +1,80 @@
+using System;
+
+using Newtonsoft.Json;
+
+namespace Dalamud.Game
+{
+ ///
+ /// Converts a to and from a string (e.g. "2010.01.01.1234.5678").
+ ///
+ public sealed class GameVersionConverter : JsonConverter
+ {
+ ///
+ /// Writes the JSON representation of the object.
+ ///
+ /// The to write to.
+ /// The value.
+ /// The calling serializer.
+ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
+ {
+ if (value == null)
+ {
+ writer.WriteNull();
+ }
+ else if (value is GameVersion)
+ {
+ writer.WriteValue(value.ToString());
+ }
+ else
+ {
+ throw new JsonSerializationException("Expected GameVersion object value");
+ }
+ }
+
+ ///
+ /// Reads the JSON representation of the object.
+ ///
+ /// The to read from.
+ /// Type of the object.
+ /// The existing property value of the JSON that is being converted.
+ /// The calling serializer.
+ /// The object value.
+ public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
+ {
+ if (reader.TokenType == JsonToken.Null)
+ {
+ return null;
+ }
+ else
+ {
+ if (reader.TokenType == JsonToken.String)
+ {
+ try
+ {
+ return new GameVersion((string)reader.Value!);
+ }
+ catch (Exception ex)
+ {
+ throw new JsonSerializationException($"Error parsing GameVersion string: {reader.Value}", ex);
+ }
+ }
+ else
+ {
+ throw new JsonSerializationException($"Unexpected token or value when parsing GameVersion. Token: {reader.TokenType}, Value: {reader.Value}");
+ }
+ }
+ }
+
+ ///
+ /// Determines whether this instance can convert the specified object type.
+ ///
+ /// Type of the object.
+ ///
+ /// true if this instance can convert the specified object type; otherwise, false.
+ ///
+ public override bool CanConvert(Type objectType)
+ {
+ return objectType == typeof(GameVersion);
+ }
+ }
+}
diff --git a/Dalamud/Game/Internal/AntiDebug.cs b/Dalamud/Game/Internal/AntiDebug.cs
index 04b107458..57c7fdeb4 100644
--- a/Dalamud/Game/Internal/AntiDebug.cs
+++ b/Dalamud/Game/Internal/AntiDebug.cs
@@ -29,13 +29,13 @@ namespace Dalamud.Game.Internal
this.debugCheckAddress = IntPtr.Zero;
}
- Log.Verbose("DebugCheck address {DebugCheckAddress}", this.debugCheckAddress);
+ Log.Verbose($"Debug check address 0x{this.debugCheckAddress.ToInt64():X}");
}
///
/// Gets a value indicating whether the anti-debugging is enabled.
///
- public bool IsEnabled { get; private set; }
+ public bool IsEnabled { get; private set; } = false;
///
/// Enables the anti-debugging by overwriting code in memory.
@@ -45,7 +45,7 @@ namespace Dalamud.Game.Internal
this.original = new byte[this.nop.Length];
if (this.debugCheckAddress != IntPtr.Zero && !this.IsEnabled)
{
- Log.Information($"Overwriting debug check @ 0x{this.debugCheckAddress.ToInt64():X}");
+ Log.Information($"Overwriting debug check at 0x{this.debugCheckAddress.ToInt64():X}");
SafeMemory.ReadBytes(this.debugCheckAddress, this.nop.Length, out this.original);
SafeMemory.WriteBytes(this.debugCheckAddress, this.nop);
}
@@ -64,7 +64,7 @@ namespace Dalamud.Game.Internal
{
if (this.debugCheckAddress != IntPtr.Zero && this.original != null)
{
- Log.Information($"Reverting debug check @ 0x{this.debugCheckAddress.ToInt64():X}");
+ Log.Information($"Reverting debug check at 0x{this.debugCheckAddress.ToInt64():X}");
SafeMemory.WriteBytes(this.debugCheckAddress, this.original);
}
else
@@ -110,9 +110,11 @@ namespace Dalamud.Game.Internal
{
// If anti-debug is enabled and is being disposed, odds are either the game is exiting, or Dalamud is being reloaded.
// If it is the latter, there's half a chance a debugger is currently attached. There's no real need to disable the
- // check in either situation anyways.
- // this.Disable();
+ // check in either situation anyways. However if Dalamud is being reloaded, the sig may fail so may as well undo it.
+ this.Disable();
}
+
+ this.disposed = true;
}
}
}
diff --git a/Dalamud/Game/Internal/DXGI/Definitions/ID3D11DeviceVtbl.cs b/Dalamud/Game/Internal/DXGI/Definitions/ID3D11DeviceVtbl.cs
new file mode 100644
index 000000000..7703713b3
--- /dev/null
+++ b/Dalamud/Game/Internal/DXGI/Definitions/ID3D11DeviceVtbl.cs
@@ -0,0 +1,227 @@
+namespace Dalamud.Game.Internal.DXGI.Definitions
+{
+ ///
+ /// Contains a full list of ID3D11Device functions to be used as an indexer into the DirectX Virtual Function Table entries.
+ ///
+ internal enum ID3D11DeviceVtbl
+ {
+ // IUnknown
+
+ ///
+ /// IUnknown::QueryInterface method (unknwn.h).
+ ///
+ QueryInterface = 0,
+
+ ///
+ /// IUnknown::AddRef method (unknwn.h).
+ ///
+ AddRef = 1,
+
+ ///
+ /// IUnknown::Release method (unknwn.h).
+ ///
+ Release = 2,
+
+ // ID3D11Device
+
+ ///
+ /// ID3D11Device::CreateBuffer method (d3d11.h).
+ ///
+ CreateBuffer = 3,
+
+ ///
+ /// ID3D11Device::CreateTexture1D method (d3d11.h).
+ ///
+ CreateTexture1D = 4,
+
+ ///
+ /// ID3D11Device::CreateTexture2D method (d3d11.h).
+ ///
+ CreateTexture2D = 5,
+
+ ///
+ /// ID3D11Device::CreateTexture3D method (d3d11.h).
+ ///
+ CreateTexture3D = 6,
+
+ ///
+ /// ID3D11Device::CreateShaderResourceView method (d3d11.h).
+ ///
+ CreateShaderResourceView = 7,
+
+ ///
+ /// ID3D11Device::CreateUnorderedAccessView method (d3d11.h).
+ ///
+ CreateUnorderedAccessView = 8,
+
+ ///
+ /// ID3D11Device::CreateRenderTargetView method (d3d11.h).
+ ///
+ CreateRenderTargetView = 9,
+
+ ///
+ /// ID3D11Device::CreateDepthStencilView method (d3d11.h).
+ ///
+ CreateDepthStencilView = 10,
+
+ ///
+ /// ID3D11Device::CreateInputLayout method (d3d11.h).
+ ///
+ CreateInputLayout = 11,
+
+ ///
+ /// ID3D11Device::CreateVertexShader method (d3d11.h).
+ ///
+ CreateVertexShader = 12,
+
+ ///
+ /// ID3D11Device::CreateGeometryShader method (d3d11.h).
+ ///
+ CreateGeometryShader = 13,
+
+ ///
+ /// ID3D11Device::CreateGeometryShaderWithStreamOutput method (d3d11.h).
+ ///
+ CreateGeometryShaderWithStreamOutput = 14,
+
+ ///
+ /// ID3D11Device::CreatePixelShader method (d3d11.h).
+ ///
+ CreatePixelShader = 15,
+
+ ///
+ /// ID3D11Device::CreateHullShader method (d3d11.h).
+ ///
+ CreateHullShader = 16,
+
+ ///
+ /// ID3D11Device::CreateDomainShader method (d3d11.h).
+ ///
+ CreateDomainShader = 17,
+
+ ///
+ /// ID3D11Device::CreateComputeShader method (d3d11.h).
+ ///
+ CreateComputeShader = 18,
+
+ ///
+ /// ID3D11Device::CreateClassLinkage method (d3d11.h).
+ ///
+ CreateClassLinkage = 19,
+
+ ///
+ /// ID3D11Device::CreateBlendState method (d3d11.h).
+ ///
+ CreateBlendState = 20,
+
+ ///
+ /// ID3D11Device::CreateDepthStencilState method (d3d11.h).
+ ///
+ CreateDepthStencilState = 21,
+
+ ///
+ /// ID3D11Device::CreateRasterizerState method (d3d11.h).
+ ///
+ CreateRasterizerState = 22,
+
+ ///
+ /// ID3D11Device::CreateSamplerState method (d3d11.h).
+ ///
+ CreateSamplerState = 23,
+
+ ///
+ /// ID3D11Device::CreateQuery method (d3d11.h).
+ ///
+ CreateQuery = 24,
+
+ ///
+ /// ID3D11Device::CreatePredicate method (d3d11.h).
+ ///
+ CreatePredicate = 25,
+
+ ///
+ /// ID3D11Device::CreateCounter method (d3d11.h).
+ ///
+ CreateCounter = 26,
+
+ ///
+ /// ID3D11Device::CreateDeferredContext method (d3d11.h).
+ ///
+ CreateDeferredContext = 27,
+
+ ///
+ /// ID3D11Device::OpenSharedResource method (d3d11.h).
+ ///
+ OpenSharedResource = 28,
+
+ ///
+ /// ID3D11Device::CheckFormatSupport method (d3d11.h).
+ ///
+ CheckFormatSupport = 29,
+
+ ///
+ /// ID3D11Device::CheckMultisampleQualityLevels method (d3d11.h).
+ ///
+ CheckMultisampleQualityLevels = 30,
+
+ ///
+ /// ID3D11Device::CheckCounterInfo method (d3d11.h).
+ ///
+ CheckCounterInfo = 31,
+
+ ///
+ /// ID3D11Device::CheckCounter method (d3d11.h).
+ ///
+ CheckCounter = 32,
+
+ ///
+ /// ID3D11Device::CheckFeatureSupport method (d3d11.h).
+ ///
+ CheckFeatureSupport = 33,
+
+ ///
+ /// ID3D11Device::GetPrivateData method (d3d11.h).
+ ///
+ GetPrivateData = 34,
+
+ ///
+ /// ID3D11Device::SetPrivateData method (d3d11.h).
+ ///
+ SetPrivateData = 35,
+
+ ///
+ /// ID3D11Device::SetPrivateDataInterface method (d3d11.h).
+ ///
+ SetPrivateDataInterface = 36,
+
+ ///
+ /// ID3D11Device::GetFeatureLevel method (d3d11.h).
+ ///
+ GetFeatureLevel = 37,
+
+ ///
+ /// ID3D11Device::GetCreationFlags method (d3d11.h).
+ ///
+ GetCreationFlags = 38,
+
+ ///
+ /// ID3D11Device::GetDeviceRemovedReason method (d3d11.h).
+ ///
+ GetDeviceRemovedReason = 39,
+
+ ///
+ /// ID3D11Device::GetImmediateContext method (d3d11.h).
+ ///
+ GetImmediateContext = 40,
+
+ ///
+ /// ID3D11Device::SetExceptionMode method (d3d11.h).
+ ///
+ SetExceptionMode = 41,
+
+ ///
+ /// ID3D11Device::GetExceptionMode method (d3d11.h).
+ ///
+ GetExceptionMode = 42,
+ }
+}
diff --git a/Dalamud/Game/Internal/DXGI/Definitions/IDXGISwapChainVtbl.cs b/Dalamud/Game/Internal/DXGI/Definitions/IDXGISwapChainVtbl.cs
new file mode 100644
index 000000000..e3d627ce3
--- /dev/null
+++ b/Dalamud/Game/Internal/DXGI/Definitions/IDXGISwapChainVtbl.cs
@@ -0,0 +1,107 @@
+namespace Dalamud.Game.Internal.DXGI.Definitions
+{
+ ///
+ /// Contains a full list of IDXGISwapChain functions to be used as an indexer into the SwapChain Virtual Function Table
+ /// entries.
+ ///
+ internal enum IDXGISwapChainVtbl
+ {
+ // IUnknown
+
+ ///
+ /// IUnknown::QueryInterface method (unknwn.h).
+ ///
+ QueryInterface = 0,
+
+ ///
+ /// IUnknown::AddRef method (unknwn.h).
+ ///
+ AddRef = 1,
+
+ ///
+ /// IUnknown::Release method (unknwn.h).
+ ///
+ Release = 2,
+
+ // IDXGIObject
+
+ ///
+ /// IDXGIObject::SetPrivateData method (dxgi.h).
+ ///
+ SetPrivateData = 3,
+
+ ///
+ /// IDXGIObject::SetPrivateDataInterface method (dxgi.h).
+ ///
+ SetPrivateDataInterface = 4,
+
+ ///
+ /// IDXGIObject::GetPrivateData method (dxgi.h).
+ ///
+ GetPrivateData = 5,
+
+ ///
+ /// IDXGIObject::GetParent method (dxgi.h).
+ ///
+ GetParent = 6,
+
+ // IDXGIDeviceSubObject
+
+ ///
+ /// IDXGIDeviceSubObject::GetDevice method (dxgi.h).
+ ///
+ GetDevice = 7,
+
+ // IDXGISwapChain
+
+ ///
+ /// IDXGISwapChain::Present method (dxgi.h).
+ ///
+ Present = 8,
+
+ ///
+ /// IUnknIDXGISwapChainown::GetBuffer method (dxgi.h).
+ ///
+ GetBuffer = 9,
+
+ ///
+ /// IDXGISwapChain::SetFullscreenState method (dxgi.h).
+ ///
+ SetFullscreenState = 10,
+
+ ///
+ /// IDXGISwapChain::GetFullscreenState method (dxgi.h).
+ ///
+ GetFullscreenState = 11,
+
+ ///
+ /// IDXGISwapChain::GetDesc method (dxgi.h).
+ ///
+ GetDesc = 12,
+
+ ///
+ /// IDXGISwapChain::ResizeBuffers method (dxgi.h).
+ ///
+ ResizeBuffers = 13,
+
+ ///
+ /// IDXGISwapChain::ResizeTarget method (dxgi.h).
+ ///
+ ResizeTarget = 14,
+
+ ///
+ /// IDXGISwapChain::GetContainingOutput method (dxgi.h).
+ ///
+ GetContainingOutput = 15,
+
+ ///
+ /// IDXGISwapChain::GetFrameStatistics method (dxgi.h).
+ ///
+ GetFrameStatistics = 16,
+
+ ///
+ /// IDXGISwapChain::GetLastPresentCount method (dxgi.h).
+ ///
+ GetLastPresentCount = 17,
+ }
+}
diff --git a/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs b/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs
index 701068ed3..e44883612 100644
--- a/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs
+++ b/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs
@@ -22,7 +22,7 @@ namespace Dalamud.Game.Internal.DXGI
{
var module = Process.GetCurrentProcess().Modules.Cast().First(m => m.ModuleName == "dxgi.dll");
- Log.Debug($"Found DXGI: {module.BaseAddress.ToInt64():X}");
+ Log.Debug($"Found DXGI: 0x{module.BaseAddress.ToInt64():X}");
var scanner = new SigScanner(module);
diff --git a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs
index 6e69a17bd..b5c0d3225 100644
--- a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs
+++ b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
-using System.Windows.Forms;
+using Dalamud.Game.Internal.DXGI.Definitions;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
@@ -19,9 +19,6 @@ namespace Dalamud.Game.Internal.DXGI
///
public class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressResolver
{
- private const int DxgiSwapchainMethodCount = 18;
- private const int D3D11DeviceMethodCount = 43;
-
private List d3d11VTblAddresses;
private List dxgiSwapChainVTblAddresses;
@@ -34,30 +31,33 @@ namespace Dalamud.Game.Internal.DXGI
///
protected override void Setup64Bit(SigScanner sig)
{
+ // Create temporary device + swapchain and determine method addresses
if (this.d3d11VTblAddresses == null)
{
- // Create temporary device + swapchain and determine method addresses
- var renderForm = new Form();
+ // A renderable object isnt required, just a handle
+ var handle = Marshal.AllocHGlobal(Marshal.SizeOf());
Device.CreateWithSwapChain(
DriverType.Hardware,
DeviceCreationFlags.BgraSupport,
- CreateSwapChainDescription(renderForm.Handle),
+ CreateSwapChainDescription(handle),
out var device,
out var swapChain);
if (device != null && swapChain != null)
{
- this.d3d11VTblAddresses = this.GetVTblAddresses(device.NativePointer, D3D11DeviceMethodCount);
- this.dxgiSwapChainVTblAddresses = this.GetVTblAddresses(swapChain.NativePointer, DxgiSwapchainMethodCount);
+ this.d3d11VTblAddresses = GetVTblAddresses(device.NativePointer, Enum.GetValues(typeof(ID3D11DeviceVtbl)).Length);
+ this.dxgiSwapChainVTblAddresses = GetVTblAddresses(swapChain.NativePointer, Enum.GetValues(typeof(IDXGISwapChainVtbl)).Length);
}
device?.Dispose();
swapChain?.Dispose();
+
+ Marshal.FreeHGlobal(handle);
}
- this.Present = this.dxgiSwapChainVTblAddresses[8];
- this.ResizeBuffers = this.dxgiSwapChainVTblAddresses[13];
+ this.Present = this.dxgiSwapChainVTblAddresses[(int)IDXGISwapChainVtbl.Present];
+ this.ResizeBuffers = this.dxgiSwapChainVTblAddresses[(int)IDXGISwapChainVtbl.ResizeBuffers];
}
private static SwapChainDescription CreateSwapChainDescription(IntPtr renderForm)
@@ -75,12 +75,12 @@ namespace Dalamud.Game.Internal.DXGI
};
}
- private List GetVTblAddresses(IntPtr pointer, int numberOfMethods)
+ private static List GetVTblAddresses(IntPtr pointer, int numberOfMethods)
{
- return this.GetVTblAddresses(pointer, 0, numberOfMethods);
+ return GetVTblAddresses(pointer, 0, numberOfMethods);
}
- private List GetVTblAddresses(IntPtr pointer, int startIndex, int numberOfMethods)
+ private static List GetVTblAddresses(IntPtr pointer, int startIndex, int numberOfMethods)
{
var vtblAddresses = new List();
var vTable = Marshal.ReadIntPtr(pointer);
diff --git a/Dalamud/Game/Internal/Framework.cs b/Dalamud/Game/Internal/Framework.cs
index 7d13318f6..abe3272e0 100644
--- a/Dalamud/Game/Internal/Framework.cs
+++ b/Dalamud/Game/Internal/Framework.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
+using System.Threading;
using Dalamud.Game.Internal.Gui;
using Dalamud.Game.Internal.Libc;
@@ -29,13 +30,13 @@ namespace Dalamud.Game.Internal
///
/// The SigScanner instance.
/// The Dalamud instance.
- public Framework(SigScanner scanner, Dalamud dalamud)
+ internal Framework(SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
this.Address = new FrameworkAddressResolver();
this.Address.Setup(scanner);
- Log.Verbose("Framework address {FrameworkAddress}", this.Address.BaseAddress);
+ Log.Verbose($"Framework address 0x{this.Address.BaseAddress.ToInt64():X}");
if (this.Address.BaseAddress == IntPtr.Zero)
{
throw new InvalidOperationException("Framework is not initalized yet.");
@@ -143,6 +144,11 @@ namespace Dalamud.Game.Internal
this.Gui.Dispose();
this.Network.Dispose();
+ this.updateHook.Disable();
+ this.destroyHook.Disable();
+ this.realDestroyHook.Disable();
+ Thread.Sleep(500);
+
this.updateHook.Dispose();
this.destroyHook.Dispose();
this.realDestroyHook.Dispose();
diff --git a/Dalamud/Game/Internal/FrameworkAddressResolver.cs b/Dalamud/Game/Internal/FrameworkAddressResolver.cs
index 551e779e5..1af2e9263 100644
--- a/Dalamud/Game/Internal/FrameworkAddressResolver.cs
+++ b/Dalamud/Game/Internal/FrameworkAddressResolver.cs
@@ -44,7 +44,7 @@ namespace Dalamud.Game.Internal
// 00007FF701AD666A | 48 8D ?? ?? ?? 00 00 | LEA RCX,QWORD PTR DS:[RBX + 2C38]
// 00007FF701AD6671 | E8 ?? ?? ?? ?? | CALL ffxiv_dx11.7FF701E2A7D0
// 00007FF701AD6676 | 48 8D ?? ?? ?? ?? ?? | LEA RAX,QWORD PTR DS:[7FF702C31F80
- var fwDtor = scanner.ScanText("48C705????????00000000 E8???????? 488D??????0000 E8???????? 488D");
+ var fwDtor = scanner.ScanText("48 C7 05 ?? ?? ?? ?? 00 00 00 00 E8 ?? ?? ?? ?? 48 8D ?? ?? ?? 00 00 E8 ?? ?? ?? ?? 48 8D");
var fwOffset = Marshal.ReadInt32(fwDtor + 3);
var pFramework = scanner.ResolveRelativeAddress(fwDtor + 11, fwOffset);
diff --git a/Dalamud/Game/Internal/Gui/ChatGui.cs b/Dalamud/Game/Internal/Gui/ChatGui.cs
index 38d7a2783..09aa234eb 100644
--- a/Dalamud/Game/Internal/Gui/ChatGui.cs
+++ b/Dalamud/Game/Internal/Gui/ChatGui.cs
@@ -36,14 +36,14 @@ namespace Dalamud.Game.Internal.Gui
/// The base address of the ChatManager.
/// The SigScanner instance.
/// The Dalamud instance.
- public ChatGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud)
+ internal ChatGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
this.address = new ChatGuiAddressResolver(baseAddress);
this.address.Setup(scanner);
- Log.Verbose("Chat manager address {ChatManager}", this.address.BaseAddress);
+ Log.Verbose($"Chat manager address 0x{this.address.BaseAddress.ToInt64():X}");
this.printMessageHook = new Hook(this.address.PrintMessage, this.HandlePrintMessageDetour);
this.populateItemLinkHook = new Hook(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
@@ -184,7 +184,7 @@ namespace Dalamud.Game.Internal.Gui
Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message);
this.PrintChat(new XivChatEntry
{
- MessageBytes = Encoding.UTF8.GetBytes(message),
+ Message = message,
Type = this.dalamud.Configuration.GeneralChatType,
});
}
@@ -199,7 +199,7 @@ namespace Dalamud.Game.Internal.Gui
Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue);
this.PrintChat(new XivChatEntry
{
- MessageBytes = message.Encode(),
+ Message = message,
Type = this.dalamud.Configuration.GeneralChatType,
});
}
@@ -214,7 +214,7 @@ namespace Dalamud.Game.Internal.Gui
Log.Verbose("[CHATGUI PRINT REGULAR ERROR]{0}", message);
this.PrintChat(new XivChatEntry
{
- MessageBytes = Encoding.UTF8.GetBytes(message),
+ Message = message,
Type = XivChatType.Urgent,
});
}
@@ -229,7 +229,7 @@ namespace Dalamud.Game.Internal.Gui
Log.Verbose("[CHATGUI PRINT SESTRING ERROR]{0}", message.TextValue);
this.PrintChat(new XivChatEntry
{
- MessageBytes = message.Encode(),
+ Message = message,
Type = XivChatType.Urgent,
});
}
@@ -249,10 +249,10 @@ namespace Dalamud.Game.Internal.Gui
continue;
}
- var senderRaw = Encoding.UTF8.GetBytes(chat.Name ?? string.Empty);
+ var senderRaw = (chat.Name ?? string.Empty).Encode();
using var senderOwned = framework.Libc.NewString(senderRaw);
- var messageRaw = chat.MessageBytes ?? new byte[0];
+ var messageRaw = (chat.Message ?? string.Empty).Encode();
using var messageOwned = framework.Libc.NewString(messageRaw);
this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters);
diff --git a/Dalamud/Game/Internal/Gui/ChatGuiAddressResolver.cs b/Dalamud/Game/Internal/Gui/ChatGuiAddressResolver.cs
index dfc6ed0f3..067558e12 100644
--- a/Dalamud/Game/Internal/Gui/ChatGuiAddressResolver.cs
+++ b/Dalamud/Game/Internal/Gui/ChatGuiAddressResolver.cs
@@ -10,10 +10,10 @@ namespace Dalamud.Game.Internal.Gui
///
/// Initializes a new instance of the class.
///
- /// The base address of the native ChatManager class.
- public ChatGuiAddressResolver(IntPtr baseAddres)
+ /// The base address of the native ChatManager class.
+ public ChatGuiAddressResolver(IntPtr baseAddress)
{
- this.BaseAddress = baseAddres;
+ this.BaseAddress = baseAddress;
}
///
diff --git a/Dalamud/Game/Internal/Gui/GameGui.cs b/Dalamud/Game/Internal/Gui/GameGui.cs
index be018f1e1..f27f75662 100644
--- a/Dalamud/Game/Internal/Gui/GameGui.cs
+++ b/Dalamud/Game/Internal/Gui/GameGui.cs
@@ -47,7 +47,7 @@ namespace Dalamud.Game.Internal.Gui
/// The base address of the native GuiManager class.
/// The SigScanner instance.
/// The Dalamud instance.
- public GameGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud)
+ internal GameGui(IntPtr baseAddress, SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
@@ -56,12 +56,12 @@ namespace Dalamud.Game.Internal.Gui
Log.Verbose("===== G A M E G U I =====");
- Log.Verbose("GameGuiManager address {Address:X}", this.address.BaseAddress.ToInt64());
- Log.Verbose("SetGlobalBgm address {Address:X}", this.address.SetGlobalBgm.ToInt64());
- Log.Verbose("HandleItemHover address {Address:X}", this.address.HandleItemHover.ToInt64());
- Log.Verbose("HandleItemOut address {Address:X}", this.address.HandleItemOut.ToInt64());
- Log.Verbose("GetUIObject address {Address:X}", this.address.GetUIObject.ToInt64());
- Log.Verbose("GetAgentModule address {Address:X}", this.address.GetAgentModule.ToInt64());
+ Log.Verbose($"GameGuiManager address 0x{this.address.BaseAddress.ToInt64():X}");
+ Log.Verbose($"SetGlobalBgm address 0x{this.address.SetGlobalBgm.ToInt64():X}");
+ Log.Verbose($"HandleItemHover address 0x{this.address.HandleItemHover.ToInt64():X}");
+ Log.Verbose($"HandleItemOut address 0x{this.address.HandleItemOut.ToInt64():X}");
+ Log.Verbose($"GetUIObject address 0x{this.address.GetUIObject.ToInt64():X}");
+ Log.Verbose($"GetAgentModule address 0x{this.address.GetAgentModule.ToInt64():X}");
this.Chat = new ChatGui(this.address.ChatManager, scanner, dalamud);
this.PartyFinder = new PartyFinderGui(scanner, dalamud);
diff --git a/Dalamud/Game/Internal/Gui/PartyFinderGui.cs b/Dalamud/Game/Internal/Gui/PartyFinderGui.cs
index bdc5eee96..24079cf4e 100755
--- a/Dalamud/Game/Internal/Gui/PartyFinderGui.cs
+++ b/Dalamud/Game/Internal/Gui/PartyFinderGui.cs
@@ -23,7 +23,7 @@ namespace Dalamud.Game.Internal.Gui
///
/// The SigScanner instance.
/// The Dalamud instance.
- public PartyFinderGui(SigScanner scanner, Dalamud dalamud)
+ internal PartyFinderGui(SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
diff --git a/Dalamud/Game/Internal/Gui/Structs/Addon.cs b/Dalamud/Game/Internal/Gui/Structs/Addon.cs
index be1f08f33..57595a2c7 100644
--- a/Dalamud/Game/Internal/Gui/Structs/Addon.cs
+++ b/Dalamud/Game/Internal/Gui/Structs/Addon.cs
@@ -1,5 +1,7 @@
using System.Runtime.InteropServices;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
namespace Dalamud.Game.Internal.Gui.Structs
{
///
diff --git a/Dalamud/Game/Internal/Gui/Structs/AtkResNode.cs b/Dalamud/Game/Internal/Gui/Structs/AtkResNode.cs
deleted file mode 100644
index 406a1262a..000000000
--- a/Dalamud/Game/Internal/Gui/Structs/AtkResNode.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-using System;
-using System.Runtime.InteropServices;
-
-namespace Dalamud.Game.Internal.Gui.Structs
-{
- ///
- /// Native memory representation of a UI resource node.
- ///
- ///
- /// This is copied from https://github.com/aers/FFXIVClientStructs/blob/main/Component/GUI/AtkResNode.cs.
- /// If you need newer features, include FFXIVClientStructs and ILMerge the assembly.
- ///
- [StructLayout(LayoutKind.Explicit, Size = 0xA8)]
- public unsafe struct AtkResNode
- {
- [FieldOffset(0x0)]
- public IntPtr AtkEventTarget;
-
- [FieldOffset(0x8)]
- public uint NodeID;
-
- [FieldOffset(0x20)]
- public AtkResNode* ParentNode;
-
- [FieldOffset(0x28)]
- public AtkResNode* PrevSiblingNode;
-
- [FieldOffset(0x30)]
- public AtkResNode* NextSiblingNode;
-
- [FieldOffset(0x38)]
- public AtkResNode* ChildNode;
-
- [FieldOffset(0x40)]
- public ushort Type;
-
- [FieldOffset(0x42)]
- public ushort ChildCount;
-
- [FieldOffset(0x44)]
- public float X;
-
- [FieldOffset(0x48)]
- public float Y;
-
- [FieldOffset(0x4C)]
- public float ScaleX;
-
- [FieldOffset(0x50)]
- public float ScaleY;
-
- [FieldOffset(0x54)]
- public float Rotation;
-
- [FieldOffset(0x58)]
- public fixed float UnkMatrix[3 * 2];
-
- [FieldOffset(0x70)]
- public uint Color;
-
- [FieldOffset(0x74)]
- public float Depth;
-
- [FieldOffset(0x78)]
- public float Depth_2;
-
- [FieldOffset(0x7C)]
- public ushort AddRed;
-
- [FieldOffset(0x7E)]
- public ushort AddGreen;
-
- [FieldOffset(0x80)]
- public ushort AddBlue;
-
- [FieldOffset(0x82)]
- public ushort AddRed_2;
-
- [FieldOffset(0x84)]
- public ushort AddGreen_2;
-
- [FieldOffset(0x86)]
- public ushort AddBlue_2;
-
- [FieldOffset(0x88)]
- public byte MultiplyRed;
-
- [FieldOffset(0x89)]
- public byte MultiplyGreen;
-
- [FieldOffset(0x8A)]
- public byte MultiplyBlue;
-
- [FieldOffset(0x8B)]
- public byte MultiplyRed_2;
-
- [FieldOffset(0x8C)]
- public byte MultiplyGreen_2;
-
- [FieldOffset(0x8D)]
- public byte MultiplyBlue_2;
-
- [FieldOffset(0x8E)]
- public byte Alpha_2;
-
- [FieldOffset(0x8F)]
- public byte UnkByte_1;
-
- [FieldOffset(0x90)]
- public ushort Width;
-
- [FieldOffset(0x92)]
- public ushort Height;
-
- [FieldOffset(0x94)]
- public float OriginX;
-
- [FieldOffset(0x98)]
- public float OriginY;
-
- [FieldOffset(0x9C)]
- public ushort Priority;
-
- [FieldOffset(0x9E)]
- public short Flags;
-
- [FieldOffset(0xA0)]
- public uint Flags_2;
- }
-}
diff --git a/Dalamud/Game/Internal/Gui/ToastGui.cs b/Dalamud/Game/Internal/Gui/ToastGui.cs
index bb4883e7a..4c1ce497f 100755
--- a/Dalamud/Game/Internal/Gui/ToastGui.cs
+++ b/Dalamud/Game/Internal/Gui/ToastGui.cs
@@ -31,7 +31,7 @@ namespace Dalamud.Game.Internal.Gui
///
/// The SigScanner instance.
/// The Dalamud instance.
- public ToastGui(SigScanner scanner, Dalamud dalamud)
+ internal ToastGui(SigScanner scanner, Dalamud dalamud)
{
this.dalamud = dalamud;
diff --git a/Dalamud/Game/Internal/Network/GameNetwork.cs b/Dalamud/Game/Internal/Network/GameNetwork.cs
index cb88c9dfc..947a4651c 100644
--- a/Dalamud/Game/Internal/Network/GameNetwork.cs
+++ b/Dalamud/Game/Internal/Network/GameNetwork.cs
@@ -34,8 +34,8 @@ namespace Dalamud.Game.Internal.Network
this.address.Setup(scanner);
Log.Verbose("===== G A M E N E T W O R K =====");
- Log.Verbose("ProcessZonePacketDown address {ProcessZonePacketDown}", this.address.ProcessZonePacketDown);
- Log.Verbose("ProcessZonePacketUp address {ProcessZonePacketUp}", this.address.ProcessZonePacketUp);
+ Log.Verbose($"ProcessZonePacketDown address 0x{this.address.ProcessZonePacketDown.ToInt64():X}");
+ Log.Verbose($"ProcessZonePacketUp address 0x{this.address.ProcessZonePacketUp.ToInt64():X}");
this.processZonePacketDownHook = new Hook(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour);
this.processZonePacketUpHook = new Hook(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour);
diff --git a/Dalamud/Game/Internal/Resource/ResourceManager.cs b/Dalamud/Game/Internal/Resource/ResourceManager.cs
index 1e80475c1..7e3c2b045 100644
--- a/Dalamud/Game/Internal/Resource/ResourceManager.cs
+++ b/Dalamud/Game/Internal/Resource/ResourceManager.cs
@@ -25,7 +25,7 @@ namespace Dalamud.Game.Internal.File
///
/// The Dalamud instance.
/// The SigScanner instance.
- public ResourceManager(Dalamud dalamud, SigScanner scanner)
+ internal ResourceManager(Dalamud dalamud, SigScanner scanner)
{
this.dalamud = dalamud;
this.address = new ResourceManagerAddressResolver();
diff --git a/Dalamud/Game/Network/NetworkHandlers.cs b/Dalamud/Game/Network/NetworkHandlers.cs
index 945627b83..fe54249df 100644
--- a/Dalamud/Game/Network/NetworkHandlers.cs
+++ b/Dalamud/Game/Network/NetworkHandlers.cs
@@ -96,11 +96,11 @@ namespace Dalamud.Game.Network
{
var flashInfo = new NativeFunctions.FlashWindowInfo
{
- cbSize = (uint)Marshal.SizeOf(),
- uCount = uint.MaxValue,
- dwTimeout = 0,
- dwFlags = NativeFunctions.FlashWindow.All | NativeFunctions.FlashWindow.TimerNoFG,
- hwnd = Process.GetCurrentProcess().MainWindowHandle,
+ Size = (uint)Marshal.SizeOf(),
+ Count = uint.MaxValue,
+ Timeout = 0,
+ Flags = NativeFunctions.FlashWindow.All | NativeFunctions.FlashWindow.TimerNoFG,
+ Hwnd = Process.GetCurrentProcess().MainWindowHandle,
};
NativeFunctions.FlashWindowEx(ref flashInfo);
}
diff --git a/Dalamud/Game/ClientState/Actors/Position3.cs b/Dalamud/Game/Position3.cs
similarity index 96%
rename from Dalamud/Game/ClientState/Actors/Position3.cs
rename to Dalamud/Game/Position3.cs
index df8b0c471..3812376df 100644
--- a/Dalamud/Game/ClientState/Actors/Position3.cs
+++ b/Dalamud/Game/Position3.cs
@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;
-namespace Dalamud.Game.ClientState.Actors
+namespace Dalamud.Game
{
///
/// A game native equivalent of a Vector3.
diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs
index ec1608bec..56d0003e3 100644
--- a/Dalamud/Game/SigScanner.cs
+++ b/Dalamud/Game/SigScanner.cs
@@ -34,8 +34,8 @@ namespace Dalamud.Game
if (this.IsCopy)
this.SetupCopiedSegments();
- Log.Verbose("Module base: {Address}", this.TextSectionBase);
- Log.Verbose("Module size: {Size}", this.TextSectionSize);
+ Log.Verbose($"Module base: 0x{this.TextSectionBase.ToInt64():X}");
+ Log.Verbose($"Module size: 0x{this.TextSectionSize:X}");
}
///
@@ -206,7 +206,7 @@ namespace Dalamud.Game
var insnByte = Marshal.ReadByte(scanRet);
if (insnByte == 0xE8 || insnByte == 0xE9)
- return ReadCallSig(scanRet);
+ return ReadJmpCallSig(scanRet);
return scanRet;
}
@@ -220,13 +220,13 @@ namespace Dalamud.Game
}
///
- /// Helper for ScanText to get the correct address for IDA sigs that mark the first CALL location.
+ /// Helper for ScanText to get the correct address for IDA sigs that mark the first JMP or CALL location.
///
- /// The address the CALL sig resolved to.
+ /// The address the JMP or CALL sig resolved to.
/// The real offset of the signature.
- private static IntPtr ReadCallSig(IntPtr sigLocation)
+ private static IntPtr ReadJmpCallSig(IntPtr sigLocation)
{
- var jumpOffset = Marshal.ReadInt32(IntPtr.Add(sigLocation, 1));
+ var jumpOffset = Marshal.ReadInt32(sigLocation, 1);
return IntPtr.Add(sigLocation, 5 + jumpOffset);
}
@@ -235,6 +235,7 @@ namespace Dalamud.Game
signature = signature.Replace(" ", string.Empty);
if (signature.Length % 2 != 0)
throw new ArgumentException("Signature without whitespaces must be divisible by two.", nameof(signature));
+
var needleLength = signature.Length / 2;
var needle = new byte[needleLength];
var mask = new bool[needleLength];
diff --git a/Dalamud/Game/Text/SeStringHandling/Payload.cs b/Dalamud/Game/Text/SeStringHandling/Payload.cs
index 5d0132815..ebf689646 100644
--- a/Dalamud/Game/Text/SeStringHandling/Payload.cs
+++ b/Dalamud/Game/Text/SeStringHandling/Payload.cs
@@ -131,6 +131,10 @@ namespace Dalamud.Game.Text.SeStringHandling
payload = new EmphasisItalicPayload();
break;
+ case SeStringChunkType.NewLine:
+ payload = NewLinePayload.Payload;
+ break;
+
case SeStringChunkType.SeHyphen:
payload = SeHyphenPayload.Payload;
break;
@@ -295,6 +299,11 @@ namespace Dalamud.Game.Text.SeStringHandling
///
EmphasisItalic = 0x1A,
+ ///
+ /// See the .
+ ///
+ NewLine = 0x10,
+
///
/// See the class.
///
diff --git a/Dalamud/Game/Text/SeStringHandling/PayloadType.cs b/Dalamud/Game/Text/SeStringHandling/PayloadType.cs
index 8e6c5ffbd..93bcc7e3e 100644
--- a/Dalamud/Game/Text/SeStringHandling/PayloadType.cs
+++ b/Dalamud/Game/Text/SeStringHandling/PayloadType.cs
@@ -5,6 +5,11 @@ namespace Dalamud.Game.Text.SeStringHandling
///
public enum PayloadType
{
+ ///
+ /// An unknown SeString.
+ ///
+ Unknown,
+
///
/// An SeString payload representing a player link.
///
@@ -66,9 +71,9 @@ namespace Dalamud.Game.Text.SeStringHandling
DalamudLink,
///
- /// An SeString payload representing any data we don't handle.
+ /// An SeString payload representing a newline character.
///
- Unknown,
+ NewLine,
///
/// An SeString payload representing a doublewide SE hypen.
diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/NewLinePayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/NewLinePayload.cs
new file mode 100644
index 000000000..13aba8077
--- /dev/null
+++ b/Dalamud/Game/Text/SeStringHandling/Payloads/NewLinePayload.cs
@@ -0,0 +1,34 @@
+using System;
+using System.IO;
+
+namespace Dalamud.Game.Text.SeStringHandling.Payloads
+{
+ ///
+ /// A wrapped newline character.
+ ///
+ public class NewLinePayload : Payload, ITextProvider
+ {
+ private readonly byte[] bytes = { START_BYTE, (byte)SeStringChunkType.NewLine, 0x01, END_BYTE };
+
+ ///
+ /// Gets an instance of NewLinePayload.
+ ///
+ public static NewLinePayload Payload => new();
+
+ ///
+ /// Gets the text of this payload, evaluates to Environment.NewLine.
+ ///
+ public string Text => Environment.NewLine;
+
+ ///
+ public override PayloadType Type => PayloadType.NewLine;
+
+ ///
+ protected override byte[] EncodeImpl() => this.bytes;
+
+ ///
+ protected override void DecodeImpl(BinaryReader reader, long endOfStream)
+ {
+ }
+ }
+}
diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs
index 43ae7600e..0e8fe1ff3 100644
--- a/Dalamud/Game/Text/SeStringHandling/SeString.cs
+++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs
@@ -13,6 +13,15 @@ namespace Dalamud.Game.Text.SeStringHandling
///
public class SeString
{
+ ///
+ /// Initializes a new instance of the class.
+ /// Creates a new SeString from an ordered list of payloads.
+ ///
+ public SeString()
+ {
+ this.Payloads = new List();
+ }
+
///
/// Initializes a new instance of the class.
/// Creates a new SeString from an ordered list of payloads.
@@ -29,7 +38,7 @@ namespace Dalamud.Game.Text.SeStringHandling
/// Creates a new SeString from an ordered list of payloads.
///
/// The Payload objects to make up this string.
- public SeString(Payload[] payloads)
+ public SeString(params Payload[] payloads)
{
this.Payloads = new List(payloads);
}
@@ -61,7 +70,7 @@ namespace Dalamud.Game.Text.SeStringHandling
///
/// string to convert.
/// Equivalent SeString.
- public static implicit operator SeString(string str) => new(new Payload[] { new TextPayload(str) });
+ public static implicit operator SeString(string str) => new(new TextPayload(str));
///
/// Creates a SeString from a json. (For testing - not recommended for production use.)
diff --git a/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs b/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs
index 7b6f45e07..ad805f528 100644
--- a/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs
+++ b/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs
@@ -88,7 +88,7 @@ namespace Dalamud.Game.Text.SeStringHandling
/// An SeString containing all the payloads necessary to display an item link in the chat log.
public SeString CreateItemLink(Item item, bool isHQ, string displayNameOverride = null)
{
- return this.CreateItemLink((uint)item.RowId, isHQ, displayNameOverride ?? item.Name);
+ return this.CreateItemLink(item.RowId, isHQ, displayNameOverride ?? item.Name);
}
///
diff --git a/Dalamud/Game/Text/XivChatEntry.cs b/Dalamud/Game/Text/XivChatEntry.cs
index dc8005f6b..a5a11766e 100644
--- a/Dalamud/Game/Text/XivChatEntry.cs
+++ b/Dalamud/Game/Text/XivChatEntry.cs
@@ -1,5 +1,7 @@
using System;
+using Dalamud.Game.Text.SeStringHandling;
+
namespace Dalamud.Game.Text
{
///
@@ -20,12 +22,12 @@ namespace Dalamud.Game.Text
///
/// Gets or sets the sender name.
///
- public string Name { get; set; } = string.Empty;
+ public SeString Name { get; set; } = string.Empty;
///
- /// Gets or sets the message bytes.
+ /// Gets or sets the message.
///
- public byte[] MessageBytes { get; set; }
+ public SeString Message { get; set; } = string.Empty;
///
/// Gets or sets the message parameters.
diff --git a/Dalamud/GlobalSuppressions.cs b/Dalamud/GlobalSuppressions.cs
index e4f2f5e86..15bce83e0 100644
--- a/Dalamud/GlobalSuppressions.cs
+++ b/Dalamud/GlobalSuppressions.cs
@@ -18,26 +18,15 @@ using System.Diagnostics.CodeAnalysis;
// Extensions
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group extensions with the relevant class", Scope = "type", Target = "~T:Dalamud.Interface.FontAwesomeExtensions")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Enum followed by an extension", Scope = "type", Target = "~T:Dalamud.Interface.FontAwesomeExtensions")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group extensions with the relevant class", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.JobFlagsExtensions")]
-[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Enum followed by an extension", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.JobFlagsExtensions")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group extensions with the relevant class", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.JobFlagsExt")]
+[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Enum followed by an extension", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.JobFlagsExt")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group extensions with the relevant class", Scope = "type", Target = "~T:Dalamud.Game.Text.XivChatTypeExtensions")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "Enum followed by an extension", Scope = "type", Target = "~T:Dalamud.Game.Text.XivChatTypeExtensions")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group attributes with the relevant class", Scope = "type", Target = "~T:Dalamud.Game.Text.XivChatTypeInfoAttribute")]
-// NativeFunctions.cs
-[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Default Microsoft naming", Scope = "type", Target = "~T:Dalamud.NativeFunctions.FlashWindowInfo")]
-
-// EntryPoint.cs
-[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Required by EasyHook", Scope = "member", Target = "~M:Dalamud.EntryPoint.#ctor(EasyHook.RemoteHooking.IContext,Dalamud.DalamudStartInfo)")]
-[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Required by EasyHook", Scope = "member", Target = "~M:Dalamud.EntryPoint.Run(EasyHook.RemoteHooking.IContext,Dalamud.DalamudStartInfo)")]
-
// DalamudStartInfo.cs
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Interop", Scope = "type", Target = "~T:Dalamud.DalamudStartInfo")]
-// AtkResNode.cs
-[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Inherit documentation or lack thereof from FFXIVClientStructs", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.AtkResNode")]
-[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Inherit naming from FFXIVClientStructs", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.AtkResNode")]
-
// PartyFinder
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "Explicit struct layout", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.PartyFinder.Packet")]
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "Explicit struct layout", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.PartyFinder.Listing")]
@@ -48,10 +37,8 @@ using System.Diagnostics.CodeAnalysis;
// Offsets.cs
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204:Static elements should appear before instance elements", Justification = "Offset classes goto the end of file.", Scope = "type", Target = "~T:Dalamud.Game.ClientState.Actors.TargetOffsets")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group offset classes with the relevant class.", Scope = "type", Target = "~T:Dalamud.Game.ClientState.Actors.TargetOffsets")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group offset classes with the relevant class.", Scope = "type", Target = "~T:Dalamud.Game.ClientState.Structs.ActorOffsets")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Group offset classes with the relevant class.", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.AddonOffsets")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the offset usage instead.", Scope = "type", Target = "~T:Dalamud.Game.ClientState.Actors.TargetOffsets")]
-[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the offset usage instead.", Scope = "type", Target = "~T:Dalamud.Game.ClientState.Structs.ActorOffsets")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the offset usage instead.", Scope = "type", Target = "~T:Dalamud.Game.Internal.Gui.Structs.AddonOffsets")]
// Breaking api changes: these should be split into a PartyFinder subdirectory
@@ -68,39 +55,21 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "breaking api change", Scope = "member", Target = "~E:Dalamud.Game.Internal.Gui.PartyFinderGui.ReceiveListing")]
// Breaking api changes
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.Actors.Resolvers.ClassJob.Id")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.Actors.Resolvers.World.Id")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.Actors.Types.Actor.Address")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.BaseAddressResolver.DebugScannedValues")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Gui.Addon.Addon.Address")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Gui.Addon.Addon.addonStruct")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Gui.GameGui.GetBaseUIObject")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Text.SeStringHandling.Payload.DataResolver")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.Actors")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.TerritoryType")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.TerritoryChanged")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.ClientLanguage")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.JobGauges")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.PartyList")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.KeyState")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.GamepadState")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.Condition")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.ClientState.Targets")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Interface.InterfaceManager.LastImGuiIoPtr")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "type", Target = "~T:Dalamud.Game.ClientState.Actors.Types.PartyMember")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Interface.InterfaceManager.OnBuildFonts")]
-[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.Actors.Types.Actor.Address")]
-[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.Structs.JobGauge.BLMGauge.NumUmbralHearts")]
-[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.Structs.JobGauge.DNCGauge.NumCompleteSteps")]
-[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.ClientState.Structs.JobGauge.DRKGauge.ShadowTimeRemaining")]
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Text.SeStringHandling.Payload.START_BYTE")]
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Text.SeStringHandling.Payload.END_BYTE")]
[assembly: SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "Unused, but eventually, maybe.", Scope = "member", Target = "~F:Dalamud.Game.ClientState.PartyList.address")]
-[assembly: SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "breaking api change", Scope = "member", Target = "~E:Dalamud.Game.ClientState.ClientState.CfPop")]
-[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Framework.StatsHistory")]
-[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Framework.StatsHistory")]
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1204:Static elements should appear before instance elements", Justification = "breaking api change, move to util", Scope = "type", Target = "~T:Dalamud.Game.Text.EnumExtensions")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "breaking api change, move to util", Scope = "type", Target = "~T:Dalamud.Game.Text.EnumExtensions")]
+[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Framework.StatsHistory")]
+[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Framework.StatsHistory")]
+[assembly: SuppressMessage("Usage", "CA2211:Non-constant fields should not be visible", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Framework.StatsHistory")]
+[assembly: SuppressMessage("Usage", "CA2208:Instantiate argument exceptions correctly", Justification = "Appears to be a bug, it is being used correctly", Scope = "member", Target = "~M:Dalamud.Data.DataManager.Initialize(System.String)")]
// I mostly didnt care to do these.
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "breaking api change", Scope = "member", Target = "~F:Dalamud.Game.Internal.Network.GameNetwork.OnNetworkMessage")]
diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs
index f709f2a63..90c611844 100644
--- a/Dalamud/Hooking/Hook.cs
+++ b/Dalamud/Hooking/Hook.cs
@@ -3,7 +3,8 @@ using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-using EasyHook;
+using CoreHook;
+using Dalamud.Hooking.Internal;
namespace Dalamud.Hooking
{
@@ -30,8 +31,8 @@ namespace Dalamud.Hooking
{
this.hookInfo = LocalHook.Create(address, detour, null); // Installs a hook here
this.address = address;
- this.original = Marshal.GetDelegateForFunctionPointer(this.hookInfo.HookBypassAddress);
- HookInfo.TrackedHooks.Add(new HookInfo() { Delegate = detour, Hook = this, Assembly = Assembly.GetCallingAssembly() });
+ this.original = Marshal.GetDelegateForFunctionPointer(this.hookInfo.OriginalAddress);
+ HookManager.TrackedHooks.Add(new HookInfo() { Delegate = detour, Hook = this, Assembly = Assembly.GetCallingAssembly() });
}
///
@@ -130,9 +131,8 @@ namespace Dalamud.Hooking
return;
}
- this.hookInfo.Dispose();
-
this.IsDisposed = true;
+ this.hookInfo.Dispose();
}
///
diff --git a/Dalamud/Hooking/HookInfo.cs b/Dalamud/Hooking/Internal/HookInfo.cs
similarity index 89%
rename from Dalamud/Hooking/HookInfo.cs
rename to Dalamud/Hooking/Internal/HookInfo.cs
index 85a815c0d..a702fed27 100644
--- a/Dalamud/Hooking/HookInfo.cs
+++ b/Dalamud/Hooking/Internal/HookInfo.cs
@@ -3,18 +3,13 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
-namespace Dalamud.Hooking
+namespace Dalamud.Hooking.Internal
{
///
/// Class containing information about registered hooks.
///
internal class HookInfo
{
- ///
- /// Static list of tracked and registered hooks.
- ///
- internal static readonly List TrackedHooks = new();
-
private ulong? inProcessMemory = 0;
///
diff --git a/Dalamud/Hooking/Internal/HookManager.cs b/Dalamud/Hooking/Internal/HookManager.cs
new file mode 100644
index 000000000..1651c696e
--- /dev/null
+++ b/Dalamud/Hooking/Internal/HookManager.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+
+namespace Dalamud.Hooking.Internal
+{
+ ///
+ /// This class manages the final disposition of hooks, cleaning up any that have not reverted their changes.
+ ///
+ internal class HookManager : IDisposable
+ {
+ // private readonly Dalamud dalamud;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Dalamud instance.
+ public HookManager(Dalamud dalamud)
+ {
+ _ = dalamud;
+ // this.dalamud = dalamud;
+ }
+
+ ///
+ /// Gets a static list of tracked and registered hooks.
+ ///
+ internal static List TrackedHooks { get; } = new();
+
+ ///
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/Dalamud/Hooking/IDalamudHook.cs b/Dalamud/Hooking/Internal/IDalamudHook.cs
similarity index 94%
rename from Dalamud/Hooking/IDalamudHook.cs
rename to Dalamud/Hooking/Internal/IDalamudHook.cs
index d33fa47b8..8fb95195f 100644
--- a/Dalamud/Hooking/IDalamudHook.cs
+++ b/Dalamud/Hooking/Internal/IDalamudHook.cs
@@ -1,6 +1,6 @@
using System;
-namespace Dalamud.Hooking
+namespace Dalamud.Hooking.Internal
{
///
/// Interface describing a generic hook.
diff --git a/Dalamud/Interface/Animation/AnimUtil.cs b/Dalamud/Interface/Animation/AnimUtil.cs
new file mode 100644
index 000000000..cff36b21c
--- /dev/null
+++ b/Dalamud/Interface/Animation/AnimUtil.cs
@@ -0,0 +1,36 @@
+using System.Numerics;
+
+namespace Dalamud.Interface.Animation
+{
+ ///
+ /// Class providing helper functions when facilitating animations.
+ ///
+ public static class AnimUtil
+ {
+ ///
+ /// Lerp between two floats.
+ ///
+ /// The first float.
+ /// The second float.
+ /// The point to lerp to.
+ /// The lerped value.
+ public static float Lerp(float firstFloat, float secondFloat, float by)
+ {
+ return (firstFloat * (1 - @by)) + (secondFloat * by);
+ }
+
+ ///
+ /// Lerp between two vectors.
+ ///
+ /// The first vector.
+ /// The second float.
+ /// The point to lerp to.
+ /// The lerped vector.
+ public static Vector2 Lerp(Vector2 firstVector, Vector2 secondVector, float by)
+ {
+ var retX = Lerp(firstVector.X, secondVector.X, by);
+ var retY = Lerp(firstVector.Y, secondVector.Y, by);
+ return new Vector2(retX, retY);
+ }
+ }
+}
diff --git a/Dalamud/Interface/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs
new file mode 100644
index 000000000..441108e6f
--- /dev/null
+++ b/Dalamud/Interface/Animation/Easing.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Diagnostics;
+using System.Numerics;
+
+namespace Dalamud.Interface.Animation
+{
+ ///
+ /// Base class facilitating the implementation of easing functions.
+ ///
+ public abstract class Easing
+ {
+ // TODO: Use game delta time here instead
+ private readonly Stopwatch animationTimer = new();
+
+ private double valueInternal;
+
+ ///
+ /// Initializes a new instance of the class with the specified duration.
+ ///
+ /// The animation duration.
+ protected Easing(TimeSpan duration)
+ {
+ this.Duration = duration;
+ }
+
+ ///
+ /// Gets or sets the origin point of the animation.
+ ///
+ public Vector2? Point1 { get; set; }
+
+ ///
+ /// Gets or sets the destination point of the animation.
+ ///
+ public Vector2? Point2 { get; set; }
+
+ ///
+ /// Gets the resulting point at the current timestep.
+ ///
+ public Vector2 EasedPoint { get; private set; }
+
+ ///
+ /// Gets or sets the current value of the animation, from 0 to 1.
+ ///
+ public double Value
+ {
+ get => this.valueInternal;
+ protected set
+ {
+ this.valueInternal = value;
+
+ if (this.Point1.HasValue && this.Point2.HasValue)
+ this.EasedPoint = AnimUtil.Lerp(this.Point1.Value, this.Point2.Value, (float)this.valueInternal);
+ }
+ }
+
+ ///
+ /// Gets or sets the duration of the animation.
+ ///
+ public TimeSpan Duration { get; set; }
+
+ ///
+ /// Gets a value indicating whether or not the animation is running.
+ ///
+ public bool IsRunning => this.animationTimer.IsRunning;
+
+ ///
+ /// Gets a value indicating whether or not the animation is done.
+ ///
+ public bool IsDone => this.animationTimer.ElapsedMilliseconds > this.Duration.TotalMilliseconds;
+
+ ///
+ /// Gets the progress of the animation, from 0 to 1.
+ ///
+ protected double Progress => this.animationTimer.ElapsedMilliseconds / this.Duration.TotalMilliseconds;
+
+ ///
+ /// Starts the animation from where it was last stopped, or from the start if it was never started before.
+ ///
+ public void Start()
+ {
+ this.animationTimer.Start();
+ }
+
+ ///
+ /// Stops the animation at the current point.
+ ///
+ public void Stop()
+ {
+ this.animationTimer.Stop();
+ }
+
+ ///
+ /// Restarts the animation.
+ ///
+ public void Restart()
+ {
+ this.animationTimer.Restart();
+ }
+
+ ///
+ /// Updates the animation.
+ ///
+ public abstract void Update();
+ }
+}
diff --git a/Dalamud/Interface/Animation/EasingFunctions/InCirc.cs b/Dalamud/Interface/Animation/EasingFunctions/InCirc.cs
new file mode 100644
index 000000000..952e3539d
--- /dev/null
+++ b/Dalamud/Interface/Animation/EasingFunctions/InCirc.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Dalamud.Interface.Animation.EasingFunctions
+{
+ ///
+ /// Class providing an "InCirc" easing animation.
+ ///
+ public class InCirc : Easing
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public InCirc(TimeSpan duration)
+ : base(duration)
+ {
+ // ignored
+ }
+
+ ///
+ public override void Update()
+ {
+ var p = this.Progress;
+ this.Value = 1 - Math.Sqrt(1 - Math.Pow(p, 2));
+ }
+ }
+}
diff --git a/Dalamud/Interface/Animation/EasingFunctions/InCubic.cs b/Dalamud/Interface/Animation/EasingFunctions/InCubic.cs
new file mode 100644
index 000000000..ebca3203d
--- /dev/null
+++ b/Dalamud/Interface/Animation/EasingFunctions/InCubic.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Dalamud.Interface.Animation.EasingFunctions
+{
+ ///
+ /// Class providing an "InCubic" easing animation.
+ ///
+ public class InCubic : Easing
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public InCubic(TimeSpan duration)
+ : base(duration)
+ {
+ // ignored
+ }
+
+ ///
+ public override void Update()
+ {
+ var p = this.Progress;
+ this.Value = p * p * p;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Animation/EasingFunctions/InElastic.cs b/Dalamud/Interface/Animation/EasingFunctions/InElastic.cs
new file mode 100644
index 000000000..97159df47
--- /dev/null
+++ b/Dalamud/Interface/Animation/EasingFunctions/InElastic.cs
@@ -0,0 +1,33 @@
+using System;
+
+namespace Dalamud.Interface.Animation.EasingFunctions
+{
+ ///
+ /// Class providing an "InElastic" easing animation.
+ ///
+ public class InElastic : Easing
+ {
+ private const double Constant = (2 * Math.PI) / 3;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public InElastic(TimeSpan duration)
+ : base(duration)
+ {
+ // ignored
+ }
+
+ ///
+ public override void Update()
+ {
+ var p = this.Progress;
+ this.Value = p == 0
+ ? 0
+ : p == 1
+ ? 1
+ : -Math.Pow(2, (10 * p) - 10) * Math.Sin(((p * 10) - 10.75) * Constant);
+ }
+ }
+}
diff --git a/Dalamud/Interface/Animation/EasingFunctions/InOutCirc.cs b/Dalamud/Interface/Animation/EasingFunctions/InOutCirc.cs
new file mode 100644
index 000000000..97fd1a2d2
--- /dev/null
+++ b/Dalamud/Interface/Animation/EasingFunctions/InOutCirc.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace Dalamud.Interface.Animation.EasingFunctions
+{
+ ///
+ /// Class providing an "InOutCirc" easing animation.
+ ///
+ public class InOutCirc : Easing
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public InOutCirc(TimeSpan duration)
+ : base(duration)
+ {
+ // ignored
+ }
+
+ ///
+ public override void Update()
+ {
+ var p = this.Progress;
+ this.Value = p < 0.5
+ ? (1 - Math.Sqrt(1 - Math.Pow(2 * p, 2))) / 2
+ : (Math.Sqrt(1 - Math.Pow((-2 * p) + 2, 2)) + 1) / 2;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Animation/EasingFunctions/InOutCubic.cs b/Dalamud/Interface/Animation/EasingFunctions/InOutCubic.cs
new file mode 100644
index 000000000..c075da538
--- /dev/null
+++ b/Dalamud/Interface/Animation/EasingFunctions/InOutCubic.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Dalamud.Interface.Animation.EasingFunctions
+{
+ ///
+ /// Class providing an "InOutCubic" easing animation.
+ ///
+ public class InOutCubic : Easing
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public InOutCubic(TimeSpan duration)
+ : base(duration)
+ {
+ // ignored
+ }
+
+ ///
+ public override void Update()
+ {
+ var p = this.Progress;
+ this.Value = p < 0.5 ? 4 * p * p * p : 1 - (Math.Pow((-2 * p) + 2, 3) / 2);
+ }
+ }
+}
diff --git a/Dalamud/Interface/Animation/EasingFunctions/InOutElastic.cs b/Dalamud/Interface/Animation/EasingFunctions/InOutElastic.cs
new file mode 100644
index 000000000..7bb36dd74
--- /dev/null
+++ b/Dalamud/Interface/Animation/EasingFunctions/InOutElastic.cs
@@ -0,0 +1,35 @@
+using System;
+
+namespace Dalamud.Interface.Animation.EasingFunctions
+{
+ ///
+ /// Class providing an "InOutCirc" easing animation.
+ ///
+ public class InOutElastic : Easing
+ {
+ private const double Constant = (2 * Math.PI) / 4.5;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public InOutElastic(TimeSpan duration)
+ : base(duration)
+ {
+ // ignored
+ }
+
+ ///
+ public override void Update()
+ {
+ var p = this.Progress;
+ this.Value = p == 0
+ ? 0
+ : p == 1
+ ? 1
+ : p < 0.5
+ ? -(Math.Pow(2, (20 * p) - 10) * Math.Sin(((20 * p) - 11.125) * Constant)) / 2
+ : (Math.Pow(2, (-20 * p) + 10) * Math.Sin(((20 * p) - 11.125) * Constant) / 2) + 1;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Animation/EasingFunctions/InOutQuint.cs b/Dalamud/Interface/Animation/EasingFunctions/InOutQuint.cs
new file mode 100644
index 000000000..70f3123aa
--- /dev/null
+++ b/Dalamud/Interface/Animation/EasingFunctions/InOutQuint.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Dalamud.Interface.Animation.EasingFunctions
+{
+ ///
+ /// Class providing an "InOutQuint" easing animation.
+ ///
+ public class InOutQuint : Easing
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public InOutQuint(TimeSpan duration)
+ : base(duration)
+ {
+ // ignored
+ }
+
+ ///
+ public override void Update()
+ {
+ var p = this.Progress;
+ this.Value = p < 0.5 ? 16 * p * p * p * p * p : 1 - (Math.Pow((-2 * p) + 2, 5) / 2);
+ }
+ }
+}
diff --git a/Dalamud/Interface/Animation/EasingFunctions/InOutSine.cs b/Dalamud/Interface/Animation/EasingFunctions/InOutSine.cs
new file mode 100644
index 000000000..4808079d3
--- /dev/null
+++ b/Dalamud/Interface/Animation/EasingFunctions/InOutSine.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Dalamud.Interface.Animation.EasingFunctions
+{
+ ///
+ /// Class providing an "InOutSine" easing animation.
+ ///
+ public class InOutSine : Easing
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public InOutSine(TimeSpan duration)
+ : base(duration)
+ {
+ // ignored
+ }
+
+ ///
+ public override void Update()
+ {
+ var p = this.Progress;
+ this.Value = -(Math.Cos(Math.PI * p) - 1) / 2;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Animation/EasingFunctions/InQuint.cs b/Dalamud/Interface/Animation/EasingFunctions/InQuint.cs
new file mode 100644
index 000000000..42604f136
--- /dev/null
+++ b/Dalamud/Interface/Animation/EasingFunctions/InQuint.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Dalamud.Interface.Animation.EasingFunctions
+{
+ ///
+ /// Class providing an "InQuint" easing animation.
+ ///
+ public class InQuint : Easing
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public InQuint(TimeSpan duration)
+ : base(duration)
+ {
+ // ignored
+ }
+
+ ///
+ public override void Update()
+ {
+ var p = this.Progress;
+ this.Value = p * p * p * p * p;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Animation/EasingFunctions/InSine.cs b/Dalamud/Interface/Animation/EasingFunctions/InSine.cs
new file mode 100644
index 000000000..aaa19aa40
--- /dev/null
+++ b/Dalamud/Interface/Animation/EasingFunctions/InSine.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Dalamud.Interface.Animation.EasingFunctions
+{
+ ///
+ /// Class providing an "InSine" easing animation.
+ ///
+ public class InSine : Easing
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public InSine(TimeSpan duration)
+ : base(duration)
+ {
+ // ignored
+ }
+
+ ///
+ public override void Update()
+ {
+ var p = this.Progress;
+ this.Value = 1 - Math.Cos((p * Math.PI) / 2);
+ }
+ }
+}
diff --git a/Dalamud/Interface/Animation/EasingFunctions/OutCirc.cs b/Dalamud/Interface/Animation/EasingFunctions/OutCirc.cs
new file mode 100644
index 000000000..da7b0029a
--- /dev/null
+++ b/Dalamud/Interface/Animation/EasingFunctions/OutCirc.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Dalamud.Interface.Animation.EasingFunctions
+{
+ ///
+ /// Class providing an "OutCirc" easing animation.
+ ///
+ public class OutCirc : Easing
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public OutCirc(TimeSpan duration)
+ : base(duration)
+ {
+ // ignored
+ }
+
+ ///
+ public override void Update()
+ {
+ var p = this.Progress;
+ this.Value = Math.Sqrt(1 - Math.Pow(p - 1, 2));
+ }
+ }
+}
diff --git a/Dalamud/Interface/Animation/EasingFunctions/OutCubic.cs b/Dalamud/Interface/Animation/EasingFunctions/OutCubic.cs
new file mode 100644
index 000000000..e527b228f
--- /dev/null
+++ b/Dalamud/Interface/Animation/EasingFunctions/OutCubic.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Dalamud.Interface.Animation.EasingFunctions
+{
+ ///
+ /// Class providing an "OutCubic" easing animation.
+ ///
+ public class OutCubic : Easing
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public OutCubic(TimeSpan duration)
+ : base(duration)
+ {
+ // ignored
+ }
+
+ ///
+ public override void Update()
+ {
+ var p = this.Progress;
+ this.Value = 1 - Math.Pow(1 - p, 3);
+ }
+ }
+}
diff --git a/Dalamud/Interface/Animation/EasingFunctions/OutElastic.cs b/Dalamud/Interface/Animation/EasingFunctions/OutElastic.cs
new file mode 100644
index 000000000..3475c4a72
--- /dev/null
+++ b/Dalamud/Interface/Animation/EasingFunctions/OutElastic.cs
@@ -0,0 +1,33 @@
+using System;
+
+namespace Dalamud.Interface.Animation.EasingFunctions
+{
+ ///
+ /// Class providing an "OutElastic" easing animation.
+ ///
+ public class OutElastic : Easing
+ {
+ private const double Constant = (2 * Math.PI) / 3;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public OutElastic(TimeSpan duration)
+ : base(duration)
+ {
+ // ignored
+ }
+
+ ///
+ public override void Update()
+ {
+ var p = this.Progress;
+ this.Value = p == 0
+ ? 0
+ : p == 1
+ ? 1
+ : (Math.Pow(2, -10 * p) * Math.Sin(((p * 10) - 0.75) * Constant)) + 1;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Animation/EasingFunctions/OutQuint.cs b/Dalamud/Interface/Animation/EasingFunctions/OutQuint.cs
new file mode 100644
index 000000000..c99c77a57
--- /dev/null
+++ b/Dalamud/Interface/Animation/EasingFunctions/OutQuint.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Dalamud.Interface.Animation.EasingFunctions
+{
+ ///
+ /// Class providing an "OutQuint" easing animation.
+ ///
+ public class OutQuint : Easing
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public OutQuint(TimeSpan duration)
+ : base(duration)
+ {
+ // ignored
+ }
+
+ ///
+ public override void Update()
+ {
+ var p = this.Progress;
+ this.Value = 1 - Math.Pow(1 - p, 5);
+ }
+ }
+}
diff --git a/Dalamud/Interface/Animation/EasingFunctions/OutSine.cs b/Dalamud/Interface/Animation/EasingFunctions/OutSine.cs
new file mode 100644
index 000000000..c1becf81c
--- /dev/null
+++ b/Dalamud/Interface/Animation/EasingFunctions/OutSine.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace Dalamud.Interface.Animation.EasingFunctions
+{
+ ///
+ /// Class providing an "OutSine" easing animation.
+ ///
+ public class OutSine : Easing
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The duration of the animation.
+ public OutSine(TimeSpan duration)
+ : base(duration)
+ {
+ // ignored
+ }
+
+ ///
+ public override void Update()
+ {
+ var p = this.Progress;
+ this.Value = Math.Sin((p * Math.PI) / 2);
+ }
+ }
+}
diff --git a/Dalamud/Interface/Colors/ColorDemoWindow.cs b/Dalamud/Interface/Colors/ColorDemoWindow.cs
deleted file mode 100644
index 4dc4e30c9..000000000
--- a/Dalamud/Interface/Colors/ColorDemoWindow.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Numerics;
-
-using Dalamud.Interface.Windowing;
-using ImGuiNET;
-
-namespace Dalamud.Interface.Colors
-{
- ///
- /// color Demo Window to view custom ImGui colors.
- ///
- internal class ColorDemoWindow : Window
- {
- private readonly List> colors;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public ColorDemoWindow()
- : base("Dalamud Colors Demo")
- {
- this.Size = new Vector2(600, 500);
- this.SizeCondition = ImGuiCond.FirstUseEver;
- this.colors = new List>
- {
- Demo("White", ImGuiColors.White),
- Demo("DalamudRed", ImGuiColors.DalamudRed),
- Demo("DalamudGrey", ImGuiColors.DalamudGrey),
- Demo("DalamudGrey2", ImGuiColors.DalamudGrey2),
- Demo("DalamudGrey3", ImGuiColors.DalamudGrey3),
- Demo("DalamudWhite", ImGuiColors.DalamudWhite),
- Demo("DalamudWhite2", ImGuiColors.DalamudWhite2),
- Demo("DalamudOrange", ImGuiColors.DalamudOrange),
- Demo("TankBlue", ImGuiColors.TankBlue),
- Demo("HealerGreen", ImGuiColors.HealerGreen),
- Demo("DPSRed", ImGuiColors.DPSRed),
- };
- this.colors = this.colors.OrderBy(colorDemo => colorDemo.Key).ToList();
- }
-
- ///
- public override void Draw()
- {
- ImGui.BeginChild("color_scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.AlwaysVerticalScrollbar | ImGuiWindowFlags.HorizontalScrollbar);
- ImGui.Text("This is a collection of UI colors you can use in your plugin.");
- ImGui.Separator();
- foreach (var color in this.colors)
- {
- ImGui.TextColored(color.Value, color.Key);
- }
-
- ImGui.EndChild();
- }
-
- private static KeyValuePair Demo(string name, Vector4 color)
- {
- return new KeyValuePair(name, color);
- }
- }
-}
diff --git a/Dalamud/Interface/Components/ComponentDemoWindow.cs b/Dalamud/Interface/Components/ComponentDemoWindow.cs
deleted file mode 100644
index bb62fba66..000000000
--- a/Dalamud/Interface/Components/ComponentDemoWindow.cs
+++ /dev/null
@@ -1,95 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Numerics;
-
-using Dalamud.Interface.Colors;
-using Dalamud.Interface.Windowing;
-using ImGuiNET;
-
-namespace Dalamud.Interface.Components
-{
- ///
- /// Component Demo Window to view custom ImGui components.
- ///
- internal class ComponentDemoWindow : Window
- {
- private readonly List> componentDemos;
- private Vector4 defaultColor = ImGuiColors.DalamudOrange;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public ComponentDemoWindow()
- : base("Dalamud Components Demo")
- {
- this.Size = new Vector2(600, 500);
- this.SizeCondition = ImGuiCond.FirstUseEver;
- this.componentDemos = new List>
- {
- Demo("Test", ImGuiComponents.Test),
- Demo("HelpMarker", HelpMarkerDemo),
- Demo("IconButton", IconButtonDemo),
- Demo("TextWithLabel", TextWithLabelDemo),
- Demo("ColorPickerWithPalette", this.ColorPickerWithPaletteDemo),
- };
- }
-
- ///
- public override void Draw()
- {
- ImGui.BeginChild("comp_scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.AlwaysVerticalScrollbar | ImGuiWindowFlags.HorizontalScrollbar);
- ImGui.Text("This is a collection of UI components you can use in your plugin.");
-
- for (var i = 0; i < this.componentDemos.Count; i++)
- {
- var componentDemo = this.componentDemos[i];
-
- if (ImGui.CollapsingHeader($"{componentDemo.Key}###comp{i}"))
- {
- componentDemo.Value();
- }
- }
-
- ImGui.EndChild();
- }
-
- private static void HelpMarkerDemo()
- {
- ImGui.Text("Hover over the icon to learn more.");
- ImGuiComponents.HelpMarker("help me!");
- }
-
- private static void IconButtonDemo()
- {
- ImGui.Text("Click on the icon to use as a button.");
- ImGui.SameLine();
- if (ImGuiComponents.IconButton(1, FontAwesomeIcon.Carrot))
- {
- ImGui.OpenPopup("IconButtonDemoPopup");
- }
-
- if (ImGui.BeginPopup("IconButtonDemoPopup"))
- {
- ImGui.Text("You clicked!");
- ImGui.EndPopup();
- }
- }
-
- private static void TextWithLabelDemo()
- {
- ImGuiComponents.TextWithLabel("Label", "Hover to see more", "more");
- }
-
- private static KeyValuePair Demo(string name, Action func)
- {
- return new KeyValuePair(name, func);
- }
-
- private void ColorPickerWithPaletteDemo()
- {
- ImGui.Text("Click on the color button to use the picker.");
- ImGui.SameLine();
- this.defaultColor = ImGuiComponents.ColorPickerWithPalette(1, "ColorPickerWithPalette Demo", this.defaultColor);
- }
- }
-}
diff --git a/Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs b/Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs
new file mode 100644
index 000000000..225b171bb
--- /dev/null
+++ b/Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs
@@ -0,0 +1,76 @@
+using System.Numerics;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.Components
+{
+ ///
+ /// Class containing various methods providing ImGui components.
+ ///
+ public static partial class ImGuiComponents
+ {
+ ///
+ /// Alpha modified IconButton component to use an icon as a button with alpha and color options.
+ ///
+ /// The icon for the button.
+ /// The ID of the button.
+ /// The default color of the button.
+ /// The color of the button when active.
+ /// The color of the button when hovered.
+ /// A multiplier for the current alpha levels.
+ /// Indicator if button is clicked.
+ public static bool DisabledButton(FontAwesomeIcon icon, int? id = null, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null, float alphaMult = .5f)
+ {
+ ImGui.PushFont(UiBuilder.IconFont);
+
+ var text = icon.ToIconString();
+ if (id.HasValue)
+ text = $"{text}{id}";
+
+ var button = DisabledButton(text, defaultColor, activeColor, hoveredColor, alphaMult);
+
+ ImGui.PopFont();
+
+ return button;
+ }
+
+ ///
+ /// Alpha modified Button component to use as a disabled button with alpha and color options.
+ ///
+ /// The button label with ID.
+ /// The default color of the button.
+ /// The color of the button when active.
+ /// The color of the button when hovered.
+ /// A multiplier for the current alpha levels.
+ /// Indicator if button is clicked.
+ public static bool DisabledButton(string labelWithId, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null, float alphaMult = .5f)
+ {
+ if (defaultColor.HasValue)
+ ImGui.PushStyleColor(ImGuiCol.Button, defaultColor.Value);
+
+ if (activeColor.HasValue)
+ ImGui.PushStyleColor(ImGuiCol.ButtonActive, activeColor.Value);
+
+ if (hoveredColor.HasValue)
+ ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hoveredColor.Value);
+
+ var style = ImGui.GetStyle();
+ ImGui.PushStyleVar(ImGuiStyleVar.Alpha, style.Alpha * alphaMult);
+
+ var button = ImGui.Button(labelWithId);
+
+ ImGui.PopStyleVar();
+
+ if (defaultColor.HasValue)
+ ImGui.PopStyleColor();
+
+ if (activeColor.HasValue)
+ ImGui.PopStyleColor();
+
+ if (hoveredColor.HasValue)
+ ImGui.PopStyleColor();
+
+ return button;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs
index 3b6f8f213..3d65deb74 100644
--- a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs
+++ b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs
@@ -9,6 +9,14 @@ namespace Dalamud.Interface.Components
///
public static partial class ImGuiComponents
{
+ ///
+ /// IconButton component to use an icon as a button.
+ ///
+ /// The icon for the button.
+ /// Indicator if button is clicked.
+ public static bool IconButton(FontAwesomeIcon icon)
+ => IconButton(icon, null, null, null);
+
///
/// IconButton component to use an icon as a button.
///
@@ -16,16 +24,26 @@ namespace Dalamud.Interface.Components
/// The icon for the button.
/// Indicator if button is clicked.
public static bool IconButton(int id, FontAwesomeIcon icon)
- {
- ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero);
- ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero);
- ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero);
- ImGui.PushFont(UiBuilder.IconFont);
- var button = ImGui.Button($"{icon.ToIconString()}{id}");
- ImGui.PopFont();
- ImGui.PopStyleColor(3);
- return button;
- }
+ => IconButton(id, icon, null, null, null);
+
+ ///
+ /// IconButton component to use an icon as a button.
+ ///
+ /// Text already containing the icon string.
+ /// Indicator if button is clicked.
+ public static bool IconButton(string iconText)
+ => IconButton(iconText, null, null, null);
+
+ ///
+ /// IconButton component to use an icon as a button.
+ ///
+ /// The icon for the button.
+ /// The default color of the button.
+ /// The color of the button when active.
+ /// The color of the button when hovered.
+ /// Indicator if button is clicked.
+ public static bool IconButton(FontAwesomeIcon icon, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null)
+ => IconButton($"{icon.ToIconString()}", defaultColor, activeColor, hoveredColor);
///
/// IconButton component to use an icon as a button with color options.
@@ -36,15 +54,48 @@ namespace Dalamud.Interface.Components
/// The color of the button when active.
/// The color of the button when hovered.
/// Indicator if button is clicked.
- public static bool IconButton(int id, FontAwesomeIcon icon, Vector4 defaultColor, Vector4 activeColor, Vector4 hoveredColor)
+ public static bool IconButton(int id, FontAwesomeIcon icon, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null)
+ => IconButton($"{icon.ToIconString()}{id}", defaultColor, activeColor, hoveredColor);
+
+ ///
+ /// IconButton component to use an icon as a button with color options.
+ ///
+ /// Text already containing the icon string.
+ /// The default color of the button.
+ /// The color of the button when active.
+ /// The color of the button when hovered.
+ /// Indicator if button is clicked.
+ public static bool IconButton(string iconText, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null)
{
- ImGui.PushStyleColor(ImGuiCol.Button, defaultColor);
- ImGui.PushStyleColor(ImGuiCol.ButtonActive, activeColor);
- ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hoveredColor);
+ var numColors = 0;
+
+ if (defaultColor.HasValue)
+ {
+ ImGui.PushStyleColor(ImGuiCol.Button, defaultColor.Value);
+ numColors++;
+ }
+
+ if (activeColor.HasValue)
+ {
+ ImGui.PushStyleColor(ImGuiCol.ButtonActive, activeColor.Value);
+ numColors++;
+ }
+
+ if (hoveredColor.HasValue)
+ {
+ ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hoveredColor.Value);
+ numColors++;
+ }
+
ImGui.PushFont(UiBuilder.IconFont);
- var button = ImGui.Button($"{icon.ToIconString()}{id}");
+
+ var button = ImGui.Button(iconText);
+
ImGui.PopFont();
- ImGui.PopStyleColor(3);
+
+ if (numColors > 0)
+ ImGui.PopStyleColor(numColors);
+
return button;
}
}
diff --git a/Dalamud/Interface/Components/ImGuiComponents.TextWithLabel.cs b/Dalamud/Interface/Components/ImGuiComponents.TextWithLabel.cs
index feb127d2a..991fefb3a 100644
--- a/Dalamud/Interface/Components/ImGuiComponents.TextWithLabel.cs
+++ b/Dalamud/Interface/Components/ImGuiComponents.TextWithLabel.cs
@@ -13,8 +13,7 @@ namespace Dalamud.Interface.Components
/// The label for text.
/// The text value.
/// The hint to show on hover.
- public static void TextWithLabel(
- string label, string value, string hint = "")
+ public static void TextWithLabel(string label, string value, string hint = "")
{
ImGui.Text(label + ": ");
ImGui.SameLine();
diff --git a/Dalamud/Interface/DalamudDataWindow.cs b/Dalamud/Interface/DalamudDataWindow.cs
deleted file mode 100644
index e34e68dae..000000000
--- a/Dalamud/Interface/DalamudDataWindow.cs
+++ /dev/null
@@ -1,716 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Dynamic;
-using System.Linq;
-using System.Numerics;
-
-using Dalamud.Game.ClientState;
-using Dalamud.Game.ClientState.Actors.Types;
-using Dalamud.Game.ClientState.Actors.Types.NonPlayer;
-using Dalamud.Game.ClientState.Structs.JobGauge;
-using Dalamud.Game.Internal;
-using Dalamud.Game.Internal.Gui.Addon;
-using Dalamud.Game.Internal.Gui.Toast;
-using Dalamud.Game.Text;
-using Dalamud.Interface.Colors;
-using Dalamud.Interface.Windowing;
-using Dalamud.Plugin;
-using ImGuiNET;
-using ImGuiScene;
-using Newtonsoft.Json;
-using Serilog;
-
-namespace Dalamud.Interface
-{
- ///
- /// Class responsible for drawing the data/debug window.
- ///
- internal class DalamudDataWindow : Window
- {
- private readonly Dalamud dalamud;
-
- private bool wasReady;
- private string serverOpString;
-
- private int currentKind;
- private string[] dataKinds = new[]
- {
- "ServerOpCode", "Address", "Actor Table", "Font Test", "Party List", "Plugin IPC", "Condition",
- "Gauge", "Command", "Addon", "Addon Inspector", "StartInfo", "Target", "Toast", "ImGui", "Tex", "Gamepad",
- };
-
- private bool drawActors = false;
- private float maxActorDrawDistance = 20;
-
- private string inputSig = string.Empty;
- private IntPtr sigResult = IntPtr.Zero;
-
- private string inputAddonName = string.Empty;
- private int inputAddonIndex;
- private Addon resultAddon;
-
- private IntPtr findAgentInterfacePtr;
-
- private bool resolveGameData = false;
-
- private UIDebug addonInspector = null;
-
- private string inputTextToast = string.Empty;
- private int toastPosition = 0;
- private int toastSpeed = 0;
- private int questToastPosition = 0;
- private bool questToastSound = false;
- private int questToastIconId = 0;
- private bool questToastCheckmark = false;
-
- private string inputTexPath = string.Empty;
- private TextureWrap debugTex = null;
- private Vector2 inputTexUv0 = Vector2.Zero;
- private Vector2 inputTexUv1 = Vector2.One;
- private Vector4 inputTintCol = Vector4.One;
- private Vector2 inputTexScale = Vector2.Zero;
-
- private uint copyButtonIndex = 0;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The Dalamud instance to access data of.
- public DalamudDataWindow(Dalamud dalamud)
- : base("Dalamud Data")
- {
- this.dalamud = dalamud;
-
- this.Size = new Vector2(500, 500);
- this.SizeCondition = ImGuiCond.FirstUseEver;
-
- this.Load();
- }
-
- ///
- /// Set the DataKind dropdown menu.
- ///
- /// Data kind name, can be lower and/or without spaces.
- public void SetDataKind(string dataKind)
- {
- if (string.IsNullOrEmpty(dataKind))
- return;
-
- if (dataKind == "ai")
- dataKind = "Addon Inspector";
-
- int index;
- dataKind = dataKind.Replace(" ", string.Empty).ToLower();
- var dataKinds = this.dataKinds.Select(k => k.Replace(" ", string.Empty).ToLower()).ToList();
- if ((index = dataKinds.IndexOf(dataKind)) != -1)
- {
- this.currentKind = index;
- }
- else
- {
- this.dalamud.Framework.Gui.Chat.PrintError("/xldata: Invalid Data Type");
- }
- }
-
- ///
- /// Draw the window via ImGui.
- ///
- public override void Draw()
- {
- this.copyButtonIndex = 0;
-
- // Main window
- if (ImGui.Button("Force Reload"))
- this.Load();
- ImGui.SameLine();
- var copy = ImGui.Button("Copy all");
- ImGui.SameLine();
-
- ImGui.Combo("Data kind", ref this.currentKind, this.dataKinds, this.dataKinds.Length);
-
- ImGui.Checkbox("Resolve GameData", ref this.resolveGameData);
-
- ImGui.BeginChild("scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.HorizontalScrollbar);
-
- if (copy)
- ImGui.LogToClipboard();
-
- ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0));
-
- try
- {
- if (this.wasReady)
- {
- switch (this.currentKind)
- {
- case 0:
- ImGui.TextUnformatted(this.serverOpString);
- break;
- case 1:
-
- ImGui.InputText(".text sig", ref this.inputSig, 400);
- if (ImGui.Button("Resolve"))
- {
- try
- {
- this.sigResult = this.dalamud.SigScanner.ScanText(this.inputSig);
- }
- catch (KeyNotFoundException)
- {
- this.sigResult = new IntPtr(-1);
- }
- }
-
- ImGui.Text($"Result: {this.sigResult.ToInt64():X}");
- ImGui.SameLine();
- if (ImGui.Button($"C{this.copyButtonIndex++}"))
- ImGui.SetClipboardText(this.sigResult.ToInt64().ToString("x"));
-
- foreach (var debugScannedValue in BaseAddressResolver.DebugScannedValues)
- {
- ImGui.TextUnformatted($"{debugScannedValue.Key}");
- foreach (var valueTuple in debugScannedValue.Value)
- {
- ImGui.TextUnformatted(
- $" {valueTuple.Item1} - 0x{valueTuple.Item2.ToInt64():x}");
- ImGui.SameLine();
-
- if (ImGui.Button($"C##copyAddress{this.copyButtonIndex++}"))
- ImGui.SetClipboardText(valueTuple.Item2.ToInt64().ToString("x"));
- }
- }
-
- break;
-
- // AT
- case 2:
- this.DrawActorTable();
-
- break;
-
- // Font
- case 3:
- var specialChars = string.Empty;
- for (var i = 0xE020; i <= 0xE0DB; i++)
- specialChars += $"0x{i:X} - {(SeIconChar)i} - {(char)i}\n";
-
- ImGui.TextUnformatted(specialChars);
-
- foreach (var fontAwesomeIcon in Enum.GetValues(typeof(FontAwesomeIcon))
- .Cast())
- {
- ImGui.Text(((int)fontAwesomeIcon.ToIconChar()).ToString("X") + " - ");
- ImGui.SameLine();
-
- ImGui.PushFont(InterfaceManager.IconFont);
- ImGui.Text(fontAwesomeIcon.ToIconString());
- ImGui.PopFont();
- }
-
- break;
-
- // Party
- case 4:
- var partyString = string.Empty;
-
- if (this.dalamud.ClientState.PartyList.Length == 0)
- {
- ImGui.TextUnformatted("Data not ready.");
- }
- else
- {
- partyString += $"{this.dalamud.ClientState.PartyList.Count} Members\n";
- for (var i = 0; i < this.dalamud.ClientState.PartyList.Count; i++)
- {
- var member = this.dalamud.ClientState.PartyList[i];
- if (member == null)
- {
- partyString +=
- $"[{i}] was null\n";
- continue;
- }
-
- partyString +=
- $"[{i}] {member.CharacterName} - {member.ObjectKind} - {member.Actor.ActorId}\n";
- }
-
- ImGui.TextUnformatted(partyString);
- }
-
- break;
-
- // Subscriptions
- case 5:
- this.DrawIpcDebug();
-
- break;
-
- // Condition
- case 6:
-#if DEBUG
- ImGui.Text($"ptr: {this.dalamud.ClientState.Condition.ConditionArrayBase.ToString("X16")}");
-#endif
-
- ImGui.Text("Current Conditions:");
- ImGui.Separator();
-
- var didAny = false;
-
- for (var i = 0; i < Condition.MaxConditionEntries; i++)
- {
- var typedCondition = (ConditionFlag)i;
- var cond = this.dalamud.ClientState.Condition[typedCondition];
-
- if (!cond) continue;
-
- didAny = true;
-
- ImGui.Text($"ID: {i} Enum: {typedCondition}");
- }
-
- if (!didAny)
- ImGui.Text("None. Talk to a shop NPC or visit a market board to find out more!!!!!!!");
-
- break;
-
- // Gauge
- case 7:
- var gauge = this.dalamud.ClientState.JobGauges.Get();
- ImGui.Text($"Moon: {gauge.ContainsSeal(SealType.MOON)} Drawn: {gauge.DrawnCard()}");
-
- break;
-
- // Command
- case 8:
- foreach (var command in this.dalamud.CommandManager.Commands)
- ImGui.Text($"{command.Key}\n -> {command.Value.HelpMessage}\n -> In help: {command.Value.ShowInHelp}\n\n");
-
- break;
-
- // Addon
- case 9:
- this.DrawAddonDebug();
- break;
-
- // Addon Inspector
- case 10:
- this.addonInspector ??= new UIDebug(this.dalamud);
- this.addonInspector.Draw();
- break;
-
- // StartInfo
- case 11:
- ImGui.Text(JsonConvert.SerializeObject(this.dalamud.StartInfo, Formatting.Indented));
- break;
-
- // Target
- case 12:
- this.DrawTargetDebug();
- break;
-
- // Toast
- case 13:
- ImGui.InputText("Toast text", ref this.inputTextToast, 200);
-
- ImGui.Combo("Toast Position", ref this.toastPosition, new[] { "Bottom", "Top", }, 2);
- ImGui.Combo("Toast Speed", ref this.toastSpeed, new[] { "Slow", "Fast", }, 2);
- ImGui.Combo("Quest Toast Position", ref this.questToastPosition, new[] { "Centre", "Right", "Left" }, 3);
- ImGui.Checkbox("Quest Checkmark", ref this.questToastCheckmark);
- ImGui.Checkbox("Quest Play Sound", ref this.questToastSound);
- ImGui.InputInt("Quest Icon ID", ref this.questToastIconId);
-
- ImGuiHelpers.ScaledDummy(new Vector2(10, 10));
-
- if (ImGui.Button("Show toast"))
- {
- this.dalamud.Framework.Gui.Toast.ShowNormal(this.inputTextToast, new ToastOptions
- {
- Position = (ToastPosition)this.toastPosition,
- Speed = (ToastSpeed)this.toastSpeed,
- });
- }
-
- if (ImGui.Button("Show Quest toast"))
- {
- this.dalamud.Framework.Gui.Toast.ShowQuest(this.inputTextToast, new QuestToastOptions
- {
- Position = (QuestToastPosition)this.questToastPosition,
- DisplayCheckmark = this.questToastCheckmark,
- IconId = (uint)this.questToastIconId,
- PlaySound = this.questToastSound,
- });
- }
-
- if (ImGui.Button("Show Error toast"))
- {
- this.dalamud.Framework.Gui.Toast.ShowError(this.inputTextToast);
- }
-
- break;
-
- // ImGui
- case 14:
- ImGui.Text("Monitor count: " + ImGui.GetPlatformIO().Monitors.Size);
- ImGui.Text("OverrideGameCursor: " + this.dalamud.InterfaceManager.OverrideGameCursor);
-
- ImGui.Button("THIS IS A BUTTON###hoverTestButton");
- this.dalamud.InterfaceManager.OverrideGameCursor = !ImGui.IsItemHovered();
-
- break;
-
- // Tex
- case 15:
- ImGui.InputText("Tex Path", ref this.inputTexPath, 255);
- ImGui.InputFloat2("UV0", ref this.inputTexUv0);
- ImGui.InputFloat2("UV1", ref this.inputTexUv1);
- ImGui.InputFloat4("Tint", ref this.inputTintCol);
- ImGui.InputFloat2("Scale", ref this.inputTexScale);
-
- if (ImGui.Button("Load Tex"))
- {
- try
- {
- this.debugTex = this.dalamud.Data.GetImGuiTexture(this.inputTexPath);
- this.inputTexScale = new Vector2(this.debugTex.Width, this.debugTex.Height);
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Could not load tex.");
- }
- }
-
- ImGuiHelpers.ScaledDummy(10);
-
- if (this.debugTex != null)
- {
- ImGui.Image(this.debugTex.ImGuiHandle, this.inputTexScale, this.inputTexUv0, this.inputTexUv1, this.inputTintCol);
- ImGuiHelpers.ScaledDummy(5);
- Util.ShowObject(this.debugTex);
- }
-
- break;
-
- // Gamepad
- case 16:
- Action> helper = (text, mask, resolve) =>
- {
- ImGui.Text($"{text} {mask:X4}");
- ImGui.Text($"DPadLeft {resolve(GamepadButtons.DpadLeft)} " +
- $"DPadUp {resolve(GamepadButtons.DpadUp)} " +
- $"DPadRight {resolve(GamepadButtons.DpadRight)} " +
- $"DPadDown {resolve(GamepadButtons.DpadDown)} ");
- ImGui.Text($"West {resolve(GamepadButtons.West)} " +
- $"North {resolve(GamepadButtons.North)} " +
- $"East {resolve(GamepadButtons.East)} " +
- $"South {resolve(GamepadButtons.South)} ");
- ImGui.Text($"L1 {resolve(GamepadButtons.L1)} " +
- $"L2 {resolve(GamepadButtons.L2)} " +
- $"R1 {resolve(GamepadButtons.R1)} " +
- $"R2 {resolve(GamepadButtons.R2)} ");
- ImGui.Text($"Select {resolve(GamepadButtons.Select)} " +
- $"Start {resolve(GamepadButtons.Start)} " +
- $"L3 {resolve(GamepadButtons.L3)} " +
- $"R3 {resolve(GamepadButtons.R3)} ");
- };
-#if DEBUG
- ImGui.Text($"GamepadInput {this.dalamud.ClientState.GamepadState.GamepadInput.ToString("X")}");
- if (ImGui.IsItemHovered()) ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
- if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"{this.dalamud.ClientState.GamepadState.GamepadInput.ToString("X")}");
-#endif
-
- helper(
- "Buttons Raw",
- this.dalamud.ClientState.GamepadState.ButtonsRaw,
- this.dalamud.ClientState.GamepadState.Raw);
- helper(
- "Buttons Pressed",
- this.dalamud.ClientState.GamepadState.ButtonsPressed,
- this.dalamud.ClientState.GamepadState.Pressed);
- helper(
- "Buttons Repeat",
- this.dalamud.ClientState.GamepadState.ButtonsRepeat,
- this.dalamud.ClientState.GamepadState.Repeat);
- helper(
- "Buttons Released",
- this.dalamud.ClientState.GamepadState.ButtonsReleased,
- this.dalamud.ClientState.GamepadState.Released);
- ImGui.Text($"LeftStickLeft {this.dalamud.ClientState.GamepadState.LeftStickLeft:0.00} " +
- $"LeftStickUp {this.dalamud.ClientState.GamepadState.LeftStickUp:0.00} " +
- $"LeftStickRight {this.dalamud.ClientState.GamepadState.LeftStickRight:0.00} " +
- $"LeftStickDown {this.dalamud.ClientState.GamepadState.LeftStickDown:0.00} ");
- ImGui.Text($"RightStickLeft {this.dalamud.ClientState.GamepadState.RightStickLeft:0.00} " +
- $"RightStickUp {this.dalamud.ClientState.GamepadState.RightStickUp:0.00} " +
- $"RightStickRight {this.dalamud.ClientState.GamepadState.RightStickRight:0.00} " +
- $"RightStickDown {this.dalamud.ClientState.GamepadState.RightStickDown:0.00} ");
- break;
- }
- }
- else
- {
- ImGui.TextUnformatted("Data not ready.");
- }
- }
- catch (Exception ex)
- {
- ImGui.TextUnformatted(ex.ToString());
- }
-
- ImGui.PopStyleVar();
-
- ImGui.EndChild();
- }
-
- private void DrawActorTable()
- {
- var stateString = string.Empty;
-
- // LocalPlayer is null in a number of situations (at least with the current visible-actors list)
- // which would crash here.
- if (this.dalamud.ClientState.Actors.Length == 0)
- {
- ImGui.TextUnformatted("Data not ready.");
- }
- else if (this.dalamud.ClientState.LocalPlayer == null)
- {
- ImGui.TextUnformatted("LocalPlayer null.");
- }
- else
- {
- stateString += $"FrameworkBase: {this.dalamud.Framework.Address.BaseAddress.ToInt64():X}\n";
- stateString += $"ActorTableLen: {this.dalamud.ClientState.Actors.Length}\n";
- stateString += $"LocalPlayerName: {this.dalamud.ClientState.LocalPlayer.Name}\n";
- stateString += $"CurrentWorldName: {(this.resolveGameData ? this.dalamud.ClientState.LocalPlayer.CurrentWorld.GameData.Name : this.dalamud.ClientState.LocalPlayer.CurrentWorld.Id.ToString())}\n";
- stateString += $"HomeWorldName: {(this.resolveGameData ? this.dalamud.ClientState.LocalPlayer.HomeWorld.GameData.Name : this.dalamud.ClientState.LocalPlayer.HomeWorld.Id.ToString())}\n";
- stateString += $"LocalCID: {this.dalamud.ClientState.LocalContentId:X}\n";
- stateString += $"LastLinkedItem: {this.dalamud.Framework.Gui.Chat.LastLinkedItemId}\n";
- stateString += $"TerritoryType: {this.dalamud.ClientState.TerritoryType}\n\n";
-
- ImGui.TextUnformatted(stateString);
-
- ImGui.Checkbox("Draw actors on screen", ref this.drawActors);
- ImGui.SliderFloat("Draw Distance", ref this.maxActorDrawDistance, 2f, 40f);
-
- for (var i = 0; i < this.dalamud.ClientState.Actors.Length; i++)
- {
- var actor = this.dalamud.ClientState.Actors[i];
-
- if (actor == null)
- continue;
-
- this.PrintActor(actor, i.ToString());
-
- if (this.drawActors && this.dalamud.Framework.Gui.WorldToScreen(actor.Position, out var screenCoords))
- {
- // So, while WorldToScreen will return false if the point is off of game client screen, to
- // to avoid performance issues, we have to manually determine if creating a window would
- // produce a new viewport, and skip rendering it if so
- var actorText =
- $"{actor.Address.ToInt64():X}:{actor.ActorId:X}[{i}] - {actor.ObjectKind} - {actor.Name}";
-
- var screenPos = ImGui.GetMainViewport().Pos;
- var screenSize = ImGui.GetMainViewport().Size;
-
- var windowSize = ImGui.CalcTextSize(actorText);
-
- // Add some extra safety padding
- windowSize.X += ImGui.GetStyle().WindowPadding.X + 10;
- windowSize.Y += ImGui.GetStyle().WindowPadding.Y + 10;
-
- if (screenCoords.X + windowSize.X > screenPos.X + screenSize.X ||
- screenCoords.Y + windowSize.Y > screenPos.Y + screenSize.Y)
- continue;
-
- if (actor.YalmDistanceX > this.maxActorDrawDistance)
- continue;
-
- ImGui.SetNextWindowPos(new Vector2(screenCoords.X, screenCoords.Y));
-
- ImGui.SetNextWindowBgAlpha(Math.Max(1f - (actor.YalmDistanceX / this.maxActorDrawDistance), 0.2f));
- if (ImGui.Begin(
- $"Actor{i}##ActorWindow{i}",
- ImGuiWindowFlags.NoDecoration |
- ImGuiWindowFlags.AlwaysAutoResize |
- ImGuiWindowFlags.NoSavedSettings |
- ImGuiWindowFlags.NoMove |
- ImGuiWindowFlags.NoMouseInputs |
- ImGuiWindowFlags.NoDocking |
- ImGuiWindowFlags.NoFocusOnAppearing |
- ImGuiWindowFlags.NoNav))
- ImGui.Text(actorText);
- ImGui.End();
- }
- }
- }
- }
-
-#pragma warning disable CS0618 // Type or member is obsolete
- private void DrawIpcDebug()
- {
- var i1 = new DalamudPluginInterface(this.dalamud, "DalamudTestSub", null, PluginLoadReason.Boot);
- var i2 = new DalamudPluginInterface(this.dalamud, "DalamudTestPub", null, PluginLoadReason.Boot);
-
- if (ImGui.Button("Add test sub"))
- {
- i1.Subscribe("DalamudTestPub", o =>
- {
- dynamic msg = o;
- Log.Debug(msg.Expand);
- });
- }
-
- if (ImGui.Button("Add test sub any"))
- {
- i1.SubscribeAny((o, a) =>
- {
- dynamic msg = a;
- Log.Debug($"From {o}: {msg.Expand}");
- });
- }
-
- if (ImGui.Button("Remove test sub"))
- i1.Unsubscribe("DalamudTestPub");
-
- if (ImGui.Button("Remove test sub any"))
- i1.UnsubscribeAny();
-
- if (ImGui.Button("Send test message"))
- {
- dynamic testMsg = new ExpandoObject();
- testMsg.Expand = "dong";
- i2.SendMessage(testMsg);
- }
-
- // This doesn't actually work, so don't mind it - impl relies on plugins being registered in PluginManager
- if (ImGui.Button("Send test message any"))
- {
- dynamic testMsg = new ExpandoObject();
- testMsg.Expand = "dong";
- i2.SendMessage("DalamudTestSub", testMsg);
- }
-
- foreach (var (sourcePluginName, subPluginName, subAction) in this.dalamud.PluginManager.IpcSubscriptions)
- ImGui.Text($"Source:{sourcePluginName} Sub:{subPluginName}");
- }
-#pragma warning restore CS0618 // Type or member is obsolete
-
- private void DrawAddonDebug()
- {
- ImGui.InputText("Addon name", ref this.inputAddonName, 256);
- ImGui.InputInt("Addon Index", ref this.inputAddonIndex);
-
- if (ImGui.Button("Get Addon"))
- {
- this.resultAddon =
- this.dalamud.Framework.Gui.GetAddonByName(
- this.inputAddonName, this.inputAddonIndex);
- }
-
- if (ImGui.Button("Find Agent"))
- this.findAgentInterfacePtr = this.dalamud.Framework.Gui.FindAgentInterface(this.inputAddonName);
-
- if (this.resultAddon != null)
- {
- ImGui.TextUnformatted(
- $"{this.resultAddon.Name} - 0x{this.resultAddon.Address.ToInt64():x}\n v:{this.resultAddon.Visible} x:{this.resultAddon.X} y:{this.resultAddon.Y} s:{this.resultAddon.Scale}, w:{this.resultAddon.Width}, h:{this.resultAddon.Height}");
- }
-
- if (this.findAgentInterfacePtr != IntPtr.Zero)
- {
- ImGui.TextUnformatted(
- $"Agent: 0x{this.findAgentInterfacePtr.ToInt64():x}");
- ImGui.SameLine();
-
- if (ImGui.Button("C"))
- ImGui.SetClipboardText(this.findAgentInterfacePtr.ToInt64().ToString("x"));
- }
-
- if (ImGui.Button("Get Base UI object"))
- {
- var addr = this.dalamud.Framework.Gui.GetBaseUIObject().ToInt64().ToString("x");
- Log.Information("{0}", addr);
- ImGui.SetClipboardText(addr);
- }
- }
-
- private void DrawTargetDebug()
- {
- var targetMgr = this.dalamud.ClientState.Targets;
-
- if (targetMgr.CurrentTarget != null)
- {
- this.PrintActor(targetMgr.CurrentTarget, "CurrentTarget");
- Util.ShowObject(targetMgr.CurrentTarget);
- }
-
- if (targetMgr.FocusTarget != null)
- this.PrintActor(targetMgr.FocusTarget, "FocusTarget");
-
- if (targetMgr.MouseOverTarget != null)
- this.PrintActor(targetMgr.MouseOverTarget, "MouseOverTarget");
-
- if (targetMgr.PreviousTarget != null)
- this.PrintActor(targetMgr.PreviousTarget, "PreviousTarget");
-
- if (targetMgr.SoftTarget != null)
- this.PrintActor(targetMgr.SoftTarget, "SoftTarget");
-
- if (ImGui.Button("Clear CT"))
- targetMgr.ClearCurrentTarget();
-
- if (ImGui.Button("Clear FT"))
- targetMgr.ClearFocusTarget();
-
- var localPlayer = this.dalamud.ClientState.LocalPlayer;
-
- if (localPlayer != null)
- {
- if (ImGui.Button("Set CT"))
- targetMgr.SetCurrentTarget(localPlayer);
-
- if (ImGui.Button("Set FT"))
- targetMgr.SetFocusTarget(localPlayer);
- }
- else
- {
- ImGui.Text("LocalPlayer is null.");
- }
- }
-
- private void Load()
- {
- if (this.dalamud.Data.IsDataReady)
- {
- this.serverOpString = JsonConvert.SerializeObject(this.dalamud.Data.ServerOpCodes, Formatting.Indented);
- this.wasReady = true;
- }
- }
-
- private void PrintActor(Actor actor, string tag)
- {
- var actorString =
- $"{actor.Address.ToInt64():X}:{actor.ActorId:X}[{tag}] - {actor.ObjectKind} - {actor.Name} - X{actor.Position.X} Y{actor.Position.Y} Z{actor.Position.Z} D{actor.YalmDistanceX} R{actor.Rotation} - Target: {actor.TargetActorID:X}\n";
-
- if (actor is Npc npc)
- actorString += $" DataId: {npc.DataId} NameId:{npc.NameId}\n";
-
- if (actor is Chara chara)
- {
- actorString +=
- $" Level: {chara.Level} ClassJob: {(this.resolveGameData ? chara.ClassJob.GameData.Name : chara.ClassJob.Id.ToString())} CHP: {chara.CurrentHp} MHP: {chara.MaxHp} CMP: {chara.CurrentMp} MMP: {chara.MaxMp}\n Customize: {BitConverter.ToString(chara.Customize).Replace("-", " ")}\n";
- }
-
- if (actor is PlayerCharacter pc)
- {
- actorString +=
- $" HomeWorld: {(this.resolveGameData ? pc.HomeWorld.GameData.Name : pc.HomeWorld.Id.ToString())} CurrentWorld: {(this.resolveGameData ? pc.CurrentWorld.GameData.Name : pc.CurrentWorld.Id.ToString())} FC: {pc.CompanyTag}\n";
- }
-
- ImGui.TextUnformatted(actorString);
- ImGui.SameLine();
- if (ImGui.Button($"C##{this.copyButtonIndex++}"))
- {
- ImGui.SetClipboardText(actor.Address.ToInt64().ToString("X"));
- }
- }
- }
-}
diff --git a/Dalamud/Interface/DalamudLogWindow.cs b/Dalamud/Interface/DalamudLogWindow.cs
deleted file mode 100644
index 78cbaa17e..000000000
--- a/Dalamud/Interface/DalamudLogWindow.cs
+++ /dev/null
@@ -1,165 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Numerics;
-
-using Dalamud.Configuration;
-using Dalamud.Game.Command;
-using Dalamud.Interface.Colors;
-using Dalamud.Interface.Windowing;
-using ImGuiNET;
-using Serilog;
-using Serilog.Events;
-
-namespace Dalamud.Interface
-{
- ///
- /// The window that displays the Dalamud log file in-game.
- ///
- internal class DalamudLogWindow : Window, IDisposable
- {
- private readonly CommandManager commandManager;
- private readonly DalamudConfiguration configuration;
- private readonly List<(string Line, Vector4 Color)> logText = new();
- private readonly object renderLock = new();
- private bool autoScroll;
- private bool openAtStartup;
-
- private string commandText = string.Empty;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The CommandManager instance.
- /// The DalamudConfiguration instance.
- public DalamudLogWindow(CommandManager commandManager, DalamudConfiguration configuration)
- : base("Dalamud LOG")
- {
- this.commandManager = commandManager;
- this.configuration = configuration;
- this.autoScroll = configuration.LogAutoScroll;
- this.openAtStartup = configuration.LogOpenAtStartup;
- SerilogEventSink.Instance.OnLogLine += this.Serilog_OnLogLine;
-
- this.Size = new Vector2(500, 400);
- this.SizeCondition = ImGuiCond.FirstUseEver;
- }
-
- ///
- /// Dispose of managed and unmanaged resources.
- ///
- public void Dispose()
- {
- SerilogEventSink.Instance.OnLogLine -= this.Serilog_OnLogLine;
- }
-
- ///
- /// Clear the window of all log entries.
- ///
- public void Clear()
- {
- lock (this.renderLock)
- {
- this.logText.Clear();
- }
- }
-
- ///
- /// Add a single log line to the display.
- ///
- /// The line to add.
- /// The line coloring.
- public void AddLog(string line, Vector4 color)
- {
- lock (this.renderLock)
- {
- this.logText.Add((line, color));
- }
- }
-
- ///
- public override void Draw()
- {
- // Options menu
- if (ImGui.BeginPopup("Options"))
- {
- if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll))
- {
- this.configuration.LogAutoScroll = this.autoScroll;
- this.configuration.Save();
- }
-
- if (ImGui.Checkbox("Open at startup", ref this.openAtStartup))
- {
- this.configuration.LogOpenAtStartup = this.openAtStartup;
- this.configuration.Save();
- }
-
- ImGui.EndPopup();
- }
-
- // Main window
- if (ImGui.Button("Options"))
- ImGui.OpenPopup("Options");
- ImGui.SameLine();
- var clear = ImGui.Button("Clear");
- ImGui.SameLine();
- var copy = ImGui.Button("Copy");
-
- ImGui.Text("Enter command: ");
- ImGui.SameLine();
- ImGui.InputText("##commandbox", ref this.commandText, 255);
- ImGui.SameLine();
- if (ImGui.Button("Send"))
- {
- if (this.commandManager.ProcessCommand(this.commandText))
- {
- Log.Information("Command was dispatched.");
- }
- else
- {
- Log.Information("Command {0} not registered.", this.commandText);
- }
- }
-
- ImGui.BeginChild("scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.HorizontalScrollbar);
-
- if (clear)
- this.Clear();
- if (copy)
- ImGui.LogToClipboard();
-
- ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0));
-
- lock (this.renderLock)
- {
- foreach (var (line, color) in this.logText)
- {
- ImGui.TextColored(color, line);
- }
- }
-
- ImGui.PopStyleVar();
-
- if (this.autoScroll && ImGui.GetScrollY() >= ImGui.GetScrollMaxY())
- ImGui.SetScrollHereY(1.0f);
-
- ImGui.EndChild();
- }
-
- private void Serilog_OnLogLine(object sender, (string Line, LogEventLevel Level) logEvent)
- {
- var color = logEvent.Level switch
- {
- LogEventLevel.Error => ImGuiColors.DalamudRed,
- LogEventLevel.Verbose => ImGuiColors.DalamudWhite,
- LogEventLevel.Debug => ImGuiColors.DalamudWhite2,
- LogEventLevel.Information => ImGuiColors.DalamudWhite,
- LogEventLevel.Warning => ImGuiColors.DalamudOrange,
- LogEventLevel.Fatal => ImGuiColors.DalamudRed,
- _ => throw new ArgumentOutOfRangeException(),
- };
-
- this.AddLog(logEvent.Line, color);
- }
- }
-}
diff --git a/Dalamud/Interface/ImGuiHelpers.cs b/Dalamud/Interface/ImGuiHelpers.cs
index d2ecda131..52c9802f9 100644
--- a/Dalamud/Interface/ImGuiHelpers.cs
+++ b/Dalamud/Interface/ImGuiHelpers.cs
@@ -20,6 +20,24 @@ namespace Dalamud.Interface
///
public static float GlobalScale { get; private set; }
+ ///
+ /// Gets a that is pre-scaled with the multiplier.
+ ///
+ /// Vector2 X parameter.
+ /// Vector2 Y parameter.
+ /// A scaled Vector2.
+ public static Vector2 ScaledVector2(float x, float y) => new Vector2(x, y) * GlobalScale;
+
+ ///
+ /// Gets a that is pre-scaled with the multiplier.
+ ///
+ /// Vector4 X parameter.
+ /// Vector4 Y parameter.
+ /// Vector4 Z parameter.
+ /// Vector4 W parameter.
+ /// A scaled Vector2.
+ public static Vector4 ScaledVector4(float x, float y, float z, float w) => new Vector4(x, y, z, w) * GlobalScale;
+
///
/// Force the next ImGui window to stay inside the main game window.
///
@@ -29,7 +47,14 @@ namespace Dalamud.Interface
/// Create a dummy scaled by the global Dalamud scale.
///
/// The size of the dummy.
- public static void ScaledDummy(float size) => ImGui.Dummy(new Vector2(size * GlobalScale, size * GlobalScale));
+ public static void ScaledDummy(float size) => ScaledDummy(size, size);
+
+ ///
+ /// Create a dummy scaled by the global Dalamud scale.
+ ///
+ /// Vector2 X parameter.
+ /// Vector2 Y parameter.
+ public static void ScaledDummy(float x, float y) => ScaledDummy(new Vector2(x, y));
///
/// Create a dummy scaled by the global Dalamud scale.
diff --git a/Dalamud/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs
similarity index 93%
rename from Dalamud/DalamudCommands.cs
rename to Dalamud/Interface/Internal/DalamudCommands.cs
index 5b59f4a70..6e0f5e7a6 100644
--- a/Dalamud/DalamudCommands.cs
+++ b/Dalamud/Interface/Internal/DalamudCommands.cs
@@ -7,7 +7,7 @@ using CheapLoc;
using Dalamud.Game.Command;
using Serilog;
-namespace Dalamud
+namespace Dalamud.Interface.Internal
{
///
/// Class handling Dalamud core commands.
@@ -149,7 +149,7 @@ namespace Dalamud
try
{
- this.dalamud.PluginManager.ReloadPlugins();
+ this.dalamud.PluginManager.ReloadAllPlugins();
this.dalamud.Framework.Gui.Chat.Print("OK");
}
@@ -221,25 +221,33 @@ namespace Dalamud
private void OnBgmSetCommand(string command, string arguments)
{
- this.dalamud.Framework.Gui.SetBgm(ushort.Parse(arguments));
+ if (ushort.TryParse(arguments, out var value))
+ {
+ this.dalamud.Framework.Gui.SetBgm(value);
+ }
+ else
+ {
+ // Revert to the original BGM by specifying an invalid one
+ this.dalamud.Framework.Gui.SetBgm(9999);
+ }
}
private void OnDebugDrawDevMenu(string command, string arguments)
{
- this.dalamud.DalamudUi.IsDevMenu = !this.dalamud.DalamudUi.IsDevMenu;
+ this.dalamud.DalamudUi.ToggleDevMenu();
}
private void OnDebugDrawDataMenu(string command, string arguments)
{
if (string.IsNullOrEmpty(arguments))
- this.dalamud.DalamudUi.ToggleData();
+ this.dalamud.DalamudUi.ToggleDataWindow();
else
- this.dalamud.DalamudUi.ToggleData(arguments);
+ this.dalamud.DalamudUi.ToggleDataWindow(arguments);
}
private void OnOpenLog(string command, string arguments)
{
- this.dalamud.DalamudUi.ToggleLog();
+ this.dalamud.DalamudUi.ToggleLogWindow();
}
private void OnDebugImInfoCommand(string command, string arguments)
@@ -267,12 +275,12 @@ namespace Dalamud
private void OnOpenInstallerCommand(string command, string arguments)
{
- this.dalamud.DalamudUi.TogglePluginInstaller();
+ this.dalamud.DalamudUi.TogglePluginInstallerWindow();
}
private void OnOpenCreditsCommand(string command, string arguments)
{
- this.dalamud.DalamudUi.ToggleCredits();
+ this.dalamud.DalamudUi.ToggleCreditsWindow();
}
private void OnSetLanguageCommand(string command, string arguments)
@@ -299,7 +307,7 @@ namespace Dalamud
private void OnOpenSettingsCommand(string command, string arguments)
{
- this.dalamud.DalamudUi.ToggleSettings();
+ this.dalamud.DalamudUi.ToggleSettingsWindow();
}
}
}
diff --git a/Dalamud/Interface/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs
similarity index 56%
rename from Dalamud/Interface/DalamudInterface.cs
rename to Dalamud/Interface/Internal/DalamudInterface.cs
index 30f1534f5..47225e815 100644
--- a/Dalamud/Interface/DalamudInterface.cs
+++ b/Dalamud/Interface/Internal/DalamudInterface.cs
@@ -1,152 +1,288 @@
using System;
using System.Diagnostics;
-using System.IO;
using System.Linq;
using System.Numerics;
-using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-using CheapLoc;
-using Dalamud.Interface.Colors;
-using Dalamud.Interface.Components;
-using Dalamud.Interface.Scratchpad;
+using Dalamud.Interface.Internal.Windows;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
+using Dalamud.Plugin.Internal;
using ImGuiNET;
-using Serilog;
using Serilog.Events;
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal
{
///
- /// Class handling Dalamud core interface.
+ /// This plugin implements all of the Dalamud interface separately, to allow for reloading of the interface and rapid prototyping.
///
internal class DalamudInterface : IDisposable
{
+ private static readonly ModuleLog Log = new("DUI");
+
private readonly Dalamud dalamud;
+ private readonly WindowSystem windowSystem;
- private readonly DalamudLogWindow logWindow;
- private readonly DalamudDataWindow dataWindow;
- private readonly DalamudCreditsWindow creditsWindow;
- private readonly DalamudSettingsWindow settingsWindow;
- private readonly PluginInstallerWindow pluginWindow;
- private readonly DalamudPluginStatWindow pluginStatWindow;
- private readonly DalamudChangelogWindow changelogWindow;
- private readonly ComponentDemoWindow componentDemoWindow;
+ private readonly ChangelogWindow changelogWindow;
private readonly ColorDemoWindow colorDemoWindow;
- private readonly ScratchpadWindow scratchpadWindow;
+ private readonly ComponentDemoWindow componentDemoWindow;
+ private readonly CreditsWindow creditsWindow;
+ private readonly DataWindow dataWindow;
private readonly GamepadModeNotifierWindow gamepadModeNotifierWindow;
-
- private readonly WindowSystem windowSystem = new("DalamudCore");
+ private readonly ConsoleWindow consoleWindow;
+ private readonly PluginStatWindow pluginStatWindow;
+ private readonly PluginInstallerWindow pluginWindow;
+ private readonly ScratchpadWindow scratchpadWindow;
+ private readonly SettingsWindow settingsWindow;
private ulong frameCount = 0;
- private bool isImguiDrawDemoWindow = false;
-
#if DEBUG
- private bool isImguiDrawDevMenu = true;
+ private bool isImGuiDrawDevMenu = true;
#else
- private bool isImguiDrawDevMenu = false;
+ private bool isImGuiDrawDevMenu = false;
#endif
+ private bool isImGuiDrawDemoWindow = false;
+
///
/// Initializes a new instance of the class.
///
- /// The Dalamud instance to register to.
+ /// The Dalamud instance.
public DalamudInterface(Dalamud dalamud)
{
this.dalamud = dalamud;
+ this.windowSystem = new WindowSystem("DalamudCore");
- this.logWindow = new DalamudLogWindow(this.dalamud.CommandManager, this.dalamud.Configuration)
- {
- IsOpen = false,
- };
- this.windowSystem.AddWindow(this.logWindow);
+ this.changelogWindow = new ChangelogWindow(dalamud) { IsOpen = false };
+ this.colorDemoWindow = new ColorDemoWindow() { IsOpen = false };
+ this.componentDemoWindow = new ComponentDemoWindow() { IsOpen = false };
+ this.creditsWindow = new CreditsWindow(dalamud) { IsOpen = false };
+ this.dataWindow = new DataWindow(dalamud) { IsOpen = false };
+ this.gamepadModeNotifierWindow = new GamepadModeNotifierWindow();
+ this.consoleWindow = new ConsoleWindow(dalamud) { IsOpen = this.dalamud.Configuration.LogOpenAtStartup };
+ this.pluginStatWindow = new PluginStatWindow(dalamud) { IsOpen = false };
+ this.pluginWindow = new PluginInstallerWindow(dalamud) { IsOpen = false };
+ this.scratchpadWindow = new ScratchpadWindow(dalamud) { IsOpen = false };
+ this.settingsWindow = new SettingsWindow(dalamud) { IsOpen = false };
- this.dataWindow = new DalamudDataWindow(this.dalamud)
- {
- IsOpen = false,
- };
- this.windowSystem.AddWindow(this.dataWindow);
-
- this.creditsWindow = new DalamudCreditsWindow(this.dalamud)
- {
- IsOpen = false,
- };
+ this.windowSystem.AddWindow(this.changelogWindow);
+ this.windowSystem.AddWindow(this.colorDemoWindow);
+ this.windowSystem.AddWindow(this.componentDemoWindow);
this.windowSystem.AddWindow(this.creditsWindow);
-
- this.settingsWindow = new DalamudSettingsWindow(this.dalamud)
- {
- IsOpen = false,
- };
+ this.windowSystem.AddWindow(this.dataWindow);
+ this.windowSystem.AddWindow(this.gamepadModeNotifierWindow);
+ this.windowSystem.AddWindow(this.consoleWindow);
+ this.windowSystem.AddWindow(this.pluginStatWindow);
+ this.windowSystem.AddWindow(this.pluginWindow);
+ this.windowSystem.AddWindow(this.scratchpadWindow);
this.windowSystem.AddWindow(this.settingsWindow);
- this.pluginWindow = new PluginInstallerWindow(this.dalamud, this.dalamud.StartInfo.GameVersion)
- {
- IsOpen = false,
- };
- this.windowSystem.AddWindow(this.pluginWindow);
+ this.dalamud.InterfaceManager.OnDraw += this.OnDraw;
- this.pluginStatWindow = new DalamudPluginStatWindow(this.dalamud.PluginManager)
- {
- IsOpen = false,
- };
- this.windowSystem.AddWindow(this.pluginStatWindow);
-
- this.changelogWindow = new DalamudChangelogWindow(this.dalamud)
- {
- IsOpen = false,
- };
- this.windowSystem.AddWindow(this.changelogWindow);
-
- this.componentDemoWindow = new ComponentDemoWindow()
- {
- IsOpen = false,
- };
- this.windowSystem.AddWindow(this.componentDemoWindow);
-
- this.colorDemoWindow = new ColorDemoWindow()
- {
- IsOpen = false,
- };
- this.windowSystem.AddWindow(this.colorDemoWindow);
-
- this.scratchpadWindow = new ScratchpadWindow(this.dalamud)
- {
- IsOpen = false,
- };
- this.windowSystem.AddWindow(this.scratchpadWindow);
-
- this.gamepadModeNotifierWindow = new GamepadModeNotifierWindow();
- this.windowSystem.AddWindow(this.gamepadModeNotifierWindow);
-
- Log.Information("[DUI] Windows added");
-
- if (dalamud.Configuration.LogOpenAtStartup)
- this.OpenLog();
+ Log.Information("Windows added");
}
///
- /// Gets or sets a value indicating whether the Dalamud dev menu is drawing.
+ /// Gets or sets a value indicating whether the /xldev menu is open.
///
- public bool IsDevMenu
+ public bool IsDevMenuOpen
{
- get => this.isImguiDrawDevMenu;
- set => this.isImguiDrawDevMenu = value;
+ get => this.isImGuiDrawDevMenu;
+ set => this.isImGuiDrawDevMenu = value;
}
///
- /// Draw the Dalamud core interface via ImGui.
+ /// Gets a value indicating whether the current Dalamud version warrants displaying the changelog.
///
- public void Draw()
+ public bool WarrantsChangelog => ChangelogWindow.WarrantsChangelog;
+
+ ///
+ public void Dispose()
+ {
+ this.dalamud.InterfaceManager.OnDraw -= this.OnDraw;
+
+ this.windowSystem.RemoveAllWindows();
+
+ this.creditsWindow.Dispose();
+ this.consoleWindow.Dispose();
+ this.scratchpadWindow.Dispose();
+ }
+
+ #region Open
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenChangelogWindow() => this.changelogWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenColorsDemoWindow() => this.colorDemoWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenComponentDemoWindow() => this.componentDemoWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenCreditsWindow() => this.creditsWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ /// The data kind to switch to after opening.
+ public void OpenDataWindow(string dataKind = null)
+ {
+ this.dataWindow.IsOpen = true;
+ if (dataKind != null && this.dataWindow.IsOpen)
+ {
+ this.dataWindow.SetDataKind(dataKind);
+ }
+ }
+
+ ///
+ /// Opens the dev menu bar.
+ ///
+ public void OpenDevMenu() => this.isImGuiDrawDevMenu = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenLogWindow() => this.consoleWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenPluginStats() => this.pluginStatWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenPluginInstaller() => this.pluginWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenScratchpadWindow() => this.scratchpadWindow.IsOpen = true;
+
+ ///
+ /// Opens the .
+ ///
+ public void OpenSettings() => this.settingsWindow.IsOpen = true;
+
+ #endregion
+
+ #region Toggle
+
+ ///
+ /// Toggles the .
+ ///
+ public void ToggleChangelogWindow() => this.changelogWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ public void ToggleColorsDemoWindow() => this.colorDemoWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ public void ToggleComponentDemoWindow() => this.componentDemoWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ public void ToggleCreditsWindow() => this.creditsWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ /// The data kind to switch to after opening.
+ public void ToggleDataWindow(string dataKind = null)
+ {
+ this.dataWindow.Toggle();
+ if (dataKind != null && this.dataWindow.IsOpen)
+ {
+ this.dataWindow.SetDataKind(dataKind);
+ }
+ }
+
+ ///
+ /// Toggles the dev menu bar.
+ ///
+ public void ToggleDevMenu() => this.isImGuiDrawDevMenu ^= true;
+
+ ///
+ /// Toggles the .
+ ///
+ public void ToggleGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ public void ToggleLogWindow() => this.consoleWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ public void TogglePluginStatsWindow() => this.pluginStatWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ public void TogglePluginInstallerWindow() => this.pluginWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ public void ToggleScratchpadWindow() => this.scratchpadWindow.Toggle();
+
+ ///
+ /// Toggles the .
+ ///
+ public void ToggleSettingsWindow() => this.settingsWindow.Toggle();
+
+ #endregion
+
+ private void OnDraw()
{
this.frameCount++;
- if (!this.IsDevMenu && !this.dalamud.ClientState.Condition.Any())
+ try
{
- ImGui.PushStyleColor(ImGuiCol.Button, new Vector4(0, 0, 0, 0));
- ImGui.PushStyleColor(ImGuiCol.ButtonActive, new Vector4(0, 0, 0, 0));
- ImGui.PushStyleColor(ImGuiCol.ButtonHovered, new Vector4(0, 0, 0, 0));
+ this.DrawHiddenDevMenuOpener();
+ this.DrawDevMenu();
+
+ if (this.dalamud.Framework.Gui.GameUiHidden)
+ return;
+
+ this.windowSystem.Draw();
+
+ if (this.isImGuiDrawDemoWindow)
+ ImGui.ShowDemoWindow();
+ }
+ catch (Exception ex)
+ {
+ PluginLog.Error(ex, "Error during OnDraw");
+ }
+ }
+
+ private void DrawHiddenDevMenuOpener()
+ {
+ if (!this.isImGuiDrawDevMenu && !this.dalamud.ClientState.Condition.Any())
+ {
+ ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero);
+ ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero);
+ ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero);
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0, 0, 0, 1));
ImGui.PushStyleColor(ImGuiCol.TextSelectedBg, new Vector4(0, 0, 0, 1));
ImGui.PushStyleColor(ImGuiCol.Border, new Vector4(0, 0, 0, 1));
@@ -160,27 +296,30 @@ namespace Dalamud.Interface
if (ImGui.Begin("DevMenu Opener", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoSavedSettings))
{
if (ImGui.Button("###devMenuOpener", new Vector2(40, 25)))
- this.IsDevMenu = true;
+ this.isImGuiDrawDevMenu = true;
ImGui.End();
}
ImGui.PopStyleColor(8);
}
+ }
- if (this.IsDevMenu)
+ private void DrawDevMenu()
+ {
+ if (this.isImGuiDrawDevMenu)
{
if (ImGui.BeginMainMenuBar())
{
if (ImGui.BeginMenu("Dalamud"))
{
- ImGui.MenuItem("Draw Dalamud dev menu", string.Empty, ref this.isImguiDrawDevMenu);
+ ImGui.MenuItem("Draw Dalamud dev menu", string.Empty, ref this.isImGuiDrawDevMenu);
ImGui.Separator();
if (ImGui.MenuItem("Open Log window"))
{
- this.OpenLog();
+ this.OpenLogWindow();
}
if (ImGui.BeginMenu("Set log level..."))
@@ -190,6 +329,8 @@ namespace Dalamud.Interface
if (ImGui.MenuItem(logLevel + "##logLevelSwitch", string.Empty, this.dalamud.LogLevelSwitch.MinimumLevel == logLevel))
{
this.dalamud.LogLevelSwitch.MinimumLevel = logLevel;
+ this.dalamud.Configuration.LogLevel = logLevel;
+ this.dalamud.Configuration.Save();
}
}
@@ -198,19 +339,26 @@ namespace Dalamud.Interface
if (ImGui.MenuItem("Enable AntiDebug", null, this.dalamud.AntiDebug.IsEnabled))
{
- this.dalamud.AntiDebug.Enable();
+ var newEnabled = !this.dalamud.AntiDebug.IsEnabled;
+ if (newEnabled)
+ this.dalamud.AntiDebug.Enable();
+ else
+ this.dalamud.AntiDebug.Disable();
+
+ this.dalamud.Configuration.IsAntiAntiDebugEnabled = newEnabled;
+ this.dalamud.Configuration.Save();
}
ImGui.Separator();
if (ImGui.MenuItem("Open Data window"))
{
- this.OpenData();
+ this.OpenDataWindow();
}
if (ImGui.MenuItem("Open Credits window"))
{
- this.OpenCredits();
+ this.OpenCreditsWindow();
}
if (ImGui.MenuItem("Open Settings window"))
@@ -220,20 +368,20 @@ namespace Dalamud.Interface
if (ImGui.MenuItem("Open Changelog window"))
{
- this.OpenChangelog();
+ this.OpenChangelogWindow();
}
if (ImGui.MenuItem("Open Components Demo"))
{
- this.OpenComponentDemo();
+ this.OpenComponentDemoWindow();
}
if (ImGui.MenuItem("Open Colors Demo"))
{
- this.OpenColorsDemo();
+ this.OpenColorsDemoWindow();
}
- ImGui.MenuItem("Draw ImGui demo", string.Empty, ref this.isImguiDrawDemoWindow);
+ ImGui.MenuItem("Draw ImGui demo", string.Empty, ref this.isImGuiDrawDemoWindow);
ImGui.Separator();
@@ -255,12 +403,12 @@ namespace Dalamud.Interface
ImGui.Separator();
if (ImGui.MenuItem("Enable Dalamud testing", string.Empty, this.dalamud.Configuration.DoDalamudTest))
{
- this.dalamud.Configuration.DoDalamudTest = !this.dalamud.Configuration.DoDalamudTest;
+ this.dalamud.Configuration.DoDalamudTest ^= true;
this.dalamud.Configuration.Save();
}
ImGui.MenuItem(Util.AssemblyVersion, false);
- ImGui.MenuItem(this.dalamud.StartInfo.GameVersion, false);
+ ImGui.MenuItem(this.dalamud.StartInfo.GameVersion.ToString(), false);
ImGui.EndMenu();
}
@@ -291,10 +439,10 @@ namespace Dalamud.Interface
if (ImGui.MenuItem("Print plugin info"))
{
- foreach (var plugin in this.dalamud.PluginManager.Plugins)
+ foreach (var plugin in this.dalamud.PluginManager.InstalledPlugins)
{
// TODO: some more here, state maybe?
- Log.Information($"{plugin.Plugin.Name}");
+ PluginLog.Information($"{plugin.Name}");
}
}
@@ -302,18 +450,31 @@ namespace Dalamud.Interface
{
try
{
- this.dalamud.PluginManager.ReloadPlugins();
+ this.dalamud.PluginManager.ReloadAllPlugins();
}
catch (Exception ex)
{
this.dalamud.Framework.Gui.Chat.PrintError("Reload failed.");
- Log.Error(ex, "Plugin reload failed.");
+ PluginLog.Error(ex, "Plugin reload failed.");
}
}
+ if (ImGui.MenuItem("Scan dev plugins"))
+ {
+ this.dalamud.PluginManager.ScanDevPlugins();
+ }
+
+ ImGui.Separator();
+
+ if (ImGui.MenuItem("Load all API levels", null, this.dalamud.Configuration.LoadAllApiLevels))
+ {
+ this.dalamud.Configuration.LoadAllApiLevels = !this.dalamud.Configuration.LoadAllApiLevels;
+ this.dalamud.Configuration.Save();
+ }
+
ImGui.Separator();
ImGui.MenuItem("API Level:" + PluginManager.DalamudApiLevel, false);
- ImGui.MenuItem("Loaded plugins:" + this.dalamud.PluginManager?.Plugins.Count, false);
+ ImGui.MenuItem("Loaded plugins:" + this.dalamud.PluginManager?.InstalledPlugins.Count, false);
ImGui.EndMenu();
}
@@ -375,197 +536,6 @@ namespace Dalamud.Interface
ImGui.EndMainMenuBar();
}
}
-
- if (this.dalamud.Framework.Gui.GameUiHidden)
- return;
-
- this.windowSystem.Draw();
-
- if (this.isImguiDrawDemoWindow)
- ImGui.ShowDemoWindow();
- }
-
- ///
- /// Dispose the window system and all windows that require it.
- ///
- public void Dispose()
- {
- this.scratchpadWindow.Dispose();
- this.windowSystem.RemoveAllWindows();
-
- this.logWindow?.Dispose();
- this.creditsWindow?.Dispose();
- }
-
- ///
- /// Open the Plugin Installer window.
- ///
- internal void OpenPluginInstaller()
- {
- this.pluginWindow.IsOpen = true;
- }
-
- ///
- /// Open the changelog window.
- ///
- internal void OpenChangelog()
- {
- this.changelogWindow.IsOpen = true;
- }
-
- ///
- /// Open the settings window.
- ///
- internal void OpenSettings()
- {
- this.settingsWindow.IsOpen = true;
- }
-
- ///
- /// Open the log window.
- ///
- internal void OpenLog()
- {
- this.logWindow.IsOpen = true;
- }
-
- ///
- /// Open the data window.
- ///
- internal void OpenData()
- {
- this.dataWindow.IsOpen = true;
- }
-
- ///
- /// Open the credits window.
- ///
- internal void OpenCredits()
- {
- this.creditsWindow.IsOpen = true;
- }
-
- ///
- /// Open the stats window.
- ///
- internal void OpenPluginStats()
- {
- this.pluginStatWindow.IsOpen = true;
- }
-
- ///
- /// Open the component test window.
- ///
- internal void OpenComponentDemo()
- {
- this.componentDemoWindow.IsOpen = true;
- }
-
- ///
- /// Open the colors test window.
- ///
- internal void OpenColorsDemo()
- {
- this.colorDemoWindow.IsOpen = true;
- }
-
- ///
- /// Open the colors test window.
- ///
- internal void OpenScratchpadWindow()
- {
- this.scratchpadWindow.IsOpen = true;
- }
-
- ///
- /// Toggle the Plugin Installer window.
- ///
- internal void TogglePluginInstaller()
- {
- this.pluginWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the changelog window.
- ///
- internal void ToggleChangelog()
- {
- this.changelogWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the settings window.
- ///
- internal void ToggleSettings()
- {
- this.settingsWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the log window.
- ///
- internal void ToggleLog()
- {
- this.logWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the data window.
- ///
- internal void ToggleData()
- {
- this.dataWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the data window and preset the dropdown.
- ///
- /// The data kind to toggle.
- internal void ToggleData(string dataKind)
- {
- this.dataWindow.IsOpen ^= true;
- if (this.dataWindow.IsOpen)
- this.dataWindow.SetDataKind(dataKind);
- }
-
- ///
- /// Toggle the credits window.
- ///
- internal void ToggleCredits()
- {
- this.creditsWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the stats window.
- ///
- internal void TogglePluginStats()
- {
- this.pluginStatWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the component test window.
- ///
- internal void ToggleComponentDemo()
- {
- this.componentDemoWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the scratchpad window.
- ///
- internal void ToggleScratchpadWindow()
- {
- this.scratchpadWindow.IsOpen ^= true;
- }
-
- ///
- /// Toggle the gamepad notifier window window.
- ///
- internal void ToggleGamePadNotifierWindow()
- {
- this.gamepadModeNotifierWindow.IsOpen ^= true;
}
}
}
diff --git a/Dalamud/Interface/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
similarity index 92%
rename from Dalamud/Interface/InterfaceManager.cs
rename to Dalamud/Interface/Internal/InterfaceManager.cs
index fec66aa19..b4c51d275 100644
--- a/Dalamud/Interface/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -10,7 +10,6 @@ using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.Internal.DXGI;
using Dalamud.Hooking;
-using EasyHook;
using ImGuiNET;
using ImGuiScene;
using Serilog;
@@ -27,35 +26,24 @@ using SharpDX.Direct3D11;
* - Might eventually want to render to a separate target and composite, especially with reshade etc in the mix.
*/
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal
{
///
/// This class manages interaction with the ImGui interface.
///
internal class InterfaceManager : IDisposable
{
- ///
- /// Code that is exexuted when fonts are rebuilt.
- ///
- public Action OnBuildFonts;
-
- ///
- /// The pointer to ImGui.IO(), when it last used..
- ///
- public ImGuiIOPtr LastImGuiIoPtr;
-
private readonly Dalamud dalamud;
+ private readonly string rtssPath;
private readonly Hook presentHook;
private readonly Hook resizeBuffersHook;
private readonly Hook setCursorHook;
- private ManualResetEvent fontBuildSignal;
- private ISwapChainAddressResolver address;
+ private readonly ManualResetEvent fontBuildSignal;
+ private readonly ISwapChainAddressResolver address;
private RawDX11Scene scene;
- private string rtssPath;
-
// can't access imgui IO before first present call
private bool lastWantCapture = false;
private bool isRebuildingFonts = false;
@@ -95,12 +83,12 @@ namespace Dalamud.Interface
try
{
- var rtss = NativeFunctions.GetModuleHandle("RTSSHooks64.dll");
+ var rtss = NativeFunctions.GetModuleHandleW("RTSSHooks64.dll");
if (rtss != IntPtr.Zero)
{
var fileName = new StringBuilder(255);
- NativeFunctions.GetModuleFileName(rtss, fileName, fileName.Capacity);
+ _ = NativeFunctions.GetModuleFileNameW(rtss, fileName, fileName.Capacity);
this.rtssPath = fileName.ToString();
Log.Verbose("RTSS at {0}", this.rtssPath);
@@ -113,17 +101,16 @@ namespace Dalamud.Interface
Log.Error(e, "RTSS Free failed");
}
- var setCursorAddr = LocalHook.GetProcAddress("user32.dll", "SetCursor");
+ var user32 = NativeFunctions.GetModuleHandleW("user32.dll");
+ var setCursorAddr = NativeFunctions.GetProcAddress(user32, "SetCursor");
Log.Verbose("===== S W A P C H A I N =====");
- Log.Verbose("SetCursor address {SetCursor}", setCursorAddr);
- Log.Verbose("Present address {Present}", this.address.Present);
- Log.Verbose("ResizeBuffers address {ResizeBuffers}", this.address.ResizeBuffers);
+ Log.Verbose($"SetCursor address 0x{setCursorAddr.ToInt64():X}");
+ Log.Verbose($"Present address 0x{this.address.Present.ToInt64():X}");
+ Log.Verbose($"ResizeBuffers address 0x{this.address.ResizeBuffers.ToInt64():X}");
this.setCursorHook = new Hook(setCursorAddr, this.SetCursorDetour);
-
this.presentHook = new Hook(this.address.Present, this.PresentDetour);
-
this.resizeBuffersHook = new Hook(this.address.ResizeBuffers, this.ResizeBuffersDetour);
}
@@ -153,6 +140,21 @@ namespace Dalamud.Interface
///
public static ImFontPtr IconFont { get; private set; }
+ ///
+ /// Gets an included monospaced font.
+ ///
+ public static ImFontPtr MonoFont { get; private set; }
+
+ ///
+ /// Gets or sets an action that is exexuted when fonts are rebuilt.
+ ///
+ public Action OnBuildFonts { get; set; }
+
+ ///
+ /// Gets or sets the pointer to ImGui.IO(), when it was last used.
+ ///
+ public ImGuiIOPtr LastImGuiIoPtr { get; set; }
+
///
/// Gets the D3D11 device instance.
///
@@ -195,11 +197,11 @@ namespace Dalamud.Interface
{
if (!string.IsNullOrEmpty(this.rtssPath))
{
- NativeFunctions.LoadLibrary(this.rtssPath);
+ NativeFunctions.LoadLibraryW(this.rtssPath);
+ var rtssModule = NativeFunctions.GetModuleHandleW("RTSSHooks64.dll");
+ var installAddr = NativeFunctions.GetProcAddress(rtssModule, "InstallRTSSHook");
- var installAddr = LocalHook.GetProcAddress("RTSSHooks64.dll", "InstallRTSSHook");
- var installDele = Marshal.GetDelegateForFunctionPointer(installAddr);
- installDele.Invoke();
+ Marshal.GetDelegateForFunctionPointer(installAddr).Invoke();
}
}
catch (Exception ex)
@@ -467,6 +469,13 @@ namespace Dalamud.Interface
GCHandleType.Pinned);
IconFont = ImGui.GetIO().Fonts.AddFontFromFileTTF(fontPathIcon, 17.0f, null, iconRangeHandle.AddrOfPinnedObject());
+ var fontPathMono = Path.Combine(this.dalamud.AssetDirectory.FullName, "UIRes", "Inconsolata-Regular.ttf");
+
+ if (!File.Exists(fontPathMono))
+ ShowFontError(fontPathMono);
+
+ MonoFont = ImGui.GetIO().Fonts.AddFontFromFileTTF(fontPathMono, 16.0f);
+
Log.Verbose("[FONT] Invoke OnBuildFonts");
this.OnBuildFonts?.Invoke();
Log.Verbose("[FONT] OnBuildFonts OK!");
@@ -567,7 +576,7 @@ namespace Dalamud.Interface
{
ImGui.GetIO().ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad;
this.dalamud.ClientState.GamepadState.NavEnableGamepad ^= true;
- this.dalamud.DalamudUi.ToggleGamePadNotifierWindow();
+ this.dalamud.DalamudUi.ToggleGamepadModeNotifierWindow();
}
if (gamepadEnabled
@@ -592,7 +601,7 @@ namespace Dalamud.Interface
if (this.dalamud.ClientState.GamepadState.Pressed(GamepadButtons.R3) > 0)
{
- this.dalamud.DalamudUi.TogglePluginInstaller();
+ this.dalamud.DalamudUi.TogglePluginInstallerWindow();
}
}
}
diff --git a/Dalamud/Interface/Scratchpad/ScratchExecutionManager.cs b/Dalamud/Interface/Internal/Scratchpad/ScratchExecutionManager.cs
similarity index 94%
rename from Dalamud/Interface/Scratchpad/ScratchExecutionManager.cs
rename to Dalamud/Interface/Internal/Scratchpad/ScratchExecutionManager.cs
index 07010bc84..729827b1e 100644
--- a/Dalamud/Interface/Scratchpad/ScratchExecutionManager.cs
+++ b/Dalamud/Interface/Internal/Scratchpad/ScratchExecutionManager.cs
@@ -9,7 +9,7 @@ using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Serilog;
-namespace Dalamud.Interface.Scratchpad
+namespace Dalamud.Interface.Internal.Scratchpad
{
///
/// This class manages the execution of classes.
@@ -83,9 +83,9 @@ namespace Dalamud.Interface.Scratchpad
{
var script = CSharpScript.Create(code, options);
- var pi = new DalamudPluginInterface(this.dalamud, "Scratch-" + doc.Id, null, PluginLoadReason.Installer);
- var plugin = script.ContinueWith("return new ScratchPlugin() as IDalamudPlugin;").RunAsync().GetAwaiter().GetResult()
- .ReturnValue;
+ var pi = new DalamudPluginInterface(this.dalamud, "Scratch-" + doc.Id, null, PluginLoadReason.Unknown);
+ var plugin = script.ContinueWith("return new ScratchPlugin() as IDalamudPlugin;")
+ .RunAsync().GetAwaiter().GetResult().ReturnValue;
plugin.Initialize(pi);
diff --git a/Dalamud/Interface/Scratchpad/ScratchFileWatcher.cs b/Dalamud/Interface/Internal/Scratchpad/ScratchFileWatcher.cs
similarity index 96%
rename from Dalamud/Interface/Scratchpad/ScratchFileWatcher.cs
rename to Dalamud/Interface/Internal/Scratchpad/ScratchFileWatcher.cs
index 805ce11fe..88212a3f0 100644
--- a/Dalamud/Interface/Scratchpad/ScratchFileWatcher.cs
+++ b/Dalamud/Interface/Internal/Scratchpad/ScratchFileWatcher.cs
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.IO;
-namespace Dalamud.Interface.Scratchpad
+namespace Dalamud.Interface.Internal.Scratchpad
{
///
/// A file watcher for classes.
diff --git a/Dalamud/Interface/Scratchpad/ScratchLoadStatus.cs b/Dalamud/Interface/Internal/Scratchpad/ScratchLoadStatus.cs
similarity index 91%
rename from Dalamud/Interface/Scratchpad/ScratchLoadStatus.cs
rename to Dalamud/Interface/Internal/Scratchpad/ScratchLoadStatus.cs
index b9398c0f5..46ac0566e 100644
--- a/Dalamud/Interface/Scratchpad/ScratchLoadStatus.cs
+++ b/Dalamud/Interface/Internal/Scratchpad/ScratchLoadStatus.cs
@@ -1,4 +1,4 @@
-namespace Dalamud.Interface.Scratchpad
+namespace Dalamud.Interface.Internal.Scratchpad
{
///
/// The load status of a class.
diff --git a/Dalamud/Interface/Scratchpad/ScratchMacroProcessor.cs b/Dalamud/Interface/Internal/Scratchpad/ScratchMacroProcessor.cs
similarity index 99%
rename from Dalamud/Interface/Scratchpad/ScratchMacroProcessor.cs
rename to Dalamud/Interface/Internal/Scratchpad/ScratchMacroProcessor.cs
index f47c2cab2..ad20b493e 100644
--- a/Dalamud/Interface/Scratchpad/ScratchMacroProcessor.cs
+++ b/Dalamud/Interface/Internal/Scratchpad/ScratchMacroProcessor.cs
@@ -5,7 +5,7 @@ using System.Text.RegularExpressions;
using Dalamud.Plugin;
-namespace Dalamud.Interface.Scratchpad
+namespace Dalamud.Interface.Internal.Scratchpad
{
///
/// This class converts ScratchPad macros into runnable scripts.
diff --git a/Dalamud/Interface/Scratchpad/ScratchpadDocument.cs b/Dalamud/Interface/Internal/Scratchpad/ScratchpadDocument.cs
similarity index 96%
rename from Dalamud/Interface/Scratchpad/ScratchpadDocument.cs
rename to Dalamud/Interface/Internal/Scratchpad/ScratchpadDocument.cs
index 22c1e6b51..cd76a0135 100644
--- a/Dalamud/Interface/Scratchpad/ScratchpadDocument.cs
+++ b/Dalamud/Interface/Internal/Scratchpad/ScratchpadDocument.cs
@@ -1,6 +1,6 @@
using System;
-namespace Dalamud.Interface.Scratchpad
+namespace Dalamud.Interface.Internal.Scratchpad
{
///
/// This class represents a single document in the ScratchPad.
diff --git a/Dalamud/Interface/SerilogEventSink.cs b/Dalamud/Interface/Internal/SerilogEventSink.cs
similarity index 86%
rename from Dalamud/Interface/SerilogEventSink.cs
rename to Dalamud/Interface/Internal/SerilogEventSink.cs
index 4810f75b8..ac2c522ba 100644
--- a/Dalamud/Interface/SerilogEventSink.cs
+++ b/Dalamud/Interface/Internal/SerilogEventSink.cs
@@ -3,7 +3,7 @@ using System;
using Serilog.Core;
using Serilog.Events;
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal
{
///
/// Serilog event sink.
@@ -25,7 +25,7 @@ namespace Dalamud.Interface
///
/// Event on a log line being emitted.
///
- public event EventHandler<(string Line, LogEventLevel Level)> OnLogLine;
+ public event EventHandler<(string Line, LogEventLevel Level, DateTimeOffset TimeStamp)> OnLogLine;
///
/// Gets the default instance.
@@ -38,12 +38,14 @@ namespace Dalamud.Interface
/// Log event to be emitted.
public void Emit(LogEvent logEvent)
{
- var message = $"[{DateTimeOffset.Now:HH:mm:ss.fff}][{logEvent.Level}] {logEvent.RenderMessage(this.formatProvider)}";
+ var message = logEvent.RenderMessage(this.formatProvider);
if (logEvent.Exception != null)
+ {
message += "\n" + logEvent.Exception;
+ }
- this.OnLogLine?.Invoke(this, (message, logEvent.Level));
+ this.OnLogLine?.Invoke(this, (message, logEvent.Level, logEvent.Timestamp));
}
}
}
diff --git a/Dalamud/Interface/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs
similarity index 99%
rename from Dalamud/Interface/UiDebug.cs
rename to Dalamud/Interface/Internal/UiDebug.cs
index 16e2a75fc..3e7310633 100644
--- a/Dalamud/Interface/UiDebug.cs
+++ b/Dalamud/Interface/Internal/UiDebug.cs
@@ -14,7 +14,7 @@ using AlignmentType = FFXIVClientStructs.FFXIV.Component.GUI.AlignmentType;
// Customised version of https://github.com/aers/FFXIVUIDebug
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal
{
///
/// This class displays a debug window to inspect native addons.
diff --git a/Dalamud/Interface/DalamudChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs
similarity index 74%
rename from Dalamud/Interface/DalamudChangelogWindow.cs
rename to Dalamud/Interface/Internal/Windows/ChangelogWindow.cs
index 3d01bef83..69c2ade06 100644
--- a/Dalamud/Interface/DalamudChangelogWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs
@@ -1,15 +1,14 @@
using System.Diagnostics;
-using System.Numerics;
using Dalamud.Interface.Windowing;
using ImGuiNET;
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal.Windows
{
///
/// For major updates, an in-game Changelog window.
///
- internal class DalamudChangelogWindow : Window
+ internal sealed class ChangelogWindow : Window
{
///
/// Whether the latest update warrants a changelog window.
@@ -23,13 +22,13 @@ namespace Dalamud.Interface
If you note any issues or need help, please make sure to ask on our discord server.";
private readonly Dalamud dalamud;
- private string assemblyVersion = Util.AssemblyVersion;
+ private readonly string assemblyVersion = Util.AssemblyVersion;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The Dalamud instance.
- public DalamudChangelogWindow(Dalamud dalamud)
+ public ChangelogWindow(Dalamud dalamud)
: base("What's new in XIVLauncher?", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoResize)
{
this.dalamud = dalamud;
@@ -44,57 +43,63 @@ If you note any issues or need help, please make sure to ask on our discord serv
{
ImGui.Text($"The in-game addon has been updated to version D{this.assemblyVersion}.");
- ImGui.Dummy(new Vector2(10, 10) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(10);
ImGui.Text("The following changes were introduced:");
ImGui.Text(ChangeLog);
- ImGui.Dummy(new Vector2(10, 10) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(10);
ImGui.Text("Thank you for using our tools!");
- ImGui.Dummy(new Vector2(10, 10) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(10);
- ImGui.PushFont(InterfaceManager.IconFont);
+ ImGui.PushFont(UiBuilder.IconFont);
if (ImGui.Button(FontAwesomeIcon.Download.ToIconString()))
+ {
this.dalamud.DalamudUi.OpenPluginInstaller();
+ }
if (ImGui.IsItemHovered())
{
ImGui.PopFont();
ImGui.SetTooltip("Open Plugin Installer");
- ImGui.PushFont(InterfaceManager.IconFont);
+ ImGui.PushFont(UiBuilder.IconFont);
}
ImGui.SameLine();
if (ImGui.Button(FontAwesomeIcon.LaughBeam.ToIconString()))
+ {
Process.Start("https://discord.gg/3NMcUV5");
+ }
if (ImGui.IsItemHovered())
{
ImGui.PopFont();
ImGui.SetTooltip("Join our Discord server");
- ImGui.PushFont(InterfaceManager.IconFont);
+ ImGui.PushFont(UiBuilder.IconFont);
}
ImGui.SameLine();
if (ImGui.Button(FontAwesomeIcon.Globe.ToIconString()))
+ {
Process.Start("https://github.com/goatcorp/FFXIVQuickLauncher");
+ }
if (ImGui.IsItemHovered())
{
ImGui.PopFont();
ImGui.SetTooltip("See our GitHub repository");
- ImGui.PushFont(InterfaceManager.IconFont);
+ ImGui.PushFont(UiBuilder.IconFont);
}
ImGui.PopFont();
ImGui.SameLine();
- ImGui.Dummy(new Vector2(20, 0) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(20, 0);
ImGui.SameLine();
if (ImGui.Button("Close"))
diff --git a/Dalamud/Interface/Internal/Windows/ColorDemoWindow.cs b/Dalamud/Interface/Internal/Windows/ColorDemoWindow.cs
new file mode 100644
index 000000000..a1c1c1866
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/ColorDemoWindow.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Windowing;
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.Windows
+{
+ ///
+ /// Color Demo Window to view custom ImGui colors.
+ ///
+ internal sealed class ColorDemoWindow : Window
+ {
+ private readonly List<(string Name, Vector4 Color)> colors;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ColorDemoWindow()
+ : base("Dalamud Colors Demo")
+ {
+ this.Size = new Vector2(600, 500);
+ this.SizeCondition = ImGuiCond.FirstUseEver;
+
+ this.colors = new List<(string Name, Vector4 Color)>()
+ {
+ ("White", ImGuiColors.White),
+ ("DalamudRed", ImGuiColors.DalamudRed),
+ ("DalamudGrey", ImGuiColors.DalamudGrey),
+ ("DalamudGrey2", ImGuiColors.DalamudGrey2),
+ ("DalamudGrey3", ImGuiColors.DalamudGrey3),
+ ("DalamudWhite", ImGuiColors.DalamudWhite),
+ ("DalamudWhite2", ImGuiColors.DalamudWhite2),
+ ("DalamudOrange", ImGuiColors.DalamudOrange),
+ ("TankBlue", ImGuiColors.TankBlue),
+ ("HealerGreen", ImGuiColors.HealerGreen),
+ ("DPSRed", ImGuiColors.DPSRed),
+ }.OrderBy(colorDemo => colorDemo.Name).ToList();
+ }
+
+ ///
+ public override void Draw()
+ {
+ ImGui.Text("This is a collection of UI colors you can use in your plugin.");
+
+ ImGui.Separator();
+
+ foreach (var (name, color) in this.colors)
+ {
+ ImGui.TextColored(color, name);
+ }
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs b/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs
new file mode 100644
index 000000000..860a2ba15
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs
@@ -0,0 +1,160 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+
+using Dalamud.Interface.Animation;
+using Dalamud.Interface.Animation.EasingFunctions;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Components;
+using Dalamud.Interface.Windowing;
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.Windows
+{
+ ///
+ /// Component Demo Window to view custom ImGui components.
+ ///
+ internal sealed class ComponentDemoWindow : Window
+ {
+ private static readonly TimeSpan DefaultEasingTime = new(0, 0, 0, 1700);
+
+ private readonly List<(string Name, Action Demo)> componentDemos;
+ private readonly IReadOnlyList easings = new Easing[]
+ {
+ new InSine(DefaultEasingTime), new OutSine(DefaultEasingTime), new InOutSine(DefaultEasingTime),
+ new InCubic(DefaultEasingTime), new OutCubic(DefaultEasingTime), new InOutCubic(DefaultEasingTime),
+ new InQuint(DefaultEasingTime), new OutQuint(DefaultEasingTime), new InOutQuint(DefaultEasingTime),
+ new InCirc(DefaultEasingTime), new OutCirc(DefaultEasingTime), new InOutCirc(DefaultEasingTime),
+ new InElastic(DefaultEasingTime), new OutElastic(DefaultEasingTime), new InOutElastic(DefaultEasingTime),
+ };
+
+ private int animationTimeMs = (int)DefaultEasingTime.TotalMilliseconds;
+ private Vector4 defaultColor = ImGuiColors.DalamudOrange;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ComponentDemoWindow()
+ : base("Dalamud Components Demo")
+ {
+ this.Size = new Vector2(600, 500);
+ this.SizeCondition = ImGuiCond.FirstUseEver;
+
+ this.componentDemos = new()
+ {
+ ("Test", ImGuiComponents.Test),
+ ("HelpMarker", HelpMarkerDemo),
+ ("IconButton", IconButtonDemo),
+ ("TextWithLabel", TextWithLabelDemo),
+ ("ColorPickerWithPalette", this.ColorPickerWithPaletteDemo),
+ };
+ }
+
+ ///
+ public override void OnOpen()
+ {
+ foreach (var easing in this.easings)
+ {
+ easing.Restart();
+ }
+ }
+
+ ///
+ public override void OnClose()
+ {
+ foreach (var easing in this.easings)
+ {
+ easing.Stop();
+ }
+ }
+
+ ///
+ public override void Draw()
+ {
+ ImGui.Text("This is a collection of UI components you can use in your plugin.");
+
+ for (var i = 0; i < this.componentDemos.Count; i++)
+ {
+ var componentDemo = this.componentDemos[i];
+
+ if (ImGui.CollapsingHeader($"{componentDemo.Name}###comp{i}"))
+ {
+ componentDemo.Demo();
+ }
+ }
+
+ if (ImGui.CollapsingHeader("Easing animations"))
+ {
+ this.EasingsDemo();
+ }
+ }
+
+ private static void HelpMarkerDemo()
+ {
+ ImGui.Text("Hover over the icon to learn more.");
+ ImGuiComponents.HelpMarker("help me!");
+ }
+
+ private static void IconButtonDemo()
+ {
+ ImGui.Text("Click on the icon to use as a button.");
+ ImGui.SameLine();
+ if (ImGuiComponents.IconButton(1, FontAwesomeIcon.Carrot))
+ {
+ ImGui.OpenPopup("IconButtonDemoPopup");
+ }
+
+ if (ImGui.BeginPopup("IconButtonDemoPopup"))
+ {
+ ImGui.Text("You clicked!");
+ ImGui.EndPopup();
+ }
+ }
+
+ private static void TextWithLabelDemo()
+ {
+ ImGuiComponents.TextWithLabel("Label", "Hover to see more", "more");
+ }
+
+ private void EasingsDemo()
+ {
+ ImGui.SliderInt("Speed in MS", ref this.animationTimeMs, 200, 5000);
+
+ foreach (var easing in this.easings)
+ {
+ easing.Duration = new TimeSpan(0, 0, 0, 0, this.animationTimeMs);
+
+ if (!easing.IsRunning)
+ {
+ easing.Start();
+ }
+
+ var cursor = ImGui.GetCursorPos();
+ var p1 = new Vector2(cursor.X + 5, cursor.Y);
+ var p2 = p1 + new Vector2(45, 0);
+ easing.Point1 = p1;
+ easing.Point2 = p2;
+ easing.Update();
+
+ if (easing.IsDone)
+ {
+ easing.Restart();
+ }
+
+ ImGui.SetCursorPos(easing.EasedPoint);
+ ImGui.Bullet();
+
+ ImGui.SetCursorPos(cursor + new Vector2(0, 10));
+ ImGui.Text($"{easing.GetType().Name} ({easing.Value})");
+ ImGuiHelpers.ScaledDummy(5);
+ }
+ }
+
+ private void ColorPickerWithPaletteDemo()
+ {
+ ImGui.Text("Click on the color button to use the picker.");
+ ImGui.SameLine();
+ this.defaultColor = ImGuiComponents.ColorPickerWithPalette(1, "ColorPickerWithPalette Demo", this.defaultColor);
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs
new file mode 100644
index 000000000..92aa1a0c9
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs
@@ -0,0 +1,479 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Numerics;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using Dalamud.Configuration.Internal;
+using Dalamud.Game.Command;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Windowing;
+using ImGuiNET;
+using Serilog;
+using Serilog.Events;
+
+namespace Dalamud.Interface.Internal.Windows
+{
+ ///
+ /// The window that displays the Dalamud log file in-game.
+ ///
+ internal class ConsoleWindow : Window, IDisposable
+ {
+ private readonly Dalamud dalamud;
+
+ private readonly List logText = new();
+ private readonly object renderLock = new();
+
+ private readonly string[] logLevelStrings = new[] { "None", "Verbose", "Debug", "Information", "Warning", "Error", "Fatal" };
+
+ private List filteredLogText = new();
+ private bool autoScroll;
+ private bool openAtStartup;
+
+ private bool? lastCmdSuccess;
+
+ private string commandText = string.Empty;
+
+ private string textFilter = string.Empty;
+ private LogEventLevel? levelFilter = null;
+ private bool isFiltered = false;
+
+ private int historyPos;
+ private List history = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Dalamud instance.
+ public ConsoleWindow(Dalamud dalamud)
+ : base("Dalamud Console")
+ {
+ this.dalamud = dalamud;
+
+ this.autoScroll = this.dalamud.Configuration.LogAutoScroll;
+ this.openAtStartup = this.dalamud.Configuration.LogOpenAtStartup;
+ SerilogEventSink.Instance.OnLogLine += this.OnLogLine;
+
+ this.Size = new Vector2(500, 400);
+ this.SizeCondition = ImGuiCond.FirstUseEver;
+ }
+
+ private List LogEntries => this.isFiltered ? this.filteredLogText : this.logText;
+
+ ///
+ /// Dispose of managed and unmanaged resources.
+ ///
+ public void Dispose()
+ {
+ SerilogEventSink.Instance.OnLogLine -= this.OnLogLine;
+ }
+
+ ///
+ /// Clear the window of all log entries.
+ ///
+ public void Clear()
+ {
+ lock (this.renderLock)
+ {
+ this.logText.Clear();
+ this.filteredLogText.Clear();
+ }
+ }
+
+ ///
+ /// Add a single log line to the display.
+ ///
+ /// The line to add.
+ /// The level of the event.
+ /// The of the event.
+ public void HandleLogLine(string line, LogEventLevel level, DateTimeOffset offset)
+ {
+ if (line.IndexOfAny(new[] { '\n', '\r' }) != -1)
+ {
+ var subLines = line.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);
+
+ this.AddAndFilter(subLines[0], level, offset, false);
+
+ for (var i = 1; i < subLines.Length; i++)
+ {
+ this.AddAndFilter(subLines[i], level, offset, true);
+ }
+ }
+ else
+ {
+ this.AddAndFilter(line, level, offset, false);
+ }
+ }
+
+ ///
+ public override void Draw()
+ {
+ // Options menu
+ if (ImGui.BeginPopup("Options"))
+ {
+ if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll))
+ {
+ this.dalamud.Configuration.LogAutoScroll = this.autoScroll;
+ this.dalamud.Configuration.Save();
+ }
+
+ if (ImGui.Checkbox("Open at startup", ref this.openAtStartup))
+ {
+ this.dalamud.Configuration.LogOpenAtStartup = this.openAtStartup;
+ this.dalamud.Configuration.Save();
+ }
+
+ var prevLevel = (int)this.dalamud.LogLevelSwitch.MinimumLevel;
+ if (ImGui.Combo("Log Level", ref prevLevel, Enum.GetValues(typeof(LogEventLevel)).Cast().Select(x => x.ToString()).ToArray(), 6))
+ {
+ this.dalamud.LogLevelSwitch.MinimumLevel = (LogEventLevel)prevLevel;
+ this.dalamud.Configuration.LogLevel = (LogEventLevel)prevLevel;
+ this.dalamud.Configuration.Save();
+ }
+
+ ImGui.EndPopup();
+ }
+
+ // Filter menu
+ if (ImGui.BeginPopup("Filters"))
+ {
+ ImGui.Checkbox("Enabled", ref this.isFiltered);
+
+ if (ImGui.InputTextWithHint("##filterText", "Text Filter", ref this.textFilter, 255, ImGuiInputTextFlags.EnterReturnsTrue))
+ {
+ this.Refilter();
+ }
+
+ ImGui.TextColored(ImGuiColors.DalamudGrey, "Enter to confirm.");
+
+ var filterVal = this.levelFilter.HasValue ? (int)this.levelFilter.Value + 1 : 0;
+ if (ImGui.Combo("Level", ref filterVal, this.logLevelStrings, 7))
+ {
+ this.levelFilter = (LogEventLevel)(filterVal - 1);
+ this.Refilter();
+ }
+
+ ImGui.EndPopup();
+ }
+
+ ImGui.SameLine();
+ ImGui.PushFont(InterfaceManager.IconFont);
+
+ if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString()))
+ ImGui.OpenPopup("Options");
+
+ ImGui.SameLine();
+ if (ImGui.Button(FontAwesomeIcon.Search.ToIconString()))
+ ImGui.OpenPopup("Filters");
+
+ ImGui.SameLine();
+ var clear = ImGui.Button(FontAwesomeIcon.Trash.ToIconString());
+
+ ImGui.SameLine();
+ var copy = ImGui.Button(FontAwesomeIcon.Copy.ToIconString());
+
+ ImGui.SameLine();
+ if (ImGui.Button(FontAwesomeIcon.Skull.ToIconString()))
+ Process.GetCurrentProcess().Kill();
+
+ ImGui.PopFont();
+
+ ImGui.BeginChild("scrolling", new Vector2(0, ImGui.GetFrameHeightWithSpacing() - 55), false, ImGuiWindowFlags.HorizontalScrollbar);
+
+ if (clear)
+ {
+ this.Clear();
+ }
+
+ if (copy)
+ {
+ ImGui.LogToClipboard();
+ }
+
+ ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
+
+ ImGuiListClipperPtr clipper;
+ unsafe
+ {
+ clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
+ }
+
+ ImGui.PushFont(InterfaceManager.MonoFont);
+
+ var childPos = ImGui.GetWindowPos();
+ var childDrawList = ImGui.GetWindowDrawList();
+ var childSize = ImGui.GetWindowSize();
+
+ lock (this.renderLock)
+ {
+ clipper.Begin(this.LogEntries.Count);
+ while (clipper.Step())
+ {
+ for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
+ {
+ var line = this.LogEntries[i];
+
+ if (!line.IsMultiline)
+ ImGui.Separator();
+
+ ImGui.PushStyleColor(ImGuiCol.Header, this.GetColorForLogEventLevel(line.Level));
+ ImGui.PushStyleColor(ImGuiCol.HeaderActive, this.GetColorForLogEventLevel(line.Level));
+ ImGui.PushStyleColor(ImGuiCol.HeaderHovered, this.GetColorForLogEventLevel(line.Level));
+
+ ImGui.Selectable("###consolenull", true, ImGuiSelectableFlags.AllowItemOverlap | ImGuiSelectableFlags.SpanAllColumns);
+ ImGui.SameLine();
+
+ ImGui.PopStyleColor(3);
+
+ if (!line.IsMultiline)
+ {
+ ImGui.TextUnformatted(line.TimeStamp.ToString("HH:mm:ss.fff"));
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(92);
+ ImGui.TextUnformatted("|");
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(100);
+ ImGui.TextUnformatted(this.GetTextForLogEventLevel(line.Level));
+ ImGui.SameLine();
+ }
+
+ ImGui.SetCursorPosX(135);
+ ImGui.TextUnformatted(line.Line);
+ }
+ }
+
+ clipper.End();
+ }
+
+ ImGui.PopFont();
+
+ ImGui.PopStyleVar();
+
+ if (this.autoScroll && ImGui.GetScrollY() >= ImGui.GetScrollMaxY())
+ {
+ ImGui.SetScrollHereY(1.0f);
+ }
+
+ // Draw dividing line
+ childDrawList.AddLine(new Vector2(childPos.X + 127, childPos.Y), new Vector2(childPos.X + 127, childPos.Y + childSize.Y), 0x4FFFFFFF, 1.0f);
+
+ ImGui.EndChild();
+
+ var hadColor = false;
+ if (this.lastCmdSuccess.HasValue)
+ {
+ hadColor = true;
+ if (this.lastCmdSuccess.Value)
+ {
+ ImGui.PushStyleColor(ImGuiCol.FrameBg, ImGuiColors.HealerGreen - new Vector4(0, 0, 0, 0.7f));
+ }
+ else
+ {
+ ImGui.PushStyleColor(ImGuiCol.FrameBg, ImGuiColors.DalamudRed - new Vector4(0, 0, 0, 0.7f));
+ }
+ }
+
+ ImGui.SetNextItemWidth(ImGui.GetWindowSize().X - 80);
+
+ var getFocus = false;
+ unsafe
+ {
+ if (ImGui.InputText("##commandbox", ref this.commandText, 255, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackCompletion | ImGuiInputTextFlags.CallbackHistory, this.CommandInputCallback))
+ {
+ this.ProcessCommand();
+ getFocus = true;
+ }
+
+ ImGui.SameLine();
+ }
+
+ ImGui.SetItemDefaultFocus();
+ if (getFocus)
+ ImGui.SetKeyboardFocusHere(-1); // Auto focus previous widget
+
+ if (hadColor)
+ ImGui.PopStyleColor();
+
+ if (ImGui.Button("Send"))
+ {
+ this.ProcessCommand();
+ }
+ }
+
+ private void ProcessCommand()
+ {
+ try
+ {
+ this.historyPos = -1;
+ for (int i = this.history.Count - 1; i >= 0; i--)
+ {
+ if (this.history[i] == this.commandText)
+ {
+ this.history.RemoveAt(i);
+ break;
+ }
+ }
+
+ this.history.Add(this.commandText);
+
+ if (this.commandText == "clear" || this.commandText == "cls")
+ {
+ this.Clear();
+ return;
+ }
+
+ this.lastCmdSuccess = this.dalamud.CommandManager.ProcessCommand("/" + this.commandText);
+ this.commandText = string.Empty;
+
+ // TODO: Force scroll to bottom
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error during command dispatch");
+ this.lastCmdSuccess = false;
+ }
+ }
+
+ private unsafe int CommandInputCallback(ImGuiInputTextCallbackData* data)
+ {
+ var ptr = new ImGuiInputTextCallbackDataPtr(data);
+
+ switch (data->EventFlag)
+ {
+ case ImGuiInputTextFlags.CallbackCompletion:
+ var textBytes = new byte[data->BufTextLen];
+ Marshal.Copy((IntPtr)data->Buf, textBytes, 0, data->BufTextLen);
+ var text = Encoding.UTF8.GetString(textBytes);
+
+ var words = text.Split();
+
+ // We can't do any completion for parameters at the moment since it just calls into CommandHandler
+ if (words.Length > 1)
+ return 0;
+
+ // TODO: Improve this, add partial completion
+ // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L6443-L6484
+ var candidates = this.dalamud.CommandManager.Commands.Where(x => x.Key.Contains("/" + words[0])).ToList();
+ if (candidates.Count > 0)
+ {
+ ptr.DeleteChars(0, ptr.BufTextLen);
+ ptr.InsertChars(0, candidates[0].Key.Replace("/", string.Empty));
+ }
+
+ break;
+ case ImGuiInputTextFlags.CallbackHistory:
+ var prevPos = this.historyPos;
+
+ if (ptr.EventKey == ImGuiKey.UpArrow)
+ {
+ if (this.historyPos == -1)
+ this.historyPos = this.history.Count - 1;
+ else if (this.historyPos > 0)
+ this.historyPos--;
+ }
+ else if (data->EventKey == ImGuiKey.DownArrow)
+ {
+ if (this.historyPos != -1)
+ {
+ if (++this.historyPos >= this.history.Count)
+ {
+ this.historyPos = -1;
+ }
+ }
+ }
+
+ if (prevPos != this.historyPos)
+ {
+ var historyStr = this.historyPos >= 0 ? this.history[this.historyPos] : string.Empty;
+
+ ptr.DeleteChars(0, ptr.BufTextLen);
+ ptr.InsertChars(0, historyStr);
+ }
+
+ break;
+ }
+
+ return 0;
+ }
+
+ private void AddAndFilter(string line, LogEventLevel level, DateTimeOffset offset, bool isMultiline)
+ {
+ var entry = new LogEntry
+ {
+ IsMultiline = isMultiline,
+ Level = level,
+ Line = line,
+ TimeStamp = offset,
+ };
+
+ this.logText.Add(entry);
+
+ if (!this.isFiltered)
+ return;
+
+ if (this.IsFilterApplicable(entry))
+ this.filteredLogText.Add(entry);
+ }
+
+ private bool IsFilterApplicable(LogEntry entry)
+ {
+ if (this.levelFilter.HasValue)
+ {
+ return entry.Level == this.levelFilter.Value;
+ }
+
+ if (!string.IsNullOrEmpty(this.textFilter))
+ return entry.Line.Contains(this.textFilter);
+
+ return true;
+ }
+
+ private void Refilter()
+ {
+ lock (this.renderLock)
+ {
+ this.filteredLogText = this.logText.Where(this.IsFilterApplicable).ToList();
+ }
+ }
+
+ private string GetTextForLogEventLevel(LogEventLevel level) => level switch
+ {
+ LogEventLevel.Error => "ERR",
+ LogEventLevel.Verbose => "VRB",
+ LogEventLevel.Debug => "DBG",
+ LogEventLevel.Information => "INF",
+ LogEventLevel.Warning => "WRN",
+ LogEventLevel.Fatal => "FTL",
+ _ => throw new ArgumentOutOfRangeException(level.ToString(), "Invalid LogEventLevel"),
+ };
+
+ private uint GetColorForLogEventLevel(LogEventLevel level) => level switch
+ {
+ LogEventLevel.Error => 0x800000EE,
+ LogEventLevel.Verbose => 0x00000000,
+ LogEventLevel.Debug => 0x00000000,
+ LogEventLevel.Information => 0x00000000,
+ LogEventLevel.Warning => 0x8A0070EE,
+ LogEventLevel.Fatal => 0xFF00000A,
+ _ => throw new ArgumentOutOfRangeException(level.ToString(), "Invalid LogEventLevel"),
+ };
+
+ private void OnLogLine(object sender, (string Line, LogEventLevel Level, DateTimeOffset Offset) logEvent)
+ {
+ this.HandleLogLine(logEvent.Line, logEvent.Level, logEvent.Offset);
+ }
+
+ private class LogEntry
+ {
+ public string Line { get; set; }
+
+ public LogEventLevel Level { get; set; }
+
+ public DateTimeOffset TimeStamp { get; set; }
+
+ public bool IsMultiline { get; set; }
+ }
+ }
+}
diff --git a/Dalamud/Interface/DalamudCreditsWindow.cs b/Dalamud/Interface/Internal/Windows/CreditsWindow.cs
similarity index 57%
rename from Dalamud/Interface/DalamudCreditsWindow.cs
rename to Dalamud/Interface/Internal/Windows/CreditsWindow.cs
index d15c64909..a307d0161 100644
--- a/Dalamud/Interface/DalamudCreditsWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/CreditsWindow.cs
@@ -1,20 +1,21 @@
using System;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Numerics;
-using Dalamud.Game.Internal;
using Dalamud.Interface.Windowing;
using ImGuiNET;
using ImGuiScene;
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal.Windows
{
///
/// A window documenting contributors to the project.
///
- internal class DalamudCreditsWindow : Window, IDisposable
+ internal class CreditsWindow : Window, IDisposable
{
+ private const float CreditFPS = 60.0f;
private const string CreditsTextTempl = @"
Dalamud
A FFXIV Hooking Framework
@@ -89,13 +90,25 @@ Kubera
Truci
Haplo
Franz
+aers
+
+
+We use these awesome
+FFXIV C# libraries:
+
+Lumina by Adam
+FFXIVClientStructs by aers
+
+
+
+Thanks to everyone in the XIVLauncher
+Discord server
-Everyone in the XIVLauncher Discord server
Join us at: https://discord.gg/3NMcUV5
-Licensed under AGPL
+Licensed under AGPL V3 or later
Contribute at: https://github.com/goatsoft/Dalamud
@@ -103,22 +116,21 @@ Thank you for using XIVLauncher and Dalamud!
";
private readonly Dalamud dalamud;
- private TextureWrap logoTexture;
- private Framework framework;
+ private readonly TextureWrap logoTexture;
+ private readonly Stopwatch creditsThrottler;
private string creditsText;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The Dalamud instance.
- public DalamudCreditsWindow(Dalamud dalamud)
+ public CreditsWindow(Dalamud dalamud)
: base("Dalamud Credits", ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize, true)
{
this.dalamud = dalamud;
- this.logoTexture = this.dalamud.InterfaceManager.LoadImage(
- Path.Combine(this.dalamud.AssetDirectory.FullName, "UIRes", "logo.png"));
- this.framework = dalamud.Framework;
+ this.logoTexture = this.dalamud.InterfaceManager.LoadImage(Path.Combine(this.dalamud.AssetDirectory.FullName, "UIRes", "logo.png"));
+ this.creditsThrottler = new();
this.Size = new Vector2(500, 400);
this.SizeCondition = ImGuiCond.Always;
@@ -131,22 +143,22 @@ Thank you for using XIVLauncher and Dalamud!
///
public override void OnOpen()
{
- base.OnOpen();
+ var pluginCredits = this.dalamud.PluginManager.InstalledPlugins
+ .Where(plugin => plugin.Manifest != null)
+ .Select(plugin => $"{plugin.Manifest.Name} by {plugin.Manifest.Author}\n")
+ .Aggregate(string.Empty, (current, next) => $"{current}{next}");
- var pluginCredits = this.dalamud.PluginManager.Plugins.Where(x => x.Definition != null).Aggregate(string.Empty, (current, plugin) => current + $"{plugin.Definition.Name} by {plugin.Definition.Author}\n");
+ this.creditsText = string.Format(CreditsTextTempl, typeof(Dalamud).Assembly.GetName().Version, pluginCredits);
- this.creditsText =
- string.Format(CreditsTextTempl, typeof(Dalamud).Assembly.GetName().Version, pluginCredits);
-
- this.framework.Gui.SetBgm(132);
+ this.dalamud.Framework.Gui.SetBgm(132);
+ this.creditsThrottler.Restart();
}
///
public override void OnClose()
{
- base.OnClose();
-
- this.framework.Gui.SetBgm(9999);
+ this.creditsThrottler.Reset();
+ this.dalamud.Framework.Gui.SetBgm(9999);
}
///
@@ -155,19 +167,19 @@ Thank you for using XIVLauncher and Dalamud!
var screenSize = ImGui.GetMainViewport().Size;
var windowSize = ImGui.GetWindowSize();
- this.Position = new Vector2((screenSize.X / 2) - (windowSize.X / 2), (screenSize.Y / 2) - (windowSize.Y / 2));
+ this.Position = (screenSize - windowSize) / 2;
- ImGui.BeginChild("scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.NoScrollbar);
+ ImGui.BeginChild("scrolling", Vector2.Zero, false, ImGuiWindowFlags.NoScrollbar);
- ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0));
+ ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
- ImGui.Dummy(new Vector2(0, 340f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(0, 340f);
ImGui.Text(string.Empty);
ImGui.SameLine(150f);
- ImGui.Image(this.logoTexture.ImGuiHandle, new Vector2(190f, 190f) * ImGui.GetIO().FontGlobalScale);
+ ImGui.Image(this.logoTexture.ImGuiHandle, ImGuiHelpers.ScaledVector2(190f, 190f));
- ImGui.Dummy(new Vector2(0, 20f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(0, 20f);
var windowX = ImGui.GetWindowSize().X;
@@ -182,12 +194,15 @@ Thank you for using XIVLauncher and Dalamud!
ImGui.PopStyleVar();
- var curY = ImGui.GetScrollY();
- var maxY = ImGui.GetScrollMaxY();
-
- if (curY < maxY - 1)
+ if (this.creditsThrottler.Elapsed.TotalMilliseconds > (1000.0f / CreditFPS))
{
- ImGui.SetScrollY(curY + 1);
+ var curY = ImGui.GetScrollY();
+ var maxY = ImGui.GetScrollMaxY();
+
+ if (curY < maxY - 1)
+ {
+ ImGui.SetScrollY(curY + 1);
+ }
}
ImGui.EndChild();
diff --git a/Dalamud/Interface/Internal/Windows/DataWindow.cs b/Dalamud/Interface/Internal/Windows/DataWindow.cs
new file mode 100644
index 000000000..6b723098f
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/DataWindow.cs
@@ -0,0 +1,825 @@
+using System;
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Linq;
+using System.Numerics;
+
+using Dalamud.Game.ClientState;
+using Dalamud.Game.ClientState.Actors.Types;
+using Dalamud.Game.ClientState.Actors.Types.NonPlayer;
+using Dalamud.Game.ClientState.Structs.JobGauge;
+using Dalamud.Game.Internal;
+using Dalamud.Game.Internal.Gui.Addon;
+using Dalamud.Game.Internal.Gui.Toast;
+using Dalamud.Game.Text;
+using Dalamud.Interface.Windowing;
+using Dalamud.Plugin;
+using ImGuiNET;
+using ImGuiScene;
+using Newtonsoft.Json;
+using Serilog;
+
+namespace Dalamud.Interface.Internal.Windows
+{
+ ///
+ /// Class responsible for drawing the data/debug window.
+ ///
+ internal class DataWindow : Window
+ {
+ private readonly Dalamud dalamud;
+ private readonly string[] dataKindNames = Enum.GetNames(typeof(DataKind)).Select(k => k.Replace("_", " ")).ToArray();
+
+ private bool wasReady;
+ private string serverOpString;
+ private DataKind currentKind;
+
+ private bool drawActors = false;
+ private float maxActorDrawDistance = 20;
+
+ private string inputSig = string.Empty;
+ private IntPtr sigResult = IntPtr.Zero;
+
+ private string inputAddonName = string.Empty;
+ private int inputAddonIndex;
+ private Addon resultAddon;
+
+ private IntPtr findAgentInterfacePtr;
+
+ private bool resolveGameData = false;
+
+ private UIDebug addonInspector = null;
+
+ private string inputTextToast = string.Empty;
+ private int toastPosition = 0;
+ private int toastSpeed = 0;
+ private int questToastPosition = 0;
+ private bool questToastSound = false;
+ private int questToastIconId = 0;
+ private bool questToastCheckmark = false;
+
+ private string inputTexPath = string.Empty;
+ private TextureWrap debugTex = null;
+ private Vector2 inputTexUv0 = Vector2.Zero;
+ private Vector2 inputTexUv1 = Vector2.One;
+ private Vector4 inputTintCol = Vector4.One;
+ private Vector2 inputTexScale = Vector2.Zero;
+
+ private uint copyButtonIndex = 0;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Dalamud instance to access data of.
+ public DataWindow(Dalamud dalamud)
+ : base("Dalamud Data")
+ {
+ this.dalamud = dalamud;
+
+ this.Size = new Vector2(500, 500);
+ this.SizeCondition = ImGuiCond.FirstUseEver;
+
+ this.Load();
+ }
+
+ private enum DataKind
+ {
+ Server_OpCode,
+ Address,
+ Actor_Table,
+ Fate_Table,
+ Font_Test,
+ Party_List,
+ Plugin_IPC,
+ Condition,
+ Gauge,
+ Command,
+ Addon,
+ Addon_Inspector,
+ StartInfo,
+ Target,
+ Toast,
+ ImGui,
+ Tex,
+ Gamepad,
+ }
+
+ ///
+ /// Set the DataKind dropdown menu.
+ ///
+ /// Data kind name, can be lower and/or without spaces.
+ public void SetDataKind(string dataKind)
+ {
+ if (string.IsNullOrEmpty(dataKind))
+ return;
+
+ if (dataKind == "ai")
+ dataKind = "Addon Inspector";
+
+ dataKind = dataKind.Replace(" ", string.Empty).ToLower();
+ var dataKinds = Enum.GetValues(typeof(DataKind))
+ .Cast()
+ .Where(k => nameof(k).Replace("_", string.Empty).ToLower() == dataKind)
+ .ToList();
+
+ if (dataKinds.Count > 0)
+ {
+ this.currentKind = dataKinds.First();
+ }
+ else
+ {
+ this.dalamud.Framework.Gui.Chat.PrintError("/xldata: Invalid Data Type");
+ }
+ }
+
+ ///
+ /// Draw the window via ImGui.
+ ///
+ public override void Draw()
+ {
+ this.copyButtonIndex = 0;
+
+ // Main window
+ if (ImGui.Button("Force Reload"))
+ this.Load();
+ ImGui.SameLine();
+ var copy = ImGui.Button("Copy all");
+ ImGui.SameLine();
+
+ var currentKindIndex = (int)this.currentKind;
+ if (ImGui.Combo("Data kind", ref currentKindIndex, this.dataKindNames, this.dataKindNames.Length))
+ {
+ this.currentKind = (DataKind)currentKindIndex;
+ }
+
+ ImGui.Checkbox("Resolve GameData", ref this.resolveGameData);
+
+ ImGui.BeginChild("scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.HorizontalScrollbar);
+
+ if (copy)
+ ImGui.LogToClipboard();
+
+ ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0));
+
+ try
+ {
+ if (this.wasReady)
+ {
+ switch (this.currentKind)
+ {
+ case DataKind.Server_OpCode:
+ this.DrawServerOpCode();
+ break;
+
+ case DataKind.Address:
+ this.DrawAddress();
+ break;
+
+ case DataKind.Actor_Table:
+ this.DrawActorTable();
+ break;
+
+ case DataKind.Fate_Table:
+ this.DrawFateTable();
+ break;
+
+ case DataKind.Font_Test:
+ this.DrawFontTest();
+ break;
+
+ case DataKind.Party_List:
+ this.DrawPartyList();
+ break;
+
+ case DataKind.Plugin_IPC:
+ this.DrawPluginIPC();
+ break;
+
+ case DataKind.Condition:
+ this.DrawCondition();
+ break;
+
+ case DataKind.Gauge:
+ this.DrawGauge();
+ break;
+
+ case DataKind.Command:
+ this.DrawCommand();
+ break;
+
+ case DataKind.Addon:
+ this.DrawAddon();
+ break;
+
+ case DataKind.Addon_Inspector:
+ this.DrawAddonInspector();
+ break;
+
+ case DataKind.StartInfo:
+ this.DrawStartInfo();
+ break;
+
+ case DataKind.Target:
+ this.DrawTarget();
+ break;
+
+ case DataKind.Toast:
+ this.DrawToast();
+ break;
+
+ case DataKind.ImGui:
+ this.DrawImGui();
+ break;
+
+ case DataKind.Tex:
+ this.DrawTex();
+ break;
+
+ case DataKind.Gamepad:
+ this.DrawGamepad();
+ break;
+ }
+ }
+ else
+ {
+ ImGui.TextUnformatted("Data not ready.");
+ }
+ }
+ catch (Exception ex)
+ {
+ ImGui.TextUnformatted(ex.ToString());
+ }
+
+ ImGui.PopStyleVar();
+
+ ImGui.EndChild();
+ }
+
+ private void DrawServerOpCode()
+ {
+ ImGui.TextUnformatted(this.serverOpString);
+ }
+
+ private void DrawAddress()
+ {
+ ImGui.InputText(".text sig", ref this.inputSig, 400);
+ if (ImGui.Button("Resolve"))
+ {
+ try
+ {
+ this.sigResult = this.dalamud.SigScanner.ScanText(this.inputSig);
+ }
+ catch (KeyNotFoundException)
+ {
+ this.sigResult = new IntPtr(-1);
+ }
+ }
+
+ ImGui.Text($"Result: {this.sigResult.ToInt64():X}");
+ ImGui.SameLine();
+ if (ImGui.Button($"C{this.copyButtonIndex++}"))
+ ImGui.SetClipboardText(this.sigResult.ToInt64().ToString("x"));
+
+ foreach (var debugScannedValue in BaseAddressResolver.DebugScannedValues)
+ {
+ ImGui.TextUnformatted($"{debugScannedValue.Key}");
+ foreach (var valueTuple in debugScannedValue.Value)
+ {
+ ImGui.TextUnformatted(
+ $" {valueTuple.Item1} - 0x{valueTuple.Item2.ToInt64():x}");
+ ImGui.SameLine();
+
+ if (ImGui.Button($"C##copyAddress{this.copyButtonIndex++}"))
+ ImGui.SetClipboardText(valueTuple.Item2.ToInt64().ToString("x"));
+ }
+ }
+ }
+
+ private void DrawActorTable()
+ {
+ var stateString = string.Empty;
+
+ // LocalPlayer is null in a number of situations (at least with the current visible-actors list)
+ // which would crash here.
+ if (this.dalamud.ClientState.Actors.Length == 0)
+ {
+ ImGui.TextUnformatted("Data not ready.");
+ }
+ else if (this.dalamud.ClientState.LocalPlayer == null)
+ {
+ ImGui.TextUnformatted("LocalPlayer null.");
+ }
+ else
+ {
+ stateString += $"FrameworkBase: {this.dalamud.Framework.Address.BaseAddress.ToInt64():X}\n";
+ stateString += $"ActorTableLen: {this.dalamud.ClientState.Actors.Length}\n";
+ stateString += $"LocalPlayerName: {this.dalamud.ClientState.LocalPlayer.Name}\n";
+ stateString += $"CurrentWorldName: {(this.resolveGameData ? this.dalamud.ClientState.LocalPlayer.CurrentWorld.GameData.Name : this.dalamud.ClientState.LocalPlayer.CurrentWorld.Id.ToString())}\n";
+ stateString += $"HomeWorldName: {(this.resolveGameData ? this.dalamud.ClientState.LocalPlayer.HomeWorld.GameData.Name : this.dalamud.ClientState.LocalPlayer.HomeWorld.Id.ToString())}\n";
+ stateString += $"LocalCID: {this.dalamud.ClientState.LocalContentId:X}\n";
+ stateString += $"LastLinkedItem: {this.dalamud.Framework.Gui.Chat.LastLinkedItemId}\n";
+ stateString += $"TerritoryType: {this.dalamud.ClientState.TerritoryType}\n\n";
+
+ ImGui.TextUnformatted(stateString);
+
+ ImGui.Checkbox("Draw actors on screen", ref this.drawActors);
+ ImGui.SliderFloat("Draw Distance", ref this.maxActorDrawDistance, 2f, 40f);
+
+ for (var i = 0; i < this.dalamud.ClientState.Actors.Length; i++)
+ {
+ var actor = this.dalamud.ClientState.Actors[i];
+
+ if (actor == null)
+ continue;
+
+ this.PrintActor(actor, i.ToString());
+
+ if (this.drawActors && this.dalamud.Framework.Gui.WorldToScreen(actor.Position, out var screenCoords))
+ {
+ // So, while WorldToScreen will return false if the point is off of game client screen, to
+ // to avoid performance issues, we have to manually determine if creating a window would
+ // produce a new viewport, and skip rendering it if so
+ var actorText = $"{actor.Address.ToInt64():X}:{actor.ActorId:X}[{i}] - {actor.ObjectKind} - {actor.Name}";
+
+ var screenPos = ImGui.GetMainViewport().Pos;
+ var screenSize = ImGui.GetMainViewport().Size;
+
+ var windowSize = ImGui.CalcTextSize(actorText);
+
+ // Add some extra safety padding
+ windowSize.X += ImGui.GetStyle().WindowPadding.X + 10;
+ windowSize.Y += ImGui.GetStyle().WindowPadding.Y + 10;
+
+ if (screenCoords.X + windowSize.X > screenPos.X + screenSize.X ||
+ screenCoords.Y + windowSize.Y > screenPos.Y + screenSize.Y)
+ continue;
+
+ if (actor.YalmDistanceX > this.maxActorDrawDistance)
+ continue;
+
+ ImGui.SetNextWindowPos(new Vector2(screenCoords.X, screenCoords.Y));
+
+ ImGui.SetNextWindowBgAlpha(Math.Max(1f - (actor.YalmDistanceX / this.maxActorDrawDistance), 0.2f));
+ if (ImGui.Begin(
+ $"Actor{i}##ActorWindow{i}",
+ ImGuiWindowFlags.NoDecoration |
+ ImGuiWindowFlags.AlwaysAutoResize |
+ ImGuiWindowFlags.NoSavedSettings |
+ ImGuiWindowFlags.NoMove |
+ ImGuiWindowFlags.NoMouseInputs |
+ ImGuiWindowFlags.NoDocking |
+ ImGuiWindowFlags.NoFocusOnAppearing |
+ ImGuiWindowFlags.NoNav))
+ ImGui.Text(actorText);
+ ImGui.End();
+ }
+ }
+ }
+ }
+
+ private void DrawFateTable()
+ {
+ var stateString = string.Empty;
+ if (this.dalamud.ClientState.Fates.Length == 0)
+ {
+ ImGui.TextUnformatted("No fates or data not ready.");
+ }
+ else
+ {
+ stateString += $"FrameworkBase: {this.dalamud.Framework.Address.BaseAddress.ToInt64():X}\n";
+ stateString += $"FateTableLen: {this.dalamud.ClientState.Fates.Length}\n";
+
+ ImGui.TextUnformatted(stateString);
+
+ for (var i = 0; i < this.dalamud.ClientState.Fates.Length; i++)
+ {
+ var fate = this.dalamud.ClientState.Fates[i];
+ if (fate == null)
+ continue;
+
+ var fateString = $"{fate.Address.ToInt64():X}:[{i}]" +
+ $" - Lv.{fate.Level} {fate.Name} ({fate.Progress}%)" +
+ $" - X{fate.Position.X} Y{fate.Position.Y} Z{fate.Position.Z}" +
+ $" - Territory {(this.resolveGameData ? (fate.TerritoryType.GameData?.Name ?? fate.TerritoryType.Id.ToString()) : fate.TerritoryType.Id.ToString())}\n";
+
+ fateString += $" StartTimeEpoch: {fate.StartTimeEpoch}" +
+ $" - Duration: {fate.Duration}" +
+ $" - State: {fate.State}" +
+ $" - GameData name: {(this.resolveGameData ? (fate.GameData?.Name ?? fate.FateId.ToString()) : fate.FateId.ToString())}";
+
+ ImGui.TextUnformatted(fateString);
+ ImGui.SameLine();
+ if (ImGui.Button("C"))
+ {
+ ImGui.SetClipboardText(fate.Address.ToString("X"));
+ }
+ }
+ }
+ }
+
+ private void DrawFontTest()
+ {
+ var specialChars = string.Empty;
+
+ for (var i = 0xE020; i <= 0xE0DB; i++)
+ specialChars += $"0x{i:X} - {(SeIconChar)i} - {(char)i}\n";
+
+ ImGui.TextUnformatted(specialChars);
+
+ foreach (var fontAwesomeIcon in Enum.GetValues(typeof(FontAwesomeIcon)).Cast())
+ {
+ ImGui.Text(((int)fontAwesomeIcon.ToIconChar()).ToString("X") + " - ");
+ ImGui.SameLine();
+
+ ImGui.PushFont(UiBuilder.IconFont);
+ ImGui.Text(fontAwesomeIcon.ToIconString());
+ ImGui.PopFont();
+ }
+ }
+
+ private void DrawPartyList()
+ {
+ var partyString = string.Empty;
+
+ if (this.dalamud.ClientState.PartyList.Length == 0)
+ {
+ ImGui.TextUnformatted("Data not ready.");
+ }
+ else
+ {
+ partyString += $"{this.dalamud.ClientState.PartyList.Count} Members\n";
+ for (var i = 0; i < this.dalamud.ClientState.PartyList.Count; i++)
+ {
+ var member = this.dalamud.ClientState.PartyList[i];
+ if (member == null)
+ {
+ partyString +=
+ $"[{i}] was null\n";
+ continue;
+ }
+
+ partyString +=
+ $"[{i}] {member.CharacterName} - {member.ObjectKind} - {member.Actor.ActorId}\n";
+ }
+
+ ImGui.TextUnformatted(partyString);
+ }
+ }
+
+ private void DrawPluginIPC()
+ {
+#pragma warning disable CS0618 // Type or member is obsolete
+ var i1 = new DalamudPluginInterface(this.dalamud, "DalamudTestSub", null, PluginLoadReason.Unknown);
+ var i2 = new DalamudPluginInterface(this.dalamud, "DalamudTestPub", null, PluginLoadReason.Unknown);
+
+ if (ImGui.Button("Add test sub"))
+ {
+ i1.Subscribe("DalamudTestPub", o =>
+ {
+ dynamic msg = o;
+ Log.Debug(msg.Expand);
+ });
+ }
+
+ if (ImGui.Button("Add test sub any"))
+ {
+ i1.SubscribeAny((o, a) =>
+ {
+ dynamic msg = a;
+ Log.Debug($"From {o}: {msg.Expand}");
+ });
+ }
+
+ if (ImGui.Button("Remove test sub"))
+ i1.Unsubscribe("DalamudTestPub");
+
+ if (ImGui.Button("Remove test sub any"))
+ i1.UnsubscribeAny();
+
+ if (ImGui.Button("Send test message"))
+ {
+ dynamic testMsg = new ExpandoObject();
+ testMsg.Expand = "dong";
+ i2.SendMessage(testMsg);
+ }
+
+ // This doesn't actually work, so don't mind it - impl relies on plugins being registered in PluginManager
+ if (ImGui.Button("Send test message any"))
+ {
+ dynamic testMsg = new ExpandoObject();
+ testMsg.Expand = "dong";
+ i2.SendMessage("DalamudTestSub", testMsg);
+ }
+
+ foreach (var ipc in this.dalamud.PluginManager.IpcSubscriptions)
+ ImGui.Text($"Source:{ipc.SourcePluginName} Sub:{ipc.SubPluginName}");
+#pragma warning restore CS0618 // Type or member is obsolete
+ }
+
+ private void DrawCondition()
+ {
+#if DEBUG
+ ImGui.Text($"ptr: 0x{this.dalamud.ClientState.Condition.ConditionArrayBase.ToInt64():X}");
+#endif
+
+ ImGui.Text("Current Conditions:");
+ ImGui.Separator();
+
+ var didAny = false;
+
+ for (var i = 0; i < Condition.MaxConditionEntries; i++)
+ {
+ var typedCondition = (ConditionFlag)i;
+ var cond = this.dalamud.ClientState.Condition[typedCondition];
+
+ if (!cond) continue;
+
+ didAny = true;
+
+ ImGui.Text($"ID: {i} Enum: {typedCondition}");
+ }
+
+ if (!didAny)
+ ImGui.Text("None. Talk to a shop NPC or visit a market board to find out more!!!!!!!");
+ }
+
+ private void DrawGauge()
+ {
+ var gauge = this.dalamud.ClientState.JobGauges.Get();
+ ImGui.Text($"Moon: {gauge.ContainsSeal(SealType.MOON)} Drawn: {gauge.DrawnCard()}");
+ }
+
+ private void DrawCommand()
+ {
+ foreach (var command in this.dalamud.CommandManager.Commands)
+ ImGui.Text($"{command.Key}\n -> {command.Value.HelpMessage}\n -> In help: {command.Value.ShowInHelp}\n\n");
+ }
+
+ private void DrawAddon()
+ {
+ ImGui.InputText("Addon name", ref this.inputAddonName, 256);
+ ImGui.InputInt("Addon Index", ref this.inputAddonIndex);
+
+ if (ImGui.Button("Get Addon"))
+ {
+ this.resultAddon =
+ this.dalamud.Framework.Gui.GetAddonByName(
+ this.inputAddonName, this.inputAddonIndex);
+ }
+
+ if (ImGui.Button("Find Agent"))
+ this.findAgentInterfacePtr = this.dalamud.Framework.Gui.FindAgentInterface(this.inputAddonName);
+
+ if (this.resultAddon != null)
+ {
+ ImGui.TextUnformatted(
+ $"{this.resultAddon.Name} - 0x{this.resultAddon.Address.ToInt64():x}\n v:{this.resultAddon.Visible} x:{this.resultAddon.X} y:{this.resultAddon.Y} s:{this.resultAddon.Scale}, w:{this.resultAddon.Width}, h:{this.resultAddon.Height}");
+ }
+
+ if (this.findAgentInterfacePtr != IntPtr.Zero)
+ {
+ ImGui.TextUnformatted(
+ $"Agent: 0x{this.findAgentInterfacePtr.ToInt64():x}");
+ ImGui.SameLine();
+
+ if (ImGui.Button("C"))
+ ImGui.SetClipboardText(this.findAgentInterfacePtr.ToInt64().ToString("x"));
+ }
+
+ if (ImGui.Button("Get Base UI object"))
+ {
+ var addr = this.dalamud.Framework.Gui.GetBaseUIObject().ToInt64().ToString("x");
+ Log.Information("{0}", addr);
+ ImGui.SetClipboardText(addr);
+ }
+ }
+
+ private void DrawAddonInspector()
+ {
+ this.addonInspector ??= new UIDebug(this.dalamud);
+ this.addonInspector.Draw();
+ }
+
+ private void DrawStartInfo()
+ {
+ ImGui.Text(JsonConvert.SerializeObject(this.dalamud.StartInfo, Formatting.Indented));
+ }
+
+ private void DrawTarget()
+ {
+ var targetMgr = this.dalamud.ClientState.Targets;
+
+ if (targetMgr.CurrentTarget != null)
+ {
+ this.PrintActor(targetMgr.CurrentTarget, "CurrentTarget");
+ Util.ShowObject(targetMgr.CurrentTarget);
+ }
+
+ if (targetMgr.FocusTarget != null)
+ this.PrintActor(targetMgr.FocusTarget, "FocusTarget");
+
+ if (targetMgr.MouseOverTarget != null)
+ this.PrintActor(targetMgr.MouseOverTarget, "MouseOverTarget");
+
+ if (targetMgr.PreviousTarget != null)
+ this.PrintActor(targetMgr.PreviousTarget, "PreviousTarget");
+
+ if (targetMgr.SoftTarget != null)
+ this.PrintActor(targetMgr.SoftTarget, "SoftTarget");
+
+ if (ImGui.Button("Clear CT"))
+ targetMgr.ClearCurrentTarget();
+
+ if (ImGui.Button("Clear FT"))
+ targetMgr.ClearFocusTarget();
+
+ var localPlayer = this.dalamud.ClientState.LocalPlayer;
+
+ if (localPlayer != null)
+ {
+ if (ImGui.Button("Set CT"))
+ targetMgr.SetCurrentTarget(localPlayer);
+
+ if (ImGui.Button("Set FT"))
+ targetMgr.SetFocusTarget(localPlayer);
+ }
+ else
+ {
+ ImGui.Text("LocalPlayer is null.");
+ }
+ }
+
+ private void DrawToast()
+ {
+ ImGui.InputText("Toast text", ref this.inputTextToast, 200);
+
+ ImGui.Combo("Toast Position", ref this.toastPosition, new[] { "Bottom", "Top", }, 2);
+ ImGui.Combo("Toast Speed", ref this.toastSpeed, new[] { "Slow", "Fast", }, 2);
+ ImGui.Combo("Quest Toast Position", ref this.questToastPosition, new[] { "Centre", "Right", "Left" }, 3);
+ ImGui.Checkbox("Quest Checkmark", ref this.questToastCheckmark);
+ ImGui.Checkbox("Quest Play Sound", ref this.questToastSound);
+ ImGui.InputInt("Quest Icon ID", ref this.questToastIconId);
+
+ ImGuiHelpers.ScaledDummy(new Vector2(10, 10));
+
+ if (ImGui.Button("Show toast"))
+ {
+ this.dalamud.Framework.Gui.Toast.ShowNormal(this.inputTextToast, new ToastOptions
+ {
+ Position = (ToastPosition)this.toastPosition,
+ Speed = (ToastSpeed)this.toastSpeed,
+ });
+ }
+
+ if (ImGui.Button("Show Quest toast"))
+ {
+ this.dalamud.Framework.Gui.Toast.ShowQuest(this.inputTextToast, new QuestToastOptions
+ {
+ Position = (QuestToastPosition)this.questToastPosition,
+ DisplayCheckmark = this.questToastCheckmark,
+ IconId = (uint)this.questToastIconId,
+ PlaySound = this.questToastSound,
+ });
+ }
+
+ if (ImGui.Button("Show Error toast"))
+ {
+ this.dalamud.Framework.Gui.Toast.ShowError(this.inputTextToast);
+ }
+ }
+
+ private void DrawImGui()
+ {
+ ImGui.Text("Monitor count: " + ImGui.GetPlatformIO().Monitors.Size);
+ ImGui.Text("OverrideGameCursor: " + this.dalamud.InterfaceManager.OverrideGameCursor);
+
+ ImGui.Button("THIS IS A BUTTON###hoverTestButton");
+ this.dalamud.InterfaceManager.OverrideGameCursor = !ImGui.IsItemHovered();
+ }
+
+ private void DrawTex()
+ {
+ ImGui.InputText("Tex Path", ref this.inputTexPath, 255);
+ ImGui.InputFloat2("UV0", ref this.inputTexUv0);
+ ImGui.InputFloat2("UV1", ref this.inputTexUv1);
+ ImGui.InputFloat4("Tint", ref this.inputTintCol);
+ ImGui.InputFloat2("Scale", ref this.inputTexScale);
+
+ if (ImGui.Button("Load Tex"))
+ {
+ try
+ {
+ this.debugTex = this.dalamud.Data.GetImGuiTexture(this.inputTexPath);
+ this.inputTexScale = new Vector2(this.debugTex.Width, this.debugTex.Height);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Could not load tex.");
+ }
+ }
+
+ ImGuiHelpers.ScaledDummy(10);
+
+ if (this.debugTex != null)
+ {
+ ImGui.Image(this.debugTex.ImGuiHandle, this.inputTexScale, this.inputTexUv0, this.inputTexUv1, this.inputTintCol);
+ ImGuiHelpers.ScaledDummy(5);
+ Util.ShowObject(this.debugTex);
+ }
+ }
+
+ private void DrawGamepad()
+ {
+ static void DrawHelper(string text, uint mask, Func resolve)
+ {
+ ImGui.Text($"{text} {mask:X4}");
+ ImGui.Text($"DPadLeft {resolve(GamepadButtons.DpadLeft)} " +
+ $"DPadUp {resolve(GamepadButtons.DpadUp)} " +
+ $"DPadRight {resolve(GamepadButtons.DpadRight)} " +
+ $"DPadDown {resolve(GamepadButtons.DpadDown)} ");
+ ImGui.Text($"West {resolve(GamepadButtons.West)} " +
+ $"North {resolve(GamepadButtons.North)} " +
+ $"East {resolve(GamepadButtons.East)} " +
+ $"South {resolve(GamepadButtons.South)} ");
+ ImGui.Text($"L1 {resolve(GamepadButtons.L1)} " +
+ $"L2 {resolve(GamepadButtons.L2)} " +
+ $"R1 {resolve(GamepadButtons.R1)} " +
+ $"R2 {resolve(GamepadButtons.R2)} ");
+ ImGui.Text($"Select {resolve(GamepadButtons.Select)} " +
+ $"Start {resolve(GamepadButtons.Start)} " +
+ $"L3 {resolve(GamepadButtons.L3)} " +
+ $"R3 {resolve(GamepadButtons.R3)} ");
+ }
+#if DEBUG
+ ImGui.Text($"GamepadInput 0x{this.dalamud.ClientState.GamepadState.GamepadInput.ToInt64():X}");
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
+
+ if (ImGui.IsItemClicked())
+ ImGui.SetClipboardText($"0x{this.dalamud.ClientState.GamepadState.GamepadInput.ToInt64():X}");
+#endif
+
+ DrawHelper(
+ "Buttons Raw",
+ this.dalamud.ClientState.GamepadState.ButtonsRaw,
+ this.dalamud.ClientState.GamepadState.Raw);
+ DrawHelper(
+ "Buttons Pressed",
+ this.dalamud.ClientState.GamepadState.ButtonsPressed,
+ this.dalamud.ClientState.GamepadState.Pressed);
+ DrawHelper(
+ "Buttons Repeat",
+ this.dalamud.ClientState.GamepadState.ButtonsRepeat,
+ this.dalamud.ClientState.GamepadState.Repeat);
+ DrawHelper(
+ "Buttons Released",
+ this.dalamud.ClientState.GamepadState.ButtonsReleased,
+ this.dalamud.ClientState.GamepadState.Released);
+ ImGui.Text($"LeftStickLeft {this.dalamud.ClientState.GamepadState.LeftStickLeft:0.00} " +
+ $"LeftStickUp {this.dalamud.ClientState.GamepadState.LeftStickUp:0.00} " +
+ $"LeftStickRight {this.dalamud.ClientState.GamepadState.LeftStickRight:0.00} " +
+ $"LeftStickDown {this.dalamud.ClientState.GamepadState.LeftStickDown:0.00} ");
+ ImGui.Text($"RightStickLeft {this.dalamud.ClientState.GamepadState.RightStickLeft:0.00} " +
+ $"RightStickUp {this.dalamud.ClientState.GamepadState.RightStickUp:0.00} " +
+ $"RightStickRight {this.dalamud.ClientState.GamepadState.RightStickRight:0.00} " +
+ $"RightStickDown {this.dalamud.ClientState.GamepadState.RightStickDown:0.00} ");
+ }
+
+ private void Load()
+ {
+ if (this.dalamud.Data.IsDataReady)
+ {
+ this.serverOpString = JsonConvert.SerializeObject(this.dalamud.Data.ServerOpCodes, Formatting.Indented);
+ this.wasReady = true;
+ }
+ }
+
+ private void PrintActor(Actor actor, string tag)
+ {
+ var actorString =
+ $"{actor.Address.ToInt64():X}:{actor.ActorId:X}[{tag}] - {actor.ObjectKind} - {actor.Name} - X{actor.Position.X} Y{actor.Position.Y} Z{actor.Position.Z} D{actor.YalmDistanceX} R{actor.Rotation} - Target: {actor.TargetActorID:X}\n";
+
+ if (actor is Npc npc)
+ actorString += $" DataId: {npc.BaseId} NameId:{npc.NameId}\n";
+
+ if (actor is Chara chara)
+ {
+ actorString +=
+ $" Level: {chara.Level} ClassJob: {(this.resolveGameData ? chara.ClassJob.GameData.Name : chara.ClassJob.Id.ToString())} CHP: {chara.CurrentHp} MHP: {chara.MaxHp} CMP: {chara.CurrentMp} MMP: {chara.MaxMp}\n Customize: {BitConverter.ToString(chara.Customize).Replace("-", " ")} StatusFlags: {chara.StatusFlags}\n";
+ }
+
+ if (actor is PlayerCharacter pc)
+ {
+ actorString +=
+ $" HomeWorld: {(this.resolveGameData ? pc.HomeWorld.GameData.Name : pc.HomeWorld.Id.ToString())} CurrentWorld: {(this.resolveGameData ? pc.CurrentWorld.GameData.Name : pc.CurrentWorld.Id.ToString())} FC: {pc.CompanyTag}\n";
+ }
+
+ ImGui.TextUnformatted(actorString);
+ ImGui.SameLine();
+ if (ImGui.Button($"C##{this.copyButtonIndex++}"))
+ {
+ ImGui.SetClipboardText(actor.Address.ToInt64().ToString("X"));
+ }
+ }
+ }
+}
diff --git a/Dalamud/Interface/GamepadModeNotifierWindow.cs b/Dalamud/Interface/Internal/Windows/GamepadModeNotifierWindow.cs
similarity index 96%
rename from Dalamud/Interface/GamepadModeNotifierWindow.cs
rename to Dalamud/Interface/Internal/Windows/GamepadModeNotifierWindow.cs
index 8794a0386..2dc174fde 100644
--- a/Dalamud/Interface/GamepadModeNotifierWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/GamepadModeNotifierWindow.cs
@@ -1,10 +1,10 @@
-using System.Numerics;
+using System.Numerics;
using CheapLoc;
using Dalamud.Interface.Windowing;
using ImGuiNET;
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal.Windows
{
///
/// Class responsible for drawing a notifier on screen that gamepad mode is active.
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs
new file mode 100644
index 000000000..e3f867211
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/PluginInstallerWindow.cs
@@ -0,0 +1,1286 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Numerics;
+using System.Threading.Tasks;
+
+using CheapLoc;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Components;
+using Dalamud.Interface.Windowing;
+using Dalamud.Plugin;
+using Dalamud.Plugin.Internal;
+using Dalamud.Plugin.Internal.Exceptions;
+using Dalamud.Plugin.Internal.Types;
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.Windows
+{
+ ///
+ /// Class responsible for drawing the plugin installer.
+ ///
+ internal class PluginInstallerWindow : Window, IDisposable
+ {
+ private static readonly ModuleLog Log = new("PLUGINW");
+
+ private readonly Dalamud dalamud;
+
+ private bool errorModalDrawing = true;
+ private bool errorModalOnNextFrame = false;
+ private string errorModalMessage = string.Empty;
+
+ private int updatePluginCount = 0;
+ private List updatedPlugins;
+
+ private List pluginListAvailable = new();
+ private List pluginListInstalled = new();
+ private List pluginListUpdatable = new();
+ private bool hasDevPlugins = false;
+
+ private string searchText = string.Empty;
+
+ private PluginSortKind sortKind = PluginSortKind.Alphabetical;
+ private string filterText = Locs.SortBy_Alphabetical;
+
+ private OperationStatus installStatus = OperationStatus.Idle;
+ private OperationStatus updateStatus = OperationStatus.Idle;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Dalamud instance.
+ public PluginInstallerWindow(Dalamud dalamud)
+ : base(
+ Locs.WindowTitle + (dalamud.Configuration.DoPluginTest ? Locs.WindowTitleMod_Testing : string.Empty) + "###XlPluginInstaller",
+ ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoScrollbar)
+ {
+ this.dalamud = dalamud;
+ this.IsOpen = true;
+
+ this.Size = new Vector2(810, 520);
+ this.SizeCondition = ImGuiCond.Always;
+
+ // For debugging
+ if (this.dalamud.PluginManager.PluginsReady)
+ this.OnInstalledPluginsChanged();
+
+ this.dalamud.PluginManager.OnAvailablePluginsChanged += this.OnAvailablePluginsChanged;
+ this.dalamud.PluginManager.OnInstalledPluginsChanged += this.OnInstalledPluginsChanged;
+ }
+
+ private enum OperationStatus
+ {
+ Idle,
+ InProgress,
+ Complete,
+ }
+
+ private enum PluginSortKind
+ {
+ Alphabetical,
+ DownloadCount,
+ LastUpdate,
+ }
+
+ ///
+ public void Dispose()
+ {
+ this.dalamud.PluginManager.OnAvailablePluginsChanged -= this.OnAvailablePluginsChanged;
+ this.dalamud.PluginManager.OnInstalledPluginsChanged -= this.OnInstalledPluginsChanged;
+ }
+
+ ///
+ public override void OnOpen()
+ {
+ Task.Run(this.dalamud.PluginManager.ReloadPluginMasters);
+
+ this.updatePluginCount = 0;
+ this.updatedPlugins = null;
+
+ this.searchText = string.Empty;
+ this.sortKind = PluginSortKind.Alphabetical;
+ this.filterText = Locs.SortBy_Alphabetical;
+ }
+
+ ///
+ public override void Draw()
+ {
+ this.DrawHeader();
+ this.DrawPluginTabBar();
+ this.DrawFooter();
+ this.DrawErrorModal();
+ }
+
+ private static Vector2 GetButtonSize(string text) => ImGui.CalcTextSize(text) + (ImGui.GetStyle().FramePadding * 2);
+
+ private void DrawHeader()
+ {
+ var style = ImGui.GetStyle();
+ var windowSize = ImGui.GetWindowContentRegionMax();
+
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5 * ImGuiHelpers.GlobalScale));
+
+ var searchInputWidth = 240 * ImGuiHelpers.GlobalScale;
+
+ var sortByText = Locs.SortBy_Label;
+ var sortByTextWidth = ImGui.CalcTextSize(sortByText).X;
+ var sortSelectables = new (string Localization, PluginSortKind SortKind)[]
+ {
+ (Locs.SortBy_Alphabetical, PluginSortKind.Alphabetical),
+ (Locs.SortBy_DownloadCounts, PluginSortKind.DownloadCount),
+ (Locs.SortBy_LastUpdate, PluginSortKind.LastUpdate),
+ };
+ var longestSelectableWidth = sortSelectables.Select(t => ImGui.CalcTextSize(t.Localization).X).Max();
+ var selectableWidth = longestSelectableWidth + (style.FramePadding.X * 2); // This does not include the label
+ var sortSelectWidth = selectableWidth + sortByTextWidth + style.ItemInnerSpacing.X; // Item spacing between the selectable and the label
+
+ var headerText = Locs.Header_Hint;
+ var headerTextSize = ImGui.CalcTextSize(headerText);
+ ImGui.Text(headerText);
+
+ ImGui.SameLine();
+
+ // Shift down a little to align with the middle of the header text
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (headerTextSize.Y / 4) - 2);
+
+ ImGui.SetCursorPosX(windowSize.X - sortSelectWidth - style.ItemSpacing.X - searchInputWidth);
+ ImGui.SetNextItemWidth(searchInputWidth);
+ ImGui.InputTextWithHint("###XlPluginInstaller_Search", Locs.Header_SearchPlaceholder, ref this.searchText, 100);
+
+ ImGui.SameLine();
+ ImGui.SetCursorPosX(windowSize.X - sortSelectWidth);
+ ImGui.SetNextItemWidth(selectableWidth);
+ if (ImGui.BeginCombo(sortByText, this.filterText, ImGuiComboFlags.NoArrowButton))
+ {
+ foreach (var selectable in sortSelectables)
+ {
+ if (ImGui.Selectable(selectable.Localization))
+ {
+ this.sortKind = selectable.SortKind;
+ this.filterText = selectable.Localization;
+
+ this.ResortPlugins();
+ }
+ }
+
+ ImGui.EndCombo();
+ }
+ }
+
+ private void DrawFooter()
+ {
+ var windowSize = ImGui.GetWindowContentRegionMax();
+ var placeholderButtonSize = GetButtonSize("placeholder");
+
+ ImGui.Separator();
+
+ ImGui.SetCursorPosY(windowSize.Y - placeholderButtonSize.Y);
+
+ this.DrawUpdatePluginsButton();
+
+ ImGui.SameLine();
+ if (ImGui.Button(Locs.FooterButton_Settings))
+ {
+ this.dalamud.DalamudUi.OpenSettings();
+ }
+
+ // If any dev plugins are installed, allow a shortcut for the /xldev menu item
+ if (this.hasDevPlugins)
+ {
+ ImGui.SameLine();
+ if (ImGui.Button(Locs.FooterButton_ScanDevPlugins))
+ {
+ this.dalamud.PluginManager.ScanDevPlugins();
+ }
+ }
+
+ var closeText = Locs.FooterButton_Close;
+ var closeButtonSize = GetButtonSize(closeText);
+
+ ImGui.SameLine(windowSize.X - closeButtonSize.X);
+ if (ImGui.Button(closeText))
+ {
+ this.IsOpen = false;
+ this.dalamud.Configuration.Save();
+ }
+ }
+
+ private void DrawUpdatePluginsButton()
+ {
+ var ready = this.dalamud.PluginManager.PluginsReady && this.dalamud.PluginManager.ReposReady;
+
+ if (!ready || this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress)
+ {
+ ImGuiComponents.DisabledButton(Locs.FooterButton_UpdatePlugins);
+ }
+ else if (this.updateStatus == OperationStatus.Complete)
+ {
+ ImGui.Button(this.updatePluginCount > 0
+ ? Locs.FooterButton_UpdateComplete(this.updatePluginCount)
+ : Locs.FooterButton_NoUpdates);
+ }
+ else
+ {
+ if (ImGui.Button(Locs.FooterButton_UpdatePlugins))
+ {
+ this.updateStatus = OperationStatus.InProgress;
+
+ Task.Run(() => this.dalamud.PluginManager.UpdatePlugins())
+ .ContinueWith(task =>
+ {
+ this.updateStatus = OperationStatus.Complete;
+
+ if (task.IsFaulted)
+ {
+ this.updatePluginCount = 0;
+ this.updatedPlugins = null;
+ this.DisplayErrorContinuation(task, Locs.ErrorModal_UpdaterFatal);
+ }
+ else
+ {
+ this.updatedPlugins = task.Result.Where(res => res.WasUpdated).ToList();
+ this.updatePluginCount = this.updatedPlugins.Count;
+
+ var errorPlugins = task.Result.Where(res => !res.WasUpdated).ToList();
+ var errorPluginCount = errorPlugins.Count;
+
+ if (errorPluginCount > 0)
+ {
+ var errorMessage = this.updatePluginCount > 0
+ ? Locs.ErrorModal_UpdaterFailPartial(this.updatePluginCount, errorPluginCount)
+ : Locs.ErrorModal_UpdaterFail(errorPluginCount);
+
+ var hintInsert = errorPlugins
+ .Aggregate(string.Empty, (current, pluginUpdateStatus) => $"{current}* {pluginUpdateStatus.InternalName}\n")
+ .TrimEnd();
+ errorMessage += Locs.ErrorModal_HintBlame(hintInsert);
+
+ this.DisplayErrorContinuation(task, errorMessage);
+ }
+
+ if (this.updatePluginCount > 0)
+ {
+ this.dalamud.PluginManager.PrintUpdatedPlugins(this.updatedPlugins, Locs.PluginUpdateHeader_Chatbox);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ private void DrawErrorModal()
+ {
+ var modalTitle = Locs.ErrorModal_Title;
+
+ if (ImGui.BeginPopupModal(modalTitle, ref this.errorModalDrawing, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar))
+ {
+ ImGui.Text(this.errorModalMessage);
+ ImGui.Spacing();
+
+ var buttonWidth = 120f;
+ ImGui.SetCursorPosX((ImGui.GetWindowWidth() - buttonWidth) / 2);
+
+ if (ImGui.Button(Locs.ErrorModalButton_Ok, new Vector2(buttonWidth, 40)))
+ {
+ ImGui.CloseCurrentPopup();
+ }
+
+ ImGui.EndPopup();
+ }
+
+ if (this.errorModalOnNextFrame)
+ {
+ ImGui.OpenPopup(modalTitle);
+ this.errorModalOnNextFrame = false;
+ }
+ }
+
+ private void DrawPluginTabBar()
+ {
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5 * ImGuiHelpers.GlobalScale));
+
+ ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, ImGuiHelpers.ScaledVector2(1, 3));
+
+ if (ImGui.BeginTabBar("PluginsTabBar", ImGuiTabBarFlags.NoTooltip))
+ {
+ this.DrawPluginTab(Locs.TabTitle_AvailablePlugins, this.DrawAvailablePluginList);
+ this.DrawPluginTab(Locs.TabTitle_InstalledPlugins, this.DrawInstalledPluginList);
+
+ if (this.hasDevPlugins)
+ {
+ this.DrawPluginTab(Locs.TabTitle_InstalledDevPlugins, this.DrawInstalledDevPluginList);
+ }
+ }
+
+ ImGui.PopStyleVar();
+ }
+
+ private void DrawPluginTab(string title, Action drawPluginList)
+ {
+ if (ImGui.BeginTabItem(title))
+ {
+ ImGui.BeginChild($"Scrolling{title}", ImGuiHelpers.ScaledVector2(0, 384), true, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoBackground);
+
+ ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 5);
+
+ var ready = this.DrawPluginListLoading();
+
+ if (ready)
+ {
+ drawPluginList();
+ }
+
+ ImGui.EndChild();
+
+ ImGui.EndTabItem();
+ }
+ }
+
+ private void DrawAvailablePluginList()
+ {
+ var pluginList = this.pluginListAvailable;
+
+ if (pluginList.Count == 0)
+ {
+ ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_SearchNoCompatible);
+ return;
+ }
+
+ var filteredList = pluginList
+ .Where(rm => !this.IsManifestFiltered(rm))
+ .ToList();
+
+ if (filteredList.Count == 0)
+ {
+ ImGui.TextColored(ImGuiColors.DalamudGrey2, Locs.TabBody_SearchNoMatching);
+ return;
+ }
+
+ var i = 0;
+ foreach (var manifest in filteredList)
+ {
+ var (isInstalled, plugin) = this.IsManifestInstalled(manifest);
+
+ ImGui.PushID($"{manifest.InternalName}{manifest.AssemblyVersion}");
+
+ if (isInstalled)
+ {
+ this.DrawInstalledPlugin(plugin, i++, true);
+ }
+ else
+ {
+ this.DrawAvailablePlugin(manifest, i++);
+ }
+
+ ImGui.PopID();
+ }
+ }
+
+ private void DrawInstalledPluginList()
+ {
+ var pluginList = this.pluginListInstalled;
+
+ if (pluginList.Count == 0)
+ {
+ ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_SearchNoInstalled);
+ return;
+ }
+
+ var filteredList = pluginList
+ .Where(plugin => !this.IsManifestFiltered(plugin.Manifest))
+ .ToList();
+
+ if (filteredList.Count == 0)
+ {
+ ImGui.TextColored(ImGuiColors.DalamudGrey2, Locs.TabBody_SearchNoMatching);
+ return;
+ }
+
+ var i = 0;
+ foreach (var plugin in filteredList)
+ {
+ this.DrawInstalledPlugin(plugin, i++);
+ }
+ }
+
+ private void DrawInstalledDevPluginList()
+ {
+ var pluginList = this.pluginListInstalled
+ .Where(plugin => plugin.IsDev)
+ .ToList();
+
+ if (pluginList.Count == 0)
+ {
+ ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_SearchNoInstalled);
+ return;
+ }
+
+ var filteredList = pluginList
+ .Where(plugin => !this.IsManifestFiltered(plugin.Manifest))
+ .ToList();
+
+ if (filteredList.Count == 0)
+ {
+ ImGui.TextColored(ImGuiColors.DalamudGrey2, Locs.TabBody_SearchNoMatching);
+ return;
+ }
+
+ var i = 0;
+ foreach (var plugin in filteredList)
+ {
+ this.DrawInstalledPlugin(plugin, i++);
+ }
+ }
+
+ private bool DrawPluginListLoading()
+ {
+ var ready = this.dalamud.PluginManager.PluginsReady && this.dalamud.PluginManager.ReposReady;
+
+ if (!ready)
+ {
+ ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_LoadingPlugins);
+ }
+
+ var failedRepos = this.dalamud.PluginManager.Repos
+ .Where(repo => repo.State == PluginRepositoryState.Fail)
+ .ToArray();
+
+ if (failedRepos.Length > 0)
+ {
+ var failText = Locs.TabBody_DownloadFailed;
+ var aggFailText = failedRepos
+ .Select(repo => $"{failText} ({repo.PluginMasterUrl})")
+ .Aggregate((s1, s2) => $"{s1}\n{s2}");
+
+ ImGui.TextColored(ImGuiColors.DalamudRed, aggFailText);
+ }
+
+ return ready;
+ }
+
+ private void DrawAvailablePlugin(RemotePluginManifest manifest, int index)
+ {
+ var useTesting = this.dalamud.PluginManager.UseTesting(manifest);
+
+ // Check for valid versions
+ if ((useTesting && manifest.TestingAssemblyVersion == null) || manifest.AssemblyVersion == null)
+ {
+ // Without a valid version, quit
+ return;
+ }
+
+ // Name
+ var label = manifest.Name;
+
+ // Testing
+ if (useTesting)
+ {
+ label += Locs.PluginTitleMod_TestingVersion;
+ }
+
+ if (ImGui.CollapsingHeader($"{label}###Header{index}{manifest.InternalName}"))
+ {
+ ImGui.Indent();
+
+ // Name
+ ImGui.Text(manifest.Name);
+
+ // Download count
+ var downloadCountText = manifest.DownloadCount > 0
+ ? Locs.PluginBody_AuthorWithDownloadCount(manifest.Author, manifest.DownloadCount)
+ : Locs.PluginBody_AuthorWithDownloadCountUnavailable(manifest.Author);
+
+ ImGui.SameLine();
+ ImGui.TextColored(ImGuiColors.DalamudGrey3, downloadCountText);
+
+ // Installable from
+ if (manifest.SourceRepo.IsThirdParty)
+ {
+ var repoText = Locs.PluginBody_Plugin3rdPartyRepo(manifest.SourceRepo.PluginMasterUrl);
+ ImGui.TextColored(ImGuiColors.DalamudGrey3, repoText);
+ }
+
+ // Description
+ if (!string.IsNullOrWhiteSpace(manifest.Description))
+ {
+ ImGui.TextWrapped(manifest.Description);
+ }
+
+ // Controls
+ var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress;
+
+ var versionString = useTesting
+ ? $"{manifest.TestingAssemblyVersion}"
+ : $"{manifest.AssemblyVersion}";
+
+ if (disabled)
+ {
+ ImGuiComponents.DisabledButton(Locs.PluginButton_InstallVersion(versionString));
+ }
+ else
+ {
+ var buttonText = Locs.PluginButton_InstallVersion(versionString);
+ if (ImGui.Button($"{buttonText}##{buttonText}{index}"))
+ {
+ this.installStatus = OperationStatus.InProgress;
+
+ Task.Run(() => this.dalamud.PluginManager.InstallPlugin(manifest, useTesting, PluginLoadReason.Installer))
+ .ContinueWith(task =>
+ {
+ // There is no need to set as Complete for an individual plugin installation
+ this.installStatus = OperationStatus.Idle;
+ this.DisplayErrorContinuation(task, Locs.ErrorModal_InstallFail(manifest.Name));
+ });
+ }
+ }
+
+ this.DrawVisitRepoUrlButton(manifest.RepoUrl);
+
+ ImGui.Unindent();
+ }
+
+ if (ImGui.BeginPopupContextItem("ItemContextMenu"))
+ {
+ if (ImGui.Selectable(Locs.PluginContext_HidePlugin))
+ {
+ Log.Debug($"Adding {manifest.InternalName} to hidden plugins");
+ this.dalamud.Configuration.HiddenPluginInternalName.Add(manifest.InternalName);
+ this.dalamud.Configuration.Save();
+ this.dalamud.PluginManager.RefilterPluginMasters();
+ }
+
+ if (ImGui.Selectable(Locs.PluginContext_DeletePluginConfig))
+ {
+ Log.Debug($"Deleting config for {manifest.InternalName}");
+
+ this.installStatus = OperationStatus.InProgress;
+
+ Task.Run(() =>
+ {
+ this.dalamud.PluginManager.PluginConfigs.Delete(manifest.InternalName);
+
+ var path = Path.Combine(this.dalamud.StartInfo.PluginDirectory, manifest.InternalName);
+ if (Directory.Exists(path))
+ Directory.Delete(path, true);
+ })
+ .ContinueWith(task =>
+ {
+ this.installStatus = OperationStatus.Idle;
+
+ this.DisplayErrorContinuation(task, Locs.ErrorModal_DeleteConfigFail(manifest.InternalName));
+ });
+ }
+
+ ImGui.EndPopup();
+ }
+ }
+
+ private void DrawInstalledPlugin(LocalPlugin plugin, int index, bool showInstalled = false)
+ {
+ // Name
+ var label = plugin.Manifest.Name;
+
+ // Testing
+ if (plugin.Manifest.Testing)
+ {
+ label += Locs.PluginTitleMod_TestingVersion;
+ }
+
+ // Freshly installed
+ if (showInstalled)
+ {
+ label += Locs.PluginTitleMod_Installed;
+ }
+
+ // Disabled
+ if (plugin.IsDisabled)
+ {
+ label += Locs.PluginTitleMod_Disabled;
+ }
+
+ // Load error
+ if (plugin.State == PluginState.LoadError)
+ {
+ label += Locs.PluginTitleMod_LoadError;
+ }
+
+ // Unload error
+ if (plugin.State == PluginState.UnloadError)
+ {
+ label += Locs.PluginTitleMod_UnloadError;
+ }
+
+ var availablePluginUpdate = this.pluginListUpdatable.FirstOrDefault(up => up.InstalledPlugin == plugin);
+ // Update available
+ if (availablePluginUpdate != default)
+ {
+ label += Locs.PluginTitleMod_HasUpdate;
+ }
+
+ // Freshly updated
+ if (this.updatedPlugins != null && !plugin.IsDev)
+ {
+ var update = this.updatedPlugins.FirstOrDefault(update => update.InternalName == plugin.Manifest.InternalName);
+ if (update != default)
+ {
+ if (update.WasUpdated)
+ {
+ label += Locs.PluginTitleMod_Updated;
+ }
+ else
+ {
+ label += Locs.PluginTitleMod_UpdateFailed;
+ }
+ }
+ }
+
+ // Outdated API level
+ if (plugin.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel)
+ {
+ label += Locs.PluginTitleMod_OutdatedError;
+ }
+
+ if (ImGui.CollapsingHeader($"{label}###Header{index}{plugin.Manifest.InternalName}"))
+ {
+ var manifest = plugin.Manifest;
+
+ ImGui.Indent();
+
+ // Name
+ ImGui.Text(manifest.Name);
+
+ // Download count
+ var downloadText = manifest.DownloadCount > 0
+ ? Locs.PluginBody_AuthorWithDownloadCount(manifest.Author, manifest.DownloadCount)
+ : Locs.PluginBody_AuthorWithDownloadCountUnavailable(manifest.Author);
+
+ ImGui.SameLine();
+ ImGui.TextColored(ImGuiColors.DalamudGrey3, downloadText);
+
+ // Installed from
+ if (!string.IsNullOrEmpty(manifest.InstalledFromUrl))
+ {
+ var repoText = Locs.PluginBody_Plugin3rdPartyRepo(manifest.InstalledFromUrl);
+ ImGui.TextColored(ImGuiColors.DalamudGrey3, repoText);
+ }
+
+ // Description
+ if (!string.IsNullOrWhiteSpace(manifest.Description))
+ {
+ ImGui.TextWrapped(manifest.Description);
+ }
+
+ if (plugin.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel)
+ {
+ ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
+ ImGui.TextWrapped(Locs.PluginBody_Outdated);
+ ImGui.PopStyleColor();
+ }
+
+ // Available commands (if loaded)
+ if (plugin.IsLoaded)
+ {
+ var commands = this.dalamud.CommandManager.Commands.Where(cInfo => cInfo.Value.ShowInHelp && cInfo.Value.LoaderAssemblyName == plugin.Manifest.InternalName);
+ if (commands.Any())
+ {
+ ImGui.Dummy(ImGuiHelpers.ScaledVector2(10f, 10f));
+ foreach (var command in commands)
+ {
+ ImGui.TextWrapped($"{command.Key} → {command.Value.HelpMessage}");
+ }
+ }
+ }
+
+ // Controls
+ this.DrawPluginControlButton(plugin);
+ this.DrawDevPluginButtons(plugin);
+ this.DrawVisitRepoUrlButton(plugin.Manifest.RepoUrl);
+
+ if (availablePluginUpdate != default)
+ this.DrawUpdateSinglePluginButton(availablePluginUpdate);
+
+ ImGui.SameLine();
+ ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{plugin.Manifest.AssemblyVersion}");
+
+ if (plugin.IsDev)
+ {
+ ImGui.SameLine();
+ ImGui.TextColored(ImGuiColors.DalamudRed, Locs.PluginBody_DeleteDevPlugin);
+ }
+
+ ImGui.Unindent();
+ }
+
+ if (ImGui.BeginPopupContextItem("InstalledItemContextMenu"))
+ {
+ if (ImGui.Selectable(Locs.PluginContext_DeletePluginConfigReload))
+ {
+ Log.Debug($"Deleting config for {plugin.Manifest.InternalName}");
+
+ this.installStatus = OperationStatus.InProgress;
+
+ Task.Run(() => this.dalamud.PluginManager.DeleteConfiguration(plugin))
+ .ContinueWith(task =>
+ {
+ this.installStatus = OperationStatus.Idle;
+
+ this.DisplayErrorContinuation(task, Locs.ErrorModal_DeleteConfigFail(plugin.Name));
+ });
+ }
+
+ ImGui.EndPopup();
+ }
+ }
+
+ private void DrawPluginControlButton(LocalPlugin plugin)
+ {
+ // Disable everything if the updater is running or another plugin is operating
+ var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress;
+
+ // Disable everything if the plugin is outdated
+ disabled = disabled || (plugin.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !this.dalamud.Configuration.LoadAllApiLevels);
+
+ if (plugin.State == PluginState.InProgress)
+ {
+ ImGuiComponents.DisabledButton(Locs.PluginButton_Working);
+ }
+ else if (plugin.State == PluginState.Loaded || plugin.State == PluginState.LoadError)
+ {
+ if (disabled)
+ {
+ ImGuiComponents.DisabledButton(Locs.PluginButton_Disable);
+ }
+ else
+ {
+ if (ImGui.Button(Locs.PluginButton_Disable))
+ {
+ Task.Run(() =>
+ {
+ var unloadTask = Task.Run(() => plugin.Unload())
+ .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_UnloadFail(plugin.Name));
+
+ unloadTask.Wait();
+ if (!unloadTask.Result)
+ return;
+
+ var disableTask = Task.Run(() => plugin.Disable())
+ .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_DisableFail(plugin.Name));
+
+ disableTask.Wait();
+ if (!disableTask.Result)
+ return;
+
+ if (!plugin.IsDev)
+ {
+ this.dalamud.PluginManager.RemovePlugin(plugin);
+ }
+ });
+ }
+ }
+
+ if (plugin.State == PluginState.Loaded)
+ {
+ // Only if the plugin isn't broken.
+ this.DrawOpenPluginSettingsButton(plugin);
+ }
+ }
+ else if (plugin.State == PluginState.Unloaded)
+ {
+ if (disabled)
+ {
+ ImGuiComponents.DisabledButton(Locs.PluginButton_Load);
+ }
+ else
+ {
+ if (ImGui.Button(Locs.PluginButton_Load))
+ {
+ Task.Run(() =>
+ {
+ var enableTask = Task.Run(() => plugin.Enable())
+ .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_EnableFail(plugin.Name));
+
+ enableTask.Wait();
+ if (!enableTask.Result)
+ return;
+
+ var loadTask = Task.Run(() => plugin.Load(PluginLoadReason.Installer))
+ .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_LoadFail(plugin.Name));
+
+ loadTask.Wait();
+ if (!loadTask.Result)
+ return;
+ });
+ }
+ }
+ }
+ else if (plugin.State == PluginState.UnloadError)
+ {
+ ImGuiComponents.DisabledButton(FontAwesomeIcon.Frown);
+ }
+ }
+
+ private void DrawUpdateSinglePluginButton(AvailablePluginUpdate update)
+ {
+ ImGui.SameLine();
+
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Download))
+ {
+ this.installStatus = OperationStatus.InProgress;
+
+ Task.Run(() => this.dalamud.PluginManager.UpdateSinglePlugin(update, true, false))
+ .ContinueWith(task =>
+ {
+ // There is no need to set as Complete for an individual plugin installation
+ this.installStatus = OperationStatus.Idle;
+
+ var errorMessage = Locs.ErrorModal_SingleUpdateFail(update.UpdateManifest.Name);
+ this.DisplayErrorContinuation(task, errorMessage);
+
+ if (!task.Result.WasUpdated)
+ {
+ this.ShowErrorModal(errorMessage);
+ }
+ });
+ }
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.PluginButtonToolTip_UpdateSingle(update.UpdateManifest.AssemblyVersion.ToString()));
+ }
+
+ private void DrawOpenPluginSettingsButton(LocalPlugin plugin)
+ {
+ if (plugin.DalamudInterface?.UiBuilder?.HasConfigUi ?? false)
+ {
+ ImGui.SameLine();
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Cog))
+ {
+ try
+ {
+ plugin.DalamudInterface.UiBuilder.OpenConfigUi();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Error during OpenConfigUi: {plugin.Name}");
+ }
+ }
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip(Locs.PluginButtonToolTip_OpenConfiguration);
+ }
+ }
+ }
+
+ private void DrawDevPluginButtons(LocalPlugin localPlugin)
+ {
+ if (localPlugin is LocalDevPlugin plugin)
+ {
+ // https://colorswall.com/palette/2868/
+ var greenColor = new Vector4(0x5C, 0xB8, 0x5C, 0xFF) / 0xFF;
+ var redColor = new Vector4(0xD9, 0x53, 0x4F, 0xFF) / 0xFF;
+
+ // Load on boot
+ ImGui.PushStyleColor(ImGuiCol.Button, plugin.StartOnBoot ? greenColor : redColor);
+ ImGui.PushStyleColor(ImGuiCol.ButtonHovered, plugin.StartOnBoot ? greenColor : redColor);
+
+ ImGui.SameLine();
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.PowerOff))
+ {
+ plugin.StartOnBoot ^= true;
+ this.dalamud.Configuration.Save();
+ }
+
+ ImGui.PopStyleColor(2);
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip(Locs.PluginButtonToolTip_StartOnBoot);
+ }
+
+ // Automatic reload
+ ImGui.PushStyleColor(ImGuiCol.Button, plugin.AutomaticReload ? greenColor : redColor);
+ ImGui.PushStyleColor(ImGuiCol.ButtonHovered, plugin.AutomaticReload ? greenColor : redColor);
+
+ ImGui.SameLine();
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.SyncAlt))
+ {
+ plugin.AutomaticReload ^= true;
+ this.dalamud.Configuration.Save();
+ }
+
+ ImGui.PopStyleColor(2);
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip(Locs.PluginButtonToolTip_AutomaticReloading);
+ }
+
+ // Delete
+ if (plugin.State == PluginState.Unloaded)
+ {
+ ImGui.SameLine();
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.TrashAlt))
+ {
+ try
+ {
+ plugin.DllFile.Delete();
+ this.dalamud.PluginManager.RemovePlugin(plugin);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Plugin installer threw an error during removal of {plugin.Name}");
+
+ this.errorModalMessage = Locs.ErrorModal_DeleteFail(plugin.Name);
+ this.errorModalDrawing = true;
+ this.errorModalOnNextFrame = true;
+ }
+ }
+
+ if (ImGui.IsItemHovered())
+ {
+ ImGui.SetTooltip(Locs.PluginBody_DeleteDevPlugin);
+ }
+ }
+ }
+ }
+
+ private void DrawVisitRepoUrlButton(string repoUrl)
+ {
+ if (!string.IsNullOrEmpty(repoUrl) && repoUrl.StartsWith("https://"))
+ {
+ ImGui.SameLine();
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Globe))
+ {
+ try
+ {
+ _ = Process.Start(new ProcessStartInfo()
+ {
+ FileName = repoUrl,
+ UseShellExecute = true,
+ });
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Could not open repoUrl: {repoUrl}");
+ }
+ }
+
+ if (ImGui.IsItemHovered())
+ ImGui.SetTooltip(Locs.PluginButtonToolTip_VisitPluginUrl);
+ }
+ }
+
+ private bool IsManifestFiltered(PluginManifest manifest)
+ {
+ var searchString = this.searchText.ToLowerInvariant();
+ var hasSearchString = !string.IsNullOrWhiteSpace(searchString);
+
+ return hasSearchString && !(
+ manifest.Name.ToLowerInvariant().Contains(searchString) ||
+ manifest.Author.Equals(this.searchText, StringComparison.InvariantCultureIgnoreCase) ||
+ (manifest.Tags != null && manifest.Tags.Contains(searchString, StringComparer.InvariantCultureIgnoreCase)));
+ }
+
+ private (bool IsInstalled, LocalPlugin Plugin) IsManifestInstalled(RemotePluginManifest manifest)
+ {
+ var plugin = this.pluginListInstalled.FirstOrDefault(plugin => plugin.Manifest.InternalName == manifest.InternalName);
+ var isInstalled = plugin != default;
+
+ return (isInstalled, plugin);
+ }
+
+ private void OnAvailablePluginsChanged()
+ {
+ // By removing installed plugins only when the available plugin list changes (basically when the window is
+ // opened), plugins that have been newly installed remain in the available plugin list as installed.
+ this.pluginListAvailable = this.dalamud.PluginManager.AvailablePlugins
+ .Where(manifest => !this.IsManifestInstalled(manifest).IsInstalled)
+ .ToList();
+ this.pluginListUpdatable = this.dalamud.PluginManager.UpdatablePlugins.ToList();
+ this.ResortPlugins();
+ }
+
+ private void OnInstalledPluginsChanged()
+ {
+ this.pluginListInstalled = this.dalamud.PluginManager.InstalledPlugins.ToList();
+ this.pluginListUpdatable = this.dalamud.PluginManager.UpdatablePlugins.ToList();
+ this.hasDevPlugins = this.pluginListInstalled.Any(plugin => plugin.IsDev);
+ this.ResortPlugins();
+ }
+
+ private void ResortPlugins()
+ {
+ switch (this.sortKind)
+ {
+ case PluginSortKind.Alphabetical:
+ this.pluginListAvailable.Sort((p1, p2) => p1.Name.CompareTo(p2.Name));
+ this.pluginListInstalled.Sort((p1, p2) => p1.Manifest.Name.CompareTo(p2.Manifest.Name));
+ break;
+ case PluginSortKind.DownloadCount:
+ this.pluginListAvailable.Sort((p1, p2) => p2.DownloadCount.CompareTo(p1.DownloadCount));
+ this.pluginListInstalled.Sort((p1, p2) => p2.Manifest.DownloadCount.CompareTo(p1.Manifest.DownloadCount));
+ break;
+ case PluginSortKind.LastUpdate:
+ this.pluginListAvailable.Sort((p1, p2) => p2.LastUpdate.CompareTo(p1.LastUpdate));
+ this.pluginListInstalled.Sort((p1, p2) => p2.Manifest.LastUpdate.CompareTo(p1.Manifest.LastUpdate));
+ break;
+ default:
+ throw new InvalidEnumArgumentException("Unknown plugin sort type.");
+ }
+ }
+
+ ///
+ /// A continuation task that displays any errors received into the error modal.
+ ///
+ /// The previous task.
+ /// An error message to be displayed.
+ /// A value indicating whether to continue with the next task.
+ private bool DisplayErrorContinuation(Task task, object state)
+ {
+ if (task.IsFaulted)
+ {
+ var errorModalMessage = state as string;
+
+ foreach (var ex in task.Exception.InnerExceptions)
+ {
+ if (ex is PluginException)
+ {
+ Log.Error(ex, "Plugin installer threw an error");
+#if DEBUG
+ if (!string.IsNullOrEmpty(ex.Message))
+ errorModalMessage += $"\n\n{ex.Message}";
+#endif
+ }
+ else
+ {
+ Log.Error(ex, "Plugin installer threw an unexpected error");
+#if DEBUG
+ if (!string.IsNullOrEmpty(ex.Message))
+ errorModalMessage += $"\n\n{ex.Message}";
+#endif
+ }
+ }
+
+ this.ShowErrorModal(errorModalMessage);
+
+ return false;
+ }
+
+ return true;
+ }
+
+ private void ShowErrorModal(string message)
+ {
+ this.errorModalMessage = message;
+ this.errorModalDrawing = true;
+ this.errorModalOnNextFrame = true;
+ }
+
+ [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Disregard here")]
+ private static class Locs
+ {
+ #region Window Title
+
+ public static string WindowTitle => Loc.Localize("InstallerHeader", "Plugin Installer");
+
+ public static string WindowTitleMod_Testing => Loc.Localize("InstallerHeaderTesting", " (TESTING)");
+
+ #endregion
+
+ #region Header
+
+ public static string Header_Hint => Loc.Localize("InstallerHint", "This window allows you to install and remove in-game plugins.\nThey are made by third-party developers.");
+
+ public static string Header_SearchPlaceholder => Loc.Localize("InstallerSearch", "Search");
+
+ #endregion
+
+ #region SortBy
+
+ public static string SortBy_Alphabetical => Loc.Localize("InstallerAlphabetical", "Alphabetical");
+
+ public static string SortBy_DownloadCounts => Loc.Localize("InstallerDownloadCount", "Download Count");
+
+ public static string SortBy_LastUpdate => Loc.Localize("InstallerLastUpdate", "Last Update");
+
+ public static string SortBy_Label => Loc.Localize("InstallerSortBy", "Sort By");
+
+ #endregion
+
+ #region Tabs
+
+ public static string TabTitle_AvailablePlugins => Loc.Localize("InstallerAvailablePlugins", "Available Plugins");
+
+ public static string TabTitle_InstalledPlugins => Loc.Localize("InstallerInstalledPlugins", "Installed Plugins");
+
+ public static string TabTitle_InstalledDevPlugins => Loc.Localize("InstallerInstalledDevPlugins", "Installed Dev Plugins");
+
+ #endregion
+
+ #region Tab body
+
+ public static string TabBody_LoadingPlugins => Loc.Localize("InstallerLoading", "Loading plugins...");
+
+ public static string TabBody_DownloadFailed => Loc.Localize("InstallerDownloadFailed", "Download failed.");
+
+ #endregion
+
+ #region Search text
+
+ public static string TabBody_SearchNoMatching => Loc.Localize("InstallerNoMatching", "No plugins were found matching your search.");
+
+ public static string TabBody_SearchNoCompatible => Loc.Localize("InstallerNoCompatible", "No compatible plugins were found :( Please restart your game and try again.");
+
+ public static string TabBody_SearchNoInstalled => Loc.Localize("InstallerNoInstalled", "No plugins are currently installed. You can install them from the Available Plugins tab.");
+
+ #endregion
+
+ #region Plugin title text
+
+ public static string PluginTitleMod_Installed => Loc.Localize("InstallerInstalled", " (installed)");
+
+ public static string PluginTitleMod_Disabled => Loc.Localize("InstallerDisabled", " (disabled)");
+
+ public static string PluginTitleMod_Unloaded => Loc.Localize("InstallerUnloaded", " (unloaded)");
+
+ public static string PluginTitleMod_HasUpdate => Loc.Localize("InstallerHasUpdate", " (has update)");
+
+ public static string PluginTitleMod_Updated => Loc.Localize("InstallerUpdated", " (updated)");
+
+ public static string PluginTitleMod_TestingVersion => Loc.Localize("InstallerTestingVersion", " (testing version)");
+
+ public static string PluginTitleMod_UpdateFailed => Loc.Localize("InstallerUpdateFailed", " (update failed)");
+
+ public static string PluginTitleMod_LoadError => Loc.Localize("InstallerLoadError", " (load error)");
+
+ public static string PluginTitleMod_UnloadError => Loc.Localize("InstallerUnloadError", " (unload error)");
+
+ public static string PluginTitleMod_OutdatedError => Loc.Localize("InstallerOutdatedError", " (outdated)");
+
+ #endregion
+
+ #region Plugin context menu
+
+ public static string PluginContext_HidePlugin => Loc.Localize("InstallerHidePlugin", "Hide from installer");
+
+ public static string PluginContext_DeletePluginConfig => Loc.Localize("InstallerDeletePluginConfig", "Reset plugin");
+
+ public static string PluginContext_DeletePluginConfigReload => Loc.Localize("InstallerDeletePluginConfig", "Reset plugin settings & reload");
+
+ #endregion
+
+ #region Plugin body
+
+ public static string PluginBody_AuthorWithDownloadCount(string author, long count) => Loc.Localize("InstallerAuthorWithDownloadCount", " by {0}, {1} downloads").Format(author, count);
+
+ public static string PluginBody_AuthorWithDownloadCountUnavailable(string author) => Loc.Localize("InstallerAuthorWithDownloadCountUnavailable", " by {0}, download count unavailable").Format(author);
+
+ public static string PluginBody_Plugin3rdPartyRepo(string url) => Loc.Localize("InstallerPlugin3rdPartyRepo", "From custom plugin repository {0}").Format(url);
+
+ public static string PluginBody_AvailableDevPlugin => Loc.Localize("InstallerDevPlugin", " This plugin is available in one of your repos, please remove it from the devPlugins folder.");
+
+ public static string PluginBody_DeleteDevPlugin => Loc.Localize("InstallerDeleteDevPlugin ", " To delete this plugin, please remove it from the devPlugins folder.");
+
+ public static string PluginBody_Outdated => Loc.Localize("InstallerOutdatedPluginBody ", "This plugin is outdated and incompatible at the moment. Please wait for it to be updated by its author.");
+
+ #endregion
+
+ #region Plugin buttons
+
+ public static string PluginButton_InstallVersion(string version) => Loc.Localize("InstallerInstall", "Install v{0}").Format(version);
+
+ public static string PluginButton_Working => Loc.Localize("InstallerWorking", "Working");
+
+ public static string PluginButton_Disable => Loc.Localize("InstallerDisable", "Disable");
+
+ public static string PluginButton_Load => Loc.Localize("InstallerLoad", "Load");
+
+ public static string PluginButton_Unload => Loc.Localize("InstallerUnload", "Unload");
+
+ #endregion
+
+ #region Plugin button tooltips
+
+ public static string PluginButtonToolTip_OpenConfiguration => Loc.Localize("InstallerOpenConfig", "Open Configuration");
+
+ public static string PluginButtonToolTip_StartOnBoot => Loc.Localize("InstallerStartOnBoot", "Start on boot");
+
+ public static string PluginButtonToolTip_AutomaticReloading => Loc.Localize("InstallerAutomaticReloading", "Automatic reloading");
+
+ public static string PluginButtonToolTip_DeletePlugin => Loc.Localize("InstallerDeletePlugin ", "Delete plugin");
+
+ public static string PluginButtonToolTip_VisitPluginUrl => Loc.Localize("InstallerVisitPluginUrl", "Visit plugin URL");
+
+ public static string PluginButtonToolTip_UpdateSingle(string version) => Loc.Localize("InstallerUpdateSingle", "Update to {0}").Format(version);
+
+ #endregion
+
+ #region Footer
+
+ public static string FooterButton_UpdatePlugins => Loc.Localize("InstallerUpdatePlugins", "Update plugins");
+
+ public static string FooterButton_InProgress => Loc.Localize("InstallerInProgress", "Install in progress...");
+
+ public static string FooterButton_NoUpdates => Loc.Localize("InstallerNoUpdates", "No updates found!");
+
+ public static string FooterButton_UpdateComplete(int count) => Loc.Localize("InstallerUpdateComplete", "{0} plugins updated!").Format(count);
+
+ public static string FooterButton_Settings => Loc.Localize("InstallerSettings", "Settings");
+
+ public static string FooterButton_ScanDevPlugins => Loc.Localize("InstallerScanDevPlugins", "Scan Dev Plugins");
+
+ public static string FooterButton_Close => Loc.Localize("InstallerClose", "Close");
+
+ #endregion
+
+ #region Error modal
+
+ public static string ErrorModal_Title => Loc.Localize("InstallerError", "Installer Error");
+
+ public static string ErrorModal_InstallFail(string name) => Loc.Localize("InstallerInstallFail", "Failed to install plugin {0}.").Format(name);
+
+ public static string ErrorModal_SingleUpdateFail(string name) => Loc.Localize("InstallerSingleUpdateFail", "Failed to update plugin {0}.").Format(name);
+
+ public static string ErrorModal_DeleteConfigFail(string name) => Loc.Localize("InstallerDeleteConfigFail", "Failed to reset the plugin {0}.\n\nThe plugin may not support this action. You can try deleting the configuration manually while the game is shut down - please see the FAQ.").Format(name);
+
+ public static string ErrorModal_EnableFail(string name) => Loc.Localize("InstallerEnableFail", "Failed to enable plugin {0}.").Format(name);
+
+ public static string ErrorModal_DisableFail(string name) => Loc.Localize("InstallerDisableFail", "Failed to disable plugin {0}.").Format(name);
+
+ public static string ErrorModal_UnloadFail(string name) => Loc.Localize("InstallerUnloadFail", "Failed to unload plugin {0}.").Format(name);
+
+ public static string ErrorModal_LoadFail(string name) => Loc.Localize("InstallerLoadFail", "Failed to load plugin {0}.").Format(name);
+
+ public static string ErrorModal_DeleteFail(string name) => Loc.Localize("InstallerDeleteFail", "Failed to delete plugin {0}.").Format(name);
+
+ public static string ErrorModal_UpdaterFatal => Loc.Localize("InstallerUpdaterFatal", "Failed to update plugins.");
+
+ public static string ErrorModal_UpdaterFail(int failCount) => Loc.Localize("InstallerUpdaterFail", "Failed to update {0} plugins.").Format(failCount);
+
+ public static string ErrorModal_UpdaterFailPartial(int successCount, int failCount) => Loc.Localize("InstallerUpdaterFailPartial", "Updated {0} plugins, failed to update {1}.").Format(successCount, failCount);
+
+ public static string ErrorModal_HintBlame(string plugins) => Loc.Localize("InstallerErrorPluginInfo", "\n\nThe following plugins caused these issues:\n\n{0}\nYou may try removing these plugins manually and reinstalling them.").Format(plugins);
+
+ // public static string ErrorModal_Hint => Loc.Localize("InstallerErrorHint", "The plugin installer ran into an issue or the plugin is incompatible.\nPlease restart the game and report this error on our discord.");
+
+ #endregion
+
+ #region Plugin Update chatbox
+
+ public static string PluginUpdateHeader_Chatbox => Loc.Localize("DalamudPluginUpdates", "Updates:");
+
+ #endregion
+
+ #region Error modal buttons
+
+ public static string ErrorModalButton_Ok => Loc.Localize("OK", "OK");
+
+ #endregion
+ }
+ }
+}
diff --git a/Dalamud/Interface/DalamudPluginStatWindow.cs b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs
similarity index 75%
rename from Dalamud/Interface/DalamudPluginStatWindow.cs
rename to Dalamud/Interface/Internal/Windows/PluginStatWindow.cs
index fca07d31b..cdb60d9b0 100644
--- a/Dalamud/Interface/DalamudPluginStatWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs
@@ -3,29 +3,30 @@ using System.Linq;
using System.Reflection;
using Dalamud.Game.Internal;
-using Dalamud.Hooking;
+using Dalamud.Hooking.Internal;
using Dalamud.Interface.Windowing;
-using Dalamud.Plugin;
+using Dalamud.Plugin.Internal;
+using Dalamud.Plugin.Internal.Types;
using ImGuiNET;
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal.Windows
{
///
/// This window displays plugin statistics for troubleshooting.
///
- internal class DalamudPluginStatWindow : Window
+ internal class PluginStatWindow : Window
{
private readonly PluginManager pluginManager;
private bool showDalamudHooks;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
- /// The PluginManager instance.
- public DalamudPluginStatWindow(PluginManager pluginManager)
+ /// The Dalamud instance.
+ public PluginStatWindow(Dalamud dalamud)
: base("Plugin Statistics###DalamudPluginStatWindow")
{
- this.pluginManager = pluginManager;
+ this.pluginManager = dalamud.PluginManager;
}
///
@@ -47,11 +48,11 @@ namespace Dalamud.Interface
ImGui.SameLine();
if (ImGui.Button("Reset"))
{
- foreach (var a in this.pluginManager.Plugins)
+ foreach (var plugin in this.pluginManager.InstalledPlugins)
{
- a.PluginInterface.UiBuilder.LastDrawTime = -1;
- a.PluginInterface.UiBuilder.MaxDrawTime = -1;
- a.PluginInterface.UiBuilder.DrawTimeHistory.Clear();
+ plugin.DalamudInterface.UiBuilder.LastDrawTime = -1;
+ plugin.DalamudInterface.UiBuilder.MaxDrawTime = -1;
+ plugin.DalamudInterface.UiBuilder.DrawTimeHistory.Clear();
}
}
@@ -60,26 +61,35 @@ namespace Dalamud.Interface
ImGui.SetColumnWidth(1, 100f);
ImGui.SetColumnWidth(2, 100f);
ImGui.SetColumnWidth(3, 100f);
+
ImGui.Text("Plugin");
ImGui.NextColumn();
+
ImGui.Text("Last");
ImGui.NextColumn();
+
ImGui.Text("Longest");
ImGui.NextColumn();
+
ImGui.Text("Average");
ImGui.NextColumn();
+
ImGui.Separator();
- foreach (var a in this.pluginManager.Plugins)
+
+ foreach (var plugin in this.pluginManager.InstalledPlugins.Where(plugin => plugin.State == PluginState.Loaded))
{
- ImGui.Text(a.Definition.Name);
+ ImGui.Text(plugin.Manifest.Name);
ImGui.NextColumn();
- ImGui.Text($"{a.PluginInterface.UiBuilder.LastDrawTime / 10000f:F4}ms");
+
+ ImGui.Text($"{plugin.DalamudInterface.UiBuilder.LastDrawTime / 10000f:F4}ms");
ImGui.NextColumn();
- ImGui.Text($"{a.PluginInterface.UiBuilder.MaxDrawTime / 10000f:F4}ms");
+
+ ImGui.Text($"{plugin.DalamudInterface.UiBuilder.MaxDrawTime / 10000f:F4}ms");
ImGui.NextColumn();
- if (a.PluginInterface.UiBuilder.DrawTimeHistory.Count > 0)
+
+ if (plugin.DalamudInterface.UiBuilder.DrawTimeHistory.Count > 0)
{
- ImGui.Text($"{a.PluginInterface.UiBuilder.DrawTimeHistory.Average() / 10000f:F4}ms");
+ ImGui.Text($"{plugin.DalamudInterface.UiBuilder.DrawTimeHistory.Average() / 10000f:F4}ms");
}
else
{
@@ -113,33 +123,46 @@ namespace Dalamud.Interface
}
ImGui.Columns(4);
+
ImGui.SetColumnWidth(0, ImGui.GetWindowContentRegionWidth() - 300);
ImGui.SetColumnWidth(1, 100f);
ImGui.SetColumnWidth(2, 100f);
ImGui.SetColumnWidth(3, 100f);
+
ImGui.Text("Method");
ImGui.NextColumn();
+
ImGui.Text("Last");
ImGui.NextColumn();
+
ImGui.Text("Longest");
ImGui.NextColumn();
+
ImGui.Text("Average");
ImGui.NextColumn();
+
ImGui.Separator();
ImGui.Separator();
foreach (var handlerHistory in Framework.StatsHistory)
{
- if (handlerHistory.Value.Count == 0) continue;
+ if (handlerHistory.Value.Count == 0)
+ continue;
+
ImGui.SameLine();
+
ImGui.Text($"{handlerHistory.Key}");
ImGui.NextColumn();
+
ImGui.Text($"{handlerHistory.Value.Last():F4}ms");
ImGui.NextColumn();
+
ImGui.Text($"{handlerHistory.Value.Max():F4}ms");
ImGui.NextColumn();
+
ImGui.Text($"{handlerHistory.Value.Average():F4}ms");
ImGui.NextColumn();
+
ImGui.Separator();
}
@@ -152,27 +175,35 @@ namespace Dalamud.Interface
if (ImGui.BeginTabItem("Hooks"))
{
ImGui.Columns(3);
+
ImGui.SetColumnWidth(0, ImGui.GetWindowContentRegionWidth() - 280);
ImGui.SetColumnWidth(1, 180f);
ImGui.SetColumnWidth(2, 100f);
+
ImGui.Text("Detour Method");
ImGui.SameLine();
+
ImGui.Text(" ");
ImGui.SameLine();
+
ImGui.Checkbox("Show Dalamud Hooks ###showDalamudHooksCheckbox", ref this.showDalamudHooks);
ImGui.NextColumn();
+
ImGui.Text("Address");
ImGui.NextColumn();
+
ImGui.Text("Status");
ImGui.NextColumn();
+
ImGui.Separator();
ImGui.Separator();
- foreach (var trackedHook in HookInfo.TrackedHooks)
+ foreach (var trackedHook in HookManager.TrackedHooks)
{
try
{
- if (!this.showDalamudHooks && trackedHook.Assembly == Assembly.GetExecutingAssembly()) continue;
+ if (!this.showDalamudHooks && trackedHook.Assembly == Assembly.GetExecutingAssembly())
+ continue;
ImGui.Text($"{trackedHook.Delegate.Target} :: {trackedHook.Delegate.Method.Name}");
ImGui.TextDisabled(trackedHook.Assembly.FullName);
@@ -180,13 +211,19 @@ namespace Dalamud.Interface
if (!trackedHook.Hook.IsDisposed)
{
ImGui.Text($"{trackedHook.Hook.Address.ToInt64():X}");
- if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"{trackedHook.Hook.Address.ToInt64():X}");
+ if (ImGui.IsItemClicked())
+ {
+ ImGui.SetClipboardText($"{trackedHook.Hook.Address.ToInt64():X}");
+ }
var processMemoryOffset = trackedHook.InProcessMemory;
if (processMemoryOffset.HasValue)
{
ImGui.Text($"ffxiv_dx11.exe + {processMemoryOffset:X}");
- if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"ffxiv_dx11.exe+{processMemoryOffset:X}");
+ if (ImGui.IsItemClicked())
+ {
+ ImGui.SetClipboardText($"ffxiv_dx11.exe+{processMemoryOffset:X}");
+ }
}
}
@@ -218,7 +255,7 @@ namespace Dalamud.Interface
if (ImGui.IsWindowAppearing())
{
- HookInfo.TrackedHooks.RemoveAll(h => h.Hook.IsDisposed);
+ HookManager.TrackedHooks.RemoveAll(h => h.Hook.IsDisposed);
}
ImGui.EndTabBar();
diff --git a/Dalamud/Interface/Scratchpad/ScratchpadWindow.cs b/Dalamud/Interface/Internal/Windows/ScratchpadWindow.cs
similarity index 95%
rename from Dalamud/Interface/Scratchpad/ScratchpadWindow.cs
rename to Dalamud/Interface/Internal/Windows/ScratchpadWindow.cs
index 91324117c..198ac0cf6 100644
--- a/Dalamud/Interface/Scratchpad/ScratchpadWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/ScratchpadWindow.cs
@@ -4,11 +4,12 @@ using System.Linq;
using System.Numerics;
using Dalamud.Interface.Colors;
+using Dalamud.Interface.Internal.Scratchpad;
using Dalamud.Interface.Windowing;
using ImGuiNET;
using Serilog;
-namespace Dalamud.Interface.Scratchpad
+namespace Dalamud.Interface.Internal.Windows
{
///
/// This class facilitates interacting with the ScratchPad window.
@@ -16,8 +17,8 @@ namespace Dalamud.Interface.Scratchpad
internal class ScratchpadWindow : Window, IDisposable
{
private readonly Dalamud dalamud;
- private List documents = new();
- private ScratchFileWatcher watcher = new();
+ private readonly List documents = new();
+ private readonly ScratchFileWatcher watcher = new();
private string pathInput = string.Empty;
///
@@ -138,7 +139,7 @@ namespace Dalamud.Interface.Scratchpad
if (ImGui.Button("Toggle Log"))
{
- this.dalamud.DalamudUi.ToggleLog();
+ this.dalamud.DalamudUi.ToggleLogWindow();
}
ImGui.SameLine();
diff --git a/Dalamud/Interface/DalamudSettingsWindow.cs b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs
similarity index 88%
rename from Dalamud/Interface/DalamudSettingsWindow.cs
rename to Dalamud/Interface/Internal/Windows/SettingsWindow.cs
index 544e14b67..f5c13dd0a 100644
--- a/Dalamud/Interface/DalamudSettingsWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs
@@ -9,24 +9,24 @@ using CheapLoc;
using Dalamud.Configuration;
using Dalamud.Game.Text;
using Dalamud.Interface.Colors;
+using Dalamud.Interface.Components;
using Dalamud.Interface.Windowing;
using ImGuiNET;
-using Serilog;
-namespace Dalamud.Interface
+namespace Dalamud.Interface.Internal.Windows
{
///
/// The window that allows for general configuration of Dalamud itself.
///
- internal class DalamudSettingsWindow : Window
+ internal class SettingsWindow : Window
{
private const float MinScale = 0.3f;
private const float MaxScale = 2.0f;
private readonly Dalamud dalamud;
- private string[] languages;
- private string[] locLanguages;
+ private readonly string[] languages;
+ private readonly string[] locLanguages;
private int langIndex;
private Vector4 hintTextColor = ImGuiColors.DalamudGrey;
@@ -44,7 +44,8 @@ namespace Dalamud.Interface
private bool doDocking;
private bool doViewport;
private bool doGamepad;
- private List thirdRepoList;
+ private List thirdRepoList;
+ private bool thirdRepoListChanged;
private bool printPluginsWelcomeMsg;
private bool autoUpdatePlugins;
@@ -60,10 +61,10 @@ namespace Dalamud.Interface
#endregion
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The Dalamud Instance.
- public DalamudSettingsWindow(Dalamud dalamud)
+ public SettingsWindow(Dalamud dalamud)
: base(Loc.Localize("DalamudSettingsHeader", "Dalamud Settings") + "###XlSettings2", ImGuiWindowFlags.NoCollapse)
{
this.dalamud = dalamud;
@@ -142,31 +143,22 @@ namespace Dalamud.Interface
///
public override void OnOpen()
{
- base.OnOpen();
-
- Log.Information("OnOpen start");
-
- Log.Information("OnOpen end");
+ this.thirdRepoListChanged = false;
}
///
public override void OnClose()
{
- base.OnClose();
-
- Log.Information("OnClose start");
-
ImGui.GetIO().FontGlobalScale = this.dalamud.Configuration.GlobalUiScale;
- this.thirdRepoList = this.dalamud.Configuration.ThirdRepoList.Select(x => x.Clone()).ToList();
- Log.Information("OnClose end");
+ this.thirdRepoList = this.dalamud.Configuration.ThirdRepoList.Select(x => x.Clone()).ToList();
}
///
public override void Draw()
{
var windowSize = ImGui.GetWindowSize();
- ImGui.BeginChild("scrolling", new Vector2(windowSize.X - 5 - (5 * ImGui.GetIO().FontGlobalScale), windowSize.Y - 35 - (35 * ImGui.GetIO().FontGlobalScale)), false, ImGuiWindowFlags.HorizontalScrollbar);
+ ImGui.BeginChild("scrolling", new Vector2(windowSize.X - 5 - (5 * ImGuiHelpers.GlobalScale), windowSize.Y - 35 - (35 * ImGuiHelpers.GlobalScale)), false, ImGuiWindowFlags.HorizontalScrollbar);
if (ImGui.BeginTabBar("SetTabBar"))
{
@@ -176,7 +168,7 @@ namespace Dalamud.Interface
ImGui.Combo("##XlLangCombo", ref this.langIndex, this.locLanguages, this.locLanguages.Length);
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsLanguageHint", "Select the language Dalamud will be displayed in."));
- ImGui.Dummy(new Vector2(5f, 5f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(5);
ImGui.Text(Loc.Localize("DalamudSettingsChannel", "General Chat Channel"));
if (ImGui.BeginCombo("##XlChatTypeCombo", this.dalamudMessagesChatType.ToString()))
@@ -194,7 +186,7 @@ namespace Dalamud.Interface
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsChannelHint", "Select the chat channel that is to be used for general Dalamud messages."));
- ImGui.Dummy(new Vector2(5f, 5f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(5);
ImGui.Checkbox(Loc.Localize("DalamudSettingsFlash", "Flash FFXIV window on duty pop"), ref this.doCfTaskBarFlash);
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsFlashHint", "Flash the FFXIV window in your task bar when a duty is ready."));
@@ -231,7 +223,7 @@ namespace Dalamud.Interface
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsGlobalUiScaleHint", "Scale all XIVLauncher UI elements - useful for 4K displays."));
- ImGui.Dummy(new Vector2(10f, 16f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(10, 16);
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingToggleUiHideOptOutNote", "Plugins may independently opt out of the settings below."));
@@ -244,7 +236,7 @@ namespace Dalamud.Interface
ImGui.Checkbox(Loc.Localize("DalamudSettingToggleUiHideDuringGpose", "Hide plugin UI while gpose is active"), ref this.doToggleUiHideDuringGpose);
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingToggleUiHideDuringGposeHint", "Hide any open windows by plugins while gpose is active."));
- ImGui.Dummy(new Vector2(10f, 16f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(10, 16);
ImGui.Checkbox(Loc.Localize("DalamudSettingToggleViewports", "Enable multi-monitor windows"), ref this.doViewport);
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingToggleViewportsHint", "This will allow you move plugin windows onto other monitors.\nWill only work in Borderless Window or Windowed mode."));
@@ -264,27 +256,31 @@ namespace Dalamud.Interface
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsPluginTestHint", "Receive testing prereleases for plugins."));
ImGui.TextColored(this.warnTextColor, Loc.Localize("DalamudSettingsPluginTestWarning", "Testing plugins may not have been vetted before being published. Please only enable this if you are aware of the risks."));
- ImGui.Dummy(new Vector2(12f, 12f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(12);
if (ImGui.Button(Loc.Localize("DalamudSettingsClearHidden", "Clear hidden plugins")))
+ {
this.dalamud.Configuration.HiddenPluginInternalName.Clear();
+ this.dalamud.PluginManager.RefilterPluginMasters();
+ }
+
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingsClearHiddenHint", "Restore plugins you have previously hidden from the plugin installer."));
- ImGui.Dummy(new Vector2(12f, 12f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(12);
- ImGui.Dummy(new Vector2(12f, 12f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(12);
ImGui.Text(Loc.Localize("DalamudSettingsCustomRepo", "Custom Plugin Repositories"));
ImGui.TextColored(this.hintTextColor, Loc.Localize("DalamudSettingCustomRepoHint", "Add custom plugin repositories."));
ImGui.TextColored(this.warnTextColor, Loc.Localize("DalamudSettingCustomRepoWarning", "We cannot take any responsibility for third-party plugins and repositories.\nTake care when installing third-party plugins from untrusted sources."));
- ImGui.Dummy(new Vector2(5f, 5f) * ImGui.GetIO().FontGlobalScale);
+ ImGuiHelpers.ScaledDummy(5);
ImGui.Columns(4);
- ImGui.SetColumnWidth(0, 18 + (5 * ImGui.GetIO().FontGlobalScale));
- ImGui.SetColumnWidth(1, ImGui.GetWindowWidth() - (18 + 16 + 14) - ((5 + 45 + 26) * ImGui.GetIO().FontGlobalScale));
- ImGui.SetColumnWidth(2, 16 + (45 * ImGui.GetIO().FontGlobalScale));
- ImGui.SetColumnWidth(3, 14 + (26 * ImGui.GetIO().FontGlobalScale));
+ ImGui.SetColumnWidth(0, 18 + (5 * ImGuiHelpers.GlobalScale));
+ ImGui.SetColumnWidth(1, ImGui.GetWindowWidth() - (18 + 16 + 14) - ((5 + 45 + 26) * ImGuiHelpers.GlobalScale));
+ ImGui.SetColumnWidth(2, 16 + (45 * ImGuiHelpers.GlobalScale));
+ ImGui.SetColumnWidth(3, 14 + (26 * ImGuiHelpers.GlobalScale));
ImGui.Separator();
@@ -307,7 +303,7 @@ namespace Dalamud.Interface
ImGui.NextColumn();
ImGui.Separator();
- ThirdRepoSetting toRemove = null;
+ ThirdPartyRepoSettings toRemove = null;
var repoNumber = 1;
foreach (var thirdRepoSetting in this.thirdRepoList)
@@ -323,17 +319,15 @@ namespace Dalamud.Interface
ImGui.TextWrapped(thirdRepoSetting.Url);
ImGui.NextColumn();
- ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (ImGui.GetColumnWidth() / 2) - 7 - (12 * ImGui.GetIO().FontGlobalScale));
+ ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (ImGui.GetColumnWidth() / 2) - 7 - (12 * ImGuiHelpers.GlobalScale));
ImGui.Checkbox("##thirdRepoCheck", ref isEnabled);
ImGui.NextColumn();
- ImGui.PushFont(InterfaceManager.IconFont);
- if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString()))
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash))
{
toRemove = thirdRepoSetting;
}
- ImGui.PopFont();
ImGui.NextColumn();
ImGui.Separator();
@@ -345,6 +339,7 @@ namespace Dalamud.Interface
if (toRemove != null)
{
this.thirdRepoList.Remove(toRemove);
+ this.thirdRepoListChanged = true;
}
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + (ImGui.GetColumnWidth() / 2) - 8 - (ImGui.CalcTextSize(repoNumber.ToString()).X / 2));
@@ -353,9 +348,9 @@ namespace Dalamud.Interface
ImGui.SetNextItemWidth(-1);
ImGui.InputText("##thirdRepoUrlInput", ref this.thirdRepoTempUrl, 300);
ImGui.NextColumn();
+ // Enabled button
ImGui.NextColumn();
- ImGui.PushFont(InterfaceManager.IconFont);
- if (!string.IsNullOrEmpty(this.thirdRepoTempUrl) && ImGui.Button(FontAwesomeIcon.Plus.ToIconString()))
+ if (!string.IsNullOrEmpty(this.thirdRepoTempUrl) && ImGuiComponents.IconButton(FontAwesomeIcon.Plus))
{
if (this.thirdRepoList.Any(r => string.Equals(r.Url, this.thirdRepoTempUrl, StringComparison.InvariantCultureIgnoreCase)))
{
@@ -364,17 +359,16 @@ namespace Dalamud.Interface
}
else
{
- this.thirdRepoList.Add(new ThirdRepoSetting
+ this.thirdRepoList.Add(new ThirdPartyRepoSettings
{
Url = this.thirdRepoTempUrl,
IsEnabled = true,
});
-
+ this.thirdRepoListChanged = true;
this.thirdRepoTempUrl = string.Empty;
}
}
- ImGui.PopFont();
ImGui.Columns(1);
ImGui.EndTabItem();
@@ -393,6 +387,12 @@ namespace Dalamud.Interface
if (ImGui.Button(Loc.Localize("Save", "Save")))
{
this.Save();
+
+ if (this.thirdRepoListChanged)
+ {
+ this.dalamud.PluginManager.SetPluginReposFromConfig(true);
+ this.thirdRepoListChanged = false;
+ }
}
ImGui.SameLine();
@@ -400,6 +400,13 @@ namespace Dalamud.Interface
if (ImGui.Button(Loc.Localize("SaveAndClose", "Save and Close")))
{
this.Save();
+
+ if (this.thirdRepoListChanged)
+ {
+ this.dalamud.PluginManager.SetPluginReposFromConfig(true);
+ this.thirdRepoListChanged = false;
+ }
+
this.IsOpen = false;
}
}
@@ -456,7 +463,7 @@ namespace Dalamud.Interface
this.dalamud.Configuration.Save();
- this.dalamud.PluginRepository.ReloadPluginMasterAsync();
+ this.dalamud.PluginManager.ReloadPluginMasters();
}
}
}
diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs
index 81067678b..e24ab8bc1 100644
--- a/Dalamud/Interface/UiBuilder.cs
+++ b/Dalamud/Interface/UiBuilder.cs
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using Dalamud.Game.ClientState;
+using Dalamud.Interface.Internal;
using ImGuiNET;
using ImGuiScene;
using Serilog;
@@ -13,11 +15,11 @@ namespace Dalamud.Interface
/// This class represents the Dalamud UI that is drawn on top of the game.
/// It can be used to draw custom windows and overlays.
///
- public class UiBuilder : IDisposable
+ public sealed class UiBuilder : IDisposable
{
- private readonly string namespaceName;
private readonly Dalamud dalamud;
- private readonly System.Diagnostics.Stopwatch stopwatch;
+ private readonly Stopwatch stopwatch;
+ private readonly string namespaceName;
private bool hasErrorWindow;
@@ -29,11 +31,11 @@ namespace Dalamud.Interface
/// The plugin namespace.
internal UiBuilder(Dalamud dalamud, string namespaceName)
{
+ this.dalamud = dalamud;
+ this.stopwatch = new Stopwatch();
this.namespaceName = namespaceName;
- this.dalamud = dalamud;
this.dalamud.InterfaceManager.OnDraw += this.OnDraw;
- this.stopwatch = new System.Diagnostics.Stopwatch();
}
///
@@ -57,6 +59,11 @@ namespace Dalamud.Interface
///
public static ImFontPtr IconFont => InterfaceManager.IconFont;
+ ///
+ /// Gets the default Dalamud monospaced font based on Inconsolata Regular in 16pt.
+ ///
+ public static ImFontPtr MonoFont => InterfaceManager.MonoFont;
+
///
/// Gets the game's active Direct3D device.
///
@@ -217,8 +224,7 @@ namespace Dalamud.Interface
if (this.hasErrorWindow && ImGui.Begin($"{this.namespaceName} Error", ref this.hasErrorWindow, ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize))
{
- ImGui.Text(
- $"The plugin {this.namespaceName} ran into an error.\nContact the plugin developer for support.\n\nPlease try restarting your game.");
+ ImGui.Text($"The plugin {this.namespaceName} ran into an error.\nContact the plugin developer for support.\n\nPlease try restarting your game.");
ImGui.Spacing();
if (ImGui.Button("OK"))
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index bd3c55bd5..6a15ef9d5 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -143,9 +143,6 @@ namespace Dalamud.Interface.Windowing
///
internal void DrawInternal()
{
- // if (WindowName.Contains("Credits"))
- // Log.Information($"Draw: {IsOpen} {this.internalIsOpen} {this.internalLastIsOpen}");
-
if (!this.IsOpen)
{
if (this.internalIsOpen != this.internalLastIsOpen)
diff --git a/Dalamud/Localization.cs b/Dalamud/Localization.cs
index 3469dcb64..5c7255a95 100644
--- a/Dalamud/Localization.cs
+++ b/Dalamud/Localization.cs
@@ -112,6 +112,7 @@ namespace Dalamud
}
this.OnLocalizationChanged?.Invoke(langCode);
+
try
{
Loc.Setup(this.ReadLocData(langCode), this.assembly);
@@ -135,9 +136,10 @@ namespace Dalamud
{
if (this.useEmbedded)
{
- var resourceStream =
- this.assembly.GetManifestResourceStream($"{this.locResourceDirectory}{this.locResourcePrefix}{langCode}.json");
- if (resourceStream == null) return null;
+ var resourceStream = this.assembly.GetManifestResourceStream($"{this.locResourceDirectory}{this.locResourcePrefix}{langCode}.json");
+ if (resourceStream == null)
+ return null;
+
using var reader = new StreamReader(resourceStream);
return reader.ReadToEnd();
}
diff --git a/Dalamud/Memory/Exceptions/MemoryAllocationException.cs b/Dalamud/Memory/Exceptions/MemoryAllocationException.cs
new file mode 100644
index 000000000..e289c8782
--- /dev/null
+++ b/Dalamud/Memory/Exceptions/MemoryAllocationException.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Dalamud.Memory.Exceptions
+{
+ ///
+ /// An exception thrown when VirtualAlloc fails.
+ ///
+ public class MemoryAllocationException : MemoryException
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MemoryAllocationException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public MemoryAllocationException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception.
+ public MemoryAllocationException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The object that holds the serialized data about the exception being thrown.
+ /// The object that contains contextual information about the source or destination.
+ protected MemoryAllocationException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
diff --git a/Dalamud/Memory/Exceptions/MemoryException.cs b/Dalamud/Memory/Exceptions/MemoryException.cs
new file mode 100644
index 000000000..810e76404
--- /dev/null
+++ b/Dalamud/Memory/Exceptions/MemoryException.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Dalamud.Memory.Exceptions
+{
+ ///
+ /// The base exception when thrown from Dalamud.Memory.
+ ///
+ public abstract class MemoryException : Exception
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MemoryException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public MemoryException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception.
+ public MemoryException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The object that holds the serialized data about the exception being thrown.
+ /// The object that contains contextual information about the source or destination.
+ protected MemoryException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
diff --git a/Dalamud/Memory/Exceptions/MemoryPermissionException.cs b/Dalamud/Memory/Exceptions/MemoryPermissionException.cs
new file mode 100644
index 000000000..e94ccc0ce
--- /dev/null
+++ b/Dalamud/Memory/Exceptions/MemoryPermissionException.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Dalamud.Memory.Exceptions
+{
+ ///
+ /// An exception thrown when VirtualProtect fails.
+ ///
+ public class MemoryPermissionException : MemoryException
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MemoryPermissionException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public MemoryPermissionException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception.
+ public MemoryPermissionException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The object that holds the serialized data about the exception being thrown.
+ /// The object that contains contextual information about the source or destination.
+ protected MemoryPermissionException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
diff --git a/Dalamud/Memory/Exceptions/MemoryReadException.cs b/Dalamud/Memory/Exceptions/MemoryReadException.cs
new file mode 100644
index 000000000..7f3f4b1f2
--- /dev/null
+++ b/Dalamud/Memory/Exceptions/MemoryReadException.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Dalamud.Memory.Exceptions
+{
+ ///
+ /// An exception thrown when ReadProcessMemory fails.
+ ///
+ public class MemoryReadException : MemoryException
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MemoryReadException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public MemoryReadException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception.
+ public MemoryReadException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The object that holds the serialized data about the exception being thrown.
+ /// The object that contains contextual information about the source or destination.
+ protected MemoryReadException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
diff --git a/Dalamud/Memory/Exceptions/MemoryWriteException.cs b/Dalamud/Memory/Exceptions/MemoryWriteException.cs
new file mode 100644
index 000000000..5aadcee53
--- /dev/null
+++ b/Dalamud/Memory/Exceptions/MemoryWriteException.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Dalamud.Memory.Exceptions
+{
+ ///
+ /// An exception thrown when WriteProcessMemory fails.
+ ///
+ public class MemoryWriteException : MemoryException
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MemoryWriteException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public MemoryWriteException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception.
+ public MemoryWriteException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The object that holds the serialized data about the exception being thrown.
+ /// The object that contains contextual information about the source or destination.
+ protected MemoryWriteException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ {
+ }
+ }
+}
diff --git a/Dalamud/Memory/MemoryHelper.cs b/Dalamud/Memory/MemoryHelper.cs
new file mode 100644
index 000000000..0e72955af
--- /dev/null
+++ b/Dalamud/Memory/MemoryHelper.cs
@@ -0,0 +1,649 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using Dalamud.Game.Text.SeStringHandling;
+using Dalamud.Memory.Exceptions;
+using FFXIVClientStructs.FFXIV.Client.System.String;
+
+using static Dalamud.NativeFunctions;
+
+// Heavily inspired from Reloaded (https://github.com/Reloaded-Project/Reloaded.Memory)
+
+namespace Dalamud.Memory
+{
+ ///
+ /// A simple class that provides read/write access to arbitrary memory.
+ ///
+ public static unsafe class MemoryHelper
+ {
+ private static SeStringManager seStringManager;
+ private static IntPtr handle;
+
+ #region Read
+
+ ///
+ /// Reads a generic type from a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// The read in struct.
+ public static T Read(IntPtr memoryAddress) where T : unmanaged
+ => Read(memoryAddress, false);
+
+ ///
+ /// Reads a generic type from a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// Set this to true to enable struct marshalling.
+ /// The read in struct.
+ public static T Read(IntPtr memoryAddress, bool marshal)
+ {
+ return marshal
+ ? Marshal.PtrToStructure(memoryAddress)
+ : Unsafe.Read((void*)memoryAddress);
+ }
+
+ ///
+ /// Reads a generic type from a specified memory address.
+ ///
+ /// The memory address to read from.
+ /// The amount of bytes to read starting from the memoryAddress.
+ /// The read in struct.
+ public static byte[] ReadRaw(IntPtr memoryAddress, int length)
+ {
+ var value = new byte[length];
+ Marshal.Copy(memoryAddress, value, 0, value.Length);
+ return value;
+ }
+
+ ///
+ /// Reads a generic type array from a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// The amount of array items to read.
+ /// The read in struct array.
+ public static T[] Read(IntPtr memoryAddress, int arrayLength) where T : unmanaged
+ => Read(memoryAddress, arrayLength, false);
+
+ ///
+ /// Reads a generic type array from a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// The amount of array items to read.
+ /// Set this to true to enable struct marshalling.
+ /// The read in struct array.
+ public static T[] Read(IntPtr memoryAddress, int arrayLength, bool marshal)
+ {
+ var structSize = SizeOf(marshal);
+ var value = new T[arrayLength];
+
+ for (var i = 0; i < arrayLength; i++)
+ {
+ var address = memoryAddress + (structSize * i);
+ Read(address, out T result, marshal);
+ value[i] = result;
+ }
+
+ return value;
+ }
+
+ #endregion
+
+ #region Read(out)
+
+ ///
+ /// Reads a generic type from a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// Local variable to receive the read in struct.
+ public static void Read(IntPtr memoryAddress, out T value) where T : unmanaged
+ => value = Read(memoryAddress);
+
+ ///
+ /// Reads a generic type from a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// Local variable to receive the read in struct.
+ /// Set this to true to enable struct marshalling.
+ public static void Read(IntPtr memoryAddress, out T value, bool marshal)
+ => value = Read(memoryAddress, marshal);
+
+ ///
+ /// Reads raw data from a specified memory address.
+ ///
+ /// The memory address to read from.
+ /// The amount of bytes to read starting from the memoryAddress.
+ /// Local variable to receive the read in bytes.
+ public static void ReadRaw(IntPtr memoryAddress, int length, out byte[] value)
+ => value = ReadRaw(memoryAddress, length);
+
+ ///
+ /// Reads a generic type array from a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// The amount of array items to read.
+ /// The read in struct array.
+ public static void Read(IntPtr memoryAddress, int arrayLength, out T[] value) where T : unmanaged
+ => value = Read(memoryAddress, arrayLength);
+
+ ///
+ /// Reads a generic type array from a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// The amount of array items to read.
+ /// Set this to true to enable struct marshalling.
+ /// The read in struct array.
+ public static void Read(IntPtr memoryAddress, int arrayLength, bool marshal, out T[] value)
+ => value = Read(memoryAddress, arrayLength, marshal);
+
+ #endregion
+
+ #region ReadString
+
+ ///
+ /// Read a UTF-8 encoded string from a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to decode or the applicable helper method.
+ ///
+ /// The memory address to read from.
+ /// The read in string.
+ public static string ReadString(IntPtr memoryAddress)
+ => ReadString(memoryAddress, 256);
+
+ ///
+ /// Read a UTF-8 encoded string from a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to decode or the applicable helper method.
+ ///
+ /// The memory address to read from.
+ /// The maximum length of the string.
+ /// The read in string.
+ public static string ReadString(IntPtr memoryAddress, int maxLength)
+ => ReadString(memoryAddress, Encoding.UTF8, maxLength);
+
+ ///
+ /// Read a string with the given encoding from a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to decode or the applicable helper method.
+ ///
+ /// The memory address to read from.
+ /// The encoding to use to decode the string.
+ /// The read in string.
+ public static string ReadString(IntPtr memoryAddress, Encoding encoding)
+ => ReadString(memoryAddress, encoding, 256);
+
+ ///
+ /// Read a string with the given encoding from a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to decode or the applicable helper method.
+ ///
+ /// The memory address to read from.
+ /// The encoding to use to decode the string.
+ /// The maximum length of the string.
+ /// The read in string.
+ public static string ReadString(IntPtr memoryAddress, Encoding encoding, int maxLength)
+ {
+ if (maxLength <= 0)
+ return string.Empty;
+
+ ReadRaw(memoryAddress, maxLength, out var buffer);
+
+ var data = encoding.GetString(buffer);
+ var eosPos = data.IndexOf('\0');
+ return eosPos >= 0 ? data.Substring(0, eosPos) : data;
+ }
+
+ ///
+ /// Read an SeString from a specified memory address.
+ ///
+ /// The memory address to read from.
+ /// The read in string.
+ public static SeString ReadSeString(IntPtr memoryAddress)
+ => ReadSeString(memoryAddress, 256);
+
+ ///
+ /// Read an SeString from a specified memory address.
+ ///
+ /// The memory address to read from.
+ /// The maximum length of the string.
+ /// The read in string.
+ public static SeString ReadSeString(IntPtr memoryAddress, int maxLength)
+ {
+ ReadRaw(memoryAddress, maxLength, out var buffer);
+
+ var eos = Array.IndexOf(buffer, (byte)0);
+ if (eos < 0)
+ {
+ return seStringManager.Parse(buffer);
+ }
+ else
+ {
+ var newBuffer = new byte[eos];
+ Buffer.BlockCopy(buffer, 0, newBuffer, 0, eos);
+ return seStringManager.Parse(newBuffer);
+ }
+ }
+
+ ///
+ /// Read an SeString from a specified Utf8String structure.
+ ///
+ /// The memory address to read from.
+ /// The read in string.
+ public static unsafe SeString ReadSeString(Utf8String* utf8String)
+ {
+ if (utf8String == null)
+ return string.Empty;
+
+ var ptr = utf8String->StringPtr;
+ if (ptr == null)
+ return string.Empty;
+
+ var len = Math.Max(utf8String->BufUsed, utf8String->StringLength);
+
+ return ReadSeString((IntPtr)ptr, (int)len);
+ }
+
+ #endregion
+
+ #region ReadString(out)
+
+ ///
+ /// Read a UTF-8 encoded string from a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to decode or the applicable helper method.
+ ///
+ /// The memory address to read from.
+ /// The read in string.
+ public static void ReadString(IntPtr memoryAddress, out string value)
+ => value = ReadString(memoryAddress);
+
+ ///
+ /// Read a UTF-8 encoded string from a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to decode or the applicable helper method.
+ ///
+ /// The memory address to read from.
+ /// The read in string.
+ /// The maximum length of the string.
+ public static void ReadString(IntPtr memoryAddress, out string value, int maxLength)
+ => value = ReadString(memoryAddress, maxLength);
+
+ ///
+ /// Read a string with the given encoding from a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to decode or the applicable helper method.
+ ///
+ /// The memory address to read from.
+ /// The encoding to use to decode the string.
+ /// The read in string.
+ public static void ReadString(IntPtr memoryAddress, Encoding encoding, out string value)
+ => value = ReadString(memoryAddress, encoding);
+
+ ///
+ /// Read a string with the given encoding from a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to decode or the applicable helper method.
+ ///
+ /// The memory address to read from.
+ /// The encoding to use to decode the string.
+ /// The maximum length of the string.
+ /// The read in string.
+ public static void ReadString(IntPtr memoryAddress, Encoding encoding, int maxLength, out string value)
+ => value = ReadString(memoryAddress, encoding, maxLength);
+
+ ///
+ /// Read an SeString from a specified memory address.
+ ///
+ /// The memory address to read from.
+ /// The read in SeString.
+ public static void ReadSeString(IntPtr memoryAddress, out SeString value)
+ => value = ReadSeString(memoryAddress);
+
+ ///
+ /// Read an SeString from a specified memory address.
+ ///
+ /// The memory address to read from.
+ /// The maximum length of the string.
+ /// The read in SeString.
+ public static void ReadSeString(IntPtr memoryAddress, int maxLength, out SeString value)
+ => value = ReadSeString(memoryAddress, maxLength);
+
+ ///
+ /// Read an SeString from a specified Utf8String structure.
+ ///
+ /// The memory address to read from.
+ /// The read in string.
+ public static unsafe void ReadSeString(Utf8String* utf8String, out SeString value)
+ => value = ReadSeString(utf8String);
+
+ #endregion
+
+ #region Write
+
+ ///
+ /// Writes a generic type to a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// The item to write to the address.
+ public static void Write(IntPtr memoryAddress, T item) where T : unmanaged
+ => Write(memoryAddress, item);
+
+ ///
+ /// Writes a generic type to a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to read from.
+ /// The item to write to the address.
+ /// Set this to true to enable struct marshalling.
+ public static void Write(IntPtr memoryAddress, T item, bool marshal)
+ {
+ if (marshal)
+ Marshal.StructureToPtr(item, memoryAddress, false);
+ else
+ Unsafe.Write((void*)memoryAddress, item);
+ }
+
+ ///
+ /// Writes raw data to a specified memory address.
+ ///
+ /// The memory address to read from.
+ /// The bytes to write to memoryAddress.
+ public static void WriteRaw(IntPtr memoryAddress, byte[] data)
+ {
+ Marshal.Copy(data, 0, memoryAddress, data.Length);
+ }
+
+ ///
+ /// Writes a generic type array to a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to write to.
+ /// The array of items to write to the address.
+ public static void Write(IntPtr memoryAddress, T[] items) where T : unmanaged
+ => Write(memoryAddress, items, false);
+
+ ///
+ /// Writes a generic type array to a specified memory address.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address to write to.
+ /// The array of items to write to the address.
+ /// Set this to true to enable struct marshalling.
+ public static void Write(IntPtr memoryAddress, T[] items, bool marshal)
+ {
+ var structSize = SizeOf(marshal);
+
+ for (var i = 0; i < items.Length; i++)
+ {
+ var address = memoryAddress + (structSize * i);
+ Write(address, items[i], marshal);
+ }
+ }
+
+ #endregion
+
+ #region WriteString
+
+ ///
+ /// Write a UTF-8 encoded string to a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to encode or the applicable helper method.
+ ///
+ /// The memory address to write to.
+ /// The string to write.
+ public static void WriteString(IntPtr memoryAddress, string value)
+ => WriteString(memoryAddress, value, Encoding.UTF8);
+
+ ///
+ /// Write a string with the given encoding to a specified memory address.
+ ///
+ ///
+ /// Attention! If this is an SeString, use the to encode or the applicable helper method.
+ ///
+ /// The memory address to write to.
+ /// The string to write.
+ /// The encoding to use.
+ public static void WriteString(IntPtr memoryAddress, string value, Encoding encoding)
+ {
+ if (string.IsNullOrEmpty(value))
+ return;
+
+ var bytes = encoding.GetBytes(value + '\0');
+
+ WriteRaw(memoryAddress, bytes);
+ }
+
+ ///
+ /// Write an SeString to a specified memory address.
+ ///
+ /// The memory address to write to.
+ /// The SeString to write.
+ public static void WriteSeString(IntPtr memoryAddress, SeString value)
+ {
+ if (value is null)
+ return;
+
+ WriteRaw(memoryAddress, value.Encode());
+ }
+
+ #endregion
+
+ #region ApiWrappers
+
+ ///
+ /// Allocates fixed size of memory inside the target memory source via Windows API calls.
+ /// Returns the address of newly allocated memory.
+ ///
+ /// Amount of bytes to be allocated.
+ /// Address to the newly allocated memory.
+ public static IntPtr Allocate(int length)
+ {
+ var address = VirtualAlloc(
+ IntPtr.Zero,
+ (UIntPtr)length,
+ AllocationType.Commit | AllocationType.Reserve,
+ MemoryProtection.ExecuteReadWrite);
+
+ if (address == IntPtr.Zero)
+ throw new MemoryAllocationException($"Unable to allocate {length} bytes.");
+
+ return address;
+ }
+
+ ///
+ /// Allocates fixed size of memory inside the target memory source via Windows API calls.
+ /// Returns the address of newly allocated memory.
+ ///
+ /// Amount of bytes to be allocated.
+ /// Address to the newly allocated memory.
+ public static void Allocate(int length, out IntPtr memoryAddress)
+ => memoryAddress = Allocate(length);
+
+ ///
+ /// Frees memory previously allocated with Allocate via Windows API calls.
+ ///
+ /// The address of the memory to free.
+ /// True if the operation is successful.
+ public static bool Free(IntPtr memoryAddress)
+ {
+ return VirtualFree(memoryAddress, UIntPtr.Zero, AllocationType.Release);
+ }
+
+ ///
+ /// Changes the page permissions for a specified combination of address and length via Windows API calls.
+ ///
+ /// The memory address for which to change page permissions for.
+ /// The region size for which to change permissions for.
+ /// The new permissions to set.
+ /// The old page permissions.
+ public static MemoryProtection ChangePermission(IntPtr memoryAddress, int length, MemoryProtection newPermissions)
+ {
+ var result = VirtualProtect(memoryAddress, (UIntPtr)length, newPermissions, out var oldPermissions);
+
+ if (!result)
+ throw new MemoryPermissionException($"Unable to change permissions at 0x{memoryAddress.ToInt64():X} of length {length} and permission {newPermissions} (result={result})");
+
+ var last = Marshal.GetLastWin32Error();
+ if (last > 0)
+ throw new MemoryPermissionException($"Unable to change permissions at 0x{memoryAddress.ToInt64():X} of length {length} and permission {newPermissions} (error={last})");
+
+ return oldPermissions;
+ }
+
+ ///
+ /// Changes the page permissions for a specified combination of address and length via Windows API calls.
+ ///
+ /// The memory address for which to change page permissions for.
+ /// The region size for which to change permissions for.
+ /// The new permissions to set.
+ /// The old page permissions.
+ public static void ChangePermission(IntPtr memoryAddress, int length, MemoryProtection newPermissions, out MemoryProtection oldPermissions)
+ => oldPermissions = ChangePermission(memoryAddress, length, newPermissions);
+
+ ///
+ /// Changes the page permissions for a specified combination of address and element from which to deduce size via Windows API calls.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The memory address for which to change page permissions for.
+ /// The struct element from which the region size to change permissions for will be calculated.
+ /// The new permissions to set.
+ /// Set to true to calculate the size of the struct after marshalling instead of before.
+ /// The old page permissions.
+ public static MemoryProtection ChangePermission(IntPtr memoryAddress, ref T baseElement, MemoryProtection newPermissions, bool marshal)
+ => ChangePermission(memoryAddress, SizeOf(marshal), newPermissions);
+
+ ///
+ /// Reads raw data from a specified memory address via Windows API calls.
+ /// This is noticably slower than Unsafe or Marshal.
+ ///
+ /// The memory address to read from.
+ /// The amount of bytes to read starting from the memoryAddress.
+ /// The read in bytes.
+ public static byte[] ReadProcessMemory(IntPtr memoryAddress, int length)
+ {
+ var value = new byte[length];
+ ReadProcessMemory(memoryAddress, ref value);
+ return value;
+ }
+
+ ///
+ /// Reads raw data from a specified memory address via Windows API calls.
+ /// This is noticably slower than Unsafe or Marshal.
+ ///
+ /// The memory address to read from.
+ /// The amount of bytes to read starting from the memoryAddress.
+ /// The read in bytes.
+ public static void ReadProcessMemory(IntPtr memoryAddress, int length, out byte[] value)
+ => value = ReadProcessMemory(memoryAddress, length);
+
+ ///
+ /// Reads raw data from a specified memory address via Windows API calls.
+ /// This is noticably slower than Unsafe or Marshal.
+ ///
+ /// The memory address to read from.
+ /// The read in bytes.
+ public static void ReadProcessMemory(IntPtr memoryAddress, ref byte[] value)
+ {
+ var length = value.Length;
+ var result = NativeFunctions.ReadProcessMemory(handle, memoryAddress, value, length, out _);
+
+ if (!result)
+ throw new MemoryReadException($"Unable to read memory at 0x{memoryAddress.ToInt64():X} of length {length} (result={result})");
+
+ var last = Marshal.GetLastWin32Error();
+ if (last > 0)
+ throw new MemoryReadException($"Unable to read memory at 0x{memoryAddress.ToInt64():X} of length {length} (error={last})");
+ }
+
+ ///
+ /// Writes raw data to a specified memory address via Windows API calls.
+ /// This is noticably slower than Unsafe or Marshal.
+ ///
+ /// The memory address to write to.
+ /// The bytes to write to memoryAddress.
+ public static void WriteProcessMemory(IntPtr memoryAddress, byte[] data)
+ {
+ var length = data.Length;
+ var result = NativeFunctions.WriteProcessMemory(handle, memoryAddress, data, length, out _);
+
+ if (!result)
+ throw new MemoryWriteException($"Unable to write memory at 0x{memoryAddress.ToInt64():X} of length {length} (result={result})");
+
+ var last = Marshal.GetLastWin32Error();
+ if (last > 0)
+ throw new MemoryWriteException($"Unable to write memory at 0x{memoryAddress.ToInt64():X} of length {length} (error={last})");
+ }
+
+ #endregion
+
+ #region Sizing
+
+ ///
+ /// Returns the size of a specific primitive or struct type.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The size of the primitive or struct.
+ public static int SizeOf()
+ => SizeOf(false);
+
+ ///
+ /// Returns the size of a specific primitive or struct type.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// If set to true; will return the size of an element after marshalling.
+ /// The size of the primitive or struct.
+ public static int SizeOf(bool marshal)
+ => marshal ? Marshal.SizeOf() : Unsafe.SizeOf();
+
+ ///
+ /// Returns the size of a specific primitive or struct type.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The number of array elements present.
+ /// The size of the primitive or struct array.
+ public static int SizeOf(int elementCount) where T : unmanaged
+ => SizeOf() * elementCount;
+
+ ///
+ /// Returns the size of a specific primitive or struct type.
+ ///
+ /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute.
+ /// The number of array elements present.
+ /// If set to true; will return the size of an element after marshalling.
+ /// The size of the primitive or struct array.
+ public static int SizeOf(int elementCount, bool marshal)
+ => SizeOf(marshal) * elementCount;
+
+ #endregion
+
+ ///
+ /// Initialize with static access to Dalamud.
+ ///
+ /// The Dalamud instance.
+ internal static void Initialize(Dalamud dalamud)
+ {
+ seStringManager = dalamud.SeStringManager;
+ handle = Process.GetCurrentProcess().Handle;
+ }
+ }
+}
diff --git a/Dalamud/Memory/MemoryProtection.cs b/Dalamud/Memory/MemoryProtection.cs
new file mode 100644
index 000000000..289c5024d
--- /dev/null
+++ b/Dalamud/Memory/MemoryProtection.cs
@@ -0,0 +1,117 @@
+using System;
+
+// This is a copy from NativeFunctions.MemoryProtection
+
+namespace Dalamud.Memory
+{
+ ///
+ /// PAGE_* from memoryapi.
+ ///
+ [Flags]
+ public enum MemoryProtection
+ {
+ // Copied from NativeFunctions to expose to the user.
+
+ ///
+ /// Enables execute access to the committed region of pages. An attempt to write to the committed region results
+ /// in an access violation. This flag is not supported by the CreateFileMapping function.
+ ///
+ Execute = 0x10,
+
+ ///
+ /// Enables execute or read-only access to the committed region of pages. An attempt to write to the committed region
+ /// results in an access violation.
+ ///
+ ExecuteRead = 0x20,
+
+ ///
+ /// Enables execute, read-only, or read/write access to the committed region of pages.
+ ///
+ ExecuteReadWrite = 0x40,
+
+ ///
+ /// Enables execute, read-only, or copy-on-write access to a mapped view of a file mapping object. An attempt to
+ /// write to a committed copy-on-write page results in a private copy of the page being made for the process. The
+ /// private page is marked as PAGE_EXECUTE_READWRITE, and the change is written to the new page. This flag is not
+ /// supported by the VirtualAlloc or VirtualAllocEx functions.
+ ///
+ ExecuteWriteCopy = 0x80,
+
+ ///
+ /// Disables all access to the committed region of pages. An attempt to read from, write to, or execute the committed
+ /// region results in an access violation. This flag is not supported by the CreateFileMapping function.
+ ///
+ NoAccess = 0x01,
+
+ ///
+ /// Enables read-only access to the committed region of pages. An attempt to write to the committed region results
+ /// in an access violation. If Data Execution Prevention is enabled, an attempt to execute code in the committed
+ /// region results in an access violation.
+ ///
+ ReadOnly = 0x02,
+
+ ///
+ /// Enables read-only or read/write access to the committed region of pages. If Data Execution Prevention is enabled,
+ /// attempting to execute code in the committed region results in an access violation.
+ ///
+ ReadWrite = 0x04,
+
+ ///
+ /// Enables read-only or copy-on-write access to a mapped view of a file mapping object. An attempt to write to
+ /// a committed copy-on-write page results in a private copy of the page being made for the process. The private
+ /// page is marked as PAGE_READWRITE, and the change is written to the new page. If Data Execution Prevention is
+ /// enabled, attempting to execute code in the committed region results in an access violation. This flag is not
+ /// supported by the VirtualAlloc or VirtualAllocEx functions.
+ ///
+ WriteCopy = 0x08,
+
+ ///
+ /// Sets all locations in the pages as invalid targets for CFG. Used along with any execute page protection like
+ /// PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY. Any indirect call to locations
+ /// in those pages will fail CFG checks and the process will be terminated. The default behavior for executable
+ /// pages allocated is to be marked valid call targets for CFG. This flag is not supported by the VirtualProtect
+ /// or CreateFileMapping functions.
+ ///
+ TargetsInvalid = 0x40000000,
+
+ ///
+ /// Pages in the region will not have their CFG information updated while the protection changes for VirtualProtect.
+ /// For example, if the pages in the region was allocated using PAGE_TARGETS_INVALID, then the invalid information
+ /// will be maintained while the page protection changes. This flag is only valid when the protection changes to
+ /// an executable type like PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY.
+ /// The default behavior for VirtualProtect protection change to executable is to mark all locations as valid call
+ /// targets for CFG.
+ ///
+ TargetsNoUpdate = TargetsInvalid,
+
+ ///
+ /// Pages in the region become guard pages. Any attempt to access a guard page causes the system to raise a
+ /// STATUS_GUARD_PAGE_VIOLATION exception and turn off the guard page status. Guard pages thus act as a one-time
+ /// access alarm. For more information, see Creating Guard Pages. When an access attempt leads the system to turn
+ /// off guard page status, the underlying page protection takes over. If a guard page exception occurs during a
+ /// system service, the service typically returns a failure status indicator. This value cannot be used with
+ /// PAGE_NOACCESS. This flag is not supported by the CreateFileMapping function.
+ ///
+ Guard = 0x100,
+
+ ///
+ /// Sets all pages to be non-cachable. Applications should not use this attribute except when explicitly required
+ /// for a device. Using the interlocked functions with memory that is mapped with SEC_NOCACHE can result in an
+ /// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_NOCACHE flag cannot be used with the PAGE_GUARD, PAGE_NOACCESS,
+ /// or PAGE_WRITECOMBINE flags. The PAGE_NOCACHE flag can be used only when allocating private memory with the
+ /// VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable non-cached memory access for shared
+ /// memory, specify the SEC_NOCACHE flag when calling the CreateFileMapping function.
+ ///
+ NoCache = 0x200,
+
+ ///
+ /// Sets all pages to be write-combined. Applications should not use this attribute except when explicitly required
+ /// for a device. Using the interlocked functions with memory that is mapped as write-combined can result in an
+ /// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_WRITECOMBINE flag cannot be specified with the PAGE_NOACCESS,
+ /// PAGE_GUARD, and PAGE_NOCACHE flags. The PAGE_WRITECOMBINE flag can be used only when allocating private memory
+ /// with the VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable write-combined memory access
+ /// for shared memory, specify the SEC_WRITECOMBINE flag when calling the CreateFileMapping function.
+ ///
+ WriteCombine = 0x400,
+ }
+}
diff --git a/Dalamud/ModuleLog.cs b/Dalamud/ModuleLog.cs
new file mode 100644
index 000000000..b78430a92
--- /dev/null
+++ b/Dalamud/ModuleLog.cs
@@ -0,0 +1,124 @@
+using System;
+
+namespace Dalamud
+{
+ ///
+ /// Class offering various methods to allow for logging in Dalamud modules.
+ ///
+ internal class ModuleLog
+ {
+ private readonly string moduleName;
+
+ ///
+ /// Initializes a new instance of the class.
+ /// This class can be used to prefix logging messages with a Dalamud module name prefix. For example, "[PLUGINR] ...".
+ ///
+ /// The module name.
+ public ModuleLog(string moduleName)
+ {
+ this.moduleName = moduleName;
+ }
+
+ ///
+ /// Log a templated verbose message to the in-game debug log.
+ ///
+ /// The message template.
+ /// Values to log.
+ public void Verbose(string messageTemplate, params object[] values)
+ => Serilog.Log.Verbose($"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated verbose message to the in-game debug log.
+ ///
+ /// The exception that caused the error.
+ /// The message template.
+ /// Values to log.
+ public void Verbose(Exception exception, string messageTemplate, params object[] values)
+ => Serilog.Log.Verbose(exception, $"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated debug message to the in-game debug log.
+ ///
+ /// The message template.
+ /// Values to log.
+ public void Debug(string messageTemplate, params object[] values)
+ => Serilog.Log.Debug($"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated debug message to the in-game debug log.
+ ///
+ /// The exception that caused the error.
+ /// The message template.
+ /// Values to log.
+ public void Debug(Exception exception, string messageTemplate, params object[] values)
+ => Serilog.Log.Debug(exception, $"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated information message to the in-game debug log.
+ ///
+ /// The message template.
+ /// Values to log.
+ public void Information(string messageTemplate, params object[] values)
+ => Serilog.Log.Information($"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated information message to the in-game debug log.
+ ///
+ /// The exception that caused the error.
+ /// The message template.
+ /// Values to log.
+ public void Information(Exception exception, string messageTemplate, params object[] values)
+ => Serilog.Log.Information(exception, $"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated warning message to the in-game debug log.
+ ///
+ /// The message template.
+ /// Values to log.
+ public void Warning(string messageTemplate, params object[] values)
+ => Serilog.Log.Warning($"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated warning message to the in-game debug log.
+ ///
+ /// The exception that caused the error.
+ /// The message template.
+ /// Values to log.
+ public void Warning(Exception exception, string messageTemplate, params object[] values)
+ => Serilog.Log.Warning(exception, $"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated error message to the in-game debug log.
+ ///
+ /// The message template.
+ /// Values to log.
+ public void Error(string messageTemplate, params object[] values)
+ => Serilog.Log.Error($"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated error message to the in-game debug log.
+ ///
+ /// The exception that caused the error.
+ /// The message template.
+ /// Values to log.
+ public void Error(Exception exception, string messageTemplate, params object[] values)
+ => Serilog.Log.Error(exception, $"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated fatal message to the in-game debug log.
+ ///
+ /// The message template.
+ /// Values to log.
+ public void Fatal(string messageTemplate, params object[] values)
+ => Serilog.Log.Fatal($"[{this.moduleName}] {messageTemplate}", values);
+
+ ///
+ /// Log a templated fatal message to the in-game debug log.
+ ///
+ /// The exception that caused the error.
+ /// The message template.
+ /// Values to log.
+ public void Fatal(Exception exception, string messageTemplate, params object[] values)
+ => Serilog.Log.Fatal(exception, $"[{this.moduleName}] {messageTemplate}", values);
+ }
+}
diff --git a/Dalamud/NativeFunctions.cs b/Dalamud/NativeFunctions.cs
index d41d64985..0a4fbc5bd 100644
--- a/Dalamud/NativeFunctions.cs
+++ b/Dalamud/NativeFunctions.cs
@@ -1,5 +1,4 @@
using System;
-using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net.Sockets;
using System.Runtime.InteropServices;
@@ -54,6 +53,11 @@ namespace Dalamud
///
public enum MessageBoxType : uint
{
+ ///
+ /// The default value for any of the various subtypes.
+ ///
+ DefaultValue = 0x0,
+
// To indicate the buttons displayed in the message box, specify one of the following values.
///
@@ -76,7 +80,7 @@ namespace Dalamud
///
/// The message box contains one push button: OK. This is the default.
///
- Ok = 0x0,
+ Ok = DefaultValue,
///
/// The message box contains two push buttons: OK and Cancel.
@@ -108,7 +112,7 @@ namespace Dalamud
///
/// An exclamation-point icon appears in the message box.
///
- IconWarning = 0x30,
+ IconWarning = IconExclamation,
///
/// An icon consisting of a lowercase letter i in a circle appears in the message box.
@@ -118,7 +122,7 @@ namespace Dalamud
///
/// An icon consisting of a lowercase letter i in a circle appears in the message box.
///
- IconAsterisk = 0x40,
+ IconAsterisk = IconInformation,
///
/// A question-mark icon appears in the message box.
@@ -138,12 +142,12 @@ namespace Dalamud
///
/// A stop-sign icon appears in the message box.
///
- IconError = 0x10,
+ IconError = IconStop,
///
/// A stop-sign icon appears in the message box.
///
- IconHand = 0x10,
+ IconHand = IconStop,
// To indicate the default button, specify one of the following values.
@@ -151,7 +155,7 @@ namespace Dalamud
/// The first button is the default button.
/// MB_DEFBUTTON1 is the default unless MB_DEFBUTTON2, MB_DEFBUTTON3, or MB_DEFBUTTON4 is specified.
///
- DefButton1 = 0x0,
+ DefButton1 = DefaultValue,
///
/// The second button is the default button.
@@ -177,7 +181,7 @@ namespace Dalamud
/// of the parent of the message box are automatically disabled, but pop-up windows are not. MB_APPLMODAL is the
/// default if neither MB_SYSTEMMODAL nor MB_TASKMODAL is specified.
///
- ApplModal = 0x0,
+ ApplModal = DefaultValue,
///
/// Same as MB_APPLMODAL except that the message box has the WS_EX_TOPMOST style.
@@ -241,14 +245,13 @@ namespace Dalamud
{
var activatedHandle = GetForegroundWindow();
if (activatedHandle == IntPtr.Zero)
- {
return false; // No window is currently activated
- }
- var procId = Process.GetCurrentProcess().Id;
- GetWindowThreadProcessId(activatedHandle, out var activeProcId);
+ _ = GetWindowThreadProcessId(activatedHandle, out var activeProcId);
+ if (Marshal.GetLastWin32Error() != 0)
+ return false;
- return activeProcId == procId;
+ return activeProcId == Environment.ProcessId;
}
///
@@ -320,8 +323,8 @@ namespace Dalamud
/// If the function fails, the return value is zero.To get extended error information, call GetLastError. If the function
/// succeeds, the return value is one of the ID* enum values.
///
- [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
- public static extern int MessageBox(IntPtr hWnd, string text, string caption, MessageBoxType type);
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern int MessageBoxW(IntPtr hWnd, string text, string caption, MessageBoxType type);
///
/// See https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-flashwinfo.
@@ -333,28 +336,28 @@ namespace Dalamud
///
/// The size of the structure, in bytes.
///
- public uint cbSize;
+ public uint Size;
///
/// A handle to the window to be flashed. The window can be either opened or minimized.
///
- public IntPtr hwnd;
+ public IntPtr Hwnd;
///
/// The flash status. This parameter can be one or more of the FlashWindow enum values.
///
- public FlashWindow dwFlags;
+ public FlashWindow Flags;
///
/// The number of times to flash the window.
///
- public uint uCount;
+ public uint Count;
///
/// The rate at which the window is to be flashed, in milliseconds. If dwTimeout is zero, the function uses the
/// default cursor blink rate.
///
- public uint dwTimeout;
+ public uint Timeout;
}
}
@@ -363,6 +366,118 @@ namespace Dalamud
///
internal static partial class NativeFunctions
{
+ ///
+ /// MEM_* from memoryapi.
+ ///
+ [Flags]
+ public enum AllocationType
+ {
+ ///
+ /// To coalesce two adjacent placeholders, specify MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS. When you coalesce
+ /// placeholders, lpAddress and dwSize must exactly match those of the placeholder.
+ ///
+ CoalescePlaceholders = 0x1,
+
+ ///
+ /// Frees an allocation back to a placeholder (after you've replaced a placeholder with a private allocation using
+ /// VirtualAlloc2 or Virtual2AllocFromApp). To split a placeholder into two placeholders, specify
+ /// MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER.
+ ///
+ PreservePlaceholder = 0x2,
+
+ ///
+ /// Allocates memory charges (from the overall size of memory and the paging files on disk) for the specified reserved
+ /// memory pages. The function also guarantees that when the caller later initially accesses the memory, the contents
+ /// will be zero. Actual physical pages are not allocated unless/until the virtual addresses are actually accessed.
+ /// To reserve and commit pages in one step, call VirtualAllocEx with MEM_COMMIT | MEM_RESERVE. Attempting to commit
+ /// a specific address range by specifying MEM_COMMIT without MEM_RESERVE and a non-NULL lpAddress fails unless the
+ /// entire range has already been reserved. The resulting error code is ERROR_INVALID_ADDRESS. An attempt to commit
+ /// a page that is already committed does not cause the function to fail. This means that you can commit pages without
+ /// first determining the current commitment state of each page. If lpAddress specifies an address within an enclave,
+ /// flAllocationType must be MEM_COMMIT.
+ ///
+ Commit = 0x1000,
+
+ ///
+ /// Reserves a range of the process's virtual address space without allocating any actual physical storage in memory
+ /// or in the paging file on disk. You commit reserved pages by calling VirtualAllocEx again with MEM_COMMIT. To
+ /// reserve and commit pages in one step, call VirtualAllocEx with MEM_COMMIT | MEM_RESERVE. Other memory allocation
+ /// functions, such as malloc and LocalAlloc, cannot use reserved memory until it has been released.
+ ///
+ Reserve = 0x2000,
+
+ ///
+ /// Decommits the specified region of committed pages. After the operation, the pages are in the reserved state.
+ /// The function does not fail if you attempt to decommit an uncommitted page. This means that you can decommit
+ /// a range of pages without first determining the current commitment state. The MEM_DECOMMIT value is not supported
+ /// when the lpAddress parameter provides the base address for an enclave.
+ ///
+ Decommit = 0x4000,
+
+ ///
+ /// Releases the specified region of pages, or placeholder (for a placeholder, the address space is released and
+ /// available for other allocations). After this operation, the pages are in the free state. If you specify this
+ /// value, dwSize must be 0 (zero), and lpAddress must point to the base address returned by the VirtualAlloc function
+ /// when the region is reserved. The function fails if either of these conditions is not met. If any pages in the
+ /// region are committed currently, the function first decommits, and then releases them. The function does not
+ /// fail if you attempt to release pages that are in different states, some reserved and some committed. This means
+ /// that you can release a range of pages without first determining the current commitment state.
+ ///
+ Release = 0x8000,
+
+ ///
+ /// Indicates that data in the memory range specified by lpAddress and dwSize is no longer of interest. The pages
+ /// should not be read from or written to the paging file. However, the memory block will be used again later, so
+ /// it should not be decommitted. This value cannot be used with any other value. Using this value does not guarantee
+ /// that the range operated on with MEM_RESET will contain zeros. If you want the range to contain zeros, decommit
+ /// the memory and then recommit it. When you use MEM_RESET, the VirtualAllocEx function ignores the value of fProtect.
+ /// However, you must still set fProtect to a valid protection value, such as PAGE_NOACCESS. VirtualAllocEx returns
+ /// an error if you use MEM_RESET and the range of memory is mapped to a file. A shared view is only acceptable
+ /// if it is mapped to a paging file.
+ ///
+ Reset = 0x80000,
+
+ ///
+ /// MEM_RESET_UNDO should only be called on an address range to which MEM_RESET was successfully applied earlier.
+ /// It indicates that the data in the specified memory range specified by lpAddress and dwSize is of interest to
+ /// the caller and attempts to reverse the effects of MEM_RESET. If the function succeeds, that means all data in
+ /// the specified address range is intact. If the function fails, at least some of the data in the address range
+ /// has been replaced with zeroes. This value cannot be used with any other value. If MEM_RESET_UNDO is called on
+ /// an address range which was not MEM_RESET earlier, the behavior is undefined. When you specify MEM_RESET, the
+ /// VirtualAllocEx function ignores the value of flProtect. However, you must still set flProtect to a valid
+ /// protection value, such as PAGE_NOACCESS.
+ ///
+ ResetUndo = 0x1000000,
+
+ ///
+ /// Reserves an address range that can be used to map Address Windowing Extensions (AWE) pages. This value must
+ /// be used with MEM_RESERVE and no other values.
+ ///
+ Physical = 0x400000,
+
+ ///
+ /// Allocates memory at the highest possible address. This can be slower than regular allocations, especially when
+ /// there are many allocations.
+ ///
+ TopDown = 0x100000,
+
+ ///
+ /// Causes the system to track pages that are written to in the allocated region. If you specify this value, you
+ /// must also specify MEM_RESERVE. To retrieve the addresses of the pages that have been written to since the region
+ /// was allocated or the write-tracking state was reset, call the GetWriteWatch function. To reset the write-tracking
+ /// state, call GetWriteWatch or ResetWriteWatch. The write-tracking feature remains enabled for the memory region
+ /// until the region is freed.
+ ///
+ WriteWatch = 0x200000,
+
+ ///
+ /// Allocates memory using large page support. The size and alignment must be a multiple of the large-page minimum.
+ /// To obtain this value, use the GetLargePageMinimum function. If you specify this value, you must also specify
+ /// MEM_RESERVE and MEM_COMMIT.
+ ///
+ LargePages = 0x20000000,
+ }
+
///
/// SEM_* from errhandlingapi.
///
@@ -402,20 +517,113 @@ namespace Dalamud
}
///
- /// See https://docs.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-debugactiveprocess.
- /// Enables a debugger to attach to an active process and debug it.
+ /// PAGE_* from memoryapi.
///
- ///
- /// The identifier for the process to be debugged. The debugger is granted debugging access to the process as if it
- /// created the process with the DEBUG_ONLY_THIS_PROCESS flag. For more information, see the Remarks section of this
- /// topic.
- ///
- ///
- /// If the function succeeds, the return value is nonzero. If the function fails, the return value is 0 (zero). To get
- /// extended error information, call GetLastError.
- ///
- [DllImport("kernel32.dll", SetLastError = true)]
- public static extern bool DebugActiveProcess(uint dwProcessId);
+ [Flags]
+ public enum MemoryProtection
+ {
+ ///
+ /// Enables execute access to the committed region of pages. An attempt to write to the committed region results
+ /// in an access violation. This flag is not supported by the CreateFileMapping function.
+ ///
+ Execute = 0x10,
+
+ ///
+ /// Enables execute or read-only access to the committed region of pages. An attempt to write to the committed region
+ /// results in an access violation.
+ ///
+ ExecuteRead = 0x20,
+
+ ///
+ /// Enables execute, read-only, or read/write access to the committed region of pages.
+ ///
+ ExecuteReadWrite = 0x40,
+
+ ///
+ /// Enables execute, read-only, or copy-on-write access to a mapped view of a file mapping object. An attempt to
+ /// write to a committed copy-on-write page results in a private copy of the page being made for the process. The
+ /// private page is marked as PAGE_EXECUTE_READWRITE, and the change is written to the new page. This flag is not
+ /// supported by the VirtualAlloc or VirtualAllocEx functions.
+ ///
+ ExecuteWriteCopy = 0x80,
+
+ ///
+ /// Disables all access to the committed region of pages. An attempt to read from, write to, or execute the committed
+ /// region results in an access violation. This flag is not supported by the CreateFileMapping function.
+ ///
+ NoAccess = 0x01,
+
+ ///
+ /// Enables read-only access to the committed region of pages. An attempt to write to the committed region results
+ /// in an access violation. If Data Execution Prevention is enabled, an attempt to execute code in the committed
+ /// region results in an access violation.
+ ///
+ ReadOnly = 0x02,
+
+ ///
+ /// Enables read-only or read/write access to the committed region of pages. If Data Execution Prevention is enabled,
+ /// attempting to execute code in the committed region results in an access violation.
+ ///
+ ReadWrite = 0x04,
+
+ ///
+ /// Enables read-only or copy-on-write access to a mapped view of a file mapping object. An attempt to write to
+ /// a committed copy-on-write page results in a private copy of the page being made for the process. The private
+ /// page is marked as PAGE_READWRITE, and the change is written to the new page. If Data Execution Prevention is
+ /// enabled, attempting to execute code in the committed region results in an access violation. This flag is not
+ /// supported by the VirtualAlloc or VirtualAllocEx functions.
+ ///
+ WriteCopy = 0x08,
+
+ ///
+ /// Sets all locations in the pages as invalid targets for CFG. Used along with any execute page protection like
+ /// PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY. Any indirect call to locations
+ /// in those pages will fail CFG checks and the process will be terminated. The default behavior for executable
+ /// pages allocated is to be marked valid call targets for CFG. This flag is not supported by the VirtualProtect
+ /// or CreateFileMapping functions.
+ ///
+ TargetsInvalid = 0x40000000,
+
+ ///
+ /// Pages in the region will not have their CFG information updated while the protection changes for VirtualProtect.
+ /// For example, if the pages in the region was allocated using PAGE_TARGETS_INVALID, then the invalid information
+ /// will be maintained while the page protection changes. This flag is only valid when the protection changes to
+ /// an executable type like PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY.
+ /// The default behavior for VirtualProtect protection change to executable is to mark all locations as valid call
+ /// targets for CFG.
+ ///
+ TargetsNoUpdate = TargetsInvalid,
+
+ ///
+ /// Pages in the region become guard pages. Any attempt to access a guard page causes the system to raise a
+ /// STATUS_GUARD_PAGE_VIOLATION exception and turn off the guard page status. Guard pages thus act as a one-time
+ /// access alarm. For more information, see Creating Guard Pages. When an access attempt leads the system to turn
+ /// off guard page status, the underlying page protection takes over. If a guard page exception occurs during a
+ /// system service, the service typically returns a failure status indicator. This value cannot be used with
+ /// PAGE_NOACCESS. This flag is not supported by the CreateFileMapping function.
+ ///
+ Guard = 0x100,
+
+ ///
+ /// Sets all pages to be non-cachable. Applications should not use this attribute except when explicitly required
+ /// for a device. Using the interlocked functions with memory that is mapped with SEC_NOCACHE can result in an
+ /// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_NOCACHE flag cannot be used with the PAGE_GUARD, PAGE_NOACCESS,
+ /// or PAGE_WRITECOMBINE flags. The PAGE_NOCACHE flag can be used only when allocating private memory with the
+ /// VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable non-cached memory access for shared
+ /// memory, specify the SEC_NOCACHE flag when calling the CreateFileMapping function.
+ ///
+ NoCache = 0x200,
+
+ ///
+ /// Sets all pages to be write-combined. Applications should not use this attribute except when explicitly required
+ /// for a device. Using the interlocked functions with memory that is mapped as write-combined can result in an
+ /// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_WRITECOMBINE flag cannot be specified with the PAGE_NOACCESS,
+ /// PAGE_GUARD, and PAGE_NOCACHE flags. The PAGE_WRITECOMBINE flag can be used only when allocating private memory
+ /// with the VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable write-combined memory access
+ /// for shared memory, specify the SEC_WRITECOMBINE flag when calling the CreateFileMapping function.
+ ///
+ WriteCombine = 0x400,
+ }
///
/// See https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-freelibrary.
@@ -463,9 +671,9 @@ namespace Dalamud
/// code is ERROR_SUCCESS. If the function fails, the return value is 0 (zero). To get extended error information, call
/// GetLastError.
///
- [DllImport("kernel32.dll", SetLastError = true)]
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
[PreserveSig]
- public static extern uint GetModuleFileName(
+ public static extern uint GetModuleFileNameW(
[In] IntPtr hModule,
[Out] StringBuilder lpFilename,
[In][MarshalAs(UnmanagedType.U4)] int nSize);
@@ -488,8 +696,28 @@ namespace Dalamud
/// If the function succeeds, the return value is a handle to the specified module. If the function fails, the return
/// value is NULL.To get extended error information, call GetLastError.
///
- [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
- public static extern IntPtr GetModuleHandle(string lpModuleName);
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
+ public static extern IntPtr GetModuleHandleW(string lpModuleName);
+
+ ///
+ /// Retrieves the address of an exported function or variable from the specified dynamic-link library (DLL).
+ ///
+ ///
+ /// A handle to the DLL module that contains the function or variable. The LoadLibrary, LoadLibraryEx, LoadPackagedLibrary,
+ /// or GetModuleHandle function returns this handle. The GetProcAddress function does not retrieve addresses from modules
+ /// that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx.
+ ///
+ ///
+ /// The function or variable name, or the function's ordinal value. If this parameter is an ordinal value, it must be
+ /// in the low-order word; the high-order word must be zero.
+ ///
+ ///
+ /// If the function succeeds, the return value is the address of the exported function or variable. If the function
+ /// fails, the return value is NULL.To get extended error information, call GetLastError.
+ ///
+ [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
+ [SuppressMessage("Globalization", "CA2101:Specify marshaling for P/Invoke string arguments", Justification = "Ansi only")]
+ public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
///
/// See https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryw.
@@ -512,8 +740,8 @@ namespace Dalamud
/// If the function succeeds, the return value is a handle to the module. If the function fails, the return value is
/// NULL.To get extended error information, call GetLastError.
///
- [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Ansi)]
- public static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)] string lpFileName);
+ [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
+ public static extern IntPtr LoadLibraryW([MarshalAs(UnmanagedType.LPWStr)] string lpFileName);
///
/// ReadProcessMemory copies the data in the specified address range from the address space of the specified process
@@ -551,6 +779,42 @@ namespace Dalamud
int dwSize,
out IntPtr lpNumberOfBytesRead);
+ ///
+ /// ReadProcessMemory copies the data in the specified address range from the address space of the specified process
+ /// into the specified buffer of the current process. Any process that has a handle with PROCESS_VM_READ access can
+ /// call the function. The entire area to be read must be accessible, and if it is not accessible, the function fails.
+ ///
+ ///
+ /// A handle to the process with memory that is being read. The handle must have PROCESS_VM_READ access to the process.
+ ///
+ ///
+ /// A pointer to the base address in the specified process from which to read. Before any data transfer occurs, the
+ /// system verifies that all data in the base address and memory of the specified size is accessible for read access,
+ /// and if it is not accessible the function fails.
+ ///
+ ///
+ /// A pointer to a buffer that receives the contents from the address space of the specified process.
+ ///
+ ///
+ /// The number of bytes to be read from the specified process.
+ ///
+ ///
+ /// A pointer to a variable that receives the number of bytes transferred into the specified buffer. If lpNumberOfBytesRead
+ /// is NULL, the parameter is ignored.
+ ///
+ ///
+ /// If the function succeeds, the return value is nonzero. If the function fails, the return value is 0 (zero). To get
+ /// extended error information, call GetLastError. The function fails if the requested read operation crosses into an
+ /// area of the process that is inaccessible.
+ ///
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern bool ReadProcessMemory(
+ IntPtr hProcess,
+ IntPtr lpBaseAddress,
+ byte[] lpBuffer,
+ int dwSize,
+ out IntPtr lpNumberOfBytesRead);
+
///
/// See https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-seterrormode.
/// Controls whether the system will handle the specified types of serious errors or whether the process will handle
@@ -585,6 +849,166 @@ namespace Dalamud
///
[DllImport("kernel32.dll")]
public static extern IntPtr SetUnhandledExceptionFilter(IntPtr lpTopLevelExceptionFilter);
+
+ ///
+ /// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc.
+ /// Reserves, commits, or changes the state of a region of pages in the virtual address space of the calling process.
+ /// Memory allocated by this function is automatically initialized to zero. To allocate memory in the address space
+ /// of another process, use the VirtualAllocEx function.
+ ///
+ ///
+ /// The starting address of the region to allocate. If the memory is being reserved, the specified address is rounded
+ /// down to the nearest multiple of the allocation granularity. If the memory is already reserved and is being committed,
+ /// the address is rounded down to the next page boundary. To determine the size of a page and the allocation granularity
+ /// on the host computer, use the GetSystemInfo function. If this parameter is NULL, the system determines where to
+ /// allocate the region. If this address is within an enclave that you have not initialized by calling InitializeEnclave,
+ /// VirtualAlloc allocates a page of zeros for the enclave at that address. The page must be previously uncommitted,
+ /// and will not be measured with the EEXTEND instruction of the Intel Software Guard Extensions programming model.
+ /// If the address in within an enclave that you initialized, then the allocation operation fails with the
+ /// ERROR_INVALID_ADDRESS error.
+ ///
+ ///
+ /// The size of the region, in bytes. If the lpAddress parameter is NULL, this value is rounded up to the next page
+ /// boundary. Otherwise, the allocated pages include all pages containing one or more bytes in the range from lpAddress
+ /// to lpAddress+dwSize. This means that a 2-byte range straddling a page boundary causes both pages to be included
+ /// in the allocated region.
+ ///
+ ///
+ /// The type of memory allocation. This parameter must contain one of the MEM_* enum values.
+ ///
+ ///
+ /// The memory protection for the region of pages to be allocated. If the pages are being committed, you can specify
+ /// any one of the memory protection constants.
+ ///
+ ///
+ /// If the function succeeds, the return value is the base address of the allocated region of pages. If the function
+ /// fails, the return value is NULL.To get extended error information, call GetLastError.
+ ///
+ [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
+ public static extern IntPtr VirtualAlloc(
+ IntPtr lpAddress,
+ UIntPtr dwSize,
+ AllocationType flAllocationType,
+ MemoryProtection flProtect);
+
+ ///
+ [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
+ public static extern IntPtr VirtualAlloc(
+ IntPtr lpAddress,
+ UIntPtr dwSize,
+ AllocationType flAllocationType,
+ Memory.MemoryProtection flProtect);
+
+ ///
+ /// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualfree.
+ /// Releases, decommits, or releases and decommits a region of pages within the virtual address space of the calling
+ /// process.
+ /// process.
+ ///
+ ///
+ /// A pointer to the base address of the region of pages to be freed. If the dwFreeType parameter is MEM_RELEASE, this
+ /// parameter must be the base address returned by the VirtualAlloc function when the region of pages is reserved.
+ ///
+ ///
+ /// The size of the region of memory to be freed, in bytes. If the dwFreeType parameter is MEM_RELEASE, this parameter
+ /// must be 0 (zero). The function frees the entire region that is reserved in the initial allocation call to VirtualAlloc.
+ /// If the dwFreeType parameter is MEM_DECOMMIT, the function decommits all memory pages that contain one or more bytes
+ /// in the range from the lpAddress parameter to (lpAddress+dwSize). This means, for example, that a 2-byte region of
+ /// memory that straddles a page boundary causes both pages to be decommitted.If lpAddress is the base address returned
+ /// by VirtualAlloc and dwSize is 0 (zero), the function decommits the entire region that is allocated by VirtualAlloc.
+ /// After that, the entire region is in the reserved state.
+ ///
+ ///
+ /// The type of free operation. This parameter must be one of the MEM_* enum values.
+ ///
+ ///
+ /// If the function succeeds, the return value is a nonzero value. If the function fails, the return value is 0 (zero).
+ /// To get extended error information, call GetLastError.
+ ///
+ [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
+ public static extern bool VirtualFree(
+ IntPtr lpAddress,
+ UIntPtr dwSize,
+ AllocationType dwFreeType);
+
+ ///
+ /// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect.
+ /// Changes the protection on a region of committed pages in the virtual address space of the calling process.
+ ///
+ ///
+ /// The address of the starting page of the region of pages whose access protection attributes are to be changed. All
+ /// pages in the specified region must be within the same reserved region allocated when calling the VirtualAlloc or
+ /// VirtualAllocEx function using MEM_RESERVE. The pages cannot span adjacent reserved regions that were allocated by
+ /// separate calls to VirtualAlloc or VirtualAllocEx using MEM_RESERVE.
+ ///
+ ///
+ /// The size of the region whose access protection attributes are to be changed, in bytes. The region of affected pages
+ /// includes all pages containing one or more bytes in the range from the lpAddress parameter to (lpAddress+dwSize).
+ /// This means that a 2-byte range straddling a page boundary causes the protection attributes of both pages to be changed.
+ ///
+ ///
+ /// The memory protection option. This parameter can be one of the memory protection constants. For mapped views, this
+ /// value must be compatible with the access protection specified when the view was mapped (see MapViewOfFile,
+ /// MapViewOfFileEx, and MapViewOfFileExNuma).
+ ///
+ ///
+ /// A pointer to a variable that receives the previous access protection value of the first page in the specified region
+ /// of pages. If this parameter is NULL or does not point to a valid variable, the function fails.
+ ///
+ ///
+ /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero.
+ /// To get extended error information, call GetLastError.
+ ///
+ [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
+ public static extern bool VirtualProtect(
+ IntPtr lpAddress,
+ UIntPtr dwSize,
+ MemoryProtection flNewProtection,
+ out MemoryProtection lpflOldProtect);
+
+ ///
+ [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
+ public static extern bool VirtualProtect(
+ IntPtr lpAddress,
+ UIntPtr dwSize,
+ Memory.MemoryProtection flNewProtection,
+ out Memory.MemoryProtection lpflOldProtect);
+
+ ///
+ /// Writes data to an area of memory in a specified process. The entire area to be written to must be accessible or
+ /// the operation fails.
+ ///
+ ///
+ /// A handle to the process memory to be modified. The handle must have PROCESS_VM_WRITE and PROCESS_VM_OPERATION access
+ /// to the process.
+ ///
+ ///
+ /// A pointer to the base address in the specified process to which data is written. Before data transfer occurs, the
+ /// system verifies that all data in the base address and memory of the specified size is accessible for write access,
+ /// and if it is not accessible, the function fails.
+ ///
+ ///
+ /// A pointer to the buffer that contains data to be written in the address space of the specified process.
+ ///
+ ///
+ /// The number of bytes to be written to the specified process.
+ ///
+ ///
+ /// A pointer to a variable that receives the number of bytes transferred into the specified process. This parameter
+ /// is optional. If lpNumberOfBytesWritten is NULL, the parameter is ignored.
+ ///
+ ///
+ /// If the function succeeds, the return value is nonzero. If the function fails, the return value is 0 (zero). To get
+ /// extended error information, call GetLastError.The function fails if the requested write operation crosses into an
+ /// area of the process that is inaccessible.
+ ///
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern bool WriteProcessMemory(
+ IntPtr hProcess,
+ IntPtr lpBaseAddress,
+ byte[] lpBuffer,
+ int dwSize,
+ out IntPtr lpNumberOfBytesWritten);
}
///
@@ -593,6 +1017,7 @@ namespace Dalamud
internal static partial class NativeFunctions
{
///
+ /// See https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-setsockopt.
/// The setsockopt function sets a socket option.
///
///
diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs
index a9ed0dc4a..9ab55d24c 100644
--- a/Dalamud/Plugin/DalamudPluginInterface.cs
+++ b/Dalamud/Plugin/DalamudPluginInterface.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Reflection;
using Dalamud.Configuration;
+using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState;
@@ -22,7 +23,7 @@ namespace Dalamud.Plugin
///
/// This class acts as an interface to various objects needed to interact with Dalamud and the game.
///
- public class DalamudPluginInterface : IDisposable
+ public sealed class DalamudPluginInterface : IDisposable
{
private readonly Dalamud dalamud;
private readonly string pluginName;
@@ -34,11 +35,10 @@ namespace Dalamud.Plugin
///
/// The dalamud instance to expose.
/// The internal name of the plugin.
- /// The plugin configurations handler.
- /// The reason this plugin was loaded.
- internal DalamudPluginInterface(Dalamud dalamud, string pluginName, PluginConfigurations configs, PluginLoadReason reason)
+ /// The equivalent of what Assembly.GetExecutingAssembly().Location should return.
+ /// The reason the plugin was loaded.
+ internal DalamudPluginInterface(Dalamud dalamud, string pluginName, string assemblyLocation, PluginLoadReason reason)
{
- this.Reason = reason;
this.CommandManager = dalamud.CommandManager;
this.Framework = dalamud.Framework;
this.ClientState = dalamud.ClientState;
@@ -49,7 +49,9 @@ namespace Dalamud.Plugin
this.dalamud = dalamud;
this.pluginName = pluginName;
- this.configs = configs;
+ this.configs = dalamud.PluginManager.PluginConfigs;
+ this.AssemblyLocation = assemblyLocation;
+ this.Reason = reason;
this.GeneralChatType = this.dalamud.Configuration.GeneralChatType;
this.Sanitizer = new Sanitizer(this.Data.Language);
@@ -60,7 +62,7 @@ namespace Dalamud.Plugin
else
{
var currentUiLang = CultureInfo.CurrentUICulture;
- if (Localization.ApplicableLangCodes.Any(x => currentUiLang.TwoLetterISOLanguageName == x))
+ if (Localization.ApplicableLangCodes.Any(langCode => currentUiLang.TwoLetterISOLanguageName == langCode))
this.UiLanguage = currentUiLang.TwoLetterISOLanguageName;
else
this.UiLanguage = "en";
@@ -86,6 +88,11 @@ namespace Dalamud.Plugin
///
public PluginLoadReason Reason { get; }
+ ///
+ /// Gets the plugin assembly location.
+ ///
+ public string AssemblyLocation { get; private set; }
+
///
/// Gets the directory Dalamud assets are stored in.
///
@@ -142,7 +149,7 @@ namespace Dalamud.Plugin
#if DEBUG
public bool IsDebugging => true;
#else
- public bool IsDebugging => this.dalamud.DalamudUi.IsDevMenu;
+ public bool IsDebugging => this.dalamud.DalamudUi.IsDevMenuOpen;
#endif
///
@@ -192,7 +199,7 @@ namespace Dalamud.Plugin
// This is here for now to support the current plugin API
foreach (var type in Assembly.GetCallingAssembly().GetTypes())
{
- if (type.GetInterface(typeof(IPluginConfiguration).FullName) != null)
+ if (type.IsAssignableTo(typeof(IPluginConfiguration)))
{
var mi = this.configs.GetType().GetMethod("LoadForType");
var fn = mi.MakeGenericMethod(type);
@@ -273,7 +280,7 @@ namespace Dalamud.Plugin
if (this.dalamud.PluginManager.IpcSubscriptions.Any(x => x.SourcePluginName == this.pluginName && x.SubPluginName == pluginName))
throw new InvalidOperationException("Can't add multiple subscriptions for the same plugin.");
- this.dalamud.PluginManager.IpcSubscriptions.Add((this.pluginName, pluginName, action));
+ this.dalamud.PluginManager.IpcSubscriptions.Add(new(this.pluginName, pluginName, action));
}
///
@@ -325,12 +332,16 @@ namespace Dalamud.Plugin
[Obsolete("The current IPC mechanism is obsolete and scheduled to be replaced after API level 3.")]
public bool SendMessage(string pluginName, ExpandoObject message)
{
- var (_, _, pluginInterface, _) = this.dalamud.PluginManager.Plugins.FirstOrDefault(x => x.Definition.InternalName == pluginName);
+ var plugin = this.dalamud.PluginManager.InstalledPlugins.FirstOrDefault(x => x.Manifest.InternalName == pluginName);
- if (pluginInterface?.AnyPluginIpcAction == null)
+ if (plugin == default)
return false;
- pluginInterface.AnyPluginIpcAction.Invoke(this.pluginName, message);
+ if (plugin.DalamudInterface?.AnyPluginIpcAction == null)
+ return false;
+
+ plugin.DalamudInterface.AnyPluginIpcAction.Invoke(this.pluginName, message);
+
return true;
}
@@ -343,22 +354,16 @@ namespace Dalamud.Plugin
///
/// The message template.
/// Values to log.
- [Obsolete]
- public void Log(string messageTemplate, params object[] values)
- {
- Serilog.Log.Information(messageTemplate, values);
- }
+ [Obsolete("Use PluginLog")]
+ public void Log(string messageTemplate, params object[] values) => Serilog.Log.Information(messageTemplate, values);
///
/// Log a templated error message to the in-game debug log.
///
/// The message template.
/// Values to log.
- [Obsolete]
- public void LogError(string messageTemplate, params object[] values)
- {
- Serilog.Log.Error(messageTemplate, values);
- }
+ [Obsolete("Use PluginLog")]
+ public void LogError(string messageTemplate, params object[] values) => Serilog.Log.Error(messageTemplate, values);
///
/// Log a templated error message to the in-game debug log.
@@ -366,11 +371,8 @@ namespace Dalamud.Plugin
/// The exception that caused the error.
/// The message template.
/// Values to log.
- [Obsolete]
- public void LogError(Exception exception, string messageTemplate, params object[] values)
- {
- Serilog.Log.Error(exception, messageTemplate, values);
- }
+ [Obsolete("Use PluginLog")]
+ public void LogError(Exception exception, string messageTemplate, params object[] values) => Serilog.Log.Error(exception, messageTemplate, values);
#endregion
diff --git a/Dalamud/Plugin/Internal/Exceptions/DuplicatePluginException.cs b/Dalamud/Plugin/Internal/Exceptions/DuplicatePluginException.cs
new file mode 100644
index 000000000..093f97e69
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Exceptions/DuplicatePluginException.cs
@@ -0,0 +1,26 @@
+namespace Dalamud.Plugin.Internal.Exceptions
+{
+ ///
+ /// This exception that is thrown when a plugin is instructed to load while another plugin with the same
+ /// assembly name is already present and loaded.
+ ///
+ internal class DuplicatePluginException : PluginException
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Name of the conflicting assembly.
+ public DuplicatePluginException(string assemblyName)
+ {
+ this.AssemblyName = assemblyName;
+ }
+
+ ///
+ /// Gets the name of the conflicting assembly.
+ ///
+ public string AssemblyName { get; init; }
+
+ ///
+ public override string Message => $"A plugin with the same assembly name of {this.AssemblyName} is already loaded";
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Exceptions/InvalidPluginException.cs b/Dalamud/Plugin/Internal/Exceptions/InvalidPluginException.cs
new file mode 100644
index 000000000..6b5c8920a
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Exceptions/InvalidPluginException.cs
@@ -0,0 +1,25 @@
+using System;
+using System.IO;
+
+namespace Dalamud.Plugin.Internal.Exceptions
+{
+ ///
+ /// This exception represents a file that does not implement IDalamudPlugin.
+ ///
+ internal class InvalidPluginException : PluginException
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The invalid file.
+ public InvalidPluginException(FileInfo dllFile)
+ {
+ this.DllFile = dllFile;
+ }
+
+ ///
+ /// Gets the invalid file.
+ ///
+ public FileInfo DllFile { get; init; }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Exceptions/InvalidPluginOperationException.cs b/Dalamud/Plugin/Internal/Exceptions/InvalidPluginOperationException.cs
new file mode 100644
index 000000000..a80d6d51d
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Exceptions/InvalidPluginOperationException.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace Dalamud.Plugin.Internal.Exceptions
+{
+ ///
+ /// This represents an invalid plugin operation.
+ ///
+ internal class InvalidPluginOperationException : PluginException
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message describing the invalid operation.
+ public InvalidPluginOperationException(string message)
+ {
+ this.Message = message;
+ }
+
+ ///
+ /// Gets the message describing the invalid operation.
+ ///
+ public override string Message { get; }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Exceptions/PluginException.cs b/Dalamud/Plugin/Internal/Exceptions/PluginException.cs
new file mode 100644
index 000000000..e4b17b686
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Exceptions/PluginException.cs
@@ -0,0 +1,11 @@
+using System;
+
+namespace Dalamud.Plugin.Internal.Exceptions
+{
+ ///
+ /// This represents the base Dalamud plugin exception.
+ ///
+ internal abstract class PluginException : Exception
+ {
+ }
+}
diff --git a/Dalamud/Plugin/Internal/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/LocalDevPlugin.cs
new file mode 100644
index 000000000..77b3be253
--- /dev/null
+++ b/Dalamud/Plugin/Internal/LocalDevPlugin.cs
@@ -0,0 +1,151 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Dalamud.Configuration.Internal;
+using Dalamud.Plugin.Internal.Types;
+
+namespace Dalamud.Plugin.Internal
+{
+ ///
+ /// This class represents a dev plugin and all facets of its lifecycle.
+ /// The DLL on disk, dependencies, loaded assembly, etc.
+ ///
+ internal class LocalDevPlugin : LocalPlugin, IDisposable
+ {
+ private static readonly ModuleLog Log = new("PLUGIN");
+
+ // Ref to Dalamud.Configuration.DevPluginSettings
+ private readonly DevPluginSettings devSettings;
+
+ private FileSystemWatcher fileWatcher;
+ private CancellationTokenSource fileWatcherTokenSource;
+ private int reloadCounter;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Dalamud instance.
+ /// Path to the DLL file.
+ /// The plugin manifest.
+ public LocalDevPlugin(Dalamud dalamud, FileInfo dllFile, LocalPluginManifest manifest)
+ : base(dalamud, dllFile, manifest)
+ {
+ if (!dalamud.Configuration.DevPluginSettings.TryGetValue(dllFile.FullName, out this.devSettings))
+ {
+ dalamud.Configuration.DevPluginSettings[dllFile.FullName] = this.devSettings = new DevPluginSettings();
+ dalamud.Configuration.Save();
+ }
+
+ if (this.AutomaticReload)
+ {
+ this.EnableReloading();
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this dev plugin should start on boot.
+ ///
+ public bool StartOnBoot
+ {
+ get => this.devSettings.StartOnBoot;
+ set => this.devSettings.StartOnBoot = value;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this dev plugin should reload on change.
+ ///
+ public bool AutomaticReload
+ {
+ get => this.devSettings.AutomaticReloading;
+ set
+ {
+ this.devSettings.AutomaticReloading = value;
+
+ if (this.devSettings.AutomaticReloading)
+ {
+ this.EnableReloading();
+ }
+ else
+ {
+ this.DisableReloading();
+ }
+ }
+ }
+
+ ///
+ public new void Dispose()
+ {
+ if (this.fileWatcher != null)
+ {
+ this.fileWatcher.Changed -= this.OnFileChanged;
+ this.fileWatcherTokenSource.Cancel();
+ this.fileWatcher.Dispose();
+ }
+
+ base.Dispose();
+ }
+
+ ///
+ /// Configure this plugin for automatic reloading and enable it.
+ ///
+ public void EnableReloading()
+ {
+ if (this.fileWatcher == null)
+ {
+ this.fileWatcherTokenSource = new();
+ this.fileWatcher = new FileSystemWatcher(this.DllFile.DirectoryName);
+ this.fileWatcher.Changed += this.OnFileChanged;
+ this.fileWatcher.Filter = this.DllFile.Name;
+ this.fileWatcher.NotifyFilter = NotifyFilters.LastWrite;
+ this.fileWatcher.EnableRaisingEvents = true;
+ }
+ }
+
+ ///
+ /// Disable automatic reloading for this plugin.
+ ///
+ public void DisableReloading()
+ {
+ if (this.fileWatcher != null)
+ {
+ this.fileWatcherTokenSource.Cancel();
+ this.fileWatcher.Changed -= this.OnFileChanged;
+ this.fileWatcher.Dispose();
+ this.fileWatcher = null;
+ }
+ }
+
+ private void OnFileChanged(object sender, FileSystemEventArgs args)
+ {
+ var current = Interlocked.Increment(ref this.reloadCounter);
+
+ Task.Delay(500).ContinueWith(
+ task =>
+ {
+ if (this.fileWatcherTokenSource.IsCancellationRequested)
+ {
+ Log.Debug($"Skipping reload of {this.Name}, file watcher was cancelled.");
+ return;
+ }
+
+ if (current != this.reloadCounter)
+ {
+ Log.Debug($"Skipping reload of {this.Name}, file has changed again.");
+ return;
+ }
+
+ if (this.State != PluginState.Loaded)
+ {
+ Log.Debug($"Skipping reload of {this.Name}, state ({this.State}) is not {PluginState.Loaded}.");
+ return;
+ }
+
+ this.Reload();
+ },
+ this.fileWatcherTokenSource.Token);
+ }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/LocalPlugin.cs b/Dalamud/Plugin/Internal/LocalPlugin.cs
new file mode 100644
index 000000000..f7bbe9615
--- /dev/null
+++ b/Dalamud/Plugin/Internal/LocalPlugin.cs
@@ -0,0 +1,414 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+
+using Dalamud.Configuration.Internal;
+using Dalamud.Game;
+using Dalamud.Plugin.Internal.Exceptions;
+using Dalamud.Plugin.Internal.Types;
+using McMaster.NETCore.Plugins;
+
+namespace Dalamud.Plugin.Internal
+{
+ ///
+ /// This class represents a plugin and all facets of its lifecycle.
+ /// The DLL on disk, dependencies, loaded assembly, etc.
+ ///
+ internal class LocalPlugin : IDisposable
+ {
+ private static readonly ModuleLog Log = new("PLUGIN");
+
+ private readonly Dalamud dalamud;
+ private readonly FileInfo manifestFile;
+ private readonly FileInfo disabledFile;
+ private readonly FileInfo testingFile;
+
+ private PluginLoader loader;
+ private Assembly pluginAssembly;
+ private Type pluginType;
+ private IDalamudPlugin instance;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Dalamud instance.
+ /// Path to the DLL file.
+ /// The plugin manifest.
+ public LocalPlugin(Dalamud dalamud, FileInfo dllFile, LocalPluginManifest manifest)
+ {
+ this.dalamud = dalamud;
+ this.DllFile = dllFile;
+ this.State = PluginState.Unloaded;
+
+ this.loader ??= PluginLoader.CreateFromAssemblyFile(
+ this.DllFile.FullName,
+ config =>
+ {
+ config.IsUnloadable = true;
+ config.LoadInMemory = true;
+ config.PreferSharedTypes = true;
+ });
+
+ Version assemblyVersion;
+
+ try
+ {
+ // BadImageFormatException
+ this.pluginAssembly ??= this.loader.LoadDefaultAssembly();
+
+ // InvalidOperationException
+ this.pluginType = this.pluginAssembly.GetTypes().First(type => type.IsAssignableTo(typeof(IDalamudPlugin)));
+
+ assemblyVersion = this.pluginAssembly.GetName().Version;
+ }
+ catch (Exception)
+ {
+ this.pluginAssembly = null;
+ this.pluginType = null;
+ this.loader.Dispose();
+
+ Log.Debug($"Not a plugin: {this.DllFile.Name}");
+ throw new InvalidPluginException(this.DllFile);
+ }
+
+ // Files that may or may not exist
+ this.manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile);
+ this.disabledFile = LocalPluginManifest.GetDisabledFile(this.DllFile);
+ this.testingFile = LocalPluginManifest.GetTestingFile(this.DllFile);
+
+ // If the parameter manifest was null
+ if (manifest == null)
+ {
+ this.Manifest = new LocalPluginManifest()
+ {
+ Author = "developer",
+ Name = Path.GetFileNameWithoutExtension(this.DllFile.Name),
+ InternalName = Path.GetFileNameWithoutExtension(this.DllFile.Name),
+ AssemblyVersion = assemblyVersion,
+ Description = string.Empty,
+ ApplicableVersion = GameVersion.Any,
+ DalamudApiLevel = PluginManager.DalamudApiLevel,
+ IsHide = false,
+ };
+
+ // Save the manifest to disk so there won't be any problems later.
+ // We'll update the name property after it can be retrieved from the instance.
+ var manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile);
+ this.Manifest.Save(manifestFile);
+ }
+ else
+ {
+ this.Manifest = manifest;
+ }
+
+ // This bit converts from ".disabled" functionality to using the manifest.
+ if (this.disabledFile.Exists)
+ {
+ this.Manifest.Disabled = true;
+ this.disabledFile.Delete();
+ }
+
+ // This bit converts from ".testing" functionality to using the manifest.
+ if (this.testingFile.Exists)
+ {
+ this.Manifest.Testing = true;
+ this.testingFile.Delete();
+ }
+
+ this.SaveManifest();
+ }
+
+ ///
+ /// Gets the associated with this plugin.
+ ///
+ public DalamudPluginInterface DalamudInterface { get; private set; }
+
+ ///
+ /// Gets the path to the plugin DLL.
+ ///
+ public FileInfo DllFile { get; }
+
+ ///
+ /// Gets the plugin manifest, if one exists.
+ ///
+ public LocalPluginManifest Manifest { get; }
+
+ ///
+ /// Gets or sets the current state of the plugin.
+ ///
+ public PluginState State { get; protected set; } = PluginState.Unloaded;
+
+ ///
+ /// Gets the AssemblyName plugin, populated during .
+ ///
+ /// Plugin type.
+ public AssemblyName AssemblyName { get; private set; } = null;
+
+ ///
+ /// Gets the plugin name, directly from the plugin or if it is not loaded from the manifest.
+ ///
+ public string Name => this.instance?.Name ?? this.Manifest.Name ?? this.DllFile.Name;
+
+ ///
+ /// Gets a value indicating whether the plugin is loaded and running.
+ ///
+ public bool IsLoaded => this.State == PluginState.Loaded;
+
+ ///
+ /// Gets a value indicating whether the plugin is disabled.
+ ///
+ public bool IsDisabled => this.Manifest.Disabled;
+
+ ///
+ /// Gets a value indicating whether the plugin is for testing use only.
+ ///
+ public bool IsTesting => this.Manifest.IsTestingExclusive || this.Manifest.Testing;
+
+ ///
+ /// Gets a value indicating whether this plugin is dev plugin.
+ ///
+ public bool IsDev => this is LocalDevPlugin;
+
+ ///
+ public void Dispose()
+ {
+ this.instance?.Dispose();
+ this.instance = null;
+
+ this.DalamudInterface.Dispose();
+ this.DalamudInterface = null;
+
+ this.pluginType = null;
+ this.pluginAssembly = null;
+
+ this.loader?.Dispose();
+ }
+
+ ///
+ /// Load this plugin.
+ ///
+ /// The reason why this plugin is being loaded.
+ /// Load while reloading.
+ public void Load(PluginLoadReason reason, bool reloading = false)
+ {
+ // Allowed: Unloaded
+ switch (this.State)
+ {
+ case PluginState.InProgress:
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, already working");
+ case PluginState.Loaded:
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, already loaded");
+ case PluginState.LoadError:
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, load previously faulted, unload first");
+ case PluginState.UnloadError:
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, unload previously faulted, restart Dalamud");
+ }
+
+ if (this.Manifest.ApplicableVersion < this.dalamud.StartInfo.GameVersion)
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version");
+
+ if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !this.dalamud.Configuration.LoadAllApiLevels)
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level");
+
+ if (this.Manifest.Disabled)
+ throw new InvalidPluginOperationException($"Unable to load {this.Name}, disabled");
+
+ this.State = PluginState.InProgress;
+ Log.Information($"Loading {this.DllFile.Name}");
+
+ try
+ {
+ this.loader ??= PluginLoader.CreateFromAssemblyFile(
+ this.DllFile.FullName,
+ config =>
+ {
+ config.IsUnloadable = true;
+ config.LoadInMemory = true;
+ config.PreferSharedTypes = true;
+ });
+
+ if (reloading)
+ {
+ this.loader.Reload();
+ }
+
+ // Load the assembly
+ this.pluginAssembly ??= this.loader.LoadDefaultAssembly();
+
+ this.AssemblyName = this.pluginAssembly.GetName();
+
+ // Find the plugin interface implementation. It is guaranteed to exist after checking in the ctor.
+ this.pluginType ??= this.pluginAssembly.GetTypes().First(type => type.IsAssignableTo(typeof(IDalamudPlugin)));
+
+ // Check for any loaded plugins with the same assembly name
+ var assemblyName = this.pluginAssembly.GetName().Name;
+ foreach (var otherPlugin in this.dalamud.PluginManager.InstalledPlugins)
+ {
+ // During hot-reloading, this plugin will be in the plugin list, and the instance will have been disposed
+ if (otherPlugin == this || otherPlugin.instance == null)
+ continue;
+
+ var otherPluginAssemblyName = otherPlugin.instance.GetType().Assembly.GetName().Name;
+ if (otherPluginAssemblyName == assemblyName)
+ {
+ this.State = PluginState.Unloaded;
+ Log.Debug($"Duplicate assembly: {this.Name}");
+
+ throw new DuplicatePluginException(assemblyName);
+ }
+ }
+
+ // Update the location for the Location and CodeBase patches
+ PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new(this.DllFile);
+
+ // Instantiate and initialize
+ this.instance = Activator.CreateInstance(this.pluginType) as IDalamudPlugin;
+
+ // In-case the manifest name was a placeholder. Can occur when no manifest was included.
+ if (this.instance.Name != this.Manifest.Name)
+ {
+ this.Manifest.Name = this.instance.Name;
+ this.Manifest.Save(this.manifestFile);
+ }
+
+ this.DalamudInterface = new DalamudPluginInterface(this.dalamud, this.pluginAssembly.GetName().Name, this.DllFile.FullName, reason);
+
+ if (this.IsDev)
+ {
+ // Inherit LPL's AssemblyLocation functionality
+ try
+ {
+ var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
+
+ this.instance.GetType()
+ ?.GetProperty("AssemblyLocation", bindingFlags)
+ ?.SetValue(this.instance, this.DllFile.FullName);
+ this.instance.GetType()
+ ?.GetMethod("SetLocation", bindingFlags)
+ ?.Invoke(this.instance, new object[] { this.DllFile.FullName });
+ }
+ catch
+ {
+ // Ignored
+ }
+ }
+
+ this.instance.Initialize(this.DalamudInterface);
+
+ this.State = PluginState.Loaded;
+ Log.Information($"Finished loading {this.DllFile.Name}");
+ }
+ catch (Exception ex)
+ {
+ this.State = PluginState.LoadError;
+ Log.Error(ex, $"Error while loading {this.Name}");
+
+ throw;
+ }
+ }
+
+ ///
+ /// Unload this plugin. This is the same as dispose, but without the "disposed" connotations. This object should stay
+ /// in the plugin list until it has been actually disposed.
+ ///
+ /// Unload while reloading.
+ public void Unload(bool reloading = false)
+ {
+ // Allowed: Loaded, LoadError(we are cleaning this up while we're at it)
+ switch (this.State)
+ {
+ case PluginState.InProgress:
+ throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already working");
+ case PluginState.Unloaded:
+ throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already unloaded");
+ case PluginState.UnloadError:
+ throw new InvalidPluginOperationException($"Unable to unload {this.Name}, unload previously faulted, restart Dalamud");
+ }
+
+ try
+ {
+ this.State = PluginState.InProgress;
+ Log.Information($"Unloading {this.DllFile.Name}");
+
+ this.instance?.Dispose();
+ this.instance = null;
+
+ this.DalamudInterface?.Dispose();
+ this.DalamudInterface = null;
+
+ this.pluginType = null;
+ this.pluginAssembly = null;
+
+ if (!reloading)
+ {
+ this.loader?.Dispose();
+ this.loader = null;
+ }
+
+ this.State = PluginState.Unloaded;
+ Log.Information($"Finished unloading {this.DllFile.Name}");
+ }
+ catch (Exception ex)
+ {
+ this.State = PluginState.UnloadError;
+ Log.Error(ex, $"Error while unloading {this.Name}");
+
+ throw;
+ }
+ }
+
+ ///
+ /// Reload this plugin.
+ ///
+ public void Reload()
+ {
+ this.Unload(true);
+ this.Load(PluginLoadReason.Reload, true);
+ }
+
+ ///
+ /// Revert a disable. Must be unloaded first, does not load.
+ ///
+ public void Enable()
+ {
+ // Allowed: Unloaded, UnloadError
+ switch (this.State)
+ {
+ case PluginState.InProgress:
+ case PluginState.Loaded:
+ case PluginState.LoadError:
+ throw new InvalidPluginOperationException($"Unable to enable {this.Name}, still loaded");
+ }
+
+ if (!this.Manifest.Disabled)
+ throw new InvalidPluginOperationException($"Unable to enable {this.Name}, not disabled");
+
+ this.Manifest.Disabled = false;
+ this.SaveManifest();
+ }
+
+ ///
+ /// Disable this plugin, must be unloaded first.
+ ///
+ public void Disable()
+ {
+ // Allowed: Unloaded, UnloadError
+ switch (this.State)
+ {
+ case PluginState.InProgress:
+ case PluginState.Loaded:
+ case PluginState.LoadError:
+ throw new InvalidPluginOperationException($"Unable to disable {this.Name}, still loaded");
+ }
+
+ if (this.Manifest.Disabled)
+ throw new InvalidPluginOperationException($"Unable to disable {this.Name}, already disabled");
+
+ this.Manifest.Disabled = true;
+ this.SaveManifest();
+ }
+
+ private void SaveManifest() => this.Manifest.Save(this.manifestFile);
+ }
+}
diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs
new file mode 100644
index 000000000..5f1165742
--- /dev/null
+++ b/Dalamud/Plugin/Internal/PluginManager.cs
@@ -0,0 +1,1088 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Net;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+
+using CheapLoc;
+using Dalamud.Configuration;
+using Dalamud.Game.Text;
+using Dalamud.Plugin.Internal.Exceptions;
+using Dalamud.Plugin.Internal.Types;
+using HarmonyLib;
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+
+namespace Dalamud.Plugin.Internal
+{
+ ///
+ /// Class responsible for loading and unloading plugins.
+ ///
+ internal partial class PluginManager : IDisposable
+ {
+ ///
+ /// The current Dalamud API level, used to handle breaking changes. Only plugins with this level will be loaded.
+ ///
+ public const int DalamudApiLevel = 4;
+
+ private static readonly ModuleLog Log = new("PLUGINM");
+
+ private readonly Dalamud dalamud;
+ private readonly DirectoryInfo pluginDirectory;
+ private readonly DirectoryInfo devPluginDirectory;
+ private readonly BannedPlugin[] bannedPlugins;
+
+ private readonly List installedPlugins = new();
+ private List availablePlugins = new();
+ private List updatablePlugins = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The instance to load plugins with.
+ public PluginManager(Dalamud dalamud)
+ {
+ this.dalamud = dalamud;
+ this.pluginDirectory = new DirectoryInfo(dalamud.StartInfo.PluginDirectory);
+ this.devPluginDirectory = new DirectoryInfo(dalamud.StartInfo.DefaultPluginDirectory);
+
+ if (!this.pluginDirectory.Exists)
+ this.pluginDirectory.Create();
+
+ if (!this.devPluginDirectory.Exists)
+ this.devPluginDirectory.Create();
+
+ this.PluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(dalamud.StartInfo.ConfigurationPath), "pluginConfigs"));
+
+ var bannedPluginsJson = File.ReadAllText(Path.Combine(this.dalamud.StartInfo.AssetDirectory, "UIRes", "bannedplugin.json"));
+ this.bannedPlugins = JsonConvert.DeserializeObject(bannedPluginsJson);
+
+ this.SetPluginReposFromConfig(false);
+
+ this.ApplyPatches();
+ }
+
+ ///
+ /// An event that fires when the installed plugins have changed.
+ ///
+ public event Action OnInstalledPluginsChanged;
+
+ ///
+ /// An event that fires when the available plugins have changed.
+ ///
+ public event Action OnAvailablePluginsChanged;
+
+ ///
+ /// Gets a list of all loaded plugins.
+ ///
+ public ImmutableList InstalledPlugins { get; private set; } = ImmutableList.Create();
+
+ ///
+ /// Gets a list of all available plugins.
+ ///
+ public ImmutableList AvailablePlugins { get; private set; } = ImmutableList.Create();
+
+ ///
+ /// Gets a list of all plugins with an available update.
+ ///
+ public ImmutableList UpdatablePlugins { get; private set; } = ImmutableList.Create();
+
+ ///
+ /// Gets a list of all plugin repositories. The main repo should always be first.
+ ///
+ public List Repos { get; private set; } = new();
+
+ ///
+ /// Gets a value indicating whether plugins are not still loading from boot.
+ ///
+ public bool PluginsReady { get; private set; } = false;
+
+ ///
+ /// Gets a value indicating whether all added repos are not in progress.
+ ///
+ public bool ReposReady => this.Repos.All(repo => repo.State != PluginRepositoryState.InProgress);
+
+ ///
+ /// Gets a list of all IPC subscriptions.
+ ///
+ public List IpcSubscriptions { get; } = new();
+
+ ///
+ /// Gets the object used when initializing plugins.
+ ///
+ public PluginConfigurations PluginConfigs { get; }
+
+ ///
+ public void Dispose()
+ {
+ foreach (var plugin in this.installedPlugins.ToArray())
+ {
+ try
+ {
+ plugin.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Error disposing {plugin.Name}");
+ }
+ }
+ }
+
+ ///
+ /// Set the list of repositories to use. Should be called when the Settings window has been updated or at instantiation.
+ ///
+ /// Whether the available plugins changed should be evented after.
+ public void SetPluginReposFromConfig(bool notify)
+ {
+ var repos = new List() { PluginRepository.MainRepo };
+ repos.AddRange(this.dalamud.Configuration.ThirdRepoList
+ .Select(repo => new PluginRepository(repo.Url, repo.IsEnabled)));
+
+ this.Repos = repos;
+
+ if (notify)
+ this.NotifyAvailablePluginsChanged();
+ }
+
+ ///
+ /// Load all plugins, sorted by priority. Any plugins with no explicit definition file or a negative priority
+ /// are loaded asynchronously. Should only be called during Dalamud startup.
+ ///
+ public void LoadAllPlugins()
+ {
+ var pluginDefs = new List();
+ var devPluginDefs = new List();
+
+ if (!this.pluginDirectory.Exists)
+ this.pluginDirectory.Create();
+
+ if (!this.devPluginDirectory.Exists)
+ this.devPluginDirectory.Create();
+
+ // Add installed plugins. These are expected to be in a specific format so we can look for exactly that.
+ foreach (var pluginDir in this.pluginDirectory.GetDirectories())
+ {
+ foreach (var versionDir in pluginDir.GetDirectories())
+ {
+ var dllFile = new FileInfo(Path.Combine(versionDir.FullName, $"{pluginDir.Name}.dll"));
+ var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
+
+ if (!manifestFile.Exists)
+ continue;
+
+ var manifest = LocalPluginManifest.Load(manifestFile);
+
+ pluginDefs.Add(new(dllFile, manifest, false));
+ }
+ }
+
+ // devPlugins are more freeform. Look for any dll and hope to get lucky.
+ var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories);
+
+ foreach (var dllFile in devDllFiles)
+ {
+ // Manifests are not required for devPlugins. the Plugin type will handle any null manifests.
+ var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
+ var manifest = manifestFile.Exists ? LocalPluginManifest.Load(manifestFile) : null;
+ devPluginDefs.Add(new(dllFile, manifest, true));
+ }
+
+ // Sort for load order - unloaded definitions have default priority of 0
+ pluginDefs.Sort(PluginDef.Sorter);
+ devPluginDefs.Sort(PluginDef.Sorter);
+
+ // Dev plugins should load first.
+ pluginDefs.InsertRange(0, devPluginDefs);
+
+ void LoadPlugins(IEnumerable pluginDefs)
+ {
+ foreach (var pluginDef in pluginDefs)
+ {
+ try
+ {
+ this.LoadPlugin(pluginDef.DllFile, pluginDef.Manifest, PluginLoadReason.Boot, pluginDef.IsDev, isBoot: true);
+ }
+ catch (InvalidPluginException)
+ {
+ // Not a plugin
+ }
+ catch (Exception)
+ {
+ Log.Error("During boot plugin load, an unexpected error occurred");
+ }
+ }
+ }
+
+ // Load sync plugins
+ var syncPlugins = pluginDefs.Where(def => def.Manifest?.LoadPriority > 0);
+ LoadPlugins(syncPlugins);
+
+ var asyncPlugins = pluginDefs.Where(def => def.Manifest == null || def.Manifest.LoadPriority <= 0);
+ Task.Run(() => LoadPlugins(asyncPlugins))
+ .ContinueWith(task => this.PluginsReady = true)
+ .ContinueWith(task => this.NotifyInstalledPluginsChanged());
+ }
+
+ ///
+ /// Reload all loaded plugins.
+ ///
+ public void ReloadAllPlugins()
+ {
+ var aggregate = new List();
+
+ for (var i = 0; i < this.installedPlugins.Count; i++)
+ {
+ var plugin = this.installedPlugins[i];
+
+ if (plugin.IsLoaded)
+ {
+ try
+ {
+ plugin.Reload();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error during reload all");
+
+ aggregate.Add(ex);
+ }
+ }
+ }
+
+ if (aggregate.Any())
+ {
+ throw new AggregateException(aggregate);
+ }
+ }
+
+ ///
+ /// Reload the PluginMaster for each repo, filter, and event that the list has updated.
+ ///
+ public void ReloadPluginMasters()
+ {
+ Task.WhenAll(this.Repos.Select(repo => repo.ReloadPluginMasterAsync()))
+ .ContinueWith(task => this.RefilterPluginMasters())
+ .Wait();
+ }
+
+ ///
+ /// Apply visibility and eligibility filters to the available plugins, then event that the list has updated.
+ ///
+ public void RefilterPluginMasters()
+ {
+ this.availablePlugins = this.dalamud.PluginManager.Repos
+ .SelectMany(repo => repo.PluginMaster)
+ .Where(this.IsManifestEligible)
+ .Where(this.IsManifestVisible)
+ .ToList();
+
+ this.NotifyAvailablePluginsChanged();
+ }
+
+ ///
+ /// Scan the devPlugins folder for new DLL files that are not already loaded into the manager. They are not loaded,
+ /// only shown as disabled in the installed plugins window. This is a modified version of LoadAllPlugins that works
+ /// a little differently.
+ ///
+ public void ScanDevPlugins()
+ {
+ if (!this.devPluginDirectory.Exists)
+ this.devPluginDirectory.Create();
+
+ // devPlugins are more freeform. Look for any dll and hope to get lucky.
+ var devDllFiles = this.devPluginDirectory.GetFiles("*.dll", SearchOption.AllDirectories);
+
+ var listChanged = false;
+
+ foreach (var dllFile in devDllFiles)
+ {
+ // This file is already known to us
+ if (this.InstalledPlugins.Any(lp => lp.DllFile.FullName == dllFile.FullName))
+ continue;
+
+ // Manifests are not required for devPlugins. the Plugin type will handle any null manifests.
+ var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
+ var manifest = manifestFile.Exists ? LocalPluginManifest.Load(manifestFile) : null;
+
+ try
+ {
+ // Add them to the list and let the user decide, nothing is auto-loaded.
+ this.LoadPlugin(dllFile, manifest, PluginLoadReason.Installer, isDev: true, doNotLoad: true);
+ listChanged = true;
+ }
+ catch (InvalidPluginException)
+ {
+ // Not a plugin
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"During devPlugin scan, an unexpected error occurred");
+ }
+ }
+
+ if (listChanged)
+ this.NotifyInstalledPluginsChanged();
+ }
+
+ ///
+ /// Install a plugin from a repository and load it.
+ ///
+ /// The plugin definition.
+ /// If the testing version should be used.
+ /// The reason this plugin was loaded.
+ public void InstallPlugin(RemotePluginManifest repoManifest, bool useTesting, PluginLoadReason reason)
+ {
+ Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting})");
+
+ var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall;
+ var version = useTesting ? repoManifest.TestingAssemblyVersion : repoManifest.AssemblyVersion;
+
+ var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory.FullName, repoManifest.InternalName, version.ToString()));
+
+ try
+ {
+ if (outputDir.Exists)
+ outputDir.Delete(true);
+
+ outputDir.Create();
+ }
+ catch
+ {
+ // ignored, since the plugin may be loaded already
+ }
+
+ using var client = new WebClient();
+
+ var tempZip = new FileInfo(Path.GetTempFileName());
+
+ try
+ {
+ Log.Debug($"Downloading plugin to {tempZip} from {downloadUrl}");
+ client.DownloadFile(downloadUrl, tempZip.FullName);
+ }
+ catch (WebException ex)
+ {
+ Log.Error(ex, $"Download of plugin {repoManifest.Name} failed unexpectedly.");
+ throw;
+ }
+
+ Log.Debug($"Extracting to {outputDir}");
+ // This throws an error, even with overwrite=false
+ // ZipFile.ExtractToDirectory(tempZip.FullName, outputDir.FullName, false);
+ using (var archive = ZipFile.OpenRead(tempZip.FullName))
+ {
+ foreach (var zipFile in archive.Entries)
+ {
+ var completeFileName = Path.GetFullPath(Path.Combine(outputDir.FullName, zipFile.FullName));
+
+ if (!completeFileName.StartsWith(outputDir.FullName, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new IOException("Trying to extract file outside of destination directory. See this link for more info: https://snyk.io/research/zip-slip-vulnerability");
+ }
+
+ if (zipFile.Name == string.Empty)
+ {
+ // Assuming Empty for Directory
+ Directory.CreateDirectory(Path.GetDirectoryName(completeFileName));
+ continue;
+ }
+
+ try
+ {
+ zipFile.ExtractToFile(completeFileName, true);
+ }
+ catch (Exception ex)
+ {
+ Log.Information($"Could not overwrite {zipFile.Name}: {ex.Message}");
+ }
+ }
+ }
+
+ tempZip.Delete();
+
+ var dllFile = LocalPluginManifest.GetPluginFile(outputDir, repoManifest);
+ var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
+
+ // Reload as a local manifest, add some attributes, and save again.
+ var manifest = LocalPluginManifest.Load(manifestFile);
+
+ if (useTesting)
+ {
+ manifest.Testing = true;
+ }
+
+ if (repoManifest.SourceRepo.IsThirdParty)
+ {
+ // Only document the url if it came from a third party repo.
+ manifest.InstalledFromUrl = repoManifest.SourceRepo.PluginMasterUrl;
+ }
+
+ manifest.Save(manifestFile);
+
+ Log.Information($"Installed plugin {manifest.Name} (testing={useTesting})");
+
+ this.LoadPlugin(dllFile, manifest, reason);
+
+ this.NotifyInstalledPluginsChanged();
+ }
+
+ ///
+ /// Load a plugin.
+ ///
+ /// The associated with the main assembly of this plugin.
+ /// The already loaded definition, if available.
+ /// The reason this plugin was loaded.
+ /// If this plugin should support development features.
+ /// If this plugin is being loaded at boot.
+ /// Don't load the plugin, just don't do it.
+ public void LoadPlugin(FileInfo dllFile, LocalPluginManifest manifest, PluginLoadReason reason, bool isDev = false, bool isBoot = false, bool doNotLoad = false)
+ {
+ var name = manifest?.Name ?? dllFile.Name;
+ var loadPlugin = !doNotLoad;
+
+ LocalPlugin plugin;
+
+ if (isDev)
+ {
+ Log.Information($"Loading dev plugin {name}");
+ var devPlugin = new LocalDevPlugin(this.dalamud, dllFile, manifest);
+ loadPlugin &= !isBoot || devPlugin.StartOnBoot;
+
+ // If we're not loading it, make sure it's disabled
+ if (!loadPlugin && !devPlugin.IsDisabled)
+ devPlugin.Disable();
+
+ plugin = devPlugin;
+ }
+ else
+ {
+ Log.Information($"Loading plugin {name}");
+ plugin = new LocalPlugin(this.dalamud, dllFile, manifest);
+ }
+
+ if (loadPlugin)
+ {
+ try
+ {
+ if (plugin.IsDisabled)
+ plugin.Enable();
+
+ plugin.Load(reason);
+ }
+ catch (InvalidPluginException)
+ {
+ PluginLocations.Remove(plugin.AssemblyName.FullName);
+ throw;
+ }
+ catch (Exception ex)
+ {
+ if (plugin.IsDev)
+ {
+ // Dev plugins always get added to the list so they can be fiddled with in the UI
+ Log.Information(ex, $"Dev plugin failed to load, adding anyways: {dllFile.Name}");
+ plugin.Disable(); // Disable here, otherwise you can't enable+load later
+ }
+ else if (plugin.Manifest.DalamudApiLevel < DalamudApiLevel)
+ {
+ // Out of date plugins get added so they can be updated.
+ Log.Information(ex, $"Plugin was outdated, adding anyways: {dllFile.Name}");
+ // plugin.Disable(); // Don't disable, or it gets deleted next boot.
+ }
+ else
+ {
+ PluginLocations.Remove(plugin.AssemblyName.FullName);
+ throw;
+ }
+ }
+ }
+
+ this.installedPlugins.Add(plugin);
+ }
+
+ ///
+ /// Remove a plugin.
+ ///
+ /// Plugin to remove.
+ public void RemovePlugin(LocalPlugin plugin)
+ {
+ if (plugin.State != PluginState.Unloaded)
+ throw new InvalidPluginOperationException($"Unable to remove {plugin.Name}, not unloaded");
+
+ this.installedPlugins.Remove(plugin);
+ PluginLocations.Remove(plugin.AssemblyName.FullName);
+
+ this.NotifyAvailablePluginsChanged();
+ this.NotifyInstalledPluginsChanged();
+ }
+
+ ///
+ /// Cleanup disabled plugins. Does not target devPlugins.
+ ///
+ public void CleanupPlugins()
+ {
+ foreach (var pluginDir in this.pluginDirectory.GetDirectories())
+ {
+ try
+ {
+ var versionDirs = pluginDir.GetDirectories();
+
+ versionDirs = versionDirs
+ .OrderByDescending(dir =>
+ {
+ var isVersion = Version.TryParse(dir.Name, out var version);
+
+ if (!isVersion)
+ {
+ Log.Debug($"Not a version, cleaning up {dir.FullName}");
+ dir.Delete(true);
+ }
+
+ return version;
+ })
+ .Where(version => version != null)
+ .ToArray();
+
+ if (versionDirs.Length == 0)
+ {
+ Log.Information($"No versions: cleaning up {pluginDir.FullName}");
+ pluginDir.Delete(true);
+ continue;
+ }
+ else
+ {
+ foreach (var versionDir in versionDirs)
+ {
+ try
+ {
+ var dllFile = new FileInfo(Path.Combine(versionDir.FullName, $"{pluginDir.Name}.dll"));
+ if (!dllFile.Exists)
+ {
+ Log.Information($"Missing dll: cleaning up {versionDir.FullName}");
+ versionDir.Delete(true);
+ continue;
+ }
+
+ var manifestFile = LocalPluginManifest.GetManifestFile(dllFile);
+ if (!manifestFile.Exists)
+ {
+ Log.Information($"Missing manifest: cleaning up {versionDir.FullName}");
+ versionDir.Delete(true);
+ continue;
+ }
+
+ var manifest = LocalPluginManifest.Load(manifestFile);
+ if (manifest.Disabled)
+ {
+ Log.Information($"Disabled: cleaning up {versionDir.FullName}");
+ versionDir.Delete(true);
+ continue;
+ }
+
+ if (manifest.DalamudApiLevel < DalamudApiLevel - 1 && !this.dalamud.Configuration.LoadAllApiLevels)
+ {
+ Log.Information($"Lower API: cleaning up {versionDir.FullName}");
+ versionDir.Delete(true);
+ continue;
+ }
+
+ if (manifest.ApplicableVersion < this.dalamud.StartInfo.GameVersion)
+ {
+ Log.Information($"Inapplicable version: cleaning up {versionDir.FullName}");
+ versionDir.Delete(true);
+ continue;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Could not clean up {versionDir.FullName}");
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Could not clean up {pluginDir.FullName}");
+ }
+ }
+ }
+
+ ///
+ /// Update all plugins.
+ ///
+ /// Perform a dry run, don't install anything.
+ /// Success or failure and a list of updated plugin metadata.
+ public List UpdatePlugins(bool dryRun = false)
+ {
+ Log.Information("Starting plugin update");
+
+ var updatedList = new List();
+
+ // Prevent collection was modified errors
+ for (var i = 0; i < this.updatablePlugins.Count; i++)
+ {
+ updatedList.Add(this.UpdateSinglePlugin(this.updatablePlugins[i], false, dryRun));
+ }
+
+ this.NotifyInstalledPluginsChanged();
+
+ Log.Information("Plugin update OK.");
+
+ return updatedList;
+ }
+
+ ///
+ /// Update a single plugin, provided a valid .
+ ///
+ /// The available plugin update.
+ /// Whether to notify that installed plugins have changed afterwards.
+ /// Whether or not to actually perform the update, or just indicate success.
+ /// The status of the update.
+ [CanBeNull]
+ public PluginUpdateStatus UpdateSinglePlugin(AvailablePluginUpdate metadata, bool notify, bool dryRun)
+ {
+ var plugin = metadata.InstalledPlugin;
+
+ // Can't update that!
+ if (plugin is LocalDevPlugin)
+ return null;
+
+ var updateStatus = new PluginUpdateStatus
+ {
+ InternalName = plugin.Manifest.InternalName,
+ Name = plugin.Manifest.Name,
+ Version = metadata.UseTesting ? metadata.UpdateManifest.TestingAssemblyVersion : metadata.UpdateManifest.AssemblyVersion,
+ };
+
+ if (dryRun)
+ {
+ updateStatus.WasUpdated = true;
+ }
+ else
+ {
+ updateStatus.WasUpdated = true;
+
+ // Unload if loaded
+ if (plugin.State == PluginState.Loaded || plugin.State == PluginState.LoadError)
+ {
+ try
+ {
+ plugin.Unload();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error during unload (update)");
+ updateStatus.WasUpdated = false;
+ return updateStatus;
+ }
+ }
+
+ try
+ {
+ plugin.Disable();
+ this.installedPlugins.Remove(plugin);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error during disable (update)");
+ updateStatus.WasUpdated = false;
+ return updateStatus;
+ }
+
+ try
+ {
+ this.InstallPlugin(metadata.UpdateManifest, metadata.UseTesting, PluginLoadReason.Update);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Error during install (update)");
+ updateStatus.WasUpdated = false;
+ return updateStatus;
+ }
+ }
+
+ if (notify && updateStatus.WasUpdated)
+ this.NotifyInstalledPluginsChanged();
+
+ return updateStatus;
+ }
+
+ ///
+ /// Unload the plugin, delete its configuration, and reload it.
+ ///
+ /// The plugin.
+ /// Throws if the plugin is still loading/unloading.
+ public void DeleteConfiguration(LocalPlugin plugin)
+ {
+ if (plugin.State == PluginState.InProgress)
+ throw new Exception("Cannot delete configuration for a loading/unloading plugin");
+
+ if (plugin.IsLoaded)
+ plugin.Unload();
+
+ // Let's wait so any handles on files in plugin configurations can be closed
+ Thread.Sleep(500);
+
+ this.PluginConfigs.Delete(plugin.Name);
+
+ Thread.Sleep(500);
+
+ // Let's indicate "installer" here since this is supposed to be a fresh install
+ plugin.Load(PluginLoadReason.Installer);
+ }
+
+ ///
+ /// Print to chat any plugin updates and whether they were successful.
+ ///
+ /// The list of updated plugin metadata.
+ /// The header text to send to chat prior to any update info.
+ public void PrintUpdatedPlugins(List updateMetadata, string header)
+ {
+ if (updateMetadata != null && updateMetadata.Count > 0)
+ {
+ this.dalamud.Framework.Gui.Chat.Print(header);
+
+ foreach (var metadata in updateMetadata)
+ {
+ if (metadata.WasUpdated)
+ {
+ this.dalamud.Framework.Gui.Chat.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version));
+ }
+ else
+ {
+ this.dalamud.Framework.Gui.Chat.PrintChat(new XivChatEntry
+ {
+ Message = Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version),
+ Type = XivChatType.Urgent,
+ });
+ }
+ }
+ }
+ }
+
+ ///
+ /// For a given manifest, determine if the testing version should be used over the normal version.
+ /// The higher of the two versions is calculated after checking other settings.
+ ///
+ /// Manifest to check.
+ /// A value indicating whether testing should be used.
+ public bool UseTesting(PluginManifest manifest)
+ {
+ if (!this.dalamud.Configuration.DoPluginTest)
+ return false;
+
+ if (manifest.IsTestingExclusive)
+ return true;
+
+ var av = manifest.AssemblyVersion;
+ var tv = manifest.TestingAssemblyVersion;
+ var hasAv = av != null;
+ var hasTv = tv != null;
+
+ if (hasAv && hasTv)
+ {
+ return tv > av;
+ }
+ else
+ {
+ return hasTv;
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether the given repo manifest should be visible to the user.
+ ///
+ /// Repo manifest.
+ /// If the manifest is visible.
+ public bool IsManifestVisible(RemotePluginManifest manifest)
+ {
+ // Hidden by user
+ if (this.dalamud.Configuration.HiddenPluginInternalName.Contains(manifest.InternalName))
+ return false;
+
+ // Hidden by manifest
+ if (manifest.IsHide)
+ return false;
+
+ return true;
+ }
+
+ ///
+ /// Gets a value indicating whether the given manifest is eligible for ANYTHING. These are hard
+ /// checks that should not allow installation or loading.
+ ///
+ /// Plugin manifest.
+ /// If the manifest is eligible.
+ public bool IsManifestEligible(PluginManifest manifest)
+ {
+ // Testing exclusive
+ if (manifest.IsTestingExclusive && !this.dalamud.Configuration.DoPluginTest)
+ return false;
+
+ // Applicable version
+ if (manifest.ApplicableVersion < this.dalamud.StartInfo.GameVersion)
+ return false;
+
+ // API level
+ if (manifest.DalamudApiLevel < DalamudApiLevel && !this.dalamud.Configuration.LoadAllApiLevels)
+ return false;
+
+ // Banned
+ if (this.IsManifestBanned(manifest))
+ return false;
+
+ return true;
+ }
+
+ private bool IsManifestBanned(PluginManifest manifest)
+ {
+ return this.bannedPlugins.Any(ban => ban.Name == manifest.InternalName && ban.AssemblyVersion == manifest.AssemblyVersion);
+ }
+
+ private void DetectAvailablePluginUpdates()
+ {
+ var updatablePlugins = new List();
+
+ for (var i = 0; i < this.installedPlugins.Count; i++)
+ {
+ var plugin = this.installedPlugins[i];
+
+ var installedVersion = plugin.IsTesting
+ ? plugin.Manifest.TestingAssemblyVersion
+ : plugin.Manifest.AssemblyVersion;
+
+ var updates = this.availablePlugins
+ .Where(remoteManifest => plugin.Manifest.InternalName == remoteManifest.InternalName)
+ .Select(remoteManifest =>
+ {
+ var useTesting = this.UseTesting(remoteManifest);
+ var candidateVersion = useTesting
+ ? remoteManifest.TestingAssemblyVersion
+ : remoteManifest.AssemblyVersion;
+ var isUpdate = candidateVersion > installedVersion;
+
+ return (isUpdate, useTesting, candidateVersion, remoteManifest);
+ })
+ .Where(tpl => tpl.isUpdate)
+ .ToList();
+
+ if (updates.Count > 0)
+ {
+ var update = updates.Aggregate((t1, t2) => t1.candidateVersion > t2.candidateVersion ? t1 : t2);
+ updatablePlugins.Add(new(plugin, update.remoteManifest, update.useTesting));
+ }
+ }
+
+ this.updatablePlugins = updatablePlugins;
+ }
+
+ private void NotifyAvailablePluginsChanged()
+ {
+ this.DetectAvailablePluginUpdates();
+
+ try
+ {
+ this.AvailablePlugins = ImmutableList.CreateRange(this.availablePlugins);
+ this.UpdatablePlugins = ImmutableList.CreateRange(this.updatablePlugins);
+ this.OnAvailablePluginsChanged.Invoke();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Error notifying {nameof(this.OnAvailablePluginsChanged)}");
+ }
+ }
+
+ private void NotifyInstalledPluginsChanged()
+ {
+ this.DetectAvailablePluginUpdates();
+
+ try
+ {
+ this.InstalledPlugins = ImmutableList.CreateRange(this.installedPlugins);
+ this.UpdatablePlugins = ImmutableList.CreateRange(this.updatablePlugins);
+ this.OnInstalledPluginsChanged.Invoke();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, $"Error notifying {nameof(this.OnInstalledPluginsChanged)}");
+ }
+ }
+
+ private struct BannedPlugin
+ {
+ [JsonProperty]
+ public string Name { get; private set; }
+
+ [JsonProperty]
+ public Version AssemblyVersion { get; private set; }
+ }
+
+ private struct PluginDef
+ {
+ public PluginDef(FileInfo dllFile, LocalPluginManifest manifest, bool isDev)
+ {
+ this.DllFile = dllFile;
+ this.Manifest = manifest;
+ this.IsDev = isDev;
+ }
+
+ public FileInfo DllFile { get; init; }
+
+ public LocalPluginManifest Manifest { get; init; }
+
+ public bool IsDev { get; init; }
+
+ public static int Sorter(PluginDef def1, PluginDef def2)
+ {
+ var prio1 = def1.Manifest?.LoadPriority ?? 0;
+ var prio2 = def2.Manifest?.LoadPriority ?? 0;
+ return prio2.CompareTo(prio1);
+ }
+ }
+
+ private static class Locs
+ {
+ public static string DalamudPluginUpdateSuccessful(string name, Version version) => Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}.").Format(name, version);
+
+ public static string DalamudPluginUpdateFailed(string name, Version version) => Loc.Localize("DalamudPluginUpdateFailed", " 》 {0} update to v{1} failed.").Format(name, version);
+ }
+ }
+
+ ///
+ /// Class responsible for loading and unloading plugins.
+ /// This contains the assembly patching functionality to resolve assembly locations.
+ ///
+ internal partial class PluginManager
+ {
+ ///
+ /// A mapping of plugin assembly name to patch data. Used to fill in missing data due to loading
+ /// plugins via byte[].
+ ///
+ internal static readonly Dictionary PluginLocations = new();
+
+ ///
+ /// Patch method for internal class RuntimeAssembly.Location, also known as Assembly.Location.
+ /// This patch facilitates resolving the assembly location for plugins that are loaded via byte[].
+ /// It should never be called manually.
+ ///
+ /// The equivalent of `this`.
+ /// The result from the original method.
+ [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Enforced naming for special injected parameters")]
+ private static void AssemblyLocationPatch(Assembly __instance, ref string __result)
+ {
+ // Assembly.GetExecutingAssembly can return this.
+ // Check for it as a special case and find the plugin.
+ if (__result.EndsWith("System.Private.CoreLib.dll", StringComparison.InvariantCultureIgnoreCase))
+ {
+ foreach (var assemblyName in GetStackFrameAssemblyNames())
+ {
+ if (PluginLocations.TryGetValue(assemblyName, out var data))
+ {
+ __result = data.Location;
+ return;
+ }
+ }
+ }
+ else if (string.IsNullOrEmpty(__result))
+ {
+ if (PluginLocations.TryGetValue(__instance.FullName, out var data))
+ {
+ __result = data.Location;
+ }
+ }
+ }
+
+ ///
+ /// Patch method for internal class RuntimeAssembly.CodeBase, also known as Assembly.CodeBase.
+ /// This patch facilitates resolving the assembly location for plugins that are loaded via byte[].
+ /// It should never be called manually.
+ ///
+ /// The equivalent of `this`.
+ /// The result from the original method.
+ [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Enforced naming for special injected parameters")]
+ private static void AssemblyCodeBasePatch(Assembly __instance, ref string __result)
+ {
+ // Assembly.GetExecutingAssembly can return this.
+ // Check for it as a special case and find the plugin.
+ if (__result.EndsWith("System.Private.CoreLib.dll"))
+ {
+ foreach (var assemblyName in GetStackFrameAssemblyNames())
+ {
+ if (PluginLocations.TryGetValue(assemblyName, out var data))
+ {
+ __result = data.Location;
+ return;
+ }
+ }
+ }
+ else if (string.IsNullOrEmpty(__result))
+ {
+ if (PluginLocations.TryGetValue(__instance.FullName, out var data))
+ {
+ __result = data.Location;
+ }
+ }
+ }
+
+ private static IEnumerable GetStackFrameAssemblyNames()
+ {
+ var stackTrace = new StackTrace();
+ var stackFrames = stackTrace.GetFrames();
+
+ foreach (var stackFrame in stackFrames)
+ {
+ var methodBase = stackFrame.GetMethod();
+ if (methodBase == null)
+ continue;
+
+ yield return methodBase.Module.Assembly.FullName;
+ }
+ }
+
+ private void ApplyPatches()
+ {
+ var harmony = new Harmony("goatcorp.dalamud.pluginmanager");
+
+ var targetType = typeof(PluginManager).Assembly.GetType();
+
+ var locationTarget = AccessTools.PropertyGetter(targetType, nameof(Assembly.Location));
+ var locationPatch = AccessTools.Method(typeof(PluginManager), nameof(PluginManager.AssemblyLocationPatch));
+ harmony.Patch(locationTarget, postfix: new(locationPatch));
+
+#pragma warning disable SYSLIB0012 // Type or member is obsolete
+ var codebaseTarget = AccessTools.PropertyGetter(targetType, nameof(Assembly.CodeBase));
+ var codebasePatch = AccessTools.Method(typeof(PluginManager), nameof(PluginManager.AssemblyCodeBasePatch));
+ harmony.Patch(codebaseTarget, postfix: new(codebasePatch));
+#pragma warning restore SYSLIB0012 // Type or member is obsolete
+ }
+
+ internal record PluginPatchData
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DLL file being loaded.
+ public PluginPatchData(FileInfo dllFile)
+ {
+ this.Location = dllFile.FullName;
+ this.CodeBase = new Uri(dllFile.FullName).AbsoluteUri;
+ }
+
+ ///
+ /// Gets simulated Assembly.Location output.
+ ///
+ public string Location { get; }
+
+ ///
+ /// Gets simulated Assembly.CodeBase output.
+ ///
+ public string CodeBase { get; }
+ }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/PluginRepository.cs b/Dalamud/Plugin/Internal/PluginRepository.cs
new file mode 100644
index 000000000..64769322d
--- /dev/null
+++ b/Dalamud/Plugin/Internal/PluginRepository.cs
@@ -0,0 +1,108 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+
+using Dalamud.Plugin.Internal.Types;
+using Newtonsoft.Json;
+
+namespace Dalamud.Plugin.Internal
+{
+ ///
+ /// This class represents a single plugin repository.
+ ///
+ internal partial class PluginRepository
+ {
+ private const string DalamudPluginsMasterUrl = "https://raw.githubusercontent.com/goatcorp/DalamudPlugins/master/pluginmaster.json";
+
+ private static readonly ModuleLog Log = new("PLUGINR");
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The plugin master URL.
+ /// Whether the plugin repo is enabled.
+ public PluginRepository(string pluginMasterUrl, bool isEnabled)
+ {
+ this.PluginMasterUrl = pluginMasterUrl;
+ this.IsThirdParty = pluginMasterUrl != DalamudPluginsMasterUrl;
+ this.IsEnabled = isEnabled;
+
+ // No need to wait for this
+ Task.Run(this.ReloadPluginMasterAsync);
+ }
+
+ ///
+ /// Gets a new instance of the class for the main repo.
+ ///
+ public static PluginRepository MainRepo => new(DalamudPluginsMasterUrl, true);
+
+ ///
+ /// Gets the pluginmaster.json URL.
+ ///
+ public string PluginMasterUrl { get; }
+
+ ///
+ /// Gets a value indicating whether this plugin repository is from a third party.
+ ///
+ public bool IsThirdParty { get; }
+
+ ///
+ /// Gets a value indicating whether this repo is enabled.
+ ///
+ public bool IsEnabled { get; }
+
+ ///
+ /// Gets the plugin master list of available plugins.
+ ///
+ public ReadOnlyCollection PluginMaster { get; private set; }
+
+ ///
+ /// Gets the initialization state of the plugin repository.
+ ///
+ public PluginRepositoryState State { get; private set; }
+
+ ///
+ /// Reload the plugin master asynchronously in a task.
+ ///
+ /// The new state.
+ public Task ReloadPluginMasterAsync()
+ {
+ this.State = PluginRepositoryState.InProgress;
+ this.PluginMaster = new List().AsReadOnly();
+
+ return Task.Run(() =>
+ {
+ using var client = new WebClient();
+
+ Log.Information($"Fetching repo: {this.PluginMasterUrl}");
+
+ var data = client.DownloadString(this.PluginMasterUrl);
+
+ var pluginMaster = JsonConvert.DeserializeObject>(data);
+ pluginMaster.Sort((pm1, pm2) => pm1.Name.CompareTo(pm2.Name));
+
+ // Set the source for each remote manifest. Allows for checking if is 3rd party.
+ foreach (var manifest in pluginMaster)
+ {
+ manifest.SourceRepo = this;
+ }
+
+ this.PluginMaster = pluginMaster.AsReadOnly();
+ }).ContinueWith(task =>
+ {
+ if (task.IsCompletedSuccessfully)
+ {
+ Log.Debug($"Successfully fetched repo: {this.PluginMasterUrl}");
+ this.State = PluginRepositoryState.Success;
+ }
+ else
+ {
+ Log.Error(task.Exception, $"PluginMaster failed: {this.PluginMasterUrl}");
+ this.State = PluginRepositoryState.Fail;
+ }
+ });
+ }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs b/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs
new file mode 100644
index 000000000..32dde337c
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/AvailablePluginUpdate.cs
@@ -0,0 +1,36 @@
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// Information about an available plugin update.
+ ///
+ internal record AvailablePluginUpdate
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The installed plugin to update.
+ /// The manifest to use for the update.
+ /// If the testing version should be used for the update.
+ public AvailablePluginUpdate(LocalPlugin installedPlugin, RemotePluginManifest updateManifest, bool useTesting)
+ {
+ this.InstalledPlugin = installedPlugin;
+ this.UpdateManifest = updateManifest;
+ this.UseTesting = useTesting;
+ }
+
+ ///
+ /// Gets the currently installed plugin.
+ ///
+ public LocalPlugin InstalledPlugin { get; init; }
+
+ ///
+ /// Gets the available update manifest.
+ ///
+ public RemotePluginManifest UpdateManifest { get; init; }
+
+ ///
+ /// Gets a value indicating whether the update should use the testing URL.
+ ///
+ public bool UseTesting { get; init; }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/IpcSubscription.cs b/Dalamud/Plugin/Internal/Types/IpcSubscription.cs
new file mode 100644
index 000000000..3427a824a
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/IpcSubscription.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Dynamic;
+
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// This class represents an IPC subscription between two plugins.
+ ///
+ internal record IpcSubscription
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The source plugin name.
+ /// The name of the plugin being subscribed to.
+ /// The subscription action.
+ public IpcSubscription(string sourcePluginName, string subPluginName, Action subAction)
+ {
+ this.SourcePluginName = sourcePluginName;
+ this.SubPluginName = subPluginName;
+ this.SubAction = subAction;
+ }
+
+ ///
+ /// Gets the name of the plugin requesting the subscription.
+ ///
+ public string SourcePluginName { get; }
+
+ ///
+ /// Gets the name of the plugin being subscribed to.
+ ///
+ public string SubPluginName { get; }
+
+ ///
+ /// Gets the subscription action.
+ ///
+ public Action SubAction { get; }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs
new file mode 100644
index 000000000..d0a3b7448
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs
@@ -0,0 +1,73 @@
+using System.IO;
+
+using Newtonsoft.Json;
+
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as
+ /// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk.
+ ///
+ internal record LocalPluginManifest : PluginManifest
+ {
+ ///
+ /// Gets or sets a value indicating whether the plugin is disabled and should not be loaded.
+ /// This value supercedes the ".disabled" file functionality and should not be included in the plugin master.
+ ///
+ public bool Disabled { get; set; } = false;
+
+ ///
+ /// Gets or sets a value indicating whether the plugin should only be loaded when testing is enabled.
+ /// This value supercedes the ".testing" file functionality and should not be included in the plugin master.
+ ///
+ public bool Testing { get; set; } = false;
+
+ ///
+ /// Gets or sets the 3rd party repo URL that this plugin was installed from. Used to display where the plugin was
+ /// sourced from on the installed plugin view. This should not be included in the plugin master.
+ ///
+ public string InstalledFromUrl { get; set; }
+
+ ///
+ /// Save a plugin manifest to file.
+ ///
+ /// Path to save at.
+ public void Save(FileInfo manifestFile) => File.WriteAllText(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented));
+
+ ///
+ /// Loads a plugin manifest from file.
+ ///
+ /// Path to the manifest.
+ /// A object.
+ public static LocalPluginManifest Load(FileInfo manifestFile) => JsonConvert.DeserializeObject(File.ReadAllText(manifestFile.FullName));
+
+ ///
+ /// A standardized way to get the plugin DLL name that should accompany a manifest file. May not exist.
+ ///
+ /// Manifest directory.
+ /// The manifest.
+ /// The file.
+ public static FileInfo GetPluginFile(DirectoryInfo dir, PluginManifest manifest) => new(Path.Combine(dir.FullName, $"{manifest.InternalName}.dll"));
+
+ ///
+ /// A standardized way to get the manifest file that should accompany a plugin DLL. May not exist.
+ ///
+ /// The plugin DLL.
+ /// The file.
+ public static FileInfo GetManifestFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, Path.GetFileNameWithoutExtension(dllFile.Name) + ".json"));
+
+ ///
+ /// A standardized way to get the obsolete .disabled file that should accompany a plugin DLL. May not exist.
+ ///
+ /// The plugin DLL.
+ /// The .disabled file.
+ public static FileInfo GetDisabledFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, ".disabled"));
+
+ ///
+ /// A standardized way to get the obsolete .testing file that should accompany a plugin DLL. May not exist.
+ ///
+ /// The plugin DLL.
+ /// The .testing file.
+ public static FileInfo GetTestingFile(FileInfo dllFile) => new(Path.Combine(dllFile.DirectoryName, ".testing"));
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs
new file mode 100644
index 000000000..3c937d710
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs
@@ -0,0 +1,137 @@
+using System;
+using System.Collections.Generic;
+
+using Dalamud.Game;
+using Newtonsoft.Json;
+
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// Information about a plugin, packaged in a json file with the DLL.
+ ///
+ internal record PluginManifest
+ {
+ ///
+ /// Gets the author/s of the plugin.
+ ///
+ [JsonProperty]
+ public string Author { get; init; }
+
+ ///
+ /// Gets or sets the public name of the plugin.
+ ///
+ [JsonProperty]
+ public string Name { get; set; }
+
+ ///
+ /// Gets a description of the plugins functions.
+ ///
+ [JsonProperty]
+ public string Description { get; init; }
+
+ ///
+ /// Gets a list of tags defined on the plugin.
+ ///
+ [JsonProperty]
+ public List Tags { get; init; }
+
+ ///
+ /// Gets a value indicating whether or not the plugin is hidden in the plugin installer.
+ /// This value comes from the plugin master and is in addition to the list of hidden names kept by Dalamud.
+ ///
+ [JsonProperty]
+ public bool IsHide { get; init; }
+
+ ///
+ /// Gets the internal name of the plugin, which should match the assembly name of the plugin.
+ ///
+ [JsonProperty]
+ public string InternalName { get; init; }
+
+ ///
+ /// Gets the current assembly version of the plugin.
+ ///
+ [JsonProperty]
+ public Version AssemblyVersion { get; init; }
+
+ ///
+ /// Gets the current testing assembly version of the plugin.
+ ///
+ [JsonProperty]
+ public Version TestingAssemblyVersion { get; init; }
+
+ ///
+ /// Gets a value indicating whether the is not null.
+ ///
+ [JsonIgnore]
+ public bool HasAssemblyVersion => this.AssemblyVersion != null;
+
+ ///
+ /// Gets a value indicating whether the is not null.
+ ///
+ [JsonIgnore]
+ public bool HasTestingAssemblyVersion => this.TestingAssemblyVersion != null;
+
+ ///
+ /// Gets a value indicating whether the plugin is only available for testing.
+ ///
+ [JsonProperty]
+ public bool IsTestingExclusive { get; init; }
+
+ ///
+ /// Gets an URL to the website or source code of the plugin.
+ ///
+ [JsonProperty]
+ public string RepoUrl { get; init; }
+
+ ///
+ /// Gets the version of the game this plugin works with.
+ ///
+ [JsonProperty]
+ [JsonConverter(typeof(GameVersionConverter))]
+ public GameVersion ApplicableVersion { get; init; } = GameVersion.Any;
+
+ ///
+ /// Gets the API level of this plugin. For the current API level, please see
+ /// for the currently used API level.
+ ///
+ [JsonProperty]
+ public int DalamudApiLevel { get; init; } = PluginManager.DalamudApiLevel;
+
+ ///
+ /// Gets the number of downloads this plugin has.
+ ///
+ [JsonProperty]
+ public long DownloadCount { get; init; }
+
+ ///
+ /// Gets the last time this plugin was updated.
+ ///
+ [JsonProperty]
+ public long LastUpdate { get; init; }
+
+ ///
+ /// Gets the download link used to install the plugin.
+ ///
+ [JsonProperty]
+ public string DownloadLinkInstall { get; init; }
+
+ ///
+ /// Gets the download link used to update the plugin.
+ ///
+ [JsonProperty]
+ public string DownloadLinkUpdate { get; init; }
+
+ ///
+ /// Gets the download link used to get testing versions of the plugin.
+ ///
+ [JsonProperty]
+ public string DownloadLinkTesting { get; init; }
+
+ ///
+ /// Gets the load priority for this plugin. Higher values means higher priority. 0 is default priority.
+ ///
+ [JsonProperty]
+ public int LoadPriority { get; init; }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/PluginOperationResult.cs b/Dalamud/Plugin/Internal/Types/PluginOperationResult.cs
new file mode 100644
index 000000000..9ac1db611
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/PluginOperationResult.cs
@@ -0,0 +1,57 @@
+using System;
+
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// This represents the result of a an operation taken against a plugin.
+ /// Loading, unloading, installation, etc.
+ ///
+ internal enum PluginOperationResult
+ {
+ ///
+ /// The result is unknown. Should not be used.
+ ///
+ [Obsolete("Do not use this", error: true)]
+ Unknown,
+
+ ///
+ /// The result is pending. Take a seat and wait.
+ ///
+ Pending,
+
+ ///
+ /// The operation was successful.
+ ///
+ Success,
+
+ ///
+ /// During the plugin operation, an unexpected error occurred.
+ ///
+ UnknownError,
+
+ ///
+ /// The plugin state was invalid for the attempted operation.
+ ///
+ InvalidState,
+
+ ///
+ /// The plugin applicable version is not compativle with the currently running game.
+ ///
+ InvalidGameVersion,
+
+ ///
+ /// The plugin API level is not compatible with the currently running Dalamud.
+ ///
+ InvalidApiLevel,
+
+ ///
+ /// During loading, the current plugin was marked as disabled.
+ ///
+ InvalidStateDisabled,
+
+ ///
+ /// During loading, another plugin was detected with the same internal name.
+ ///
+ InvalidStateDuplicate,
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/PluginRepositoryState.cs b/Dalamud/Plugin/Internal/Types/PluginRepositoryState.cs
new file mode 100644
index 000000000..46aa2c351
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/PluginRepositoryState.cs
@@ -0,0 +1,28 @@
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// Values representing plugin repository state.
+ ///
+ internal enum PluginRepositoryState
+ {
+ ///
+ /// State is unknown.
+ ///
+ Unknown,
+
+ ///
+ /// Currently loading.
+ ///
+ InProgress,
+
+ ///
+ /// Load was successful.
+ ///
+ Success,
+
+ ///
+ /// Load failed.
+ ///
+ Fail,
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/PluginState.cs b/Dalamud/Plugin/Internal/Types/PluginState.cs
new file mode 100644
index 000000000..f32543b39
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/PluginState.cs
@@ -0,0 +1,33 @@
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// Values representing plugin load state.
+ ///
+ internal enum PluginState
+ {
+ ///
+ /// Plugin is defined, but unloaded.
+ ///
+ Unloaded,
+
+ ///
+ /// Plugin has thrown an error during unload.
+ ///
+ UnloadError,
+
+ ///
+ /// Currently loading.
+ ///
+ InProgress,
+
+ ///
+ /// Load is successful.
+ ///
+ Loaded,
+
+ ///
+ /// Plugin has thrown an error during loading.
+ ///
+ LoadError,
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs b/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs
new file mode 100644
index 000000000..f0394b9b7
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// Plugin update status.
+ ///
+ internal class PluginUpdateStatus
+ {
+ ///
+ /// Gets or sets the plugin internal name.
+ ///
+ public string InternalName { get; set; }
+
+ ///
+ /// Gets or sets the plugin name.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets the plugin version.
+ ///
+ public Version Version { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the plugin was updated.
+ ///
+ public bool WasUpdated { get; set; }
+ }
+}
diff --git a/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs b/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs
new file mode 100644
index 000000000..cbb989159
--- /dev/null
+++ b/Dalamud/Plugin/Internal/Types/RemotePluginManifest.cs
@@ -0,0 +1,18 @@
+using Newtonsoft.Json;
+
+namespace Dalamud.Plugin.Internal.Types
+{
+ ///
+ /// Information about a plugin, packaged in a json file with the DLL. This variant includes additional information such as
+ /// if the plugin is disabled and if it was installed from a testing URL. This is designed for use with manifests on disk.
+ ///
+ internal record RemotePluginManifest : PluginManifest
+ {
+ ///
+ /// Gets or sets the plugin repository this manifest came from. Used in reporting which third party repo a manifest
+ /// may have come from in the plugins available view. This functionality should not be included in the plugin master.
+ ///
+ [JsonIgnore]
+ public PluginRepository SourceRepo { get; set; } = null;
+ }
+}
diff --git a/Dalamud/Plugin/PluginDefinition.cs b/Dalamud/Plugin/PluginDefinition.cs
deleted file mode 100644
index 088688811..000000000
--- a/Dalamud/Plugin/PluginDefinition.cs
+++ /dev/null
@@ -1,109 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Dalamud.Plugin
-{
- ///
- /// Class containing information about a plugin.
- ///
- public class PluginDefinition
- {
- ///
- /// Gets or sets the author/s of the plugin.
- ///
- public string Author { get; set; }
-
- ///
- /// Gets or sets the public name of the plugin.
- ///
- public string Name { get; set; }
-
- ///
- /// Gets or sets the internal name of the plugin, which should match the assembly name of the plugin.
- ///
- public string InternalName { get; set; }
-
- ///
- /// Gets or sets the current assembly version of the plugin.
- ///
- public string AssemblyVersion { get; set; }
-
- ///
- /// Gets or sets the current testing assembly version of the plugin.
- ///
- public string TestingAssemblyVersion { get; set; }
-
- ///
- /// Gets or sets a value indicating whether the plugin is only available for testing.
- ///
- public bool IsTestingExclusive { get; set; }
-
- ///
- /// Gets or sets a description of the plugins functions.
- ///
- public string Description { get; set; }
-
- ///
- /// Gets or sets the version of the game this plugin works with.
- ///
- public string ApplicableVersion { get; set; }
-
- ///
- /// Gets or sets an URL to the website or source code of the plugin.
- ///
- public string RepoUrl { get; set; }
-
- ///
- /// Gets or sets a list of tags defined on the plugin.
- ///
- public List Tags { get; set; }
-
- ///
- /// Gets or sets a value indicating whether or not the plugin is hidden in the plugin installer.
- ///
- public bool IsHide { get; set; }
-
- ///
- /// Gets or sets the API level of this plugin. For the current API level, please see for the currently used API level.
- ///
- public int DalamudApiLevel { get; set; }
-
- ///
- /// Gets or sets the number of downloads this plugin has.
- ///
- public long DownloadCount { get; set; }
-
- ///
- /// Gets or sets the last time this plugin was updated.
- ///
- public long LastUpdate { get; set; }
-
- ///
- /// Gets or sets the index of the third party repo.
- ///
- public int RepoNumber { get; set; }
-
- ///
- /// Gets or sets the download link used to install the plugin.
- ///
- public string DownloadLinkInstall { get; set; }
-
- ///
- /// Gets or sets the download link used to update the plugin.
- ///
- public string DownloadLinkUpdate { get; set; }
-
- ///
- /// Gets or sets the download link used to get testing versions of the plugin.
- ///
- public string DownloadLinkTesting { get; set; }
-
- ///
- /// Gets or sets the load priority for this plugin. Higher values means higher priority. 0 is default priority.
- ///
- public int LoadPriority { get; set; }
- }
-}
diff --git a/Dalamud/Plugin/PluginInstallerWindow.cs b/Dalamud/Plugin/PluginInstallerWindow.cs
deleted file mode 100644
index 3be2305e2..000000000
--- a/Dalamud/Plugin/PluginInstallerWindow.cs
+++ /dev/null
@@ -1,607 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Numerics;
-using System.Threading.Tasks;
-
-using CheapLoc;
-using Dalamud.Interface;
-using Dalamud.Interface.Colors;
-using Dalamud.Interface.Windowing;
-using ImGuiNET;
-using Serilog;
-
-namespace Dalamud.Plugin
-{
- ///
- /// Class responsible for drawing the plugin installer.
- ///
- internal class PluginInstallerWindow : Window
- {
- private readonly Dalamud dalamud;
-
- private string gameVersion;
-
- private bool errorModalDrawing = true;
- private bool errorModalOnNextFrame = false;
-
- private bool updateComplete = false;
- private int updatePluginCount = 0;
- private List updatedPlugins;
-
- private List pluginListAvailable;
- private List pluginListInstalled;
-
- private string searchText = string.Empty;
-
- private PluginSortKind sortKind = PluginSortKind.Alphabetical;
- private string filterText = Loc.Localize("SortAlphabetical", "Alphabetical");
-
- private PluginInstallStatus installStatus = PluginInstallStatus.None;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The relevant Dalamud instance.
- /// The version of the game.
- public PluginInstallerWindow(Dalamud dalamud, string gameVersion)
- : base(
- Loc.Localize("InstallerHeader", "Plugin Installer") + (dalamud.Configuration.DoPluginTest ? " (TESTING)" : string.Empty) + "###XlPluginInstaller",
- ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoScrollbar)
- {
- this.dalamud = dalamud;
- this.gameVersion = gameVersion;
-
- this.Size = new Vector2(810, 520);
- this.SizeCondition = ImGuiCond.Always;
- }
-
- private enum PluginInstallStatus
- {
- None,
- InProgress,
- Success,
- Fail,
- }
-
- private enum PluginSortKind
- {
- Alphabetical,
- DownloadCount,
- LastUpdate,
- }
-
- ///
- /// Code to be executed when the window is opened.
- ///
- public override void OnOpen()
- {
- base.OnOpen();
-
- if (this.dalamud.PluginRepository.State != PluginRepository.InitializationState.InProgress)
- this.dalamud.PluginRepository.ReloadPluginMasterAsync();
-
- this.pluginListAvailable = null;
- this.pluginListInstalled = null;
- this.updateComplete = false;
- this.updatePluginCount = 0;
- this.updatedPlugins = null;
- this.searchText = string.Empty;
- this.sortKind = PluginSortKind.Alphabetical;
- this.filterText = Loc.Localize("SortAlphabetical", "Alphabetical");
- }
-
- ///
- /// Draw the plugin installer view ImGui.
- ///
- public override void Draw()
- {
- ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5 * ImGui.GetIO().FontGlobalScale));
- var descriptionText = Loc.Localize("InstallerHint", "This window allows you to install and remove in-game plugins.\nThey are made by third-party developers.");
- ImGui.Text(descriptionText);
-
- var sortingTextSize = ImGui.CalcTextSize(Loc.Localize("SortDownloadCounts", "Download Count")) + ImGui.CalcTextSize(Loc.Localize("PluginSort", "Sort By"));
- ImGui.SameLine(ImGui.GetWindowWidth() - sortingTextSize.X - ((250 + 20) * ImGui.GetIO().FontGlobalScale));
- ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (ImGui.CalcTextSize(descriptionText).Y / 4) - 2);
- ImGui.SetCursorPosX(ImGui.GetWindowWidth() - sortingTextSize.X - ((250 + 20) * ImGui.GetIO().FontGlobalScale));
-
- ImGui.SetNextItemWidth(240 * ImGui.GetIO().FontGlobalScale);
- ImGui.InputTextWithHint("###XPlPluginInstaller_Search", Loc.Localize("InstallerSearch", "Search"), ref this.searchText, 100);
-
- ImGui.SameLine();
- ImGui.SetNextItemWidth((10 * ImGui.GetIO().FontGlobalScale) + ImGui.CalcTextSize(Loc.Localize("SortDownloadCounts", "Download Count")).X);
- if (ImGui.BeginCombo(Loc.Localize("PluginSort", "Sort By"), this.filterText, ImGuiComboFlags.NoArrowButton))
- {
- if (ImGui.Selectable(Loc.Localize("SortAlphabetical", "Alphabetical")))
- {
- this.sortKind = PluginSortKind.Alphabetical;
- this.filterText = Loc.Localize("SortAlphabetical", "Alphabetical");
-
- this.ResortPlugins();
- }
-
- if (ImGui.Selectable(Loc.Localize("SortDownloadCounts", "Download Count")))
- {
- this.sortKind = PluginSortKind.DownloadCount;
- this.filterText = Loc.Localize("SortDownloadCounts", "Download Count");
-
- this.ResortPlugins();
- }
-
- if (ImGui.Selectable(Loc.Localize("SortLastUpdate", "Last Update")))
- {
- this.sortKind = PluginSortKind.LastUpdate;
- this.filterText = Loc.Localize("SortLastUpdate", "Last Update");
-
- this.ResortPlugins();
- }
-
- ImGui.EndCombo();
- }
-
- ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5 * ImGui.GetIO().FontGlobalScale));
-
- (string Text, Vector4 Color) initializationStatusText = (null, ImGuiColors.DalamudGrey);
- if (this.dalamud.PluginRepository.State == PluginRepository.InitializationState.InProgress)
- {
- initializationStatusText.Text = Loc.Localize("InstallerLoading", "Loading plugins...");
- this.pluginListAvailable = null;
- }
- else if (this.dalamud.PluginRepository.State == PluginRepository.InitializationState.Fail)
- {
- initializationStatusText.Text = Loc.Localize("InstallerDownloadFailed", "Download failed.");
- this.pluginListAvailable = null;
- }
- else
- {
- if (this.dalamud.PluginRepository.State == PluginRepository.InitializationState.FailThirdRepo)
- {
- initializationStatusText.Text = Loc.Localize("InstallerDownloadFailedThird", "One of your third party repos is unreachable or there is no internet connection.");
- initializationStatusText.Color = ImGuiColors.DalamudRed;
- }
-
- if (this.pluginListAvailable == null)
- {
- this.RefetchPlugins();
- }
- }
-
- ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1, 3) * ImGui.GetIO().FontGlobalScale);
-
- if (ImGui.BeginTabBar("PluginsTabBar", ImGuiTabBarFlags.NoTooltip))
- {
- this.DrawTab(false, initializationStatusText);
- this.DrawTab(true, initializationStatusText);
-
- ImGui.EndTabBar();
- ImGui.Separator();
- }
-
- ImGui.PopStyleVar();
-
- ImGui.Dummy(new Vector2(3f, 3f) * ImGui.GetIO().FontGlobalScale);
-
- if (this.installStatus == PluginInstallStatus.InProgress)
- {
- ImGui.Button(Loc.Localize("InstallerUpdating", "Updating..."));
- }
- else
- {
- if (this.updateComplete)
- {
- ImGui.Button(this.updatePluginCount == 0
- ? Loc.Localize("InstallerNoUpdates", "No updates found!")
- : string.Format(Loc.Localize("InstallerUpdateComplete", "{0} plugins updated!"), this.updatePluginCount));
- }
- else
- {
- if (ImGui.Button(Loc.Localize("InstallerUpdatePlugins", "Update plugins")) &&
- this.dalamud.PluginRepository.State == PluginRepository.InitializationState.Success)
- {
- this.installStatus = PluginInstallStatus.InProgress;
-
- Task.Run(() => this.dalamud.PluginRepository.UpdatePlugins()).ContinueWith(t =>
- {
- this.installStatus =
- t.Result.Success ? PluginInstallStatus.Success : PluginInstallStatus.Fail;
- this.installStatus =
- t.IsFaulted ? PluginInstallStatus.Fail : this.installStatus;
-
- if (this.installStatus == PluginInstallStatus.Success)
- {
- this.updateComplete = true;
- }
-
- if (t.Result.UpdatedPlugins != null)
- {
- this.updatePluginCount = t.Result.UpdatedPlugins.Count;
- this.updatedPlugins = t.Result.UpdatedPlugins;
- }
-
- this.errorModalDrawing = this.installStatus == PluginInstallStatus.Fail;
- this.errorModalOnNextFrame = this.installStatus == PluginInstallStatus.Fail;
-
- this.dalamud.PluginRepository.PrintUpdatedPlugins(
- this.updatedPlugins, Loc.Localize("DalamudPluginUpdates", "Updates:"));
-
- this.RefetchPlugins();
- });
- }
- }
- }
-
- ImGui.SameLine();
-
- if (ImGui.Button(Loc.Localize("SettingsInstaller", "Settings")))
- {
- this.dalamud.DalamudUi.OpenSettings();
- }
-
- var closeText = Loc.Localize("Close", "Close");
-
- ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(closeText).X - (16 * ImGui.GetIO().FontGlobalScale));
- if (ImGui.Button(closeText))
- {
- this.IsOpen = false;
- this.dalamud.Configuration.Save();
- }
-
- if (ImGui.BeginPopupModal(Loc.Localize("InstallerError", "Installer failed"), ref this.errorModalDrawing, ImGuiWindowFlags.AlwaysAutoResize))
- {
- var message = Loc.Localize(
- "InstallerErrorHint",
- "The plugin installer ran into an issue or the plugin is incompatible.\nPlease restart the game and report this error on our discord.");
-
- if (this.updatedPlugins != null)
- {
- if (this.updatedPlugins.Any(x => x.WasUpdated == false))
- {
- var extraInfoMessage = Loc.Localize(
- "InstallerErrorPluginInfo",
- "\n\nThe following plugins caused these issues:\n\n{0}\nYou may try removing these plugins manually and reinstalling them.");
-
- var insert = this.updatedPlugins.Where(x => x.WasUpdated == false)
- .Aggregate(
- string.Empty,
- (current, pluginUpdateStatus) =>
- current + $"* {pluginUpdateStatus.InternalName}\n");
- extraInfoMessage = string.Format(extraInfoMessage, insert);
- message += extraInfoMessage;
- }
- }
-
- ImGui.Text(message);
-
- ImGui.Spacing();
-
- if (ImGui.Button(Loc.Localize("OK", "OK"), new Vector2(120, 40)))
- {
- ImGui.CloseCurrentPopup();
- }
-
- ImGui.EndPopup();
- }
-
- if (this.errorModalOnNextFrame)
- {
- ImGui.OpenPopup(Loc.Localize("InstallerError", "Installer failed"));
- this.errorModalOnNextFrame = false;
- }
- }
-
- private void RefetchPlugins()
- {
- var hiddenPlugins = this.dalamud.PluginManager.Plugins.Where(
- x => this.dalamud.PluginRepository.PluginMaster.All(
- y => y.InternalName != x.Definition.InternalName || (y.InternalName == x.Definition.InternalName && y.IsHide))).Select(x => x.Definition).ToList();
- this.pluginListInstalled = this.dalamud.PluginRepository.PluginMaster
- .Where(def =>
- {
- return this.dalamud.PluginManager.Plugins.Where(x => x.Definition != null).Any(
- x => x.Definition.InternalName == def.InternalName);
- })
- .GroupBy(x => new { x.InternalName, x.AssemblyVersion })
- .Select(y => y.First()).ToList();
- this.pluginListInstalled.AddRange(hiddenPlugins);
- this.pluginListInstalled.Sort((x, y) => x.Name.CompareTo(y.Name));
-
- this.ResortPlugins();
- }
-
- private void ResortPlugins()
- {
- var availableDefs = this.dalamud.PluginRepository.PluginMaster.Where(
- x => this.pluginListInstalled.All(y => x.InternalName != y.InternalName))
- .GroupBy(x => new { x.InternalName, x.AssemblyVersion })
- .Select(y => y.First()).ToList();
-
- switch (this.sortKind)
- {
- case PluginSortKind.Alphabetical:
- this.pluginListAvailable = availableDefs.OrderBy(x => x.Name).ToList();
- this.pluginListInstalled.Sort((x, y) => x.Name.CompareTo(y.Name));
- break;
- case PluginSortKind.DownloadCount:
- this.pluginListAvailable = availableDefs.OrderByDescending(x => x.DownloadCount).ToList();
- this.pluginListInstalled.Sort((x, y) => y.DownloadCount.CompareTo(x.DownloadCount));
- break;
- case PluginSortKind.LastUpdate:
- this.pluginListAvailable = availableDefs.OrderByDescending(x => x.LastUpdate).ToList();
- this.pluginListInstalled.Sort((x, y) => y.LastUpdate.CompareTo(x.LastUpdate));
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
- }
-
- private void DrawTab(bool installed, (string Text, Vector4 Color) statusText)
- {
- if (ImGui.BeginTabItem(installed ? Loc.Localize("InstallerInstalledPluginList", "Installed Plugins")
- : Loc.Localize("InstallerAvailablePluginList", "Available Plugins")))
- {
- ImGui.BeginChild(
- "Scrolling" + (installed ? "Installed" : "Available"),
- new Vector2(0, 384 * ImGui.GetIO().FontGlobalScale),
- true,
- ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoBackground);
- ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 5);
-
- if (statusText.Text != null)
- ImGui.TextColored(statusText.Color, statusText.Text);
- var list = installed ? this.pluginListInstalled : this.pluginListAvailable;
- if (list != null)
- {
- this.DrawPluginList(list, installed);
- }
-
- ImGui.EndChild();
- ImGui.EndTabItem();
- }
- }
-
- private void DrawPluginList(List pluginDefinitions, bool installed)
- {
- var didAny = false;
- var didAnyWithSearch = false;
- var hasSearchString = !string.IsNullOrWhiteSpace(this.searchText);
-
- for (var index = 0; index < pluginDefinitions.Count; index++)
- {
- var pluginDefinition = pluginDefinitions[index];
-
- if (pluginDefinition.ApplicableVersion != this.gameVersion &&
- pluginDefinition.ApplicableVersion != "any")
- continue;
-
- if (pluginDefinition.IsHide)
- continue;
-
- if (pluginDefinition.DalamudApiLevel < PluginManager.DalamudApiLevel)
- continue;
-
- if (this.dalamud.Configuration.HiddenPluginInternalName.Contains(pluginDefinition.InternalName))
- continue;
-
- didAny = true;
-
- if (hasSearchString &&
- !(pluginDefinition.Name.ToLowerInvariant().Contains(this.searchText.ToLowerInvariant()) ||
- string.Equals(pluginDefinition.Author, this.searchText, StringComparison.InvariantCultureIgnoreCase) ||
- (pluginDefinition.Tags != null && pluginDefinition.Tags.Contains(
- this.searchText.ToLowerInvariant(),
- StringComparer.InvariantCultureIgnoreCase))))
- continue;
-
- didAnyWithSearch = true;
-
- var isInstalled = this.dalamud.PluginManager.Plugins.Where(x => x.Definition != null).Any(
- x => x.Definition.InternalName == pluginDefinition.InternalName);
-
- var isTestingAvailable = false;
- if (Version.TryParse(pluginDefinition.AssemblyVersion, out var assemblyVersion) &&
- Version.TryParse(pluginDefinition.TestingAssemblyVersion, out var testingAssemblyVersion))
- {
- isTestingAvailable = this.dalamud.Configuration.DoPluginTest &&
- testingAssemblyVersion > assemblyVersion;
- }
-
- if (this.dalamud.Configuration.DoPluginTest && pluginDefinition.IsTestingExclusive)
- isTestingAvailable = true;
- else if (!installed && !this.dalamud.Configuration.DoPluginTest && pluginDefinition.IsTestingExclusive) continue;
-
- var label = string.Empty;
- if (isInstalled && !installed)
- {
- label += Loc.Localize("InstallerInstalled", " (installed)");
- }
- else if (!isInstalled && installed)
- {
- label += Loc.Localize("InstallerDisabled", " (disabled)");
- }
-
- if (this.updatedPlugins != null &&
- this.updatedPlugins.Any(x => x.InternalName == pluginDefinition.InternalName && x.WasUpdated))
- label += Loc.Localize("InstallerUpdated", " (updated)");
- else if (this.updatedPlugins != null &&
- this.updatedPlugins.Any(x => x.InternalName == pluginDefinition.InternalName &&
- x.WasUpdated == false))
- label += Loc.Localize("InstallerUpdateFailed", " (update failed)");
-
- if (isTestingAvailable)
- label += Loc.Localize("InstallerTestingVersion", " (testing version)");
-
- ImGui.PushID(pluginDefinition.InternalName + pluginDefinition.AssemblyVersion + installed + index);
-
- if (ImGui.CollapsingHeader(pluginDefinition.Name + label + "###Header" + pluginDefinition.InternalName))
- {
- ImGui.Indent();
-
- ImGui.Text(pluginDefinition.Name);
-
- ImGui.SameLine();
-
- var info = $" by {pluginDefinition.Author}";
- info += pluginDefinition.DownloadCount != 0
- ? $", {pluginDefinition.DownloadCount} downloads"
- : ", download count unavailable";
- if (pluginDefinition.RepoNumber != 0)
- info += $", from custom plugin repository #{pluginDefinition.RepoNumber}";
- ImGui.TextColored(ImGuiColors.DalamudGrey3, info);
-
- if (!string.IsNullOrWhiteSpace(pluginDefinition.Description))
- ImGui.TextWrapped(pluginDefinition.Description);
-
- if (!isInstalled)
- {
- if (this.installStatus == PluginInstallStatus.InProgress)
- {
- ImGui.Button(Loc.Localize("InstallerInProgress", "Install in progress..."));
- }
- else
- {
- var versionString = isTestingAvailable
- ? pluginDefinition.TestingAssemblyVersion + " (testing version)"
- : pluginDefinition.AssemblyVersion;
-
- if (ImGui.Button($"Install v{versionString}"))
- {
- this.installStatus = PluginInstallStatus.InProgress;
-
- Task.Run(() => this.dalamud.PluginRepository.InstallPlugin(pluginDefinition, true, false, isTestingAvailable)).ContinueWith(t =>
- {
- this.installStatus =
- t.Result ? PluginInstallStatus.Success : PluginInstallStatus.Fail;
- this.installStatus =
- t.IsFaulted ? PluginInstallStatus.Fail : this.installStatus;
-
- this.errorModalDrawing = this.installStatus == PluginInstallStatus.Fail;
- this.errorModalOnNextFrame = this.installStatus == PluginInstallStatus.Fail;
- });
- }
- }
-
- if (!string.IsNullOrEmpty(pluginDefinition.RepoUrl))
- {
- ImGui.PushFont(InterfaceManager.IconFont);
-
- ImGui.SameLine();
- if (ImGui.Button(FontAwesomeIcon.Globe.ToIconString()) &&
- pluginDefinition.RepoUrl.StartsWith("https://"))
- Process.Start(pluginDefinition.RepoUrl);
-
- ImGui.PopFont();
- }
- }
- else
- {
- var installedPlugin = this.dalamud.PluginManager.Plugins.Where(x => x.Definition != null).First(
- x => x.Definition.InternalName ==
- pluginDefinition.InternalName);
-
- var commands = this.dalamud.CommandManager.Commands.Where(
- x => x.Value.LoaderAssemblyName == installedPlugin.Definition?.InternalName &&
- x.Value.ShowInHelp);
- if (commands.Any())
- {
- ImGui.Dummy(new Vector2(10f, 10f) * ImGui.GetIO().FontGlobalScale);
- foreach (var command in commands)
- ImGui.TextWrapped($"{command.Key} → {command.Value.HelpMessage}");
- }
-
- ImGui.NewLine();
-
- if (!installedPlugin.IsRaw)
- {
- ImGui.SameLine();
-
- if (ImGui.Button(Loc.Localize("InstallerDisable", "Disable")))
- {
- try
- {
- this.dalamud.PluginManager.DisablePlugin(installedPlugin.Definition);
- }
- catch (Exception exception)
- {
- Log.Error(exception, "Could not disable plugin.");
- this.errorModalDrawing = true;
- this.errorModalOnNextFrame = true;
- }
- }
- }
-
- if (installedPlugin.PluginInterface.UiBuilder.HasConfigUi)
- {
- ImGui.SameLine();
-
- if (ImGui.Button(Loc.Localize("InstallerOpenConfig", "Open Configuration")))
- installedPlugin.PluginInterface.UiBuilder.OpenConfigUi();
- }
-
- if (!string.IsNullOrEmpty(installedPlugin.Definition.RepoUrl))
- {
- ImGui.PushFont(InterfaceManager.IconFont);
-
- ImGui.SameLine();
- if (ImGui.Button(FontAwesomeIcon.Globe.ToIconString()) &&
- installedPlugin.Definition.RepoUrl.StartsWith("https://"))
- Process.Start(installedPlugin.Definition.RepoUrl);
-
- ImGui.PopFont();
- }
-
- ImGui.SameLine();
- ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{installedPlugin.Definition.AssemblyVersion}");
-
- if (installedPlugin.IsRaw)
- {
- ImGui.SameLine();
- ImGui.TextColored(
- ImGuiColors.DalamudRed,
- this.dalamud.PluginRepository.PluginMaster.Any(x => x.InternalName == installedPlugin.Definition.InternalName)
- ? " This plugin is available in one of your repos, please remove it from the devPlugins folder."
- : " To disable this plugin, please remove it from the devPlugins folder.");
- }
- }
-
- ImGui.Unindent();
- }
-
- if (ImGui.BeginPopupContextItem("item context menu"))
- {
- if (ImGui.Selectable("Hide from installer"))
- this.dalamud.Configuration.HiddenPluginInternalName.Add(pluginDefinition.InternalName);
- ImGui.EndPopup();
- }
-
- ImGui.PopID();
- }
-
- if (!didAny)
- {
- if (installed)
- {
- ImGui.TextColored(
- ImGuiColors.DalamudGrey,
- Loc.Localize(
- "InstallerNoInstalled",
- "No plugins are currently installed. You can install them from the Available Plugins tab."));
- }
- else
- {
- ImGui.TextColored(
- ImGuiColors.DalamudGrey,
- Loc.Localize(
- "InstallerNoCompatible",
- "No compatible plugins were found :( Please restart your game and try again."));
- }
- }
- else if (!didAnyWithSearch)
- {
- ImGui.TextColored(
- ImGuiColors.DalamudGrey2,
- Loc.Localize("InstallNoMatching", "No plugins were found matching your search."));
- }
- }
- }
-}
diff --git a/Dalamud/Plugin/PluginLoadReason.cs b/Dalamud/Plugin/PluginLoadReason.cs
index 789d4d094..846525b0f 100644
--- a/Dalamud/Plugin/PluginLoadReason.cs
+++ b/Dalamud/Plugin/PluginLoadReason.cs
@@ -15,6 +15,16 @@ namespace Dalamud.Plugin
///
Installer,
+ ///
+ /// This plugin was loaded because it was just updated.
+ ///
+ Update,
+
+ ///
+ /// This plugin was loaded because it was told to reload.
+ ///
+ Reload,
+
///
/// This plugin was loaded because the game was started or Dalamud was reinjected.
///
diff --git a/Dalamud/Plugin/PluginManager.cs b/Dalamud/Plugin/PluginManager.cs
deleted file mode 100644
index f45b0fc91..000000000
--- a/Dalamud/Plugin/PluginManager.cs
+++ /dev/null
@@ -1,387 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Dynamic;
-using System.IO;
-using System.Linq;
-using System.Reflection;
-
-using Dalamud.Configuration;
-using Newtonsoft.Json;
-using Serilog;
-
-namespace Dalamud.Plugin
-{
- ///
- /// Class responsible for loading and unloading plugins.
- ///
- internal class PluginManager
- {
- ///
- /// The current Dalamud API level, used to handle breaking changes. Only plugins with this level will be loaded.
- ///
- public const int DalamudApiLevel = 3;
-
- private readonly Dalamud dalamud;
- private readonly string pluginDirectory;
- private readonly string devPluginDirectory;
-
- private readonly PluginConfigurations pluginConfigs;
-
- private readonly Type interfaceType = typeof(IDalamudPlugin);
-
- private readonly List bannedPlugins;
-
- private IEnumerable<(FileInfo DllFile, PluginDefinition Definition, bool IsRaw)> deferredPlugins;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The instance to load plugins with.
- /// The directory for regular plugins.
- /// The directory for dev plugins.
- public PluginManager(Dalamud dalamud, string pluginDirectory, string devPluginDirectory)
- {
- this.dalamud = dalamud;
- this.pluginDirectory = pluginDirectory;
- this.devPluginDirectory = devPluginDirectory;
-
- this.Plugins = new List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface, bool IsRaw)>();
- this.IpcSubscriptions = new List<(string SourcePluginName, string SubPluginName, Action SubAction)>();
-
- this.pluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(dalamud.StartInfo.ConfigurationPath), "pluginConfigs"));
-
- this.bannedPlugins = JsonConvert.DeserializeObject>(
- File.ReadAllText(Path.Combine(this.dalamud.StartInfo.AssetDirectory, "UIRes", "bannedplugin.json")));
-
- // Try to load missing assemblies from the local directory of the requesting assembly
- // This would usually be implicit when using Assembly.Load(), but Assembly.LoadFile() doesn't do it...
- // This handler should only be invoked on things that fail regular lookups, but it *is* global to this appdomain
- AppDomain.CurrentDomain.AssemblyResolve += (object source, ResolveEventArgs e) =>
- {
- try
- {
- Log.Debug($"Resolving missing assembly {e.Name}");
-
- // This looks weird but I'm pretty sure it's actually correct. Pretty sure. Probably.
- var assemblyPath = Path.Combine(
- Path.GetDirectoryName(e.RequestingAssembly.Location),
- new AssemblyName(e.Name).Name + ".dll");
-
- if (!File.Exists(assemblyPath))
- {
- Log.Error($"Assembly not found at {assemblyPath}");
- return null;
- }
-
- return Assembly.LoadFrom(assemblyPath);
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Could not load assembly " + e.Name);
- return null;
- }
- };
- }
-
- ///
- /// Gets a list of all loaded plugins.
- ///
- public List<(IDalamudPlugin Plugin, PluginDefinition Definition, DalamudPluginInterface PluginInterface, bool IsRaw)> Plugins { get; private set; }
-
- ///
- /// Gets a list of all IPC subscriptions.
- ///
- public List<(string SourcePluginName, string SubPluginName, Action SubAction)> IpcSubscriptions { get; private set; }
-
- ///
- /// Unload all plugins.
- ///
- public void UnloadPlugins()
- {
- if (this.Plugins == null)
- return;
-
- for (var i = 0; i < this.Plugins.Count; i++)
- {
- this.Plugins[i].Plugin.Dispose();
- }
-
- this.Plugins.Clear();
- }
-
- ///
- /// Load plugins that need to be loaded synchronously and prepare plugins that can be loaded asynchronously.
- ///
- public void LoadSynchronousPlugins()
- {
- var loadDirectories = new List<(DirectoryInfo DirInfo, bool IsRaw)>
- {
- (new DirectoryInfo(this.pluginDirectory), false),
- (new DirectoryInfo(this.devPluginDirectory), true),
- };
-
- var pluginDefs = new List<(FileInfo DllFile, PluginDefinition Definition, bool IsRaw)>();
- foreach (var (dirInfo, isRaw) in loadDirectories)
- {
- if (!dirInfo.Exists) continue;
-
- var pluginDlls = dirInfo.GetFiles("*.dll", SearchOption.AllDirectories).Where(x => x.Extension == ".dll");
-
- // Preload definitions to be able to determine load order
- foreach (var dllFile in pluginDlls)
- {
- var defJson = new FileInfo(Path.Combine(dllFile.Directory.FullName, $"{Path.GetFileNameWithoutExtension(dllFile.Name)}.json"));
- PluginDefinition def = null;
- if (defJson.Exists)
- def = JsonConvert.DeserializeObject(File.ReadAllText(defJson.FullName));
- pluginDefs.Add((dllFile, def, isRaw));
- }
- }
-
- // Sort for load order - unloaded definitions have default priority of 0
- pluginDefs.Sort(
- (info1, info2) =>
- {
- var prio1 = info1.Definition?.LoadPriority ?? 0;
- var prio2 = info2.Definition?.LoadPriority ?? 0;
- return prio2.CompareTo(prio1);
- });
-
- this.deferredPlugins = pluginDefs.Where(x => x.Definition == null || x.Definition.LoadPriority <= 0);
-
- // Pass preloaded definitions for "synchronous load" plugins to LoadPluginFromAssembly, because we already loaded them anyways
- foreach (var (dllFile, definition, isRaw) in pluginDefs.Where(x => x.Definition?.LoadPriority > 0))
- {
- try
- {
- this.LoadPluginFromAssembly(dllFile, isRaw, PluginLoadReason.Boot, definition);
- }
- catch (Exception ex)
- {
- Log.Error(ex, $"Plugin load for {dllFile.FullName} failed.");
- if (ex is ReflectionTypeLoadException typeLoadException)
- {
- foreach (var exception in typeLoadException.LoaderExceptions)
- {
- Log.Error(exception, "LoaderException:");
- }
- }
- }
- }
- }
-
- ///
- /// Load plugins that have been explicitly deferred.
- ///
- public void LoadDeferredPlugins()
- {
- if (this.deferredPlugins == null)
- throw new Exception("Synchronous plugins need to be loaded before deferred plugins.");
-
- // Pass preloaded definitions for "deferred load" plugins to LoadPluginFromAssembly, because we already loaded them anyways
- foreach (var (dllFile, definition, isRaw) in this.deferredPlugins)
- {
- try
- {
- this.LoadPluginFromAssembly(dllFile, isRaw, PluginLoadReason.Boot, definition);
- }
- catch (Exception ex)
- {
- Log.Error(ex, $"Plugin load for {dllFile.FullName} failed.");
- if (ex is ReflectionTypeLoadException typeLoadException)
- {
- foreach (var exception in typeLoadException.LoaderExceptions)
- {
- Log.Error(exception, "LoaderException:");
- }
- }
- }
- }
- }
-
- ///
- /// Disable/unload a single plugin.
- ///
- /// The plugin definition of the plugin to be disabled/unloaded.
- public void DisablePlugin(PluginDefinition definition)
- {
- var thisPlugin = this.Plugins.Where(x => x.Definition != null)
- .First(x => x.Definition.InternalName == definition.InternalName);
-
- var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory, definition.InternalName, definition.AssemblyVersion));
-
- // Need to do it with Open so the file handle gets closed immediately
- // TODO: Don't use the ".disabled" crap, do it in a config
- try
- {
- File.Open(Path.Combine(outputDir.FullName, ".disabled"), FileMode.Create).Close();
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Could not create the .disabled file, disabling all versions...");
- foreach (var version in outputDir.Parent.GetDirectories())
- {
- if (!File.Exists(Path.Combine(version.FullName, ".disabled")))
- File.Open(Path.Combine(version.FullName, ".disabled"), FileMode.Create).Close();
- }
- }
-
- thisPlugin.Plugin.Dispose();
-
- this.Plugins.Remove(thisPlugin);
- }
-
- ///
- /// Load a plugin from an assembly.
- ///
- /// The associated with the main assembly of this plugin.
- /// Whether or not the plugin is a dev plugin.
- /// The reason this plugin was loaded.
- /// The already loaded definition, if available.
- /// Whether or not the plugin was loaded successfully.
- public bool LoadPluginFromAssembly(FileInfo dllFile, bool isRaw, PluginLoadReason reason, PluginDefinition pluginDef = null)
- {
- Log.Information("Loading plugin at {0}", dllFile.Directory.FullName);
-
- // If this entire folder has been marked as a disabled plugin, don't even try to load anything
- var disabledFile = new FileInfo(Path.Combine(dllFile.Directory.FullName, ".disabled"));
-
- // should raw/dev plugins really not respect this?
- if (disabledFile.Exists && !isRaw)
- {
- Log.Information("Plugin {0} is disabled.", dllFile.FullName);
- return false;
- }
-
- var testingFile = new FileInfo(Path.Combine(dllFile.Directory.FullName, ".testing"));
- if (testingFile.Exists && !this.dalamud.Configuration.DoPluginTest)
- {
- Log.Information("Plugin {0} was testing, but testing is disabled.", dllFile.FullName);
- return false;
- }
-
- // Preloaded
- if (pluginDef == null)
- {
- // read the plugin def if present - again, fail before actually trying to load the dll if there is a problem
- var defJsonFile = new FileInfo(Path.Combine(dllFile.Directory.FullName, $"{Path.GetFileNameWithoutExtension(dllFile.Name)}.json"));
-
- // load the definition if it exists, even for raw/developer plugins
- if (defJsonFile.Exists)
- {
- Log.Information("Loading definition for plugin DLL {0}", dllFile.FullName);
-
- pluginDef =
- JsonConvert.DeserializeObject(
- File.ReadAllText(defJsonFile.FullName));
- }
- }
-
- // Perform checks
- if (!isRaw)
- {
- if (pluginDef != null)
- {
- if (pluginDef.ApplicableVersion != this.dalamud.StartInfo.GameVersion &&
- pluginDef.ApplicableVersion != "any")
- {
- Log.Information("Plugin {0} has not applicable version.", dllFile.FullName);
- return false;
- }
-
- if (pluginDef.DalamudApiLevel < DalamudApiLevel)
- {
- Log.Error("Incompatible API level: {0}", dllFile.FullName);
- return false;
- }
-
- if (this.bannedPlugins.Any(x => x.Name == pluginDef.InternalName &&
- x.AssemblyVersion == pluginDef.AssemblyVersion))
- {
- Log.Error($"[PLUGINM] Banned plugin {pluginDef.InternalName} {pluginDef.AssemblyVersion}");
- return false;
- }
- }
- else
- {
- Log.Information("Plugin DLL {0} has no definition.", dllFile.FullName);
- return false;
- }
- }
-
- // TODO: given that it exists, the pluginDef's InternalName should probably be used
- // as the actual assembly to load
- // But plugins should also probably be loaded by directory and not by looking for every dll
- Log.Information("Loading assembly at {0}", dllFile);
-
- // Assembly.Load() by name here will not load multiple versions with the same name, in the case of updates
- var pluginAssembly = Assembly.LoadFile(dllFile.FullName);
-
- Log.Information("Loading types for {0}", pluginAssembly.FullName);
- var types = pluginAssembly.GetTypes();
- foreach (var type in types)
- {
- if (type.IsInterface || type.IsAbstract)
- {
- continue;
- }
-
- if (type.GetInterface(this.interfaceType.FullName) != null)
- {
- if (this.Plugins.Any(x => x.Plugin.GetType().Assembly.GetName().Name == type.Assembly.GetName().Name))
- {
- Log.Error("Duplicate plugin found: {0}", dllFile.FullName);
- return false;
- }
-
- Log.Verbose("Plugin CreateInstance...");
-
- var plugin = (IDalamudPlugin)Activator.CreateInstance(type);
-
- // this happens for raw plugins that don't specify a PluginDefinition - just generate a dummy one to avoid crashes anywhere
- pluginDef ??= new PluginDefinition
- {
- Author = "developer",
- Name = plugin.Name,
- InternalName = Path.GetFileNameWithoutExtension(dllFile.Name),
- AssemblyVersion = plugin.GetType().Assembly.GetName().Version.ToString(),
- Description = string.Empty,
- ApplicableVersion = "any",
- IsHide = false,
- DalamudApiLevel = DalamudApiLevel,
- };
-
- Log.Verbose("Plugin Initialize...");
-
- var dalamudInterface = new DalamudPluginInterface(this.dalamud, type.Assembly.GetName().Name, this.pluginConfigs, reason);
- plugin.Initialize(dalamudInterface);
-
- Log.Information("Loaded plugin: {0}", plugin.Name);
- this.Plugins.Add((plugin, pluginDef, dalamudInterface, isRaw));
-
- return true;
- }
- }
-
- Log.Information("Plugin DLL {0} has no plugin interface.", dllFile.FullName);
-
- return false;
- }
-
- ///
- /// Unload and reload all plugins.
- ///
- public void ReloadPlugins()
- {
- this.UnloadPlugins();
- this.LoadSynchronousPlugins();
- }
-
- private class BannedPlugin
- {
- public string Name { get; set; }
-
- public string AssemblyVersion { get; set; }
- }
- }
-}
diff --git a/Dalamud/Plugin/PluginRepository.cs b/Dalamud/Plugin/PluginRepository.cs
deleted file mode 100644
index fe936f94f..000000000
--- a/Dalamud/Plugin/PluginRepository.cs
+++ /dev/null
@@ -1,571 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Collections.ObjectModel;
-using System.IO;
-using System.IO.Compression;
-using System.Linq;
-using System.Net;
-using System.Reflection;
-using System.Text;
-using System.Threading.Tasks;
-
-using CheapLoc;
-using Dalamud.Game.Text;
-using Newtonsoft.Json;
-using Serilog;
-
-namespace Dalamud.Plugin
-{
- ///
- /// This class represents a single plugin repository.
- ///
- internal class PluginRepository
- {
- private const string PluginMasterUrl = "https://raw.githubusercontent.com/goatcorp/DalamudPlugins/master/pluginmaster.json";
-
- private readonly Dalamud dalamud;
- private string pluginDirectory;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The Dalamud instance.
- /// The plugin directory path.
- /// The current game version.
- public PluginRepository(Dalamud dalamud, string pluginDirectory, string gameVersion)
- {
- this.dalamud = dalamud;
- this.pluginDirectory = pluginDirectory;
-
- this.ReloadPluginMasterAsync();
- }
-
- ///
- /// Values representing plugin initialization state.
- ///
- public enum InitializationState
- {
- ///
- /// State is unknown.
- ///
- Unknown,
-
- ///
- /// State is in progress.
- ///
- InProgress,
-
- ///
- /// State is successful.
- ///
- Success,
-
- ///
- /// State is failure.
- ///
- Fail,
-
- ///
- /// State is failure, for a 3rd party repo plugin.
- ///
- FailThirdRepo,
- }
-
- ///
- /// Gets the plugin master list of available plugins.
- ///
- public ReadOnlyCollection PluginMaster { get; private set; }
-
- ///
- /// Gets the initialization state of the plugin repository.
- ///
- public InitializationState State { get; private set; }
-
- ///
- /// Reload the plugin master asynchronously in a task.
- ///
- public void ReloadPluginMasterAsync()
- {
- this.State = InitializationState.InProgress;
-
- Task.Run(() =>
- {
- this.PluginMaster = null;
-
- var allPlugins = new List();
-
- var repos = this.dalamud.Configuration.ThirdRepoList.Where(x => x.IsEnabled).Select(x => x.Url)
- .Prepend(PluginMasterUrl).ToArray();
-
- using var client = new WebClient();
-
- var repoNumber = 0;
- var anyError = false;
- foreach (var repo in repos)
- {
- Log.Information("[PLUGINR] Fetching repo: {0}", repo);
-
- try
- {
- var data = client.DownloadString(repo);
-
- var unsortedPluginMaster = JsonConvert.DeserializeObject>(data);
-
- foreach (var pluginDefinition in unsortedPluginMaster)
- {
- pluginDefinition.RepoNumber = repoNumber;
- }
-
- allPlugins.AddRange(unsortedPluginMaster);
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Could not download PluginMaster");
-
- this.State = repos.Length > 1 ? InitializationState.FailThirdRepo : InitializationState.Fail;
-
- anyError = true;
- }
-
- repoNumber++;
- }
-
- this.PluginMaster = allPlugins.AsReadOnly();
- if (!anyError)
- {
- this.State = InitializationState.Success;
- }
- }).ContinueWith(t =>
- {
- if (t.IsFaulted)
- this.State = InitializationState.Fail;
- });
- }
-
- ///
- /// Install a plugin.
- ///
- /// The plugin definition.
- /// Whether the plugin should be immediately enabled.
- /// Whether this install is an update.
- /// Whether this install is flagged as testing.
- /// Success or failure.
- public bool InstallPlugin(PluginDefinition definition, bool enableAfterInstall = true, bool isUpdate = false, bool fromTesting = false)
- {
- try
- {
- using var client = new WebClient();
-
- var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory, definition.InternalName, fromTesting ? definition.TestingAssemblyVersion : definition.AssemblyVersion));
- var dllFile = new FileInfo(Path.Combine(outputDir.FullName, $"{definition.InternalName}.dll"));
- var disabledFile = new FileInfo(Path.Combine(outputDir.FullName, ".disabled"));
- var testingFile = new FileInfo(Path.Combine(outputDir.FullName, ".testing"));
- var wasDisabled = disabledFile.Exists;
-
- if (dllFile.Exists && enableAfterInstall)
- {
- if (disabledFile.Exists)
- disabledFile.Delete();
-
- return this.dalamud.PluginManager.LoadPluginFromAssembly(dllFile, false, PluginLoadReason.Installer);
- }
-
- if (dllFile.Exists && !enableAfterInstall)
- {
- return true;
- }
-
- try
- {
- if (outputDir.Exists)
- outputDir.Delete(true);
- outputDir.Create();
- }
- catch
- {
- // ignored, since the plugin may be loaded already
- }
-
- var path = Path.GetTempFileName();
-
- var doTestingDownload = false;
- if ((Version.TryParse(definition.TestingAssemblyVersion, out var testingAssemblyVer) || definition.IsTestingExclusive)
- && fromTesting)
- {
- doTestingDownload = testingAssemblyVer > Version.Parse(definition.AssemblyVersion) || definition.IsTestingExclusive;
- }
-
- var url = definition.DownloadLinkInstall;
- if (doTestingDownload)
- url = definition.DownloadLinkTesting;
- else if (isUpdate)
- url = definition.DownloadLinkUpdate;
-
- Log.Information("Downloading plugin to {0} from {1} doTestingDownload:{2} isTestingExclusive:{3}", path, url, doTestingDownload, definition.IsTestingExclusive);
-
- client.DownloadFile(url, path);
-
- Log.Information("Extracting to {0}", outputDir);
-
- ZipFile.ExtractToDirectory(path, outputDir.FullName);
-
- if (wasDisabled || !enableAfterInstall)
- {
- disabledFile.Create().Close();
- return true;
- }
-
- if (doTestingDownload)
- {
- testingFile.Create().Close();
- }
- else
- {
- if (testingFile.Exists)
- testingFile.Delete();
- }
-
- return this.dalamud.PluginManager.LoadPluginFromAssembly(dllFile, false, PluginLoadReason.Installer);
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Plugin download failed hard.");
- if (ex is ReflectionTypeLoadException typeLoadException)
- {
- foreach (var exception in typeLoadException.LoaderExceptions)
- {
- Log.Error(exception, "LoaderException:");
- }
- }
-
- return false;
- }
- }
-
- ///
- /// Update all plugins.
- ///
- /// Perform a dry run of the update and skip the actual installation.
- /// A tuple of whether the update was successful and the list of updated plugins.
- public (bool Success, List UpdatedPlugins) UpdatePlugins(bool dryRun = false)
- {
- Log.Information("Starting plugin update... dry:{0}", dryRun);
-
- var updatedList = new List();
- var hasError = false;
-
- try
- {
- var pluginsDirectory = new DirectoryInfo(this.pluginDirectory);
- foreach (var installed in pluginsDirectory.GetDirectories())
- {
- try
- {
- var versions = installed.GetDirectories();
-
- if (versions.Length == 0)
- {
- Log.Information("Has no versions: {0}", installed.FullName);
- continue;
- }
-
- var sortedVersions = versions.OrderBy(dirInfo =>
- {
- var success = Version.TryParse(dirInfo.Name, out var version);
- if (!success)
- {
- Log.Debug("Unparseable version: {0}", dirInfo.Name);
- }
-
- return version;
- });
- var latest = sortedVersions.Last();
-
- var isEnabled = !File.Exists(Path.Combine(latest.FullName, ".disabled"));
- if (!isEnabled && File.Exists(Path.Combine(latest.FullName, ".testing")))
- {
- // In case testing is installed, but stable is enabled
- foreach (var version in versions)
- {
- if (!File.Exists(Path.Combine(version.FullName, ".disabled")))
- {
- isEnabled = true;
- break;
- }
- }
- }
-
- if (!isEnabled)
- {
- Log.Verbose("Is disabled: {0}", installed.FullName);
- continue;
- }
-
- var localInfoFile = new FileInfo(Path.Combine(latest.FullName, $"{installed.Name}.json"));
-
- if (!localInfoFile.Exists)
- {
- Log.Information("Has no definition: {0}", localInfoFile.FullName);
- continue;
- }
-
- var info = JsonConvert.DeserializeObject(
- File.ReadAllText(localInfoFile.FullName));
-
- var remoteInfo = this.PluginMaster.FirstOrDefault(x => x.InternalName == info.InternalName);
-
- if (remoteInfo == null)
- {
- Log.Information("Is not in pluginmaster: {0}", info.Name);
- continue;
- }
-
- if (remoteInfo.DalamudApiLevel < PluginManager.DalamudApiLevel)
- {
- Log.Information("Has not applicable API level: {0}", info.Name);
- continue;
- }
-
- Version.TryParse(remoteInfo.AssemblyVersion, out var remoteAssemblyVer);
- Version.TryParse(info.AssemblyVersion, out var localAssemblyVer);
-
- var testingAvailable = false;
- if (!string.IsNullOrEmpty(remoteInfo.TestingAssemblyVersion))
- {
- Version.TryParse(remoteInfo.TestingAssemblyVersion, out var testingAssemblyVer);
- testingAvailable = testingAssemblyVer > localAssemblyVer && this.dalamud.Configuration.DoPluginTest;
- }
-
- if (remoteAssemblyVer > localAssemblyVer || testingAvailable)
- {
- Log.Information("Eligible for update: {0}", remoteInfo.InternalName);
-
- // DisablePlugin() below immediately creates a .disabled file anyway, but will fail
- // with an exception if we try to do it twice in row like this
-
- if (!dryRun)
- {
- var wasLoaded =
- this.dalamud.PluginManager.Plugins.Where(x => x.Definition != null).Any(
- x => x.Definition.InternalName == info.InternalName);
-
- Log.Verbose("isEnabled: {0} / wasLoaded: {1}", isEnabled, wasLoaded);
-
- // Try to disable plugin if it is loaded
- if (wasLoaded)
- {
- try
- {
- this.dalamud.PluginManager.DisablePlugin(info);
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Plugin disable failed");
- // hasError = true;
- }
- }
-
- try
- {
- // Just to be safe
- foreach (var sortedVersion in sortedVersions)
- {
- var disabledFile =
- new FileInfo(Path.Combine(sortedVersion.FullName, ".disabled"));
- if (!disabledFile.Exists)
- disabledFile.Create().Close();
- }
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Plugin disable old versions failed");
- }
-
- var installSuccess = this.InstallPlugin(remoteInfo, isEnabled, true, testingAvailable);
-
- if (!installSuccess)
- {
- Log.Error("InstallPlugin failed.");
- hasError = true;
- }
-
- updatedList.Add(new PluginUpdateStatus
- {
- InternalName = remoteInfo.InternalName,
- Name = remoteInfo.Name,
- Version = testingAvailable ? remoteInfo.TestingAssemblyVersion : remoteInfo.AssemblyVersion,
- WasUpdated = installSuccess,
- });
- }
- else
- {
- updatedList.Add(new PluginUpdateStatus
- {
- InternalName = remoteInfo.InternalName,
- Name = remoteInfo.Name,
- Version = testingAvailable ? remoteInfo.TestingAssemblyVersion : remoteInfo.AssemblyVersion,
- WasUpdated = true,
- });
- }
- }
- else
- {
- Log.Information("Up to date: {0}", remoteInfo.InternalName);
- }
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Could not update plugin: {0}", installed.FullName);
- }
- }
- }
- catch (Exception ex)
- {
- Log.Error(ex, "Plugin update failed.");
- hasError = true;
- }
-
- Log.Information("Plugin update OK.");
-
- return (!hasError, updatedList);
- }
-
- ///
- /// Print to chat any plugin updates and whether they were successful.
- ///
- /// The list of updated plugins.
- /// The header text to send to chat prior to any update info.
- public void PrintUpdatedPlugins(List updatedPlugins, string header)
- {
- if (updatedPlugins != null && updatedPlugins.Any())
- {
- this.dalamud.Framework.Gui.Chat.Print(header);
- foreach (var plugin in updatedPlugins)
- {
- if (plugin.WasUpdated)
- {
- this.dalamud.Framework.Gui.Chat.Print(string.Format(Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}."), plugin.Name, plugin.Version));
- }
- else
- {
- this.dalamud.Framework.Gui.Chat.PrintChat(new XivChatEntry
- {
- MessageBytes = Encoding.UTF8.GetBytes(string.Format(Loc.Localize("DalamudPluginUpdateFailed", " 》 {0} update to v{1} failed."), plugin.Name, plugin.Version)),
- Type = XivChatType.Urgent,
- });
- }
- }
- }
- }
-
- ///
- /// Cleanup disabled plugins.
- ///
- public void CleanupPlugins()
- {
- try
- {
- var pluginsDirectory = new DirectoryInfo(this.pluginDirectory);
- foreach (var installed in pluginsDirectory.GetDirectories())
- {
- var versions = installed.GetDirectories();
-
- var sortedVersions = versions.OrderBy(dirInfo =>
- {
- var success = Version.TryParse(dirInfo.Name, out var version);
- if (!success)
- {
- Log.Debug("Unparseable version: {0}", dirInfo.Name);
- }
-
- return version;
- }).ToArray();
-
- foreach (var version in sortedVersions)
- {
- try
- {
- var disabledFile = new FileInfo(Path.Combine(version.FullName, ".disabled"));
- var definition = JsonConvert.DeserializeObject(
- File.ReadAllText(Path.Combine(version.FullName, version.Parent.Name + ".json")));
-
- if (disabledFile.Exists)
- {
- Log.Information("[PLUGINR] Disabled: cleaning up {0} at {1}", installed.Name, version.FullName);
- try
- {
- version.Delete(true);
- }
- catch (Exception ex)
- {
- Log.Error(ex, $"[PLUGINR] Could not clean up {disabledFile.FullName}");
- }
- }
-
- if (definition.DalamudApiLevel < PluginManager.DalamudApiLevel - 1)
- {
- Log.Information("[PLUGINR] Lower API: cleaning up {0} at {1}", installed.Name, version.FullName);
- try
- {
- version.Delete(true);
- }
- catch (Exception ex)
- {
- Log.Error(ex, $"[PLUGINR] Could not clean up {disabledFile.FullName}");
- }
- }
- }
- catch (Exception ex)
- {
- Log.Error(ex, $"[PLUGINR] Could not clean up {version.FullName}");
- }
-
- if (installed.GetDirectories().Length == 0)
- {
- Log.Information("[PLUGINR] Has no versions, cleaning up: {0}", installed.FullName);
-
- try
- {
- installed.Delete();
- }
- catch (Exception ex)
- {
- Log.Error(ex, $"[PLUGINR] Could not clean up {installed.FullName}");
- }
- }
- }
- }
- }
- catch (Exception ex)
- {
- Log.Error(ex, "[PLUGINR] Plugin cleanup failed.");
- }
- }
-
- ///
- /// Plugin update status.
- ///
- internal class PluginUpdateStatus
- {
- ///
- /// Gets or sets the plugin internal name.
- ///
- public string InternalName { get; set; }
-
- ///
- /// Gets or sets the plugin name.
- ///
- public string Name { get; set; }
-
- ///
- /// Gets or sets the plugin version.
- ///
- public string Version { get; set; }
-
- ///
- /// Gets or sets a value indicating whether the plugin was updated.
- ///
- public bool WasUpdated { get; set; }
- }
- }
-}
diff --git a/Dalamud/Properties/Resources.Designer.cs b/Dalamud/Properties/Resources.Designer.cs
index f8904c30a..aa3d29241 100644
--- a/Dalamud/Properties/Resources.Designer.cs
+++ b/Dalamud/Properties/Resources.Designer.cs
@@ -19,7 +19,7 @@ namespace Dalamud.Properties {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
diff --git a/Dalamud/Resources/Lumina.Generated.dll b/Dalamud/Resources/Lumina.Generated.dll
deleted file mode 100644
index 301694057..000000000
Binary files a/Dalamud/Resources/Lumina.Generated.dll and /dev/null differ
diff --git a/Dalamud/SafeMemory.cs b/Dalamud/SafeMemory.cs
index 458cc6824..362291506 100644
--- a/Dalamud/SafeMemory.cs
+++ b/Dalamud/SafeMemory.cs
@@ -1,5 +1,4 @@
using System;
-using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
@@ -17,12 +16,10 @@ namespace Dalamud
public static class SafeMemory
{
private static readonly IntPtr Handle;
- private static readonly IntPtr MainModule;
static SafeMemory()
{
Handle = Imports.GetCurrentProcess();
- MainModule = Process.GetCurrentProcess().MainModule?.BaseAddress ?? IntPtr.Zero;
}
///
diff --git a/Dalamud/Troubleshooting.cs b/Dalamud/Troubleshooting.cs
index d8137165a..69b0df23e 100644
--- a/Dalamud/Troubleshooting.cs
+++ b/Dalamud/Troubleshooting.cs
@@ -1,14 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Text;
using Dalamud.Configuration;
using Dalamud.Plugin;
+using Dalamud.Plugin.Internal.Types;
using Newtonsoft.Json;
using Serilog;
-using Encoding = System.Text.Encoding;
-
namespace Dalamud
{
///
@@ -27,9 +27,10 @@ namespace Dalamud
{
var payload = new TroubleshootingPayload
{
- LoadedPlugins = dalamud.PluginManager.Plugins.Select(x => x.Definition).ToArray(),
+ LoadedPlugins = dalamud.PluginManager.InstalledPlugins.Select(x => x.Manifest).ToArray(),
DalamudVersion = Util.AssemblyVersion,
- GameVersion = dalamud.StartInfo.GameVersion,
+ DalamudGitHash = Util.GetGitHash(),
+ GameVersion = dalamud.StartInfo.GameVersion.ToString(),
Language = dalamud.StartInfo.Language.ToString(),
DoDalamudTest = dalamud.Configuration.DoDalamudTest,
DoPluginTest = dalamud.Configuration.DoPluginTest,
@@ -48,10 +49,12 @@ namespace Dalamud
private class TroubleshootingPayload
{
- public PluginDefinition[] LoadedPlugins { get; set; }
+ public PluginManifest[] LoadedPlugins { get; set; }
public string DalamudVersion { get; set; }
+ public string DalamudGitHash { get; set; }
+
public string GameVersion { get; set; }
public string Language { get; set; }
@@ -62,7 +65,7 @@ namespace Dalamud
public bool InterfaceLoaded { get; set; }
- public List ThirdRepo { get; set; }
+ public List ThirdRepo { get; set; }
}
}
}
diff --git a/Dalamud/Util.cs b/Dalamud/Util.cs
index 456c93689..3084e1bab 100644
--- a/Dalamud/Util.cs
+++ b/Dalamud/Util.cs
@@ -161,8 +161,8 @@ namespace Dalamud
public static void Fatal(string message, string caption)
{
var flags = NativeFunctions.MessageBoxType.Ok | NativeFunctions.MessageBoxType.IconError;
+ _ = NativeFunctions.MessageBoxW(Process.GetCurrentProcess().MainWindowHandle, message, caption, flags);
- NativeFunctions.MessageBox(Process.GetCurrentProcess().MainWindowHandle, message, caption, flags);
Environment.Exit(-1);
}
@@ -196,5 +196,13 @@ namespace Dalamud
// TODO: Someone implement GetUTF8String with some IntPtr overloads.
// while(Marshal.ReadByte(0, sz) != 0) { sz++; }
+
+ ///
+ /// An extension method to chain usage of string.Format.
+ ///
+ /// Format string.
+ /// Format arguments.
+ /// Formatted string.
+ public static string Format(this string format, params object[] args) => string.Format(format, args);
}
}
diff --git a/Dalamud/corehook64.dll b/Dalamud/corehook64.dll
new file mode 100644
index 000000000..9b21a40d1
Binary files /dev/null and b/Dalamud/corehook64.dll differ
diff --git a/Dalamud/stylecop.json b/Dalamud/stylecop.json
deleted file mode 100644
index 6881efc6d..000000000
--- a/Dalamud/stylecop.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
- "settings": {
- "orderingRules": {
- "systemUsingDirectivesFirst": true,
- "usingDirectivesPlacement": "outsideNamespace",
- "blankLinesBetweenUsingGroups": "require"
- },
- "maintainabilityRules": {
- "topLevelTypes": [ "class", "interface", "struct", "enum" ]
- }
- }
-}
diff --git a/README.md b/README.md
index f85b9cc30..7069facfa 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,15 @@ If you need any support regarding the API or usage of Dalamud, please [join our
Thanks to Mino, whose work has made this possible!
+## Branches
+
+We are currently working from the following branches.
+
+| Name | Purpose | .NET Version |
+|---|---|---|
+| *master* | .NET Core rework, in-development, will replace stable | .NET 5.0.6 (May 2021) |
+| *api3* | Legacy, current stable version, to be replaced in November | .NET 4.7.5 (April 2017) |
+
##### Final Fantasy XIV © 2010-2021 SQUARE ENIX CO., LTD. All Rights Reserved. We are not affiliated with SQUARE ENIX CO., LTD. in any way.
diff --git a/Resources/EasyHook32.dll b/Resources/EasyHook32.dll
deleted file mode 100644
index 135609b46..000000000
Binary files a/Resources/EasyHook32.dll and /dev/null differ
diff --git a/Resources/EasyHook32Svc.exe b/Resources/EasyHook32Svc.exe
deleted file mode 100644
index 0d470eca4..000000000
Binary files a/Resources/EasyHook32Svc.exe and /dev/null differ
diff --git a/Resources/EasyHook64.dll b/Resources/EasyHook64.dll
deleted file mode 100644
index 4fb1331aa..000000000
Binary files a/Resources/EasyHook64.dll and /dev/null differ
diff --git a/Resources/EasyHook64Svc.exe b/Resources/EasyHook64Svc.exe
deleted file mode 100644
index 770663386..000000000
Binary files a/Resources/EasyHook64Svc.exe and /dev/null differ
diff --git a/Resources/EasyLoad32.dll b/Resources/EasyLoad32.dll
deleted file mode 100644
index 5b30daaa7..000000000
Binary files a/Resources/EasyLoad32.dll and /dev/null differ
diff --git a/Resources/EasyLoad64.dll b/Resources/EasyLoad64.dll
deleted file mode 100644
index 1696334ff..000000000
Binary files a/Resources/EasyLoad64.dll and /dev/null differ
diff --git a/build.cmd b/build.cmd
new file mode 100644
index 000000000..b08cc590f
--- /dev/null
+++ b/build.cmd
@@ -0,0 +1,7 @@
+:; set -eo pipefail
+:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
+:; ${SCRIPT_DIR}/build.sh "$@"
+:; exit $?
+
+@ECHO OFF
+powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %*
diff --git a/build.ps1 b/build.ps1
new file mode 100644
index 000000000..c8aea5cc8
--- /dev/null
+++ b/build.ps1
@@ -0,0 +1,69 @@
+[CmdletBinding()]
+Param(
+ [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
+ [string[]]$BuildArguments
+)
+
+Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)"
+
+Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 }
+$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
+
+###########################################################################
+# CONFIGURATION
+###########################################################################
+
+$BuildProjectFile = "$PSScriptRoot\build\build.csproj"
+$TempDirectory = "$PSScriptRoot\\.nuke\temp"
+
+$DotNetGlobalFile = "$PSScriptRoot\\global.json"
+$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1"
+$DotNetChannel = "Current"
+
+$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1
+$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1
+$env:DOTNET_MULTILEVEL_LOOKUP = 0
+
+###########################################################################
+# EXECUTION
+###########################################################################
+
+function ExecSafe([scriptblock] $cmd) {
+ & $cmd
+ if ($LASTEXITCODE) { exit $LASTEXITCODE }
+}
+
+# If dotnet CLI is installed globally and it matches requested version, use for execution
+if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and `
+ $(dotnet --version) -and $LASTEXITCODE -eq 0) {
+ $env:DOTNET_EXE = (Get-Command "dotnet").Path
+}
+else {
+ # Download install script
+ $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1"
+ New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile)
+
+ # If global.json exists, load expected version
+ if (Test-Path $DotNetGlobalFile) {
+ $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json)
+ if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) {
+ $DotNetVersion = $DotNetGlobal.sdk.version
+ }
+ }
+
+ # Install by channel or version
+ $DotNetDirectory = "$TempDirectory\dotnet-win"
+ if (!(Test-Path variable:DotNetVersion)) {
+ ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
+ } else {
+ ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
+ }
+ $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe"
+}
+
+Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)"
+
+ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet }
+ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments }
diff --git a/build.sh b/build.sh
new file mode 100644
index 000000000..eca8e308c
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,62 @@
+#!/usr/bin/env bash
+
+bash --version 2>&1 | head -n 1
+
+set -eo pipefail
+SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
+
+###########################################################################
+# CONFIGURATION
+###########################################################################
+
+BUILD_PROJECT_FILE="$SCRIPT_DIR/build/build.csproj"
+TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp"
+
+DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json"
+DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh"
+DOTNET_CHANNEL="Current"
+
+export DOTNET_CLI_TELEMETRY_OPTOUT=1
+export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
+export DOTNET_MULTILEVEL_LOOKUP=0
+
+###########################################################################
+# EXECUTION
+###########################################################################
+
+function FirstJsonValue {
+ perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}"
+}
+
+# If dotnet CLI is installed globally and it matches requested version, use for execution
+if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then
+ export DOTNET_EXE="$(command -v dotnet)"
+else
+ # Download install script
+ DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh"
+ mkdir -p "$TEMP_DIRECTORY"
+ curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL"
+ chmod +x "$DOTNET_INSTALL_FILE"
+
+ # If global.json exists, load expected version
+ if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then
+ DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")")
+ if [[ "$DOTNET_VERSION" == "" ]]; then
+ unset DOTNET_VERSION
+ fi
+ fi
+
+ # Install by channel or version
+ DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
+ if [[ -z ${DOTNET_VERSION+x} ]]; then
+ "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path
+ else
+ "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path
+ fi
+ export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet"
+fi
+
+echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)"
+
+"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet
+"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"
diff --git a/build/.editorconfig b/build/.editorconfig
new file mode 100644
index 000000000..06b0d10a7
--- /dev/null
+++ b/build/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*.cs]
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+dotnet_style_require_accessibility_modifiers = never:warning
+dotnet_diagnostic.CA1822.severity=silent
+dotnet_diagnostic.CA1050.severity=silent
+
+csharp_style_expression_bodied_methods = true:silent
+csharp_style_expression_bodied_properties = true:warning
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_accessors = true:warning
diff --git a/build/Configuration.cs b/build/Configuration.cs
new file mode 100644
index 000000000..f4f6b4a51
--- /dev/null
+++ b/build/Configuration.cs
@@ -0,0 +1,14 @@
+using System.ComponentModel;
+using Nuke.Common.Tooling;
+
+[TypeConverter(typeof(TypeConverter))]
+public class Configuration : Enumeration
+{
+ public static readonly Configuration Debug = new() { Value = nameof(Debug) };
+ public static readonly Configuration Release = new() { Value = nameof(Release) };
+
+ public static implicit operator string(Configuration configuration)
+ {
+ return configuration.Value;
+ }
+}
diff --git a/build/DalamudBuild.cs b/build/DalamudBuild.cs
new file mode 100644
index 000000000..01d6d3038
--- /dev/null
+++ b/build/DalamudBuild.cs
@@ -0,0 +1,133 @@
+using System.Collections.Generic;
+using System.IO;
+using Nuke.Common;
+using Nuke.Common.Execution;
+using Nuke.Common.Git;
+using Nuke.Common.IO;
+using Nuke.Common.ProjectModel;
+using Nuke.Common.Tools.DotNet;
+using Nuke.Common.Tools.MSBuild;
+
+[CheckBuildProjectConfigurations]
+[UnsetVisualStudioEnvironmentVariables]
+public class DalamudBuild : NukeBuild
+{
+ /// Support plugins are available for:
+ /// - Microsoft VisualStudio https://nuke.build/visualstudio
+ /// - JetBrains ReSharper https://nuke.build/resharper
+ /// - JetBrains Rider https://nuke.build/rider
+ /// - Microsoft VSCode https://nuke.build/vscode
+
+ public static int Main() => Execute(x => x.Compile);
+
+ [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")]
+ readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;
+
+ [Solution] Solution Solution;
+ [GitRepository] GitRepository GitRepository;
+
+ AbsolutePath DalamudProjectDir => RootDirectory / "Dalamud";
+ AbsolutePath DalamudProjectFile => DalamudProjectDir / "Dalamud.csproj";
+
+ AbsolutePath DalamudBootProjectDir => RootDirectory / "Dalamud.Boot";
+ AbsolutePath DalamudBootProjectFile => DalamudBootProjectDir / "Dalamud.Boot.vcxproj";
+
+ AbsolutePath InjectorProjectDir => RootDirectory / "Dalamud.Injector";
+ AbsolutePath InjectorProjectFile => InjectorProjectDir / "Dalamud.Injector.csproj";
+
+ AbsolutePath InjectorBootProjectDir => RootDirectory / "Dalamud.Injector.Boot";
+ AbsolutePath InjectorBootProjectFile => InjectorBootProjectDir / "Dalamud.Injector.Boot.vcxproj";
+
+ AbsolutePath TestProjectDir => RootDirectory / "Dalamud.Test";
+ AbsolutePath TestProjectFile => TestProjectDir / "Dalamud.Test.csproj";
+
+ AbsolutePath ArtifactsDirectory => RootDirectory / "bin" / Configuration;
+
+ private static AbsolutePath LibraryDirectory => RootDirectory / "lib";
+
+ private static Dictionary EnvironmentVariables => new(EnvironmentInfo.Variables);
+
+ Target Restore => _ => _
+ .Executes(() =>
+ {
+ DotNetTasks.DotNetRestore(s => s
+ .SetProjectFile(Solution));
+ });
+
+ Target CompileDalamud => _ => _
+ .DependsOn(Restore)
+ .Executes(() =>
+ {
+ DotNetTasks.DotNetBuild(s => s
+ .SetProjectFile(DalamudProjectFile)
+ .SetConfiguration(Configuration)
+ .EnableNoRestore());
+ });
+
+ Target CompileDalamudBoot => _ => _
+ .Executes(() =>
+ {
+ MSBuildTasks.MSBuild(s => s
+ .SetTargetPath(DalamudBootProjectFile)
+ .SetConfiguration(Configuration));
+ });
+
+ Target CompileInjector => _ => _
+ .DependsOn(Restore)
+ .Executes(() =>
+ {
+ DotNetTasks.DotNetBuild(s => s
+ .SetProjectFile(InjectorProjectFile)
+ .SetConfiguration(Configuration)
+ .EnableNoRestore());
+ });
+
+ Target CompileInjectorBoot => _ => _
+ .Executes(() =>
+ {
+ MSBuildTasks.MSBuild(s => s
+ .SetTargetPath(InjectorBootProjectFile)
+ .SetConfiguration(Configuration));
+ });
+
+ Target Compile => _ => _
+ .DependsOn(CompileDalamud)
+ .DependsOn(CompileDalamudBoot)
+ .DependsOn(CompileInjector)
+ .DependsOn(CompileInjectorBoot);
+
+ Target Test => _ => _
+ .DependsOn(Compile)
+ .Executes(() =>
+ {
+ DotNetTasks.DotNetTest(s => s
+ .SetProjectFile(TestProjectFile)
+ .SetConfiguration(Configuration)
+ .EnableNoRestore());
+ });
+
+ Target Clean => _ => _
+ .Executes(() =>
+ {
+ DotNetTasks.DotNetClean(s => s
+ .SetProject(DalamudProjectFile)
+ .SetConfiguration(Configuration));
+
+ MSBuildTasks.MSBuild(s => s
+ .SetProjectFile(DalamudBootProjectFile)
+ .SetConfiguration(Configuration)
+ .SetTargets("Clean"));
+
+ DotNetTasks.DotNetClean(s => s
+ .SetProject(InjectorProjectFile)
+ .SetConfiguration(Configuration));
+
+ MSBuildTasks.MSBuild(s => s
+ .SetProjectFile(InjectorBootProjectFile)
+ .SetConfiguration(Configuration)
+ .SetTargets("Clean"));
+
+ FileSystemTasks.DeleteDirectory(ArtifactsDirectory);
+ Directory.CreateDirectory(ArtifactsDirectory);
+ });
+}
diff --git a/build/build.csproj b/build/build.csproj
new file mode 100644
index 000000000..b5403f5ce
--- /dev/null
+++ b/build/build.csproj
@@ -0,0 +1,14 @@
+
+
+ Exe
+ net5.0
+ disable
+
+ IDE0002;IDE0051;IDE1006;CS0649;CS0169
+ ..
+ ..
+
+
+
+
+
diff --git a/build/build.csproj.DotSettings b/build/build.csproj.DotSettings
new file mode 100644
index 000000000..c8947fcec
--- /dev/null
+++ b/build/build.csproj.DotSettings
@@ -0,0 +1,26 @@
+
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ Implicit
+ Implicit
+ ExpressionBody
+ 0
+ NEXT_LINE
+ True
+ False
+ 120
+ IF_OWNER_IS_SINGLE_LINE
+ WRAP_IF_LONG
+ False
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
diff --git a/lib/CoreCLR/CoreCLR.cpp b/lib/CoreCLR/CoreCLR.cpp
new file mode 100644
index 000000000..ec8e548cd
--- /dev/null
+++ b/lib/CoreCLR/CoreCLR.cpp
@@ -0,0 +1,111 @@
+#define WIN32_LEAN_AND_MEAN
+
+#include "CoreCLR.h"
+#include
+#include
+#include "nethost/nethost.h"
+
+#pragma comment(lib, "nethost/libnethost.lib")
+
+CoreCLR::CoreCLR() {}
+
+/* Core public functions */
+int CoreCLR::load_hostfxr()
+{
+ return CoreCLR::load_hostfxr(nullptr);
+}
+
+int CoreCLR::load_hostfxr(const struct get_hostfxr_parameters* parameters)
+{
+ // Get the path to CoreCLR's hostfxr
+ wchar_t buffer[MAX_PATH]{};
+ size_t buffer_size = sizeof buffer / sizeof(wchar_t);
+ if (int rc = get_hostfxr_path(buffer, &buffer_size, parameters); rc != 0)
+ return rc;
+
+ // Load hostfxr and get desired exports
+ auto lib = reinterpret_cast(load_library(buffer));
+ m_hostfxr_initialize_for_runtime_config_fptr = reinterpret_cast(
+ get_export(lib, "hostfxr_initialize_for_runtime_config"));
+ m_hostfxr_get_runtime_delegate_fptr = reinterpret_cast(
+ get_export(lib, "hostfxr_get_runtime_delegate"));
+ m_hostfxr_close_fptr = reinterpret_cast(
+ get_export(lib, "hostfxr_close"));
+
+ return m_hostfxr_initialize_for_runtime_config_fptr
+ && m_hostfxr_get_runtime_delegate_fptr
+ && m_hostfxr_close_fptr ? 0 : -1;
+}
+
+bool CoreCLR::load_runtime(const std::wstring& runtime_config_path)
+{
+ return CoreCLR::load_runtime(runtime_config_path, nullptr);
+}
+
+bool CoreCLR::load_runtime(const std::wstring& runtime_config_path, const struct hostfxr_initialize_parameters* parameters)
+{
+ int result;
+
+ // Load .NET Core
+ hostfxr_handle context = nullptr;
+ result = m_hostfxr_initialize_for_runtime_config_fptr(
+ runtime_config_path.c_str(),
+ parameters,
+ &context);
+
+ // Success_HostAlreadyInitialized
+ if (result == 1)
+ {
+ printf("Success_HostAlreadyInitialized (0x1) ");
+ result = 0;
+ }
+
+ if (result != 0 || context == nullptr)
+ {
+ m_hostfxr_close_fptr(context);
+ return result;
+ }
+
+ // Get the load assembly function pointer
+ result = m_hostfxr_get_runtime_delegate_fptr(
+ context,
+ hdt_load_assembly_and_get_function_pointer,
+ reinterpret_cast(&m_load_assembly_and_get_function_pointer_fptr));
+
+ if (result != 0 || m_load_assembly_and_get_function_pointer_fptr == nullptr)
+ {
+ m_hostfxr_close_fptr(context);
+ return result;
+ }
+
+ m_hostfxr_close_fptr(context);
+
+ return 0;
+}
+
+int CoreCLR::load_assembly_and_get_function_pointer(
+ const wchar_t* assembly_path,
+ const wchar_t* type_name,
+ const wchar_t* method_name,
+ const wchar_t* delegate_type_name,
+ void* reserved,
+ void** delegate) const
+{
+ int result = m_load_assembly_and_get_function_pointer_fptr(assembly_path, type_name, method_name, delegate_type_name, reserved, delegate);
+
+ if (result != 0)
+ delegate = nullptr;
+
+ return result;
+};
+
+/* Helpers */
+uint64_t CoreCLR::load_library(const wchar_t* path)
+{
+ return reinterpret_cast(LoadLibraryW(path));
+}
+
+uint64_t CoreCLR::get_export(void* h, const char* name)
+{
+ return reinterpret_cast(GetProcAddress(static_cast(h), name));
+}
diff --git a/lib/CoreCLR/CoreCLR.h b/lib/CoreCLR/CoreCLR.h
new file mode 100644
index 000000000..c2395d07d
--- /dev/null
+++ b/lib/CoreCLR/CoreCLR.h
@@ -0,0 +1,38 @@
+#pragma once
+#include
+#include "core/hostfxr.h"
+#include "core/coreclr_delegates.h"
+#include "nethost/nethost.h"
+
+class CoreCLR {
+ public:
+ explicit CoreCLR();
+ ~CoreCLR() = default;
+
+ int load_hostfxr();
+ int load_hostfxr(const get_hostfxr_parameters* parameters);
+
+ bool load_runtime(const std::wstring& runtime_config_path);
+ bool load_runtime(
+ const std::wstring& runtime_config_path,
+ const struct hostfxr_initialize_parameters* parameters);
+
+ int load_assembly_and_get_function_pointer(
+ const wchar_t* assembly_path,
+ const wchar_t* type_name,
+ const wchar_t* method_name,
+ const wchar_t* delegate_type_name,
+ void* reserved,
+ void** delegate) const;
+
+ private:
+ /* HostFXR delegates. */
+ hostfxr_initialize_for_runtime_config_fn m_hostfxr_initialize_for_runtime_config_fptr{};
+ hostfxr_get_runtime_delegate_fn m_hostfxr_get_runtime_delegate_fptr{};
+ hostfxr_close_fn m_hostfxr_close_fptr{};
+ load_assembly_and_get_function_pointer_fn m_load_assembly_and_get_function_pointer_fptr = nullptr;
+
+ /* Helper functions. */
+ static uint64_t load_library(const wchar_t* path);
+ static uint64_t get_export(void* h, const char* name);
+};
diff --git a/lib/CoreCLR/boot.cpp b/lib/CoreCLR/boot.cpp
new file mode 100644
index 000000000..0ba4dc19f
--- /dev/null
+++ b/lib/CoreCLR/boot.cpp
@@ -0,0 +1,110 @@
+#define WIN32_LEAN_AND_MEAN
+
+#include
+#include
+#include
+#include
+#include "CoreCLR.h"
+
+FILE* g_CmdStream;
+void ConsoleSetup(const std::wstring console_name)
+{
+ if (!AllocConsole())
+ return;
+
+ SetConsoleTitleW(console_name.c_str());
+ freopen_s(&g_CmdStream, "CONOUT$", "w", stdout);
+ freopen_s(&g_CmdStream, "CONOUT$", "w", stderr);
+ freopen_s(&g_CmdStream, "CONIN$", "r", stdin);
+}
+
+void ConsoleTeardown()
+{
+ FreeConsole();
+}
+
+int InitializeClrAndGetEntryPoint(
+ std::wstring runtimeconfig_path,
+ std::wstring module_path,
+ std::wstring entrypoint_assembly_name,
+ std::wstring entrypoint_method_name,
+ std::wstring entrypoint_delegate_type_name,
+ void** entrypoint_fn)
+{
+ int result;
+ CoreCLR clr;
+ SetEnvironmentVariable(L"DOTNET_MULTILEVEL_LOOKUP", L"0");
+
+ wchar_t* _appdata;
+ result = SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_DEFAULT, NULL, &_appdata);
+ if (result != 0)
+ {
+ printf("Error: Unable to get RoamingAppData path (err=%d)\n", result);
+ return result;
+ }
+ std::filesystem::path fs_app_data(_appdata);
+ wchar_t* dotnet_path = _wcsdup(fs_app_data.append("XIVLauncher").append("runtime").c_str());
+
+ // =========================================================================== //
+
+ wprintf(L"with dotnet_path: %s\n", dotnet_path);
+ wprintf(L"with config_path: %s\n", runtimeconfig_path.c_str());
+ wprintf(L"with module_path: %s\n", module_path.c_str());
+
+ if (!std::filesystem::exists(dotnet_path))
+ {
+ printf("Error: Unable to find .NET runtime path\n");
+ return 1;
+ }
+
+ get_hostfxr_parameters init_parameters
+ {
+ sizeof(get_hostfxr_parameters),
+ nullptr,
+ dotnet_path,
+ };
+
+ printf("Loading hostfxr... ");
+ if ((result = clr.load_hostfxr(&init_parameters)) != 0)
+ {
+ printf("\nError: Failed to load the `hostfxr` library (err=%d)\n", result);
+ return result;
+ }
+ printf("Done!\n");
+
+ // =========================================================================== //
+
+ hostfxr_initialize_parameters runtime_parameters
+ {
+ sizeof(hostfxr_initialize_parameters),
+ module_path.c_str(),
+ dotnet_path,
+ };
+
+ printf("Loading coreclr... ");;
+ if ((result = clr.load_runtime(runtimeconfig_path, &runtime_parameters)) != 0)
+ {
+ printf("\nError: Failed to load coreclr (err=%d)\n", result);
+ return result;
+ }
+ printf("Done!\n");
+
+ // =========================================================================== //
+
+ printf("Loading module... ");
+ if ((result = clr.load_assembly_and_get_function_pointer(
+ module_path.c_str(),
+ entrypoint_assembly_name.c_str(),
+ entrypoint_method_name.c_str(),
+ entrypoint_delegate_type_name.c_str(),
+ nullptr, entrypoint_fn)) != 0)
+ {
+ printf("\nError: Failed to load module (err=%d)\n", result);
+ return result;
+ }
+ printf("Done!\n");
+
+ // =========================================================================== //
+
+ return 0;
+}
diff --git a/lib/CoreCLR/boot.h b/lib/CoreCLR/boot.h
new file mode 100644
index 000000000..8f58042a1
--- /dev/null
+++ b/lib/CoreCLR/boot.h
@@ -0,0 +1,10 @@
+void ConsoleSetup(const std::wstring console_name);
+void ConsoleTeardown();
+
+int InitializeClrAndGetEntryPoint(
+ std::wstring runtimeconfig_path,
+ std::wstring module_path,
+ std::wstring entrypoint_assembly_name,
+ std::wstring entrypoint_method_name,
+ std::wstring entrypoint_delegate_type_name,
+ void** entrypoint_fn);
diff --git a/lib/CoreCLR/core/coreclr_delegates.h b/lib/CoreCLR/core/coreclr_delegates.h
new file mode 100644
index 000000000..20c1221bd
--- /dev/null
+++ b/lib/CoreCLR/core/coreclr_delegates.h
@@ -0,0 +1,47 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#ifndef __CORECLR_DELEGATES_H__
+#define __CORECLR_DELEGATES_H__
+
+#include
+
+#if defined(_WIN32)
+ #define CORECLR_DELEGATE_CALLTYPE __stdcall
+ #ifdef _WCHAR_T_DEFINED
+ typedef wchar_t char_t;
+ #else
+ typedef unsigned short char_t;
+ #endif
+#else
+ #define CORECLR_DELEGATE_CALLTYPE
+ typedef char char_t;
+#endif
+
+#define UNMANAGEDCALLERSONLY_METHOD ((const char_t*)-1)
+
+// Signature of delegate returned by coreclr_delegate_type::load_assembly_and_get_function_pointer
+typedef int (CORECLR_DELEGATE_CALLTYPE* load_assembly_and_get_function_pointer_fn)(
+ const char_t* assembly_path /* Fully qualified path to assembly */,
+ const char_t* type_name /* Assembly qualified type name */,
+ const char_t* method_name /* Public static method name compatible with delegateType */,
+ const char_t* delegate_type_name /* Assembly qualified delegate type name or null
+ or UNMANAGEDCALLERSONLY_METHOD if the method is marked with
+ the UnmanagedCallersOnlyAttribute. */,
+ void* reserved /* Extensibility parameter (currently unused and must be 0) */,
+ /*out*/ void** delegate /* Pointer where to store the function pointer result */);
+
+// Signature of delegate returned by load_assembly_and_get_function_pointer_fn when delegate_type_name == null (default)
+typedef int (CORECLR_DELEGATE_CALLTYPE* component_entry_point_fn)(void* arg, int32_t arg_size_in_bytes);
+
+typedef int (CORECLR_DELEGATE_CALLTYPE* get_function_pointer_fn)(
+ const char_t* type_name /* Assembly qualified type name */,
+ const char_t* method_name /* Public static method name compatible with delegateType */,
+ const char_t* delegate_type_name /* Assembly qualified delegate type name or null,
+ or UNMANAGEDCALLERSONLY_METHOD if the method is marked with
+ the UnmanagedCallersOnlyAttribute. */,
+ void* load_context /* Extensibility parameter (currently unused and must be 0) */,
+ void* reserved /* Extensibility parameter (currently unused and must be 0) */,
+ /*out*/ void** delegate /* Pointer where to store the function pointer result */);
+
+#endif // __CORECLR_DELEGATES_H__
diff --git a/lib/CoreCLR/core/hostfxr.h b/lib/CoreCLR/core/hostfxr.h
new file mode 100644
index 000000000..a0a526d90
--- /dev/null
+++ b/lib/CoreCLR/core/hostfxr.h
@@ -0,0 +1,288 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#ifndef __HOSTFXR_H__
+#define __HOSTFXR_H__
+
+#include
+#include
+
+#if defined(_WIN32)
+ #define HOSTFXR_CALLTYPE __cdecl
+ #ifdef _WCHAR_T_DEFINED
+ typedef wchar_t char_t;
+ #else
+ typedef unsigned short char_t;
+ #endif
+#else
+ #define HOSTFXR_CALLTYPE
+ typedef char char_t;
+#endif
+
+enum hostfxr_delegate_type
+{
+ hdt_com_activation,
+ hdt_load_in_memory_assembly,
+ hdt_winrt_activation,
+ hdt_com_register,
+ hdt_com_unregister,
+ hdt_load_assembly_and_get_function_pointer,
+ hdt_get_function_pointer,
+};
+
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_fn)(const int argc, const char_t** argv);
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_startupinfo_fn)(
+ const int argc,
+ const char_t** argv,
+ const char_t* host_path,
+ const char_t* dotnet_root,
+ const char_t* app_path);
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_bundle_startupinfo_fn)(
+ const int argc,
+ const char_t** argv,
+ const char_t* host_path,
+ const char_t* dotnet_root,
+ const char_t* app_path,
+ int64_t bundle_header_offset);
+
+typedef void(HOSTFXR_CALLTYPE* hostfxr_error_writer_fn)(const char_t* message);
+
+//
+// Sets a callback which is to be used to write errors to.
+//
+// Parameters:
+// error_writer
+// A callback function which will be invoked every time an error is to be reported.
+// Or nullptr to unregister previously registered callback and return to the default behavior.
+// Return value:
+// The previously registered callback (which is now unregistered), or nullptr if no previous callback
+// was registered
+//
+// The error writer is registered per-thread, so the registration is thread-local. On each thread
+// only one callback can be registered. Subsequent registrations overwrite the previous ones.
+//
+// By default no callback is registered in which case the errors are written to stderr.
+//
+// Each call to the error writer is sort of like writing a single line (the EOL character is omitted).
+// Multiple calls to the error writer may occure for one failure.
+//
+// If the hostfxr invokes functions in hostpolicy as part of its operation, the error writer
+// will be propagated to hostpolicy for the duration of the call. This means that errors from
+// both hostfxr and hostpolicy will be reporter through the same error writer.
+//
+typedef hostfxr_error_writer_fn(HOSTFXR_CALLTYPE* hostfxr_set_error_writer_fn)(hostfxr_error_writer_fn error_writer);
+
+typedef void* hostfxr_handle;
+struct hostfxr_initialize_parameters
+{
+ size_t size;
+ const char_t* host_path;
+ const char_t* dotnet_root;
+};
+
+//
+// Initializes the hosting components for a dotnet command line running an application
+//
+// Parameters:
+// argc
+// Number of argv arguments
+// argv
+// Command-line arguments for running an application (as if through the dotnet executable).
+// parameters
+// Optional. Additional parameters for initialization
+// host_context_handle
+// On success, this will be populated with an opaque value representing the initialized host context
+//
+// Return value:
+// Success - Hosting components were successfully initialized
+// HostInvalidState - Hosting components are already initialized
+//
+// This function parses the specified command-line arguments to determine the application to run. It will
+// then find the corresponding .runtimeconfig.json and .deps.json with which to resolve frameworks and
+// dependencies and prepare everything needed to load the runtime.
+//
+// This function only supports arguments for running an application. It does not support SDK commands.
+//
+// This function does not load the runtime.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_initialize_for_dotnet_command_line_fn)(
+ int argc,
+ const char_t** argv,
+ const struct hostfxr_initialize_parameters* parameters,
+ /*out*/ hostfxr_handle* host_context_handle);
+
+//
+// Initializes the hosting components using a .runtimeconfig.json file
+//
+// Parameters:
+// runtime_config_path
+// Path to the .runtimeconfig.json file
+// parameters
+// Optional. Additional parameters for initialization
+// host_context_handle
+// On success, this will be populated with an opaque value representing the initialized host context
+//
+// Return value:
+// Success - Hosting components were successfully initialized
+// Success_HostAlreadyInitialized - Config is compatible with already initialized hosting components
+// Success_DifferentRuntimeProperties - Config has runtime properties that differ from already initialized hosting components
+// CoreHostIncompatibleConfig - Config is incompatible with already initialized hosting components
+//
+// This function will process the .runtimeconfig.json to resolve frameworks and prepare everything needed
+// to load the runtime. It will only process the .deps.json from frameworks (not any app/component that
+// may be next to the .runtimeconfig.json).
+//
+// This function does not load the runtime.
+//
+// If called when the runtime has already been loaded, this function will check if the specified runtime
+// config is compatible with the existing runtime.
+//
+// Both Success_HostAlreadyInitialized and Success_DifferentRuntimeProperties codes are considered successful
+// initializations. In the case of Success_DifferentRuntimeProperties, it is left to the consumer to verify that
+// the difference in properties is acceptable.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_initialize_for_runtime_config_fn)(
+ const char_t* runtime_config_path,
+ const struct hostfxr_initialize_parameters* parameters,
+ /*out*/ hostfxr_handle* host_context_handle);
+
+//
+// Gets the runtime property value for an initialized host context
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+// name
+// Runtime property name
+// value
+// Out parameter. Pointer to a buffer with the property value.
+//
+// Return value:
+// The error code result.
+//
+// The buffer pointed to by value is owned by the host context. The lifetime of the buffer is only
+// guaranteed until any of the below occur:
+// - a 'run' method is called for the host context
+// - properties are changed via hostfxr_set_runtime_property_value
+// - the host context is closed via 'hostfxr_close'
+//
+// If host_context_handle is nullptr and an active host context exists, this function will get the
+// property value for the active host context.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_runtime_property_value_fn)(
+ const hostfxr_handle host_context_handle,
+ const char_t* name,
+ /*out*/ const char_t** value);
+
+//
+// Sets the value of a runtime property for an initialized host context
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+// name
+// Runtime property name
+// value
+// Value to set
+//
+// Return value:
+// The error code result.
+//
+// Setting properties is only supported for the first host context, before the runtime has been loaded.
+//
+// If the property already exists in the host context, it will be overwritten. If value is nullptr, the
+// property will be removed.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_set_runtime_property_value_fn)(
+ const hostfxr_handle host_context_handle,
+ const char_t* name,
+ const char_t* value);
+
+//
+// Gets all the runtime properties for an initialized host context
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+// count
+// [in] Size of the keys and values buffers
+// [out] Number of properties returned (size of keys/values buffers used). If the input value is too
+// small or keys/values is nullptr, this is populated with the number of available properties
+// keys
+// Array of pointers to buffers with runtime property keys
+// values
+// Array of pointers to buffers with runtime property values
+//
+// Return value:
+// The error code result.
+//
+// The buffers pointed to by keys and values are owned by the host context. The lifetime of the buffers is only
+// guaranteed until any of the below occur:
+// - a 'run' method is called for the host context
+// - properties are changed via hostfxr_set_runtime_property_value
+// - the host context is closed via 'hostfxr_close'
+//
+// If host_context_handle is nullptr and an active host context exists, this function will get the
+// properties for the active host context.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_runtime_properties_fn)(
+ const hostfxr_handle host_context_handle,
+ /*inout*/ size_t* count,
+ /*out*/ const char_t** keys,
+ /*out*/ const char_t** values);
+
+//
+// Load CoreCLR and run the application for an initialized host context
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+//
+// Return value:
+// If the app was successfully run, the exit code of the application. Otherwise, the error code result.
+//
+// The host_context_handle must have been initialized using hostfxr_initialize_for_dotnet_command_line.
+//
+// This function will not return until the managed application exits.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_run_app_fn)(const hostfxr_handle host_context_handle);
+
+//
+// Gets a typed delegate from the currently loaded CoreCLR or from a newly created one.
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+// type
+// Type of runtime delegate requested
+// delegate
+// An out parameter that will be assigned the delegate.
+//
+// Return value:
+// The error code result.
+//
+// If the host_context_handle was initialized using hostfxr_initialize_for_runtime_config,
+// then all delegate types are supported.
+// If the host_context_handle was initialized using hostfxr_initialize_for_dotnet_command_line,
+// then only the following delegate types are currently supported:
+// hdt_load_assembly_and_get_function_pointer
+// hdt_get_function_pointer
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_runtime_delegate_fn)(
+ const hostfxr_handle host_context_handle,
+ enum hostfxr_delegate_type type,
+ /*out*/ void** delegate);
+
+//
+// Closes an initialized host context
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+//
+// Return value:
+// The error code result.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_close_fn)(const hostfxr_handle host_context_handle);
+
+#endif //__HOSTFXR_H__
diff --git a/lib/CoreCLR/framework.h b/lib/CoreCLR/framework.h
new file mode 100644
index 000000000..8bb54776a
--- /dev/null
+++ b/lib/CoreCLR/framework.h
@@ -0,0 +1,7 @@
+#pragma once
+
+// Exclude rarely-used stuff from Windows headers
+#define WIN32_LEAN_AND_MEAN
+
+// Windows Header Files
+#include
diff --git a/lib/CoreCLR/nethost/libnethost.lib b/lib/CoreCLR/nethost/libnethost.lib
new file mode 100644
index 000000000..4291a5387
Binary files /dev/null and b/lib/CoreCLR/nethost/libnethost.lib differ
diff --git a/lib/CoreCLR/nethost/nethost.dll b/lib/CoreCLR/nethost/nethost.dll
new file mode 100644
index 000000000..6673b3079
Binary files /dev/null and b/lib/CoreCLR/nethost/nethost.dll differ
diff --git a/lib/CoreCLR/nethost/nethost.h b/lib/CoreCLR/nethost/nethost.h
new file mode 100644
index 000000000..31adde5e8
--- /dev/null
+++ b/lib/CoreCLR/nethost/nethost.h
@@ -0,0 +1,99 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#ifndef __NETHOST_H__
+#define __NETHOST_H__
+
+#include
+
+#ifdef _WIN32
+ #ifdef NETHOST_EXPORT
+ #define NETHOST_API __declspec(dllexport)
+ #else
+ // Consuming the nethost as a static library
+ // Shouldn't export attempt to dllimport.
+ #ifdef NETHOST_USE_AS_STATIC
+ #define NETHOST_API
+ #else
+ #define NETHOST_API __declspec(dllimport)
+ #endif
+ #endif
+
+ #define NETHOST_CALLTYPE __stdcall
+ #ifdef _WCHAR_T_DEFINED
+ typedef wchar_t char_t;
+ #else
+ typedef unsigned short char_t;
+ #endif
+#else
+ #ifdef NETHOST_EXPORT
+ #define NETHOST_API __attribute__((__visibility__("default")))
+ #else
+ #define NETHOST_API
+ #endif
+
+ #define NETHOST_CALLTYPE
+ typedef char char_t;
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Parameters for get_hostfxr_path
+//
+// Fields:
+// size
+// Size of the struct. This is used for versioning.
+//
+// assembly_path
+// Path to the compenent's assembly.
+// If specified, hostfxr is located as if the assembly_path is the apphost
+//
+// dotnet_root
+// Path to directory containing the dotnet executable.
+// If specified, hostfxr is located as if an application is started using
+// 'dotnet app.dll', which means it will be searched for under the dotnet_root
+// path and the assembly_path is ignored.
+//
+struct get_hostfxr_parameters {
+ size_t size;
+ const char_t *assembly_path;
+ const char_t *dotnet_root;
+};
+
+//
+// Get the path to the hostfxr library
+//
+// Parameters:
+// buffer
+// Buffer that will be populated with the hostfxr path, including a null terminator.
+//
+// buffer_size
+// [in] Size of buffer in char_t units.
+// [out] Size of buffer used in char_t units. If the input value is too small
+// or buffer is nullptr, this is populated with the minimum required size
+// in char_t units for a buffer to hold the hostfxr path
+//
+// get_hostfxr_parameters
+// Optional. Parameters that modify the behaviour for locating the hostfxr library.
+// If nullptr, hostfxr is located using the enviroment variable or global registration
+//
+// Return value:
+// 0 on success, otherwise failure
+// 0x80008098 - buffer is too small (HostApiBufferTooSmall)
+//
+// Remarks:
+// The full search for the hostfxr library is done on every call. To minimize the need
+// to call this function multiple times, pass a large buffer (e.g. PATH_MAX).
+//
+NETHOST_API int NETHOST_CALLTYPE get_hostfxr_path(
+ char_t * buffer,
+ size_t * buffer_size,
+ const struct get_hostfxr_parameters *parameters);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // __NETHOST_H__
diff --git a/lib/CoreCLR/nethost/nethost.lib b/lib/CoreCLR/nethost/nethost.lib
new file mode 100644
index 000000000..4cd6f0382
Binary files /dev/null and b/lib/CoreCLR/nethost/nethost.lib differ
diff --git a/lib/CoreCLR/pch.cpp b/lib/CoreCLR/pch.cpp
new file mode 100644
index 000000000..64b7eef6d
--- /dev/null
+++ b/lib/CoreCLR/pch.cpp
@@ -0,0 +1,5 @@
+// pch.cpp: source file corresponding to the pre-compiled header
+
+#include "pch.h"
+
+// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.
diff --git a/lib/CoreCLR/pch.h b/lib/CoreCLR/pch.h
new file mode 100644
index 000000000..885d5d62e
--- /dev/null
+++ b/lib/CoreCLR/pch.h
@@ -0,0 +1,13 @@
+// pch.h: This is a precompiled header file.
+// Files listed below are compiled only once, improving build performance for future builds.
+// This also affects IntelliSense performance, including code completion and many code browsing features.
+// However, files listed here are ALL re-compiled if any one of them is updated between builds.
+// Do not add files here that you will be updating frequently as this negates the performance advantage.
+
+#ifndef PCH_H
+#define PCH_H
+
+// add headers that you want to pre-compile here
+#include "framework.h"
+
+#endif //PCH_H
diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs
index aace50182..e3bec1189 160000
--- a/lib/FFXIVClientStructs
+++ b/lib/FFXIVClientStructs
@@ -1 +1 @@
-Subproject commit aace501829dcc765eb06eab446c6cdc087d351ca
+Subproject commit e3bec118909b0eafdd0ee7c3312c646ce0c3f3dd
diff --git a/lib/ImGuiScene b/lib/ImGuiScene
index 89f6b1b92..b326a22da 160000
--- a/lib/ImGuiScene
+++ b/lib/ImGuiScene
@@ -1 +1 @@
-Subproject commit 89f6b1b92547d5a54fc4e8aa81fe3e454da30318
+Subproject commit b326a22dad825fc036d63956bc9901165d825d86
diff --git a/lib/SharpDX.Desktop b/lib/SharpDX.Desktop
new file mode 160000
index 000000000..7fc56bc0a
--- /dev/null
+++ b/lib/SharpDX.Desktop
@@ -0,0 +1 @@
+Subproject commit 7fc56bc0a240030d4736e6b16da33b08c73c3ba4
diff --git a/Dalamud.Injector/stylecop.json b/stylecop.json
similarity index 100%
rename from Dalamud.Injector/stylecop.json
rename to stylecop.json