mirror of
https://github.com/goatcorp/Dalamud.git
synced 2026-01-03 14:23:40 +01:00
Merge master
This commit is contained in:
commit
2951dc93ec
413 changed files with 36477 additions and 6572 deletions
|
|
@ -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
|
||||
|
|
|
|||
36
.github/workflows/main.yml
vendored
36
.github/workflows/main.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
8
.github/workflows/rollup.yml
vendored
8
.github/workflows/rollup.yml
vendored
|
|
@ -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
9
.gitmodules
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
|
|||
78
Dalamud.Common/OSPlatformConverter.cs
Normal file
78
Dalamud.Common/OSPlatformConverter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
18
Dalamud.Common/Util/EnvironmentUtils.cs
Normal file
18
Dalamud.Common/Util/EnvironmentUtils.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 ="$(CommitHash)" > $(CommitHashFile)" IgnoreExitCode="true" />
|
||||
<Exec Command="echo|set /P ="$(SCMVersion)" > $(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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
22
Dalamud/Data/LuminaUtils.cs
Normal file
22
Dalamud/Data/LuminaUtils.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
51
Dalamud/Data/RsvResolver.cs
Normal file
51
Dalamud/Data/RsvResolver.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
89
Dalamud/Game/ActionKind.cs
Normal file
89
Dalamud/Game/ActionKind.cs
Normal 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,
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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++)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
22
Dalamud/Game/ClientState/JobGauge/Enums/DeliriumStep.cs
Normal file
22
Dalamud/Game/ClientState/JobGauge/Enums/DeliriumStep.cs
Normal 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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
30
Dalamud/Game/ClientState/JobGauge/Enums/SummonAttunement.cs
Normal file
30
Dalamud/Game/ClientState/JobGauge/Enums/SummonAttunement.cs
Normal 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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
85
Dalamud/Game/Config/PadButtonValue.cs
Normal file
85
Dalamud/Game/Config/PadButtonValue.cs
Normal 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,
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ??");
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue