Merge master

This commit is contained in:
goaaats 2025-04-03 21:14:12 +02:00
commit 2951dc93ec
413 changed files with 36477 additions and 6572 deletions

View file

@ -6,6 +6,7 @@ charset = utf-8
end_of_line = lf end_of_line = lf
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true
# 4 space indentation # 4 space indentation
indent_style = space indent_style = space

View file

@ -1,5 +1,6 @@
name: Build Dalamud name: Build Dalamud
on: [push, pull_request, workflow_dispatch] on: [push, pull_request, workflow_dispatch]
concurrency: concurrency:
group: build_dalamud_${{ github.ref_name }} group: build_dalamud_${{ github.ref_name }}
cancel-in-progress: true cancel-in-progress: true
@ -22,7 +23,7 @@ jobs:
uses: microsoft/setup-msbuild@v1.0.2 uses: microsoft/setup-msbuild@v1.0.2
- uses: actions/setup-dotnet@v3 - uses: actions/setup-dotnet@v3
with: with:
dotnet-version: '8.0.100' dotnet-version: '9.0.200'
- name: Define VERSION - name: Define VERSION
run: | run: |
$env:COMMIT = $env:GITHUB_SHA.Substring(0, 7) $env:COMMIT = $env:GITHUB_SHA.Substring(0, 7)
@ -32,10 +33,8 @@ jobs:
($env:REPO_NAME) >> VERSION ($env:REPO_NAME) >> VERSION
($env:BRANCH) >> VERSION ($env:BRANCH) >> VERSION
($env:COMMIT) >> VERSION ($env:COMMIT) >> VERSION
- name: Build Dalamud - name: Build and Test Dalamud
run: .\build.ps1 compile run: .\build.ps1 ci
- name: Test Dalamud
run: .\build.ps1 test
- name: Sign Dalamud - name: Sign Dalamud
if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }} if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }}
env: env:
@ -55,8 +54,9 @@ jobs:
bin/Release/Dalamud.*.dll bin/Release/Dalamud.*.dll
bin/Release/Dalamud.*.exe bin/Release/Dalamud.*.exe
bin/Release/FFXIVClientStructs.dll bin/Release/FFXIVClientStructs.dll
bin/Release/cim*.dll
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v4
with: with:
name: dalamud-artifact name: dalamud-artifact
path: bin\Release path: bin\Release
@ -75,7 +75,7 @@ jobs:
run: | run: |
dotnet tool install -g Microsoft.DotNet.ApiCompat.Tool dotnet tool install -g Microsoft.DotNet.ApiCompat.Tool
- name: "Download Proposed Artifacts" - name: "Download Proposed Artifacts"
uses: actions/download-artifact@v2 uses: actions/download-artifact@v4.1.7
with: with:
name: dalamud-artifact name: dalamud-artifact
path: .\right path: .\right
@ -86,9 +86,9 @@ jobs:
- name: "Verify Compatibility" - name: "Verify Compatibility"
run: | run: |
$FILES_TO_VALIDATE = "Dalamud.dll","FFXIVClientStructs.dll","Lumina.dll","Lumina.Excel.dll" $FILES_TO_VALIDATE = "Dalamud.dll","FFXIVClientStructs.dll","Lumina.dll","Lumina.Excel.dll"
$retcode = 0 $retcode = 0
foreach ($file in $FILES_TO_VALIDATE) { foreach ($file in $FILES_TO_VALIDATE) {
$testout = "" $testout = ""
Write-Output "::group::=== API COMPATIBILITY CHECK: ${file} ===" Write-Output "::group::=== API COMPATIBILITY CHECK: ${file} ==="
@ -99,7 +99,7 @@ jobs:
$retcode = 1 $retcode = 1
} }
} }
exit $retcode exit $retcode
deploy_stg: deploy_stg:
@ -112,7 +112,7 @@ jobs:
with: with:
repository: goatcorp/dalamud-distrib repository: goatcorp/dalamud-distrib
token: ${{ secrets.UPDATE_PAT }} token: ${{ secrets.UPDATE_PAT }}
- uses: actions/download-artifact@v2 - uses: actions/download-artifact@v4.1.7
with: with:
name: dalamud-artifact name: dalamud-artifact
path: .\scratch path: .\scratch
@ -128,18 +128,18 @@ jobs:
GH_BRANCH: ${{ steps.extract_branch.outputs.branch }} GH_BRANCH: ${{ steps.extract_branch.outputs.branch }}
run: | run: |
Compress-Archive .\scratch\* .\canary.zip # Recreate the release zip Compress-Archive .\scratch\* .\canary.zip # Recreate the release zip
$branchName = $env:GH_BRANCH $branchName = $env:GH_BRANCH
if ($branchName -eq "master") { if ($branchName -eq "master") {
$branchName = "stg" $branchName = "stg"
} }
$newVersion = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\TEMP_gitver.txt") $newVersion = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\TEMP_gitver.txt")
$revision = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\revision.txt") $revision = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\revision.txt")
$commitHash = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\commit_hash.txt") $commitHash = [System.IO.File]::ReadAllText("$(Get-Location)\scratch\commit_hash.txt")
Remove-Item -Force -Recurse .\scratch Remove-Item -Force -Recurse .\scratch
if (Test-Path -Path $branchName) { if (Test-Path -Path $branchName) {
$versionData = Get-Content ".\${branchName}\version" | ConvertFrom-Json $versionData = Get-Content ".\${branchName}\version" | ConvertFrom-Json
$oldVersion = $versionData.AssemblyVersion $oldVersion = $versionData.AssemblyVersion
@ -158,7 +158,7 @@ jobs:
Write-Host "Deployment folder doesn't exist. Not doing anything." Write-Host "Deployment folder doesn't exist. Not doing anything."
Remove-Item .\canary.zip Remove-Item .\canary.zip
} }
- name: Commit changes - name: Commit changes
shell: bash shell: bash
env: env:
@ -166,8 +166,8 @@ jobs:
run: | run: |
git config --global user.name "Actions User" git config --global user.name "Actions User"
git config --global user.email "actions@github.com" git config --global user.email "actions@github.com"
git add . git add .
git commit -m "[CI] Update staging for ${DVER} on ${GH_BRANCH}" || true git commit -m "[CI] Update staging for ${DVER} on ${GH_BRANCH}" || true
git push origin main || true git push origin main || true

View file

@ -1,8 +1,8 @@
name: Rollup changes to next version name: Rollup changes to next version
on: on:
push: # push:
branches: # branches:
- master # - master
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@ -11,7 +11,7 @@ jobs:
strategy: strategy:
matrix: matrix:
branches: branches:
- WORKFLOW_DISABLED_REMOVE_BEFORE_RUNNING - net9
defaults: defaults:
run: run:

9
.gitmodules vendored
View file

@ -10,3 +10,12 @@
[submodule "lib/ImGui.NET"] [submodule "lib/ImGui.NET"]
path = lib/ImGui.NET path = lib/ImGui.NET
url = https://github.com/goatcorp/ImGui.NET.git url = https://github.com/goatcorp/ImGui.NET.git
[submodule "lib/cimgui"]
path = lib/cimgui
url = https://github.com/goatcorp/gc-cimgui
[submodule "lib/cimplot"]
path = lib/cimplot
url = https://github.com/goatcorp/gc-cimplot
[submodule "lib/cimguizmo"]
path = lib/cimguizmo
url = https://github.com/goatcorp/gc-cimguizmo

View file

@ -76,14 +76,20 @@
"items": { "items": {
"type": "string", "type": "string",
"enum": [ "enum": [
"CI",
"Clean", "Clean",
"Compile", "Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud", "CompileDalamud",
"CompileDalamudBoot", "CompileDalamudBoot",
"CompileDalamudCrashHandler", "CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector", "CompileInjector",
"CompileInjectorBoot", "CompileInjectorBoot",
"Restore", "Restore",
"SetCILogging",
"Test" "Test"
] ]
} }
@ -98,14 +104,20 @@
"items": { "items": {
"type": "string", "type": "string",
"enum": [ "enum": [
"CI",
"Clean", "Clean",
"Compile", "Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud", "CompileDalamud",
"CompileDalamudBoot", "CompileDalamudBoot",
"CompileDalamudCrashHandler", "CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector", "CompileInjector",
"CompileInjectorBoot", "CompileInjectorBoot",
"Restore", "Restore",
"SetCILogging",
"Test" "Test"
] ]
} }

View file

@ -28,7 +28,7 @@
<PlatformToolset>v143</PlatformToolset> <PlatformToolset>v143</PlatformToolset>
<LinkIncremental>false</LinkIncremental> <LinkIncremental>false</LinkIncremental>
<CharacterSet>Unicode</CharacterSet> <CharacterSet>Unicode</CharacterSet>
<OutDir>..\bin\$(Configuration)\</OutDir> <OutDir>bin\$(Configuration)\</OutDir>
<IntDir>obj\$(Configuration)\</IntDir> <IntDir>obj\$(Configuration)\</IntDir>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
@ -200,8 +200,10 @@
<ItemGroup> <ItemGroup>
<Manifest Include="themes.manifest" /> <Manifest Include="themes.manifest" />
</ItemGroup> </ItemGroup>
<Target Name="RemoveExtraFiles" AfterTargets="PostBuildEvent"> <Target Name="CopyOutputDlls" AfterTargets="PostBuildEvent">
<Delete Files="$(OutDir)$(TargetName).lib" /> <Copy SourceFiles="$(OutDir)$(TargetName).dll" DestinationFolder="..\bin\$(Configuration)\" />
<Delete Files="$(OutDir)$(TargetName).exp" /> <Copy SourceFiles="$(OutDir)$(TargetName).pdb" DestinationFolder="..\bin\$(Configuration)\" />
<Copy SourceFiles="$(OutDir)nethost.dll" DestinationFolder="..\bin\$(Configuration)\" />
</Target> </Target>
</Project> </Project>

View file

@ -109,6 +109,7 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory); config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory);
config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory); config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory);
config.Language = json.value("Language", config.Language); config.Language = json.value("Language", config.Language);
config.Platform = json.value("Platform", config.Platform);
config.GameVersion = json.value("GameVersion", config.GameVersion); config.GameVersion = json.value("GameVersion", config.GameVersion);
config.TroubleshootingPackData = json.value("TroubleshootingPackData", std::string{}); config.TroubleshootingPackData = json.value("TroubleshootingPackData", std::string{});
config.DelayInitializeMs = json.value("DelayInitializeMs", config.DelayInitializeMs); config.DelayInitializeMs = json.value("DelayInitializeMs", config.DelayInitializeMs);

View file

@ -17,7 +17,7 @@ struct DalamudStartInfo {
DirectHook = 1, DirectHook = 1,
}; };
friend void from_json(const nlohmann::json&, DotNetOpenProcessHookMode&); friend void from_json(const nlohmann::json&, DotNetOpenProcessHookMode&);
enum class ClientLanguage : int { enum class ClientLanguage : int {
Japanese, Japanese,
English, English,
@ -47,6 +47,7 @@ struct DalamudStartInfo {
std::string PluginDirectory; std::string PluginDirectory;
std::string AssetDirectory; std::string AssetDirectory;
ClientLanguage Language = ClientLanguage::English; ClientLanguage Language = ClientLanguage::English;
std::string Platform;
std::string GameVersion; std::string GameVersion;
std::string TroubleshootingPackData; std::string TroubleshootingPackData;
int DelayInitializeMs = 0; int DelayInitializeMs = 0;

View file

@ -15,7 +15,7 @@ HINSTANCE g_hGameInstance = GetModuleHandleW(nullptr);
HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
g_startInfo.from_envvars(); g_startInfo.from_envvars();
std::string jsonParseError; std::string jsonParseError;
try { try {
from_json(nlohmann::json::parse(std::string_view(static_cast<char*>(lpParam))), g_startInfo); from_json(nlohmann::json::parse(std::string_view(static_cast<char*>(lpParam))), g_startInfo);
@ -25,7 +25,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
if (g_startInfo.BootShowConsole) if (g_startInfo.BootShowConsole)
ConsoleSetup(L"Dalamud Boot"); ConsoleSetup(L"Dalamud Boot");
logging::update_dll_load_status(true); logging::update_dll_load_status(true);
const auto logFilePath = unicode::convert<std::wstring>(g_startInfo.BootLogPath); const auto logFilePath = unicode::convert<std::wstring>(g_startInfo.BootLogPath);
@ -33,16 +33,16 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
auto attemptFallbackLog = false; auto attemptFallbackLog = false;
if (logFilePath.empty()) { if (logFilePath.empty()) {
attemptFallbackLog = true; attemptFallbackLog = true;
logging::I("No log file path given; not logging to file."); logging::I("No log file path given; not logging to file.");
} else { } else {
try { try {
logging::start_file_logging(logFilePath, !g_startInfo.BootShowConsole); logging::start_file_logging(logFilePath, !g_startInfo.BootShowConsole);
logging::I("Logging to file: {}", logFilePath); logging::I("Logging to file: {}", logFilePath);
} catch (const std::exception& e) { } catch (const std::exception& e) {
attemptFallbackLog = true; attemptFallbackLog = true;
logging::E("Couldn't open log file: {}", logFilePath); logging::E("Couldn't open log file: {}", logFilePath);
logging::E("Error: {} / {}", errno, e.what()); logging::E("Error: {} / {}", errno, e.what());
} }
@ -63,15 +63,15 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
SYSTEMTIME st; SYSTEMTIME st;
GetLocalTime(&st); GetLocalTime(&st);
logFilePath += std::format(L"Dalamud.Boot.{:04}{:02}{:02}.{:02}{:02}{:02}.{:03}.{}.log", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, GetCurrentProcessId()); logFilePath += std::format(L"Dalamud.Boot.{:04}{:02}{:02}.{:02}{:02}{:02}.{:03}.{}.log", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, GetCurrentProcessId());
try { try {
logging::start_file_logging(logFilePath, !g_startInfo.BootShowConsole); logging::start_file_logging(logFilePath, !g_startInfo.BootShowConsole);
logging::I("Logging to fallback log file: {}", logFilePath); logging::I("Logging to fallback log file: {}", logFilePath);
} catch (const std::exception& e) { } catch (const std::exception& e) {
if (!g_startInfo.BootShowConsole && !g_startInfo.BootDisableFallbackConsole) if (!g_startInfo.BootShowConsole && !g_startInfo.BootDisableFallbackConsole)
ConsoleSetup(L"Dalamud Boot - Fallback Console"); ConsoleSetup(L"Dalamud Boot - Fallback Console");
logging::E("Couldn't open fallback log file: {}", logFilePath); logging::E("Couldn't open fallback log file: {}", logFilePath);
logging::E("Error: {} / {}", errno, e.what()); logging::E("Error: {} / {}", errno, e.what());
} }
@ -87,7 +87,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
} else { } else {
logging::E("Failed to initialize MinHook (status={}({}))", MH_StatusToString(mhStatus), static_cast<int>(mhStatus)); logging::E("Failed to initialize MinHook (status={}({}))", MH_StatusToString(mhStatus), static_cast<int>(mhStatus));
} }
logging::I("Dalamud.Boot Injectable, (c) 2021 XIVLauncher Contributors"); logging::I("Dalamud.Boot Injectable, (c) 2021 XIVLauncher Contributors");
logging::I("Built at: " __DATE__ "@" __TIME__); logging::I("Built at: " __DATE__ "@" __TIME__);
@ -241,11 +241,11 @@ BOOL APIENTRY DllMain(const HMODULE hModule, const DWORD dwReason, LPVOID lpRese
case DLL_PROCESS_DETACH: case DLL_PROCESS_DETACH:
// do not show debug message boxes on abort() here // do not show debug message boxes on abort() here
_set_abort_behavior(0, _WRITE_ABORT_MSG); _set_abort_behavior(0, _WRITE_ABORT_MSG);
// process is terminating; don't bother cleaning up // process is terminating; don't bother cleaning up
if (lpReserved) if (lpReserved)
return TRUE; return TRUE;
logging::update_dll_load_status(false); logging::update_dll_load_status(false);
xivfixes::apply_all(false); xivfixes::apply_all(false);

View file

@ -1,4 +1,5 @@
#include "pch.h" #include "pch.h"
#include "DalamudStartInfo.h"
#include "utils.h" #include "utils.h"
@ -103,7 +104,7 @@ bool utils::loaded_module::find_imported_function_pointer(const char* pcszDllNam
ppFunctionAddress = nullptr; ppFunctionAddress = nullptr;
// This span might be too long in terms of meaningful data; it only serves to prevent accessing memory outsides boundaries. // This span might be too long in terms of meaningful data; it only serves to prevent accessing memory outsides boundaries.
for (const auto& importDescriptor : span_as<IMAGE_IMPORT_DESCRIPTOR>(directory.VirtualAddress, directory.Size / sizeof IMAGE_IMPORT_DESCRIPTOR)) { for (const auto& importDescriptor : span_as<IMAGE_IMPORT_DESCRIPTOR>(directory.VirtualAddress, directory.Size / sizeof(IMAGE_IMPORT_DESCRIPTOR))) {
// Having all zero values signals the end of the table. We didn't find anything. // Having all zero values signals the end of the table. We didn't find anything.
if (!importDescriptor.OriginalFirstThunk && !importDescriptor.TimeDateStamp && !importDescriptor.ForwarderChain && !importDescriptor.FirstThunk) if (!importDescriptor.OriginalFirstThunk && !importDescriptor.TimeDateStamp && !importDescriptor.ForwarderChain && !importDescriptor.FirstThunk)
@ -584,6 +585,10 @@ std::vector<std::string> utils::get_env_list(const wchar_t* pcszName) {
return res; return res;
} }
bool utils::is_running_on_wine() {
return g_startInfo.Platform != "WINDOWS";
}
std::filesystem::path utils::get_module_path(HMODULE hModule) { std::filesystem::path utils::get_module_path(HMODULE hModule) {
std::wstring buf(MAX_PATH, L'\0'); std::wstring buf(MAX_PATH, L'\0');
while (true) { while (true) {

View file

@ -121,7 +121,7 @@ namespace utils {
memory_tenderizer(const void* pAddress, size_t length, DWORD dwNewProtect); memory_tenderizer(const void* pAddress, size_t length, DWORD dwNewProtect);
template<typename T, typename = std::enable_if_t<std::is_trivial_v<T>&& std::is_standard_layout_v<T>>> template<typename T, typename = std::enable_if_t<std::is_trivial_v<T>&& std::is_standard_layout_v<T>>>
memory_tenderizer(const T& object, DWORD dwNewProtect) : memory_tenderizer(&object, sizeof T, dwNewProtect) {} memory_tenderizer(const T& object, DWORD dwNewProtect) : memory_tenderizer(&object, sizeof(T), dwNewProtect) {}
template<typename T> template<typename T>
memory_tenderizer(std::span<const T> s, DWORD dwNewProtect) : memory_tenderizer(&s[0], s.size(), dwNewProtect) {} memory_tenderizer(std::span<const T> s, DWORD dwNewProtect) : memory_tenderizer(&s[0], s.size(), dwNewProtect) {}
@ -267,6 +267,8 @@ namespace utils {
return get_env_list<T>(unicode::convert<std::wstring>(pcszName).c_str()); return get_env_list<T>(unicode::convert<std::wstring>(pcszName).c_str());
} }
bool is_running_on_wine();
std::filesystem::path get_module_path(HMODULE hModule); std::filesystem::path get_module_path(HMODULE hModule);
/// @brief Find the game main window. /// @brief Find the game main window.

View file

@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View file

@ -1,3 +1,4 @@
using System.Runtime.InteropServices;
using Dalamud.Common.Game; using Dalamud.Common.Game;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -15,7 +16,7 @@ public record DalamudStartInfo
/// </summary> /// </summary>
public DalamudStartInfo() public DalamudStartInfo()
{ {
// ignored this.Platform = OSPlatform.Create("UNKNOWN");
} }
/// <summary> /// <summary>
@ -58,6 +59,12 @@ public record DalamudStartInfo
/// </summary> /// </summary>
public ClientLanguage Language { get; set; } = ClientLanguage.English; public ClientLanguage Language { get; set; } = ClientLanguage.English;
/// <summary>
/// Gets or sets the underlying platform<72>Dalamud runs on.
/// </summary>
[JsonConverter(typeof(OSPlatformConverter))]
public OSPlatform Platform { get; set; }
/// <summary> /// <summary>
/// Gets or sets the current game version code. /// Gets or sets the current game version code.
/// </summary> /// </summary>
@ -125,7 +132,7 @@ public record DalamudStartInfo
public bool BootVehFull { get; set; } public bool BootVehFull { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not ETW should be enabled. /// Gets or sets a value indicating whether ETW should be enabled.
/// </summary> /// </summary>
public bool BootEnableEtw { get; set; } public bool BootEnableEtw { get; set; }

View file

@ -0,0 +1,78 @@
using System.Runtime.InteropServices;
using Newtonsoft.Json;
namespace Dalamud.Common;
/// <summary>
/// Converts a <see cref="OSPlatform"/> to and from a string (e.g. <c>"FreeBSD"</c>).
/// </summary>
public sealed class OSPlatformConverter : JsonConverter
{
/// <summary>
/// Writes the JSON representation of the object.
/// </summary>
/// <param name="writer">The <see cref="JsonWriter"/> to write to.</param>
/// <param name="value">The value.</param>
/// <param name="serializer">The calling serializer.</param>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
}
else if (value is OSPlatform)
{
writer.WriteValue(value.ToString());
}
else
{
throw new JsonSerializationException("Expected OSPlatform object value");
}
}
/// <summary>
/// Reads the JSON representation of the object.
/// </summary>
/// <param name="reader">The <see cref="JsonReader"/> to read from.</param>
/// <param name="objectType">Type of the object.</param>
/// <param name="existingValue">The existing property value of the JSON that is being converted.</param>
/// <param name="serializer">The calling serializer.</param>
/// <returns>The object value.</returns>
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}
else
{
if (reader.TokenType == JsonToken.String)
{
try
{
return OSPlatform.Create((string)reader.Value!);
}
catch (Exception ex)
{
throw new JsonSerializationException($"Error parsing OSPlatform string: {reader.Value}", ex);
}
}
else
{
throw new JsonSerializationException($"Unexpected token or value when parsing OSPlatform. Token: {reader.TokenType}, Value: {reader.Value}");
}
}
}
/// <summary>
/// Determines whether this instance can convert the specified object type.
/// </summary>
/// <param name="objectType">Type of the object.</param>
/// <returns>
/// <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
/// </returns>
public override bool CanConvert(Type objectType)
{
return objectType == typeof(OSPlatform);
}
}

View file

@ -0,0 +1,18 @@
using System.Diagnostics.CodeAnalysis;
namespace Dalamud.Common.Util;
public static class EnvironmentUtils
{
/// <summary>
/// Attempts to get an environment variable using the Try pattern.
/// </summary>
/// <param name="variableName">The env var to get.</param>
/// <param name="value">An output containing the env var, if present.</param>
/// <returns>A boolean indicating whether the var was present.</returns>
public static bool TryGetEnvironmentVariable(string variableName, [NotNullWhen(true)] out string? value)
{
value = Environment.GetEnvironmentVariable(variableName);
return value != null;
}
}

View file

@ -1,9 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<AssemblyName>Dalamud.CorePlugin</AssemblyName> <AssemblyName>Dalamud.CorePlugin</AssemblyName>
<TargetFramework>net8.0-windows</TargetFramework>
<Platforms>x64</Platforms> <Platforms>x64</Platforms>
<LangVersion>10.0</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly> <ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
@ -27,9 +25,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Lumina" Version="4.1.0" /> <PackageReference Include="Lumina" Version="$(LuminaVersion)" />
<PackageReference Include="Lumina.Excel" Version="7.0.1" /> <PackageReference Include="Lumina.Excel" Version="$(LuminaExcelVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333"> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

@ -1,11 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Target"> <PropertyGroup Label="Target">
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PlatformTarget>x64</PlatformTarget>
<Platforms>x64;AnyCPU</Platforms>
<LangVersion>10.0</LangVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Feature"> <PropertyGroup Label="Feature">
@ -45,10 +41,6 @@
<PropertyGroup Condition="'$(Configuration)'=='Debug'"> <PropertyGroup Condition="'$(Configuration)'=='Debug'">
<DefineConstants>DEBUG;TRACE</DefineConstants> <DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<AppOutputBase>$(MSBuildProjectDirectory)\</AppOutputBase>
<PathMap>$(AppOutputBase)=C:\goatsoft\companysecrets\injector\</PathMap>
</PropertyGroup>
<PropertyGroup Label="Warnings"> <PropertyGroup Label="Warnings">
<NoWarn>IDE0003;IDE0044;IDE1006;CS1591;CS1701;CS1702</NoWarn> <NoWarn>IDE0003;IDE0044;IDE1006;CS1591;CS1701;CS1702</NoWarn>

View file

@ -11,6 +11,8 @@ using System.Text.RegularExpressions;
using Dalamud.Common; using Dalamud.Common;
using Dalamud.Common.Game; using Dalamud.Common.Game;
using Dalamud.Common.Util;
using Newtonsoft.Json; using Newtonsoft.Json;
using Reloaded.Memory.Buffers; using Reloaded.Memory.Buffers;
using Serilog; using Serilog;
@ -95,6 +97,7 @@ namespace Dalamud.Injector
args.Remove("--msgbox2"); args.Remove("--msgbox2");
args.Remove("--msgbox3"); args.Remove("--msgbox3");
args.Remove("--etw"); args.Remove("--etw");
args.Remove("--no-legacy-corrupted-state-exceptions");
args.Remove("--veh"); args.Remove("--veh");
args.Remove("--veh-full"); args.Remove("--veh-full");
args.Remove("--no-plugin"); args.Remove("--no-plugin");
@ -263,6 +266,35 @@ namespace Dalamud.Injector
} }
} }
private static OSPlatform DetectPlatformHeuristic()
{
var ntdll = NativeFunctions.GetModuleHandleW("ntdll.dll");
var wineServerCallPtr = NativeFunctions.GetProcAddress(ntdll, "wine_server_call");
var wineGetHostVersionPtr = NativeFunctions.GetProcAddress(ntdll, "wine_get_host_version");
var winePlatform = GetWinePlatform(wineGetHostVersionPtr);
var isWine = wineServerCallPtr != nint.Zero;
static unsafe string? GetWinePlatform(nint wineGetHostVersionPtr)
{
if (wineGetHostVersionPtr == nint.Zero) return null;
var methodDelegate = (delegate* unmanaged[Cdecl]<out char*, out char*, void>)wineGetHostVersionPtr;
methodDelegate(out var platformPtr, out var _);
if (platformPtr == null) return null;
return Marshal.PtrToStringAnsi((nint)platformPtr);
}
if (!isWine)
return OSPlatform.Windows;
if (winePlatform == "Darwin")
return OSPlatform.OSX;
return OSPlatform.Linux;
}
private static DalamudStartInfo ExtractAndInitializeStartInfoFromArguments(DalamudStartInfo? startInfo, List<string> args) private static DalamudStartInfo ExtractAndInitializeStartInfoFromArguments(DalamudStartInfo? startInfo, List<string> args)
{ {
int len; int len;
@ -278,9 +310,14 @@ namespace Dalamud.Injector
var logName = startInfo.LogName; var logName = startInfo.LogName;
var logPath = startInfo.LogPath; var logPath = startInfo.LogPath;
var languageStr = startInfo.Language.ToString().ToLowerInvariant(); var languageStr = startInfo.Language.ToString().ToLowerInvariant();
var platformStr = startInfo.Platform.ToString().ToLowerInvariant();
var unhandledExceptionStr = startInfo.UnhandledException.ToString().ToLowerInvariant(); var unhandledExceptionStr = startInfo.UnhandledException.ToString().ToLowerInvariant();
var troubleshootingData = "{\"empty\": true, \"description\": \"No troubleshooting data supplied.\"}"; var troubleshootingData = "{\"empty\": true, \"description\": \"No troubleshooting data supplied.\"}";
// env vars are brought in prior to launch args, since args can override them.
if (EnvironmentUtils.TryGetEnvironmentVariable("XL_PLATFORM", out var xlPlatformEnv))
platformStr = xlPlatformEnv.ToLowerInvariant();
for (var i = 2; i < args.Count; i++) for (var i = 2; i < args.Count; i++)
{ {
if (args[i].StartsWith(key = "--dalamud-working-directory=")) if (args[i].StartsWith(key = "--dalamud-working-directory="))
@ -307,6 +344,10 @@ namespace Dalamud.Injector
{ {
languageStr = args[i][key.Length..].ToLowerInvariant(); languageStr = args[i][key.Length..].ToLowerInvariant();
} }
else if (args[i].StartsWith(key = "--dalamud-platform="))
{
platformStr = args[i][key.Length..].ToLowerInvariant();
}
else if (args[i].StartsWith(key = "--dalamud-tspack-b64=")) else if (args[i].StartsWith(key = "--dalamud-tspack-b64="))
{ {
troubleshootingData = Encoding.UTF8.GetString(Convert.FromBase64String(args[i][key.Length..])); troubleshootingData = Encoding.UTF8.GetString(Convert.FromBase64String(args[i][key.Length..]));
@ -378,11 +419,37 @@ namespace Dalamud.Injector
throw new CommandLineException($"\"{languageStr}\" is not a valid supported language."); throw new CommandLineException($"\"{languageStr}\" is not a valid supported language.");
} }
OSPlatform platform;
// covers both win32 and Windows
if (platformStr[0..(len = Math.Min(platformStr.Length, (key = "win").Length))] == key[0..len])
{
platform = OSPlatform.Windows;
}
else if (platformStr[0..(len = Math.Min(platformStr.Length, (key = "linux").Length))] == key[0..len])
{
platform = OSPlatform.Linux;
}
else if (platformStr[0..(len = Math.Min(platformStr.Length, (key = "macos").Length))] == key[0..len])
{
platform = OSPlatform.OSX;
}
else if (platformStr[0..(len = Math.Min(platformStr.Length, (key = "osx").Length))] == key[0..len])
{
platform = OSPlatform.OSX;
}
else
{
platform = DetectPlatformHeuristic();
Log.Warning("Heuristically determined host system platform as {platform}", platform);
}
startInfo.WorkingDirectory = workingDirectory; startInfo.WorkingDirectory = workingDirectory;
startInfo.ConfigurationPath = configurationPath; startInfo.ConfigurationPath = configurationPath;
startInfo.PluginDirectory = pluginDirectory; startInfo.PluginDirectory = pluginDirectory;
startInfo.AssetDirectory = assetDirectory; startInfo.AssetDirectory = assetDirectory;
startInfo.Language = clientLanguage; startInfo.Language = clientLanguage;
startInfo.Platform = platform;
startInfo.DelayInitializeMs = delayInitializeMs; startInfo.DelayInitializeMs = delayInitializeMs;
startInfo.GameVersion = null; startInfo.GameVersion = null;
startInfo.TroubleshootingPackData = troubleshootingData; startInfo.TroubleshootingPackData = troubleshootingData;
@ -465,13 +532,14 @@ namespace Dalamud.Injector
} }
Console.WriteLine("Specifying dalamud start info: [--dalamud-working-directory=path] [--dalamud-configuration-path=path]"); Console.WriteLine("Specifying dalamud start info: [--dalamud-working-directory=path] [--dalamud-configuration-path=path]");
Console.WriteLine(" [--dalamud-plugin-directory=path]"); Console.WriteLine(" [--dalamud-plugin-directory=path] [--dalamud-platform=win32|linux|macOS]");
Console.WriteLine(" [--dalamud-asset-directory=path] [--dalamud-delay-initialize=0(ms)]"); Console.WriteLine(" [--dalamud-asset-directory=path] [--dalamud-delay-initialize=0(ms)]");
Console.WriteLine(" [--dalamud-client-language=0-3|j(apanese)|e(nglish)|d|g(erman)|f(rench)]"); Console.WriteLine(" [--dalamud-client-language=0-3|j(apanese)|e(nglish)|d|g(erman)|f(rench)]");
Console.WriteLine("Verbose logging:\t[-v]"); Console.WriteLine("Verbose logging:\t[-v]");
Console.WriteLine("Show Console:\t[--console] [--crash-handler-console]"); Console.WriteLine("Show Console:\t[--console] [--crash-handler-console]");
Console.WriteLine("Enable ETW:\t[--etw]"); Console.WriteLine("Enable ETW:\t[--etw]");
Console.WriteLine("Disable legacy corrupted state exceptions:\t[--no-legacy-corrupted-state-exceptions]");
Console.WriteLine("Enable VEH:\t[--veh], [--veh-full], [--unhandled-exception=default|stalldebug|none]"); Console.WriteLine("Enable VEH:\t[--veh], [--veh-full], [--unhandled-exception=default|stalldebug|none]");
Console.WriteLine("Show messagebox:\t[--msgbox1], [--msgbox2], [--msgbox3]"); Console.WriteLine("Show messagebox:\t[--msgbox1], [--msgbox2], [--msgbox3]");
Console.WriteLine("No plugins:\t[--no-plugin] [--no-3rd-plugin]"); Console.WriteLine("No plugins:\t[--no-plugin] [--no-3rd-plugin]");
@ -731,15 +799,42 @@ namespace Dalamud.Injector
{ {
try try
{ {
var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); if (dalamudStartInfo.Platform == OSPlatform.Windows)
var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher"); {
var launcherConfigPath = Path.Combine(xivlauncherDir, "launcherConfigV3.json"); var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
gamePath = Path.Combine(JsonSerializer.CreateDefault().Deserialize<Dictionary<string, string>>(new JsonTextReader(new StringReader(File.ReadAllText(launcherConfigPath))))["GamePath"], "game", "ffxiv_dx11.exe"); var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher");
Log.Information("Using game installation path configuration from from XIVLauncher: {0}", gamePath); var launcherConfigPath = Path.Combine(xivlauncherDir, "launcherConfigV3.json");
gamePath = Path.Combine(
JsonSerializer.CreateDefault()
.Deserialize<Dictionary<string, string>>(
new JsonTextReader(new StringReader(File.ReadAllText(launcherConfigPath))))["GamePath"],
"game",
"ffxiv_dx11.exe");
Log.Information("Using game installation path configuration from from XIVLauncher: {0}", gamePath);
}
else if (dalamudStartInfo.Platform == OSPlatform.Linux)
{
var homeDir = $"Z:\\home\\{Environment.UserName}";
var xivlauncherDir = Path.Combine(homeDir, ".xlcore");
var launcherConfigPath = Path.Combine(xivlauncherDir, "launcher.ini");
var config = File.ReadAllLines(launcherConfigPath)
.Where(line => line.Contains('='))
.ToDictionary(line => line.Split('=')[0], line => line.Split('=')[1]);
gamePath = Path.Combine("Z:" + config["GamePath"].Replace('/', '\\'), "game", "ffxiv_dx11.exe");
Log.Information("Using game installation path configuration from from XIVLauncher Core: {0}", gamePath);
}
else
{
var homeDir = $"Z:\\Users\\{Environment.UserName}";
var xomlauncherDir = Path.Combine(homeDir, "Library", "Application Support", "XIV on Mac");
// we could try to parse the binary plist file here if we really wanted to...
gamePath = Path.Combine(xomlauncherDir, "ffxiv", "game", "ffxiv_dx11.exe");
Log.Information("Using default game installation path from XOM: {0}", gamePath);
}
} }
catch (Exception) catch (Exception)
{ {
Log.Error("Failed to read launcherConfigV3.json to get the set-up game path, please specify one using -g"); Log.Error("Failed to read launcher config to get the set-up game path, please specify one using -g");
return -1; return -1;
} }
@ -794,20 +889,6 @@ namespace Dalamud.Injector
if (encryptArguments) if (encryptArguments)
{ {
var rawTickCount = (uint)Environment.TickCount; var rawTickCount = (uint)Environment.TickCount;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
[System.Runtime.InteropServices.DllImport("c")]
#pragma warning disable SA1300
static extern ulong clock_gettime_nsec_np(int clockId);
#pragma warning restore SA1300
const int CLOCK_MONOTONIC_RAW = 4;
var rawTickCountFixed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) / 1000000;
Log.Information("ArgumentBuilder::DeriveKey() fixing up rawTickCount from {0} to {1} on macOS", rawTickCount, rawTickCountFixed);
rawTickCount = (uint)rawTickCountFixed;
}
var ticks = rawTickCount & 0xFFFF_FFFFu; var ticks = rawTickCount & 0xFFFF_FFFFu;
var key = ticks & 0xFFFF_0000u; var key = ticks & 0xFFFF_0000u;
gameArguments.Insert(0, $"T={ticks}"); gameArguments.Insert(0, $"T={ticks}");

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Dalamud.Injector namespace Dalamud.Injector
@ -910,5 +911,46 @@ namespace Dalamud.Injector
uint dwDesiredAccess, uint dwDesiredAccess,
[MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
DuplicateOptions dwOptions); DuplicateOptions dwOptions);
/// <summary>
/// See https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlew.
/// Retrieves a module handle for the specified module. The module must have been loaded by the calling process. To
/// avoid the race conditions described in the Remarks section, use the GetModuleHandleEx function.
/// </summary>
/// <param name="lpModuleName">
/// The name of the loaded module (either a .dll or .exe file). If the file name extension is omitted, the default
/// library extension .dll is appended. The file name string can include a trailing point character (.) to indicate
/// that the module name has no extension. The string does not have to specify a path. When specifying a path, be sure
/// to use backslashes (\), not forward slashes (/). The name is compared (case independently) to the names of modules
/// currently mapped into the address space of the calling process. If this parameter is NULL, GetModuleHandle returns
/// a handle to the file used to create the calling process (.exe file). The GetModuleHandle function does not retrieve
/// handles for modules that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx.
/// </param>
/// <returns>
/// If the function succeeds, the return value is a handle to the specified module. If the function fails, the return
/// value is NULL.To get extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
public static extern IntPtr GetModuleHandleW(string lpModuleName);
/// <summary>
/// Retrieves the address of an exported function or variable from the specified dynamic-link library (DLL).
/// </summary>
/// <param name="hModule">
/// A handle to the DLL module that contains the function or variable. The LoadLibrary, LoadLibraryEx, LoadPackagedLibrary,
/// or GetModuleHandle function returns this handle. The GetProcAddress function does not retrieve addresses from modules
/// that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx.
/// </param>
/// <param name="procName">
/// The function or variable name, or the function's ordinal value. If this parameter is an ordinal value, it must be
/// in the low-order word; the high-order word must be zero.
/// </param>
/// <returns>
/// If the function succeeds, the return value is the address of the exported function or variable. If the function
/// fails, the return value is NULL.To get extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
[SuppressMessage("Globalization", "CA2101:Specify marshaling for P/Invoke string arguments", Justification = "Ansi only")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
} }
} }

View file

@ -1,13 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Target">
<TargetFramework>net8.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PlatformTarget>x64</PlatformTarget>
<Platforms>x64;AnyCPU</Platforms>
<LangVersion>11.0</LangVersion>
</PropertyGroup>
<PropertyGroup Label="Feature"> <PropertyGroup Label="Feature">
<RootNamespace>Dalamud.Test</RootNamespace> <RootNamespace>Dalamud.Test</RootNamespace>
<AssemblyTitle>Dalamud.Test</AssemblyTitle> <AssemblyTitle>Dalamud.Test</AssemblyTitle>

View file

@ -54,6 +54,7 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002EFormat_002ESettingsUpgrade_002EAlignmentTabFillStyleMigration/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002EFormat_002ESettingsUpgrade_002EAlignmentTabFillStyleMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=bannedplugin/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=bannedplugin/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=clientopcode/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=clientopcode/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=collectability/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Dalamud/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Dalamud/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=FFXIV/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=FFXIV/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Flytext/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Flytext/@EntryIndexedValue">True</s:Boolean>
@ -66,6 +67,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=PLUGINR/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=PLUGINR/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Refilter/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Refilter/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=serveropcode/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=serveropcode/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=spiritbond/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Universalis/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Universalis/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unsanitized/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=unsanitized/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Uploaders/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/UserDictionary/Words/=Uploaders/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View file

@ -4,6 +4,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Interface; using Dalamud.Interface;
@ -11,6 +12,7 @@ using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.ReShadeHandling; using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.Style; using Dalamud.Interface.Style;
using Dalamud.Interface.Windowing.Persistence;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal.AutoUpdate; using Dalamud.Plugin.Internal.AutoUpdate;
using Dalamud.Plugin.Internal.Profiles; using Dalamud.Plugin.Internal.Profiles;
@ -45,6 +47,8 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
[JsonIgnore] [JsonIgnore]
private bool isSaveQueued; private bool isSaveQueued;
private Task? writeTask;
/// <summary> /// <summary>
/// Delegate for the <see cref="DalamudConfiguration.DalamudConfigurationSaved"/> event that occurs when the dalamud configuration is saved. /// Delegate for the <see cref="DalamudConfiguration.DalamudConfigurationSaved"/> event that occurs when the dalamud configuration is saved.
/// </summary> /// </summary>
@ -57,7 +61,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public event DalamudConfigurationSavedDelegate? DalamudConfigurationSaved; public event DalamudConfigurationSavedDelegate? DalamudConfigurationSaved;
/// <summary> /// <summary>
/// Gets or sets a list of muted works. /// Gets or sets a list of muted words.
/// </summary> /// </summary>
public List<string>? BadWords { get; set; } public List<string>? BadWords { get; set; }
@ -243,13 +247,13 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not ImGui asserts should be enabled at startup. /// Gets or sets a value indicating whether or not ImGui asserts should be enabled at startup.
/// </summary> /// </summary>
public bool AssertsEnabledAtStartup { get; set; } public bool? ImGuiAssertsEnabledAtStartup { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not docking should be globally enabled in ImGui. /// Gets or sets a value indicating whether or not docking should be globally enabled in ImGui.
/// </summary> /// </summary>
public bool IsDocking { get; set; } public bool IsDocking { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not plugin user interfaces should trigger sound effects. /// Gets or sets a value indicating whether or not plugin user interfaces should trigger sound effects.
/// This setting is effected by the in-game "System Sounds" option and volume. /// This setting is effected by the in-game "System Sounds" option and volume.
@ -261,8 +265,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// Gets or sets a value indicating whether or not an additional button allowing pinning and clickthrough options should be shown /// Gets or sets a value indicating whether or not an additional button allowing pinning and clickthrough options should be shown
/// on plugin title bars when using the Window System. /// on plugin title bars when using the Window System.
/// </summary> /// </summary>
[JsonProperty("EnablePluginUiAdditionalOptionsExperimental")] public bool EnablePluginUiAdditionalOptions { get; set; } = true;
public bool EnablePluginUiAdditionalOptions { get; set; } = false;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether viewports should always be disabled. /// Gets or sets a value indicating whether viewports should always be disabled.
@ -348,6 +351,11 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary> /// </summary>
public bool ProfilesHasSeenTutorial { get; set; } = false; public bool ProfilesHasSeenTutorial { get; set; } = false;
/// <summary>
/// Gets or sets the default UI preset.
/// </summary>
public PresetModel DefaultUiPreset { get; set; } = new();
/// <summary> /// <summary>
/// Gets or sets the order of DTR elements, by title. /// Gets or sets the order of DTR elements, by title.
/// </summary> /// </summary>
@ -484,10 +492,15 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public AutoUpdateBehavior? AutoUpdateBehavior { get; set; } = null; public AutoUpdateBehavior? AutoUpdateBehavior { get; set; } = null;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether or not users should be notified regularly about pending updates. /// Gets or sets a value indicating whether users should be notified regularly about pending updates.
/// </summary> /// </summary>
public bool CheckPeriodicallyForUpdates { get; set; } = true; public bool CheckPeriodicallyForUpdates { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether users should be notified about updates in chat.
/// </summary>
public bool SendUpdateNotificationToChat { get; set; } = false;
/// <summary> /// <summary>
/// Load a configuration from the provided path. /// Load a configuration from the provided path.
/// </summary> /// </summary>
@ -504,7 +517,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
{ {
deserialized = deserialized =
JsonConvert.DeserializeObject<DalamudConfiguration>(text, SerializerSettings); JsonConvert.DeserializeObject<DalamudConfiguration>(text, SerializerSettings);
// If this reads as null, the file was empty, that's no good // If this reads as null, the file was empty, that's no good
if (deserialized == null) if (deserialized == null)
throw new Exception("Read config was null."); throw new Exception("Read config was null.");
@ -530,7 +543,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
{ {
Log.Error(e, "Failed to set defaults for DalamudConfiguration"); Log.Error(e, "Failed to set defaults for DalamudConfiguration");
} }
return deserialized; return deserialized;
} }
@ -549,12 +562,15 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
{ {
this.Save(); this.Save();
} }
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
// Make sure that we save, if a save is queued while we are shutting down // Make sure that we save, if a save is queued while we are shutting down
this.Update(); this.Update();
// Wait for the write task to finish
this.writeTask?.Wait();
} }
/// <summary> /// <summary>
@ -595,22 +611,36 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
this.ReduceMotions = winAnimEnabled == 0; this.ReduceMotions = winAnimEnabled == 0;
} }
} }
// Migrate old auto-update setting to new auto-update behavior // Migrate old auto-update setting to new auto-update behavior
this.AutoUpdateBehavior ??= this.AutoUpdatePlugins this.AutoUpdateBehavior ??= this.AutoUpdatePlugins
? Plugin.Internal.AutoUpdate.AutoUpdateBehavior.UpdateAll ? Plugin.Internal.AutoUpdate.AutoUpdateBehavior.UpdateAll
: Plugin.Internal.AutoUpdate.AutoUpdateBehavior.OnlyNotify; : Plugin.Internal.AutoUpdate.AutoUpdateBehavior.OnlyNotify;
#pragma warning restore CS0618 #pragma warning restore CS0618
} }
private void Save() private void Save()
{ {
ThreadSafety.AssertMainThread(); ThreadSafety.AssertMainThread();
if (this.configPath is null) if (this.configPath is null)
throw new InvalidOperationException("configPath is not set."); throw new InvalidOperationException("configPath is not set.");
Service<ReliableFileStorage>.Get().WriteAllText( // Wait for previous write to finish
this.configPath, JsonConvert.SerializeObject(this, SerializerSettings)); this.writeTask?.Wait();
this.writeTask = Task.Run(() =>
{
Service<ReliableFileStorage>.Get().WriteAllText(
this.configPath,
JsonConvert.SerializeObject(this, SerializerSettings));
}).ContinueWith(t =>
{
if (t.IsFaulted)
{
Log.Error(t.Exception, "Failed to save DalamudConfiguration to {Path}", this.configPath);
}
});
this.DalamudConfigurationSaved?.Invoke(this); this.DalamudConfigurationSaved?.Invoke(this);
} }
} }

