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
insert_final_newline = true
trim_trailing_whitespace = true
# 4 space indentation
indent_style = space

View file

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

View file

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

9
.gitmodules vendored
View file

@ -10,3 +10,12 @@
[submodule "lib/ImGui.NET"]
path = lib/ImGui.NET
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": {
"type": "string",
"enum": [
"CI",
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"CompileInjectorBoot",
"Restore",
"SetCILogging",
"Test"
]
}
@ -98,14 +104,20 @@
"items": {
"type": "string",
"enum": [
"CI",
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"CompileInjectorBoot",
"Restore",
"SetCILogging",
"Test"
]
}

View file

@ -28,7 +28,7 @@
<PlatformToolset>v143</PlatformToolset>
<LinkIncremental>false</LinkIncremental>
<CharacterSet>Unicode</CharacterSet>
<OutDir>..\bin\$(Configuration)\</OutDir>
<OutDir>bin\$(Configuration)\</OutDir>
<IntDir>obj\$(Configuration)\</IntDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
@ -200,8 +200,10 @@
<ItemGroup>
<Manifest Include="themes.manifest" />
</ItemGroup>
<Target Name="RemoveExtraFiles" AfterTargets="PostBuildEvent">
<Delete Files="$(OutDir)$(TargetName).lib" />
<Delete Files="$(OutDir)$(TargetName).exp" />
<Target Name="CopyOutputDlls" AfterTargets="PostBuildEvent">
<Copy SourceFiles="$(OutDir)$(TargetName).dll" DestinationFolder="..\bin\$(Configuration)\" />
<Copy SourceFiles="$(OutDir)$(TargetName).pdb" DestinationFolder="..\bin\$(Configuration)\" />
<Copy SourceFiles="$(OutDir)nethost.dll" DestinationFolder="..\bin\$(Configuration)\" />
</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.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory);
config.Language = json.value("Language", config.Language);
config.Platform = json.value("Platform", config.Platform);
config.GameVersion = json.value("GameVersion", config.GameVersion);
config.TroubleshootingPackData = json.value("TroubleshootingPackData", std::string{});
config.DelayInitializeMs = json.value("DelayInitializeMs", config.DelayInitializeMs);

View file

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

View file

