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

View file

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

View file

@ -1,57 +1,19 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Build Schema",
"$ref": "#/definitions/build",
"definitions": {
"Host": {
"type": "string",
"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": {
"build": {
"type": "object",
"properties": {
"Configuration": {
"type": "string",
"description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
"enum": [
"Debug",
"Release"
]
},
"Continue": {
"type": "boolean",
"description": "Indicates to continue a previously failed build attempt"
@ -61,8 +23,29 @@
"description": "Shows the help text for this build assembly"
},
"Host": {
"type": "string",
"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": {
"type": "boolean",
@ -91,46 +74,65 @@
"type": "array",
"description": "List of targets to be skipped. Empty list skips all dependencies",
"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": {
"type": "array",
"description": "List of targets to be invoked. Default is '{default_target}'",
"items": {
"$ref": "#/definitions/ExecutableTarget"
"type": "string",
"enum": [
"CI",
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"CompileInjectorBoot",
"Restore",
"SetCILogging",
"Test"
]
}
},
"Verbosity": {
"type": "string",
"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": [
"Debug",
"Release"
"Minimal",
"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.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory);
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.Platform = json.value("Platform", config.Platform);
config.GameVersion = json.value("GameVersion", config.GameVersion);

View file

@ -44,7 +44,6 @@ struct DalamudStartInfo {
std::string ConfigurationPath;
std::string LogPath;
std::string LogName;
std::string TempDirectory;
std::string PluginDirectory;
std::string AssetDirectory;
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"--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-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-delay-initialize={}", g_startInfo.DelayInitializeMs));
// NoLoadPlugins/NoLoadThirdPartyPlugins: supplied from DalamudCrashHandler

View file

@ -34,12 +34,6 @@ public record DalamudStartInfo
/// </summary>
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>
/// Gets or sets the path of the log files.
/// </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 Label="Output">
<OutputType>Exe</OutputType>
<OutputType>Library</OutputType>
<OutputPath>..\bin\$(Configuration)\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<ApplicationIcon>dalamud.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup Label="Documentation">

View file

@ -25,20 +25,34 @@ namespace Dalamud.Injector
/// <summary>
/// Entrypoint to the program.
/// </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>
/// Start the Dalamud injector.
/// </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>
public static int Main(string[] argsArray)
public static int Main(int argc, IntPtr argvPtr)
{
try
{
// API14 TODO: Refactor
var args = argsArray.ToList();
args.Insert(0, Assembly.GetExecutingAssembly().Location);
List<string> args = new(argc);
unsafe
{
var argv = (IntPtr*)argvPtr;
for (var i = 0; i < argc; i++)
args.Add(Marshal.PtrToStringUni(argv[i]));
}
Init(args);
args.Remove("-v"); // Remove "verbose" flag
@ -291,7 +305,6 @@ namespace Dalamud.Injector
var configurationPath = startInfo.ConfigurationPath;
var pluginDirectory = startInfo.PluginDirectory;
var assetDirectory = startInfo.AssetDirectory;
var tempDirectory = startInfo.TempDirectory;
var delayInitializeMs = startInfo.DelayInitializeMs;
var logName = startInfo.LogName;
var logPath = startInfo.LogPath;
@ -322,10 +335,6 @@ namespace Dalamud.Injector
{
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="))
{
delayInitializeMs = int.Parse(args[i][key.Length..]);
@ -438,7 +447,6 @@ namespace Dalamud.Injector
startInfo.ConfigurationPath = configurationPath;
startInfo.PluginDirectory = pluginDirectory;
startInfo.AssetDirectory = assetDirectory;
startInfo.TempDirectory = tempDirectory;
startInfo.Language = clientLanguage;
startInfo.Platform = platform;
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
VisualStudioVersion = 17.1.32319.34
MinimumVisualStudioVersion = 10.0.40219.1
@ -7,6 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
.gitignore = .gitignore
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
Directory.Build.props = Directory.Build.props
Directory.Packages.props = Directory.Packages.props
@ -25,6 +27,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Boot", "Dalamud.Boo
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Injector", "Dalamud.Injector\Dalamud.Injector.csproj", "{5B832F73-5F54-4ADC-870F-D0095EF72C9A}"
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}"
EndProject
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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{A6AA1C3F-9470-4922-9D3F-D4549657AB22}"
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}"
EndProject
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}.Release|Any CPU.ActiveCfg = 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.Build.0 = Debug|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64
@ -178,6 +188,8 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
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}
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {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 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>
/// Load a configuration from the provided path.
/// </summary>

View file

@ -11,7 +11,7 @@ namespace Dalamud.Configuration;
/// <summary>
/// Configuration to store settings for a dalamud plugin.
/// </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
{
private readonly DirectoryInfo configDirectory;

View file

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

View file

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

View file

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

View file

@ -82,13 +82,8 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
var tsInfo =
JsonConvert.DeserializeObject<LauncherTroubleshootingInfo>(
dalamud.StartInfo.TroubleshootingPackData);
// Don't fail for IndexIntegrityResult.Exception, since the check during launch has a very small timeout
// this.HasModifiedGameDataFiles =
// tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed;
// TODO: Put above back when check in XL is fixed
this.HasModifiedGameDataFiles = false;
this.HasModifiedGameDataFiles =
tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception;
if (this.HasModifiedGameDataFiles)
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);
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]",
Versioning.GetScmVersion(),
Versioning.GetGitHashClientStructs(),
Util.GetScmVersion(),
Util.GetGitHashClientStructs(),
FFXIVClientStructs.ThisAssembly.Git.Commits);
dalamud.WaitForUnload();
@ -263,7 +263,7 @@ public sealed class EntryPoint
var symbolPath = Path.Combine(info.AssetDirectory, "UIRes", "pdb");
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
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>
/// Base class for AddonLifecycle AddonArgTypes.
/// </summary>
public class AddonArgs
public abstract unsafe class AddonArgs
{
/// <summary>
/// Constant string representing the name of an addon that is invalid.
/// </summary>
public const string InvalidAddon = "NullAddon";
/// <summary>
/// Initializes a new instance of the <see cref="AddonArgs"/> class.
/// </summary>
internal AddonArgs()
{
}
private string? addonName;
/// <summary>
/// Gets the name of the addon this args referrers to.
/// </summary>
public string AddonName { get; private set; } = InvalidAddon;
public string AddonName => this.GetAddonName();
/// <summary>
/// Gets the pointer to the addons AtkUnitBase.
@ -30,17 +25,55 @@ public class AddonArgs
public AtkUnitBasePtr Addon
{
get;
internal set
{
field = value;
if (!this.Addon.IsNull && !string.IsNullOrEmpty(value.Name))
this.AddonName = value.Name;
}
internal set;
}
/// <summary>
/// Gets the type of these args.
/// </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>
/// Addon argument data for ReceiveEvent events.
/// </summary>
public class AddonReceiveEventArgs : AddonArgs
public class AddonReceiveEventArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonReceiveEventArgs"/> class.
/// </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; }
/// <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>
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.Interop;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Refresh events.
/// </summary>
public class AddonRefreshArgs : AddonArgs
public class AddonRefreshArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonRefreshArgs"/> class.
/// </summary>
internal AddonRefreshArgs()
[Obsolete("Not intended for public construction.", false)]
public AddonRefreshArgs()
{
}
@ -36,32 +31,19 @@ public class AddonRefreshArgs : AddonArgs
/// <summary>
/// Gets the AtkValues in the form of a span.
/// </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);
/// <summary>
/// Gets an enumerable collection of <see cref="AtkValuePtr"/> of the event's AtkValues.
/// </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
}
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone();
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>
/// Addon argument data for OnRequestedUpdate events.
/// </summary>
public class AddonRequestedUpdateArgs : AddonArgs
public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonRequestedUpdateArgs"/> class.
/// </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.
/// </summary>
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.Interop;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Setup events.
/// </summary>
public class AddonSetupArgs : AddonArgs
public class AddonSetupArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonSetupArgs"/> class.
/// </summary>
internal AddonSetupArgs()
[Obsolete("Not intended for public construction.", false)]
public AddonSetupArgs()
{
}
@ -36,32 +31,19 @@ public class AddonSetupArgs : AddonArgs
/// <summary>
/// Gets the AtkValues in the form of a span.
/// </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);
/// <summary>
/// Gets an enumerable collection of <see cref="AtkValuePtr"/> of the event's AtkValues.
/// </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
}
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone();
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>
public enum AddonArgsType
{
/// <summary>
/// Generic arg type that contains no meaningful data.
/// </summary>
Generic,
/// <summary>
/// Contains argument data for Setup.
/// </summary>
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>
/// Contains argument data for RequestedUpdate.
/// </summary>
/// </summary>
RequestedUpdate,
/// <summary>
/// Contains argument data for Refresh.
/// </summary>
/// </summary>
Refresh,
/// <summary>
/// Contains argument data for ReceiveEvent.
/// </summary>
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>
/// <seealso cref="AddonSetupArgs"/>
PreSetup,
/// <summary>
/// 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
@ -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
/// is fired every frame that an addon is loaded, regardless of visibility.
/// </summary>
/// <seealso cref="AddonUpdateArgs"/>
PreUpdate,
/// <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
/// <see cref="PreUpdate"/>, this event is only fired if an addon is visible or otherwise drawing to screen.
/// </summary>
/// <seealso cref="AddonDrawArgs"/>
PreDraw,
/// <summary>
@ -60,8 +62,9 @@ public enum AddonEvent
/// <br />
/// As this is part of the destruction process for an addon, this event does not have an associated Post event.
/// </remarks>
/// <seealso cref="AddonFinalizeArgs"/>
PreFinalize,
/// <summary>
/// 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
@ -78,13 +81,13 @@ public enum AddonEvent
/// to the Free Company's overview.
/// </example>
PreRequestedUpdate,
/// <summary>
/// An event that is fired after an addon has finished processing an <c>ArrayData</c> update.
/// See <see cref="PreRequestedUpdate"/> for more information.
/// </summary>
PostRequestedUpdate,
/// <summary>
/// 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
@ -93,13 +96,13 @@ public enum AddonEvent
/// <seealso cref="AddonRefreshArgs"/>
/// <seealso cref="PostRefresh"/>
PreRefresh,
/// <summary>
/// An event that is fired after an addon has finished its refresh.
/// See <see cref="PreRefresh"/> for more information.
/// </summary>
PostRefresh,
/// <summary>
/// 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
@ -109,98 +112,10 @@ public enum AddonEvent
/// <seealso cref="AddonReceiveEventArgs"/>
/// <seealso cref="PostReceiveEvent"/>
PreReceiveEvent,
/// <summary>
/// An event that is fired after an addon finishes calling its <see cref="AtkEventListener.ReceiveEvent"/> method.
/// See <see cref="PreReceiveEvent"/> for more information.
/// </summary>
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.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking;
using Dalamud.Hooking.Internal;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle;
@ -20,36 +21,75 @@ namespace Dalamud.Game.Addon.Lifecycle;
[ServiceManager.EarlyLoadedService]
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 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]
private AddonLifecycle()
private AddonLifecycle(TargetSigScanner sigScanner)
{
this.onInitializeAddonHook = Hook<AtkUnitBase.Delegates.Initialize>.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize);
this.onInitializeAddonHook.Enable();
this.address = new AddonLifecycleAddressResolver();
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>
/// Gets a list of all AddonLifecycle Event Listeners.
/// </summary> <br/>
/// Mapping is: EventType -> AddonName -> ListenerList
internal Dictionary<AddonEvent, Dictionary<string, HashSet<AddonLifecycleEventListener>>> EventListeners { get; } = [];
/// </summary>
internal List<AddonLifecycleEventListener> EventListeners { get; } = new();
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.onInitializeAddonHook?.Dispose();
this.onInitializeAddonHook = null;
this.onAddonSetupHook.Dispose();
this.onAddonFinalizeHook.Dispose();
this.onAddonDrawHook.Dispose();
this.onAddonUpdateHook.Dispose();
this.onAddonRefreshHook.Dispose();
this.onAddonRequestedUpdateHook.Dispose();
AllocatedTables.ForEach(entry => entry.Dispose());
AllocatedTables.Clear();
foreach (var receiveEventListener in this.ReceiveEventListeners)
{
receiveEventListener.Dispose();
}
}
/// <summary>
@ -58,20 +98,20 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to register.</param>
internal void RegisterListener(AddonLifecycleEventListener listener)
{
if (!this.EventListeners.ContainsKey(listener.EventType))
this.framework.RunOnTick(() =>
{
if (!this.EventListeners.TryAdd(listener.EventType, []))
return;
}
// Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type
if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName))
{
if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []))
return;
}
this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
this.EventListeners.Add(listener);
// 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.
if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
{
if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
{
receiveEventListener.TryEnable();
}
}
});
}
/// <summary>
@ -80,13 +120,27 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to unregister.</param>
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>
@ -97,76 +151,226 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="blame">What to blame on errors.</param>
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
{
// Early return if we don't have any listeners of this type
if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return;
// Handle listeners for this event type that don't care which addon is triggering it
if (addonListeners.TryGetValue(string.Empty, out var globalListeners))
// Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better.
foreach (var listener in this.EventListeners)
{
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);
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global addon event listener.");
}
listener.FunctionDelegate.Invoke(eventType, args);
}
}
// 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)
catch (Exception e)
{
try
{
listener.FunctionDelegate.Invoke(eventType, args);
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific addon {args.AddonName}.");
}
Log.Error(e, $"Exception in {blame} during {eventType} invoke.");
}
}
}
/// <summary>
/// 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)
private void RegisterReceiveEventHook(AtkUnitBase* addon)
{
var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress);
if (matchedTable == null) return null;
// Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener.
// 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
{
this.LogInitialize(addon->NameString);
// AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
AllocatedTables.Add(new AddonVirtualTable(addon, this));
this.RegisterReceiveEventHook(addon);
}
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 LogInitialize(string addonName)
private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
{
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]
private readonly AddonLifecycle addonLifecycleService = Service<AddonLifecycle>.Get();
private readonly List<AddonLifecycleEventListener> eventListeners = [];
private readonly List<AddonLifecycleEventListener> eventListeners = new();
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
@ -254,14 +458,10 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
this.eventListeners.RemoveAll(entry =>
{
if (entry.FunctionDelegate != handler) return false;
this.addonLifecycleService.UnregisterListener(entry);
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.
/// </summary>
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>
/// Gets the event type this listener is looking for.
/// </summary>
public AddonEvent EventType { get; init; }
/// <summary>
/// Gets the delegate this listener invokes.
/// </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.Runtime.InteropServices;
using Dalamud.Plugin.Services;
namespace Dalamud.Game;
/// <summary>

View file

@ -104,7 +104,7 @@ internal partial class ChatHandlers : IServiceType
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)));
}
@ -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(
(_, _) => dalamudInterface.OpenPluginInstallerTo(PluginInstallerOpenKind.Changelogs));
@ -137,7 +137,7 @@ internal partial class ChatHandlers : IServiceType
Type = XivChatType.Notice,
});
this.configuration.LastVersion = Versioning.GetAssemblyVersion();
this.configuration.LastVersion = Util.AssemblyVersion;
this.configuration.QueueSave();
}