View file

@ -5,11 +5,6 @@ namespace Dalamud.Configuration.Internal;
/// </summary> /// </summary>
internal class EnvironmentConfiguration internal class EnvironmentConfiguration
{ {
/// <summary>
/// Gets a value indicating whether the XL_WINEONLINUX setting has been enabled.
/// </summary>
public static bool XlWineOnLinux { get; } = GetEnvironmentVariable("XL_WINEONLINUX");
/// <summary> /// <summary>
/// Gets a value indicating whether the DALAMUD_NOT_HAVE_PLUGINS setting has been enabled. /// Gets a value indicating whether the DALAMUD_NOT_HAVE_PLUGINS setting has been enabled.
/// </summary> /// </summary>

View file

@ -1,16 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Target"> <PropertyGroup Label="Target">
<TargetFramework>net8.0-windows</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<Platforms>x64;AnyCPU</Platforms>
<LangVersion>12.0</LangVersion>
<EnableWindowsTargeting>True</EnableWindowsTargeting> <EnableWindowsTargeting>True</EnableWindowsTargeting>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Feature"> <PropertyGroup Label="Feature">
<DalamudVersion>10.0.0.7</DalamudVersion>
<Description>XIV Launcher addon framework</Description> <Description>XIV Launcher addon framework</Description>
<DalamudVersion>12.0.0.7</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion> <AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version> <Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion> <FileVersion>$(DalamudVersion)</FileVersion>
@ -47,10 +43,6 @@
<PropertyGroup Label="Configuration" Condition="'$(Configuration)'=='Debug'"> <PropertyGroup Label="Configuration" Condition="'$(Configuration)'=='Debug'">
<DefineConstants>DEBUG;TRACE</DefineConstants> <DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Configuration" Condition="'$(Configuration)'=='Release'">
<AppOutputBase>$(MSBuildProjectDirectory)\</AppOutputBase>
<PathMap>$(AppOutputBase)=C:\goatsoft\companysecrets\dalamud\</PathMap>
</PropertyGroup>
<PropertyGroup Label="Warnings"> <PropertyGroup Label="Warnings">
<NoWarn>IDE0002;IDE0003;IDE1006;IDE0044;CA1822;CS1591;CS1701;CS1702</NoWarn> <NoWarn>IDE0002;IDE0003;IDE1006;IDE0044;CA1822;CS1591;CS1701;CS1702</NoWarn>
@ -71,8 +63,8 @@
<PackageReference Include="goaaats.Reloaded.Hooks" Version="4.2.0-goat.4" /> <PackageReference Include="goaaats.Reloaded.Hooks" Version="4.2.0-goat.4" />
<PackageReference Include="goaaats.Reloaded.Assembler" Version="1.0.14-goat.2" /> <PackageReference Include="goaaats.Reloaded.Assembler" Version="1.0.14-goat.2" />
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" /> <PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
<PackageReference Include="Lumina" Version="4.1.0" /> <PackageReference Include="Lumina" Version="$(LuminaVersion)" />
<PackageReference Include="Lumina.Excel" Version="7.0.1" /> <PackageReference Include="Lumina.Excel" Version="$(LuminaExcelVersion)" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0-preview.1.24081.5" /> <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0-preview.1.24081.5" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta"> <PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@ -83,12 +75,13 @@
<PackageReference Include="PInvoke.Kernel32" Version="0.7.104" /> <PackageReference Include="PInvoke.Kernel32" Version="0.7.104" />
<PackageReference Include="PInvoke.User32" Version="0.7.104" /> <PackageReference Include="PInvoke.User32" Version="0.7.104" />
<PackageReference Include="PInvoke.Win32" Version="0.7.104" /> <PackageReference Include="PInvoke.Win32" Version="0.7.104" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" /> <PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" /> <PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
<PackageReference Include="Serilog" Version="4.0.2" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="sqlite-net-pcl" Version="1.8.116" /> <PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556"> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
@ -113,6 +106,7 @@
<ProjectReference Include="..\Dalamud.Common\Dalamud.Common.csproj" /> <ProjectReference Include="..\Dalamud.Common\Dalamud.Common.csproj" />
<ProjectReference Include="..\lib\FFXIVClientStructs\FFXIVClientStructs\FFXIVClientStructs.csproj" /> <ProjectReference Include="..\lib\FFXIVClientStructs\FFXIVClientStructs\FFXIVClientStructs.csproj" />
<ProjectReference Include="..\lib\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj" /> <ProjectReference Include="..\lib\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj" />
<ProjectReference Include="..\lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -126,6 +120,13 @@
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Interface\ImGuiSeStringRenderer\Internal\TextProcessing\DerivedGeneralCategory.txt" LogicalName="DerivedGeneralCategory.txt" />
<EmbeddedResource Include="Interface\ImGuiSeStringRenderer\Internal\TextProcessing\EastAsianWidth.txt" LogicalName="EastAsianWidth.txt" />
<EmbeddedResource Include="Interface\ImGuiSeStringRenderer\Internal\TextProcessing\emoji-data.txt" LogicalName="emoji-data.txt" />
<EmbeddedResource Include="Interface\ImGuiSeStringRenderer\Internal\TextProcessing\LineBreak.txt" LogicalName="LineBreak.txt" />
</ItemGroup>
<Target Name="AddRuntimeDependenciesToContent" BeforeTargets="GetCopyToOutputDirectoryItems" DependsOnTargets="GenerateBuildDependencyFile;GenerateBuildRuntimeConfigurationFiles"> <Target Name="AddRuntimeDependenciesToContent" BeforeTargets="GetCopyToOutputDirectoryItems" DependsOnTargets="GenerateBuildDependencyFile;GenerateBuildRuntimeConfigurationFiles">
<ItemGroup> <ItemGroup>
<ContentWithTargetPath Include="$(ProjectDepsFilePath)" CopyToOutputDirectory="PreserveNewest" TargetPath="$(ProjectDepsFileName)" /> <ContentWithTargetPath Include="$(ProjectDepsFilePath)" CopyToOutputDirectory="PreserveNewest" TargetPath="$(ProjectDepsFileName)" />
@ -165,7 +166,7 @@
<Exec Command="echo|set /P =&quot;$(CommitHash)&quot; &gt; $(CommitHashFile)" IgnoreExitCode="true" /> <Exec Command="echo|set /P =&quot;$(CommitHash)&quot; &gt; $(CommitHashFile)" IgnoreExitCode="true" />
<Exec Command="echo|set /P =&quot;$(SCMVersion)&quot; &gt; $(TempVerFile)" IgnoreExitCode="true" /> <Exec Command="echo|set /P =&quot;$(SCMVersion)&quot; &gt; $(TempVerFile)" IgnoreExitCode="true" />
</Target> </Target>
<Target Name="GenerateStubVersionData" BeforeTargets="WriteVersionData" Condition="'$(SCMVersion)'=='' And '$(Configuration)'!='Release'"> <Target Name="GenerateStubVersionData" BeforeTargets="WriteVersionData" Condition="'$(SCMVersion)'=='' And '$(Configuration)'!='Release'">
<!-- stub out version since it takes a while. --> <!-- stub out version since it takes a while. -->
<PropertyGroup> <PropertyGroup>
@ -173,7 +174,7 @@
<CommitHashClientStructs>???</CommitHashClientStructs> <CommitHashClientStructs>???</CommitHashClientStructs>
</PropertyGroup> </PropertyGroup>
</Target> </Target>
<Target Name="WriteVersionData" BeforeTargets="CoreCompile"> <Target Name="WriteVersionData" BeforeTargets="CoreCompile">
<!-- names the obj/.../CustomAssemblyInfo.cs file --> <!-- names the obj/.../CustomAssemblyInfo.cs file -->
<PropertyGroup> <PropertyGroup>

View file

@ -9,6 +9,7 @@ namespace Dalamud;
/// <strong>Any asset can cease to exist at any point, even if the enum value exists.</strong><br /> /// <strong>Any asset can cease to exist at any point, even if the enum value exists.</strong><br />
/// Either ship your own assets, or be prepared for errors. /// Either ship your own assets, or be prepared for errors.
/// </summary> /// </summary>
// Implementation notes: avoid specifying numbers too high here. Lookup table is currently implemented as an array.
public enum DalamudAsset public enum DalamudAsset
{ {
/// <summary> /// <summary>

View file

@ -1,5 +1,6 @@
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.IoC; using Dalamud.IoC;
@ -10,6 +11,7 @@ using Dalamud.Utility.Timing;
using Lumina; using Lumina;
using Lumina.Data; using Lumina.Data;
using Lumina.Excel; using Lumina.Excel;
using Newtonsoft.Json; using Newtonsoft.Json;
using Serilog; using Serilog;
@ -27,12 +29,15 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
{ {
private readonly Thread luminaResourceThread; private readonly Thread luminaResourceThread;
private readonly CancellationTokenSource luminaCancellationTokenSource; private readonly CancellationTokenSource luminaCancellationTokenSource;
private readonly RsvResolver rsvResolver;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private DataManager(Dalamud dalamud) private DataManager(Dalamud dalamud)
{ {
this.Language = (ClientLanguage)dalamud.StartInfo.Language; this.Language = (ClientLanguage)dalamud.StartInfo.Language;
this.rsvResolver = new();
try try
{ {
Log.Verbose("Starting data load..."); Log.Verbose("Starting data load...");
@ -43,11 +48,8 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
{ {
LoadMultithreaded = true, LoadMultithreaded = true,
CacheFileResources = true, CacheFileResources = true,
#if NEVER // Lumina bug
PanicOnSheetChecksumMismatch = true, PanicOnSheetChecksumMismatch = true,
#else RsvResolver = this.rsvResolver.TryResolve,
PanicOnSheetChecksumMismatch = false,
#endif
DefaultExcelLanguage = this.Language.ToLumina(), DefaultExcelLanguage = this.Language.ToLumina(),
}; };
@ -128,12 +130,12 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
#region Lumina Wrappers #region Lumina Wrappers
/// <inheritdoc/> /// <inheritdoc/>
public ExcelSheet<T>? GetExcelSheet<T>() where T : ExcelRow public ExcelSheet<T> GetExcelSheet<T>(ClientLanguage? language = null, string? name = null) where T : struct, IExcelRow<T>
=> this.Excel.GetSheet<T>(); => this.Excel.GetSheet<T>(language?.ToLumina(), name);
/// <inheritdoc/> /// <inheritdoc/>
public ExcelSheet<T>? GetExcelSheet<T>(ClientLanguage language) where T : ExcelRow public SubrowExcelSheet<T> GetSubrowExcelSheet<T>(ClientLanguage? language = null, string? name = null) where T : struct, IExcelSubrow<T>
=> this.Excel.GetSheet<T>(language.ToLumina()); => this.Excel.GetSubrowSheet<T>(language?.ToLumina(), name);
/// <inheritdoc/> /// <inheritdoc/>
public FileResource? GetFile(string path) public FileResource? GetFile(string path)
@ -148,6 +150,16 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
return this.GameData.Repositories.TryGetValue(filePath.Repository, out var repository) ? repository.GetFile<T>(filePath.Category, filePath) : default; return this.GameData.Repositories.TryGetValue(filePath.Repository, out var repository) ? repository.GetFile<T>(filePath.Category, filePath) : default;
} }
/// <inheritdoc/>
public Task<T> GetFileAsync<T>(string path, CancellationToken cancellationToken) where T : FileResource =>
GameData.ParseFilePath(path) is { } filePath &&
this.GameData.Repositories.TryGetValue(filePath.Repository, out var repository)
? Task.Run(
() => repository.GetFile<T>(filePath.Category, filePath) ?? throw new FileNotFoundException(
"Failed to load file, most likely because the file could not be found."),
cancellationToken)
: Task.FromException<T>(new FileNotFoundException("The file could not be found."));
/// <inheritdoc/> /// <inheritdoc/>
public bool FileExists(string path) public bool FileExists(string path)
=> this.GameData.FileExists(path); => this.GameData.FileExists(path);
@ -159,6 +171,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
{ {
this.luminaCancellationTokenSource.Cancel(); this.luminaCancellationTokenSource.Cancel();
this.GameData.Dispose(); this.GameData.Dispose();
this.rsvResolver.Dispose();
} }
private class LauncherTroubleshootingInfo private class LauncherTroubleshootingInfo

View file

@ -0,0 +1,22 @@
using Lumina.Excel;
namespace Dalamud.Data;
/// <summary>
/// A helper class to easily resolve Lumina data within Dalamud.
/// </summary>
internal static class LuminaUtils
{
private static ExcelModule Module => Service<DataManager>.Get().Excel;
/// <summary>
/// Initializes a new instance of the <see cref="RowRef{T}"/> class using the default <see cref="ExcelModule"/>.
/// </summary>
/// <typeparam name="T">The type of Lumina sheet to resolve.</typeparam>
/// <param name="rowId">The id of the row to resolve.</param>
/// <returns>A new <see cref="RowRef{T}"/> object.</returns>
public static RowRef<T> CreateRef<T>(uint rowId) where T : struct, IExcelRow<T>
{
return new(Module, rowId);
}
}

View file

@ -0,0 +1,51 @@
using System.Collections.Generic;
using Dalamud.Hooking;
using Dalamud.Logging.Internal;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.LayoutEngine;
using Lumina.Text.ReadOnly;
namespace Dalamud.Data;
/// <summary>
/// Provides functionality for resolving RSV strings.
/// </summary>
internal sealed unsafe class RsvResolver : IDisposable
{
private static readonly ModuleLog Log = new("RsvProvider");
private readonly Hook<LayoutWorld.Delegates.AddRsvString> addRsvStringHook;
/// <summary>
/// Initializes a new instance of the <see cref="RsvResolver"/> class.
/// </summary>
public RsvResolver()
{
this.addRsvStringHook = Hook<LayoutWorld.Delegates.AddRsvString>.FromAddress((nint)LayoutWorld.MemberFunctionPointers.AddRsvString, this.AddRsvStringDetour);
this.addRsvStringHook.Enable();
}
private Dictionary<ReadOnlySeString, ReadOnlySeString> Lookup { get; } = [];
/// <summary>Attemps to resolve an RSV string.</summary>
/// <inheritdoc cref="Lumina.Excel.ExcelModule.ResolveRsvDelegate"/>
public bool TryResolve(ReadOnlySeString rsvString, out ReadOnlySeString resolvedString) =>
this.Lookup.TryGetValue(rsvString, out resolvedString);
/// <inheritdoc/>
public void Dispose()
{
this.addRsvStringHook.Dispose();
}
private bool AddRsvStringDetour(LayoutWorld* @this, byte* rsvString, byte* resolvedString, nuint resolvedStringSize)
{
var rsv = new ReadOnlySeString(MemoryHelper.ReadRawNullTerminated((nint)rsvString));
var resolved = new ReadOnlySeString(new ReadOnlySpan<byte>(resolvedString, (int)resolvedStringSize).ToArray());
Log.Debug($"Resolving RSV \"{rsv}\" to \"{resolved}\".");
this.Lookup[rsv] = resolved;
return this.addRsvStringHook.Original(@this, rsvString, resolvedString, resolvedStringSize);
}
}

View file

@ -179,16 +179,17 @@ public sealed class EntryPoint
Reloaded.Hooks.Tools.Utilities.FasmBasePath = new DirectoryInfo(info.WorkingDirectory); Reloaded.Hooks.Tools.Utilities.FasmBasePath = new DirectoryInfo(info.WorkingDirectory);
// This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally // Apply common fixes for culture issues
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls; CultureFixes.Apply();
if (!Util.IsWine()) // Currently VEH is not fully functional on WINE
if (info.Platform != OSPlatform.Windows)
InitSymbolHandler(info); InitSymbolHandler(info);
var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent); var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent);
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]", Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]",
Util.GetScmVersion(), Util.GetScmVersion(),
Util.GetGitHashClientStructs(), Util.GetGitHashClientStructs(),
FFXIVClientStructs.ThisAssembly.Git.Commits); FFXIVClientStructs.ThisAssembly.Git.Commits);
dalamud.WaitForUnload(); dalamud.WaitForUnload();

View file

@ -0,0 +1,89 @@
namespace Dalamud.Game;
/// <summary>
/// Enum describing possible action kinds.
/// </summary>
public enum ActionKind
{
/// <summary>
/// A Trait.
/// </summary>
Trait = 0,
/// <summary>
/// An Action.
/// </summary>
Action = 1,
/// <summary>
/// A usable Item.
/// </summary>
Item = 2, // does not work?
/// <summary>
/// A usable EventItem.
/// </summary>
EventItem = 3, // does not work?
/// <summary>
/// An EventAction.
/// </summary>
EventAction = 4,
/// <summary>
/// A GeneralAction.
/// </summary>
GeneralAction = 5,
/// <summary>
/// A BuddyAction.
/// </summary>
BuddyAction = 6,
/// <summary>
/// A MainCommand.
/// </summary>
MainCommand = 7,
/// <summary>
/// A Companion.
/// </summary>
Companion = 8, // unresolved?!
/// <summary>
/// A CraftAction.
/// </summary>
CraftAction = 9,
/// <summary>
/// An Action (again).
/// </summary>
Action2 = 10, // what's the difference?
/// <summary>
/// A PetAction.
/// </summary>
PetAction = 11,
/// <summary>
/// A CompanyAction.
/// </summary>
CompanyAction = 12,
/// <summary>
/// A Mount.
/// </summary>
Mount = 13,
// 14-18 unused
/// <summary>
/// A BgcArmyAction.
/// </summary>
BgcArmyAction = 19,
/// <summary>
/// An Ornament.
/// </summary>
Ornament = 20,
}

View file

@ -11,9 +11,9 @@ namespace Dalamud.Game.Addon.Events;
internal unsafe class AddonEventListener : IDisposable internal unsafe class AddonEventListener : IDisposable
{ {
private ReceiveEventDelegate? receiveEventDelegate; private ReceiveEventDelegate? receiveEventDelegate;
private AtkEventListener* eventListener; private AtkEventListener* eventListener;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AddonEventListener"/> class. /// Initializes a new instance of the <see cref="AddonEventListener"/> class.
/// </summary> /// </summary>
@ -24,7 +24,7 @@ internal unsafe class AddonEventListener : IDisposable
this.eventListener = (AtkEventListener*)Marshal.AllocHGlobal(sizeof(AtkEventListener)); this.eventListener = (AtkEventListener*)Marshal.AllocHGlobal(sizeof(AtkEventListener));
this.eventListener->VirtualTable = (AtkEventListener.AtkEventListenerVirtualTable*)Marshal.AllocHGlobal(sizeof(void*) * 3); this.eventListener->VirtualTable = (AtkEventListener.AtkEventListenerVirtualTable*)Marshal.AllocHGlobal(sizeof(void*) * 3);
this.eventListener->VirtualTable->Dtor = (delegate* unmanaged<AtkEventListener*, byte, void>)(delegate* unmanaged<void>)&NullSub; this.eventListener->VirtualTable->Dtor = (delegate* unmanaged<AtkEventListener*, byte, AtkEventListener*>)(delegate* unmanaged<void>)&NullSub;
this.eventListener->VirtualTable->ReceiveGlobalEvent = (delegate* unmanaged<AtkEventListener*, AtkEventType, int, AtkEvent*, AtkEventData*, void>)(delegate* unmanaged<void>)&NullSub; this.eventListener->VirtualTable->ReceiveGlobalEvent = (delegate* unmanaged<AtkEventListener*, AtkEventType, int, AtkEvent*, AtkEventData*, void>)(delegate* unmanaged<void>)&NullSub;
this.eventListener->VirtualTable->ReceiveEvent = (delegate* unmanaged<AtkEventListener*, AtkEventType, int, AtkEvent*, AtkEventData*, void>)Marshal.GetFunctionPointerForDelegate(this.receiveEventDelegate); this.eventListener->VirtualTable->ReceiveEvent = (delegate* unmanaged<AtkEventListener*, AtkEventType, int, AtkEvent*, AtkEventData*, void>)Marshal.GetFunctionPointerForDelegate(this.receiveEventDelegate);
} }
@ -38,17 +38,17 @@ internal unsafe class AddonEventListener : IDisposable
/// <param name="eventPtr">Pointer to the AtkEvent.</param> /// <param name="eventPtr">Pointer to the AtkEvent.</param>
/// <param name="eventDataPtr">Pointer to the AtkEventData.</param> /// <param name="eventDataPtr">Pointer to the AtkEventData.</param>
public delegate void ReceiveEventDelegate(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventPtr, AtkEventData* eventDataPtr); public delegate void ReceiveEventDelegate(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventPtr, AtkEventData* eventDataPtr);
/// <summary> /// <summary>
/// Gets the address of this listener. /// Gets the address of this listener.
/// </summary> /// </summary>
public nint Address => (nint)this.eventListener; public nint Address => (nint)this.eventListener;
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
if (this.eventListener is null) return; if (this.eventListener is null) return;
Marshal.FreeHGlobal((nint)this.eventListener->VirtualTable); Marshal.FreeHGlobal((nint)this.eventListener->VirtualTable);
Marshal.FreeHGlobal((nint)this.eventListener); Marshal.FreeHGlobal((nint)this.eventListener);
@ -88,7 +88,7 @@ internal unsafe class AddonEventListener : IDisposable
node->RemoveEvent(eventType, param, this.eventListener, false); node->RemoveEvent(eventType, param, this.eventListener, false);
}); });
} }
[UnmanagedCallersOnly] [UnmanagedCallersOnly]
private static void NullSub() private static void NullSub()
{ {

View file

@ -16,6 +16,6 @@ internal class AddonEventManagerAddressResolver : BaseAddressResolver
/// <param name="scanner">The signature scanner to facilitate setup.</param> /// <param name="scanner">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner scanner) protected override void Setup64Bit(ISigScanner scanner)
{ {
this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE"); this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE"); // unnamed in CS
} }
} }

