Compare commits

..

No commits in common. "master" and "13.0.0.13" have entirely different histories.

189 changed files with 3517 additions and 4174 deletions

View file

@ -1,8 +1,9 @@
name: Build Dalamud name: Build Dalamud
on: [push, pull_request, workflow_dispatch] on: [push, pull_request, workflow_dispatch]
# Globally blocking because of git pushes in deploy step
concurrency: concurrency:
group: build_dalamud_${{ github.ref_name }} group: build_dalamud_${{ github.repository_owner }}
cancel-in-progress: false cancel-in-progress: false
jobs: jobs:
@ -23,7 +24,7 @@ jobs:
uses: microsoft/setup-msbuild@v1.0.2 uses: microsoft/setup-msbuild@v1.0.2
- uses: actions/setup-dotnet@v3 - uses: actions/setup-dotnet@v3
with: with:
dotnet-version: '10.0.100' dotnet-version: '9.0.200'
- name: Define VERSION - name: Define VERSION
run: | run: |
$env:COMMIT = $env:GITHUB_SHA.Substring(0, 7) $env:COMMIT = $env:GITHUB_SHA.Substring(0, 7)

View file

@ -1,8 +1,8 @@
name: Rollup changes to next version name: Rollup changes to next version
on: on:
# push: push:
# branches: branches:
# - master - master
workflow_dispatch: workflow_dispatch:
jobs: jobs:

View file

@ -1,57 +1,19 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"title": "Build Schema",
"$ref": "#/definitions/build",
"definitions": { "definitions": {
"Host": { "build": {
"type": "string", "type": "object",
"enum": [
"AppVeyor",
"AzurePipelines",
"Bamboo",
"Bitbucket",
"Bitrise",
"GitHubActions",
"GitLab",
"Jenkins",
"Rider",
"SpaceAutomation",
"TeamCity",
"Terminal",
"TravisCI",
"VisualStudio",
"VSCode"
]
},
"ExecutableTarget": {
"type": "string",
"enum": [
"CI",
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"Restore",
"SetCILogging",
"Test"
]
},
"Verbosity": {
"type": "string",
"description": "",
"enum": [
"Verbose",
"Normal",
"Minimal",
"Quiet"
]
},
"NukeBuild": {
"properties": { "properties": {
"Configuration": {
"type": "string",
"description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
"enum": [
"Debug",
"Release"
]
},
"Continue": { "Continue": {
"type": "boolean", "type": "boolean",
"description": "Indicates to continue a previously failed build attempt" "description": "Indicates to continue a previously failed build attempt"
@ -61,8 +23,29 @@
"description": "Shows the help text for this build assembly" "description": "Shows the help text for this build assembly"
}, },
"Host": { "Host": {
"type": "string",
"description": "Host for execution. Default is 'automatic'", "description": "Host for execution. Default is 'automatic'",
"$ref": "#/definitions/Host" "enum": [
"AppVeyor",
"AzurePipelines",
"Bamboo",
"Bitbucket",
"Bitrise",
"GitHubActions",
"GitLab",
"Jenkins",
"Rider",
"SpaceAutomation",
"TeamCity",
"Terminal",
"TravisCI",
"VisualStudio",
"VSCode"
]
},
"IsDocsBuild": {
"type": "boolean",
"description": "Whether we are building for documentation - emits generated files"
}, },
"NoLogo": { "NoLogo": {
"type": "boolean", "type": "boolean",
@ -91,46 +74,65 @@
"type": "array", "type": "array",
"description": "List of targets to be skipped. Empty list skips all dependencies", "description": "List of targets to be skipped. Empty list skips all dependencies",
"items": { "items": {
"$ref": "#/definitions/ExecutableTarget" "type": "string",
"enum": [
"CI",
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"CompileInjectorBoot",
"Restore",
"SetCILogging",
"Test"
]
} }
}, },
"Solution": {
"type": "string",
"description": "Path to a solution file that is automatically loaded"
},
"Target": { "Target": {
"type": "array", "type": "array",
"description": "List of targets to be invoked. Default is '{default_target}'", "description": "List of targets to be invoked. Default is '{default_target}'",
"items": { "items": {
"$ref": "#/definitions/ExecutableTarget" "type": "string",
"enum": [
"CI",
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"CompileInjectorBoot",
"Restore",
"SetCILogging",
"Test"
]
} }
}, },
"Verbosity": { "Verbosity": {
"type": "string",
"description": "Logging verbosity during build execution. Default is 'Normal'", "description": "Logging verbosity during build execution. Default is 'Normal'",
"$ref": "#/definitions/Verbosity"
}
}
}
},
"allOf": [
{
"properties": {
"Configuration": {
"type": "string",
"description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
"enum": [ "enum": [
"Debug", "Minimal",
"Release" "Normal",
"Quiet",
"Verbose"
] ]
},
"IsDocsBuild": {
"type": "boolean",
"description": "Whether we are building for documentation - emits generated files"
},
"Solution": {
"type": "string",
"description": "Path to a solution file that is automatically loaded"
} }
} }
},
{
"$ref": "#/definitions/NukeBuild"
} }
] }
} }

View file

@ -108,11 +108,6 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
config.LogName = json.value("LogName", config.LogName); config.LogName = json.value("LogName", config.LogName);
config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory); config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory);
config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory); config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory);
if (json.contains("TempDirectory") && !json["TempDirectory"].is_null()) {
config.TempDirectory = json.value("TempDirectory", config.TempDirectory);
}
config.Language = json.value("Language", config.Language); config.Language = json.value("Language", config.Language);
config.Platform = json.value("Platform", config.Platform); config.Platform = json.value("Platform", config.Platform);
config.GameVersion = json.value("GameVersion", config.GameVersion); config.GameVersion = json.value("GameVersion", config.GameVersion);

View file

@ -44,7 +44,6 @@ struct DalamudStartInfo {
std::string ConfigurationPath; std::string ConfigurationPath;
std::string LogPath; std::string LogPath;
std::string LogName; std::string LogName;
std::string TempDirectory;
std::string PluginDirectory; std::string PluginDirectory;
std::string AssetDirectory; std::string AssetDirectory;
ClientLanguage Language = ClientLanguage::English; ClientLanguage Language = ClientLanguage::English;

View file

@ -124,7 +124,6 @@ static DalamudExpected<void> append_injector_launch_args(std::vector<std::wstrin
args.emplace_back(L"--logname=\"" + unicode::convert<std::wstring>(g_startInfo.LogName) + L"\""); args.emplace_back(L"--logname=\"" + unicode::convert<std::wstring>(g_startInfo.LogName) + L"\"");
args.emplace_back(L"--dalamud-plugin-directory=\"" + unicode::convert<std::wstring>(g_startInfo.PluginDirectory) + L"\""); args.emplace_back(L"--dalamud-plugin-directory=\"" + unicode::convert<std::wstring>(g_startInfo.PluginDirectory) + L"\"");
args.emplace_back(L"--dalamud-asset-directory=\"" + unicode::convert<std::wstring>(g_startInfo.AssetDirectory) + L"\""); args.emplace_back(L"--dalamud-asset-directory=\"" + unicode::convert<std::wstring>(g_startInfo.AssetDirectory) + L"\"");
args.emplace_back(L"--dalamud-temp-directory=\"" + unicode::convert<std::wstring>(g_startInfo.TempDirectory) + L"\"");
args.emplace_back(std::format(L"--dalamud-client-language={}", static_cast<int>(g_startInfo.Language))); args.emplace_back(std::format(L"--dalamud-client-language={}", static_cast<int>(g_startInfo.Language)));
args.emplace_back(std::format(L"--dalamud-delay-initialize={}", g_startInfo.DelayInitializeMs)); args.emplace_back(std::format(L"--dalamud-delay-initialize={}", g_startInfo.DelayInitializeMs));
// NoLoadPlugins/NoLoadThirdPartyPlugins: supplied from DalamudCrashHandler // NoLoadPlugins/NoLoadThirdPartyPlugins: supplied from DalamudCrashHandler

View file

@ -34,12 +34,6 @@ public record DalamudStartInfo
/// </summary> /// </summary>
public string? ConfigurationPath { get; set; } public string? ConfigurationPath { get; set; }
/// <summary>
/// Gets or sets the directory for temporary files. This directory needs to exist and be writable to the user.
/// It should also be predictable and easy for launchers to find.
/// </summary>
public string? TempDirectory { get; set; }
/// <summary> /// <summary>
/// Gets or sets the path of the log files. /// Gets or sets the path of the log files.
/// </summary> /// </summary>

View file

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>{8874326B-E755-4D13-90B4-59AB263A3E6B}</ProjectGuid>
<RootNamespace>Dalamud_Injector_Boot</RootNamespace>
<Configuration Condition=" '$(Configuration)'=='' ">Debug</Configuration>
<Platform>x64</Platform>
</PropertyGroup>
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<TargetName>Dalamud.Injector</TargetName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<LinkIncremental>false</LinkIncremental>
<CharacterSet>Unicode</CharacterSet>
<OutDir>..\bin\$(Configuration)\</OutDir>
<IntDir>obj\$(Configuration)\</IntDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp23</LanguageStandard>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<PreprocessorDefinitions>CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalLibraryDirectories>..\lib\CoreCLR;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<ProgramDatabaseFile>$(OutDir)$(TargetName).Boot.pdb</ProgramDatabaseFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>false</IntrinsicFunctions>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<EnableCOMDATFolding>false</EnableCOMDATFolding>
<OptimizeReferences>false</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ItemGroup>
<Content Include="..\lib\CoreCLR\nethost\nethost.dll">
<Link>nethost.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Image Include="dalamud.ico" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="resources.rc" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\Dalamud.Boot\logging.cpp" />
<ClCompile Include="..\Dalamud.Boot\unicode.cpp" />
<ClCompile Include="..\lib\CoreCLR\boot.cpp" />
<ClCompile Include="..\lib\CoreCLR\CoreCLR.cpp" />
<ClCompile Include="main.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\Dalamud.Boot\logging.h" />
<ClInclude Include="..\Dalamud.Boot\unicode.h" />
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h" />
<ClInclude Include="..\lib\CoreCLR\core\coreclr_delegates.h" />
<ClInclude Include="..\lib\CoreCLR\core\hostfxr.h" />
<ClInclude Include="..\lib\CoreCLR\nethost\nethost.h" />
<ClInclude Include="pch.h" />
</ItemGroup>
<Target Name="RemoveExtraFiles" AfterTargets="PostBuildEvent">
<Delete Files="$(OutDir)$(TargetName).lib" />
<Delete Files="$(OutDir)$(TargetName).exp" />
</Target>
</Project>

View file

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{4faac519-3a73-4b2b-96e7-fb597f02c0be}</UniqueIdentifier>
<Extensions>ico;rc</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<Image Include="dalamud.ico">
<Filter>Resource Files</Filter>
</Image>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="resources.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\lib\CoreCLR\boot.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\lib\CoreCLR\CoreCLR.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Dalamud.Boot\logging.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Dalamud.Boot\unicode.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\lib\CoreCLR\nethost\nethost.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\lib\CoreCLR\core\hostfxr.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\lib\CoreCLR\core\coreclr_delegates.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\Dalamud.Boot\logging.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\Dalamud.Boot\unicode.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>

View file

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Before After
Before After

View file

@ -0,0 +1,48 @@
#define WIN32_LEAN_AND_MEAN
#include <filesystem>
#include <Windows.h>
#include <shellapi.h>
#include "..\Dalamud.Boot\logging.h"
#include "..\lib\CoreCLR\CoreCLR.h"
#include "..\lib\CoreCLR\boot.h"
int wmain(int argc, wchar_t** argv)
{
// Take care: don't redirect stderr/out here, we need to write our pid to stdout for XL to read
//logging::start_file_logging("dalamud.injector.boot.log", false);
logging::I("Dalamud Injector, (c) 2021 XIVLauncher Contributors");
logging::I("Built at : " __DATE__ "@" __TIME__);
wchar_t _module_path[MAX_PATH];
GetModuleFileNameW(NULL, _module_path, sizeof _module_path / 2);
std::filesystem::path fs_module_path(_module_path);
std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.runtimeconfig.json").c_str());
std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.dll").c_str());
// =========================================================================== //
void* entrypoint_vfn;
const auto result = InitializeClrAndGetEntryPoint(
GetModuleHandleW(nullptr),
false,
runtimeconfig_path,
module_path,
L"Dalamud.Injector.EntryPoint, Dalamud.Injector",
L"Main",
L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector",
&entrypoint_vfn);
if (FAILED(result))
return result;
typedef int (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, wchar_t**);
custom_component_entry_point_fn entrypoint_fn = reinterpret_cast<custom_component_entry_point_fn>(entrypoint_vfn);
logging::I("Running Dalamud Injector...");
const auto ret = entrypoint_fn(argc, argv);
logging::I("Done!");
return ret;
}

View file

@ -0,0 +1 @@
#pragma once

View file

@ -0,0 +1 @@
MAINICON ICON "dalamud.ico"

View file

@ -13,13 +13,12 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Output"> <PropertyGroup Label="Output">
<OutputType>Exe</OutputType> <OutputType>Library</OutputType>
<OutputPath>..\bin\$(Configuration)\</OutputPath> <OutputPath>..\bin\$(Configuration)\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles> <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly> <ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<ApplicationIcon>dalamud.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Documentation"> <PropertyGroup Label="Documentation">

View file

@ -25,20 +25,34 @@ namespace Dalamud.Injector
/// <summary> /// <summary>
/// Entrypoint to the program. /// Entrypoint to the program.
/// </summary> /// </summary>
public sealed class Program public sealed class EntryPoint
{ {
/// <summary>
/// A delegate used during initialization of the CLR from Dalamud.Injector.Boot.
/// </summary>
/// <param name="argc">Count of arguments.</param>
/// <param name="argvPtr">char** string arguments.</param>
/// <returns>Return value (HRESULT).</returns>
public delegate int MainDelegate(int argc, IntPtr argvPtr);
/// <summary> /// <summary>
/// Start the Dalamud injector. /// Start the Dalamud injector.
/// </summary> /// </summary>
/// <param name="argsArray">Command line arguments.</param> /// <param name="argc">Count of arguments.</param>
/// <param name="argvPtr">byte** string arguments.</param>
/// <returns>Return value (HRESULT).</returns> /// <returns>Return value (HRESULT).</returns>
public static int Main(string[] argsArray) public static int Main(int argc, IntPtr argvPtr)
{ {
try try
{ {
// API14 TODO: Refactor List<string> args = new(argc);
var args = argsArray.ToList();
args.Insert(0, Assembly.GetExecutingAssembly().Location); unsafe
{
var argv = (IntPtr*)argvPtr;
for (var i = 0; i < argc; i++)
args.Add(Marshal.PtrToStringUni(argv[i]));
}
Init(args); Init(args);
args.Remove("-v"); // Remove "verbose" flag args.Remove("-v"); // Remove "verbose" flag
@ -291,7 +305,6 @@ namespace Dalamud.Injector
var configurationPath = startInfo.ConfigurationPath; var configurationPath = startInfo.ConfigurationPath;
var pluginDirectory = startInfo.PluginDirectory; var pluginDirectory = startInfo.PluginDirectory;
var assetDirectory = startInfo.AssetDirectory; var assetDirectory = startInfo.AssetDirectory;
var tempDirectory = startInfo.TempDirectory;
var delayInitializeMs = startInfo.DelayInitializeMs; var delayInitializeMs = startInfo.DelayInitializeMs;
var logName = startInfo.LogName; var logName = startInfo.LogName;
var logPath = startInfo.LogPath; var logPath = startInfo.LogPath;
@ -322,10 +335,6 @@ namespace Dalamud.Injector
{ {
assetDirectory = args[i][key.Length..]; assetDirectory = args[i][key.Length..];
} }
else if (args[i].StartsWith(key = "--dalamud-temp-directory="))
{
tempDirectory = args[i][key.Length..];
}
else if (args[i].StartsWith(key = "--dalamud-delay-initialize=")) else if (args[i].StartsWith(key = "--dalamud-delay-initialize="))
{ {
delayInitializeMs = int.Parse(args[i][key.Length..]); delayInitializeMs = int.Parse(args[i][key.Length..]);
@ -438,7 +447,6 @@ namespace Dalamud.Injector
startInfo.ConfigurationPath = configurationPath; startInfo.ConfigurationPath = configurationPath;
startInfo.PluginDirectory = pluginDirectory; startInfo.PluginDirectory = pluginDirectory;
startInfo.AssetDirectory = assetDirectory; startInfo.AssetDirectory = assetDirectory;
startInfo.TempDirectory = tempDirectory;
startInfo.Language = clientLanguage; startInfo.Language = clientLanguage;
startInfo.Platform = platform; startInfo.Platform = platform;
startInfo.DelayInitializeMs = delayInitializeMs; startInfo.DelayInitializeMs = delayInitializeMs;

View file

@ -1,4 +1,4 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.1.32319.34 VisualStudioVersion = 17.1.32319.34
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
@ -7,6 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig .editorconfig = .editorconfig
.gitignore = .gitignore .gitignore = .gitignore
tools\BannedSymbols.txt = tools\BannedSymbols.txt tools\BannedSymbols.txt = tools\BannedSymbols.txt
targets\Dalamud.Plugin.Bootstrap.targets = targets\Dalamud.Plugin.Bootstrap.targets
targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.targets
tools\dalamud.ruleset = tools\dalamud.ruleset tools\dalamud.ruleset = tools\dalamud.ruleset
Directory.Build.props = Directory.Build.props Directory.Build.props = Directory.Build.props
Directory.Packages.props = Directory.Packages.props Directory.Packages.props = Directory.Packages.props
@ -25,6 +27,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Boot", "Dalamud.Boo
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Injector", "Dalamud.Injector\Dalamud.Injector.csproj", "{5B832F73-5F54-4ADC-870F-D0095EF72C9A}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Injector", "Dalamud.Injector\Dalamud.Injector.csproj", "{5B832F73-5F54-4ADC-870F-D0095EF72C9A}"
EndProject EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Injector.Boot", "Dalamud.Injector.Boot\Dalamud.Injector.Boot.vcxproj", "{8874326B-E755-4D13-90B4-59AB263A3E6B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}"
@ -45,6 +49,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator", "lib\FFX
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{A6AA1C3F-9470-4922-9D3F-D4549657AB22}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{A6AA1C3F-9470-4922-9D3F-D4549657AB22}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Injector", "Injector", "{19775C83-7117-4A5F-AA00-18889F46A490}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{8F079208-C227-4D96-9427-2BEBE0003944}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{8F079208-C227-4D96-9427-2BEBE0003944}"
EndProject EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimgui", "external\cimgui\cimgui.vcxproj", "{8430077C-F736-4246-A052-8EA1CECE844E}" Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimgui", "external\cimgui\cimgui.vcxproj", "{8430077C-F736-4246-A052-8EA1CECE844E}"
@ -97,6 +103,10 @@ Global
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.ActiveCfg = Debug|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.Build.0 = Debug|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.ActiveCfg = Release|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.Build.0 = Release|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.ActiveCfg = Debug|x64 {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.ActiveCfg = Debug|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|x64 {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64 {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64
@ -178,6 +188,8 @@ Global
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{5B832F73-5F54-4ADC-870F-D0095EF72C9A} = {19775C83-7117-4A5F-AA00-18889F46A490}
{8874326B-E755-4D13-90B4-59AB263A3E6B} = {19775C83-7117-4A5F-AA00-18889F46A490}
{4AFDB34A-7467-4D41-B067-53BC4101D9D0} = {8F079208-C227-4D96-9427-2BEBE0003944} {4AFDB34A-7467-4D41-B067-53BC4101D9D0} = {8F079208-C227-4D96-9427-2BEBE0003944}
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {8BBACF2D-7AB8-4610-A115-0E363D35C291} {C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}
{E0D51896-604F-4B40-8CFE-51941607B3A1} = {8BBACF2D-7AB8-4610-A115-0E363D35C291} {E0D51896-604F-4B40-8CFE-51941607B3A1} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}

View file

@ -495,16 +495,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
#pragma warning restore SA1516 #pragma warning restore SA1516
#pragma warning restore SA1600 #pragma warning restore SA1600
/// <summary>
/// Gets or sets a list of badge passwords used to unlock badges.
/// </summary>
public List<string> UsedBadgePasswords { get; set; } = [];
/// <summary>
/// Gets or sets a value indicating whether badges should be shown on the title screen.
/// </summary>
public bool ShowBadgesOnTitleScreen { get; set; } = true;
/// <summary> /// <summary>
/// Load a configuration from the provided path. /// Load a configuration from the provided path.
/// </summary> /// </summary>

View file

@ -11,7 +11,7 @@ namespace Dalamud.Configuration;
/// <summary> /// <summary>
/// Configuration to store settings for a dalamud plugin. /// Configuration to store settings for a dalamud plugin.
/// </summary> /// </summary>
[Api15ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")] [Api13ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")]
public sealed class PluginConfigurations public sealed class PluginConfigurations
{ {
private readonly DirectoryInfo configDirectory; private readonly DirectoryInfo configDirectory;

View file

@ -9,7 +9,6 @@ using System.Threading.Tasks;
using Dalamud.Common; using Dalamud.Common;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Hooking.Internal.Verification;
using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal;
using Dalamud.Storage; using Dalamud.Storage;
using Dalamud.Utility; using Dalamud.Utility;
@ -74,11 +73,6 @@ internal sealed unsafe class Dalamud : IServiceType
scanner, scanner,
Localization.FromAssets(info.AssetDirectory!, configuration.LanguageOverride)); Localization.FromAssets(info.AssetDirectory!, configuration.LanguageOverride));
using (Timings.Start("HookVerifier Init"))
{
HookVerifier.Initialize(scanner);
}
// Set up FFXIVClientStructs // Set up FFXIVClientStructs
this.SetupClientStructsResolver(cacheDir); this.SetupClientStructsResolver(cacheDir);

View file

@ -6,7 +6,7 @@
<PropertyGroup Label="Feature"> <PropertyGroup Label="Feature">
<Description>XIV Launcher addon framework</Description> <Description>XIV Launcher addon framework</Description>
<DalamudVersion>14.0.0.2</DalamudVersion> <DalamudVersion>13.0.0.13</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion> <AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version> <Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion> <FileVersion>$(DalamudVersion)</FileVersion>
@ -73,6 +73,8 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MinSharp" /> <PackageReference Include="MinSharp" />
<PackageReference Include="SharpDX.Direct3D11" />
<PackageReference Include="SharpDX.Mathematics" />
<PackageReference Include="Newtonsoft.Json" /> <PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Serilog" /> <PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Sinks.Async" /> <PackageReference Include="Serilog.Sinks.Async" />
@ -83,8 +85,11 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="System.Collections.Immutable" />
<PackageReference Include="System.Drawing.Common" />
<PackageReference Include="System.Reactive" /> <PackageReference Include="System.Reactive" />
<PackageReference Include="System.Reflection.MetadataLoadContext" /> <PackageReference Include="System.Reflection.MetadataLoadContext" />
<PackageReference Include="System.Resources.Extensions" />
<PackageReference Include="TerraFX.Interop.Windows" /> <PackageReference Include="TerraFX.Interop.Windows" />
</ItemGroup> </ItemGroup>
@ -117,8 +122,6 @@
<Content Include="licenses.txt"> <Content Include="licenses.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<None Remove="Interface\ImGuiBackend\Renderers\gaussian.hlsl" />
<None Remove="Interface\ImGuiBackend\Renderers\fullscreen-quad.hlsl.bytes" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -223,4 +226,9 @@
<!-- writes the attribute to the customAssemblyInfo file --> <!-- writes the attribute to the customAssemblyInfo file -->
<WriteCodeFragment Language="C#" OutputFile="$(CustomAssemblyInfoFile)" AssemblyAttributes="@(AssemblyAttributes)" /> <WriteCodeFragment Language="C#" OutputFile="$(CustomAssemblyInfoFile)" AssemblyAttributes="@(AssemblyAttributes)" />
</Target> </Target>
<!-- Copy plugin .targets folder into distrib -->
<Target Name="CopyPluginTargets" AfterTargets="Build">
<Copy SourceFiles="$(ProjectDir)\..\targets\Dalamud.Plugin.targets;$(ProjectDir)\..\targets\Dalamud.Plugin.Bootstrap.targets" DestinationFolder="$(OutDir)\targets" />
</Target>
</Project> </Project>

View file

@ -73,7 +73,7 @@ public enum DalamudAsset
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "troubleIcon.png")] [DalamudAssetPath("UIRes", "troubleIcon.png")]
TroubleIcon = 1006, TroubleIcon = 1006,
/// <summary> /// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The plugin trouble icon overlay. /// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The plugin trouble icon overlay.
/// </summary> /// </summary>
@ -124,13 +124,6 @@ public enum DalamudAsset
[DalamudAssetPath("UIRes", "tsmShade.png")] [DalamudAssetPath("UIRes", "tsmShade.png")]
TitleScreenMenuShade = 1013, TitleScreenMenuShade = 1013,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: Atlas containing badges.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "badgeAtlas.png")]
BadgeAtlas = 1015,
/// <summary> /// <summary>
/// <see cref="DalamudAssetPurpose.Font"/>: Noto Sans CJK JP Medium. /// <see cref="DalamudAssetPurpose.Font"/>: Noto Sans CJK JP Medium.
/// </summary> /// </summary>
@ -158,7 +151,7 @@ public enum DalamudAsset
/// <see cref="DalamudAssetPurpose.Font"/>: FontAwesome Free Solid. /// <see cref="DalamudAssetPurpose.Font"/>: FontAwesome Free Solid.
/// </summary> /// </summary>
[DalamudAsset(DalamudAssetPurpose.Font)] [DalamudAsset(DalamudAssetPurpose.Font)]
[DalamudAssetPath("UIRes", "FontAwesome710FreeSolid.otf")] [DalamudAssetPath("UIRes", "FontAwesomeFreeSolid.otf")]
FontAwesomeFreeSolid = 2003, FontAwesomeFreeSolid = 2003,
/// <summary> /// <summary>

View file

@ -82,13 +82,8 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
var tsInfo = var tsInfo =
JsonConvert.DeserializeObject<LauncherTroubleshootingInfo>( JsonConvert.DeserializeObject<LauncherTroubleshootingInfo>(
dalamud.StartInfo.TroubleshootingPackData); dalamud.StartInfo.TroubleshootingPackData);
this.HasModifiedGameDataFiles =
// Don't fail for IndexIntegrityResult.Exception, since the check during launch has a very small timeout tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception;
// this.HasModifiedGameDataFiles =
// tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed;
// TODO: Put above back when check in XL is fixed
this.HasModifiedGameDataFiles = false;
if (this.HasModifiedGameDataFiles) if (this.HasModifiedGameDataFiles)
Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData); Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData);

View file

@ -192,8 +192,8 @@ 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}]",
Versioning.GetScmVersion(), Util.GetScmVersion(),
Versioning.GetGitHashClientStructs(), Util.GetGitHashClientStructs(),
FFXIVClientStructs.ThisAssembly.Git.Commits); FFXIVClientStructs.ThisAssembly.Git.Commits);
dalamud.WaitForUnload(); dalamud.WaitForUnload();
@ -263,7 +263,7 @@ public sealed class EntryPoint
var symbolPath = Path.Combine(info.AssetDirectory, "UIRes", "pdb"); var symbolPath = Path.Combine(info.AssetDirectory, "UIRes", "pdb");
var searchPath = $".;{symbolPath}"; var searchPath = $".;{symbolPath}";
var currentProcess = Windows.Win32.PInvoke.GetCurrentProcess(); var currentProcess = Windows.Win32.PInvoke.GetCurrentProcess_SafeHandle();
// Remove any existing Symbol Handler and Init a new one with our search path added // Remove any existing Symbol Handler and Init a new one with our search path added
Windows.Win32.PInvoke.SymCleanup(currentProcess); Windows.Win32.PInvoke.SymCleanup(currentProcess);

View file

@ -0,0 +1,107 @@
using System.Runtime.CompilerServices;
using System.Threading;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
namespace Dalamud.Game.Addon;
/// <summary>Argument pool for Addon Lifecycle services.</summary>
[ServiceManager.EarlyLoadedService]
internal sealed class AddonLifecyclePooledArgs : IServiceType
{
private readonly AddonSetupArgs?[] addonSetupArgPool = new AddonSetupArgs?[64];
private readonly AddonFinalizeArgs?[] addonFinalizeArgPool = new AddonFinalizeArgs?[64];
private readonly AddonDrawArgs?[] addonDrawArgPool = new AddonDrawArgs?[64];
private readonly AddonUpdateArgs?[] addonUpdateArgPool = new AddonUpdateArgs?[64];
private readonly AddonRefreshArgs?[] addonRefreshArgPool = new AddonRefreshArgs?[64];
private readonly AddonRequestedUpdateArgs?[] addonRequestedUpdateArgPool = new AddonRequestedUpdateArgs?[64];
private readonly AddonReceiveEventArgs?[] addonReceiveEventArgPool = new AddonReceiveEventArgs?[64];
[ServiceManager.ServiceConstructor]
private AddonLifecyclePooledArgs()
{
}
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonSetupArgs> Rent(out AddonSetupArgs arg) => new(out arg, this.addonSetupArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonFinalizeArgs> Rent(out AddonFinalizeArgs arg) => new(out arg, this.addonFinalizeArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonDrawArgs> Rent(out AddonDrawArgs arg) => new(out arg, this.addonDrawArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonUpdateArgs> Rent(out AddonUpdateArgs arg) => new(out arg, this.addonUpdateArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonRefreshArgs> Rent(out AddonRefreshArgs arg) => new(out arg, this.addonRefreshArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonRequestedUpdateArgs> Rent(out AddonRequestedUpdateArgs arg) =>
new(out arg, this.addonRequestedUpdateArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonReceiveEventArgs> Rent(out AddonReceiveEventArgs arg) =>
new(out arg, this.addonReceiveEventArgPool);
/// <summary>Returns the object to the pool on dispose.</summary>
/// <typeparam name="T">The type.</typeparam>
public readonly ref struct PooledEntry<T>
where T : AddonArgs, new()
{
private readonly Span<T> pool;
private readonly T obj;
/// <summary>Initializes a new instance of the <see cref="PooledEntry{T}"/> struct.</summary>
/// <param name="arg">An instance of the argument.</param>
/// <param name="pool">The pool to rent from and return to.</param>
public PooledEntry(out T arg, Span<T> pool)
{
this.pool = pool;
foreach (ref var item in pool)
{
if (Interlocked.Exchange(ref item, null) is { } v)
{
this.obj = arg = v;
return;
}
}
this.obj = arg = new();
}
/// <summary>Returns the item to the pool.</summary>
public void Dispose()
{
var tmp = this.obj;
foreach (ref var item in this.pool)
{
if (Interlocked.Exchange(ref item, tmp) is not { } tmp2)
return;
tmp = tmp2;
}
}
}
}

View file

@ -5,24 +5,19 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary> /// <summary>
/// Base class for AddonLifecycle AddonArgTypes. /// Base class for AddonLifecycle AddonArgTypes.
/// </summary> /// </summary>
public class AddonArgs public abstract unsafe class AddonArgs
{ {
/// <summary> /// <summary>
/// Constant string representing the name of an addon that is invalid. /// Constant string representing the name of an addon that is invalid.
/// </summary> /// </summary>
public const string InvalidAddon = "NullAddon"; public const string InvalidAddon = "NullAddon";
/// <summary> private string? addonName;
/// Initializes a new instance of the <see cref="AddonArgs"/> class.
/// </summary>
internal AddonArgs()
{
}
/// <summary> /// <summary>
/// Gets the name of the addon this args referrers to. /// Gets the name of the addon this args referrers to.
/// </summary> /// </summary>
public string AddonName { get; private set; } = InvalidAddon; public string AddonName => this.GetAddonName();
/// <summary> /// <summary>
/// Gets the pointer to the addons AtkUnitBase. /// Gets the pointer to the addons AtkUnitBase.
@ -30,17 +25,55 @@ public class AddonArgs
public AtkUnitBasePtr Addon public AtkUnitBasePtr Addon
{ {
get; get;
internal set internal set;
{
field = value;
if (!this.Addon.IsNull && !string.IsNullOrEmpty(value.Name))
this.AddonName = value.Name;
}
} }
/// <summary> /// <summary>
/// Gets the type of these args. /// Gets the type of these args.
/// </summary> /// </summary>
public virtual AddonArgsType Type => AddonArgsType.Generic; public abstract AddonArgsType Type { get; }
/// <summary>
/// Checks if addon name matches the given span of char.
/// </summary>
/// <param name="name">The name to check.</param>
/// <returns>Whether it is the case.</returns>
internal bool IsAddon(string name)
{
if (this.Addon.IsNull)
return false;
if (name.Length is 0 or > 32)
return false;
if (string.IsNullOrEmpty(this.Addon.Name))
return false;
return name == this.Addon.Name;
}
/// <summary>
/// Clears this AddonArgs values.
/// </summary>
internal virtual void Clear()
{
this.addonName = null;
this.Addon = 0;
}
/// <summary>
/// Helper method for ensuring the name of the addon is valid.
/// </summary>
/// <returns>The name of the addon for this object. <see cref="InvalidAddon"/> when invalid.</returns>
private string GetAddonName()
{
if (this.Addon.IsNull) return InvalidAddon;
var name = this.Addon.Name;
if (string.IsNullOrEmpty(name))
return InvalidAddon;
return this.addonName ??= name;
}
} }

View file

@ -1,22 +0,0 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Close events.
/// </summary>
public class AddonCloseArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonCloseArgs"/> class.
/// </summary>
internal AddonCloseArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Close;
/// <summary>
/// Gets or sets a value indicating whether the window should fire the callback method on close.
/// </summary>
public bool FireCallback { get; set; }
}

View file

@ -0,0 +1,24 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Draw events.
/// </summary>
public class AddonDrawArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonDrawArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonDrawArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Draw;
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -0,0 +1,24 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for ReceiveEvent events.
/// </summary>
public class AddonFinalizeArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonFinalizeArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonFinalizeArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Finalize;
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -1,32 +0,0 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Hide events.
/// </summary>
public class AddonHideArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonHideArgs"/> class.
/// </summary>
internal AddonHideArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Hide;
/// <summary>
/// Gets or sets a value indicating whether to call the hide callback handler when this hides.
/// </summary>
public bool CallHideCallback { get; set; }
/// <summary>
/// Gets or sets the flags that the window will set when it Shows/Hides.
/// </summary>
public uint SetShowHideFlags { get; set; }
/// <summary>
/// Gets or sets a value indicating whether something for this event message.
/// </summary>
internal bool UnknownBool { get; set; }
}

View file

@ -3,12 +3,13 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary> /// <summary>
/// Addon argument data for ReceiveEvent events. /// Addon argument data for ReceiveEvent events.
/// </summary> /// </summary>
public class AddonReceiveEventArgs : AddonArgs public class AddonReceiveEventArgs : AddonArgs, ICloneable
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AddonReceiveEventArgs"/> class. /// Initializes a new instance of the <see cref="AddonReceiveEventArgs"/> class.
/// </summary> /// </summary>
internal AddonReceiveEventArgs() [Obsolete("Not intended for public construction.", false)]
public AddonReceiveEventArgs()
{ {
} }
@ -31,7 +32,23 @@ public class AddonReceiveEventArgs : AddonArgs
public nint AtkEvent { get; set; } public nint AtkEvent { get; set; }
/// <summary> /// <summary>
/// Gets or sets the pointer to an AtkEventData for this event message. /// Gets or sets the pointer to a block of data for this event message.
/// </summary> /// </summary>
public nint AtkEventData { get; set; } public nint Data { get; set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.AtkEventType = default;
this.EventParam = default;
this.AtkEvent = default;
this.Data = default;
}
} }

View file

@ -1,22 +1,17 @@
using System.Collections.Generic;
using Dalamud.Game.NativeWrapper;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using FFXIVClientStructs.Interop;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary> /// <summary>
/// Addon argument data for Refresh events. /// Addon argument data for Refresh events.
/// </summary> /// </summary>
public class AddonRefreshArgs : AddonArgs public class AddonRefreshArgs : AddonArgs, ICloneable
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AddonRefreshArgs"/> class. /// Initializes a new instance of the <see cref="AddonRefreshArgs"/> class.
/// </summary> /// </summary>
internal AddonRefreshArgs() [Obsolete("Not intended for public construction.", false)]
public AddonRefreshArgs()
{ {
} }
@ -36,32 +31,19 @@ public class AddonRefreshArgs : AddonArgs
/// <summary> /// <summary>
/// Gets the AtkValues in the form of a span. /// Gets the AtkValues in the form of a span.
/// </summary> /// </summary>
[Obsolete("Pending removal, Use AtkValueEnumerable instead.")]
[Api15ToDo("Make this internal, remove obsolete")]
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
/// <summary> /// <inheritdoc cref="ICloneable.Clone"/>
/// Gets an enumerable collection of <see cref="AtkValuePtr"/> of the event's AtkValues. public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone();
/// </summary>
/// <returns>
/// An <see cref="IEnumerable{T}"/> of <see cref="AtkValuePtr"/> corresponding to the event's AtkValues.
/// </returns>
public IEnumerable<AtkValuePtr> AtkValueEnumerable
{
get
{
for (var i = 0; i < this.AtkValueCount; i++)
{
AtkValuePtr ptr;
unsafe
{
#pragma warning disable CS0618 // Type or member is obsolete
ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i));
#pragma warning restore CS0618 // Type or member is obsolete
}
yield return ptr; /// <inheritdoc cref="Clone"/>
} object ICloneable.Clone() => this.Clone();
}
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.AtkValueCount = default;
this.AtkValues = default;
} }
} }