View file

@ -63,37 +63,47 @@ public interface IAetheryteEntry
}
/// <summary>
/// This struct represents an aetheryte entry available to the game.
/// Class representing an aetheryte entry available to the game.
/// </summary>
/// <param name="data">Data read from the Aetheryte List.</param>
internal readonly struct AetheryteEntry(TeleportInfo data) : IAetheryteEntry
internal sealed class AetheryteEntry : IAetheryteEntry
{
/// <inheritdoc />
public uint AetheryteId => data.AetheryteId;
private readonly TeleportInfo data;
/// <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 />
public uint TerritoryId => data.TerritoryId;
public uint AetheryteId => this.data.AetheryteId;
/// <inheritdoc />
public byte SubIndex => data.SubIndex;
public uint TerritoryId => this.data.TerritoryId;
/// <inheritdoc />
public byte Ward => data.Ward;
public byte SubIndex => this.data.SubIndex;
/// <inheritdoc />
public byte Plot => data.Plot;
public byte Ward => this.data.Ward;
/// <inheritdoc />
public uint GilCost => data.GilCost;
public byte Plot => this.data.Plot;
/// <inheritdoc />
public bool IsFavourite => data.IsFavourite;
public uint GilCost => this.data.GilCost;
/// <inheritdoc />
public bool IsSharedHouse => data.IsSharedHouse;
public bool IsFavourite => this.data.IsFavourite;
/// <inheritdoc />
public bool IsApartment => data.IsApartment;
public bool IsSharedHouse => this.data.IsSharedHouse;
/// <inheritdoc />
public bool IsApartment => this.data.IsApartment;
/// <inheritdoc />
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/>
public IEnumerator<IAetheryteEntry> GetEnumerator()
{
return new Enumerator(this);
for (var i = 0; i < this.Length; i++)
{
yield return this[i];
}
}
/// <inheritdoc/>
@ -96,34 +99,4 @@ internal sealed partial class AetheryteList
{
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 CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy;
using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState;
namespace Dalamud.Game.ClientState.Buddy;
@ -24,7 +23,7 @@ namespace Dalamud.Game.ClientState.Buddy;
#pragma warning restore SA1015
internal sealed partial class BuddyList : IServiceType, IBuddyList
{
private const uint InvalidEntityId = 0xE0000000;
private const uint InvalidObjectID = 0xE0000000;
[ServiceManager.ServiceDependency]
private readonly PlayerState playerState = Service<PlayerState>.Get();
@ -85,37 +84,37 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
}
/// <inheritdoc/>
public unsafe nint GetCompanionBuddyMemberAddress()
public unsafe IntPtr GetCompanionBuddyMemberAddress()
{
return (nint)this.BuddyListStruct->CompanionInfo.Companion;
return (IntPtr)this.BuddyListStruct->CompanionInfo.Companion;
}
/// <inheritdoc/>
public unsafe nint GetPetBuddyMemberAddress()
public unsafe IntPtr GetPetBuddyMemberAddress()
{
return (nint)this.BuddyListStruct->PetInfo.Pet;
return (IntPtr)this.BuddyListStruct->PetInfo.Pet;
}
/// <inheritdoc/>
public unsafe nint GetBattleBuddyMemberAddress(int index)
public unsafe IntPtr GetBattleBuddyMemberAddress(int index)
{
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/>
public unsafe IBuddyMember? CreateBuddyMemberReference(nint address)
public IBuddyMember? CreateBuddyMemberReference(IntPtr address)
{
if (address == 0)
if (address == IntPtr.Zero)
return null;
if (this.playerState.ContentId == 0)
if (!this.playerState.IsLoaded)
return null;
var buddy = new BuddyMember((CSBuddyMember*)address);
if (buddy.EntityId == InvalidEntityId)
var buddy = new BuddyMember(address);
if (buddy.ObjectId == InvalidObjectID)
return null;
return buddy;
@ -133,39 +132,12 @@ internal sealed partial class BuddyList
/// <inheritdoc/>
public IEnumerator<IBuddyMember> GetEnumerator()
{
return new Enumerator(this);
for (var i = 0; i < this.Length; i++)
{
yield return this[i];
}
}
/// <inheritdoc/>
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.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Lumina.Excel;
using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
namespace Dalamud.Game.ClientState.Buddy;
/// <summary>
/// Interface representing represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
/// </summary>
public interface IBuddyMember : IEquatable<IBuddyMember>
public interface IBuddyMember
{
/// <summary>
/// Gets the address of the buddy in memory.
/// </summary>
nint Address { get; }
IntPtr Address { get; }
/// <summary>
/// Gets the object ID of this buddy.
@ -71,34 +67,42 @@ public interface IBuddyMember : IEquatable<IBuddyMember>
}
/// <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>
/// <param name="ptr">A pointer to the BuddyMember.</param>
internal readonly unsafe struct BuddyMember(CSBuddyMember* ptr) : IBuddyMember
internal unsafe class BuddyMember : IBuddyMember
{
[ServiceManager.ServiceDependency]
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
/// <inheritdoc />
public nint Address => (nint)ptr;
/// <summary>
/// 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 />
public uint ObjectId => this.EntityId;
public IntPtr Address { get; }
/// <inheritdoc />
public uint EntityId => ptr->EntityId;
public uint ObjectId => this.Struct->EntityId;
/// <inheritdoc />
public IGameObject? GameObject => this.objectTable.SearchById(this.EntityId);
public uint EntityId => this.Struct->EntityId;
/// <inheritdoc />
public uint CurrentHP => ptr->CurrentHealth;
public IGameObject? GameObject => this.objectTable.SearchById(this.ObjectId);
/// <inheritdoc />
public uint MaxHP => ptr->MaxHealth;
public uint CurrentHP => this.Struct->CurrentHealth;
/// <inheritdoc />
public uint DataID => ptr->DataId;
public uint MaxHP => this.Struct->MaxHealth;
/// <inheritdoc />
public uint DataID => this.Struct->DataId;
/// <inheritdoc />
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 />
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);
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();
}
private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address;
}

View file

@ -37,6 +37,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
private readonly GameLifecycle lifecycle;
private readonly ClientStateAddressResolver address;
private readonly Hook<HandleZoneInitPacketDelegate> handleZoneInitPacketHook;
private readonly Hook<UIModule.Delegates.HandlePacket> uiModuleHandlePacketHook;
private readonly Hook<SetCurrentInstanceDelegate> setCurrentInstanceHook;
@ -71,11 +72,13 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
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.setCurrentInstanceHook = Hook<SetCurrentInstanceDelegate>.FromAddress(this.AddressResolver.SetCurrentInstance, this.SetCurrentInstanceDetour);
this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
this.handleZoneInitPacketHook.Enable();
this.uiModuleHandlePacketHook.Enable();
this.setCurrentInstanceHook.Enable();
@ -268,6 +271,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
/// </summary>
void IInternalDisposableService.DisposeService()
{
this.handleZoneInitPacketHook.Dispose();
this.uiModuleHandlePacketHook.Dispose();
this.onLogoutHook.Dispose();
this.setCurrentInstanceHook.Dispose();
@ -290,6 +294,23 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
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(
UIModule* thisPtr, UIModulePacketType type, uint uintParam, void* packet)
{
@ -335,15 +356,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
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;
/// <summary>
@ -21,6 +19,11 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
// Functions
/// <summary>
/// Gets the address of the method that handles the ZoneInit packet.
/// </summary>
public nint HandleZoneInitPacket { get; private set; }
/// <summary>
/// Gets the address of the method that sets the current public instance.
/// </summary>
@ -32,6 +35,7 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
/// <param name="sig">The signature scanner to facilitate setup.</param>
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
// 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>
/// 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>
internal const int MaxConditionEntries = 112;
internal const int MaxConditionEntries = 104;
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();

View file

@ -520,17 +520,4 @@ public enum ConditionFlag
PilotingMech = 102,
// 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 Dalamud.Data;
@ -8,12 +7,10 @@ using Dalamud.Memory;
using Lumina.Excel;
using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext;
namespace Dalamud.Game.ClientState.Fates;
/// <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>
public interface IFate : IEquatable<IFate>
{
@ -115,96 +112,129 @@ public interface IFate : IEquatable<IFate>
/// <summary>
/// Gets the address of this Fate in memory.
/// </summary>
nint Address { get; }
IntPtr Address { get; }
}
/// <summary>
/// This struct represents a Fate.
/// This class represents an FFXIV Fate.
/// </summary>
/// <param name="ptr">A pointer to the FateContext.</param>
internal readonly unsafe struct Fate(CSFateContext* ptr) : IFate
internal unsafe partial class Fate
{
/// <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 />
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/>
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/>
public RowRef<Lumina.Excel.Sheets.Fate> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Fate>(this.FateId);
/// <inheritdoc/>
public int StartTimeEpoch => ptr->StartTimeEpoch;
public int StartTimeEpoch => this.Struct->StartTimeEpoch;
/// <inheritdoc/>
public short Duration => ptr->Duration;
public short Duration => this.Struct->Duration;
/// <inheritdoc/>
public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds();
/// <inheritdoc/>
public SeString Name => MemoryHelper.ReadSeString(&ptr->Name);
public SeString Name => MemoryHelper.ReadSeString(&this.Struct->Name);
/// <inheritdoc/>
public SeString Description => MemoryHelper.ReadSeString(&ptr->Description);
public SeString Description => MemoryHelper.ReadSeString(&this.Struct->Description);
/// <inheritdoc/>
public SeString Objective => MemoryHelper.ReadSeString(&ptr->Objective);
public SeString Objective => MemoryHelper.ReadSeString(&this.Struct->Objective);
/// <inheritdoc/>
public FateState State => (FateState)ptr->State;
public FateState State => (FateState)this.Struct->State;
/// <inheritdoc/>
public byte HandInCount => ptr->HandInCount;
public byte HandInCount => this.Struct->HandInCount;
/// <inheritdoc/>
public byte Progress => ptr->Progress;
public byte Progress => this.Struct->Progress;
/// <inheritdoc/>
public bool HasBonus => ptr->IsBonus;
public bool HasBonus => this.Struct->IsBonus;
/// <inheritdoc/>
public uint IconId => ptr->IconId;
public uint IconId => this.Struct->IconId;
/// <inheritdoc/>
public byte Level => ptr->Level;
public byte Level => this.Struct->Level;
/// <inheritdoc/>
public byte MaxLevel => ptr->MaxLevel;
public byte MaxLevel => this.Struct->MaxLevel;
/// <inheritdoc/>
public Vector3 Position => ptr->Location;
public Vector3 Position => this.Struct->Location;
/// <inheritdoc/>
public float Radius => ptr->Radius;
public float Radius => this.Struct->Radius;
/// <inheritdoc/>
public uint MapIconId => ptr->MapIconId;
public uint MapIconId => this.Struct->MapIconId;
/// <summary>
/// Gets the territory this <see cref="Fate"/> is located in.
/// </summary>
public RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(ptr->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();
}
public RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->MapMarkers[0].MapMarkerData.TerritoryTypeId);
}

View file

@ -6,7 +6,6 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext;
using CSFateManager = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager;
namespace Dalamud.Game.ClientState.Fates;
@ -27,7 +26,7 @@ internal sealed partial class FateTable : IServiceType, IFateTable
}
/// <inheritdoc/>
public unsafe nint Address => (nint)CSFateManager.Instance();
public unsafe IntPtr Address => (nint)CSFateManager.Instance();
/// <inheritdoc/>
public unsafe int Length
@ -70,29 +69,29 @@ internal sealed partial class FateTable : IServiceType, IFateTable
}
/// <inheritdoc/>
public unsafe nint GetFateAddress(int index)
public unsafe IntPtr GetFateAddress(int index)
{
if (index >= this.Length)
return 0;
return IntPtr.Zero;
var fateManager = CSFateManager.Instance();
if (fateManager == null)
return 0;
return IntPtr.Zero;
return (nint)fateManager->Fates[index].Value;
return (IntPtr)fateManager->Fates[index].Value;
}
/// <inheritdoc/>
public unsafe IFate? CreateFateReference(IntPtr address)
public IFate? CreateFateReference(IntPtr offset)
{
if (address == 0)
if (offset == IntPtr.Zero)
return null;
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
var playerState = Service<PlayerState>.Get();
if (!playerState.IsLoaded)
return null;
return new Fate((CSFateContext*)address);
return new Fate(offset);
}
}
@ -107,39 +106,12 @@ internal sealed partial class FateTable
/// <inheritdoc/>
public IEnumerator<IFate> GetEnumerator()
{
return new Enumerator(this);
for (var i = 0; i < this.Length; i++)
{
yield return this[i];
}
}
/// <inheritdoc/>
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 Microsoft.Extensions.ObjectPool;
using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
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 Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4];
[ServiceManager.ServiceConstructor]
private unsafe ObjectTable()
{
@ -44,6 +48,9 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
this.cachedObjectTable = new CachedEntry[objectTableLength];
for (var i = 0; i < this.cachedObjectTable.Length; i++)
this.cachedObjectTable[i] = new(nativeObjectTable.GetPointer(i));
for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++)
this.frameworkThreadEnumerators[i] = new(this, i);
}
/// <inheritdoc/>
@ -236,25 +243,43 @@ internal sealed partial class ObjectTable
public IEnumerator<IGameObject> GetEnumerator()
{
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/>
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;
public IGameObject Current { get; private set; }
public IGameObject Current { get; private set; } = null!;
object IEnumerator.Current => this.Current;
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)
{
@ -263,17 +288,24 @@ internal sealed partial class ObjectTable
}
}
this.Current = default;
return false;
}
public void Reset()
{
this.index = -1;
}
public void Reset() => this.index = -1;
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.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Control;

