Merge branch 'api12'

This commit is contained in:
goaaats 2025-03-26 20:44:48 +01:00
commit f94f03e114
137 changed files with 6870 additions and 944 deletions

View file

@ -23,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)

View file

@ -109,6 +109,7 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory);
config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory);
config.Language = json.value("Language", config.Language);
config.Platform = json.value("Platform", config.Platform);
config.GameVersion = json.value("GameVersion", config.GameVersion);
config.TroubleshootingPackData = json.value("TroubleshootingPackData", std::string{});
config.DelayInitializeMs = json.value("DelayInitializeMs", config.DelayInitializeMs);
@ -123,6 +124,7 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
config.BootVehEnabled = json.value("BootVehEnabled", config.BootVehEnabled);
config.BootVehFull = json.value("BootVehFull", config.BootVehFull);
config.BootEnableEtw = json.value("BootEnableEtw", config.BootEnableEtw);
config.BootDisableLegacyCorruptedStateExceptions = json.value("BootDisableLegacyCorruptedStateExceptions", config.BootDisableLegacyCorruptedStateExceptions);
config.BootDotnetOpenProcessHookMode = json.value("BootDotnetOpenProcessHookMode", config.BootDotnetOpenProcessHookMode);
if (const auto it = json.find("BootEnabledGameFixes"); it != json.end() && it->is_array()) {
config.BootEnabledGameFixes.clear();
@ -148,6 +150,7 @@ void DalamudStartInfo::from_envvars() {
BootVehEnabled = utils::get_env<bool>(L"DALAMUD_IS_VEH");
BootVehFull = utils::get_env<bool>(L"DALAMUD_IS_VEH_FULL");
BootEnableEtw = utils::get_env<bool>(L"DALAMUD_ENABLE_ETW");
BootDisableLegacyCorruptedStateExceptions = utils::get_env<bool>(L"DALAMUD_DISABLE_LEGACY_CORRUPTED_STATE_EXCEPTIONS");
BootDotnetOpenProcessHookMode = static_cast<DotNetOpenProcessHookMode>(utils::get_env<int>(L"DALAMUD_DOTNET_OPENPROCESS_HOOKMODE"));
for (const auto& item : utils::get_env_list<std::string>(L"DALAMUD_GAMEFIX_LIST"))
BootEnabledGameFixes.insert(unicode::convert<std::string>(item, &unicode::lower));

View file

@ -17,7 +17,7 @@ struct DalamudStartInfo {
DirectHook = 1,
};
friend void from_json(const nlohmann::json&, DotNetOpenProcessHookMode&);
enum class ClientLanguage : int {
Japanese,
English,
@ -47,6 +47,7 @@ struct DalamudStartInfo {
std::string PluginDirectory;
std::string AssetDirectory;
ClientLanguage Language = ClientLanguage::English;
std::string Platform;
std::string GameVersion;
std::string TroubleshootingPackData;
int DelayInitializeMs = 0;
@ -61,6 +62,7 @@ struct DalamudStartInfo {
bool BootVehEnabled = false;
bool BootVehFull = false;
bool BootEnableEtw = false;
bool BootDisableLegacyCorruptedStateExceptions = false;
DotNetOpenProcessHookMode BootDotnetOpenProcessHookMode = DotNetOpenProcessHookMode::ImportHooks;
std::set<std::string> BootEnabledGameFixes{};
std::set<std::string> BootUnhookDlls{};

View file

@ -11,7 +11,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);
@ -21,7 +21,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);
@ -29,16 +29,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());
}
@ -59,15 +59,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());
}
@ -83,7 +83,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__);
@ -117,6 +117,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
const auto result = InitializeClrAndGetEntryPoint(
g_hModule,
g_startInfo.BootEnableEtw,
false, // !g_startInfo.BootDisableLegacyCorruptedStateExceptions,
runtimeconfig_path,
module_path,
L"Dalamud.EntryPoint, Dalamud",
@ -174,11 +175,11 @@ BOOL APIENTRY DllMain(const HMODULE hModule, const DWORD dwReason, LPVOID lpRese
case DLL_PROCESS_DETACH:
// do not show debug message boxes on abort() here
_set_abort_behavior(0, _WRITE_ABORT_MSG);
// process is terminating; don't bother cleaning up
if (lpReserved)
return TRUE;
logging::update_dll_load_status(false);
xivfixes::apply_all(false);

View file

@ -1,4 +1,5 @@
#include "pch.h"
#include "DalamudStartInfo.h"
#include "utils.h"
@ -584,6 +585,10 @@ std::vector<std::string> utils::get_env_list(const wchar_t* pcszName) {
return res;
}
bool utils::is_running_on_wine() {
return g_startInfo.Platform != "WINDOWS";
}
std::filesystem::path utils::get_module_path(HMODULE hModule) {
std::wstring buf(MAX_PATH, L'\0');
while (true) {

View file

@ -267,6 +267,8 @@ namespace utils {
return get_env_list<T>(unicode::convert<std::wstring>(pcszName).c_str());
}
bool is_running_on_wine();
std::filesystem::path get_module_path(HMODULE hModule);
/// @brief Find the game main window.

View file

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

View file

@ -1,3 +1,4 @@
using System.Runtime.InteropServices;
using Dalamud.Common.Game;
using Newtonsoft.Json;
@ -15,7 +16,7 @@ public record DalamudStartInfo
/// </summary>
public DalamudStartInfo()
{
// ignored
this.Platform = OSPlatform.Create("UNKNOWN");
}
/// <summary>
@ -58,6 +59,12 @@ public record DalamudStartInfo
/// </summary>
public ClientLanguage Language { get; set; } = ClientLanguage.English;
/// <summary>
/// Gets or sets the underlying platform<72>Dalamud runs on.
/// </summary>
[JsonConverter(typeof(OSPlatformConverter))]
public OSPlatform Platform { get; set; }
/// <summary>
/// Gets or sets the current game version code.
/// </summary>
@ -120,10 +127,15 @@ 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; }
/// <summary>
/// Gets or sets a value indicating whether to enable legacy corrupted state exceptions.
/// </summary>
public bool BootDisableLegacyCorruptedStateExceptions { get; set; }
/// <summary>
/// Gets or sets a value choosing the OpenProcess hookmode.
/// </summary>

View file

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

View file

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

View file

@ -1,9 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>Dalamud.CorePlugin</AssemblyName>
<TargetFramework>net8.0-windows</TargetFramework>
<Platforms>x64</Platforms>
<LangVersion>10.0</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>

View file

@ -27,6 +27,7 @@ int wmain(int argc, wchar_t** argv)
const auto result = InitializeClrAndGetEntryPoint(
GetModuleHandleW(nullptr),
false,
false,
runtimeconfig_path,
module_path,
L"Dalamud.Injector.EntryPoint, Dalamud.Injector",

View file

@ -1,11 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Target">
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PlatformTarget>x64</PlatformTarget>
<Platforms>x64;AnyCPU</Platforms>
<LangVersion>10.0</LangVersion>
</PropertyGroup>
<PropertyGroup Label="Feature">

View file

@ -11,6 +11,8 @@ using System.Text.RegularExpressions;
using Dalamud.Common;
using Dalamud.Common.Game;
using Dalamud.Common.Util;
using Newtonsoft.Json;
using Reloaded.Memory.Buffers;
using Serilog;
@ -94,6 +96,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");
@ -262,6 +265,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;
@ -277,9 +309,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="))
@ -306,6 +343,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..]));
@ -377,11 +418,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;
@ -399,6 +466,7 @@ namespace Dalamud.Injector
// Set boot defaults
startInfo.BootShowConsole = args.Contains("--console");
startInfo.BootEnableEtw = args.Contains("--etw");
startInfo.BootDisableLegacyCorruptedStateExceptions = args.Contains("--no-legacy-corrupted-state-exceptions");
startInfo.BootLogPath = GetLogPath(startInfo.LogPath, "dalamud.boot", startInfo.LogName);
startInfo.BootEnabledGameFixes = new()
{
@ -463,13 +531,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]");
@ -729,15 +798,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;
}
@ -792,20 +888,6 @@ namespace Dalamud.Injector
if (encryptArguments)
{
var rawTickCount = (uint)Environment.TickCount;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
[System.Runtime.InteropServices.DllImport("c")]
#pragma warning disable SA1300
static extern ulong clock_gettime_nsec_np(int clockId);
#pragma warning restore SA1300
const int CLOCK_MONOTONIC_RAW = 4;
var rawTickCountFixed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) / 1000000;
Log.Information("ArgumentBuilder::DeriveKey() fixing up rawTickCount from {0} to {1} on macOS", rawTickCount, rawTickCountFixed);
rawTickCount = (uint)rawTickCountFixed;
}
var ticks = rawTickCount & 0xFFFF_FFFFu;
var key = ticks & 0xFFFF_0000u;
gameArguments.Insert(0, $"T={ticks}");

View file

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

View file

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

View file

@ -74,8 +74,10 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.ActiveCfg = Debug|x64
{94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.Build.0 = Debug|x64
{94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.ActiveCfg = Release|x64
{94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.Build.0 = Release|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.ActiveCfg = Debug|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.Build.0 = Debug|x64
{B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.ActiveCfg = Release|x64
@ -124,14 +126,14 @@ Global
{317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|Any CPU.Build.0 = Debug|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|Any CPU.ActiveCfg = Release|x64
{317A264C-920B-44A1-8A34-F3A6827B0705}.Release|Any CPU.Build.0 = Release|x64
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.Build.0 = Release|Any CPU
{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Release|Any CPU.Build.0 = Release|Any CPU
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.ActiveCfg = Debug|x64
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.Build.0 = Debug|x64
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.ActiveCfg = Release|x64
{F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.Build.0 = Release|x64
{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Debug|Any CPU.ActiveCfg = Debug|x64
{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Debug|Any CPU.Build.0 = Debug|x64
{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Release|Any CPU.ActiveCfg = Release|x64
{A568929D-6FF6-4DFA-9D14-5D7DC08FA5E0}.Release|Any CPU.Build.0 = Release|x64
{3620414C-7DFC-423E-929F-310E19F5D930}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3620414C-7DFC-423E-929F-310E19F5D930}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3620414C-7DFC-423E-929F-310E19F5D930}.Release|Any CPU.ActiveCfg = Release|Any CPU

View file

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

View file

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

View file

@ -1,16 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Target">
<TargetFramework>net8.0-windows</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<Platforms>x64</Platforms>
<LangVersion>12.0</LangVersion>
<EnableWindowsTargeting>True</EnableWindowsTargeting>
</PropertyGroup>
<PropertyGroup Label="Feature">
<Description>XIV Launcher addon framework</Description>
<DalamudVersion>11.0.8.0</DalamudVersion>
<DalamudVersion>12.0.0.0</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion>
@ -94,6 +90,7 @@
<ItemGroup>
<ProjectReference Include="..\Dalamud.Common\Dalamud.Common.csproj" />
<ProjectReference Include="..\lib\FFXIVClientStructs\FFXIVClientStructs\FFXIVClientStructs.csproj" />
<ProjectReference Include="..\lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj" />
<ProjectReference Include="..\lib\ImGuiScene\deps\ImGui.NET\src\ImGui.NET-472\ImGui.NET-472.csproj" />
<ProjectReference Include="..\lib\ImGuiScene\deps\SDL2-CS\SDL2-CS.csproj" />
<ProjectReference Include="..\lib\ImGuiScene\ImGuiScene\ImGuiScene.csproj" />

View file

@ -178,20 +178,18 @@ public sealed class EntryPoint
throw new Exception("Working directory was invalid");
Reloaded.Hooks.Tools.Utilities.FasmBasePath = new DirectoryInfo(info.WorkingDirectory);
// Apply common fixes for culture issues
CultureFixes.Apply();
// 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;
if (!Util.IsWine())
// Currently VEH is not fully functional on WINE
if (info.Platform != OSPlatform.Windows)
InitSymbolHandler(info);
var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent);
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]",
Util.GetScmVersion(),
Util.GetGitHashClientStructs(),
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]",
Util.GetScmVersion(),
Util.GetGitHashClientStructs(),
FFXIVClientStructs.ThisAssembly.Git.Commits);
dalamud.WaitForUnload();

View file

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

View file

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

View file

@ -13,19 +13,19 @@ internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver
/// This is called for a majority of all addon OnSetup's.
/// </summary>
public nint AddonSetup { get; private set; }
/// <summary>
/// Gets the address of the other addon setup hook invoked by the AtkUnitManager.
/// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
/// This seems to be called rarely for specific addons.
/// </summary>
public nint AddonSetup2 { get; private set; }
/// <summary>
/// Gets the address of the addon finalize hook invoked by the AtkUnitManager.
/// </summary>
public nint AddonFinalize { get; private set; }
/// <summary>
/// Gets the address of the addon draw hook invoked by virtual function call.
/// </summary>
@ -35,7 +35,7 @@ internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver
/// Gets the address of the addon update hook invoked by virtual function call.
/// </summary>
public nint AddonUpdate { get; private set; }
/// <summary>
/// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call.
/// </summary>
@ -51,6 +51,6 @@ internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver
this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5");
this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C4 48 81 EF ?? ?? ?? ?? 48 83 ED 01");
this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF ?? ?? ?? ?? 45 33 D2");
this.AddonOnRequestedUpdate = sig.ScanText("FF 90 98 01 00 00 48 8B 5C 24 30 48 83 C4 20");
this.AddonOnRequestedUpdate = sig.ScanText("FF 90 A0 01 00 00 48 8B 5C 24 30");
}
}

View file

@ -24,12 +24,6 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
/// </summary>
public IntPtr ProcessPacketPlayerSetup { get; private set; }
/// <summary>
/// Gets the address of the method which polls the gamepads for data.
/// Called every frame, even when `Enable Gamepad` is off in the settings.
/// </summary>
public IntPtr GamepadPoll { get; private set; }
/// <summary>
/// Scan for and setup any configured address pointers.
/// </summary>
@ -43,7 +37,5 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver
// 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.GamepadPoll = sig.ScanText("40 55 53 57 41 54 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC ?? ?? ?? ?? 44 0F 29 B4 24"); // unnamed in cs
}
}