@ -15,7 +15,7 @@ HINSTANCE g_hGameInstance = GetModuleHandleW(nullptr);
HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
g_startInfo.from_envvars();
std::string jsonParseError;
try {
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)
ConsoleSetup(L"Dalamud Boot");
logging::update_dll_load_status(true);
const auto logFilePath = unicode::convert<std::wstring>(g_startInfo.BootLogPath);
@ -33,16 +33,16 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
auto attemptFallbackLog = false;
if (logFilePath.empty()) {
attemptFallbackLog = true;
logging::I("No log file path given; not logging to file.");
} else {
try {
logging::start_file_logging(logFilePath, !g_startInfo.BootShowConsole);
logging::I("Logging to file: {}", logFilePath);
} catch (const std::exception& e) {
attemptFallbackLog = true;
logging::E("Couldn't open log file: {}", logFilePath);
logging::E("Error: {} / {}", errno, e.what());
}
@ -63,15 +63,15 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
SYSTEMTIME 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());
try {
logging::start_file_logging(logFilePath, !g_startInfo.BootShowConsole);
logging::I("Logging to fallback log file: {}", logFilePath);
} catch (const std::exception& e) {
if (!g_startInfo.BootShowConsole && !g_startInfo.BootDisableFallbackConsole)
ConsoleSetup(L"Dalamud Boot - Fallback Console");
logging::E("Couldn't open fallback log file: {}", logFilePath);
logging::E("Error: {} / {}", errno, e.what());
}
@ -87,7 +87,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
} else {
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("Built at: " __DATE__ "@" __TIME__);
@ -241,11 +241,11 @@ BOOL APIENTRY DllMain(const HMODULE hModule, const DWORD dwReason, LPVOID lpRese
case DLL_PROCESS_DETACH:
// do not show debug message boxes on abort() here
_set_abort_behavior(0, _WRITE_ABORT_MSG);
// process is terminating; don't bother cleaning up
if (lpReserved)
return TRUE;
logging::update_dll_load_status(false);
xivfixes::apply_all(false);

View file

@ -1,4 +1,5 @@
#include "pch.h"
#include "DalamudStartInfo.h"
#include "utils.h"
@ -103,7 +104,7 @@ bool utils::loaded_module::find_imported_function_pointer(const char* pcszDllNam
ppFunctionAddress = nullptr;
// 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.
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;
}
bool utils::is_running_on_wine() {
return g_startInfo.Platform != "WINDOWS";
}
std::filesystem::path utils::get_module_path(HMODULE hModule) {
std::wstring buf(MAX_PATH, L'\0');
while (true) {

View file

@ -121,7 +121,7 @@ namespace utils {
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>>>
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>
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());
}
bool is_running_on_wine();
std::filesystem::path get_module_path(HMODULE hModule);
/// @brief Find the game main window.

View file

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

View file

@ -1,3 +1,4 @@
using System.Runtime.InteropServices;
using Dalamud.Common.Game;
using Newtonsoft.Json;
@ -15,7 +16,7 @@ public record DalamudStartInfo
/// </summary>
public DalamudStartInfo()
{
// ignored
this.Platform = OSPlatform.Create("UNKNOWN");
}
/// <summary>
@ -58,6 +59,12 @@ public record DalamudStartInfo
/// </summary>
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>
/// Gets or sets the current game version code.
/// </summary>
@ -125,7 +132,7 @@ public record DalamudStartInfo
public bool BootVehFull { get; set; }
/// <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>
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">
<PropertyGroup>
<AssemblyName>Dalamud.CorePlugin</AssemblyName>
<TargetFramework>net8.0-windows</TargetFramework>
<Platforms>x64</Platforms>
<LangVersion>10.0</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
@ -27,9 +25,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Lumina" Version="4.1.0" />
<PackageReference Include="Lumina.Excel" Version="7.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Lumina" Version="$(LuminaVersion)" />
<PackageReference Include="Lumina.Excel" Version="$(LuminaExcelVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

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

View file

@ -11,6 +11,8 @@ using System.Text.RegularExpressions;
using Dalamud.Common;
using Dalamud.Common.Game;
using Dalamud.Common.Util;
using Newtonsoft.Json;
using Reloaded.Memory.Buffers;
using Serilog;
@ -95,6 +97,7 @@ namespace Dalamud.Injector
args.Remove("--msgbox2");
args.Remove("--msgbox3");
args.Remove("--etw");
args.Remove("--no-legacy-corrupted-state-exceptions");
args.Remove("--veh");
args.Remove("--veh-full");
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)
{
int len;
@ -278,9 +310,14 @@ namespace Dalamud.Injector
var logName = startInfo.LogName;
var logPath = startInfo.LogPath;
var languageStr = startInfo.Language.ToString().ToLowerInvariant();
var platformStr = startInfo.Platform.ToString().ToLowerInvariant();
var unhandledExceptionStr = startInfo.UnhandledException.ToString().ToLowerInvariant();
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++)
{
if (args[i].StartsWith(key = "--dalamud-working-directory="))
@ -307,6 +344,10 @@ namespace Dalamud.Injector
{
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="))
{
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.");
}
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.ConfigurationPath = configurationPath;
startInfo.PluginDirectory = pluginDirectory;
startInfo.AssetDirectory = assetDirectory;
startInfo.Language = clientLanguage;
startInfo.Platform = platform;
startInfo.DelayInitializeMs = delayInitializeMs;
startInfo.GameVersion = null;
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(" [--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-client-language=0-3|j(apanese)|e(nglish)|d|g(erman)|f(rench)]");
Console.WriteLine("Verbose logging:\t[-v]");
Console.WriteLine("Show Console:\t[--console] [--crash-handler-console]");
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("Show messagebox:\t[--msgbox1], [--msgbox2], [--msgbox3]");
Console.WriteLine("No plugins:\t[--no-plugin] [--no-3rd-plugin]");
@ -731,15 +799,42 @@ namespace Dalamud.Injector
{
try
{
var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher");
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);
if (dalamudStartInfo.Platform == OSPlatform.Windows)
{
var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher");
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)
{
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;
}
@ -794,20 +889,6 @@ namespace Dalamud.Injector
if (encryptArguments)
{
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 key = ticks & 0xFFFF_0000u;
gameArguments.Insert(0, $"T={ticks}");

View file

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace Dalamud.Injector
@ -910,5 +911,46 @@ namespace Dalamud.Injector
uint dwDesiredAccess,
[MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
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"?>
<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">
<RootNamespace>Dalamud.Test</RootNamespace>
<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/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/=collectability/@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/=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/=Refilter/@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/=unsanitized/@EntryIndexedValue">True</s:Boolean>
<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.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Dalamud.Game.Text;
using Dalamud.Interface;
@ -11,6 +12,7 @@ using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.Style;
using Dalamud.Interface.Windowing.Persistence;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal.AutoUpdate;
using Dalamud.Plugin.Internal.Profiles;
@ -45,6 +47,8 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
[JsonIgnore]
private bool isSaveQueued;
private Task? writeTask;
/// <summary>
/// Delegate for the <see cref="DalamudConfiguration.DalamudConfigurationSaved"/> event that occurs when the dalamud configuration is saved.
/// </summary>
@ -57,7 +61,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public event DalamudConfigurationSavedDelegate? DalamudConfigurationSaved;
/// <summary>
/// Gets or sets a list of muted works.
/// Gets or sets a list of muted words.
/// </summary>
public List<string>? BadWords { get; set; }
@ -243,13 +247,13 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary>
/// Gets or sets a value indicating whether or not ImGui asserts should be enabled at startup.
/// </summary>
public bool AssertsEnabledAtStartup { get; set; }
public bool? ImGuiAssertsEnabledAtStartup { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not docking should be globally enabled in ImGui.
/// </summary>
public bool IsDocking { get; set; }
/// <summary>
/// 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.
@ -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
/// on plugin title bars when using the Window System.
/// </summary>
[JsonProperty("EnablePluginUiAdditionalOptionsExperimental")]
public bool EnablePluginUiAdditionalOptions { get; set; } = false;
public bool EnablePluginUiAdditionalOptions { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether viewports should always be disabled.
@ -348,6 +351,11 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary>
public bool ProfilesHasSeenTutorial { get; set; } = false;
/// <summary>
/// Gets or sets the default UI preset.
/// </summary>
public PresetModel DefaultUiPreset { get; set; } = new();
/// <summary>
/// Gets or sets the order of DTR elements, by title.
/// </summary>
@ -484,10 +492,15 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
public AutoUpdateBehavior? AutoUpdateBehavior { get; set; } = null;
/// <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>
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>
/// Load a configuration from the provided path.
/// </summary>
@ -504,7 +517,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
{
deserialized =
JsonConvert.DeserializeObject<DalamudConfiguration>(text, SerializerSettings);
// If this reads as null, the file was empty, that's no good
if (deserialized == 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");
}
return deserialized;
}
@ -549,12 +562,15 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
{
this.Save();
}
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
// Make sure that we save, if a save is queued while we are shutting down
this.Update();
// Wait for the write task to finish
this.writeTask?.Wait();
}
/// <summary>
@ -595,22 +611,36 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
this.ReduceMotions = winAnimEnabled == 0;
}
}
// Migrate old auto-update setting to new auto-update behavior
this.AutoUpdateBehavior ??= this.AutoUpdatePlugins
? Plugin.Internal.AutoUpdate.AutoUpdateBehavior.UpdateAll
: Plugin.Internal.AutoUpdate.AutoUpdateBehavior.OnlyNotify;
#pragma warning restore CS0618
}
private void Save()
{
ThreadSafety.AssertMainThread();
if (this.configPath is null)
throw new InvalidOperationException("configPath is not set.");
Service<ReliableFileStorage>.Get().WriteAllText(
this.configPath, JsonConvert.SerializeObject(this, SerializerSettings));
// Wait for previous write to finish
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);
}
}

View file

@ -5,11 +5,6 @@ namespace Dalamud.Configuration.Internal;
/// </summary>
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>
/// Gets a value indicating whether the DALAMUD_NOT_HAVE_PLUGINS setting has been enabled.
/// </summary>

View file

@ -1,16 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Target">
<TargetFramework>net8.0-windows</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<Platforms>x64;AnyCPU</Platforms>
<LangVersion>12.0</LangVersion>
<EnableWindowsTargeting>True</EnableWindowsTargeting>
</PropertyGroup>
<PropertyGroup Label="Feature">
<DalamudVersion>10.0.0.7</DalamudVersion>
<Description>XIV Launcher addon framework</Description>
<DalamudVersion>12.0.0.7</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion>
@ -47,10 +43,6 @@
<PropertyGroup Label="Configuration" Condition="'$(Configuration)'=='Debug'">
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Label="Configuration" Condition="'$(Configuration)'=='Release'">
<AppOutputBase>$(MSBuildProjectDirectory)\</AppOutputBase>
<PathMap>$(AppOutputBase)=C:\goatsoft\companysecrets\dalamud\</PathMap>
</PropertyGroup>
<PropertyGroup Label="Warnings">
<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.Assembler" Version="1.0.14-goat.2" />
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
<PackageReference Include="Lumina" Version="4.1.0" />
<PackageReference Include="Lumina.Excel" Version="7.0.1" />
<PackageReference Include="Lumina" Version="$(LuminaVersion)" />
<PackageReference Include="Lumina.Excel" Version="$(LuminaExcelVersion)" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0-preview.1.24081.5" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">
<PrivateAssets>all</PrivateAssets>
@ -83,12 +75,13 @@
<PackageReference Include="PInvoke.Kernel32" Version="0.7.104" />
<PackageReference Include="PInvoke.User32" 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.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="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
@ -113,6 +106,7 @@
<ProjectReference Include="..\Dalamud.Common\Dalamud.Common.csproj" />
<ProjectReference Include="..\lib\FFXIVClientStructs\FFXIVClientStructs\FFXIVClientStructs.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>
@ -126,6 +120,13 @@
</Content>
</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">
<ItemGroup>
<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;$(SCMVersion)&quot; &gt; $(TempVerFile)" IgnoreExitCode="true" />
</Target>
<Target Name="GenerateStubVersionData" BeforeTargets="WriteVersionData" Condition="'$(SCMVersion)'=='' And '$(Configuration)'!='Release'">
<!-- stub out version since it takes a while. -->
<PropertyGroup>
@ -173,7 +174,7 @@
<CommitHashClientStructs>???</CommitHashClientStructs>
</PropertyGroup>
</Target>
<Target Name="WriteVersionData" BeforeTargets="CoreCompile">
<!-- names the obj/.../CustomAssemblyInfo.cs file -->
<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 />
/// Either ship your own assets, or be prepared for errors.
/// </summary>
// Implementation notes: avoid specifying numbers too high here. Lookup table is currently implemented as an array.
public enum DalamudAsset
{
/// <summary>

View file

@ -1,5 +1,6 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Game;
using Dalamud.IoC;
@ -10,6 +11,7 @@ using Dalamud.Utility.Timing;
using Lumina;
using Lumina.Data;
using Lumina.Excel;
using Newtonsoft.Json;
using Serilog;
@ -27,12 +29,15 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
{
private readonly Thread luminaResourceThread;
private readonly CancellationTokenSource luminaCancellationTokenSource;
private readonly RsvResolver rsvResolver;
[ServiceManager.ServiceConstructor]
private DataManager(Dalamud dalamud)
{
this.Language = (ClientLanguage)dalamud.StartInfo.Language;
this.rsvResolver = new();
try
{
Log.Verbose("Starting data load...");
@ -43,11 +48,8 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
{
LoadMultithreaded = true,
CacheFileResources = true,
#if NEVER // Lumina bug
PanicOnSheetChecksumMismatch = true,
#else
PanicOnSheetChecksumMismatch = false,
#endif
RsvResolver = this.rsvResolver.TryResolve,
DefaultExcelLanguage = this.Language.ToLumina(),
};
@ -128,12 +130,12 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
#region Lumina Wrappers
/// <inheritdoc/>
public ExcelSheet<T>? GetExcelSheet<T>() where T : ExcelRow
=> this.Excel.GetSheet<T>();
public ExcelSheet<T> GetExcelSheet<T>(ClientLanguage? language = null, string? name = null) where T : struct, IExcelRow<T>
=> this.Excel.GetSheet<T>(language?.ToLumina(), name);
/// <inheritdoc/>
public ExcelSheet<T>? GetExcelSheet<T>(ClientLanguage language) where T : ExcelRow
=> this.Excel.GetSheet<T>(language.ToLumina());
public SubrowExcelSheet<T> GetSubrowExcelSheet<T>(ClientLanguage? language = null, string? name = null) where T : struct, IExcelSubrow<T>
=> this.Excel.GetSubrowSheet<T>(language?.ToLumina(), name);
/// <inheritdoc/>
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;
}
/// <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/>
public bool FileExists(string path)
=> this.GameData.FileExists(path);
@ -159,6 +171,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
{
this.luminaCancellationTokenSource.Cancel();
this.GameData.Dispose();
this.rsvResolver.Dispose();
}
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);
// This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls;
// Apply common fixes for culture issues
CultureFixes.Apply();
if (!Util.IsWine())
// Currently VEH is not fully functional on WINE
if (info.Platform != OSPlatform.Windows)
InitSymbolHandler(info);
var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent);
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]",
Util.GetScmVersion(),
Util.GetGitHashClientStructs(),
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]",
Util.GetScmVersion(),
Util.GetGitHashClientStructs(),
FFXIVClientStructs.ThisAssembly.Git.Commits);
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
{
private ReceiveEventDelegate? receiveEventDelegate;
private AtkEventListener* eventListener;
/// <summary>
/// Initializes a new instance of the <see cref="AddonEventListener"/> class.
/// </summary>
@ -24,7 +24,7 @@ internal unsafe class AddonEventListener : IDisposable
this.eventListener = (AtkEventListener*)Marshal.AllocHGlobal(sizeof(AtkEventListener));
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->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="eventDataPtr">Pointer to the AtkEventData.</param>
public delegate void ReceiveEventDelegate(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventPtr, AtkEventData* eventDataPtr);
/// <summary>
/// Gets the address of this listener.
/// </summary>
public nint Address => (nint)this.eventListener;
/// <inheritdoc />
public void Dispose()
{
if (this.eventListener is null) return;
Marshal.FreeHGlobal((nint)this.eventListener->VirtualTable);
Marshal.FreeHGlobal((nint)this.eventListener);
@ -88,7 +88,7 @@ internal unsafe class AddonEventListener : IDisposable
node->RemoveEvent(eventType, param, this.eventListener, false);
});
}
[UnmanagedCallersOnly]
private static void NullSub()
{

View file

@ -16,6 +16,6 @@ internal class AddonEventManagerAddressResolver : BaseAddressResolver
/// <param name="scanner">The signature scanner to facilitate setup.</param>
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 eventListenerAddressMatches = (nint)currentEvent->Listener == this.EventListener.Address;
var eventTypeMatches = currentEvent->Type == eventType;
var eventTypeMatches = currentEvent->State.EventType == eventType;
if (paramKeyMatches && eventListenerAddressMatches && eventTypeMatches)
{

View file

@ -52,7 +52,7 @@ public enum AddonEvent
PostDraw,
/// <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.
/// This event can be used for cleanup and tracking tasks.
/// </summary>

View file

@ -13,19 +13,19 @@ internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver
/// This is called for a majority of all addon OnSetup's.
/// </summary>
public nint AddonSetup { get; private set; }
/// <summary>
/// Gets the address of the other addon setup hook invoked by the AtkUnitManager.
/// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
/// This seems to be called rarely for specific addons.
/// </summary>
public nint AddonSetup2 { get; private set; }
/// <summary>
/// Gets the address of the addon finalize hook invoked by the AtkUnitManager.
/// </summary>
public nint AddonFinalize { get; private set; }
/// <summary>
/// Gets the address of the addon draw hook invoked by virtual function call.
/// </summary>
@ -35,7 +35,7 @@ internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver
/// Gets the address of the addon update hook invoked by virtual function call.
/// </summary>
public nint AddonUpdate { get; private set; }
/// <summary>
/// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call.
/// </summary>
@ -51,6 +51,6 @@ internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver
this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5");
this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C4 48 81 EF ?? ?? ?? ?? 48 83 ED 01");
this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF ?? ?? ?? ?? 45 33 D2");
this.AddonOnRequestedUpdate = sig.ScanText("FF 90 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.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using CheapLoc;
using Dalamud.Configuration.Internal;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.Gui;
using Dalamud.Game.Text;
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.Windows;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Utility;
@ -27,49 +19,10 @@ namespace Dalamud.Game;
/// Chat events and public helper functions.
/// </summary>
[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]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
@ -92,6 +45,9 @@ internal class ChatHandlers : IServiceType
/// </summary>
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)
{
var textVal = message.TextValue;
@ -100,7 +56,7 @@ internal class ChatHandlers : IServiceType
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
Log.Debug("Blocklist triggered");
Log.Debug("Filtered a message that contained a muted word");
isHandled = true;
return;
}
@ -127,41 +83,10 @@ internal class ChatHandlers : IServiceType
return;
#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 senderCopy = sender;
var linkMatch = this.urlRegex.Match(message.TextValue);
var linkMatch = CompiledUrlRegex().Match(message.TextValue);
if (linkMatch.Value.Length > 0)
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 Lumina.Excel;
namespace Dalamud.Game.ClientState.Aetherytes;
/// <summary>
@ -56,7 +59,7 @@ public interface IAetheryteEntry
/// <summary>
/// Gets the Aetheryte data related to this aetheryte.
/// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.Aetheryte> AetheryteData { get; }
RowRef<Lumina.Excel.Sheets.Aetheryte> AetheryteData { get; }
}
/// <summary>
@ -103,5 +106,5 @@ internal sealed class AetheryteEntry : IAetheryteEntry
public bool IsApartment => this.data.IsApartment;
/// <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.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
namespace Dalamud.Game.ClientState.Buddy;
@ -28,14 +26,9 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get();
private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor]
private BuddyList()
{
this.address = this.clientState.AddressResolver;
Log.Verbose($"Buddy list address {Util.DescribeAddress(this.address.BuddyList)}");
}
/// <inheritdoc/>
@ -76,14 +69,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
}
}
/// <summary>
/// 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;
private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => &UIState.Instance()->Buddy;
/// <inheritdoc/>
public IBuddyMember? this[int index]