View file

@ -9,7 +9,6 @@ using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using CSGroupManager = FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager;
using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember;
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;
/// <inheritdoc/>
public unsafe nint GroupManagerAddress => (nint)CSGroupManager.Instance();
public unsafe IntPtr GroupManagerAddress => (nint)CSGroupManager.Instance();
/// <inheritdoc/>
public nint GroupListAddress => (nint)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]);
public IntPtr GroupListAddress => (IntPtr)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]);
/// <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/>
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/>
public IPartyMember? this[int index]
@ -82,45 +81,39 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
}
/// <inheritdoc/>
public nint GetPartyMemberAddress(int index)
public IntPtr GetPartyMemberAddress(int index)
{
if (index < 0 || index >= GroupLength)
return 0;
return IntPtr.Zero;
return this.GroupListAddress + (index * PartyMemberSize);
}
/// <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;
if (address == 0)
return null;
return new PartyMember((CSPartyMember*)address);
return new PartyMember(address);
}
/// <inheritdoc/>
public nint GetAllianceMemberAddress(int index)
public IntPtr GetAllianceMemberAddress(int index)
{
if (index < 0 || index >= AllianceLength)
return 0;
return IntPtr.Zero;
return this.AllianceListAddress + (index * PartyMemberSize);
}
/// <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;
if (address == 0)
return null;
return new PartyMember((CSPartyMember*)address);
return new PartyMember(address);
}
}
@ -135,43 +128,18 @@ internal sealed partial class PartyList
/// <inheritdoc/>
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/>
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.Runtime.CompilerServices;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Statuses;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Utility;
using Dalamud.Memory;
using Lumina.Excel;
using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember;
namespace Dalamud.Game.ClientState.Party;
/// <summary>
/// Interface representing a party member.
/// </summary>
public interface IPartyMember : IEquatable<IPartyMember>
public interface IPartyMember
{
/// <summary>
/// Gets the address of this party member in memory.
/// </summary>
nint Address { get; }
IntPtr Address { get; }
/// <summary>
/// Gets a list of buffs or debuffs applied to this party member.
@ -110,82 +108,69 @@ public interface IPartyMember : IEquatable<IPartyMember>
}
/// <summary>
/// This struct represents a party member in the group manager.
/// This class represents a party member in the group manager.
/// </summary>
/// <param name="ptr">A pointer to the PartyMember.</param>
internal unsafe readonly struct PartyMember(CSPartyMember* ptr) : IPartyMember
internal unsafe class PartyMember : IPartyMember
{
/// <inheritdoc/>
public nint Address => (nint)ptr;
/// <summary>
/// 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/>
public StatusList Statuses => new(&ptr->StatusManager);
public IntPtr Address { get; }
/// <inheritdoc/>
public Vector3 Position => ptr->Position;
public StatusList Statuses => new(&this.Struct->StatusManager);
/// <inheritdoc/>
[Api15ToDo("Change type to ulong.")]
public long ContentId => (long)ptr->ContentId;
public Vector3 Position => this.Struct->Position;
/// <inheritdoc/>
public uint ObjectId => ptr->EntityId;
public long ContentId => (long)this.Struct->ContentId;
/// <inheritdoc/>
public uint EntityId => ptr->EntityId;
public uint ObjectId => this.Struct->EntityId;
/// <inheritdoc/>
public uint EntityId => this.Struct->EntityId;
/// <inheritdoc/>
public IGameObject? GameObject => Service<ObjectTable>.Get().SearchById(this.EntityId);
/// <inheritdoc/>
public uint CurrentHP => ptr->CurrentHP;
public uint CurrentHP => this.Struct->CurrentHP;
/// <inheritdoc/>
public uint MaxHP => ptr->MaxHP;
public uint MaxHP => this.Struct->MaxHP;
/// <inheritdoc/>
public ushort CurrentMP => ptr->CurrentMP;
public ushort CurrentMP => this.Struct->CurrentMP;
/// <inheritdoc/>
public ushort MaxMP => ptr->MaxMP;
public ushort MaxMP => this.Struct->MaxMP;
/// <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/>
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/>
public SeString Name => SeString.Parse(ptr->Name);
public SeString Name => SeString.Parse(this.Struct->Name);
/// <inheritdoc/>
public byte Sex => ptr->Sex;
public byte Sex => this.Struct->Sex;
/// <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/>
public byte Level => ptr->Level;
public byte Level => this.Struct->Level;
public static bool operator ==(PartyMember x, PartyMember y) => x.Equals(y);
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();
}
private FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember*)this.Address;
}