View file

@ -1,75 +0,0 @@
using System.Runtime.InteropServices;
namespace Dalamud.Game.ClientState.GamePad;
/// <summary>
/// Struct which gets populated by polling the gamepads.
///
/// Has an array of gamepads, among many other things (here not mapped).
/// All we really care about is the final data which the game uses to determine input.
///
/// The size is definitely bigger than only the following fields but I do not know how big.
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct GamepadInput
{
/// <summary>
/// Left analogue stick's horizontal value, -99 for left, 99 for right.
/// </summary>
[FieldOffset(0x78)]
public int LeftStickX;
/// <summary>
/// Left analogue stick's vertical value, -99 for down, 99 for up.
/// </summary>
[FieldOffset(0x7C)]
public int LeftStickY;
/// <summary>
/// Right analogue stick's horizontal value, -99 for left, 99 for right.
/// </summary>
[FieldOffset(0x80)]
public int RightStickX;
/// <summary>
/// Right analogue stick's vertical value, -99 for down, 99 for up.
/// </summary>
[FieldOffset(0x84)]
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(0x88)]
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(0x8C)]
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(0x90)]
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(0x94)]
public ushort ButtonsRepeat;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,7 +25,13 @@ 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.
@ -40,10 +46,25 @@ public unsafe class SMNGauge : JobGaugeBase<SummonerGauge>
public PetGlam ReturnSummonGlam => (PetGlam)this.Struct->ReturnSummonGlam;
/// <summary>
/// Gets the amount of aspected Attunment remaining.
/// Gets the amount of aspected Attunement remaining.
/// </summary>
/// <remarks>
/// As of 7.01, this should be treated as a bit field.
/// Use <see cref="AttunementCount"/> and <see cref="AttunementType"/> instead.
/// </remarks>
public byte Attunement => this.Struct->Attunement;
/// <summary>
/// Gets the count of attunement cost resource available.
/// </summary>
public byte AttunementCount => this.Struct->AttunementCount;
/// <summary>
/// Gets the type of attunement available.
/// Use the summon attuned accessors instead.
/// </summary>
public SummonAttunement AttunementType => (SummonAttunement)this.Struct->AttunementType;
/// <summary>
/// Gets the current aether flags.
/// Use the summon accessors instead.
@ -84,19 +105,19 @@ public unsafe class SMNGauge : JobGaugeBase<SummonerGauge>
/// Gets a value indicating whether if Ifrit is currently attuned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsIfritAttuned => this.AetherFlags.HasFlag(AetherFlags.IfritAttuned) && !this.AetherFlags.HasFlag(AetherFlags.GarudaAttuned);
public bool IsIfritAttuned => this.AttunementType == SummonAttunement.Ifrit;
/// <summary>
/// Gets a value indicating whether if Titan is currently attuned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsTitanAttuned => this.AetherFlags.HasFlag(AetherFlags.TitanAttuned) && !this.AetherFlags.HasFlag(AetherFlags.GarudaAttuned);
public bool IsTitanAttuned => this.AttunementType == SummonAttunement.Titan;
/// <summary>
/// Gets a value indicating whether if Garuda is currently attuned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsGarudaAttuned => this.AetherFlags.HasFlag(AetherFlags.GarudaAttuned);
public bool IsGarudaAttuned => this.AttunementType == SummonAttunement.Garuda;
/// <summary>
/// Gets a value indicating whether there are any Aetherflow stacks available.

View file

@ -7,8 +7,6 @@ using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
@ -31,20 +29,13 @@ namespace Dalamud.Game.ClientState.Objects;
#pragma warning restore SA1015
internal sealed partial class ObjectTable : IServiceType, IObjectTable
{
private static readonly ModuleLog Log = new("ObjectTable");
private static int objectTableLength;
private readonly ClientState clientState;
private readonly CachedEntry[] cachedObjectTable;
private readonly ObjectPool<Enumerator> multiThreadedEnumerators =
new DefaultObjectPoolProvider().Create<Enumerator>();
private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4];
private long nextMultithreadedUsageWarnTime;
[ServiceManager.ServiceConstructor]
private unsafe ObjectTable(ClientState clientState)
{
@ -66,7 +57,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
{
get
{
_ = this.WarnMultithreadedUsage();
ThreadSafety.AssertMainThread();
return (nint)(&CSGameObjectManager.Instance()->Objects);
}
@ -80,7 +71,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
{
get
{
_ = this.WarnMultithreadedUsage();
ThreadSafety.AssertMainThread();
return (index >= objectTableLength || index < 0) ? null : this.cachedObjectTable[index].Update();
}
@ -89,7 +80,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
/// <inheritdoc/>
public IGameObject? SearchById(ulong gameObjectId)
{
_ = this.WarnMultithreadedUsage();
ThreadSafety.AssertMainThread();
if (gameObjectId is 0)
return null;
@ -106,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;
@ -123,7 +114,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
/// <inheritdoc/>
public unsafe nint GetObjectAddress(int index)
{
_ = this.WarnMultithreadedUsage();
ThreadSafety.AssertMainThread();
return (index >= objectTableLength || index < 0) ? nint.Zero : (nint)this.cachedObjectTable[index].Address;
}
@ -131,7 +122,7 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
/// <inheritdoc/>
public unsafe IGameObject? CreateObjectReference(nint address)
{
_ = this.WarnMultithreadedUsage();
ThreadSafety.AssertMainThread();
if (this.clientState.LocalContentId == 0)
return null;
@ -155,27 +146,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
};
}
[Api12ToDo("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>
/// <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>
@ -228,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())
@ -256,21 +219,12 @@ 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;
@ -293,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()
@ -302,10 +254,8 @@ internal sealed partial class ObjectTable
if (this.owner is not { } o)
return;
if (this.slotId == -1)
o.multiThreadedEnumerators.Return(this);
else
o.frameworkThreadEnumerators[this.slotId] = this;
if (slotId != -1)
o.frameworkThreadEnumerators[slotId] = this;
}
public bool TryReset()

View file

@ -42,8 +42,10 @@ public unsafe class Status
/// <summary>
/// Gets the stack count of this status.
/// Only valid if this is a non-food status.
/// </summary>
public byte StackCount => this.Struct->StackCount;
[Obsolete($"Replaced with {nameof(Param)}", true)]
public byte StackCount => (byte)this.Struct->Param;
/// <summary>
/// Gets the time remaining of this status.

View file

@ -52,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.
@ -526,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);
@ -544,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))
@ -556,12 +556,12 @@ public class GameConfigSection
var e = configBase->ConfigEntry;
for (var i = 0U; i < configBase->ConfigCount; i++, e++)
{
if (e->Name == null)
if (e->Name.Value == null)
{
continue;
}
var eName = MemoryHelper.ReadStringNullTerminated(new IntPtr(e->Name));
var eName = e->Name.ToString();
if (eName.Equals(name))
{
this.indexMap.TryAdd(name, i);

View file

@ -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.
@ -1436,46 +1478,4 @@ public enum SystemConfigOption
/// </summary>
[GameConfigOption("PadButton_R3", ConfigType.String)]
PadButton_R3,
/// <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 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 FirstConfigBackup.
/// This option is a UInt.
/// </summary>
[GameConfigOption("FirstConfigBackup", ConfigType.UInt)]
FirstConfigBackup,
}

View file

@ -37,6 +37,13 @@ public enum UiConfigOption
[GameConfigOption("BattleEffectPvPEnemyPc", ConfigType.UInt)]
BattleEffectPvPEnemyPc,
/// <summary>
/// UiConfig option with the internal name PadMode.
/// This option is a UInt.
/// </summary>
[GameConfigOption("PadMode", ConfigType.UInt)]
PadMode,
/// <summary>
/// UiConfig option with the internal name WeaponAutoPutAway.
/// This option is a UInt.
@ -114,14 +121,6 @@ public enum UiConfigOption
[GameConfigOption("LockonDefaultZoom", ConfigType.Float)]
LockonDefaultZoom,
/// <summary>
/// UiConfig option with the internal name LockonDefaultZoom_179.
/// This option is a Float.
/// </summary>
[Obsolete("This option won't work. Use LockonDefaultZoom.", true)]
[GameConfigOption("LockonDefaultZoom_179", ConfigType.Float)]
LockonDefaultZoom_179,
/// <summary>
/// UiConfig option with the internal name CameraProductionOfAction.
/// This option is a UInt.
@ -311,6 +310,27 @@ public enum UiConfigOption
[GameConfigOption("RightClickExclusionMinion", ConfigType.UInt)]
RightClickExclusionMinion,
/// <summary>
/// UiConfig option with the internal name EnableMoveTiltCharacter.
/// This option is a UInt.
/// </summary>
[GameConfigOption("EnableMoveTiltCharacter", ConfigType.UInt)]
EnableMoveTiltCharacter,
/// <summary>
/// UiConfig option with the internal name EnableMoveTiltMountGround.
/// This option is a UInt.
/// </summary>
[GameConfigOption("EnableMoveTiltMountGround", ConfigType.UInt)]
EnableMoveTiltMountGround,
/// <summary>
/// UiConfig option with the internal name EnableMoveTiltMountFly.
/// This option is a UInt.
/// </summary>
[GameConfigOption("EnableMoveTiltMountFly", ConfigType.UInt)]
EnableMoveTiltMountFly,
/// <summary>
/// UiConfig option with the internal name TurnSpeed.
/// This option is a UInt.
@ -1130,6 +1150,27 @@ public enum UiConfigOption
[GameConfigOption("HotbarXHBEditEnable", ConfigType.UInt)]
HotbarXHBEditEnable,
/// <summary>
/// UiConfig option with the internal name HotbarContentsAction2ReverseOperation.
/// This option is a UInt.
/// </summary>
[GameConfigOption("HotbarContentsAction2ReverseOperation", ConfigType.UInt)]
HotbarContentsAction2ReverseOperation,
/// <summary>
/// UiConfig option with the internal name HotbarContentsAction2ReturnInitialSlot.
/// This option is a UInt.
/// </summary>
[GameConfigOption("HotbarContentsAction2ReturnInitialSlot", ConfigType.UInt)]
HotbarContentsAction2ReturnInitialSlot,
/// <summary>
/// UiConfig option with the internal name HotbarContentsAction2ReverseRotate.
/// This option is a UInt.
/// </summary>
[GameConfigOption("HotbarContentsAction2ReverseRotate", ConfigType.UInt)]
HotbarContentsAction2ReverseRotate,
/// <summary>
/// UiConfig option with the internal name PlateType.
/// This option is a UInt.
@ -3572,32 +3613,4 @@ public enum UiConfigOption
/// </summary>
[GameConfigOption("PvPFrontlinesGCFree", ConfigType.UInt)]
PvPFrontlinesGCFree,
/// <summary>
/// UiConfig option with the internal name PadMode.
/// This option is a UInt.
/// </summary>
[GameConfigOption("PadMode", ConfigType.UInt)]
PadMode,
/// <summary>
/// UiConfig option with the internal name EnableMoveTiltCharacter.
/// This option is a UInt.
/// </summary>
[GameConfigOption("EnableMoveTiltCharacter", ConfigType.UInt)]
EnableMoveTiltCharacter,
/// <summary>
/// UiConfig option with the internal name EnableMoveTiltMountGround.
/// This option is a UInt.
/// </summary>
[GameConfigOption("EnableMoveTiltMountGround", ConfigType.UInt)]
EnableMoveTiltMountGround,
/// <summary>
/// UiConfig option with the internal name EnableMoveTiltMountFly.
/// This option is a UInt.
/// </summary>
[GameConfigOption("EnableMoveTiltMountFly", ConfigType.UInt)]
EnableMoveTiltMountFly,
}