View file

@ -165,7 +165,7 @@ internal unsafe class PluginEventController : IDisposable
{ {
var paramKeyMatches = currentEvent->Param == eventEntry.ParamKey; var paramKeyMatches = currentEvent->Param == eventEntry.ParamKey;
var eventListenerAddressMatches = (nint)currentEvent->Listener == this.EventListener.Address; var eventListenerAddressMatches = (nint)currentEvent->Listener == this.EventListener.Address;
var eventTypeMatches = currentEvent->Type == eventType; var eventTypeMatches = currentEvent->State.EventType == eventType;
if (paramKeyMatches && eventListenerAddressMatches && eventTypeMatches) if (paramKeyMatches && eventListenerAddressMatches && eventTypeMatches)
{ {

View file

@ -52,7 +52,7 @@ public enum AddonEvent
PostDraw, PostDraw,
/// <summary> /// <summary>
/// An event that is fired immediately before an addon is finalized via <see cref="AtkUnitBase.Finalize"/> and /// An event that is fired immediately before an addon is finalized via <see cref="AtkUnitBase.Finalizer"/> and
/// destroyed. After this event, the addon will destruct its UI node data as well as free any allocated memory. /// destroyed. After this event, the addon will destruct its UI node data as well as free any allocated memory.
/// This event can be used for cleanup and tracking tasks. /// This event can be used for cleanup and tracking tasks.
/// </summary> /// </summary>

View file

@ -13,19 +13,19 @@ internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver
/// This is called for a majority of all addon OnSetup's. /// This is called for a majority of all addon OnSetup's.
/// </summary> /// </summary>
public nint AddonSetup { get; private set; } public nint AddonSetup { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the other addon setup hook invoked by the AtkUnitManager. /// 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. /// 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. /// This seems to be called rarely for specific addons.
/// </summary> /// </summary>
public nint AddonSetup2 { get; private set; } public nint AddonSetup2 { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the addon finalize hook invoked by the AtkUnitManager. /// Gets the address of the addon finalize hook invoked by the AtkUnitManager.
/// </summary> /// </summary>
public nint AddonFinalize { get; private set; } public nint AddonFinalize { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the addon draw hook invoked by virtual function call. /// Gets the address of the addon draw hook invoked by virtual function call.
/// </summary> /// </summary>
@ -35,7 +35,7 @@ internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver
/// Gets the address of the addon update hook invoked by virtual function call. /// Gets the address of the addon update hook invoked by virtual function call.
/// </summary> /// </summary>
public nint AddonUpdate { get; private set; } public nint AddonUpdate { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call. /// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call.
/// </summary> /// </summary>
@ -51,6 +51,6 @@ internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver
this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5"); 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.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.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF ?? ?? ?? ?? 45 33 D2");
this.AddonOnRequestedUpdate = sig.ScanText("FF 90 98 01 00 00 48 8B 5C 24 30 48 83 C4 20"); this.AddonOnRequestedUpdate = sig.ScanText("FF 90 A0 01 00 00 48 8B 5C 24 30");
} }
} }

View file

@ -1,22 +1,14 @@
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using CheapLoc; using CheapLoc;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.Windows;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal;
using Dalamud.Utility; using Dalamud.Utility;
@ -27,49 +19,10 @@ namespace Dalamud.Game;
/// Chat events and public helper functions. /// Chat events and public helper functions.
/// </summary> /// </summary>
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal class ChatHandlers : IServiceType internal partial class ChatHandlers : IServiceType
{ {
private static readonly ModuleLog Log = new("CHATHANDLER"); private static readonly ModuleLog Log = new("ChatHandlers");
private readonly Dictionary<ClientLanguage, Regex[]> retainerSaleRegexes = new()
{
{
ClientLanguage.Japanese,
new Regex[]
{
new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)×(?<count>[\d,.]+)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled),
new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled),
}
},
{
ClientLanguage.English,
new Regex[]
{
new Regex(@"^(?<item>.+) you put up for sale in the (?:.+) markets (?:have|has) sold for (?<value>[\d,.]+) gil \(after fees\)\.$", RegexOptions.Compiled),
}
},
{
ClientLanguage.German,
new Regex[]
{
new Regex(@"^Dein Gehilfe hat (?<item>.+) auf dem Markt von (?:.+) für (?<value>[\d,.]+) Gil verkauft\.$", RegexOptions.Compiled),
new Regex(@"^Dein Gehilfe hat (?<item>.+) auf dem Markt von (?:.+) verkauft und (?<value>[\d,.]+) Gil erhalten\.$", RegexOptions.Compiled),
}
},
{
ClientLanguage.French,
new Regex[]
{
new Regex(@"^Un servant a vendu (?<item>.+) pour (?<value>[\d,.]+) gil à (?:.+)\.$", RegexOptions.Compiled),
}
},
};
private readonly Regex urlRegex = new(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled);
[ServiceManager.ServiceDependency]
private readonly Dalamud dalamud = Service<Dalamud>.Get();
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get(); private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
@ -92,6 +45,9 @@ internal class ChatHandlers : IServiceType
/// </summary> /// </summary>
public bool IsAutoUpdateComplete { get; private set; } public bool IsAutoUpdateComplete { get; private set; }
[GeneratedRegex(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled)]
private static partial Regex CompiledUrlRegex();
private void OnCheckMessageHandled(XivChatType type, int timestamp, ref SeString sender, ref SeString message, ref bool isHandled) private void OnCheckMessageHandled(XivChatType type, int timestamp, ref SeString sender, ref SeString message, ref bool isHandled)
{ {
var textVal = message.TextValue; var textVal = message.TextValue;
@ -100,7 +56,7 @@ internal class ChatHandlers : IServiceType
this.configuration.BadWords.Any(x => !string.IsNullOrEmpty(x) && textVal.Contains(x))) this.configuration.BadWords.Any(x => !string.IsNullOrEmpty(x) && textVal.Contains(x)))
{ {
// This seems to be in the user block list - let's not show it // This seems to be in the user block list - let's not show it
Log.Debug("Blocklist triggered"); Log.Debug("Filtered a message that contained a muted word");
isHandled = true; isHandled = true;
return; return;
} }
@ -127,41 +83,10 @@ internal class ChatHandlers : IServiceType
return; return;
#endif #endif
if (type == XivChatType.RetainerSale)
{
foreach (var regex in this.retainerSaleRegexes[(ClientLanguage)this.dalamud.StartInfo.Language])
{
var matchInfo = regex.Match(message.TextValue);
// we no longer really need to do/validate the item matching since we read the id from the byte array
// but we'd be checking the main match anyway
var itemInfo = matchInfo.Groups["item"];
if (!itemInfo.Success)
continue;
var itemLink = message.Payloads.FirstOrDefault(x => x.Type == PayloadType.Item) as ItemPayload;
if (itemLink == default)
{
Log.Error("itemLink was null. Msg: {0}", BitConverter.ToString(message.Encode()));
break;
}
Log.Debug($"Probable retainer sale: {message}, decoded item {itemLink.Item.RowId}, HQ {itemLink.IsHQ}");
var valueInfo = matchInfo.Groups["value"];
// not sure if using a culture here would work correctly, so just strip symbols instead
if (!valueInfo.Success || !int.TryParse(valueInfo.Value.Replace(",", string.Empty).Replace(".", string.Empty), out var itemValue))
continue;
// Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemLink.Item.RowId, itemValue, itemLink.IsHQ));
break;
}
}
var messageCopy = message; var messageCopy = message;
var senderCopy = sender; var senderCopy = sender;
var linkMatch = this.urlRegex.Match(message.TextValue); var linkMatch = CompiledUrlRegex().Match(message.TextValue);
if (linkMatch.Value.Length > 0) if (linkMatch.Value.Length > 0)
this.LastLink = linkMatch.Value; this.LastLink = linkMatch.Value;
} }

View file

@ -1,6 +1,9 @@
using Dalamud.Game.ClientState.Resolvers; using Dalamud.Data;
using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Lumina.Excel;
namespace Dalamud.Game.ClientState.Aetherytes; namespace Dalamud.Game.ClientState.Aetherytes;
/// <summary> /// <summary>
@ -56,7 +59,7 @@ public interface IAetheryteEntry
/// <summary> /// <summary>
/// Gets the Aetheryte data related to this aetheryte. /// Gets the Aetheryte data related to this aetheryte.
/// </summary> /// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.Aetheryte> AetheryteData { get; } RowRef<Lumina.Excel.Sheets.Aetheryte> AetheryteData { get; }
} }
/// <summary> /// <summary>
@ -103,5 +106,5 @@ internal sealed class AetheryteEntry : IAetheryteEntry
public bool IsApartment => this.data.IsApartment; public bool IsApartment => this.data.IsApartment;
/// <inheritdoc /> /// <inheritdoc />
public ExcelResolver<Lumina.Excel.GeneratedSheets.Aetheryte> AetheryteData => new(this.AetheryteId); public RowRef<Lumina.Excel.Sheets.Aetheryte> AetheryteData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Aetheryte>(this.AetheryteId);
} }

View file

@ -1,14 +1,12 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog; using FFXIVClientStructs.FFXIV.Client.Game.UI;
namespace Dalamud.Game.ClientState.Buddy; namespace Dalamud.Game.ClientState.Buddy;
@ -28,14 +26,9 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get(); private readonly ClientState clientState = Service<ClientState>.Get();
private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private BuddyList() private BuddyList()
{ {
this.address = this.clientState.AddressResolver;
Log.Verbose($"Buddy list address {Util.DescribeAddress(this.address.BuddyList)}");
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -76,14 +69,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
} }
} }
/// <summary> private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => &UIState.Instance()->Buddy;
/// Gets the address of the buddy list.
/// </summary>
internal IntPtr BuddyListAddress => this.address.BuddyList;
private static int BuddyMemberSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember>();
private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy*)this.BuddyListAddress;
/// <inheritdoc/> /// <inheritdoc/>
public IBuddyMember? this[int index] public IBuddyMember? this[int index]

View file

@ -1,6 +1,8 @@
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers;
using Lumina.Excel;
namespace Dalamud.Game.ClientState.Buddy; namespace Dalamud.Game.ClientState.Buddy;
@ -45,17 +47,17 @@ public interface IBuddyMember
/// <summary> /// <summary>
/// Gets the Mount data related to this buddy. It should only be used with companion buddies. /// Gets the Mount data related to this buddy. It should only be used with companion buddies.
/// </summary> /// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.Mount> MountData { get; } RowRef<Lumina.Excel.Sheets.Mount> MountData { get; }
/// <summary> /// <summary>
/// Gets the Pet data related to this buddy. It should only be used with pet buddies. /// Gets the Pet data related to this buddy. It should only be used with pet buddies.
/// </summary> /// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.Pet> PetData { get; } RowRef<Lumina.Excel.Sheets.Pet> PetData { get; }
/// <summary> /// <summary>
/// Gets the Trust data related to this buddy. It should only be used with battle buddies. /// Gets the Trust data related to this buddy. It should only be used with battle buddies.
/// </summary> /// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.DawnGrowMember> TrustData { get; } RowRef<Lumina.Excel.Sheets.DawnGrowMember> TrustData { get; }
} }
/// <summary> /// <summary>
@ -94,13 +96,13 @@ internal unsafe class BuddyMember : IBuddyMember
public uint DataID => this.Struct->DataId; public uint DataID => this.Struct->DataId;
/// <inheritdoc /> /// <inheritdoc />
public ExcelResolver<Lumina.Excel.GeneratedSheets.Mount> MountData => new(this.DataID); public RowRef<Lumina.Excel.Sheets.Mount> MountData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Mount>(this.DataID);
/// <inheritdoc /> /// <inheritdoc />
public ExcelResolver<Lumina.Excel.GeneratedSheets.Pet> PetData => new(this.DataID); public RowRef<Lumina.Excel.Sheets.Pet> PetData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Pet>(this.DataID);
/// <inheritdoc /> /// <inheritdoc />
public ExcelResolver<Lumina.Excel.GeneratedSheets.DawnGrowMember> TrustData => new(this.DataID); public RowRef<Lumina.Excel.Sheets.DawnGrowMember> TrustData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.DawnGrowMember>(this.DataID);
private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address; private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address;
} }

View file

@ -1,5 +1,4 @@
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
@ -13,10 +12,15 @@ using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Application.Network;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Event;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.Sheets;
using Action = System.Action; using Action = System.Action;
@ -29,22 +33,23 @@ namespace Dalamud.Game.ClientState;
internal sealed class ClientState : IInternalDisposableService, IClientState internal sealed class ClientState : IInternalDisposableService, IClientState
{ {
private static readonly ModuleLog Log = new("ClientState"); private static readonly ModuleLog Log = new("ClientState");
private readonly GameLifecycle lifecycle; private readonly GameLifecycle lifecycle;
private readonly ClientStateAddressResolver address; private readonly ClientStateAddressResolver address;
private readonly Hook<SetupTerritoryTypeDelegate> setupTerritoryTypeHook; private readonly Hook<EventFramework.Delegates.SetTerritoryTypeId> setupTerritoryTypeHook;
private readonly Hook<UIModule.Delegates.HandlePacket> uiModuleHandlePacketHook;
private readonly Hook<LogoutCallbackInterface.Delegates.OnLogout> onLogoutHook;
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get(); private readonly Framework framework = Service<Framework>.Get();
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly NetworkHandlers networkHandlers = Service<NetworkHandlers>.Get(); private readonly NetworkHandlers networkHandlers = Service<NetworkHandlers>.Get();
private bool lastConditionNone = true; private bool lastConditionNone = true;
private bool lastFramePvP;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private ClientState(TargetSigScanner sigScanner, Dalamud dalamud, GameLifecycle lifecycle) private unsafe ClientState(TargetSigScanner sigScanner, Dalamud dalamud, GameLifecycle lifecycle)
{ {
this.lifecycle = lifecycle; this.lifecycle = lifecycle;
this.address = new ClientStateAddressResolver(); this.address = new ClientStateAddressResolver();
@ -54,28 +59,37 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
this.ClientLanguage = (ClientLanguage)dalamud.StartInfo.Language; this.ClientLanguage = (ClientLanguage)dalamud.StartInfo.Language;
Log.Verbose($"SetupTerritoryType address {Util.DescribeAddress(this.address.SetupTerritoryType)}"); var setTerritoryTypeAddr = EventFramework.Addresses.SetTerritoryTypeId.Value;
Log.Verbose($"SetupTerritoryType address {Util.DescribeAddress(setTerritoryTypeAddr)}");
this.setupTerritoryTypeHook = Hook<SetupTerritoryTypeDelegate>.FromAddress(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour); this.setupTerritoryTypeHook = Hook<EventFramework.Delegates.SetTerritoryTypeId>.FromAddress(setTerritoryTypeAddr, this.SetupTerritoryTypeDetour);
this.uiModuleHandlePacketHook = Hook<UIModule.Delegates.HandlePacket>.FromAddress((nint)UIModule.StaticVirtualTablePointer->HandlePacket, this.UIModuleHandlePacketDetour);
this.onLogoutHook = Hook<LogoutCallbackInterface.Delegates.OnLogout>.FromAddress((nint)LogoutCallbackInterface.StaticVirtualTablePointer->OnLogout, this.OnLogoutDetour);
this.framework.Update += this.FrameworkOnOnUpdateEvent; this.framework.Update += this.FrameworkOnOnUpdateEvent;
this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop; this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
this.setupTerritoryTypeHook.Enable(); this.setupTerritoryTypeHook.Enable();
this.uiModuleHandlePacketHook.Enable();
this.onLogoutHook.Enable();
} }
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private unsafe delegate void ProcessPacketPlayerSetupDelegate(nint a1, nint packet);
private delegate IntPtr SetupTerritoryTypeDelegate(IntPtr manager, ushort terriType);
/// <inheritdoc/> /// <inheritdoc/>
public event Action<ushort>? TerritoryChanged; public event Action<ushort>? TerritoryChanged;
/// <inheritdoc/>
public event IClientState.ClassJobChangeDelegate? ClassJobChanged;
/// <inheritdoc/>
public event IClientState.LevelChangeDelegate? LevelChanged;
/// <inheritdoc/> /// <inheritdoc/>
public event Action? Login; public event Action? Login;
/// <inheritdoc/> /// <inheritdoc/>
public event Action? Logout; public event IClientState.LogoutDelegate? Logout;
/// <inheritdoc/> /// <inheritdoc/>
public event Action? EnterPvP; public event Action? EnterPvP;
@ -98,7 +112,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
get get
{ {
var agentMap = AgentMap.Instance(); var agentMap = AgentMap.Instance();
return agentMap != null ? AgentMap.Instance()->CurrentMapId : 0; return agentMap != null ? agentMap->CurrentMapId : 0;
} }
} }
@ -106,16 +120,23 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
public IPlayerCharacter? LocalPlayer => Service<ObjectTable>.GetNullable()?[0] as IPlayerCharacter; public IPlayerCharacter? LocalPlayer => Service<ObjectTable>.GetNullable()?[0] as IPlayerCharacter;
/// <inheritdoc/> /// <inheritdoc/>
public ulong LocalContentId => (ulong)Marshal.ReadInt64(this.address.LocalContentId); public unsafe ulong LocalContentId => PlayerState.Instance()->ContentId;
/// <inheritdoc/> /// <inheritdoc/>
public bool IsLoggedIn { get; private set; } public unsafe bool IsLoggedIn
{
get
{
var agentLobby = AgentLobby.Instance();
return agentLobby != null && agentLobby->IsLoggedIn;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public bool IsPvP { get; private set; } public bool IsPvP { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
public bool IsPvPExcludingDen { get; private set; } public bool IsPvPExcludingDen => this.IsPvP && this.TerritoryType != 250;
/// <inheritdoc /> /// <inheritdoc />
public bool IsGPosing => GameMain.IsInGPose(); public bool IsGPosing => GameMain.IsInGPose();
@ -124,25 +145,25 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
/// Gets client state address resolver. /// Gets client state address resolver.
/// </summary> /// </summary>
internal ClientStateAddressResolver AddressResolver => this.address; internal ClientStateAddressResolver AddressResolver => this.address;
/// <inheritdoc/> /// <inheritdoc/>
public bool IsClientIdle(out ConditionFlag blockingFlag) public bool IsClientIdle(out ConditionFlag blockingFlag)
{ {
blockingFlag = 0; blockingFlag = 0;
if (this.LocalPlayer is null) return true; if (this.LocalPlayer is null) return true;
var condition = Service<Conditions.Condition>.GetNullable(); var condition = Service<Conditions.Condition>.GetNullable();
var blockingConditions = condition.AsReadOnlySet().Except([ var blockingConditions = condition.AsReadOnlySet().Except([
ConditionFlag.NormalConditions, ConditionFlag.NormalConditions,
ConditionFlag.Jumping, ConditionFlag.Jumping,
ConditionFlag.Mounted, ConditionFlag.Mounted,
ConditionFlag.UsingParasol]); ConditionFlag.UsingParasol]);
blockingFlag = blockingConditions.FirstOrDefault(); blockingFlag = blockingConditions.FirstOrDefault();
return blockingFlag == 0; return blockingFlag == 0;
} }
/// <inheritdoc/> /// <inheritdoc/>
public bool IsClientIdle() => this.IsClientIdle(out _); public bool IsClientIdle() => this.IsClientIdle(out _);
@ -152,23 +173,89 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.setupTerritoryTypeHook.Dispose(); this.setupTerritoryTypeHook.Dispose();
this.framework.Update -= this.FrameworkOnOnUpdateEvent; this.uiModuleHandlePacketHook.Dispose();
this.onLogoutHook.Dispose();
this.framework.Update -= this.FrameworkOnOnUpdateEvent;
this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop; this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop;
} }
private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType) private unsafe void SetupTerritoryTypeDetour(EventFramework* eventFramework, ushort territoryType)
{ {
this.TerritoryType = terriType; Log.Debug("TerritoryType changed: {0}", territoryType);
this.TerritoryChanged?.InvokeSafely(terriType);
Log.Debug("TerritoryType changed: {0}", terriType); this.TerritoryType = territoryType;
this.TerritoryChanged?.InvokeSafely(territoryType);
return this.setupTerritoryTypeHook.Original(manager, terriType); var rowRef = LuminaUtils.CreateRef<TerritoryType>(territoryType);
if (rowRef.IsValid)
{
var isPvP = rowRef.Value.IsPvpZone;
if (isPvP != this.IsPvP)
{
this.IsPvP = isPvP;
if (this.IsPvP)
{
Log.Debug("EnterPvP");
this.EnterPvP?.InvokeSafely();
}
else
{
Log.Debug("LeavePvP");
this.LeavePvP?.InvokeSafely();
}
}
}
this.setupTerritoryTypeHook.Original(eventFramework, territoryType);
} }
private void NetworkHandlersOnCfPop(ContentFinderCondition e) private unsafe void UIModuleHandlePacketDetour(UIModule* thisPtr, UIModulePacketType type, uint uintParam, void* packet)
{ {
this.CfPop?.InvokeSafely(e); this.uiModuleHandlePacketHook.Original(thisPtr, type, uintParam, packet);
switch (type)
{
case UIModulePacketType.ClassJobChange when this.ClassJobChanged is { } callback:
{
var classJobId = uintParam;
foreach (var action in callback.GetInvocationList().Cast<IClientState.ClassJobChangeDelegate>())
{
try
{
action(classJobId);
}
catch (Exception ex)
{
Log.Error(ex, "Exception during raise of {handler}", action.Method);
}
}
break;
}
case UIModulePacketType.LevelChange when this.LevelChanged is { } callback:
{
var classJobId = *(uint*)packet;
var level = *(ushort*)((nint)packet + 4);
foreach (var action in callback.GetInvocationList().Cast<IClientState.LevelChangeDelegate>())
{
try
{
action(classJobId, level);
}
catch (Exception ex)
{
Log.Error(ex, "Exception during raise of {handler}", action.Method);
}
}
break;
}
}
} }
private void FrameworkOnOnUpdateEvent(IFramework framework1) private void FrameworkOnOnUpdateEvent(IFramework framework1)
@ -184,40 +271,58 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
{ {
Log.Debug("Is login"); Log.Debug("Is login");
this.lastConditionNone = false; this.lastConditionNone = false;
this.IsLoggedIn = true;
this.Login?.InvokeSafely(); this.Login?.InvokeSafely();
gameGui.ResetUiHideState(); gameGui.ResetUiHideState();
this.lifecycle.ResetLogout(); this.lifecycle.ResetLogout();
} }
}
if (!condition.Any() && this.lastConditionNone == false) private unsafe void OnLogoutDetour(LogoutCallbackInterface* thisPtr, LogoutCallbackInterface.LogoutParams* logoutParams)
{
var gameGui = Service<GameGui>.GetNullable();
if (logoutParams != null)
{ {
Log.Debug("Is logout"); try
this.lastConditionNone = true;
this.IsLoggedIn = false;
this.Logout?.InvokeSafely();
gameGui.ResetUiHideState();
this.lifecycle.SetLogout();
}
this.IsPvP = GameMain.IsInPvPArea();
this.IsPvPExcludingDen = this.IsPvP && this.TerritoryType != 250;
if (this.IsPvP != this.lastFramePvP)
{
this.lastFramePvP = this.IsPvP;
if (this.IsPvP)
{ {
this.EnterPvP?.InvokeSafely(); var type = logoutParams->Type;
var code = logoutParams->Code;
Log.Debug("Logout: Type {type}, Code {code}", type, code);
if (this.Logout is { } callback)
{
foreach (var action in callback.GetInvocationList().Cast<IClientState.LogoutDelegate>())
{
try
{
action(type, code);
}
catch (Exception ex)
{
Log.Error(ex, "Exception during raise of {handler}", action.Method);
}
}
}
gameGui?.ResetUiHideState();
this.lastConditionNone = true; // unblock login flag
this.lifecycle.SetLogout();
} }
else catch (Exception ex)
{ {
this.LeavePvP?.InvokeSafely(); Log.Error(ex, "Exception during OnLogoutDetour");
} }
} }
this.onLogoutHook.Original(thisPtr, logoutParams);
}
private void NetworkHandlersOnCfPop(ContentFinderCondition e)
{
this.CfPop?.InvokeSafely(e);
} }
} }
@ -240,28 +345,36 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
internal ClientStatePluginScoped() internal ClientStatePluginScoped()
{ {
this.clientStateService.TerritoryChanged += this.TerritoryChangedForward; this.clientStateService.TerritoryChanged += this.TerritoryChangedForward;
this.clientStateService.ClassJobChanged += this.ClassJobChangedForward;
this.clientStateService.LevelChanged += this.LevelChangedForward;
this.clientStateService.Login += this.LoginForward; this.clientStateService.Login += this.LoginForward;
this.clientStateService.Logout += this.LogoutForward; this.clientStateService.Logout += this.LogoutForward;
this.clientStateService.EnterPvP += this.EnterPvPForward; this.clientStateService.EnterPvP += this.EnterPvPForward;
this.clientStateService.LeavePvP += this.ExitPvPForward; this.clientStateService.LeavePvP += this.ExitPvPForward;
this.clientStateService.CfPop += this.ContentFinderPopForward; this.clientStateService.CfPop += this.ContentFinderPopForward;
} }
/// <inheritdoc/> /// <inheritdoc/>
public event Action<ushort>? TerritoryChanged; public event Action<ushort>? TerritoryChanged;
/// <inheritdoc/>
public event IClientState.ClassJobChangeDelegate? ClassJobChanged;
/// <inheritdoc/>
public event IClientState.LevelChangeDelegate? LevelChanged;
/// <inheritdoc/> /// <inheritdoc/>
public event Action? Login; public event Action? Login;
/// <inheritdoc/> /// <inheritdoc/>
public event Action? Logout; public event IClientState.LogoutDelegate? Logout;
/// <inheritdoc/> /// <inheritdoc/>
public event Action? EnterPvP; public event Action? EnterPvP;
/// <inheritdoc/> /// <inheritdoc/>
public event Action? LeavePvP; public event Action? LeavePvP;
/// <inheritdoc/> /// <inheritdoc/>
public event Action<ContentFinderCondition>? CfPop; public event Action<ContentFinderCondition>? CfPop;
@ -270,7 +383,7 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
/// <inheritdoc/> /// <inheritdoc/>
public ushort TerritoryType => this.clientStateService.TerritoryType; public ushort TerritoryType => this.clientStateService.TerritoryType;
/// <inheritdoc/> /// <inheritdoc/>
public uint MapId => this.clientStateService.MapId; public uint MapId => this.clientStateService.MapId;
@ -302,6 +415,8 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward; this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward;
this.clientStateService.ClassJobChanged -= this.ClassJobChangedForward;
this.clientStateService.LevelChanged -= this.LevelChangedForward;
this.clientStateService.Login -= this.LoginForward; this.clientStateService.Login -= this.LoginForward;
this.clientStateService.Logout -= this.LogoutForward; this.clientStateService.Logout -= this.LogoutForward;
this.clientStateService.EnterPvP -= this.EnterPvPForward; this.clientStateService.EnterPvP -= this.EnterPvPForward;
@ -317,13 +432,17 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
} }
private void TerritoryChangedForward(ushort territoryId) => this.TerritoryChanged?.Invoke(territoryId); private void TerritoryChangedForward(ushort territoryId) => this.TerritoryChanged?.Invoke(territoryId);
private void ClassJobChangedForward(uint classJobId) => this.ClassJobChanged?.Invoke(classJobId);
private void LevelChangedForward(uint classJobId, uint level) => this.LevelChanged?.Invoke(classJobId, level);
private void LoginForward() => this.Login?.Invoke(); private void LoginForward() => this.Login?.Invoke();
private void LogoutForward() => this.Logout?.Invoke(); private void LogoutForward(int type, int code) => this.Logout?.Invoke(type, code);
private void EnterPvPForward() => this.EnterPvP?.Invoke(); private void EnterPvPForward() => this.EnterPvP?.Invoke();
private void ExitPvPForward() => this.LeavePvP?.Invoke(); private void ExitPvPForward() => this.LeavePvP?.Invoke();
private void ContentFinderPopForward(ContentFinderCondition cfc) => this.CfPop?.Invoke(cfc); private void ContentFinderPopForward(ContentFinderCondition cfc) => this.CfPop?.Invoke(cfc);