View file

@ -1,49 +1,61 @@
using System.Diagnostics.CodeAnalysis;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Lumina.Excel;
using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status;
namespace Dalamud.Game.ClientState.Statuses;
/// <summary>
/// Interface representing a status.
/// This class represents a status effect an actor is afflicted by.
/// </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>
/// Gets the address of the status in memory.
/// </summary>
nint Address { get; }
public IntPtr Address { get; }
/// <summary>
/// Gets the status ID of this status.
/// </summary>
uint StatusId { get; }
public uint StatusId => this.Struct->StatusId;
/// <summary>
/// Gets the GameData associated with this status.
/// </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>
/// Gets the parameter value of the status.
/// </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>
/// Gets the time remaining of this status.
/// </summary>
float RemainingTime { get; }
public float RemainingTime => this.Struct->RemainingTime;
/// <summary>
/// Gets the source ID of this status.
/// </summary>
uint SourceId { get; }
public uint SourceId => this.Struct->SourceObject.ObjectId;
/// <summary>
/// Gets the source actor associated with this status.
@ -51,55 +63,7 @@ public interface IStatus : IEquatable<IStatus>
/// <remarks>
/// This iterates the actor table, it should be used with care.
/// </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 static bool operator ==(Status x, Status y) => x.Equals(y);
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);
}
private FFXIVClientStructs.FFXIV.Client.Game.Status* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Status*)this.Address;
}