View file

@ -30,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();
@ -58,7 +58,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
private ImmutableList<IReadOnlyDtrBarEntry>? entriesReadOnlyCopy;
private Utf8String* emptyString;
private uint runningNodeIds = BaseNodeId;
private float entryStartPos = float.NaN;
@ -72,7 +72,7 @@ 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 ??= [];
@ -522,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}");
@ -590,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 };
@ -609,7 +609,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
return newTextNode;
}
private void DtrEventHandler(AddonEventType atkEventType, IntPtr atkUnitBase, IntPtr atkResNode)
{
var addon = (AtkUnitBase*)atkUnitBase;
@ -632,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;
@ -646,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;

View file

@ -92,214 +92,232 @@ public enum FlyTextKind : int
/// </summary>
IslandExp = 15,
/// <summary>
/// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle.
/// Added in 7.2, usage currently unknown.
/// </summary>
Unknown16 = 16,
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle.
/// Added in 7.2, usage currently unknown.
/// </summary>
Unknown17 = 17,
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle.
/// Added in 7.2, usage currently unknown.
/// </summary>
Unknown18 = 18,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle.
/// </summary>
MpDrain = 16,
MpDrain = 19,
/// <summary>
/// Currently not used by the game.
/// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle.
/// </summary>
NamedTp = 17,
NamedTp = 20,
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle with sans-serif Text1 to the left of the Val1.
/// </summary>
Healing = 18,
Healing = 21,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle.
/// </summary>
MpRegen = 19,
MpRegen = 22,
/// <summary>
/// Currently not used by the game.
/// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle.
/// </summary>
NamedTp2 = 20,
NamedTp2 = 23,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font EP with Text2 in sans-serif as subtitle.
/// </summary>
EpRegen = 21,
EpRegen = 24,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font CP with Text2 in sans-serif as subtitle.
/// </summary>
CpRegen = 22,
CpRegen = 25,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font GP with Text2 in sans-serif as subtitle.
/// </summary>
GpRegen = 23,
GpRegen = 26,
/// <summary>
/// Displays nothing.
/// </summary>
None = 24,
None = 27,
/// <summary>
/// All caps serif INVULNERABLE.
/// </summary>
Invulnerable = 25,
Invulnerable = 28,
/// <summary>
/// All caps sans-serif condensed font INTERRUPTED!
/// Does a large bounce effect on appearance.
/// Does not scroll up or down the screen.
/// </summary>
Interrupted = 26,
Interrupted = 29,
/// <summary>
/// Val1 in serif font.
/// </summary>
CraftingProgress = 27,
CraftingProgress = 30,
/// <summary>
/// Val1 in serif font.
/// </summary>
CraftingQuality = 28,
CraftingQuality = 31,
/// <summary>
/// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. Does a bigger bounce effect on appearance.
/// </summary>
CraftingQualityCrit = 29,
CraftingQualityCrit = 32,
/// <summary>
/// Currently not used by the game.
/// Val1 in serif font.
/// </summary>
AutoAttackNoText3 = 30,
AutoAttackNoText3 = 33,
/// <summary>
/// CriticalHit with sans-serif Text1 to the left of the Val1 (2).
/// </summary>
HealingCrit = 31,
HealingCrit = 34,
/// <summary>
/// Currently not used by the game.
/// Same as DamageCrit with a MP in condensed font to the right of Val1.
/// Does a jiggle effect to the right on appearance.
/// </summary>
NamedCriticalHitWithMp = 32,
NamedCriticalHitWithMp = 35,
/// <summary>
/// Currently not used by the game.
/// Same as DamageCrit with a TP in condensed font to the right of Val1.
/// Does a jiggle effect to the right on appearance.
/// </summary>
NamedCriticalHitWithTp = 33,
NamedCriticalHitWithTp = 36,
/// <summary>
/// Icon next to sans-serif Text1 with sans-serif "has no effect!" to the right.
/// </summary>
DebuffNoEffect = 34,
DebuffNoEffect = 37,
/// <summary>
/// Icon next to sans-serif slightly faded Text1.
/// </summary>
BuffFading = 35,
BuffFading = 38,
/// <summary>
/// Icon next to sans-serif slightly faded Text1.
/// </summary>
DebuffFading = 36,
DebuffFading = 39,
/// <summary>
/// Text1 in sans-serif font.
/// </summary>
Named = 37,
Named = 40,
/// <summary>
/// Icon next to sans-serif Text1 with sans-serif "(fully resisted)" to the right.
/// </summary>
DebuffResisted = 38,
DebuffResisted = 41,
/// <summary>
/// All caps serif 'INCAPACITATED!'.
/// </summary>
Incapacitated = 39,
Incapacitated = 42,
/// <summary>
/// Text1 with sans-serif "(fully resisted)" to the right.
/// </summary>
FullyResisted = 40,
FullyResisted = 43,
/// <summary>
/// Text1 with sans-serif "has no effect!" to the right.
/// </summary>
HasNoEffect = 41,
HasNoEffect = 44,
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle with sans-serif Text1 to the left of the Val1.
/// </summary>
HpDrain = 42,
HpDrain = 45,
/// <summary>
/// Currently not used by the game.
/// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle.
/// </summary>
NamedMp3 = 43,
NamedMp3 = 46,
/// <summary>
/// Currently not used by the game.
/// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle.
/// </summary>
NamedTp3 = 44,
NamedTp3 = 47,
/// <summary>
/// Icon next to sans-serif Text1 with serif "INVULNERABLE!" beneath the Text1.
/// </summary>
DebuffInvulnerable = 45,
DebuffInvulnerable = 48,
/// <summary>
/// All caps serif RESIST.
/// </summary>
Resist = 46,
Resist = 49,
/// <summary>
/// Icon with an item icon outline next to sans-serif Text1.
/// </summary>
LootedItem = 47,
LootedItem = 50,
/// <summary>
/// Val1 in serif font.
/// </summary>
Collectability = 48,
Collectability = 51,
/// <summary>
/// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle.
/// Does a bigger bounce effect on appearance.
/// </summary>
CollectabilityCrit = 49,
CollectabilityCrit = 52,
/// <summary>
/// All caps serif REFLECT.
/// </summary>
Reflect = 50,
Reflect = 53,
/// <summary>
/// All caps serif REFLECTED.
/// </summary>
Reflected = 51,
Reflected = 54,
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle.
/// Does a bounce effect on appearance.
/// </summary>
CraftingQualityDh = 52,
CraftingQualityDh = 55,
/// <summary>
/// Currently not used by the game.
/// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle.
/// Does a bigger bounce effect on appearance.
/// </summary>
CriticalHit4 = 53,
CriticalHit4 = 56,
/// <summary>
/// Val1 in even larger serif font with 2 exclamations, Text2 in sans-serif as subtitle.
/// Does a large bounce effect on appearance. Does not scroll up or down the screen.
/// </summary>
CraftingQualityCritDh = 54,
CraftingQualityCritDh = 57,
}

View file

@ -323,7 +323,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
return ret;
}
private void HandleActionHoverDetour(AgentActionDetail* hoverState, ActionKind actionKind, uint actionId, int a4, byte a5)
private void HandleActionHoverDetour(AgentActionDetail* hoverState, FFXIVClientStructs.FFXIV.Client.UI.Agent.ActionKind actionKind, uint actionId, int a4, byte a5)
{
this.handleActionHoverHook.Original(hoverState, actionKind, actionId, a4, a5);
this.HoveredAction.ActionKind = (HoverActionKind)actionKind;

View file

@ -53,7 +53,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
this.locDalamudSettings = Loc.Localize("SystemMenuSettings", "Dalamud Settings");
// this.contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened;
this.hookAgentHudOpenSystemMenu.Enable();
this.hookUiModuleExecuteMainCommand.Enable();
this.hookAtkUnitBaseReceiveGlobalEvent.Enable();
@ -180,7 +180,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
// about hooking the exd reader, thank god
var firstStringEntry = &atkValueArgs[5 + 18];
firstStringEntry->ChangeType(ValueType.String);
var secondStringEntry = &atkValueArgs[6 + 18];
secondStringEntry->ChangeType(ValueType.String);
@ -193,7 +193,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
.Append($"{SeIconChar.BoxedLetterD.ToIconString()} ")
.Append(new UIForegroundPayload(0))
.Append(this.locDalamudSettings).Encode();
firstStringEntry->SetManagedString(strPlugins);
secondStringEntry->SetManagedString(strSettings);

View file

@ -17,7 +17,7 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
/// </summary>
[FieldOffset(0)]
internal readonly InventoryItem InternalItem;
/// <summary>
/// The view of the backing data, in <see cref="ulong"/>.
/// </summary>
@ -55,10 +55,16 @@ public unsafe struct GameInventoryItem : IEquatable<GameInventoryItem>
/// </summary>
public int Quantity => this.InternalItem.Quantity;
/// <summary>
/// Gets the spiritbond or collectability of this item.
/// </summary>
public uint SpiritbondOrCollectability => this.InternalItem.SpiritbondOrCollectability;
/// <summary>
/// Gets the spiritbond of this item.
/// </summary>
public uint Spiritbond => this.InternalItem.Spiritbond;
[Obsolete($"Renamed to {nameof(SpiritbondOrCollectability)}", true)]
public uint Spiritbond => this.SpiritbondOrCollectability;
/// <summary>
/// Gets the repair condition of this item.

View file

@ -565,7 +565,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
return this.configuration.IsMbCollect;
}
private void MarketPurchasePacketDetour(PacketDispatcher* a1, nint packetData)
private void MarketPurchasePacketDetour(uint targetId, nint packetData)
{
try
{
@ -576,7 +576,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
Log.Error(ex, "MarketPurchasePacketHandler threw an exception");
}
this.mbPurchaseHook.OriginalDisposeSafe(a1, packetData);
this.mbPurchaseHook.OriginalDisposeSafe(targetId, packetData);
}
private void MarketHistoryPacketDetour(InfoProxyItemSearch* a1, nint packetData)
@ -609,7 +609,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
this.customTalkHook.OriginalDisposeSafe(a1, eventId, responseId, args, argCount);
}
private void MarketItemRequestStartDetour(PacketDispatcher* a1, nint packetRef)
private void MarketItemRequestStartDetour(uint targetId, nint packetRef)
{
try
{
@ -620,7 +620,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
Log.Error(ex, "MarketItemRequestStartDetour threw an exception");
}
this.mbItemRequestStartHook.OriginalDisposeSafe(a1, packetRef);
this.mbItemRequestStartHook.OriginalDisposeSafe(targetId, packetRef);
}
private void MarketBoardOfferingsDetour(InfoProxyItemSearch* a1, nint packetRef)

View file

@ -0,0 +1,30 @@
using Lumina.Text;
namespace Dalamud.Game.Text.Evaluator.Internal;
/// <summary>
/// Wraps payloads in an open and close icon, for example the Auto Translation open/close brackets.
/// </summary>
internal readonly struct SeStringBuilderIconWrap : IDisposable
{
private readonly SeStringBuilder builder;
private readonly uint iconClose;
/// <summary>
/// Initializes a new instance of the <see cref="SeStringBuilderIconWrap"/> struct.<br/>
/// Appends an icon macro with <paramref name="iconOpen"/> on creation, and an icon macro with
/// <paramref name="iconClose"/> on disposal.
/// </summary>
/// <param name="builder">The builder to use.</param>
/// <param name="iconOpen">The open icon id.</param>
/// <param name="iconClose">The close icon id.</param>
public SeStringBuilderIconWrap(SeStringBuilder builder, uint iconOpen, uint iconClose)
{
this.builder = builder;
this.iconClose = iconClose;
this.builder.AppendIcon(iconOpen);
}
/// <inheritdoc/>
public void Dispose() => this.builder.AppendIcon(this.iconClose);
}

View file