View file

@ -3,12 +3,13 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary> /// <summary>
/// Addon argument data for OnRequestedUpdate events. /// Addon argument data for OnRequestedUpdate events.
/// </summary> /// </summary>
public class AddonRequestedUpdateArgs : AddonArgs public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AddonRequestedUpdateArgs"/> class. /// Initializes a new instance of the <see cref="AddonRequestedUpdateArgs"/> class.
/// </summary> /// </summary>
internal AddonRequestedUpdateArgs() [Obsolete("Not intended for public construction.", false)]
public AddonRequestedUpdateArgs()
{ {
} }
@ -24,4 +25,18 @@ public class AddonRequestedUpdateArgs : AddonArgs
/// Gets or sets the StringArrayData** for this event. /// Gets or sets the StringArrayData** for this event.
/// </summary> /// </summary>
public nint StringArrayData { get; set; } public nint StringArrayData { get; set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.NumberArrayData = default;
this.StringArrayData = default;
}
} }

View file

@ -1,22 +1,17 @@
using System.Collections.Generic;
using Dalamud.Game.NativeWrapper;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using FFXIVClientStructs.Interop;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary> /// <summary>
/// Addon argument data for Setup events. /// Addon argument data for Setup events.
/// </summary> /// </summary>
public class AddonSetupArgs : AddonArgs public class AddonSetupArgs : AddonArgs, ICloneable
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AddonSetupArgs"/> class. /// Initializes a new instance of the <see cref="AddonSetupArgs"/> class.
/// </summary> /// </summary>
internal AddonSetupArgs() [Obsolete("Not intended for public construction.", false)]
public AddonSetupArgs()
{ {
} }
@ -36,32 +31,19 @@ public class AddonSetupArgs : AddonArgs
/// <summary> /// <summary>
/// Gets the AtkValues in the form of a span. /// Gets the AtkValues in the form of a span.
/// </summary> /// </summary>
[Obsolete("Pending removal, Use AtkValueEnumerable instead.")]
[Api15ToDo("Make this internal, remove obsolete")]
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
/// <summary> /// <inheritdoc cref="ICloneable.Clone"/>
/// Gets an enumerable collection of <see cref="AtkValuePtr"/> of the event's AtkValues. public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone();
/// </summary>
/// <returns>
/// An <see cref="IEnumerable{T}"/> of <see cref="AtkValuePtr"/> corresponding to the event's AtkValues.
/// </returns>
public IEnumerable<AtkValuePtr> AtkValueEnumerable
{
get
{
for (var i = 0; i < this.AtkValueCount; i++)
{
AtkValuePtr ptr;
unsafe
{
#pragma warning disable CS0618 // Type or member is obsolete
ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i));
#pragma warning restore CS0618 // Type or member is obsolete
}
yield return ptr; /// <inheritdoc cref="Clone"/>
} object ICloneable.Clone() => this.Clone();
}
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.AtkValueCount = default;
this.AtkValues = default;
} }
} }

View file

@ -1,27 +0,0 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Show events.
/// </summary>
public class AddonShowArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonShowArgs"/> class.
/// </summary>
internal AddonShowArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Show;
/// <summary>
/// Gets or sets a value indicating whether the window should play open sound effects.
/// </summary>
public bool SilenceOpenSoundEffect { get; set; }
/// <summary>
/// Gets or sets the flags that the window will unset when it Shows/Hides.
/// </summary>
public uint UnsetShowHideFlags { get; set; }
}

View file

@ -0,0 +1,45 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Update events.
/// </summary>
public class AddonUpdateArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonUpdateArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonUpdateArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Update;
/// <summary>
/// Gets the time since the last update.
/// </summary>
public float TimeDelta
{
get => this.TimeDeltaInternal;
init => this.TimeDeltaInternal = value;
}
/// <summary>
/// Gets or sets the time since the last update.
/// </summary>
internal float TimeDeltaInternal { get; set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.TimeDeltaInternal = default;
}
}

View file

@ -5,43 +5,38 @@
/// </summary> /// </summary>
public enum AddonArgsType public enum AddonArgsType
{ {
/// <summary>
/// Generic arg type that contains no meaningful data.
/// </summary>
Generic,
/// <summary> /// <summary>
/// Contains argument data for Setup. /// Contains argument data for Setup.
/// </summary> /// </summary>
Setup, Setup,
/// <summary>
/// Contains argument data for Update.
/// </summary>
Update,
/// <summary>
/// Contains argument data for Draw.
/// </summary>
Draw,
/// <summary>
/// Contains argument data for Finalize.
/// </summary>
Finalize,
/// <summary> /// <summary>
/// Contains argument data for RequestedUpdate. /// Contains argument data for RequestedUpdate.
/// </summary> /// </summary>
RequestedUpdate, RequestedUpdate,
/// <summary> /// <summary>
/// Contains argument data for Refresh. /// Contains argument data for Refresh.
/// </summary> /// </summary>
Refresh, Refresh,
/// <summary> /// <summary>
/// Contains argument data for ReceiveEvent. /// Contains argument data for ReceiveEvent.
/// </summary> /// </summary>
ReceiveEvent, ReceiveEvent,
/// <summary>
/// Contains argument data for Show.
/// </summary>
Show,
/// <summary>
/// Contains argument data for Hide.
/// </summary>
Hide,
/// <summary>
/// Contains argument data for Close.
/// </summary>
Close,
} }

View file

@ -16,7 +16,7 @@ public enum AddonEvent
/// </summary> /// </summary>
/// <seealso cref="AddonSetupArgs"/> /// <seealso cref="AddonSetupArgs"/>
PreSetup, PreSetup,
/// <summary> /// <summary>
/// An event that is fired after an addon has finished its initial setup. This event is particularly useful for /// 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 /// developers seeking to add custom elements to now-initialized and populated node lists, as well as reading data
@ -29,6 +29,7 @@ public enum AddonEvent
/// An event that is fired before an addon begins its update cycle via <see cref="AtkUnitBase.Update"/>. This event /// 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. /// is fired every frame that an addon is loaded, regardless of visibility.
/// </summary> /// </summary>
/// <seealso cref="AddonUpdateArgs"/>
PreUpdate, PreUpdate,
/// <summary> /// <summary>
@ -41,6 +42,7 @@ public enum AddonEvent
/// An event that is fired before an addon begins drawing to screen via <see cref="AtkUnitBase.Draw"/>. Unlike /// 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. /// <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>
@ -60,8 +62,9 @@ public enum AddonEvent
/// <br /> /// <br />
/// As this is part of the destruction process for an addon, this event does not have an associated Post event. /// As this is part of the destruction process for an addon, this event does not have an associated Post event.
/// </remarks> /// </remarks>
/// <seealso cref="AddonFinalizeArgs"/>
PreFinalize, PreFinalize,
/// <summary> /// <summary>
/// An event that is fired before a call to <see cref="AtkUnitBase.OnRequestedUpdate"/> is made in response to a /// 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 /// change in the subscribed <see cref="AddonRequestedUpdateArgs.NumberArrayData"/> or
@ -78,13 +81,13 @@ public enum AddonEvent
/// to the Free Company's overview. /// to the Free Company's overview.
/// </example> /// </example>
PreRequestedUpdate, PreRequestedUpdate,
/// <summary> /// <summary>
/// An event that is fired after an addon has finished processing an <c>ArrayData</c> update. /// An event that is fired after an addon has finished processing an <c>ArrayData</c> update.
/// See <see cref="PreRequestedUpdate"/> for more information. /// See <see cref="PreRequestedUpdate"/> for more information.
/// </summary> /// </summary>
PostRequestedUpdate, PostRequestedUpdate,
/// <summary> /// <summary>
/// An event that is fired before an addon calls its <see cref="AtkUnitManager.RefreshAddon"/> method. Refreshes are /// An event that is fired before an addon calls its <see cref="AtkUnitManager.RefreshAddon"/> method. Refreshes are
/// generally triggered in response to certain user interactions such as changing tabs, and are primarily used to /// generally triggered in response to certain user interactions such as changing tabs, and are primarily used to
@ -93,13 +96,13 @@ public enum AddonEvent
/// <seealso cref="AddonRefreshArgs"/> /// <seealso cref="AddonRefreshArgs"/>
/// <seealso cref="PostRefresh"/> /// <seealso cref="PostRefresh"/>
PreRefresh, PreRefresh,
/// <summary> /// <summary>
/// An event that is fired after an addon has finished its refresh. /// An event that is fired after an addon has finished its refresh.
/// See <see cref="PreRefresh"/> for more information. /// See <see cref="PreRefresh"/> for more information.
/// </summary> /// </summary>
PostRefresh, PostRefresh,
/// <summary> /// <summary>
/// An event that is fired before an addon begins processing a user-driven event via /// 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 /// <see cref="AtkEventListener.ReceiveEvent"/>, such as mousing over an element or clicking a button. This event
@ -109,98 +112,10 @@ public enum AddonEvent
/// <seealso cref="AddonReceiveEventArgs"/> /// <seealso cref="AddonReceiveEventArgs"/>
/// <seealso cref="PostReceiveEvent"/> /// <seealso cref="PostReceiveEvent"/>
PreReceiveEvent, PreReceiveEvent,
/// <summary> /// <summary>
/// An event that is fired after an addon finishes calling its <see cref="AtkEventListener.ReceiveEvent"/> method. /// An event that is fired after an addon finishes calling its <see cref="AtkEventListener.ReceiveEvent"/> method.
/// See <see cref="PreReceiveEvent"/> for more information. /// See <see cref="PreReceiveEvent"/> for more information.
/// </summary> /// </summary>
PostReceiveEvent, PostReceiveEvent,
/// <summary>
/// An event that is fired before an addon processes its open method.
/// </summary>
PreOpen,
/// <summary>
/// An event that is fired after an addon has processed its open method.
/// </summary>
PostOpen,
/// <summary>
/// An even that is fired before an addon processes its Close method.
/// </summary>
PreClose,
/// <summary>
/// An event that is fired after an addon has processed its Close method.
/// </summary>
PostClose,
/// <summary>
/// An event that is fired before an addon processes its Show method.
/// </summary>
PreShow,
/// <summary>
/// An event that is fired after an addon has processed its Show method.
/// </summary>
PostShow,
/// <summary>
/// An event that is fired before an addon processes its Hide method.
/// </summary>
PreHide,
/// <summary>
/// An event that is fired after an addon has processed its Hide method.
/// </summary>
PostHide,
/// <summary>
/// An event that is fired before an addon processes its OnMove method.
/// OnMove is triggered only when a move is completed.
/// </summary>
PreMove,
/// <summary>
/// An event that is fired after an addon has processed its OnMove method.
/// OnMove is triggered only when a move is completed.
/// </summary>
PostMove,
/// <summary>
/// An event that is fired before an addon processes its MouseOver method.
/// </summary>
PreMouseOver,
/// <summary>
/// An event that is fired after an addon has processed its MouseOver method.
/// </summary>
PostMouseOver,
/// <summary>
/// An event that is fired before an addon processes its MouseOut method.
/// </summary>
PreMouseOut,
/// <summary>
/// An event that is fired after an addon has processed its MouseOut method.
/// </summary>
PostMouseOut,
/// <summary>
/// An event that is fired before an addon processes its Focus method.
/// </summary>
/// <remarks>
/// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows.
/// </remarks>
PreFocus,
/// <summary>
/// An event that is fired after an addon has processed its Focus method.
/// </summary>
/// <remarks>
/// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows.
/// </remarks>
PostFocus,
} }

View file