View file

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

View file

@ -1,5 +1,4 @@
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Data;
using Dalamud.Game.ClientState.Conditions;
@ -13,10 +12,15 @@ using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Application.Network;
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 Lumina.Excel.GeneratedSheets;
using Lumina.Excel.Sheets;
using Action = System.Action;
@ -29,22 +33,23 @@ namespace Dalamud.Game.ClientState;
internal sealed class ClientState : IInternalDisposableService, IClientState
{
private static readonly ModuleLog Log = new("ClientState");
private readonly GameLifecycle lifecycle;
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]
private readonly Framework framework = Service<Framework>.Get();
[ServiceManager.ServiceDependency]
private readonly NetworkHandlers networkHandlers = Service<NetworkHandlers>.Get();
private bool lastConditionNone = true;
private bool lastFramePvP;
[ServiceManager.ServiceConstructor]
private ClientState(TargetSigScanner sigScanner, Dalamud dalamud, GameLifecycle lifecycle)
private unsafe ClientState(TargetSigScanner sigScanner, Dalamud dalamud, GameLifecycle lifecycle)
{
this.lifecycle = lifecycle;
this.address = new ClientStateAddressResolver();
@ -54,28 +59,37 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
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.networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
this.setupTerritoryTypeHook.Enable();
this.uiModuleHandlePacketHook.Enable();
this.onLogoutHook.Enable();
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr SetupTerritoryTypeDelegate(IntPtr manager, ushort terriType);
private unsafe delegate void ProcessPacketPlayerSetupDelegate(nint a1, nint packet);
/// <inheritdoc/>
public event Action<ushort>? TerritoryChanged;
/// <inheritdoc/>
public event IClientState.ClassJobChangeDelegate? ClassJobChanged;
/// <inheritdoc/>
public event IClientState.LevelChangeDelegate? LevelChanged;
/// <inheritdoc/>
public event Action? Login;
/// <inheritdoc/>
public event Action? Logout;
public event IClientState.LogoutDelegate? Logout;
/// <inheritdoc/>
public event Action? EnterPvP;
@ -98,7 +112,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
get
{
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;
/// <inheritdoc/>
public ulong LocalContentId => (ulong)Marshal.ReadInt64(this.address.LocalContentId);
public unsafe ulong LocalContentId => PlayerState.Instance()->ContentId;
/// <inheritdoc/>
public bool IsLoggedIn { get; private set; }
public unsafe bool IsLoggedIn
{
get
{
var agentLobby = AgentLobby.Instance();
return agentLobby != null && agentLobby->IsLoggedIn;
}
}
/// <inheritdoc/>
public bool IsPvP { get; private set; }
/// <inheritdoc/>
public bool IsPvPExcludingDen { get; private set; }
public bool IsPvPExcludingDen => this.IsPvP && this.TerritoryType != 250;
/// <inheritdoc />
public bool IsGPosing => GameMain.IsInGPose();
@ -124,25 +145,25 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
/// Gets client state address resolver.
/// </summary>
internal ClientStateAddressResolver AddressResolver => this.address;
/// <inheritdoc/>
public bool IsClientIdle(out ConditionFlag blockingFlag)
{
blockingFlag = 0;
if (this.LocalPlayer is null) return true;
var condition = Service<Conditions.Condition>.GetNullable();
var blockingConditions = condition.AsReadOnlySet().Except([
ConditionFlag.NormalConditions,
ConditionFlag.Jumping,
ConditionFlag.Mounted,
ConditionFlag.NormalConditions,
ConditionFlag.Jumping,
ConditionFlag.Mounted,
ConditionFlag.UsingParasol]);
blockingFlag = blockingConditions.FirstOrDefault();
return blockingFlag == 0;
}
/// <inheritdoc/>
public bool IsClientIdle() => this.IsClientIdle(out _);
@ -152,23 +173,89 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
void IInternalDisposableService.DisposeService()
{
this.setupTerritoryTypeHook.Dispose();
this.framework.Update -= this.FrameworkOnOnUpdateEvent;
this.uiModuleHandlePacketHook.Dispose();
this.onLogoutHook.Dispose();
this.framework.Update -= this.FrameworkOnOnUpdateEvent;
this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop;
}
private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType)
private unsafe void SetupTerritoryTypeDetour(EventFramework* eventFramework, ushort territoryType)
{
this.TerritoryType = terriType;
this.TerritoryChanged?.InvokeSafely(terriType);
Log.Debug("TerritoryType changed: {0}", territoryType);
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)
@ -184,40 +271,58 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
{
Log.Debug("Is login");
this.lastConditionNone = false;
this.IsLoggedIn = true;
this.Login?.InvokeSafely();
gameGui.ResetUiHideState();
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");
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)
try
{
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()
{
this.clientStateService.TerritoryChanged += this.TerritoryChangedForward;
this.clientStateService.ClassJobChanged += this.ClassJobChangedForward;
this.clientStateService.LevelChanged += this.LevelChangedForward;
this.clientStateService.Login += this.LoginForward;
this.clientStateService.Logout += this.LogoutForward;
this.clientStateService.EnterPvP += this.EnterPvPForward;
this.clientStateService.LeavePvP += this.ExitPvPForward;
this.clientStateService.CfPop += this.ContentFinderPopForward;
}
/// <inheritdoc/>
public event Action<ushort>? TerritoryChanged;
/// <inheritdoc/>
public event IClientState.ClassJobChangeDelegate? ClassJobChanged;
/// <inheritdoc/>
public event IClientState.LevelChangeDelegate? LevelChanged;
/// <inheritdoc/>
public event Action? Login;
/// <inheritdoc/>
public event Action? Logout;
public event IClientState.LogoutDelegate? Logout;
/// <inheritdoc/>
public event Action? EnterPvP;
/// <inheritdoc/>
public event Action? LeavePvP;
/// <inheritdoc/>
public event Action<ContentFinderCondition>? CfPop;
@ -270,7 +383,7 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
/// <inheritdoc/>
public ushort TerritoryType => this.clientStateService.TerritoryType;
/// <inheritdoc/>
public uint MapId => this.clientStateService.MapId;
@ -302,6 +415,8 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
void IInternalDisposableService.DisposeService()
{
this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward;
this.clientStateService.ClassJobChanged -= this.ClassJobChangedForward;
this.clientStateService.LevelChanged -= this.LevelChangedForward;
this.clientStateService.Login -= this.LoginForward;
this.clientStateService.Logout -= this.LogoutForward;
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 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 LogoutForward() => this.Logout?.Invoke();
private void LogoutForward(int type, int code) => this.Logout?.Invoke(type, code);
private void EnterPvPForward() => this.EnterPvP?.Invoke();
private void ExitPvPForward() => this.LeavePvP?.Invoke();
private void ContentFinderPopForward(ContentFinderCondition cfc) => this.CfPop?.Invoke(cfc);

View file

@ -7,39 +7,6 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
{
// 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>
/// Gets the address of the keyboard state.
/// </summary>
@ -50,23 +17,12 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
/// </summary>
public IntPtr KeyboardStateIndexArray { get; private set; }
/// <summary>
/// Gets the address of the condition flag array.
/// </summary>
public IntPtr ConditionFlags { get; private set; }
// Functions
/// <summary>
/// Gets the address of the method which sets the territory type.
/// Gets the address of the method which sets up the player.
/// </summary>
public IntPtr SetupTerritoryType { 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; }
public IntPtr ProcessPacketPlayerSetup { get; private set; }
/// <summary>
/// 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>
protected override void Setup64Bit(ISigScanner sig)
{
this.ObjectTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 83 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ??");
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");
this.ProcessPacketPlayerSetup = sig.ScanText("40 53 48 83 EC 20 48 8D 0D ?? ?? ?? ?? 48 8B DA E8 ?? ?? ?? ?? 48 8B D3"); // not in cs struct
// 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
// 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.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;
[ServiceManager.ServiceConstructor]
private Condition(ClientState clientState)
private unsafe Condition()
{
var resolver = clientState.AddressResolver;
this.Address = resolver.ConditionFlags;
this.Address = (nint)FFXIVClientStructs.FFXIV.Client.Game.Conditions.Instance();
// Initialization
for (var i = 0; i < MaxConditionEntries; i++)

View file

@ -1,10 +1,11 @@
using System.Numerics;
using Dalamud.Data;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory;
using Lumina.Excel;
namespace Dalamud.Game.ClientState.Fates;
/// <summary>
@ -20,7 +21,7 @@ public interface IFate : IEquatable<IFate>
/// <summary>
/// Gets game data linked to this Fate.
/// </summary>
Lumina.Excel.GeneratedSheets.Fate GameData { get; }
RowRef<Lumina.Excel.Sheets.Fate> GameData { get; }
/// <summary>
/// Gets the time this <see cref="Fate"/> started.
@ -70,8 +71,14 @@ public interface IFate : IEquatable<IFate>
/// <summary>
/// Gets a value indicating whether or not this <see cref="Fate"/> has a EXP bonus.
/// </summary>
[Obsolete($"Use {nameof(HasBonus)} instead")]
bool HasExpBonus { get; }
/// <summary>
/// Gets a value indicating whether or not this <see cref="Fate"/> has a bonus.
/// </summary>
bool HasBonus { get; }
/// <summary>
/// Gets the icon id of this <see cref="Fate"/>.
/// </summary>
@ -105,7 +112,7 @@ public interface IFate : IEquatable<IFate>
/// <summary>
/// Gets the territory this <see cref="Fate"/> is located in.
/// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.TerritoryType> TerritoryType { get; }
RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType { get; }
/// <summary>
/// Gets the address of this Fate in memory.
@ -185,7 +192,7 @@ internal unsafe partial class Fate : IFate
public ushort FateId => this.Struct->FateId;
/// <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/>
public int StartTimeEpoch => this.Struct->StartTimeEpoch;
@ -215,8 +222,12 @@ internal unsafe partial class Fate : IFate
public byte Progress => this.Struct->Progress;
/// <inheritdoc/>
[Obsolete($"Use {nameof(HasBonus)} instead")]
public bool HasExpBonus => this.Struct->IsExpBonus;
/// <inheritdoc/>
public bool HasBonus => this.Struct->IsBonus;
/// <inheritdoc/>
public uint IconId => this.Struct->IconId;
@ -238,5 +249,5 @@ internal unsafe partial class Fate : IFate
/// <summary>
/// Gets the territory this <see cref="Fate"/> is located in.
/// </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>
/// The Fate is active.
/// </summary>
Running = 0x02,
Running = 0x04,
/// <summary>
/// The Fate has ended.
/// </summary>
Ended = 0x04,
Ended = 0x07,
/// <summary>
/// The player failed the Fate.
/// </summary>
Failed = 0x05,
Failed = 0x08,
/// <summary>
/// The Fate is preparing to run.
/// </summary>
Preparation = 0x07,
Preparation = 0x03,
/// <summary>
/// The Fate is preparing to end.
/// </summary>
WaitingForEnd = 0x08,
WaitingForEnd = 0x05,
}

View file

@ -4,9 +4,8 @@ using System.Collections.Generic;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Serilog;
using CSFateManager = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager;
namespace Dalamud.Game.ClientState.Fates;
@ -20,55 +19,34 @@ namespace Dalamud.Game.ClientState.Fates;
#pragma warning restore SA1015
internal sealed partial class FateTable : IServiceType, IFateTable
{
private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor]
private FateTable(ClientState clientState)
private FateTable()
{
this.address = clientState.AddressResolver;
Log.Verbose($"Fate table address {Util.DescribeAddress(this.address.FateTablePtr)}");
}
/// <inheritdoc/>
public IntPtr Address => this.address.FateTablePtr;
public unsafe IntPtr Address => (nint)CSFateManager.Instance();
/// <inheritdoc/>
public unsafe int Length
{
get
{
var fateTable = this.FateTableAddress;
if (fateTable == IntPtr.Zero)
var fateManager = CSFateManager.Instance();
if (fateManager == null)
return 0;
// Sonar used this to check if the table was safe to read
if (Struct->FateDirector == null)
if (fateManager->FateDirector == null)
return 0;
if (Struct->Fates.First == null || Struct->Fates.Last == null)
if (fateManager->Fates.First == null || fateManager->Fates.Last == null)
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/>
public IFate? this[int index]
{
@ -99,11 +77,11 @@ internal sealed partial class FateTable : IServiceType, IFateTable
if (index >= this.Length)
return IntPtr.Zero;
var fateTable = this.FateTableAddress;
if (fateTable == IntPtr.Zero)
var fateManager = CSFateManager.Instance();
if (fateManager == null)
return IntPtr.Zero;
return (IntPtr)this.Struct->Fates[index].Value;
return (IntPtr)fateManager->Fates[index].Value;
}
/// <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.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.Input;
using ImGuiNET;
using Serilog;
@ -23,7 +24,7 @@ namespace Dalamud.Game.ClientState.GamePad;
#pragma warning restore SA1015
internal unsafe class GamepadState : IInternalDisposableService, IGamepadState
{
private readonly Hook<ControllerPoll>? gamepadPoll;
private readonly Hook<PadDevice.Delegates.Poll>? gamepadPoll;
private bool isDisposed;
@ -35,25 +36,21 @@ internal unsafe class GamepadState : IInternalDisposableService, IGamepadState
[ServiceManager.ServiceConstructor]
private GamepadState(ClientState clientState)
{
var resolver = clientState.AddressResolver;
Log.Verbose($"GamepadPoll address {Util.DescribeAddress(resolver.GamepadPoll)}");
this.gamepadPoll = Hook<ControllerPoll>.FromAddress(resolver.GamepadPoll, this.GamepadPollDetour);
this.gamepadPoll = Hook<PadDevice.Delegates.Poll>.FromAddress((nint)PadDevice.StaticVirtualTablePointer->Poll, this.GamepadPollDetour);
this.gamepadPoll?.Enable();
}
private delegate int ControllerPoll(IntPtr controllerInput);
/// <summary>
/// Gets the pointer to the current instance of the GamepadInput struct.
/// </summary>
public IntPtr GamepadInputAddress { get; private set; }
/// <inheritdoc/>
public Vector2 LeftStick =>
public Vector2 LeftStick =>
new(this.leftStickX, this.leftStickY);
/// <inheritdoc/>
public Vector2 RightStick =>
public Vector2 RightStick =>
new(this.rightStickX, this.rightStickY);
/// <summary>
@ -61,28 +58,28 @@ internal unsafe class GamepadState : IInternalDisposableService, IGamepadState
///
/// Exposed internally for Debug Data window.
/// </summary>
internal ushort ButtonsPressed { get; private set; }
internal GamepadButtons ButtonsPressed { get; private set; }
/// <summary>
/// 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.
/// </summary>
internal ushort ButtonsRaw { get; private set; }
internal GamepadButtons ButtonsRaw { get; private set; }
/// <summary>
/// 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.
/// </summary>
internal ushort ButtonsReleased { get; private set; }
internal GamepadButtons ButtonsReleased { get; private set; }
/// <summary>
/// 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.
/// </summary>
internal ushort ButtonsRepeat { get; private set; }
internal GamepadButtons ButtonsRepeat { get; private set; }
/// <summary>
/// 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; }
/// <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/>
public float Repeat(GamepadButtons button) => (this.ButtonsRepeat & (ushort)button) > 0 ? 1 : 0;
public float Repeat(GamepadButtons button) => (this.ButtonsRepeat & button) > 0 ? 1 : 0;
/// <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/>
public float Raw(GamepadButtons button) => (this.ButtonsRaw & (ushort)button) > 0 ? 1 : 0;
public float Raw(GamepadButtons button) => (this.ButtonsRaw & button) > 0 ? 1 : 0;
/// <summary>
/// Disposes this instance, alongside its hooks.
@ -115,28 +112,28 @@ internal unsafe class GamepadState : IInternalDisposableService, IGamepadState
GC.SuppressFinalize(this);
}
private int GamepadPollDetour(IntPtr gamepadInput)
private nint GamepadPollDetour(PadDevice* gamepadInput)
{
var original = this.gamepadPoll!.Original(gamepadInput);
try
{
this.GamepadInputAddress = gamepadInput;
var input = (GamepadInput*)gamepadInput;
this.leftStickX = input->LeftStickX;
this.leftStickY = input->LeftStickY;
this.rightStickX = input->RightStickX;
this.rightStickY = input->RightStickY;
this.ButtonsRaw = input->ButtonsRaw;
this.ButtonsPressed = input->ButtonsPressed;
this.ButtonsReleased = input->ButtonsReleased;
this.ButtonsRepeat = input->ButtonsRepeat;
this.GamepadInputAddress = (nint)gamepadInput;
this.leftStickX = gamepadInput->GamepadInputData.LeftStickX;
this.leftStickY = gamepadInput->GamepadInputData.LeftStickY;
this.rightStickX = gamepadInput->GamepadInputData.RightStickX;
this.rightStickY = gamepadInput->GamepadInputData.RightStickY;
this.ButtonsRaw = (GamepadButtons)gamepadInput->GamepadInputData.Buttons;
this.ButtonsPressed = (GamepadButtons)gamepadInput->GamepadInputData.ButtonsPressed;
this.ButtonsReleased = (GamepadButtons)gamepadInput->GamepadInputData.ButtonsReleased;
this.ButtonsRepeat = (GamepadButtons)gamepadInput->GamepadInputData.ButtonsRepeat;
if (this.NavEnableGamepad)
{
input->LeftStickX = 0;
input->LeftStickY = 0;
input->RightStickX = 0;
input->RightStickY = 0;
gamepadInput->GamepadInputData.LeftStickX = 0;
gamepadInput->GamepadInputData.LeftStickY = 0;
gamepadInput->GamepadInputData.RightStickX = 0;
gamepadInput->GamepadInputData.RightStickY = 0;
// NOTE (Chiv) Zeroing `ButtonsRaw` destroys `ButtonPressed`, `ButtonReleased`
// 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.
// This is debatable.
// 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
& ~GamepadButtons.R2
& ~GamepadButtons.DpadDown
& ~GamepadButtons.DpadLeft
& ~GamepadButtons.DpadUp
& ~GamepadButtons.DpadRight);
input->ButtonsRaw &= deletionMask;
input->ButtonsPressed = 0;
input->ButtonsReleased = 0;
input->ButtonsRepeat = 0;
const GamepadButtonsFlags deletionMask = ~GamepadButtonsFlags.L2
& ~GamepadButtonsFlags.R2
& ~GamepadButtonsFlags.DPadDown
& ~GamepadButtonsFlags.DPadLeft
& ~GamepadButtonsFlags.DPadUp
& ~GamepadButtonsFlags.DPadRight;
gamepadInput->GamepadInputData.Buttons &= deletionMask;
gamepadInput->GamepadInputData.ButtonsPressed = 0;
gamepadInput->GamepadInputData.ButtonsReleased = 0;
gamepadInput->GamepadInputData.ButtonsRepeat = 0;
return 0;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,20 +8,20 @@ public enum Song : byte
/// <summary>
/// No song is active type.
/// </summary>
NONE = 0,
None = 0,
/// <summary>
/// Mage's Ballad type.
/// </summary>
MAGE = 1,
Mage = 1,
/// <summary>
/// Army's Paeon type.
/// </summary>
ARMY = 2,
Army = 2,
/// <summary>
/// The Wanderer's Minuet type.
/// </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>
/// No pet.
/// </summary>
NONE = 0,
None = 0,
/// <summary>
/// The summoned pet Carbuncle.
/// </summary>
CARBUNCLE = 23,
Carbuncle = 23,
}

View file

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

View file

@ -19,11 +19,6 @@ public unsafe class BLMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game
/// </summary>
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>
/// Gets the number of Polyglot stacks remaining.
/// </summary>

View file

@ -40,15 +40,15 @@ public unsafe class BRDGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game
get
{
if (this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuet))
return Song.WANDERER;
return Song.Wanderer;
if (this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeon))
return Song.ARMY;
return Song.Army;
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
{
if (this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuetLastPlayed))
return Song.WANDERER;
return Song.Wanderer;
if (this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeonLastPlayed))
return Song.ARMY;
return Song.Army;
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.
/// </summary>
/// <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>
public Song[] Coda
{
@ -84,9 +84,9 @@ public unsafe class BRDGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game
{
return new[]
{
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.WanderersMinuetCoda) ? Song.WANDERER : 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.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;
/// <summary>
/// In-memory DRK job gauge.
/// </summary>
public unsafe class DRKGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.DarkKnightGauge>
public unsafe class DRKGauge : JobGaugeBase<DarkKnightGauge>
{
/// <summary>
/// Initializes a new instance of the <see cref="DRKGauge"/> class.
@ -34,4 +37,16 @@ public unsafe class DRKGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
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.
/// </summary>
/// <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>
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.
/// </summary>
/// <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>
/// Gets a value indicating whether the Getsu Sen is active.
/// </summary>
/// <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>
/// Gets a value indicating whether the Ka Sen is active.
/// </summary>
/// <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>
/// Gets the time remaining for the current attunement.
/// </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>
/// 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>
public SummonPet ReturnSummon => (SummonPet)this.Struct->ReturnSummon;
/// <summary>
/// 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>
public PetGlam ReturnSummonGlam => (PetGlam)this.Struct->ReturnSummonGlam;
/// <summary>
/// Gets the amount of aspected Attunment remaining.
/// Gets the amount of aspected Attunement remaining.
/// </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;
/// <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>
/// Gets the current aether flags.
/// 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.
/// </summary>
/// <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>
/// Gets a value indicating whether if Titan is currently attuned.
/// </summary>
/// <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>
/// Gets a value indicating whether if Garuda is currently attuned.
/// </summary>
/// <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>
/// Gets a value indicating whether there are any Aetherflow stacks available.

View file

@ -27,7 +27,12 @@ public enum BattleNpcSubKind : byte
Chocobo = 3,
/// <summary>
/// BattleNpc representing a standard enemy.
/// BattleNpc representing a standard enemy. This includes allies (overworld guards and allies in single-player duties).
/// </summary>
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.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.Interop;
using Microsoft.Extensions.ObjectPool;
using Serilog;
using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using CSGameObjectManager = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager;
namespace Dalamud.Game.ClientState.Objects;
@ -29,62 +29,58 @@ namespace Dalamud.Game.ClientState.Objects;
#pragma warning restore SA1015
internal sealed partial class ObjectTable : IServiceType, IObjectTable
{
private const int ObjectTableLength = 599;
private static int objectTableLength;
private readonly ClientState clientState;
private readonly CachedEntry[] cachedObjectTable = new CachedEntry[ObjectTableLength];
private readonly ObjectPool<Enumerator> multiThreadedEnumerators =
new DefaultObjectPoolProvider().Create<Enumerator>();
private readonly CachedEntry[] cachedObjectTable;
private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4];
private long nextMultithreadedUsageWarnTime;
[ServiceManager.ServiceConstructor]
private unsafe ObjectTable(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++)
this.cachedObjectTable[i] = new(nativeObjectTableAddress, i);
this.cachedObjectTable[i] = new(nativeObjectTable.GetPointer(i));
for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++)
this.frameworkThreadEnumerators[i] = new(this, i);
Log.Verbose($"Object table address {Util.DescribeAddress(this.clientState.AddressResolver.ObjectTable)}");
}
/// <inheritdoc/>
public nint Address
public unsafe nint Address
{
get
{
_ = this.WarnMultithreadedUsage();
ThreadSafety.AssertMainThread();
return this.clientState.AddressResolver.ObjectTable;
return (nint)(&CSGameObjectManager.Instance()->Objects);
}
}
/// <inheritdoc/>
public int Length => ObjectTableLength;
public int Length => objectTableLength;
/// <inheritdoc/>
public IGameObject? this[int index]
{
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/>
public IGameObject? SearchById(ulong gameObjectId)
{
_ = this.WarnMultithreadedUsage();
ThreadSafety.AssertMainThread();
if (gameObjectId is 0)
return null;
@ -101,7 +97,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
/// <inheritdoc/>
public IGameObject? SearchByEntityId(uint entityId)
{
_ = this.WarnMultithreadedUsage();
ThreadSafety.AssertMainThread();
if (entityId is 0 or 0xE0000000)
return null;
@ -118,15 +114,15 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
/// <inheritdoc/>
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/>
public unsafe IGameObject? CreateObjectReference(nint address)
{
_ = this.WarnMultithreadedUsage();
ThreadSafety.AssertMainThread();
if (this.clientState.LocalContentId == 0)
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>
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;
private readonly BattleNpc battleNpc;
private readonly Npc npc;
private readonly EventObj eventObj;
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);
}
private readonly PlayerCharacter playerCharacter = new(nint.Zero);
private readonly BattleNpc battleNpc = new(nint.Zero);
private readonly Npc npc = new(nint.Zero);
private readonly EventObj eventObj = new(nint.Zero);
private readonly GameObject gameObject = new(nint.Zero);
/// <summary>Gets the address of the underlying native object. May be null.</summary>
public CSGameObject* Address
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => *this.gameObjectPtrPtr;
get => gameObjectPtr->Value;
}
/// <summary>Updates and gets the wrapped game object pointed by this struct.</summary>
@ -235,14 +198,7 @@ internal sealed partial class ObjectTable
/// <inheritdoc/>
public IEnumerator<IGameObject> GetEnumerator()
{
// If something's trying to enumerate outside the framework thread, we use the ObjectPool.
if (this.WarnMultithreadedUsage())
{
// let's not
var e = this.multiThreadedEnumerators.Get();
e.InitializeForPooledObjects(this);
return e;
}
ThreadSafety.AssertMainThread();
// 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())
@ -263,32 +219,23 @@ internal sealed partial class ObjectTable
/// <inheritdoc/>
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;
private ObjectTable? owner = owner;
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!;
object IEnumerator.Current => this.Current;
public bool MoveNext()
{
if (this.index == ObjectTableLength)
if (this.index == objectTableLength)
return false;
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)
{
@ -300,8 +247,6 @@ internal sealed partial class ObjectTable
return false;
}
public void InitializeForPooledObjects(ObjectTable ot) => this.owner = ot;
public void Reset() => this.index = -1;
public void Dispose()
@ -309,10 +254,8 @@ internal sealed partial class ObjectTable
if (this.owner is not { } o)
return;
if (this.slotId == -1)
o.multiThreadedEnumerators.Return(this);
else
o.frameworkThreadEnumerators[this.slotId] = this;
if (slotId != -1)
o.frameworkThreadEnumerators[slotId] = this;
}
public bool TryReset()

View file

@ -1,12 +1,8 @@
using System.Numerics;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Data;
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;
@ -16,14 +12,14 @@ namespace Dalamud.Game.ClientState.Objects.SubKinds;
public interface IPlayerCharacter : IBattleChara
{
/// <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>
ExcelResolver<World> CurrentWorld { get; }
RowRef<World> CurrentWorld { get; }
/// <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>
ExcelResolver<World> HomeWorld { get; }
RowRef<World> HomeWorld { get; }
}
/// <summary>
@ -42,10 +38,10 @@ internal unsafe class PlayerCharacter : BattleChara, IPlayerCharacter
}
/// <inheritdoc/>
public ExcelResolver<World> CurrentWorld => new(this.Struct->CurrentWorld);
public RowRef<World> CurrentWorld => LuminaUtils.CreateRef<World>(this.Struct->CurrentWorld);
/// <inheritdoc/>
public ExcelResolver<World> HomeWorld => new(this.Struct->HomeWorld);
public RowRef<World> HomeWorld => LuminaUtils.CreateRef<World>(this.Struct->HomeWorld);
/// <summary>
/// Gets the target actor ID of the PlayerCharacter.

View file

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

View file

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

View file

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

View file

@ -1,13 +1,15 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.ClientState.Statuses;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory;
using Lumina.Excel;
namespace Dalamud.Game.ClientState.Party;
/// <summary>
@ -71,12 +73,12 @@ public interface IPartyMember
/// <summary>
/// Gets the territory this party member is located in.
/// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.TerritoryType> Territory { get; }
RowRef<Lumina.Excel.Sheets.TerritoryType> Territory { get; }
/// <summary>
/// Gets the World this party member resides in.
/// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.World> World { get; }
RowRef<Lumina.Excel.Sheets.World> World { get; }
/// <summary>
/// Gets the displayname of this party member.
@ -91,7 +93,7 @@ public interface IPartyMember
/// <summary>
/// Gets the classjob of this party member.
/// </summary>
ExcelResolver<Lumina.Excel.GeneratedSheets.ClassJob> ClassJob { get; }
RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob { get; }
/// <summary>
/// Gets the level of this party member.
@ -169,17 +171,17 @@ internal unsafe class PartyMember : IPartyMember
/// <summary>
/// Gets the territory this party member is located in.
/// </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>
/// Gets the World this party member resides in.
/// </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>
/// Gets the displayname of this party member.
/// </summary>
public SeString Name => MemoryHelper.ReadSeString((nint)Unsafe.AsPointer(ref Struct->Name[0]), 0x40);
public SeString Name => SeString.Parse(this.Struct->Name);
/// <summary>
/// Gets the sex of this party member.
@ -189,7 +191,7 @@ internal unsafe class PartyMember : IPartyMember
/// <summary>
/// Gets the classjob of this party member.
/// </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>
/// 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.Types;
using Dalamud.Game.ClientState.Resolvers;
using Lumina.Excel;
namespace Dalamud.Game.ClientState.Statuses;
@ -31,7 +33,7 @@ public unsafe class Status
/// <summary>
/// Gets the GameData associated with this status.
/// </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>
/// Gets the parameter value of the status.
@ -40,8 +42,10 @@ public unsafe class Status
/// <summary>
/// Gets the stack count of this status.
/// Only valid if this is a non-food status.
/// </summary>
public byte StackCount => this.Struct->StackCount;
[Obsolete($"Replaced with {nameof(Param)}", true)]
public byte StackCount => (byte)this.Struct->Param;
/// <summary>
/// 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="arguments">The arguments supplied to the command, ready for parsing.</param>
public delegate void HandlerDelegate(string command, string arguments);
/// <summary>
/// Gets a <see cref="HandlerDelegate"/> which will be called when the command is dispatched.
/// </summary>
@ -26,6 +26,11 @@ public interface IReadOnlyCommandInfo
/// Gets a value indicating whether if this command should be shown in the help output.
/// </summary>
bool ShowInHelp { get; }
/// <summary>
/// Gets the display order of this command. Defaults to alphabetical ordering.
/// </summary>
int DisplayOrder { get; }
}
/// <summary>
@ -51,4 +56,7 @@ public sealed class CommandInfo : IReadOnlyCommandInfo
/// <inheritdoc/>
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.ObjectModel;
using System.Linq;
using System.Text.RegularExpressions;
using Dalamud.Console;
using Dalamud.Game.Gui;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
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;
@ -20,38 +22,26 @@ namespace Dalamud.Game.Command;
/// This class manages registered in-game slash commands.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed class CommandManager : IInternalDisposableService, ICommandManager
internal sealed unsafe class CommandManager : IInternalDisposableService, ICommandManager
{
private static readonly ModuleLog Log = new("Command");
private readonly ConcurrentDictionary<string, IReadOnlyCommandInfo> commandMap = 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 ChatGui chatGui = Service<ChatGui>.Get();
private readonly Hook<ShellCommands.Delegates.TryInvokeDebugCommand>? tryInvokeDebugCommandHook;
[ServiceManager.ServiceDependency]
private readonly ConsoleManager console = Service<ConsoleManager>.Get();
[ServiceManager.ServiceConstructor]
private CommandManager(Dalamud dalamud)
{
this.currentLangCommandRegex = (ClientLanguage)dalamud.StartInfo.Language switch
{
ClientLanguage.Japanese => this.commandRegexJp,
ClientLanguage.English => this.commandRegexEn,
ClientLanguage.German => this.commandRegexDe,
ClientLanguage.French => this.commandRegexFr,
_ => this.commandRegexEn,
};
this.tryInvokeDebugCommandHook = Hook<ShellCommands.Delegates.TryInvokeDebugCommand>.FromAddress(
(nint)ShellCommands.MemberFunctionPointers.TryInvokeDebugCommand,
this.OnTryInvokeDebugCommand);
this.tryInvokeDebugCommandHook.Enable();
this.chatGui.CheckMessageHandled += this.OnCheckMessageHandled;
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);
}
}
/// <summary>
/// Add a command handler, which you can use to add your own custom commands to the in-game chat.
/// </summary>
@ -131,7 +121,7 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag
Log.Error("Command {CommandName} is already registered", command);
return false;
}
if (!this.commandAssemblyNameMap.TryAdd((command, info), loaderAssemblyName))
{
this.commandMap.Remove(command, out _);
@ -160,6 +150,11 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag
/// <inheritdoc/>
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 _);
}
@ -184,7 +179,8 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag
/// </summary>
/// <param name="assemblyName">The name of the assembly.</param>
/// <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();
}
@ -193,37 +189,20 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag
void IInternalDisposableService.DisposeService()
{
this.console.Invoke -= this.ConsoleOnInvoke;
this.chatGui.CheckMessageHandled -= this.OnCheckMessageHandled;
this.tryInvokeDebugCommandHook?.Dispose();
}
private bool ConsoleOnInvoke(string 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 cmdMatch = this.currentLangCommandRegex.Match(message.TextValue).Groups["command"];
if (cmdMatch.Success)
{
// 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;
}
}
}
var result = this.tryInvokeDebugCommandHook!.OriginalDisposeSafe(self, command, uiModule);
if (result != -1) return result;
return this.ProcessCommand(command->ToString()) ? 0 : result;
}
}
@ -238,7 +217,7 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag
internal class CommandManagerPluginScoped : IInternalDisposableService, ICommandManager
{
private static readonly ModuleLog Log = new("Command");
[ServiceManager.ServiceDependency]
private readonly CommandManager commandManagerService = Service<CommandManager>.Get();
@ -253,10 +232,10 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand
{
this.pluginInfo = localPlugin;
}
/// <inheritdoc/>
public ReadOnlyDictionary<string, IReadOnlyCommandInfo> Commands => this.commandManagerService.Commands;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
@ -264,7 +243,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand
{
this.commandManagerService.RemoveHandler(command);
}
this.pluginRegisteredCommands.Clear();
}
@ -275,7 +254,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand
/// <inheritdoc/>
public void DispatchCommand(string command, string argument, IReadOnlyCommandInfo info)
=> this.commandManagerService.DispatchCommand(command, argument, info);
/// <inheritdoc/>
public bool AddHandler(string command, CommandInfo info)
{
@ -294,7 +273,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand
return false;
}
/// <inheritdoc/>
public bool RemoveHandler(string command)
{

View file

@ -121,7 +121,10 @@ internal sealed class GameConfig : IInternalDisposableService, IGameConfig
/// <inheritdoc/>
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/>
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/>
public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties)
=> this.gameConfigService.TryGet(option, out properties);
/// <inheritdoc/>
public bool TryGet(SystemConfigOption option, out PadButtonValue value)
=> this.gameConfigService.TryGet(option, out value);
/// <inheritdoc/>
public bool TryGet(UiConfigOption option, out bool value)
=> this.gameConfigService.TryGet(option, out value);