View file

@ -7,39 +7,6 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
{ {
// Static offsets // Static offsets
/// <summary>
/// Gets the address of the actor table.
/// </summary>
public IntPtr ObjectTable { get; private set; }
/// <summary>
/// Gets the address of the buddy list.
/// </summary>
public IntPtr BuddyList { get; private set; }
/// <summary>
/// Gets the address of a pointer to the fate table.
/// </summary>
/// <remarks>
/// This is a static address to a pointer, not the address of the table itself.
/// </remarks>
public IntPtr FateTablePtr { get; private set; }
/// <summary>
/// Gets the address of the Group Manager.
/// </summary>
public IntPtr GroupManager { get; private set; }
/// <summary>
/// Gets the address of the local content id.
/// </summary>
public IntPtr LocalContentId { get; private set; }
/// <summary>
/// Gets the address of job gauge data.
/// </summary>
public IntPtr JobGaugeData { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the keyboard state. /// Gets the address of the keyboard state.
/// </summary> /// </summary>
@ -50,23 +17,12 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
/// </summary> /// </summary>
public IntPtr KeyboardStateIndexArray { get; private set; } public IntPtr KeyboardStateIndexArray { get; private set; }
/// <summary>
/// Gets the address of the condition flag array.
/// </summary>
public IntPtr ConditionFlags { get; private set; }
// Functions // Functions
/// <summary> /// <summary>
/// Gets the address of the method which sets the territory type. /// Gets the address of the method which sets up the player.
/// </summary> /// </summary>
public IntPtr SetupTerritoryType { get; private set; } public IntPtr ProcessPacketPlayerSetup { get; private set; }
/// <summary>
/// Gets the address of the method which polls the gamepads for data.
/// Called every frame, even when `Enable Gamepad` is off in the settings.
/// </summary>
public IntPtr GamepadPoll { get; private set; }
/// <summary> /// <summary>
/// Scan for and setup any configured address pointers. /// Scan for and setup any configured address pointers.
@ -74,27 +30,12 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
/// <param name="sig">The signature scanner to facilitate setup.</param> /// <param name="sig">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner sig) protected override void Setup64Bit(ISigScanner sig)
{ {
this.ObjectTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 83 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ??"); this.ProcessPacketPlayerSetup = sig.ScanText("40 53 48 83 EC 20 48 8D 0D ?? ?? ?? ?? 48 8B DA E8 ?? ?? ?? ?? 48 8B D3"); // not in cs struct
this.BuddyList = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 45 84 E4 75 1A F6 45 12 04");
this.FateTablePtr = sig.GetStaticAddressFromSig("48 8B 15 ?? ?? ?? ?? 48 8B F1 44 0F B7 41");
this.GroupManager = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 80 B8 ?? ?? ?? ?? ?? 77 71");
this.LocalContentId = sig.GetStaticAddressFromSig("48 0F 44 0D ?? ?? ?? ?? 48 8D 57 08");
this.JobGaugeData = sig.GetStaticAddressFromSig("48 8B 3D ?? ?? ?? ?? 33 ED") + 0x8;
this.SetupTerritoryType = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 57 48 83 EC 20 0F B7 DA");
// These resolve to fixed offsets only, without the base address added in, so GetStaticAddressFromSig() can't be used. // These resolve to fixed offsets only, without the base address added in, so GetStaticAddressFromSig() can't be used.
// lea rcx, ds:1DB9F74h[rax*4] KeyboardState // lea rcx, ds:1DB9F74h[rax*4] KeyboardState
// movzx edx, byte ptr [rbx+rsi+1D5E0E0h] KeyboardStateIndexArray // movzx edx, byte ptr [rbx+rsi+1D5E0E0h] KeyboardStateIndexArray
this.KeyboardState = sig.ScanText("48 8D 0C 85 ?? ?? ?? ?? 8B 04 31 85 C2 0F 85") + 0x4; this.KeyboardState = sig.ScanText("48 8D 0C 85 ?? ?? ?? ?? 8B 04 31 85 C2 0F 85") + 0x4;
this.KeyboardStateIndexArray = sig.ScanText("0F B6 94 33 ?? ?? ?? ?? 84 D2") + 0x4; this.KeyboardStateIndexArray = sig.ScanText("0F B6 94 33 ?? ?? ?? ?? 84 D2") + 0x4;
this.ConditionFlags = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? 8B D3 E8 ?? ?? ?? ?? 32 C0 48 83 C4 20");
this.GamepadPoll = sig.ScanText("40 55 53 57 41 54 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 44 0F 29 B4 24");
} }
} }

View file

@ -28,10 +28,9 @@ internal sealed class Condition : IInternalDisposableService, ICondition
private bool isDisposed; private bool isDisposed;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private Condition(ClientState clientState) private unsafe Condition()
{ {
var resolver = clientState.AddressResolver; this.Address = (nint)FFXIVClientStructs.FFXIV.Client.Game.Conditions.Instance();
this.Address = resolver.ConditionFlags;
// Initialization // Initialization
for (var i = 0; i < MaxConditionEntries; i++) for (var i = 0; i < MaxConditionEntries; i++)

View file

@ -1,10 +1,11 @@
using System.Numerics; using System.Numerics;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory; using Dalamud.Memory;
using Lumina.Excel;
namespace Dalamud.Game.ClientState.Fates; namespace Dalamud.Game.ClientState.Fates;
/// <summary> /// <summary>
@ -20,7 +21,7 @@ public interface IFate : IEquatable<IFate>
/// <summary> /// <summary>
/// Gets game data linked to this Fate. /// Gets game data linked to this Fate.
/// </summary> /// </summary>
Lumina.Excel.GeneratedSheets.Fate GameData { get; } RowRef<Lumina.Excel.Sheets.Fate> GameData { get; }
/// <summary> /// <summary>
/// Gets the time this <see cref="Fate"/> started. /// Gets the time this <see cref="Fate"/> started.
@ -70,8 +71,14 @@ public interface IFate : IEquatable<IFate>
/// <summary> /// <summary>
/// Gets a value indicating whether or not this <see cref="Fate"/> has a EXP bonus. /// Gets a value indicating whether or not this <see cref="Fate"/> has a EXP bonus.
/// </summary> /// </summary>
[Obsolete($"Use {nameof(HasBonus)} instead")]
bool HasExpBonus { get; } bool HasExpBonus { get; }
/// <summary>
/// Gets a value indicating whether or not this <see cref="Fate"/> has a bonus.
/// </summary>
bool HasBonus { get; }
/// <summary> /// <summary>
/// Gets the icon id of this <see cref="Fate"/>. /// Gets the icon id of this <see cref="Fate"/>.
/// </summary> /// </summary>
@ -105,7 +112,7 @@ public interface IFate : IEquatable<IFate>
/// <summary> /// <summary>
/// Gets the territory this <see cref="Fate"/> is located in. /// Gets the territory this <see cref="Fate"/> is located in.
/// </summary> /// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.TerritoryType> TerritoryType { get; } RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType { get; }
/// <summary> /// <summary>
/// Gets the address of this Fate in memory. /// Gets the address of this Fate in memory.
@ -185,7 +192,7 @@ internal unsafe partial class Fate : IFate
public ushort FateId => this.Struct->FateId; public ushort FateId => this.Struct->FateId;
/// <inheritdoc/> /// <inheritdoc/>
public Lumina.Excel.GeneratedSheets.Fate GameData => Service<DataManager>.Get().GetExcelSheet<Lumina.Excel.GeneratedSheets.Fate>().GetRow(this.FateId); public RowRef<Lumina.Excel.Sheets.Fate> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Fate>(this.FateId);
/// <inheritdoc/> /// <inheritdoc/>
public int StartTimeEpoch => this.Struct->StartTimeEpoch; public int StartTimeEpoch => this.Struct->StartTimeEpoch;
@ -215,8 +222,12 @@ internal unsafe partial class Fate : IFate
public byte Progress => this.Struct->Progress; public byte Progress => this.Struct->Progress;
/// <inheritdoc/> /// <inheritdoc/>
[Obsolete($"Use {nameof(HasBonus)} instead")]
public bool HasExpBonus => this.Struct->IsExpBonus; public bool HasExpBonus => this.Struct->IsExpBonus;
/// <inheritdoc/>
public bool HasBonus => this.Struct->IsBonus;
/// <inheritdoc/> /// <inheritdoc/>
public uint IconId => this.Struct->IconId; public uint IconId => this.Struct->IconId;
@ -238,5 +249,5 @@ internal unsafe partial class Fate : IFate
/// <summary> /// <summary>
/// Gets the territory this <see cref="Fate"/> is located in. /// Gets the territory this <see cref="Fate"/> is located in.
/// </summary> /// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.TerritoryType> TerritoryType => new(this.Struct->TerritoryId); public RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->MapMarkers[0].TerritoryId);
} }

View file

@ -8,25 +8,25 @@ public enum FateState : byte
/// <summary> /// <summary>
/// The Fate is active. /// The Fate is active.
/// </summary> /// </summary>
Running = 0x02, Running = 0x04,
/// <summary> /// <summary>
/// The Fate has ended. /// The Fate has ended.
/// </summary> /// </summary>
Ended = 0x04, Ended = 0x07,
/// <summary> /// <summary>
/// The player failed the Fate. /// The player failed the Fate.
/// </summary> /// </summary>
Failed = 0x05, Failed = 0x08,
/// <summary> /// <summary>
/// The Fate is preparing to run. /// The Fate is preparing to run.
/// </summary> /// </summary>
Preparation = 0x07, Preparation = 0x03,
/// <summary> /// <summary>
/// The Fate is preparing to end. /// The Fate is preparing to end.
/// </summary> /// </summary>
WaitingForEnd = 0x08, WaitingForEnd = 0x05,
} }

View file

@ -4,9 +4,8 @@ using System.Collections.Generic;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog; using CSFateManager = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager;
namespace Dalamud.Game.ClientState.Fates; namespace Dalamud.Game.ClientState.Fates;
@ -20,55 +19,34 @@ namespace Dalamud.Game.ClientState.Fates;
#pragma warning restore SA1015 #pragma warning restore SA1015
internal sealed partial class FateTable : IServiceType, IFateTable internal sealed partial class FateTable : IServiceType, IFateTable
{ {
private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private FateTable(ClientState clientState) private FateTable()
{ {
this.address = clientState.AddressResolver;
Log.Verbose($"Fate table address {Util.DescribeAddress(this.address.FateTablePtr)}");
} }
/// <inheritdoc/> /// <inheritdoc/>
public IntPtr Address => this.address.FateTablePtr; public unsafe IntPtr Address => (nint)CSFateManager.Instance();
/// <inheritdoc/> /// <inheritdoc/>
public unsafe int Length public unsafe int Length
{ {
get get
{ {
var fateTable = this.FateTableAddress; var fateManager = CSFateManager.Instance();
if (fateTable == IntPtr.Zero) if (fateManager == null)
return 0; return 0;
// Sonar used this to check if the table was safe to read // Sonar used this to check if the table was safe to read
if (Struct->FateDirector == null) if (fateManager->FateDirector == null)
return 0; return 0;
if (Struct->Fates.First == null || Struct->Fates.Last == null) if (fateManager->Fates.First == null || fateManager->Fates.Last == null)
return 0; return 0;
return Struct->Fates.Count; return fateManager->Fates.Count;
} }
} }
/// <summary>
/// Gets the address of the Fate table.
/// </summary>
internal unsafe IntPtr FateTableAddress
{
get
{
if (this.address.FateTablePtr == IntPtr.Zero)
return IntPtr.Zero;
return *(IntPtr*)this.address.FateTablePtr;
}
}
private unsafe FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager*)this.FateTableAddress;
/// <inheritdoc/> /// <inheritdoc/>
public IFate? this[int index] public IFate? this[int index]
{ {
@ -99,11 +77,11 @@ internal sealed partial class FateTable : IServiceType, IFateTable
if (index >= this.Length) if (index >= this.Length)
return IntPtr.Zero; return IntPtr.Zero;
var fateTable = this.FateTableAddress; var fateManager = CSFateManager.Instance();
if (fateTable == IntPtr.Zero) if (fateManager == null)
return IntPtr.Zero; return IntPtr.Zero;
return (IntPtr)this.Struct->Fates[index].Value; return (IntPtr)fateManager->Fates[index].Value;
} }
/// <inheritdoc/> /// <inheritdoc/>

View file

@ -1,75 +0,0 @@
using System.Runtime.InteropServices;
namespace Dalamud.Game.ClientState.GamePad;
/// <summary>
/// Struct which gets populated by polling the gamepads.
///
/// Has an array of gamepads, among many other things (here not mapped).
/// All we really care about is the final data which the game uses to determine input.
///
/// The size is definitely bigger than only the following fields but I do not know how big.
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct GamepadInput
{
/// <summary>
/// Left analogue stick's horizontal value, -99 for left, 99 for right.
/// </summary>
[FieldOffset(0x88)]
public int LeftStickX;
/// <summary>
/// Left analogue stick's vertical value, -99 for down, 99 for up.
/// </summary>
[FieldOffset(0x8C)]
public int LeftStickY;
/// <summary>
/// Right analogue stick's horizontal value, -99 for left, 99 for right.
/// </summary>
[FieldOffset(0x90)]
public int RightStickX;
/// <summary>
/// Right analogue stick's vertical value, -99 for down, 99 for up.
/// </summary>
[FieldOffset(0x94)]
public int RightStickY;
/// <summary>
/// Raw input, set the whole time while a button is held. See <see cref="GamepadButtons"/> for the mapping.
/// </summary>
/// <remarks>
/// This is a bitfield.
/// </remarks>
[FieldOffset(0x98)]
public ushort ButtonsRaw;
/// <summary>
/// Button pressed, set once when the button is pressed. See <see cref="GamepadButtons"/> for the mapping.
/// </summary>
/// <remarks>
/// This is a bitfield.
/// </remarks>
[FieldOffset(0x9C)]
public ushort ButtonsPressed;
/// <summary>
/// Button released input, set once right after the button is not hold anymore. See <see cref="GamepadButtons"/> for the mapping.
/// </summary>
/// <remarks>
/// This is a bitfield.
/// </remarks>
[FieldOffset(0xA0)]
public ushort ButtonsReleased;
/// <summary>
/// Repeatedly emits the held button input in fixed intervals. See <see cref="GamepadButtons"/> for the mapping.
/// </summary>
/// <remarks>
/// This is a bitfield.
/// </remarks>
[FieldOffset(0xA4)]
public ushort ButtonsRepeat;
}

View file

@ -4,7 +4,8 @@ using Dalamud.Hooking;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.Input;
using ImGuiNET; using ImGuiNET;
using Serilog; using Serilog;
@ -23,7 +24,7 @@ namespace Dalamud.Game.ClientState.GamePad;
#pragma warning restore SA1015 #pragma warning restore SA1015
internal unsafe class GamepadState : IInternalDisposableService, IGamepadState internal unsafe class GamepadState : IInternalDisposableService, IGamepadState
{ {
private readonly Hook<ControllerPoll>? gamepadPoll; private readonly Hook<PadDevice.Delegates.Poll>? gamepadPoll;
private bool isDisposed; private bool isDisposed;
@ -35,25 +36,21 @@ internal unsafe class GamepadState : IInternalDisposableService, IGamepadState
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private GamepadState(ClientState clientState) private GamepadState(ClientState clientState)
{ {
var resolver = clientState.AddressResolver; this.gamepadPoll = Hook<PadDevice.Delegates.Poll>.FromAddress((nint)PadDevice.StaticVirtualTablePointer->Poll, this.GamepadPollDetour);
Log.Verbose($"GamepadPoll address {Util.DescribeAddress(resolver.GamepadPoll)}");
this.gamepadPoll = Hook<ControllerPoll>.FromAddress(resolver.GamepadPoll, this.GamepadPollDetour);
this.gamepadPoll?.Enable(); this.gamepadPoll?.Enable();
} }
private delegate int ControllerPoll(IntPtr controllerInput);
/// <summary> /// <summary>
/// Gets the pointer to the current instance of the GamepadInput struct. /// Gets the pointer to the current instance of the GamepadInput struct.
/// </summary> /// </summary>
public IntPtr GamepadInputAddress { get; private set; } public IntPtr GamepadInputAddress { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
public Vector2 LeftStick => public Vector2 LeftStick =>
new(this.leftStickX, this.leftStickY); new(this.leftStickX, this.leftStickY);
/// <inheritdoc/> /// <inheritdoc/>
public Vector2 RightStick => public Vector2 RightStick =>
new(this.rightStickX, this.rightStickY); new(this.rightStickX, this.rightStickY);
/// <summary> /// <summary>
@ -61,28 +58,28 @@ internal unsafe class GamepadState : IInternalDisposableService, IGamepadState
/// ///
/// Exposed internally for Debug Data window. /// Exposed internally for Debug Data window.
/// </summary> /// </summary>
internal ushort ButtonsPressed { get; private set; } internal GamepadButtons ButtonsPressed { get; private set; }
/// <summary> /// <summary>
/// Gets raw button bitmask, set the whole time while a button is held. See <see cref="GamepadButtons"/> for the mapping. /// Gets raw button bitmask, set the whole time while a button is held. See <see cref="GamepadButtons"/> for the mapping.
/// ///
/// Exposed internally for Debug Data window. /// Exposed internally for Debug Data window.
/// </summary> /// </summary>
internal ushort ButtonsRaw { get; private set; } internal GamepadButtons ButtonsRaw { get; private set; }
/// <summary> /// <summary>
/// Gets button released bitmask, set once right after the button is not hold anymore. See <see cref="GamepadButtons"/> for the mapping. /// Gets button released bitmask, set once right after the button is not hold anymore. See <see cref="GamepadButtons"/> for the mapping.
/// ///
/// Exposed internally for Debug Data window. /// Exposed internally for Debug Data window.
/// </summary> /// </summary>
internal ushort ButtonsReleased { get; private set; } internal GamepadButtons ButtonsReleased { get; private set; }
/// <summary> /// <summary>
/// Gets button repeat bitmask, emits the held button input in fixed intervals. See <see cref="GamepadButtons"/> for the mapping. /// Gets button repeat bitmask, emits the held button input in fixed intervals. See <see cref="GamepadButtons"/> for the mapping.
/// ///
/// Exposed internally for Debug Data window. /// Exposed internally for Debug Data window.
/// </summary> /// </summary>
internal ushort ButtonsRepeat { get; private set; } internal GamepadButtons ButtonsRepeat { get; private set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether detour should block gamepad input for game. /// Gets or sets a value indicating whether detour should block gamepad input for game.
@ -95,16 +92,16 @@ internal unsafe class GamepadState : IInternalDisposableService, IGamepadState
internal bool NavEnableGamepad { get; set; } internal bool NavEnableGamepad { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public float Pressed(GamepadButtons button) => (this.ButtonsPressed & (ushort)button) > 0 ? 1 : 0; public float Pressed(GamepadButtons button) => (this.ButtonsPressed & button) > 0 ? 1 : 0;
/// <inheritdoc/> /// <inheritdoc/>
public float Repeat(GamepadButtons button) => (this.ButtonsRepeat & (ushort)button) > 0 ? 1 : 0; public float Repeat(GamepadButtons button) => (this.ButtonsRepeat & button) > 0 ? 1 : 0;
/// <inheritdoc/> /// <inheritdoc/>
public float Released(GamepadButtons button) => (this.ButtonsReleased & (ushort)button) > 0 ? 1 : 0; public float Released(GamepadButtons button) => (this.ButtonsReleased & button) > 0 ? 1 : 0;
/// <inheritdoc/> /// <inheritdoc/>
public float Raw(GamepadButtons button) => (this.ButtonsRaw & (ushort)button) > 0 ? 1 : 0; public float Raw(GamepadButtons button) => (this.ButtonsRaw & button) > 0 ? 1 : 0;
/// <summary> /// <summary>
/// Disposes this instance, alongside its hooks. /// Disposes this instance, alongside its hooks.
@ -115,28 +112,28 @@ internal unsafe class GamepadState : IInternalDisposableService, IGamepadState
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }
private int GamepadPollDetour(IntPtr gamepadInput) private nint GamepadPollDetour(PadDevice* gamepadInput)
{ {
var original = this.gamepadPoll!.Original(gamepadInput); var original = this.gamepadPoll!.Original(gamepadInput);
try try
{ {
this.GamepadInputAddress = gamepadInput; this.GamepadInputAddress = (nint)gamepadInput;
var input = (GamepadInput*)gamepadInput;
this.leftStickX = input->LeftStickX; this.leftStickX = gamepadInput->GamepadInputData.LeftStickX;
this.leftStickY = input->LeftStickY; this.leftStickY = gamepadInput->GamepadInputData.LeftStickY;
this.rightStickX = input->RightStickX; this.rightStickX = gamepadInput->GamepadInputData.RightStickX;
this.rightStickY = input->RightStickY; this.rightStickY = gamepadInput->GamepadInputData.RightStickY;
this.ButtonsRaw = input->ButtonsRaw; this.ButtonsRaw = (GamepadButtons)gamepadInput->GamepadInputData.Buttons;
this.ButtonsPressed = input->ButtonsPressed; this.ButtonsPressed = (GamepadButtons)gamepadInput->GamepadInputData.ButtonsPressed;
this.ButtonsReleased = input->ButtonsReleased; this.ButtonsReleased = (GamepadButtons)gamepadInput->GamepadInputData.ButtonsReleased;
this.ButtonsRepeat = input->ButtonsRepeat; this.ButtonsRepeat = (GamepadButtons)gamepadInput->GamepadInputData.ButtonsRepeat;
if (this.NavEnableGamepad) if (this.NavEnableGamepad)
{ {
input->LeftStickX = 0; gamepadInput->GamepadInputData.LeftStickX = 0;
input->LeftStickY = 0; gamepadInput->GamepadInputData.LeftStickY = 0;
input->RightStickX = 0; gamepadInput->GamepadInputData.RightStickX = 0;
input->RightStickY = 0; gamepadInput->GamepadInputData.RightStickY = 0;
// NOTE (Chiv) Zeroing `ButtonsRaw` destroys `ButtonPressed`, `ButtonReleased` // NOTE (Chiv) Zeroing `ButtonsRaw` destroys `ButtonPressed`, `ButtonReleased`
// and `ButtonRepeat` as the game uses the RAW input to determine those (apparently). // and `ButtonRepeat` as the game uses the RAW input to determine those (apparently).
@ -153,16 +150,16 @@ internal unsafe class GamepadState : IInternalDisposableService, IGamepadState
// `ButtonPressed` while ImGuiConfigFlags.NavEnableGamepad is set. // `ButtonPressed` while ImGuiConfigFlags.NavEnableGamepad is set.
// This is debatable. // This is debatable.
// ImGui itself does not care either way as it uses the Raw values and does its own state handling. // ImGui itself does not care either way as it uses the Raw values and does its own state handling.
const ushort deletionMask = (ushort)(~GamepadButtons.L2 const GamepadButtonsFlags deletionMask = ~GamepadButtonsFlags.L2
& ~GamepadButtons.R2 & ~GamepadButtonsFlags.R2
& ~GamepadButtons.DpadDown & ~GamepadButtonsFlags.DPadDown
& ~GamepadButtons.DpadLeft & ~GamepadButtonsFlags.DPadLeft
& ~GamepadButtons.DpadUp & ~GamepadButtonsFlags.DPadUp
& ~GamepadButtons.DpadRight); & ~GamepadButtonsFlags.DPadRight;
input->ButtonsRaw &= deletionMask; gamepadInput->GamepadInputData.Buttons &= deletionMask;
input->ButtonsPressed = 0; gamepadInput->GamepadInputData.ButtonsPressed = 0;
input->ButtonsReleased = 0; gamepadInput->GamepadInputData.ButtonsReleased = 0;
input->ButtonsRepeat = 0; gamepadInput->GamepadInputData.ButtonsRepeat = 0;
return 0; return 0;
} }

View file

@ -8,20 +8,20 @@ public enum BeastChakra : byte
/// <summary> /// <summary>
/// No chakra. /// No chakra.
/// </summary> /// </summary>
NONE = 0, None = 0,
/// <summary> /// <summary>
/// The Opo-Opo chakra. /// The Opo-Opo chakra.
/// </summary> /// </summary>
OPOOPO = 1, OpoOpo = 1,
/// <summary> /// <summary>
/// The Raptor chakra. /// The Raptor chakra.
/// </summary> /// </summary>
RAPTOR = 2, Raptor = 2,
/// <summary> /// <summary>
/// The Coeurl chakra. /// The Coeurl chakra.
/// </summary> /// </summary>
COEURL = 3, Coeurl = 3,
} }

View file

@ -8,45 +8,45 @@ public enum CardType : byte
/// <summary> /// <summary>
/// No card. /// No card.
/// </summary> /// </summary>
NONE = 0, None = 0,
/// <summary> /// <summary>
/// The Balance card. /// The Balance card.
/// </summary> /// </summary>
BALANCE = 1, Balance = 1,
/// <summary> /// <summary>
/// The Bole card. /// The Bole card.
/// </summary> /// </summary>
BOLE = 2, Bole = 2,
/// <summary> /// <summary>
/// The Arrow card. /// The Arrow card.
/// </summary> /// </summary>
ARROW = 3, Arrow = 3,
/// <summary> /// <summary>
/// The Spear card. /// The Spear card.
/// </summary> /// </summary>
SPEAR = 4, Spear = 4,
/// <summary> /// <summary>
/// The Ewer card. /// The Ewer card.
/// </summary> /// </summary>
EWER = 5, Ewer = 5,
/// <summary> /// <summary>
/// The Spire card. /// The Spire card.
/// </summary> /// </summary>
SPIRE = 6, Spire = 6,
/// <summary> /// <summary>
/// The Lord of Crowns card. /// The Lord of Crowns card.
/// </summary> /// </summary>
LORD = 7, Lord = 7,
/// <summary> /// <summary>
/// The Lady of Crowns card. /// The Lady of Crowns card.
/// </summary> /// </summary>
LADY = 8, Lady = 8,
} }

View file

@ -0,0 +1,22 @@
namespace Dalamud.Game.ClientState.JobGauge.Enums;
/// <summary>
/// Enum representing the current step of Delirium.
/// </summary>
public enum DeliriumStep
{
/// <summary>
/// Scarlet Delirium can be used.
/// </summary>
ScarletDelirium = 0,
/// <summary>
/// Comeuppance can be used.
/// </summary>
Comeuppance = 1,
/// <summary>
/// Torcleaver can be used.
/// </summary>
Torcleaver = 2,
}

View file

@ -8,10 +8,10 @@ public enum DismissedFairy : byte
/// <summary> /// <summary>
/// Dismissed fairy is Eos. /// Dismissed fairy is Eos.
/// </summary> /// </summary>
EOS = 6, Eos = 6,
/// <summary> /// <summary>
/// Dismissed fairy is Selene. /// Dismissed fairy is Selene.
/// </summary> /// </summary>
SELENE = 7, Selene = 7,
} }

View file

@ -8,10 +8,10 @@ public enum DrawType : byte
/// <summary> /// <summary>
/// Astral Draw active. /// Astral Draw active.
/// </summary> /// </summary>
ASTRAL = 0, Astral = 0,
/// <summary> /// <summary>
/// Umbral Draw active. /// Umbral Draw active.
/// </summary> /// </summary>
UMBRAL = 1, Umbral = 1,
} }

View file

@ -8,25 +8,25 @@ public enum Kaeshi : byte
/// <summary> /// <summary>
/// No Kaeshi is active. /// No Kaeshi is active.
/// </summary> /// </summary>
NONE = 0, None = 0,
/// <summary> /// <summary>
/// Kaeshi: Higanbana type. /// Kaeshi: Higanbana type.
/// </summary> /// </summary>
HIGANBANA = 1, Higanbana = 1,
/// <summary> /// <summary>
/// Kaeshi: Goken type. /// Kaeshi: Goken type.
/// </summary> /// </summary>
GOKEN = 2, Goken = 2,
/// <summary> /// <summary>
/// Kaeshi: Setsugekka type. /// Kaeshi: Setsugekka type.
/// </summary> /// </summary>
SETSUGEKKA = 3, Setsugekka = 3,
/// <summary> /// <summary>
/// Kaeshi: Namikiri type. /// Kaeshi: Namikiri type.
/// </summary> /// </summary>
NAMIKIRI = 4, Namikiri = 4,
} }

View file

@ -8,15 +8,15 @@ public enum Mudras : byte
/// <summary> /// <summary>
/// Ten mudra. /// Ten mudra.
/// </summary> /// </summary>
TEN = 1, Ten = 1,
/// <summary> /// <summary>
/// Chi mudra. /// Chi mudra.
/// </summary> /// </summary>
CHI = 2, Chi = 2,
/// <summary> /// <summary>
/// Jin mudra. /// Jin mudra.
/// </summary> /// </summary>
JIN = 3, Jin = 3,
} }

View file

@ -9,15 +9,15 @@ public enum Nadi : byte
/// <summary> /// <summary>
/// No nadi. /// No nadi.
/// </summary> /// </summary>
NONE = 0, None = 0,
/// <summary> /// <summary>
/// The Lunar nadi. /// The Lunar nadi.
/// </summary> /// </summary>
LUNAR = 1, Lunar = 1,
/// <summary> /// <summary>
/// The Solar nadi. /// The Solar nadi.
/// </summary> /// </summary>
SOLAR = 2, Solar = 2,
} }

View file

@ -8,40 +8,40 @@ public enum PetGlam : byte
/// <summary> /// <summary>
/// No pet glam. /// No pet glam.
/// </summary> /// </summary>
NONE = 0, None = 0,
/// <summary> /// <summary>
/// Emerald carbuncle pet glam. /// Emerald carbuncle pet glam.
/// </summary> /// </summary>
EMERALD = 1, Emerald = 1,
/// <summary> /// <summary>
/// Topaz carbuncle pet glam. /// Topaz carbuncle pet glam.
/// </summary> /// </summary>
TOPAZ = 2, Topaz = 2,
/// <summary> /// <summary>
/// Ruby carbuncle pet glam. /// Ruby carbuncle pet glam.
/// </summary> /// </summary>
RUBY = 3, Ruby = 3,
/// <summary> /// <summary>
/// Normal carbuncle pet glam. /// Normal carbuncle pet glam.
/// </summary> /// </summary>
CARBUNCLE = 4, Carbuncle = 4,
/// <summary> /// <summary>
/// Ifrit Egi pet glam. /// Ifrit Egi pet glam.
/// </summary> /// </summary>
IFRIT = 5, Ifrit = 5,
/// <summary> /// <summary>
/// Titan Egi pet glam. /// Titan Egi pet glam.
/// </summary> /// </summary>
TITAN = 6, Titan = 6,
/// <summary> /// <summary>
/// Garuda Egi pet glam. /// Garuda Egi pet glam.
/// </summary> /// </summary>
GARUDA = 7, Garuda = 7,
} }

View file

@ -9,20 +9,20 @@ public enum Sen : byte
/// <summary> /// <summary>
/// No Sen. /// No Sen.
/// </summary> /// </summary>
NONE = 0, None = 0,
/// <summary> /// <summary>
/// Setsu Sen type. /// Setsu Sen type.
/// </summary> /// </summary>
SETSU = 1 << 0, Setsu = 1 << 0,
/// <summary> /// <summary>
/// Getsu Sen type. /// Getsu Sen type.
/// </summary> /// </summary>
GETSU = 1 << 1, Getsu = 1 << 1,
/// <summary> /// <summary>
/// Ka Sen type. /// Ka Sen type.
/// </summary> /// </summary>
KA = 1 << 2, Ka = 1 << 2,
} }

View file