@ -0,0 +1,83 @@
using System.Globalization;
using Dalamud.Utility;
using Lumina.Text;
using Lumina.Text.ReadOnly;
namespace Dalamud.Game.Text.Evaluator.Internal;
/// <summary>
/// A context wrapper used in <see cref="SeStringEvaluator"/>.
/// </summary>
internal readonly ref struct SeStringContext
{
/// <summary>
/// The <see cref="SeStringBuilder"/> to append text and macros to.
/// </summary>
internal readonly SeStringBuilder Builder;
/// <summary>
/// A list of local parameters.
/// </summary>
internal readonly Span<SeStringParameter> LocalParameters;
/// <summary>
/// The target language, used for sheet lookups.
/// </summary>
internal readonly ClientLanguage Language;
/// <summary>
/// Initializes a new instance of the <see cref="SeStringContext"/> struct.
/// </summary>
/// <param name="builder">The <see cref="SeStringBuilder"/> to append text and macros to.</param>
/// <param name="localParameters">A list of local parameters.</param>
/// <param name="language">The target language, used for sheet lookups.</param>
internal SeStringContext(SeStringBuilder builder, Span<SeStringParameter> localParameters, ClientLanguage language)
{
this.Builder = builder;
this.LocalParameters = localParameters;
this.Language = language;
}
/// <summary>
/// Gets the <see cref="System.Globalization.CultureInfo"/> of the current target <see cref="Language"/>.
/// </summary>
internal CultureInfo CultureInfo => Localization.GetCultureInfoFromLangCode(this.Language.ToCode());
/// <summary>
/// Tries to get a number from the local parameters at the specified index.
/// </summary>
/// <param name="index">The index in the <see cref="LocalParameters"/> list.</param>
/// <param name="value">The local parameter number.</param>
/// <returns><c>true</c> if the local parameters list contained a parameter at given index, <c>false</c> otherwise.</returns>
internal bool TryGetLNum(int index, out uint value)
{
if (index >= 0 && this.LocalParameters.Length > index)
{
value = this.LocalParameters[index].UIntValue;
return true;
}
value = 0;
return false;
}
/// <summary>
/// Tries to get a string from the local parameters at the specified index.
/// </summary>
/// <param name="index">The index in the <see cref="LocalParameters"/> list.</param>
/// <param name="value">The local parameter string.</param>
/// <returns><c>true</c> if the local parameters list contained a parameter at given index, <c>false</c> otherwise.</returns>
internal bool TryGetLStr(int index, out ReadOnlySeString value)
{
if (index >= 0 && this.LocalParameters.Length > index)
{
value = this.LocalParameters[index].StringValue;
return true;
}
value = default;
return false;
}
}

View file

@ -0,0 +1,49 @@
namespace Dalamud.Game.Text.Evaluator.Internal;
/// <summary>
/// An enum providing additional information about the sheet redirect.
/// </summary>
[Flags]
internal enum SheetRedirectFlags
{
/// <summary>
/// No flags.
/// </summary>
None = 0,
/// <summary>
/// Resolved to a sheet related with items.
/// </summary>
Item = 1,
/// <summary>
/// Resolved to the EventItem sheet.
/// </summary>
EventItem = 2,
/// <summary>
/// Resolved to a high quality item.
/// </summary>
/// <remarks>
/// Append Addon#9.
/// </remarks>
HighQuality = 4,
/// <summary>
/// Resolved to a collectible item.
/// </summary>
/// <remarks>
/// Append Addon#150.
/// </remarks>
Collectible = 8,
/// <summary>
/// Resolved to a sheet related with actions.
/// </summary>
Action = 16,
/// <summary>
/// Resolved to the Action sheet.
/// </summary>
ActionSheet = 32,
}

View file