View file

@ -13,6 +13,6 @@ internal sealed class GameConfigAddressResolver : BaseAddressResolver
/// <inheritdoc/>
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.Diagnostics;
using System.Text;
using Dalamud.Memory;
using Dalamud.Utility;
@ -51,7 +52,7 @@ public class GameConfigSection
/// <summary>
/// Event which is fired when a game config option is changed within the section.
/// </summary>
internal event EventHandler<ConfigChangeEvent>? Changed;
internal event EventHandler<ConfigChangeEvent>? Changed;
/// <summary>
/// Gets the number of config entries contained within the section.
@ -357,6 +358,40 @@ public class GameConfigSection
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>
/// Set a string config option.
/// 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 (entry->Name == null) return null;
var name = MemoryHelper.ReadStringNullTerminated(new IntPtr(entry->Name));
if (entry->Name.Value == null) return null;
var name = entry->Name.ToString();
if (Enum.TryParse(typeof(TEnum), name, out enumObject))
{
this.enumMap.TryAdd(entry->Index, enumObject);
@ -509,7 +544,7 @@ public class GameConfigSection
this.Changed?.InvokeSafely(this, eventArgs);
return eventArgs;
}
private unsafe bool TryGetIndex(string name, out uint index)
{
if (this.indexMap.TryGetValue(name, out index))
@ -521,12 +556,12 @@ public class GameConfigSection
var e = configBase->ConfigEntry;
for (var i = 0U; i < configBase->ConfigCount; i++, e++)
{
if (e->Name == null)
if (e->Name.Value == null)
{
continue;
}
var eName = MemoryHelper.ReadStringNullTerminated(new IntPtr(e->Name));
var eName = e->Name.ToString();
if (eName.Equals(name))
{
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)]
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>
/// System option with the internal name WaterWet.
/// This option is a UInt.
@ -996,6 +1010,27 @@ public enum SystemConfigOption
[GameConfigOption("AutoChangeCameraMode", ConfigType.UInt)]
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>
/// System option with the internal name AccessibilitySoundVisualEnable.
/// This option is a UInt.
@ -1059,6 +1094,13 @@ public enum SystemConfigOption
[GameConfigOption("IdlingCameraAFK", ConfigType.UInt)]
IdlingCameraAFK,
/// <summary>
/// System option with the internal name FirstConfigBackup.
/// This option is a UInt.
/// </summary>
[GameConfigOption("FirstConfigBackup", ConfigType.UInt)]
FirstConfigBackup,
/// <summary>
/// System option with the internal name MouseSpeed.
/// 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.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
namespace Dalamud.Game.DutyState;
@ -81,33 +82,33 @@ internal unsafe class DutyState : IInternalDisposableService, IDutyState
// Duty Commenced
case 0x4000_0001:
this.IsDutyStarted = true;
this.DutyStarted?.Invoke(this, this.clientState.TerritoryType);
this.DutyStarted?.InvokeSafely(this, this.clientState.TerritoryType);
break;
// Party Wipe
case 0x4000_0005:
this.IsDutyStarted = false;
this.DutyWiped?.Invoke(this, this.clientState.TerritoryType);
this.DutyWiped?.InvokeSafely(this, this.clientState.TerritoryType);
break;
// Duty Recommence
case 0x4000_0006:
this.IsDutyStarted = true;
this.DutyRecommenced?.Invoke(this, this.clientState.TerritoryType);
this.DutyRecommenced?.InvokeSafely(this, this.clientState.TerritoryType);
break;
// Duty Completed Flytext Shown
case 0x4000_0002 when !this.CompletedThisTerritory:
this.IsDutyStarted = false;
this.CompletedThisTerritory = true;
this.DutyCompleted?.Invoke(this, this.clientState.TerritoryType);
this.DutyCompleted?.InvokeSafely(this, this.clientState.TerritoryType);
break;
// Duty Completed
case 0x4000_0003 when !this.CompletedThisTerritory:
this.IsDutyStarted = false;
this.CompletedThisTerritory = true;
this.DutyCompleted?.Invoke(this, this.clientState.TerritoryType);
this.DutyCompleted?.InvokeSafely(this, this.clientState.TerritoryType);
break;
}
}