View file

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status;
using Dalamud.Game.Player;
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.
/// </summary>
/// <param name="address">Address of the status list.</param>
internal StatusList(nint address)
internal StatusList(IntPtr address)
{
this.Address = address;
}
@ -26,14 +26,14 @@ public sealed unsafe partial class StatusList
/// </summary>
/// <param name="pointer">Pointer to the status list.</param>
internal unsafe StatusList(void* pointer)
: this((nint)pointer)
: this((IntPtr)pointer)
{
}
/// <summary>
/// Gets the address of the status list in memory.
/// </summary>
public nint Address { get; }
public IntPtr Address { get; }
/// <summary>
/// Gets the amount of status effect slots the actor has.
@ -49,7 +49,7 @@ public sealed unsafe partial class StatusList
/// </summary>
/// <param name="index">Status Index.</param>
/// <returns>The status at the specified index.</returns>
public IStatus? this[int index]
public Status? this[int index]
{
get
{
@ -66,7 +66,7 @@ public sealed unsafe partial class StatusList
/// </summary>
/// <param name="address">The address of the status list in memory.</param>
/// <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)
return null;
@ -74,12 +74,8 @@ public sealed unsafe partial class StatusList
// 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
// here or somewhere else.
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
return null;
if (address == 0)
var playerState = Service<PlayerState>.Get();
if (!playerState.IsLoaded)
return null;
return new StatusList(address);
@ -90,15 +86,16 @@ public sealed unsafe partial class StatusList
/// </summary>
/// <param name="address">The address of the status effect in memory.</param>
/// <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)
return null;
if (address == 0)
var playerState = Service<PlayerState>.Get();
if (!playerState.IsLoaded)
return null;
return new Status((CSStatus*)address);
return new Status(address);
}
/// <summary>
@ -106,22 +103,22 @@ public sealed unsafe partial class StatusList
/// </summary>
/// <param name="index">The index of the status.</param>
/// <returns>The memory address of the status.</returns>
public nint GetStatusAddress(int index)
public IntPtr GetStatusAddress(int index)
{
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>
/// This collection represents the status effects an actor is afflicted by.
/// </summary>
public sealed partial class StatusList : IReadOnlyCollection<IStatus>, ICollection
public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollection
{
/// <inheritdoc/>
int IReadOnlyCollection<IStatus>.Count => this.Length;
int IReadOnlyCollection<Status>.Count => this.Length;
/// <inheritdoc/>
int ICollection.Count => this.Length;
@ -133,9 +130,17 @@ public sealed partial class StatusList : IReadOnlyCollection<IStatus>, ICollecti
object ICollection.SyncRoot => this;
/// <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/>
@ -150,38 +155,4 @@ public sealed partial class StatusList : IReadOnlyCollection<IStatus>, ICollecti
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.Weather = dataManager.GetExcelSheet<Weather>().GetRow(*(byte*)(packet + 0x10));
const int NumFestivals = 8;
const int NumFestivals = 4;
eventArgs.ActiveFestivals = new Festival[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.
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));
}

View file

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

View file

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

View file

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

View file

@ -26,6 +26,7 @@ using Lumina.Text;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
using LSeStringBuilder = Lumina.Text.SeStringBuilder;
using SeString = Dalamud.Game.Text.SeStringHandling.SeString;
using SeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder;
@ -206,21 +207,21 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
if (this.chatQueue.Count == 0)
return;
using var rssb = new RentedSeStringBuilder();
var sb = LSeStringBuilder.SharedPool.Get();
Span<byte> namebuf = stackalloc byte[256];
using var sender = new Utf8String();
using var message = new Utf8String();
while (this.chatQueue.TryDequeue(out var chat))
{
rssb.Builder.Clear();
sb.Clear();
foreach (var c in UtfEnumerator.From(chat.MessageBytes, UtfEnumeratorFlags.Utf8SeString))
{
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)
rssb.Builder.BeginMacro(MacroCode.NonBreakingSpace).EndMacro();
sb.BeginMacro(MacroCode.NonBreakingSpace).EndMacro();
else
rssb.Builder.Append(c);
sb.Append(c);
}
if (chat.NameBytes.Length + 1 < namebuf.Length)
@ -234,7 +235,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
sender.SetString(chat.NameBytes.NullTerminate());
}
message.SetString(rssb.Builder.GetViewAsSpan());
message.SetString(sb.GetViewAsSpan());
var targetChannel = chat.Type ?? this.configuration.GeneralChatType;
@ -246,6 +247,8 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
chat.Timestamp,
(byte)(chat.Silent ? 1 : 0));
}
LSeStringBuilder.SharedPool.Return(sb);
}
/// <summary>
@ -323,28 +326,29 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
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 (color is not null)
{
rssb.Builder
.PushColorType(color.Value)
.Append($"[{tag}] ")
.PopColorType();
sb.PushColorType(color.Value);
sb.Append($"[{tag}] ");
sb.PopColorType();
}
else
{
rssb.Builder.Append($"[{tag}] ");
sb.Append($"[{tag}] ");
}
}
this.Print(new XivChatEntry
{
MessageBytes = rssb.Builder.Append((ReadOnlySeStringSpan)message).ToArray(),
MessageBytes = sb.Append((ReadOnlySeStringSpan)message).ToArray(),
Type = channel,
});
LSeStringBuilder.SharedPool.Return(sb);
}
private void InventoryItemCopyDetour(InventoryItem* thisPtr, InventoryItem* otherPtr)
@ -453,8 +457,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}");
using var rssb = new RentedSeStringBuilder();
var sb = LSeStringBuilder.SharedPool.Get();
try
{
var seStringSpan = new ReadOnlySeStringSpan(linkData->Payload);
@ -462,7 +465,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
// read until link terminator
foreach (var payload in seStringSpan)
{
rssb.Builder.Append(payload);
sb.Append(payload);
if (payload.Type == ReadOnlySePayloadType.Macro &&
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)
return;
@ -492,6 +495,10 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
{
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 readonly Hook<AtkModuleVf22OpenAddonByAgentDelegate> atkModuleVf22OpenAddonByAgentHook;
private readonly Hook<AddonContextMenu.Delegates.OnMenuSelected> addonContextMenuOnMenuSelectedHook;
private readonly Hook<AddonContextMenuOnMenuSelectedDelegate> addonContextMenuOnMenuSelectedHook;
private uint? addonContextSubNameId;
@ -40,7 +40,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
{
var raptureAtkModuleVtable = (nint*)RaptureAtkModule.StaticVirtualTablePointer;
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.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 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/>
public event IContextMenu.OnMenuOpenedDelegate? OnMenuOpened;
@ -181,7 +185,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
values[0].ChangeType(ValueType.UInt);
values[0].UInt = 0;
values[1].ChangeType(ValueType.String);
values[1].SetManagedString(name.EncodeWithNullTerminator());
values[1].SetManagedString(name.Encode().NullTerminate());
values[2].ChangeType(ValueType.Int);
values[2].Int = x;
values[3].ChangeType(ValueType.Int);
@ -261,7 +265,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
submenuMask |= 1u << i;
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)
@ -291,9 +295,8 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
// 2: UInt = Return Mask (?)
// 3: UInt = Submenu Mask
// 4: UInt = OpenAtCursorPosition ? 2 : 1
// 5: UInt = ?
// 6: UInt = ?
// 7: UInt = ?
// 5: UInt = 0
// 6: UInt = 0
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)

View file

@ -150,7 +150,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
}
/// <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;
/// <inheritdoc/>

View file

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

View file

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

View file

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

View file

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

View file

@ -11,6 +11,8 @@ using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.Completion;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Text;
namespace Dalamud.Game.Internal;
/// <summary>
@ -251,14 +253,16 @@ internal sealed unsafe class DalamudCompletion : IInternalDisposableService
{
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)
.Append(command)
.PopColorType()
.GetViewAsSpan());
SeStringBuilder.SharedPool.Return(rssb);
this.Match = Utf8String.FromString(command);
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,8 @@
using System.Diagnostics;
using System.Diagnostics;
using System.IO;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
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,
// and then remove try...finally block (discard builder from the pool on exception)
var previousCulture = CultureInfo.CurrentCulture;
using var rssb = new RentedSeStringBuilder();
var builder = SeStringBuilder.SharedPool.Get();
try
{
CultureInfo.CurrentCulture = Localization.GetCultureInfoFromLangCode(lang.ToCode());
return this.EvaluateAndAppendTo(rssb.Builder, str, localParameters, lang).ToReadOnlySeString();
return this.EvaluateAndAppendTo(builder, str, localParameters, lang).ToReadOnlySeString();
}
finally
{
CultureInfo.CurrentCulture = previousCulture;
SeStringBuilder.SharedPool.Return(builder);
}
}
@ -929,8 +930,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
itemId += 1000000;
}
using var rssb = new RentedSeStringBuilder();
var sb = rssb.Builder;
var sb = SeStringBuilder.SharedPool.Get();
sb.Append(this.EvaluateFromAddon(6, [rarity], context.Language));
@ -956,6 +956,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
sb.PopLink();
text = sb.ToReadOnlySeString();
SeStringBuilder.SharedPool.Return(sb);
}
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))
return false;
using var rssb = new RentedSeStringBuilder();
var builder = SeStringBuilder.SharedPool.Get();
var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language);
if (!this.ResolveStringExpression(headContext, eStr))
return false;
var str = rssb.Builder.ToReadOnlySeString();
var pIdx = 0;
foreach (var p in str)
try
{
pIdx++;
var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
if (p.Type == ReadOnlySePayloadType.Invalid)
continue;
if (!this.ResolveStringExpression(headContext, eStr))
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));
continue;
pIdx++;
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)
@ -1061,33 +1069,40 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (!payload.TryGetExpression(out var eStr))
return false;
using var rssb = new RentedSeStringBuilder();
var builder = SeStringBuilder.SharedPool.Get();
var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language);
if (!this.ResolveStringExpression(headContext, eStr))
return false;
var str = rssb.Builder.ToReadOnlySeString();
var pIdx = 0;
foreach (var p in str)
try
{
pIdx++;
var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
if (p.Type == ReadOnlySePayloadType.Invalid)
continue;
if (!this.ResolveStringExpression(headContext, eStr))
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));
continue;
pIdx++;
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)
@ -1098,25 +1113,32 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (!eSeparator.TryGetString(out var eSeparatorVal) || !eIndex.TryGetUInt(out var eIndexVal) || eIndexVal <= 0)
return false;
using var rssb = new RentedSeStringBuilder();
var builder = SeStringBuilder.SharedPool.Get();
var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language);
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)
try
{
context.Builder.Append(splitted[eIndexVal - 1]);
return true;
}
var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
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)
@ -1124,30 +1146,37 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (!payload.TryGetExpression(out var eStr))
return false;
using var rssb = new RentedSeStringBuilder();
var builder = SeStringBuilder.SharedPool.Get();
var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language);
if (!this.ResolveStringExpression(headContext, eStr))
return false;
var str = rssb.Builder.ToReadOnlySeString();
foreach (var p in str)
try
{
if (p.Type == ReadOnlySePayloadType.Invalid)
continue;
var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
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));
continue;
if (p.Type == ReadOnlySePayloadType.Invalid)
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)
@ -1277,13 +1306,14 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (!this.dataManager.GetExcelSheet<Lumina.Excel.Sheets.Map>().TryGetRow(mapId, out var mapRow))
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)
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 mapPosY = ConvertRawToMapPosY(mapRow, rawY / 1000f);
@ -1432,22 +1462,23 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
statusDescription = statusRow.Description.AsSpan();
}
using var rssb = new RentedSeStringBuilder();
var sb = SeStringBuilder.SharedPool.Get();
switch (statusRow.StatusCategory)
{
case 1:
rssb.Builder.Append(this.EvaluateFromAddon(376, default, context.Language));
sb.Append(this.EvaluateFromAddon(376, default, context.Language));
break;
case 2:
rssb.Builder.Append(this.EvaluateFromAddon(377, default, context.Language));
sb.Append(this.EvaluateFromAddon(377, default, context.Language));
break;
}
rssb.Builder.Append(statusName);
sb.Append(statusName);
var linkText = rssb.Builder.ToReadOnlySeString();
var linkText = sb.ToReadOnlySeString();
SeStringBuilder.SharedPool.Return(sb);
context.Builder
.BeginMacro(MacroCode.Link)
@ -1702,31 +1733,38 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (!payload.TryGetExpression(out var eStr))
return false;
using var rssb = new RentedSeStringBuilder();
var builder = SeStringBuilder.SharedPool.Get();
var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language);
if (!this.ResolveStringExpression(headContext, eStr))
return false;
var str = rssb.Builder.ToReadOnlySeString();
foreach (var p in str)
try
{
if (p.Type == ReadOnlySePayloadType.Invalid)
continue;
var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
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)
@ -1796,33 +1834,40 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (!payload.TryGetExpression(out var eStr))
return false;
using var rssb = new RentedSeStringBuilder();
var builder = SeStringBuilder.SharedPool.Get();
var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language);
if (!this.ResolveStringExpression(headContext, eStr))
return false;
var str = rssb.Builder.ToReadOnlySeString();
var pIdx = 0;
foreach (var p in str)
try
{
pIdx++;
var headContext = new SeStringContext(builder, context.LocalParameters, context.Language);
if (p.Type == ReadOnlySePayloadType.Invalid)
continue;
if (!this.ResolveStringExpression(headContext, eStr))
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));
continue;
pIdx++;
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)
@ -2087,19 +2132,19 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
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(
rssb1.Builder,
SeStringBuilder.SharedPool.Get(),
strval1,
context.LocalParameters,
context.Language);
var resolvedStr2 = this.EvaluateAndAppendTo(
rssb2.Builder,
SeStringBuilder.SharedPool.Get(),
strval2,
context.LocalParameters,
context.Language);
var equals = resolvedStr1.GetViewAsSpan().SequenceEqual(resolvedStr2.GetViewAsSpan());
SeStringBuilder.SharedPool.Return(resolvedStr1);
SeStringBuilder.SharedPool.Return(resolvedStr2);
if ((ExpressionType)exprType == ExpressionType.Equal)
value = equals ? 1u : 0u;