@ -0,0 +1,233 @@
using Dalamud.Data;
using Dalamud.Utility;
using Lumina.Extensions;
using ItemKind = Dalamud.Game.Text.SeStringHandling.Payloads.ItemPayload.ItemKind;
using LSheets = Lumina.Excel.Sheets;
namespace Dalamud.Game.Text.Evaluator.Internal;
/// <summary>
/// A service to resolve sheet redirects in expressions.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class SheetRedirectResolver : IServiceType
{
private static readonly (string SheetName, uint ColumnIndex, bool ReturnActionSheetFlag)[] ActStrSheets =
[
(nameof(LSheets.Trait), 0, false),
(nameof(LSheets.Action), 0, true),
(nameof(LSheets.Item), 0, false),
(nameof(LSheets.EventItem), 0, false),
(nameof(LSheets.EventAction), 0, false),
(nameof(LSheets.GeneralAction), 0, false),
(nameof(LSheets.BuddyAction), 0, false),
(nameof(LSheets.MainCommand), 5, false),
(nameof(LSheets.Companion), 0, false),
(nameof(LSheets.CraftAction), 0, false),
(nameof(LSheets.Action), 0, true),
(nameof(LSheets.PetAction), 0, false),
(nameof(LSheets.CompanyAction), 0, false),
(nameof(LSheets.Mount), 0, false),
(string.Empty, 0, false),
(string.Empty, 0, false),
(string.Empty, 0, false),
(string.Empty, 0, false),
(string.Empty, 0, false),
(nameof(LSheets.BgcArmyAction), 1, false),
(nameof(LSheets.Ornament), 8, false),
];
private static readonly string[] ObjStrSheetNames =
[
nameof(LSheets.BNpcName),
nameof(LSheets.ENpcResident),
nameof(LSheets.Treasure),
nameof(LSheets.Aetheryte),
nameof(LSheets.GatheringPointName),
nameof(LSheets.EObjName),
nameof(LSheets.Mount),
nameof(LSheets.Companion),
string.Empty,
string.Empty,
nameof(LSheets.Item),
];
[ServiceManager.ServiceDependency]
private readonly DataManager dataManager = Service<DataManager>.Get();
[ServiceManager.ServiceConstructor]
private SheetRedirectResolver()
{
}
/// <summary>
/// Resolves the sheet redirect, if any is present.
/// </summary>
/// <param name="sheetName">The sheet name.</param>
/// <param name="rowId">The row id.</param>
/// <param name="colIndex">The column index. Use <c>ushort.MaxValue</c> as default.</param>
/// <returns>Flags giving additional information about the redirect.</returns>
internal SheetRedirectFlags Resolve(ref string sheetName, ref uint rowId, ref uint colIndex)
{
var flags = SheetRedirectFlags.None;
switch (sheetName)
{
case nameof(LSheets.Item) or "ItemHQ" or "ItemMP":
{
flags |= SheetRedirectFlags.Item;
var (itemId, kind) = ItemUtil.GetBaseId(rowId);
if (kind == ItemKind.Hq || sheetName == "ItemHQ")
{
flags |= SheetRedirectFlags.HighQuality;
}
else if (kind == ItemKind.Collectible || sheetName == "ItemMP")
{
// MP for Masterpiece?!
flags |= SheetRedirectFlags.Collectible;
}
if (kind == ItemKind.EventItem &&
rowId - 2_000_000 <= this.dataManager.GetExcelSheet<LSheets.EventItem>().Count)
{
flags |= SheetRedirectFlags.EventItem;
sheetName = nameof(LSheets.EventItem);
}
else
{
sheetName = nameof(LSheets.Item);
rowId = itemId;
}
if (colIndex is >= 4 and <= 7)
return SheetRedirectFlags.None;
break;
}
case "ActStr":
{
var returnActionSheetFlag = false;
(var index, rowId) = uint.DivRem(rowId, 1000000);
if (index < ActStrSheets.Length)
(sheetName, colIndex, returnActionSheetFlag) = ActStrSheets[index];
if (sheetName != nameof(LSheets.Companion) && colIndex != 13)
flags |= SheetRedirectFlags.Action;
if (returnActionSheetFlag)
flags |= SheetRedirectFlags.ActionSheet;
break;
}
case "ObjStr":
{
(var index, rowId) = uint.DivRem(rowId, 1000000);
if (index < ObjStrSheetNames.Length)
sheetName = ObjStrSheetNames[index];
colIndex = 0;
switch (index)
{
case 0: // BNpcName
if (rowId >= 100000)
rowId += 900000;
break;
case 1: // ENpcResident
rowId += 1000000;
break;
case 2: // Treasure
if (this.dataManager.GetExcelSheet<LSheets.Treasure>().TryGetRow(rowId, out var treasureRow) &&
treasureRow.Unknown0.IsEmpty)
rowId = 0; // defaulting to "Treasure Coffer"
break;
case 3: // Aetheryte
rowId = this.dataManager.GetExcelSheet<LSheets.Aetheryte>()
.TryGetRow(rowId, out var aetheryteRow) && aetheryteRow.IsAetheryte
? 0u // "Aetheryte"
: 1; // "Aethernet Shard"
break;
case 5: // EObjName
rowId += 2000000;
break;
}
break;
}
case nameof(LSheets.EObj) when colIndex is <= 7 or ushort.MaxValue:
sheetName = nameof(LSheets.EObjName);
break;
case nameof(LSheets.Treasure)
when this.dataManager.GetExcelSheet<LSheets.Treasure>().TryGetRow(rowId, out var treasureRow) &&
treasureRow.Unknown0.IsEmpty:
rowId = 0; // defaulting to "Treasure Coffer"
break;
case "WeatherPlaceName":
{
sheetName = nameof(LSheets.PlaceName);
var placeNameSubId = rowId;
if (this.dataManager.GetExcelSheet<LSheets.WeatherReportReplace>().TryGetFirst(
r => r.PlaceNameSub.RowId == placeNameSubId,
out var row))
rowId = row.PlaceNameParent.RowId;
break;
}
case nameof(LSheets.InstanceContent) when colIndex == 3:
{
sheetName = nameof(LSheets.ContentFinderCondition);
colIndex = 43;
if (this.dataManager.GetExcelSheet<LSheets.InstanceContent>().TryGetRow(rowId, out var row))
rowId = row.ContentFinderCondition.RowId;
break;
}
case nameof(LSheets.PartyContent) when colIndex == 2:
{
sheetName = nameof(LSheets.ContentFinderCondition);
colIndex = 43;
if (this.dataManager.GetExcelSheet<LSheets.PartyContent>().TryGetRow(rowId, out var row))
rowId = row.ContentFinderCondition.RowId;
break;
}
case nameof(LSheets.PublicContent) when colIndex == 3:
{
sheetName = nameof(LSheets.ContentFinderCondition);
colIndex = 43;
if (this.dataManager.GetExcelSheet<LSheets.PublicContent>().TryGetRow(rowId, out var row))
rowId = row.ContentFinderCondition.RowId;
break;
}
case nameof(LSheets.AkatsukiNote):
{
sheetName = nameof(LSheets.AkatsukiNoteString);
colIndex = 0;
if (this.dataManager.Excel.GetSubrowSheet<LSheets.AkatsukiNote>().TryGetRow(rowId, out var row))
rowId = (uint)row[0].Unknown2;
break;
}
}
return flags;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,79 @@
using System.Globalization;
using Lumina.Text.ReadOnly;
using DSeString = Dalamud.Game.Text.SeStringHandling.SeString;
using LSeString = Lumina.Text.SeString;
namespace Dalamud.Game.Text.Evaluator;
/// <summary>
/// A wrapper for a local parameter, holding either a number or a string.
/// </summary>
public readonly struct SeStringParameter
{
private readonly uint num;
private readonly ReadOnlySeString str;
/// <summary>
/// Initializes a new instance of the <see cref="SeStringParameter"/> struct for a number parameter.
/// </summary>
/// <param name="value">The number value.</param>
public SeStringParameter(uint value)
{
this.num = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="SeStringParameter"/> struct for a string parameter.
/// </summary>
/// <param name="value">The string value.</param>
public SeStringParameter(ReadOnlySeString value)
{
this.str = value;
this.IsString = true;
}
/// <summary>
/// Initializes a new instance of the <see cref="SeStringParameter"/> struct for a string parameter.
/// </summary>
/// <param name="value">The string value.</param>
public SeStringParameter(string value)
{
this.str = new ReadOnlySeString(value);
this.IsString = true;
}
/// <summary>
/// Gets a value indicating whether the backing type of this parameter is a string.
/// </summary>
public bool IsString { get; }
/// <summary>
/// Gets a numeric value.
/// </summary>
public uint UIntValue =>
!this.IsString
? this.num
: uint.TryParse(this.str.ExtractText(), out var value) ? value : 0;
/// <summary>
/// Gets a string value.
/// </summary>
public ReadOnlySeString StringValue =>
this.IsString ? this.str : new(this.num.ToString("D", CultureInfo.InvariantCulture));
public static implicit operator SeStringParameter(int value) => new((uint)value);
public static implicit operator SeStringParameter(uint value) => new(value);
public static implicit operator SeStringParameter(ReadOnlySeString value) => new(value);
public static implicit operator SeStringParameter(ReadOnlySeStringSpan value) => new(new ReadOnlySeString(value));
public static implicit operator SeStringParameter(LSeString value) => new(new ReadOnlySeString(value.RawData));
public static implicit operator SeStringParameter(DSeString value) => new(new ReadOnlySeString(value.Encode()));
public static implicit operator SeStringParameter(string value) => new(value);
}

View file

@ -0,0 +1,17 @@
namespace Dalamud.Game.Text.Noun.Enums;
/// <summary>
/// Article types for <see cref="ClientLanguage.English"/>.
/// </summary>
public enum EnglishArticleType
{
/// <summary>
/// Indefinite article (a, an).
/// </summary>
Indefinite = 1,
/// <summary>
/// Definite article (the).
/// </summary>
Definite = 2,
}

View file

@ -0,0 +1,32 @@
namespace Dalamud.Game.Text.Noun.Enums;
/// <summary>
/// Article types for <see cref="ClientLanguage.French"/>.
/// </summary>
public enum FrenchArticleType
{
/// <summary>
/// Indefinite article (une, des).
/// </summary>
Indefinite = 1,
/// <summary>
/// Definite article (le, la, les).
/// </summary>
Definite = 2,
/// <summary>
/// Possessive article (mon, mes).
/// </summary>
PossessiveFirstPerson = 3,
/// <summary>
/// Possessive article (ton, tes).
/// </summary>
PossessiveSecondPerson = 4,
/// <summary>
/// Possessive article (son, ses).
/// </summary>
PossessiveThirdPerson = 5,
}

View file

@ -0,0 +1,37 @@
namespace Dalamud.Game.Text.Noun.Enums;
/// <summary>
/// Article types for <see cref="ClientLanguage.German"/>.
/// </summary>
public enum GermanArticleType
{
/// <summary>
/// Unbestimmter Artikel (ein, eine, etc.).
/// </summary>
Indefinite = 1,
/// <summary>
/// Bestimmter Artikel (der, die, das, etc.).
/// </summary>
Definite = 2,
/// <summary>
/// Possessivartikel "dein" (dein, deine, etc.).
/// </summary>
Possessive = 3,
/// <summary>
/// Negativartikel "kein" (kein, keine, etc.).
/// </summary>
Negative = 4,
/// <summary>
/// Nullartikel.
/// </summary>
ZeroArticle = 5,
/// <summary>
/// Demonstrativpronomen "dieser" (dieser, diese, etc.).
/// </summary>
Demonstrative = 6,
}

View file

@ -0,0 +1,17 @@
namespace Dalamud.Game.Text.Noun.Enums;
/// <summary>
/// Article types for <see cref="ClientLanguage.Japanese"/>.
/// </summary>
public enum JapaneseArticleType
{
/// <summary>
/// Near listener (それら).
/// </summary>
NearListener = 1,
/// <summary>
/// Distant from both speaker and listener (あれら).
/// </summary>
Distant = 2,
}

View file

@ -0,0 +1,73 @@
using Dalamud.Game.Text.Noun.Enums;
using Lumina.Text.ReadOnly;
using LSheets = Lumina.Excel.Sheets;
namespace Dalamud.Game.Text.Noun;
/// <summary>
/// Parameters for noun processing.
/// </summary>
internal record struct NounParams()
{
/// <summary>
/// The language of the sheet to be processed.
/// </summary>
public required ClientLanguage Language;
/// <summary>
/// The name of the sheet containing the row to process.
/// </summary>
public required string SheetName = string.Empty;
/// <summary>
/// The row id within the sheet to process.
/// </summary>
public required uint RowId;
/// <summary>
/// The quantity of the entity (default is 1). Used to determine grammatical number (e.g., singular or plural).
/// </summary>
public int Quantity = 1;
/// <summary>
/// The article type.
/// </summary>
/// <remarks>
/// Depending on the <see cref="Language"/>, this has different meanings.<br/>
/// See <see cref="JapaneseArticleType"/>, <see cref="GermanArticleType"/>, <see cref="FrenchArticleType"/>, <see cref="EnglishArticleType"/>.
/// </remarks>
public int ArticleType = 1;
/// <summary>
/// The grammatical case (e.g., Nominative, Genitive, Dative, Accusative) used for German texts (default is 0).
/// </summary>
public int GrammaticalCase = 0;
/// <summary>
/// An optional string that is placed in front of the text that should be linked, such as item names (default is an empty string; the game uses "//").
/// </summary>
public ReadOnlySeString LinkMarker = default;
/// <summary>
/// An indicator that this noun will be processed from an Action sheet. Only used for German texts.
/// </summary>
public bool IsActionSheet;
/// <summary>
/// Gets the column offset.
/// </summary>
public readonly int ColumnOffset => this.SheetName switch
{
// See "E8 ?? ?? ?? ?? 44 8B 6B 08"
nameof(LSheets.BeastTribe) => 10,
nameof(LSheets.DeepDungeonItem) => 1,
nameof(LSheets.DeepDungeonEquipment) => 1,
nameof(LSheets.DeepDungeonMagicStone) => 1,
nameof(LSheets.DeepDungeonDemiclone) => 1,
nameof(LSheets.Glasses) => 4,
nameof(LSheets.GlassesStyle) => 15,
_ => 0,
};
}

View file

@ -0,0 +1,461 @@
using System.Collections.Concurrent;
using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game.Text.Noun.Enums;
using Dalamud.Logging.Internal;
using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Text.ReadOnly;
using LSeStringBuilder = Lumina.Text.SeStringBuilder;
using LSheets = Lumina.Excel.Sheets;
namespace Dalamud.Game.Text.Noun;
/*
Attributive sheet:
Japanese:
Unknown0 = Singular Demonstrative
Unknown1 = Plural Demonstrative
English:
Unknown2 = Article before a singular noun beginning with a consonant sound
Unknown3 = Article before a generic noun beginning with a consonant sound
Unknown4 = N/A
Unknown5 = Article before a singular noun beginning with a vowel sound
Unknown6 = Article before a generic noun beginning with a vowel sound
Unknown7 = N/A
German:
Unknown8 = Nominative Masculine
Unknown9 = Nominative Feminine
Unknown10 = Nominative Neutral
Unknown11 = Nominative Plural
Unknown12 = Genitive Masculine
Unknown13 = Genitive Feminine
Unknown14 = Genitive Neutral
Unknown15 = Genitive Plural
Unknown16 = Dative Masculine
Unknown17 = Dative Feminine
Unknown18 = Dative Neutral
Unknown19 = Dative Plural
Unknown20 = Accusative Masculine
Unknown21 = Accusative Feminine
Unknown22 = Accusative Neutral
Unknown23 = Accusative Plural
French (unsure):
Unknown24 = Singular Article
Unknown25 = Singular Masculine Article
Unknown26 = Plural Masculine Article
Unknown27 = ?
Unknown28 = ?
Unknown29 = Singular Masculine/Feminine Article, before a noun beginning in a vowel or an h
Unknown30 = Plural Masculine/Feminine Article, before a noun beginning in a vowel or an h
Unknown31 = ?
Unknown32 = ?
Unknown33 = Singular Feminine Article
Unknown34 = Plural Feminine Article
Unknown35 = ?
Unknown36 = ?
Unknown37 = Singular Masculine/Feminine Article, before a noun beginning in a vowel or an h
Unknown38 = Plural Masculine/Feminine Article, before a noun beginning in a vowel or an h
Unknown39 = ?
Unknown40 = ?
Placeholders:
[t] = article or grammatical gender (EN: the, DE: der, die, das)
[n] = amount (number)
[a] = declension
[p] = plural
[pa] = ?
*/
/// <summary>
/// Provides functionality to process texts from sheets containing grammatical placeholders.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal class NounProcessor : IServiceType
{
// column names from ExdSchema, most likely incorrect
private const int SingularColumnIdx = 0;
private const int AdjectiveColumnIdx = 1;
private const int PluralColumnIdx = 2;
private const int PossessivePronounColumnIdx = 3;
private const int StartsWithVowelColumnIdx = 4;
private const int Unknown5ColumnIdx = 5; // probably used in Chinese texts
private const int PronounColumnIdx = 6;
private const int ArticleColumnIdx = 7;
private static readonly ModuleLog Log = new("NounProcessor");
[ServiceManager.ServiceDependency]
private readonly DataManager dataManager = Service<DataManager>.Get();
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration dalamudConfiguration = Service<DalamudConfiguration>.Get();
private readonly ConcurrentDictionary<NounParams, ReadOnlySeString> cache = [];
[ServiceManager.ServiceConstructor]
private NounProcessor()
{
}
/// <summary>
/// Processes a specific row from a sheet and generates a formatted string based on grammatical and language-specific rules.
/// </summary>
/// <param name="nounParams">Parameters for processing.</param>
/// <returns>A ReadOnlySeString representing the processed text.</returns>
public ReadOnlySeString ProcessNoun(NounParams nounParams)
{
if (nounParams.GrammaticalCase < 0 || nounParams.GrammaticalCase > 5)
return default;
if (this.cache.TryGetValue(nounParams, out var value))
return value;
var output = nounParams.Language switch
{
ClientLanguage.Japanese => this.ResolveNounJa(nounParams),
ClientLanguage.English => this.ResolveNounEn(nounParams),
ClientLanguage.German => this.ResolveNounDe(nounParams),
ClientLanguage.French => this.ResolveNounFr(nounParams),
_ => default,
};
this.cache.TryAdd(nounParams, output);
return output;
}
/// <summary>
/// Resolves noun placeholders in Japanese text.
/// </summary>
/// <param name="nounParams">Parameters for processing.</param>
/// <returns>A ReadOnlySeString representing the processed text.</returns>
/// <remarks>
/// This is a C# implementation of Component::Text::Localize::NounJa.Resolve.
/// </remarks>
private ReadOnlySeString ResolveNounJa(NounParams nounParams)
{
var sheet = this.dataManager.Excel.GetSheet<RawRow>(nounParams.Language.ToLumina(), nounParams.SheetName);
if (!sheet.TryGetRow(nounParams.RowId, out var row))
{
Log.Warning("Sheet {SheetName} does not contain row #{RowId}", nounParams.SheetName, nounParams.RowId);
return default;
}
var attributiveSheet = this.dataManager.Excel.GetSheet<RawRow>(nounParams.Language.ToLumina(), nameof(LSheets.Attributive));
var builder = LSeStringBuilder.SharedPool.Get();
// Ko-So-A-Do
var ksad = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(nounParams.Quantity > 1 ? 1 : 0);
if (!ksad.IsEmpty)
{
builder.Append(ksad);
if (nounParams.Quantity > 1)
{
builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
}
}
if (!nounParams.LinkMarker.IsEmpty)
builder.Append(nounParams.LinkMarker);
var text = row.ReadStringColumn(nounParams.ColumnOffset);
if (!text.IsEmpty)
builder.Append(text);
var ross = builder.ToReadOnlySeString();
LSeStringBuilder.SharedPool.Return(builder);
return ross;
}
/// <summary>
/// Resolves noun placeholders in English text.
/// </summary>
/// <param name="nounParams">Parameters for processing.</param>
/// <returns>A ReadOnlySeString representing the processed text.</returns>
/// <remarks>
/// This is a C# implementation of Component::Text::Localize::NounEn.Resolve.
/// </remarks>
private ReadOnlySeString ResolveNounEn(NounParams nounParams)
{
/*
a1->Offsets[0] = SingularColumnIdx
a1->Offsets[1] = PluralColumnIdx
a1->Offsets[2] = StartsWithVowelColumnIdx
a1->Offsets[3] = PossessivePronounColumnIdx
a1->Offsets[4] = ArticleColumnIdx
*/
var sheet = this.dataManager.Excel.GetSheet<RawRow>(nounParams.Language.ToLumina(), nounParams.SheetName);
if (!sheet.TryGetRow(nounParams.RowId, out var row))
{
Log.Warning("Sheet {SheetName} does not contain row #{RowId}", nounParams.SheetName, nounParams.RowId);
return default;
}
var attributiveSheet = this.dataManager.Excel.GetSheet<RawRow>(nounParams.Language.ToLumina(), nameof(LSheets.Attributive));
var builder = LSeStringBuilder.SharedPool.Get();
var isProperNounColumn = nounParams.ColumnOffset + ArticleColumnIdx;
var isProperNoun = isProperNounColumn >= 0 ? row.ReadInt8Column(isProperNounColumn) : ~isProperNounColumn;
if (isProperNoun == 0)
{
var startsWithVowelColumn = nounParams.ColumnOffset + StartsWithVowelColumnIdx;
var startsWithVowel = startsWithVowelColumn >= 0
? row.ReadInt8Column(startsWithVowelColumn)
: ~startsWithVowelColumn;
var articleColumn = startsWithVowel + (2 * (startsWithVowel + 1));
var grammaticalNumberColumnOffset = nounParams.Quantity == 1 ? SingularColumnIdx : PluralColumnIdx;
var article = attributiveSheet.GetRow((uint)nounParams.ArticleType)
.ReadStringColumn(articleColumn + grammaticalNumberColumnOffset);
if (!article.IsEmpty)
builder.Append(article);
if (!nounParams.LinkMarker.IsEmpty)
builder.Append(nounParams.LinkMarker);
}
var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity == 1 ? SingularColumnIdx : PluralColumnIdx));
if (!text.IsEmpty)
builder.Append(text);
builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
var ross = builder.ToReadOnlySeString();
LSeStringBuilder.SharedPool.Return(builder);
return ross;
}
/// <summary>
/// Resolves noun placeholders in German text.
/// </summary>
/// <param name="nounParams">Parameters for processing.</param>
/// <returns>A ReadOnlySeString representing the processed text.</returns>
/// <remarks>
/// This is a C# implementation of Component::Text::Localize::NounDe.Resolve.
/// </remarks>
private ReadOnlySeString ResolveNounDe(NounParams nounParams)
{
/*
a1->Offsets[0] = SingularColumnIdx
a1->Offsets[1] = PluralColumnIdx
a1->Offsets[2] = PronounColumnIdx
a1->Offsets[3] = AdjectiveColumnIdx
a1->Offsets[4] = PossessivePronounColumnIdx
a1->Offsets[5] = Unknown5ColumnIdx
a1->Offsets[6] = ArticleColumnIdx
*/
var sheet = this.dataManager.Excel.GetSheet<RawRow>(nounParams.Language.ToLumina(), nounParams.SheetName);
if (!sheet.TryGetRow(nounParams.RowId, out var row))
{
Log.Warning("Sheet {SheetName} does not contain row #{RowId}", nounParams.SheetName, nounParams.RowId);
return default;
}
var attributiveSheet = this.dataManager.Excel.GetSheet<RawRow>(nounParams.Language.ToLumina(), nameof(LSheets.Attributive));
var builder = LSeStringBuilder.SharedPool.Get();
ReadOnlySeString ross;
if (nounParams.IsActionSheet)
{
builder.Append(row.ReadStringColumn(nounParams.GrammaticalCase));
builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
ross = builder.ToReadOnlySeString();
LSeStringBuilder.SharedPool.Return(builder);
return ross;
}
var genderIndexColumn = nounParams.ColumnOffset + PronounColumnIdx;
var genderIndex = genderIndexColumn >= 0 ? row.ReadInt8Column(genderIndexColumn) : ~genderIndexColumn;
var articleIndexColumn = nounParams.ColumnOffset + ArticleColumnIdx;
var articleIndex = articleIndexColumn >= 0 ? row.ReadInt8Column(articleIndexColumn) : ~articleIndexColumn;
var caseColumnOffset = (4 * nounParams.GrammaticalCase) + 8;
var caseRowOffsetColumn = nounParams.ColumnOffset + (nounParams.Quantity == 1 ? AdjectiveColumnIdx : PossessivePronounColumnIdx);
var caseRowOffset = caseRowOffsetColumn >= 0
? row.ReadInt8Column(caseRowOffsetColumn)
: (sbyte)~caseRowOffsetColumn;
if (nounParams.Quantity != 1)
genderIndex = 3;
var hasT = false;
var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity == 1 ? SingularColumnIdx : PluralColumnIdx));
if (!text.IsEmpty)
{
hasT = text.ContainsText("[t]"u8);
if (articleIndex == 0 && !hasT)
{
var grammaticalGender = attributiveSheet.GetRow((uint)nounParams.ArticleType)
.ReadStringColumn(caseColumnOffset + genderIndex); // Genus
if (!grammaticalGender.IsEmpty)
builder.Append(grammaticalGender);
}
if (!nounParams.LinkMarker.IsEmpty)
builder.Append(nounParams.LinkMarker);
builder.Append(text);
var plural = attributiveSheet.GetRow((uint)(caseRowOffset + 26))
.ReadStringColumn(caseColumnOffset + genderIndex);
if (builder.ContainsText("[p]"u8))
builder.ReplaceText("[p]"u8, plural);
else
builder.Append(plural);
if (hasT)
{
var article =
attributiveSheet.GetRow(39).ReadStringColumn(caseColumnOffset + genderIndex); // Definiter Artikel
builder.ReplaceText("[t]"u8, article);
}
}
var pa = attributiveSheet.GetRow(24).ReadStringColumn(caseColumnOffset + genderIndex);
builder.ReplaceText("[pa]"u8, pa);
RawRow declensionRow;
declensionRow = (GermanArticleType)nounParams.ArticleType switch
{
// Schwache Flexion eines Adjektivs?!
GermanArticleType.Possessive or GermanArticleType.Demonstrative => attributiveSheet.GetRow(25),
_ when hasT => attributiveSheet.GetRow(25),
// Starke Deklination
GermanArticleType.ZeroArticle => attributiveSheet.GetRow(38),
// Gemischte Deklination
GermanArticleType.Definite => attributiveSheet.GetRow(37),
// Starke Flexion eines Artikels?!
GermanArticleType.Indefinite or GermanArticleType.Negative => attributiveSheet.GetRow(26),
_ => attributiveSheet.GetRow(26),
};
var declension = declensionRow.ReadStringColumn(caseColumnOffset + genderIndex);
builder.ReplaceText("[a]"u8, declension);
builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
ross = builder.ToReadOnlySeString();
LSeStringBuilder.SharedPool.Return(builder);
return ross;
}
/// <summary>
/// Resolves noun placeholders in French text.
/// </summary>
/// <param name="nounParams">Parameters for processing.</param>
/// <returns>A ReadOnlySeString representing the processed text.</returns>
/// <remarks>
/// This is a C# implementation of Component::Text::Localize::NounFr.Resolve.
/// </remarks>
private ReadOnlySeString ResolveNounFr(NounParams nounParams)
{
/*
a1->Offsets[0] = SingularColumnIdx
a1->Offsets[1] = PluralColumnIdx
a1->Offsets[2] = StartsWithVowelColumnIdx
a1->Offsets[3] = PronounColumnIdx
a1->Offsets[4] = Unknown5ColumnIdx
a1->Offsets[5] = ArticleColumnIdx
*/
var sheet = this.dataManager.Excel.GetSheet<RawRow>(nounParams.Language.ToLumina(), nounParams.SheetName);
if (!sheet.TryGetRow(nounParams.RowId, out var row))
{
Log.Warning("Sheet {SheetName} does not contain row #{RowId}", nounParams.SheetName, nounParams.RowId);
return default;
}
var attributiveSheet = this.dataManager.Excel.GetSheet<RawRow>(nounParams.Language.ToLumina(), nameof(LSheets.Attributive));
var builder = LSeStringBuilder.SharedPool.Get();
ReadOnlySeString ross;
var startsWithVowelColumn = nounParams.ColumnOffset + StartsWithVowelColumnIdx;
var startsWithVowel = startsWithVowelColumn >= 0
? row.ReadInt8Column(startsWithVowelColumn)
: ~startsWithVowelColumn;
var pronounColumn = nounParams.ColumnOffset + PronounColumnIdx;
var pronoun = pronounColumn >= 0 ? row.ReadInt8Column(pronounColumn) : ~pronounColumn;
var articleColumn = nounParams.ColumnOffset + ArticleColumnIdx;
var article = articleColumn >= 0 ? row.ReadInt8Column(articleColumn) : ~articleColumn;
var v20 = 4 * (startsWithVowel + 6 + (2 * pronoun));
if (article != 0)
{
var v21 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20);
if (!v21.IsEmpty)
builder.Append(v21);
if (!nounParams.LinkMarker.IsEmpty)
builder.Append(nounParams.LinkMarker);
var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity <= 1 ? SingularColumnIdx : PluralColumnIdx));
if (!text.IsEmpty)
builder.Append(text);
if (nounParams.Quantity <= 1)
builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
ross = builder.ToReadOnlySeString();
LSeStringBuilder.SharedPool.Return(builder);
return ross;
}
var v17 = row.ReadInt8Column(nounParams.ColumnOffset + Unknown5ColumnIdx);
if (v17 != 0 && (nounParams.Quantity > 1 || v17 == 2))
{
var v29 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20 + 2);
if (!v29.IsEmpty)
{
builder.Append(v29);
if (!nounParams.LinkMarker.IsEmpty)
builder.Append(nounParams.LinkMarker);
var text = row.ReadStringColumn(nounParams.ColumnOffset + PluralColumnIdx);
if (!text.IsEmpty)
builder.Append(text);
}
}
else
{
var v27 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20 + (v17 != 0 ? 1 : 3));
if (!v27.IsEmpty)
builder.Append(v27);
if (!nounParams.LinkMarker.IsEmpty)
builder.Append(nounParams.LinkMarker);
var text = row.ReadStringColumn(nounParams.ColumnOffset + SingularColumnIdx);
if (!text.IsEmpty)
builder.Append(text);
}
builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString()));
ross = builder.ToReadOnlySeString();
LSeStringBuilder.SharedPool.Return(builder);
return ross;
}
}