View file

@ -16,6 +16,6 @@ internal class DutyStateAddressResolver : BaseAddressResolver
/// <param name="sig">The signature scanner to facilitate setup.</param>
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.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@ -16,6 +15,8 @@ using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using CSFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
namespace Dalamud.Game;
/// <summary>
@ -31,11 +32,9 @@ internal sealed class Framework : IInternalDisposableService, IFramework
private readonly Stopwatch updateStopwatch = new();
private readonly HitchDetector hitchDetector;
private readonly Hook<OnUpdateDetour> updateHook;
private readonly Hook<OnRealDestroyDelegate> destroyHook;
private readonly Hook<CSFramework.Delegates.Tick> updateHook;
private readonly Hook<CSFramework.Delegates.Destroy> destroyHook;
private readonly FrameworkAddressResolver addressResolver;
[ServiceManager.ServiceDependency]
private readonly GameLifecycle lifecycle = Service<GameLifecycle>.Get();
@ -51,13 +50,10 @@ internal sealed class Framework : IInternalDisposableService, IFramework
private ulong tickCounter;
[ServiceManager.ServiceConstructor]
private Framework(TargetSigScanner sigScanner)
private unsafe Framework()
{
this.hitchDetector = new HitchDetector("FrameworkUpdate", this.configuration.FrameworkUpdateHitch);
this.addressResolver = new FrameworkAddressResolver();
this.addressResolver.Setup(sigScanner);
this.frameworkDestroy = new();
this.frameworkThreadTaskScheduler = new();
this.FrameworkThreadTaskFactory = new(
@ -66,23 +62,13 @@ internal sealed class Framework : IInternalDisposableService, IFramework
TaskContinuationOptions.None,
this.frameworkThreadTaskScheduler);
this.updateHook = Hook<OnUpdateDetour>.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate);
this.destroyHook = Hook<OnRealDestroyDelegate>.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy);
this.updateHook = Hook<CSFramework.Delegates.Tick>.FromAddress((nint)CSFramework.StaticVirtualTablePointer->Tick, this.HandleFrameworkUpdate);
this.destroyHook = Hook<CSFramework.Delegates.Destroy>.FromAddress((nint)CSFramework.StaticVirtualTablePointer->Destroy, this.HandleFrameworkDestroy);
this.updateHook.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/>
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;
@ -483,10 +469,10 @@ internal sealed class Framework : IInternalDisposableService, IFramework
this.hitchDetector.Stop();
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.DispatchUpdateEvents = false;
@ -504,7 +490,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework
ServiceManager.WaitForServiceUnload();
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.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
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;
@ -27,14 +39,12 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
{
private static readonly ModuleLog Log = new("ChatGui");
private readonly ChatGuiAddressResolver address;
private readonly Queue<XivChatEntry> chatQueue = new();
private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = new();
private readonly Hook<PrintMessageDelegate> printMessageHook;
private readonly Hook<PopulateItemLinkDelegate> populateItemLinkHook;
private readonly Hook<InteractableLinkClickedDelegate> interactableLinkClickedHook;
private readonly Hook<InventoryItem.Delegates.Copy> inventoryItemCopyHook;
private readonly Hook<LogViewer.Delegates.HandleLinkClick> handleLinkClickHook;
[ServiceManager.ServiceDependency]
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;
[ServiceManager.ServiceConstructor]
private ChatGui(TargetSigScanner sigScanner)
private ChatGui()
{
this.address = new ChatGuiAddressResolver();
this.address.Setup(sigScanner);
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 = Hook<PrintMessageDelegate>.FromAddress(RaptureLogModule.Addresses.PrintMessage.Value, this.HandlePrintMessageDetour);
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.Enable();
this.populateItemLinkHook.Enable();
this.interactableLinkClickedHook.Enable();
this.inventoryItemCopyHook.Enable();
this.handleLinkClickHook.Enable();
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
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/>
public event IChatGui.OnMessageDelegate? ChatMessage;
@ -78,7 +79,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
/// <inheritdoc/>
public int LastLinkedItemId { get; private set; }
public uint LastLinkedItemId { get; private set; }
/// <inheritdoc/>
public byte LastLinkedItemFlags { get; private set; }
@ -106,10 +107,12 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
void IInternalDisposableService.DisposeService()
{
this.printMessageHook.Dispose();
this.populateItemLinkHook.Dispose();
this.interactableLinkClickedHook.Dispose();
this.inventoryItemCopyHook.Dispose();
this.handleLinkClickHook.Dispose();
}
#region DalamudSeString
/// <inheritdoc/>
public void Print(XivChatEntry chat)
{
@ -140,43 +143,74 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
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>
/// Process a chat queue.
/// </summary>
public void UpdateQueue()
{
while (this.chatQueue.Count > 0)
{
var chat = this.chatQueue.Dequeue();
var replacedMessage = new SeStringBuilder();
if (this.chatQueue.Count == 0)
return;
// Normalize Unicode NBSP to the built-in one, as the former won't renderl
foreach (var payload in chat.Message.Payloads)
var sb = LSeStringBuilder.SharedPool.Get();
Span<byte> namebuf = stackalloc byte[256];
using var sender = new Utf8String();
using var message = new Utf8String();
while (this.chatQueue.TryDequeue(out var chat))
{
sb.Clear();
foreach (var c in UtfEnumerator.From(chat.MessageBytes, UtfEnumeratorFlags.Utf8SeString))
{
if (payload is TextPayload { Text: not null } textPayload)
{
var split = textPayload.Text.Split("\u202f"); // NARROW NO-BREAK SPACE
for (var i = 0; i < split.Length; i++)
{
replacedMessage.AddText(split[i]);
if (i + 1 < split.Length)
replacedMessage.Add(new RawPayload([0x02, (byte)Lumina.Text.Payloads.PayloadType.Indent, 0x01, 0x03]));
}
}
if (c.IsSeStringPayload)
sb.Append((ReadOnlySeStringSpan)chat.MessageBytes.AsSpan(c.ByteOffset, c.ByteLength));
else if (c.Value.IntValue == 0x202F)
sb.BeginMacro(MacroCode.NonBreakingSpace).EndMacro();
else
{
replacedMessage.Add(payload);
}
sb.Append(c);
}
var sender = Utf8String.FromSequence(chat.Name.Encode());
var message = Utf8String.FromSequence(replacedMessage.BuiltString.Encode());
if (chat.NameBytes.Length + 1 < namebuf.Length)
{
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);
message->Dtor(true);
var targetChannel = chat.Type ?? this.configuration.GeneralChatType;
this.HandlePrintMessageDetour(
RaptureLogModule.Instance(),
targetChannel,
&sender,
&message,
chat.Timestamp,
(byte)(chat.Silent ? 1 : 0));
}
LSeStringBuilder.SharedPool.Return(sb);
}
/// <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)
{
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
{
this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr);
this.LastLinkedItemId = otherPtr->ItemId;
this.LastLinkedItemFlags = (byte)otherPtr->Flags;
this.LastLinkedItemId = Marshal.ReadInt32(itemInfoPtr, 8);
this.LastLinkedItemFlags = Marshal.ReadByte(itemInfoPtr, 0x14);
// Log.Verbose($"HandlePopulateItemLinkDetour {linkObjectPtr} {itemInfoPtr} - linked:{this.LastLinkedItemId}");
// Log.Verbose($"InventoryItemCopyDetour {thisPtr} {otherPtr} - linked:{this.LastLinkedItemId}");
}
catch (Exception ex)
{
Log.Error(ex, "Exception onPopulateItemLink hook.");
this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr);
Log.Error(ex, "Exception in InventoryItemCopyHook");
}
}
@ -299,58 +336,57 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
try
{
var originalSenderData = sender->AsSpan().ToArray();
var originalMessageData = message->AsSpan().ToArray();
var parsedSender = SeString.Parse(sender->AsSpan());
var parsedMessage = SeString.Parse(message->AsSpan());
var parsedSender = SeString.Parse(originalSenderData);
var parsedMessage = SeString.Parse(originalMessageData);
var terminatedSender = parsedSender.EncodeWithNullTerminator();
var terminatedMessage = parsedMessage.EncodeWithNullTerminator();
// Call events
var isHandled = false;
var invocationList = this.CheckMessageHandled!.GetInvocationList();
foreach (var @delegate in invocationList)
if (this.CheckMessageHandled is { } handledCallback)
{
try
{
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)
foreach (var action in handledCallback.GetInvocationList().Cast<IChatGui.OnCheckMessageHandledDelegate>())
{
try
{
var messageHandledDelegate = @delegate as IChatGui.OnMessageDelegate;
messageHandledDelegate!.Invoke(chatType, timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
action(chatType, timestamp, ref parsedSender, ref parsedMessage, ref isHandled);
}
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();
var possiblyModifiedMessageData = parsedMessage.Encode();
if (!Util.FastByteArrayCompare(originalSenderData, possiblyModifiedSenderData))
if (!isHandled && this.ChatMessage is { } callback)
{
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);
}
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);
}
@ -374,42 +410,57 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
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
{
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);
return;
sb.Append(payload);
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);
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))
{
if (this.RegisteredLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value))
{
Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}");
value.Invoke(link.CommandId, new SeString(payloads));
}
else
{
Log.Debug($"No DalamudLink registered for {link.Plugin} with ID of {link.CommandId}");
}
Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}");
value.Invoke(link.CommandId, seStr);
}
else
{
Log.Debug($"No DalamudLink registered for {link.Plugin} with ID of {link.CommandId}");
}
}
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;
/// <inheritdoc/>
public int LastLinkedItemId => this.chatGuiService.LastLinkedItemId;
public uint LastLinkedItemId => this.chatGuiService.LastLinkedItemId;
/// <inheritdoc/>
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)
=> 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)
=> 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 bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3);
private delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2);
/// <inheritdoc/>
@ -92,16 +92,22 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.atkModuleVf22OpenAddonByAgentHook.Dispose();
this.addonContextMenuOnMenuSelectedHook.Dispose();
var manager = RaptureAtkUnitManager.Instance();
if (manager == null)
return;
var menu = manager->GetAddonByName("ContextMenu");
var submenu = manager->GetAddonByName("AddonContextSub");
if (menu == null || submenu == null)
return;
if (menu->IsVisible)
menu->FireCallbackInt(-1);
if (submenu->IsVisible)
submenu->FireCallbackInt(-1);
this.atkModuleVf22OpenAddonByAgentHook.Dispose();
this.addonContextMenuOnMenuSelectedHook.Dispose();
}
/// <inheritdoc/>