@ -1,15 +1,16 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Hooking.Internal;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle; namespace Dalamud.Game.Addon.Lifecycle;
@ -20,36 +21,75 @@ namespace Dalamud.Game.Addon.Lifecycle;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal unsafe class AddonLifecycle : IInternalDisposableService internal unsafe class AddonLifecycle : IInternalDisposableService
{ {
/// <summary>
/// Gets a list of all allocated addon virtual tables.
/// </summary>
public static readonly List<AddonVirtualTable> AllocatedTables = [];
private static readonly ModuleLog Log = new("AddonLifecycle"); private static readonly ModuleLog Log = new("AddonLifecycle");
private Hook<AtkUnitBase.Delegates.Initialize>? onInitializeAddonHook; [ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
[ServiceManager.ServiceDependency]
private readonly AddonLifecyclePooledArgs argsPool = Service<AddonLifecyclePooledArgs>.Get();
private readonly nint disallowedReceiveEventAddress;
private readonly AddonLifecycleAddressResolver address;
private readonly AddonSetupHook<AtkUnitBase.Delegates.OnSetup> onAddonSetupHook;
private readonly Hook<AddonFinalizeDelegate> onAddonFinalizeHook;
private readonly CallHook<AtkUnitBase.Delegates.Draw> onAddonDrawHook;
private readonly CallHook<AtkUnitBase.Delegates.Update> onAddonUpdateHook;
private readonly Hook<AtkUnitManager.Delegates.RefreshAddon> onAddonRefreshHook;
private readonly CallHook<AtkUnitBase.Delegates.OnRequestedUpdate> onAddonRequestedUpdateHook;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private AddonLifecycle() private AddonLifecycle(TargetSigScanner sigScanner)
{ {
this.onInitializeAddonHook = Hook<AtkUnitBase.Delegates.Initialize>.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize); this.address = new AddonLifecycleAddressResolver();
this.onInitializeAddonHook.Enable(); this.address.Setup(sigScanner);
this.disallowedReceiveEventAddress = (nint)AtkUnitBase.StaticVirtualTablePointer->ReceiveEvent;
var refreshAddonAddress = (nint)RaptureAtkUnitManager.StaticVirtualTablePointer->RefreshAddon;
this.onAddonSetupHook = new AddonSetupHook<AtkUnitBase.Delegates.OnSetup>(this.address.AddonSetup, this.OnAddonSetup);
this.onAddonFinalizeHook = Hook<AddonFinalizeDelegate>.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize);
this.onAddonDrawHook = new CallHook<AtkUnitBase.Delegates.Draw>(this.address.AddonDraw, this.OnAddonDraw);
this.onAddonUpdateHook = new CallHook<AtkUnitBase.Delegates.Update>(this.address.AddonUpdate, this.OnAddonUpdate);
this.onAddonRefreshHook = Hook<AtkUnitManager.Delegates.RefreshAddon>.FromAddress(refreshAddonAddress, this.OnAddonRefresh);
this.onAddonRequestedUpdateHook = new CallHook<AtkUnitBase.Delegates.OnRequestedUpdate>(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate);
this.onAddonSetupHook.Enable();
this.onAddonFinalizeHook.Enable();
this.onAddonDrawHook.Enable();
this.onAddonUpdateHook.Enable();
this.onAddonRefreshHook.Enable();
this.onAddonRequestedUpdateHook.Enable();
} }
private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase);
/// <summary>
/// Gets a list of all AddonLifecycle ReceiveEvent Listener Hooks.
/// </summary>
internal List<AddonLifecycleReceiveEventListener> ReceiveEventListeners { get; } = new();
/// <summary> /// <summary>
/// Gets a list of all AddonLifecycle Event Listeners. /// Gets a list of all AddonLifecycle Event Listeners.
/// </summary> <br/> /// </summary>
/// Mapping is: EventType -> AddonName -> ListenerList internal List<AddonLifecycleEventListener> EventListeners { get; } = new();
internal Dictionary<AddonEvent, Dictionary<string, HashSet<AddonLifecycleEventListener>>> EventListeners { get; } = [];
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.onInitializeAddonHook?.Dispose(); this.onAddonSetupHook.Dispose();
this.onInitializeAddonHook = null; this.onAddonFinalizeHook.Dispose();
this.onAddonDrawHook.Dispose();
this.onAddonUpdateHook.Dispose();
this.onAddonRefreshHook.Dispose();
this.onAddonRequestedUpdateHook.Dispose();
AllocatedTables.ForEach(entry => entry.Dispose()); foreach (var receiveEventListener in this.ReceiveEventListeners)
AllocatedTables.Clear(); {
receiveEventListener.Dispose();
}
} }
/// <summary> /// <summary>
@ -58,20 +98,20 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to register.</param> /// <param name="listener">The listener to register.</param>
internal void RegisterListener(AddonLifecycleEventListener listener) internal void RegisterListener(AddonLifecycleEventListener listener)
{ {
if (!this.EventListeners.ContainsKey(listener.EventType)) this.framework.RunOnTick(() =>
{ {
if (!this.EventListeners.TryAdd(listener.EventType, [])) this.EventListeners.Add(listener);
return;
} // If we want receive event messages have an already active addon, enable the receive event hook.
// If the addon isn't active yet, we'll grab the hook when it sets up.
// Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName)) {
{ if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, [])) {
return; receiveEventListener.TryEnable();
} }
}
this.EventListeners[listener.EventType][listener.AddonName].Add(listener); });
} }
/// <summary> /// <summary>
@ -80,13 +120,27 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to unregister.</param> /// <param name="listener">The listener to unregister.</param>
internal void UnregisterListener(AddonLifecycleEventListener listener) internal void UnregisterListener(AddonLifecycleEventListener listener)
{ {
if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) // Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update.
listener.Removed = true;
this.framework.RunOnTick(() =>
{ {
if (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) this.EventListeners.Remove(listener);
// If we are disabling an ReceiveEvent listener, check if we should disable the hook.
if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
{ {
addonListener.Remove(listener); // Get the ReceiveEvent Listener for this addon
if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
{
// If there are no other listeners listening for this event, disable the hook.
if (!this.EventListeners.Any(listeners => listeners.AddonName.Contains(listener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent))
{
receiveEventListener.Disable();
}
}
} }
} });
} }
/// <summary> /// <summary>
@ -97,76 +151,226 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="blame">What to blame on errors.</param> /// <param name="blame">What to blame on errors.</param>
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
{ {
// Early return if we don't have any listeners of this type // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better.
if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return; foreach (var listener in this.EventListeners)
// Handle listeners for this event type that don't care which addon is triggering it
if (addonListeners.TryGetValue(string.Empty, out var globalListeners))
{ {
foreach (var listener in globalListeners) if (listener.EventType != eventType)
continue;
// If the listener is pending removal, and is waiting until the next Framework Update, don't invoke listener.
if (listener.Removed)
continue;
// Match on string.empty for listeners that want events for all addons.
if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName))
continue;
try
{ {
try listener.FunctionDelegate.Invoke(eventType, args);
{
listener.FunctionDelegate.Invoke(eventType, args);
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global addon event listener.");
}
} }
} catch (Exception e)
// Handle listeners that are listening for this addon and event type specifically
if (addonListeners.TryGetValue(args.AddonName, out var addonListener))
{
foreach (var listener in addonListener)
{ {
try Log.Error(e, $"Exception in {blame} during {eventType} invoke.");
{
listener.FunctionDelegate.Invoke(eventType, args);
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific addon {args.AddonName}.");
}
} }
} }
} }
/// <summary> private void RegisterReceiveEventHook(AtkUnitBase* addon)
/// Resolves a virtual table address to the original virtual table address.
/// </summary>
/// <param name="tableAddress">The modified address to resolve.</param>
/// <returns>The original address.</returns>
internal AtkUnitBase.AtkUnitBaseVirtualTable* GetOriginalVirtualTable(AtkUnitBase.AtkUnitBaseVirtualTable* tableAddress)
{ {
var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress); // Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener.
if (matchedTable == null) return null; // Disallows hooking the core internal event handler.
var addonName = addon->NameString;
var receiveEventAddress = (nint)addon->VirtualTable->ReceiveEvent;
if (receiveEventAddress != this.disallowedReceiveEventAddress)
{
// If we have a ReceiveEvent listener already made for this hook address, add this addon's name to that handler.
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.FunctionAddress == receiveEventAddress) is { } existingListener)
{
if (!existingListener.AddonNames.Contains(addonName))
{
existingListener.AddonNames.Add(addonName);
}
}
return matchedTable.OriginalVirtualTable; // Else, we have an addon that we don't have the ReceiveEvent for yet, make it.
else
{
this.ReceiveEventListeners.Add(new AddonLifecycleReceiveEventListener(this, addonName, receiveEventAddress));
}
// If we have an active listener for this addon already, we need to activate this hook.
if (this.EventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName))
{
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } receiveEventListener)
{
receiveEventListener.TryEnable();
}
}
}
} }
private void OnAddonInitialize(AtkUnitBase* addon) private void UnregisterReceiveEventHook(string addonName)
{
// Remove this addons ReceiveEvent Registration
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener)
{
eventListener.AddonNames.Remove(addonName);
// If there are no more listeners let's remove and dispose.
if (eventListener.AddonNames.Count is 0)
{
this.ReceiveEventListeners.Remove(eventListener);
eventListener.Dispose();
}
}
}
private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
{ {
try try
{ {
this.LogInitialize(addon->NameString); this.RegisterReceiveEventHook(addon);
// AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
AllocatedTables.Add(new AddonVirtualTable(addon, this));
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e, "Exception in AddonLifecycle during OnAddonInitialize."); Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
} }
this.onInitializeAddonHook!.Original(addon); using var returner = this.argsPool.Rent(out AddonSetupArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.AtkValueCount = valueCount;
arg.AtkValues = (nint)values;
this.InvokeListenersSafely(AddonEvent.PreSetup, arg);
valueCount = arg.AtkValueCount;
values = (AtkValue*)arg.AtkValues;
try
{
addon->OnSetup(valueCount, values);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostSetup, arg);
} }
[Conditional("DEBUG")] private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
private void LogInitialize(string addonName)
{ {
Log.Debug($"Initializing {addonName}"); try
{
var addonName = atkUnitBase[0]->NameString;
this.UnregisterReceiveEventHook(addonName);
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal.");
}
using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg);
arg.Clear();
arg.Addon = (nint)atkUnitBase[0];
this.InvokeListenersSafely(AddonEvent.PreFinalize, arg);
try
{
this.onAddonFinalizeHook.Original(unitManager, atkUnitBase);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method.");
}
}
private void OnAddonDraw(AtkUnitBase* addon)
{
using var returner = this.argsPool.Rent(out AddonDrawArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
this.InvokeListenersSafely(AddonEvent.PreDraw, arg);
try
{
addon->Draw();
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostDraw, arg);
}
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
{
using var returner = this.argsPool.Rent(out AddonUpdateArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.TimeDeltaInternal = delta;
this.InvokeListenersSafely(AddonEvent.PreUpdate, arg);
try
{
addon->Update(delta);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostUpdate, arg);
}
private bool OnAddonRefresh(AtkUnitManager* thisPtr, AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
var result = false;
using var returner = this.argsPool.Rent(out AddonRefreshArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.AtkValueCount = valueCount;
arg.AtkValues = (nint)values;
this.InvokeListenersSafely(AddonEvent.PreRefresh, arg);
valueCount = arg.AtkValueCount;
values = (AtkValue*)arg.AtkValues;
try
{
result = this.onAddonRefreshHook.Original(thisPtr, addon, valueCount, values);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostRefresh, arg);
return result;
}
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.NumberArrayData = (nint)numberArrayData;
arg.StringArrayData = (nint)stringArrayData;
this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg);
numberArrayData = (NumberArrayData**)arg.NumberArrayData;
stringArrayData = (StringArrayData**)arg.StringArrayData;
try
{
addon->OnRequestedUpdate(numberArrayData, stringArrayData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg);
} }
} }
@ -183,7 +387,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycleService = Service<AddonLifecycle>.Get(); private readonly AddonLifecycle addonLifecycleService = Service<AddonLifecycle>.Get();
private readonly List<AddonLifecycleEventListener> eventListeners = []; private readonly List<AddonLifecycleEventListener> eventListeners = new();
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
@ -254,14 +458,10 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
this.eventListeners.RemoveAll(entry => this.eventListeners.RemoveAll(entry =>
{ {
if (entry.FunctionDelegate != handler) return false; if (entry.FunctionDelegate != handler) return false;
this.addonLifecycleService.UnregisterListener(entry); this.addonLifecycleService.UnregisterListener(entry);
return true; return true;
}); });
} }
} }
/// <inheritdoc/>
public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress)
=> (nint)this.addonLifecycleService.GetOriginalVirtualTable((AtkUnitBase.AtkUnitBaseVirtualTable*)virtualTableAddress);
} }

View file

@ -0,0 +1,56 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// AddonLifecycleService memory address resolver.
/// </summary>
internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the addon setup hook invoked by the AtkUnitManager.
/// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
/// This is called for a majority of all addon OnSetup's.
/// </summary>
public nint AddonSetup { get; private set; }
/// <summary>
/// Gets the address of the other addon setup hook invoked by the AtkUnitManager.
/// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
/// This seems to be called rarely for specific addons.
/// </summary>
public nint AddonSetup2 { get; private set; }
/// <summary>
/// Gets the address of the addon finalize hook invoked by the AtkUnitManager.
/// </summary>
public nint AddonFinalize { get; private set; }
/// <summary>
/// Gets the address of the addon draw hook invoked by virtual function call.
/// </summary>
public nint AddonDraw { get; private set; }
/// <summary>
/// Gets the address of the addon update hook invoked by virtual function call.
/// </summary>
public nint AddonUpdate { get; private set; }
/// <summary>
/// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call.
/// </summary>
public nint AddonOnRequestedUpdate { get; private set; }
/// <summary>
/// Scan for and setup any configured address pointers.
/// </summary>
/// <param name="sig">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner sig)
{
this.AddonSetup = sig.ScanText("4C 8B 88 ?? ?? ?? ?? 66 44 39 BB");
this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5");
this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C4 48 81 EF ?? ?? ?? ?? 48 83 ED 01");
this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF ?? ?? ?? ?? 45 33 D2");
this.AddonOnRequestedUpdate = sig.ScanText("FF 90 A0 01 00 00 48 8B 5C 24 30");
}
}

View file

@ -25,12 +25,17 @@ internal class AddonLifecycleEventListener
/// string.Empty if it wants to be called for any addon. /// string.Empty if it wants to be called for any addon.
/// </summary> /// </summary>
public string AddonName { get; init; } public string AddonName { get; init; }
/// <summary>
/// Gets or sets a value indicating whether this event has been unregistered.
/// </summary>
public bool Removed { get; set; }
/// <summary> /// <summary>
/// Gets the event type this listener is looking for. /// Gets the event type this listener is looking for.
/// </summary> /// </summary>
public AddonEvent EventType { get; init; } public AddonEvent EventType { get; init; }
/// <summary> /// <summary>
/// Gets the delegate this listener invokes. /// Gets the delegate this listener invokes.
/// </summary> /// </summary>

View file

@ -0,0 +1,112 @@
using System.Collections.Generic;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking;
using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// This class is a helper for tracking and invoking listener delegates for Addon_OnReceiveEvent.
/// Multiple addons may use the same ReceiveEvent function, this helper makes sure that those addon events are handled properly.
/// </summary>
internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
{
private static readonly ModuleLog Log = new("AddonLifecycle");
[ServiceManager.ServiceDependency]
private readonly AddonLifecyclePooledArgs argsPool = Service<AddonLifecyclePooledArgs>.Get();
/// <summary>
/// Initializes a new instance of the <see cref="AddonLifecycleReceiveEventListener"/> class.
/// </summary>
/// <param name="service">AddonLifecycle service instance.</param>
/// <param name="addonName">Initial Addon Requesting this listener.</param>
/// <param name="receiveEventAddress">Address of Addon's ReceiveEvent function.</param>
internal AddonLifecycleReceiveEventListener(AddonLifecycle service, string addonName, nint receiveEventAddress)
{
this.AddonLifecycle = service;
this.AddonNames = [addonName];
this.FunctionAddress = receiveEventAddress;
}
/// <summary>
/// Gets the list of addons that use this receive event hook.
/// </summary>
public List<string> AddonNames { get; init; }
/// <summary>
/// Gets the address of the ReceiveEvent function as provided by the vtable on setup.
/// </summary>
public nint FunctionAddress { get; init; }
/// <summary>
/// Gets the contained hook for these addons.
/// </summary>
public Hook<AtkUnitBase.Delegates.ReceiveEvent>? Hook { get; private set; }
/// <summary>
/// Gets or sets the Reference to AddonLifecycle service instance.
/// </summary>
private AddonLifecycle AddonLifecycle { get; set; }
/// <summary>
/// Try to hook and enable this receive event handler.
/// </summary>
public void TryEnable()
{
this.Hook ??= Hook<AtkUnitBase.Delegates.ReceiveEvent>.FromAddress(this.FunctionAddress, this.OnReceiveEvent);
this.Hook?.Enable();
}
/// <summary>
/// Disable the hook for this receive event handler.
/// </summary>
public void Disable()
{
this.Hook?.Disable();
}
/// <inheritdoc/>
public void Dispose()
{
this.Hook?.Dispose();
}
private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
{
// Check that we didn't get here through a call to another addons handler.
var addonName = addon->NameString;
if (!this.AddonNames.Contains(addonName))
{
this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData);
return;
}
using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.AtkEventType = (byte)eventType;
arg.EventParam = eventParam;
arg.AtkEvent = (IntPtr)atkEvent;
arg.Data = (nint)atkEventData;
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, arg);
eventType = (AtkEventType)arg.AtkEventType;
eventParam = arg.EventParam;
atkEvent = (AtkEvent*)arg.AtkEvent;
atkEventData = (AtkEventData*)arg.Data;
try
{
this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
}
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, arg);
}
}

View file

@ -0,0 +1,80 @@
using System.Runtime.InteropServices;
using Reloaded.Hooks.Definitions;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// This class represents a callsite hook used to replace the address of the OnSetup function in r9.
/// </summary>
/// <typeparam name="T">Delegate signature for this hook.</typeparam>
internal class AddonSetupHook<T> : IDisposable where T : Delegate
{
private readonly Reloaded.Hooks.AsmHook asmHook;
private T? detour;
private bool activated;
/// <summary>
/// Initializes a new instance of the <see cref="AddonSetupHook{T}"/> class.
/// </summary>
/// <param name="address">Address of the instruction to replace.</param>
/// <param name="detour">Delegate to invoke.</param>
internal AddonSetupHook(nint address, T detour)
{
this.detour = detour;
var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour);
var code = new[]
{
"use64",
$"mov r9, 0x{detourPtr:X8}",
};
var opt = new AsmHookOptions
{
PreferRelativeJump = true,
Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal,
MaxOpcodeSize = 5,
};
this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt);
}
/// <summary>
/// Gets a value indicating whether the hook is enabled.
/// </summary>
public bool IsEnabled => this.asmHook.IsEnabled;
/// <summary>
/// Starts intercepting a call to the function.
/// </summary>
public void Enable()
{
if (!this.activated)
{
this.activated = true;
this.asmHook.Activate();
return;
}
this.asmHook.Enable();
}
/// <summary>
/// Stops intercepting a call to the function.
/// </summary>
public void Disable()
{
this.asmHook.Disable();
}
/// <summary>
/// Remove a hook from the current process.
/// </summary>
public void Dispose()
{
this.asmHook.Disable();
this.detour = null;
}
}

View file

@ -1,645 +0,0 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// Represents a class that holds references to an addons original and modified virtual table entries.
/// </summary>
internal unsafe class AddonVirtualTable : IDisposable
{
// This need to be at minimum the largest virtual table size of all addons
// Copying extra entries is not problematic, and is considered safe.
private const int VirtualTableEntryCount = 200;
private const bool EnableLogging = false;
private static readonly ModuleLog Log = new("LifecycleVT");
private readonly AddonLifecycle lifecycleService;
// Each addon gets its own set of args that are used to mutate the original call when used in pre-calls
private readonly AddonSetupArgs setupArgs = new();
private readonly AddonArgs finalizeArgs = new();
private readonly AddonArgs drawArgs = new();
private readonly AddonArgs updateArgs = new();
private readonly AddonRefreshArgs refreshArgs = new();
private readonly AddonRequestedUpdateArgs requestedUpdateArgs = new();
private readonly AddonReceiveEventArgs receiveEventArgs = new();
private readonly AddonArgs openArgs = new();
private readonly AddonCloseArgs closeArgs = new();
private readonly AddonShowArgs showArgs = new();
private readonly AddonHideArgs hideArgs = new();
private readonly AddonArgs onMoveArgs = new();
private readonly AddonArgs onMouseOverArgs = new();
private readonly AddonArgs onMouseOutArgs = new();
private readonly AddonArgs focusArgs = new();
private readonly AtkUnitBase* atkUnitBase;
// Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table,
// the CLR needs to know they are in use, or it will invalidate them causing random crashing.
private readonly AtkUnitBase.Delegates.Dtor destructorFunction;
private readonly AtkUnitBase.Delegates.OnSetup onSetupFunction;
private readonly AtkUnitBase.Delegates.Finalizer finalizerFunction;
private readonly AtkUnitBase.Delegates.Draw drawFunction;
private readonly AtkUnitBase.Delegates.Update updateFunction;
private readonly AtkUnitBase.Delegates.OnRefresh onRefreshFunction;
private readonly AtkUnitBase.Delegates.OnRequestedUpdate onRequestedUpdateFunction;
private readonly AtkUnitBase.Delegates.ReceiveEvent onReceiveEventFunction;
private readonly AtkUnitBase.Delegates.Open openFunction;
private readonly AtkUnitBase.Delegates.Close closeFunction;
private readonly AtkUnitBase.Delegates.Show showFunction;
private readonly AtkUnitBase.Delegates.Hide hideFunction;
private readonly AtkUnitBase.Delegates.OnMove onMoveFunction;
private readonly AtkUnitBase.Delegates.OnMouseOver onMouseOverFunction;
private readonly AtkUnitBase.Delegates.OnMouseOut onMouseOutFunction;
private readonly AtkUnitBase.Delegates.Focus focusFunction;
/// <summary>
/// Initializes a new instance of the <see cref="AddonVirtualTable"/> class.
/// </summary>
/// <param name="addon">AtkUnitBase* for the addon to replace the table of.</param>
/// <param name="lifecycleService">Reference to AddonLifecycle service to callback and invoke listeners.</param>
internal AddonVirtualTable(AtkUnitBase* addon, AddonLifecycle lifecycleService)
{
this.atkUnitBase = addon;
this.lifecycleService = lifecycleService;
// Save original virtual table
this.OriginalVirtualTable = addon->VirtualTable;
// Create copy of original table
// Note this will copy any derived/overriden functions that this specific addon has.
// Note: currently there are 73 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game
this.ModifiedVirtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8);
NativeMemory.Copy(addon->VirtualTable, this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
// Overwrite the addons existing virtual table with our own
addon->VirtualTable = this.ModifiedVirtualTable;
// Pin each of our listener functions
this.destructorFunction = this.OnAddonDestructor;
this.onSetupFunction = this.OnAddonSetup;
this.finalizerFunction = this.OnAddonFinalize;
this.drawFunction = this.OnAddonDraw;
this.updateFunction = this.OnAddonUpdate;
this.onRefreshFunction = this.OnAddonRefresh;
this.onRequestedUpdateFunction = this.OnRequestedUpdate;
this.onReceiveEventFunction = this.OnAddonReceiveEvent;
this.openFunction = this.OnAddonOpen;
this.closeFunction = this.OnAddonClose;
this.showFunction = this.OnAddonShow;
this.hideFunction = this.OnAddonHide;
this.onMoveFunction = this.OnAddonMove;
this.onMouseOverFunction = this.OnAddonMouseOver;
this.onMouseOutFunction = this.OnAddonMouseOut;
this.focusFunction = this.OnAddonFocus;
// Overwrite specific virtual table entries
this.ModifiedVirtualTable->Dtor = (delegate* unmanaged<AtkUnitBase*, byte, AtkEventListener*>)Marshal.GetFunctionPointerForDelegate(this.destructorFunction);
this.ModifiedVirtualTable->OnSetup = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, void>)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction);
this.ModifiedVirtualTable->Finalizer = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction);
this.ModifiedVirtualTable->Draw = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.drawFunction);
this.ModifiedVirtualTable->Update = (delegate* unmanaged<AtkUnitBase*, float, void>)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
this.ModifiedVirtualTable->OnRefresh = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, bool>)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction);
this.ModifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged<AtkUnitBase*, NumberArrayData**, StringArrayData**, void>)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction);
this.ModifiedVirtualTable->ReceiveEvent = (delegate* unmanaged<AtkUnitBase*, AtkEventType, int, AtkEvent*, AtkEventData*, void>)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction);
this.ModifiedVirtualTable->Open = (delegate* unmanaged<AtkUnitBase*, uint, bool>)Marshal.GetFunctionPointerForDelegate(this.openFunction);
this.ModifiedVirtualTable->Close = (delegate* unmanaged<AtkUnitBase*, bool, bool>)Marshal.GetFunctionPointerForDelegate(this.closeFunction);
this.ModifiedVirtualTable->Show = (delegate* unmanaged<AtkUnitBase*, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(this.showFunction);
this.ModifiedVirtualTable->Hide = (delegate* unmanaged<AtkUnitBase*, bool, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
this.ModifiedVirtualTable->OnMove = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMoveFunction);
this.ModifiedVirtualTable->OnMouseOver = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction);
this.ModifiedVirtualTable->OnMouseOut = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction);
this.ModifiedVirtualTable->Focus = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.focusFunction);
}
/// <summary>
/// Gets the original virtual table address for this addon.
/// </summary>
internal AtkUnitBase.AtkUnitBaseVirtualTable* OriginalVirtualTable { get; private set; }
/// <summary>
/// Gets the modified virtual address for this addon.
/// </summary>
internal AtkUnitBase.AtkUnitBaseVirtualTable* ModifiedVirtualTable { get; private set; }
/// <inheritdoc/>
public void Dispose()
{
// Ensure restoration is done atomically.
Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.OriginalVirtualTable);
IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
}
private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags)
{
AtkEventListener* result = null;
try
{
this.LogEvent(EnableLogging);
try
{
result = this.OriginalVirtualTable->Dtor(thisPtr, freeFlags);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Dtor. This may be a bug in the game or another plugin hooking this method.");
}
if ((freeFlags & 1) == 1)
{
IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
AddonLifecycle.AllocatedTables.Remove(this);
}
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonDestructor.");
}
return result;
}
private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
try
{
this.LogEvent(EnableLogging);
this.setupArgs.Addon = addon;
this.setupArgs.AtkValueCount = valueCount;
this.setupArgs.AtkValues = (nint)values;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.setupArgs);
valueCount = this.setupArgs.AtkValueCount;
values = (AtkValue*)this.setupArgs.AtkValues;
try
{
this.OriginalVirtualTable->OnSetup(addon, valueCount, values);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnSetup. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.setupArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonSetup.");
}
}
private void OnAddonFinalize(AtkUnitBase* thisPtr)
{
try
{
this.LogEvent(EnableLogging);
this.finalizeArgs.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.finalizeArgs);
try
{
this.OriginalVirtualTable->Finalizer(thisPtr);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Finalizer. This may be a bug in the game or another plugin hooking this method.");
}
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFinalize.");
}
}
private void OnAddonDraw(AtkUnitBase* addon)
{
try
{
this.LogEvent(EnableLogging);
this.drawArgs.Addon = addon;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.drawArgs);
try
{
this.OriginalVirtualTable->Draw(addon);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Draw. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.drawArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonDraw.");
}
}
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
{
try
{
this.LogEvent(EnableLogging);
this.updateArgs.Addon = addon;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.updateArgs);
// Note: Do not pass or allow manipulation of delta.
// It's realistically not something that should be needed.
// And even if someone does, they are encouraged to hook Update themselves.
try
{
this.OriginalVirtualTable->Update(addon, delta);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Update. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.updateArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonUpdate.");
}
}
private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
var result = false;
try
{
this.LogEvent(EnableLogging);
this.refreshArgs.Addon = addon;
this.refreshArgs.AtkValueCount = valueCount;
this.refreshArgs.AtkValues = (nint)values;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.refreshArgs);
valueCount = this.refreshArgs.AtkValueCount;
values = (AtkValue*)this.refreshArgs.AtkValues;
try
{
result = this.OriginalVirtualTable->OnRefresh(addon, valueCount, values);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnRefresh. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.refreshArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonRefresh.");
}
return result;
}
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
try
{
this.LogEvent(EnableLogging);
this.requestedUpdateArgs.Addon = addon;
this.requestedUpdateArgs.NumberArrayData = (nint)numberArrayData;
this.requestedUpdateArgs.StringArrayData = (nint)stringArrayData;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.requestedUpdateArgs);
numberArrayData = (NumberArrayData**)this.requestedUpdateArgs.NumberArrayData;
stringArrayData = (StringArrayData**)this.requestedUpdateArgs.StringArrayData;
try
{
this.OriginalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.requestedUpdateArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnRequestedUpdate.");
}
}
private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
{
try
{
this.LogEvent(EnableLogging);
this.receiveEventArgs.Addon = (nint)addon;
this.receiveEventArgs.AtkEventType = (byte)eventType;
this.receiveEventArgs.EventParam = eventParam;
this.receiveEventArgs.AtkEvent = (IntPtr)atkEvent;
this.receiveEventArgs.AtkEventData = (nint)atkEventData;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.receiveEventArgs);
eventType = (AtkEventType)this.receiveEventArgs.AtkEventType;
eventParam = this.receiveEventArgs.EventParam;
atkEvent = (AtkEvent*)this.receiveEventArgs.AtkEvent;
atkEventData = (AtkEventData*)this.receiveEventArgs.AtkEventData;
try
{
this.OriginalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon ReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.receiveEventArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonReceiveEvent.");
}
}
private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer)
{
var result = false;
try
{
this.LogEvent(EnableLogging);
this.openArgs.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.openArgs);
try
{
result = this.OriginalVirtualTable->Open(thisPtr, depthLayer);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Open. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.openArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonOpen.");
}
return result;
}
private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback)
{
var result = false;
try
{
this.LogEvent(EnableLogging);
this.closeArgs.Addon = thisPtr;
this.closeArgs.FireCallback = fireCallback;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.closeArgs);
fireCallback = this.closeArgs.FireCallback;
try
{
result = this.OriginalVirtualTable->Close(thisPtr, fireCallback);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Close. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.closeArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonClose.");
}
return result;
}
private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags)
{
try
{
this.LogEvent(EnableLogging);
this.showArgs.Addon = thisPtr;
this.showArgs.SilenceOpenSoundEffect = silenceOpenSoundEffect;
this.showArgs.UnsetShowHideFlags = unsetShowHideFlags;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.showArgs);
silenceOpenSoundEffect = this.showArgs.SilenceOpenSoundEffect;
unsetShowHideFlags = this.showArgs.UnsetShowHideFlags;
try
{
this.OriginalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Show. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.showArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonShow.");
}
}
private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags)
{
try
{
this.LogEvent(EnableLogging);
this.hideArgs.Addon = thisPtr;
this.hideArgs.UnknownBool = unkBool;
this.hideArgs.CallHideCallback = callHideCallback;
this.hideArgs.SetShowHideFlags = setShowHideFlags;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.hideArgs);
unkBool = this.hideArgs.UnknownBool;
callHideCallback = this.hideArgs.CallHideCallback;
setShowHideFlags = this.hideArgs.SetShowHideFlags;
try
{
this.OriginalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Hide. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.hideArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonHide.");
}
}
private void OnAddonMove(AtkUnitBase* thisPtr)
{
try
{
this.LogEvent(EnableLogging);
this.onMoveArgs.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMove, this.onMoveArgs);
try
{
this.OriginalVirtualTable->OnMove(thisPtr);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnMove. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMove, this.onMoveArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMove.");
}
}
private void OnAddonMouseOver(AtkUnitBase* thisPtr)
{
try
{
this.LogEvent(EnableLogging);
this.onMouseOverArgs.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOver, this.onMouseOverArgs);
try
{
this.OriginalVirtualTable->OnMouseOver(thisPtr);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnMouseOver. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOver, this.onMouseOverArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMouseOver.");
}
}
private void OnAddonMouseOut(AtkUnitBase* thisPtr)
{
try
{
this.LogEvent(EnableLogging);
this.onMouseOutArgs.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOut, this.onMouseOutArgs);
try
{
this.OriginalVirtualTable->OnMouseOut(thisPtr);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnMouseOut. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOut, this.onMouseOutArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMouseOut.");
}
}
private void OnAddonFocus(AtkUnitBase* thisPtr)
{
try
{
this.LogEvent(EnableLogging);
this.focusArgs.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocus, this.focusArgs);
try
{
this.OriginalVirtualTable->Focus(thisPtr);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Focus. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocus, this.focusArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFocus.");
}
}
[Conditional("DEBUG")]
private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "")
{
if (loggingEnabled)
{
// Manually disable the really spammy log events, you can comment this out if you need to debug them.
if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate")
return;
Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}");
}
}
}