View file

@ -3,6 +3,7 @@ using System.Globalization;
using Lumina.Text.ReadOnly;
using DSeString = Dalamud.Game.Text.SeStringHandling.SeString;
using LSeString = Lumina.Text.SeString;
namespace Dalamud.Game.Text.Evaluator;
@ -70,6 +71,9 @@ public readonly struct SeStringParameter
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(string value) => new(value);

View file

@ -9,6 +9,7 @@ using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Text.ReadOnly;
using LSeStringBuilder = Lumina.Text.SeStringBuilder;
using LSheets = Lumina.Excel.Sheets;
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));
using var rssb = new RentedSeStringBuilder();
var builder = LSeStringBuilder.SharedPool.Get();
// Ko-So-A-Do
var ksad = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(nounParams.Quantity > 1 ? 1 : 0);
if (!ksad.IsEmpty)
{
rssb.Builder.Append(ksad);
builder.Append(ksad);
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)
rssb.Builder.Append(nounParams.LinkMarker);
builder.Append(nounParams.LinkMarker);
var text = row.ReadStringColumn(nounParams.ColumnOffset);
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>
@ -197,7 +200,7 @@ internal class NounProcessor : IServiceType
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 isProperNoun = isProperNounColumn >= 0 ? row.ReadInt8Column(isProperNounColumn) : ~isProperNounColumn;
@ -213,19 +216,21 @@ internal class NounProcessor : IServiceType
var article = attributiveSheet.GetRow((uint)nounParams.ArticleType)
.ReadStringColumn(articleColumn + grammaticalNumberColumnOffset);
if (!article.IsEmpty)
rssb.Builder.Append(article);
builder.Append(article);
if (!nounParams.LinkMarker.IsEmpty)
rssb.Builder.Append(nounParams.LinkMarker);
builder.Append(nounParams.LinkMarker);
}
var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity == 1 ? SingularColumnIdx : PluralColumnIdx));
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>
@ -257,13 +262,17 @@ internal class NounProcessor : IServiceType
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)
{
rssb.Builder.Append(row.ReadStringColumn(nounParams.GrammaticalCase));
rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
return rssb.Builder.ToReadOnlySeString();
builder.Append(row.ReadStringColumn(nounParams.GrammaticalCase));
builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
ross = builder.ToReadOnlySeString();
LSeStringBuilder.SharedPool.Return(builder);
return ross;
}
var genderIndexColumn = nounParams.ColumnOffset + PronounColumnIdx;
@ -293,32 +302,35 @@ internal class NounProcessor : IServiceType
var grammaticalGender = attributiveSheet.GetRow((uint)nounParams.ArticleType)
.ReadStringColumn(caseColumnOffset + genderIndex); // Genus
if (!grammaticalGender.IsEmpty)
rssb.Builder.Append(grammaticalGender);
builder.Append(grammaticalGender);
}
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))
.ReadStringColumn(caseColumnOffset + genderIndex);
if (rssb.Builder.ContainsText("[p]"u8))
rssb.Builder.ReplaceText("[p]"u8, plural);
if (builder.ContainsText("[p]"u8))
builder.ReplaceText("[p]"u8, plural);
else
rssb.Builder.Append(plural);
builder.Append(plural);
if (hasT)
{
var article =
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?!
GermanArticleType.Possessive or GermanArticleType.Demonstrative => attributiveSheet.GetRow(25),
@ -335,10 +347,14 @@ internal class NounProcessor : IServiceType
_ => attributiveSheet.GetRow(26),
};
rssb.Builder.ReplaceText("[a]"u8, declensionRow.ReadStringColumn(caseColumnOffset + genderIndex));
rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
var declension = declensionRow.ReadStringColumn(caseColumnOffset + genderIndex);
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>
@ -369,7 +385,8 @@ internal class NounProcessor : IServiceType
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 startsWithVowel = startsWithVowelColumn >= 0
@ -388,19 +405,21 @@ internal class NounProcessor : IServiceType
{
var v21 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20);
if (!v21.IsEmpty)
rssb.Builder.Append(v21);
builder.Append(v21);
if (!nounParams.LinkMarker.IsEmpty)
rssb.Builder.Append(nounParams.LinkMarker);
builder.Append(nounParams.LinkMarker);
var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity <= 1 ? SingularColumnIdx : PluralColumnIdx));
if (!text.IsEmpty)
rssb.Builder.Append(text);
builder.Append(text);
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);
@ -409,32 +428,34 @@ internal class NounProcessor : IServiceType
var v29 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20 + 2);
if (!v29.IsEmpty)
{
rssb.Builder.Append(v29);
builder.Append(v29);
if (!nounParams.LinkMarker.IsEmpty)
rssb.Builder.Append(nounParams.LinkMarker);
builder.Append(nounParams.LinkMarker);
var text = row.ReadStringColumn(nounParams.ColumnOffset + PluralColumnIdx);
if (!text.IsEmpty)
rssb.Builder.Append(text);
builder.Append(text);
}
}
else
{
var v27 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20 + (v17 != 0 ? 1 : 3));
if (!v27.IsEmpty)
rssb.Builder.Append(v27);
builder.Append(v27);
if (!nounParams.LinkMarker.IsEmpty)
rssb.Builder.Append(nounParams.LinkMarker);
builder.Append(nounParams.LinkMarker);
var text = row.ReadStringColumn(nounParams.ColumnOffset + SingularColumnIdx);
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 Dalamud.Game.Text.Evaluator;
using Dalamud.Utility;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
@ -33,14 +32,13 @@ public class AutoTranslatePayload : Payload, ITextProvider
this.Group = group;
this.Key = key;
using var rssb = new RentedSeStringBuilder();
this.payload = rssb.Builder
.BeginMacro(MacroCode.Fixed)
.AppendUIntExpression(group - 1)
.AppendUIntExpression(key)
.EndMacro()
.ToReadOnlySeString();
var ssb = Lumina.Text.SeStringBuilder.SharedPool.Get();
this.payload = ssb.BeginMacro(MacroCode.Fixed)
.AppendUIntExpression(group - 1)
.AppendUIntExpression(key)
.EndMacro()
.ToReadOnlySeString();
Lumina.Text.SeStringBuilder.SharedPool.Return(ssb);
}
/// <summary>

