mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 10:17:22 +01:00
Merge remote-tracking branch 'upstream/master' into imguiscene-inside
This commit is contained in:
commit
3c4b9f96ff
83 changed files with 6304 additions and 871 deletions
5
.github/workflows/main.yml
vendored
5
.github/workflows/main.yml
vendored
|
|
@ -92,7 +92,7 @@ jobs:
|
||||||
foreach ($file in $FILES_TO_VALIDATE) {
|
foreach ($file in $FILES_TO_VALIDATE) {
|
||||||
$testout = ""
|
$testout = ""
|
||||||
Write-Output "::group::=== API COMPATIBILITY CHECK: ${file} ==="
|
Write-Output "::group::=== API COMPATIBILITY CHECK: ${file} ==="
|
||||||
apicompat -l "left\${file}" -r "right\${file}" | Tee-Object -Variable testout
|
apicompat -l "left\${file}" -r "right\${file}" --noWarn "CP0006" | Tee-Object -Variable testout
|
||||||
Write-Output "::endgroup::"
|
Write-Output "::endgroup::"
|
||||||
if ($testout -ne "APICompat ran successfully without finding any breaking changes.") {
|
if ($testout -ne "APICompat ran successfully without finding any breaking changes.") {
|
||||||
Write-Output "::error::${file} did not pass. Please review it for problems."
|
Write-Output "::error::${file} did not pass. Please review it for problems."
|
||||||
|
|
@ -137,6 +137,7 @@ jobs:
|
||||||
|
|
||||||
$newVersion = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\TEMP_gitver.txt")
|
$newVersion = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\TEMP_gitver.txt")
|
||||||
$revision = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\revision.txt")
|
$revision = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\revision.txt")
|
||||||
|
$commitHash = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\commit_hash.txt")
|
||||||
Remove-Item -Force -Recurse .\scratch
|
Remove-Item -Force -Recurse .\scratch
|
||||||
|
|
||||||
if (Test-Path -Path $branchName) {
|
if (Test-Path -Path $branchName) {
|
||||||
|
|
@ -147,7 +148,7 @@ jobs:
|
||||||
} else {
|
} else {
|
||||||
Move-Item -Force ".\canary.zip" ".\${branchName}\latest.zip"
|
Move-Item -Force ".\canary.zip" ".\${branchName}\latest.zip"
|
||||||
$versionData.AssemblyVersion = $newVersion
|
$versionData.AssemblyVersion = $newVersion
|
||||||
$versionData | add-member -Force -Name "GitSha" $newVersion -MemberType NoteProperty
|
$versionData | add-member -Force -Name "GitSha" $commitHash -MemberType NoteProperty
|
||||||
$versionData | add-member -Force -Name "Revision" $revision -MemberType NoteProperty
|
$versionData | add-member -Force -Name "Revision" $revision -MemberType NoteProperty
|
||||||
$versionData | ConvertTo-Json -Compress | Out-File ".\${branchName}\version"
|
$versionData | ConvertTo-Json -Compress | Out-File ".\${branchName}\version"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
.github/workflows/rollup.yml
vendored
3
.github/workflows/rollup.yml
vendored
|
|
@ -11,8 +11,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
branches:
|
branches:
|
||||||
- new_im_hooks
|
- WORKFLOW_DISABLED_REMOVE_BEFORE_RUNNING
|
||||||
# - apiX
|
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,24 @@
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
#undef APSTUDIO_READONLY_SYMBOLS
|
#undef APSTUDIO_READONLY_SYMBOLS
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// English (United States) resources
|
||||||
|
|
||||||
|
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
||||||
|
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||||
|
#pragma code_page(1252)
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// RT_MANIFEST
|
||||||
|
//
|
||||||
|
|
||||||
|
RT_MANIFEST_THEMES RT_MANIFEST "themes.manifest"
|
||||||
|
|
||||||
|
#endif // English (United States) resources
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
// English (United Kingdom) resources
|
// English (United Kingdom) resources
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -197,8 +197,11 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="module.def" />
|
<None Include="module.def" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Manifest Include="themes.manifest" />
|
||||||
|
</ItemGroup>
|
||||||
<Target Name="RemoveExtraFiles" AfterTargets="PostBuildEvent">
|
<Target Name="RemoveExtraFiles" AfterTargets="PostBuildEvent">
|
||||||
<Delete Files="$(OutDir)$(TargetName).lib" />
|
<Delete Files="$(OutDir)$(TargetName).lib" />
|
||||||
<Delete Files="$(OutDir)$(TargetName).exp" />
|
<Delete Files="$(OutDir)$(TargetName).exp" />
|
||||||
</Target>
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -163,4 +163,7 @@
|
||||||
<Filter>Dalamud.Boot DLL</Filter>
|
<Filter>Dalamud.Boot DLL</Filter>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Manifest Include="themes.manifest" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -5,6 +5,14 @@
|
||||||
#include "ntdll.h"
|
#include "ntdll.h"
|
||||||
#include "logging.h"
|
#include "logging.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
int s_dllChanged = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" __declspec(dllexport) int* GetDllChangedStorage() {
|
||||||
|
return &s_dllChanged;
|
||||||
|
}
|
||||||
|
|
||||||
hooks::getprocaddress_singleton_import_hook::getprocaddress_singleton_import_hook()
|
hooks::getprocaddress_singleton_import_hook::getprocaddress_singleton_import_hook()
|
||||||
: m_pfnGetProcAddress(GetProcAddress)
|
: m_pfnGetProcAddress(GetProcAddress)
|
||||||
, m_thunk("kernel32!GetProcAddress(Singleton Import Hook)",
|
, m_thunk("kernel32!GetProcAddress(Singleton Import Hook)",
|
||||||
|
|
@ -71,6 +79,7 @@ void hooks::getprocaddress_singleton_import_hook::initialize() {
|
||||||
m_getProcAddressHandler = set_handler(L"kernel32.dll", "GetProcAddress", m_thunk.get_thunk(), [this](void*) {});
|
m_getProcAddressHandler = set_handler(L"kernel32.dll", "GetProcAddress", m_thunk.get_thunk(), [this](void*) {});
|
||||||
|
|
||||||
LdrRegisterDllNotification(0, [](ULONG notiReason, const LDR_DLL_NOTIFICATION_DATA* pData, void* context) {
|
LdrRegisterDllNotification(0, [](ULONG notiReason, const LDR_DLL_NOTIFICATION_DATA* pData, void* context) {
|
||||||
|
s_dllChanged = 1;
|
||||||
if (notiReason == LDR_DLL_NOTIFICATION_REASON_LOADED) {
|
if (notiReason == LDR_DLL_NOTIFICATION_REASON_LOADED) {
|
||||||
const auto dllName = unicode::convert<std::string>(pData->Loaded.FullDllName->Buffer);
|
const auto dllName = unicode::convert<std::string>(pData->Loaded.FullDllName->Buffer);
|
||||||
|
|
||||||
|
|
|
||||||
9
Dalamud.Boot/themes.manifest
Normal file
9
Dalamud.Boot/themes.manifest
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||||
|
<description>Windows Forms Common Control manifest</description>
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*" />
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
</assembly>
|
||||||
|
|
@ -199,9 +199,10 @@ namespace Dalamud.Injector
|
||||||
|
|
||||||
CullLogFile(logPath, 1 * 1024 * 1024);
|
CullLogFile(logPath, 1 * 1024 * 1024);
|
||||||
|
|
||||||
|
const long maxLogSize = 100 * 1024 * 1024; // 100MB
|
||||||
Log.Logger = new LoggerConfiguration()
|
Log.Logger = new LoggerConfiguration()
|
||||||
.WriteTo.Console(standardErrorFromLevel: LogEventLevel.Debug)
|
.WriteTo.Console(standardErrorFromLevel: LogEventLevel.Debug)
|
||||||
.WriteTo.File(logPath, fileSizeLimitBytes: null)
|
.WriteTo.File(logPath, fileSizeLimitBytes: maxLogSize)
|
||||||
.MinimumLevel.ControlledBy(levelSwitch)
|
.MinimumLevel.ControlledBy(levelSwitch)
|
||||||
.CreateLogger();
|
.CreateLogger();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ using System.Runtime.InteropServices;
|
||||||
using Dalamud.Game.Text;
|
using Dalamud.Game.Text;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.FontIdentifier;
|
using Dalamud.Interface.FontIdentifier;
|
||||||
|
using Dalamud.Interface.Internal;
|
||||||
|
using Dalamud.Interface.Internal.ReShadeHandling;
|
||||||
using Dalamud.Interface.Style;
|
using Dalamud.Interface.Style;
|
||||||
using Dalamud.IoC.Internal;
|
using Dalamud.IoC.Internal;
|
||||||
using Dalamud.Plugin.Internal.AutoUpdate;
|
using Dalamud.Plugin.Internal.AutoUpdate;
|
||||||
|
|
@ -441,6 +443,13 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool WindowIsImmersive { get; set; } = false;
|
public bool WindowIsImmersive { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the mode specifying how to handle ReShade.</summary>
|
||||||
|
[JsonProperty("ReShadeHandlingModeV2")]
|
||||||
|
public ReShadeHandlingMode ReShadeHandlingMode { get; set; } = ReShadeHandlingMode.Default;
|
||||||
|
|
||||||
|
/// <summary>Gets or sets the swap chain hook mode.</summary>
|
||||||
|
public SwapChainHelper.HookMode SwapChainHookMode { get; set; } = SwapChainHelper.HookMode.ByteCode;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets hitch threshold for game network up in milliseconds.
|
/// Gets or sets hitch threshold for game network up in milliseconds.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,12 @@ internal sealed class Dalamud : IServiceType
|
||||||
true, new FileInfo(Path.Combine(cacheDir.FullName, $"{this.StartInfo.GameVersion}.json")));
|
true, new FileInfo(Path.Combine(cacheDir.FullName, $"{this.StartInfo.GameVersion}.json")));
|
||||||
}
|
}
|
||||||
|
|
||||||
ServiceManager.InitializeProvidedServices(this, fs, configuration, scanner);
|
ServiceManager.InitializeProvidedServices(
|
||||||
|
this,
|
||||||
|
fs,
|
||||||
|
configuration,
|
||||||
|
scanner,
|
||||||
|
Localization.FromAssets(info.AssetDirectory!, configuration.LanguageOverride));
|
||||||
|
|
||||||
// Set up FFXIVClientStructs
|
// Set up FFXIVClientStructs
|
||||||
this.SetupClientStructsResolver(cacheDir);
|
this.SetupClientStructsResolver(cacheDir);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Label="Feature">
|
<PropertyGroup Label="Feature">
|
||||||
<DalamudVersion>10.0.0.4</DalamudVersion>
|
<DalamudVersion>10.0.0.7</DalamudVersion>
|
||||||
<Description>XIV Launcher addon framework</Description>
|
<Description>XIV Launcher addon framework</Description>
|
||||||
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
|
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
|
||||||
<Version>$(DalamudVersion)</Version>
|
<Version>$(DalamudVersion)</Version>
|
||||||
|
|
@ -136,57 +136,45 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Needed temporarily for CI -->
|
<!-- Needed temporarily for CI -->
|
||||||
<TempVerFile>$(OutputPath)TEMP_gitver.txt</TempVerFile>
|
<TempVerFile>$(OutputPath)TEMP_gitver.txt</TempVerFile>
|
||||||
|
<CommitHashFile>$(OutputPath)commit_hash.txt</CommitHashFile>
|
||||||
<DalamudRevisionFile>$(OutputPath)revision.txt</DalamudRevisionFile>
|
<DalamudRevisionFile>$(OutputPath)revision.txt</DalamudRevisionFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<Target Name="GetGitCommitCount" BeforeTargets="WriteGitHash" Condition="'$(CommitCount)'==''">
|
<Target Name="GetVersionData" BeforeTargets="WriteVersionData" Condition="'$(SCMVersion)'=='' And '$(Configuration)'=='Release'">
|
||||||
<Exec Command="git -C "$(ProjectDir.Replace('\','\\'))" rev-list --count HEAD" ConsoleToMSBuild="true">
|
<Exec Command="git -C "$(ProjectDir.Replace('\','\\'))" rev-list --count HEAD" ConsoleToMSBuild="true">
|
||||||
<Output TaskParameter="ConsoleOutput" PropertyName="DalamudGitCommitCount" />
|
<Output TaskParameter="ConsoleOutput" PropertyName="DalamudGitCommitCount" />
|
||||||
</Exec>
|
</Exec>
|
||||||
|
<Exec Command="git -C "$(ProjectDir.Replace('\','\\'))" describe --match=NeVeRmAtCh --always --abbrev=40 --dirty" ConsoleToMSBuild="true">
|
||||||
<!-- Set the BuildHash property to contain the GitVersion, if it wasn't already set.-->
|
<Output TaskParameter="ConsoleOutput" PropertyName="DalamudGitCommitHash" />
|
||||||
<PropertyGroup>
|
|
||||||
<CommitCount>$([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitCommitCount), @"\t|\n|\r", ""))</CommitCount>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<Exec Command="echo|set /P ="$(CommitCount)" > $(DalamudRevisionFile)" IgnoreExitCode="true" />
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
<Target Name="GetGitHash" BeforeTargets="WriteGitHash" Condition="'$(BuildHash)'=='' And '$(Configuration)'=='Release'">
|
|
||||||
<!-- write the hash to the temp file.-->
|
|
||||||
<Exec Command="git -C "$(ProjectDir.Replace('\','\\'))" describe --long --tags --always --dirty" ConsoleToMSBuild="true">
|
|
||||||
<Output TaskParameter="ConsoleOutput" PropertyName="DalamudGitDescribeOutput" />
|
|
||||||
</Exec>
|
</Exec>
|
||||||
<Exec Command="git -C "$(ProjectDir.Replace('\','\\'))" rev-parse" ConsoleToMSBuild="true">
|
<Exec Command="git -C "$(ProjectDir.Replace('\','\\'))" describe --tags --always --dirty" ConsoleToMSBuild="true">
|
||||||
<Output TaskParameter="ConsoleOutput" PropertyName="DalamudFullGitCommitHash" />
|
<Output TaskParameter="ConsoleOutput" PropertyName="DalamudGitDescribeOutput" />
|
||||||
</Exec>
|
</Exec>
|
||||||
<Exec Command="git -C "$(ProjectDir.Replace('\','\\'))\..\lib\FFXIVClientStructs" describe --long --always --dirty" ConsoleToMSBuild="true">
|
<Exec Command="git -C "$(ProjectDir.Replace('\','\\'))\..\lib\FFXIVClientStructs" describe --long --always --dirty" ConsoleToMSBuild="true">
|
||||||
<Output TaskParameter="ConsoleOutput" PropertyName="ClientStructsGitDescribeOutput" />
|
<Output TaskParameter="ConsoleOutput" PropertyName="ClientStructsGitDescribeOutput" />
|
||||||
</Exec>
|
</Exec>
|
||||||
|
|
||||||
<!-- Set the BuildHash property to contain the GitVersion, if it wasn't already set.-->
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<BuildHash>$([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitDescribeOutput), @"\t|\n|\r", ""))</BuildHash>
|
<CommitCount>$([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitCommitCount), @"\t|\n|\r", ""))</CommitCount>
|
||||||
<BuildHashClientStructs>$([System.Text.RegularExpressions.Regex]::Replace($(ClientStructsGitDescribeOutput), @"\t|\n|\r", ""))</BuildHashClientStructs>
|
<CommitHash>$([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitCommitHash), @"\t|\n|\r", ""))</CommitHash>
|
||||||
|
<SCMVersion>$([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitDescribeOutput), @"\t|\n|\r", ""))</SCMVersion>
|
||||||
|
<CommitHashClientStructs>$([System.Text.RegularExpressions.Regex]::Replace($(ClientStructsGitDescribeOutput), @"\t|\n|\r", ""))</CommitHashClientStructs>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Looks like this is the only way to write a file without a carriage return in msbuild... -->
|
<Exec Command="echo|set /P ="$(CommitCount)" > $(DalamudRevisionFile)" IgnoreExitCode="true" />
|
||||||
<Exec Command="echo|set /P ="$(BuildHash)" > $(TempVerFile)" IgnoreExitCode="true" />
|
<Exec Command="echo|set /P ="$(CommitHash)" > $(CommitHashFile)" IgnoreExitCode="true" />
|
||||||
</Target>
|
<Exec Command="echo|set /P ="$(SCMVersion)" > $(TempVerFile)" IgnoreExitCode="true" />
|
||||||
|
|
||||||
<Target Name="GetGitHashStub" BeforeTargets="WriteGitHash" Condition="'$(BuildHash)'=='' And '$(Configuration)'=='Debug'">
|
|
||||||
<!-- Set the BuildHash property to contain some placeholder, if it wasn't already set.-->
|
|
||||||
<PropertyGroup>
|
|
||||||
<LocalBuildText>Local build at $([System.DateTime]::Now.ToString(yyyy-MM-dd HH:mm:ss))</LocalBuildText>
|
|
||||||
<BuildHash>$(LocalBuildText)</BuildHash>
|
|
||||||
<BuildHashClientStructs>???</BuildHashClientStructs>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<!-- Looks like this is the only way to write a file without a carriage return in msbuild... -->
|
|
||||||
<Exec Command="echo|set /P ="$(BuildHash)" > $(TempVerFile)" IgnoreExitCode="true" />
|
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<Target Name="WriteGitHash" BeforeTargets="CoreCompile">
|
<Target Name="GenerateStubVersionData" BeforeTargets="WriteVersionData" Condition="'$(SCMVersion)'=='' And '$(Configuration)'!='Release'">
|
||||||
|
<!-- stub out version since it takes a while. -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<SCMVersion>Local build at $([System.DateTime]::Now.ToString(yyyy-MM-dd HH:mm:ss))</SCMVersion>
|
||||||
|
<CommitHashClientStructs>???</CommitHashClientStructs>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
<Target Name="WriteVersionData" BeforeTargets="CoreCompile">
|
||||||
<!-- names the obj/.../CustomAssemblyInfo.cs file -->
|
<!-- names the obj/.../CustomAssemblyInfo.cs file -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<CustomAssemblyInfoFile>$(IntermediateOutputPath)CustomAssemblyInfo.cs</CustomAssemblyInfoFile>
|
<CustomAssemblyInfoFile>$(IntermediateOutputPath)CustomAssemblyInfo.cs</CustomAssemblyInfoFile>
|
||||||
|
|
@ -197,21 +185,21 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- defines the AssemblyMetadata attribute that will be written -->
|
<!-- defines the AssemblyMetadata attribute that will be written -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AssemblyAttributes Include="AssemblyMetadata">
|
<AssemblyAttributes Include="AssemblyMetadata" Condition="'$(SCMVersion)' != ''">
|
||||||
<_Parameter1>GitHash</_Parameter1>
|
<_Parameter1>SCMVersion</_Parameter1>
|
||||||
<_Parameter2>$(BuildHash)</_Parameter2>
|
<_Parameter2>$(SCMVersion)</_Parameter2>
|
||||||
</AssemblyAttributes>
|
</AssemblyAttributes>
|
||||||
<AssemblyAttributes Include="AssemblyMetadata">
|
<AssemblyAttributes Include="AssemblyMetadata" Condition="'$(CommitCount)' != ''">
|
||||||
<_Parameter1>GitCommitCount</_Parameter1>
|
<_Parameter1>GitCommitCount</_Parameter1>
|
||||||
<_Parameter2>$(CommitCount)</_Parameter2>
|
<_Parameter2>$(CommitCount)</_Parameter2>
|
||||||
</AssemblyAttributes>
|
</AssemblyAttributes>
|
||||||
<AssemblyAttributes Include="AssemblyMetadata">
|
<AssemblyAttributes Include="AssemblyMetadata" Condition="'$(CommitHashClientStructs)' != ''">
|
||||||
<_Parameter1>GitHashClientStructs</_Parameter1>
|
<_Parameter1>GitHashClientStructs</_Parameter1>
|
||||||
<_Parameter2>$(BuildHashClientStructs)</_Parameter2>
|
<_Parameter2>$(CommitHashClientStructs)</_Parameter2>
|
||||||
</AssemblyAttributes>
|
</AssemblyAttributes>
|
||||||
<AssemblyAttributes Include="AssemblyMetadata" Condition="'$(DalamudFullGitCommitHash)' != ''">
|
<AssemblyAttributes Include="AssemblyMetadata" Condition="'$(CommitHash)' != ''">
|
||||||
<_Parameter1>FullGitHash</_Parameter1>
|
<_Parameter1>GitHash</_Parameter1>
|
||||||
<_Parameter2>$(DalamudFullGitCommitHash)</_Parameter2>
|
<_Parameter2>$(CommitHash)</_Parameter2>
|
||||||
</AssemblyAttributes>
|
</AssemblyAttributes>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- writes the attribute to the customAssemblyInfo file -->
|
<!-- writes the attribute to the customAssemblyInfo file -->
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||||
|
|
||||||
using Dalamud.Common;
|
using Dalamud.Common;
|
||||||
using Dalamud.Configuration.Internal;
|
using Dalamud.Configuration.Internal;
|
||||||
|
using Dalamud.Interface.Internal.Windows;
|
||||||
using Dalamud.Logging.Internal;
|
using Dalamud.Logging.Internal;
|
||||||
using Dalamud.Logging.Retention;
|
using Dalamud.Logging.Retention;
|
||||||
using Dalamud.Plugin.Internal;
|
using Dalamud.Plugin.Internal;
|
||||||
|
|
@ -107,15 +108,16 @@ public sealed class EntryPoint
|
||||||
.WriteTo.Sink(SerilogEventSink.Instance)
|
.WriteTo.Sink(SerilogEventSink.Instance)
|
||||||
.MinimumLevel.ControlledBy(LogLevelSwitch);
|
.MinimumLevel.ControlledBy(LogLevelSwitch);
|
||||||
|
|
||||||
|
const long maxLogSize = 100 * 1024 * 1024; // 100MB
|
||||||
if (logSynchronously)
|
if (logSynchronously)
|
||||||
{
|
{
|
||||||
config = config.WriteTo.File(logPath.FullName, fileSizeLimitBytes: null);
|
config = config.WriteTo.File(logPath.FullName, fileSizeLimitBytes: maxLogSize);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
config = config.WriteTo.Async(a => a.File(
|
config = config.WriteTo.Async(a => a.File(
|
||||||
logPath.FullName,
|
logPath.FullName,
|
||||||
fileSizeLimitBytes: null,
|
fileSizeLimitBytes: maxLogSize,
|
||||||
buffered: false,
|
buffered: false,
|
||||||
flushToDiskInterval: TimeSpan.FromSeconds(1)));
|
flushToDiskInterval: TimeSpan.FromSeconds(1)));
|
||||||
}
|
}
|
||||||
|
|
@ -185,7 +187,7 @@ public sealed class EntryPoint
|
||||||
|
|
||||||
var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent);
|
var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent);
|
||||||
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]",
|
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]",
|
||||||
Util.GetGitHash(),
|
Util.GetScmVersion(),
|
||||||
Util.GetGitHashClientStructs(),
|
Util.GetGitHashClientStructs(),
|
||||||
FFXIVClientStructs.ThisAssembly.Git.Commits);
|
FFXIVClientStructs.ThisAssembly.Git.Commits);
|
||||||
|
|
||||||
|
|
@ -231,6 +233,10 @@ public sealed class EntryPoint
|
||||||
|
|
||||||
private static void SerilogOnLogLine(object? sender, (string Line, LogEvent LogEvent) ev)
|
private static void SerilogOnLogLine(object? sender, (string Line, LogEvent LogEvent) ev)
|
||||||
{
|
{
|
||||||
|
if (!LoadingDialog.IsGloballyHidden)
|
||||||
|
LoadingDialog.NewLogEntries.Enqueue(ev);
|
||||||
|
ConsoleWindow.NewLogEntries.Enqueue(ev);
|
||||||
|
|
||||||
if (ev.LogEvent.Exception == null)
|
if (ev.LogEvent.Exception == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
namespace Dalamud.Game.Addon.Lifecycle;
|
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||||
|
|
||||||
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
|
||||||
|
namespace Dalamud.Game.Addon.Lifecycle;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enumeration for available AddonLifecycle events.
|
/// Enumeration for available AddonLifecycle events.
|
||||||
|
|
@ -6,67 +10,112 @@
|
||||||
public enum AddonEvent
|
public enum AddonEvent
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event that is fired before an addon begins it's setup process.
|
/// An event that is fired prior to an addon being setup with its implementation of
|
||||||
|
/// <see cref="AtkUnitBase.OnSetup"/>. This event is useful for modifying the initial data contained within
|
||||||
|
/// <see cref="AddonSetupArgs.AtkValueSpan"/> prior to the addon being created.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <seealso cref="AddonSetupArgs"/>
|
||||||
PreSetup,
|
PreSetup,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event that is fired after an addon has completed it's setup process.
|
/// An event that is fired after an addon has finished its initial setup. This event is particularly useful for
|
||||||
|
/// developers seeking to add custom elements to now-initialized and populated node lists, as well as reading data
|
||||||
|
/// placed in the AtkValues by the game during the setup process.
|
||||||
|
/// See <see cref="PreSetup"/> for more information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
PostSetup,
|
PostSetup,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event that is fired before an addon begins update.
|
/// An event that is fired before an addon begins its update cycle via <see cref="AtkUnitBase.Update"/>. This event
|
||||||
|
/// is fired every frame that an addon is loaded, regardless of visibility.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <seealso cref="AddonUpdateArgs"/>
|
||||||
PreUpdate,
|
PreUpdate,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event that is fired after an addon has completed update.
|
/// An event that is fired after an addon has finished its update.
|
||||||
|
/// See <see cref="PreUpdate"/> for more information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
PostUpdate,
|
PostUpdate,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event that is fired before an addon begins draw.
|
/// An event that is fired before an addon begins drawing to screen via <see cref="AtkUnitBase.Draw"/>. Unlike
|
||||||
|
/// <see cref="PreUpdate"/>, this event is only fired if an addon is visible or otherwise drawing to screen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <seealso cref="AddonDrawArgs"/>
|
||||||
PreDraw,
|
PreDraw,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event that is fired after an addon has completed draw.
|
/// An event that is fired after an addon has finished its draw to screen.
|
||||||
|
/// See <see cref="PreDraw"/> for more information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
PostDraw,
|
PostDraw,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event that is fired before an addon is finalized.
|
/// An event that is fired immediately before an addon is finalized via <see cref="AtkUnitBase.Finalize"/> and
|
||||||
|
/// destroyed. After this event, the addon will destruct its UI node data as well as free any allocated memory.
|
||||||
|
/// This event can be used for cleanup and tracking tasks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This event is <em>NOT</em> fired when the addon is being hidden, but tends to be fired when it's being properly
|
||||||
|
/// closed.
|
||||||
|
/// <br />
|
||||||
|
/// As this is part of the destruction process for an addon, this event does not have an associated Post event.
|
||||||
|
/// </remarks>
|
||||||
|
/// <seealso cref="AddonFinalizeArgs"/>
|
||||||
PreFinalize,
|
PreFinalize,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event that is fired before an addon begins a requested update.
|
/// An event that is fired before a call to <see cref="AtkUnitBase.OnRequestedUpdate"/> is made in response to a
|
||||||
|
/// change in the subscribed <see cref="AddonRequestedUpdateArgs.NumberArrayData"/> or
|
||||||
|
/// <see cref="AddonRequestedUpdateArgs.StringArrayData"/> backing this addon. This generally occurs in response to
|
||||||
|
/// receiving data from the game server, but can happen in other cases as well. This event is useful for modifying
|
||||||
|
/// the data received before it's passed to the UI for display. Contrast to <see cref="PreRefresh"/> which tends to
|
||||||
|
/// be in response to <em>client-driven</em> interactions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <seealso cref="AddonRequestedUpdateArgs"/>
|
||||||
|
/// <seealso cref="PostRequestedUpdate"/>
|
||||||
|
/// <example>
|
||||||
|
/// A developer would use this event to intercept free company information after it's received from the server, but
|
||||||
|
/// before it's displayed to the user. This would allow the developer to add user-driven notes or other information
|
||||||
|
/// to the Free Company's overview.
|
||||||
|
/// </example>
|
||||||
PreRequestedUpdate,
|
PreRequestedUpdate,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event that is fired after an addon finishes a requested update.
|
/// An event that is fired after an addon has finished processing an <c>ArrayData</c> update.
|
||||||
|
/// See <see cref="PreRequestedUpdate"/> for more information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
PostRequestedUpdate,
|
PostRequestedUpdate,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event that is fired before an addon begins a refresh.
|
/// An event that is fired before an addon calls its <see cref="AtkUnitManager.RefreshAddon"/> method. Refreshes are
|
||||||
/// </summary>
|
/// generally triggered in response to certain user interactions such as changing tabs, and are primarily used to
|
||||||
|
/// update the <c>AtkValue</c>s present in this addon. Contrast to <see cref="PreRequestedUpdate"/> which is called
|
||||||
|
/// in response to <c>ArrayData</c> updates.</summary>
|
||||||
|
/// <seealso cref="AddonRefreshArgs"/>
|
||||||
|
/// <seealso cref="PostRefresh"/>
|
||||||
PreRefresh,
|
PreRefresh,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event that is fired after an addon has finished a refresh.
|
/// An event that is fired after an addon has finished its refresh.
|
||||||
|
/// See <see cref="PreRefresh"/> for more information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
PostRefresh,
|
PostRefresh,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event that is fired before an addon begins processing an event.
|
/// An event that is fired before an addon begins processing a user-driven event via
|
||||||
|
/// <see cref="AtkEventListener.ReceiveEvent"/>, such as mousing over an element or clicking a button. This event
|
||||||
|
/// is only valid for addons that actually override the <c>ReceiveEvent</c> method of the underlying
|
||||||
|
/// AtkEventListener.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <seealso cref="AddonReceiveEventArgs"/>
|
||||||
|
/// <seealso cref="PostReceiveEvent"/>
|
||||||
PreReceiveEvent,
|
PreReceiveEvent,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event that is fired after an addon has processed an event.
|
/// An event that is fired after an addon finishes calling its <see cref="AtkEventListener.ReceiveEvent"/> method.
|
||||||
|
/// See <see cref="PreReceiveEvent"/> for more information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
PostReceiveEvent,
|
PostReceiveEvent,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ internal class ChatHandlers : IServiceType
|
||||||
|
|
||||||
if (this.configuration.PrintDalamudWelcomeMsg)
|
if (this.configuration.PrintDalamudWelcomeMsg)
|
||||||
{
|
{
|
||||||
chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Util.GetGitHash())
|
chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Util.GetScmVersion())
|
||||||
+ string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count(x => x.IsLoaded)));
|
+ string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count(x => x.IsLoaded)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
42
Dalamud/Game/ClientState/JobGauge/Enums/SerpentCombo.cs
Normal file
42
Dalamud/Game/ClientState/JobGauge/Enums/SerpentCombo.cs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
namespace Dalamud.Game.ClientState.JobGauge.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enum representing the SerpentCombo actions for the VPR job gauge.
|
||||||
|
/// </summary>
|
||||||
|
public enum SerpentCombo : byte
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// No Serpent combo is active.
|
||||||
|
/// </summary>
|
||||||
|
NONE = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Death Rattle action.
|
||||||
|
/// </summary>
|
||||||
|
DEATHRATTLE = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Last Lash action.
|
||||||
|
/// </summary>
|
||||||
|
LASTLASH = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// First Legacy action.
|
||||||
|
/// </summary>
|
||||||
|
FIRSTLEGACY = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Second Legacy action.
|
||||||
|
/// </summary>
|
||||||
|
SECONDLEGACY = 4,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Third Legacy action.
|
||||||
|
/// </summary>
|
||||||
|
THIRDLEGACY = 5,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fourth Legacy action.
|
||||||
|
/// </summary>
|
||||||
|
FOURTHLEGACY = 6,
|
||||||
|
}
|
||||||
|
|
@ -18,4 +18,9 @@ public unsafe class NINGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game
|
||||||
/// Gets the amount of Ninki available.
|
/// Gets the amount of Ninki available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public byte Ninki => this.Struct->Ninki;
|
public byte Ninki => this.Struct->Ninki;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current charges for Kazematoi.
|
||||||
|
/// </summary>
|
||||||
|
public byte Kazematoi => this.Struct->Kazematoi;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
|
using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
|
||||||
|
|
||||||
using Reloaded.Memory;
|
using Reloaded.Memory;
|
||||||
|
|
||||||
using DreadCombo = Dalamud.Game.ClientState.JobGauge.Enums.DreadCombo;
|
using DreadCombo = Dalamud.Game.ClientState.JobGauge.Enums.DreadCombo;
|
||||||
|
using SerpentCombo = Dalamud.Game.ClientState.JobGauge.Enums.SerpentCombo;
|
||||||
|
|
||||||
namespace Dalamud.Game.ClientState.JobGauge.Types;
|
namespace Dalamud.Game.ClientState.JobGauge.Types;
|
||||||
|
|
||||||
|
|
@ -39,4 +40,9 @@ public unsafe class VPRGauge : JobGaugeBase<ViperGauge>
|
||||||
/// Gets the last Weaponskill used in DreadWinder/Pit of Dread combo.
|
/// Gets the last Weaponskill used in DreadWinder/Pit of Dread combo.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DreadCombo DreadCombo => (DreadCombo)Struct->DreadCombo;
|
public DreadCombo DreadCombo => (DreadCombo)Struct->DreadCombo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets current ability for Serpent's Tail.
|
||||||
|
/// </summary>
|
||||||
|
public SerpentCombo SerpentCombo => (SerpentCombo)Struct->SerpentCombo;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,18 @@ public interface ICharacter : IGameObject
|
||||||
/// Gets the status flags.
|
/// Gets the status flags.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public StatusFlags StatusFlags { get; }
|
public StatusFlags StatusFlags { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current mount for this character. Will be <c>null</c> if the character doesn't have a mount.
|
||||||
|
/// </summary>
|
||||||
|
public ExcelResolver<Mount>? CurrentMount { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current minion summoned for this character. Will be <c>null</c> if the character doesn't have a minion.
|
||||||
|
/// This method *will* return information about a spawned (but invisible) minion, e.g. if the character is riding a
|
||||||
|
/// mount.
|
||||||
|
/// </summary>
|
||||||
|
public ExcelResolver<Companion>? CurrentMinion { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -172,6 +184,32 @@ internal unsafe class Character : GameObject, ICharacter
|
||||||
(this.Struct->IsAllianceMember ? StatusFlags.AllianceMember : StatusFlags.None) |
|
(this.Struct->IsAllianceMember ? StatusFlags.AllianceMember : StatusFlags.None) |
|
||||||
(this.Struct->IsFriend ? StatusFlags.Friend : StatusFlags.None) |
|
(this.Struct->IsFriend ? StatusFlags.Friend : StatusFlags.None) |
|
||||||
(this.Struct->IsCasting ? StatusFlags.IsCasting : StatusFlags.None);
|
(this.Struct->IsCasting ? StatusFlags.IsCasting : StatusFlags.None);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ExcelResolver<Mount>? CurrentMount
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (this.Struct->IsNotMounted()) return null; // just for safety.
|
||||||
|
|
||||||
|
var mountId = this.Struct->Mount.MountId;
|
||||||
|
return mountId == 0 ? null : new ExcelResolver<Mount>(mountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ExcelResolver<Companion>? CurrentMinion
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (this.Struct->CompanionObject != null)
|
||||||
|
return new ExcelResolver<Companion>(this.Struct->CompanionObject->BaseId);
|
||||||
|
|
||||||
|
// this is only present if a minion is summoned but hidden (e.g. the player's on a mount).
|
||||||
|
var hiddenCompanionId = this.Struct->CompanionData.CompanionId;
|
||||||
|
return hiddenCompanionId == 0 ? null : new ExcelResolver<Companion>(hiddenCompanionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the underlying structure.
|
/// Gets the underlying structure.
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,17 @@ namespace Dalamud.Game.Config;
|
||||||
[ServiceManager.EarlyLoadedService]
|
[ServiceManager.EarlyLoadedService]
|
||||||
internal sealed class GameConfig : IInternalDisposableService, IGameConfig
|
internal sealed class GameConfig : IInternalDisposableService, IGameConfig
|
||||||
{
|
{
|
||||||
private readonly TaskCompletionSource tcsInitialization = new();
|
private readonly TaskCompletionSource tcsInitialization =
|
||||||
private readonly TaskCompletionSource<GameConfigSection> tcsSystem = new();
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
private readonly TaskCompletionSource<GameConfigSection> tcsUiConfig = new();
|
|
||||||
private readonly TaskCompletionSource<GameConfigSection> tcsUiControl = new();
|
private readonly TaskCompletionSource<GameConfigSection> tcsSystem =
|
||||||
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
private readonly TaskCompletionSource<GameConfigSection> tcsUiConfig =
|
||||||
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
|
private readonly TaskCompletionSource<GameConfigSection> tcsUiControl =
|
||||||
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
private readonly GameConfigAddressResolver address = new();
|
private readonly GameConfigAddressResolver address = new();
|
||||||
private Hook<ConfigChangeDelegate>? configChangeHook;
|
private Hook<ConfigChangeDelegate>? configChangeHook;
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework
|
||||||
if (numTicks <= 0)
|
if (numTicks <= 0)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource();
|
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
this.tickDelayedTaskCompletionSources[tcs] = (this.tickCounter + (ulong)numTicks, cancellationToken);
|
this.tickDelayedTaskCompletionSources[tcs] = (this.tickCounter + (ulong)numTicks, cancellationToken);
|
||||||
return tcs.Task;
|
return tcs.Task;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,17 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
||||||
this.configuration.QueueSave();
|
this.configuration.QueueSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event type fired each time a DtrEntry was removed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">The title of the bar entry.</param>
|
||||||
|
internal delegate void DtrEntryRemovedDelegate(string title);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event fired each time a DtrEntry was removed.
|
||||||
|
/// </summary>
|
||||||
|
internal event DtrEntryRemovedDelegate? DtrEntryRemoved;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IReadOnlyList<IReadOnlyDtrBarEntry> Entries => this.entries;
|
public IReadOnlyList<IReadOnlyDtrBarEntry> Entries => this.entries;
|
||||||
|
|
||||||
|
|
@ -131,9 +142,13 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal void HandleRemovedNodes()
|
internal void HandleRemovedNodes()
|
||||||
{
|
{
|
||||||
foreach (var data in this.entries.Where(d => d.ShouldBeRemoved))
|
foreach (var data in this.entries)
|
||||||
{
|
{
|
||||||
this.RemoveEntry(data);
|
if (data.ShouldBeRemoved)
|
||||||
|
{
|
||||||
|
this.RemoveEntry(data);
|
||||||
|
this.DtrEntryRemoved?.Invoke(data.Title);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.entries.RemoveAll(d => d.ShouldBeRemoved);
|
this.entries.RemoveAll(d => d.ShouldBeRemoved);
|
||||||
|
|
@ -210,7 +225,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
||||||
|
|
||||||
// If we have an unmodified DTR but still have entries, we need to
|
// If we have an unmodified DTR but still have entries, we need to
|
||||||
// work to reset our state.
|
// work to reset our state.
|
||||||
if (!this.CheckForDalamudNodes())
|
if (!this.CheckForDalamudNodes(dtr))
|
||||||
this.RecreateNodes();
|
this.RecreateNodes();
|
||||||
|
|
||||||
var collisionNode = dtr->GetNodeById(17);
|
var collisionNode = dtr->GetNodeById(17);
|
||||||
|
|
@ -223,40 +238,36 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
||||||
|
|
||||||
foreach (var data in this.entries)
|
foreach (var data in this.entries)
|
||||||
{
|
{
|
||||||
var isHide = data.UserHidden || !data.Shown;
|
if (!data.Added)
|
||||||
|
|
||||||
if (data is { Dirty: true, Added: true, Text: not null, TextNode: not null })
|
|
||||||
{
|
{
|
||||||
var node = data.TextNode;
|
data.Added = this.AddNode(data.TextNode);
|
||||||
|
data.Dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.Storage == null)
|
var isHide = !data.Shown || data.UserHidden;
|
||||||
|
var node = data.TextNode;
|
||||||
|
var nodeHidden = !node->AtkResNode.IsVisible();
|
||||||
|
|
||||||
|
if (!isHide)
|
||||||
|
{
|
||||||
|
if (nodeHidden)
|
||||||
|
node->AtkResNode.ToggleVisibility(true);
|
||||||
|
|
||||||
|
if (data is { Added: true, Text: not null, TextNode: not null } && (data.Dirty || nodeHidden))
|
||||||
{
|
{
|
||||||
data.Storage = Utf8String.CreateEmpty();
|
if (data.Storage == null)
|
||||||
}
|
{
|
||||||
|
data.Storage = Utf8String.CreateEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
data.Storage->SetString(data.Text.EncodeWithNullTerminator());
|
data.Storage->SetString(data.Text.EncodeWithNullTerminator());
|
||||||
node->SetText(data.Storage->StringPtr);
|
node->SetText(data.Storage->StringPtr);
|
||||||
|
|
||||||
ushort w = 0, h = 0;
|
ushort w = 0, h = 0;
|
||||||
|
|
||||||
if (!isHide)
|
|
||||||
{
|
|
||||||
node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr);
|
node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr);
|
||||||
node->AtkResNode.SetWidth(w);
|
node->AtkResNode.SetWidth(w);
|
||||||
}
|
}
|
||||||
|
|
||||||
node->AtkResNode.ToggleVisibility(!isHide);
|
|
||||||
|
|
||||||
data.Dirty = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.Added)
|
|
||||||
{
|
|
||||||
data.Added = this.AddNode(data.TextNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isHide)
|
|
||||||
{
|
|
||||||
var elementWidth = data.TextNode->AtkResNode.Width + this.configuration.DtrSpacing;
|
var elementWidth = data.TextNode->AtkResNode.Width + this.configuration.DtrSpacing;
|
||||||
|
|
||||||
if (this.configuration.DtrSwapDirection)
|
if (this.configuration.DtrSwapDirection)
|
||||||
|
|
@ -270,17 +281,20 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
||||||
data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2);
|
data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else if (!nodeHidden)
|
||||||
{
|
{
|
||||||
// If we want the node hidden, shift it up, to prevent collision conflicts
|
// If we want the node hidden, shift it up, to prevent collision conflicts
|
||||||
data.TextNode->AtkResNode.SetYFloat(-collisionNode->Height * dtr->RootNode->ScaleX);
|
node->AtkResNode.SetYFloat(-collisionNode->Height * dtr->RootNode->ScaleX);
|
||||||
|
node->AtkResNode.ToggleVisibility(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.Dirty = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleAddedNodes()
|
private void HandleAddedNodes()
|
||||||
{
|
{
|
||||||
if (this.newEntries.Any())
|
if (!this.newEntries.IsEmpty)
|
||||||
{
|
{
|
||||||
foreach (var newEntry in this.newEntries)
|
foreach (var newEntry in this.newEntries)
|
||||||
{
|
{
|
||||||
|
|
@ -354,11 +368,8 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
||||||
/// Checks if there are any Dalamud nodes in the DTR.
|
/// Checks if there are any Dalamud nodes in the DTR.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>True if there are nodes with an ID > 1000.</returns>
|
/// <returns>True if there are nodes with an ID > 1000.</returns>
|
||||||
private bool CheckForDalamudNodes()
|
private bool CheckForDalamudNodes(AtkUnitBase* dtr)
|
||||||
{
|
{
|
||||||
var dtr = this.GetDtr();
|
|
||||||
if (dtr == null || dtr->RootNode == null) return false;
|
|
||||||
|
|
||||||
for (var i = 0; i < dtr->UldManager.NodeListCount; i++)
|
for (var i = 0; i < dtr->UldManager.NodeListCount; i++)
|
||||||
{
|
{
|
||||||
if (dtr->UldManager.NodeList[i]->NodeId > 1000)
|
if (dtr->UldManager.NodeList[i]->NodeId > 1000)
|
||||||
|
|
@ -526,7 +537,6 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
|
||||||
/// Plugin-scoped version of a AddonEventManager service.
|
/// Plugin-scoped version of a AddonEventManager service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[PluginInterface]
|
[PluginInterface]
|
||||||
|
|
||||||
[ServiceManager.ScopedService]
|
[ServiceManager.ScopedService]
|
||||||
#pragma warning disable SA1015
|
#pragma warning disable SA1015
|
||||||
[ResolveVia<IDtrBar>]
|
[ResolveVia<IDtrBar>]
|
||||||
|
|
@ -537,13 +547,23 @@ internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar
|
||||||
private readonly DtrBar dtrBarService = Service<DtrBar>.Get();
|
private readonly DtrBar dtrBarService = Service<DtrBar>.Get();
|
||||||
|
|
||||||
private readonly Dictionary<string, IDtrBarEntry> pluginEntries = new();
|
private readonly Dictionary<string, IDtrBarEntry> pluginEntries = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DtrBarPluginScoped"/> class.
|
||||||
|
/// </summary>
|
||||||
|
internal DtrBarPluginScoped()
|
||||||
|
{
|
||||||
|
this.dtrBarService.DtrEntryRemoved += this.OnDtrEntryRemoved;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IReadOnlyList<IReadOnlyDtrBarEntry> Entries => this.dtrBarService.Entries;
|
public IReadOnlyList<IReadOnlyDtrBarEntry> Entries => this.dtrBarService.Entries;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
void IInternalDisposableService.DisposeService()
|
void IInternalDisposableService.DisposeService()
|
||||||
{
|
{
|
||||||
|
this.dtrBarService.DtrEntryRemoved -= this.OnDtrEntryRemoved;
|
||||||
|
|
||||||
foreach (var entry in this.pluginEntries)
|
foreach (var entry in this.pluginEntries)
|
||||||
{
|
{
|
||||||
entry.Value.Remove();
|
entry.Value.Remove();
|
||||||
|
|
@ -570,4 +590,9 @@ internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar
|
||||||
this.pluginEntries.Remove(title);
|
this.pluginEntries.Remove(title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnDtrEntryRemoved(string title)
|
||||||
|
{
|
||||||
|
this.pluginEntries.Remove(title);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,14 +137,17 @@ public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
|
||||||
get => this.shownBacking;
|
get => this.shownBacking;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
this.shownBacking = value;
|
if (value != this.shownBacking)
|
||||||
this.Dirty = true;
|
{
|
||||||
|
this.shownBacking = value;
|
||||||
|
this.Dirty = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
[Api10ToDo("Maybe make this config scoped to internalname?")]
|
[Api10ToDo("Maybe make this config scoped to internalname?")]
|
||||||
public bool UserHidden => this.configuration.DtrIgnore?.Any(x => x == this.Title) ?? false;
|
public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the internal text node of this entry.
|
/// Gets or sets the internal text node of this entry.
|
||||||
|
|
|
||||||
302
Dalamud/Game/Gui/NamePlate/NamePlateGui.cs
Normal file
302
Dalamud/Game/Gui/NamePlate/NamePlateGui.cs
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
using Dalamud.Game.Addon.Lifecycle;
|
||||||
|
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||||
|
using Dalamud.Game.ClientState.Objects;
|
||||||
|
using Dalamud.IoC;
|
||||||
|
using Dalamud.IoC.Internal;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
|
||||||
|
namespace Dalamud.Game.Gui.NamePlate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class used to modify the data used when rendering nameplates.
|
||||||
|
/// </summary>
|
||||||
|
[ServiceManager.EarlyLoadedService]
|
||||||
|
internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The index for the number array used by the NamePlate addon.
|
||||||
|
/// </summary>
|
||||||
|
public const int NumberArrayIndex = 5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The index for the string array used by the NamePlate addon.
|
||||||
|
/// </summary>
|
||||||
|
public const int StringArrayIndex = 4;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The index for of the FullUpdate entry in the NamePlate number array.
|
||||||
|
/// </summary>
|
||||||
|
internal const int NumberArrayFullUpdateIndex = 4;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An empty null-terminated string pointer allocated in unmanaged memory, used to tag removed fields.
|
||||||
|
/// </summary>
|
||||||
|
internal static readonly nint EmptyStringPointer = CreateEmptyStringPointer();
|
||||||
|
|
||||||
|
[ServiceManager.ServiceDependency]
|
||||||
|
private readonly AddonLifecycle addonLifecycle = Service<AddonLifecycle>.Get();
|
||||||
|
|
||||||
|
[ServiceManager.ServiceDependency]
|
||||||
|
private readonly GameGui gameGui = Service<GameGui>.Get();
|
||||||
|
|
||||||
|
[ServiceManager.ServiceDependency]
|
||||||
|
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
|
||||||
|
|
||||||
|
private readonly AddonLifecycleEventListener preRequestedUpdateListener;
|
||||||
|
|
||||||
|
private NamePlateUpdateContext? context;
|
||||||
|
|
||||||
|
private NamePlateUpdateHandler[] updateHandlers = [];
|
||||||
|
|
||||||
|
[ServiceManager.ServiceConstructor]
|
||||||
|
private NamePlateGui()
|
||||||
|
{
|
||||||
|
this.preRequestedUpdateListener = new AddonLifecycleEventListener(
|
||||||
|
AddonEvent.PreRequestedUpdate,
|
||||||
|
"NamePlate",
|
||||||
|
this.OnPreRequestedUpdate);
|
||||||
|
|
||||||
|
this.addonLifecycle.RegisterListener(this.preRequestedUpdateListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdate;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public unsafe void RequestRedraw()
|
||||||
|
{
|
||||||
|
var addon = this.gameGui.GetAddonByName("NamePlate");
|
||||||
|
if (addon != 0)
|
||||||
|
{
|
||||||
|
var raptureAtkModule = RaptureAtkModule.Instance();
|
||||||
|
if (raptureAtkModule == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
((AddonNamePlate*)addon)->DoFullUpdate = 1;
|
||||||
|
var namePlateNumberArrayData = raptureAtkModule->AtkArrayDataHolder.NumberArrays[NumberArrayIndex];
|
||||||
|
namePlateNumberArrayData->SetValue(NumberArrayFullUpdateIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
void IInternalDisposableService.DisposeService()
|
||||||
|
{
|
||||||
|
this.addonLifecycle.UnregisterListener(this.preRequestedUpdateListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Strips the surrounding quotes from a free company tag. If the quotes are not present in the expected location,
|
||||||
|
/// no modifications will be made.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">A quoted free company tag.</param>
|
||||||
|
/// <returns>A span containing the free company tag without its surrounding quote characters.</returns>
|
||||||
|
internal static ReadOnlySpan<byte> StripFreeCompanyTagQuotes(ReadOnlySpan<byte> text)
|
||||||
|
{
|
||||||
|
if (text.Length > 4 && text.StartsWith(" «"u8) && text.EndsWith("»"u8))
|
||||||
|
{
|
||||||
|
return text[3..^2];
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Strips the surrounding quotes from a title. If the quotes are not present in the expected location, no
|
||||||
|
/// modifications will be made.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">A quoted title.</param>
|
||||||
|
/// <returns>A span containing the title without its surrounding quote characters.</returns>
|
||||||
|
internal static ReadOnlySpan<byte> StripTitleQuotes(ReadOnlySpan<byte> text)
|
||||||
|
{
|
||||||
|
if (text.Length > 5 && text.StartsWith("《"u8) && text.EndsWith("》"u8))
|
||||||
|
{
|
||||||
|
return text[3..^3];
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static nint CreateEmptyStringPointer()
|
||||||
|
{
|
||||||
|
var pointer = Marshal.AllocHGlobal(1);
|
||||||
|
Marshal.WriteByte(pointer, 0, 0);
|
||||||
|
return pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateHandlers(NamePlateUpdateContext createdContext)
|
||||||
|
{
|
||||||
|
var handlers = new List<NamePlateUpdateHandler>();
|
||||||
|
for (var i = 0; i < AddonNamePlate.NumNamePlateObjects; i++)
|
||||||
|
{
|
||||||
|
handlers.Add(new NamePlateUpdateHandler(createdContext, i));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateHandlers = handlers.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPreRequestedUpdate(AddonEvent type, AddonArgs args)
|
||||||
|
{
|
||||||
|
if (this.OnDataUpdate == null && this.OnNamePlateUpdate == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reqArgs = (AddonRequestedUpdateArgs)args;
|
||||||
|
if (this.context == null)
|
||||||
|
{
|
||||||
|
this.context = new NamePlateUpdateContext(this.objectTable, reqArgs);
|
||||||
|
this.CreateHandlers(this.context);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.context.ResetState(reqArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeNamePlateCount = this.context.ActiveNamePlateCount;
|
||||||
|
if (activeNamePlateCount == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var activeHandlers = this.updateHandlers[..activeNamePlateCount];
|
||||||
|
|
||||||
|
if (this.context.IsFullUpdate)
|
||||||
|
{
|
||||||
|
foreach (var handler in activeHandlers)
|
||||||
|
{
|
||||||
|
handler.ResetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.OnDataUpdate?.Invoke(this.context, activeHandlers);
|
||||||
|
this.OnNamePlateUpdate?.Invoke(this.context, activeHandlers);
|
||||||
|
if (this.context.HasParts)
|
||||||
|
this.ApplyBuilders(activeHandlers);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var udpatedHandlers = new List<NamePlateUpdateHandler>(activeNamePlateCount);
|
||||||
|
foreach (var handler in activeHandlers)
|
||||||
|
{
|
||||||
|
handler.ResetState();
|
||||||
|
if (handler.IsUpdating)
|
||||||
|
udpatedHandlers.Add(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.OnDataUpdate is not null)
|
||||||
|
{
|
||||||
|
this.OnDataUpdate?.Invoke(this.context, activeHandlers);
|
||||||
|
this.OnNamePlateUpdate?.Invoke(this.context, udpatedHandlers);
|
||||||
|
if (this.context.HasParts)
|
||||||
|
this.ApplyBuilders(activeHandlers);
|
||||||
|
}
|
||||||
|
else if (udpatedHandlers.Count != 0)
|
||||||
|
{
|
||||||
|
var changedHandlersSpan = udpatedHandlers.ToArray().AsSpan();
|
||||||
|
this.OnNamePlateUpdate?.Invoke(this.context, udpatedHandlers);
|
||||||
|
if (this.context.HasParts)
|
||||||
|
this.ApplyBuilders(changedHandlersSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyBuilders(Span<NamePlateUpdateHandler> handlers)
|
||||||
|
{
|
||||||
|
foreach (var handler in handlers)
|
||||||
|
{
|
||||||
|
if (handler.PartsContainer is { } container)
|
||||||
|
{
|
||||||
|
container.ApplyBuilders(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Plugin-scoped version of a AddonEventManager service.
|
||||||
|
/// </summary>
|
||||||
|
[PluginInterface]
|
||||||
|
[ServiceManager.ScopedService]
|
||||||
|
#pragma warning disable SA1015
|
||||||
|
[ResolveVia<INamePlateGui>]
|
||||||
|
#pragma warning restore SA1015
|
||||||
|
internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlateGui
|
||||||
|
{
|
||||||
|
[ServiceManager.ServiceDependency]
|
||||||
|
private readonly NamePlateGui parentService = Service<NamePlateGui>.Get();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdate
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (this.OnNamePlateUpdateScoped == null)
|
||||||
|
this.parentService.OnNamePlateUpdate += this.OnNamePlateUpdateForward;
|
||||||
|
this.OnNamePlateUpdateScoped += value;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
this.OnNamePlateUpdateScoped -= value;
|
||||||
|
if (this.OnNamePlateUpdateScoped == null)
|
||||||
|
this.parentService.OnNamePlateUpdate -= this.OnNamePlateUpdateForward;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate
|
||||||
|
{
|
||||||
|
add
|
||||||
|
{
|
||||||
|
if (this.OnDataUpdateScoped == null)
|
||||||
|
this.parentService.OnDataUpdate += this.OnDataUpdateForward;
|
||||||
|
this.OnDataUpdateScoped += value;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove
|
||||||
|
{
|
||||||
|
this.OnDataUpdateScoped -= value;
|
||||||
|
if (this.OnDataUpdateScoped == null)
|
||||||
|
this.parentService.OnDataUpdate -= this.OnDataUpdateForward;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdateScoped;
|
||||||
|
|
||||||
|
private event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdateScoped;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void RequestRedraw()
|
||||||
|
{
|
||||||
|
this.parentService.RequestRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void DisposeService()
|
||||||
|
{
|
||||||
|
this.parentService.OnNamePlateUpdate -= this.OnNamePlateUpdateForward;
|
||||||
|
this.OnNamePlateUpdateScoped = null;
|
||||||
|
|
||||||
|
this.parentService.OnDataUpdate -= this.OnDataUpdateForward;
|
||||||
|
this.OnDataUpdateScoped = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNamePlateUpdateForward(
|
||||||
|
INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
||||||
|
{
|
||||||
|
this.OnNamePlateUpdateScoped?.Invoke(context, handlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDataUpdateForward(
|
||||||
|
INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
||||||
|
{
|
||||||
|
this.OnDataUpdateScoped?.Invoke(context, handlers);
|
||||||
|
}
|
||||||
|
}
|
||||||
105
Dalamud/Game/Gui/NamePlate/NamePlateInfoView.cs
Normal file
105
Dalamud/Game/Gui/NamePlate/NamePlateInfoView.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
|
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
|
||||||
|
namespace Dalamud.Game.Gui.NamePlate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a read-only view of the nameplate info object data for a nameplate. Modifications to
|
||||||
|
/// <see cref="NamePlateUpdateHandler"/> fields do not affect this data.
|
||||||
|
/// </summary>
|
||||||
|
public interface INamePlateInfoView
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the displayed name for this nameplate according to the nameplate info object.
|
||||||
|
/// </summary>
|
||||||
|
SeString Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the displayed free company tag for this nameplate according to the nameplate info object. For this field,
|
||||||
|
/// the quote characters which appear on either side of the title are NOT included.
|
||||||
|
/// </summary>
|
||||||
|
SeString FreeCompanyTag { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the displayed free company tag for this nameplate according to the nameplate info object. For this field,
|
||||||
|
/// the quote characters which appear on either side of the title ARE included.
|
||||||
|
/// </summary>
|
||||||
|
SeString QuotedFreeCompanyTag { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the displayed title for this nameplate according to the nameplate info object. For this field, the quote
|
||||||
|
/// characters which appear on either side of the title are NOT included.
|
||||||
|
/// </summary>
|
||||||
|
SeString Title { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the displayed title for this nameplate according to the nameplate info object. For this field, the quote
|
||||||
|
/// characters which appear on either side of the title ARE included.
|
||||||
|
/// </summary>
|
||||||
|
SeString QuotedTitle { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the displayed level text for this nameplate according to the nameplate info object.
|
||||||
|
/// </summary>
|
||||||
|
SeString LevelText { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the flags for this nameplate according to the nameplate info object.
|
||||||
|
/// </summary>
|
||||||
|
int Flags { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this nameplate is considered 'dirty' or not according to the nameplate
|
||||||
|
/// info object.
|
||||||
|
/// </summary>
|
||||||
|
bool IsDirty { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the title for this nameplate is a prefix title or not according to the nameplate
|
||||||
|
/// info object. This value is derived from the <see cref="Flags"/> field.
|
||||||
|
/// </summary>
|
||||||
|
bool IsPrefixTitle { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides a read-only view of the nameplate info object data for a nameplate. Modifications to
|
||||||
|
/// <see cref="NamePlateUpdateHandler"/> fields do not affect this data.
|
||||||
|
/// </summary>
|
||||||
|
internal unsafe class NamePlateInfoView(RaptureAtkModule.NamePlateInfo* info) : INamePlateInfoView
|
||||||
|
{
|
||||||
|
private SeString? name;
|
||||||
|
private SeString? freeCompanyTag;
|
||||||
|
private SeString? quotedFreeCompanyTag;
|
||||||
|
private SeString? title;
|
||||||
|
private SeString? quotedTitle;
|
||||||
|
private SeString? levelText;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public SeString Name => this.name ??= SeString.Parse(info->Name);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public SeString FreeCompanyTag => this.freeCompanyTag ??=
|
||||||
|
SeString.Parse(NamePlateGui.StripFreeCompanyTagQuotes(info->FcName));
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public SeString QuotedFreeCompanyTag => this.quotedFreeCompanyTag ??= SeString.Parse(info->FcName);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public SeString Title => this.title ??= SeString.Parse(info->Title);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public SeString QuotedTitle => this.quotedTitle ??= SeString.Parse(info->DisplayTitle);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public SeString LevelText => this.levelText ??= SeString.Parse(info->LevelText);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public int Flags => info->Flags;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsDirty => info->IsDirty;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsPrefixTitle => ((info->Flags >> (8 * 3)) & 0xFF) == 1;
|
||||||
|
}
|
||||||
57
Dalamud/Game/Gui/NamePlate/NamePlateKind.cs
Normal file
57
Dalamud/Game/Gui/NamePlate/NamePlateKind.cs
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
namespace Dalamud.Game.Gui.NamePlate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An enum describing what kind of game object this nameplate represents.
|
||||||
|
/// </summary>
|
||||||
|
public enum NamePlateKind : byte
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A player character.
|
||||||
|
/// </summary>
|
||||||
|
PlayerCharacter = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An event NPC or companion.
|
||||||
|
/// </summary>
|
||||||
|
EventNpcCompanion = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A retainer.
|
||||||
|
/// </summary>
|
||||||
|
Retainer = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An enemy battle NPC.
|
||||||
|
/// </summary>
|
||||||
|
BattleNpcEnemy = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A friendly battle NPC.
|
||||||
|
/// </summary>
|
||||||
|
BattleNpcFriendly = 4,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An event object.
|
||||||
|
/// </summary>
|
||||||
|
EventObject = 5,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Treasure.
|
||||||
|
/// </summary>
|
||||||
|
Treasure = 6,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A gathering point.
|
||||||
|
/// </summary>
|
||||||
|
GatheringPoint = 7,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A battle NPC with subkind 6.
|
||||||
|
/// </summary>
|
||||||
|
BattleNpcSubkind6 = 8,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Something else.
|
||||||
|
/// </summary>
|
||||||
|
Other = 9,
|
||||||
|
}
|
||||||
46
Dalamud/Game/Gui/NamePlate/NamePlatePartsContainer.cs
Normal file
46
Dalamud/Game/Gui/NamePlate/NamePlatePartsContainer.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
namespace Dalamud.Game.Gui.NamePlate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A container for parts.
|
||||||
|
/// </summary>
|
||||||
|
internal class NamePlatePartsContainer
|
||||||
|
{
|
||||||
|
private NamePlateSimpleParts? nameParts;
|
||||||
|
private NamePlateQuotedParts? titleParts;
|
||||||
|
private NamePlateQuotedParts? freeCompanyTagParts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="NamePlatePartsContainer"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The currently executing update context.</param>
|
||||||
|
public NamePlatePartsContainer(NamePlateUpdateContext context)
|
||||||
|
{
|
||||||
|
context.HasParts = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a parts object for constructing a nameplate name.
|
||||||
|
/// </summary>
|
||||||
|
internal NamePlateSimpleParts Name => this.nameParts ??= new NamePlateSimpleParts(NamePlateStringField.Name);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a parts object for constructing a nameplate title.
|
||||||
|
/// </summary>
|
||||||
|
internal NamePlateQuotedParts Title => this.titleParts ??= new NamePlateQuotedParts(NamePlateStringField.Title, false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a parts object for constructing a nameplate free company tag.
|
||||||
|
/// </summary>
|
||||||
|
internal NamePlateQuotedParts FreeCompanyTag => this.freeCompanyTagParts ??= new NamePlateQuotedParts(NamePlateStringField.FreeCompanyTag, true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies all container parts.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="handler">The handler to apply the builders to.</param>
|
||||||
|
internal void ApplyBuilders(NamePlateUpdateHandler handler)
|
||||||
|
{
|
||||||
|
this.nameParts?.Apply(handler);
|
||||||
|
this.freeCompanyTagParts?.Apply(handler);
|
||||||
|
this.titleParts?.Apply(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
105
Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs
Normal file
105
Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
|
|
||||||
|
namespace Dalamud.Game.Gui.NamePlate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A part builder for constructing and setting quoted nameplate fields (i.e. free company tag and title).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field type which should be set.</param>
|
||||||
|
/// <param name="isFreeCompany">Whether or not this is a Free Company part.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// This class works as a lazy writer initialized with empty parts, where an empty part signifies no change should be
|
||||||
|
/// performed. Only after all handler processing is complete does it write out any parts which were set to the
|
||||||
|
/// associated field. Reading fields from this class is usually not what you want to do, as you'll only be reading the
|
||||||
|
/// contents of parts which other plugins have written to. Prefer reading from the base handler's properties or using
|
||||||
|
/// <see cref="NamePlateInfoView"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the opening and closing SeStrings which will wrap the entire contents, which can be used to apply
|
||||||
|
/// colors or styling to the entire field.
|
||||||
|
/// </summary>
|
||||||
|
public (SeString, SeString)? OuterWrap { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the opening quote string which appears before the text and opening text-wrap.
|
||||||
|
/// </summary>
|
||||||
|
public SeString? LeftQuote { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the closing quote string which appears after the text and closing text-wrap.
|
||||||
|
/// </summary>
|
||||||
|
public SeString? RightQuote { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the opening and closing SeStrings which will wrap the text, which can be used to apply colors or
|
||||||
|
/// styling to the field's text.
|
||||||
|
/// </summary>
|
||||||
|
public (SeString, SeString)? TextWrap { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets this field's text.
|
||||||
|
/// </summary>
|
||||||
|
public SeString? Text { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies the changes from this builder to the actual field.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="handler">The handler to perform the changes on.</param>
|
||||||
|
internal unsafe void Apply(NamePlateUpdateHandler handler)
|
||||||
|
{
|
||||||
|
if ((nint)handler.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var sb = new SeStringBuilder();
|
||||||
|
if (this.OuterWrap is { Item1: var outerLeft })
|
||||||
|
{
|
||||||
|
sb.Append(outerLeft);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.LeftQuote is not null)
|
||||||
|
{
|
||||||
|
sb.Append(this.LeftQuote);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append(isFreeCompany ? " «" : "《");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.TextWrap is { Item1: var left, Item2: var right })
|
||||||
|
{
|
||||||
|
sb.Append(left);
|
||||||
|
sb.Append(this.Text ?? this.GetStrippedField(handler));
|
||||||
|
sb.Append(right);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append(this.Text ?? this.GetStrippedField(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.RightQuote is not null)
|
||||||
|
{
|
||||||
|
sb.Append(this.RightQuote);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append(isFreeCompany ? "»" : "》");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.OuterWrap is { Item2: var outerRight })
|
||||||
|
{
|
||||||
|
sb.Append(outerRight);
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.SetField(field, sb.Build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SeString GetStrippedField(NamePlateUpdateHandler handler)
|
||||||
|
{
|
||||||
|
return SeString.Parse(
|
||||||
|
isFreeCompany
|
||||||
|
? NamePlateGui.StripFreeCompanyTagQuotes(handler.GetFieldAsSpan(field))
|
||||||
|
: NamePlateGui.StripTitleQuotes(handler.GetFieldAsSpan(field)));
|
||||||
|
}
|
||||||
|
}
|
||||||
51
Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs
Normal file
51
Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
|
|
||||||
|
namespace Dalamud.Game.Gui.NamePlate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A part builder for constructing and setting a simple (unquoted) nameplate field.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field type which should be set.</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// This class works as a lazy writer initialized with empty parts, where an empty part signifies no change should be
|
||||||
|
/// performed. Only after all handler processing is complete does it write out any parts which were set to the
|
||||||
|
/// associated field. Reading fields from this class is usually not what you want to do, as you'll only be reading the
|
||||||
|
/// contents of parts which other plugins have written to. Prefer reading from the base handler's properties or using
|
||||||
|
/// <see cref="NamePlateInfoView"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public class NamePlateSimpleParts(NamePlateStringField field)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the opening and closing SeStrings which will wrap the text, which can be used to apply colors or
|
||||||
|
/// styling to the field's text.
|
||||||
|
/// </summary>
|
||||||
|
public (SeString, SeString)? TextWrap { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets this field's text.
|
||||||
|
/// </summary>
|
||||||
|
public SeString? Text { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies the changes from this builder to the actual field.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="handler">The handler to perform the changes on.</param>
|
||||||
|
internal unsafe void Apply(NamePlateUpdateHandler handler)
|
||||||
|
{
|
||||||
|
if ((nint)handler.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (this.TextWrap is { Item1: var left, Item2: var right })
|
||||||
|
{
|
||||||
|
var sb = new SeStringBuilder();
|
||||||
|
sb.Append(left);
|
||||||
|
sb.Append(this.Text ?? handler.GetFieldAsSeString(field));
|
||||||
|
sb.Append(right);
|
||||||
|
handler.SetField(field, sb.Build());
|
||||||
|
}
|
||||||
|
else if (this.Text is not null)
|
||||||
|
{
|
||||||
|
handler.SetField(field, this.Text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Dalamud/Game/Gui/NamePlate/NamePlateStringField.cs
Normal file
38
Dalamud/Game/Gui/NamePlate/NamePlateStringField.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
namespace Dalamud.Game.Gui.NamePlate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An enum describing the string fields available in nameplate data. The <see cref="NamePlateKind"/> and various flags
|
||||||
|
/// determine which fields will actually be rendered.
|
||||||
|
/// </summary>
|
||||||
|
public enum NamePlateStringField
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The object's name.
|
||||||
|
/// </summary>
|
||||||
|
Name = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The object's title.
|
||||||
|
/// </summary>
|
||||||
|
Title = 50,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The object's free company tag.
|
||||||
|
/// </summary>
|
||||||
|
FreeCompanyTag = 100,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The object's status prefix.
|
||||||
|
/// </summary>
|
||||||
|
StatusPrefix = 150,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The object's target suffix.
|
||||||
|
/// </summary>
|
||||||
|
TargetSuffix = 200,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The object's level prefix.
|
||||||
|
/// </summary>
|
||||||
|
LevelPrefix = 250,
|
||||||
|
}
|
||||||
152
Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs
Normal file
152
Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||||
|
using Dalamud.Game.ClientState.Objects;
|
||||||
|
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
|
||||||
|
namespace Dalamud.Game.Gui.NamePlate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains information related to the pending nameplate data update. This is only valid for a single frame and should
|
||||||
|
/// not be kept across frames.
|
||||||
|
/// </summary>
|
||||||
|
public interface INamePlateUpdateContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the number of active nameplates. The actual number visible may be lower than this in cases where some
|
||||||
|
/// nameplates are hidden by default (based on in-game "Display Name Settings" and so on).
|
||||||
|
/// </summary>
|
||||||
|
int ActiveNamePlateCount { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the game is currently performing a full update of all active nameplates.
|
||||||
|
/// </summary>
|
||||||
|
bool IsFullUpdate { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the address of the NamePlate addon.
|
||||||
|
/// </summary>
|
||||||
|
nint AddonAddress { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the address of the NamePlate addon's number array data container.
|
||||||
|
/// </summary>
|
||||||
|
nint NumberArrayDataAddress { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the address of the NamePlate addon's string array data container.
|
||||||
|
/// </summary>
|
||||||
|
nint StringArrayDataAddress { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the address of the first entry in the NamePlate addon's int array.
|
||||||
|
/// </summary>
|
||||||
|
nint NumberArrayDataEntryAddress { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains information related to the pending nameplate data update. This is only valid for a single frame and should
|
||||||
|
/// not be kept across frames.
|
||||||
|
/// </summary>
|
||||||
|
internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="NamePlateUpdateContext"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="objectTable">An object table.</param>
|
||||||
|
/// <param name="args">The addon lifecycle arguments for the update request.</param>
|
||||||
|
internal NamePlateUpdateContext(ObjectTable objectTable, AddonRequestedUpdateArgs args)
|
||||||
|
{
|
||||||
|
this.ObjectTable = objectTable;
|
||||||
|
this.RaptureAtkModule = FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkModule.Instance();
|
||||||
|
this.Ui3DModule = UIModule.Instance()->GetUI3DModule();
|
||||||
|
this.ResetState(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the number of active nameplates. The actual number visible may be lower than this in cases where some
|
||||||
|
/// nameplates are hidden by default (based on in-game "Display Name Settings" and so on).
|
||||||
|
/// </summary>
|
||||||
|
public int ActiveNamePlateCount { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether the game is currently performing a full update of all active nameplates.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsFullUpdate { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the address of the NamePlate addon.
|
||||||
|
/// </summary>
|
||||||
|
public nint AddonAddress => (nint)this.Addon;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the address of the NamePlate addon's number array data container.
|
||||||
|
/// </summary>
|
||||||
|
public nint NumberArrayDataAddress => (nint)this.NumberData;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the address of the NamePlate addon's string array data container.
|
||||||
|
/// </summary>
|
||||||
|
public nint StringArrayDataAddress => (nint)this.StringData;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the address of the first entry in the NamePlate addon's int array.
|
||||||
|
/// </summary>
|
||||||
|
public nint NumberArrayDataEntryAddress => (nint)this.NumberStruct;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the RaptureAtkModule.
|
||||||
|
/// </summary>
|
||||||
|
internal RaptureAtkModule* RaptureAtkModule { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Ui3DModule.
|
||||||
|
/// </summary>
|
||||||
|
internal UI3DModule* Ui3DModule { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the ObjectTable.
|
||||||
|
/// </summary>
|
||||||
|
internal ObjectTable ObjectTable { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a pointer to the NamePlate addon.
|
||||||
|
/// </summary>
|
||||||
|
internal AddonNamePlate* Addon { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a pointer to the NamePlate addon's number array data container.
|
||||||
|
/// </summary>
|
||||||
|
internal NumberArrayData* NumberData { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a pointer to the NamePlate addon's string array data container.
|
||||||
|
/// </summary>
|
||||||
|
internal StringArrayData* StringData { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a pointer to the NamePlate addon's number array entries as a struct.
|
||||||
|
/// </summary>
|
||||||
|
internal AddonNamePlate.NamePlateIntArrayData* NumberStruct { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether any handler in the current context has instantiated a part builder.
|
||||||
|
/// </summary>
|
||||||
|
internal bool HasParts { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the state of the context based on the provided addon lifecycle arguments.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">The addon lifecycle arguments for the update request.</param>
|
||||||
|
internal void ResetState(AddonRequestedUpdateArgs args)
|
||||||
|
{
|
||||||
|
this.Addon = (AddonNamePlate*)args.Addon;
|
||||||
|
this.NumberData = ((NumberArrayData**)args.NumberArrayData)![NamePlateGui.NumberArrayIndex];
|
||||||
|
this.NumberStruct = (AddonNamePlate.NamePlateIntArrayData*)this.NumberData->IntArray;
|
||||||
|
this.StringData = ((StringArrayData**)args.StringArrayData)![NamePlateGui.StringArrayIndex];
|
||||||
|
this.HasParts = false;
|
||||||
|
|
||||||
|
this.ActiveNamePlateCount = this.NumberStruct->ActiveNamePlateCount;
|
||||||
|
this.IsFullUpdate = this.Addon->DoFullUpdate != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
616
Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs
Normal file
616
Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs
Normal file
|
|
@ -0,0 +1,616 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
|
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
using FFXIVClientStructs.Interop;
|
||||||
|
|
||||||
|
namespace Dalamud.Game.Gui.NamePlate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A class representing a single nameplate. Provides mechanisms to look up the game object associated with the
|
||||||
|
/// nameplate and allows for modification of various backing fields in number and string array data, which in turn
|
||||||
|
/// affect aspects of the nameplate's appearance when drawn. Instances of this class are only valid for a single frame
|
||||||
|
/// and should not be kept across frames.
|
||||||
|
/// </summary>
|
||||||
|
public interface INamePlateUpdateHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the GameObjectId of the game object associated with this nameplate.
|
||||||
|
/// </summary>
|
||||||
|
ulong GameObjectId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="IGameObject"/> associated with this nameplate, if possible. Performs an object table scan
|
||||||
|
/// and caches the result if successful.
|
||||||
|
/// </summary>
|
||||||
|
IGameObject? GameObject { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a read-only view of the nameplate info object data for a nameplate. Modifications to
|
||||||
|
/// <see cref="NamePlateUpdateHandler"/> fields do not affect fields in the returned view.
|
||||||
|
/// </summary>
|
||||||
|
INamePlateInfoView InfoView { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the index for this nameplate data in the backing number and string array data. This is not the same as the
|
||||||
|
/// rendered or object index, which can be retrieved from <see cref="NamePlateIndex"/>.
|
||||||
|
/// </summary>
|
||||||
|
int ArrayIndex { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="IBattleChara"/> associated with this nameplate, if possible. Returns null if the nameplate
|
||||||
|
/// has an associated <see cref="IGameObject"/>, but that object cannot be assigned to <see cref="IBattleChara"/>.
|
||||||
|
/// </summary>
|
||||||
|
IBattleChara? BattleChara { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="IPlayerCharacter"/> associated with this nameplate, if possible. Returns null if the
|
||||||
|
/// nameplate has an associated <see cref="IGameObject"/>, but that object cannot be assigned to
|
||||||
|
/// <see cref="IPlayerCharacter"/>.
|
||||||
|
/// </summary>
|
||||||
|
IPlayerCharacter? PlayerCharacter { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the address of the nameplate info struct.
|
||||||
|
/// </summary>
|
||||||
|
nint NamePlateInfoAddress { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the address of the first entry associated with this nameplate in the NamePlate addon's int array.
|
||||||
|
/// </summary>
|
||||||
|
nint NamePlateObjectAddress { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating what kind of nameplate this is, based on the kind of object it is associated with.
|
||||||
|
/// </summary>
|
||||||
|
NamePlateKind NamePlateKind { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the update flags for this nameplate.
|
||||||
|
/// </summary>
|
||||||
|
int UpdateFlags { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the overall text color for this nameplate. If this value is changed, the appropriate update flag
|
||||||
|
/// will be set so that the game will reflect this change immediately.
|
||||||
|
/// </summary>
|
||||||
|
uint TextColor { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the overall text edge color for this nameplate. If this value is changed, the appropriate update
|
||||||
|
/// flag will be set so that the game will reflect this change immediately.
|
||||||
|
/// </summary>
|
||||||
|
uint EdgeColor { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the icon ID for the nameplate's marker icon, which is the large icon used to indicate quest
|
||||||
|
/// availability and so on. This value is read from and reset by the game every frame, not just when a nameplate
|
||||||
|
/// changes. Setting this to 0 disables the icon.
|
||||||
|
/// </summary>
|
||||||
|
int MarkerIconId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the icon ID for the nameplate's name icon, which is the small icon shown to the left of the name.
|
||||||
|
/// Setting this to -1 disables the icon.
|
||||||
|
/// </summary>
|
||||||
|
int NameIconId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the nameplate index, which is the index used for rendering and looking up entries in the object array. For
|
||||||
|
/// number and string array data, <see cref="ArrayIndex"/> is used.
|
||||||
|
/// </summary>
|
||||||
|
int NamePlateIndex { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the draw flags for this nameplate.
|
||||||
|
/// </summary>
|
||||||
|
int DrawFlags { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the visibility flags for this nameplate.
|
||||||
|
/// </summary>
|
||||||
|
int VisibilityFlags { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether this nameplate is undergoing a major update or not. This is usually true when a
|
||||||
|
/// nameplate has just appeared or something meaningful about the entity has changed (e.g. its job or status). This
|
||||||
|
/// flag is reset by the game during the update process (during requested update and before draw).
|
||||||
|
/// </summary>
|
||||||
|
bool IsUpdating { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the title (when visible) will be displayed above the object's name (a
|
||||||
|
/// prefix title) instead of below the object's name (a suffix title).
|
||||||
|
/// </summary>
|
||||||
|
bool IsPrefixTitle { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the title should be displayed at all.
|
||||||
|
/// </summary>
|
||||||
|
bool DisplayTitle { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name for this nameplate.
|
||||||
|
/// </summary>
|
||||||
|
SeString Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a builder which can be used to help cooperatively build a new name for this nameplate even when other
|
||||||
|
/// plugins modifying the name are present. Specifically, this builder allows setting text and text-wrapping
|
||||||
|
/// payloads (e.g. for setting text color) separately.
|
||||||
|
/// </summary>
|
||||||
|
NamePlateSimpleParts NameParts { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the title for this nameplate.
|
||||||
|
/// </summary>
|
||||||
|
SeString Title { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a builder which can be used to help cooperatively build a new title for this nameplate even when other
|
||||||
|
/// plugins modifying the title are present. Specifically, this builder allows setting text, text-wrapping
|
||||||
|
/// payloads (e.g. for setting text color), and opening and closing quote sequences separately.
|
||||||
|
/// </summary>
|
||||||
|
NamePlateQuotedParts TitleParts { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the free company tag for this nameplate.
|
||||||
|
/// </summary>
|
||||||
|
SeString FreeCompanyTag { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a builder which can be used to help cooperatively build a new FC tag for this nameplate even when other
|
||||||
|
/// plugins modifying the FC tag are present. Specifically, this builder allows setting text, text-wrapping
|
||||||
|
/// payloads (e.g. for setting text color), and opening and closing quote sequences separately.
|
||||||
|
/// </summary>
|
||||||
|
NamePlateQuotedParts FreeCompanyTagParts { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the status prefix for this nameplate. This prefix is used by the game to add BitmapFontIcon-based
|
||||||
|
/// online status icons to player nameplates.
|
||||||
|
/// </summary>
|
||||||
|
SeString StatusPrefix { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the target suffix for this nameplate. This suffix is used by the game to add the squared-letter
|
||||||
|
/// target tags to the end of combat target nameplates.
|
||||||
|
/// </summary>
|
||||||
|
SeString TargetSuffix { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the level prefix for this nameplate. This "Lv60" style prefix is added to enemy and friendly battle
|
||||||
|
/// NPC nameplates to indicate the NPC level.
|
||||||
|
/// </summary>
|
||||||
|
SeString LevelPrefix { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the contents of the name field for this nameplate. This differs from simply setting the field
|
||||||
|
/// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
|
||||||
|
/// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
|
||||||
|
/// </summary>
|
||||||
|
void RemoveName();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the contents of the title field for this nameplate. This differs from simply setting the field
|
||||||
|
/// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
|
||||||
|
/// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
|
||||||
|
/// </summary>
|
||||||
|
void RemoveTitle();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the contents of the FC tag field for this nameplate. This differs from simply setting the field
|
||||||
|
/// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
|
||||||
|
/// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
|
||||||
|
/// </summary>
|
||||||
|
void RemoveFreeCompanyTag();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the contents of the status prefix field for this nameplate. This differs from simply setting the field
|
||||||
|
/// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
|
||||||
|
/// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
|
||||||
|
/// </summary>
|
||||||
|
void RemoveStatusPrefix();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the contents of the target suffix field for this nameplate. This differs from simply setting the field
|
||||||
|
/// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
|
||||||
|
/// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
|
||||||
|
/// </summary>
|
||||||
|
void RemoveTargetSuffix();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the contents of the level prefix field for this nameplate. This differs from simply setting the field
|
||||||
|
/// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
|
||||||
|
/// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
|
||||||
|
/// </summary>
|
||||||
|
void RemoveLevelPrefix();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a pointer to the string array value in the provided field.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field to read from.</param>
|
||||||
|
/// <returns>A pointer to a sequence of non-null bytes.</returns>
|
||||||
|
unsafe byte* GetFieldAsPointer(NamePlateStringField field);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a byte span containing the string array value in the provided field.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field to read from.</param>
|
||||||
|
/// <returns>A ReadOnlySpan containing a sequence of non-null bytes.</returns>
|
||||||
|
ReadOnlySpan<byte> GetFieldAsSpan(NamePlateStringField field);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a UTF8 string copy of the string array value in the provided field.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field to read from.</param>
|
||||||
|
/// <returns>A copy of the string array value as a string.</returns>
|
||||||
|
string GetFieldAsString(NamePlateStringField field);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a parsed SeString copy of the string array value in the provided field.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field to read from.</param>
|
||||||
|
/// <returns>A copy of the string array value as a parsed SeString.</returns>
|
||||||
|
SeString GetFieldAsSeString(NamePlateStringField field);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the string array value for the provided field.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field to write to.</param>
|
||||||
|
/// <param name="value">The string to write.</param>
|
||||||
|
void SetField(NamePlateStringField field, string value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the string array value for the provided field.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field to write to.</param>
|
||||||
|
/// <param name="value">The SeString to write.</param>
|
||||||
|
void SetField(NamePlateStringField field, SeString value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the string array value for the provided field. The provided byte sequence must be null-terminated.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field to write to.</param>
|
||||||
|
/// <param name="value">The ReadOnlySpan of bytes to write.</param>
|
||||||
|
void SetField(NamePlateStringField field, ReadOnlySpan<byte> value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the string array value for the provided field. The provided byte sequence must be null-terminated.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field to write to.</param>
|
||||||
|
/// <param name="value">The pointer to a null-terminated sequence of bytes to write.</param>
|
||||||
|
unsafe void SetField(NamePlateStringField field, byte* value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the string array value for the provided field to a fixed pointer to an empty string in unmanaged memory.
|
||||||
|
/// Other methods may notice this fixed pointer and refuse to overwrite it, preserving the emptiness of the field.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field to write to.</param>
|
||||||
|
void RemoveField(NamePlateStringField field);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A class representing a single nameplate. Provides mechanisms to look up the game object associated with the
|
||||||
|
/// nameplate and allows for modification of various backing fields in number and string array data, which in turn
|
||||||
|
/// affect aspects of the nameplate's appearance when drawn. Instances of this class are only valid for a single frame
|
||||||
|
/// and should not be kept across frames.
|
||||||
|
/// </summary>
|
||||||
|
internal unsafe class NamePlateUpdateHandler : INamePlateUpdateHandler
|
||||||
|
{
|
||||||
|
private readonly NamePlateUpdateContext context;
|
||||||
|
|
||||||
|
private ulong? gameObjectId;
|
||||||
|
private IGameObject? gameObject;
|
||||||
|
private NamePlateInfoView? infoView;
|
||||||
|
private NamePlatePartsContainer? partsContainer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="NamePlateUpdateHandler"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The current update context.</param>
|
||||||
|
/// <param name="arrayIndex">The index for this nameplate data in the backing number and string array data. This is
|
||||||
|
/// not the same as the rendered index, which can be retrieved from <see cref="NamePlateIndex"/>.</param>
|
||||||
|
internal NamePlateUpdateHandler(NamePlateUpdateContext context, int arrayIndex)
|
||||||
|
{
|
||||||
|
this.context = context;
|
||||||
|
this.ArrayIndex = arrayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public int ArrayIndex { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ulong GameObjectId => this.gameObjectId ??= this.NamePlateInfo->ObjectId;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IGameObject? GameObject => this.gameObject ??= this.context.ObjectTable.CreateObjectReference(
|
||||||
|
(nint)this.context.Ui3DModule->NamePlateObjectInfoPointers[
|
||||||
|
this.ArrayIndex].Value->GameObject);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IBattleChara? BattleChara => this.GameObject as IBattleChara;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public IPlayerCharacter? PlayerCharacter => this.GameObject as IPlayerCharacter;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public INamePlateInfoView InfoView => this.infoView ??= new NamePlateInfoView(this.NamePlateInfo);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public nint NamePlateInfoAddress => (nint)this.NamePlateInfo;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public nint NamePlateObjectAddress => (nint)this.NamePlateObject;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public NamePlateKind NamePlateKind => (NamePlateKind)this.ObjectData->NamePlateKind;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public int UpdateFlags
|
||||||
|
{
|
||||||
|
get => this.ObjectData->UpdateFlags;
|
||||||
|
private set => this.ObjectData->UpdateFlags = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public uint TextColor
|
||||||
|
{
|
||||||
|
get => this.ObjectData->NameTextColor;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value != this.TextColor) this.UpdateFlags |= 2;
|
||||||
|
this.ObjectData->NameTextColor = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public uint EdgeColor
|
||||||
|
{
|
||||||
|
get => this.ObjectData->NameEdgeColor;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value != this.EdgeColor) this.UpdateFlags |= 2;
|
||||||
|
this.ObjectData->NameEdgeColor = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public int MarkerIconId
|
||||||
|
{
|
||||||
|
get => this.ObjectData->MarkerIconId;
|
||||||
|
set => this.ObjectData->MarkerIconId = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public int NameIconId
|
||||||
|
{
|
||||||
|
get => this.ObjectData->NameIconId;
|
||||||
|
set => this.ObjectData->NameIconId = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public int NamePlateIndex => this.ObjectData->NamePlateObjectIndex;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public int DrawFlags
|
||||||
|
{
|
||||||
|
get => this.ObjectData->DrawFlags;
|
||||||
|
private set => this.ObjectData->DrawFlags = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public int VisibilityFlags
|
||||||
|
{
|
||||||
|
get => ObjectData->VisibilityFlags;
|
||||||
|
set => ObjectData->VisibilityFlags = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsUpdating => (this.UpdateFlags & 1) != 0;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool IsPrefixTitle
|
||||||
|
{
|
||||||
|
get => (this.DrawFlags & 1) != 0;
|
||||||
|
set => this.DrawFlags = value ? this.DrawFlags | 1 : this.DrawFlags & ~1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool DisplayTitle
|
||||||
|
{
|
||||||
|
get => (this.DrawFlags & 0x80) == 0;
|
||||||
|
set => this.DrawFlags = value ? this.DrawFlags & ~0x80 : this.DrawFlags | 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public SeString Name
|
||||||
|
{
|
||||||
|
get => this.GetFieldAsSeString(NamePlateStringField.Name);
|
||||||
|
set => this.WeakSetField(NamePlateStringField.Name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public NamePlateSimpleParts NameParts => this.PartsContainer.Name;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public SeString Title
|
||||||
|
{
|
||||||
|
get => this.GetFieldAsSeString(NamePlateStringField.Title);
|
||||||
|
set => this.WeakSetField(NamePlateStringField.Title, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public NamePlateQuotedParts TitleParts => this.PartsContainer.Title;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public SeString FreeCompanyTag
|
||||||
|
{
|
||||||
|
get => this.GetFieldAsSeString(NamePlateStringField.FreeCompanyTag);
|
||||||
|
set => this.WeakSetField(NamePlateStringField.FreeCompanyTag, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public NamePlateQuotedParts FreeCompanyTagParts => this.PartsContainer.FreeCompanyTag;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public SeString StatusPrefix
|
||||||
|
{
|
||||||
|
get => this.GetFieldAsSeString(NamePlateStringField.StatusPrefix);
|
||||||
|
set => this.WeakSetField(NamePlateStringField.StatusPrefix, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public SeString TargetSuffix
|
||||||
|
{
|
||||||
|
get => this.GetFieldAsSeString(NamePlateStringField.TargetSuffix);
|
||||||
|
set => this.WeakSetField(NamePlateStringField.TargetSuffix, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public SeString LevelPrefix
|
||||||
|
{
|
||||||
|
get => this.GetFieldAsSeString(NamePlateStringField.LevelPrefix);
|
||||||
|
set => this.WeakSetField(NamePlateStringField.LevelPrefix, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or (lazily) creates a part builder container for this nameplate.
|
||||||
|
/// </summary>
|
||||||
|
internal NamePlatePartsContainer PartsContainer =>
|
||||||
|
this.partsContainer ??= new NamePlatePartsContainer(this.context);
|
||||||
|
|
||||||
|
private RaptureAtkModule.NamePlateInfo* NamePlateInfo =>
|
||||||
|
this.context.RaptureAtkModule->NamePlateInfoEntries.GetPointer(this.NamePlateIndex);
|
||||||
|
|
||||||
|
private AddonNamePlate.NamePlateObject* NamePlateObject =>
|
||||||
|
&this.context.Addon->NamePlateObjectArray[this.NamePlateIndex];
|
||||||
|
|
||||||
|
private AddonNamePlate.NamePlateIntArrayData.NamePlateObjectIntArrayData* ObjectData =>
|
||||||
|
this.context.NumberStruct->ObjectData.GetPointer(this.ArrayIndex);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void RemoveName() => this.RemoveField(NamePlateStringField.Name);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void RemoveTitle() => this.RemoveField(NamePlateStringField.Title);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void RemoveFreeCompanyTag() => this.RemoveField(NamePlateStringField.FreeCompanyTag);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void RemoveStatusPrefix() => this.RemoveField(NamePlateStringField.StatusPrefix);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void RemoveTargetSuffix() => this.RemoveField(NamePlateStringField.TargetSuffix);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void RemoveLevelPrefix() => this.RemoveField(NamePlateStringField.LevelPrefix);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public byte* GetFieldAsPointer(NamePlateStringField field)
|
||||||
|
{
|
||||||
|
return this.context.StringData->StringArray[this.ArrayIndex + (int)field];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public ReadOnlySpan<byte> GetFieldAsSpan(NamePlateStringField field)
|
||||||
|
{
|
||||||
|
return MemoryMarshal.CreateReadOnlySpanFromNullTerminated(this.GetFieldAsPointer(field));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public string GetFieldAsString(NamePlateStringField field)
|
||||||
|
{
|
||||||
|
return Encoding.UTF8.GetString(this.GetFieldAsSpan(field));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public SeString GetFieldAsSeString(NamePlateStringField field)
|
||||||
|
{
|
||||||
|
return SeString.Parse(this.GetFieldAsSpan(field));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void SetField(NamePlateStringField field, string value)
|
||||||
|
{
|
||||||
|
this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void SetField(NamePlateStringField field, SeString value)
|
||||||
|
{
|
||||||
|
this.context.StringData->SetValue(
|
||||||
|
this.ArrayIndex + (int)field,
|
||||||
|
value.EncodeWithNullTerminator(),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void SetField(NamePlateStringField field, ReadOnlySpan<byte> value)
|
||||||
|
{
|
||||||
|
this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void SetField(NamePlateStringField field, byte* value)
|
||||||
|
{
|
||||||
|
this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void RemoveField(NamePlateStringField field)
|
||||||
|
{
|
||||||
|
this.context.StringData->SetValue(
|
||||||
|
this.ArrayIndex + (int)field,
|
||||||
|
(byte*)NamePlateGui.EmptyStringPointer,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the state of this handler for re-use in a new update.
|
||||||
|
/// </summary>
|
||||||
|
internal void ResetState()
|
||||||
|
{
|
||||||
|
this.gameObjectId = null;
|
||||||
|
this.gameObject = null;
|
||||||
|
this.infoView = null;
|
||||||
|
this.partsContainer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the string array value for the provided field, unless it was already set to the special empty string
|
||||||
|
/// pointer used by the Remove methods.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="field">The field to write to.</param>
|
||||||
|
/// <param name="value">The SeString to write.</param>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private void WeakSetField(NamePlateStringField field, SeString value)
|
||||||
|
{
|
||||||
|
if ((nint)this.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer)
|
||||||
|
return;
|
||||||
|
this.context.StringData->SetValue(
|
||||||
|
this.ArrayIndex + (int)field,
|
||||||
|
value.EncodeWithNullTerminator(),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
}
|
||||||
295
Dalamud/Hooking/Internal/ObjectVTableHook.cs
Normal file
295
Dalamud/Hooking/Internal/ObjectVTableHook.cs
Normal file
|
|
@ -0,0 +1,295 @@
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
using Dalamud.Utility;
|
||||||
|
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Dalamud.Hooking.Internal;
|
||||||
|
|
||||||
|
/// <summary>Manages a hook that works by replacing the vtable of target object.</summary>
|
||||||
|
internal unsafe class ObjectVTableHook : IDisposable
|
||||||
|
{
|
||||||
|
private readonly nint** ppVtbl;
|
||||||
|
private readonly int numMethods;
|
||||||
|
|
||||||
|
private readonly nint* pVtblOriginal;
|
||||||
|
private readonly nint[] vtblOverriden;
|
||||||
|
|
||||||
|
/// <summary>Extra data for overriden vtable entries, primarily for keeping references to delegates that are used
|
||||||
|
/// with <see cref="Marshal.GetFunctionPointerForDelegate"/>.</summary>
|
||||||
|
private readonly object?[] vtblOverridenTag;
|
||||||
|
|
||||||
|
private bool released;
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance of the <see cref="ObjectVTableHook"/> class.</summary>
|
||||||
|
/// <param name="ppVtbl">Address to vtable. Usually the address of the object itself.</param>
|
||||||
|
/// <param name="numMethods">Number of methods in this vtable.</param>
|
||||||
|
public ObjectVTableHook(nint ppVtbl, int numMethods)
|
||||||
|
{
|
||||||
|
this.ppVtbl = (nint**)ppVtbl;
|
||||||
|
this.numMethods = numMethods;
|
||||||
|
this.vtblOverridenTag = new object?[numMethods];
|
||||||
|
this.pVtblOriginal = *this.ppVtbl;
|
||||||
|
this.vtblOverriden = GC.AllocateArray<nint>(numMethods, true);
|
||||||
|
this.OriginalVTableSpan.CopyTo(this.vtblOverriden);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance of the <see cref="ObjectVTableHook"/> class.</summary>
|
||||||
|
/// <param name="ppVtbl">Address to vtable. Usually the address of the object itself.</param>
|
||||||
|
/// <param name="numMethods">Number of methods in this vtable.</param>
|
||||||
|
public ObjectVTableHook(void* ppVtbl, int numMethods)
|
||||||
|
: this((nint)ppVtbl, numMethods)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Finalizes an instance of the <see cref="ObjectVTableHook"/> class.</summary>
|
||||||
|
~ObjectVTableHook() => this.ReleaseUnmanagedResources();
|
||||||
|
|
||||||
|
/// <summary>Gets the span view of original vtable.</summary>
|
||||||
|
public ReadOnlySpan<nint> OriginalVTableSpan => new(this.pVtblOriginal, this.numMethods);
|
||||||
|
|
||||||
|
/// <summary>Gets the span view of overriden vtable.</summary>
|
||||||
|
public ReadOnlySpan<nint> OverridenVTableSpan => this.vtblOverriden.AsSpan();
|
||||||
|
|
||||||
|
/// <summary>Gets the address of the pointer to the vtable.</summary>
|
||||||
|
public nint Address => (nint)this.ppVtbl;
|
||||||
|
|
||||||
|
/// <summary>Gets the address of the original vtable.</summary>
|
||||||
|
public nint OriginalVTableAddress => (nint)this.pVtblOriginal;
|
||||||
|
|
||||||
|
/// <summary>Gets the address of the overriden vtable.</summary>
|
||||||
|
public nint OverridenVTableAddress => (nint)Unsafe.AsPointer(ref this.vtblOverriden[0]);
|
||||||
|
|
||||||
|
/// <summary>Disables the hook.</summary>
|
||||||
|
public void Disable()
|
||||||
|
{
|
||||||
|
// already disabled
|
||||||
|
if (*this.ppVtbl == this.pVtblOriginal)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (*this.ppVtbl != Unsafe.AsPointer(ref this.vtblOverriden[0]))
|
||||||
|
{
|
||||||
|
Log.Warning(
|
||||||
|
"[{who}]: the object was hooked by something else; disabling may result in a crash.",
|
||||||
|
this.GetType().Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
*this.ppVtbl = this.pVtblOriginal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
this.ReleaseUnmanagedResources();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Enables the hook.</summary>
|
||||||
|
public void Enable()
|
||||||
|
{
|
||||||
|
// already enabled
|
||||||
|
if (*this.ppVtbl == Unsafe.AsPointer(ref this.vtblOverriden[0]))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (*this.ppVtbl != this.pVtblOriginal)
|
||||||
|
{
|
||||||
|
Log.Warning(
|
||||||
|
"[{who}]: the object was hooked by something else; enabling may result in a crash.",
|
||||||
|
this.GetType().Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
*this.ppVtbl = (nint*)Unsafe.AsPointer(ref this.vtblOverriden[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets the original method address of the given method index.</summary>
|
||||||
|
/// <param name="methodIndex">Index of the method.</param>
|
||||||
|
/// <returns>Address of the original method.</returns>
|
||||||
|
public nint GetOriginalMethodAddress(int methodIndex)
|
||||||
|
{
|
||||||
|
this.EnsureMethodIndex(methodIndex);
|
||||||
|
return this.pVtblOriginal[methodIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets the original method of the given method index, as a delegate of given type.</summary>
|
||||||
|
/// <param name="methodIndex">Index of the method.</param>
|
||||||
|
/// <typeparam name="T">Type of delegate.</typeparam>
|
||||||
|
/// <returns>Delegate to the original method.</returns>
|
||||||
|
public T GetOriginalMethodDelegate<T>(int methodIndex)
|
||||||
|
where T : Delegate
|
||||||
|
{
|
||||||
|
this.EnsureMethodIndex(methodIndex);
|
||||||
|
return Marshal.GetDelegateForFunctionPointer<T>(this.pVtblOriginal[methodIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Resets a method to the original function.</summary>
|
||||||
|
/// <param name="methodIndex">Index of the method.</param>
|
||||||
|
public void ResetVtableEntry(int methodIndex)
|
||||||
|
{
|
||||||
|
this.EnsureMethodIndex(methodIndex);
|
||||||
|
this.vtblOverriden[methodIndex] = this.pVtblOriginal[methodIndex];
|
||||||
|
this.vtblOverridenTag[methodIndex] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Sets a method in vtable to the given address of function.</summary>
|
||||||
|
/// <param name="methodIndex">Index of the method.</param>
|
||||||
|
/// <param name="pfn">Address of the detour function.</param>
|
||||||
|
/// <param name="refkeep">Additional reference to keep in memory.</param>
|
||||||
|
public void SetVtableEntry(int methodIndex, nint pfn, object? refkeep)
|
||||||
|
{
|
||||||
|
this.EnsureMethodIndex(methodIndex);
|
||||||
|
this.vtblOverriden[methodIndex] = pfn;
|
||||||
|
this.vtblOverridenTag[methodIndex] = refkeep;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Sets a method in vtable to the given delegate.</summary>
|
||||||
|
/// <param name="methodIndex">Index of the method.</param>
|
||||||
|
/// <param name="detourDelegate">Detour delegate.</param>
|
||||||
|
/// <typeparam name="T">Type of delegate.</typeparam>
|
||||||
|
public void SetVtableEntry<T>(int methodIndex, T detourDelegate)
|
||||||
|
where T : Delegate =>
|
||||||
|
this.SetVtableEntry(methodIndex, Marshal.GetFunctionPointerForDelegate(detourDelegate), detourDelegate);
|
||||||
|
|
||||||
|
/// <summary>Sets a method in vtable to the given delegate.</summary>
|
||||||
|
/// <param name="methodIndex">Index of the method.</param>
|
||||||
|
/// <param name="detourDelegate">Detour delegate.</param>
|
||||||
|
/// <param name="originalMethodDelegate">Original method delegate.</param>
|
||||||
|
/// <typeparam name="T">Type of delegate.</typeparam>
|
||||||
|
public void SetVtableEntry<T>(int methodIndex, T detourDelegate, out T originalMethodDelegate)
|
||||||
|
where T : Delegate
|
||||||
|
{
|
||||||
|
originalMethodDelegate = this.GetOriginalMethodDelegate<T>(methodIndex);
|
||||||
|
this.SetVtableEntry(methodIndex, Marshal.GetFunctionPointerForDelegate(detourDelegate), detourDelegate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Creates a new instance of <see cref="Hook{T}"/> that manages one entry in the vtable hook.</summary>
|
||||||
|
/// <param name="methodIndex">Index of the method.</param>
|
||||||
|
/// <param name="detourDelegate">Detour delegate.</param>
|
||||||
|
/// <typeparam name="T">Type of delegate.</typeparam>
|
||||||
|
/// <returns>A new instance of <see cref="Hook{T}"/>.</returns>
|
||||||
|
/// <remarks>Even if a single hook is enabled, without <see cref="Enable"/>, the hook will remain disabled.
|
||||||
|
/// </remarks>
|
||||||
|
public Hook<T> CreateHook<T>(int methodIndex, T detourDelegate) where T : Delegate =>
|
||||||
|
new SingleHook<T>(this, methodIndex, detourDelegate);
|
||||||
|
|
||||||
|
private void EnsureMethodIndex(int methodIndex)
|
||||||
|
{
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegative(methodIndex);
|
||||||
|
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(methodIndex, this.numMethods);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReleaseUnmanagedResources()
|
||||||
|
{
|
||||||
|
if (!this.released)
|
||||||
|
{
|
||||||
|
this.Disable();
|
||||||
|
this.released = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SingleHook<T>(ObjectVTableHook hook, int methodIndex, T detourDelegate)
|
||||||
|
: Hook<T>((nint)hook.ppVtbl)
|
||||||
|
where T : Delegate
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override T Original { get; } = hook.GetOriginalMethodDelegate<T>(methodIndex);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool IsEnabled =>
|
||||||
|
hook.OriginalVTableSpan[methodIndex] != hook.OverridenVTableSpan[methodIndex];
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override string BackendName => nameof(ObjectVTableHook);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Enable() => hook.SetVtableEntry(methodIndex, detourDelegate);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Disable() => hook.ResetVtableEntry(methodIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Typed version of <see cref="ObjectVTableHook"/>.</summary>
|
||||||
|
/// <typeparam name="TVTable">VTable struct.</typeparam>
|
||||||
|
internal unsafe class ObjectVTableHook<TVTable> : ObjectVTableHook
|
||||||
|
where TVTable : unmanaged
|
||||||
|
{
|
||||||
|
private static readonly string[] Fields =
|
||||||
|
typeof(TVTable).GetFields(BindingFlags.Instance | BindingFlags.Public).Select(x => x.Name).ToArray();
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance of the <see cref="ObjectVTableHook{TVTable}"/> class.</summary>
|
||||||
|
/// <param name="ppVtbl">Address to vtable. Usually the address of the object itself.</param>
|
||||||
|
public ObjectVTableHook(void* ppVtbl)
|
||||||
|
: base(ppVtbl, Fields.Length)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets the original vtable.</summary>
|
||||||
|
public ref readonly TVTable OriginalVTable => ref MemoryMarshal.Cast<nint, TVTable>(this.OriginalVTableSpan)[0];
|
||||||
|
|
||||||
|
/// <summary>Gets the overriden vtable.</summary>
|
||||||
|
public ref readonly TVTable OverridenVTable => ref MemoryMarshal.Cast<nint, TVTable>(this.OverridenVTableSpan)[0];
|
||||||
|
|
||||||
|
/// <summary>Gets the index of the method by method name.</summary>
|
||||||
|
/// <param name="methodName">Name of the method.</param>
|
||||||
|
/// <returns>Index of the method.</returns>
|
||||||
|
public int GetMethodIndex(string methodName) => Fields.IndexOf(methodName);
|
||||||
|
|
||||||
|
/// <summary>Gets the original method address of the given method index.</summary>
|
||||||
|
/// <param name="methodName">Name of the method.</param>
|
||||||
|
/// <returns>Address of the original method.</returns>
|
||||||
|
public nint GetOriginalMethodAddress(string methodName) =>
|
||||||
|
this.GetOriginalMethodAddress(this.GetMethodIndex(methodName));
|
||||||
|
|
||||||
|
/// <summary>Gets the original method of the given method index, as a delegate of given type.</summary>
|
||||||
|
/// <param name="methodName">Name of the method.</param>
|
||||||
|
/// <typeparam name="T">Type of delegate.</typeparam>
|
||||||
|
/// <returns>Delegate to the original method.</returns>
|
||||||
|
public T GetOriginalMethodDelegate<T>(string methodName)
|
||||||
|
where T : Delegate
|
||||||
|
=> this.GetOriginalMethodDelegate<T>(this.GetMethodIndex(methodName));
|
||||||
|
|
||||||
|
/// <summary>Resets a method to the original function.</summary>
|
||||||
|
/// <param name="methodName">Name of the method.</param>
|
||||||
|
public void ResetVtableEntry(string methodName)
|
||||||
|
=> this.ResetVtableEntry(this.GetMethodIndex(methodName));
|
||||||
|
|
||||||
|
/// <summary>Sets a method in vtable to the given address of function.</summary>
|
||||||
|
/// <param name="methodName">Name of the method.</param>
|
||||||
|
/// <param name="pfn">Address of the detour function.</param>
|
||||||
|
/// <param name="refkeep">Additional reference to keep in memory.</param>
|
||||||
|
public void SetVtableEntry(string methodName, nint pfn, object? refkeep)
|
||||||
|
=> this.SetVtableEntry(this.GetMethodIndex(methodName), pfn, refkeep);
|
||||||
|
|
||||||
|
/// <summary>Sets a method in vtable to the given delegate.</summary>
|
||||||
|
/// <param name="methodName">Name of the method.</param>
|
||||||
|
/// <param name="detourDelegate">Detour delegate.</param>
|
||||||
|
/// <typeparam name="T">Type of delegate.</typeparam>
|
||||||
|
public void SetVtableEntry<T>(string methodName, T detourDelegate)
|
||||||
|
where T : Delegate =>
|
||||||
|
this.SetVtableEntry(
|
||||||
|
this.GetMethodIndex(methodName),
|
||||||
|
Marshal.GetFunctionPointerForDelegate(detourDelegate),
|
||||||
|
detourDelegate);
|
||||||
|
|
||||||
|
/// <summary>Sets a method in vtable to the given delegate.</summary>
|
||||||
|
/// <param name="methodName">Name of the method.</param>
|
||||||
|
/// <param name="detourDelegate">Detour delegate.</param>
|
||||||
|
/// <param name="originalMethodDelegate">Original method delegate.</param>
|
||||||
|
/// <typeparam name="T">Type of delegate.</typeparam>
|
||||||
|
public void SetVtableEntry<T>(string methodName, T detourDelegate, out T originalMethodDelegate)
|
||||||
|
where T : Delegate
|
||||||
|
=> this.SetVtableEntry(this.GetMethodIndex(methodName), detourDelegate, out originalMethodDelegate);
|
||||||
|
|
||||||
|
/// <summary>Creates a new instance of <see cref="Hook{T}"/> that manages one entry in the vtable hook.</summary>
|
||||||
|
/// <param name="methodName">Name of the method.</param>
|
||||||
|
/// <param name="detourDelegate">Detour delegate.</param>
|
||||||
|
/// <typeparam name="T">Type of delegate.</typeparam>
|
||||||
|
/// <returns>A new instance of <see cref="Hook{T}"/>.</returns>
|
||||||
|
/// <remarks>Even if a single hook is enabled, without <see cref="ObjectVTableHook.Enable"/>, the hook will remain
|
||||||
|
/// disabled.</remarks>
|
||||||
|
public Hook<T> CreateHook<T>(string methodName, T detourDelegate) where T : Delegate =>
|
||||||
|
this.CreateHook(this.GetMethodIndex(methodName), detourDelegate);
|
||||||
|
}
|
||||||
|
|
@ -59,7 +59,7 @@ public sealed class SingleFontChooserDialog : IDisposable
|
||||||
|
|
||||||
private readonly int counter;
|
private readonly int counter;
|
||||||
private readonly byte[] fontPreviewText = new byte[2048];
|
private readonly byte[] fontPreviewText = new byte[2048];
|
||||||
private readonly TaskCompletionSource<SingleFontSpec> tcs = new();
|
private readonly TaskCompletionSource<SingleFontSpec> tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
private readonly IFontAtlas atlas;
|
private readonly IFontAtlas atlas;
|
||||||
|
|
||||||
private string popupImGuiName;
|
private string popupImGuiName;
|
||||||
|
|
|
||||||
|
|
@ -329,7 +329,7 @@ internal class DalamudCommands : IServiceType
|
||||||
|
|
||||||
chatGui.Print(new SeStringBuilder()
|
chatGui.Print(new SeStringBuilder()
|
||||||
.AddItalics("Dalamud:")
|
.AddItalics("Dalamud:")
|
||||||
.AddText($" D{Util.AssemblyVersion}({Util.GetGitHash()}")
|
.AddText($" {Util.GetScmVersion()}")
|
||||||
.Build());
|
.Build());
|
||||||
|
|
||||||
chatGui.Print(new SeStringBuilder()
|
chatGui.Print(new SeStringBuilder()
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,29 @@ internal class DalamudInterface : IInternalDisposableService
|
||||||
|
|
||||||
this.creditsDarkeningAnimation.Point1 = Vector2.Zero;
|
this.creditsDarkeningAnimation.Point1 = Vector2.Zero;
|
||||||
this.creditsDarkeningAnimation.Point2 = new Vector2(CreditsDarkeningMaxAlpha);
|
this.creditsDarkeningAnimation.Point2 = new Vector2(CreditsDarkeningMaxAlpha);
|
||||||
|
|
||||||
|
// This is temporary, until we know the repercussions of vtable hooking mode
|
||||||
|
consoleManager.AddCommand(
|
||||||
|
"dalamud.interface.swapchain_mode",
|
||||||
|
"Set swapchain hooking mode",
|
||||||
|
(string mode) =>
|
||||||
|
{
|
||||||
|
switch (mode)
|
||||||
|
{
|
||||||
|
case "vtable":
|
||||||
|
this.configuration.SwapChainHookMode = SwapChainHelper.HookMode.VTable;
|
||||||
|
break;
|
||||||
|
case "bytecode":
|
||||||
|
this.configuration.SwapChainHookMode = SwapChainHelper.HookMode.ByteCode;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.Error("Unknown swapchain mode: {Mode}", mode);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.configuration.QueueSave();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private delegate nint CrashDebugDelegate(nint self);
|
private delegate nint CrashDebugDelegate(nint self);
|
||||||
|
|
@ -818,10 +841,9 @@ internal class DalamudInterface : IInternalDisposableService
|
||||||
{
|
{
|
||||||
this.OpenBranchSwitcher();
|
this.OpenBranchSwitcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.MenuItem(Util.AssemblyVersion, false);
|
|
||||||
ImGui.MenuItem(this.dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false);
|
ImGui.MenuItem(this.dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false);
|
||||||
ImGui.MenuItem($"D: {Util.GetGitHash()}[{Util.GetGitCommitCount()}] CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false);
|
ImGui.MenuItem($"D: {Util.GetScmVersion()} CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false);
|
||||||
ImGui.MenuItem($"CLR: {Environment.Version}", false);
|
ImGui.MenuItem($"CLR: {Environment.Version}", false);
|
||||||
|
|
||||||
ImGui.EndMenu();
|
ImGui.EndMenu();
|
||||||
|
|
@ -1020,7 +1042,7 @@ internal class DalamudInterface : IInternalDisposableService
|
||||||
{
|
{
|
||||||
ImGui.PushFont(InterfaceManager.MonoFont);
|
ImGui.PushFont(InterfaceManager.MonoFont);
|
||||||
|
|
||||||
ImGui.BeginMenu($"{Util.GetGitHash()}({Util.GetGitCommitCount()})", false);
|
ImGui.BeginMenu(Util.GetScmVersion(), false);
|
||||||
ImGui.BeginMenu(this.FrameCount.ToString("000000"), false);
|
ImGui.BeginMenu(this.FrameCount.ToString("000000"), false);
|
||||||
ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false);
|
ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false);
|
||||||
ImGui.BeginMenu($"W:{Util.FormatBytes(GC.GetTotalMemory(false))}", false);
|
ImGui.BeginMenu($"W:{Util.FormatBytes(GC.GetTotalMemory(false))}", false);
|
||||||
|
|
|
||||||
141
Dalamud/Interface/Internal/InterfaceManager.AsHook.cs
Normal file
141
Dalamud/Interface/Internal/InterfaceManager.AsHook.cs
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
using Dalamud.Utility;
|
||||||
|
|
||||||
|
using TerraFX.Interop.DirectX;
|
||||||
|
using TerraFX.Interop.Windows;
|
||||||
|
|
||||||
|
namespace Dalamud.Interface.Internal;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This class manages interaction with the ImGui interface.
|
||||||
|
/// </summary>
|
||||||
|
internal unsafe partial class InterfaceManager
|
||||||
|
{
|
||||||
|
// NOTE: Do not use HRESULT as return value type. It appears that .NET marshaller thinks HRESULT needs to be still
|
||||||
|
// treated as a type that does not fit into RAX.
|
||||||
|
|
||||||
|
/// <summary>Delegate for <c>DXGISwapChain::on_present(UINT flags, const DXGI_PRESENT_PARAMETERS *params)</c> in
|
||||||
|
/// <c>dxgi_swapchain.cpp</c>.</summary>
|
||||||
|
/// <param name="swapChain">Pointer to an instance of <c>DXGISwapChain</c>, which happens to be an
|
||||||
|
/// <see cref="IDXGISwapChain"/>.</param>
|
||||||
|
/// <param name="flags">An integer value that contains swap-chain presentation options. These options are defined by
|
||||||
|
/// the <c>DXGI_PRESENT</c> constants.</param>
|
||||||
|
/// <param name="presentParams">Optional; DXGI present parameters.</param>
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||||
|
private delegate void ReShadeDxgiSwapChainPresentDelegate(
|
||||||
|
ReShadeDxgiSwapChain* swapChain,
|
||||||
|
uint flags,
|
||||||
|
DXGI_PRESENT_PARAMETERS* presentParams);
|
||||||
|
|
||||||
|
/// <summary>Delegate for <see cref="IDXGISwapChain.Present"/>.
|
||||||
|
/// <a href="https://learn.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-idxgiswapchain-present">Microsoft
|
||||||
|
/// Learn</a>.</summary>
|
||||||
|
/// <param name="swapChain">Pointer to an instance of <see cref="IDXGISwapChain"/>.</param>
|
||||||
|
/// <param name="syncInterval">An integer that specifies how to synchronize presentation of a frame with the
|
||||||
|
/// vertical blank.</param>
|
||||||
|
/// <param name="flags">An integer value that contains swap-chain presentation options. These options are defined by
|
||||||
|
/// the <c>DXGI_PRESENT</c> constants.</param>
|
||||||
|
/// <returns>A <see cref="HRESULT"/> representing the result of the operation.</returns>
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||||
|
private delegate int DxgiSwapChainPresentDelegate(IDXGISwapChain* swapChain, uint syncInterval, uint flags);
|
||||||
|
|
||||||
|
/// <summary>Detour function for <see cref="IDXGISwapChain.ResizeBuffers"/>.
|
||||||
|
/// <a href="https://learn.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-idxgiswapchain-resizebuffers">
|
||||||
|
/// Microsoft Learn</a>.</summary>
|
||||||
|
/// <param name="swapChain">Pointer to an instance of <see cref="IDXGISwapChain"/>.</param>
|
||||||
|
/// <param name="bufferCount">The number of buffers in the swap chain (including all back and front buffers).
|
||||||
|
/// This number can be different from the number of buffers with which you created the swap chain. This number
|
||||||
|
/// can't be greater than <see cref="DXGI.DXGI_MAX_SWAP_CHAIN_BUFFERS"/>. Set this number to zero to preserve the
|
||||||
|
/// existing number of buffers in the swap chain. You can't specify less than two buffers for the flip presentation
|
||||||
|
/// model.</param>
|
||||||
|
/// <param name="width">The new width of the back buffer. If you specify zero, DXGI will use the width of the client
|
||||||
|
/// area of the target window. You can't specify the width as zero if you called the
|
||||||
|
/// <see cref="IDXGIFactory2.CreateSwapChainForComposition"/> method to create the swap chain for a composition
|
||||||
|
/// surface.</param>
|
||||||
|
/// <param name="height">The new height of the back buffer. If you specify zero, DXGI will use the height of the
|
||||||
|
/// client area of the target window. You can't specify the height as zero if you called the
|
||||||
|
/// <see cref="IDXGIFactory2.CreateSwapChainForComposition"/> method to create the swap chain for a composition
|
||||||
|
/// surface.</param>
|
||||||
|
/// <param name="newFormat">A DXGI_FORMAT-typed value for the new format of the back buffer. Set this value to
|
||||||
|
/// <see cref="DXGI_FORMAT.DXGI_FORMAT_UNKNOWN"/> to preserve the existing format of the back buffer. The flip
|
||||||
|
/// presentation model supports a more restricted set of formats than the bit-block transfer (bitblt) model.</param>
|
||||||
|
/// <param name="swapChainFlags">A combination of <see cref="DXGI_SWAP_CHAIN_FLAG"/>-typed values that are combined
|
||||||
|
/// by using a bitwise OR operation. The resulting value specifies options for swap-chain behavior.</param>
|
||||||
|
/// <returns>A <see cref="HRESULT"/> representing the result of the operation.</returns>
|
||||||
|
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||||
|
private delegate int ResizeBuffersDelegate(
|
||||||
|
IDXGISwapChain* swapChain,
|
||||||
|
uint bufferCount,
|
||||||
|
uint width,
|
||||||
|
uint height,
|
||||||
|
DXGI_FORMAT newFormat,
|
||||||
|
uint swapChainFlags);
|
||||||
|
|
||||||
|
private void ReShadeDxgiSwapChainOnPresentDetour(
|
||||||
|
ReShadeDxgiSwapChain* swapChain,
|
||||||
|
uint flags,
|
||||||
|
DXGI_PRESENT_PARAMETERS* presentParams)
|
||||||
|
{
|
||||||
|
Debug.Assert(
|
||||||
|
this.reShadeDxgiSwapChainPresentHook is not null,
|
||||||
|
"this.reShadeDxgiSwapChainPresentHook is not null");
|
||||||
|
|
||||||
|
if (this.RenderDalamudCheckAndInitialize(swapChain->AsIDxgiSwapChain(), flags) is { } activeScene)
|
||||||
|
this.RenderDalamudDraw(activeScene);
|
||||||
|
|
||||||
|
this.reShadeDxgiSwapChainPresentHook!.Original(swapChain, flags, presentParams);
|
||||||
|
|
||||||
|
// Upstream call to system IDXGISwapChain::Present will be called by ReShade.
|
||||||
|
}
|
||||||
|
|
||||||
|
private int DxgiSwapChainPresentDetour(IDXGISwapChain* swapChain, uint syncInterval, uint flags)
|
||||||
|
{
|
||||||
|
Debug.Assert(this.dxgiSwapChainPresentHook is not null, "this.dxgiSwapChainPresentHook is not null");
|
||||||
|
|
||||||
|
if (this.RenderDalamudCheckAndInitialize(swapChain, flags) is { } activeScene)
|
||||||
|
this.RenderDalamudDraw(activeScene);
|
||||||
|
|
||||||
|
return this.dxgiSwapChainPresentHook!.Original(swapChain, syncInterval, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int AsHookDxgiSwapChainResizeBuffersDetour(
|
||||||
|
IDXGISwapChain* swapChain,
|
||||||
|
uint bufferCount,
|
||||||
|
uint width,
|
||||||
|
uint height,
|
||||||
|
DXGI_FORMAT newFormat,
|
||||||
|
uint swapChainFlags)
|
||||||
|
{
|
||||||
|
if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain))
|
||||||
|
return this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Log.Verbose(
|
||||||
|
$"Calling resizebuffers swap@{(nint)swapChain:X}{bufferCount} {width} {height} {newFormat} {swapChainFlags}");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
this.ResizeBuffers?.InvokeSafely();
|
||||||
|
|
||||||
|
this.backend?.OnPreResize();
|
||||||
|
|
||||||
|
var ret = this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
|
||||||
|
if (ret == DXGI.DXGI_ERROR_INVALID_CALL)
|
||||||
|
Log.Error("invalid call to resizeBuffers");
|
||||||
|
|
||||||
|
this.backend?.OnPostResize((int)width, (int)height);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Represents <c>DXGISwapChain</c> in ReShade.</summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct ReShadeDxgiSwapChain
|
||||||
|
{
|
||||||
|
// DXGISwapChain only implements IDXGISwapChain4. The only vtable should be that.
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public IDXGISwapChain* AsIDxgiSwapChain() => (IDXGISwapChain*)Unsafe.AsPointer(ref this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
using Dalamud.Interface.Internal.ReShadeHandling;
|
||||||
|
using Dalamud.Utility;
|
||||||
|
|
||||||
|
using TerraFX.Interop.DirectX;
|
||||||
|
using TerraFX.Interop.Windows;
|
||||||
|
|
||||||
|
namespace Dalamud.Interface.Internal;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This class manages interaction with the ImGui interface.
|
||||||
|
/// </summary>
|
||||||
|
internal unsafe partial class InterfaceManager
|
||||||
|
{
|
||||||
|
private void ReShadeAddonInterfaceOnDestroySwapChain(ref ReShadeAddonInterface.ApiObject swapChain)
|
||||||
|
{
|
||||||
|
var swapChainNative = swapChain.GetNative<IDXGISwapChain>();
|
||||||
|
if (this.backend?.IsAttachedToPresentationTarget((nint)swapChainNative) is not true)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.backend?.OnPreResize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReShadeAddonInterfaceOnInitSwapChain(ref ReShadeAddonInterface.ApiObject swapChain)
|
||||||
|
{
|
||||||
|
var swapChainNative = swapChain.GetNative<IDXGISwapChain>();
|
||||||
|
if (this.backend?.IsAttachedToPresentationTarget((nint)swapChainNative) is not true)
|
||||||
|
return;
|
||||||
|
|
||||||
|
DXGI_SWAP_CHAIN_DESC desc;
|
||||||
|
if (swapChainNative->GetDesc(&desc).FAILED)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.backend?.OnPostResize((int)desc.BufferDesc.Width, (int)desc.BufferDesc.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReShadeAddonInterfaceOnPresent(
|
||||||
|
ref ReShadeAddonInterface.ApiObject runtime,
|
||||||
|
ref ReShadeAddonInterface.ApiObject swapChain,
|
||||||
|
ReadOnlySpan<RECT> sourceRect,
|
||||||
|
ReadOnlySpan<RECT> destRect,
|
||||||
|
ReadOnlySpan<RECT> dirtyRects)
|
||||||
|
{
|
||||||
|
var swapChainNative = swapChain.GetNative<IDXGISwapChain>();
|
||||||
|
|
||||||
|
if (this.RenderDalamudCheckAndInitialize(swapChainNative, 0) is { } activebackend)
|
||||||
|
this.RenderDalamudDraw(activebackend);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReShadeAddonInterfaceOnReShadeOverlay(ref ReShadeAddonInterface.ApiObject runtime)
|
||||||
|
{
|
||||||
|
var swapChainNative = runtime.GetNative<IDXGISwapChain>();
|
||||||
|
|
||||||
|
if (this.RenderDalamudCheckAndInitialize(swapChainNative, 0) is { } activebackend)
|
||||||
|
this.RenderDalamudDraw(activebackend);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int AsReShadeAddonDxgiSwapChainResizeBuffersDetour(
|
||||||
|
IDXGISwapChain* swapChain,
|
||||||
|
uint bufferCount,
|
||||||
|
uint width,
|
||||||
|
uint height,
|
||||||
|
DXGI_FORMAT newFormat,
|
||||||
|
uint swapChainFlags)
|
||||||
|
{
|
||||||
|
// Hooked vtbl instead of registering ReShade event. This check is correct.
|
||||||
|
if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain))
|
||||||
|
return this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
|
||||||
|
|
||||||
|
this.ResizeBuffers?.InvokeSafely();
|
||||||
|
return this.dxgiSwapChainResizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,20 +3,26 @@ using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using CheapLoc;
|
||||||
|
|
||||||
using Dalamud.Configuration.Internal;
|
using Dalamud.Configuration.Internal;
|
||||||
using Dalamud.Game;
|
using Dalamud.Game;
|
||||||
using Dalamud.Game.ClientState.GamePad;
|
using Dalamud.Game.ClientState.GamePad;
|
||||||
using Dalamud.Game.ClientState.Keys;
|
using Dalamud.Game.ClientState.Keys;
|
||||||
using Dalamud.Hooking;
|
using Dalamud.Hooking;
|
||||||
|
using Dalamud.Hooking.Internal;
|
||||||
using Dalamud.Hooking.WndProcHook;
|
using Dalamud.Hooking.WndProcHook;
|
||||||
using Dalamud.Interface.ImGuiBackend;
|
using Dalamud.Interface.ImGuiBackend;
|
||||||
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
using Dalamud.Interface.ImGuiNotification.Internal;
|
using Dalamud.Interface.ImGuiNotification.Internal;
|
||||||
|
using Dalamud.Interface.Internal.DesignSystem;
|
||||||
using Dalamud.Interface.Internal.ManagedAsserts;
|
using Dalamud.Interface.Internal.ManagedAsserts;
|
||||||
|
using Dalamud.Interface.Internal.ReShadeHandling;
|
||||||
using Dalamud.Interface.ManagedFontAtlas;
|
using Dalamud.Interface.ManagedFontAtlas;
|
||||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||||
using Dalamud.Interface.Style;
|
using Dalamud.Interface.Style;
|
||||||
|
|
@ -28,7 +34,7 @@ using Dalamud.Utility.Timing;
|
||||||
|
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
|
|
||||||
using PInvoke;
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
using TerraFX.Interop.DirectX;
|
using TerraFX.Interop.DirectX;
|
||||||
using TerraFX.Interop.Windows;
|
using TerraFX.Interop.Windows;
|
||||||
|
|
@ -53,7 +59,7 @@ namespace Dalamud.Interface.Internal;
|
||||||
/// This class manages interaction with the ImGui interface.
|
/// This class manages interaction with the ImGui interface.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ServiceManager.EarlyLoadedService]
|
[ServiceManager.EarlyLoadedService]
|
||||||
internal class InterfaceManager : IInternalDisposableService
|
internal partial class InterfaceManager : IInternalDisposableService
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default font size, in points.
|
/// The default font size, in points.
|
||||||
|
|
@ -71,19 +77,30 @@ internal class InterfaceManager : IInternalDisposableService
|
||||||
private readonly ConcurrentBag<IDisposable> deferredDisposeDisposables = new();
|
private readonly ConcurrentBag<IDisposable> deferredDisposeDisposables = new();
|
||||||
|
|
||||||
[ServiceManager.ServiceDependency]
|
[ServiceManager.ServiceDependency]
|
||||||
private readonly WndProcHookManager wndProcHookManager = Service<WndProcHookManager>.Get();
|
private readonly DalamudConfiguration dalamudConfiguration = Service<DalamudConfiguration>.Get();
|
||||||
|
|
||||||
[ServiceManager.ServiceDependency]
|
[ServiceManager.ServiceDependency]
|
||||||
private readonly Framework framework = Service<Framework>.Get();
|
private readonly Framework framework = Service<Framework>.Get();
|
||||||
|
|
||||||
|
// ReShadeAddonInterface requires hooks to be alive to unregister itself.
|
||||||
|
[ServiceManager.ServiceDependency]
|
||||||
|
[UsedImplicitly]
|
||||||
|
private readonly HookManager hookManager = Service<HookManager>.Get();
|
||||||
|
|
||||||
|
[ServiceManager.ServiceDependency]
|
||||||
|
private readonly WndProcHookManager wndProcHookManager = Service<WndProcHookManager>.Get();
|
||||||
|
|
||||||
private readonly ConcurrentQueue<Action> runBeforeImGuiRender = new();
|
private readonly ConcurrentQueue<Action> runBeforeImGuiRender = new();
|
||||||
private readonly ConcurrentQueue<Action> runAfterImGuiRender = new();
|
private readonly ConcurrentQueue<Action> runAfterImGuiRender = new();
|
||||||
|
|
||||||
private IWin32Backend? backend;
|
private IWin32Backend? backend;
|
||||||
|
|
||||||
private Hook<SetCursorDelegate>? setCursorHook;
|
private Hook<SetCursorDelegate>? setCursorHook;
|
||||||
private Hook<PresentDelegate>? presentHook;
|
private Hook<ReShadeDxgiSwapChainPresentDelegate>? reShadeDxgiSwapChainPresentHook;
|
||||||
private Hook<ResizeBuffersDelegate>? resizeBuffersHook;
|
private Hook<DxgiSwapChainPresentDelegate>? dxgiSwapChainPresentHook;
|
||||||
|
private Hook<ResizeBuffersDelegate>? dxgiSwapChainResizeBuffersHook;
|
||||||
|
private ObjectVTableHook<IDXGISwapChain4.Vtbl<IDXGISwapChain4>>? dxgiSwapChainHook;
|
||||||
|
private ReShadeAddonInterface? reShadeAddonInterface;
|
||||||
|
|
||||||
private IFontAtlas? dalamudAtlas;
|
private IFontAtlas? dalamudAtlas;
|
||||||
private ILockedImFont? defaultFontResourceLock;
|
private ILockedImFont? defaultFontResourceLock;
|
||||||
|
|
@ -99,14 +116,7 @@ internal class InterfaceManager : IInternalDisposableService
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
||||||
private unsafe delegate HRESULT PresentDelegate(IDXGISwapChain* swapChain, uint syncInterval, uint presentFlags);
|
private delegate nint SetCursorDelegate(nint hCursor);
|
||||||
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
|
||||||
private unsafe delegate HRESULT ResizeBuffersDelegate(
|
|
||||||
IDXGISwapChain* swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags);
|
|
||||||
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
|
|
||||||
private delegate HCURSOR SetCursorDelegate(HCURSOR hCursor);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This event gets called each frame to facilitate ImGui drawing.
|
/// This event gets called each frame to facilitate ImGui drawing.
|
||||||
|
|
@ -205,7 +215,7 @@ internal class InterfaceManager : IInternalDisposableService
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsDispatchingEvents { get; set; } = true;
|
public bool IsDispatchingEvents { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>Gets a value indicating whether the main thread is executing <see cref="PresentDetour"/>.</summary>
|
/// <summary>Gets a value indicating whether the main thread is executing <see cref="DxgiSwapChainPresentDetour"/>.</summary>
|
||||||
/// <remarks>This still will be <c>true</c> even when queried off the main thread.</remarks>
|
/// <remarks>This still will be <c>true</c> even when queried off the main thread.</remarks>
|
||||||
public bool IsMainThreadInPresent { get; private set; }
|
public bool IsMainThreadInPresent { get; private set; }
|
||||||
|
|
||||||
|
|
@ -243,7 +253,7 @@ internal class InterfaceManager : IInternalDisposableService
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask;
|
public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask;
|
||||||
|
|
||||||
/// <summary>Gets the number of calls to <see cref="PresentDetour"/> so far.</summary>
|
/// <summary>Gets the number of calls to <see cref="DxgiSwapChainPresentDetour"/> so far.</summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The value increases even when Dalamud is hidden via "/xlui hide".
|
/// The value increases even when Dalamud is hidden via "/xlui hide".
|
||||||
/// <see cref="DalamudInterface.FrameCount"/> does not.
|
/// <see cref="DalamudInterface.FrameCount"/> does not.
|
||||||
|
|
@ -290,8 +300,11 @@ internal class InterfaceManager : IInternalDisposableService
|
||||||
{
|
{
|
||||||
this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc;
|
this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc;
|
||||||
Interlocked.Exchange(ref this.setCursorHook, null)?.Dispose();
|
Interlocked.Exchange(ref this.setCursorHook, null)?.Dispose();
|
||||||
Interlocked.Exchange(ref this.presentHook, null)?.Dispose();
|
Interlocked.Exchange(ref this.dxgiSwapChainPresentHook, null)?.Dispose();
|
||||||
Interlocked.Exchange(ref this.resizeBuffersHook, null)?.Dispose();
|
Interlocked.Exchange(ref this.reShadeDxgiSwapChainPresentHook, null)?.Dispose();
|
||||||
|
Interlocked.Exchange(ref this.dxgiSwapChainResizeBuffersHook, null)?.Dispose();
|
||||||
|
Interlocked.Exchange(ref this.dxgiSwapChainHook, null)?.Dispose();
|
||||||
|
Interlocked.Exchange(ref this.reShadeAddonInterface, null)?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -327,7 +340,7 @@ internal class InterfaceManager : IInternalDisposableService
|
||||||
/// <returns>A <see cref="Task"/> that resolves once <paramref name="action"/> is run.</returns>
|
/// <returns>A <see cref="Task"/> that resolves once <paramref name="action"/> is run.</returns>
|
||||||
public Task RunBeforeImGuiRender(Action action)
|
public Task RunBeforeImGuiRender(Action action)
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource();
|
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
this.runBeforeImGuiRender.Enqueue(
|
this.runBeforeImGuiRender.Enqueue(
|
||||||
() =>
|
() =>
|
||||||
{
|
{
|
||||||
|
|
@ -350,7 +363,7 @@ internal class InterfaceManager : IInternalDisposableService
|
||||||
/// <returns>A <see cref="Task"/> that resolves once <paramref name="func"/> is run.</returns>
|
/// <returns>A <see cref="Task"/> that resolves once <paramref name="func"/> is run.</returns>
|
||||||
public Task<T> RunBeforeImGuiRender<T>(Func<T> func)
|
public Task<T> RunBeforeImGuiRender<T>(Func<T> func)
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<T>();
|
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
this.runBeforeImGuiRender.Enqueue(
|
this.runBeforeImGuiRender.Enqueue(
|
||||||
() =>
|
() =>
|
||||||
{
|
{
|
||||||
|
|
@ -371,7 +384,7 @@ internal class InterfaceManager : IInternalDisposableService
|
||||||
/// <returns>A <see cref="Task"/> that resolves once <paramref name="action"/> is run.</returns>
|
/// <returns>A <see cref="Task"/> that resolves once <paramref name="action"/> is run.</returns>
|
||||||
public Task RunAfterImGuiRender(Action action)
|
public Task RunAfterImGuiRender(Action action)
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource();
|
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
this.runAfterImGuiRender.Enqueue(
|
this.runAfterImGuiRender.Enqueue(
|
||||||
() =>
|
() =>
|
||||||
{
|
{
|
||||||
|
|
@ -394,7 +407,7 @@ internal class InterfaceManager : IInternalDisposableService
|
||||||
/// <returns>A <see cref="Task"/> that resolves once <paramref name="func"/> is run.</returns>
|
/// <returns>A <see cref="Task"/> that resolves once <paramref name="func"/> is run.</returns>
|
||||||
public Task<T> RunAfterImGuiRender<T>(Func<T> func)
|
public Task<T> RunAfterImGuiRender<T>(Func<T> func)
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<T>();
|
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
this.runAfterImGuiRender.Enqueue(
|
this.runAfterImGuiRender.Enqueue(
|
||||||
() =>
|
() =>
|
||||||
{
|
{
|
||||||
|
|
@ -473,24 +486,72 @@ internal class InterfaceManager : IInternalDisposableService
|
||||||
return im;
|
return im;
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
/// <summary>Checks if the provided swap chain is the target that Dalamud should draw its interface onto,
|
||||||
private static void RenderImGui(IImGuiBackend backend)
|
/// and initializes ImGui for drawing.</summary>
|
||||||
|
/// <param name="swapChain">The swap chain to test and initialize ImGui with if conditions are met.</param>
|
||||||
|
/// <param name="flags">Flags passed to <see cref="IDXGISwapChain.Present"/>.</param>
|
||||||
|
/// <returns>An initialized instance of <see cref="IDXGISwapChain"/>, or <c>null</c> if <paramref name="swapChain"/>
|
||||||
|
/// is not the main swap chain.</returns>
|
||||||
|
private unsafe IImGuiBackend? RenderDalamudCheckAndInitialize(IDXGISwapChain* swapChain, uint flags)
|
||||||
{
|
{
|
||||||
var conf = Service<DalamudConfiguration>.Get();
|
// Quoting ReShade dxgi_swapchain.cpp DXGISwapChain::on_present:
|
||||||
|
// > Some D3D11 games test presentation for timing and composition purposes
|
||||||
|
// > These calls are not rendering related, but rather a status request for the D3D runtime and as such should be ignored
|
||||||
|
if ((flags & DXGI.DXGI_PRESENT_TEST) != 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!SwapChainHelper.IsGameDeviceSwapChain(swapChain))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already");
|
||||||
|
|
||||||
|
var activeBackend = this.backend ?? this.InitBackend(swapChain);
|
||||||
|
|
||||||
|
if (!this.dalamudAtlas!.HasBuiltAtlas)
|
||||||
|
{
|
||||||
|
if (this.dalamudAtlas.BuildTask.Exception != null)
|
||||||
|
{
|
||||||
|
// TODO: Can we do something more user-friendly here? Unload instead?
|
||||||
|
Log.Error(this.dalamudAtlas.BuildTask.Exception, "Failed to initialize Dalamud base fonts");
|
||||||
|
Util.Fatal("Failed to initialize Dalamud base fonts.\nPlease report this error.", "Dalamud");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return activeBackend;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Draws Dalamud to the given scene representing the ImGui context.</summary>
|
||||||
|
/// <param name="activeBackend">The scene to draw to.</param>
|
||||||
|
private void RenderDalamudDraw(IImGuiBackend activeBackend)
|
||||||
|
{
|
||||||
|
this.CumulativePresentCalls++;
|
||||||
|
this.IsMainThreadInPresent = true;
|
||||||
|
|
||||||
|
while (this.runBeforeImGuiRender.TryDequeue(out var action))
|
||||||
|
action.InvokeSafely();
|
||||||
|
|
||||||
// Process information needed by ImGuiHelpers each frame.
|
// Process information needed by ImGuiHelpers each frame.
|
||||||
ImGuiHelpers.NewFrame();
|
ImGuiHelpers.NewFrame();
|
||||||
|
|
||||||
// Enable viewports if there are no issues.
|
// Enable viewports if there are no issues.
|
||||||
if (conf.IsDisableViewport || backend.IsMainViewportFullScreen() || ImGui.GetPlatformIO().Monitors.Size == 1)
|
var viewportsEnable = this.dalamudConfiguration.IsDisableViewport ||
|
||||||
|
activeBackend.IsMainViewportFullScreen() ||
|
||||||
|
ImGui.GetPlatformIO().Monitors.Size == 1;
|
||||||
|
if (viewportsEnable)
|
||||||
ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable;
|
ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable;
|
||||||
else
|
else
|
||||||
ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable;
|
ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable;
|
||||||
|
|
||||||
backend.Render();
|
// Call drawing functions, which in turn will call Draw event.
|
||||||
|
activeBackend.Render();
|
||||||
|
|
||||||
|
this.PostImGuiRender();
|
||||||
|
this.IsMainThreadInPresent = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void InitScene(IDXGISwapChain* swapChain)
|
private unsafe IImGuiBackend InitBackend(IDXGISwapChain* swapChain)
|
||||||
{
|
{
|
||||||
IWin32Backend newBackend;
|
IWin32Backend newBackend;
|
||||||
using (Timings.Start("IM Scene Init"))
|
using (Timings.Start("IM Scene Init"))
|
||||||
|
|
@ -504,27 +565,33 @@ internal class InterfaceManager : IInternalDisposableService
|
||||||
Service<InterfaceManagerWithScene>.ProvideException(ex);
|
Service<InterfaceManagerWithScene>.ProvideException(ex);
|
||||||
Log.Error(ex, "Could not load ImGui dependencies.");
|
Log.Error(ex, "Could not load ImGui dependencies.");
|
||||||
|
|
||||||
var res = User32.MessageBox(
|
fixed (void* lpText =
|
||||||
IntPtr.Zero,
|
"Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?")
|
||||||
"Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?",
|
|
||||||
"Dalamud Error",
|
|
||||||
User32.MessageBoxOptions.MB_YESNO | User32.MessageBoxOptions.MB_TOPMOST |
|
|
||||||
User32.MessageBoxOptions.MB_ICONERROR);
|
|
||||||
|
|
||||||
if (res == User32.MessageBoxResult.IDYES)
|
|
||||||
{
|
{
|
||||||
var psi = new ProcessStartInfo
|
fixed (void* lpCaption = "Dalamud Error")
|
||||||
{
|
{
|
||||||
FileName = "https://aka.ms/vs/16/release/vc_redist.x64.exe",
|
var res = MessageBoxW(
|
||||||
UseShellExecute = true,
|
default,
|
||||||
};
|
(ushort*)lpText,
|
||||||
Process.Start(psi);
|
(ushort*)lpCaption,
|
||||||
|
MB.MB_YESNO | MB.MB_TOPMOST | MB.MB_ICONERROR);
|
||||||
|
|
||||||
|
if (res == IDYES)
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "https://aka.ms/vs/16/release/vc_redist.x64.exe",
|
||||||
|
UseShellExecute = true,
|
||||||
|
};
|
||||||
|
Process.Start(psi);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Environment.Exit(-1);
|
Environment.Exit(-1);
|
||||||
|
|
||||||
// Doesn't reach here, but to make the compiler not complain
|
// Doesn't reach here, but to make the compiler not complain
|
||||||
return;
|
throw new InvalidOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var startInfo = Service<Dalamud>.Get().StartInfo;
|
var startInfo = Service<Dalamud>.Get().StartInfo;
|
||||||
|
|
@ -621,6 +688,7 @@ internal class InterfaceManager : IInternalDisposableService
|
||||||
Service<InterfaceManagerWithScene>.Provide(new(this));
|
Service<InterfaceManagerWithScene>.Provide(new(this));
|
||||||
|
|
||||||
this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc;
|
this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc;
|
||||||
|
return newBackend;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WndProcHookManagerOnPreWndProc(WndProcEventArgs args)
|
private void WndProcHookManagerOnPreWndProc(WndProcEventArgs args)
|
||||||
|
|
@ -630,53 +698,6 @@ internal class InterfaceManager : IInternalDisposableService
|
||||||
args.SuppressWithValue(r.Value);
|
args.SuppressWithValue(r.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* NOTE(goat): When hooking ReShade DXGISwapChain::runtime_present, this is missing the syncInterval arg.
|
|
||||||
* Seems to work fine regardless, I guess, so whatever.
|
|
||||||
*/
|
|
||||||
private unsafe HRESULT PresentDetour(IDXGISwapChain* swapChain, uint syncInterval, uint presentFlags)
|
|
||||||
{
|
|
||||||
Debug.Assert(this.presentHook is not null, "How did PresentDetour get called when presentHook is null?");
|
|
||||||
|
|
||||||
if (this.backend is null)
|
|
||||||
{
|
|
||||||
this.InitScene(swapChain);
|
|
||||||
if (this.backend is null)
|
|
||||||
throw new InvalidOperationException("InitScene did not set this.scene?");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.backend.IsAttachedToPresentationTarget((nint)swapChain))
|
|
||||||
return this.presentHook!.Original(swapChain, syncInterval, presentFlags);
|
|
||||||
|
|
||||||
// Do not do anything yet if no font atlas has been built yet.
|
|
||||||
if (this.dalamudAtlas?.HasBuiltAtlas is not true)
|
|
||||||
{
|
|
||||||
if (this.dalamudAtlas?.BuildTask.Exception != null)
|
|
||||||
{
|
|
||||||
// TODO: Can we do something more user-friendly here? Unload instead?
|
|
||||||
Log.Error(this.dalamudAtlas.BuildTask.Exception, "Failed to initialize Dalamud base fonts");
|
|
||||||
Util.Fatal("Failed to initialize Dalamud base fonts.\nPlease report this error.", "Dalamud");
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.presentHook!.Original(swapChain, syncInterval, presentFlags);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.IsMainThreadInPresent = true;
|
|
||||||
this.CumulativePresentCalls++;
|
|
||||||
this.PreImGuiRender();
|
|
||||||
RenderImGui(this.backend!);
|
|
||||||
this.PostImGuiRender();
|
|
||||||
this.IsMainThreadInPresent = false;
|
|
||||||
|
|
||||||
return this.presentHook!.Original(swapChain, syncInterval, presentFlags);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PreImGuiRender()
|
|
||||||
{
|
|
||||||
while (this.runBeforeImGuiRender.TryDequeue(out var action))
|
|
||||||
action.InvokeSafely();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PostImGuiRender()
|
private void PostImGuiRender()
|
||||||
{
|
{
|
||||||
while (this.runAfterImGuiRender.TryDequeue(out var action))
|
while (this.runAfterImGuiRender.TryDequeue(out var action))
|
||||||
|
|
@ -726,14 +747,13 @@ internal class InterfaceManager : IInternalDisposableService
|
||||||
GlyphMaxAdvanceX = DefaultFontSizePx,
|
GlyphMaxAdvanceX = DefaultFontSizePx,
|
||||||
})));
|
})));
|
||||||
this.IconFontFixedWidthHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
|
this.IconFontFixedWidthHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
|
||||||
e => e.OnPreBuild(
|
e => e.OnPreBuild(tk => tk.AddDalamudAssetFont(
|
||||||
tk => tk.AddDalamudAssetFont(
|
DalamudAsset.FontAwesomeFreeSolid,
|
||||||
DalamudAsset.FontAwesomeFreeSolid,
|
new()
|
||||||
new()
|
{
|
||||||
{
|
SizePx = Service<FontAtlasFactory>.Get().DefaultFontSpec.SizePx,
|
||||||
SizePx = Service<FontAtlasFactory>.Get().DefaultFontSpec.SizePx,
|
GlyphRanges = [0x20, 0x20, 0x00],
|
||||||
GlyphRanges = [0x20, 0x20, 0x00],
|
})));
|
||||||
})));
|
|
||||||
this.MonoFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
|
this.MonoFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle(
|
||||||
e => e.OnPreBuild(
|
e => e.OnPreBuild(
|
||||||
tk => tk.AddDalamudAssetFont(
|
tk => tk.AddDalamudAssetFont(
|
||||||
|
|
@ -781,9 +801,13 @@ internal class InterfaceManager : IInternalDisposableService
|
||||||
_ = this.dalamudAtlas.BuildFontsAsync();
|
_ = this.dalamudAtlas.BuildFontsAsync();
|
||||||
|
|
||||||
SwapChainHelper.BusyWaitForGameDeviceSwapChain();
|
SwapChainHelper.BusyWaitForGameDeviceSwapChain();
|
||||||
|
var swapChainDesc = default(DXGI_SWAP_CHAIN_DESC);
|
||||||
|
if (SwapChainHelper.GameDeviceSwapChain->GetDesc(&swapChainDesc).SUCCEEDED)
|
||||||
|
this.gameWindowHandle = swapChainDesc.OutputWindow;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Requires that game window to be there, which will be the case once game swap chain is initialized.
|
||||||
if (Service<DalamudConfiguration>.Get().WindowIsImmersive)
|
if (Service<DalamudConfiguration>.Get().WindowIsImmersive)
|
||||||
this.SetImmersiveMode(true);
|
this.SetImmersiveMode(true);
|
||||||
}
|
}
|
||||||
|
|
@ -799,60 +823,206 @@ internal class InterfaceManager : IInternalDisposableService
|
||||||
0,
|
0,
|
||||||
this.SetCursorDetour);
|
this.SetCursorDetour);
|
||||||
|
|
||||||
Log.Verbose("===== S W A P C H A I N =====");
|
if (ReShadeAddonInterface.ReShadeIsSignedByReShade)
|
||||||
this.resizeBuffersHook = Hook<ResizeBuffersDelegate>.FromAddress(
|
{
|
||||||
(nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers,
|
Log.Warning("Signed ReShade binary detected");
|
||||||
this.ResizeBuffersDetour);
|
Service<NotificationManager>
|
||||||
Log.Verbose($"ResizeBuffers address {Util.DescribeAddress(this.resizeBuffersHook!.Address)}");
|
.GetAsync()
|
||||||
|
.ContinueWith(
|
||||||
|
nmt => nmt.Result.AddNotification(
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
MinimizedText = Loc.Localize(
|
||||||
|
"ReShadeNoAddonSupportNotificationMinimizedText",
|
||||||
|
"Wrong ReShade installation"),
|
||||||
|
Content = Loc.Localize(
|
||||||
|
"ReShadeNoAddonSupportNotificationContent",
|
||||||
|
"Your installation of ReShade does not have full addon support, and may not work with Dalamud and/or the game.\n" +
|
||||||
|
"Download and install ReShade with full addon-support."),
|
||||||
|
Type = NotificationType.Warning,
|
||||||
|
InitialDuration = TimeSpan.MaxValue,
|
||||||
|
ShowIndeterminateIfNoExpiry = false,
|
||||||
|
})).ContinueWith(
|
||||||
|
t =>
|
||||||
|
{
|
||||||
|
t.Result.DrawActions += _ =>
|
||||||
|
{
|
||||||
|
ImGuiHelpers.ScaledDummy(2);
|
||||||
|
if (DalamudComponents.PrimaryButton(Loc.Localize("LearnMore", "Learn more...")))
|
||||||
|
{
|
||||||
|
Util.OpenLink("https://dalamud.dev/news/2024/07/23/reshade/");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.presentHook = Hook<PresentDelegate>.FromAddress(
|
Log.Information("===== S W A P C H A I N =====");
|
||||||
(nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present,
|
var sb = new StringBuilder();
|
||||||
this.PresentDetour);
|
foreach (var m in ReShadeAddonInterface.AllReShadeModules)
|
||||||
Log.Verbose(
|
{
|
||||||
$"IDXGISwapChain::Present address {Util.DescribeAddress(SwapChainHelper.GameDeviceSwapChainVtbl->Present)}");
|
sb.Clear();
|
||||||
|
sb.Append("ReShade detected: ");
|
||||||
|
sb.Append(m.FileName).Append('(');
|
||||||
|
sb.Append(m.FileVersionInfo.OriginalFilename);
|
||||||
|
sb.Append("; ").Append(m.FileVersionInfo.ProductName);
|
||||||
|
sb.Append("; ").Append(m.FileVersionInfo.ProductVersion);
|
||||||
|
sb.Append("; ").Append(m.FileVersionInfo.FileDescription);
|
||||||
|
sb.Append("; ").Append(m.FileVersionInfo.FileVersion);
|
||||||
|
sb.Append($"@ 0x{m.BaseAddress:X}");
|
||||||
|
if (!ReferenceEquals(m, ReShadeAddonInterface.ReShadeModule))
|
||||||
|
sb.Append(" [ignored by Dalamud]");
|
||||||
|
Log.Information(sb.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReShadeAddonInterface.AllReShadeModules.Length > 1)
|
||||||
|
Log.Warning("Multiple ReShade dlls are detected.");
|
||||||
|
|
||||||
|
ResizeBuffersDelegate dxgiSwapChainResizeBuffersDelegate;
|
||||||
|
ReShadeDxgiSwapChainPresentDelegate? reShadeDxgiSwapChainPresentDelegate = null;
|
||||||
|
DxgiSwapChainPresentDelegate? dxgiSwapChainPresentDelegate = null;
|
||||||
|
nint pfnReShadeDxgiSwapChainPresent = 0;
|
||||||
|
switch (this.dalamudConfiguration.ReShadeHandlingMode)
|
||||||
|
{
|
||||||
|
// If ReShade is not found, do no special handling.
|
||||||
|
case var _ when ReShadeAddonInterface.ReShadeModule is null:
|
||||||
|
goto default;
|
||||||
|
|
||||||
|
// This is the only mode honored when SwapChainHookMode is set to VTable.
|
||||||
|
case ReShadeHandlingMode.Default:
|
||||||
|
case ReShadeHandlingMode.UnwrapReShade:
|
||||||
|
if (SwapChainHelper.UnwrapReShade())
|
||||||
|
Log.Information("Unwrapped ReShade");
|
||||||
|
else
|
||||||
|
Log.Warning("Could not unwrap ReShade");
|
||||||
|
goto default;
|
||||||
|
|
||||||
|
// Do no special ReShade handling.
|
||||||
|
// If SwapChainHookMode is set to VTable, do no special handling.
|
||||||
|
case ReShadeHandlingMode.None:
|
||||||
|
case var _ when this.dalamudConfiguration.SwapChainHookMode == SwapChainHelper.HookMode.VTable:
|
||||||
|
default:
|
||||||
|
dxgiSwapChainResizeBuffersDelegate = this.AsHookDxgiSwapChainResizeBuffersDetour;
|
||||||
|
dxgiSwapChainPresentDelegate = this.DxgiSwapChainPresentDetour;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Register Dalamud as a ReShade addon.
|
||||||
|
case ReShadeHandlingMode.ReShadeAddonPresent:
|
||||||
|
case ReShadeHandlingMode.ReShadeAddonReShadeOverlay:
|
||||||
|
if (!ReShadeAddonInterface.TryRegisterAddon(out this.reShadeAddonInterface))
|
||||||
|
{
|
||||||
|
Log.Warning("Could not register as ReShade addon");
|
||||||
|
goto default;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Information("Registered as a ReShade addon");
|
||||||
|
this.reShadeAddonInterface.InitSwapChain += this.ReShadeAddonInterfaceOnInitSwapChain;
|
||||||
|
this.reShadeAddonInterface.DestroySwapChain += this.ReShadeAddonInterfaceOnDestroySwapChain;
|
||||||
|
if (this.dalamudConfiguration.ReShadeHandlingMode == ReShadeHandlingMode.ReShadeAddonPresent)
|
||||||
|
this.reShadeAddonInterface.Present += this.ReShadeAddonInterfaceOnPresent;
|
||||||
|
else
|
||||||
|
this.reShadeAddonInterface.ReShadeOverlay += this.ReShadeAddonInterfaceOnReShadeOverlay;
|
||||||
|
|
||||||
|
dxgiSwapChainResizeBuffersDelegate = this.AsReShadeAddonDxgiSwapChainResizeBuffersDetour;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Hook ReShade's DXGISwapChain::on_present. This is the legacy and the default option.
|
||||||
|
case ReShadeHandlingMode.HookReShadeDxgiSwapChainOnPresent:
|
||||||
|
pfnReShadeDxgiSwapChainPresent = ReShadeAddonInterface.FindReShadeDxgiSwapChainOnPresent();
|
||||||
|
|
||||||
|
if (pfnReShadeDxgiSwapChainPresent == 0)
|
||||||
|
{
|
||||||
|
Log.Warning("ReShade::DXGISwapChain::on_present could not be found");
|
||||||
|
goto default;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Information(
|
||||||
|
"Found ReShade::DXGISwapChain::on_present at {addr}",
|
||||||
|
Util.DescribeAddress(pfnReShadeDxgiSwapChainPresent));
|
||||||
|
reShadeDxgiSwapChainPresentDelegate = this.ReShadeDxgiSwapChainOnPresentDetour;
|
||||||
|
dxgiSwapChainResizeBuffersDelegate = this.AsHookDxgiSwapChainResizeBuffersDetour;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.dalamudConfiguration.SwapChainHookMode)
|
||||||
|
{
|
||||||
|
case SwapChainHelper.HookMode.ByteCode:
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
Log.Information("Hooking using bytecode...");
|
||||||
|
this.dxgiSwapChainResizeBuffersHook = Hook<ResizeBuffersDelegate>.FromAddress(
|
||||||
|
(nint)SwapChainHelper.GameDeviceSwapChainVtbl->ResizeBuffers,
|
||||||
|
dxgiSwapChainResizeBuffersDelegate);
|
||||||
|
Log.Information(
|
||||||
|
"Hooked IDXGISwapChain::ResizeBuffers using bytecode: {addr}",
|
||||||
|
Util.DescribeAddress(this.dxgiSwapChainResizeBuffersHook.Address));
|
||||||
|
|
||||||
|
if (dxgiSwapChainPresentDelegate is not null)
|
||||||
|
{
|
||||||
|
this.dxgiSwapChainPresentHook = Hook<DxgiSwapChainPresentDelegate>.FromAddress(
|
||||||
|
(nint)SwapChainHelper.GameDeviceSwapChainVtbl->Present,
|
||||||
|
dxgiSwapChainPresentDelegate);
|
||||||
|
Log.Information(
|
||||||
|
"Hooked IDXGISwapChain::Present using bytecode: {addr}",
|
||||||
|
Util.DescribeAddress(this.dxgiSwapChainPresentHook.Address));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reShadeDxgiSwapChainPresentDelegate is not null && pfnReShadeDxgiSwapChainPresent != 0)
|
||||||
|
{
|
||||||
|
this.reShadeDxgiSwapChainPresentHook = Hook<ReShadeDxgiSwapChainPresentDelegate>.FromAddress(
|
||||||
|
pfnReShadeDxgiSwapChainPresent,
|
||||||
|
reShadeDxgiSwapChainPresentDelegate);
|
||||||
|
Log.Information(
|
||||||
|
"Hooked ReShade::DXGISwapChain::on_present using bytecode: {addr}",
|
||||||
|
Util.DescribeAddress(this.reShadeDxgiSwapChainPresentHook.Address));
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SwapChainHelper.HookMode.VTable:
|
||||||
|
{
|
||||||
|
Log.Information("Hooking using VTable...");
|
||||||
|
this.dxgiSwapChainHook = new(SwapChainHelper.GameDeviceSwapChain);
|
||||||
|
this.dxgiSwapChainResizeBuffersHook = this.dxgiSwapChainHook.CreateHook(
|
||||||
|
nameof(IDXGISwapChain.ResizeBuffers),
|
||||||
|
dxgiSwapChainResizeBuffersDelegate);
|
||||||
|
Log.Information(
|
||||||
|
"Hooked IDXGISwapChain::ResizeBuffers using VTable: {addr}",
|
||||||
|
Util.DescribeAddress(this.dxgiSwapChainResizeBuffersHook.Address));
|
||||||
|
|
||||||
|
if (dxgiSwapChainPresentDelegate is not null)
|
||||||
|
{
|
||||||
|
this.dxgiSwapChainPresentHook = this.dxgiSwapChainHook.CreateHook(
|
||||||
|
nameof(IDXGISwapChain.Present),
|
||||||
|
dxgiSwapChainPresentDelegate);
|
||||||
|
Log.Information(
|
||||||
|
"Hooked IDXGISwapChain::Present using VTable: {addr}",
|
||||||
|
Util.DescribeAddress(this.dxgiSwapChainPresentHook.Address));
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Information(
|
||||||
|
"Detouring vtable at {addr}: {prev} to {new}",
|
||||||
|
Util.DescribeAddress(this.dxgiSwapChainHook.Address),
|
||||||
|
Util.DescribeAddress(this.dxgiSwapChainHook.OriginalVTableAddress),
|
||||||
|
Util.DescribeAddress(this.dxgiSwapChainHook.OverridenVTableAddress));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.setCursorHook.Enable();
|
this.setCursorHook.Enable();
|
||||||
this.presentHook.Enable();
|
this.reShadeDxgiSwapChainPresentHook?.Enable();
|
||||||
this.resizeBuffersHook.Enable();
|
this.dxgiSwapChainResizeBuffersHook.Enable();
|
||||||
|
this.dxgiSwapChainPresentHook?.Enable();
|
||||||
|
this.dxgiSwapChainHook?.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe HRESULT ResizeBuffersDetour(
|
private nint SetCursorDetour(nint hCursor)
|
||||||
IDXGISwapChain* swapChain,
|
|
||||||
uint bufferCount,
|
|
||||||
uint width,
|
|
||||||
uint height,
|
|
||||||
uint newFormat,
|
|
||||||
uint swapChainFlags)
|
|
||||||
{
|
|
||||||
#if DEBUG
|
|
||||||
Log.Verbose(
|
|
||||||
$"Calling resizebuffers swap@{(nint)swapChain:X}{bufferCount} {width} {height} {newFormat} {swapChainFlags}");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
this.ResizeBuffers?.InvokeSafely();
|
|
||||||
|
|
||||||
// We have to ensure we're working with the main swapchain, as other viewports might be resizing as well.
|
|
||||||
if (this.backend?.IsAttachedToPresentationTarget((nint)swapChain) is not true)
|
|
||||||
return this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
|
|
||||||
|
|
||||||
this.backend?.OnPreResize();
|
|
||||||
|
|
||||||
var ret = this.resizeBuffersHook!.Original(swapChain, bufferCount, width, height, newFormat, swapChainFlags);
|
|
||||||
if (ret == DXGI.DXGI_ERROR_INVALID_CALL)
|
|
||||||
Log.Error("invalid call to resizeBuffers");
|
|
||||||
|
|
||||||
this.backend?.OnPostResize((int)width, (int)height);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private HCURSOR SetCursorDetour(HCURSOR hCursor)
|
|
||||||
{
|
{
|
||||||
if (this.lastWantCapture && (!this.backend?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor)
|
if (this.lastWantCapture && (!this.backend?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return this.setCursorHook?.IsDisposed is not false
|
return this.setCursorHook?.IsDisposed is not false
|
||||||
? SetCursor(hCursor)
|
? SetCursor((HCURSOR)hCursor)
|
||||||
: this.setCursorHook.Original(hCursor);
|
: this.setCursorHook.Original(hCursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ internal class PluginCategoryManager
|
||||||
new(CategoryKind.All, "special.all", () => Locs.Category_All),
|
new(CategoryKind.All, "special.all", () => Locs.Category_All),
|
||||||
new(CategoryKind.IsTesting, "special.isTesting", () => Locs.Category_IsTesting, CategoryInfo.AppearCondition.DoPluginTest),
|
new(CategoryKind.IsTesting, "special.isTesting", () => Locs.Category_IsTesting, CategoryInfo.AppearCondition.DoPluginTest),
|
||||||
new(CategoryKind.AvailableForTesting, "special.availableForTesting", () => Locs.Category_AvailableForTesting, CategoryInfo.AppearCondition.DoPluginTest),
|
new(CategoryKind.AvailableForTesting, "special.availableForTesting", () => Locs.Category_AvailableForTesting, CategoryInfo.AppearCondition.DoPluginTest),
|
||||||
|
new(CategoryKind.Hidden, "special.hidden", () => Locs.Category_Hidden, CategoryInfo.AppearCondition.AnyHiddenPlugins),
|
||||||
new(CategoryKind.DevInstalled, "special.devInstalled", () => Locs.Category_DevInstalled),
|
new(CategoryKind.DevInstalled, "special.devInstalled", () => Locs.Category_DevInstalled),
|
||||||
new(CategoryKind.IconTester, "special.devIconTester", () => Locs.Category_IconTester),
|
new(CategoryKind.IconTester, "special.devIconTester", () => Locs.Category_IconTester),
|
||||||
new(CategoryKind.DalamudChangelogs, "special.dalamud", () => Locs.Category_Dalamud),
|
new(CategoryKind.DalamudChangelogs, "special.dalamud", () => Locs.Category_Dalamud),
|
||||||
|
|
@ -106,6 +107,11 @@ internal class PluginCategoryManager
|
||||||
/// </summary>
|
/// </summary>
|
||||||
AvailableForTesting = 2,
|
AvailableForTesting = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Plugins that were hidden.
|
||||||
|
/// </summary>
|
||||||
|
Hidden = 3,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Installed dev plugins.
|
/// Installed dev plugins.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -309,6 +315,9 @@ internal class PluginCategoryManager
|
||||||
{
|
{
|
||||||
groupAvail.Categories.Add(this.CategoryList[categoryIdx].CategoryKind);
|
groupAvail.Categories.Add(this.CategoryList[categoryIdx].CategoryKind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hidden at the end
|
||||||
|
groupAvail.Categories.Add(CategoryKind.Hidden);
|
||||||
|
|
||||||
// compare with prev state and mark as dirty if needed
|
// compare with prev state and mark as dirty if needed
|
||||||
var noCategoryChanges = prevCategoryIds.SequenceEqual(groupAvail.Categories);
|
var noCategoryChanges = prevCategoryIds.SequenceEqual(groupAvail.Categories);
|
||||||
|
|
@ -332,7 +341,10 @@ internal class PluginCategoryManager
|
||||||
{
|
{
|
||||||
var groupInfo = this.groupList[this.currentGroupIdx];
|
var groupInfo = this.groupList[this.currentGroupIdx];
|
||||||
|
|
||||||
var includeAll = (this.currentCategoryKind == CategoryKind.All) || (groupInfo.GroupKind != GroupKind.Available);
|
var includeAll = this.currentCategoryKind == CategoryKind.All ||
|
||||||
|
this.currentCategoryKind == CategoryKind.Hidden ||
|
||||||
|
groupInfo.GroupKind != GroupKind.Available;
|
||||||
|
|
||||||
if (includeAll)
|
if (includeAll)
|
||||||
{
|
{
|
||||||
result.AddRange(plugins);
|
result.AddRange(plugins);
|
||||||
|
|
@ -455,6 +467,11 @@ internal class PluginCategoryManager
|
||||||
/// Check if plugin testing is enabled.
|
/// Check if plugin testing is enabled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
DoPluginTest,
|
DoPluginTest,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if there are any hidden plugins.
|
||||||
|
/// </summary>
|
||||||
|
AnyHiddenPlugins,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -529,6 +546,8 @@ internal class PluginCategoryManager
|
||||||
|
|
||||||
public static string Category_AvailableForTesting => Loc.Localize("InstallerCategoryAvailableForTesting", "Testing Available");
|
public static string Category_AvailableForTesting => Loc.Localize("InstallerCategoryAvailableForTesting", "Testing Available");
|
||||||
|
|
||||||
|
public static string Category_Hidden => Loc.Localize("InstallerCategoryHidden", "Hidden");
|
||||||
|
|
||||||
public static string Category_DevInstalled => Loc.Localize("InstallerInstalledDevPlugins", "Installed Dev Plugins");
|
public static string Category_DevInstalled => Loc.Localize("InstallerInstalledDevPlugins", "Installed Dev Plugins");
|
||||||
|
|
||||||
public static string Category_IconTester => "Image/Icon Tester";
|
public static string Category_IconTester => "Image/Icon Tester";
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,200 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
using TerraFX.Interop.Windows;
|
||||||
|
|
||||||
|
using static TerraFX.Interop.Windows.Windows;
|
||||||
|
|
||||||
|
namespace Dalamud.Interface.Internal.ReShadeHandling;
|
||||||
|
|
||||||
|
/// <summary>ReShade interface.</summary>
|
||||||
|
[SuppressMessage(
|
||||||
|
"StyleCop.CSharp.LayoutRules",
|
||||||
|
"SA1519:Braces should not be omitted from multi-line child statement",
|
||||||
|
Justification = "Multiple fixed blocks")]
|
||||||
|
internal sealed unsafe partial class ReShadeAddonInterface
|
||||||
|
{
|
||||||
|
private static readonly ExportsStruct Exports;
|
||||||
|
|
||||||
|
static ReShadeAddonInterface()
|
||||||
|
{
|
||||||
|
var modules = new List<ProcessModule>();
|
||||||
|
foreach (var m in Process.GetCurrentProcess().Modules.Cast<ProcessModule>())
|
||||||
|
{
|
||||||
|
ExportsStruct e;
|
||||||
|
if (!GetProcAddressInto(m, nameof(e.ReShadeRegisterAddon), &e.ReShadeRegisterAddon) ||
|
||||||
|
!GetProcAddressInto(m, nameof(e.ReShadeUnregisterAddon), &e.ReShadeUnregisterAddon) ||
|
||||||
|
!GetProcAddressInto(m, nameof(e.ReShadeRegisterEvent), &e.ReShadeRegisterEvent) ||
|
||||||
|
!GetProcAddressInto(m, nameof(e.ReShadeUnregisterEvent), &e.ReShadeUnregisterEvent))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
modules.Add(m);
|
||||||
|
if (modules.Count == 1)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var signerName = GetSignatureSignerNameWithoutVerification(m.FileName);
|
||||||
|
ReShadeIsSignedByReShade = signerName == "ReShade";
|
||||||
|
Log.Information(
|
||||||
|
"ReShade DLL is signed by {signerName}. {vn}={v}",
|
||||||
|
signerName,
|
||||||
|
nameof(ReShadeIsSignedByReShade),
|
||||||
|
ReShadeIsSignedByReShade);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Information(ex, "ReShade DLL did not had a valid signature.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ReShadeModule = m;
|
||||||
|
Exports = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AllReShadeModules = [..modules];
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool GetProcAddressInto(ProcessModule m, ReadOnlySpan<char> name, void* res)
|
||||||
|
{
|
||||||
|
Span<byte> name8 = stackalloc byte[Encoding.UTF8.GetByteCount(name) + 1];
|
||||||
|
name8[Encoding.UTF8.GetBytes(name, name8)] = 0;
|
||||||
|
*(nint*)res = GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)Unsafe.AsPointer(ref name8[0]));
|
||||||
|
return *(nint*)res != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets the active ReShade module.</summary>
|
||||||
|
public static ProcessModule? ReShadeModule { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Gets all the detected ReShade modules.</summary>
|
||||||
|
public static ImmutableArray<ProcessModule> AllReShadeModules { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Gets a value indicating whether the loaded ReShade has signatures.</summary>
|
||||||
|
/// <remarks>ReShade without addon support is signed, but may not pass signature verification.</remarks>
|
||||||
|
public static bool ReShadeIsSignedByReShade { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Finds the address of <c>DXGISwapChain::on_present</c> in <see cref="ReShadeModule"/>.</summary>
|
||||||
|
/// <returns>Address of the function, or <c>0</c> if not found.</returns>
|
||||||
|
public static nint FindReShadeDxgiSwapChainOnPresent()
|
||||||
|
{
|
||||||
|
if (ReShadeModule is not { } rsm)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var m = new ReadOnlySpan<byte>((void*)rsm.BaseAddress, rsm.ModuleMemorySize);
|
||||||
|
|
||||||
|
// Signature validated against 5.0.0 to 6.2.0
|
||||||
|
var i = m.IndexOf(new byte[] { 0xCC, 0xF6, 0xC2, 0x01, 0x0F, 0x85 });
|
||||||
|
if (i == -1)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return rsm.BaseAddress + i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets the name of the signer of a file that has a certificate embedded within, without verifying if the
|
||||||
|
/// file has a valid signature.</summary>
|
||||||
|
/// <param name="path">Path to the file.</param>
|
||||||
|
/// <returns>Name of the signer.</returns>
|
||||||
|
// https://learn.microsoft.com/en-us/previous-versions/troubleshoot/windows/win32/get-information-authenticode-signed-executables
|
||||||
|
private static string GetSignatureSignerNameWithoutVerification(ReadOnlySpan<char> path)
|
||||||
|
{
|
||||||
|
var hCertStore = default(HCERTSTORE);
|
||||||
|
var hMsg = default(HCRYPTMSG);
|
||||||
|
var pCertContext = default(CERT_CONTEXT*);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fixed (void* pwszFile = path)
|
||||||
|
{
|
||||||
|
uint dwMsgAndCertEncodingType;
|
||||||
|
uint dwContentType;
|
||||||
|
uint dwFormatType;
|
||||||
|
void* pvContext;
|
||||||
|
if (!CryptQueryObject(
|
||||||
|
CERT.CERT_QUERY_OBJECT_FILE,
|
||||||
|
pwszFile,
|
||||||
|
CERT.CERT_QUERY_CONTENT_FLAG_ALL,
|
||||||
|
CERT.CERT_QUERY_FORMAT_FLAG_ALL,
|
||||||
|
0,
|
||||||
|
&dwMsgAndCertEncodingType,
|
||||||
|
&dwContentType,
|
||||||
|
&dwFormatType,
|
||||||
|
&hCertStore,
|
||||||
|
&hMsg,
|
||||||
|
&pvContext))
|
||||||
|
{
|
||||||
|
throw new Win32Exception("CryptQueryObject");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pcb = 0u;
|
||||||
|
if (!CryptMsgGetParam(hMsg, CMSG.CMSG_SIGNER_INFO_PARAM, 0, null, &pcb))
|
||||||
|
throw new Win32Exception("CryptMsgGetParam(1)");
|
||||||
|
|
||||||
|
var signerInfo = GC.AllocateArray<byte>((int)pcb, true);
|
||||||
|
var pSignerInfo = (CMSG_SIGNER_INFO*)Unsafe.AsPointer(ref signerInfo[0]);
|
||||||
|
if (!CryptMsgGetParam(hMsg, CMSG.CMSG_SIGNER_INFO_PARAM, 0, pSignerInfo, &pcb))
|
||||||
|
throw new Win32Exception("CryptMsgGetParam(2)");
|
||||||
|
|
||||||
|
var certInfo = new CERT_INFO
|
||||||
|
{
|
||||||
|
Issuer = pSignerInfo->Issuer,
|
||||||
|
SerialNumber = pSignerInfo->SerialNumber,
|
||||||
|
};
|
||||||
|
pCertContext = CertFindCertificateInStore(
|
||||||
|
hCertStore,
|
||||||
|
X509.X509_ASN_ENCODING | PKCS.PKCS_7_ASN_ENCODING,
|
||||||
|
0,
|
||||||
|
CERT.CERT_FIND_SUBJECT_CERT,
|
||||||
|
&certInfo,
|
||||||
|
null);
|
||||||
|
if (pCertContext == default)
|
||||||
|
throw new Win32Exception("CertFindCertificateInStore");
|
||||||
|
|
||||||
|
pcb = CertGetNameStringW(
|
||||||
|
pCertContext,
|
||||||
|
CERT.CERT_NAME_SIMPLE_DISPLAY_TYPE,
|
||||||
|
CERT.CERT_NAME_ISSUER_FLAG,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
pcb);
|
||||||
|
if (pcb == 0)
|
||||||
|
throw new Win32Exception("CertGetNameStringW(1)");
|
||||||
|
|
||||||
|
var issuerName = GC.AllocateArray<char>((int)pcb, true);
|
||||||
|
pcb = CertGetNameStringW(
|
||||||
|
pCertContext,
|
||||||
|
CERT.CERT_NAME_SIMPLE_DISPLAY_TYPE,
|
||||||
|
CERT.CERT_NAME_ISSUER_FLAG,
|
||||||
|
null,
|
||||||
|
(ushort*)Unsafe.AsPointer(ref issuerName[0]),
|
||||||
|
pcb);
|
||||||
|
if (pcb == 0)
|
||||||
|
throw new Win32Exception("CertGetNameStringW(2)");
|
||||||
|
|
||||||
|
// The string is null-terminated.
|
||||||
|
return new(issuerName.AsSpan()[..^1]);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (pCertContext != default) CertFreeCertificateContext(pCertContext);
|
||||||
|
if (hCertStore != default) CertCloseStore(hCertStore, 0);
|
||||||
|
if (hMsg != default) CryptMsgClose(hMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ExportsStruct
|
||||||
|
{
|
||||||
|
public delegate* unmanaged<HMODULE, uint, bool> ReShadeRegisterAddon;
|
||||||
|
public delegate* unmanaged<HMODULE, void> ReShadeUnregisterAddon;
|
||||||
|
public delegate* unmanaged<AddonEvent, void*, void> ReShadeRegisterEvent;
|
||||||
|
public delegate* unmanaged<AddonEvent, void*, void> ReShadeUnregisterEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,251 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
|
using TerraFX.Interop.Windows;
|
||||||
|
|
||||||
|
using static TerraFX.Interop.Windows.Windows;
|
||||||
|
|
||||||
|
namespace Dalamud.Interface.Internal.ReShadeHandling;
|
||||||
|
|
||||||
|
/// <summary>ReShade interface.</summary>
|
||||||
|
internal sealed unsafe partial class ReShadeAddonInterface : IDisposable
|
||||||
|
{
|
||||||
|
private const int ReShadeApiVersion = 1;
|
||||||
|
|
||||||
|
private readonly HMODULE hDalamudModule;
|
||||||
|
|
||||||
|
private readonly Hook<GetModuleHandleExWDelegate> addonModuleResolverHook;
|
||||||
|
|
||||||
|
private readonly DelegateStorage<UnsafePresentDelegate> presentDelegate;
|
||||||
|
private readonly DelegateStorage<ReShadeOverlayDelegate> reShadeOverlayDelegate;
|
||||||
|
private readonly DelegateStorage<ReShadeInitSwapChain> initSwapChainDelegate;
|
||||||
|
private readonly DelegateStorage<ReShadeDestroySwapChain> destroySwapChainDelegate;
|
||||||
|
|
||||||
|
private bool requiresFinalize;
|
||||||
|
|
||||||
|
private ReShadeAddonInterface()
|
||||||
|
{
|
||||||
|
this.hDalamudModule = (HMODULE)Marshal.GetHINSTANCE(typeof(ReShadeAddonInterface).Assembly.ManifestModule);
|
||||||
|
if (!Exports.ReShadeRegisterAddon(this.hDalamudModule, ReShadeApiVersion))
|
||||||
|
throw new InvalidOperationException("ReShadeRegisterAddon failure.");
|
||||||
|
|
||||||
|
// https://github.com/crosire/reshade/commit/eaaa2a2c5adf5749ad17b358305da3f2d0f6baf4
|
||||||
|
// TODO: when ReShade gets a proper release with this commit, make this hook optional
|
||||||
|
this.addonModuleResolverHook = Hook<GetModuleHandleExWDelegate>.FromImport(
|
||||||
|
ReShadeModule!,
|
||||||
|
"kernel32.dll",
|
||||||
|
nameof(GetModuleHandleExW),
|
||||||
|
0,
|
||||||
|
this.GetModuleHandleExWDetour);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.addonModuleResolverHook.Enable();
|
||||||
|
Exports.ReShadeRegisterEvent(
|
||||||
|
AddonEvent.Present,
|
||||||
|
this.presentDelegate = new(
|
||||||
|
(
|
||||||
|
ref ApiObject commandQueue,
|
||||||
|
ref ApiObject swapChain,
|
||||||
|
RECT* pSourceRect,
|
||||||
|
RECT* pDestRect,
|
||||||
|
uint dirtyRectCount,
|
||||||
|
void* pDirtyRects) =>
|
||||||
|
this.Present?.Invoke(
|
||||||
|
ref commandQueue,
|
||||||
|
ref swapChain,
|
||||||
|
pSourceRect is null ? default : new(pSourceRect, 1),
|
||||||
|
pDestRect is null ? default : new(pDestRect, 1),
|
||||||
|
new(pDirtyRects, (int)dirtyRectCount))));
|
||||||
|
Exports.ReShadeRegisterEvent(
|
||||||
|
AddonEvent.ReShadeOverlay,
|
||||||
|
this.reShadeOverlayDelegate = new((ref ApiObject rt) => this.ReShadeOverlay?.Invoke(ref rt)));
|
||||||
|
Exports.ReShadeRegisterEvent(
|
||||||
|
AddonEvent.InitSwapChain,
|
||||||
|
this.initSwapChainDelegate = new((ref ApiObject rt) => this.InitSwapChain?.Invoke(ref rt)));
|
||||||
|
Exports.ReShadeRegisterEvent(
|
||||||
|
AddonEvent.DestroySwapChain,
|
||||||
|
this.destroySwapChainDelegate = new((ref ApiObject rt) => this.DestroySwapChain?.Invoke(ref rt)));
|
||||||
|
}
|
||||||
|
catch (Exception e1)
|
||||||
|
{
|
||||||
|
Exports.ReShadeUnregisterAddon(this.hDalamudModule);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.addonModuleResolverHook.Disable();
|
||||||
|
this.addonModuleResolverHook.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception e2)
|
||||||
|
{
|
||||||
|
throw new AggregateException(e1, e2);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requiresFinalize = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Finalizes an instance of the <see cref="ReShadeAddonInterface"/> class.</summary>
|
||||||
|
~ReShadeAddonInterface() => this.ReleaseUnmanagedResources();
|
||||||
|
|
||||||
|
/// <summary>Delegate for <see cref="ReShadeAddonInterface.AddonEvent.ReShadeOverlay"/>.</summary>
|
||||||
|
/// <param name="commandQueue">Current command queue. Type: <c>api::command_queue</c>.</param>
|
||||||
|
/// <param name="swapChain">Current swap chain. Type: <c>api::swapchain</c>.</param>
|
||||||
|
/// <param name="sourceRect">Optional; source rectangle. May contain up to 1 element.</param>
|
||||||
|
/// <param name="destRect">Optional; target rectangle. May contain up to 1 element.</param>
|
||||||
|
/// <param name="dirtyRects">Dirty rectangles.</param>
|
||||||
|
public delegate void PresentDelegate(
|
||||||
|
ref ApiObject commandQueue,
|
||||||
|
ref ApiObject swapChain,
|
||||||
|
ReadOnlySpan<RECT> sourceRect,
|
||||||
|
ReadOnlySpan<RECT> destRect,
|
||||||
|
ReadOnlySpan<RECT> dirtyRects);
|
||||||
|
|
||||||
|
/// <summary>Delegate for <see cref="ReShadeAddonInterface.AddonEvent.ReShadeOverlay"/>.</summary>
|
||||||
|
/// <param name="effectRuntime">Reference to the ReShade runtime.</param>
|
||||||
|
public delegate void ReShadeOverlayDelegate(ref ApiObject effectRuntime);
|
||||||
|
|
||||||
|
/// <summary>Delegate for <see cref="ReShadeAddonInterface.AddonEvent.InitSwapChain"/>.</summary>
|
||||||
|
/// <param name="swapChain">Reference to the ReShade SwapChain wrapper.</param>
|
||||||
|
public delegate void ReShadeInitSwapChain(ref ApiObject swapChain);
|
||||||
|
|
||||||
|
/// <summary>Delegate for <see cref="ReShadeAddonInterface.AddonEvent.DestroySwapChain"/>.</summary>
|
||||||
|
/// <param name="swapChain">Reference to the ReShade SwapChain wrapper.</param>
|
||||||
|
public delegate void ReShadeDestroySwapChain(ref ApiObject swapChain);
|
||||||
|
|
||||||
|
/// <summary>Delegate for <see cref="ReShadeAddonInterface.AddonEvent.ReShadeOverlay"/>.</summary>
|
||||||
|
/// <param name="commandQueue">Current command queue. Type: <c>api::command_queue</c>.</param>
|
||||||
|
/// <param name="swapChain">Current swap chain. Type: <c>api::swapchain</c>.</param>
|
||||||
|
/// <param name="pSourceRect">Optional; source rectangle.</param>
|
||||||
|
/// <param name="pDestRect">Optional; target rectangle.</param>
|
||||||
|
/// <param name="dirtyRectCount">Number of dirty rectangles.</param>
|
||||||
|
/// <param name="pDirtyRects">Optional; dirty rectangles.</param>
|
||||||
|
private delegate void UnsafePresentDelegate(
|
||||||
|
ref ApiObject commandQueue,
|
||||||
|
ref ApiObject swapChain,
|
||||||
|
RECT* pSourceRect,
|
||||||
|
RECT* pDestRect,
|
||||||
|
uint dirtyRectCount,
|
||||||
|
void* pDirtyRects);
|
||||||
|
|
||||||
|
private delegate BOOL GetModuleHandleExWDelegate(uint dwFlags, ushort* lpModuleName, HMODULE* phModule);
|
||||||
|
|
||||||
|
/// <summary>Called on <see cref="ReShadeAddonInterface.AddonEvent.Present"/>.</summary>
|
||||||
|
public event PresentDelegate? Present;
|
||||||
|
|
||||||
|
/// <summary>Called on <see cref="ReShadeAddonInterface.AddonEvent.ReShadeOverlay"/>.</summary>
|
||||||
|
public event ReShadeOverlayDelegate? ReShadeOverlay;
|
||||||
|
|
||||||
|
/// <summary>Called on <see cref="ReShadeAddonInterface.AddonEvent.InitSwapChain"/>.</summary>
|
||||||
|
public event ReShadeInitSwapChain? InitSwapChain;
|
||||||
|
|
||||||
|
/// <summary>Called on <see cref="ReShadeAddonInterface.AddonEvent.DestroySwapChain"/>.</summary>
|
||||||
|
public event ReShadeDestroySwapChain? DestroySwapChain;
|
||||||
|
|
||||||
|
/// <summary>Registers Dalamud as a ReShade addon.</summary>
|
||||||
|
/// <param name="r">Initialized interface.</param>
|
||||||
|
/// <returns><c>true</c> on success.</returns>
|
||||||
|
public static bool TryRegisterAddon([NotNullWhen(true)] out ReShadeAddonInterface? r)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
r = Exports.ReShadeRegisterAddon is null ? null : new();
|
||||||
|
return r is not null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
r = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
this.ReleaseUnmanagedResources();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReleaseUnmanagedResources()
|
||||||
|
{
|
||||||
|
if (!this.requiresFinalize)
|
||||||
|
return;
|
||||||
|
this.requiresFinalize = false;
|
||||||
|
// This will also unregister addon event registrations.
|
||||||
|
Exports.ReShadeUnregisterAddon(this.hDalamudModule);
|
||||||
|
this.addonModuleResolverHook.Disable();
|
||||||
|
this.addonModuleResolverHook.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private BOOL GetModuleHandleExWDetour(uint dwFlags, ushort* lpModuleName, HMODULE* phModule)
|
||||||
|
{
|
||||||
|
if ((dwFlags & GET.GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS) == 0)
|
||||||
|
return this.addonModuleResolverHook.Original(dwFlags, lpModuleName, phModule);
|
||||||
|
if ((dwFlags & GET.GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT) == 0)
|
||||||
|
return this.addonModuleResolverHook.Original(dwFlags, lpModuleName, phModule);
|
||||||
|
if (lpModuleName == this.initSwapChainDelegate ||
|
||||||
|
lpModuleName == this.destroySwapChainDelegate ||
|
||||||
|
lpModuleName == this.presentDelegate ||
|
||||||
|
lpModuleName == this.reShadeOverlayDelegate)
|
||||||
|
{
|
||||||
|
*phModule = this.hDalamudModule;
|
||||||
|
return BOOL.TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.addonModuleResolverHook.Original(dwFlags, lpModuleName, phModule);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>ReShade effect runtime object.</summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct ApiObject
|
||||||
|
{
|
||||||
|
/// <summary>The vtable.</summary>
|
||||||
|
public VTable* Vtbl;
|
||||||
|
|
||||||
|
/// <summary>Gets this object as a typed pointer.</summary>
|
||||||
|
/// <returns>Address of this instance.</returns>
|
||||||
|
/// <remarks>This call is invalid if this object is not already fixed.</remarks>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public ApiObject* AsPointer() => (ApiObject*)Unsafe.AsPointer(ref this);
|
||||||
|
|
||||||
|
/// <summary>Gets the native object.</summary>
|
||||||
|
/// <returns>The native object.</returns>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public nint GetNative() => this.Vtbl->GetNative(this.AsPointer());
|
||||||
|
|
||||||
|
/// <inheritdoc cref="GetNative"/>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public T* GetNative<T>() where T : unmanaged => (T*)this.GetNative();
|
||||||
|
|
||||||
|
/// <summary>VTable of <see cref="ApiObject"/>.</summary>
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct VTable
|
||||||
|
{
|
||||||
|
/// <inheritdoc cref="ApiObject.GetNative"/>
|
||||||
|
public delegate* unmanaged<ApiObject*, nint> GetNative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly struct DelegateStorage<T> where T : Delegate
|
||||||
|
{
|
||||||
|
[UsedImplicitly]
|
||||||
|
public readonly T Delegate;
|
||||||
|
|
||||||
|
public readonly void* Address;
|
||||||
|
|
||||||
|
public DelegateStorage(T @delegate)
|
||||||
|
{
|
||||||
|
this.Delegate = @delegate;
|
||||||
|
this.Address = (void*)Marshal.GetFunctionPointerForDelegate(@delegate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static implicit operator void*(DelegateStorage<T> sto) => sto.Address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
namespace Dalamud.Interface.Internal.ReShadeHandling;
|
||||||
|
|
||||||
|
/// <summary>Available handling modes for working with ReShade.</summary>
|
||||||
|
internal enum ReShadeHandlingMode
|
||||||
|
{
|
||||||
|
/// <summary>Use the default method, whatever it is for the current Dalamud version.</summary>
|
||||||
|
Default = 0,
|
||||||
|
|
||||||
|
/// <summary>Unwrap ReShade from the swap chain obtained from the game.</summary>
|
||||||
|
UnwrapReShade,
|
||||||
|
|
||||||
|
/// <summary>Register as a ReShade addon, and draw on <see cref="ReShadeAddonInterface.AddonEvent.Present"/> event.
|
||||||
|
/// </summary>
|
||||||
|
ReShadeAddonPresent,
|
||||||
|
|
||||||
|
/// <summary>Register as a ReShade addon, and draw on <see cref="ReShadeAddonInterface.AddonEvent.ReShadeOverlay"/>
|
||||||
|
/// event. </summary>
|
||||||
|
ReShadeAddonReShadeOverlay,
|
||||||
|
|
||||||
|
/// <summary>Hook <c>DXGISwapChain::on_present(UINT flags, const DXGI_PRESENT_PARAMETERS *params)</c> in
|
||||||
|
/// <c>dxgi_swapchain.cpp</c>.</summary>
|
||||||
|
HookReShadeDxgiSwapChainOnPresent,
|
||||||
|
|
||||||
|
/// <summary>Do not do anything special about it. ReShade will process Dalamud rendered stuff.</summary>
|
||||||
|
None = -1,
|
||||||
|
}
|
||||||
194
Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs
Normal file
194
Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
using TerraFX.Interop.Windows;
|
||||||
|
|
||||||
|
using static TerraFX.Interop.Windows.Windows;
|
||||||
|
|
||||||
|
namespace Dalamud.Interface.Internal.ReShadeHandling;
|
||||||
|
|
||||||
|
/// <summary>Unwraps IUnknown wrapped by ReShade.</summary>
|
||||||
|
internal static unsafe class ReShadeUnwrapper
|
||||||
|
{
|
||||||
|
/// <summary>Unwraps <typeparamref name="T"/> if it is wrapped by ReShade.</summary>
|
||||||
|
/// <param name="comptr">[inout] The COM pointer to an instance of <typeparamref name="T"/>.</param>
|
||||||
|
/// <typeparam name="T">A COM type that is or extends <see cref="IUnknown"/>.</typeparam>
|
||||||
|
/// <returns><c>true</c> if peeled.</returns>
|
||||||
|
public static bool Unwrap<T>(ComPtr<T>* comptr)
|
||||||
|
where T : unmanaged, IUnknown.Interface
|
||||||
|
{
|
||||||
|
if (typeof(T).GetNestedType("Vtbl`1") is not { } vtblType)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
nint vtblSize = vtblType.GetFields().Length * sizeof(nint);
|
||||||
|
var changed = false;
|
||||||
|
while (comptr->Get() != null && IsReShadedComObject(comptr->Get()))
|
||||||
|
{
|
||||||
|
// Expectation: the pointer to the underlying object should come early after the overriden vtable.
|
||||||
|
for (nint i = sizeof(nint); i <= 0x20; i += sizeof(nint))
|
||||||
|
{
|
||||||
|
var ppObjectBehind = (nint)comptr->Get() + i;
|
||||||
|
|
||||||
|
// Is the thing directly pointed from the address an actual something in the memory?
|
||||||
|
if (!IsValidReadableMemoryAddress(ppObjectBehind, 8))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var pObjectBehind = *(nint*)ppObjectBehind;
|
||||||
|
|
||||||
|
// Is the address of vtable readable?
|
||||||
|
if (!IsValidReadableMemoryAddress(pObjectBehind, sizeof(nint)))
|
||||||
|
continue;
|
||||||
|
var pObjectBehindVtbl = *(nint*)pObjectBehind;
|
||||||
|
|
||||||
|
// Is the vtable itself readable?
|
||||||
|
if (!IsValidReadableMemoryAddress(pObjectBehindVtbl, vtblSize))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Are individual functions in vtable executable?
|
||||||
|
var valid = true;
|
||||||
|
for (var j = 0; valid && j < vtblSize; j += sizeof(nint))
|
||||||
|
valid &= IsValidExecutableMemoryAddress(*(nint*)(pObjectBehindVtbl + j), 1);
|
||||||
|
if (!valid)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Interpret the object as an IUnknown.
|
||||||
|
// Note that `using` is not used, and `Attach` is used. We do not alter the reference count yet.
|
||||||
|
var punk = default(ComPtr<IUnknown>);
|
||||||
|
punk.Attach((IUnknown*)pObjectBehind);
|
||||||
|
|
||||||
|
// Is the IUnknown object also the type we want?
|
||||||
|
using var comptr2 = default(ComPtr<T>);
|
||||||
|
if (punk.As(&comptr2).FAILED)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
comptr2.Swap(comptr);
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool BelongsInReShadeDll(nint ptr)
|
||||||
|
{
|
||||||
|
foreach (ProcessModule processModule in Process.GetCurrentProcess().Modules)
|
||||||
|
{
|
||||||
|
if (ptr < processModule.BaseAddress ||
|
||||||
|
ptr >= processModule.BaseAddress + processModule.ModuleMemorySize ||
|
||||||
|
!HasProcExported(processModule, "ReShadeRegisterAddon"u8) ||
|
||||||
|
!HasProcExported(processModule, "ReShadeUnregisterAddon"u8) ||
|
||||||
|
!HasProcExported(processModule, "ReShadeRegisterEvent"u8) ||
|
||||||
|
!HasProcExported(processModule, "ReShadeUnregisterEvent"u8))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
static bool HasProcExported(ProcessModule m, ReadOnlySpan<byte> name)
|
||||||
|
{
|
||||||
|
fixed (byte* p = name)
|
||||||
|
return GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)p) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsReShadedComObject<T>(T* obj)
|
||||||
|
where T : unmanaged, IUnknown.Interface
|
||||||
|
{
|
||||||
|
if (!IsValidReadableMemoryAddress((nint)obj, sizeof(nint)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var vtbl = (nint**)Marshal.ReadIntPtr((nint)obj);
|
||||||
|
if (!IsValidReadableMemoryAddress((nint)vtbl, sizeof(nint) * 3))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (var i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
var pfn = Marshal.ReadIntPtr((nint)(vtbl + i));
|
||||||
|
if (!IsValidExecutableMemoryAddress(pfn, 1))
|
||||||
|
return false;
|
||||||
|
if (!BelongsInReShadeDll(pfn))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidReadableMemoryAddress(nint p, nint size)
|
||||||
|
{
|
||||||
|
while (size > 0)
|
||||||
|
{
|
||||||
|
if (!IsValidUserspaceMemoryAddress(p))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
MEMORY_BASIC_INFORMATION mbi;
|
||||||
|
if (VirtualQuery((void*)p, &mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION)) == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (mbi is not
|
||||||
|
{
|
||||||
|
State: MEM.MEM_COMMIT,
|
||||||
|
Protect: PAGE.PAGE_READONLY or PAGE.PAGE_READWRITE or PAGE.PAGE_EXECUTE_READ
|
||||||
|
or PAGE.PAGE_EXECUTE_READWRITE,
|
||||||
|
})
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var regionSize = (nint)((mbi.RegionSize + 0xFFFUL) & ~0x1000UL);
|
||||||
|
var checkedSize = ((nint)mbi.BaseAddress + regionSize) - p;
|
||||||
|
size -= checkedSize;
|
||||||
|
p += checkedSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidExecutableMemoryAddress(nint p, nint size)
|
||||||
|
{
|
||||||
|
while (size > 0)
|
||||||
|
{
|
||||||
|
if (!IsValidUserspaceMemoryAddress(p))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
MEMORY_BASIC_INFORMATION mbi;
|
||||||
|
if (VirtualQuery((void*)p, &mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION)) == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (mbi is not
|
||||||
|
{
|
||||||
|
State: MEM.MEM_COMMIT,
|
||||||
|
Protect: PAGE.PAGE_EXECUTE or PAGE.PAGE_EXECUTE_READ or PAGE.PAGE_EXECUTE_READWRITE
|
||||||
|
or PAGE.PAGE_EXECUTE_WRITECOPY,
|
||||||
|
})
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var regionSize = (nint)((mbi.RegionSize + 0xFFFUL) & ~0x1000UL);
|
||||||
|
var checkedSize = ((nint)mbi.BaseAddress + regionSize) - p;
|
||||||
|
size -= checkedSize;
|
||||||
|
p += checkedSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool IsValidUserspaceMemoryAddress(nint p)
|
||||||
|
{
|
||||||
|
// https://learn.microsoft.com/en-us/windows-hardware/drivers/gettingstarted/virtual-address-spaces
|
||||||
|
// A 64-bit process on 64-bit Windows has a virtual address space within the 128-terabyte range
|
||||||
|
// 0x000'00000000 through 0x7FFF'FFFFFFFF.
|
||||||
|
return p >= 0x10000 && p <= unchecked((nint)0x7FFF_FFFFFFFFUL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,38 @@
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
|
using Dalamud.Interface.Internal.ReShadeHandling;
|
||||||
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
||||||
|
|
||||||
using TerraFX.Interop.DirectX;
|
using TerraFX.Interop.DirectX;
|
||||||
|
using TerraFX.Interop.Windows;
|
||||||
|
|
||||||
namespace Dalamud.Interface.Internal;
|
namespace Dalamud.Interface.Internal;
|
||||||
|
|
||||||
/// <summary>Helper for dealing with swap chains.</summary>
|
/// <summary>Helper for dealing with swap chains.</summary>
|
||||||
internal static unsafe class SwapChainHelper
|
internal static unsafe class SwapChainHelper
|
||||||
{
|
{
|
||||||
|
private static IDXGISwapChain* foundGameDeviceSwapChain;
|
||||||
|
|
||||||
|
/// <summary>Describes how to hook <see cref="IDXGISwapChain"/> methods.</summary>
|
||||||
|
public enum HookMode
|
||||||
|
{
|
||||||
|
/// <summary>Hooks by rewriting the native bytecode.</summary>
|
||||||
|
ByteCode,
|
||||||
|
|
||||||
|
/// <summary>Hooks by providing an alternative vtable.</summary>
|
||||||
|
VTable,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Gets the game's active instance of IDXGISwapChain that is initialized.</summary>
|
/// <summary>Gets the game's active instance of IDXGISwapChain that is initialized.</summary>
|
||||||
/// <value>Address of the game's instance of IDXGISwapChain, or <c>null</c> if not available (yet.)</value>
|
/// <value>Address of the game's instance of IDXGISwapChain, or <c>null</c> if not available (yet.)</value>
|
||||||
public static IDXGISwapChain* GameDeviceSwapChain
|
public static IDXGISwapChain* GameDeviceSwapChain
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
if (foundGameDeviceSwapChain is not null)
|
||||||
|
return foundGameDeviceSwapChain;
|
||||||
|
|
||||||
var kernelDev = Device.Instance();
|
var kernelDev = Device.Instance();
|
||||||
if (kernelDev == null)
|
if (kernelDev == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -28,7 +46,7 @@ internal static unsafe class SwapChainHelper
|
||||||
if (swapChain->BackBuffer == null)
|
if (swapChain->BackBuffer == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return (IDXGISwapChain*)swapChain->DXGISwapChain;
|
return foundGameDeviceSwapChain = (IDXGISwapChain*)swapChain->DXGISwapChain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,10 +60,57 @@ internal static unsafe class SwapChainHelper
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IsGameDeviceSwapChain{T}"/>
|
||||||
|
public static bool IsGameDeviceSwapChain(nint punk) => IsGameDeviceSwapChain((IUnknown*)punk);
|
||||||
|
|
||||||
|
/// <summary>Determines if the given instance of IUnknown is the game device's swap chain.</summary>
|
||||||
|
/// <param name="punk">Object to check.</param>
|
||||||
|
/// <typeparam name="T">Type of the object to check.</typeparam>
|
||||||
|
/// <returns><c>true</c> if the object is the game's swap chain.</returns>
|
||||||
|
public static bool IsGameDeviceSwapChain<T>(T* punk) where T : unmanaged, IUnknown.Interface
|
||||||
|
{
|
||||||
|
using var psc = default(ComPtr<IDXGISwapChain>);
|
||||||
|
fixed (Guid* piid = &IID.IID_IDXGISwapChain)
|
||||||
|
{
|
||||||
|
if (punk->QueryInterface(piid, (void**)psc.GetAddressOf()).FAILED)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return IsGameDeviceSwapChain(psc.Get());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IsGameDeviceSwapChain{T}"/>
|
||||||
|
public static bool IsGameDeviceSwapChain(IDXGISwapChain* punk)
|
||||||
|
{
|
||||||
|
DXGI_SWAP_CHAIN_DESC desc1;
|
||||||
|
if (punk->GetDesc(&desc1).FAILED)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
DXGI_SWAP_CHAIN_DESC desc2;
|
||||||
|
if (GameDeviceSwapChain->GetDesc(&desc2).FAILED)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return desc1.OutputWindow == desc2.OutputWindow;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Wait for the game to have finished initializing the IDXGISwapChain.</summary>
|
/// <summary>Wait for the game to have finished initializing the IDXGISwapChain.</summary>
|
||||||
public static void BusyWaitForGameDeviceSwapChain()
|
public static void BusyWaitForGameDeviceSwapChain()
|
||||||
{
|
{
|
||||||
while (GameDeviceSwapChain is null)
|
while (GameDeviceSwapChain is null)
|
||||||
Thread.Yield();
|
Thread.Yield();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Make <see cref="GameDeviceSwapChain"/> store address of unwrapped swap chain, if it was wrapped with ReShade.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns><c>true</c> if it was wrapped with ReShade.</returns>
|
||||||
|
public static bool UnwrapReShade()
|
||||||
|
{
|
||||||
|
using var swapChain = new ComPtr<IDXGISwapChain>(GameDeviceSwapChain);
|
||||||
|
if (!ReShadeUnwrapper.Unwrap(&swapChain))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
foundGameDeviceSwapChain = swapChain.Get();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,7 @@ internal unsafe class UiDebug
|
||||||
case NodeType.Image: Util.ShowStruct(*(AtkImageNode*)node, (ulong)node); break;
|
case NodeType.Image: Util.ShowStruct(*(AtkImageNode*)node, (ulong)node); break;
|
||||||
case NodeType.Collision: Util.ShowStruct(*(AtkCollisionNode*)node, (ulong)node); break;
|
case NodeType.Collision: Util.ShowStruct(*(AtkCollisionNode*)node, (ulong)node); break;
|
||||||
case NodeType.NineGrid: Util.ShowStruct(*(AtkNineGridNode*)node, (ulong)node); break;
|
case NodeType.NineGrid: Util.ShowStruct(*(AtkNineGridNode*)node, (ulong)node); break;
|
||||||
|
case NodeType.ClippingMask: Util.ShowStruct(*(AtkClippingMaskNode*)node, (ulong)node); break;
|
||||||
case NodeType.Counter: Util.ShowStruct(*(AtkCounterNode*)node, (ulong)node); break;
|
case NodeType.Counter: Util.ShowStruct(*(AtkCounterNode*)node, (ulong)node); break;
|
||||||
default: Util.ShowStruct(*node, (ulong)node); break;
|
default: Util.ShowStruct(*node, (ulong)node); break;
|
||||||
}
|
}
|
||||||
|
|
@ -233,48 +234,15 @@ internal unsafe class UiDebug
|
||||||
break;
|
break;
|
||||||
case NodeType.Image:
|
case NodeType.Image:
|
||||||
var imageNode = (AtkImageNode*)node;
|
var imageNode = (AtkImageNode*)node;
|
||||||
if (imageNode->PartsList != null)
|
PrintTextureInfo(imageNode->PartsList, imageNode->PartId);
|
||||||
{
|
break;
|
||||||
if (imageNode->PartId > imageNode->PartsList->PartCount)
|
case NodeType.NineGrid:
|
||||||
{
|
var ngNode = (AtkNineGridNode*)node;
|
||||||
ImGui.Text("part id > part count?");
|
PrintTextureInfo(ngNode->PartsList, ngNode->PartId);
|
||||||
}
|
break;
|
||||||
else
|
case NodeType.ClippingMask:
|
||||||
{
|
var cmNode = (AtkClippingMaskNode*)node;
|
||||||
var textureInfo = imageNode->PartsList->Parts[imageNode->PartId].UldAsset;
|
PrintTextureInfo(cmNode->PartsList, cmNode->PartId);
|
||||||
var texType = textureInfo->AtkTexture.TextureType;
|
|
||||||
ImGui.Text($"texture type: {texType} part_id={imageNode->PartId} part_id_count={imageNode->PartsList->PartCount}");
|
|
||||||
if (texType == TextureType.Resource)
|
|
||||||
{
|
|
||||||
var texFileNameStdString = &textureInfo->AtkTexture.Resource->TexFileResourceHandle->ResourceHandle.FileName;
|
|
||||||
var texString = texFileNameStdString->Length < 16
|
|
||||||
? MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->Buffer)
|
|
||||||
: MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->BufferPtr);
|
|
||||||
|
|
||||||
ImGui.Text($"texture path: {texString}");
|
|
||||||
var kernelTexture = textureInfo->AtkTexture.Resource->KernelTextureObject;
|
|
||||||
|
|
||||||
if (ImGui.TreeNode($"Texture##{(ulong)kernelTexture->D3D11ShaderResourceView:X}"))
|
|
||||||
{
|
|
||||||
ImGui.Image(new IntPtr(kernelTexture->D3D11ShaderResourceView), new Vector2(kernelTexture->Width, kernelTexture->Height));
|
|
||||||
ImGui.TreePop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (texType == TextureType.KernelTexture)
|
|
||||||
{
|
|
||||||
if (ImGui.TreeNode($"Texture##{(ulong)textureInfo->AtkTexture.KernelTexture->D3D11ShaderResourceView:X}"))
|
|
||||||
{
|
|
||||||
ImGui.Image(new IntPtr(textureInfo->AtkTexture.KernelTexture->D3D11ShaderResourceView), new Vector2(textureInfo->AtkTexture.KernelTexture->Width, textureInfo->AtkTexture.KernelTexture->Height));
|
|
||||||
ImGui.TreePop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.Text("no texture loaded");
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -287,6 +255,60 @@ internal unsafe class UiDebug
|
||||||
|
|
||||||
if (isVisible && !popped)
|
if (isVisible && !popped)
|
||||||
ImGui.PopStyleColor();
|
ImGui.PopStyleColor();
|
||||||
|
|
||||||
|
static void PrintTextureInfo(AtkUldPartsList* partsList, uint partId)
|
||||||
|
{
|
||||||
|
if (partsList != null)
|
||||||
|
{
|
||||||
|
if (partId > partsList->PartCount)
|
||||||
|
{
|
||||||
|
ImGui.Text("part id > part count?");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var textureInfo = partsList->Parts[partId].UldAsset;
|
||||||
|
var texType = textureInfo->AtkTexture.TextureType;
|
||||||
|
ImGui.Text(
|
||||||
|
$"texture type: {texType} part_id={partId} part_id_count={partsList->PartCount}");
|
||||||
|
if (texType == TextureType.Resource)
|
||||||
|
{
|
||||||
|
var texFileNameStdString =
|
||||||
|
&textureInfo->AtkTexture.Resource->TexFileResourceHandle->ResourceHandle.FileName;
|
||||||
|
var texString = texFileNameStdString->Length < 16
|
||||||
|
? MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->Buffer)
|
||||||
|
: MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->BufferPtr);
|
||||||
|
|
||||||
|
ImGui.Text($"texture path: {texString}");
|
||||||
|
var kernelTexture = textureInfo->AtkTexture.Resource->KernelTextureObject;
|
||||||
|
|
||||||
|
if (ImGui.TreeNode($"Texture##{(ulong)kernelTexture->D3D11ShaderResourceView:X}"))
|
||||||
|
{
|
||||||
|
ImGui.Image(
|
||||||
|
new IntPtr(kernelTexture->D3D11ShaderResourceView),
|
||||||
|
new Vector2(kernelTexture->Width, kernelTexture->Height));
|
||||||
|
ImGui.TreePop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (texType == TextureType.KernelTexture)
|
||||||
|
{
|
||||||
|
if (ImGui.TreeNode(
|
||||||
|
$"Texture##{(ulong)textureInfo->AtkTexture.KernelTexture->D3D11ShaderResourceView:X}"))
|
||||||
|
{
|
||||||
|
ImGui.Image(
|
||||||
|
new IntPtr(textureInfo->AtkTexture.KernelTexture->D3D11ShaderResourceView),
|
||||||
|
new Vector2(
|
||||||
|
textureInfo->AtkTexture.KernelTexture->Width,
|
||||||
|
textureInfo->AtkTexture.KernelTexture->Height));
|
||||||
|
ImGui.TreePop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.Text("no texture loaded");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PrintComponentNode(AtkResNode* node, string treePrefix)
|
private void PrintComponentNode(AtkResNode* node, string treePrefix)
|
||||||
|
|
@ -541,11 +563,13 @@ internal unsafe class UiDebug
|
||||||
private Vector2 GetNodePosition(AtkResNode* node)
|
private Vector2 GetNodePosition(AtkResNode* node)
|
||||||
{
|
{
|
||||||
var pos = new Vector2(node->X, node->Y);
|
var pos = new Vector2(node->X, node->Y);
|
||||||
|
pos -= new Vector2(node->OriginX * (node->ScaleX - 1), node->OriginY * (node->ScaleY - 1));
|
||||||
var par = node->ParentNode;
|
var par = node->ParentNode;
|
||||||
while (par != null)
|
while (par != null)
|
||||||
{
|
{
|
||||||
pos *= new Vector2(par->ScaleX, par->ScaleY);
|
pos *= new Vector2(par->ScaleX, par->ScaleY);
|
||||||
pos += new Vector2(par->X, par->Y);
|
pos += new Vector2(par->X, par->Y);
|
||||||
|
pos -= new Vector2(par->OriginX * (par->ScaleX - 1), par->OriginY * (par->ScaleY - 1));
|
||||||
par = par->ParentNode;
|
par = par->ParentNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -360,7 +360,7 @@ internal sealed class ChangelogWindow : Window, IDisposable
|
||||||
{
|
{
|
||||||
case State.WindowFadeIn:
|
case State.WindowFadeIn:
|
||||||
case State.ExplainerIntro:
|
case State.ExplainerIntro:
|
||||||
ImGui.TextWrapped($"Welcome to Dalamud v{Util.AssemblyVersion}!");
|
ImGui.TextWrapped($"Welcome to Dalamud v{Util.GetScmVersion()}!");
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
ImGui.TextWrapped(ChangeLog);
|
ImGui.TextWrapped(ChangeLog);
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ using Dalamud.Interface.ImGuiNotification.Internal;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
using Dalamud.Logging.Internal;
|
|
||||||
using Dalamud.Plugin.Internal;
|
using Dalamud.Plugin.Internal;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
|
|
@ -39,9 +38,6 @@ internal class ConsoleWindow : Window, IDisposable
|
||||||
private const int LogLinesMaximum = 1000000;
|
private const int LogLinesMaximum = 1000000;
|
||||||
private const int HistorySize = 50;
|
private const int HistorySize = 50;
|
||||||
|
|
||||||
// Only this field may be touched from any thread.
|
|
||||||
private readonly ConcurrentQueue<(string Line, LogEvent LogEvent)> newLogEntries;
|
|
||||||
|
|
||||||
// Fields below should be touched only from the main thread.
|
// Fields below should be touched only from the main thread.
|
||||||
private readonly RollingList<LogEntry> logText;
|
private readonly RollingList<LogEntry> logText;
|
||||||
private readonly RollingList<LogEntry> filteredLogEntries;
|
private readonly RollingList<LogEntry> filteredLogEntries;
|
||||||
|
|
@ -94,7 +90,6 @@ internal class ConsoleWindow : Window, IDisposable
|
||||||
|
|
||||||
this.autoScroll = configuration.LogAutoScroll;
|
this.autoScroll = configuration.LogAutoScroll;
|
||||||
this.autoOpen = configuration.LogOpenAtStartup;
|
this.autoOpen = configuration.LogOpenAtStartup;
|
||||||
SerilogEventSink.Instance.LogLine += this.OnLogLine;
|
|
||||||
|
|
||||||
Service<Framework>.GetAsync().ContinueWith(r => r.Result.Update += this.FrameworkOnUpdate);
|
Service<Framework>.GetAsync().ContinueWith(r => r.Result.Update += this.FrameworkOnUpdate);
|
||||||
|
|
||||||
|
|
@ -114,7 +109,6 @@ internal class ConsoleWindow : Window, IDisposable
|
||||||
this.logLinesLimit = configuration.LogLinesLimit;
|
this.logLinesLimit = configuration.LogLinesLimit;
|
||||||
|
|
||||||
var limit = Math.Max(LogLinesMinimum, this.logLinesLimit);
|
var limit = Math.Max(LogLinesMinimum, this.logLinesLimit);
|
||||||
this.newLogEntries = new();
|
|
||||||
this.logText = new(limit);
|
this.logText = new(limit);
|
||||||
this.filteredLogEntries = new(limit);
|
this.filteredLogEntries = new(limit);
|
||||||
|
|
||||||
|
|
@ -126,6 +120,9 @@ internal class ConsoleWindow : Window, IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets the queue where log entries that are not processed yet are stored.</summary>
|
||||||
|
public static ConcurrentQueue<(string Line, LogEvent LogEvent)> NewLogEntries { get; } = new();
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public override void OnOpen()
|
public override void OnOpen()
|
||||||
{
|
{
|
||||||
|
|
@ -136,7 +133,6 @@ internal class ConsoleWindow : Window, IDisposable
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
SerilogEventSink.Instance.LogLine -= this.OnLogLine;
|
|
||||||
this.configuration.DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved;
|
this.configuration.DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved;
|
||||||
if (Service<Framework>.GetNullable() is { } framework)
|
if (Service<Framework>.GetNullable() is { } framework)
|
||||||
framework.Update -= this.FrameworkOnUpdate;
|
framework.Update -= this.FrameworkOnUpdate;
|
||||||
|
|
@ -324,7 +320,7 @@ internal class ConsoleWindow : Window, IDisposable
|
||||||
ImGuiInputTextFlags.CallbackHistory | ImGuiInputTextFlags.CallbackEdit,
|
ImGuiInputTextFlags.CallbackHistory | ImGuiInputTextFlags.CallbackEdit,
|
||||||
this.CommandInputCallback))
|
this.CommandInputCallback))
|
||||||
{
|
{
|
||||||
this.newLogEntries.Enqueue((this.commandText, new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, new MessageTemplate(string.Empty, []), [])));
|
NewLogEntries.Enqueue((this.commandText, new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, new MessageTemplate(string.Empty, []), [])));
|
||||||
this.ProcessCommand();
|
this.ProcessCommand();
|
||||||
getFocus = true;
|
getFocus = true;
|
||||||
}
|
}
|
||||||
|
|
@ -372,7 +368,7 @@ internal class ConsoleWindow : Window, IDisposable
|
||||||
this.pendingClearLog = false;
|
this.pendingClearLog = false;
|
||||||
this.logText.Clear();
|
this.logText.Clear();
|
||||||
this.filteredLogEntries.Clear();
|
this.filteredLogEntries.Clear();
|
||||||
this.newLogEntries.Clear();
|
NewLogEntries.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.pendingRefilter)
|
if (this.pendingRefilter)
|
||||||
|
|
@ -388,7 +384,7 @@ internal class ConsoleWindow : Window, IDisposable
|
||||||
|
|
||||||
var numPrevFilteredLogEntries = this.filteredLogEntries.Count;
|
var numPrevFilteredLogEntries = this.filteredLogEntries.Count;
|
||||||
var addedLines = 0;
|
var addedLines = 0;
|
||||||
while (this.newLogEntries.TryDequeue(out var logLine))
|
while (NewLogEntries.TryDequeue(out var logLine))
|
||||||
addedLines += this.HandleLogLine(logLine.Line, logLine.LogEvent);
|
addedLines += this.HandleLogLine(logLine.Line, logLine.LogEvent);
|
||||||
this.newRolledLines = addedLines - (this.filteredLogEntries.Count - numPrevFilteredLogEntries);
|
this.newRolledLines = addedLines - (this.filteredLogEntries.Count - numPrevFilteredLogEntries);
|
||||||
}
|
}
|
||||||
|
|
@ -1062,11 +1058,6 @@ internal class ConsoleWindow : Window, IDisposable
|
||||||
/// <summary>Queues filtering the log entries again, before next call to <see cref="Draw"/>.</summary>
|
/// <summary>Queues filtering the log entries again, before next call to <see cref="Draw"/>.</summary>
|
||||||
private void QueueRefilter() => this.pendingRefilter = true;
|
private void QueueRefilter() => this.pendingRefilter = true;
|
||||||
|
|
||||||
/// <summary>Enqueues the new log line to the log-to-be-processed queue.</summary>
|
|
||||||
/// <remarks>See <see cref="FrameworkOnUpdate"/> for the handler for the queued log entries.</remarks>
|
|
||||||
private void OnLogLine(object sender, (string Line, LogEvent LogEvent) logEvent) =>
|
|
||||||
this.newLogEntries.Enqueue(logEvent);
|
|
||||||
|
|
||||||
private bool DrawToggleButtonWithTooltip(
|
private bool DrawToggleButtonWithTooltip(
|
||||||
string buttonId, string tooltip, FontAwesomeIcon icon, ref bool enabledState)
|
string buttonId, string tooltip, FontAwesomeIcon icon, ref bool enabledState)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -315,7 +315,7 @@ internal class PluginImageCache : IInternalDisposableService
|
||||||
|
|
||||||
private Task<T> RunInDownloadQueue<T>(Func<Task<T>> func, ulong requestedFrame)
|
private Task<T> RunInDownloadQueue<T>(Func<Task<T>> func, ulong requestedFrame)
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<T>();
|
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
this.downloadQueue.Add(Tuple.Create(requestedFrame, async () =>
|
this.downloadQueue.Add(Tuple.Create(requestedFrame, async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -332,7 +332,7 @@ internal class PluginImageCache : IInternalDisposableService
|
||||||
|
|
||||||
private Task<T> RunInLoadQueue<T>(Func<Task<T>> func)
|
private Task<T> RunInLoadQueue<T>(Func<Task<T>> func)
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<T>();
|
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
this.loadQueue.Add(async () =>
|
this.loadQueue.Add(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,8 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
private List<LocalPlugin> pluginListInstalled = new();
|
private List<LocalPlugin> pluginListInstalled = new();
|
||||||
private List<AvailablePluginUpdate> pluginListUpdatable = new();
|
private List<AvailablePluginUpdate> pluginListUpdatable = new();
|
||||||
private bool hasDevPlugins = false;
|
private bool hasDevPlugins = false;
|
||||||
|
private bool hasHiddenPlugins = false;
|
||||||
|
|
||||||
private string searchText = string.Empty;
|
private string searchText = string.Empty;
|
||||||
private bool isSearchTextPrefilled = false;
|
private bool isSearchTextPrefilled = false;
|
||||||
|
|
||||||
|
|
@ -304,7 +305,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var versionInfo = t.Result;
|
var versionInfo = t.Result;
|
||||||
if (versionInfo.AssemblyVersion != Util.GetGitHash() &&
|
if (versionInfo.AssemblyVersion != Util.GetScmVersion() &&
|
||||||
versionInfo.Track != "release" &&
|
versionInfo.Track != "release" &&
|
||||||
string.Equals(versionInfo.Key, config.DalamudBetaKey, StringComparison.OrdinalIgnoreCase))
|
string.Equals(versionInfo.Key, config.DalamudBetaKey, StringComparison.OrdinalIgnoreCase))
|
||||||
this.staleDalamudNewVersion = versionInfo.AssemblyVersion;
|
this.staleDalamudNewVersion = versionInfo.AssemblyVersion;
|
||||||
|
|
@ -1277,6 +1278,19 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
proxies.Add(new PluginInstallerAvailablePluginProxy(null, installedPlugin));
|
proxies.Add(new PluginInstallerAvailablePluginProxy(null, installedPlugin));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var configuration = Service<DalamudConfiguration>.Get();
|
||||||
|
bool IsProxyHidden(PluginInstallerAvailablePluginProxy proxy)
|
||||||
|
{
|
||||||
|
var isHidden =
|
||||||
|
configuration.HiddenPluginInternalName.Contains(proxy.RemoteManifest?.InternalName);
|
||||||
|
if (this.categoryManager.CurrentCategoryKind == PluginCategoryManager.CategoryKind.Hidden)
|
||||||
|
return isHidden;
|
||||||
|
return !isHidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out plugins that are not hidden
|
||||||
|
proxies = proxies.Where(IsProxyHidden).ToList();
|
||||||
|
|
||||||
return proxies;
|
return proxies;
|
||||||
}
|
}
|
||||||
#pragma warning restore SA1201
|
#pragma warning restore SA1201
|
||||||
|
|
@ -1305,6 +1319,12 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
|
|
||||||
ImGui.PopID();
|
ImGui.PopID();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset the category to "All" if we're on the "Hidden" category and there are no hidden plugins (we removed the last one)
|
||||||
|
if (i == 0 && this.categoryManager.CurrentCategoryKind == PluginCategoryManager.CategoryKind.Hidden)
|
||||||
|
{
|
||||||
|
this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawInstalledPluginList(InstalledPluginListFilter filter)
|
private void DrawInstalledPluginList(InstalledPluginListFilter filter)
|
||||||
|
|
@ -1471,6 +1491,10 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
if (!Service<DalamudConfiguration>.Get().DoPluginTest)
|
if (!Service<DalamudConfiguration>.Get().DoPluginTest)
|
||||||
continue;
|
continue;
|
||||||
break;
|
break;
|
||||||
|
case PluginCategoryManager.CategoryInfo.AppearCondition.AnyHiddenPlugins:
|
||||||
|
if (!this.hasHiddenPlugins)
|
||||||
|
continue;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException();
|
throw new ArgumentOutOfRangeException();
|
||||||
}
|
}
|
||||||
|
|
@ -1540,7 +1564,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
DrawWarningIcon();
|
DrawWarningIcon();
|
||||||
DrawLinesCentered("A new version of Dalamud is available.\n" +
|
DrawLinesCentered("A new version of Dalamud is available.\n" +
|
||||||
"Please restart the game to ensure compatibility with updated plugins.\n" +
|
"Please restart the game to ensure compatibility with updated plugins.\n" +
|
||||||
$"old: {Util.GetGitHash()} new: {this.staleDalamudNewVersion}");
|
$"old: {Util.GetScmVersion()} new: {this.staleDalamudNewVersion}");
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(10);
|
ImGuiHelpers.ScaledDummy(10);
|
||||||
}
|
}
|
||||||
|
|
@ -2276,6 +2300,16 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
ImGui.TextColored(ImGuiColors.DalamudGrey3, Locs.PluginBody_AuthorWithoutDownloadCount(log.Author));
|
ImGui.TextColored(ImGuiColors.DalamudGrey3, Locs.PluginBody_AuthorWithoutDownloadCount(log.Author));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (log.Date != DateTime.MinValue)
|
||||||
|
{
|
||||||
|
var whenText = log.Date.LocRelativePastLong();
|
||||||
|
var whenSize = ImGui.CalcTextSize(whenText);
|
||||||
|
ImGui.SameLine(ImGui.GetWindowWidth() - whenSize.X - (25 * ImGuiHelpers.GlobalScale));
|
||||||
|
ImGui.TextColored(ImGuiColors.DalamudGrey3, whenText);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Published on " + log.Date.LocAbsolute());
|
||||||
|
}
|
||||||
|
|
||||||
cursor.Y += ImGui.GetTextLineHeightWithSpacing();
|
cursor.Y += ImGui.GetTextLineHeightWithSpacing();
|
||||||
ImGui.SetCursorPos(cursor);
|
ImGui.SetCursorPos(cursor);
|
||||||
|
|
||||||
|
|
@ -2446,12 +2480,19 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
pluginManager.RefilterPluginMasters();
|
pluginManager.RefilterPluginMasters();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui.Selectable(Locs.PluginContext_HidePlugin))
|
var isHidden = configuration.HiddenPluginInternalName.Contains(manifest.InternalName);
|
||||||
|
switch (isHidden)
|
||||||
{
|
{
|
||||||
Log.Debug($"Adding {manifest.InternalName} to hidden plugins");
|
case false when ImGui.Selectable(Locs.PluginContext_HidePlugin):
|
||||||
configuration.HiddenPluginInternalName.Add(manifest.InternalName);
|
configuration.HiddenPluginInternalName.Add(manifest.InternalName);
|
||||||
configuration.QueueSave();
|
configuration.QueueSave();
|
||||||
pluginManager.RefilterPluginMasters();
|
pluginManager.RefilterPluginMasters();
|
||||||
|
break;
|
||||||
|
case true when ImGui.Selectable(Locs.PluginContext_UnhidePlugin):
|
||||||
|
configuration.HiddenPluginInternalName.Remove(manifest.InternalName);
|
||||||
|
configuration.QueueSave();
|
||||||
|
pluginManager.RefilterPluginMasters();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ImGui.Selectable(Locs.PluginContext_DeletePluginConfig))
|
if (ImGui.Selectable(Locs.PluginContext_DeletePluginConfig))
|
||||||
|
|
@ -2614,7 +2655,24 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
|
|
||||||
var applicableChangelog = plugin.IsTesting ? remoteManifest?.Changelog : remoteManifest?.TestingChangelog;
|
var applicableChangelog = plugin.IsTesting ? remoteManifest?.Changelog : remoteManifest?.TestingChangelog;
|
||||||
var hasChangelog = !applicableChangelog.IsNullOrWhitespace();
|
var hasChangelog = !applicableChangelog.IsNullOrWhitespace();
|
||||||
var didDrawChangelogInsideCollapsible = false;
|
var didDrawApplicableChangelogInsideCollapsible = false;
|
||||||
|
|
||||||
|
Version? availablePluginUpdateVersion = null;
|
||||||
|
string? availableChangelog = null;
|
||||||
|
var didDrawAvailableChangelogInsideCollapsible = false;
|
||||||
|
|
||||||
|
if (availablePluginUpdate != default)
|
||||||
|
{
|
||||||
|
availablePluginUpdateVersion =
|
||||||
|
availablePluginUpdate.UseTesting ?
|
||||||
|
availablePluginUpdate.UpdateManifest.TestingAssemblyVersion :
|
||||||
|
availablePluginUpdate.UpdateManifest.AssemblyVersion;
|
||||||
|
|
||||||
|
availableChangelog =
|
||||||
|
availablePluginUpdate.UseTesting ?
|
||||||
|
availablePluginUpdate.UpdateManifest.TestingChangelog :
|
||||||
|
availablePluginUpdate.UpdateManifest.Changelog;
|
||||||
|
}
|
||||||
|
|
||||||
var flags = PluginHeaderFlags.None;
|
var flags = PluginHeaderFlags.None;
|
||||||
if (plugin.IsThirdParty)
|
if (plugin.IsThirdParty)
|
||||||
|
|
@ -2748,28 +2806,33 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
{
|
{
|
||||||
if (ImGui.TreeNode(Locs.PluginBody_CurrentChangeLog(plugin.EffectiveVersion)))
|
if (ImGui.TreeNode(Locs.PluginBody_CurrentChangeLog(plugin.EffectiveVersion)))
|
||||||
{
|
{
|
||||||
didDrawChangelogInsideCollapsible = true;
|
didDrawApplicableChangelogInsideCollapsible = true;
|
||||||
this.DrawInstalledPluginChangelog(applicableChangelog);
|
this.DrawInstalledPluginChangelog(applicableChangelog);
|
||||||
ImGui.TreePop();
|
ImGui.TreePop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (availablePluginUpdate != default && !availablePluginUpdate.UpdateManifest.Changelog.IsNullOrWhitespace())
|
if (!availableChangelog.IsNullOrWhitespace() && ImGui.TreeNode(Locs.PluginBody_UpdateChangeLog(availablePluginUpdateVersion)))
|
||||||
{
|
{
|
||||||
var availablePluginUpdateVersion = availablePluginUpdate.UseTesting ? availablePluginUpdate.UpdateManifest.TestingAssemblyVersion : availablePluginUpdate.UpdateManifest.AssemblyVersion;
|
this.DrawInstalledPluginChangelog(availableChangelog);
|
||||||
var availableChangelog = availablePluginUpdate.UseTesting ? availablePluginUpdate.UpdateManifest.TestingChangelog : availablePluginUpdate.UpdateManifest.Changelog;
|
ImGui.TreePop();
|
||||||
if (!availableChangelog.IsNullOrWhitespace() && ImGui.TreeNode(Locs.PluginBody_UpdateChangeLog(availablePluginUpdateVersion)))
|
didDrawAvailableChangelogInsideCollapsible = true;
|
||||||
{
|
|
||||||
this.DrawInstalledPluginChangelog(availableChangelog);
|
|
||||||
ImGui.TreePop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (thisWasUpdated && hasChangelog && !didDrawChangelogInsideCollapsible)
|
if (thisWasUpdated &&
|
||||||
|
hasChangelog &&
|
||||||
|
!didDrawApplicableChangelogInsideCollapsible)
|
||||||
{
|
{
|
||||||
this.DrawInstalledPluginChangelog(applicableChangelog);
|
this.DrawInstalledPluginChangelog(applicableChangelog);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.categoryManager.CurrentCategoryKind == PluginCategoryManager.CategoryKind.UpdateablePlugins &&
|
||||||
|
!availableChangelog.IsNullOrWhitespace() &&
|
||||||
|
!didDrawAvailableChangelogInsideCollapsible)
|
||||||
|
{
|
||||||
|
this.DrawInstalledPluginChangelog(availableChangelog);
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.PopID();
|
ImGui.PopID();
|
||||||
}
|
}
|
||||||
|
|
@ -2826,6 +2889,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
configuration.QueueSave();
|
configuration.QueueSave();
|
||||||
|
_ = pluginManager.ReloadPluginMastersAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (repoManifest?.IsTestingExclusive == true)
|
if (repoManifest?.IsTestingExclusive == true)
|
||||||
|
|
@ -3606,6 +3670,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
private void OnAvailablePluginsChanged()
|
private void OnAvailablePluginsChanged()
|
||||||
{
|
{
|
||||||
var pluginManager = Service<PluginManager>.Get();
|
var pluginManager = Service<PluginManager>.Get();
|
||||||
|
var configuration = Service<DalamudConfiguration>.Get();
|
||||||
|
|
||||||
lock (this.listLock)
|
lock (this.listLock)
|
||||||
{
|
{
|
||||||
|
|
@ -3615,6 +3680,8 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
this.pluginListUpdatable = pluginManager.UpdatablePlugins.ToList();
|
this.pluginListUpdatable = pluginManager.UpdatablePlugins.ToList();
|
||||||
this.ResortPlugins();
|
this.ResortPlugins();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.hasHiddenPlugins = this.pluginListAvailable.Any(x => configuration.HiddenPluginInternalName.Contains(x.InternalName));
|
||||||
|
|
||||||
this.UpdateCategoriesOnPluginsChange();
|
this.UpdateCategoriesOnPluginsChange();
|
||||||
}
|
}
|
||||||
|
|
@ -3708,7 +3775,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
this.errorModalMessage = message;
|
this.errorModalMessage = message;
|
||||||
this.errorModalDrawing = true;
|
this.errorModalDrawing = true;
|
||||||
this.errorModalOnNextFrame = true;
|
this.errorModalOnNextFrame = true;
|
||||||
this.errorModalTaskCompletionSource = new TaskCompletionSource();
|
this.errorModalTaskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
return this.errorModalTaskCompletionSource.Task;
|
return this.errorModalTaskCompletionSource.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3716,7 +3783,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
{
|
{
|
||||||
this.updateModalOnNextFrame = true;
|
this.updateModalOnNextFrame = true;
|
||||||
this.updateModalPlugin = plugin;
|
this.updateModalPlugin = plugin;
|
||||||
this.updateModalTaskCompletionSource = new TaskCompletionSource<bool>();
|
this.updateModalTaskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
return this.updateModalTaskCompletionSource.Task;
|
return this.updateModalTaskCompletionSource.Task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3875,7 +3942,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
|
|
||||||
public static string TabBody_NoPluginsUpdateable => Loc.Localize("InstallerNoPluginsUpdate", "No plugins have updates available at the moment.");
|
public static string TabBody_NoPluginsUpdateable => Loc.Localize("InstallerNoPluginsUpdate", "No plugins have updates available at the moment.");
|
||||||
|
|
||||||
public static string TabBody_NoPluginsDev => Loc.Localize("InstallerNoPluginsDev", "You don't have any dev plugins. Add them some the settings.");
|
public static string TabBody_NoPluginsDev => Loc.Localize("InstallerNoPluginsDev", "You don't have any dev plugins. Add them from the settings.");
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
|
@ -3942,6 +4009,8 @@ internal class PluginInstallerWindow : Window, IDisposable
|
||||||
public static string PluginContext_MarkAllSeen => Loc.Localize("InstallerMarkAllSeen", "Mark all as seen");
|
public static string PluginContext_MarkAllSeen => Loc.Localize("InstallerMarkAllSeen", "Mark all as seen");
|
||||||
|
|
||||||
public static string PluginContext_HidePlugin => Loc.Localize("InstallerHidePlugin", "Hide from installer");
|
public static string PluginContext_HidePlugin => Loc.Localize("InstallerHidePlugin", "Hide from installer");
|
||||||
|
|
||||||
|
public static string PluginContext_UnhidePlugin => Loc.Localize("InstallerUnhidePlugin", "Unhide from installer");
|
||||||
|
|
||||||
public static string PluginContext_DeletePluginConfig => Loc.Localize("InstallerDeletePluginConfig", "Reset plugin data");
|
public static string PluginContext_DeletePluginConfig => Loc.Localize("InstallerDeletePluginConfig", "Reset plugin data");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using Dalamud.Game.Gui.NamePlate;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
|
|
||||||
|
using ImGuiNET;
|
||||||
|
|
||||||
|
namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests for nameplates.
|
||||||
|
/// </summary>
|
||||||
|
internal class NamePlateAgingStep : IAgingStep
|
||||||
|
{
|
||||||
|
private SubStep currentSubStep;
|
||||||
|
private Dictionary<ulong, int>? updateCount;
|
||||||
|
|
||||||
|
private enum SubStep
|
||||||
|
{
|
||||||
|
Start,
|
||||||
|
Confirm,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public string Name => "Test Nameplates";
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public SelfTestStepResult RunStep()
|
||||||
|
{
|
||||||
|
var namePlateGui = Service<NamePlateGui>.Get();
|
||||||
|
|
||||||
|
switch (this.currentSubStep)
|
||||||
|
{
|
||||||
|
case SubStep.Start:
|
||||||
|
namePlateGui.OnNamePlateUpdate += this.OnNamePlateUpdate;
|
||||||
|
namePlateGui.OnDataUpdate += this.OnDataUpdate;
|
||||||
|
namePlateGui.RequestRedraw();
|
||||||
|
this.updateCount = new Dictionary<ulong, int>();
|
||||||
|
this.currentSubStep++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SubStep.Confirm:
|
||||||
|
ImGui.Text("Click to redraw all visible nameplates");
|
||||||
|
if (ImGui.Button("Request redraw"))
|
||||||
|
namePlateGui.RequestRedraw();
|
||||||
|
|
||||||
|
ImGui.TextUnformatted("Can you see marker icons above nameplates, and does\n" +
|
||||||
|
"the update count increase when using request redraw?");
|
||||||
|
|
||||||
|
if (ImGui.Button("Yes"))
|
||||||
|
{
|
||||||
|
this.CleanUp();
|
||||||
|
return SelfTestStepResult.Pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
if (ImGui.Button("No"))
|
||||||
|
{
|
||||||
|
this.CleanUp();
|
||||||
|
return SelfTestStepResult.Fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelfTestStepResult.Waiting;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public void CleanUp()
|
||||||
|
{
|
||||||
|
var namePlateGui = Service<NamePlateGui>.Get();
|
||||||
|
namePlateGui.OnNamePlateUpdate -= this.OnNamePlateUpdate;
|
||||||
|
namePlateGui.OnDataUpdate -= this.OnDataUpdate;
|
||||||
|
namePlateGui.RequestRedraw();
|
||||||
|
this.updateCount = null;
|
||||||
|
this.currentSubStep = SubStep.Start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDataUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
||||||
|
{
|
||||||
|
foreach (var handler in handlers)
|
||||||
|
{
|
||||||
|
// Force nameplates to be visible
|
||||||
|
handler.VisibilityFlags |= 1;
|
||||||
|
|
||||||
|
// Set marker icon based on nameplate kind, and flicker when updating
|
||||||
|
if (handler.IsUpdating || context.IsFullUpdate)
|
||||||
|
{
|
||||||
|
handler.MarkerIconId = 66181 + (int)handler.NamePlateKind;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
handler.MarkerIconId = 66161 + (int)handler.NamePlateKind;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
||||||
|
{
|
||||||
|
foreach (var handler in handlers)
|
||||||
|
{
|
||||||
|
// Append GameObject address to name
|
||||||
|
var gameObjectAddress = handler.GameObject?.Address ?? 0;
|
||||||
|
|
||||||
|
handler.Name = handler.Name.Append(new SeString(new UIForegroundPayload(9)))
|
||||||
|
.Append($" (0x{gameObjectAddress:X})")
|
||||||
|
.Append(new SeString(UIForegroundPayload.UIForegroundOff));
|
||||||
|
|
||||||
|
// Track update count and set it as title
|
||||||
|
var count = this.updateCount!.GetValueOrDefault(handler.GameObjectId);
|
||||||
|
this.updateCount[handler.GameObjectId] = count + 1;
|
||||||
|
|
||||||
|
handler.TitleParts.Text = $"Updates: {count}";
|
||||||
|
handler.TitleParts.TextWrap = (new SeString(new UIForegroundPayload(43)),
|
||||||
|
new SeString(UIForegroundPayload.UIForegroundOff));
|
||||||
|
handler.DisplayTitle = true;
|
||||||
|
handler.IsPrefixTitle = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,7 @@ internal class SelfTestWindow : Window
|
||||||
new EnterTerritoryAgingStep(148, "Central Shroud"),
|
new EnterTerritoryAgingStep(148, "Central Shroud"),
|
||||||
new ItemPayloadAgingStep(),
|
new ItemPayloadAgingStep(),
|
||||||
new ContextMenuAgingStep(),
|
new ContextMenuAgingStep(),
|
||||||
|
new NamePlateAgingStep(),
|
||||||
new ActorTableAgingStep(),
|
new ActorTableAgingStep(),
|
||||||
new FateTableAgingStep(),
|
new FateTableAgingStep(),
|
||||||
new AetheryteListAgingStep(),
|
new AetheryteListAgingStep(),
|
||||||
|
|
@ -82,6 +83,7 @@ internal class SelfTestWindow : Window
|
||||||
if (ImGuiComponents.IconButton(FontAwesomeIcon.StepForward))
|
if (ImGuiComponents.IconButton(FontAwesomeIcon.StepForward))
|
||||||
{
|
{
|
||||||
this.stepResults.Add((SelfTestStepResult.NotRan, null));
|
this.stepResults.Add((SelfTestStepResult.NotRan, null));
|
||||||
|
this.steps[this.currentStep].CleanUp();
|
||||||
this.currentStep++;
|
this.currentStep++;
|
||||||
this.lastTestStart = DateTimeOffset.Now;
|
this.lastTestStart = DateTimeOffset.Now;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
using CheapLoc;
|
using CheapLoc;
|
||||||
|
|
||||||
using Dalamud.Configuration.Internal;
|
using Dalamud.Configuration.Internal;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
|
using Dalamud.Interface.Internal.ReShadeHandling;
|
||||||
using Dalamud.Interface.Internal.Windows.PluginInstaller;
|
using Dalamud.Interface.Internal.Windows.PluginInstaller;
|
||||||
using Dalamud.Interface.Internal.Windows.Settings.Widgets;
|
using Dalamud.Interface.Internal.Windows.Settings.Widgets;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
|
|
@ -11,28 +13,39 @@ using Dalamud.Utility;
|
||||||
|
|
||||||
namespace Dalamud.Interface.Internal.Windows.Settings.Tabs;
|
namespace Dalamud.Interface.Internal.Windows.Settings.Tabs;
|
||||||
|
|
||||||
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")]
|
[SuppressMessage(
|
||||||
|
"StyleCop.CSharp.DocumentationRules",
|
||||||
|
"SA1600:Elements should be documented",
|
||||||
|
Justification = "Internals")]
|
||||||
public class SettingsTabExperimental : SettingsTab
|
public class SettingsTabExperimental : SettingsTab
|
||||||
{
|
{
|
||||||
public override SettingsEntry[] Entries { get; } =
|
public override SettingsEntry[] Entries { get; } =
|
||||||
{
|
[
|
||||||
new SettingsEntry<bool>(
|
new SettingsEntry<bool>(
|
||||||
Loc.Localize("DalamudSettingsPluginTest", "Get plugin testing builds"),
|
Loc.Localize("DalamudSettingsPluginTest", "Get plugin testing builds"),
|
||||||
string.Format(
|
string.Format(
|
||||||
Loc.Localize("DalamudSettingsPluginTestHint", "Receive testing prereleases for selected plugins.\nTo opt-in to testing builds for a plugin, you have to right click it in the \"{0}\" tab of the plugin installer and select \"{1}\"."),
|
Loc.Localize(
|
||||||
|
"DalamudSettingsPluginTestHint",
|
||||||
|
"Receive testing prereleases for selected plugins.\nTo opt-in to testing builds for a plugin, you have to right click it in the \"{0}\" tab of the plugin installer and select \"{1}\"."),
|
||||||
PluginCategoryManager.Locs.Group_Installed,
|
PluginCategoryManager.Locs.Group_Installed,
|
||||||
PluginInstallerWindow.Locs.PluginContext_TestingOptIn),
|
PluginInstallerWindow.Locs.PluginContext_TestingOptIn),
|
||||||
c => c.DoPluginTest,
|
c => c.DoPluginTest,
|
||||||
(v, c) => c.DoPluginTest = v),
|
(v, c) => c.DoPluginTest = v),
|
||||||
new HintSettingsEntry(
|
new HintSettingsEntry(
|
||||||
Loc.Localize("DalamudSettingsPluginTestWarning", "Testing plugins may contain bugs or crash your game. Please only enable this if you are aware of the risks."),
|
Loc.Localize(
|
||||||
|
"DalamudSettingsPluginTestWarning",
|
||||||
|
"Testing plugins may contain bugs or crash your game. Please only enable this if you are aware of the risks."),
|
||||||
ImGuiColors.DalamudRed),
|
ImGuiColors.DalamudRed),
|
||||||
|
|
||||||
new GapSettingsEntry(5),
|
new GapSettingsEntry(5),
|
||||||
|
|
||||||
new SettingsEntry<bool>(
|
new SettingsEntry<bool>(
|
||||||
Loc.Localize("DalamudSettingEnablePluginUIAdditionalOptions", "Add a button to the title bar of plugin windows to open additional options"),
|
Loc.Localize(
|
||||||
Loc.Localize("DalamudSettingEnablePluginUIAdditionalOptionsHint", "This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."),
|
"DalamudSettingEnablePluginUIAdditionalOptions",
|
||||||
|
"Add a button to the title bar of plugin windows to open additional options"),
|
||||||
|
Loc.Localize(
|
||||||
|
"DalamudSettingEnablePluginUIAdditionalOptionsHint",
|
||||||
|
"This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."),
|
||||||
c => c.EnablePluginUiAdditionalOptions,
|
c => c.EnablePluginUiAdditionalOptions,
|
||||||
(v, c) => c.EnablePluginUiAdditionalOptions = v),
|
(v, c) => c.EnablePluginUiAdditionalOptions = v),
|
||||||
|
|
||||||
|
|
@ -40,7 +53,9 @@ public class SettingsTabExperimental : SettingsTab
|
||||||
|
|
||||||
new ButtonSettingsEntry(
|
new ButtonSettingsEntry(
|
||||||
Loc.Localize("DalamudSettingsClearHidden", "Clear hidden plugins"),
|
Loc.Localize("DalamudSettingsClearHidden", "Clear hidden plugins"),
|
||||||
Loc.Localize("DalamudSettingsClearHiddenHint", "Restore plugins you have previously hidden from the plugin installer."),
|
Loc.Localize(
|
||||||
|
"DalamudSettingsClearHiddenHint",
|
||||||
|
"Restore plugins you have previously hidden from the plugin installer."),
|
||||||
() =>
|
() =>
|
||||||
{
|
{
|
||||||
Service<DalamudConfiguration>.Get().HiddenPluginInternalName.Clear();
|
Service<DalamudConfiguration>.Get().HiddenPluginInternalName.Clear();
|
||||||
|
|
@ -55,6 +70,47 @@ public class SettingsTabExperimental : SettingsTab
|
||||||
|
|
||||||
new ThirdRepoSettingsEntry(),
|
new ThirdRepoSettingsEntry(),
|
||||||
|
|
||||||
|
new GapSettingsEntry(5, true),
|
||||||
|
|
||||||
|
new EnumSettingsEntry<ReShadeHandlingMode>(
|
||||||
|
Loc.Localize("DalamudSettingsReShadeHandlingMode", "ReShade handling mode"),
|
||||||
|
Loc.Localize(
|
||||||
|
"DalamudSettingsReShadeHandlingModeHint",
|
||||||
|
"You may try different options to work around problems you may encounter.\nRestart is required for changes to take effect."),
|
||||||
|
c => c.ReShadeHandlingMode,
|
||||||
|
(v, c) => c.ReShadeHandlingMode = v,
|
||||||
|
fallbackValue: ReShadeHandlingMode.Default,
|
||||||
|
warning: static rshm =>
|
||||||
|
rshm is ReShadeHandlingMode.UnwrapReShade or ReShadeHandlingMode.None ||
|
||||||
|
Service<DalamudConfiguration>.Get().SwapChainHookMode == SwapChainHelper.HookMode.ByteCode
|
||||||
|
? null
|
||||||
|
: "Current option will be ignored and no special ReShade handling will be done, because SwapChain vtable hook mode is set.")
|
||||||
|
{
|
||||||
|
FriendlyEnumNameGetter = x => x switch
|
||||||
|
{
|
||||||
|
ReShadeHandlingMode.Default => "Default",
|
||||||
|
ReShadeHandlingMode.UnwrapReShade => "Unwrap",
|
||||||
|
ReShadeHandlingMode.ReShadeAddonPresent => "ReShade Addon (present)",
|
||||||
|
ReShadeHandlingMode.ReShadeAddonReShadeOverlay => "ReShade Addon (reshade_overlay)",
|
||||||
|
ReShadeHandlingMode.HookReShadeDxgiSwapChainOnPresent => "Hook ReShade::DXGISwapChain::OnPresent",
|
||||||
|
ReShadeHandlingMode.None => "Do not handle",
|
||||||
|
_ => "<invalid>",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/* // Making this a console command instead, for now
|
||||||
|
new GapSettingsEntry(5, true),
|
||||||
|
|
||||||
|
new EnumSettingsEntry<SwapChainHelper.HookMode>(
|
||||||
|
Loc.Localize("DalamudSettingsSwapChainHookMode", "Swap chain hooking mode"),
|
||||||
|
Loc.Localize(
|
||||||
|
"DalamudSettingsSwapChainHookModeHint",
|
||||||
|
"Depending on addons aside from Dalamud you use, you may have to use different options for Dalamud and other addons to cooperate.\nRestart is required for changes to take effect."),
|
||||||
|
c => c.SwapChainHookMode,
|
||||||
|
(v, c) => c.SwapChainHookMode = v,
|
||||||
|
fallbackValue: SwapChainHelper.HookMode.ByteCode),
|
||||||
|
*/
|
||||||
|
|
||||||
/* Disabling profiles after they've been enabled doesn't make much sense, at least not if the user has already created profiles.
|
/* Disabling profiles after they've been enabled doesn't make much sense, at least not if the user has already created profiles.
|
||||||
new GapSettingsEntry(5, true),
|
new GapSettingsEntry(5, true),
|
||||||
|
|
||||||
|
|
@ -64,7 +120,7 @@ public class SettingsTabExperimental : SettingsTab
|
||||||
c => c.ProfilesEnabled,
|
c => c.ProfilesEnabled,
|
||||||
(v, c) => c.ProfilesEnabled = v),
|
(v, c) => c.ProfilesEnabled = v),
|
||||||
*/
|
*/
|
||||||
};
|
];
|
||||||
|
|
||||||
public override string Title => Loc.Localize("DalamudSettingsExperimental", "Experimental");
|
public override string Title => Loc.Localize("DalamudSettingsExperimental", "Experimental");
|
||||||
|
|
||||||
|
|
@ -72,7 +128,9 @@ public class SettingsTabExperimental : SettingsTab
|
||||||
{
|
{
|
||||||
base.Draw();
|
base.Draw();
|
||||||
|
|
||||||
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "Total memory used by Dalamud & Plugins: " + Util.FormatBytes(GC.GetTotalMemory(false)));
|
ImGuiHelpers.SafeTextColoredWrapped(
|
||||||
|
ImGuiColors.DalamudGrey,
|
||||||
|
"Total memory used by Dalamud & Plugins: " + Util.FormatBytes(GC.GetTotalMemory(false)));
|
||||||
ImGuiHelpers.ScaledDummy(15);
|
ImGuiHelpers.ScaledDummy(15);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ public class SettingsTabGeneral : SettingsTab
|
||||||
|
|
||||||
new GapSettingsEntry(5),
|
new GapSettingsEntry(5),
|
||||||
|
|
||||||
new SettingsEntry<XivChatType>(
|
new EnumSettingsEntry<XivChatType>(
|
||||||
Loc.Localize("DalamudSettingsChannel", "Dalamud Chat Channel"),
|
Loc.Localize("DalamudSettingsChannel", "Dalamud Chat Channel"),
|
||||||
Loc.Localize("DalamudSettingsChannelHint", "Select the chat channel that is to be used for general Dalamud messages."),
|
Loc.Localize("DalamudSettingsChannelHint", "Select the chat channel that is to be used for general Dalamud messages."),
|
||||||
c => c.GeneralChatType,
|
c => c.GeneralChatType,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
using Dalamud.Configuration.Internal;
|
||||||
|
|
||||||
|
using Dalamud.Interface.Colors;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
|
||||||
|
using ImGuiNET;
|
||||||
|
|
||||||
|
namespace Dalamud.Interface.Internal.Windows.Settings.Widgets;
|
||||||
|
|
||||||
|
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")]
|
||||||
|
internal sealed class EnumSettingsEntry<T> : SettingsEntry
|
||||||
|
where T : struct, Enum
|
||||||
|
{
|
||||||
|
private readonly LoadSettingDelegate load;
|
||||||
|
private readonly SaveSettingDelegate save;
|
||||||
|
private readonly Action<T>? change;
|
||||||
|
|
||||||
|
private readonly T fallbackValue;
|
||||||
|
|
||||||
|
private T valueBacking;
|
||||||
|
|
||||||
|
public EnumSettingsEntry(
|
||||||
|
string name,
|
||||||
|
string description,
|
||||||
|
LoadSettingDelegate load,
|
||||||
|
SaveSettingDelegate save,
|
||||||
|
Action<T>? change = null,
|
||||||
|
Func<T, string?>? warning = null,
|
||||||
|
Func<T, string?>? validity = null,
|
||||||
|
Func<bool>? visibility = null,
|
||||||
|
T fallbackValue = default)
|
||||||
|
{
|
||||||
|
this.load = load;
|
||||||
|
this.save = save;
|
||||||
|
this.change = change;
|
||||||
|
this.Name = name;
|
||||||
|
this.Description = description;
|
||||||
|
this.CheckWarning = warning;
|
||||||
|
this.CheckValidity = validity;
|
||||||
|
this.CheckVisibility = visibility;
|
||||||
|
|
||||||
|
this.fallbackValue = fallbackValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate T LoadSettingDelegate(DalamudConfiguration config);
|
||||||
|
|
||||||
|
public delegate void SaveSettingDelegate(T value, DalamudConfiguration config);
|
||||||
|
|
||||||
|
public T Value
|
||||||
|
{
|
||||||
|
get => this.valueBacking;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (Equals(value, this.valueBacking))
|
||||||
|
return;
|
||||||
|
this.valueBacking = value;
|
||||||
|
this.change?.Invoke(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Description { get; }
|
||||||
|
|
||||||
|
public Action<EnumSettingsEntry<T>>? CustomDraw { get; init; }
|
||||||
|
|
||||||
|
public Func<T, string?>? CheckValidity { get; init; }
|
||||||
|
|
||||||
|
public Func<T, string?>? CheckWarning { get; init; }
|
||||||
|
|
||||||
|
public Func<bool>? CheckVisibility { get; init; }
|
||||||
|
|
||||||
|
public Func<T, string> FriendlyEnumNameGetter { get; init; } = x => x.ToString();
|
||||||
|
|
||||||
|
public Func<T, string> FriendlyEnumDescriptionGetter { get; init; } = _ => string.Empty;
|
||||||
|
|
||||||
|
public override bool IsVisible => this.CheckVisibility?.Invoke() ?? true;
|
||||||
|
|
||||||
|
public override void Draw()
|
||||||
|
{
|
||||||
|
Debug.Assert(this.Name != null, "this.Name != null");
|
||||||
|
|
||||||
|
if (this.CustomDraw is not null)
|
||||||
|
{
|
||||||
|
this.CustomDraw.Invoke(this);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGuiHelpers.SafeTextWrapped(this.Name);
|
||||||
|
|
||||||
|
var idx = this.valueBacking;
|
||||||
|
var values = Enum.GetValues<T>();
|
||||||
|
|
||||||
|
if (!values.Contains(idx))
|
||||||
|
{
|
||||||
|
idx = Enum.IsDefined(this.fallbackValue)
|
||||||
|
? this.fallbackValue
|
||||||
|
: throw new InvalidOperationException("No fallback value for enum");
|
||||||
|
this.valueBacking = idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.BeginCombo($"###{this.Id.ToString()}", this.FriendlyEnumNameGetter(idx)))
|
||||||
|
{
|
||||||
|
foreach (var value in values)
|
||||||
|
{
|
||||||
|
if (ImGui.Selectable(this.FriendlyEnumNameGetter(value), idx.Equals(value)))
|
||||||
|
{
|
||||||
|
this.valueBacking = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndCombo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
|
||||||
|
{
|
||||||
|
var desc = this.FriendlyEnumDescriptionGetter(this.valueBacking);
|
||||||
|
if (!string.IsNullOrWhiteSpace(desc))
|
||||||
|
{
|
||||||
|
ImGuiHelpers.SafeTextWrapped(desc);
|
||||||
|
ImGuiHelpers.ScaledDummy(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiHelpers.SafeTextWrapped(this.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.CheckValidity != null)
|
||||||
|
{
|
||||||
|
var validityMsg = this.CheckValidity.Invoke(this.Value);
|
||||||
|
this.IsValid = string.IsNullOrEmpty(validityMsg);
|
||||||
|
|
||||||
|
if (!this.IsValid)
|
||||||
|
{
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
|
||||||
|
{
|
||||||
|
ImGui.Text(validityMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.IsValid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var warningMessage = this.CheckWarning?.Invoke(this.Value);
|
||||||
|
|
||||||
|
if (warningMessage != null)
|
||||||
|
{
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
|
||||||
|
{
|
||||||
|
ImGui.Text(warningMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Load()
|
||||||
|
{
|
||||||
|
this.valueBacking = this.load(Service<DalamudConfiguration>.Get());
|
||||||
|
|
||||||
|
if (this.CheckValidity != null)
|
||||||
|
{
|
||||||
|
this.IsValid = this.CheckValidity(this.Value) == null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.IsValid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Save() => this.save(this.Value, Service<DalamudConfiguration>.Get());
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
using Dalamud.Configuration.Internal;
|
using Dalamud.Configuration.Internal;
|
||||||
|
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Utility;
|
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
|
|
||||||
namespace Dalamud.Interface.Internal.Windows.Settings.Widgets;
|
namespace Dalamud.Interface.Internal.Windows.Settings.Widgets;
|
||||||
|
|
@ -22,7 +20,6 @@ internal sealed class SettingsEntry<T> : SettingsEntry
|
||||||
private readonly Action<T?>? change;
|
private readonly Action<T?>? change;
|
||||||
|
|
||||||
private object? valueBacking;
|
private object? valueBacking;
|
||||||
private object? fallbackValue;
|
|
||||||
|
|
||||||
public SettingsEntry(
|
public SettingsEntry(
|
||||||
string name,
|
string name,
|
||||||
|
|
@ -32,8 +29,7 @@ internal sealed class SettingsEntry<T> : SettingsEntry
|
||||||
Action<T?>? change = null,
|
Action<T?>? change = null,
|
||||||
Func<T?, string?>? warning = null,
|
Func<T?, string?>? warning = null,
|
||||||
Func<T?, string?>? validity = null,
|
Func<T?, string?>? validity = null,
|
||||||
Func<bool>? visibility = null,
|
Func<bool>? visibility = null)
|
||||||
object? fallbackValue = null)
|
|
||||||
{
|
{
|
||||||
this.load = load;
|
this.load = load;
|
||||||
this.save = save;
|
this.save = save;
|
||||||
|
|
@ -43,8 +39,6 @@ internal sealed class SettingsEntry<T> : SettingsEntry
|
||||||
this.CheckWarning = warning;
|
this.CheckWarning = warning;
|
||||||
this.CheckValidity = validity;
|
this.CheckValidity = validity;
|
||||||
this.CheckVisibility = visibility;
|
this.CheckVisibility = visibility;
|
||||||
|
|
||||||
this.fallbackValue = fallbackValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public delegate T? LoadSettingDelegate(DalamudConfiguration config);
|
public delegate T? LoadSettingDelegate(DalamudConfiguration config);
|
||||||
|
|
@ -118,34 +112,6 @@ internal sealed class SettingsEntry<T> : SettingsEntry
|
||||||
this.change?.Invoke(this.Value);
|
this.change?.Invoke(this.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (type.IsEnum)
|
|
||||||
{
|
|
||||||
ImGuiHelpers.SafeTextWrapped(this.Name);
|
|
||||||
|
|
||||||
var idx = (Enum)(this.valueBacking ?? 0);
|
|
||||||
var values = Enum.GetValues(type);
|
|
||||||
var descriptions =
|
|
||||||
values.Cast<Enum>().ToDictionary(x => x, x => x.GetAttribute<SettingsAnnotationAttribute>() ?? new SettingsAnnotationAttribute(x.ToString(), string.Empty));
|
|
||||||
|
|
||||||
if (!descriptions.ContainsKey(idx))
|
|
||||||
{
|
|
||||||
idx = (Enum)this.fallbackValue ?? throw new Exception("No fallback value for enum");
|
|
||||||
this.valueBacking = idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.BeginCombo($"###{this.Id.ToString()}", descriptions[idx].FriendlyName))
|
|
||||||
{
|
|
||||||
foreach (Enum value in values)
|
|
||||||
{
|
|
||||||
if (ImGui.Selectable(descriptions[value].FriendlyName, idx.Equals(value)))
|
|
||||||
{
|
|
||||||
this.valueBacking = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.EndCombo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
|
||||||
{
|
{
|
||||||
|
|
@ -197,18 +163,3 @@ internal sealed class SettingsEntry<T> : SettingsEntry
|
||||||
|
|
||||||
public override void Save() => this.save(this.Value, Service<DalamudConfiguration>.Get());
|
public override void Save() => this.save(this.Value, Service<DalamudConfiguration>.Get());
|
||||||
}
|
}
|
||||||
|
|
||||||
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")]
|
|
||||||
[AttributeUsage(AttributeTargets.Field)]
|
|
||||||
internal class SettingsAnnotationAttribute : Attribute
|
|
||||||
{
|
|
||||||
public SettingsAnnotationAttribute(string friendlyName, string description)
|
|
||||||
{
|
|
||||||
this.FriendlyName = friendlyName;
|
|
||||||
this.Description = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string FriendlyName { get; set; }
|
|
||||||
|
|
||||||
public string Description { get; set; }
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -496,7 +496,7 @@ internal sealed partial class FontAtlasFactory
|
||||||
$"{nameof(FontAtlasAutoRebuildMode.Async)}.");
|
$"{nameof(FontAtlasAutoRebuildMode.Async)}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource<FontAtlasBuiltData>();
|
var tcs = new TaskCompletionSource<FontAtlasBuiltData>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var rebuildIndex = Interlocked.Increment(ref this.buildIndex);
|
var rebuildIndex = Interlocked.Increment(ref this.buildIndex);
|
||||||
|
|
|
||||||
|
|
@ -242,7 +242,7 @@ internal abstract class FontHandle : IFontHandle
|
||||||
if (this.Available)
|
if (this.Available)
|
||||||
return Task.FromResult<IFontHandle>(this);
|
return Task.FromResult<IFontHandle>(this);
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource<IFontHandle>();
|
var tcs = new TaskCompletionSource<IFontHandle>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
this.ImFontChanged += OnImFontChanged;
|
this.ImFontChanged += OnImFontChanged;
|
||||||
this.Disposed += OnDisposed;
|
this.Disposed += OnDisposed;
|
||||||
if (this.Available)
|
if (this.Available)
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,10 @@ internal sealed partial class TextureManager
|
||||||
ISharedImmediateTexture ITextureProvider.GetFromFile(FileInfo file) =>
|
ISharedImmediateTexture ITextureProvider.GetFromFile(FileInfo file) =>
|
||||||
this.Shared.GetFromFile(file);
|
this.Shared.GetFromFile(file);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ISharedImmediateTexture GetFromFileAbsolute(string fullPath) =>
|
||||||
|
this.Shared.GetFromFileAbsolute(fullPath);
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
ISharedImmediateTexture ITextureProvider.GetFromManifestResource(Assembly assembly, string name) =>
|
ISharedImmediateTexture ITextureProvider.GetFromManifestResource(Assembly assembly, string name) =>
|
||||||
this.Shared.GetFromManifestResource(assembly, name);
|
this.Shared.GetFromManifestResource(assembly, name);
|
||||||
|
|
@ -141,7 +145,12 @@ internal sealed partial class TextureManager
|
||||||
/// <inheritdoc cref="ITextureProvider.GetFromFile(FileInfo)"/>
|
/// <inheritdoc cref="ITextureProvider.GetFromFile(FileInfo)"/>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public SharedImmediateTexture.PureImpl GetFromFile(FileInfo file) =>
|
public SharedImmediateTexture.PureImpl GetFromFile(FileInfo file) =>
|
||||||
this.fileDict.GetOrAdd(file.FullName, FileSystemSharedImmediateTexture.CreatePlaceholder)
|
this.GetFromFileAbsolute(file.FullName);
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ITextureProvider.GetFromFileAbsolute(string)"/>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public SharedImmediateTexture.PureImpl GetFromFileAbsolute(string fullPath) =>
|
||||||
|
this.fileDict.GetOrAdd(fullPath, FileSystemSharedImmediateTexture.CreatePlaceholder)
|
||||||
.PublicUseInstance;
|
.PublicUseInstance;
|
||||||
|
|
||||||
/// <inheritdoc cref="ITextureProvider.GetFromManifestResource"/>
|
/// <inheritdoc cref="ITextureProvider.GetFromManifestResource"/>
|
||||||
|
|
|
||||||
|
|
@ -313,6 +313,14 @@ internal sealed class TextureManagerPluginScoped
|
||||||
return shared;
|
return shared;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public ISharedImmediateTexture GetFromFileAbsolute(string fullPath)
|
||||||
|
{
|
||||||
|
var shared = this.ManagerOrThrow.Shared.GetFromFileAbsolute(fullPath);
|
||||||
|
shared.AddOwnerPlugin(this.plugin);
|
||||||
|
return shared;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name)
|
public ISharedImmediateTexture GetFromManifestResource(Assembly assembly, string name)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,8 @@ internal sealed class ViewportTextureWrap : IDalamudTextureWrap, IDeferredDispos
|
||||||
private readonly string? debugName;
|
private readonly string? debugName;
|
||||||
private readonly LocalPlugin? ownerPlugin;
|
private readonly LocalPlugin? ownerPlugin;
|
||||||
private readonly CancellationToken cancellationToken;
|
private readonly CancellationToken cancellationToken;
|
||||||
private readonly TaskCompletionSource<IDalamudTextureWrap> firstUpdateTaskCompletionSource = new();
|
private readonly TaskCompletionSource<IDalamudTextureWrap> firstUpdateTaskCompletionSource =
|
||||||
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
private ImGuiViewportTextureArgs args;
|
private ImGuiViewportTextureArgs args;
|
||||||
private D3D11_TEXTURE2D_DESC desc;
|
private D3D11_TEXTURE2D_DESC desc;
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
|
||||||
{
|
{
|
||||||
var first = true;
|
var first = true;
|
||||||
var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList();
|
var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList();
|
||||||
var tcs = new TaskCompletionSource<BitmapCodecInfo>();
|
var tcs = new TaskCompletionSource<BitmapCodecInfo>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
Service<InterfaceManager>.Get().Draw += DrawChoices;
|
Service<InterfaceManager>.Get().Draw += DrawChoices;
|
||||||
|
|
||||||
encoder = await tcs.Task;
|
encoder = await tcs.Task;
|
||||||
|
|
@ -108,7 +108,7 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService
|
||||||
|
|
||||||
string path;
|
string path;
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<string>();
|
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
this.fileDialogManager.SaveFileDialog(
|
this.fileDialogManager.SaveFileDialog(
|
||||||
"Save texture...",
|
"Save texture...",
|
||||||
$"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}",
|
$"{encoder.Name.Replace(',', '.')}{{{string.Join(',', encoder.Extensions)}}}",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Dalamud.Logging.Internal;
|
using Dalamud.Logging.Internal;
|
||||||
|
|
@ -83,53 +84,57 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
||||||
/// <param name="scopedObjects">Scoped objects to be included in the constructor.</param>
|
/// <param name="scopedObjects">Scoped objects to be included in the constructor.</param>
|
||||||
/// <param name="scope">The scope to be used to create scoped services.</param>
|
/// <param name="scope">The scope to be used to create scoped services.</param>
|
||||||
/// <returns>The created object.</returns>
|
/// <returns>The created object.</returns>
|
||||||
public async Task<object?> CreateAsync(Type objectType, object[] scopedObjects, IServiceScope? scope = null)
|
public async Task<object> CreateAsync(Type objectType, object[] scopedObjects, IServiceScope? scope = null)
|
||||||
{
|
{
|
||||||
var scopeImpl = scope as ServiceScopeImpl;
|
var errorStep = "constructor lookup";
|
||||||
|
|
||||||
var ctor = this.FindApplicableCtor(objectType, scopedObjects);
|
try
|
||||||
if (ctor == null)
|
|
||||||
{
|
{
|
||||||
Log.Error("Failed to create {TypeName}, an eligible ctor with satisfiable services could not be found", objectType.FullName!);
|
var scopeImpl = scope as ServiceScopeImpl;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate dependency versions (if they exist)
|
var ctor = this.FindApplicableCtor(objectType, scopedObjects)
|
||||||
var parameterTypes = ctor.GetParameters().Select(p => p.ParameterType).ToList();
|
?? throw new InvalidOperationException("An eligible ctor with satisfiable services could not be found");
|
||||||
|
|
||||||
var resolvedParams =
|
errorStep = "requested service resolution";
|
||||||
await Task.WhenAll(
|
var resolvedParams =
|
||||||
parameterTypes
|
await Task.WhenAll(
|
||||||
.Select(async type =>
|
ctor.GetParameters()
|
||||||
|
.Select(p => p.ParameterType)
|
||||||
|
.Select(type => this.GetService(type, scopeImpl, scopedObjects)));
|
||||||
|
|
||||||
|
var instance = RuntimeHelpers.GetUninitializedObject(objectType);
|
||||||
|
|
||||||
|
errorStep = "property injection";
|
||||||
|
await this.InjectProperties(instance, scopedObjects, scope);
|
||||||
|
|
||||||
|
errorStep = "ctor invocation";
|
||||||
|
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
var thr = new Thread(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var service = await this.GetService(type, scopeImpl, scopedObjects);
|
ctor.Invoke(instance, resolvedParams);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
tcs.SetException(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (service == null)
|
tcs.SetResult();
|
||||||
{
|
});
|
||||||
Log.Error("Requested ctor service type {TypeName} was not available (null)", type.FullName!);
|
|
||||||
}
|
|
||||||
|
|
||||||
return service;
|
thr.Start();
|
||||||
}));
|
await tcs.Task.ConfigureAwait(false);
|
||||||
|
thr.Join();
|
||||||
|
|
||||||
var hasNull = resolvedParams.Any(p => p == null);
|
return instance;
|
||||||
if (hasNull)
|
|
||||||
{
|
|
||||||
Log.Error("Failed to create {TypeName}, a requested service type could not be satisfied", objectType.FullName!);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
var instance = RuntimeHelpers.GetUninitializedObject(objectType);
|
|
||||||
|
|
||||||
if (!await this.InjectProperties(instance, scopedObjects, scope))
|
|
||||||
{
|
{
|
||||||
Log.Error("Failed to create {TypeName}, a requested property service type could not be satisfied", objectType.FullName!);
|
throw new AggregateException($"Failed to create {objectType.FullName ?? objectType.Name} ({errorStep})", e);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctor.Invoke(instance, resolvedParams);
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -139,28 +144,21 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
||||||
/// <param name="instance">The object instance.</param>
|
/// <param name="instance">The object instance.</param>
|
||||||
/// <param name="publicScopes">Scoped objects to be injected.</param>
|
/// <param name="publicScopes">Scoped objects to be injected.</param>
|
||||||
/// <param name="scope">The scope to be used to create scoped services.</param>
|
/// <param name="scope">The scope to be used to create scoped services.</param>
|
||||||
/// <returns>Whether or not the injection was successful.</returns>
|
/// <returns>A <see cref="ValueTask"/> representing the operation.</returns>
|
||||||
public async Task<bool> InjectProperties(object instance, object[] publicScopes, IServiceScope? scope = null)
|
public async Task InjectProperties(object instance, object[] publicScopes, IServiceScope? scope = null)
|
||||||
{
|
{
|
||||||
var scopeImpl = scope as ServiceScopeImpl;
|
var scopeImpl = scope as ServiceScopeImpl;
|
||||||
var objectType = instance.GetType();
|
var objectType = instance.GetType();
|
||||||
|
|
||||||
var props = objectType.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public |
|
var props =
|
||||||
BindingFlags.NonPublic).Where(x => x.GetCustomAttributes(typeof(PluginServiceAttribute)).Any()).ToArray();
|
objectType
|
||||||
|
.GetProperties(
|
||||||
|
BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
||||||
|
.Where(x => x.GetCustomAttributes(typeof(PluginServiceAttribute)).Any())
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
foreach (var prop in props)
|
foreach (var prop in props)
|
||||||
{
|
prop.SetValue(instance, await this.GetService(prop.PropertyType, scopeImpl, publicScopes));
|
||||||
var service = await this.GetService(prop.PropertyType, scopeImpl, publicScopes);
|
|
||||||
if (service == null)
|
|
||||||
{
|
|
||||||
Log.Error("Requested service type {TypeName} was not available (null)", prop.PropertyType.FullName!);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
prop.SetValue(instance, service);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -172,7 +170,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
object? IServiceProvider.GetService(Type serviceType) => this.GetSingletonService(serviceType);
|
object? IServiceProvider.GetService(Type serviceType) => this.GetSingletonService(serviceType);
|
||||||
|
|
||||||
private async Task<object?> GetService(Type serviceType, ServiceScopeImpl? scope, object[] scopedObjects)
|
private async Task<object> GetService(Type serviceType, ServiceScopeImpl? scope, object[] scopedObjects)
|
||||||
{
|
{
|
||||||
if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType))
|
if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType))
|
||||||
serviceType = implementingType;
|
serviceType = implementingType;
|
||||||
|
|
@ -181,8 +179,8 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
||||||
{
|
{
|
||||||
if (scope == null)
|
if (scope == null)
|
||||||
{
|
{
|
||||||
Log.Error("Failed to create {TypeName}, is scoped but no scope provided", serviceType.FullName!);
|
throw new InvalidOperationException(
|
||||||
return null;
|
$"Failed to create {serviceType.FullName ?? serviceType.Name}, is scoped but no scope provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await scope.CreatePrivateScopedObject(serviceType, scopedObjects);
|
return await scope.CreatePrivateScopedObject(serviceType, scopedObjects);
|
||||||
|
|
@ -190,18 +188,12 @@ internal class ServiceContainer : IServiceProvider, IServiceType
|
||||||
|
|
||||||
var singletonService = await this.GetSingletonService(serviceType, false);
|
var singletonService = await this.GetSingletonService(serviceType, false);
|
||||||
if (singletonService != null)
|
if (singletonService != null)
|
||||||
{
|
|
||||||
return singletonService;
|
return singletonService;
|
||||||
}
|
|
||||||
|
|
||||||
// resolve dependency from scoped objects
|
// resolve dependency from scoped objects
|
||||||
var scoped = scopedObjects.FirstOrDefault(o => o.GetType().IsAssignableTo(serviceType));
|
return scopedObjects.FirstOrDefault(o => o.GetType().IsAssignableTo(serviceType))
|
||||||
if (scoped == default)
|
?? throw new InvalidOperationException(
|
||||||
{
|
$"Requested type {serviceType.FullName ?? serviceType.Name} could not be found from {nameof(scopedObjects)}");
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return scoped;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<object?> GetSingletonService(Type serviceType, bool tryGetInterface = true)
|
private async Task<object?> GetSingletonService(Type serviceType, bool tryGetInterface = true)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
namespace Dalamud.IoC.Internal;
|
namespace Dalamud.IoC.Internal;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -14,7 +17,7 @@ internal interface IServiceScope : IDisposable
|
||||||
/// but not directly to created objects.
|
/// but not directly to created objects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="scopes">The scopes to add.</param>
|
/// <param name="scopes">The scopes to add.</param>
|
||||||
public void RegisterPrivateScopes(params object[] scopes);
|
void RegisterPrivateScopes(params object[] scopes);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create an object.
|
/// Create an object.
|
||||||
|
|
@ -22,7 +25,7 @@ internal interface IServiceScope : IDisposable
|
||||||
/// <param name="objectType">The type of object to create.</param>
|
/// <param name="objectType">The type of object to create.</param>
|
||||||
/// <param name="scopedObjects">Scoped objects to be included in the constructor.</param>
|
/// <param name="scopedObjects">Scoped objects to be included in the constructor.</param>
|
||||||
/// <returns>The created object.</returns>
|
/// <returns>The created object.</returns>
|
||||||
public Task<object?> CreateAsync(Type objectType, params object[] scopedObjects);
|
Task<object> CreateAsync(Type objectType, params object[] scopedObjects);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Inject <see cref="PluginInterfaceAttribute" /> interfaces into public or static properties on the provided object.
|
/// Inject <see cref="PluginInterfaceAttribute" /> interfaces into public or static properties on the provided object.
|
||||||
|
|
@ -30,8 +33,8 @@ internal interface IServiceScope : IDisposable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="instance">The object instance.</param>
|
/// <param name="instance">The object instance.</param>
|
||||||
/// <param name="scopedObjects">Scoped objects to be injected.</param>
|
/// <param name="scopedObjects">Scoped objects to be injected.</param>
|
||||||
/// <returns>Whether or not the injection was successful.</returns>
|
/// <returns>A <see cref="ValueTask"/> representing the status of the operation.</returns>
|
||||||
public Task<bool> InjectPropertiesAsync(object instance, params object[] scopedObjects);
|
Task InjectPropertiesAsync(object instance, params object[] scopedObjects);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -41,35 +44,24 @@ internal class ServiceScopeImpl : IServiceScope
|
||||||
{
|
{
|
||||||
private readonly ServiceContainer container;
|
private readonly ServiceContainer container;
|
||||||
|
|
||||||
private readonly List<object> privateScopedObjects = new();
|
private readonly List<object> privateScopedObjects = [];
|
||||||
private readonly List<object> scopeCreatedObjects = new();
|
private readonly ConcurrentDictionary<Type, Task<object>> scopeCreatedObjects = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Initializes a new instance of the <see cref="ServiceScopeImpl" /> class.</summary>
|
||||||
/// Initializes a new instance of the <see cref="ServiceScopeImpl" /> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="container">The container this scope will use to create services.</param>
|
/// <param name="container">The container this scope will use to create services.</param>
|
||||||
public ServiceScopeImpl(ServiceContainer container)
|
public ServiceScopeImpl(ServiceContainer container) => this.container = container;
|
||||||
{
|
|
||||||
this.container = container;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void RegisterPrivateScopes(params object[] scopes)
|
public void RegisterPrivateScopes(params object[] scopes) =>
|
||||||
{
|
|
||||||
this.privateScopedObjects.AddRange(scopes);
|
this.privateScopedObjects.AddRange(scopes);
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<object?> CreateAsync(Type objectType, params object[] scopedObjects)
|
public Task<object> CreateAsync(Type objectType, params object[] scopedObjects) =>
|
||||||
{
|
this.container.CreateAsync(objectType, scopedObjects, this);
|
||||||
return this.container.CreateAsync(objectType, scopedObjects, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<bool> InjectPropertiesAsync(object instance, params object[] scopedObjects)
|
public Task InjectPropertiesAsync(object instance, params object[] scopedObjects) =>
|
||||||
{
|
this.container.InjectProperties(instance, scopedObjects, this);
|
||||||
return this.container.InjectProperties(instance, scopedObjects, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a service scoped to this scope, with private scoped objects.
|
/// Create a service scoped to this scope, with private scoped objects.
|
||||||
|
|
@ -77,34 +69,39 @@ internal class ServiceScopeImpl : IServiceScope
|
||||||
/// <param name="objectType">The type of object to create.</param>
|
/// <param name="objectType">The type of object to create.</param>
|
||||||
/// <param name="scopedObjects">Additional scoped objects.</param>
|
/// <param name="scopedObjects">Additional scoped objects.</param>
|
||||||
/// <returns>The created object, or null.</returns>
|
/// <returns>The created object, or null.</returns>
|
||||||
public async Task<object?> CreatePrivateScopedObject(Type objectType, params object[] scopedObjects)
|
public Task<object> CreatePrivateScopedObject(Type objectType, params object[] scopedObjects) =>
|
||||||
{
|
this.scopeCreatedObjects.GetOrAdd(
|
||||||
var instance = this.scopeCreatedObjects.FirstOrDefault(x => x.GetType() == objectType);
|
objectType,
|
||||||
if (instance != null)
|
static (objectType, p) => p.Scope.container.CreateAsync(
|
||||||
return instance;
|
objectType,
|
||||||
|
p.Objects.Concat(p.Scope.privateScopedObjects).ToArray()),
|
||||||
instance =
|
(Scope: this, Objects: scopedObjects));
|
||||||
await this.container.CreateAsync(objectType, scopedObjects.Concat(this.privateScopedObjects).ToArray());
|
|
||||||
if (instance != null)
|
|
||||||
this.scopeCreatedObjects.Add(instance);
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
foreach (var createdObject in this.scopeCreatedObjects)
|
foreach (var objectTask in this.scopeCreatedObjects)
|
||||||
{
|
{
|
||||||
switch (createdObject)
|
objectTask.Value.ContinueWith(
|
||||||
{
|
static r =>
|
||||||
case IInternalDisposableService d:
|
{
|
||||||
d.DisposeService();
|
if (!r.IsCompletedSuccessfully)
|
||||||
break;
|
{
|
||||||
case IDisposable d:
|
if (r.Exception is { } e)
|
||||||
d.Dispose();
|
Log.Error(e, "{what}: Failed to load.", nameof(ServiceScopeImpl));
|
||||||
break;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (r.Result)
|
||||||
|
{
|
||||||
|
case IInternalDisposableService d:
|
||||||
|
d.DisposeService();
|
||||||
|
break;
|
||||||
|
case IDisposable d:
|
||||||
|
d.Dispose();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
using CheapLoc;
|
using CheapLoc;
|
||||||
using Dalamud.Configuration.Internal;
|
|
||||||
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
|
|
@ -13,7 +12,7 @@ namespace Dalamud;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class handling localization.
|
/// Class handling localization.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ServiceManager.EarlyLoadedService]
|
[ServiceManager.ProvidedService]
|
||||||
public class Localization : IServiceType
|
public class Localization : IServiceType
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -43,16 +42,6 @@ public class Localization : IServiceType
|
||||||
this.assembly = Assembly.GetCallingAssembly();
|
this.assembly = Assembly.GetCallingAssembly();
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServiceManager.ServiceConstructor]
|
|
||||||
private Localization(Dalamud dalamud, DalamudConfiguration configuration)
|
|
||||||
: this(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "loc", "dalamud"), "dalamud_")
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(configuration.LanguageOverride))
|
|
||||||
this.SetupWithLangCode(configuration.LanguageOverride);
|
|
||||||
else
|
|
||||||
this.SetupWithUiCulture();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delegate for the <see cref="Localization.LocalizationChanged"/> event that occurs when the language is changed.
|
/// Delegate for the <see cref="Localization.LocalizationChanged"/> event that occurs when the language is changed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -167,6 +156,22 @@ public class Localization : IServiceType
|
||||||
Loc.ExportLocalizableForAssembly(this.assembly, ignoreInvalidFunctions);
|
Loc.ExportLocalizableForAssembly(this.assembly, ignoreInvalidFunctions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the <see cref="Localization"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="assetDirectory">Path to Dalamud assets.</param>
|
||||||
|
/// <param name="languageOverride">Optional language override.</param>
|
||||||
|
/// <returns>A new instance.</returns>
|
||||||
|
internal static Localization FromAssets(string assetDirectory, string? languageOverride)
|
||||||
|
{
|
||||||
|
var t = new Localization(Path.Combine(assetDirectory, "UIRes", "loc", "dalamud"), "dalamud_");
|
||||||
|
if (!string.IsNullOrEmpty(languageOverride))
|
||||||
|
t.SetupWithLangCode(languageOverride);
|
||||||
|
else
|
||||||
|
t.SetupWithUiCulture();
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
private string ReadLocData(string langCode)
|
private string ReadLocData(string langCode)
|
||||||
{
|
{
|
||||||
if (this.useEmbedded)
|
if (this.useEmbedded)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Dalamud.Configuration;
|
using Dalamud.Configuration;
|
||||||
using Dalamud.Configuration.Internal;
|
using Dalamud.Configuration.Internal;
|
||||||
|
|
@ -26,6 +27,8 @@ using Dalamud.Plugin.Ipc;
|
||||||
using Dalamud.Plugin.Ipc.Exceptions;
|
using Dalamud.Plugin.Ipc.Exceptions;
|
||||||
using Dalamud.Plugin.Ipc.Internal;
|
using Dalamud.Plugin.Ipc.Internal;
|
||||||
|
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
namespace Dalamud.Plugin;
|
namespace Dalamud.Plugin;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -458,34 +461,52 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa
|
||||||
|
|
||||||
#region Dependency Injection
|
#region Dependency Injection
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Create a new object of the provided type using its default constructor, then inject objects and properties.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="scopedObjects">Objects to inject additionally.</param>
|
|
||||||
/// <typeparam name="T">The type to create.</typeparam>
|
|
||||||
/// <returns>The created and initialized type.</returns>
|
|
||||||
public T? Create<T>(params object[] scopedObjects) where T : class
|
public T? Create<T>(params object[] scopedObjects) where T : class
|
||||||
{
|
{
|
||||||
var svcContainer = Service<IoC.Internal.ServiceContainer>.Get();
|
var t = this.CreateAsync<T>(scopedObjects);
|
||||||
|
t.Wait();
|
||||||
|
|
||||||
return (T)this.plugin.ServiceScope!.CreateAsync(
|
if (t.Exception is { } e)
|
||||||
typeof(T),
|
{
|
||||||
this.GetPublicIocScopes(scopedObjects)).GetAwaiter().GetResult();
|
Log.Error(
|
||||||
|
e,
|
||||||
|
"{who}: Exception during {where}: {what}",
|
||||||
|
this.plugin.Name,
|
||||||
|
nameof(this.Create),
|
||||||
|
typeof(T).FullName ?? typeof(T).Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.IsCompletedSuccessfully ? t.Result : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Inject services into properties on the provided object instance.
|
public async Task<T> CreateAsync<T>(params object[] scopedObjects) where T : class =>
|
||||||
/// </summary>
|
(T)await this.plugin.ServiceScope!.CreateAsync(typeof(T), this.GetPublicIocScopes(scopedObjects));
|
||||||
/// <param name="instance">The instance to inject services into.</param>
|
|
||||||
/// <param name="scopedObjects">Objects to inject additionally.</param>
|
/// <inheritdoc/>
|
||||||
/// <returns>Whether or not the injection succeeded.</returns>
|
|
||||||
public bool Inject(object instance, params object[] scopedObjects)
|
public bool Inject(object instance, params object[] scopedObjects)
|
||||||
{
|
{
|
||||||
return this.plugin.ServiceScope!.InjectPropertiesAsync(
|
var t = this.InjectAsync(instance, scopedObjects);
|
||||||
instance,
|
t.Wait();
|
||||||
this.GetPublicIocScopes(scopedObjects)).GetAwaiter().GetResult();
|
|
||||||
|
if (t.Exception is { } e)
|
||||||
|
{
|
||||||
|
Log.Error(
|
||||||
|
e,
|
||||||
|
"{who}: Exception during {where}: {what}",
|
||||||
|
this.plugin.Name,
|
||||||
|
nameof(this.Inject),
|
||||||
|
instance.GetType().FullName ?? instance.GetType().Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.IsCompletedSuccessfully;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public Task InjectAsync(object instance, params object[] scopedObjects) =>
|
||||||
|
this.plugin.ServiceScope!.InjectPropertiesAsync(instance, this.GetPublicIocScopes(scopedObjects));
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>Unregister the plugin and dispose all references.</summary>
|
/// <summary>Unregister the plugin and dispose all references.</summary>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Dalamud.Configuration;
|
using Dalamud.Configuration;
|
||||||
using Dalamud.Game.Text;
|
using Dalamud.Game.Text;
|
||||||
|
|
@ -304,14 +305,30 @@ public interface IDalamudPluginInterface
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="scopedObjects">Objects to inject additionally.</param>
|
/// <param name="scopedObjects">Objects to inject additionally.</param>
|
||||||
/// <typeparam name="T">The type to create.</typeparam>
|
/// <typeparam name="T">The type to create.</typeparam>
|
||||||
/// <returns>The created and initialized type.</returns>
|
/// <returns>The created and initialized type, or <c>null</c> on failure.</returns>
|
||||||
T? Create<T>(params object[] scopedObjects) where T : class;
|
T? Create<T>(params object[] scopedObjects) where T : class;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new object of the provided type using its default constructor, then inject objects and properties.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="scopedObjects">Objects to inject additionally.</param>
|
||||||
|
/// <typeparam name="T">The type to create.</typeparam>
|
||||||
|
/// <returns>A task representing the created and initialized type.</returns>
|
||||||
|
Task<T> CreateAsync<T>(params object[] scopedObjects) where T : class;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Inject services into properties on the provided object instance.
|
/// Inject services into properties on the provided object instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="instance">The instance to inject services into.</param>
|
/// <param name="instance">The instance to inject services into.</param>
|
||||||
/// <param name="scopedObjects">Objects to inject additionally.</param>
|
/// <param name="scopedObjects">Objects to inject additionally.</param>
|
||||||
/// <returns>Whether or not the injection succeeded.</returns>
|
/// <returns>Whether the injection succeeded.</returns>
|
||||||
bool Inject(object instance, params object[] scopedObjects);
|
bool Inject(object instance, params object[] scopedObjects);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inject services into properties on the provided object instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="instance">The instance to inject services into.</param>
|
||||||
|
/// <param name="scopedObjects">Objects to inject additionally.</param>
|
||||||
|
/// <returns>A <see cref="ValueTask"/> representing the status of the operation.</returns>
|
||||||
|
Task InjectAsync(object instance, params object[] scopedObjects);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ internal sealed class ExposedPlugin(LocalPlugin plugin) : IExposedPlugin
|
||||||
public bool HasMainUi => plugin.DalamudInterface?.LocalUiBuilder.HasMainUi ?? false;
|
public bool HasMainUi => plugin.DalamudInterface?.LocalUiBuilder.HasMainUi ?? false;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public bool HasConfigUi => plugin.DalamudInterface?.LocalUiBuilder.HasMainUi ?? false;
|
public bool HasConfigUi => plugin.DalamudInterface?.LocalUiBuilder.HasConfigUi ?? false;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void OpenMainUi()
|
public void OpenMainUi()
|
||||||
|
|
|
||||||
|
|
@ -258,12 +258,6 @@ internal class PluginManager : IInternalDisposableService
|
||||||
/// <returns>If the manifest is visible.</returns>
|
/// <returns>If the manifest is visible.</returns>
|
||||||
public static bool IsManifestVisible(RemotePluginManifest manifest)
|
public static bool IsManifestVisible(RemotePluginManifest manifest)
|
||||||
{
|
{
|
||||||
var configuration = Service<DalamudConfiguration>.Get();
|
|
||||||
|
|
||||||
// Hidden by user
|
|
||||||
if (configuration.HiddenPluginInternalName.Contains(manifest.InternalName))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Hidden by manifest
|
// Hidden by manifest
|
||||||
return !manifest.IsHide;
|
return !manifest.IsHide;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -417,24 +417,16 @@ internal class LocalPlugin : IDisposable
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (this.manifest.LoadSync && this.manifest.LoadRequiredState is 0 or 1)
|
var forceFrameworkThread = this.manifest.LoadSync && this.manifest.LoadRequiredState is 0 or 1;
|
||||||
{
|
var newInstanceTask = forceFrameworkThread ? framework.RunOnFrameworkThread(Create) : Create();
|
||||||
var newInstance = await framework.RunOnFrameworkThread(
|
this.instance = await newInstanceTask.ConfigureAwait(false);
|
||||||
() => this.ServiceScope.CreateAsync(
|
|
||||||
this.pluginType!,
|
async Task<IDalamudPlugin> Create() =>
|
||||||
this.DalamudInterface!)).ConfigureAwait(false);
|
(IDalamudPlugin)await this.ServiceScope!.CreateAsync(this.pluginType!, this.DalamudInterface!);
|
||||||
|
|
||||||
this.instance = newInstance as IDalamudPlugin;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.instance =
|
|
||||||
await this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!) as IDalamudPlugin;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error(ex, "Exception in plugin constructor");
|
Log.Error(ex, "Exception during plugin initialization");
|
||||||
this.instance = null;
|
this.instance = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
48
Dalamud/Plugin/Services/INamePlateGui.cs
Normal file
48
Dalamud/Plugin/Services/INamePlateGui.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using Dalamud.Game.Gui.NamePlate;
|
||||||
|
|
||||||
|
namespace Dalamud.Plugin.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Class used to modify the data used when rendering nameplates.
|
||||||
|
/// </summary>
|
||||||
|
public interface INamePlateGui
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The delegate used for receiving nameplate update events.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">An object containing information about the pending data update.</param>
|
||||||
|
/// <param name="handlers>">A list of handlers used for updating nameplate data.</param>
|
||||||
|
public delegate void OnPlateUpdateDelegate(
|
||||||
|
INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An event which fires when nameplate data is updated and at least one nameplate has important updates. The
|
||||||
|
/// subscriber is provided with a list of handlers for nameplates with important updates.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Fires after <see cref="OnDataUpdate"/>.
|
||||||
|
/// </remarks>
|
||||||
|
event OnPlateUpdateDelegate? OnNamePlateUpdate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An event which fires when nameplate data is updated. The subscriber is provided with a list of handlers for all
|
||||||
|
/// nameplates.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This event is likely to fire every frame even when no nameplates are actually updated, so in most cases
|
||||||
|
/// <see cref="OnNamePlateUpdate"/> is preferred. Fires before <see cref="OnNamePlateUpdate"/>.
|
||||||
|
/// </remarks>
|
||||||
|
event OnPlateUpdateDelegate? OnDataUpdate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Requests that all nameplates should be redrawn on the following frame.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This causes extra work for the game, and should not need to be called every frame. However, it is acceptable to
|
||||||
|
/// call frequently when needed (e.g. in response to a manual settings change by the user) or when necessary (e.g.
|
||||||
|
/// after a change of zone, party type, etc.).
|
||||||
|
/// </remarks>
|
||||||
|
void RequestRedraw();
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
@ -232,6 +232,15 @@ public interface ITextureProvider
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
ISharedImmediateTexture GetFromFile(FileInfo file);
|
ISharedImmediateTexture GetFromFile(FileInfo file);
|
||||||
|
|
||||||
|
/// <summary>Gets a shared texture corresponding to the given file on the filesystem.</summary>
|
||||||
|
/// <param name="fullPath">The file on the filesystem to load. Requires a full path.</param>
|
||||||
|
/// <returns>The shared texture that you may use to obtain the loaded texture wrap and load states.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>This function does not throw exceptions.</para>
|
||||||
|
/// <para>Caching the returned object is not recommended. Performance benefit will be minimal.</para>
|
||||||
|
/// </remarks>
|
||||||
|
ISharedImmediateTexture GetFromFileAbsolute(string fullPath);
|
||||||
|
|
||||||
/// <summary>Gets a shared texture corresponding to the given file of the assembly manifest resources.</summary>
|
/// <summary>Gets a shared texture corresponding to the given file of the assembly manifest resources.</summary>
|
||||||
/// <param name="assembly">The assembly containing manifest resources.</param>
|
/// <param name="assembly">The assembly containing manifest resources.</param>
|
||||||
/// <param name="name">The case-sensitive name of the manifest resource being requested.</param>
|
/// <param name="name">The case-sensitive name of the manifest resource being requested.</param>
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,46 @@
|
||||||
using System.Drawing;
|
using System.Collections.Concurrent;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Drawing;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Forms;
|
using CheapLoc;
|
||||||
|
|
||||||
using Dalamud.Plugin.Internal;
|
using Dalamud.Plugin.Internal;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using Windows.Win32.Foundation;
|
|
||||||
using Windows.Win32.UI.WindowsAndMessaging;
|
using Serilog;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
using TerraFX.Interop.Windows;
|
||||||
|
|
||||||
|
using static TerraFX.Interop.Windows.TASKDIALOG_FLAGS;
|
||||||
|
using static TerraFX.Interop.Windows.Windows;
|
||||||
|
|
||||||
namespace Dalamud;
|
namespace Dalamud;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class providing an early-loading dialog.
|
/// Class providing an early-loading dialog.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class LoadingDialog
|
[SuppressMessage(
|
||||||
|
"StyleCop.CSharp.LayoutRules",
|
||||||
|
"SA1519:Braces should not be omitted from multi-line child statement",
|
||||||
|
Justification = "Multiple fixed blocks")]
|
||||||
|
internal sealed unsafe class LoadingDialog
|
||||||
{
|
{
|
||||||
// TODO: We can't localize any of what's in here at the moment, because Localization is an EarlyLoadedService.
|
private readonly RollingList<string> logs = new(20);
|
||||||
|
|
||||||
private static int wasGloballyHidden = 0;
|
|
||||||
|
|
||||||
private Thread? thread;
|
private Thread? thread;
|
||||||
private TaskDialogButton? inProgressHideButton;
|
private HWND hwndTaskDialog;
|
||||||
private TaskDialogPage? page;
|
|
||||||
private bool canHide;
|
|
||||||
private State currentState = State.LoadingDalamud;
|
|
||||||
private DateTime firstShowTime;
|
private DateTime firstShowTime;
|
||||||
|
private State currentState = State.LoadingDalamud;
|
||||||
|
private bool canHide;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enum representing the state of the dialog.
|
/// Enum representing the state of the dialog.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -38,18 +50,25 @@ internal class LoadingDialog
|
||||||
/// Show a message stating that Dalamud is currently loading.
|
/// Show a message stating that Dalamud is currently loading.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
LoadingDalamud,
|
LoadingDalamud,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Show a message stating that Dalamud is currently loading plugins.
|
/// Show a message stating that Dalamud is currently loading plugins.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
LoadingPlugins,
|
LoadingPlugins,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Show a message stating that Dalamud is currently updating plugins.
|
/// Show a message stating that Dalamud is currently updating plugins.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
AutoUpdatePlugins,
|
AutoUpdatePlugins,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets the queue where log entries that are not processed yet are stored.</summary>
|
||||||
|
public static ConcurrentQueue<(string Line, LogEvent LogEvent)> NewLogEntries { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>Gets a value indicating whether the initial Dalamud loading dialog will not show again until next
|
||||||
|
/// game restart.</summary>
|
||||||
|
public static bool IsGloballyHidden { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the current state of the dialog.
|
/// Gets or sets the current state of the dialog.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -58,13 +77,16 @@ internal class LoadingDialog
|
||||||
get => this.currentState;
|
get => this.currentState;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
if (this.currentState == value)
|
||||||
|
return;
|
||||||
|
|
||||||
this.currentState = value;
|
this.currentState = value;
|
||||||
this.UpdatePage();
|
this.UpdateMainInstructionText();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether or not the dialog can be hidden by the user.
|
/// Gets or sets a value indicating whether the dialog can be hidden by the user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <exception cref="InvalidOperationException">Thrown if called before the dialog has been created.</exception>
|
/// <exception cref="InvalidOperationException">Thrown if called before the dialog has been created.</exception>
|
||||||
public bool CanHide
|
public bool CanHide
|
||||||
|
|
@ -72,8 +94,11 @@ internal class LoadingDialog
|
||||||
get => this.canHide;
|
get => this.canHide;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
if (this.canHide == value)
|
||||||
|
return;
|
||||||
|
|
||||||
this.canHide = value;
|
this.canHide = value;
|
||||||
this.UpdatePage();
|
this.UpdateButtonEnabled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,19 +107,19 @@ internal class LoadingDialog
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Show()
|
public void Show()
|
||||||
{
|
{
|
||||||
if (Volatile.Read(ref wasGloballyHidden) == 1)
|
if (IsGloballyHidden)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.thread?.IsAlive == true)
|
if (this.thread?.IsAlive == true)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.thread = new Thread(this.ThreadStart)
|
this.thread = new Thread(this.ThreadStart)
|
||||||
{
|
{
|
||||||
Name = "Dalamud Loading Dialog",
|
Name = "Dalamud Loading Dialog",
|
||||||
};
|
};
|
||||||
this.thread.SetApartmentState(ApartmentState.STA);
|
this.thread.SetApartmentState(ApartmentState.STA);
|
||||||
this.thread.Start();
|
this.thread.Start();
|
||||||
|
|
||||||
this.firstShowTime = DateTime.Now;
|
this.firstShowTime = DateTime.Now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,150 +128,287 @@ internal class LoadingDialog
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void HideAndJoin()
|
public void HideAndJoin()
|
||||||
{
|
{
|
||||||
if (this.thread == null || !this.thread.IsAlive)
|
IsGloballyHidden = true;
|
||||||
|
if (this.thread?.IsAlive is not true)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.inProgressHideButton?.PerformClick();
|
SendMessageW(this.hwndTaskDialog, WM.WM_CLOSE, default, default);
|
||||||
this.thread!.Join();
|
this.thread.Join();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdatePage()
|
private void UpdateMainInstructionText()
|
||||||
{
|
{
|
||||||
if (this.page == null)
|
if (this.hwndTaskDialog == default)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.page.Heading = this.currentState switch
|
fixed (void* pszText = this.currentState switch
|
||||||
|
{
|
||||||
|
State.LoadingDalamud => Loc.Localize(
|
||||||
|
"LoadingDialogMainInstructionLoadingDalamud",
|
||||||
|
"Dalamud is loading..."),
|
||||||
|
State.LoadingPlugins => Loc.Localize(
|
||||||
|
"LoadingDialogMainInstructionLoadingPlugins",
|
||||||
|
"Waiting for plugins to load..."),
|
||||||
|
State.AutoUpdatePlugins => Loc.Localize(
|
||||||
|
"LoadingDialogMainInstructionAutoUpdatePlugins",
|
||||||
|
"Updating plugins..."),
|
||||||
|
_ => string.Empty, // should not happen
|
||||||
|
})
|
||||||
{
|
{
|
||||||
State.LoadingDalamud => "Dalamud is loading...",
|
SendMessageW(
|
||||||
State.LoadingPlugins => "Waiting for plugins to load...",
|
this.hwndTaskDialog,
|
||||||
State.AutoUpdatePlugins => "Updating plugins...",
|
(uint)TASKDIALOG_MESSAGES.TDM_SET_ELEMENT_TEXT,
|
||||||
_ => throw new ArgumentOutOfRangeException(),
|
(WPARAM)(int)TASKDIALOG_ELEMENTS.TDE_MAIN_INSTRUCTION,
|
||||||
};
|
(LPARAM)pszText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var context = string.Empty;
|
private void UpdateContentText()
|
||||||
if (this.currentState == State.LoadingPlugins)
|
{
|
||||||
|
if (this.hwndTaskDialog == default)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var contentBuilder = new StringBuilder(
|
||||||
|
Loc.Localize(
|
||||||
|
"LoadingDialogContentInfo",
|
||||||
|
"Some of the plugins you have installed through Dalamud are taking a long time to load.\n" +
|
||||||
|
"This is likely normal, please wait a little while longer."));
|
||||||
|
|
||||||
|
if (this.CurrentState == State.LoadingPlugins)
|
||||||
{
|
{
|
||||||
context = "\nPreparing...";
|
|
||||||
|
|
||||||
var tracker = Service<PluginManager>.GetNullable()?.StartupLoadTracking;
|
var tracker = Service<PluginManager>.GetNullable()?.StartupLoadTracking;
|
||||||
if (tracker != null)
|
if (tracker != null)
|
||||||
{
|
{
|
||||||
var nameString = tracker.GetPendingInternalNames()
|
var nameString = string.Join(
|
||||||
.Select(x => tracker.GetPublicName(x))
|
", ",
|
||||||
.Where(x => x != null)
|
tracker.GetPendingInternalNames()
|
||||||
.Aggregate(string.Empty, (acc, x) => acc + x + ", ");
|
.Select(x => tracker.GetPublicName(x))
|
||||||
|
.Where(x => x != null));
|
||||||
|
|
||||||
if (!nameString.IsNullOrEmpty())
|
if (!nameString.IsNullOrEmpty())
|
||||||
context = $"\nWaiting for: {nameString[..^2]}";
|
{
|
||||||
|
contentBuilder
|
||||||
|
.AppendLine()
|
||||||
|
.AppendLine()
|
||||||
|
.Append(
|
||||||
|
string.Format(
|
||||||
|
Loc.Localize("LoadingDialogContentCurrentPlugin", "Waiting for: {0}"),
|
||||||
|
nameString));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add some text if loading takes more than a few minutes
|
// Add some text if loading takes more than a few minutes
|
||||||
if (DateTime.Now - this.firstShowTime > TimeSpan.FromMinutes(3))
|
if (DateTime.Now - this.firstShowTime > TimeSpan.FromMinutes(3))
|
||||||
context += "\nIt's been a while now. Please report this issue on our Discord server.";
|
|
||||||
|
|
||||||
this.page.Text = this.currentState switch
|
|
||||||
{
|
{
|
||||||
State.LoadingDalamud => "Please wait while Dalamud loads...",
|
contentBuilder
|
||||||
State.LoadingPlugins => "Please wait while Dalamud loads plugins...",
|
.AppendLine()
|
||||||
State.AutoUpdatePlugins => "Please wait while Dalamud updates your plugins...",
|
.AppendLine()
|
||||||
_ => throw new ArgumentOutOfRangeException(),
|
.Append(
|
||||||
#pragma warning disable SA1513
|
Loc.Localize(
|
||||||
} + context;
|
"LoadingDialogContentTakingTooLong",
|
||||||
#pragma warning restore SA1513
|
"It's been a while now. Please report this issue on our Discord server."));
|
||||||
|
|
||||||
this.inProgressHideButton!.Enabled = this.canHide;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DialogStatePeriodicUpdate(CancellationToken token)
|
|
||||||
{
|
|
||||||
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(50));
|
|
||||||
while (!token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
await timer.WaitForNextTickAsync(token);
|
|
||||||
this.UpdatePage();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fixed (void* pszText = contentBuilder.ToString())
|
||||||
|
{
|
||||||
|
SendMessageW(
|
||||||
|
this.hwndTaskDialog,
|
||||||
|
(uint)TASKDIALOG_MESSAGES.TDM_SET_ELEMENT_TEXT,
|
||||||
|
(WPARAM)(int)TASKDIALOG_ELEMENTS.TDE_CONTENT,
|
||||||
|
(LPARAM)pszText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateExpandedInformation()
|
||||||
|
{
|
||||||
|
const int maxCharactersPerLine = 80;
|
||||||
|
|
||||||
|
if (NewLogEntries.IsEmpty)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
while (NewLogEntries.TryDequeue(out var e))
|
||||||
|
{
|
||||||
|
var t = e.Line.AsSpan();
|
||||||
|
var first = true;
|
||||||
|
while (!t.IsEmpty)
|
||||||
|
{
|
||||||
|
var i = t.IndexOfAny('\r', '\n');
|
||||||
|
var line = i == -1 ? t : t[..i];
|
||||||
|
t = i == -1 ? ReadOnlySpan<char>.Empty : t[(i + 1)..];
|
||||||
|
if (line.IsEmpty)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sb.Clear();
|
||||||
|
if (first)
|
||||||
|
sb.Append($"{e.LogEvent.Timestamp:HH:mm:ss} | ");
|
||||||
|
else
|
||||||
|
sb.Append(" | ");
|
||||||
|
first = false;
|
||||||
|
if (line.Length < maxCharactersPerLine)
|
||||||
|
sb.Append(line);
|
||||||
|
else
|
||||||
|
sb.Append($"{line[..(maxCharactersPerLine - 3)]}...");
|
||||||
|
this.logs.Add(sb.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Clear();
|
||||||
|
foreach (var l in this.logs)
|
||||||
|
sb.AppendLine(l);
|
||||||
|
|
||||||
|
fixed (void* pszText = sb.ToString())
|
||||||
|
{
|
||||||
|
SendMessageW(
|
||||||
|
this.hwndTaskDialog,
|
||||||
|
(uint)TASKDIALOG_MESSAGES.TDM_SET_ELEMENT_TEXT,
|
||||||
|
(WPARAM)(int)TASKDIALOG_ELEMENTS.TDE_EXPANDED_INFORMATION,
|
||||||
|
(LPARAM)pszText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateButtonEnabled()
|
||||||
|
{
|
||||||
|
if (this.hwndTaskDialog == default)
|
||||||
|
return;
|
||||||
|
|
||||||
|
SendMessageW(this.hwndTaskDialog, (uint)TASKDIALOG_MESSAGES.TDM_ENABLE_BUTTON, IDOK, this.canHide ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HRESULT TaskDialogCallback(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam)
|
||||||
|
{
|
||||||
|
switch ((TASKDIALOG_NOTIFICATIONS)msg)
|
||||||
|
{
|
||||||
|
case TASKDIALOG_NOTIFICATIONS.TDN_CREATED:
|
||||||
|
this.hwndTaskDialog = hwnd;
|
||||||
|
|
||||||
|
this.UpdateMainInstructionText();
|
||||||
|
this.UpdateContentText();
|
||||||
|
this.UpdateExpandedInformation();
|
||||||
|
this.UpdateButtonEnabled();
|
||||||
|
SendMessageW(hwnd, (int)TASKDIALOG_MESSAGES.TDM_SET_PROGRESS_BAR_MARQUEE, 1, 0);
|
||||||
|
|
||||||
|
// Bring to front
|
||||||
|
SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SWP.SWP_NOSIZE | SWP.SWP_NOMOVE);
|
||||||
|
SetWindowPos(hwnd, HWND.HWND_NOTOPMOST, 0, 0, 0, 0, SWP.SWP_NOSIZE | SWP.SWP_NOMOVE);
|
||||||
|
ShowWindow(hwnd, SW.SW_SHOW);
|
||||||
|
SetForegroundWindow(hwnd);
|
||||||
|
SetFocus(hwnd);
|
||||||
|
SetActiveWindow(hwnd);
|
||||||
|
return S.S_OK;
|
||||||
|
|
||||||
|
case TASKDIALOG_NOTIFICATIONS.TDN_DESTROYED:
|
||||||
|
this.hwndTaskDialog = default;
|
||||||
|
return S.S_OK;
|
||||||
|
|
||||||
|
case TASKDIALOG_NOTIFICATIONS.TDN_TIMER:
|
||||||
|
this.UpdateContentText();
|
||||||
|
this.UpdateExpandedInformation();
|
||||||
|
return S.S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return S.S_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ThreadStart()
|
private void ThreadStart()
|
||||||
{
|
{
|
||||||
Application.EnableVisualStyles();
|
|
||||||
|
|
||||||
this.inProgressHideButton = new TaskDialogButton("Hide", this.canHide);
|
|
||||||
|
|
||||||
// We don't have access to the asset service here.
|
// We don't have access to the asset service here.
|
||||||
var workingDirectory = Service<Dalamud>.Get().StartInfo.WorkingDirectory;
|
var workingDirectory = Service<Dalamud>.Get().StartInfo.WorkingDirectory;
|
||||||
TaskDialogIcon? dialogIcon = null;
|
using var extractedIcon =
|
||||||
if (!workingDirectory.IsNullOrEmpty())
|
string.IsNullOrEmpty(workingDirectory)
|
||||||
|
? null
|
||||||
|
: Icon.ExtractAssociatedIcon(Path.Combine(workingDirectory, "Dalamud.Injector.exe"));
|
||||||
|
|
||||||
|
fixed (void* pszEmpty = "-")
|
||||||
|
fixed (void* pszWindowTitle = "Dalamud")
|
||||||
|
fixed (void* pszDalamudBoot = "Dalamud.Boot.dll")
|
||||||
|
fixed (void* pszThemesManifestResourceName = "RT_MANIFEST_THEMES")
|
||||||
|
fixed (void* pszHide = Loc.Localize("LoadingDialogHide", "Hide"))
|
||||||
|
fixed (void* pszShowLatestLogs = Loc.Localize("LoadingDialogShowLatestLogs", "Show Latest Logs"))
|
||||||
|
fixed (void* pszHideLatestLogs = Loc.Localize("LoadingDialogHideLatestLogs", "Hide Latest Logs"))
|
||||||
{
|
{
|
||||||
var extractedIcon = Icon.ExtractAssociatedIcon(Path.Combine(workingDirectory, "Dalamud.Injector.exe"));
|
var taskDialogButton = new TASKDIALOG_BUTTON
|
||||||
if (extractedIcon != null)
|
|
||||||
{
|
{
|
||||||
dialogIcon = new TaskDialogIcon(extractedIcon);
|
nButtonID = IDOK,
|
||||||
|
pszButtonText = (ushort*)pszHide,
|
||||||
|
};
|
||||||
|
var taskDialogConfig = new TASKDIALOGCONFIG
|
||||||
|
{
|
||||||
|
cbSize = (uint)sizeof(TASKDIALOGCONFIG),
|
||||||
|
hwndParent = default,
|
||||||
|
hInstance = (HINSTANCE)Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().ManifestModule),
|
||||||
|
dwFlags = (int)TDF_CAN_BE_MINIMIZED |
|
||||||
|
(int)TDF_SHOW_MARQUEE_PROGRESS_BAR |
|
||||||
|
(int)TDF_EXPAND_FOOTER_AREA |
|
||||||
|
(int)TDF_CALLBACK_TIMER |
|
||||||
|
(extractedIcon is null ? 0 : (int)TDF_USE_HICON_MAIN),
|
||||||
|
dwCommonButtons = 0,
|
||||||
|
pszWindowTitle = (ushort*)pszWindowTitle,
|
||||||
|
pszMainIcon = extractedIcon is null ? TD.TD_INFORMATION_ICON : (ushort*)extractedIcon.Handle,
|
||||||
|
pszMainInstruction = null,
|
||||||
|
pszContent = null,
|
||||||
|
cButtons = 1,
|
||||||
|
pButtons = &taskDialogButton,
|
||||||
|
nDefaultButton = IDOK,
|
||||||
|
cRadioButtons = 0,
|
||||||
|
pRadioButtons = null,
|
||||||
|
nDefaultRadioButton = 0,
|
||||||
|
pszVerificationText = null,
|
||||||
|
pszExpandedInformation = (ushort*)pszEmpty,
|
||||||
|
pszExpandedControlText = (ushort*)pszShowLatestLogs,
|
||||||
|
pszCollapsedControlText = (ushort*)pszHideLatestLogs,
|
||||||
|
pszFooterIcon = null,
|
||||||
|
pszFooter = null,
|
||||||
|
pfCallback = &HResultFuncBinder,
|
||||||
|
lpCallbackData = 0,
|
||||||
|
cxWidth = 360,
|
||||||
|
};
|
||||||
|
|
||||||
|
HANDLE hActCtx = default;
|
||||||
|
GCHandle gch = default;
|
||||||
|
nuint cookie = 0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var actctx = new ACTCTXW
|
||||||
|
{
|
||||||
|
cbSize = (uint)sizeof(ACTCTXW),
|
||||||
|
dwFlags = ACTCTX_FLAG_HMODULE_VALID | ACTCTX_FLAG_RESOURCE_NAME_VALID,
|
||||||
|
lpResourceName = (ushort*)pszThemesManifestResourceName,
|
||||||
|
hModule = GetModuleHandleW((ushort*)pszDalamudBoot),
|
||||||
|
};
|
||||||
|
hActCtx = CreateActCtxW(&actctx);
|
||||||
|
if (hActCtx == default)
|
||||||
|
throw new Win32Exception("CreateActCtxW failure.");
|
||||||
|
|
||||||
|
if (!ActivateActCtx(hActCtx, &cookie))
|
||||||
|
throw new Win32Exception("ActivateActCtx failure.");
|
||||||
|
|
||||||
|
gch = GCHandle.Alloc((Func<HWND, uint, WPARAM, LPARAM, HRESULT>)this.TaskDialogCallback);
|
||||||
|
taskDialogConfig.lpCallbackData = GCHandle.ToIntPtr(gch);
|
||||||
|
TaskDialogIndirect(&taskDialogConfig, null, null, null).ThrowOnError();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error(e, "TaskDialogIndirect failure.");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (gch.IsAllocated)
|
||||||
|
gch.Free();
|
||||||
|
if (cookie != 0)
|
||||||
|
DeactivateActCtx(0, cookie);
|
||||||
|
ReleaseActCtx(hActCtx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogIcon ??= TaskDialogIcon.Information;
|
IsGloballyHidden = true;
|
||||||
this.page = new TaskDialogPage
|
|
||||||
{
|
|
||||||
ProgressBar = new TaskDialogProgressBar(TaskDialogProgressBarState.Marquee),
|
|
||||||
Caption = "Dalamud",
|
|
||||||
Icon = dialogIcon,
|
|
||||||
Buttons = { this.inProgressHideButton },
|
|
||||||
AllowMinimize = false,
|
|
||||||
AllowCancel = false,
|
|
||||||
Expander = new TaskDialogExpander
|
|
||||||
{
|
|
||||||
CollapsedButtonText = "What does this mean?",
|
|
||||||
ExpandedButtonText = "What does this mean?",
|
|
||||||
Text = "Some of the plugins you have installed through Dalamud are taking a long time to load.\n" +
|
|
||||||
"This is likely normal, please wait a little while longer.",
|
|
||||||
},
|
|
||||||
SizeToContent = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.UpdatePage();
|
|
||||||
|
|
||||||
// Call private TaskDialog ctor
|
return;
|
||||||
var ctor = typeof(TaskDialog).GetConstructor(
|
|
||||||
BindingFlags.Instance | BindingFlags.NonPublic,
|
|
||||||
null,
|
|
||||||
Array.Empty<Type>(),
|
|
||||||
null);
|
|
||||||
|
|
||||||
var taskDialog = (TaskDialog)ctor!.Invoke(Array.Empty<object>())!;
|
[UnmanagedCallersOnly]
|
||||||
|
static HRESULT HResultFuncBinder(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam, nint user) =>
|
||||||
this.page.Created += (_, _) =>
|
((Func<HWND, uint, WPARAM, LPARAM, HRESULT>)GCHandle.FromIntPtr(user).Target!)
|
||||||
{
|
.Invoke(hwnd, msg, wParam, lParam);
|
||||||
var hwnd = new HWND(taskDialog.Handle);
|
|
||||||
|
|
||||||
// Bring to front
|
|
||||||
Windows.Win32.PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0,
|
|
||||||
SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOMOVE);
|
|
||||||
Windows.Win32.PInvoke.SetWindowPos(hwnd, HWND.HWND_NOTOPMOST, 0, 0, 0, 0,
|
|
||||||
SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW | SET_WINDOW_POS_FLAGS.SWP_NOSIZE |
|
|
||||||
SET_WINDOW_POS_FLAGS.SWP_NOMOVE);
|
|
||||||
Windows.Win32.PInvoke.SetForegroundWindow(hwnd);
|
|
||||||
Windows.Win32.PInvoke.SetFocus(hwnd);
|
|
||||||
Windows.Win32.PInvoke.SetActiveWindow(hwnd);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Call private "ShowDialogInternal"
|
|
||||||
var showDialogInternal = typeof(TaskDialog).GetMethod(
|
|
||||||
"ShowDialogInternal",
|
|
||||||
BindingFlags.Instance | BindingFlags.NonPublic,
|
|
||||||
null,
|
|
||||||
[typeof(IntPtr), typeof(TaskDialogPage), typeof(TaskDialogStartupLocation)],
|
|
||||||
null);
|
|
||||||
|
|
||||||
var cts = new CancellationTokenSource();
|
|
||||||
_ = this.DialogStatePeriodicUpdate(cts.Token);
|
|
||||||
|
|
||||||
showDialogInternal!.Invoke(
|
|
||||||
taskDialog,
|
|
||||||
[IntPtr.Zero, this.page, TaskDialogStartupLocation.CenterScreen]);
|
|
||||||
|
|
||||||
Interlocked.Exchange(ref wasGloballyHidden, 1);
|
|
||||||
cts.Cancel();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,9 @@ internal static class ServiceManager
|
||||||
private static readonly List<Type> LoadedServices = new();
|
private static readonly List<Type> LoadedServices = new();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new();
|
private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource =
|
||||||
|
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
|
||||||
private static readonly CancellationTokenSource UnloadCancellationTokenSource = new();
|
private static readonly CancellationTokenSource UnloadCancellationTokenSource = new();
|
||||||
|
|
||||||
private static ManualResetEvent unloadResetEvent = new(false);
|
private static ManualResetEvent unloadResetEvent = new(false);
|
||||||
|
|
@ -126,7 +128,13 @@ internal static class ServiceManager
|
||||||
/// <param name="fs">Instance of <see cref="ReliableFileStorage"/>.</param>
|
/// <param name="fs">Instance of <see cref="ReliableFileStorage"/>.</param>
|
||||||
/// <param name="configuration">Instance of <see cref="DalamudConfiguration"/>.</param>
|
/// <param name="configuration">Instance of <see cref="DalamudConfiguration"/>.</param>
|
||||||
/// <param name="scanner">Instance of <see cref="TargetSigScanner"/>.</param>
|
/// <param name="scanner">Instance of <see cref="TargetSigScanner"/>.</param>
|
||||||
public static void InitializeProvidedServices(Dalamud dalamud, ReliableFileStorage fs, DalamudConfiguration configuration, TargetSigScanner scanner)
|
/// <param name="localization">Instance of <see cref="Localization"/>.</param>
|
||||||
|
public static void InitializeProvidedServices(
|
||||||
|
Dalamud dalamud,
|
||||||
|
ReliableFileStorage fs,
|
||||||
|
DalamudConfiguration configuration,
|
||||||
|
TargetSigScanner scanner,
|
||||||
|
Localization localization)
|
||||||
{
|
{
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
lock (LoadedServices)
|
lock (LoadedServices)
|
||||||
|
|
@ -136,6 +144,7 @@ internal static class ServiceManager
|
||||||
ProvideService(configuration);
|
ProvideService(configuration);
|
||||||
ProvideService(new ServiceContainer());
|
ProvideService(new ServiceContainer());
|
||||||
ProvideService(scanner);
|
ProvideService(scanner);
|
||||||
|
ProvideService(localization);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
@ -152,6 +161,7 @@ internal static class ServiceManager
|
||||||
ProvideService(configuration);
|
ProvideService(configuration);
|
||||||
ProvideService(new ServiceContainer());
|
ProvideService(new ServiceContainer());
|
||||||
ProvideService(scanner);
|
ProvideService(scanner);
|
||||||
|
ProvideService(localization);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
void ProvideService<T>(T service) where T : IServiceType => Service<T>.Provide(service);
|
void ProvideService<T>(T service) where T : IServiceType => Service<T>.Provide(service);
|
||||||
|
|
@ -242,19 +252,20 @@ internal static class ServiceManager
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Wait for all blocking constructors to complete first.
|
// Wait for all blocking constructors to complete first.
|
||||||
await WaitWithTimeoutConsent(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x]),
|
await WaitWithTimeoutConsent(
|
||||||
|
blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x]),
|
||||||
LoadingDialog.State.LoadingDalamud);
|
LoadingDialog.State.LoadingDalamud);
|
||||||
|
|
||||||
// All the BlockingEarlyLoadedService constructors have been run,
|
// All the BlockingEarlyLoadedService constructors have been run,
|
||||||
// and blockerTasks now will not change. Now wait for them.
|
// and blockerTasks now will not change. Now wait for them.
|
||||||
// Note that ServiceManager.CallWhenServicesReady does not get to register a blocker.
|
// Note that ServiceManager.CallWhenServicesReady does not get to register a blocker.
|
||||||
await WaitWithTimeoutConsent(blockerTasks,
|
await WaitWithTimeoutConsent(
|
||||||
|
blockerTasks,
|
||||||
LoadingDialog.State.LoadingPlugins);
|
LoadingDialog.State.LoadingPlugins);
|
||||||
|
|
||||||
Log.Verbose("=============== BLOCKINGSERVICES & TASKS INITIALIZED ===============");
|
Log.Verbose("=============== BLOCKINGSERVICES & TASKS INITIALIZED ===============");
|
||||||
Timings.Event("BlockingServices Initialized");
|
Timings.Event("BlockingServices Initialized");
|
||||||
BlockingServicesLoadedTaskCompletionSource.SetResult();
|
BlockingServicesLoadedTaskCompletionSource.SetResult();
|
||||||
loadingDialog.HideAndJoin();
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
@ -269,11 +280,16 @@ internal static class ServiceManager
|
||||||
|
|
||||||
Log.Error(e, "Failed resolving blocking services");
|
Log.Error(e, "Failed resolving blocking services");
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
loadingDialog.HideAndJoin();
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
async Task WaitWithTimeoutConsent(IEnumerable<Task> tasksEnumerable, LoadingDialog.State state)
|
async Task WaitWithTimeoutConsent(IEnumerable<Task> tasksEnumerable, LoadingDialog.State state)
|
||||||
{
|
{
|
||||||
|
loadingDialog.CurrentState = state;
|
||||||
var tasks = tasksEnumerable.AsReadOnlyCollection();
|
var tasks = tasksEnumerable.AsReadOnlyCollection();
|
||||||
if (tasks.Count == 0)
|
if (tasks.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
@ -286,7 +302,6 @@ internal static class ServiceManager
|
||||||
{
|
{
|
||||||
loadingDialog.Show();
|
loadingDialog.Show();
|
||||||
loadingDialog.CanHide = true;
|
loadingDialog.CanHide = true;
|
||||||
loadingDialog.CurrentState = state;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
|
|
||||||
|
|
@ -332,7 +332,7 @@ internal static class Service<T> where T : IServiceType
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
instanceTcs = new TaskCompletionSource<T>();
|
instanceTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
instanceTcs.SetException(new UnloadedException());
|
instanceTcs.SetException(new UnloadedException());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamud
|
||||||
.Where(x => x is not DalamudAsset.Empty4X4)
|
.Where(x => x is not DalamudAsset.Empty4X4)
|
||||||
.Where(x => x.GetAttribute<DalamudAssetAttribute>()?.Required is false)
|
.Where(x => x.GetAttribute<DalamudAssetAttribute>()?.Required is false)
|
||||||
.Select(this.CreateStreamAsync)
|
.Select(this.CreateStreamAsync)
|
||||||
.Select(x => x.ToContentDisposedTask()))
|
.Select(x => x.ToContentDisposedTask(true)))
|
||||||
.ContinueWith(r => Log.Verbose($"Optional assets load state: {r}"));
|
.ContinueWith(r => Log.Verbose($"Optional assets load state: {r}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ internal static class BugBait
|
||||||
Reporter = reporter,
|
Reporter = reporter,
|
||||||
Name = plugin.InternalName,
|
Name = plugin.InternalName,
|
||||||
Version = isTesting ? plugin.TestingAssemblyVersion?.ToString() : plugin.AssemblyVersion.ToString(),
|
Version = isTesting ? plugin.TestingAssemblyVersion?.ToString() : plugin.AssemblyVersion.ToString(),
|
||||||
DalamudHash = Util.GetGitHash(),
|
DalamudHash = Util.GetScmVersion(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (includeException)
|
if (includeException)
|
||||||
|
|
|
||||||
|
|
@ -1,145 +1,40 @@
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
using TerraFX.Interop.Windows;
|
|
||||||
|
|
||||||
namespace Dalamud.Support;
|
namespace Dalamud.Support;
|
||||||
|
|
||||||
/// <summary>Tracks the loaded process modules.</summary>
|
/// <summary>Tracks the loaded process modules.</summary>
|
||||||
[ServiceManager.EarlyLoadedService]
|
internal static unsafe partial class CurrentProcessModules
|
||||||
internal sealed unsafe partial class CurrentProcessModules : IInternalDisposableService
|
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentQueue<string> LogQueue = new();
|
private static ProcessModuleCollection? moduleCollection;
|
||||||
private static readonly SemaphoreSlim LogSemaphore = new(0);
|
|
||||||
|
|
||||||
private static Process? process;
|
|
||||||
private static nint cookie;
|
|
||||||
|
|
||||||
private readonly CancellationTokenSource logTaskStop = new();
|
|
||||||
private readonly Task logTask;
|
|
||||||
|
|
||||||
[ServiceManager.ServiceConstructor]
|
|
||||||
private CurrentProcessModules()
|
|
||||||
{
|
|
||||||
var res = LdrRegisterDllNotification(0, &DllNotificationCallback, 0, out cookie);
|
|
||||||
if (res != STATUS.STATUS_SUCCESS)
|
|
||||||
{
|
|
||||||
Log.Error("{what}: LdrRegisterDllNotification failure: 0x{err}", nameof(CurrentProcessModules), res);
|
|
||||||
cookie = 0;
|
|
||||||
this.logTask = Task.CompletedTask;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logTask = Task.Factory.StartNew(
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
while (!this.logTaskStop.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
LogSemaphore.Wait();
|
|
||||||
while (LogQueue.TryDequeue(out var log))
|
|
||||||
Log.Verbose(log);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
this.logTaskStop.Token,
|
|
||||||
TaskCreationOptions.LongRunning,
|
|
||||||
TaskScheduler.Default);
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum LdrDllNotificationReason : uint
|
|
||||||
{
|
|
||||||
Loaded = 1,
|
|
||||||
Unloaded = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Gets all the loaded modules, up to date.</summary>
|
/// <summary>Gets all the loaded modules, up to date.</summary>
|
||||||
public static ProcessModuleCollection ModuleCollection
|
public static ProcessModuleCollection ModuleCollection
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (cookie == 0)
|
ref var t = ref *GetDllChangedStorage();
|
||||||
|
if (t != 0)
|
||||||
{
|
{
|
||||||
// This service has not been initialized; return a fresh copy without storing it.
|
t = 0;
|
||||||
return Process.GetCurrentProcess().Modules;
|
moduleCollection = null;
|
||||||
|
Log.Verbose("{what}: Fetching fresh copy of current process modules.", nameof(CurrentProcessModules));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process is null)
|
try
|
||||||
Log.Verbose("{what}: Fetchling fresh copy of current process modules.", nameof(CurrentProcessModules));
|
{
|
||||||
|
return moduleCollection ??= Process.GetCurrentProcess().Modules;
|
||||||
return (process ??= Process.GetCurrentProcess()).Modules;
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Verbose(e, "{what}: Failed to fetch module list.", nameof(CurrentProcessModules));
|
||||||
|
return new([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
[LibraryImport("Dalamud.Boot.dll")]
|
||||||
void IInternalDisposableService.DisposeService()
|
private static partial int* GetDllChangedStorage();
|
||||||
{
|
|
||||||
if (Interlocked.Exchange(ref cookie, 0) is var copy and not 0)
|
|
||||||
LdrUnregisterDllNotification(copy);
|
|
||||||
if (!this.logTask.IsCompleted)
|
|
||||||
{
|
|
||||||
this.logTaskStop.Cancel();
|
|
||||||
LogSemaphore.Release();
|
|
||||||
this.logTask.Wait();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[UnmanagedCallersOnly]
|
|
||||||
private static void DllNotificationCallback(
|
|
||||||
LdrDllNotificationReason reason,
|
|
||||||
LdrDllNotificationData* data,
|
|
||||||
nint context) => process = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Registers for notification when a DLL is first loaded.
|
|
||||||
/// This notification occurs before dynamic linking takes place.<br /><br />
|
|
||||||
/// <a href="https://learn.microsoft.com/en-us/windows/win32/devnotes/ldrregisterdllnotification">Docs.</a>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="flags">This parameter must be zero.</param>
|
|
||||||
/// <param name="notificationFunction">A pointer to a callback function to call when the DLL is loaded.</param>
|
|
||||||
/// <param name="context">A pointer to context data for the callback function.</param>
|
|
||||||
/// <param name="cookie">A pointer to a variable to receive an identifier for the callback function.
|
|
||||||
/// This identifier is used to unregister the notification callback function.</param>
|
|
||||||
/// <returns>Returns an NTSTATUS or error code.</returns>
|
|
||||||
[LibraryImport("ntdll.dll", SetLastError = true)]
|
|
||||||
private static partial int LdrRegisterDllNotification(
|
|
||||||
uint flags,
|
|
||||||
delegate* unmanaged<LdrDllNotificationReason, LdrDllNotificationData*, nint, void>
|
|
||||||
notificationFunction,
|
|
||||||
nint context,
|
|
||||||
out nint cookie);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cancels DLL load notification previously registered by calling the LdrRegisterDllNotification function.<br />
|
|
||||||
/// <br />
|
|
||||||
/// <a href="https://learn.microsoft.com/en-us/windows/win32/devnotes/ldrunregisterdllnotification">Docs.</a>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="cookie">A pointer to the callback identifier received from the LdrRegisterDllNotification call
|
|
||||||
/// that registered for notification.
|
|
||||||
/// </param>
|
|
||||||
/// <returns>Returns an NTSTATUS or error code.</returns>
|
|
||||||
[LibraryImport("ntdll.dll", SetLastError = true)]
|
|
||||||
private static partial int LdrUnregisterDllNotification(nint cookie);
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
private struct LdrDllNotificationData
|
|
||||||
{
|
|
||||||
/// <summary>Reserved.</summary>
|
|
||||||
public uint Flags;
|
|
||||||
|
|
||||||
/// <summary>The full path name of the DLL module.</summary>
|
|
||||||
public UNICODE_STRING* FullDllName;
|
|
||||||
|
|
||||||
/// <summary>The base file name of the DLL module.</summary>
|
|
||||||
public UNICODE_STRING* BaseDllName;
|
|
||||||
|
|
||||||
/// <summary>A pointer to the base address for the DLL in memory.</summary>
|
|
||||||
public nint DllBase;
|
|
||||||
|
|
||||||
/// <summary>The size of the DLL image, in bytes.</summary>
|
|
||||||
public uint SizeOfImage;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,8 @@ public static class Troubleshooting
|
||||||
LoadedPlugins = pluginManager?.InstalledPlugins?.Select(x => x.Manifest as LocalPluginManifest)?.OrderByDescending(x => x.InternalName).ToArray(),
|
LoadedPlugins = pluginManager?.InstalledPlugins?.Select(x => x.Manifest as LocalPluginManifest)?.OrderByDescending(x => x.InternalName).ToArray(),
|
||||||
PluginStates = pluginManager?.InstalledPlugins?.Where(x => !x.IsDev).ToDictionary(x => x.Manifest.InternalName, x => x.IsBanned ? "Banned" : x.State.ToString()),
|
PluginStates = pluginManager?.InstalledPlugins?.Where(x => !x.IsDev).ToDictionary(x => x.Manifest.InternalName, x => x.IsBanned ? "Banned" : x.State.ToString()),
|
||||||
EverStartedLoadingPlugins = pluginManager?.InstalledPlugins.Where(x => x.HasEverStartedLoad).Select(x => x.InternalName).ToList(),
|
EverStartedLoadingPlugins = pluginManager?.InstalledPlugins.Where(x => x.HasEverStartedLoad).Select(x => x.InternalName).ToList(),
|
||||||
DalamudVersion = Util.AssemblyVersion,
|
DalamudVersion = Util.GetScmVersion(),
|
||||||
DalamudGitHash = Util.GetGitHash(),
|
DalamudGitHash = Util.GetGitHash() ?? "Unknown",
|
||||||
GameVersion = startInfo.GameVersion?.ToString() ?? "Unknown",
|
GameVersion = startInfo.GameVersion?.ToString() ?? "Unknown",
|
||||||
Language = startInfo.Language.ToString(),
|
Language = startInfo.Language.ToString(),
|
||||||
BetaKey = configuration.DalamudBetaKey,
|
BetaKey = configuration.DalamudBetaKey,
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ public static class AsyncUtils
|
||||||
/// <returns>Returns the first task that completes, according to <see cref="Task.IsCompletedSuccessfully"/>.</returns>
|
/// <returns>Returns the first task that completes, according to <see cref="Task.IsCompletedSuccessfully"/>.</returns>
|
||||||
public static Task<T> FirstSuccessfulTask<T>(ICollection<Task<T>> tasks)
|
public static Task<T> FirstSuccessfulTask<T>(ICollection<Task<T>> tasks)
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<T>();
|
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
var remainingTasks = tasks.Count;
|
var remainingTasks = tasks.Count;
|
||||||
|
|
||||||
foreach (var task in tasks)
|
foreach (var task in tasks)
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,7 @@ internal class DynamicPriorityQueueLoader : IDisposable
|
||||||
params IDisposable?[] disposables)
|
params IDisposable?[] disposables)
|
||||||
: base(basis, cancellationToken, disposables)
|
: base(basis, cancellationToken, disposables)
|
||||||
{
|
{
|
||||||
this.taskCompletionSource = new();
|
this.taskCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
this.immediateLoadFunction = immediateLoadFunction;
|
this.immediateLoadFunction = immediateLoadFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
using Dalamud.Interface.Colors;
|
using Dalamud.Interface.Colors;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Support;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using Lumina.Excel.GeneratedSheets;
|
using Lumina.Excel.GeneratedSheets;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
@ -27,8 +28,6 @@ using Windows.Win32.Storage.FileSystem;
|
||||||
using Windows.Win32.System.Memory;
|
using Windows.Win32.System.Memory;
|
||||||
using Windows.Win32.System.Ole;
|
using Windows.Win32.System.Ole;
|
||||||
|
|
||||||
using Dalamud.Support;
|
|
||||||
|
|
||||||
using static TerraFX.Interop.Windows.Windows;
|
using static TerraFX.Interop.Windows.Windows;
|
||||||
|
|
||||||
using Win32_PInvoke = Windows.Win32.PInvoke;
|
using Win32_PInvoke = Windows.Win32.PInvoke;
|
||||||
|
|
@ -63,6 +62,7 @@ public static class Util
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly Type GenericSpanType = typeof(Span<>);
|
private static readonly Type GenericSpanType = typeof(Span<>);
|
||||||
|
private static string? scmVersionInternal;
|
||||||
private static string? gitHashInternal;
|
private static string? gitHashInternal;
|
||||||
private static int? gitCommitCountInternal;
|
private static int? gitCommitCountInternal;
|
||||||
private static string? gitHashClientStructsInternal;
|
private static string? gitHashClientStructsInternal;
|
||||||
|
|
@ -129,11 +129,28 @@ public static class Util
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the git hash value from the assembly
|
/// Gets the SCM Version from the assembly, or null if it cannot be found. This method will generally return
|
||||||
/// or null if it cannot be found.
|
/// the <c>git describe</c> output for this build, which will be a raw version if this is a stable build or an
|
||||||
|
/// appropriately-annotated version if this is *not* stable. Local builds will return a `Local Build` text string.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The SCM version of the assembly.</returns>
|
||||||
|
public static string GetScmVersion()
|
||||||
|
{
|
||||||
|
if (scmVersionInternal != null) return scmVersionInternal;
|
||||||
|
|
||||||
|
var asm = typeof(Util).Assembly;
|
||||||
|
var attrs = asm.GetCustomAttributes<AssemblyMetadataAttribute>();
|
||||||
|
|
||||||
|
return scmVersionInternal = attrs.First(a => a.Key == "SCMVersion").Value
|
||||||
|
?? asm.GetName().Version!.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the git commit hash value from the assembly or null if it cannot be found. Will be null for Debug builds,
|
||||||
|
/// and will be suffixed with `-dirty` if in release with pending changes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The git hash of the assembly.</returns>
|
/// <returns>The git hash of the assembly.</returns>
|
||||||
public static string GetGitHash()
|
public static string? GetGitHash()
|
||||||
{
|
{
|
||||||
if (gitHashInternal != null)
|
if (gitHashInternal != null)
|
||||||
return gitHashInternal;
|
return gitHashInternal;
|
||||||
|
|
@ -141,15 +158,14 @@ public static class Util
|
||||||
var asm = typeof(Util).Assembly;
|
var asm = typeof(Util).Assembly;
|
||||||
var attrs = asm.GetCustomAttributes<AssemblyMetadataAttribute>();
|
var attrs = asm.GetCustomAttributes<AssemblyMetadataAttribute>();
|
||||||
|
|
||||||
gitHashInternal = attrs.First(a => a.Key == "GitHash").Value;
|
return gitHashInternal = attrs.First(a => a.Key == "GitHash").Value;
|
||||||
|
|
||||||
return gitHashInternal;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the amount of commits in the current branch, or null if undetermined.
|
/// Gets the amount of commits in the current branch, or null if undetermined.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The amount of commits in the current branch.</returns>
|
/// <returns>The amount of commits in the current branch.</returns>
|
||||||
|
[Obsolete($"Planned for removal in API 11. Use {nameof(GetScmVersion)} for version tracking.")]
|
||||||
public static int? GetGitCommitCount()
|
public static int? GetGitCommitCount()
|
||||||
{
|
{
|
||||||
if (gitCommitCountInternal != null)
|
if (gitCommitCountInternal != null)
|
||||||
|
|
@ -171,7 +187,7 @@ public static class Util
|
||||||
/// or null if it cannot be found.
|
/// or null if it cannot be found.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The git hash of the assembly.</returns>
|
/// <returns>The git hash of the assembly.</returns>
|
||||||
public static string GetGitHashClientStructs()
|
public static string? GetGitHashClientStructs()
|
||||||
{
|
{
|
||||||
if (gitHashClientStructsInternal != null)
|
if (gitHashClientStructsInternal != null)
|
||||||
return gitHashClientStructsInternal;
|
return gitHashClientStructsInternal;
|
||||||
|
|
@ -270,7 +286,7 @@ public static class Util
|
||||||
{
|
{
|
||||||
if ((mbi.Protect & (1 << i)) == 0)
|
if ((mbi.Protect & (1 << i)) == 0)
|
||||||
continue;
|
continue;
|
||||||
if (c++ == 0)
|
if (c++ != 0)
|
||||||
sb.Append(" | ");
|
sb.Append(" | ");
|
||||||
sb.Append(PageProtectionFlagNames[i]);
|
sb.Append(PageProtectionFlagNames[i]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 07f7b3fda2da0f9f8891241ca70839c2acdf2c4a
|
Subproject commit 731e3ab0006ce56c4fe789aee148bc967965b914
|
||||||
Loading…
Add table
Add a link
Reference in a new issue