View file

@ -2,8 +2,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Plugin.Services;
namespace Dalamud.Game; namespace Dalamud.Game;
/// <summary> /// <summary>

View file

@ -104,7 +104,7 @@ internal partial class ChatHandlers : IServiceType
if (this.configuration.PrintDalamudWelcomeMsg) if (this.configuration.PrintDalamudWelcomeMsg)
{ {
chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Versioning.GetScmVersion()) 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)));
} }
@ -116,7 +116,7 @@ internal partial class ChatHandlers : IServiceType
} }
} }
if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Versioning.GetAssemblyVersion().StartsWith(this.configuration.LastVersion)) if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Util.AssemblyVersion.StartsWith(this.configuration.LastVersion))
{ {
var linkPayload = chatGui.AddChatLinkHandler( var linkPayload = chatGui.AddChatLinkHandler(
(_, _) => dalamudInterface.OpenPluginInstallerTo(PluginInstallerOpenKind.Changelogs)); (_, _) => dalamudInterface.OpenPluginInstallerTo(PluginInstallerOpenKind.Changelogs));
@ -137,7 +137,7 @@ internal partial class ChatHandlers : IServiceType
Type = XivChatType.Notice, Type = XivChatType.Notice,
}); });
this.configuration.LastVersion = Versioning.GetAssemblyVersion(); this.configuration.LastVersion = Util.AssemblyVersion;
this.configuration.QueueSave(); this.configuration.QueueSave();
} }

View file

@ -63,37 +63,47 @@ public interface IAetheryteEntry
} }
/// <summary> /// <summary>
/// This struct represents an aetheryte entry available to the game. /// Class representing an aetheryte entry available to the game.
/// </summary> /// </summary>
/// <param name="data">Data read from the Aetheryte List.</param> internal sealed class AetheryteEntry : IAetheryteEntry
internal readonly struct AetheryteEntry(TeleportInfo data) : IAetheryteEntry
{ {
/// <inheritdoc /> private readonly TeleportInfo data;
public uint AetheryteId => data.AetheryteId;
/// <summary>
/// Initializes a new instance of the <see cref="AetheryteEntry"/> class.
/// </summary>
/// <param name="data">Data read from the Aetheryte List.</param>
internal AetheryteEntry(TeleportInfo data)
{
this.data = data;
}
/// <inheritdoc /> /// <inheritdoc />
public uint TerritoryId => data.TerritoryId; public uint AetheryteId => this.data.AetheryteId;
/// <inheritdoc /> /// <inheritdoc />
public byte SubIndex => data.SubIndex; public uint TerritoryId => this.data.TerritoryId;
/// <inheritdoc /> /// <inheritdoc />
public byte Ward => data.Ward; public byte SubIndex => this.data.SubIndex;
/// <inheritdoc /> /// <inheritdoc />
public byte Plot => data.Plot; public byte Ward => this.data.Ward;
/// <inheritdoc /> /// <inheritdoc />
public uint GilCost => data.GilCost; public byte Plot => this.data.Plot;
/// <inheritdoc /> /// <inheritdoc />
public bool IsFavourite => data.IsFavourite; public uint GilCost => this.data.GilCost;
/// <inheritdoc /> /// <inheritdoc />
public bool IsSharedHouse => data.IsSharedHouse; public bool IsFavourite => this.data.IsFavourite;
/// <inheritdoc /> /// <inheritdoc />
public bool IsApartment => data.IsApartment; public bool IsSharedHouse => this.data.IsSharedHouse;
/// <inheritdoc />
public bool IsApartment => this.data.IsApartment;
/// <inheritdoc /> /// <inheritdoc />
public RowRef<Lumina.Excel.Sheets.Aetheryte> AetheryteData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Aetheryte>(this.AetheryteId); public RowRef<Lumina.Excel.Sheets.Aetheryte> AetheryteData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Aetheryte>(this.AetheryteId);

View file

@ -88,7 +88,10 @@ internal sealed partial class AetheryteList
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerator<IAetheryteEntry> GetEnumerator() public IEnumerator<IAetheryteEntry> GetEnumerator()
{ {
return new Enumerator(this); for (var i = 0; i < this.Length; i++)
{
yield return this[i];
}
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -96,34 +99,4 @@ internal sealed partial class AetheryteList
{ {
return this.GetEnumerator(); return this.GetEnumerator();
} }
private struct Enumerator(AetheryteList aetheryteList) : IEnumerator<IAetheryteEntry>
{
private int index = -1;
public IAetheryteEntry Current { get; private set; }
object IEnumerator.Current => this.Current;
public bool MoveNext()
{
if (++this.index < aetheryteList.Length)
{
this.Current = aetheryteList[this.index];
return true;
}
this.Current = default;
return false;
}
public void Reset()
{
this.index = -1;
}
public void Dispose()
{
}
}
} }

View file

@ -8,7 +8,6 @@ using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy; using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy;
using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState; using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState;
namespace Dalamud.Game.ClientState.Buddy; namespace Dalamud.Game.ClientState.Buddy;
@ -24,7 +23,7 @@ namespace Dalamud.Game.ClientState.Buddy;
#pragma warning restore SA1015 #pragma warning restore SA1015
internal sealed partial class BuddyList : IServiceType, IBuddyList internal sealed partial class BuddyList : IServiceType, IBuddyList
{ {
private const uint InvalidEntityId = 0xE0000000; private const uint InvalidObjectID = 0xE0000000;
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly PlayerState playerState = Service<PlayerState>.Get(); private readonly PlayerState playerState = Service<PlayerState>.Get();
@ -85,37 +84,37 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
} }
/// <inheritdoc/> /// <inheritdoc/>
public unsafe nint GetCompanionBuddyMemberAddress() public unsafe IntPtr GetCompanionBuddyMemberAddress()
{ {
return (nint)this.BuddyListStruct->CompanionInfo.Companion; return (IntPtr)this.BuddyListStruct->CompanionInfo.Companion;
} }
/// <inheritdoc/> /// <inheritdoc/>
public unsafe nint GetPetBuddyMemberAddress() public unsafe IntPtr GetPetBuddyMemberAddress()
{ {
return (nint)this.BuddyListStruct->PetInfo.Pet; return (IntPtr)this.BuddyListStruct->PetInfo.Pet;
} }
/// <inheritdoc/> /// <inheritdoc/>
public unsafe nint GetBattleBuddyMemberAddress(int index) public unsafe IntPtr GetBattleBuddyMemberAddress(int index)
{ {
if (index < 0 || index >= 3) if (index < 0 || index >= 3)
return 0; return IntPtr.Zero;
return (nint)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]); return (IntPtr)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]);
} }
/// <inheritdoc/> /// <inheritdoc/>
public unsafe IBuddyMember? CreateBuddyMemberReference(nint address) public IBuddyMember? CreateBuddyMemberReference(IntPtr address)
{ {
if (address == 0) if (address == IntPtr.Zero)
return null; return null;
if (this.playerState.ContentId == 0) if (!this.playerState.IsLoaded)
return null; return null;
var buddy = new BuddyMember((CSBuddyMember*)address); var buddy = new BuddyMember(address);
if (buddy.EntityId == InvalidEntityId) if (buddy.ObjectId == InvalidObjectID)
return null; return null;
return buddy; return buddy;
@ -133,39 +132,12 @@ internal sealed partial class BuddyList
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerator<IBuddyMember> GetEnumerator() public IEnumerator<IBuddyMember> GetEnumerator()
{ {
return new Enumerator(this); for (var i = 0; i < this.Length; i++)
{
yield return this[i];
}
} }
/// <inheritdoc/> /// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
private struct Enumerator(BuddyList buddyList) : IEnumerator<IBuddyMember>
{
private int index = -1;
public IBuddyMember Current { get; private set; }
object IEnumerator.Current => this.Current;
public bool MoveNext()
{
if (++this.index < buddyList.Length)
{
this.Current = buddyList[this.index];
return true;
}
this.Current = default;
return false;
}
public void Reset()
{
this.index = -1;
}
public void Dispose()
{
}
}
} }

View file

@ -1,24 +1,20 @@
using System.Diagnostics.CodeAnalysis;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Lumina.Excel; using Lumina.Excel;
using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
namespace Dalamud.Game.ClientState.Buddy; namespace Dalamud.Game.ClientState.Buddy;
/// <summary> /// <summary>
/// Interface representing represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties. /// Interface representing represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
/// </summary> /// </summary>
public interface IBuddyMember : IEquatable<IBuddyMember> public interface IBuddyMember
{ {
/// <summary> /// <summary>
/// Gets the address of the buddy in memory. /// Gets the address of the buddy in memory.
/// </summary> /// </summary>
nint Address { get; } IntPtr Address { get; }
/// <summary> /// <summary>
/// Gets the object ID of this buddy. /// Gets the object ID of this buddy.
@ -71,34 +67,42 @@ public interface IBuddyMember : IEquatable<IBuddyMember>
} }
/// <summary> /// <summary>
/// This struct represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties. /// This class represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
/// </summary> /// </summary>
/// <param name="ptr">A pointer to the BuddyMember.</param> internal unsafe class BuddyMember : IBuddyMember
internal readonly unsafe struct BuddyMember(CSBuddyMember* ptr) : IBuddyMember
{ {
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly ObjectTable objectTable = Service<ObjectTable>.Get(); private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
/// <inheritdoc /> /// <summary>
public nint Address => (nint)ptr; /// Initializes a new instance of the <see cref="BuddyMember"/> class.
/// </summary>
/// <param name="address">Buddy address.</param>
internal BuddyMember(IntPtr address)
{
this.Address = address;
}
/// <inheritdoc /> /// <inheritdoc />
public uint ObjectId => this.EntityId; public IntPtr Address { get; }
/// <inheritdoc /> /// <inheritdoc />
public uint EntityId => ptr->EntityId; public uint ObjectId => this.Struct->EntityId;
/// <inheritdoc /> /// <inheritdoc />
public IGameObject? GameObject => this.objectTable.SearchById(this.EntityId); public uint EntityId => this.Struct->EntityId;
/// <inheritdoc /> /// <inheritdoc />
public uint CurrentHP => ptr->CurrentHealth; public IGameObject? GameObject => this.objectTable.SearchById(this.ObjectId);
/// <inheritdoc /> /// <inheritdoc />
public uint MaxHP => ptr->MaxHealth; public uint CurrentHP => this.Struct->CurrentHealth;
/// <inheritdoc /> /// <inheritdoc />
public uint DataID => ptr->DataId; public uint MaxHP => this.Struct->MaxHealth;
/// <inheritdoc />
public uint DataID => this.Struct->DataId;
/// <inheritdoc /> /// <inheritdoc />
public RowRef<Lumina.Excel.Sheets.Mount> MountData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Mount>(this.DataID); public RowRef<Lumina.Excel.Sheets.Mount> MountData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Mount>(this.DataID);
@ -109,25 +113,5 @@ internal readonly unsafe struct BuddyMember(CSBuddyMember* ptr) : IBuddyMember
/// <inheritdoc /> /// <inheritdoc />
public RowRef<Lumina.Excel.Sheets.DawnGrowMember> TrustData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.DawnGrowMember>(this.DataID); public RowRef<Lumina.Excel.Sheets.DawnGrowMember> TrustData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.DawnGrowMember>(this.DataID);
public static bool operator ==(BuddyMember x, BuddyMember y) => x.Equals(y); private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address;
public static bool operator !=(BuddyMember x, BuddyMember y) => !(x == y);
/// <inheritdoc/>
public bool Equals(IBuddyMember? other)
{
return this.EntityId == other.EntityId;
}
/// <inheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is BuddyMember fate && this.Equals(fate);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return this.EntityId.GetHashCode();
}
} }

View file

@ -37,6 +37,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
private readonly GameLifecycle lifecycle; private readonly GameLifecycle lifecycle;
private readonly ClientStateAddressResolver address; private readonly ClientStateAddressResolver address;
private readonly Hook<HandleZoneInitPacketDelegate> handleZoneInitPacketHook;
private readonly Hook<UIModule.Delegates.HandlePacket> uiModuleHandlePacketHook; private readonly Hook<UIModule.Delegates.HandlePacket> uiModuleHandlePacketHook;
private readonly Hook<SetCurrentInstanceDelegate> setCurrentInstanceHook; private readonly Hook<SetCurrentInstanceDelegate> setCurrentInstanceHook;
@ -71,11 +72,13 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
this.ClientLanguage = (ClientLanguage)dalamud.StartInfo.Language; this.ClientLanguage = (ClientLanguage)dalamud.StartInfo.Language;
this.handleZoneInitPacketHook = Hook<HandleZoneInitPacketDelegate>.FromAddress(this.AddressResolver.HandleZoneInitPacket, this.HandleZoneInitPacketDetour);
this.uiModuleHandlePacketHook = Hook<UIModule.Delegates.HandlePacket>.FromAddress((nint)UIModule.StaticVirtualTablePointer->HandlePacket, this.UIModuleHandlePacketDetour); this.uiModuleHandlePacketHook = Hook<UIModule.Delegates.HandlePacket>.FromAddress((nint)UIModule.StaticVirtualTablePointer->HandlePacket, this.UIModuleHandlePacketDetour);
this.setCurrentInstanceHook = Hook<SetCurrentInstanceDelegate>.FromAddress(this.AddressResolver.SetCurrentInstance, this.SetCurrentInstanceDetour); this.setCurrentInstanceHook = Hook<SetCurrentInstanceDelegate>.FromAddress(this.AddressResolver.SetCurrentInstance, this.SetCurrentInstanceDetour);
this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop; this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
this.handleZoneInitPacketHook.Enable();
this.uiModuleHandlePacketHook.Enable(); this.uiModuleHandlePacketHook.Enable();
this.setCurrentInstanceHook.Enable(); this.setCurrentInstanceHook.Enable();
@ -268,6 +271,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
/// </summary> /// </summary>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.handleZoneInitPacketHook.Dispose();
this.uiModuleHandlePacketHook.Dispose(); this.uiModuleHandlePacketHook.Dispose();
this.onLogoutHook.Dispose(); this.onLogoutHook.Dispose();
this.setCurrentInstanceHook.Dispose(); this.setCurrentInstanceHook.Dispose();
@ -290,6 +294,23 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
this.framework.Update += this.OnFrameworkUpdate; this.framework.Update += this.OnFrameworkUpdate;
} }
private void HandleZoneInitPacketDetour(nint a1, uint localPlayerEntityId, nint packet, byte type)
{
this.handleZoneInitPacketHook.Original(a1, localPlayerEntityId, packet, type);
try
{
var eventArgs = ZoneInitEventArgs.Read(packet);
Log.Debug($"ZoneInit: {eventArgs}");
this.ZoneInit?.InvokeSafely(eventArgs);
this.TerritoryType = (ushort)eventArgs.TerritoryType.RowId;
}
catch (Exception ex)
{
Log.Error(ex, "Exception during ZoneInit");
}
}
private unsafe void UIModuleHandlePacketDetour( private unsafe void UIModuleHandlePacketDetour(
UIModule* thisPtr, UIModulePacketType type, uint uintParam, void* packet) UIModule* thisPtr, UIModulePacketType type, uint uintParam, void* packet)
{ {
@ -335,15 +356,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
break; break;
} }
case (UIModulePacketType)5: // TODO: Use UIModulePacketType.InitZone when available
{
var eventArgs = ZoneInitEventArgs.Read((nint)packet);
Log.Debug($"ZoneInit: {eventArgs}");
this.ZoneInit?.InvokeSafely(eventArgs);
this.TerritoryType = (ushort)eventArgs.TerritoryType.RowId;
break;
}
} }
} }

View file

@ -1,5 +1,3 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.ClientState; namespace Dalamud.Game.ClientState;
/// <summary> /// <summary>
@ -21,6 +19,11 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
// Functions // Functions
/// <summary>
/// Gets the address of the method that handles the ZoneInit packet.
/// </summary>
public nint HandleZoneInitPacket { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the method that sets the current public instance. /// Gets the address of the method that sets the current public instance.
/// </summary> /// </summary>
@ -32,6 +35,7 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
/// <param name="sig">The signature scanner to facilitate setup.</param> /// <param name="sig">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner sig) protected override void Setup64Bit(ISigScanner sig)
{ {
this.HandleZoneInitPacket = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 45");
this.SetCurrentInstance = sig.ScanText("E8 ?? ?? ?? ?? 0F B6 55 ?? 48 8D 0D ?? ?? ?? ?? C0 EA"); // NetworkModuleProxy.SetCurrentInstance this.SetCurrentInstance = sig.ScanText("E8 ?? ?? ?? ?? 0F B6 55 ?? 48 8D 0D ?? ?? ?? ?? C0 EA"); // NetworkModuleProxy.SetCurrentInstance
// These resolve to fixed offsets only, without the base address added in, so GetStaticAddressFromSig() can't be used. // These resolve to fixed offsets only, without the base address added in, so GetStaticAddressFromSig() can't be used.

View file

@ -18,7 +18,7 @@ internal sealed class Condition : IInternalDisposableService, ICondition
/// <summary> /// <summary>
/// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has. /// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has.
/// </summary> /// </summary>
internal const int MaxConditionEntries = 112; internal const int MaxConditionEntries = 104;
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get(); private readonly Framework framework = Service<Framework>.Get();

View file

@ -520,17 +520,4 @@ public enum ConditionFlag
PilotingMech = 102, PilotingMech = 102,
// Unknown103 = 103, // Unknown103 = 103,
/// <summary>
/// Unable to execute command while editing a strategy board.
/// </summary>
EditingStrategyBoard = 104,
// Unknown105 = 105,
// Unknown106 = 106,
// Unknown107 = 107,
// Unknown108 = 108,
// Unknown109 = 109,
// Unknown110 = 110,
// Unknown111 = 111,
} }

View file

@ -1,4 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics; using System.Numerics;
using Dalamud.Data; using Dalamud.Data;
@ -8,12 +7,10 @@ using Dalamud.Memory;
using Lumina.Excel; using Lumina.Excel;
using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext;
namespace Dalamud.Game.ClientState.Fates; namespace Dalamud.Game.ClientState.Fates;
/// <summary> /// <summary>
/// Interface representing a fate entry that can be seen in the current area. /// Interface representing an fate entry that can be seen in the current area.
/// </summary> /// </summary>
public interface IFate : IEquatable<IFate> public interface IFate : IEquatable<IFate>
{ {
@ -115,96 +112,129 @@ public interface IFate : IEquatable<IFate>
/// <summary> /// <summary>
/// Gets the address of this Fate in memory. /// Gets the address of this Fate in memory.
/// </summary> /// </summary>
nint Address { get; } IntPtr Address { get; }
} }
/// <summary> /// <summary>
/// This struct represents a Fate. /// This class represents an FFXIV Fate.
/// </summary> /// </summary>
/// <param name="ptr">A pointer to the FateContext.</param> internal unsafe partial class Fate
internal readonly unsafe struct Fate(CSFateContext* ptr) : IFate
{ {
/// <summary>
/// Initializes a new instance of the <see cref="Fate"/> class.
/// </summary>
/// <param name="address">The address of this fate in memory.</param>
internal Fate(IntPtr address)
{
this.Address = address;
}
/// <inheritdoc /> /// <inheritdoc />
public nint Address => (nint)ptr; public IntPtr Address { get; }
private FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext*)this.Address;
public static bool operator ==(Fate fate1, Fate fate2)
{
if (fate1 is null || fate2 is null)
return Equals(fate1, fate2);
return fate1.Equals(fate2);
}
public static bool operator !=(Fate fate1, Fate fate2) => !(fate1 == fate2);
/// <summary>
/// Gets a value indicating whether this Fate is still valid in memory.
/// </summary>
/// <param name="fate">The fate to check.</param>
/// <returns>True or false.</returns>
public static bool IsValid(Fate fate)
{
if (fate == null)
return false;
var playerState = Service<PlayerState>.Get();
return playerState.IsLoaded == true;
}
/// <summary>
/// Gets a value indicating whether this actor is still valid in memory.
/// </summary>
/// <returns>True or false.</returns>
public bool IsValid() => IsValid(this);
/// <inheritdoc/> /// <inheritdoc/>
public ushort FateId => ptr->FateId; bool IEquatable<IFate>.Equals(IFate other) => this.FateId == other?.FateId;
/// <inheritdoc/>
public override bool Equals(object obj) => ((IEquatable<IFate>)this).Equals(obj as IFate);
/// <inheritdoc/>
public override int GetHashCode() => this.FateId.GetHashCode();
}
/// <summary>
/// This class represents an FFXIV Fate.
/// </summary>
internal unsafe partial class Fate : IFate
{
/// <inheritdoc/>
public ushort FateId => this.Struct->FateId;
/// <inheritdoc/> /// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.Fate> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Fate>(this.FateId); public RowRef<Lumina.Excel.Sheets.Fate> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Fate>(this.FateId);
/// <inheritdoc/> /// <inheritdoc/>
public int StartTimeEpoch => ptr->StartTimeEpoch; public int StartTimeEpoch => this.Struct->StartTimeEpoch;
/// <inheritdoc/> /// <inheritdoc/>
public short Duration => ptr->Duration; public short Duration => this.Struct->Duration;
/// <inheritdoc/> /// <inheritdoc/>
public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds(); public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds();
/// <inheritdoc/> /// <inheritdoc/>
public SeString Name => MemoryHelper.ReadSeString(&ptr->Name); public SeString Name => MemoryHelper.ReadSeString(&this.Struct->Name);
/// <inheritdoc/> /// <inheritdoc/>
public SeString Description => MemoryHelper.ReadSeString(&ptr->Description); public SeString Description => MemoryHelper.ReadSeString(&this.Struct->Description);
/// <inheritdoc/> /// <inheritdoc/>
public SeString Objective => MemoryHelper.ReadSeString(&ptr->Objective); public SeString Objective => MemoryHelper.ReadSeString(&this.Struct->Objective);
/// <inheritdoc/> /// <inheritdoc/>
public FateState State => (FateState)ptr->State; public FateState State => (FateState)this.Struct->State;
/// <inheritdoc/> /// <inheritdoc/>
public byte HandInCount => ptr->HandInCount; public byte HandInCount => this.Struct->HandInCount;
/// <inheritdoc/> /// <inheritdoc/>
public byte Progress => ptr->Progress; public byte Progress => this.Struct->Progress;
/// <inheritdoc/> /// <inheritdoc/>
public bool HasBonus => ptr->IsBonus; public bool HasBonus => this.Struct->IsBonus;
/// <inheritdoc/> /// <inheritdoc/>
public uint IconId => ptr->IconId; public uint IconId => this.Struct->IconId;
/// <inheritdoc/> /// <inheritdoc/>
public byte Level => ptr->Level; public byte Level => this.Struct->Level;
/// <inheritdoc/> /// <inheritdoc/>
public byte MaxLevel => ptr->MaxLevel; public byte MaxLevel => this.Struct->MaxLevel;
/// <inheritdoc/> /// <inheritdoc/>
public Vector3 Position => ptr->Location; public Vector3 Position => this.Struct->Location;
/// <inheritdoc/> /// <inheritdoc/>
public float Radius => ptr->Radius; public float Radius => this.Struct->Radius;
/// <inheritdoc/> /// <inheritdoc/>
public uint MapIconId => ptr->MapIconId; public uint MapIconId => this.Struct->MapIconId;
/// <summary> /// <summary>
/// Gets the territory this <see cref="Fate"/> is located in. /// Gets the territory this <see cref="Fate"/> is located in.
/// </summary> /// </summary>
public RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(ptr->MapMarkers[0].MapMarkerData.TerritoryTypeId); public RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->MapMarkers[0].MapMarkerData.TerritoryTypeId);
public static bool operator ==(Fate x, Fate y) => x.Equals(y);
public static bool operator !=(Fate x, Fate y) => !(x == y);
/// <inheritdoc/>
public bool Equals(IFate? other)
{
return this.FateId == other.FateId;
}
/// <inheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is Fate fate && this.Equals(fate);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return this.FateId.GetHashCode();
}
} }

View file

@ -6,7 +6,6 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext;
using CSFateManager = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager; using CSFateManager = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager;
namespace Dalamud.Game.ClientState.Fates; namespace Dalamud.Game.ClientState.Fates;
@ -27,7 +26,7 @@ internal sealed partial class FateTable : IServiceType, IFateTable
} }
/// <inheritdoc/> /// <inheritdoc/>
public unsafe nint Address => (nint)CSFateManager.Instance(); public unsafe IntPtr Address => (nint)CSFateManager.Instance();
/// <inheritdoc/> /// <inheritdoc/>
public unsafe int Length public unsafe int Length
@ -70,29 +69,29 @@ internal sealed partial class FateTable : IServiceType, IFateTable
} }
/// <inheritdoc/> /// <inheritdoc/>
public unsafe nint GetFateAddress(int index) public unsafe IntPtr GetFateAddress(int index)
{ {
if (index >= this.Length) if (index >= this.Length)
return 0; return IntPtr.Zero;
var fateManager = CSFateManager.Instance(); var fateManager = CSFateManager.Instance();
if (fateManager == null) if (fateManager == null)
return 0; return IntPtr.Zero;
return (nint)fateManager->Fates[index].Value; return (IntPtr)fateManager->Fates[index].Value;
} }
/// <inheritdoc/> /// <inheritdoc/>
public unsafe IFate? CreateFateReference(IntPtr address) public IFate? CreateFateReference(IntPtr offset)
{ {
if (address == 0) if (offset == IntPtr.Zero)
return null; return null;
var clientState = Service<ClientState>.Get(); var playerState = Service<PlayerState>.Get();
if (clientState.LocalContentId == 0) if (!playerState.IsLoaded)
return null; return null;
return new Fate((CSFateContext*)address); return new Fate(offset);
} }
} }
@ -107,39 +106,12 @@ internal sealed partial class FateTable
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerator<IFate> GetEnumerator() public IEnumerator<IFate> GetEnumerator()
{ {
return new Enumerator(this); for (var i = 0; i < this.Length; i++)
{
yield return this[i];
}
} }
/// <inheritdoc/> /// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
private struct Enumerator(FateTable fateTable) : IEnumerator<IFate>
{
private int index = -1;
public IFate Current { get; private set; }
object IEnumerator.Current => this.Current;
public bool MoveNext()
{
if (++this.index < fateTable.Length)
{
this.Current = fateTable[this.index];
return true;
}
this.Current = default;
return false;
}
public void Reset()
{
this.index = -1;
}
public void Dispose()
{
}
}
} }