View file

@ -201,7 +201,7 @@ public abstract partial class Payload
case SeStringChunkType.Icon:
payload = new IconPayload();
break;
default:
// Log.Verbose("Unhandled SeStringChunkType: {0}", chunkType);
break;
@ -306,7 +306,7 @@ public abstract partial class Payload
/// See the <see cref="NewLinePayload"/>.
/// </summary>
NewLine = 0x10,
/// <summary>
/// See the <see cref="IconPayload"/> class.
/// </summary>

View file

@ -4,6 +4,8 @@ using System.Linq;
using System.Text;
using Dalamud.Data;
using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
@ -73,6 +75,7 @@ public class ItemPayload : Payload
/// <summary>
/// Kinds of items that can be fetched from this payload.
/// </summary>
[Api12ToDo("Move this out of ItemPayload. It's used in other classes too.")]
public enum ItemKind : uint
{
/// <summary>
@ -121,7 +124,7 @@ public class ItemPayload : Payload
/// Gets the actual item ID of this payload.
/// </summary>
[JsonIgnore]
public uint ItemId => GetAdjustedId(this.rawItemId).ItemId;
public uint ItemId => ItemUtil.GetBaseId(this.rawItemId).ItemId;
/// <summary>
/// Gets the raw, unadjusted item ID of this payload.
@ -161,7 +164,7 @@ public class ItemPayload : Payload
/// <returns>The created item payload.</returns>
public static ItemPayload FromRaw(uint rawItemId, string? displayNameOverride = null)
{
var (id, kind) = GetAdjustedId(rawItemId);
var (id, kind) = ItemUtil.GetBaseId(rawItemId);
return new ItemPayload(id, kind, displayNameOverride);
}
@ -230,7 +233,7 @@ public class ItemPayload : Payload
protected override void DecodeImpl(BinaryReader reader, long endOfStream)
{
this.rawItemId = GetInteger(reader);
this.Kind = GetAdjustedId(this.rawItemId).Kind;
this.Kind = ItemUtil.GetBaseId(this.rawItemId).Kind;
if (reader.BaseStream.Position + 3 < endOfStream)
{
@ -255,15 +258,4 @@ public class ItemPayload : Payload
this.displayName = Encoding.UTF8.GetString(itemNameBytes);
}
}
private static (uint ItemId, ItemKind Kind) GetAdjustedId(uint rawItemId)
{
return rawItemId switch
{
> 500_000 and < 1_000_000 => (rawItemId - 500_000, ItemKind.Collectible),
> 1_000_000 and < 2_000_000 => (rawItemId - 1_000_000, ItemKind.Hq),
> 2_000_000 => (rawItemId, ItemKind.EventItem), // EventItem IDs are NOT adjusted
_ => (rawItemId, ItemKind.Normal),
};
}
}

View file