View file

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

View file

@ -1,11 +1,12 @@
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.Network.Structures.InfoProxy;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace Dalamud.Game.Gui.ContextMenu;
@ -46,7 +47,7 @@ public sealed unsafe class MenuTargetDefault : MenuTarget
/// <summary>
/// Gets the home world id of the target.
/// </summary>
public ExcelResolver<World> TargetHomeWorld => new((uint)this.Context->TargetHomeWorldId);
public RowRef<World> TargetHomeWorld => LuminaUtils.CreateRef<World>((uint)this.Context->TargetHomeWorldId);
/// <summary>
/// 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.Immutable;
using System.Linq;
using System.Threading;
using Dalamud.Configuration.Internal;
using Dalamud.Game.Addon.Events;
@ -10,6 +11,7 @@ using Dalamud.Game.Text.SeStringHandling;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics;
@ -28,7 +30,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
private const uint BaseNodeId = 1000;
private static readonly ModuleLog Log = new("DtrBar");
[ServiceManager.ServiceDependency]
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 dtrPreFinalizeListener;
private readonly ConcurrentBag<DtrBarEntry> newEntries = new();
private readonly List<DtrBarEntry> entries = new();
private readonly ReaderWriterLockSlim entriesLock = new();
private readonly List<DtrBarEntry> entries = [];
private readonly Dictionary<uint, List<IAddonEventHandle>> eventHandles = new();
private ImmutableList<IReadOnlyDtrBarEntry>? entriesReadOnlyCopy;
private Utf8String* emptyString;
private uint runningNodeIds = BaseNodeId;
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.dtrPostRequestedUpdateListener);
this.addonLifecycle.RegisterListener(this.dtrPreFinalizeListener);
this.framework.Update += this.Update;
this.configuration.DtrOrder ??= new List<string>();
this.configuration.DtrIgnore ??= new List<string>();
this.configuration.DtrOrder ??= [];
this.configuration.DtrIgnore ??= [];
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/>
public IReadOnlyList<IReadOnlyDtrBarEntry> Entries => this.entries;
/// <inheritdoc/>
public IDtrBarEntry Get(string title, SeString? text = null)
public IReadOnlyList<IReadOnlyDtrBarEntry> Entries
{
if (this.entries.Any(x => x.Title == title) || this.newEntries.Any(x => x.Title == title))
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)
get
{
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/>
void IInternalDisposableService.DisposeService()
{
@ -124,10 +230,17 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
this.addonLifecycle.UnregisterListener(this.dtrPostRequestedUpdateListener);
this.addonLifecycle.UnregisterListener(this.dtrPreFinalizeListener);
foreach (var entry in this.entries)
this.RemoveEntry(entry);
this.framework.RunOnFrameworkThread(
() =>
{
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;
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>
/// Remove native resources for the specified entry.
/// </summary>
@ -174,7 +270,17 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
/// </summary>
/// <param name="title">The title to check for.</param>
/// <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>
/// 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>
internal bool MakeDirty(string title)
{
var entry = this.entries.FirstOrDefault(x => x.Title == title);
if (entry == null)
return false;
var found = false;
entry.Dirty = true;
return true;
this.entriesLock.EnterReadLock();
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>
/// Reapply the DTR entry ordering from <see cref="DalamudConfiguration"/>.
/// </summary>
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.
var positions = this.configuration
.DtrOrder!
.Select(entry => (entry, index: this.configuration.DtrOrder!.IndexOf(entry)))
.ToDictionary(x => x.entry, x => x.index);
var positions = dtrOrder
.Select(entry => (entry, index: dtrOrder.IndexOf(entry)))
.ToDictionary(x => x.entry, x => x.index);
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;
return xPos.CompareTo(yPos);
});
this.entriesReadOnlyCopy = null;
}
private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR").ToPointer();
private void Update(IFramework unused)
{
this.HandleRemovedNodes();
this.HandleAddedNodes();
var dtr = this.GetDtr();
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;
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);
data.Dirty = true;
Log.Debug("Removing entry from Framework.Update: {what}", data.Title);
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 node = data.TextNode;
var nodeHidden = !node->AtkResNode.IsVisible();
@ -290,23 +421,10 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
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)
{
var addon = (AtkUnitBase*)addonInfo.Addon;
@ -316,7 +434,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
var additionalWidth = 0;
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];
if (node->IsVisible())
@ -382,22 +500,20 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
private void RecreateNodes()
{
this.runningNodeIds = BaseNodeId;
if (this.entries.Any())
{
this.eventHandles.Clear();
}
this.entriesLock.EnterReadLock();
this.eventHandles.Clear();
foreach (var entry in this.entries)
{
entry.TextNode = this.MakeNode(++this.runningNodeIds);
entry.Added = false;
}
this.entriesLock.ExitReadLock();
}
private bool AddNode(AtkTextNode* node)
private bool AddNode(DtrBarEntry data)
{
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[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.MouseClick, this.DtrEventHandler),
});
var lastChild = dtr->RootNode->ChildNode;
while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode;
Log.Debug($"Found last sibling: {(ulong)lastChild:X}");
@ -420,6 +536,8 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
dtr->UldManager.UpdateDrawNodeList();
dtr->UpdateCollisionNodeList(false);
Log.Debug("Updated node draw list");
data.Dirty = true;
return true;
}
@ -472,7 +590,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
if (this.emptyString == null)
this.emptyString = Utf8String.FromString(" ");
newTextNode->SetText(this.emptyString->StringPtr);
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;
}
private void DtrEventHandler(AddonEventType atkEventType, IntPtr atkUnitBase, IntPtr atkResNode)
{
var addon = (AtkUnitBase*)atkUnitBase;
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 })
{
@ -506,7 +632,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
case AddonEventType.MouseOver:
AtkStage.Instance()->TooltipManager.ShowTooltip(addon->Id, node, dtrBarEntry.Tooltip.Encode());
break;
case AddonEventType.MouseOut:
AtkStage.Instance()->TooltipManager.HideTooltip(addon->Id);
break;
@ -520,11 +646,11 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
case AddonEventType.MouseOver:
this.uiEventManager.SetCursor(AddonCursorType.Clickable);
break;
case AddonEventType.MouseOut:
this.uiEventManager.ResetCursor();
break;
case AddonEventType.MouseClick:
dtrBarEntry.OnClick.Invoke();
break;
@ -541,58 +667,25 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
#pragma warning disable SA1015
[ResolveVia<IDtrBar>]
#pragma warning restore SA1015
internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar
internal sealed class DtrBarPluginScoped : IInternalDisposableService, IDtrBar
{
private readonly LocalPlugin plugin;
[ServiceManager.ServiceDependency]
private readonly DtrBar dtrBarService = Service<DtrBar>.Get();
private readonly Dictionary<string, IDtrBarEntry> pluginEntries = new();
/// <summary>
/// Initializes a new instance of the <see cref="DtrBarPluginScoped"/> class.
/// </summary>
internal DtrBarPluginScoped()
{
this.dtrBarService.DtrEntryRemoved += this.OnDtrEntryRemoved;
}
[ServiceManager.ServiceConstructor]
private DtrBarPluginScoped(LocalPlugin plugin) => this.plugin = plugin;
/// <inheritdoc/>
public IReadOnlyList<IReadOnlyDtrBarEntry> Entries => this.dtrBarService.Entries;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.dtrBarService.DtrEntryRemoved -= this.OnDtrEntryRemoved;
foreach (var entry in this.pluginEntries)
{
entry.Value.Remove();
}
this.pluginEntries.Clear();
}
void IInternalDisposableService.DisposeService() => this.dtrBarService.Remove(this.plugin, null);
/// <inheritdoc/>
public IDtrBarEntry Get(string title, SeString? text = null)
{
// If we already have a known entry for this plugin, return it.
if (this.pluginEntries.TryGetValue(title, out var existingEntry)) return existingEntry;
public IDtrBarEntry Get(string title, SeString? text = null) => this.dtrBarService.Get(this.plugin, title, text);
return this.pluginEntries[title] = this.dtrBarService.Get(title, text);
}
/// <inheritdoc/>
public void Remove(string title)
{
if (this.pluginEntries.TryGetValue(title, out var existingEntry))
{
existingEntry.Remove();
this.pluginEntries.Remove(title);
}
}
private void OnDtrEntryRemoved(string title)
{
this.pluginEntries.Remove(title);
}
public void Remove(string title) => this.dtrBarService.Remove(this.plugin, 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.Plugin.Internal.Types;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.String;
@ -27,12 +26,12 @@ public interface IReadOnlyDtrBarEntry
/// <summary>
/// Gets the text of this entry.
/// </summary>
public SeString Text { get; }
public SeString? Text { get; }
/// <summary>
/// Gets a tooltip to be shown when the user mouses over the dtr entry.
/// </summary>
public SeString Tooltip { get; }
public SeString? Tooltip { get; }
/// <summary>
/// Gets a value indicating whether this entry should be shown.
@ -86,7 +85,7 @@ public interface IDtrBarEntry : IReadOnlyDtrBarEntry
/// <summary>
/// Class representing an entry in the server info bar.
/// </summary>
public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
{
private readonly DalamudConfiguration configuration;
@ -146,7 +145,7 @@ public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
}
/// <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;
/// <summary>
@ -160,9 +159,9 @@ public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
internal Utf8String* Storage { get; set; }
/// <summary>
/// Gets a value indicating whether this entry should be removed.
/// Gets or sets a value indicating whether this entry should be removed.
/// </summary>
internal bool ShouldBeRemoved { get; private set; }
internal bool ShouldBeRemoved { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this entry is dirty.
@ -174,6 +173,11 @@ public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
/// </summary>
internal bool Added { get; set; }
/// <summary>
/// Gets or sets the plugin that owns this entry.
/// </summary>
internal LocalPlugin? OwnerPlugin { get; set; }
/// <inheritdoc/>
public bool TriggerClickAction()
{

View file

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