View file

@ -13,6 +13,8 @@ using Dalamud.Utility;
using FFXIVClientStructs.Interop; using FFXIVClientStructs.Interop;
using Microsoft.Extensions.ObjectPool;
using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using CSGameObjectManager = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager; using CSGameObjectManager = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager;
@ -35,6 +37,8 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
private readonly CachedEntry[] cachedObjectTable; private readonly CachedEntry[] cachedObjectTable;
private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4];
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private unsafe ObjectTable() private unsafe ObjectTable()
{ {
@ -44,6 +48,9 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
this.cachedObjectTable = new CachedEntry[objectTableLength]; this.cachedObjectTable = new CachedEntry[objectTableLength];
for (var i = 0; i < this.cachedObjectTable.Length; i++) for (var i = 0; i < this.cachedObjectTable.Length; i++)
this.cachedObjectTable[i] = new(nativeObjectTable.GetPointer(i)); this.cachedObjectTable[i] = new(nativeObjectTable.GetPointer(i));
for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++)
this.frameworkThreadEnumerators[i] = new(this, i);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -236,25 +243,43 @@ internal sealed partial class ObjectTable
public IEnumerator<IGameObject> GetEnumerator() public IEnumerator<IGameObject> GetEnumerator()
{ {
ThreadSafety.AssertMainThread(); ThreadSafety.AssertMainThread();
return new Enumerator(this);
// If we're on the framework thread, see if there's an already allocated enumerator available for use.
foreach (ref var x in this.frameworkThreadEnumerators.AsSpan())
{
if (x is not null)
{
var t = x;
x = null;
t.Reset();
return t;
}
}
// No reusable enumerator is available; allocate a new temporary one.
return new Enumerator(this, -1);
} }
/// <inheritdoc/> /// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
private struct Enumerator(ObjectTable owner) : IEnumerator<IGameObject> private sealed class Enumerator(ObjectTable owner, int slotId) : IEnumerator<IGameObject>, IResettable
{ {
private ObjectTable? owner = owner;
private int index = -1; private int index = -1;
public IGameObject Current { get; private set; } public IGameObject Current { get; private set; } = null!;
object IEnumerator.Current => this.Current; object IEnumerator.Current => this.Current;
public bool MoveNext() public bool MoveNext()
{ {
var cache = owner.cachedObjectTable.AsSpan(); if (this.index == objectTableLength)
return false;
while (++this.index < objectTableLength) var cache = this.owner!.cachedObjectTable.AsSpan();
for (this.index++; this.index < objectTableLength; this.index++)
{ {
if (cache[this.index].Update() is { } ao) if (cache[this.index].Update() is { } ao)
{ {
@ -263,17 +288,24 @@ internal sealed partial class ObjectTable
} }
} }
this.Current = default;
return false; return false;
} }
public void Reset() public void Reset() => this.index = -1;
{
this.index = -1;
}
public void Dispose() public void Dispose()
{ {
if (this.owner is not { } o)
return;
if (slotId != -1)
o.frameworkThreadEnumerators[slotId] = this;
}
public bool TryReset()
{
this.Reset();
return true;
} }
} }
} }

View file

@ -1,7 +1,6 @@
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Game.Control;

View file

@ -9,7 +9,6 @@ using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using CSGroupManager = FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager; using CSGroupManager = FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager;
using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember;
namespace Dalamud.Game.ClientState.Party; namespace Dalamud.Game.ClientState.Party;
@ -44,20 +43,20 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
public bool IsAlliance => this.GroupManagerStruct->MainGroup.AllianceFlags > 0; public bool IsAlliance => this.GroupManagerStruct->MainGroup.AllianceFlags > 0;
/// <inheritdoc/> /// <inheritdoc/>
public unsafe nint GroupManagerAddress => (nint)CSGroupManager.Instance(); public unsafe IntPtr GroupManagerAddress => (nint)CSGroupManager.Instance();
/// <inheritdoc/> /// <inheritdoc/>
public nint GroupListAddress => (nint)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]); public IntPtr GroupListAddress => (IntPtr)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]);
/// <inheritdoc/> /// <inheritdoc/>
public nint AllianceListAddress => (nint)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]); public IntPtr AllianceListAddress => (IntPtr)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]);
/// <inheritdoc/> /// <inheritdoc/>
public long PartyId => this.GroupManagerStruct->MainGroup.PartyId; public long PartyId => this.GroupManagerStruct->MainGroup.PartyId;
private static int PartyMemberSize { get; } = Marshal.SizeOf<CSPartyMember>(); private static int PartyMemberSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember>();
private CSGroupManager* GroupManagerStruct => (CSGroupManager*)this.GroupManagerAddress; private FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager* GroupManagerStruct => (FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager*)this.GroupManagerAddress;
/// <inheritdoc/> /// <inheritdoc/>
public IPartyMember? this[int index] public IPartyMember? this[int index]
@ -82,45 +81,39 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
} }
/// <inheritdoc/> /// <inheritdoc/>
public nint GetPartyMemberAddress(int index) public IntPtr GetPartyMemberAddress(int index)
{ {
if (index < 0 || index >= GroupLength) if (index < 0 || index >= GroupLength)
return 0; return IntPtr.Zero;
return this.GroupListAddress + (index * PartyMemberSize); return this.GroupListAddress + (index * PartyMemberSize);
} }
/// <inheritdoc/> /// <inheritdoc/>
public IPartyMember? CreatePartyMemberReference(nint address) public IPartyMember? CreatePartyMemberReference(IntPtr address)
{ {
if (this.playerState.ContentId == 0) if (address == IntPtr.Zero || !this.playerState.IsLoaded)
return null; return null;
if (address == 0) return new PartyMember(address);
return null;
return new PartyMember((CSPartyMember*)address);
} }
/// <inheritdoc/> /// <inheritdoc/>
public nint GetAllianceMemberAddress(int index) public IntPtr GetAllianceMemberAddress(int index)
{ {
if (index < 0 || index >= AllianceLength) if (index < 0 || index >= AllianceLength)
return 0; return IntPtr.Zero;
return this.AllianceListAddress + (index * PartyMemberSize); return this.AllianceListAddress + (index * PartyMemberSize);
} }
/// <inheritdoc/> /// <inheritdoc/>
public IPartyMember? CreateAllianceMemberReference(nint address) public IPartyMember? CreateAllianceMemberReference(IntPtr address)
{ {
if (this.playerState.ContentId == 0) if (address == IntPtr.Zero || !this.playerState.IsLoaded)
return null; return null;
if (address == 0) return new PartyMember(address);
return null;
return new PartyMember((CSPartyMember*)address);
} }
} }
@ -135,43 +128,18 @@ internal sealed partial class PartyList
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerator<IPartyMember> GetEnumerator() public IEnumerator<IPartyMember> GetEnumerator()
{ {
return new Enumerator(this); // Normally using Length results in a recursion crash, however we know the party size via ptr.
for (var i = 0; i < this.Length; i++)
{
var member = this[i];
if (member == null)
break;
yield return member;
}
} }
/// <inheritdoc/> /// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
private struct Enumerator(PartyList partyList) : IEnumerator<IPartyMember>
{
private int index = -1;
public IPartyMember Current { get; private set; }
object IEnumerator.Current => this.Current;
public bool MoveNext()
{
while (++this.index < partyList.Length)
{
var partyMember = partyList[this.index];
if (partyMember != null)
{
this.Current = partyMember;
return true;
}
}
this.Current = default;
return false;
}
public void Reset()
{
this.index = -1;
}
public void Dispose()
{
}
}
} }

View file

@ -1,28 +1,26 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Statuses; using Dalamud.Game.ClientState.Statuses;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Utility; using Dalamud.Memory;
using Lumina.Excel; using Lumina.Excel;
using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember;
namespace Dalamud.Game.ClientState.Party; namespace Dalamud.Game.ClientState.Party;
/// <summary> /// <summary>
/// Interface representing a party member. /// Interface representing a party member.
/// </summary> /// </summary>
public interface IPartyMember : IEquatable<IPartyMember> public interface IPartyMember
{ {
/// <summary> /// <summary>
/// Gets the address of this party member in memory. /// Gets the address of this party member in memory.
/// </summary> /// </summary>
nint Address { get; } IntPtr Address { get; }
/// <summary> /// <summary>
/// Gets a list of buffs or debuffs applied to this party member. /// Gets a list of buffs or debuffs applied to this party member.
@ -110,82 +108,69 @@ public interface IPartyMember : IEquatable<IPartyMember>
} }
/// <summary> /// <summary>
/// This struct represents a party member in the group manager. /// This class represents a party member in the group manager.
/// </summary> /// </summary>
/// <param name="ptr">A pointer to the PartyMember.</param> internal unsafe class PartyMember : IPartyMember
internal unsafe readonly struct PartyMember(CSPartyMember* ptr) : IPartyMember
{ {
/// <inheritdoc/> /// <summary>
public nint Address => (nint)ptr; /// Initializes a new instance of the <see cref="PartyMember"/> class.
/// </summary>
/// <param name="address">Address of the party member.</param>
internal PartyMember(IntPtr address)
{
this.Address = address;
}
/// <inheritdoc/> /// <inheritdoc/>
public StatusList Statuses => new(&ptr->StatusManager); public IntPtr Address { get; }
/// <inheritdoc/> /// <inheritdoc/>
public Vector3 Position => ptr->Position; public StatusList Statuses => new(&this.Struct->StatusManager);
/// <inheritdoc/> /// <inheritdoc/>
[Api15ToDo("Change type to ulong.")] public Vector3 Position => this.Struct->Position;
public long ContentId => (long)ptr->ContentId;
/// <inheritdoc/> /// <inheritdoc/>
public uint ObjectId => ptr->EntityId; public long ContentId => (long)this.Struct->ContentId;
/// <inheritdoc/> /// <inheritdoc/>
public uint EntityId => ptr->EntityId; public uint ObjectId => this.Struct->EntityId;
/// <inheritdoc/>
public uint EntityId => this.Struct->EntityId;
/// <inheritdoc/> /// <inheritdoc/>
public IGameObject? GameObject => Service<ObjectTable>.Get().SearchById(this.EntityId); public IGameObject? GameObject => Service<ObjectTable>.Get().SearchById(this.EntityId);
/// <inheritdoc/> /// <inheritdoc/>
public uint CurrentHP => ptr->CurrentHP; public uint CurrentHP => this.Struct->CurrentHP;
/// <inheritdoc/> /// <inheritdoc/>
public uint MaxHP => ptr->MaxHP; public uint MaxHP => this.Struct->MaxHP;
/// <inheritdoc/> /// <inheritdoc/>
public ushort CurrentMP => ptr->CurrentMP; public ushort CurrentMP => this.Struct->CurrentMP;
/// <inheritdoc/> /// <inheritdoc/>
public ushort MaxMP => ptr->MaxMP; public ushort MaxMP => this.Struct->MaxMP;
/// <inheritdoc/> /// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.TerritoryType> Territory => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(ptr->TerritoryType); public RowRef<Lumina.Excel.Sheets.TerritoryType> Territory => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->TerritoryType);
/// <inheritdoc/> /// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.World> World => LuminaUtils.CreateRef<Lumina.Excel.Sheets.World>(ptr->HomeWorld); public RowRef<Lumina.Excel.Sheets.World> World => LuminaUtils.CreateRef<Lumina.Excel.Sheets.World>(this.Struct->HomeWorld);
/// <inheritdoc/> /// <inheritdoc/>
public SeString Name => SeString.Parse(ptr->Name); public SeString Name => SeString.Parse(this.Struct->Name);
/// <inheritdoc/> /// <inheritdoc/>
public byte Sex => ptr->Sex; public byte Sex => this.Struct->Sex;
/// <inheritdoc/> /// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob => LuminaUtils.CreateRef<Lumina.Excel.Sheets.ClassJob>(ptr->ClassJob); public RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob => LuminaUtils.CreateRef<Lumina.Excel.Sheets.ClassJob>(this.Struct->ClassJob);
/// <inheritdoc/> /// <inheritdoc/>
public byte Level => ptr->Level; public byte Level => this.Struct->Level;
public static bool operator ==(PartyMember x, PartyMember y) => x.Equals(y); private FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember*)this.Address;
public static bool operator !=(PartyMember x, PartyMember y) => !(x == y);
/// <inheritdoc/>
public bool Equals(IPartyMember? other)
{
return this.EntityId == other.EntityId;
}
/// <inheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is PartyMember fate && this.Equals(fate);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return this.EntityId.GetHashCode();
}
} }

View file

@ -1,49 +1,61 @@
using System.Diagnostics.CodeAnalysis;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Lumina.Excel; using Lumina.Excel;
using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status;
namespace Dalamud.Game.ClientState.Statuses; namespace Dalamud.Game.ClientState.Statuses;
/// <summary> /// <summary>
/// Interface representing a status. /// This class represents a status effect an actor is afflicted by.
/// </summary> /// </summary>
public interface IStatus : IEquatable<IStatus> public unsafe class Status
{ {
/// <summary>
/// Initializes a new instance of the <see cref="Status"/> class.
/// </summary>
/// <param name="address">Status address.</param>
internal Status(IntPtr address)
{
this.Address = address;
}
/// <summary> /// <summary>
/// Gets the address of the status in memory. /// Gets the address of the status in memory.
/// </summary> /// </summary>
nint Address { get; } public IntPtr Address { get; }
/// <summary> /// <summary>
/// Gets the status ID of this status. /// Gets the status ID of this status.
/// </summary> /// </summary>
uint StatusId { get; } public uint StatusId => this.Struct->StatusId;
/// <summary> /// <summary>
/// Gets the GameData associated with this status. /// Gets the GameData associated with this status.
/// </summary> /// </summary>
RowRef<Lumina.Excel.Sheets.Status> GameData { get; } public RowRef<Lumina.Excel.Sheets.Status> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Status>(this.Struct->StatusId);
/// <summary> /// <summary>
/// Gets the parameter value of the status. /// Gets the parameter value of the status.
/// </summary> /// </summary>
ushort Param { get; } public ushort Param => this.Struct->Param;
/// <summary>
/// Gets the stack count of this status.
/// Only valid if this is a non-food status.
/// </summary>
[Obsolete($"Replaced with {nameof(Param)}", true)]
public byte StackCount => (byte)this.Struct->Param;
/// <summary> /// <summary>
/// Gets the time remaining of this status. /// Gets the time remaining of this status.
/// </summary> /// </summary>
float RemainingTime { get; } public float RemainingTime => this.Struct->RemainingTime;
/// <summary> /// <summary>
/// Gets the source ID of this status. /// Gets the source ID of this status.
/// </summary> /// </summary>
uint SourceId { get; } public uint SourceId => this.Struct->SourceObject.ObjectId;
/// <summary> /// <summary>
/// Gets the source actor associated with this status. /// Gets the source actor associated with this status.
@ -51,55 +63,7 @@ public interface IStatus : IEquatable<IStatus>
/// <remarks> /// <remarks>
/// This iterates the actor table, it should be used with care. /// This iterates the actor table, it should be used with care.
/// </remarks> /// </remarks>
IGameObject? SourceObject { get; }
}
/// <summary>
/// This struct represents a status effect an actor is afflicted by.
/// </summary>
/// <param name="ptr">A pointer to the Status.</param>
internal unsafe readonly struct Status(CSStatus* ptr) : IStatus
{
/// <inheritdoc/>
public nint Address => (nint)ptr;
/// <inheritdoc/>
public uint StatusId => ptr->StatusId;
/// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.Status> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Status>(ptr->StatusId);
/// <inheritdoc/>
public ushort Param => ptr->Param;
/// <inheritdoc/>
public float RemainingTime => ptr->RemainingTime;
/// <inheritdoc/>
public uint SourceId => ptr->SourceObject.ObjectId;
/// <inheritdoc/>
public IGameObject? SourceObject => Service<ObjectTable>.Get().SearchById(this.SourceId); public IGameObject? SourceObject => Service<ObjectTable>.Get().SearchById(this.SourceId);
public static bool operator ==(Status x, Status y) => x.Equals(y); private FFXIVClientStructs.FFXIV.Client.Game.Status* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Status*)this.Address;
public static bool operator !=(Status x, Status y) => !(x == y);
/// <inheritdoc/>
public bool Equals(IStatus? other)
{
return this.StatusId == other.StatusId && this.SourceId == other.SourceId && this.Param == other.Param && this.RemainingTime == other.RemainingTime;
}
/// <inheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is Status fate && this.Equals(fate);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(this.StatusId, this.SourceId, this.Param, this.RemainingTime);
}
} }

View file

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status; using Dalamud.Game.Player;
namespace Dalamud.Game.ClientState.Statuses; namespace Dalamud.Game.ClientState.Statuses;
@ -16,7 +16,7 @@ public sealed unsafe partial class StatusList
/// Initializes a new instance of the <see cref="StatusList"/> class. /// Initializes a new instance of the <see cref="StatusList"/> class.
/// </summary> /// </summary>
/// <param name="address">Address of the status list.</param> /// <param name="address">Address of the status list.</param>
internal StatusList(nint address) internal StatusList(IntPtr address)
{ {
this.Address = address; this.Address = address;
} }
@ -26,14 +26,14 @@ public sealed unsafe partial class StatusList
/// </summary> /// </summary>
/// <param name="pointer">Pointer to the status list.</param> /// <param name="pointer">Pointer to the status list.</param>
internal unsafe StatusList(void* pointer) internal unsafe StatusList(void* pointer)
: this((nint)pointer) : this((IntPtr)pointer)
{ {
} }
/// <summary> /// <summary>
/// Gets the address of the status list in memory. /// Gets the address of the status list in memory.
/// </summary> /// </summary>
public nint Address { get; } public IntPtr Address { get; }
/// <summary> /// <summary>
/// Gets the amount of status effect slots the actor has. /// Gets the amount of status effect slots the actor has.
@ -49,7 +49,7 @@ public sealed unsafe partial class StatusList
/// </summary> /// </summary>
/// <param name="index">Status Index.</param> /// <param name="index">Status Index.</param>
/// <returns>The status at the specified index.</returns> /// <returns>The status at the specified index.</returns>
public IStatus? this[int index] public Status? this[int index]
{ {
get get
{ {
@ -66,7 +66,7 @@ public sealed unsafe partial class StatusList
/// </summary> /// </summary>
/// <param name="address">The address of the status list in memory.</param> /// <param name="address">The address of the status list in memory.</param>
/// <returns>The status object containing the requested data.</returns> /// <returns>The status object containing the requested data.</returns>
public static StatusList? CreateStatusListReference(nint address) public static StatusList? CreateStatusListReference(IntPtr address)
{ {
if (address == IntPtr.Zero) if (address == IntPtr.Zero)
return null; return null;
@ -74,12 +74,8 @@ public sealed unsafe partial class StatusList
// The use case for CreateStatusListReference and CreateStatusReference to be static is so // The use case for CreateStatusListReference and CreateStatusReference to be static is so
// fake status lists can be generated. Since they aren't exposed as services, it's either // fake status lists can be generated. Since they aren't exposed as services, it's either
// here or somewhere else. // here or somewhere else.
var clientState = Service<ClientState>.Get(); var playerState = Service<PlayerState>.Get();
if (!playerState.IsLoaded)
if (clientState.LocalContentId == 0)
return null;
if (address == 0)
return null; return null;
return new StatusList(address); return new StatusList(address);
@ -90,15 +86,16 @@ public sealed unsafe partial class StatusList
/// </summary> /// </summary>
/// <param name="address">The address of the status effect in memory.</param> /// <param name="address">The address of the status effect in memory.</param>
/// <returns>The status object containing the requested data.</returns> /// <returns>The status object containing the requested data.</returns>
public static IStatus? CreateStatusReference(nint address) public static Status? CreateStatusReference(IntPtr address)
{ {
if (address == IntPtr.Zero) if (address == IntPtr.Zero)
return null; return null;
if (address == 0) var playerState = Service<PlayerState>.Get();
if (!playerState.IsLoaded)
return null; return null;
return new Status((CSStatus*)address); return new Status(address);
} }
/// <summary> /// <summary>
@ -106,22 +103,22 @@ public sealed unsafe partial class StatusList
/// </summary> /// </summary>
/// <param name="index">The index of the status.</param> /// <param name="index">The index of the status.</param>
/// <returns>The memory address of the status.</returns> /// <returns>The memory address of the status.</returns>
public nint GetStatusAddress(int index) public IntPtr GetStatusAddress(int index)
{ {
if (index < 0 || index >= this.Length) if (index < 0 || index >= this.Length)
return 0; return IntPtr.Zero;
return (nint)Unsafe.AsPointer(ref this.Struct->Status[index]); return (IntPtr)Unsafe.AsPointer(ref this.Struct->Status[index]);
} }
} }
/// <summary> /// <summary>
/// This collection represents the status effects an actor is afflicted by. /// This collection represents the status effects an actor is afflicted by.
/// </summary> /// </summary>
public sealed partial class StatusList : IReadOnlyCollection<IStatus>, ICollection public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollection
{ {
/// <inheritdoc/> /// <inheritdoc/>
int IReadOnlyCollection<IStatus>.Count => this.Length; int IReadOnlyCollection<Status>.Count => this.Length;
/// <inheritdoc/> /// <inheritdoc/>
int ICollection.Count => this.Length; int ICollection.Count => this.Length;
@ -133,9 +130,17 @@ public sealed partial class StatusList : IReadOnlyCollection<IStatus>, ICollecti
object ICollection.SyncRoot => this; object ICollection.SyncRoot => this;
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerator<IStatus> GetEnumerator() public IEnumerator<Status> GetEnumerator()
{ {
return new Enumerator(this); for (var i = 0; i < this.Length; i++)
{
var status = this[i];
if (status == null || status.StatusId == 0)
continue;
yield return status;
}
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -150,38 +155,4 @@ public sealed partial class StatusList : IReadOnlyCollection<IStatus>, ICollecti
index++; index++;
} }
} }
private struct Enumerator(StatusList statusList) : IEnumerator<IStatus>
{
private int index = -1;
public IStatus Current { get; private set; }
object IEnumerator.Current => this.Current;
public bool MoveNext()
{
while (++this.index < statusList.Length)
{
var status = statusList[this.index];
if (status != null && status.StatusId != 0)
{
this.Current = status;
return true;
}
}
this.Current = default;
return false;
}
public void Reset()
{
this.index = -1;
}
public void Dispose()
{
}
}
} }

View file

@ -0,0 +1,35 @@
using System.Runtime.InteropServices;
namespace Dalamud.Game.ClientState.Structs;
/// <summary>
/// Native memory representation of a FFXIV status effect.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct StatusEffect
{
/// <summary>
/// The effect ID.
/// </summary>
public short EffectId;
/// <summary>
/// How many stacks are present.
/// </summary>
public byte StackCount;
/// <summary>
/// Additional parameters.
/// </summary>
public byte Param;
/// <summary>
/// The duration remaining.
/// </summary>
public float Duration;
/// <summary>
/// The ID of the actor that caused this effect.
/// </summary>
public int OwnerId;
}

View file

@ -59,7 +59,7 @@ public class ZoneInitEventArgs : EventArgs
eventArgs.ContentFinderCondition = dataManager.GetExcelSheet<ContentFinderCondition>().GetRow(*(ushort*)(packet + 0x06)); eventArgs.ContentFinderCondition = dataManager.GetExcelSheet<ContentFinderCondition>().GetRow(*(ushort*)(packet + 0x06));
eventArgs.Weather = dataManager.GetExcelSheet<Weather>().GetRow(*(byte*)(packet + 0x10)); eventArgs.Weather = dataManager.GetExcelSheet<Weather>().GetRow(*(byte*)(packet + 0x10));
const int NumFestivals = 8; const int NumFestivals = 4;
eventArgs.ActiveFestivals = new Festival[NumFestivals]; eventArgs.ActiveFestivals = new Festival[NumFestivals];
eventArgs.ActiveFestivalPhases = new ushort[NumFestivals]; eventArgs.ActiveFestivalPhases = new ushort[NumFestivals];
@ -67,7 +67,7 @@ public class ZoneInitEventArgs : EventArgs
// but it's unclear why they exist as separate entries and why they would be different. // but it's unclear why they exist as separate entries and why they would be different.
for (var i = 0; i < NumFestivals; i++) for (var i = 0; i < NumFestivals; i++)
{ {
eventArgs.ActiveFestivals[i] = dataManager.GetExcelSheet<Festival>().GetRow(*(ushort*)(packet + 0x26 + (i * 2))); eventArgs.ActiveFestivals[i] = dataManager.GetExcelSheet<Festival>().GetRow(*(ushort*)(packet + 0x2E + (i * 2)));
eventArgs.ActiveFestivalPhases[i] = *(ushort*)(packet + 0x36 + (i * 2)); eventArgs.ActiveFestivalPhases[i] = *(ushort*)(packet + 0x36 + (i * 2));
} }

View file

@ -1,6 +1,4 @@
using Dalamud.Plugin.Services; namespace Dalamud.Game.Config;
namespace Dalamud.Game.Config;
/// <summary> /// <summary>
/// Game config system address resolver. /// Game config system address resolver.

View file

@ -4069,13 +4069,6 @@ public enum UiConfigOption
[GameConfigOption("GposePortraitRotateType", ConfigType.UInt)] [GameConfigOption("GposePortraitRotateType", ConfigType.UInt)]
GposePortraitRotateType, GposePortraitRotateType,
/// <summary>
/// UiConfig option with the internal name GroupPosePortraitUnlockAspectLimit.
/// This option is a UInt.
/// </summary>
[GameConfigOption("GroupPosePortraitUnlockAspectLimit", ConfigType.UInt)]
GroupPosePortraitUnlockAspectLimit,
/// <summary> /// <summary>
/// UiConfig option with the internal name LsListSortPriority. /// UiConfig option with the internal name LsListSortPriority.
/// This option is a UInt. /// This option is a UInt.

View file

@ -1,5 +1,3 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.DutyState; namespace Dalamud.Game.DutyState;
/// <summary> /// <summary>

View file

@ -26,6 +26,7 @@ using Lumina.Text;
using Lumina.Text.Payloads; using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
using LSeStringBuilder = Lumina.Text.SeStringBuilder;
using SeString = Dalamud.Game.Text.SeStringHandling.SeString; using SeString = Dalamud.Game.Text.SeStringHandling.SeString;
using SeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder; using SeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder;
@ -206,21 +207,21 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
if (this.chatQueue.Count == 0) if (this.chatQueue.Count == 0)
return; return;
using var rssb = new RentedSeStringBuilder(); var sb = LSeStringBuilder.SharedPool.Get();
Span<byte> namebuf = stackalloc byte[256]; Span<byte> namebuf = stackalloc byte[256];
using var sender = new Utf8String(); using var sender = new Utf8String();
using var message = new Utf8String(); using var message = new Utf8String();
while (this.chatQueue.TryDequeue(out var chat)) while (this.chatQueue.TryDequeue(out var chat))
{ {
rssb.Builder.Clear(); sb.Clear();
foreach (var c in UtfEnumerator.From(chat.MessageBytes, UtfEnumeratorFlags.Utf8SeString)) foreach (var c in UtfEnumerator.From(chat.MessageBytes, UtfEnumeratorFlags.Utf8SeString))
{ {
if (c.IsSeStringPayload) if (c.IsSeStringPayload)
rssb.Builder.Append((ReadOnlySeStringSpan)chat.MessageBytes.AsSpan(c.ByteOffset, c.ByteLength)); sb.Append((ReadOnlySeStringSpan)chat.MessageBytes.AsSpan(c.ByteOffset, c.ByteLength));
else if (c.Value.IntValue == 0x202F) else if (c.Value.IntValue == 0x202F)
rssb.Builder.BeginMacro(MacroCode.NonBreakingSpace).EndMacro(); sb.BeginMacro(MacroCode.NonBreakingSpace).EndMacro();
else else
rssb.Builder.Append(c); sb.Append(c);
} }
if (chat.NameBytes.Length + 1 < namebuf.Length) if (chat.NameBytes.Length + 1 < namebuf.Length)
@ -234,7 +235,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
sender.SetString(chat.NameBytes.NullTerminate()); sender.SetString(chat.NameBytes.NullTerminate());
} }
message.SetString(rssb.Builder.GetViewAsSpan()); message.SetString(sb.GetViewAsSpan());
var targetChannel = chat.Type ?? this.configuration.GeneralChatType; var targetChannel = chat.Type ?? this.configuration.GeneralChatType;
@ -246,6 +247,8 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
chat.Timestamp, chat.Timestamp,
(byte)(chat.Silent ? 1 : 0)); (byte)(chat.Silent ? 1 : 0));
} }
LSeStringBuilder.SharedPool.Return(sb);
} }
/// <summary> /// <summary>
@ -323,28 +326,29 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
private void PrintTagged(ReadOnlySpan<byte> message, XivChatType channel, string? tag, ushort? color) private void PrintTagged(ReadOnlySpan<byte> message, XivChatType channel, string? tag, ushort? color)
{ {
using var rssb = new RentedSeStringBuilder(); var sb = LSeStringBuilder.SharedPool.Get();
if (!tag.IsNullOrEmpty()) if (!tag.IsNullOrEmpty())
{ {
if (color is not null) if (color is not null)
{ {
rssb.Builder sb.PushColorType(color.Value);
.PushColorType(color.Value) sb.Append($"[{tag}] ");
.Append($"[{tag}] ") sb.PopColorType();
.PopColorType();
} }
else else
{ {
rssb.Builder.Append($"[{tag}] "); sb.Append($"[{tag}] ");
} }
} }
this.Print(new XivChatEntry this.Print(new XivChatEntry
{ {
MessageBytes = rssb.Builder.Append((ReadOnlySeStringSpan)message).ToArray(), MessageBytes = sb.Append((ReadOnlySeStringSpan)message).ToArray(),
Type = channel, Type = channel,
}); });
LSeStringBuilder.SharedPool.Return(sb);
} }
private void InventoryItemCopyDetour(InventoryItem* thisPtr, InventoryItem* otherPtr) private void InventoryItemCopyDetour(InventoryItem* thisPtr, InventoryItem* otherPtr)
@ -453,8 +457,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}"); Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}");
using var rssb = new RentedSeStringBuilder(); var sb = LSeStringBuilder.SharedPool.Get();
try try
{ {
var seStringSpan = new ReadOnlySeStringSpan(linkData->Payload); var seStringSpan = new ReadOnlySeStringSpan(linkData->Payload);
@ -462,7 +465,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
// read until link terminator // read until link terminator
foreach (var payload in seStringSpan) foreach (var payload in seStringSpan)
{ {
rssb.Builder.Append(payload); sb.Append(payload);
if (payload.Type == ReadOnlySePayloadType.Macro && if (payload.Type == ReadOnlySePayloadType.Macro &&
payload.MacroCode == MacroCode.Link && payload.MacroCode == MacroCode.Link &&
@ -474,7 +477,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
} }
} }
var seStr = SeString.Parse(rssb.Builder.ToArray()); var seStr = SeString.Parse(sb.ToArray());
if (seStr.Payloads.Count == 0 || seStr.Payloads[0] is not DalamudLinkPayload link) if (seStr.Payloads.Count == 0 || seStr.Payloads[0] is not DalamudLinkPayload link)
return; return;
@ -492,6 +495,10 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
{ {
Log.Error(ex, "Exception in HandleLinkClickDetour"); Log.Error(ex, "Exception in HandleLinkClickDetour");
} }
finally
{
LSeStringBuilder.SharedPool.Return(sb);
}
} }
} }

View file

@ -31,7 +31,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
private static readonly ModuleLog Log = new("ContextMenu"); private static readonly ModuleLog Log = new("ContextMenu");
private readonly Hook<AtkModuleVf22OpenAddonByAgentDelegate> atkModuleVf22OpenAddonByAgentHook; private readonly Hook<AtkModuleVf22OpenAddonByAgentDelegate> atkModuleVf22OpenAddonByAgentHook;
private readonly Hook<AddonContextMenu.Delegates.OnMenuSelected> addonContextMenuOnMenuSelectedHook; private readonly Hook<AddonContextMenuOnMenuSelectedDelegate> addonContextMenuOnMenuSelectedHook;
private uint? addonContextSubNameId; private uint? addonContextSubNameId;
@ -40,7 +40,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
{ {
var raptureAtkModuleVtable = (nint*)RaptureAtkModule.StaticVirtualTablePointer; var raptureAtkModuleVtable = (nint*)RaptureAtkModule.StaticVirtualTablePointer;
this.atkModuleVf22OpenAddonByAgentHook = Hook<AtkModuleVf22OpenAddonByAgentDelegate>.FromAddress(raptureAtkModuleVtable[22], this.AtkModuleVf22OpenAddonByAgentDetour); this.atkModuleVf22OpenAddonByAgentHook = Hook<AtkModuleVf22OpenAddonByAgentDelegate>.FromAddress(raptureAtkModuleVtable[22], this.AtkModuleVf22OpenAddonByAgentDetour);
this.addonContextMenuOnMenuSelectedHook = Hook<AddonContextMenu.Delegates.OnMenuSelected>.FromAddress((nint)AddonContextMenu.StaticVirtualTablePointer->OnMenuSelected, this.AddonContextMenuOnMenuSelectedDetour); this.addonContextMenuOnMenuSelectedHook = Hook<AddonContextMenuOnMenuSelectedDelegate>.FromAddress((nint)AddonContextMenu.StaticVirtualTablePointer->OnMenuSelected, this.AddonContextMenuOnMenuSelectedDetour);
this.atkModuleVf22OpenAddonByAgentHook.Enable(); this.atkModuleVf22OpenAddonByAgentHook.Enable();
this.addonContextMenuOnMenuSelectedHook.Enable(); this.addonContextMenuOnMenuSelectedHook.Enable();
@ -48,6 +48,10 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
private delegate ushort AtkModuleVf22OpenAddonByAgentDelegate(AtkModule* module, byte* addonName, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, bool a8); private delegate ushort AtkModuleVf22OpenAddonByAgentDelegate(AtkModule* module, byte* addonName, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, bool a8);
private delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3);
private delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2);
/// <inheritdoc/> /// <inheritdoc/>
public event IContextMenu.OnMenuOpenedDelegate? OnMenuOpened; public event IContextMenu.OnMenuOpenedDelegate? OnMenuOpened;
@ -181,7 +185,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
values[0].ChangeType(ValueType.UInt); values[0].ChangeType(ValueType.UInt);
values[0].UInt = 0; values[0].UInt = 0;
values[1].ChangeType(ValueType.String); values[1].ChangeType(ValueType.String);
values[1].SetManagedString(name.EncodeWithNullTerminator()); values[1].SetManagedString(name.Encode().NullTerminate());
values[2].ChangeType(ValueType.Int); values[2].ChangeType(ValueType.Int);
values[2].Int = x; values[2].Int = x;
values[3].ChangeType(ValueType.Int); values[3].ChangeType(ValueType.Int);
@ -261,7 +265,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
submenuMask |= 1u << i; submenuMask |= 1u << i;
nameData[i].ChangeType(ValueType.String); nameData[i].ChangeType(ValueType.String);
nameData[i].SetManagedString(this.GetPrefixedName(item).EncodeWithNullTerminator()); nameData[i].SetManagedString(this.GetPrefixedName(item).Encode().NullTerminate());
} }
for (var i = 0; i < prefixMenuSize; ++i) for (var i = 0; i < prefixMenuSize; ++i)
@ -291,9 +295,8 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
// 2: UInt = Return Mask (?) // 2: UInt = Return Mask (?)
// 3: UInt = Submenu Mask // 3: UInt = Submenu Mask
// 4: UInt = OpenAtCursorPosition ? 2 : 1 // 4: UInt = OpenAtCursorPosition ? 2 : 1
// 5: UInt = ? // 5: UInt = 0
// 6: UInt = ? // 6: UInt = 0
// 7: UInt = ?
foreach (var item in items) foreach (var item in items)
{ {
@ -309,7 +312,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
} }
} }
this.SetupGenericMenu(8, 0, 2, 3, items, ref valueCount, ref values); this.SetupGenericMenu(7, 0, 2, 3, items, ref valueCount, ref values);
} }
private void SetupContextSubMenu(IReadOnlyList<IMenuItem> items, ref int valueCount, ref AtkValue* values) private void SetupContextSubMenu(IReadOnlyList<IMenuItem> items, ref int valueCount, ref AtkValue* values)

View file

@ -150,7 +150,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
} }
/// <inheritdoc/> /// <inheritdoc/>
[Api15ToDo("Maybe make this config scoped to internal name?")] [Api13ToDo("Maybe make this config scoped to internal name?")]
public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
/// <inheritdoc/> /// <inheritdoc/>

View file

@ -92,16 +92,34 @@ public enum FlyTextKind : int
/// </summary> /// </summary>
IslandExp = 15, IslandExp = 15,
/// <summary>
/// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle.
/// </summary>
[Obsolete("Use Dataset instead", true)]
Unknown16 = 16,
/// <summary> /// <summary>
/// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle. /// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle.
/// </summary> /// </summary>
Dataset = 16, Dataset = 16,
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle.
/// </summary>
[Obsolete("Use Knowledge instead", true)]
Unknown17 = 17,
/// <summary> /// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle. /// Val1 in serif font, Text2 in sans-serif as subtitle.
/// </summary> /// </summary>
Knowledge = 17, Knowledge = 17,
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle.
/// </summary>
[Obsolete("Use PhantomExp instead", true)]
Unknown18 = 18,
/// <summary> /// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle. /// Val1 in serif font, Text2 in sans-serif as subtitle.
/// </summary> /// </summary>

View file

@ -1,5 +1,3 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.Gui; namespace Dalamud.Game.Gui;
/// <summary> /// <summary>

View file

@ -14,145 +14,140 @@ public enum HoverActionKind
/// <summary> /// <summary>
/// A regular action is hovered. /// A regular action is hovered.
/// </summary> /// </summary>
Action = 29, Action = 28,
/// <summary> /// <summary>
/// A crafting action is hovered. /// A crafting action is hovered.
/// </summary> /// </summary>
CraftingAction = 30, CraftingAction = 29,
/// <summary> /// <summary>
/// A general action is hovered. /// A general action is hovered.
/// </summary> /// </summary>
GeneralAction = 31, GeneralAction = 30,
/// <summary> /// <summary>
/// A companion order type of action is hovered. /// A companion order type of action is hovered.
/// </summary> /// </summary>
CompanionOrder = 32, // Game Term: BuddyOrder CompanionOrder = 31, // Game Term: BuddyOrder
/// <summary> /// <summary>
/// A main command type of action is hovered. /// A main command type of action is hovered.
/// </summary> /// </summary>
MainCommand = 33, MainCommand = 32,
/// <summary> /// <summary>
/// An extras command type of action is hovered. /// An extras command type of action is hovered.
/// </summary> /// </summary>
ExtraCommand = 34, ExtraCommand = 33,
/// <summary> /// <summary>
/// A companion action is hovered. /// A companion action is hovered.
/// </summary> /// </summary>
Companion = 35, Companion = 34,
/// <summary> /// <summary>
/// A pet order type of action is hovered. /// A pet order type of action is hovered.
/// </summary> /// </summary>
PetOrder = 36, PetOrder = 35,
/// <summary> /// <summary>
/// A trait is hovered. /// A trait is hovered.
/// </summary> /// </summary>
Trait = 37, Trait = 36,
/// <summary> /// <summary>
/// A buddy action is hovered. /// A buddy action is hovered.
/// </summary> /// </summary>
BuddyAction = 38, BuddyAction = 37,
/// <summary> /// <summary>
/// A company action is hovered. /// A company action is hovered.
/// </summary> /// </summary>
CompanyAction = 39, CompanyAction = 38,
/// <summary> /// <summary>
/// A mount is hovered. /// A mount is hovered.
/// </summary> /// </summary>
Mount = 40, Mount = 39,
/// <summary> /// <summary>
/// A chocobo race action is hovered. /// A chocobo race action is hovered.
/// </summary> /// </summary>
ChocoboRaceAction = 41, ChocoboRaceAction = 40,
/// <summary> /// <summary>
/// A chocobo race item is hovered. /// A chocobo race item is hovered.
/// </summary> /// </summary>
ChocoboRaceItem = 42, ChocoboRaceItem = 41,
/// <summary> /// <summary>
/// A deep dungeon equipment is hovered. /// A deep dungeon equipment is hovered.
/// </summary> /// </summary>
DeepDungeonEquipment = 43, DeepDungeonEquipment = 42,
/// <summary> /// <summary>
/// A deep dungeon equipment 2 is hovered. /// A deep dungeon equipment 2 is hovered.
/// </summary> /// </summary>
DeepDungeonEquipment2 = 44, DeepDungeonEquipment2 = 43,
/// <summary> /// <summary>
/// A deep dungeon item is hovered. /// A deep dungeon item is hovered.
/// </summary> /// </summary>
DeepDungeonItem = 45, DeepDungeonItem = 44,
/// <summary> /// <summary>
/// A quick chat is hovered. /// A quick chat is hovered.
/// </summary> /// </summary>
QuickChat = 46, QuickChat = 45,
/// <summary> /// <summary>
/// An action combo route is hovered. /// An action combo route is hovered.
/// </summary> /// </summary>
ActionComboRoute = 47, ActionComboRoute = 46,
/// <summary> /// <summary>
/// A pvp trait is hovered. /// A pvp trait is hovered.
/// </summary> /// </summary>
PvPSelectTrait = 48, PvPSelectTrait = 47,
/// <summary> /// <summary>
/// A squadron action is hovered. /// A squadron action is hovered.
/// </summary> /// </summary>
BgcArmyAction = 49, BgcArmyAction = 48,
/// <summary> /// <summary>
/// A perform action is hovered. /// A perform action is hovered.
/// </summary> /// </summary>
Perform = 50, Perform = 49,
/// <summary> /// <summary>
/// A deep dungeon magic stone is hovered. /// A deep dungeon magic stone is hovered.
/// </summary> /// </summary>
DeepDungeonMagicStone = 51, DeepDungeonMagicStone = 50,
/// <summary> /// <summary>
/// A deep dungeon demiclone is hovered. /// A deep dungeon demiclone is hovered.
/// </summary> /// </summary>
DeepDungeonDemiclone = 52, DeepDungeonDemiclone = 51,
/// <summary> /// <summary>
/// An eureka magia action is hovered. /// An eureka magia action is hovered.
/// </summary> /// </summary>
EurekaMagiaAction = 53, EurekaMagiaAction = 52,
/// <summary> /// <summary>
/// An island sanctuary temporary item is hovered. /// An island sanctuary temporary item is hovered.
/// </summary> /// </summary>
MYCTemporaryItem = 54, MYCTemporaryItem = 53,
/// <summary> /// <summary>
/// An ornament is hovered. /// An ornament is hovered.
/// </summary> /// </summary>
Ornament = 55, Ornament = 54,
/// <summary> /// <summary>
/// Glasses are hovered. /// Glasses are hovered.
/// </summary> /// </summary>
Glasses = 56, Glasses = 55,
/// <summary>
/// Phantom Job Trait is hovered.
/// </summary>
MKDTrait = 58,
} }

View file

@ -1,5 +1,3 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.Gui.NamePlate; namespace Dalamud.Game.Gui.NamePlate;
/// <summary> /// <summary>

View file