@ -8,35 +8,35 @@ public enum SerpentCombo : byte
/// <summary> /// <summary>
/// No Serpent combo is active. /// No Serpent combo is active.
/// </summary> /// </summary>
NONE = 0, None = 0,
/// <summary> /// <summary>
/// Death Rattle action. /// Death Rattle action.
/// </summary> /// </summary>
DEATHRATTLE = 1, DeathRattle = 1,
/// <summary> /// <summary>
/// Last Lash action. /// Last Lash action.
/// </summary> /// </summary>
LASTLASH = 2, LastLash = 2,
/// <summary> /// <summary>
/// First Legacy action. /// First Legacy action.
/// </summary> /// </summary>
FIRSTLEGACY = 3, FirstLegacy = 3,
/// <summary> /// <summary>
/// Second Legacy action. /// Second Legacy action.
/// </summary> /// </summary>
SECONDLEGACY = 4, SecondLegacy = 4,
/// <summary> /// <summary>
/// Third Legacy action. /// Third Legacy action.
/// </summary> /// </summary>
THIRDLEGACY = 5, ThirdLegacy = 5,
/// <summary> /// <summary>
/// Fourth Legacy action. /// Fourth Legacy action.
/// </summary> /// </summary>
FOURTHLEGACY = 6, FourthLegacy = 6,
} }

View file

@ -8,20 +8,20 @@ public enum Song : byte
/// <summary> /// <summary>
/// No song is active type. /// No song is active type.
/// </summary> /// </summary>
NONE = 0, None = 0,
/// <summary> /// <summary>
/// Mage's Ballad type. /// Mage's Ballad type.
/// </summary> /// </summary>
MAGE = 1, Mage = 1,
/// <summary> /// <summary>
/// Army's Paeon type. /// Army's Paeon type.
/// </summary> /// </summary>
ARMY = 2, Army = 2,
/// <summary> /// <summary>
/// The Wanderer's Minuet type. /// The Wanderer's Minuet type.
/// </summary> /// </summary>
WANDERER = 3, Wanderer = 3,
} }

View file

@ -0,0 +1,30 @@
namespace Dalamud.Game.ClientState.JobGauge.Enums;
/// <summary>
/// Enum representing the current attunement of a summoner.
/// </summary>
public enum SummonAttunement
{
/// <summary>
/// No attunement.
/// </summary>
None = 0,
/// <summary>
/// Attuned to the summon Ifrit.
/// Same as <see cref="JobGauge.Types.SMNGauge.IsIfritAttuned"/>.
/// </summary>
Ifrit = 1,
/// <summary>
/// Attuned to the summon Titan.
/// Same as <see cref="JobGauge.Types.SMNGauge.IsTitanAttuned"/>.
/// </summary>
Titan = 2,
/// <summary>
/// Attuned to the summon Garuda.
/// Same as <see cref="JobGauge.Types.SMNGauge.IsGarudaAttuned"/>.
/// </summary>
Garuda = 3,
}

View file

@ -8,10 +8,10 @@ public enum SummonPet : byte
/// <summary> /// <summary>
/// No pet. /// No pet.
/// </summary> /// </summary>
NONE = 0, None = 0,
/// <summary> /// <summary>
/// The summoned pet Carbuncle. /// The summoned pet Carbuncle.
/// </summary> /// </summary>
CARBUNCLE = 23, Carbuncle = 23,
} }

View file

@ -5,9 +5,8 @@ using Dalamud.Game.ClientState.JobGauge.Types;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog; using CSJobGaugeManager = FFXIVClientStructs.FFXIV.Client.Game.JobGaugeManager;
namespace Dalamud.Game.ClientState.JobGauge; namespace Dalamud.Game.ClientState.JobGauge;
@ -21,18 +20,15 @@ namespace Dalamud.Game.ClientState.JobGauge;
#pragma warning restore SA1015 #pragma warning restore SA1015
internal class JobGauges : IServiceType, IJobGauges internal class JobGauges : IServiceType, IJobGauges
{ {
private Dictionary<Type, JobGaugeBase> cache = new(); private Dictionary<Type, JobGaugeBase> cache = [];
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private JobGauges(ClientState clientState) private JobGauges()
{ {
this.Address = clientState.AddressResolver.JobGaugeData;
Log.Verbose($"JobGaugeData address {Util.DescribeAddress(this.Address)}");
} }
/// <inheritdoc/> /// <inheritdoc/>
public IntPtr Address { get; } public unsafe IntPtr Address => (nint)(&CSJobGaugeManager.Instance()->EmptyGauge);
/// <inheritdoc/> /// <inheritdoc/>
public T Get<T>() where T : JobGaugeBase public T Get<T>() where T : JobGaugeBase

View file

@ -19,11 +19,6 @@ public unsafe class BLMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game
/// </summary> /// </summary>
public short EnochianTimer => this.Struct->EnochianTimer; public short EnochianTimer => this.Struct->EnochianTimer;
/// <summary>
/// Gets the time remaining for Astral Fire or Umbral Ice in milliseconds.
/// </summary>
public short ElementTimeRemaining => this.Struct->ElementTimeRemaining;
/// <summary> /// <summary>
/// Gets the number of Polyglot stacks remaining. /// Gets the number of Polyglot stacks remaining.
/// </summary> /// </summary>

View file

@ -40,15 +40,15 @@ public unsafe class BRDGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game
get get
{ {
if (this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuet)) if (this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuet))
return Song.WANDERER; return Song.Wanderer;
if (this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeon)) if (this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeon))
return Song.ARMY; return Song.Army;
if (this.Struct->SongFlags.HasFlag(SongFlags.MagesBallad)) if (this.Struct->SongFlags.HasFlag(SongFlags.MagesBallad))
return Song.MAGE; return Song.Mage;
return Song.NONE; return Song.None;
} }
} }
@ -60,15 +60,15 @@ public unsafe class BRDGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game
get get
{ {
if (this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuetLastPlayed)) if (this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuetLastPlayed))
return Song.WANDERER; return Song.Wanderer;
if (this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeonLastPlayed)) if (this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeonLastPlayed))
return Song.ARMY; return Song.Army;
if (this.Struct->SongFlags.HasFlag(SongFlags.MagesBalladLastPlayed)) if (this.Struct->SongFlags.HasFlag(SongFlags.MagesBalladLastPlayed))
return Song.MAGE; return Song.Mage;
return Song.NONE; return Song.None;
} }
} }
@ -76,7 +76,7 @@ public unsafe class BRDGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game
/// Gets the song Coda that are currently active. /// Gets the song Coda that are currently active.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This will always return an array of size 3, inactive Coda are represented by <see cref="Song.NONE"/>. /// This will always return an array of size 3, inactive Coda are represented by <see cref="Enums.Song.None"/>.
/// </remarks> /// </remarks>
public Song[] Coda public Song[] Coda
{ {
@ -84,9 +84,9 @@ public unsafe class BRDGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game
{ {
return new[] return new[]
{ {
this.Struct->SongFlags.HasFlag(SongFlags.MagesBalladCoda) ? Song.MAGE : Song.NONE, this.Struct->SongFlags.HasFlag(SongFlags.MagesBalladCoda) ? Song.Mage : Song.None,
this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeonCoda) ? Song.ARMY : Song.NONE, this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeonCoda) ? Song.Army : Song.None,
this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuetCoda) ? Song.WANDERER : Song.NONE, this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuetCoda) ? Song.Wanderer : Song.None,
}; };
} }
} }

View file

@ -1,9 +1,12 @@
using Dalamud.Game.ClientState.JobGauge.Enums;
using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary> /// <summary>
/// In-memory DRK job gauge. /// In-memory DRK job gauge.
/// </summary> /// </summary>
public unsafe class DRKGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.DarkKnightGauge> public unsafe class DRKGauge : JobGaugeBase<DarkKnightGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DRKGauge"/> class. /// Initializes a new instance of the <see cref="DRKGauge"/> class.
@ -34,4 +37,16 @@ public unsafe class DRKGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game
/// </summary> /// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns> /// <returns><c>true</c> or <c>false</c>.</returns>
public bool HasDarkArts => this.Struct->DarkArtsState > 0; public bool HasDarkArts => this.Struct->DarkArtsState > 0;
/// <summary>
/// Gets the step of the Delirium Combo (Scarlet Delirium, Comeuppance,
/// Torcleaver) that the player is on.<br/>
/// Does not in any way consider whether the player is still under Delirium, or
/// if the player still has stacks of Delirium to use.
/// </summary>
/// <remarks>
/// Value will persist until combo is finished OR
/// if the combo is not completed then the value will stay until about halfway into Delirium's cooldown.
/// </remarks>
public DeliriumStep DeliriumComboStep => (DeliriumStep)this.Struct->DeliriumStep;
} }

View file

@ -27,7 +27,7 @@ public unsafe class MNKGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game
/// Gets the types of Beast Chakra available. /// Gets the types of Beast Chakra available.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This will always return an array of size 3, inactive Beast Chakra are represented by <see cref="BeastChakra.NONE"/>. /// This will always return an array of size 3, inactive Beast Chakra are represented by <see cref="Enums.BeastChakra.None"/>.
/// </remarks> /// </remarks>
public BeastChakra[] BeastChakra => this.Struct->BeastChakra.Select(c => (BeastChakra)c).ToArray(); public BeastChakra[] BeastChakra => this.Struct->BeastChakra.Select(c => (BeastChakra)c).ToArray();

View file

@ -40,17 +40,17 @@ public unsafe class SAMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game
/// Gets a value indicating whether the Setsu Sen is active. /// Gets a value indicating whether the Setsu Sen is active.
/// </summary> /// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns> /// <returns><c>true</c> or <c>false</c>.</returns>
public bool HasSetsu => (this.Sen & Sen.SETSU) != 0; public bool HasSetsu => (this.Sen & Sen.Setsu) != 0;
/// <summary> /// <summary>
/// Gets a value indicating whether the Getsu Sen is active. /// Gets a value indicating whether the Getsu Sen is active.
/// </summary> /// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns> /// <returns><c>true</c> or <c>false</c>.</returns>
public bool HasGetsu => (this.Sen & Sen.GETSU) != 0; public bool HasGetsu => (this.Sen & Sen.Getsu) != 0;
/// <summary> /// <summary>
/// Gets a value indicating whether the Ka Sen is active. /// Gets a value indicating whether the Ka Sen is active.
/// </summary> /// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns> /// <returns><c>true</c> or <c>false</c>.</returns>
public bool HasKa => (this.Sen & Sen.KA) != 0; public bool HasKa => (this.Sen & Sen.Ka) != 0;
} }

View file

@ -25,25 +25,46 @@ public unsafe class SMNGauge : JobGaugeBase<SummonerGauge>
/// <summary> /// <summary>
/// Gets the time remaining for the current attunement. /// Gets the time remaining for the current attunement.
/// </summary> /// </summary>
public ushort AttunmentTimerRemaining => this.Struct->AttunementTimer; [Obsolete("Typo fixed. Use AttunementTimerRemaining instead.", true)]
public ushort AttunmentTimerRemaining => this.AttunementTimerRemaining;
/// <summary>
/// Gets the time remaining for the current attunement.
/// </summary>
public ushort AttunementTimerRemaining => this.Struct->AttunementTimer;
/// <summary> /// <summary>
/// Gets the summon that will return after the current summon expires. /// Gets the summon that will return after the current summon expires.
/// This maps to the <see cref="Lumina.Excel.GeneratedSheets.Pet"/> sheet. /// This maps to the <see cref="Lumina.Excel.Sheets.Pet"/> sheet.
/// </summary> /// </summary>
public SummonPet ReturnSummon => (SummonPet)this.Struct->ReturnSummon; public SummonPet ReturnSummon => (SummonPet)this.Struct->ReturnSummon;
/// <summary> /// <summary>
/// Gets the summon glam for the <see cref="ReturnSummon"/>. /// Gets the summon glam for the <see cref="ReturnSummon"/>.
/// This maps to the <see cref="Lumina.Excel.GeneratedSheets.PetMirage"/> sheet. /// This maps to the <see cref="Lumina.Excel.Sheets.PetMirage"/> sheet.
/// </summary> /// </summary>
public PetGlam ReturnSummonGlam => (PetGlam)this.Struct->ReturnSummonGlam; public PetGlam ReturnSummonGlam => (PetGlam)this.Struct->ReturnSummonGlam;
/// <summary> /// <summary>
/// Gets the amount of aspected Attunment remaining. /// Gets the amount of aspected Attunement remaining.
/// </summary> /// </summary>
/// <remarks>
/// As of 7.01, this should be treated as a bit field.
/// Use <see cref="AttunementCount"/> and <see cref="AttunementType"/> instead.
/// </remarks>
public byte Attunement => this.Struct->Attunement; public byte Attunement => this.Struct->Attunement;
/// <summary>
/// Gets the count of attunement cost resource available.
/// </summary>
public byte AttunementCount => this.Struct->AttunementCount;
/// <summary>
/// Gets the type of attunement available.
/// Use the summon attuned accessors instead.
/// </summary>
public SummonAttunement AttunementType => (SummonAttunement)this.Struct->AttunementType;
/// <summary> /// <summary>
/// Gets the current aether flags. /// Gets the current aether flags.
/// Use the summon accessors instead. /// Use the summon accessors instead.
@ -84,19 +105,19 @@ public unsafe class SMNGauge : JobGaugeBase<SummonerGauge>
/// Gets a value indicating whether if Ifrit is currently attuned. /// Gets a value indicating whether if Ifrit is currently attuned.
/// </summary> /// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns> /// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsIfritAttuned => this.AetherFlags.HasFlag(AetherFlags.IfritAttuned) && !this.AetherFlags.HasFlag(AetherFlags.GarudaAttuned); public bool IsIfritAttuned => this.AttunementType == SummonAttunement.Ifrit;
/// <summary> /// <summary>
/// Gets a value indicating whether if Titan is currently attuned. /// Gets a value indicating whether if Titan is currently attuned.
/// </summary> /// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns> /// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsTitanAttuned => this.AetherFlags.HasFlag(AetherFlags.TitanAttuned) && !this.AetherFlags.HasFlag(AetherFlags.GarudaAttuned); public bool IsTitanAttuned => this.AttunementType == SummonAttunement.Titan;
/// <summary> /// <summary>
/// Gets a value indicating whether if Garuda is currently attuned. /// Gets a value indicating whether if Garuda is currently attuned.
/// </summary> /// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns> /// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsGarudaAttuned => this.AetherFlags.HasFlag(AetherFlags.GarudaAttuned); public bool IsGarudaAttuned => this.AttunementType == SummonAttunement.Garuda;
/// <summary> /// <summary>
/// Gets a value indicating whether there are any Aetherflow stacks available. /// Gets a value indicating whether there are any Aetherflow stacks available.

View file

@ -27,7 +27,12 @@ public enum BattleNpcSubKind : byte
Chocobo = 3, Chocobo = 3,
/// <summary> /// <summary>
/// BattleNpc representing a standard enemy. /// BattleNpc representing a standard enemy. This includes allies (overworld guards and allies in single-player duties).
/// </summary> /// </summary>
Enemy = 5, Enemy = 5,
/// <summary>
/// BattleNpc representing an NPC party member (from Duty Support, Trust, or Grand Company Command Mission).
/// </summary>
NpcPartyMember = 9,
} }

View file