View file

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

View file

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

View file

@ -113,6 +113,14 @@ public class SeString
/// <returns>Equivalent SeString.</returns>
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>
/// Parse a binary game message into an SeString.
/// </summary>
@ -198,9 +206,8 @@ public class SeString
var textColor = ItemUtil.GetItemRarityColorType(rawId);
var textEdgeColor = textColor + 1u;
using var rssb = new RentedSeStringBuilder();
var itemLink = rssb.Builder
var sb = LSeStringBuilder.SharedPool.Get();
var itemLink = sb
.PushColorType(textColor)
.PushEdgeColorType(textEdgeColor)
.PushLinkItem(rawId, copyName)
@ -209,6 +216,7 @@ public class SeString
.PopEdgeColorType()
.PopColorType()
.ToReadOnlySeString();
LSeStringBuilder.SharedPool.Return(sb);
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 nameString = GetMapLinkNameString(mapPayload.PlaceName, instance, mapPayload.CoordinateString);
return new SeString(new List<Payload>([
var payloads = new List<Payload>(new Payload[]
{
mapPayload,
..TextArrowPayloads,
// arrow goes here
new TextPayload(nameString),
RawPayload.LinkTerminator,
]));
});
payloads.InsertRange(1, TextArrowPayloads);
return new SeString(payloads);
}
/// <summary>
@ -286,12 +298,16 @@ public class SeString
var mapPayload = new MapLinkPayload(territoryId, mapId, xCoord, yCoord, fudgeFactor);
var nameString = GetMapLinkNameString(mapPayload.PlaceName, instance, mapPayload.CoordinateString);
return new SeString(new List<Payload>([
var payloads = new List<Payload>(new Payload[]
{
mapPayload,
..TextArrowPayloads,
// arrow goes here
new TextPayload(nameString),
RawPayload.LinkTerminator,
]));
});
payloads.InsertRange(1, TextArrowPayloads);
return new SeString(payloads);
}
/// <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>
public static SeString CreatePartyFinderLink(uint listingId, string recruiterName, bool isCrossWorld = false)
{
var clientState = Service<ClientState.ClientState>.Get();
var seStringEvaluator = Service<SeStringEvaluator>.Get();
return new SeString(new List<Payload>([
var payloads = new List<Payload>()
{
new PartyFinderPayload(listingId, isCrossWorld ? PartyFinderPayload.PartyFinderLinkType.NotSpecified : PartyFinderPayload.PartyFinderLinkType.LimitedToHomeWorld),
..TextArrowPayloads,
..SeString.Parse(seStringEvaluator.EvaluateFromAddon(2265, [recruiterName, isCrossWorld ? 0 : 1], clientState.ClientLanguage)).Payloads,
RawPayload.LinkTerminator
]));
// ->
new TextPayload($"Looking for Party ({recruiterName})" + (isCrossWorld ? " " : string.Empty)),
};
payloads.InsertRange(1, TextArrowPayloads);
if (isCrossWorld)
payloads.Add(new IconPayload(BitmapFontIcon.CrossWorld));
payloads.Add(RawPayload.LinkTerminator);
return new SeString(payloads);
}
/// <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>
public static SeString CreatePartyFinderSearchConditionsLink(string message)
{
return new SeString(new List<Payload>([
var payloads = new List<Payload>()
{
new PartyFinderPayload(),
..TextArrowPayloads,
// ->
new TextPayload(message),
RawPayload.LinkTerminator
]));
};
payloads.InsertRange(1, TextArrowPayloads);
payloads.Add(RawPayload.LinkTerminator);
return new SeString(payloads);
}
/// <summary>