@ -11,6 +11,8 @@ using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.Completion; using FFXIVClientStructs.FFXIV.Component.Completion;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Text;
namespace Dalamud.Game.Internal; namespace Dalamud.Game.Internal;
/// <summary> /// <summary>
@ -251,14 +253,16 @@ internal sealed unsafe class DalamudCompletion : IInternalDisposableService
{ {
public EntryStrings(string command) public EntryStrings(string command)
{ {
using var rssb = new RentedSeStringBuilder(); var rssb = SeStringBuilder.SharedPool.Get();
this.Display = Utf8String.FromSequence(rssb.Builder this.Display = Utf8String.FromSequence(rssb
.PushColorType(539) .PushColorType(539)
.Append(command) .Append(command)
.PopColorType() .PopColorType()
.GetViewAsSpan()); .GetViewAsSpan());
SeStringBuilder.SharedPool.Return(rssb);
this.Match = Utf8String.FromString(command); this.Match = Utf8String.FromString(command);
} }

View file

@ -305,8 +305,7 @@ internal class GameInventory : IInternalDisposableService
private GameInventoryItem[] CreateItemsArray(int length) private GameInventoryItem[] CreateItemsArray(int length)
{ {
var items = new GameInventoryItem[length]; var items = new GameInventoryItem[length];
foreach (ref var item in items.AsSpan()) items.Initialize();
item = new();
return items; return items;
} }

View file

@ -1,5 +1,3 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.Network; namespace Dalamud.Game.Network;
/// <summary> /// <summary>

View file

@ -1,6 +1,4 @@
using Dalamud.Plugin.Services; namespace Dalamud.Game.Network.Internal;
namespace Dalamud.Game.Network.Internal;
/// <summary> /// <summary>
/// Internal address resolver for the network handlers. /// Internal address resolver for the network handlers.

View file

@ -8,8 +8,6 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using Dalamud.Plugin.Services;
using Iced.Intel; using Iced.Intel;
using Newtonsoft.Json; using Newtonsoft.Json;
using Serilog; using Serilog;

View file

@ -1,9 +1,8 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
namespace Dalamud.Game; namespace Dalamud.Game;

View file

@ -102,15 +102,16 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
// TODO: remove culture info toggling after supporting CultureInfo for SeStringBuilder.Append, // TODO: remove culture info toggling after supporting CultureInfo for SeStringBuilder.Append,
// and then remove try...finally block (discard builder from the pool on exception) // and then remove try...finally block (discard builder from the pool on exception)
var previousCulture = CultureInfo.CurrentCulture; var previousCulture = CultureInfo.CurrentCulture;
using var rssb = new RentedSeStringBuilder(); var builder = SeStringBuilder.SharedPool.Get();
try try
{ {
CultureInfo.CurrentCulture = Localization.GetCultureInfoFromLangCode(lang.ToCode()); CultureInfo.CurrentCulture = Localization.GetCultureInfoFromLangCode(lang.ToCode());
return this.EvaluateAndAppendTo(rssb.Builder, str, localParameters, lang).ToReadOnlySeString(); return this.EvaluateAndAppendTo(builder, str, localParameters, lang).ToReadOnlySeString();
} }
finally finally
{ {
CultureInfo.CurrentCulture = previousCulture; CultureInfo.CurrentCulture = previousCulture;
SeStringBuilder.SharedPool.Return(builder);
} }
} }
@ -929,8 +930,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
itemId += 1000000; itemId += 1000000;
} }
using var rssb = new RentedSeStringBuilder(); var sb = SeStringBuilder.SharedPool.Get();
var sb = rssb.Builder;
sb.Append(this.EvaluateFromAddon(6, [rarity], context.Language)); sb.Append(this.EvaluateFromAddon(6, [rarity], context.Language));
@ -956,6 +956,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
sb.PopLink(); sb.PopLink();
text = sb.ToReadOnlySeString(); text = sb.ToReadOnlySeString();
SeStringBuilder.SharedPool.Return(sb);
} }
private void CreateSheetLink(in SeStringContext context, string resolvedSheetName, ReadOnlySeString text, uint eRowIdValue, uint eColParamValue) private void CreateSheetLink(in SeStringContext context, string resolvedSheetName, ReadOnlySeString text, uint eRowIdValue, uint eColParamValue)
@ -1027,33 +1028,40 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (!payload.TryGetExpression(out var eStr)) if (!payload.TryGetExpression(out var eStr))
return false; return false;
using var rssb = new RentedSeStringBuilder(); var builder = SeStringBuilder.SharedPool.Get();
var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); try
if (!this.ResolveStringExpression(headContext, eStr))
return false;
var str = rssb.Builder.ToReadOnlySeString();
var pIdx = 0;
foreach (var p in str)
{ {
pIdx++; var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
if (p.Type == ReadOnlySePayloadType.Invalid) if (!this.ResolveStringExpression(headContext, eStr))
continue; return false;
if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) var str = builder.ToReadOnlySeString();
var pIdx = 0;
foreach (var p in str)
{ {
context.Builder.Append(Encoding.UTF8.GetString(p.Body.ToArray()).ToUpper(context.CultureInfo)); pIdx++;
continue;
if (p.Type == ReadOnlySePayloadType.Invalid)
continue;
if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text)
{
context.Builder.Append(Encoding.UTF8.GetString(p.Body.ToArray()).ToUpper(context.CultureInfo));
continue;
}
context.Builder.Append(p);
} }
context.Builder.Append(p); return true;
}
finally
{
SeStringBuilder.SharedPool.Return(builder);
} }
return true;
} }
private bool TryResolveHead(in SeStringContext context, in ReadOnlySePayloadSpan payload) private bool TryResolveHead(in SeStringContext context, in ReadOnlySePayloadSpan payload)
@ -1061,33 +1069,40 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (!payload.TryGetExpression(out var eStr)) if (!payload.TryGetExpression(out var eStr))
return false; return false;
using var rssb = new RentedSeStringBuilder(); var builder = SeStringBuilder.SharedPool.Get();
var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); try
if (!this.ResolveStringExpression(headContext, eStr))
return false;
var str = rssb.Builder.ToReadOnlySeString();
var pIdx = 0;
foreach (var p in str)
{ {
pIdx++; var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
if (p.Type == ReadOnlySePayloadType.Invalid) if (!this.ResolveStringExpression(headContext, eStr))
continue; return false;
if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) var str = builder.ToReadOnlySeString();
var pIdx = 0;
foreach (var p in str)
{ {
context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).FirstCharToUpper(context.CultureInfo)); pIdx++;
continue;
if (p.Type == ReadOnlySePayloadType.Invalid)
continue;
if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text)
{
context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).FirstCharToUpper(context.CultureInfo));
continue;
}
context.Builder.Append(p);
} }
context.Builder.Append(p); return true;
}
finally
{
SeStringBuilder.SharedPool.Return(builder);
} }
return true;
} }
private bool TryResolveSplit(in SeStringContext context, in ReadOnlySePayloadSpan payload) private bool TryResolveSplit(in SeStringContext context, in ReadOnlySePayloadSpan payload)
@ -1098,25 +1113,32 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (!eSeparator.TryGetString(out var eSeparatorVal) || !eIndex.TryGetUInt(out var eIndexVal) || eIndexVal <= 0) if (!eSeparator.TryGetString(out var eSeparatorVal) || !eIndex.TryGetUInt(out var eIndexVal) || eIndexVal <= 0)
return false; return false;
using var rssb = new RentedSeStringBuilder(); var builder = SeStringBuilder.SharedPool.Get();
var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); try
if (!this.ResolveStringExpression(headContext, eText))
return false;
var separator = eSeparatorVal.ExtractText();
if (separator.Length < 1)
return false;
var splitted = rssb.Builder.ToReadOnlySeString().ExtractText().Split(separator[0]);
if (eIndexVal <= splitted.Length)
{ {
context.Builder.Append(splitted[eIndexVal - 1]); var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
return true;
}
return false; if (!this.ResolveStringExpression(headContext, eText))
return false;
var separator = eSeparatorVal.ExtractText();
if (separator.Length < 1)
return false;
var splitted = builder.ToReadOnlySeString().ExtractText().Split(separator[0]);
if (eIndexVal <= splitted.Length)
{
context.Builder.Append(splitted[eIndexVal - 1]);
return true;
}
return false;
}
finally
{
SeStringBuilder.SharedPool.Return(builder);
}
} }
private bool TryResolveHeadAll(in SeStringContext context, in ReadOnlySePayloadSpan payload) private bool TryResolveHeadAll(in SeStringContext context, in ReadOnlySePayloadSpan payload)
@ -1124,30 +1146,37 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (!payload.TryGetExpression(out var eStr)) if (!payload.TryGetExpression(out var eStr))
return false; return false;
using var rssb = new RentedSeStringBuilder(); var builder = SeStringBuilder.SharedPool.Get();
var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); try
if (!this.ResolveStringExpression(headContext, eStr))
return false;
var str = rssb.Builder.ToReadOnlySeString();
foreach (var p in str)
{ {
if (p.Type == ReadOnlySePayloadType.Invalid) var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
continue;
if (p.Type == ReadOnlySePayloadType.Text) if (!this.ResolveStringExpression(headContext, eStr))
return false;
var str = builder.ToReadOnlySeString();
foreach (var p in str)
{ {
context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).ToUpper(true, true, false, context.Language)); if (p.Type == ReadOnlySePayloadType.Invalid)
continue; continue;
if (p.Type == ReadOnlySePayloadType.Text)
{
context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).ToUpper(true, true, false, context.Language));
continue;
}
context.Builder.Append(p);
} }
context.Builder.Append(p); return true;
}
finally
{
SeStringBuilder.SharedPool.Return(builder);
} }
return true;
} }
private bool TryResolveFixed(in SeStringContext context, in ReadOnlySePayloadSpan payload) private bool TryResolveFixed(in SeStringContext context, in ReadOnlySePayloadSpan payload)
@ -1277,13 +1306,14 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (!this.dataManager.GetExcelSheet<Lumina.Excel.Sheets.Map>().TryGetRow(mapId, out var mapRow)) if (!this.dataManager.GetExcelSheet<Lumina.Excel.Sheets.Map>().TryGetRow(mapId, out var mapRow))
return false; return false;
using var rssb = new RentedSeStringBuilder(); var sb = SeStringBuilder.SharedPool.Get();
rssb.Builder.Append(placeNameRow.Name); sb.Append(placeNameRow.Name);
if (instance is > 0 and <= 9) if (instance is > 0 and <= 9)
rssb.Builder.Append((char)((char)0xE0B0 + (char)instance)); sb.Append((char)((char)0xE0B0 + (char)instance));
var placeNameWithInstance = rssb.Builder.ToReadOnlySeString(); var placeNameWithInstance = sb.ToReadOnlySeString();
SeStringBuilder.SharedPool.Return(sb);
var mapPosX = ConvertRawToMapPosX(mapRow, rawX / 1000f); var mapPosX = ConvertRawToMapPosX(mapRow, rawX / 1000f);
var mapPosY = ConvertRawToMapPosY(mapRow, rawY / 1000f); var mapPosY = ConvertRawToMapPosY(mapRow, rawY / 1000f);
@ -1432,22 +1462,23 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
statusDescription = statusRow.Description.AsSpan(); statusDescription = statusRow.Description.AsSpan();
} }
using var rssb = new RentedSeStringBuilder(); var sb = SeStringBuilder.SharedPool.Get();
switch (statusRow.StatusCategory) switch (statusRow.StatusCategory)
{ {
case 1: case 1:
rssb.Builder.Append(this.EvaluateFromAddon(376, default, context.Language)); sb.Append(this.EvaluateFromAddon(376, default, context.Language));
break; break;
case 2: case 2:
rssb.Builder.Append(this.EvaluateFromAddon(377, default, context.Language)); sb.Append(this.EvaluateFromAddon(377, default, context.Language));
break; break;
} }
rssb.Builder.Append(statusName); sb.Append(statusName);
var linkText = rssb.Builder.ToReadOnlySeString(); var linkText = sb.ToReadOnlySeString();
SeStringBuilder.SharedPool.Return(sb);
context.Builder context.Builder
.BeginMacro(MacroCode.Link) .BeginMacro(MacroCode.Link)
@ -1702,31 +1733,38 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (!payload.TryGetExpression(out var eStr)) if (!payload.TryGetExpression(out var eStr))
return false; return false;
using var rssb = new RentedSeStringBuilder(); var builder = SeStringBuilder.SharedPool.Get();
var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); try
if (!this.ResolveStringExpression(headContext, eStr))
return false;
var str = rssb.Builder.ToReadOnlySeString();
foreach (var p in str)
{ {
if (p.Type == ReadOnlySePayloadType.Invalid) var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
continue;
if (p.Type == ReadOnlySePayloadType.Text) if (!this.ResolveStringExpression(headContext, eStr))
return false;
var str = builder.ToReadOnlySeString();
foreach (var p in str)
{ {
context.Builder.Append(Encoding.UTF8.GetString(p.Body.ToArray()).ToLower(context.CultureInfo)); if (p.Type == ReadOnlySePayloadType.Invalid)
continue;
continue; if (p.Type == ReadOnlySePayloadType.Text)
{
context.Builder.Append(Encoding.UTF8.GetString(p.Body.ToArray()).ToLower(context.CultureInfo));
continue;
}
context.Builder.Append(p);
} }
context.Builder.Append(p); return true;
}
finally
{
SeStringBuilder.SharedPool.Return(builder);
} }
return true;
} }
private bool TryResolveNoun(ClientLanguage language, in SeStringContext context, in ReadOnlySePayloadSpan payload) private bool TryResolveNoun(ClientLanguage language, in SeStringContext context, in ReadOnlySePayloadSpan payload)
@ -1796,33 +1834,40 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (!payload.TryGetExpression(out var eStr)) if (!payload.TryGetExpression(out var eStr))
return false; return false;
using var rssb = new RentedSeStringBuilder(); var builder = SeStringBuilder.SharedPool.Get();
var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); try
if (!this.ResolveStringExpression(headContext, eStr))
return false;
var str = rssb.Builder.ToReadOnlySeString();
var pIdx = 0;
foreach (var p in str)
{ {
pIdx++; var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
if (p.Type == ReadOnlySePayloadType.Invalid) if (!this.ResolveStringExpression(headContext, eStr))
continue; return false;
if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) var str = builder.ToReadOnlySeString();
var pIdx = 0;
foreach (var p in str)
{ {
context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).FirstCharToLower(context.CultureInfo)); pIdx++;
continue;
if (p.Type == ReadOnlySePayloadType.Invalid)
continue;
if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text)
{
context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).FirstCharToLower(context.CultureInfo));
continue;
}
context.Builder.Append(p);
} }
context.Builder.Append(p); return true;
}
finally
{
SeStringBuilder.SharedPool.Return(builder);
} }
return true;
} }
private bool TryResolveColorType(in SeStringContext context, in ReadOnlySePayloadSpan payload) private bool TryResolveColorType(in SeStringContext context, in ReadOnlySePayloadSpan payload)
@ -2087,19 +2132,19 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (operand1.TryGetString(out var strval1) && operand2.TryGetString(out var strval2)) if (operand1.TryGetString(out var strval1) && operand2.TryGetString(out var strval2))
{ {
using var rssb1 = new RentedSeStringBuilder();
using var rssb2 = new RentedSeStringBuilder();
var resolvedStr1 = this.EvaluateAndAppendTo( var resolvedStr1 = this.EvaluateAndAppendTo(
rssb1.Builder, SeStringBuilder.SharedPool.Get(),
strval1, strval1,
context.LocalParameters, context.LocalParameters,
context.Language); context.Language);
var resolvedStr2 = this.EvaluateAndAppendTo( var resolvedStr2 = this.EvaluateAndAppendTo(
rssb2.Builder, SeStringBuilder.SharedPool.Get(),
strval2, strval2,
context.LocalParameters, context.LocalParameters,
context.Language); context.Language);
var equals = resolvedStr1.GetViewAsSpan().SequenceEqual(resolvedStr2.GetViewAsSpan()); var equals = resolvedStr1.GetViewAsSpan().SequenceEqual(resolvedStr2.GetViewAsSpan());
SeStringBuilder.SharedPool.Return(resolvedStr1);
SeStringBuilder.SharedPool.Return(resolvedStr2);
if ((ExpressionType)exprType == ExpressionType.Equal) if ((ExpressionType)exprType == ExpressionType.Equal)
value = equals ? 1u : 0u; value = equals ? 1u : 0u;

View file

@ -3,6 +3,7 @@ using System.Globalization;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
using DSeString = Dalamud.Game.Text.SeStringHandling.SeString; using DSeString = Dalamud.Game.Text.SeStringHandling.SeString;
using LSeString = Lumina.Text.SeString;
namespace Dalamud.Game.Text.Evaluator; namespace Dalamud.Game.Text.Evaluator;
@ -70,6 +71,9 @@ public readonly struct SeStringParameter
public static implicit operator SeStringParameter(ReadOnlySeStringSpan value) => new(new ReadOnlySeString(value)); public static implicit operator SeStringParameter(ReadOnlySeStringSpan value) => new(new ReadOnlySeString(value));
[Obsolete("Switch to using ReadOnlySeString instead of Lumina's SeString.", true)]
public static implicit operator SeStringParameter(LSeString value) => new(new ReadOnlySeString(value.RawData));
public static implicit operator SeStringParameter(DSeString value) => new(new ReadOnlySeString(value.Encode())); public static implicit operator SeStringParameter(DSeString value) => new(new ReadOnlySeString(value.Encode()));
public static implicit operator SeStringParameter(string value) => new(value); public static implicit operator SeStringParameter(string value) => new(value);

View file

@ -9,6 +9,7 @@ using Dalamud.Utility;
using Lumina.Excel; using Lumina.Excel;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
using LSeStringBuilder = Lumina.Text.SeStringBuilder;
using LSheets = Lumina.Excel.Sheets; using LSheets = Lumina.Excel.Sheets;
namespace Dalamud.Game.Text.Noun; namespace Dalamud.Game.Text.Noun;
@ -146,28 +147,30 @@ internal class NounProcessor : IServiceType
var attributiveSheet = this.dataManager.Excel.GetSheet<RawRow>(nounParams.Language.ToLumina(), nameof(LSheets.Attributive)); var attributiveSheet = this.dataManager.Excel.GetSheet<RawRow>(nounParams.Language.ToLumina(), nameof(LSheets.Attributive));
using var rssb = new RentedSeStringBuilder(); var builder = LSeStringBuilder.SharedPool.Get();
// Ko-So-A-Do // Ko-So-A-Do
var ksad = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(nounParams.Quantity > 1 ? 1 : 0); var ksad = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(nounParams.Quantity > 1 ? 1 : 0);
if (!ksad.IsEmpty) if (!ksad.IsEmpty)
{ {
rssb.Builder.Append(ksad); builder.Append(ksad);
if (nounParams.Quantity > 1) if (nounParams.Quantity > 1)
{ {
rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
} }
} }
if (!nounParams.LinkMarker.IsEmpty) if (!nounParams.LinkMarker.IsEmpty)
rssb.Builder.Append(nounParams.LinkMarker); builder.Append(nounParams.LinkMarker);
var text = row.ReadStringColumn(nounParams.ColumnOffset); var text = row.ReadStringColumn(nounParams.ColumnOffset);
if (!text.IsEmpty) if (!text.IsEmpty)
rssb.Builder.Append(text); builder.Append(text);
return rssb.Builder.ToReadOnlySeString(); var ross = builder.ToReadOnlySeString();
LSeStringBuilder.SharedPool.Return(builder);
return ross;
} }
/// <summary> /// <summary>
@ -197,7 +200,7 @@ internal class NounProcessor : IServiceType
var attributiveSheet = this.dataManager.Excel.GetSheet<RawRow>(nounParams.Language.ToLumina(), nameof(LSheets.Attributive)); var attributiveSheet = this.dataManager.Excel.GetSheet<RawRow>(nounParams.Language.ToLumina(), nameof(LSheets.Attributive));
using var rssb = new RentedSeStringBuilder(); var builder = LSeStringBuilder.SharedPool.Get();
var isProperNounColumn = nounParams.ColumnOffset + ArticleColumnIdx; var isProperNounColumn = nounParams.ColumnOffset + ArticleColumnIdx;
var isProperNoun = isProperNounColumn >= 0 ? row.ReadInt8Column(isProperNounColumn) : ~isProperNounColumn; var isProperNoun = isProperNounColumn >= 0 ? row.ReadInt8Column(isProperNounColumn) : ~isProperNounColumn;
@ -213,19 +216,21 @@ internal class NounProcessor : IServiceType
var article = attributiveSheet.GetRow((uint)nounParams.ArticleType) var article = attributiveSheet.GetRow((uint)nounParams.ArticleType)
.ReadStringColumn(articleColumn + grammaticalNumberColumnOffset); .ReadStringColumn(articleColumn + grammaticalNumberColumnOffset);
if (!article.IsEmpty) if (!article.IsEmpty)
rssb.Builder.Append(article); builder.Append(article);
if (!nounParams.LinkMarker.IsEmpty) if (!nounParams.LinkMarker.IsEmpty)
rssb.Builder.Append(nounParams.LinkMarker); builder.Append(nounParams.LinkMarker);
} }
var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity == 1 ? SingularColumnIdx : PluralColumnIdx)); var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity == 1 ? SingularColumnIdx : PluralColumnIdx));
if (!text.IsEmpty) if (!text.IsEmpty)
rssb.Builder.Append(text); builder.Append(text);
rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
return rssb.Builder.ToReadOnlySeString(); var ross = builder.ToReadOnlySeString();
LSeStringBuilder.SharedPool.Return(builder);
return ross;
} }
/// <summary> /// <summary>
@ -257,13 +262,17 @@ internal class NounProcessor : IServiceType
var attributiveSheet = this.dataManager.Excel.GetSheet<RawRow>(nounParams.Language.ToLumina(), nameof(LSheets.Attributive)); var attributiveSheet = this.dataManager.Excel.GetSheet<RawRow>(nounParams.Language.ToLumina(), nameof(LSheets.Attributive));
using var rssb = new RentedSeStringBuilder(); var builder = LSeStringBuilder.SharedPool.Get();
ReadOnlySeString ross;
if (nounParams.IsActionSheet) if (nounParams.IsActionSheet)
{ {
rssb.Builder.Append(row.ReadStringColumn(nounParams.GrammaticalCase)); builder.Append(row.ReadStringColumn(nounParams.GrammaticalCase));
rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
return rssb.Builder.ToReadOnlySeString();
ross = builder.ToReadOnlySeString();
LSeStringBuilder.SharedPool.Return(builder);
return ross;
} }
var genderIndexColumn = nounParams.ColumnOffset + PronounColumnIdx; var genderIndexColumn = nounParams.ColumnOffset + PronounColumnIdx;
@ -293,32 +302,35 @@ internal class NounProcessor : IServiceType
var grammaticalGender = attributiveSheet.GetRow((uint)nounParams.ArticleType) var grammaticalGender = attributiveSheet.GetRow((uint)nounParams.ArticleType)
.ReadStringColumn(caseColumnOffset + genderIndex); // Genus .ReadStringColumn(caseColumnOffset + genderIndex); // Genus
if (!grammaticalGender.IsEmpty) if (!grammaticalGender.IsEmpty)
rssb.Builder.Append(grammaticalGender); builder.Append(grammaticalGender);
} }
if (!nounParams.LinkMarker.IsEmpty) if (!nounParams.LinkMarker.IsEmpty)
rssb.Builder.Append(nounParams.LinkMarker); builder.Append(nounParams.LinkMarker);
rssb.Builder.Append(text); builder.Append(text);
var plural = attributiveSheet.GetRow((uint)(caseRowOffset + 26)) var plural = attributiveSheet.GetRow((uint)(caseRowOffset + 26))
.ReadStringColumn(caseColumnOffset + genderIndex); .ReadStringColumn(caseColumnOffset + genderIndex);
if (rssb.Builder.ContainsText("[p]"u8)) if (builder.ContainsText("[p]"u8))
rssb.Builder.ReplaceText("[p]"u8, plural); builder.ReplaceText("[p]"u8, plural);
else else
rssb.Builder.Append(plural); builder.Append(plural);
if (hasT) if (hasT)
{ {
var article = var article =
attributiveSheet.GetRow(39).ReadStringColumn(caseColumnOffset + genderIndex); // Definiter Artikel attributiveSheet.GetRow(39).ReadStringColumn(caseColumnOffset + genderIndex); // Definiter Artikel
rssb.Builder.ReplaceText("[t]"u8, article); builder.ReplaceText("[t]"u8, article);
} }
} }
rssb.Builder.ReplaceText("[pa]"u8, attributiveSheet.GetRow(24).ReadStringColumn(caseColumnOffset + genderIndex)); var pa = attributiveSheet.GetRow(24).ReadStringColumn(caseColumnOffset + genderIndex);
builder.ReplaceText("[pa]"u8, pa);
var declensionRow = (GermanArticleType)nounParams.ArticleType switch RawRow declensionRow;
declensionRow = (GermanArticleType)nounParams.ArticleType switch
{ {
// Schwache Flexion eines Adjektivs?! // Schwache Flexion eines Adjektivs?!
GermanArticleType.Possessive or GermanArticleType.Demonstrative => attributiveSheet.GetRow(25), GermanArticleType.Possessive or GermanArticleType.Demonstrative => attributiveSheet.GetRow(25),
@ -335,10 +347,14 @@ internal class NounProcessor : IServiceType
_ => attributiveSheet.GetRow(26), _ => attributiveSheet.GetRow(26),
}; };
rssb.Builder.ReplaceText("[a]"u8, declensionRow.ReadStringColumn(caseColumnOffset + genderIndex)); var declension = declensionRow.ReadStringColumn(caseColumnOffset + genderIndex);
rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); builder.ReplaceText("[a]"u8, declension);
return rssb.Builder.ToReadOnlySeString(); builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
ross = builder.ToReadOnlySeString();
LSeStringBuilder.SharedPool.Return(builder);
return ross;
} }
/// <summary> /// <summary>
@ -369,7 +385,8 @@ internal class NounProcessor : IServiceType
var attributiveSheet = this.dataManager.Excel.GetSheet<RawRow>(nounParams.Language.ToLumina(), nameof(LSheets.Attributive)); var attributiveSheet = this.dataManager.Excel.GetSheet<RawRow>(nounParams.Language.ToLumina(), nameof(LSheets.Attributive));
using var rssb = new RentedSeStringBuilder(); var builder = LSeStringBuilder.SharedPool.Get();
ReadOnlySeString ross;
var startsWithVowelColumn = nounParams.ColumnOffset + StartsWithVowelColumnIdx; var startsWithVowelColumn = nounParams.ColumnOffset + StartsWithVowelColumnIdx;
var startsWithVowel = startsWithVowelColumn >= 0 var startsWithVowel = startsWithVowelColumn >= 0
@ -388,19 +405,21 @@ internal class NounProcessor : IServiceType
{ {
var v21 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20); var v21 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20);
if (!v21.IsEmpty) if (!v21.IsEmpty)
rssb.Builder.Append(v21); builder.Append(v21);
if (!nounParams.LinkMarker.IsEmpty) if (!nounParams.LinkMarker.IsEmpty)
rssb.Builder.Append(nounParams.LinkMarker); builder.Append(nounParams.LinkMarker);
var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity <= 1 ? SingularColumnIdx : PluralColumnIdx)); var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity <= 1 ? SingularColumnIdx : PluralColumnIdx));
if (!text.IsEmpty) if (!text.IsEmpty)
rssb.Builder.Append(text); builder.Append(text);
if (nounParams.Quantity <= 1) if (nounParams.Quantity <= 1)
rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
return rssb.Builder.ToReadOnlySeString(); ross = builder.ToReadOnlySeString();
LSeStringBuilder.SharedPool.Return(builder);
return ross;
} }
var v17 = row.ReadInt8Column(nounParams.ColumnOffset + Unknown5ColumnIdx); var v17 = row.ReadInt8Column(nounParams.ColumnOffset + Unknown5ColumnIdx);
@ -409,32 +428,34 @@ internal class NounProcessor : IServiceType
var v29 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20 + 2); var v29 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20 + 2);
if (!v29.IsEmpty) if (!v29.IsEmpty)
{ {
rssb.Builder.Append(v29); builder.Append(v29);
if (!nounParams.LinkMarker.IsEmpty) if (!nounParams.LinkMarker.IsEmpty)
rssb.Builder.Append(nounParams.LinkMarker); builder.Append(nounParams.LinkMarker);
var text = row.ReadStringColumn(nounParams.ColumnOffset + PluralColumnIdx); var text = row.ReadStringColumn(nounParams.ColumnOffset + PluralColumnIdx);
if (!text.IsEmpty) if (!text.IsEmpty)
rssb.Builder.Append(text); builder.Append(text);
} }
} }
else else
{ {
var v27 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20 + (v17 != 0 ? 1 : 3)); var v27 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20 + (v17 != 0 ? 1 : 3));
if (!v27.IsEmpty) if (!v27.IsEmpty)
rssb.Builder.Append(v27); builder.Append(v27);
if (!nounParams.LinkMarker.IsEmpty) if (!nounParams.LinkMarker.IsEmpty)
rssb.Builder.Append(nounParams.LinkMarker); builder.Append(nounParams.LinkMarker);
var text = row.ReadStringColumn(nounParams.ColumnOffset + SingularColumnIdx); var text = row.ReadStringColumn(nounParams.ColumnOffset + SingularColumnIdx);
if (!text.IsEmpty) if (!text.IsEmpty)
rssb.Builder.Append(text); builder.Append(text);
} }
rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
return rssb.Builder.ToReadOnlySeString(); ross = builder.ToReadOnlySeString();
LSeStringBuilder.SharedPool.Return(builder);
return ross;
} }
} }

View file

@ -1,7 +1,6 @@
using System.IO; using System.IO;
using Dalamud.Game.Text.Evaluator; using Dalamud.Game.Text.Evaluator;
using Dalamud.Utility;
using Lumina.Text.Payloads; using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
@ -33,14 +32,13 @@ public class AutoTranslatePayload : Payload, ITextProvider
this.Group = group; this.Group = group;
this.Key = key; this.Key = key;
using var rssb = new RentedSeStringBuilder(); var ssb = Lumina.Text.SeStringBuilder.SharedPool.Get();
this.payload = ssb.BeginMacro(MacroCode.Fixed)
this.payload = rssb.Builder .AppendUIntExpression(group - 1)
.BeginMacro(MacroCode.Fixed) .AppendUIntExpression(key)
.AppendUIntExpression(group - 1) .EndMacro()
.AppendUIntExpression(key) .ToReadOnlySeString();
.EndMacro() Lumina.Text.SeStringBuilder.SharedPool.Return(ssb);
.ToReadOnlySeString();
} }
/// <summary> /// <summary>

View file

@ -1,7 +1,5 @@
using System.IO; using System.IO;
using Dalamud.Utility;
using Lumina.Text.Payloads; using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
@ -39,18 +37,19 @@ public class DalamudLinkPayload : Payload
/// <inheritdoc/> /// <inheritdoc/>
protected override byte[] EncodeImpl() protected override byte[] EncodeImpl()
{ {
using var rssb = new RentedSeStringBuilder(); var ssb = Lumina.Text.SeStringBuilder.SharedPool.Get();
return rssb.Builder var res = ssb.BeginMacro(MacroCode.Link)
.BeginMacro(MacroCode.Link) .AppendIntExpression((int)EmbeddedInfoType.DalamudLink - 1)
.AppendIntExpression((int)EmbeddedInfoType.DalamudLink - 1) .AppendUIntExpression(this.CommandId)
.AppendUIntExpression(this.CommandId) .AppendIntExpression(this.Extra1)
.AppendIntExpression(this.Extra1) .AppendIntExpression(this.Extra2)
.AppendIntExpression(this.Extra2) .BeginStringExpression()
.BeginStringExpression() .Append(JsonConvert.SerializeObject(new[] { this.Plugin, this.ExtraString }))
.Append(JsonConvert.SerializeObject(new[] { this.Plugin, this.ExtraString })) .EndExpression()
.EndExpression() .EndMacro()
.EndMacro() .ToArray();
.ToArray(); Lumina.Text.SeStringBuilder.SharedPool.Return(ssb);
return res;
} }
/// <inheritdoc/> /// <inheritdoc/>

View file

@ -1,7 +1,8 @@
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Utility;
using Lumina.Excel; using Lumina.Excel;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
@ -86,12 +87,14 @@ public class PlayerPayload : Payload
/// <inheritdoc/> /// <inheritdoc/>
protected override byte[] EncodeImpl() protected override byte[] EncodeImpl()
{ {
using var rssb = new RentedSeStringBuilder(); var ssb = Lumina.Text.SeStringBuilder.SharedPool.Get();
return rssb.Builder var res = ssb
.PushLinkCharacter(this.playerName, this.serverId) .PushLinkCharacter(this.playerName, this.serverId)
.Append(this.playerName) .Append(this.playerName)
.PopLink() .PopLink()
.ToArray(); .ToArray();
Lumina.Text.SeStringBuilder.SharedPool.Return(ssb);
return res;
} }
/// <inheritdoc/> /// <inheritdoc/>

View file

@ -113,6 +113,14 @@ public class SeString
/// <returns>Equivalent SeString.</returns> /// <returns>Equivalent SeString.</returns>
public static implicit operator SeString(string str) => new(new TextPayload(str)); public static implicit operator SeString(string str) => new(new TextPayload(str));
/// <summary>
/// Implicitly convert a string into a SeString containing a <see cref="TextPayload"/>.
/// </summary>
/// <param name="str">string to convert.</param>
/// <returns>Equivalent SeString.</returns>
[Obsolete("Switch to using ReadOnlySeString instead of Lumina's SeString.", true)]
public static explicit operator SeString(Lumina.Text.SeString str) => str.ToDalamudString();
/// <summary> /// <summary>
/// Parse a binary game message into an SeString. /// Parse a binary game message into an SeString.
/// </summary> /// </summary>
@ -198,9 +206,8 @@ public class SeString
var textColor = ItemUtil.GetItemRarityColorType(rawId); var textColor = ItemUtil.GetItemRarityColorType(rawId);
var textEdgeColor = textColor + 1u; var textEdgeColor = textColor + 1u;
using var rssb = new RentedSeStringBuilder(); var sb = LSeStringBuilder.SharedPool.Get();
var itemLink = sb
var itemLink = rssb.Builder
.PushColorType(textColor) .PushColorType(textColor)
.PushEdgeColorType(textEdgeColor) .PushEdgeColorType(textEdgeColor)
.PushLinkItem(rawId, copyName) .PushLinkItem(rawId, copyName)
@ -209,6 +216,7 @@ public class SeString
.PopEdgeColorType() .PopEdgeColorType()
.PopColorType() .PopColorType()
.ToReadOnlySeString(); .ToReadOnlySeString();
LSeStringBuilder.SharedPool.Return(sb);
return SeString.Parse(seStringEvaluator.EvaluateFromAddon(371, [itemLink], clientState.ClientLanguage)); return SeString.Parse(seStringEvaluator.EvaluateFromAddon(371, [itemLink], clientState.ClientLanguage));
} }
@ -250,12 +258,16 @@ public class SeString
var mapPayload = new MapLinkPayload(territoryId, mapId, rawX, rawY); var mapPayload = new MapLinkPayload(territoryId, mapId, rawX, rawY);
var nameString = GetMapLinkNameString(mapPayload.PlaceName, instance, mapPayload.CoordinateString); var nameString = GetMapLinkNameString(mapPayload.PlaceName, instance, mapPayload.CoordinateString);
return new SeString(new List<Payload>([ var payloads = new List<Payload>(new Payload[]
{
mapPayload, mapPayload,
..TextArrowPayloads, // arrow goes here
new TextPayload(nameString), new TextPayload(nameString),
RawPayload.LinkTerminator, RawPayload.LinkTerminator,
])); });
payloads.InsertRange(1, TextArrowPayloads);
return new SeString(payloads);
} }
/// <summary> /// <summary>
@ -286,12 +298,16 @@ public class SeString
var mapPayload = new MapLinkPayload(territoryId, mapId, xCoord, yCoord, fudgeFactor); var mapPayload = new MapLinkPayload(territoryId, mapId, xCoord, yCoord, fudgeFactor);
var nameString = GetMapLinkNameString(mapPayload.PlaceName, instance, mapPayload.CoordinateString); var nameString = GetMapLinkNameString(mapPayload.PlaceName, instance, mapPayload.CoordinateString);
return new SeString(new List<Payload>([ var payloads = new List<Payload>(new Payload[]
{
mapPayload, mapPayload,
..TextArrowPayloads, // arrow goes here
new TextPayload(nameString), new TextPayload(nameString),
RawPayload.LinkTerminator, RawPayload.LinkTerminator,
])); });
payloads.InsertRange(1, TextArrowPayloads);
return new SeString(payloads);
} }
/// <summary> /// <summary>
@ -347,15 +363,21 @@ public class SeString
/// <returns>An SeString containing all the payloads necessary to display a party finder link in the chat log.</returns> /// <returns>An SeString containing all the payloads necessary to display a party finder link in the chat log.</returns>
public static SeString CreatePartyFinderLink(uint listingId, string recruiterName, bool isCrossWorld = false) public static SeString CreatePartyFinderLink(uint listingId, string recruiterName, bool isCrossWorld = false)
{ {
var clientState = Service<ClientState.ClientState>.Get(); var payloads = new List<Payload>()
var seStringEvaluator = Service<SeStringEvaluator>.Get(); {
return new SeString(new List<Payload>([
new PartyFinderPayload(listingId, isCrossWorld ? PartyFinderPayload.PartyFinderLinkType.NotSpecified : PartyFinderPayload.PartyFinderLinkType.LimitedToHomeWorld), new PartyFinderPayload(listingId, isCrossWorld ? PartyFinderPayload.PartyFinderLinkType.NotSpecified : PartyFinderPayload.PartyFinderLinkType.LimitedToHomeWorld),
..TextArrowPayloads, // ->
..SeString.Parse(seStringEvaluator.EvaluateFromAddon(2265, [recruiterName, isCrossWorld ? 0 : 1], clientState.ClientLanguage)).Payloads, new TextPayload($"Looking for Party ({recruiterName})" + (isCrossWorld ? " " : string.Empty)),
RawPayload.LinkTerminator };
]));
payloads.InsertRange(1, TextArrowPayloads);
if (isCrossWorld)
payloads.Add(new IconPayload(BitmapFontIcon.CrossWorld));
payloads.Add(RawPayload.LinkTerminator);
return new SeString(payloads);
} }
/// <summary> /// <summary>
@ -365,12 +387,16 @@ public class SeString
/// <returns>An SeString containing all the payloads necessary to display a link to the party finder search conditions.</returns> /// <returns>An SeString containing all the payloads necessary to display a link to the party finder search conditions.</returns>
public static SeString CreatePartyFinderSearchConditionsLink(string message) public static SeString CreatePartyFinderSearchConditionsLink(string message)
{ {
return new SeString(new List<Payload>([ var payloads = new List<Payload>()
{
new PartyFinderPayload(), new PartyFinderPayload(),
..TextArrowPayloads, // ->
new TextPayload(message), new TextPayload(message),
RawPayload.LinkTerminator };
])); payloads.InsertRange(1, TextArrowPayloads);
payloads.Add(RawPayload.LinkTerminator);
return new SeString(payloads);
} }
/// <summary> /// <summary>