@ -7,15 +7,15 @@ using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.Interop;
using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.ObjectPool;
using Serilog;
using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using CSGameObjectManager = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager;
namespace Dalamud.Game.ClientState.Objects; namespace Dalamud.Game.ClientState.Objects;
@ -29,62 +29,58 @@ namespace Dalamud.Game.ClientState.Objects;
#pragma warning restore SA1015 #pragma warning restore SA1015
internal sealed partial class ObjectTable : IServiceType, IObjectTable internal sealed partial class ObjectTable : IServiceType, IObjectTable
{ {
private const int ObjectTableLength = 599; private static int objectTableLength;
private readonly ClientState clientState; private readonly ClientState clientState;
private readonly CachedEntry[] cachedObjectTable = new CachedEntry[ObjectTableLength]; private readonly CachedEntry[] cachedObjectTable;
private readonly ObjectPool<Enumerator> multiThreadedEnumerators =
new DefaultObjectPoolProvider().Create<Enumerator>();
private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4]; private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4];
private long nextMultithreadedUsageWarnTime;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private unsafe ObjectTable(ClientState clientState) private unsafe ObjectTable(ClientState clientState)
{ {
this.clientState = clientState; this.clientState = clientState;
var nativeObjectTableAddress = (CSGameObject**)this.clientState.AddressResolver.ObjectTable; var nativeObjectTable = CSGameObjectManager.Instance()->Objects.IndexSorted;
objectTableLength = nativeObjectTable.Length;
this.cachedObjectTable = new CachedEntry[objectTableLength];
for (var i = 0; i < this.cachedObjectTable.Length; i++) for (var i = 0; i < this.cachedObjectTable.Length; i++)
this.cachedObjectTable[i] = new(nativeObjectTableAddress, i); this.cachedObjectTable[i] = new(nativeObjectTable.GetPointer(i));
for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++) for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++)
this.frameworkThreadEnumerators[i] = new(this, i); this.frameworkThreadEnumerators[i] = new(this, i);
Log.Verbose($"Object table address {Util.DescribeAddress(this.clientState.AddressResolver.ObjectTable)}");
} }
/// <inheritdoc/> /// <inheritdoc/>
public nint Address public unsafe nint Address
{ {
get get
{ {
_ = this.WarnMultithreadedUsage(); ThreadSafety.AssertMainThread();
return this.clientState.AddressResolver.ObjectTable; return (nint)(&CSGameObjectManager.Instance()->Objects);
} }
} }
/// <inheritdoc/> /// <inheritdoc/>
public int Length => ObjectTableLength; public int Length => objectTableLength;
/// <inheritdoc/> /// <inheritdoc/>
public IGameObject? this[int index] public IGameObject? this[int index]
{ {
get get
{ {
_ = this.WarnMultithreadedUsage(); ThreadSafety.AssertMainThread();
return index is >= ObjectTableLength or < 0 ? null : this.cachedObjectTable[index].Update(); return (index >= objectTableLength || index < 0) ? null : this.cachedObjectTable[index].Update();
} }
} }
/// <inheritdoc/> /// <inheritdoc/>
public IGameObject? SearchById(ulong gameObjectId) public IGameObject? SearchById(ulong gameObjectId)
{ {
_ = this.WarnMultithreadedUsage(); ThreadSafety.AssertMainThread();
if (gameObjectId is 0) if (gameObjectId is 0)
return null; return null;
@ -101,7 +97,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
/// <inheritdoc/> /// <inheritdoc/>
public IGameObject? SearchByEntityId(uint entityId) public IGameObject? SearchByEntityId(uint entityId)
{ {
_ = this.WarnMultithreadedUsage(); ThreadSafety.AssertMainThread();
if (entityId is 0 or 0xE0000000) if (entityId is 0 or 0xE0000000)
return null; return null;
@ -118,15 +114,15 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
/// <inheritdoc/> /// <inheritdoc/>
public unsafe nint GetObjectAddress(int index) public unsafe nint GetObjectAddress(int index)
{ {
_ = this.WarnMultithreadedUsage(); ThreadSafety.AssertMainThread();
return index is < 0 or >= ObjectTableLength ? nint.Zero : (nint)this.cachedObjectTable[index].Address; return (index >= objectTableLength || index < 0) ? nint.Zero : (nint)this.cachedObjectTable[index].Address;
} }
/// <inheritdoc/> /// <inheritdoc/>
public unsafe IGameObject? CreateObjectReference(nint address) public unsafe IGameObject? CreateObjectReference(nint address)
{ {
_ = this.WarnMultithreadedUsage(); ThreadSafety.AssertMainThread();
if (this.clientState.LocalContentId == 0) if (this.clientState.LocalContentId == 0)
return null; return null;
@ -150,55 +146,22 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
}; };
} }
[Api10ToDo("Use ThreadSafety.AssertMainThread() instead of this.")]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool WarnMultithreadedUsage()
{
if (ThreadSafety.IsMainThread)
return false;
var n = Environment.TickCount64;
if (this.nextMultithreadedUsageWarnTime < n)
{
this.nextMultithreadedUsageWarnTime = n + 30000;
Log.Warning(
"{plugin} is accessing {objectTable} outside the main thread. This is deprecated.",
Service<PluginManager>.Get().FindCallingPlugin()?.Name ?? "<unknown plugin>",
nameof(ObjectTable));
}
return true;
}
/// <summary>Stores an object table entry, with preallocated concrete types.</summary> /// <summary>Stores an object table entry, with preallocated concrete types.</summary>
internal readonly unsafe struct CachedEntry /// <remarks>Initializes a new instance of the <see cref="CachedEntry"/> struct.</remarks>
/// <param name="gameObjectPtr">A pointer to the object table entry this entry should be pointing to.</param>
internal readonly unsafe struct CachedEntry(Pointer<CSGameObject>* gameObjectPtr)
{ {
private readonly CSGameObject** gameObjectPtrPtr; private readonly PlayerCharacter playerCharacter = new(nint.Zero);
private readonly PlayerCharacter playerCharacter; private readonly BattleNpc battleNpc = new(nint.Zero);
private readonly BattleNpc battleNpc; private readonly Npc npc = new(nint.Zero);
private readonly Npc npc; private readonly EventObj eventObj = new(nint.Zero);
private readonly EventObj eventObj; private readonly GameObject gameObject = new(nint.Zero);
private readonly GameObject gameObject;
/// <summary>Initializes a new instance of the <see cref="CachedEntry"/> struct.</summary>
/// <param name="ownerTable">The object table that this entry should be pointing to.</param>
/// <param name="slot">The slot index inside the table.</param>
public CachedEntry(CSGameObject** ownerTable, int slot)
{
this.gameObjectPtrPtr = ownerTable + slot;
this.playerCharacter = new(nint.Zero);
this.battleNpc = new(nint.Zero);
this.npc = new(nint.Zero);
this.eventObj = new(nint.Zero);
this.gameObject = new(nint.Zero);
}
/// <summary>Gets the address of the underlying native object. May be null.</summary> /// <summary>Gets the address of the underlying native object. May be null.</summary>
public CSGameObject* Address public CSGameObject* Address
{ {
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
get => *this.gameObjectPtrPtr; get => gameObjectPtr->Value;
} }
/// <summary>Updates and gets the wrapped game object pointed by this struct.</summary> /// <summary>Updates and gets the wrapped game object pointed by this struct.</summary>
@ -235,14 +198,7 @@ internal sealed partial class ObjectTable
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerator<IGameObject> GetEnumerator() public IEnumerator<IGameObject> GetEnumerator()
{ {
// If something's trying to enumerate outside the framework thread, we use the ObjectPool. ThreadSafety.AssertMainThread();
if (this.WarnMultithreadedUsage())
{
// let's not
var e = this.multiThreadedEnumerators.Get();
e.InitializeForPooledObjects(this);
return e;
}
// If we're on the framework thread, see if there's an already allocated enumerator available for use. // 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()) foreach (ref var x in this.frameworkThreadEnumerators.AsSpan())
@ -263,32 +219,23 @@ internal sealed partial class ObjectTable
/// <inheritdoc/> /// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
private sealed class Enumerator : IEnumerator<IGameObject>, IResettable private sealed class Enumerator(ObjectTable owner, int slotId) : IEnumerator<IGameObject>, IResettable
{ {
private readonly int slotId; private ObjectTable? owner = owner;
private ObjectTable? owner;
private int index = -1; private int index = -1;
public Enumerator() => this.slotId = -1;
public Enumerator(ObjectTable owner, int slotId)
{
this.owner = owner;
this.slotId = slotId;
}
public IGameObject Current { get; private set; } = null!; public IGameObject Current { get; private set; } = null!;
object IEnumerator.Current => this.Current; object IEnumerator.Current => this.Current;
public bool MoveNext() public bool MoveNext()
{ {
if (this.index == ObjectTableLength) if (this.index == objectTableLength)
return false; return false;
var cache = this.owner!.cachedObjectTable.AsSpan(); var cache = this.owner!.cachedObjectTable.AsSpan();
for (this.index++; this.index < ObjectTableLength; this.index++) for (this.index++; this.index < objectTableLength; this.index++)
{ {
if (cache[this.index].Update() is { } ao) if (cache[this.index].Update() is { } ao)
{ {
@ -300,8 +247,6 @@ internal sealed partial class ObjectTable
return false; return false;
} }
public void InitializeForPooledObjects(ObjectTable ot) => this.owner = ot;
public void Reset() => this.index = -1; public void Reset() => this.index = -1;
public void Dispose() public void Dispose()
@ -309,10 +254,8 @@ internal sealed partial class ObjectTable
if (this.owner is not { } o) if (this.owner is not { } o)
return; return;
if (this.slotId == -1) if (slotId != -1)
o.multiThreadedEnumerators.Return(this); o.frameworkThreadEnumerators[slotId] = this;
else
o.frameworkThreadEnumerators[this.slotId] = this;
} }
public bool TryReset() public bool TryReset()

View file

@ -1,12 +1,8 @@
using System.Numerics; using Dalamud.Data;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.ClientState.Statuses;
using Dalamud.Game.Text.SeStringHandling;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace Dalamud.Game.ClientState.Objects.SubKinds; namespace Dalamud.Game.ClientState.Objects.SubKinds;
@ -16,14 +12,14 @@ namespace Dalamud.Game.ClientState.Objects.SubKinds;
public interface IPlayerCharacter : IBattleChara public interface IPlayerCharacter : IBattleChara
{ {
/// <summary> /// <summary>
/// Gets the current <see cref="ExcelResolver{T}">world</see> of the character. /// Gets the current <see cref="RowRef{T}">world</see> of the character.
/// </summary> /// </summary>
ExcelResolver<World> CurrentWorld { get; } RowRef<World> CurrentWorld { get; }
/// <summary> /// <summary>
/// Gets the home <see cref="ExcelResolver{T}">world</see> of the character. /// Gets the home <see cref="RowRef{T}">world</see> of the character.
/// </summary> /// </summary>
ExcelResolver<World> HomeWorld { get; } RowRef<World> HomeWorld { get; }
} }
/// <summary> /// <summary>
@ -42,10 +38,10 @@ internal unsafe class PlayerCharacter : BattleChara, IPlayerCharacter
} }
/// <inheritdoc/> /// <inheritdoc/>
public ExcelResolver<World> CurrentWorld => new(this.Struct->CurrentWorld); public RowRef<World> CurrentWorld => LuminaUtils.CreateRef<World>(this.Struct->CurrentWorld);
/// <inheritdoc/> /// <inheritdoc/>
public ExcelResolver<World> HomeWorld => new(this.Struct->HomeWorld); public RowRef<World> HomeWorld => LuminaUtils.CreateRef<World>(this.Struct->HomeWorld);
/// <summary> /// <summary>
/// Gets the target actor ID of the PlayerCharacter. /// Gets the target actor ID of the PlayerCharacter.

View file

@ -20,7 +20,7 @@ internal sealed unsafe class TargetManager : IServiceType, ITargetManager
{ {
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly ObjectTable objectTable = Service<ObjectTable>.Get(); private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private TargetManager() private TargetManager()
{ {
@ -29,8 +29,8 @@ internal sealed unsafe class TargetManager : IServiceType, ITargetManager
/// <inheritdoc/> /// <inheritdoc/>
public IGameObject? Target public IGameObject? Target
{ {
get => this.objectTable.CreateObjectReference((IntPtr)Struct->Target); get => this.objectTable.CreateObjectReference((IntPtr)Struct->GetHardTarget());
set => Struct->Target = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; set => Struct->SetHardTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -57,8 +57,8 @@ internal sealed unsafe class TargetManager : IServiceType, ITargetManager
/// <inheritdoc/> /// <inheritdoc/>
public IGameObject? SoftTarget public IGameObject? SoftTarget
{ {
get => this.objectTable.CreateObjectReference((IntPtr)Struct->SoftTarget); get => this.objectTable.CreateObjectReference((IntPtr)Struct->GetSoftTarget());
set => Struct->SoftTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; set => Struct->SetSoftTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -67,7 +67,7 @@ internal sealed unsafe class TargetManager : IServiceType, ITargetManager
get => this.objectTable.CreateObjectReference((IntPtr)Struct->GPoseTarget); get => this.objectTable.CreateObjectReference((IntPtr)Struct->GPoseTarget);
set => Struct->GPoseTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; set => Struct->GPoseTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
} }
/// <inheritdoc/> /// <inheritdoc/>
public IGameObject? MouseOverNameplateTarget public IGameObject? MouseOverNameplateTarget
{ {

View file

@ -1,10 +1,12 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory; using Dalamud.Memory;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace Dalamud.Game.ClientState.Objects.Types; namespace Dalamud.Game.ClientState.Objects.Types;
@ -61,7 +63,7 @@ public interface ICharacter : IGameObject
/// <summary> /// <summary>
/// Gets the ClassJob of this Chara. /// Gets the ClassJob of this Chara.
/// </summary> /// </summary>
public ExcelResolver<ClassJob> ClassJob { get; } public RowRef<ClassJob> ClassJob { get; }
/// <summary> /// <summary>
/// Gets the level of this Chara. /// Gets the level of this Chara.
@ -87,7 +89,7 @@ public interface ICharacter : IGameObject
/// <summary> /// <summary>
/// Gets the current online status of the character. /// Gets the current online status of the character.
/// </summary> /// </summary>
public ExcelResolver<OnlineStatus> OnlineStatus { get; } public RowRef<OnlineStatus> OnlineStatus { get; }
/// <summary> /// <summary>
/// Gets the status flags. /// Gets the status flags.
@ -97,14 +99,14 @@ public interface ICharacter : IGameObject
/// <summary> /// <summary>
/// Gets the current mount for this character. Will be <c>null</c> if the character doesn't have a mount. /// Gets the current mount for this character. Will be <c>null</c> if the character doesn't have a mount.
/// </summary> /// </summary>
public ExcelResolver<Mount>? CurrentMount { get; } public RowRef<Mount>? CurrentMount { get; }
/// <summary> /// <summary>
/// Gets the current minion summoned for this character. Will be <c>null</c> if the character doesn't have a minion. /// Gets the current minion summoned for this character. Will be <c>null</c> if the character doesn't have a minion.
/// This method *will* return information about a spawned (but invisible) minion, e.g. if the character is riding a /// This method *will* return information about a spawned (but invisible) minion, e.g. if the character is riding a
/// mount. /// mount.
/// </summary> /// </summary>
public ExcelResolver<Companion>? CurrentMinion { get; } public RowRef<Companion>? CurrentMinion { get; }
} }
/// <summary> /// <summary>
@ -150,7 +152,7 @@ internal unsafe class Character : GameObject, ICharacter
public byte ShieldPercentage => this.Struct->CharacterData.ShieldValue; public byte ShieldPercentage => this.Struct->CharacterData.ShieldValue;
/// <inheritdoc/> /// <inheritdoc/>
public ExcelResolver<ClassJob> ClassJob => new(this.Struct->CharacterData.ClassJob); public RowRef<ClassJob> ClassJob => LuminaUtils.CreateRef<ClassJob>(this.Struct->CharacterData.ClassJob);
/// <inheritdoc/> /// <inheritdoc/>
public byte Level => this.Struct->CharacterData.Level; public byte Level => this.Struct->CharacterData.Level;
@ -159,7 +161,7 @@ internal unsafe class Character : GameObject, ICharacter
public byte[] Customize => this.Struct->DrawData.CustomizeData.Data.ToArray(); public byte[] Customize => this.Struct->DrawData.CustomizeData.Data.ToArray();
/// <inheritdoc/> /// <inheritdoc/>
public SeString CompanyTag => MemoryHelper.ReadSeString((nint)Unsafe.AsPointer(ref this.Struct->FreeCompanyTag[0]), 6); public SeString CompanyTag => SeString.Parse(this.Struct->FreeCompanyTag);
/// <summary> /// <summary>
/// Gets the target object ID of the character. /// Gets the target object ID of the character.
@ -170,7 +172,7 @@ internal unsafe class Character : GameObject, ICharacter
public uint NameId => this.Struct->NameId; public uint NameId => this.Struct->NameId;
/// <inheritdoc/> /// <inheritdoc/>
public ExcelResolver<OnlineStatus> OnlineStatus => new(this.Struct->CharacterData.OnlineStatus); public RowRef<OnlineStatus> OnlineStatus => LuminaUtils.CreateRef<OnlineStatus>(this.Struct->CharacterData.OnlineStatus);
/// <summary> /// <summary>
/// Gets the status flags. /// Gets the status flags.
@ -186,28 +188,28 @@ internal unsafe class Character : GameObject, ICharacter
(this.Struct->IsCasting ? StatusFlags.IsCasting : StatusFlags.None); (this.Struct->IsCasting ? StatusFlags.IsCasting : StatusFlags.None);
/// <inheritdoc /> /// <inheritdoc />
public ExcelResolver<Mount>? CurrentMount public RowRef<Mount>? CurrentMount
{ {
get get
{ {
if (this.Struct->IsNotMounted()) return null; // just for safety. if (this.Struct->IsNotMounted()) return null; // just for safety.
var mountId = this.Struct->Mount.MountId; var mountId = this.Struct->Mount.MountId;
return mountId == 0 ? null : new ExcelResolver<Mount>(mountId); return mountId == 0 ? null : LuminaUtils.CreateRef<Mount>(mountId);
} }
} }
/// <inheritdoc /> /// <inheritdoc />
public ExcelResolver<Companion>? CurrentMinion public RowRef<Companion>? CurrentMinion
{ {
get get
{ {
if (this.Struct->CompanionObject != null) if (this.Struct->CompanionObject != null)
return new ExcelResolver<Companion>(this.Struct->CompanionObject->BaseId); return LuminaUtils.CreateRef<Companion>(this.Struct->CompanionObject->BaseId);
// this is only present if a minion is summoned but hidden (e.g. the player's on a mount). // this is only present if a minion is summoned but hidden (e.g. the player's on a mount).
var hiddenCompanionId = this.Struct->CompanionData.CompanionId; var hiddenCompanionId = this.Struct->CompanionData.CompanionId;
return hiddenCompanionId == 0 ? null : new ExcelResolver<Companion>(hiddenCompanionId); return hiddenCompanionId == 0 ? null : LuminaUtils.CreateRef<Companion>(hiddenCompanionId);
} }
} }

View file

@ -197,7 +197,7 @@ internal partial class GameObject
internal unsafe partial class GameObject : IGameObject internal unsafe partial class GameObject : IGameObject
{ {
/// <inheritdoc/> /// <inheritdoc/>
public SeString Name => MemoryHelper.ReadSeString((nint)Unsafe.AsPointer(ref this.Struct->Name[0]), 64); public SeString Name => SeString.Parse(this.Struct->Name);
/// <inheritdoc/> /// <inheritdoc/>
public ulong GameObjectId => this.Struct->GetGameObjectId(); public ulong GameObjectId => this.Struct->GetGameObjectId();

View file

@ -6,9 +6,8 @@ using System.Runtime.InteropServices;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog; using CSGroupManager = FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager;
namespace Dalamud.Game.ClientState.Party; namespace Dalamud.Game.ClientState.Party;
@ -28,14 +27,9 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get(); private readonly ClientState clientState = Service<ClientState>.Get();
private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private PartyList() private PartyList()
{ {
this.address = this.clientState.AddressResolver;
Log.Verbose($"Group manager address {Util.DescribeAddress(this.address.GroupManager)}");
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -48,7 +42,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
public bool IsAlliance => this.GroupManagerStruct->MainGroup.AllianceFlags > 0; public bool IsAlliance => this.GroupManagerStruct->MainGroup.AllianceFlags > 0;
/// <inheritdoc/> /// <inheritdoc/>
public IntPtr GroupManagerAddress => this.address.GroupManager; public unsafe IntPtr GroupManagerAddress => (nint)CSGroupManager.Instance();
/// <inheritdoc/> /// <inheritdoc/>
public IntPtr GroupListAddress => (IntPtr)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]); public IntPtr GroupListAddress => (IntPtr)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]);

View file

@ -1,13 +1,15 @@
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.ClientState.Statuses; using Dalamud.Game.ClientState.Statuses;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory; using Dalamud.Memory;
using Lumina.Excel;
namespace Dalamud.Game.ClientState.Party; namespace Dalamud.Game.ClientState.Party;
/// <summary> /// <summary>
@ -71,12 +73,12 @@ public interface IPartyMember
/// <summary> /// <summary>
/// Gets the territory this party member is located in. /// Gets the territory this party member is located in.
/// </summary> /// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.TerritoryType> Territory { get; } RowRef<Lumina.Excel.Sheets.TerritoryType> Territory { get; }
/// <summary> /// <summary>
/// Gets the World this party member resides in. /// Gets the World this party member resides in.
/// </summary> /// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.World> World { get; } RowRef<Lumina.Excel.Sheets.World> World { get; }
/// <summary> /// <summary>
/// Gets the displayname of this party member. /// Gets the displayname of this party member.
@ -91,7 +93,7 @@ public interface IPartyMember
/// <summary> /// <summary>
/// Gets the classjob of this party member. /// Gets the classjob of this party member.
/// </summary> /// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.ClassJob> ClassJob { get; } RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob { get; }
/// <summary> /// <summary>
/// Gets the level of this party member. /// Gets the level of this party member.
@ -169,17 +171,17 @@ internal unsafe class PartyMember : IPartyMember
/// <summary> /// <summary>
/// Gets the territory this party member is located in. /// Gets the territory this party member is located in.
/// </summary> /// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.TerritoryType> Territory => new(this.Struct->TerritoryType); public RowRef<Lumina.Excel.Sheets.TerritoryType> Territory => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->TerritoryType);
/// <summary> /// <summary>
/// Gets the World this party member resides in. /// Gets the World this party member resides in.
/// </summary> /// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.World> World => new(this.Struct->HomeWorld); public RowRef<Lumina.Excel.Sheets.World> World => LuminaUtils.CreateRef<Lumina.Excel.Sheets.World>(this.Struct->HomeWorld);
/// <summary> /// <summary>
/// Gets the displayname of this party member. /// Gets the displayname of this party member.
/// </summary> /// </summary>
public SeString Name => MemoryHelper.ReadSeString((nint)Unsafe.AsPointer(ref Struct->Name[0]), 0x40); public SeString Name => SeString.Parse(this.Struct->Name);
/// <summary> /// <summary>
/// Gets the sex of this party member. /// Gets the sex of this party member.
@ -189,7 +191,7 @@ internal unsafe class PartyMember : IPartyMember
/// <summary> /// <summary>
/// Gets the classjob of this party member. /// Gets the classjob of this party member.
/// </summary> /// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.ClassJob> ClassJob => new(this.Struct->ClassJob); public RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob => LuminaUtils.CreateRef<Lumina.Excel.Sheets.ClassJob>(this.Struct->ClassJob);
/// <summary> /// <summary>
/// Gets the level of this party member. /// Gets the level of this party member.

View file

@ -1,38 +0,0 @@
using Dalamud.Data;
using Lumina.Excel;
namespace Dalamud.Game.ClientState.Resolvers;
/// <summary>
/// This object resolves a rowID within an Excel sheet.
/// </summary>
/// <typeparam name="T">The type of Lumina sheet to resolve.</typeparam>
public class ExcelResolver<T> where T : ExcelRow
{
/// <summary>
/// Initializes a new instance of the <see cref="ExcelResolver{T}"/> class.
/// </summary>
/// <param name="id">The ID of the classJob.</param>
internal ExcelResolver(uint id)
{
this.Id = id;
}
/// <summary>
/// Gets the ID to be resolved.
/// </summary>
public uint Id { get; }
/// <summary>
/// Gets GameData linked to this excel row.
/// </summary>
public T? GameData => Service<DataManager>.Get().GetExcelSheet<T>()?.GetRow(this.Id);
/// <summary>
/// Gets GameData linked to this excel row with the specified language.
/// </summary>
/// <param name="language">The language.</param>
/// <returns>The ExcelRow in the specified language.</returns>
public T? GetWithLanguage(ClientLanguage language) => Service<DataManager>.Get().GetExcelSheet<T>(language)?.GetRow(this.Id);
}

View file

@ -1,6 +1,8 @@
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers;
using Lumina.Excel;
namespace Dalamud.Game.ClientState.Statuses; namespace Dalamud.Game.ClientState.Statuses;
@ -31,7 +33,7 @@ public unsafe class Status
/// <summary> /// <summary>
/// Gets the GameData associated with this status. /// Gets the GameData associated with this status.
/// </summary> /// </summary>
public Lumina.Excel.GeneratedSheets.Status GameData => new ExcelResolver<Lumina.Excel.GeneratedSheets.Status>(this.Struct->StatusId).GameData; public RowRef<Lumina.Excel.Sheets.Status> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Status>(this.Struct->StatusId);
/// <summary> /// <summary>
/// Gets the parameter value of the status. /// Gets the parameter value of the status.
@ -40,8 +42,10 @@ public unsafe class Status
/// <summary> /// <summary>
/// Gets the stack count of this status. /// Gets the stack count of this status.
/// Only valid if this is a non-food status.
/// </summary> /// </summary>
public byte StackCount => this.Struct->StackCount; [Obsolete($"Replaced with {nameof(Param)}", true)]
public byte StackCount => (byte)this.Struct->Param;
/// <summary> /// <summary>
/// Gets the time remaining of this status. /// Gets the time remaining of this status.

View file

@ -11,7 +11,7 @@ public interface IReadOnlyCommandInfo
/// <param name="command">The command itself.</param> /// <param name="command">The command itself.</param>
/// <param name="arguments">The arguments supplied to the command, ready for parsing.</param> /// <param name="arguments">The arguments supplied to the command, ready for parsing.</param>
public delegate void HandlerDelegate(string command, string arguments); public delegate void HandlerDelegate(string command, string arguments);
/// <summary> /// <summary>
/// Gets a <see cref="HandlerDelegate"/> which will be called when the command is dispatched. /// Gets a <see cref="HandlerDelegate"/> which will be called when the command is dispatched.
/// </summary> /// </summary>
@ -26,6 +26,11 @@ public interface IReadOnlyCommandInfo
/// Gets a value indicating whether if this command should be shown in the help output. /// Gets a value indicating whether if this command should be shown in the help output.
/// </summary> /// </summary>
bool ShowInHelp { get; } bool ShowInHelp { get; }
/// <summary>
/// Gets the display order of this command. Defaults to alphabetical ordering.
/// </summary>
int DisplayOrder { get; }
} }
/// <summary> /// <summary>
@ -51,4 +56,7 @@ public sealed class CommandInfo : IReadOnlyCommandInfo
/// <inheritdoc/> /// <inheritdoc/>
public bool ShowInHelp { get; set; } = true; public bool ShowInHelp { get; set; } = true;
/// <inheritdoc/>
public int DisplayOrder { get; set; } = -1;
} }

View file

@ -2,17 +2,19 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using Dalamud.Console; using Dalamud.Console;
using Dalamud.Game.Gui; using Dalamud.Hooking;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.Shell;
namespace Dalamud.Game.Command; namespace Dalamud.Game.Command;
@ -20,38 +22,26 @@ namespace Dalamud.Game.Command;
/// This class manages registered in-game slash commands. /// This class manages registered in-game slash commands.
/// </summary> /// </summary>
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal sealed class CommandManager : IInternalDisposableService, ICommandManager internal sealed unsafe class CommandManager : IInternalDisposableService, ICommandManager
{ {
private static readonly ModuleLog Log = new("Command"); private static readonly ModuleLog Log = new("Command");
private readonly ConcurrentDictionary<string, IReadOnlyCommandInfo> commandMap = new(); private readonly ConcurrentDictionary<string, IReadOnlyCommandInfo> commandMap = new();
private readonly ConcurrentDictionary<(string, IReadOnlyCommandInfo), string> commandAssemblyNameMap = new(); private readonly ConcurrentDictionary<(string, IReadOnlyCommandInfo), string> commandAssemblyNameMap = new();
private readonly Regex commandRegexEn = new(@"^The command (?<command>.+) does not exist\.$", RegexOptions.Compiled);
private readonly Regex commandRegexJp = new(@"^そのコマンドはありません。: (?<command>.+)$", RegexOptions.Compiled);
private readonly Regex commandRegexDe = new(@"^„(?<command>.+)“ existiert nicht als Textkommando\.$", RegexOptions.Compiled);
private readonly Regex commandRegexFr = new(@"^La commande texte “(?<command>.+)” n'existe pas\.$", RegexOptions.Compiled);
private readonly Regex commandRegexCn = new(@"^^(“|「)(?<command>.+)(”|」)(出现问题:该命令不存在|出現問題:該命令不存在)。$", RegexOptions.Compiled);
private readonly Regex currentLangCommandRegex;
[ServiceManager.ServiceDependency] private readonly Hook<ShellCommands.Delegates.TryInvokeDebugCommand>? tryInvokeDebugCommandHook;
private readonly ChatGui chatGui = Service<ChatGui>.Get();
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly ConsoleManager console = Service<ConsoleManager>.Get(); private readonly ConsoleManager console = Service<ConsoleManager>.Get();
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private CommandManager(Dalamud dalamud) private CommandManager(Dalamud dalamud)
{ {
this.currentLangCommandRegex = (ClientLanguage)dalamud.StartInfo.Language switch this.tryInvokeDebugCommandHook = Hook<ShellCommands.Delegates.TryInvokeDebugCommand>.FromAddress(
{ (nint)ShellCommands.MemberFunctionPointers.TryInvokeDebugCommand,
ClientLanguage.Japanese => this.commandRegexJp, this.OnTryInvokeDebugCommand);
ClientLanguage.English => this.commandRegexEn, this.tryInvokeDebugCommandHook.Enable();
ClientLanguage.German => this.commandRegexDe,
ClientLanguage.French => this.commandRegexFr,
_ => this.commandRegexEn,
};
this.chatGui.CheckMessageHandled += this.OnCheckMessageHandled;
this.console.Invoke += this.ConsoleOnInvoke; this.console.Invoke += this.ConsoleOnInvoke;
} }
@ -113,7 +103,7 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag
Log.Error(ex, "Error while dispatching command {CommandName} (Argument: {Argument})", command, argument); Log.Error(ex, "Error while dispatching command {CommandName} (Argument: {Argument})", command, argument);
} }
} }
/// <summary> /// <summary>
/// Add a command handler, which you can use to add your own custom commands to the in-game chat. /// Add a command handler, which you can use to add your own custom commands to the in-game chat.
/// </summary> /// </summary>
@ -131,7 +121,7 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag
Log.Error("Command {CommandName} is already registered", command); Log.Error("Command {CommandName} is already registered", command);
return false; return false;
} }
if (!this.commandAssemblyNameMap.TryAdd((command, info), loaderAssemblyName)) if (!this.commandAssemblyNameMap.TryAdd((command, info), loaderAssemblyName))
{ {
this.commandMap.Remove(command, out _); this.commandMap.Remove(command, out _);
@ -160,6 +150,11 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag
/// <inheritdoc/> /// <inheritdoc/>
public bool RemoveHandler(string command) public bool RemoveHandler(string command)
{ {
if (this.commandAssemblyNameMap.FindFirst(c => c.Key.Item1 == command, out var assemblyKeyValuePair))
{
this.commandAssemblyNameMap.TryRemove(assemblyKeyValuePair.Key, out _);
}
return this.commandMap.Remove(command, out _); return this.commandMap.Remove(command, out _);
} }
@ -184,7 +179,8 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag
/// </summary> /// </summary>
/// <param name="assemblyName">The name of the assembly.</param> /// <param name="assemblyName">The name of the assembly.</param>
/// <returns>A list of commands and their associated activation string.</returns> /// <returns>A list of commands and their associated activation string.</returns>
public List<KeyValuePair<(string Command, IReadOnlyCommandInfo CommandInfo), string>> GetHandlersByAssemblyName(string assemblyName) public List<KeyValuePair<(string Command, IReadOnlyCommandInfo CommandInfo), string>> GetHandlersByAssemblyName(
string assemblyName)
{ {
return this.commandAssemblyNameMap.Where(c => c.Value == assemblyName).ToList(); return this.commandAssemblyNameMap.Where(c => c.Value == assemblyName).ToList();
} }
@ -193,37 +189,20 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.console.Invoke -= this.ConsoleOnInvoke; this.console.Invoke -= this.ConsoleOnInvoke;
this.chatGui.CheckMessageHandled -= this.OnCheckMessageHandled; this.tryInvokeDebugCommandHook?.Dispose();
} }
private bool ConsoleOnInvoke(string arg) private bool ConsoleOnInvoke(string arg)
{ {
return arg.StartsWith('/') && this.ProcessCommand(arg); return arg.StartsWith('/') && this.ProcessCommand(arg);
} }
private void OnCheckMessageHandled(XivChatType type, int timestamp, ref SeString sender, ref SeString message, ref bool isHandled) private int OnTryInvokeDebugCommand(ShellCommands* self, Utf8String* command, UIModule* uiModule)
{ {
if (type == XivChatType.ErrorMessage && timestamp == 0) var result = this.tryInvokeDebugCommandHook!.OriginalDisposeSafe(self, command, uiModule);
{ if (result != -1) return result;
var cmdMatch = this.currentLangCommandRegex.Match(message.TextValue).Groups["command"];
if (cmdMatch.Success) return this.ProcessCommand(command->ToString()) ? 0 : result;
{
// Yes, it's a chat command.
var command = cmdMatch.Value;
if (this.ProcessCommand(command)) isHandled = true;
}
else
{
// Always match for china, since they patch in language files without changing the ClientLanguage.
cmdMatch = this.commandRegexCn.Match(message.TextValue).Groups["command"];
if (cmdMatch.Success)
{
// Yes, it's a Chinese fallback chat command.
var command = cmdMatch.Value;
if (this.ProcessCommand(command)) isHandled = true;
}
}
}
} }
} }
@ -238,7 +217,7 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag
internal class CommandManagerPluginScoped : IInternalDisposableService, ICommandManager internal class CommandManagerPluginScoped : IInternalDisposableService, ICommandManager
{ {
private static readonly ModuleLog Log = new("Command"); private static readonly ModuleLog Log = new("Command");
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly CommandManager commandManagerService = Service<CommandManager>.Get(); private readonly CommandManager commandManagerService = Service<CommandManager>.Get();
@ -253,10 +232,10 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand
{ {
this.pluginInfo = localPlugin; this.pluginInfo = localPlugin;
} }
/// <inheritdoc/> /// <inheritdoc/>
public ReadOnlyDictionary<string, IReadOnlyCommandInfo> Commands => this.commandManagerService.Commands; public ReadOnlyDictionary<string, IReadOnlyCommandInfo> Commands => this.commandManagerService.Commands;
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
@ -264,7 +243,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand
{ {
this.commandManagerService.RemoveHandler(command); this.commandManagerService.RemoveHandler(command);
} }
this.pluginRegisteredCommands.Clear(); this.pluginRegisteredCommands.Clear();
} }
@ -275,7 +254,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand
/// <inheritdoc/> /// <inheritdoc/>
public void DispatchCommand(string command, string argument, IReadOnlyCommandInfo info) public void DispatchCommand(string command, string argument, IReadOnlyCommandInfo info)
=> this.commandManagerService.DispatchCommand(command, argument, info); => this.commandManagerService.DispatchCommand(command, argument, info);
/// <inheritdoc/> /// <inheritdoc/>
public bool AddHandler(string command, CommandInfo info) public bool AddHandler(string command, CommandInfo info)
{ {
@ -294,7 +273,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand
return false; return false;
} }
/// <inheritdoc/> /// <inheritdoc/>
public bool RemoveHandler(string command) public bool RemoveHandler(string command)
{ {

View file

@ -121,7 +121,10 @@ internal sealed class GameConfig : IInternalDisposableService, IGameConfig
/// <inheritdoc/> /// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties) => this.System.TryGetProperties(option.GetName(), out properties); public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties) => this.System.TryGetProperties(option.GetName(), out properties);
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out PadButtonValue value) => this.System.TryGetStringAsEnum(option.GetName(), out value);
/// <inheritdoc/> /// <inheritdoc/>
public bool TryGet(UiConfigOption option, out bool value) => this.UiConfig.TryGet(option.GetName(), out value); public bool TryGet(UiConfigOption option, out bool value) => this.UiConfig.TryGet(option.GetName(), out value);
@ -346,7 +349,11 @@ internal class GameConfigPluginScoped : IInternalDisposableService, IGameConfig
/// <inheritdoc/> /// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties) public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties)
=> this.gameConfigService.TryGet(option, out properties); => this.gameConfigService.TryGet(option, out properties);
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out PadButtonValue value)
=> this.gameConfigService.TryGet(option, out value);
/// <inheritdoc/> /// <inheritdoc/>
public bool TryGet(UiConfigOption option, out bool value) public bool TryGet(UiConfigOption option, out bool value)
=> this.gameConfigService.TryGet(option, out value); => this.gameConfigService.TryGet(option, out value);

View file

@ -13,6 +13,6 @@ internal sealed class GameConfigAddressResolver : BaseAddressResolver
/// <inheritdoc/> /// <inheritdoc/>
protected override void Setup64Bit(ISigScanner scanner) protected override void Setup64Bit(ISigScanner scanner)
{ {
this.ConfigChangeAddress = scanner.ScanText("E8 ?? ?? ?? ?? 48 8B 3F 49 3B 3E"); this.ConfigChangeAddress = scanner.ScanText("E8 ?? ?? ?? ?? 48 8B 3F 49 3B 3E"); // unnamed in CS
} }
} }

View file

@ -1,5 +1,6 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
using System.Text;
using Dalamud.Memory; using Dalamud.Memory;
using Dalamud.Utility; using Dalamud.Utility;
@ -51,7 +52,7 @@ public class GameConfigSection
/// <summary> /// <summary>
/// Event which is fired when a game config option is changed within the section. /// Event which is fired when a game config option is changed within the section.
/// </summary> /// </summary>
internal event EventHandler<ConfigChangeEvent>? Changed; internal event EventHandler<ConfigChangeEvent>? Changed;
/// <summary> /// <summary>
/// Gets the number of config entries contained within the section. /// Gets the number of config entries contained within the section.
@ -357,6 +358,40 @@ public class GameConfigSection
return value; return value;
} }
/// <summary>Attempts to get a string config value as an enum value.</summary>
/// <param name="name">Name of the config option.</param>
/// <param name="value">The returned value of the config option.</param>
/// <typeparam name="T">Type of the enum. Name of each enum fields are compared against.</typeparam>
/// <returns>A value representing the success.</returns>
public unsafe bool TryGetStringAsEnum<T>(string name, out T value) where T : struct, Enum
{
value = default;
if (!this.TryGetIndex(name, out var index))
{
return false;
}
if (!this.TryGetEntry(index, out var entry))
{
return false;
}
if (entry->Type != 4)
{
return false;
}
if (entry->Value.String == null)
{
return false;
}
var n8 = entry->Value.String->AsSpan();
Span<char> n16 = stackalloc char[Encoding.UTF8.GetCharCount(n8)];
Encoding.UTF8.GetChars(n8, n16);
return Enum.TryParse(n16, out value);
}
/// <summary> /// <summary>
/// Set a string config option. /// Set a string config option.
/// Note: Not all config options will be be immediately reflected in the game. /// Note: Not all config options will be be immediately reflected in the game.
@ -491,8 +526,8 @@ public class GameConfigSection
{ {
if (!this.enumMap.TryGetValue(entry->Index, out var enumObject)) if (!this.enumMap.TryGetValue(entry->Index, out var enumObject))
{ {
if (entry->Name == null) return null; if (entry->Name.Value == null) return null;
var name = MemoryHelper.ReadStringNullTerminated(new IntPtr(entry->Name)); var name = entry->Name.ToString();
if (Enum.TryParse(typeof(TEnum), name, out enumObject)) if (Enum.TryParse(typeof(TEnum), name, out enumObject))
{ {
this.enumMap.TryAdd(entry->Index, enumObject); this.enumMap.TryAdd(entry->Index, enumObject);
@ -509,7 +544,7 @@ public class GameConfigSection
this.Changed?.InvokeSafely(this, eventArgs); this.Changed?.InvokeSafely(this, eventArgs);
return eventArgs; return eventArgs;
} }
private unsafe bool TryGetIndex(string name, out uint index) private unsafe bool TryGetIndex(string name, out uint index)
{ {
if (this.indexMap.TryGetValue(name, out index)) if (this.indexMap.TryGetValue(name, out index))
@ -521,12 +556,12 @@ public class GameConfigSection
var e = configBase->ConfigEntry; var e = configBase->ConfigEntry;
for (var i = 0U; i < configBase->ConfigCount; i++, e++) for (var i = 0U; i < configBase->ConfigCount; i++, e++)
{ {
if (e->Name == null) if (e->Name.Value == null)
{ {
continue; continue;
} }
var eName = MemoryHelper.ReadStringNullTerminated(new IntPtr(e->Name)); var eName = e->Name.ToString();
if (eName.Equals(name)) if (eName.Equals(name))
{ {
this.indexMap.TryAdd(name, i); this.indexMap.TryAdd(name, i);

View file

@ -0,0 +1,85 @@
namespace Dalamud.Game.Config;
// ReSharper disable InconsistentNaming
// ReSharper disable IdentifierTypo
// ReSharper disable CommentTypo
/// <summary>Valid values for PadButton options under <see cref="SystemConfigOption"/>.</summary>
/// <remarks>Names are the valid part. Enum values are exclusively for use with current Dalamud version.</remarks>
public enum PadButtonValue
{
/// <summary>Auto-run.</summary>
Autorun_Support,
/// <summary>Change Hotbar Set.</summary>
Hotbar_Set_Change,
/// <summary>Highlight Left Hotbar.</summary>
XHB_Left_Start,
/// <summary>Highlight Right Hotbar.</summary>
XHB_Right_Start,
/// <summary>Not directly referenced by Gamepad button customization window.</summary>
Cursor_Operation,
/// <summary>Draw Weapon/Lock On.</summary>
Lockon_and_Sword,
/// <summary>Sit/Lock On.</summary>
Lockon_and_Sit,
/// <summary>Change Camera.</summary>
Camera_Modechange,
/// <summary>Reset Camera Position.</summary>
Camera_Reset,
/// <summary>Draw/Sheathe Weapon.</summary>
Drawn_Sword,
/// <summary>Lock On.</summary>
Camera_Lockononly,
/// <summary>Face Target.</summary>
FaceTarget,
/// <summary>Assist Target.</summary>
AssistTarget,
/// <summary>Face Camera.</summary>
LookCamera,
/// <summary>Execute Macro #98 (Exclusive).</summary>
Macro98,
/// <summary>Execute Macro #99 (Exclusive).</summary>
Macro99,
/// <summary>Not Assigned.</summary>
Notset,
/// <summary>Jump/Cancel Casting.</summary>
Jump,
/// <summary>Select Target/Confirm.</summary>
Accept,
/// <summary>Cancel.</summary>
Cancel,
/// <summary>Open Map/Subcommands.</summary>
Map_Sub,
/// <summary>Open Main Menu.</summary>
MainCommand,
/// <summary>Select HUD.</summary>
HUD_Select,
/// <summary>Move Character.</summary>
Move_Operation,
/// <summary>Move Camera.</summary>
Camera_Operation,
}

View file

@ -597,6 +597,20 @@ public enum SystemConfigOption
[GameConfigOption("EnablePsFunction", ConfigType.UInt)] [GameConfigOption("EnablePsFunction", ConfigType.UInt)]
EnablePsFunction, EnablePsFunction,
/// <summary>
/// System option with the internal name ActiveInstanceGuid.
/// This option is a String.
/// </summary>
[GameConfigOption("ActiveInstanceGuid", ConfigType.String)]
ActiveInstanceGuid,
/// <summary>
/// System option with the internal name ActiveProductGuid.
/// This option is a String.
/// </summary>
[GameConfigOption("ActiveProductGuid", ConfigType.String)]
ActiveProductGuid,
/// <summary> /// <summary>
/// System option with the internal name WaterWet. /// System option with the internal name WaterWet.
/// This option is a UInt. /// This option is a UInt.
@ -996,6 +1010,27 @@ public enum SystemConfigOption
[GameConfigOption("AutoChangeCameraMode", ConfigType.UInt)] [GameConfigOption("AutoChangeCameraMode", ConfigType.UInt)]
AutoChangeCameraMode, AutoChangeCameraMode,
/// <summary>
/// System option with the internal name MsqProgress.
/// This option is a UInt.
/// </summary>
[GameConfigOption("MsqProgress", ConfigType.UInt)]
MsqProgress,
/// <summary>
/// System option with the internal name PromptConfigUpdate.
/// This option is a UInt.
/// </summary>
[GameConfigOption("PromptConfigUpdate", ConfigType.UInt)]
PromptConfigUpdate,
/// <summary>
/// System option with the internal name TitleScreenType.
/// This option is a UInt.
/// </summary>
[GameConfigOption("TitleScreenType", ConfigType.UInt)]
TitleScreenType,
/// <summary> /// <summary>
/// System option with the internal name AccessibilitySoundVisualEnable. /// System option with the internal name AccessibilitySoundVisualEnable.
/// This option is a UInt. /// This option is a UInt.
@ -1059,6 +1094,13 @@ public enum SystemConfigOption
[GameConfigOption("IdlingCameraAFK", ConfigType.UInt)] [GameConfigOption("IdlingCameraAFK", ConfigType.UInt)]
IdlingCameraAFK, IdlingCameraAFK,
/// <summary>
/// System option with the internal name FirstConfigBackup.
/// This option is a UInt.
/// </summary>
[GameConfigOption("FirstConfigBackup", ConfigType.UInt)]
FirstConfigBackup,
/// <summary> /// <summary>
/// System option with the internal name MouseSpeed. /// System option with the internal name MouseSpeed.
/// This option is a Float. /// This option is a Float.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,11 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
namespace Dalamud.Game.DutyState; namespace Dalamud.Game.DutyState;
@ -81,33 +82,33 @@ internal unsafe class DutyState : IInternalDisposableService, IDutyState
// Duty Commenced // Duty Commenced
case 0x4000_0001: case 0x4000_0001:
this.IsDutyStarted = true; this.IsDutyStarted = true;
this.DutyStarted?.Invoke(this, this.clientState.TerritoryType); this.DutyStarted?.InvokeSafely(this, this.clientState.TerritoryType);
break; break;
// Party Wipe // Party Wipe
case 0x4000_0005: case 0x4000_0005:
this.IsDutyStarted = false; this.IsDutyStarted = false;
this.DutyWiped?.Invoke(this, this.clientState.TerritoryType); this.DutyWiped?.InvokeSafely(this, this.clientState.TerritoryType);
break; break;
// Duty Recommence // Duty Recommence
case 0x4000_0006: case 0x4000_0006:
this.IsDutyStarted = true; this.IsDutyStarted = true;
this.DutyRecommenced?.Invoke(this, this.clientState.TerritoryType); this.DutyRecommenced?.InvokeSafely(this, this.clientState.TerritoryType);
break; break;
// Duty Completed Flytext Shown // Duty Completed Flytext Shown
case 0x4000_0002 when !this.CompletedThisTerritory: case 0x4000_0002 when !this.CompletedThisTerritory:
this.IsDutyStarted = false; this.IsDutyStarted = false;
this.CompletedThisTerritory = true; this.CompletedThisTerritory = true;
this.DutyCompleted?.Invoke(this, this.clientState.TerritoryType); this.DutyCompleted?.InvokeSafely(this, this.clientState.TerritoryType);
break; break;
// Duty Completed // Duty Completed
case 0x4000_0003 when !this.CompletedThisTerritory: case 0x4000_0003 when !this.CompletedThisTerritory:
this.IsDutyStarted = false; this.IsDutyStarted = false;
this.CompletedThisTerritory = true; this.CompletedThisTerritory = true;
this.DutyCompleted?.Invoke(this, this.clientState.TerritoryType); this.DutyCompleted?.InvokeSafely(this, this.clientState.TerritoryType);
break; break;
} }
} }

View file

@ -16,6 +16,6 @@ internal class DutyStateAddressResolver : BaseAddressResolver
/// <param name="sig">The signature scanner to facilitate setup.</param> /// <param name="sig">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner sig) protected override void Setup64Bit(ISigScanner sig)
{ {
this.ContentDirectorNetworkMessage = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 33 FF 48 8B D9 41 0F B7 08"); this.ContentDirectorNetworkMessage = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 33 FF 48 8B D9 41 0F B7 08"); // unnamed in cs
} }
} }

View file

@ -2,7 +2,6 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -16,6 +15,8 @@ using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using CSFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace Dalamud.Game; namespace Dalamud.Game;
/// <summary> /// <summary>
@ -31,11 +32,9 @@ internal sealed class Framework : IInternalDisposableService, IFramework
private readonly Stopwatch updateStopwatch = new(); private readonly Stopwatch updateStopwatch = new();
private readonly HitchDetector hitchDetector; private readonly HitchDetector hitchDetector;
private readonly Hook<OnUpdateDetour> updateHook; private readonly Hook<CSFramework.Delegates.Tick> updateHook;
private readonly Hook<OnRealDestroyDelegate> destroyHook; private readonly Hook<CSFramework.Delegates.Destroy> destroyHook;
private readonly FrameworkAddressResolver addressResolver;
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly GameLifecycle lifecycle = Service<GameLifecycle>.Get(); private readonly GameLifecycle lifecycle = Service<GameLifecycle>.Get();
@ -51,13 +50,10 @@ internal sealed class Framework : IInternalDisposableService, IFramework
private ulong tickCounter; private ulong tickCounter;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private Framework(TargetSigScanner sigScanner) private unsafe Framework()
{ {
this.hitchDetector = new HitchDetector("FrameworkUpdate", this.configuration.FrameworkUpdateHitch); this.hitchDetector = new HitchDetector("FrameworkUpdate", this.configuration.FrameworkUpdateHitch);
this.addressResolver = new FrameworkAddressResolver();
this.addressResolver.Setup(sigScanner);
this.frameworkDestroy = new(); this.frameworkDestroy = new();
this.frameworkThreadTaskScheduler = new(); this.frameworkThreadTaskScheduler = new();
this.FrameworkThreadTaskFactory = new( this.FrameworkThreadTaskFactory = new(
@ -66,23 +62,13 @@ internal sealed class Framework : IInternalDisposableService, IFramework
TaskContinuationOptions.None, TaskContinuationOptions.None,
this.frameworkThreadTaskScheduler); this.frameworkThreadTaskScheduler);
this.updateHook = Hook<OnUpdateDetour>.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate); this.updateHook = Hook<CSFramework.Delegates.Tick>.FromAddress((nint)CSFramework.StaticVirtualTablePointer->Tick, this.HandleFrameworkUpdate);
this.destroyHook = Hook<OnRealDestroyDelegate>.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy); this.destroyHook = Hook<CSFramework.Delegates.Destroy>.FromAddress((nint)CSFramework.StaticVirtualTablePointer->Destroy, this.HandleFrameworkDestroy);
this.updateHook.Enable(); this.updateHook.Enable();
this.destroyHook.Enable(); this.destroyHook.Enable();
} }
/// <summary>
/// A delegate type used during the native Framework::destroy.
/// </summary>
/// <param name="framework">The native Framework address.</param>
/// <returns>A value indicating if the call was successful.</returns>
public delegate bool OnRealDestroyDelegate(IntPtr framework);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate bool OnUpdateDetour(IntPtr framework);
/// <inheritdoc/> /// <inheritdoc/>
public event IFramework.OnUpdateDelegate? Update; public event IFramework.OnUpdateDelegate? Update;
@ -390,7 +376,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework
} }
} }
private bool HandleFrameworkUpdate(IntPtr framework) private unsafe bool HandleFrameworkUpdate(CSFramework* thisPtr)
{ {
this.frameworkThreadTaskScheduler.BoundThread ??= Thread.CurrentThread; this.frameworkThreadTaskScheduler.BoundThread ??= Thread.CurrentThread;
@ -483,10 +469,10 @@ internal sealed class Framework : IInternalDisposableService, IFramework
this.hitchDetector.Stop(); this.hitchDetector.Stop();
original: original:
return this.updateHook.OriginalDisposeSafe(framework); return this.updateHook.OriginalDisposeSafe(thisPtr);
} }
private bool HandleFrameworkDestroy(IntPtr framework) private unsafe bool HandleFrameworkDestroy(CSFramework* thisPtr)
{ {
this.frameworkDestroy.Cancel(); this.frameworkDestroy.Cancel();
this.DispatchUpdateEvents = false; this.DispatchUpdateEvents = false;
@ -504,7 +490,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework
ServiceManager.WaitForServiceUnload(); ServiceManager.WaitForServiceUnload();
Log.Information("Framework::Destroy OK!"); Log.Information("Framework::Destroy OK!");
return this.destroyHook.OriginalDisposeSafe(framework); return this.destroyHook.OriginalDisposeSafe(thisPtr);
} }
} }