@ -10,7 +10,8 @@ using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// An SeString Payload representing a UI foreground color applied to following text payloads.
/// An SeString Payload that allows text to have a specific color. The color selected will be determined by the
/// <see cref="Lumina.Excel.Sheets.UIColor.Dark"/> theme's coloring, regardless of the active theme.
/// </summary>
public class UIForegroundPayload : Payload
{
@ -74,13 +75,13 @@ public class UIForegroundPayload : Payload
/// Gets the Red/Green/Blue/Alpha values for this foreground color, encoded as a typical hex color.
/// </summary>
[JsonIgnore]
public uint RGBA => this.UIColor.Value.UIForeground;
public uint RGBA => this.UIColor.Value.Dark;
/// <summary>
/// Gets the ABGR value for this foreground color, as ImGui requires it in PushColor.
/// </summary>
[JsonIgnore]
public uint ABGR => Interface.ColorHelpers.SwapEndianness(this.UIColor.Value.UIForeground);
public uint ABGR => Interface.ColorHelpers.SwapEndianness(this.UIColor.Value.Dark);
/// <inheritdoc/>
public override string ToString()

View file

@ -10,7 +10,8 @@ using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// <summary>
/// An SeString Payload representing a UI glow color applied to following text payloads.
/// An SeString Payload that allows text to have a specific edge glow. The color selected will be determined by the
/// <see cref="Lumina.Excel.Sheets.UIColor.Light"/> theme's coloring, regardless of the active theme.
/// </summary>
public class UIGlowPayload : Payload
{
@ -71,13 +72,13 @@ public class UIGlowPayload : Payload
/// Gets the Red/Green/Blue/Alpha values for this glow color, encoded as a typical hex color.
/// </summary>
[JsonIgnore]
public uint RGBA => this.UIColor.Value.UIGlow;
public uint RGBA => this.UIColor.Value.Light;
/// <summary>
/// Gets the ABGR value for this glow color, as ImGui requires it in PushColor.
/// </summary>
[JsonIgnore]
public uint ABGR => Interface.ColorHelpers.SwapEndianness(this.UIColor.Value.UIGlow);
public uint ABGR => Interface.ColorHelpers.SwapEndianness(this.UIColor.Value.Light);
/// <summary>
/// Gets a Lumina UIColor object representing this payload. The actual color data is at UIColor.UIGlow.

View file

@ -5,11 +5,16 @@ using System.Runtime.InteropServices;
using System.Text;
using Dalamud.Data;
using Dalamud.Game.Text.Evaluator;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Utility;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
using LSeStringBuilder = Lumina.Text.SeStringBuilder;
namespace Dalamud.Game.Text.SeStringHandling;
/// <summary>
@ -187,57 +192,32 @@ public class SeString
/// <returns>An SeString containing all the payloads necessary to display an item link in the chat log.</returns>
public static SeString CreateItemLink(uint itemId, ItemPayload.ItemKind kind = ItemPayload.ItemKind.Normal, string? displayNameOverride = null)
{
var data = Service<DataManager>.Get();
var clientState = Service<ClientState.ClientState>.Get();
var seStringEvaluator = Service<SeStringEvaluator>.Get();
var displayName = displayNameOverride;
var rarity = 1; // default: white
if (displayName == null)
{
switch (kind)
{
case ItemPayload.ItemKind.Normal:
case ItemPayload.ItemKind.Collectible:
case ItemPayload.ItemKind.Hq:
var item = data.GetExcelSheet<Item>()?.GetRowOrDefault(itemId);
displayName = item?.Name.ExtractText();
rarity = item?.Rarity ?? 1;
break;
case ItemPayload.ItemKind.EventItem:
displayName = data.GetExcelSheet<EventItem>()?.GetRowOrDefault(itemId)?.Name.ExtractText();
break;
default:
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
}
}
var rawId = ItemUtil.GetRawId(itemId, kind);
if (displayName == null)
{
var displayName = displayNameOverride ?? ItemUtil.GetItemName(rawId);
if (displayName.IsEmpty)
throw new Exception("Invalid item ID specified, could not determine item name.");
}
if (kind == ItemPayload.ItemKind.Hq)
{
displayName += $" {(char)SeIconChar.HighQuality}";
}
else if (kind == ItemPayload.ItemKind.Collectible)
{
displayName += $" {(char)SeIconChar.Collectible}";
}
var copyName = ItemUtil.GetItemName(rawId, false).ExtractText();
var textColor = ItemUtil.GetItemRarityColorType(rawId);
var textEdgeColor = textColor + 1u;
var textColor = (ushort)(549 + ((rarity - 1) * 2));
var textGlowColor = (ushort)(textColor + 1);
var sb = LSeStringBuilder.SharedPool.Get();
var itemLink = sb
.PushColorType(textColor)
.PushEdgeColorType(textEdgeColor)
.PushLinkItem(rawId, copyName)
.Append(displayName)
.PopLink()
.PopEdgeColorType()
.PopColorType()
.ToReadOnlySeString();
LSeStringBuilder.SharedPool.Return(sb);
// Note: `SeStringBuilder.AddItemLink` uses this function, so don't call it here!
return new SeStringBuilder()
.AddUiForeground(textColor)
.AddUiGlow(textGlowColor)
.Add(new ItemPayload(itemId, kind))
.Append(TextArrowPayloads)
.AddText(displayName)
.AddUiGlowOff()
.AddUiForegroundOff()
.Add(RawPayload.LinkTerminator)
.Build();
return SeString.Parse(seStringEvaluator.EvaluateFromAddon(371, [itemLink], clientState.ClientLanguage));
}
/// <summary>
@ -301,7 +281,7 @@ public class SeString
public static SeString CreateMapLink(
uint territoryId, uint mapId, float xCoord, float yCoord, float fudgeFactor = 0.05f) =>
CreateMapLinkWithInstance(territoryId, mapId, null, xCoord, yCoord, fudgeFactor);
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log.
/// </summary>
@ -340,7 +320,7 @@ public class SeString
/// <returns>An SeString containing all of the payloads necessary to display a map link in the chat log.</returns>
public static SeString? CreateMapLink(string placeName, float xCoord, float yCoord, float fudgeFactor = 0.05f) =>
CreateMapLinkWithInstance(placeName, null, xCoord, yCoord, fudgeFactor);
/// <summary>
/// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log, matching a specified zone name.
/// Returns null if no corresponding PlaceName was found.
@ -511,7 +491,7 @@ public class SeString
{
messageBytes.AddRange(p.Encode());
}
// Add Null Terminator
messageBytes.Add(0);
@ -526,7 +506,7 @@ public class SeString
{
return this.TextValue;
}
private static string GetMapLinkNameString(string placeName, int? instance, string coordinateString)
{
var instanceString = string.Empty;
@ -534,7 +514,7 @@ public class SeString
{
instanceString = (SeIconChar.Instance1 + instance.Value - 1).ToIconString();
}
return $"{placeName}{instanceString} {coordinateString}";
}
}

View file

@ -8,7 +8,6 @@ namespace Dalamud.Interface.Animation;
/// <summary>
/// Base class facilitating the implementation of easing functions.
/// </summary>
[Api12ToDo("Re-apply https://github.com/goatcorp/Dalamud/commit/1aada983931d9e45a250eebbc17c8b782d07701b")]
public abstract class Easing
{
// TODO: Use game delta time here instead
@ -46,9 +45,22 @@ public abstract class Easing
public bool IsInverse { get; set; }
/// <summary>
/// Gets or sets the current value of the animation, from 0 to 1.
/// Gets the current value of the animation, following unclamped logic.
/// </summary>
public double Value
[Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)]
[Api13ToDo("Map this field to ValueClamped, probably.")]
public double Value => this.ValueUnclamped;
/// <summary>
/// Gets the current value of the animation, from 0 to 1.
/// </summary>
public double ValueClamped => Math.Clamp(this.ValueUnclamped, 0, 1);
/// <summary>
/// Gets or sets the current value of the animation, not limited to a range of 0 to 1.
/// Will return numbers outside of this range if accessed beyond animation time.
/// </summary>
public double ValueUnclamped
{
get
{

View file

@ -19,6 +19,6 @@ public class InCirc : Easing
public override void Update()
{
var p = this.Progress;
this.Value = 1 - Math.Sqrt(1 - Math.Pow(p, 2));
this.ValueUnclamped = 1 - Math.Sqrt(1 - Math.Pow(p, 2));
}
}

View file

@ -19,6 +19,6 @@ public class InCubic : Easing
public override void Update()
{
var p = this.Progress;
this.Value = p * p * p;
this.ValueUnclamped = p * p * p;
}
}

View file

@ -21,10 +21,10 @@ public class InElastic : Easing
public override void Update()
{
var p = this.Progress;
this.Value = p == 0
? 0
: p == 1
? 1
: -Math.Pow(2, (10 * p) - 10) * Math.Sin(((p * 10) - 10.75) * Constant);
this.ValueUnclamped = p == 0
? 0
: p == 1
? 1
: -Math.Pow(2, (10 * p) - 10) * Math.Sin(((p * 10) - 10.75) * Constant);
}
}

View file

@ -19,8 +19,8 @@ public class InOutCirc : Easing
public override void Update()
{
var p = this.Progress;
this.Value = p < 0.5
? (1 - Math.Sqrt(1 - Math.Pow(2 * p, 2))) / 2
: (Math.Sqrt(1 - Math.Pow((-2 * p) + 2, 2)) + 1) / 2;
this.ValueUnclamped = p < 0.5
? (1 - Math.Sqrt(1 - Math.Pow(2 * p, 2))) / 2
: (Math.Sqrt(1 - Math.Pow((-2 * p) + 2, 2)) + 1) / 2;
}
}

View file

@ -19,6 +19,6 @@ public class InOutCubic : Easing
public override void Update()
{
var p = this.Progress;
this.Value = p < 0.5 ? 4 * p * p * p : 1 - (Math.Pow((-2 * p) + 2, 3) / 2);
this.ValueUnclamped = p < 0.5 ? 4 * p * p * p : 1 - (Math.Pow((-2 * p) + 2, 3) / 2);
}
}

View file

@ -21,12 +21,12 @@ public class InOutElastic : Easing
public override void Update()
{
var p = this.Progress;
this.Value = p == 0
? 0
: p == 1
? 1
: p < 0.5
? -(Math.Pow(2, (20 * p) - 10) * Math.Sin(((20 * p) - 11.125) * Constant)) / 2
: (Math.Pow(2, (-20 * p) + 10) * Math.Sin(((20 * p) - 11.125) * Constant) / 2) + 1;
this.ValueUnclamped = p == 0
? 0
: p == 1
? 1
: p < 0.5
? -(Math.Pow(2, (20 * p) - 10) * Math.Sin(((20 * p) - 11.125) * Constant)) / 2
: (Math.Pow(2, (-20 * p) + 10) * Math.Sin(((20 * p) - 11.125) * Constant) / 2) + 1;
}
}

View file

@ -19,6 +19,6 @@ public class InOutQuint : Easing
public override void Update()
{
var p = this.Progress;
this.Value = p < 0.5 ? 16 * p * p * p * p * p : 1 - (Math.Pow((-2 * p) + 2, 5) / 2);
this.ValueUnclamped = p < 0.5 ? 16 * p * p * p * p * p : 1 - (Math.Pow((-2 * p) + 2, 5) / 2);
}
}

View file

@ -19,6 +19,6 @@ public class InOutSine : Easing
public override void Update()
{
var p = this.Progress;
this.Value = -(Math.Cos(Math.PI * p) - 1) / 2;
this.ValueUnclamped = -(Math.Cos(Math.PI * p) - 1) / 2;
}
}

View file

@ -19,6 +19,6 @@ public class InQuint : Easing
public override void Update()
{
var p = this.Progress;
this.Value = p * p * p * p * p;
this.ValueUnclamped = p * p * p * p * p;
}
}

View file

@ -19,6 +19,6 @@ public class InSine : Easing
public override void Update()
{
var p = this.Progress;
this.Value = 1 - Math.Cos((p * Math.PI) / 2);
this.ValueUnclamped = 1 - Math.Cos((p * Math.PI) / 2);
}
}

View file

@ -19,6 +19,6 @@ public class OutCirc : Easing
public override void Update()
{
var p = this.Progress;
this.Value = Math.Sqrt(1 - Math.Pow(p - 1, 2));
this.ValueUnclamped = Math.Sqrt(1 - Math.Pow(p - 1, 2));
}
}

View file

@ -19,6 +19,6 @@ public class OutCubic : Easing
public override void Update()
{
var p = this.Progress;
this.Value = 1 - Math.Pow(1 - p, 3);
this.ValueUnclamped = 1 - Math.Pow(1 - p, 3);
}
}

View file

@ -21,10 +21,10 @@ public class OutElastic : Easing
public override void Update()
{
var p = this.Progress;
this.Value = p == 0
? 0
: p == 1
? 1
: (Math.Pow(2, -10 * p) * Math.Sin(((p * 10) - 0.75) * Constant)) + 1;
this.ValueUnclamped = p == 0
? 0
: p == 1
? 1
: (Math.Pow(2, -10 * p) * Math.Sin(((p * 10) - 0.75) * Constant)) + 1;
}
}

View file

@ -19,6 +19,6 @@ public class OutQuint : Easing
public override void Update()
{
var p = this.Progress;
this.Value = 1 - Math.Pow(1 - p, 5);
this.ValueUnclamped = 1 - Math.Pow(1 - p, 5);
}
}

View file

@ -19,6 +19,6 @@ public class OutSine : Easing
public override void Update()
{
var p = this.Progress;
this.Value = Math.Sin((p * Math.PI) / 2);
this.ValueUnclamped = Math.Sin((p * Math.PI) / 2);
}
}

View file