View file

@ -3,9 +3,9 @@ using Lumina.Excel.Sheets;
namespace Dalamud.Game.UnlockState; namespace Dalamud.Game.UnlockState;
/// <summary> /// <summary>
/// Enum for <see cref="ItemAction.Action"/>. /// Enum for <see cref="ItemAction.Type"/>.
/// </summary> /// </summary>
internal enum ItemActionAction : ushort internal enum ItemActionType : ushort
{ {
/// <summary> /// <summary>
/// No item action. /// No item action.

View file

@ -158,23 +158,67 @@ internal unsafe class RecipeData : IInternalDisposableService
{ {
noteBookDivisionIndex++; noteBookDivisionIndex++;
if (!noteBookDivisionRow.AllowedCraftTypes[craftType]) // For future Lumina.Excel update, replace with:
continue; // if (!notebookDivisionRow.AllowedCraftTypes[craftType])
// continue;
switch (craftTypeRow.RowId)
{
case 0 when !noteBookDivisionRow.CRPCraft: continue;
case 1 when !noteBookDivisionRow.BSMCraft: continue;
case 2 when !noteBookDivisionRow.ARMCraft: continue;
case 3 when !noteBookDivisionRow.GSMCraft: continue;
case 4 when !noteBookDivisionRow.LTWCraft: continue;
case 5 when !noteBookDivisionRow.WVRCraft: continue;
case 6 when !noteBookDivisionRow.ALCCraft: continue;
case 7 when !noteBookDivisionRow.CULCraft: continue;
}
if (noteBookDivisionRow.GatheringOpeningLevel != byte.MaxValue) if (noteBookDivisionRow.GatheringOpeningLevel != byte.MaxValue)
continue; continue;
if (noteBookDivisionRow.RequiresSecretRecipeBookGroupUnlock) // For future Lumina.Excel update, replace with:
// if (notebookDivisionRow.RequiresSecretRecipeBookGroupUnlock)
if (noteBookDivisionRow.Unknown1)
{ {
var secretRecipeBookUnlocked = false; var secretRecipeBookUnlocked = false;
foreach (var secretRecipeBookGroup in noteBookDivisionRow.SecretRecipeBookGroups) // For future Lumina.Excel update, iterate over notebookDivisionRow.SecretRecipeBookGroups
for (var i = 0; i < 2; i++)
{ {
if (secretRecipeBookGroup.RowId == 0 || !secretRecipeBookGroup.IsValid) // For future Lumina.Excel update, replace with:
// if (secretRecipeBookGroup.RowId == 0 || !secretRecipeBookGroup.IsValid)
// continue;
var secretRecipeBookGroupRowId = i switch
{
0 => noteBookDivisionRow.Unknown2,
1 => noteBookDivisionRow.Unknown2,
_ => default,
};
if (secretRecipeBookGroupRowId == 0)
continue; continue;
var bitIndex = secretRecipeBookGroup.Value.SecretRecipeBook[craftType].RowId; if (!this.dataManager.GetExcelSheet<SecretRecipeBookGroup>().TryGetRow(secretRecipeBookGroupRowId, out var secretRecipeBookGroupRow))
if (PlayerState.Instance()->UnlockedSecretRecipeBooksBitArray.Get((int)bitIndex)) continue;
// For future Lumina.Excel update, replace with:
// var bitIndex = secretRecipeBookGroup.Value.UnlockBitIndex[craftType];
var bitIndex = craftType switch
{
0 => secretRecipeBookGroupRow.Unknown0,
1 => secretRecipeBookGroupRow.Unknown1,
2 => secretRecipeBookGroupRow.Unknown2,
3 => secretRecipeBookGroupRow.Unknown3,
4 => secretRecipeBookGroupRow.Unknown4,
5 => secretRecipeBookGroupRow.Unknown5,
6 => secretRecipeBookGroupRow.Unknown6,
7 => secretRecipeBookGroupRow.Unknown7,
_ => default,
};
if (PlayerState.Instance()->UnlockedSecretRecipeBooksBitArray.Get(bitIndex))
{ {
secretRecipeBookUnlocked = true; secretRecipeBookUnlocked = true;
break; break;

View file

@ -209,7 +209,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
/// <inheritdoc/> /// <inheritdoc/>
public bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row) public bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row)
{ {
return this.IsUnlockLinkUnlocked(row.UnlockLink); return this.IsUnlockLinkUnlocked(row.Unknown26);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -217,7 +217,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
{ {
return this.dataManager.GetExcelSheet<EmjVoiceNpc>().TryGetRow(row.RowId, out var emjVoiceNpcRow) return this.dataManager.GetExcelSheet<EmjVoiceNpc>().TryGetRow(row.RowId, out var emjVoiceNpcRow)
&& this.IsEmjVoiceNpcUnlocked(emjVoiceNpcRow) && this.IsEmjVoiceNpcUnlocked(emjVoiceNpcRow)
&& QuestManager.IsQuestComplete(row.UnlockQuest.RowId); && QuestManager.IsQuestComplete(row.Unknown1);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -264,47 +264,47 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
// To avoid the ExdModule.GetItemRowById call, which can return null if the excel page // To avoid the ExdModule.GetItemRowById call, which can return null if the excel page
// is not loaded, we're going to imitate the IsItemActionUnlocked call first: // is not loaded, we're going to imitate the IsItemActionUnlocked call first:
switch ((ItemActionAction)row.ItemAction.Value.Action.RowId) switch ((ItemActionType)row.ItemAction.Value.Type)
{ {
case ItemActionAction.Companion: case ItemActionType.Companion:
return UIState.Instance()->IsCompanionUnlocked(row.ItemAction.Value.Data[0]); return UIState.Instance()->IsCompanionUnlocked(row.ItemAction.Value.Data[0]);
case ItemActionAction.BuddyEquip: case ItemActionType.BuddyEquip:
return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.ItemAction.Value.Data[0]); return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.ItemAction.Value.Data[0]);
case ItemActionAction.Mount: case ItemActionType.Mount:
return PlayerState.Instance()->IsMountUnlocked(row.ItemAction.Value.Data[0]); return PlayerState.Instance()->IsMountUnlocked(row.ItemAction.Value.Data[0]);
case ItemActionAction.SecretRecipeBook: case ItemActionType.SecretRecipeBook:
return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0]); return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0]);
case ItemActionAction.UnlockLink: case ItemActionType.UnlockLink:
case ItemActionAction.OccultRecords: case ItemActionType.OccultRecords:
return UIState.Instance()->IsUnlockLinkUnlocked(row.ItemAction.Value.Data[0]); return UIState.Instance()->IsUnlockLinkUnlocked(row.ItemAction.Value.Data[0]);
case ItemActionAction.TripleTriadCard when row.AdditionalData.Is<TripleTriadCard>(): case ItemActionType.TripleTriadCard when row.AdditionalData.Is<TripleTriadCard>():
return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)row.AdditionalData.RowId); return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)row.AdditionalData.RowId);
case ItemActionAction.FolkloreTome: case ItemActionType.FolkloreTome:
return PlayerState.Instance()->IsFolkloreBookUnlocked(row.ItemAction.Value.Data[0]); return PlayerState.Instance()->IsFolkloreBookUnlocked(row.ItemAction.Value.Data[0]);
case ItemActionAction.OrchestrionRoll when row.AdditionalData.Is<Orchestrion>(): case ItemActionType.OrchestrionRoll when row.AdditionalData.Is<Orchestrion>():
return PlayerState.Instance()->IsOrchestrionRollUnlocked(row.AdditionalData.RowId); return PlayerState.Instance()->IsOrchestrionRollUnlocked(row.AdditionalData.RowId);
case ItemActionAction.FramersKit: case ItemActionType.FramersKit:
return PlayerState.Instance()->IsFramersKitUnlocked(row.AdditionalData.RowId); return PlayerState.Instance()->IsFramersKitUnlocked(row.AdditionalData.RowId);
case ItemActionAction.Ornament: case ItemActionType.Ornament:
return PlayerState.Instance()->IsOrnamentUnlocked(row.ItemAction.Value.Data[0]); return PlayerState.Instance()->IsOrnamentUnlocked(row.ItemAction.Value.Data[0]);
case ItemActionAction.Glasses: case ItemActionType.Glasses:
return PlayerState.Instance()->IsGlassesUnlocked((ushort)row.AdditionalData.RowId); return PlayerState.Instance()->IsGlassesUnlocked((ushort)row.AdditionalData.RowId);
case ItemActionAction.SoulShards when PublicContentOccultCrescent.GetState() is var occultCrescentState && occultCrescentState != null: case ItemActionType.SoulShards when PublicContentOccultCrescent.GetState() is var occultCrescentState && occultCrescentState != null:
var supportJobId = (byte)row.ItemAction.Value.Data[0]; var supportJobId = (byte)row.ItemAction.Value.Data[0];
return supportJobId < occultCrescentState->SupportJobLevels.Length && occultCrescentState->SupportJobLevels[supportJobId] != 0; return supportJobId < occultCrescentState->SupportJobLevels.Length && occultCrescentState->SupportJobLevels[supportJobId] != 0;
case ItemActionAction.CompanySealVouchers: case ItemActionType.CompanySealVouchers:
return false; return false;
} }
@ -327,7 +327,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
/// <inheritdoc/> /// <inheritdoc/>
public bool IsMKDLoreUnlocked(MKDLore row) public bool IsMKDLoreUnlocked(MKDLore row)
{ {
return this.IsUnlockLinkUnlocked(row.UnlockLink); return this.IsUnlockLinkUnlocked(row.Unknown2);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -414,20 +414,20 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
if (row.ItemAction.RowId == 0) if (row.ItemAction.RowId == 0)
return false; return false;
return (ItemActionAction)row.ItemAction.Value.Action.RowId return (ItemActionType)row.ItemAction.Value.Type
is ItemActionAction.Companion is ItemActionType.Companion
or ItemActionAction.BuddyEquip or ItemActionType.BuddyEquip
or ItemActionAction.Mount or ItemActionType.Mount
or ItemActionAction.SecretRecipeBook or ItemActionType.SecretRecipeBook
or ItemActionAction.UnlockLink or ItemActionType.UnlockLink
or ItemActionAction.TripleTriadCard or ItemActionType.TripleTriadCard
or ItemActionAction.FolkloreTome or ItemActionType.FolkloreTome
or ItemActionAction.OrchestrionRoll or ItemActionType.OrchestrionRoll
or ItemActionAction.FramersKit or ItemActionType.FramersKit
or ItemActionAction.Ornament or ItemActionType.Ornament
or ItemActionAction.Glasses or ItemActionType.Glasses
or ItemActionAction.OccultRecords or ItemActionType.OccultRecords
or ItemActionAction.SoulShards; or ItemActionType.SoulShards;
} }
/// <inheritdoc/> /// <inheritdoc/>

View file

@ -21,7 +21,6 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1116:SplitParametersMustStartOnLineAfterDeclaration", Justification = "Reviewed.")] [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1116:SplitParametersMustStartOnLineAfterDeclaration", Justification = "Reviewed.")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "This would be nice, but a big refactor")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "This would be nice, but a big refactor")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:FileNameMustMatchTypeName", Justification = "I don't like this one so much")] [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:FileNameMustMatchTypeName", Justification = "I don't like this one so much")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1108:BlockStatementsMustNotContainEmbeddedComments", Justification = "I like having comments in blocks")]
// ImRAII stuff // ImRAII stuff
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Raii")] [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Raii")]

View file

@ -4,7 +4,6 @@ using System.Runtime.InteropServices;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Hooking.Internal; using Dalamud.Hooking.Internal;
using Dalamud.Hooking.Internal.Verification;
namespace Dalamud.Hooking; namespace Dalamud.Hooking;
@ -202,19 +201,19 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate
if (EnvironmentConfiguration.DalamudForceMinHook) if (EnvironmentConfiguration.DalamudForceMinHook)
useMinHook = true; useMinHook = true;
var moduleHandle = Windows.Win32.PInvoke.GetModuleHandle(moduleName); using var moduleHandle = Windows.Win32.PInvoke.GetModuleHandle(moduleName);
if (moduleHandle.IsNull) if (moduleHandle.IsInvalid)
throw new Exception($"Could not get a handle to module {moduleName}"); throw new Exception($"Could not get a handle to module {moduleName}");
var procAddress = Windows.Win32.PInvoke.GetProcAddress(moduleHandle, exportName); var procAddress = (nint)Windows.Win32.PInvoke.GetProcAddress(moduleHandle, exportName);
if (procAddress.IsNull) if (procAddress == IntPtr.Zero)
throw new Exception($"Could not get the address of {moduleName}::{exportName}"); throw new Exception($"Could not get the address of {moduleName}::{exportName}");
var address = HookManager.FollowJmp(procAddress.Value); procAddress = HookManager.FollowJmp(procAddress);
if (useMinHook) if (useMinHook)
return new MinHookHook<T>(address, detour, Assembly.GetCallingAssembly()); return new MinHookHook<T>(procAddress, detour, Assembly.GetCallingAssembly());
else else
return new ReloadedHook<T>(address, detour, Assembly.GetCallingAssembly()); return new ReloadedHook<T>(procAddress, detour, Assembly.GetCallingAssembly());
} }
/// <summary> /// <summary>
@ -231,8 +230,6 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate
if (EnvironmentConfiguration.DalamudForceMinHook) if (EnvironmentConfiguration.DalamudForceMinHook)
useMinHook = true; useMinHook = true;
HookVerifier.Verify<T>(procAddress);
procAddress = HookManager.FollowJmp(procAddress); procAddress = HookManager.FollowJmp(procAddress);
if (useMinHook) if (useMinHook)
return new MinHookHook<T>(procAddress, detour, Assembly.GetCallingAssembly()); return new MinHookHook<T>(procAddress, detour, Assembly.GetCallingAssembly());

View file

@ -0,0 +1,100 @@
using System.Runtime.InteropServices;
using Reloaded.Hooks.Definitions;
namespace Dalamud.Hooking.Internal;
/// <summary>
/// This class represents a callsite hook. Only the specific address's instructions are replaced with this hook.
/// This is a destructive operation, no other callsite hooks can coexist at the same address.
///
/// There's no .Original for this hook type.
/// This is only intended for be for functions where the parameters provided allow you to invoke the original call.
///
/// This class was specifically added for hooking virtual function callsites.
/// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered.
/// </summary>
/// <typeparam name="T">Delegate signature for this hook.</typeparam>
internal class CallHook<T> : IDalamudHook where T : Delegate
{
private readonly Reloaded.Hooks.AsmHook asmHook;
private T? detour;
private bool activated;
/// <summary>
/// Initializes a new instance of the <see cref="CallHook{T}"/> class.
/// </summary>
/// <param name="address">Address of the instruction to replace.</param>
/// <param name="detour">Delegate to invoke.</param>
internal CallHook(nint address, T detour)
{
ArgumentNullException.ThrowIfNull(detour);
this.detour = detour;
this.Address = address;
var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour);
var code = new[]
{
"use64",
$"mov rax, 0x{detourPtr:X8}",
"call rax",
};
var opt = new AsmHookOptions
{
PreferRelativeJump = true,
Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal,
MaxOpcodeSize = 5,
};
this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt);
}
/// <summary>
/// Gets a value indicating whether the hook is enabled.
/// </summary>
public bool IsEnabled => this.asmHook.IsEnabled;
/// <inheritdoc/>
public IntPtr Address { get; }
/// <inheritdoc/>
public string BackendName => "Reloaded AsmHook";
/// <inheritdoc/>
public bool IsDisposed => this.detour == null;
/// <summary>
/// Starts intercepting a call to the function.
/// </summary>
public void Enable()
{
if (!this.activated)
{
this.activated = true;
this.asmHook.Activate();
return;
}
this.asmHook.Enable();
}
/// <summary>
/// Stops intercepting a call to the function.
/// </summary>
public void Disable()
{
this.asmHook.Disable();
}
/// <summary>
/// Remove a hook from the current process.
/// </summary>
public void Dispose()
{
this.asmHook.Disable();
this.detour = null;
}
}

View file

@ -1,41 +0,0 @@
using System.Linq;
namespace Dalamud.Hooking.Internal.Verification;
/// <summary>
/// Exception thrown when a provided delegate for a hook does not match a known delegate.
/// </summary>
public class HookVerificationException : Exception
{
private HookVerificationException(string message)
: base(message)
{
}
/// <summary>
/// Create a new <see cref="HookVerificationException"/> exception.
/// </summary>
/// <param name="address">The address of the function that is being hooked.</param>
/// <param name="passed">The delegate passed by the user.</param>
/// <param name="enforced">The delegate we think is correct.</param>
/// <param name="message">Additional context to show to the user.</param>
/// <returns>The created exception.</returns>
internal static HookVerificationException Create(IntPtr address, Type passed, Type enforced, string message)
{
return new HookVerificationException(
$"Hook verification failed for address 0x{address.ToInt64():X}\n\n" +
$"Why: {message}\n" +
$"Passed Delegate: {GetSignature(passed)}\n" +
$"Correct Delegate: {GetSignature(enforced)}\n\n" +
"The hook delegate must exactly match the provided signature to prevent memory corruption and wrong data passed to originals.");
}
private static string GetSignature(Type delegateType)
{
var method = delegateType.GetMethod("Invoke");
if (method == null) return delegateType.Name;
var parameters = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.Name));
return $"{method.ReturnType.Name} ({parameters})";
}
}

View file

@ -1,107 +0,0 @@
using System.Linq;
using Dalamud.Game;
using Dalamud.Logging.Internal;
namespace Dalamud.Hooking.Internal.Verification;
/// <summary>
/// Global utility that can verify whether hook delegates are correctly declared.
/// Initialized out-of-band, since Hook is instantiated all over the place without a service, so this cannot be
/// a service either.
/// </summary>
internal static class HookVerifier
{
private static readonly ModuleLog Log = new("HookVerifier");
private static readonly VerificationEntry[] ToVerify =
[
new(
"ActorControlSelf",
"E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64",
typeof(ActorControlSelfDelegate),
"Signature changed in Patch 7.4") // 7.4 (new parameters)
];
private delegate void ActorControlSelfDelegate(uint category, uint eventId, uint param1, uint param2, uint param3, uint param4, uint param5, uint param6, uint param7, uint param8, ulong targetId, byte param9);
/// <summary>
/// Initializes a new instance of the <see cref="HookVerifier"/> class.
/// </summary>
/// <param name="scanner">Process to scan in.</param>
public static void Initialize(TargetSigScanner scanner)
{
foreach (var entry in ToVerify)
{
if (!scanner.TryScanText(entry.Signature, out var address))
{
Log.Error("Could not resolve signature for hook {Name} ({Sig})", entry.Name, entry.Signature);
continue;
}
entry.Address = address;
}
}
/// <summary>
/// Verify the hook with the provided address and exception.
/// </summary>
/// <param name="address">The address of the function we are hooking.</param>
/// <typeparam name="T">The delegate type passed by the creator of the hook.</typeparam>
/// <exception cref="HookVerificationException">Exception thrown when we think the hook is not correctly declared.</exception>
public static void Verify<T>(IntPtr address) where T : Delegate
{
var entry = ToVerify.FirstOrDefault(x => x.Address == address);
// Nothing to verify for this hook?
if (entry == null)
{
return;
}
var passedType = typeof(T);
// Directly compare delegates
if (passedType == entry.TargetDelegateType)
{
return;
}
var passedInvoke = passedType.GetMethod("Invoke")!;
var enforcedInvoke = entry.TargetDelegateType.GetMethod("Invoke")!;
// Compare Return Type
var mismatch = passedInvoke.ReturnType != enforcedInvoke.ReturnType;
// Compare Parameter Count
var passedParams = passedInvoke.GetParameters();
var enforcedParams = enforcedInvoke.GetParameters();
if (passedParams.Length != enforcedParams.Length)
{
mismatch = true;
}
else
{
// Compare Parameter Types
for (var i = 0; i < passedParams.Length; i++)
{
if (passedParams[i].ParameterType != enforcedParams[i].ParameterType)
{
mismatch = true;
break;
}
}
}
if (mismatch)
{
throw HookVerificationException.Create(address, passedType, entry.TargetDelegateType, entry.Message);
}
}
private record VerificationEntry(string Name, string Signature, Type TargetDelegateType, string Message)
{
public nint Address { get; set; }
}
}

View file

@ -48,7 +48,7 @@ public abstract class Easing
/// Gets the current value of the animation, following unclamped logic. /// Gets the current value of the animation, following unclamped logic.
/// </summary> /// </summary>
[Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)] [Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)]
[Api15ToDo("Map this field to ValueClamped, probably.")] [Api13ToDo("Map this field to ValueClamped, probably.")]
public double Value => this.ValueUnclamped; public double Value => this.ValueUnclamped;
/// <summary> /// <summary>

View file

@ -56,11 +56,6 @@ public enum SettingsOpenKind
/// </summary> /// </summary>
ServerInfoBar, ServerInfoBar,
/// <summary>
/// Open to the "Badges" page.
/// </summary>
Badge,
/// <summary> /// <summary>
/// Open to the "Experimental" page. /// Open to the "Experimental" page.
/// </summary> /// </summary>

File diff suppressed because it is too large Load diff

View file

@ -64,9 +64,9 @@ public interface IObjectWithLocalizableName
var result = new Dictionary<string, string>((int)count); var result = new Dictionary<string, string>((int)count);
for (var i = 0u; i < count; i++) for (var i = 0u; i < count; i++)
{ {
fn->GetLocaleName(i, buf, maxStrLen).ThrowOnError(); fn->GetLocaleName(i, (ushort*)buf, maxStrLen).ThrowOnError();
var key = new string(buf); var key = new string(buf);
fn->GetString(i, buf, maxStrLen).ThrowOnError(); fn->GetString(i, (ushort*)buf, maxStrLen).ThrowOnError();
var value = new string(buf); var value = new string(buf);
result[key.ToLowerInvariant()] = value; result[key.ToLowerInvariant()] = value;
} }

View file

@ -133,8 +133,8 @@ public sealed class SystemFontFamilyId : IFontFamilyId
var familyIndex = 0u; var familyIndex = 0u;
BOOL exists = false; BOOL exists = false;
fixed (char* pName = this.EnglishName) fixed (void* pName = this.EnglishName)
sfc.Get()->FindFamilyName(pName, &familyIndex, &exists).ThrowOnError(); sfc.Get()->FindFamilyName((ushort*)pName, &familyIndex, &exists).ThrowOnError();
if (!exists) if (!exists)
throw new FileNotFoundException($"Font \"{this.EnglishName}\" not found."); throw new FileNotFoundException($"Font \"{this.EnglishName}\" not found.");

View file

@ -113,8 +113,8 @@ public sealed class SystemFontId : IFontId
var familyIndex = 0u; var familyIndex = 0u;
BOOL exists = false; BOOL exists = false;
fixed (char* name = this.Family.EnglishName) fixed (void* name = this.Family.EnglishName)
sfc.Get()->FindFamilyName(name, &familyIndex, &exists).ThrowOnError(); sfc.Get()->FindFamilyName((ushort*)name, &familyIndex, &exists).ThrowOnError();
if (!exists) if (!exists)
throw new FileNotFoundException($"Font \"{this.Family.EnglishName}\" not found."); throw new FileNotFoundException($"Font \"{this.Family.EnglishName}\" not found.");
@ -151,7 +151,7 @@ public sealed class SystemFontId : IFontId
flocal.Get()->GetFilePathLengthFromKey(refKey, refKeySize, &pathSize).ThrowOnError(); flocal.Get()->GetFilePathLengthFromKey(refKey, refKeySize, &pathSize).ThrowOnError();
var path = stackalloc char[(int)pathSize + 1]; var path = stackalloc char[(int)pathSize + 1];
flocal.Get()->GetFilePathFromKey(refKey, refKeySize, path, pathSize + 1).ThrowOnError(); flocal.Get()->GetFilePathFromKey(refKey, refKeySize, (ushort*)path, pathSize + 1).ThrowOnError();
return (new(path, 0, (int)pathSize), (int)fface.Get()->GetIndex()); return (new(path, 0, (int)pathSize), (int)fface.Get()->GetIndex());
} }

Some files were not shown because too many files have changed in this diff Show more