View file

@ -1,40 +0,0 @@
namespace Dalamud.Game;
/// <summary>
/// The address resolver for the <see cref="Framework"/> class.
/// </summary>
internal sealed class FrameworkAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address for the function that is called once the Framework is destroyed.
/// </summary>
public IntPtr DestroyAddress { get; private set; }
/// <summary>
/// Gets the address for the function that is called once the Framework is free'd.
/// </summary>
public IntPtr FreeAddress { get; private set; }
/// <summary>
/// Gets the function that is called every tick.
/// </summary>
public IntPtr TickAddress { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(ISigScanner sig)
{
this.SetupFramework(sig);
}
private void SetupFramework(ISigScanner scanner)
{
this.DestroyAddress =
scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B 3D ?? ?? ?? ?? 48 8B D9 48 85 FF");
this.FreeAddress =
scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B D9 48 8B 0D ?? ?? ?? ?? 48 85 C9");
this.TickAddress =
scanner.ScanText("40 53 48 83 EC 20 FF 81 ?? ?? ?? ?? 48 8B D9 48 8D 4C 24 ??");
}
}

View file

@ -14,8 +14,20 @@ using Dalamud.Logging.Internal;
using Dalamud.Memory; using Dalamud.Memory;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.GUI;
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;
namespace Dalamud.Game.Gui; namespace Dalamud.Game.Gui;
@ -27,14 +39,12 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
{ {
private static readonly ModuleLog Log = new("ChatGui"); private static readonly ModuleLog Log = new("ChatGui");
private readonly ChatGuiAddressResolver address;
private readonly Queue<XivChatEntry> chatQueue = new(); private readonly Queue<XivChatEntry> chatQueue = new();
private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = new(); private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = new();
private readonly Hook<PrintMessageDelegate> printMessageHook; private readonly Hook<PrintMessageDelegate> printMessageHook;
private readonly Hook<PopulateItemLinkDelegate> populateItemLinkHook; private readonly Hook<InventoryItem.Delegates.Copy> inventoryItemCopyHook;
private readonly Hook<InteractableLinkClickedDelegate> interactableLinkClickedHook; private readonly Hook<LogViewer.Delegates.HandleLinkClick> handleLinkClickHook;
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get(); private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
@ -42,29 +52,20 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
private ImmutableDictionary<(string PluginName, uint CommandId), Action<uint, SeString>>? dalamudLinkHandlersCopy; private ImmutableDictionary<(string PluginName, uint CommandId), Action<uint, SeString>>? dalamudLinkHandlersCopy;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private ChatGui(TargetSigScanner sigScanner) private ChatGui()
{ {
this.address = new ChatGuiAddressResolver(); this.printMessageHook = Hook<PrintMessageDelegate>.FromAddress(RaptureLogModule.Addresses.PrintMessage.Value, this.HandlePrintMessageDetour);
this.address.Setup(sigScanner); this.inventoryItemCopyHook = Hook<InventoryItem.Delegates.Copy>.FromAddress((nint)InventoryItem.StaticVirtualTablePointer->Copy, this.InventoryItemCopyDetour);
this.handleLinkClickHook = Hook<LogViewer.Delegates.HandleLinkClick>.FromAddress(LogViewer.Addresses.HandleLinkClick.Value, this.HandleLinkClickDetour);
this.printMessageHook = Hook<PrintMessageDelegate>.FromAddress((nint)RaptureLogModule.Addresses.PrintMessage.Value, this.HandlePrintMessageDetour);
this.populateItemLinkHook = Hook<PopulateItemLinkDelegate>.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
this.interactableLinkClickedHook = Hook<InteractableLinkClickedDelegate>.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
this.printMessageHook.Enable(); this.printMessageHook.Enable();
this.populateItemLinkHook.Enable(); this.inventoryItemCopyHook.Enable();
this.interactableLinkClickedHook.Enable(); this.handleLinkClickHook.Enable();
} }
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate uint PrintMessageDelegate(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, byte silent); private delegate uint PrintMessageDelegate(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, byte silent);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void PopulateItemLinkDelegate(IntPtr linkObjectPtr, IntPtr itemInfoPtr);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void InteractableLinkClickedDelegate(IntPtr managerPtr, IntPtr messagePtr);
/// <inheritdoc/> /// <inheritdoc/>
public event IChatGui.OnMessageDelegate? ChatMessage; public event IChatGui.OnMessageDelegate? ChatMessage;
@ -78,7 +79,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled; public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
/// <inheritdoc/> /// <inheritdoc/>
public int LastLinkedItemId { get; private set; } public uint LastLinkedItemId { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
public byte LastLinkedItemFlags { get; private set; } public byte LastLinkedItemFlags { get; private set; }
@ -106,10 +107,12 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.printMessageHook.Dispose(); this.printMessageHook.Dispose();
this.populateItemLinkHook.Dispose(); this.inventoryItemCopyHook.Dispose();
this.interactableLinkClickedHook.Dispose(); this.handleLinkClickHook.Dispose();
} }
#region DalamudSeString
/// <inheritdoc/> /// <inheritdoc/>
public void Print(XivChatEntry chat) public void Print(XivChatEntry chat)
{ {
@ -140,43 +143,74 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
this.PrintTagged(message, XivChatType.Urgent, messageTag, tagColor); this.PrintTagged(message, XivChatType.Urgent, messageTag, tagColor);
} }
#endregion
#region LuminaSeString
/// <inheritdoc/>
public void Print(ReadOnlySpan<byte> message, string? messageTag = null, ushort? tagColor = null)
{
this.PrintTagged(message, this.configuration.GeneralChatType, messageTag, tagColor);
}
/// <inheritdoc/>
public void PrintError(ReadOnlySpan<byte> message, string? messageTag = null, ushort? tagColor = null)
{
this.PrintTagged(message, XivChatType.Urgent, messageTag, tagColor);
}
#endregion
/// <summary> /// <summary>
/// Process a chat queue. /// Process a chat queue.
/// </summary> /// </summary>
public void UpdateQueue() public void UpdateQueue()
{ {
while (this.chatQueue.Count > 0) if (this.chatQueue.Count == 0)
{ return;
var chat = this.chatQueue.Dequeue();
var replacedMessage = new SeStringBuilder();
// Normalize Unicode NBSP to the built-in one, as the former won't renderl var sb = LSeStringBuilder.SharedPool.Get();
foreach (var payload in chat.Message.Payloads) Span<byte> namebuf = stackalloc byte[256];
using var sender = new Utf8String();
using var message = new Utf8String();
while (this.chatQueue.TryDequeue(out var chat))
{
sb.Clear();
foreach (var c in UtfEnumerator.From(chat.MessageBytes, UtfEnumeratorFlags.Utf8SeString))
{ {
if (payload is TextPayload { Text: not null } textPayload) if (c.IsSeStringPayload)
{ sb.Append((ReadOnlySeStringSpan)chat.MessageBytes.AsSpan(c.ByteOffset, c.ByteLength));
var split = textPayload.Text.Split("\u202f"); // NARROW NO-BREAK SPACE else if (c.Value.IntValue == 0x202F)
for (var i = 0; i < split.Length; i++) sb.BeginMacro(MacroCode.NonBreakingSpace).EndMacro();
{
replacedMessage.AddText(split[i]);
if (i + 1 < split.Length)
replacedMessage.Add(new RawPayload([0x02, (byte)Lumina.Text.Payloads.PayloadType.Indent, 0x01, 0x03]));
}
}
else else
{ sb.Append(c);
replacedMessage.Add(payload);
}
} }
var sender = Utf8String.FromSequence(chat.Name.Encode()); if (chat.NameBytes.Length + 1 < namebuf.Length)
var message = Utf8String.FromSequence(replacedMessage.BuiltString.Encode()); {
chat.NameBytes.AsSpan().CopyTo(namebuf);
namebuf[chat.NameBytes.Length] = 0;
sender.SetString(namebuf);
}
else
{
sender.SetString(chat.NameBytes.NullTerminate());
}
this.HandlePrintMessageDetour(RaptureLogModule.Instance(), chat.Type, sender, message, chat.Timestamp, (byte)(chat.Silent ? 1 : 0)); message.SetString(sb.GetViewAsSpan());
sender->Dtor(true); var targetChannel = chat.Type ?? this.configuration.GeneralChatType;
message->Dtor(true);
this.HandlePrintMessageDetour(
RaptureLogModule.Instance(),
targetChannel,
&sender,
&message,
chat.Timestamp,
(byte)(chat.Silent ? 1 : 0));
} }
LSeStringBuilder.SharedPool.Return(sb);
} }
/// <summary> /// <summary>
@ -229,29 +263,6 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
} }
} }
private void PrintTagged(string message, XivChatType channel, string? tag, ushort? color)
{
var builder = new SeStringBuilder();
if (!tag.IsNullOrEmpty())
{
if (color is not null)
{
builder.AddUiForeground($"[{tag}] ", color.Value);
}
else
{
builder.AddText($"[{tag}] ");
}
}
this.Print(new XivChatEntry
{
Message = builder.AddText(message).Build(),
Type = channel,
});
}
private void PrintTagged(SeString message, XivChatType channel, string? tag, ushort? color) private void PrintTagged(SeString message, XivChatType channel, string? tag, ushort? color)
{ {
var builder = new SeStringBuilder(); var builder = new SeStringBuilder();
@ -275,21 +286,47 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
}); });
} }
private void HandlePopulateItemLinkDetour(IntPtr linkObjectPtr, IntPtr itemInfoPtr) private void PrintTagged(ReadOnlySpan<byte> message, XivChatType channel, string? tag, ushort? color)
{ {
var sb = LSeStringBuilder.SharedPool.Get();
if (!tag.IsNullOrEmpty())
{
if (color is not null)
{
sb.PushColorType(color.Value);
sb.Append($"[{tag}] ");
sb.PopColorType();
}
else
{
sb.Append($"[{tag}] ");
}
}
this.Print(new XivChatEntry
{
MessageBytes = sb.Append((ReadOnlySeStringSpan)message).ToArray(),
Type = channel,
});
LSeStringBuilder.SharedPool.Return(sb);
}
private void InventoryItemCopyDetour(InventoryItem* thisPtr, InventoryItem* otherPtr)
{
this.inventoryItemCopyHook.Original(thisPtr, otherPtr);
try try
{ {
this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr); this.LastLinkedItemId = otherPtr->ItemId;
this.LastLinkedItemFlags = (byte)otherPtr->Flags;
this.LastLinkedItemId = Marshal.ReadInt32(itemInfoPtr, 8); // Log.Verbose($"InventoryItemCopyDetour {thisPtr} {otherPtr} - linked:{this.LastLinkedItemId}");
this.LastLinkedItemFlags = Marshal.ReadByte(itemInfoPtr, 0x14);
// Log.Verbose($"HandlePopulateItemLinkDetour {linkObjectPtr} {itemInfoPtr} - linked:{this.LastLinkedItemId}");
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "Exception onPopulateItemLink hook."); Log.Error(ex, "Exception in InventoryItemCopyHook");
this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr);
} }
} }
@ -299,58 +336,57 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
try try
{ {
var originalSenderData = sender->AsSpan().ToArray(); var parsedSender = SeString.Parse(sender->AsSpan());
var originalMessageData = message->AsSpan().ToArray(); var parsedMessage = SeString.Parse(message->AsSpan());
var parsedSender = SeString.Parse(originalSenderData); var terminatedSender = parsedSender.EncodeWithNullTerminator();
var parsedMessage = SeString.Parse(originalMessageData); var terminatedMessage = parsedMessage.EncodeWithNullTerminator();
// Call events // Call events
var isHandled = false; var isHandled = false;
var invocationList = this.CheckMessageHandled!.GetInvocationList(); if (this.CheckMessageHandled is { } handledCallback)
foreach (var @delegate in invocationList)
{ {
try foreach (var action in handledCallback.GetInvocationList().Cast<IChatGui.OnCheckMessageHandledDelegate>())
{
var messageHandledDelegate = @delegate as IChatGui.OnCheckMessageHandledDelegate;
messageHandledDelegate!.Invoke(chatType, timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
}
catch (Exception e)
{
Log.Error(e, "Could not invoke registered OnCheckMessageHandledDelegate for {Name}", @delegate.Method.Name);
}
}
if (!isHandled)
{
invocationList = this.ChatMessage!.GetInvocationList();
foreach (var @delegate in invocationList)
{ {
try try
{ {
var messageHandledDelegate = @delegate as IChatGui.OnMessageDelegate; action(chatType, timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
messageHandledDelegate!.Invoke(chatType, timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e, "Could not invoke registered OnMessageDelegate for {Name}", @delegate.Method.Name); Log.Error(e, "Could not invoke registered OnCheckMessageHandledDelegate for {Name}", action.Method);
} }
} }
} }
var possiblyModifiedSenderData = parsedSender.Encode(); if (!isHandled && this.ChatMessage is { } callback)
var possiblyModifiedMessageData = parsedMessage.Encode();
if (!Util.FastByteArrayCompare(originalSenderData, possiblyModifiedSenderData))
{ {
Log.Verbose($"HandlePrintMessageDetour Sender modified: {SeString.Parse(originalSenderData)} -> {parsedSender}"); foreach (var action in callback.GetInvocationList().Cast<IChatGui.OnMessageDelegate>())
{
try
{
action(chatType, timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
}
catch (Exception e)
{
Log.Error(e, "Could not invoke registered OnMessageDelegate for {Name}", action.Method);
}
}
}
var possiblyModifiedSenderData = parsedSender.EncodeWithNullTerminator();
var possiblyModifiedMessageData = parsedMessage.EncodeWithNullTerminator();
if (!terminatedSender.SequenceEqual(possiblyModifiedSenderData))
{
Log.Verbose($"HandlePrintMessageDetour Sender modified: {SeString.Parse(terminatedSender)} -> {parsedSender}");
sender->SetString(possiblyModifiedSenderData); sender->SetString(possiblyModifiedSenderData);
} }
if (!Util.FastByteArrayCompare(originalMessageData, possiblyModifiedMessageData)) if (!terminatedMessage.SequenceEqual(possiblyModifiedMessageData))
{ {
Log.Verbose($"HandlePrintMessageDetour Message modified: {SeString.Parse(originalMessageData)} -> {parsedMessage}"); Log.Verbose($"HandlePrintMessageDetour Message modified: {SeString.Parse(terminatedMessage)} -> {parsedMessage}");
message->SetString(possiblyModifiedMessageData); message->SetString(possiblyModifiedMessageData);
} }
@ -374,42 +410,57 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
return messageId; return messageId;
} }
private void InteractableLinkClickedDetour(IntPtr managerPtr, IntPtr messagePtr) private void HandleLinkClickDetour(LogViewer* thisPtr, LinkData* linkData)
{ {
if (linkData == null || linkData->Payload == null || (Payload.EmbeddedInfoType)(linkData->LinkType + 1) != Payload.EmbeddedInfoType.DalamudLink)
{
this.handleLinkClickHook.Original(thisPtr, linkData);
return;
}
Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}");
var sb = LSeStringBuilder.SharedPool.Get();
try try
{ {
var interactableType = (Payload.EmbeddedInfoType)(Marshal.ReadByte(messagePtr, 0x1B) + 1); var seStringSpan = new ReadOnlySeStringSpan(linkData->Payload);
if (interactableType != Payload.EmbeddedInfoType.DalamudLink) // read until link terminator
foreach (var payload in seStringSpan)
{ {
this.interactableLinkClickedHook.Original(managerPtr, messagePtr); sb.Append(payload);
return;
if (payload.Type == ReadOnlySePayloadType.Macro &&
payload.MacroCode == MacroCode.Link &&
payload.TryGetExpression(out var expr1) &&
expr1.TryGetInt(out var expr1Val) &&
expr1Val == (int)LinkMacroPayloadType.Terminator)
{
break;
}
} }
Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}"); var seStr = SeString.Parse(sb.ToArray());
if (seStr.Payloads.Count == 0 || seStr.Payloads[0] is not DalamudLinkPayload link)
return;
var payloadPtr = Marshal.ReadIntPtr(messagePtr, 0x10); if (this.RegisteredLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value))
var seStr = MemoryHelper.ReadSeStringNullTerminated(payloadPtr);
var terminatorIndex = seStr.Payloads.IndexOf(RawPayload.LinkTerminator);
var payloads = terminatorIndex >= 0 ? seStr.Payloads.Take(terminatorIndex + 1).ToList() : seStr.Payloads;
if (payloads.Count == 0) return;
var linkPayload = payloads[0];
if (linkPayload is DalamudLinkPayload link)
{ {
if (this.RegisteredLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value)) Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}");
{ value.Invoke(link.CommandId, seStr);
Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}"); }
value.Invoke(link.CommandId, new SeString(payloads)); else
} {
else Log.Debug($"No DalamudLink registered for {link.Plugin} with ID of {link.CommandId}");
{
Log.Debug($"No DalamudLink registered for {link.Plugin} with ID of {link.CommandId}");
}
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "Exception on InteractableLinkClicked hook"); Log.Error(ex, "Exception in HandleLinkClickDetour");
}
finally
{
LSeStringBuilder.SharedPool.Return(sb);
} }
} }
} }
@ -451,7 +502,7 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled; public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
/// <inheritdoc/> /// <inheritdoc/>
public int LastLinkedItemId => this.chatGuiService.LastLinkedItemId; public uint LastLinkedItemId => this.chatGuiService.LastLinkedItemId;
/// <inheritdoc/> /// <inheritdoc/>
public byte LastLinkedItemFlags => this.chatGuiService.LastLinkedItemFlags; public byte LastLinkedItemFlags => this.chatGuiService.LastLinkedItemFlags;
@ -493,6 +544,14 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
public void PrintError(SeString message, string? messageTag = null, ushort? tagColor = null) public void PrintError(SeString message, string? messageTag = null, ushort? tagColor = null)
=> this.chatGuiService.PrintError(message, messageTag, tagColor); => this.chatGuiService.PrintError(message, messageTag, tagColor);
/// <inheritdoc/>
public void Print(ReadOnlySpan<byte> message, string? messageTag = null, ushort? tagColor = null)
=> this.chatGuiService.Print(message, messageTag, tagColor);
/// <inheritdoc/>
public void PrintError(ReadOnlySpan<byte> message, string? messageTag = null, ushort? tagColor = null)
=> this.chatGuiService.PrintError(message, messageTag, tagColor);
private void OnMessageForward(XivChatType type, int timestamp, ref SeString sender, ref SeString message, ref bool isHandled) private void OnMessageForward(XivChatType type, int timestamp, ref SeString sender, ref SeString message, ref bool isHandled)
=> this.ChatMessage?.Invoke(type, timestamp, ref sender, ref message, ref isHandled); => this.ChatMessage?.Invoke(type, timestamp, ref sender, ref message, ref isHandled);

View file

@ -1,28 +0,0 @@
namespace Dalamud.Game.Gui;
/// <summary>
/// The address resolver for the <see cref="ChatGui"/> class.
/// </summary>
internal sealed class ChatGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the native PopulateItemLinkObject method.
/// </summary>
public IntPtr PopulateItemLinkObject { get; private set; }
/// <summary>
/// Gets the address of the native InteractableLinkClicked method.
/// </summary>
public IntPtr InteractableLinkClicked { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(ISigScanner sig)
{
// PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 FA F2 B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A");
// PopulateItemLinkObject = sig.ScanText( "48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); 5.0
this.PopulateItemLinkObject = sig.ScanText("E8 ?? ?? ?? ?? 8B 4E FC");
this.InteractableLinkClicked = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 4B ?? E8 ?? ?? ?? ?? 33 D2");
}
}

View file

@ -47,9 +47,9 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
} }
private delegate ushort AtkModuleVf22OpenAddonByAgentDelegate(AtkModule* module, byte* addonName, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, bool a8); private delegate ushort AtkModuleVf22OpenAddonByAgentDelegate(AtkModule* module, byte* addonName, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, bool a8);
private delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3); private delegate 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); private delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2);
/// <inheritdoc/> /// <inheritdoc/>
@ -92,16 +92,22 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.atkModuleVf22OpenAddonByAgentHook.Dispose();
this.addonContextMenuOnMenuSelectedHook.Dispose();
var manager = RaptureAtkUnitManager.Instance(); var manager = RaptureAtkUnitManager.Instance();
if (manager == null)
return;
var menu = manager->GetAddonByName("ContextMenu"); var menu = manager->GetAddonByName("ContextMenu");
var submenu = manager->GetAddonByName("AddonContextSub"); var submenu = manager->GetAddonByName("AddonContextSub");
if (menu == null || submenu == null)
return;
if (menu->IsVisible) if (menu->IsVisible)
menu->FireCallbackInt(-1); menu->FireCallbackInt(-1);
if (submenu->IsVisible) if (submenu->IsVisible)
submenu->FireCallbackInt(-1); submenu->FireCallbackInt(-1);
this.atkModuleVf22OpenAddonByAgentHook.Dispose();
this.addonContextMenuOnMenuSelectedHook.Dispose();
} }
/// <inheritdoc/> /// <inheritdoc/>

View file

@ -1,7 +1,7 @@
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.Sheets;
namespace Dalamud.Game.Gui.ContextMenu; namespace Dalamud.Game.Gui.ContextMenu;

View file

@ -1,11 +1,12 @@
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.Network.Structures.InfoProxy; using Dalamud.Game.Network.Structures.InfoProxy;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace Dalamud.Game.Gui.ContextMenu; namespace Dalamud.Game.Gui.ContextMenu;
@ -46,7 +47,7 @@ public sealed unsafe class MenuTargetDefault : MenuTarget
/// <summary> /// <summary>
/// Gets the home world id of the target. /// Gets the home world id of the target.
/// </summary> /// </summary>
public ExcelResolver<World> TargetHomeWorld => new((uint)this.Context->TargetHomeWorldId); public RowRef<World> TargetHomeWorld => LuminaUtils.CreateRef<World>((uint)this.Context->TargetHomeWorldId);
/// <summary> /// <summary>
/// Gets the currently targeted character. Only shows up for specific targets, like friends, party finder listings, or party members. /// Gets the currently targeted character. Only shows up for specific targets, like friends, party finder listings, or party members.

View file