@ -21,8 +21,8 @@ internal sealed partial class ActiveNotification
var opacity =
Math.Clamp(
(float)(this.hideEasing.IsRunning
? (this.hideEasing.IsDone || ReducedMotions ? 0 : 1f - this.hideEasing.Value)
: (this.showEasing.IsDone || ReducedMotions ? 1 : this.showEasing.Value)),
? (this.hideEasing.IsDone || ReducedMotions ? 0 : 1f - this.hideEasing.ValueClamped)
: (this.showEasing.IsDone || ReducedMotions ? 1 : this.showEasing.ValueClamped)),
0f,
1f);
if (opacity <= 0)
@ -106,7 +106,7 @@ internal sealed partial class ActiveNotification
}
else if (this.expandoEasing.IsRunning)
{
var easedValue = ReducedMotions ? 1f : (float)this.expandoEasing.Value;
var easedValue = ReducedMotions ? 1f : (float)this.expandoEasing.ValueClamped;
if (this.underlyingNotification.Minimized)
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - easedValue));
else
@ -295,8 +295,8 @@ internal sealed partial class ActiveNotification
{
relativeOpacity =
this.underlyingNotification.Minimized
? 1f - (float)this.expandoEasing.Value
: (float)this.expandoEasing.Value;
? 1f - (float)this.expandoEasing.ValueClamped
: (float)this.expandoEasing.ValueClamped;
}
else
{
@ -543,7 +543,7 @@ internal sealed partial class ActiveNotification
float barL, barR;
if (this.DismissReason is not null)
{
var v = this.hideEasing.IsDone || ReducedMotions ? 0f : 1f - (float)this.hideEasing.Value;
var v = this.hideEasing.IsDone || ReducedMotions ? 0f : 1f - (float)this.hideEasing.ValueClamped;
var midpoint = (this.prevProgressL + this.prevProgressR) / 2f;
var length = (this.prevProgressR - this.prevProgressL) / 2f;
barL = midpoint - (length * v);

View file

@ -200,7 +200,7 @@ internal sealed partial class ActiveNotification : IActiveNotification
if (Math.Abs(underlyingProgress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone || ReducedMotions)
return underlyingProgress;
var state = ReducedMotions ? 1f : Math.Clamp((float)this.progressEasing.Value, 0f, 1f);
var state = ReducedMotions ? 1f : Math.Clamp((float)this.progressEasing.ValueClamped, 0f, 1f);
return this.progressBefore + (state * (underlyingProgress - this.progressBefore));
}
}

View file

@ -43,10 +43,10 @@ internal sealed class SeStringColorStackSet
foreach (var row in uiColor)
{
// Contains ABGR.
this.colorTypes[row.RowId, 0] = row.UIForeground;
this.colorTypes[row.RowId, 1] = row.UIGlow;
this.colorTypes[row.RowId, 2] = row.Unknown0;
this.colorTypes[row.RowId, 3] = row.Unknown1;
this.colorTypes[row.RowId, 0] = row.Dark;
this.colorTypes[row.RowId, 1] = row.Light;
this.colorTypes[row.RowId, 2] = row.ClassicFF;
this.colorTypes[row.RowId, 3] = row.ClearBlue;
}
if (BitConverter.IsLittleEndian)

View file

@ -7,7 +7,6 @@ using BitFaster.Caching.Lru;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.Config;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal.TextProcessing;
using Dalamud.Interface.Utility;
@ -44,9 +43,6 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// of this placeholder. On its own, usually displayed like <c>[OBJ]</c>.</summary>
private const int ObjectReplacementCharacter = '\uFFFC';
[ServiceManager.ServiceDependency]
private readonly GameConfig gameConfig = Service<GameConfig>.Get();
/// <summary>Cache of compiled SeStrings from <see cref="CompileAndCache"/>.</summary>
private readonly ConcurrentLru<string, ReadOnlySeString> cache = new(1024);
@ -570,70 +566,16 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
// Apply gamepad key mapping to icons.
case MacroCode.Icon2
when payload.TryGetExpression(out var icon) && icon.TryGetInt(out var iconId):
var configName = (BitmapFontIcon)iconId switch
ref var iconMapping = ref RaptureAtkModule.Instance()->AtkFontManager.Icon2RemapTable;
for (var i = 0; i < 30; i++)
{
ControllerShoulderLeft => SystemConfigOption.PadButton_L1,
ControllerShoulderRight => SystemConfigOption.PadButton_R1,
ControllerTriggerLeft => SystemConfigOption.PadButton_L2,
ControllerTriggerRight => SystemConfigOption.PadButton_R2,
ControllerButton3 => SystemConfigOption.PadButton_Triangle,
ControllerButton1 => SystemConfigOption.PadButton_Cross,
ControllerButton0 => SystemConfigOption.PadButton_Circle,
ControllerButton2 => SystemConfigOption.PadButton_Square,
ControllerStart => SystemConfigOption.PadButton_Start,
ControllerBack => SystemConfigOption.PadButton_Select,
ControllerAnalogLeftStick => SystemConfigOption.PadButton_LS,
ControllerAnalogLeftStickIn => SystemConfigOption.PadButton_LS,
ControllerAnalogLeftStickUpDown => SystemConfigOption.PadButton_LS,
ControllerAnalogLeftStickLeftRight => SystemConfigOption.PadButton_LS,
ControllerAnalogRightStick => SystemConfigOption.PadButton_RS,
ControllerAnalogRightStickIn => SystemConfigOption.PadButton_RS,
ControllerAnalogRightStickUpDown => SystemConfigOption.PadButton_RS,
ControllerAnalogRightStickLeftRight => SystemConfigOption.PadButton_RS,
_ => (SystemConfigOption?)null,
};
if (configName is null || !this.gameConfig.TryGet(configName.Value, out PadButtonValue pb))
return (BitmapFontIcon)iconId;
return pb switch
{
PadButtonValue.Autorun_Support => ControllerShoulderLeft,
PadButtonValue.Hotbar_Set_Change => ControllerShoulderRight,
PadButtonValue.XHB_Left_Start => ControllerTriggerLeft,
PadButtonValue.XHB_Right_Start => ControllerTriggerRight,
PadButtonValue.Jump => ControllerButton3,
PadButtonValue.Accept => ControllerButton0,
PadButtonValue.Cancel => ControllerButton1,
PadButtonValue.Map_Sub => ControllerButton2,
PadButtonValue.MainCommand => ControllerStart,
PadButtonValue.HUD_Select => ControllerBack,
PadButtonValue.Move_Operation => (BitmapFontIcon)iconId switch
if (iconMapping[i].IconId == iconId)
{
ControllerAnalogLeftStick => ControllerAnalogLeftStick,
ControllerAnalogLeftStickIn => ControllerAnalogLeftStickIn,
ControllerAnalogLeftStickUpDown => ControllerAnalogLeftStickUpDown,
ControllerAnalogLeftStickLeftRight => ControllerAnalogLeftStickLeftRight,
ControllerAnalogRightStick => ControllerAnalogLeftStick,
ControllerAnalogRightStickIn => ControllerAnalogLeftStickIn,
ControllerAnalogRightStickUpDown => ControllerAnalogLeftStickUpDown,
ControllerAnalogRightStickLeftRight => ControllerAnalogLeftStickLeftRight,
_ => (BitmapFontIcon)iconId,
},
PadButtonValue.Camera_Operation => (BitmapFontIcon)iconId switch
{
ControllerAnalogLeftStick => ControllerAnalogRightStick,
ControllerAnalogLeftStickIn => ControllerAnalogRightStickIn,
ControllerAnalogLeftStickUpDown => ControllerAnalogRightStickUpDown,
ControllerAnalogLeftStickLeftRight => ControllerAnalogRightStickLeftRight,
ControllerAnalogRightStick => ControllerAnalogRightStick,
ControllerAnalogRightStickIn => ControllerAnalogRightStickIn,
ControllerAnalogRightStickUpDown => ControllerAnalogRightStickUpDown,
ControllerAnalogRightStickLeftRight => ControllerAnalogRightStickLeftRight,
_ => (BitmapFontIcon)iconId,
},
_ => (BitmapFontIcon)iconId,
};
return (BitmapFontIcon)iconMapping[i].RemappedIconId;
}
}
return (BitmapFontIcon)iconId;
}
return None;

View file

@ -225,7 +225,7 @@ internal unsafe class UiDebug
ImGui.SameLine();
if (ImGui.Button($"Decode##{(ulong)textNode:X}"))
textNode->NodeText.SetString(new ReadOnlySeStringSpan(textNode->NodeText.StringPtr).ToString());
textNode->NodeText.SetString(textNode->NodeText.StringPtr.AsReadOnlySeStringSpan().ToString());
ImGui.Text($"AlignmentType: {(AlignmentType)textNode->AlignmentFontType} FontSize: {textNode->FontSize}");
int b = textNode->AlignmentFontType;
@ -418,27 +418,27 @@ internal unsafe class UiDebug
ImGui.Text("InputBase Text1: ");
ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->AtkComponentInputBase.UnkText1);
ImGui.Text("InputBase Text2: ");
ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->AtkComponentInputBase.UnkText2);
ImGui.Text("Text1: ");
ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText01);
ImGui.Text("Text2: ");
ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText02);
ImGui.Text("Text3: ");
ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText03);
ImGui.Text("Text4: ");
ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText04);
ImGui.Text("Text5: ");
ImGui.SameLine();
Service<SeStringRenderer>.Get().Draw(textInputComponent->UnkText05);

View file

@ -76,14 +76,13 @@ public unsafe partial class AddonTree
case ValueType.String8:
case ValueType.String:
{
if (atkValue->String == null)
if (atkValue->String.Value == null)
{
ImGui.TextDisabled("null");
}
else
{
var str = MemoryHelper.ReadSeStringNullTerminated(new nint(atkValue->String));
Util.ShowStruct(str, (ulong)atkValue);
Util.ShowStruct(atkValue->String.ToString(), (ulong)atkValue);
}
break;

View file

@ -89,7 +89,7 @@ internal unsafe partial class TextNodeTree : ResNodeTree
var seStringBytes = new byte[utf8String.BufUsed];
for (var i = 0L; i < utf8String.BufUsed; i++)
{
seStringBytes[i] = utf8String.StringPtr[i];
seStringBytes[i] = utf8String.StringPtr.Value[i];
}
var seString = SeString.Parse(seStringBytes);

View file

@ -147,7 +147,7 @@ internal sealed class ComponentDemoWindow : Window
ImGui.Bullet();
ImGui.SetCursorPos(cursor + new Vector2(0, 10));
ImGui.Text($"{easing.GetType().Name} ({easing.Value})");
ImGui.Text($"{easing.GetType().Name} ({easing.ValueClamped})");
ImGuiHelpers.ScaledDummy(5);
}
}

View file

@ -47,11 +47,13 @@ internal class DataWindow : Window, IDisposable
new KeyStateWidget(),
new MarketBoardWidget(),
new NetworkMonitorWidget(),
new NounProcessorWidget(),
new ObjectTableWidget(),
new PartyListWidget(),
new PluginIpcWidget(),
new SeFontTestWidget(),
new ServicesWidget(),
new SeStringCreatorWidget(),
new SeStringRendererTestWidget(),
new StartInfoWidget(),
new TargetWidget(),
@ -68,6 +70,7 @@ internal class DataWindow : Window, IDisposable
private bool isExcept;
private bool selectionCollapsed;
private IDataWindowWidget currentWidget;
private bool isLoaded;
/// <summary>
/// Initializes a new instance of the <see cref="DataWindow"/> class.
@ -81,8 +84,6 @@ internal class DataWindow : Window, IDisposable
this.RespectCloseHotkey = false;
this.orderedModules = this.modules.OrderBy(module => module.DisplayName);
this.currentWidget = this.orderedModules.First();
this.Load();
}
/// <inheritdoc/>
@ -91,6 +92,7 @@ internal class DataWindow : Window, IDisposable
/// <inheritdoc/>
public override void OnOpen()
{
this.Load();
}
/// <inheritdoc/>
@ -183,6 +185,7 @@ internal class DataWindow : Window, IDisposable
if (ImGuiComponents.IconButton("forceReload", FontAwesomeIcon.Sync))
{
this.isLoaded = false;
this.Load();
}
@ -236,6 +239,11 @@ internal class DataWindow : Window, IDisposable
private void Load()
{
if (this.isLoaded)
return;
this.isLoaded = true;
foreach (var widget in this.modules)
{
widget.Load();

View file

@ -0,0 +1,34 @@
using Dalamud.Interface.Utility;
using ImGuiNET;
namespace Dalamud.Interface.Internal.Windows.Data;
/// <summary>
/// Common utilities used in Widgets.
/// </summary>
internal class WidgetUtil
{
/// <summary>
/// Draws text that can be copied on click.
/// </summary>
/// <param name="text">The text shown and to be copied.</param>
/// <param name="tooltipText">The text in the tooltip.</param>
internal static void DrawCopyableText(string text, string tooltipText = "Copy")
{
ImGuiHelpers.SafeTextWrapped(text);
if (ImGui.IsItemHovered())
{
ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
ImGui.BeginTooltip();
ImGui.TextUnformatted(tooltipText);
ImGui.EndTooltip();
}
if (ImGui.IsItemClicked())
{
ImGui.SetClipboardText(text);
}
}
}

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