View file

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

View file

@ -158,23 +158,67 @@ internal unsafe class RecipeData : IInternalDisposableService
{
noteBookDivisionIndex++;
if (!noteBookDivisionRow.AllowedCraftTypes[craftType])
continue;
// For future Lumina.Excel update, replace with:
// 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)
continue;
if (noteBookDivisionRow.RequiresSecretRecipeBookGroupUnlock)
// For future Lumina.Excel update, replace with:
// if (notebookDivisionRow.RequiresSecretRecipeBookGroupUnlock)
if (noteBookDivisionRow.Unknown1)
{
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;
var bitIndex = secretRecipeBookGroup.Value.SecretRecipeBook[craftType].RowId;
if (PlayerState.Instance()->UnlockedSecretRecipeBooksBitArray.Get((int)bitIndex))
if (!this.dataManager.GetExcelSheet<SecretRecipeBookGroup>().TryGetRow(secretRecipeBookGroupRowId, out var secretRecipeBookGroupRow))
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;
break;

View file

@ -209,7 +209,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
/// <inheritdoc/>
public bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row)
{
return this.IsUnlockLinkUnlocked(row.UnlockLink);
return this.IsUnlockLinkUnlocked(row.Unknown26);
}
/// <inheritdoc/>
@ -217,7 +217,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
{
return this.dataManager.GetExcelSheet<EmjVoiceNpc>().TryGetRow(row.RowId, out var emjVoiceNpcRow)
&& this.IsEmjVoiceNpcUnlocked(emjVoiceNpcRow)
&& QuestManager.IsQuestComplete(row.UnlockQuest.RowId);
&& QuestManager.IsQuestComplete(row.Unknown1);
}
/// <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
// 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]);
case ItemActionAction.BuddyEquip:
case ItemActionType.BuddyEquip:
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]);
case ItemActionAction.SecretRecipeBook:
case ItemActionType.SecretRecipeBook:
return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0]);
case ItemActionAction.UnlockLink:
case ItemActionAction.OccultRecords:
case ItemActionType.UnlockLink:
case ItemActionType.OccultRecords:
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);
case ItemActionAction.FolkloreTome:
case ItemActionType.FolkloreTome:
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);
case ItemActionAction.FramersKit:
case ItemActionType.FramersKit:
return PlayerState.Instance()->IsFramersKitUnlocked(row.AdditionalData.RowId);
case ItemActionAction.Ornament:
case ItemActionType.Ornament:
return PlayerState.Instance()->IsOrnamentUnlocked(row.ItemAction.Value.Data[0]);
case ItemActionAction.Glasses:
case ItemActionType.Glasses:
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];
return supportJobId < occultCrescentState->SupportJobLevels.Length && occultCrescentState->SupportJobLevels[supportJobId] != 0;
case ItemActionAction.CompanySealVouchers:
case ItemActionType.CompanySealVouchers:
return false;
}
@ -327,7 +327,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
/// <inheritdoc/>
public bool IsMKDLoreUnlocked(MKDLore row)
{
return this.IsUnlockLinkUnlocked(row.UnlockLink);
return this.IsUnlockLinkUnlocked(row.Unknown2);
}
/// <inheritdoc/>
@ -414,20 +414,20 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
if (row.ItemAction.RowId == 0)
return false;
return (ItemActionAction)row.ItemAction.Value.Action.RowId
is ItemActionAction.Companion
or ItemActionAction.BuddyEquip
or ItemActionAction.Mount
or ItemActionAction.SecretRecipeBook
or ItemActionAction.UnlockLink
or ItemActionAction.TripleTriadCard
or ItemActionAction.FolkloreTome
or ItemActionAction.OrchestrionRoll
or ItemActionAction.FramersKit
or ItemActionAction.Ornament
or ItemActionAction.Glasses
or ItemActionAction.OccultRecords
or ItemActionAction.SoulShards;
return (ItemActionType)row.ItemAction.Value.Type
is ItemActionType.Companion
or ItemActionType.BuddyEquip
or ItemActionType.Mount
or ItemActionType.SecretRecipeBook
or ItemActionType.UnlockLink
or ItemActionType.TripleTriadCard
or ItemActionType.FolkloreTome
or ItemActionType.OrchestrionRoll
or ItemActionType.FramersKit
or ItemActionType.Ornament
or ItemActionType.Glasses
or ItemActionType.OccultRecords
or ItemActionType.SoulShards;
}
/// <inheritdoc/>

View file

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

View file

@ -56,11 +56,6 @@ public enum SettingsOpenKind
/// </summary>
ServerInfoBar,
/// <summary>
/// Open to the "Badges" page.
/// </summary>
Badge,
/// <summary>
/// Open to the "Experimental" page.
/// </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);
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);
fn->GetString(i, buf, maxStrLen).ThrowOnError();
fn->GetString(i, (ushort*)buf, maxStrLen).ThrowOnError();
var value = new string(buf);
result[key.ToLowerInvariant()] = value;
}

View file

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

View file

@ -113,8 +113,8 @@ public sealed class SystemFontId : IFontId
var familyIndex = 0u;
BOOL exists = false;
fixed (char* name = this.Family.EnglishName)
sfc.Get()->FindFamilyName(name, &familyIndex, &exists).ThrowOnError();
fixed (void* name = this.Family.EnglishName)
sfc.Get()->FindFamilyName((ushort*)name, &familyIndex, &exists).ThrowOnError();
if (!exists)
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();
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());
}

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