@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Game.Addon.Events; using Dalamud.Game.Addon.Events;
@ -10,6 +11,7 @@ using Dalamud.Game.Text.SeStringHandling;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics; using FFXIVClientStructs.FFXIV.Client.Graphics;
@ -28,7 +30,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
private const uint BaseNodeId = 1000; private const uint BaseNodeId = 1000;
private static readonly ModuleLog Log = new("DtrBar"); private static readonly ModuleLog Log = new("DtrBar");
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get(); private readonly Framework framework = Service<Framework>.Get();
@ -48,13 +50,15 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
private readonly AddonLifecycleEventListener dtrPostRequestedUpdateListener; private readonly AddonLifecycleEventListener dtrPostRequestedUpdateListener;
private readonly AddonLifecycleEventListener dtrPreFinalizeListener; private readonly AddonLifecycleEventListener dtrPreFinalizeListener;
private readonly ConcurrentBag<DtrBarEntry> newEntries = new(); private readonly ReaderWriterLockSlim entriesLock = new();
private readonly List<DtrBarEntry> entries = new(); private readonly List<DtrBarEntry> entries = [];
private readonly Dictionary<uint, List<IAddonEventHandle>> eventHandles = new(); private readonly Dictionary<uint, List<IAddonEventHandle>> eventHandles = new();
private ImmutableList<IReadOnlyDtrBarEntry>? entriesReadOnlyCopy;
private Utf8String* emptyString; private Utf8String* emptyString;
private uint runningNodeIds = BaseNodeId; private uint runningNodeIds = BaseNodeId;
private float entryStartPos = float.NaN; private float entryStartPos = float.NaN;
@ -68,55 +72,157 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
this.addonLifecycle.RegisterListener(this.dtrPostDrawListener); this.addonLifecycle.RegisterListener(this.dtrPostDrawListener);
this.addonLifecycle.RegisterListener(this.dtrPostRequestedUpdateListener); this.addonLifecycle.RegisterListener(this.dtrPostRequestedUpdateListener);
this.addonLifecycle.RegisterListener(this.dtrPreFinalizeListener); this.addonLifecycle.RegisterListener(this.dtrPreFinalizeListener);
this.framework.Update += this.Update; this.framework.Update += this.Update;
this.configuration.DtrOrder ??= new List<string>(); this.configuration.DtrOrder ??= [];
this.configuration.DtrIgnore ??= new List<string>(); this.configuration.DtrIgnore ??= [];
this.configuration.QueueSave(); this.configuration.QueueSave();
} }
/// <summary>
/// Event type fired each time a DtrEntry was removed.
/// </summary>
/// <param name="title">The title of the bar entry.</param>
internal delegate void DtrEntryRemovedDelegate(string title);
/// <summary>
/// Event fired each time a DtrEntry was removed.
/// </summary>
internal event DtrEntryRemovedDelegate? DtrEntryRemoved;
/// <inheritdoc/> /// <inheritdoc/>
public IReadOnlyList<IReadOnlyDtrBarEntry> Entries => this.entries; public IReadOnlyList<IReadOnlyDtrBarEntry> Entries
/// <inheritdoc/>
public IDtrBarEntry Get(string title, SeString? text = null)
{ {
if (this.entries.Any(x => x.Title == title) || this.newEntries.Any(x => x.Title == title)) get
throw new ArgumentException("An entry with the same title already exists.");
var entry = new DtrBarEntry(this.configuration, title, null);
entry.Text = text;
// Add the entry to the end of the order list, if it's not there already.
if (!this.configuration.DtrOrder!.Contains(title))
this.configuration.DtrOrder!.Add(title);
this.newEntries.Add(entry);
return entry;
}
/// <inheritdoc/>
public void Remove(string title)
{
if (this.entries.FirstOrDefault(entry => entry.Title == title) is { } dtrBarEntry)
{ {
dtrBarEntry.Remove(); var erc = this.entriesReadOnlyCopy;
if (erc is null)
{
this.entriesLock.EnterReadLock();
this.entriesReadOnlyCopy = erc = [..this.entries];
this.entriesLock.ExitReadLock();
}
return erc;
} }
} }
/// <summary>
/// Get a DTR bar entry.
/// This allows you to add your own text, and users to sort it.
/// </summary>
/// <param name="plugin">Plugin that owns the DTR bar, or <c>null</c> if owned by Dalamud.</param>
/// <param name="title">A user-friendly name for sorting.</param>
/// <param name="text">The text the entry shows.</param>
/// <returns>The entry object used to update, hide and remove the entry.</returns>
/// <exception cref="ArgumentException">Thrown when an entry with the specified title exists.</exception>
public IDtrBarEntry Get(LocalPlugin? plugin, string title, SeString? text = null)
{
this.entriesLock.EnterUpgradeableReadLock();
foreach (var existingEntry in this.entries)
{
if (existingEntry.Title == title)
{
if (existingEntry.ShouldBeRemoved)
{
if (plugin == existingEntry.OwnerPlugin)
{
Log.Debug(
"Reviving entry: {what}; owner: {plugin}({pluginId})",
title,
plugin?.InternalName,
plugin?.EffectiveWorkingPluginId);
}
else
{
Log.Debug(
"Reviving entry: {what}; old owner: {old}({oldId}); new owner: {new}({newId})",
title,
existingEntry.OwnerPlugin?.InternalName,
existingEntry.OwnerPlugin?.EffectiveWorkingPluginId,
plugin?.InternalName,
plugin?.EffectiveWorkingPluginId);
existingEntry.OwnerPlugin = plugin;
}
existingEntry.ShouldBeRemoved = false;
}
this.entriesLock.ExitUpgradeableReadLock();
if (plugin == existingEntry.OwnerPlugin)
return existingEntry;
Log.Debug(
"Entry already has a different owner: {what}; owner: {old}({oldId}); requester: {new}({newId})",
title,
existingEntry.OwnerPlugin?.InternalName,
existingEntry.OwnerPlugin?.EffectiveWorkingPluginId,
plugin?.InternalName,
plugin?.EffectiveWorkingPluginId);
throw new ArgumentException("An entry with the same title already exists.");
}
}
this.entriesLock.EnterWriteLock();
var entry = new DtrBarEntry(this.configuration, title, null) { Text = text, OwnerPlugin = plugin };
this.entries.Add(entry);
Log.Debug(
"Adding entry: {what}; owner: {owner}({id})",
title,
plugin?.InternalName,
plugin?.EffectiveWorkingPluginId);
// Add the entry to the end of the order list, if it's not there already.
var dtrOrder = this.configuration.DtrOrder ??= [];
if (!dtrOrder.Contains(entry.Title))
dtrOrder.Add(entry.Title);
this.ApplySortUnsafe(dtrOrder);
this.entriesReadOnlyCopy = null;
this.entriesLock.ExitWriteLock();
this.entriesLock.ExitUpgradeableReadLock();
return entry;
}
/// <inheritdoc/>
public IDtrBarEntry Get(string title, SeString? text = null) => this.Get(null, title, text);
/// <summary>
/// Removes a DTR bar entry from the system.
/// </summary>
/// <param name="plugin">Plugin that owns the DTR bar, or <c>null</c> if owned by Dalamud.</param>
/// <param name="title">Title of the entry to remove, or <c>null</c> to remove all entries under the plugin.</param>
/// <remarks>Remove operation is not immediate. If you try to add right after removing, the operation may fail.
/// </remarks>
public void Remove(LocalPlugin? plugin, string? title)
{
this.entriesLock.EnterUpgradeableReadLock();
foreach (var entry in this.entries)
{
if ((title is null || entry.Title == title) && (plugin is null || entry.OwnerPlugin == plugin))
{
if (!entry.Added)
{
Log.Debug("Removing entry immediately because it is not added yet: {what}", entry.Title);
this.entriesLock.EnterWriteLock();
this.RemoveEntry(entry);
this.entries.Remove(entry);
this.entriesReadOnlyCopy = null;
this.entriesLock.ExitWriteLock();
}
else if (!entry.ShouldBeRemoved)
{
Log.Debug("Queueing entry for removal: {what}", entry.Title);
entry.Remove();
}
else
{
Log.Debug("Entry is already marked for removal: {what}", entry.Title);
}
break;
}
}
this.entriesLock.ExitUpgradeableReadLock();
}
/// <inheritdoc/>
public void Remove(string title) => this.Remove(null, title);
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
@ -124,10 +230,17 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
this.addonLifecycle.UnregisterListener(this.dtrPostRequestedUpdateListener); this.addonLifecycle.UnregisterListener(this.dtrPostRequestedUpdateListener);
this.addonLifecycle.UnregisterListener(this.dtrPreFinalizeListener); this.addonLifecycle.UnregisterListener(this.dtrPreFinalizeListener);
foreach (var entry in this.entries) this.framework.RunOnFrameworkThread(
this.RemoveEntry(entry); () =>
{
this.entriesLock.EnterWriteLock();
foreach (var entry in this.entries)
this.RemoveEntry(entry);
this.entries.Clear();
this.entriesReadOnlyCopy = null;
this.entriesLock.ExitWriteLock();
}).Wait();
this.entries.Clear();
this.framework.Update -= this.Update; this.framework.Update -= this.Update;
if (this.emptyString != null) if (this.emptyString != null)
@ -137,23 +250,6 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
} }
} }
/// <summary>
/// Remove nodes marked as "should be removed" from the bar.
/// </summary>
internal void HandleRemovedNodes()
{
foreach (var data in this.entries)
{
if (data.ShouldBeRemoved)
{
this.RemoveEntry(data);
this.DtrEntryRemoved?.Invoke(data.Title);
}
}
this.entries.RemoveAll(d => d.ShouldBeRemoved);
}
/// <summary> /// <summary>
/// Remove native resources for the specified entry. /// Remove native resources for the specified entry.
/// </summary> /// </summary>
@ -174,7 +270,17 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
/// </summary> /// </summary>
/// <param name="title">The title to check for.</param> /// <param name="title">The title to check for.</param>
/// <returns>Whether or not an entry with that title is registered.</returns> /// <returns>Whether or not an entry with that title is registered.</returns>
internal bool HasEntry(string title) => this.entries.Any(x => x.Title == title); internal bool HasEntry(string title)
{
var found = false;
this.entriesLock.EnterReadLock();
for (var i = 0; i < this.entries.Count && !found; i++)
found = this.entries[i].Title == title;
this.entriesLock.ExitReadLock();
return found;
}
/// <summary> /// <summary>
/// Dirty the DTR bar entry with the specified title. /// Dirty the DTR bar entry with the specified title.
@ -183,24 +289,37 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
/// <returns>Whether the entry was found.</returns> /// <returns>Whether the entry was found.</returns>
internal bool MakeDirty(string title) internal bool MakeDirty(string title)
{ {
var entry = this.entries.FirstOrDefault(x => x.Title == title); var found = false;
if (entry == null)
return false;
entry.Dirty = true; this.entriesLock.EnterReadLock();
return true; for (var i = 0; i < this.entries.Count && !found; i++)
{
found = this.entries[i].Title == title;
if (found)
this.entries[i].Dirty = true;
}
this.entriesLock.ExitReadLock();
return found;
} }
/// <summary> /// <summary>
/// Reapply the DTR entry ordering from <see cref="DalamudConfiguration"/>. /// Reapply the DTR entry ordering from <see cref="DalamudConfiguration"/>.
/// </summary> /// </summary>
internal void ApplySort() internal void ApplySort()
{
this.entriesLock.EnterWriteLock();
this.ApplySortUnsafe(this.configuration.DtrOrder ??= []);
this.entriesLock.ExitWriteLock();
}
private void ApplySortUnsafe(List<string> dtrOrder)
{ {
// Sort the current entry list, based on the order in the configuration. // Sort the current entry list, based on the order in the configuration.
var positions = this.configuration var positions = dtrOrder
.DtrOrder! .Select(entry => (entry, index: dtrOrder.IndexOf(entry)))
.Select(entry => (entry, index: this.configuration.DtrOrder!.IndexOf(entry))) .ToDictionary(x => x.entry, x => x.index);
.ToDictionary(x => x.entry, x => x.index);
this.entries.Sort((x, y) => this.entries.Sort((x, y) =>
{ {
@ -208,15 +327,13 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
var yPos = positions.TryGetValue(y.Title, out var yIndex) ? yIndex : int.MaxValue; var yPos = positions.TryGetValue(y.Title, out var yIndex) ? yIndex : int.MaxValue;
return xPos.CompareTo(yPos); return xPos.CompareTo(yPos);
}); });
this.entriesReadOnlyCopy = null;
} }
private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR").ToPointer(); private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR").ToPointer();
private void Update(IFramework unused) private void Update(IFramework unused)
{ {
this.HandleRemovedNodes();
this.HandleAddedNodes();
var dtr = this.GetDtr(); var dtr = this.GetDtr();
if (dtr == null || dtr->RootNode == null || dtr->RootNode->ChildNode == null) return; if (dtr == null || dtr->RootNode == null || dtr->RootNode->ChildNode == null) return;
@ -236,14 +353,28 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
var runningXPos = this.entryStartPos; var runningXPos = this.entryStartPos;
foreach (var data in this.entries) this.entriesLock.EnterUpgradeableReadLock();
for (var i = 0; i < this.entries.Count; i++)
{ {
if (!data.Added) var data = this.entries[i];
if (data.ShouldBeRemoved)
{ {
data.Added = this.AddNode(data.TextNode); Log.Debug("Removing entry from Framework.Update: {what}", data.Title);
data.Dirty = true; this.entriesLock.EnterWriteLock();
this.entries.RemoveAt(i);
this.RemoveEntry(data);
this.entriesReadOnlyCopy = null;
this.entriesLock.ExitWriteLock();
i--;
continue;
} }
if (!data.Added)
data.Added = this.AddNode(data);
if (!data.Added || data.TextNode is null) // TextNode check is unnecessary, but just in case.
continue;
var isHide = !data.Shown || data.UserHidden; var isHide = !data.Shown || data.UserHidden;
var node = data.TextNode; var node = data.TextNode;
var nodeHidden = !node->AtkResNode.IsVisible(); var nodeHidden = !node->AtkResNode.IsVisible();
@ -290,23 +421,10 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
data.Dirty = false; data.Dirty = false;
} }
this.entriesLock.ExitUpgradeableReadLock();
} }
private void HandleAddedNodes()
{
if (!this.newEntries.IsEmpty)
{
foreach (var newEntry in this.newEntries)
{
newEntry.TextNode = this.MakeNode(++this.runningNodeIds);
this.entries.Add(newEntry);
}
this.newEntries.Clear();
this.ApplySort();
}
}
private void FixCollision(AddonEvent eventType, AddonArgs addonInfo) private void FixCollision(AddonEvent eventType, AddonArgs addonInfo)
{ {
var addon = (AtkUnitBase*)addonInfo.Addon; var addon = (AtkUnitBase*)addonInfo.Addon;
@ -316,7 +434,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
var additionalWidth = 0; var additionalWidth = 0;
AtkResNode* collisionNode = null; AtkResNode* collisionNode = null;
foreach (var index in Enumerable.Range(0, addon->UldManager.NodeListCount)) for (var index = 0; index < addon->UldManager.NodeListCount; index++)
{ {
var node = addon->UldManager.NodeList[index]; var node = addon->UldManager.NodeList[index];
if (node->IsVisible()) if (node->IsVisible())
@ -382,22 +500,20 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
private void RecreateNodes() private void RecreateNodes()
{ {
this.runningNodeIds = BaseNodeId; this.runningNodeIds = BaseNodeId;
if (this.entries.Any())
{
this.eventHandles.Clear();
}
this.entriesLock.EnterReadLock();
this.eventHandles.Clear();
foreach (var entry in this.entries) foreach (var entry in this.entries)
{
entry.TextNode = this.MakeNode(++this.runningNodeIds);
entry.Added = false; entry.Added = false;
} this.entriesLock.ExitReadLock();
} }
private bool AddNode(AtkTextNode* node) private bool AddNode(DtrBarEntry data)
{ {
var dtr = this.GetDtr(); var dtr = this.GetDtr();
if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null) return false;
var node = data.TextNode = this.MakeNode(++this.runningNodeIds);
this.eventHandles.TryAdd(node->AtkResNode.NodeId, new List<IAddonEventHandle>()); this.eventHandles.TryAdd(node->AtkResNode.NodeId, new List<IAddonEventHandle>());
this.eventHandles[node->AtkResNode.NodeId].AddRange(new List<IAddonEventHandle> this.eventHandles[node->AtkResNode.NodeId].AddRange(new List<IAddonEventHandle>
@ -406,7 +522,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler), this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler),
this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseClick, this.DtrEventHandler), this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseClick, this.DtrEventHandler),
}); });
var lastChild = dtr->RootNode->ChildNode; var lastChild = dtr->RootNode->ChildNode;
while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode; while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode;
Log.Debug($"Found last sibling: {(ulong)lastChild:X}"); Log.Debug($"Found last sibling: {(ulong)lastChild:X}");
@ -420,6 +536,8 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
dtr->UldManager.UpdateDrawNodeList(); dtr->UldManager.UpdateDrawNodeList();
dtr->UpdateCollisionNodeList(false); dtr->UpdateCollisionNodeList(false);
Log.Debug("Updated node draw list"); Log.Debug("Updated node draw list");
data.Dirty = true;
return true; return true;
} }
@ -472,7 +590,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
if (this.emptyString == null) if (this.emptyString == null)
this.emptyString = Utf8String.FromString(" "); this.emptyString = Utf8String.FromString(" ");
newTextNode->SetText(this.emptyString->StringPtr); newTextNode->SetText(this.emptyString->StringPtr);
newTextNode->TextColor = new ByteColor { R = 255, G = 255, B = 255, A = 255 }; newTextNode->TextColor = new ByteColor { R = 255, G = 255, B = 255, A = 255 };
@ -491,13 +609,21 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
return newTextNode; return newTextNode;
} }
private void DtrEventHandler(AddonEventType atkEventType, IntPtr atkUnitBase, IntPtr atkResNode) private void DtrEventHandler(AddonEventType atkEventType, IntPtr atkUnitBase, IntPtr atkResNode)
{ {
var addon = (AtkUnitBase*)atkUnitBase; var addon = (AtkUnitBase*)atkUnitBase;
var node = (AtkResNode*)atkResNode; var node = (AtkResNode*)atkResNode;
if (this.entries.FirstOrDefault(entry => entry.TextNode == node) is not { } dtrBarEntry) return; DtrBarEntry? dtrBarEntry = null;
this.entriesLock.EnterReadLock();
foreach (var entry in this.entries)
{
if (entry.TextNode == node)
dtrBarEntry = entry;
}
this.entriesLock.ExitReadLock();
if (dtrBarEntry is { Tooltip: not null }) if (dtrBarEntry is { Tooltip: not null })
{ {
@ -506,7 +632,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
case AddonEventType.MouseOver: case AddonEventType.MouseOver:
AtkStage.Instance()->TooltipManager.ShowTooltip(addon->Id, node, dtrBarEntry.Tooltip.Encode()); AtkStage.Instance()->TooltipManager.ShowTooltip(addon->Id, node, dtrBarEntry.Tooltip.Encode());
break; break;
case AddonEventType.MouseOut: case AddonEventType.MouseOut:
AtkStage.Instance()->TooltipManager.HideTooltip(addon->Id); AtkStage.Instance()->TooltipManager.HideTooltip(addon->Id);
break; break;
@ -520,11 +646,11 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
case AddonEventType.MouseOver: case AddonEventType.MouseOver:
this.uiEventManager.SetCursor(AddonCursorType.Clickable); this.uiEventManager.SetCursor(AddonCursorType.Clickable);
break; break;
case AddonEventType.MouseOut: case AddonEventType.MouseOut:
this.uiEventManager.ResetCursor(); this.uiEventManager.ResetCursor();
break; break;
case AddonEventType.MouseClick: case AddonEventType.MouseClick:
dtrBarEntry.OnClick.Invoke(); dtrBarEntry.OnClick.Invoke();
break; break;
@ -541,58 +667,25 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
#pragma warning disable SA1015 #pragma warning disable SA1015
[ResolveVia<IDtrBar>] [ResolveVia<IDtrBar>]
#pragma warning restore SA1015 #pragma warning restore SA1015
internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar internal sealed class DtrBarPluginScoped : IInternalDisposableService, IDtrBar
{ {
private readonly LocalPlugin plugin;
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly DtrBar dtrBarService = Service<DtrBar>.Get(); private readonly DtrBar dtrBarService = Service<DtrBar>.Get();
private readonly Dictionary<string, IDtrBarEntry> pluginEntries = new(); [ServiceManager.ServiceConstructor]
private DtrBarPluginScoped(LocalPlugin plugin) => this.plugin = plugin;
/// <summary>
/// Initializes a new instance of the <see cref="DtrBarPluginScoped"/> class.
/// </summary>
internal DtrBarPluginScoped()
{
this.dtrBarService.DtrEntryRemoved += this.OnDtrEntryRemoved;
}
/// <inheritdoc/> /// <inheritdoc/>
public IReadOnlyList<IReadOnlyDtrBarEntry> Entries => this.dtrBarService.Entries; public IReadOnlyList<IReadOnlyDtrBarEntry> Entries => this.dtrBarService.Entries;
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService() => this.dtrBarService.Remove(this.plugin, null);
{
this.dtrBarService.DtrEntryRemoved -= this.OnDtrEntryRemoved;
foreach (var entry in this.pluginEntries)
{
entry.Value.Remove();
}
this.pluginEntries.Clear();
}
/// <inheritdoc/> /// <inheritdoc/>
public IDtrBarEntry Get(string title, SeString? text = null) public IDtrBarEntry Get(string title, SeString? text = null) => this.dtrBarService.Get(this.plugin, title, text);
{
// If we already have a known entry for this plugin, return it.
if (this.pluginEntries.TryGetValue(title, out var existingEntry)) return existingEntry;
return this.pluginEntries[title] = this.dtrBarService.Get(title, text);
}
/// <inheritdoc/> /// <inheritdoc/>
public void Remove(string title) public void Remove(string title) => this.dtrBarService.Remove(this.plugin, title);
{
if (this.pluginEntries.TryGetValue(title, out var existingEntry))
{
existingEntry.Remove();
this.pluginEntries.Remove(title);
}
}
private void OnDtrEntryRemoved(string title)
{
this.pluginEntries.Remove(title);
}
} }

View file

@ -1,7 +1,6 @@
using System.Linq; using Dalamud.Configuration.Internal;
using Dalamud.Configuration.Internal;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.System.String;
@ -27,12 +26,12 @@ public interface IReadOnlyDtrBarEntry
/// <summary> /// <summary>
/// Gets the text of this entry. /// Gets the text of this entry.
/// </summary> /// </summary>
public SeString Text { get; } public SeString? Text { get; }
/// <summary> /// <summary>
/// Gets a tooltip to be shown when the user mouses over the dtr entry. /// Gets a tooltip to be shown when the user mouses over the dtr entry.
/// </summary> /// </summary>
public SeString Tooltip { get; } public SeString? Tooltip { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether this entry should be shown. /// Gets a value indicating whether this entry should be shown.
@ -86,7 +85,7 @@ public interface IDtrBarEntry : IReadOnlyDtrBarEntry
/// <summary> /// <summary>
/// Class representing an entry in the server info bar. /// Class representing an entry in the server info bar.
/// </summary> /// </summary>
public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
{ {
private readonly DalamudConfiguration configuration; private readonly DalamudConfiguration configuration;
@ -146,7 +145,7 @@ public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
} }
/// <inheritdoc/> /// <inheritdoc/>
[Api10ToDo("Maybe make this config scoped to internalname?")] [Api12ToDo("Maybe make this config scoped to internalname?")]
public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
/// <summary> /// <summary>
@ -160,9 +159,9 @@ public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
internal Utf8String* Storage { get; set; } internal Utf8String* Storage { get; set; }
/// <summary> /// <summary>
/// Gets a value indicating whether this entry should be removed. /// Gets or sets a value indicating whether this entry should be removed.
/// </summary> /// </summary>
internal bool ShouldBeRemoved { get; private set; } internal bool ShouldBeRemoved { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether this entry is dirty. /// Gets or sets a value indicating whether this entry is dirty.
@ -174,6 +173,11 @@ public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
/// </summary> /// </summary>
internal bool Added { get; set; } internal bool Added { get; set; }
/// <summary>
/// Gets or sets the plugin that owns this entry.
/// </summary>
internal LocalPlugin? OwnerPlugin { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public bool TriggerClickAction() public bool TriggerClickAction()
{ {

View file

@ -1,3 +1,4 @@
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -8,6 +9,9 @@ using Dalamud.IoC.Internal;
using Dalamud.Memory; using Dalamud.Memory;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Serilog; using Serilog;
namespace Dalamud.Game.Gui.FlyText; namespace Dalamud.Game.Gui.FlyText;
@ -19,62 +23,21 @@ namespace Dalamud.Game.Gui.FlyText;
internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
{ {
/// <summary> /// <summary>
/// The native function responsible for adding fly text to the UI. See <see cref="FlyTextGuiAddressResolver.AddFlyText"/>. /// The hook that fires when the game creates a fly text element.
/// </summary> /// </summary>
private readonly AddFlyTextDelegate addFlyTextNative; private readonly Hook<AddonFlyText.Delegates.CreateFlyText> createFlyTextHook;
/// <summary>
/// The hook that fires when the game creates a fly text element. See <see cref="FlyTextGuiAddressResolver.CreateFlyText"/>.
/// </summary>
private readonly Hook<CreateFlyTextDelegate> createFlyTextHook;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private FlyTextGui(TargetSigScanner sigScanner) private unsafe FlyTextGui(TargetSigScanner sigScanner)
{ {
this.Address = new FlyTextGuiAddressResolver(); this.createFlyTextHook = Hook<AddonFlyText.Delegates.CreateFlyText>.FromAddress(AddonFlyText.Addresses.CreateFlyText.Value, this.CreateFlyTextDetour);
this.Address.Setup(sigScanner);
this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer<AddFlyTextDelegate>(this.Address.AddFlyText);
this.createFlyTextHook = Hook<CreateFlyTextDelegate>.FromAddress(this.Address.CreateFlyText, this.CreateFlyTextDetour);
this.createFlyTextHook.Enable(); this.createFlyTextHook.Enable();
} }
/// <summary>
/// Private delegate for the native CreateFlyText function's hook.
/// </summary>
private delegate IntPtr CreateFlyTextDelegate(
IntPtr addonFlyText,
FlyTextKind kind,
int val1,
int val2,
IntPtr text2,
uint color,
uint icon,
uint damageTypeIcon,
IntPtr text1,
float yOffset);
/// <summary>
/// Private delegate for the native AddFlyText function pointer.
/// </summary>
private delegate void AddFlyTextDelegate(
IntPtr addonFlyText,
uint actorIndex,
uint messageMax,
IntPtr numbers,
uint offsetNum,
uint offsetNumMax,
IntPtr strings,
uint offsetStr,
uint offsetStrMax,
int unknown);
/// <inheritdoc/> /// <inheritdoc/>
public event IFlyTextGui.OnFlyTextCreatedDelegate? FlyTextCreated; public event IFlyTextGui.OnFlyTextCreatedDelegate? FlyTextCreated;
private FlyTextGuiAddressResolver Address { get; }
/// <summary> /// <summary>
/// Disposes of managed and unmanaged resources. /// Disposes of managed and unmanaged resources.
/// </summary> /// </summary>
@ -87,26 +50,16 @@ internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
public unsafe void AddFlyText(FlyTextKind kind, uint actorIndex, uint val1, uint val2, SeString text1, SeString text2, uint color, uint icon, uint damageTypeIcon) public unsafe void AddFlyText(FlyTextKind kind, uint actorIndex, uint val1, uint val2, SeString text1, SeString text2, uint color, uint icon, uint damageTypeIcon)
{ {
// Known valid flytext region within the atk arrays // Known valid flytext region within the atk arrays
var numIndex = 30;
var strIndex = 27;
var numOffset = 161u; var numOffset = 161u;
var strOffset = 28u; var strOffset = 28u;
// Get the UI module and flytext addon pointers var flytext = (AddonFlyText*)RaptureAtkUnitManager.Instance()->GetAddonByName("_FlyText");
var gameGui = Service<GameGui>.GetNullable(); if (flytext == null)
if (gameGui == null)
return;
var ui = (FFXIVClientStructs.FFXIV.Client.UI.UIModule*)gameGui.GetUIModule();
var flytext = gameGui.GetAddonByName("_FlyText");
if (ui == null || flytext == IntPtr.Zero)
return; return;
// Get the number and string arrays we need // Get the number and string arrays we need
var atkArrayDataHolder = ui->GetRaptureAtkModule()->AtkModule.AtkArrayDataHolder; var numArray = AtkStage.Instance()->GetNumberArrayData(NumberArrayType.FlyText);
var numArray = atkArrayDataHolder._NumberArrays[numIndex]; var strArray = AtkStage.Instance()->GetStringArrayData(StringArrayType.FlyText);
var strArray = atkArrayDataHolder._StringArrays[strIndex];
// Write the values to the arrays using a known valid flytext region // Write the values to the arrays using a known valid flytext region
numArray->IntArray[numOffset + 0] = 1; // Some kind of "Enabled" flag for this section numArray->IntArray[numOffset + 0] = 1; // Some kind of "Enabled" flag for this section
@ -120,66 +73,47 @@ internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
numArray->IntArray[numOffset + 8] = 0; // Unknown numArray->IntArray[numOffset + 8] = 0; // Unknown
numArray->IntArray[numOffset + 9] = 0; // Unknown, has something to do with yOffset numArray->IntArray[numOffset + 9] = 0; // Unknown, has something to do with yOffset
strArray->SetValue((int)strOffset + 0, text1.Encode(), false, true, false); strArray->SetValue((int)strOffset + 0, text1.EncodeWithNullTerminator(), false, true, false);
strArray->SetValue((int)strOffset + 1, text2.Encode(), false, true, false); strArray->SetValue((int)strOffset + 1, text2.EncodeWithNullTerminator(), false, true, false);
this.addFlyTextNative( flytext->AddFlyText(actorIndex, 1, numArray, numOffset, 10, strArray, strOffset, 2, 0);
flytext,
actorIndex,
1,
(IntPtr)numArray,
numOffset,
10,
(IntPtr)strArray,
strOffset,
2,
0);
} }
private static byte[] Terminate(byte[] source) private unsafe nint CreateFlyTextDetour(
{ AddonFlyText* thisPtr,
var terminated = new byte[source.Length + 1]; int kind,
Array.Copy(source, 0, terminated, 0, source.Length);
terminated[^1] = 0;
return terminated;
}
private IntPtr CreateFlyTextDetour(
IntPtr addonFlyText,
FlyTextKind kind,
int val1, int val1,
int val2, int val2,
IntPtr text2, byte* text2,
uint color, uint color,
uint icon, uint icon,
uint damageTypeIcon, uint damageTypeIcon,
IntPtr text1, byte* text1,
float yOffset) float yOffset)
{ {
var retVal = IntPtr.Zero; var retVal = nint.Zero;
try try
{ {
Log.Verbose("[FlyText] Enter CreateFlyText detour!"); Log.Verbose("[FlyText] Enter CreateFlyText detour!");
var handled = false; var handled = false;
var tmpKind = kind; var tmpKind = (FlyTextKind)kind;
var tmpVal1 = val1; var tmpVal1 = val1;
var tmpVal2 = val2; var tmpVal2 = val2;
var tmpText1 = text1 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(text1); var tmpText1 = text1 == null ? string.Empty : MemoryHelper.ReadSeStringNullTerminated((nint)text1);
var tmpText2 = text2 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(text2); var tmpText2 = text2 == null ? string.Empty : MemoryHelper.ReadSeStringNullTerminated((nint)text2);
var tmpColor = color; var tmpColor = color;
var tmpIcon = icon; var tmpIcon = icon;
var tmpDamageTypeIcon = damageTypeIcon; var tmpDamageTypeIcon = damageTypeIcon;
var tmpYOffset = yOffset; var tmpYOffset = yOffset;
var cmpText1 = tmpText1.ToString(); var originalText1 = tmpText1.EncodeWithNullTerminator();
var cmpText2 = tmpText2.ToString(); var originalText2 = tmpText2.EncodeWithNullTerminator();
Log.Verbose($"[FlyText] Called with addonFlyText({addonFlyText.ToInt64():X}) " + Log.Verbose($"[FlyText] Called with addonFlyText({(nint)thisPtr:X}) " +
$"kind({kind}) val1({val1}) val2({val2}) damageTypeIcon({damageTypeIcon}) " + $"kind({kind}) val1({val1}) val2({val2}) damageTypeIcon({damageTypeIcon}) " +
$"text1({text1.ToInt64():X}, \"{tmpText1}\") text2({text2.ToInt64():X}, \"{tmpText2}\") " + $"text1({(nint)text1:X}, \"{tmpText1}\") text2({(nint)text2:X}, \"{tmpText2}\") " +
$"color({color:X}) icon({icon}) yOffset({yOffset})"); $"color({color:X}) icon({icon}) yOffset({yOffset})");
Log.Verbose("[FlyText] Calling flytext events!"); Log.Verbose("[FlyText] Calling flytext events!");
this.FlyTextCreated?.Invoke( this.FlyTextCreated?.Invoke(
@ -204,12 +138,15 @@ internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
return IntPtr.Zero; return IntPtr.Zero;
} }
var maybeModifiedText1 = tmpText1.EncodeWithNullTerminator();
var maybeModifiedText2 = tmpText2.EncodeWithNullTerminator();
// Check if any values have changed // Check if any values have changed
var dirty = tmpKind != kind || var dirty = (int)tmpKind != kind ||
tmpVal1 != val1 || tmpVal1 != val1 ||
tmpVal2 != val2 || tmpVal2 != val2 ||
tmpText1.ToString() != cmpText1 || !maybeModifiedText1.SequenceEqual(originalText1) ||
tmpText2.ToString() != cmpText2 || !maybeModifiedText2.SequenceEqual(originalText2) ||
tmpDamageTypeIcon != damageTypeIcon || tmpDamageTypeIcon != damageTypeIcon ||
tmpColor != color || tmpColor != color ||
tmpIcon != icon || tmpIcon != icon ||
@ -219,28 +156,26 @@ internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui
if (!dirty) if (!dirty)
{ {
Log.Verbose("[FlyText] Calling flytext with original args."); Log.Verbose("[FlyText] Calling flytext with original args.");
return this.createFlyTextHook.Original(addonFlyText, kind, val1, val2, text2, color, icon, return this.createFlyTextHook.Original(thisPtr, kind, val1, val2, text2, color, icon,
damageTypeIcon, text1, yOffset); damageTypeIcon, text1, yOffset);
} }
var terminated1 = Terminate(tmpText1.Encode()); var pText1 = Marshal.AllocHGlobal(maybeModifiedText1.Length);
var terminated2 = Terminate(tmpText2.Encode()); var pText2 = Marshal.AllocHGlobal(maybeModifiedText2.Length);
var pText1 = Marshal.AllocHGlobal(terminated1.Length); Marshal.Copy(maybeModifiedText1, 0, pText1, maybeModifiedText1.Length);
var pText2 = Marshal.AllocHGlobal(terminated2.Length); Marshal.Copy(maybeModifiedText2, 0, pText2, maybeModifiedText2.Length);
Marshal.Copy(terminated1, 0, pText1, terminated1.Length);
Marshal.Copy(terminated2, 0, pText2, terminated2.Length);
Log.Verbose("[FlyText] Allocated and set strings."); Log.Verbose("[FlyText] Allocated and set strings.");
retVal = this.createFlyTextHook.Original( retVal = this.createFlyTextHook.Original(
addonFlyText, thisPtr,
tmpKind, (int)tmpKind,
tmpVal1, tmpVal1,
tmpVal2, tmpVal2,
pText2, (byte*)pText2,
tmpColor, tmpColor,
tmpIcon, tmpIcon,
tmpDamageTypeIcon, tmpDamageTypeIcon,
pText1, (byte*)pText1,
tmpYOffset); tmpYOffset);
Log.Verbose("[FlyText] Returned from original. Delaying free task."); Log.Verbose("[FlyText] Returned from original. Delaying free task.");

View file

@ -1,29 +0,0 @@
namespace Dalamud.Game.Gui.FlyText;
/// <summary>
/// An address resolver for the <see cref="FlyTextGui"/> class.
/// </summary>
internal class FlyTextGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the native AddFlyText method, which occurs
/// when the game adds fly text elements to the UI. Multiple fly text
/// elements can be added in a single AddFlyText call.
/// </summary>
public IntPtr AddFlyText { get; private set; }
/// <summary>
/// Gets the address of the native CreateFlyText method, which occurs
/// when the game creates a new fly text element. This method is called
/// once per fly text element, and can be called multiple times per
/// AddFlyText call.
/// </summary>
public IntPtr CreateFlyText { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(ISigScanner sig)
{
this.AddFlyText = sig.ScanText("E8 ?? ?? ?? ?? FF C7 41 D1 C7");
this.CreateFlyText = sig.ScanText("E8 ?? ?? ?? ?? 48 8B F8 48 85 C0 0F 84 ?? ?? ?? ?? 48 8B 18");
}
}

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