Revert "refactor(Dalamud): switch to file-scoped namespaces"

This reverts commit b5f34c3199.
This commit is contained in:
goat 2021-11-18 15:23:40 +01:00
parent d473826247
commit 1561fbac00
No known key found for this signature in database
GPG key ID: 7773BB5B43BA52E5
325 changed files with 45549 additions and 45209 deletions

View file

@ -1,27 +1,28 @@
namespace Dalamud; namespace Dalamud
/// <summary>
/// Enum describing the language the game loads in.
/// </summary>
public enum ClientLanguage
{ {
/// <summary> /// <summary>
/// Indicating a Japanese game client. /// Enum describing the language the game loads in.
/// </summary> /// </summary>
Japanese, public enum ClientLanguage
{
/// <summary>
/// Indicating a Japanese game client.
/// </summary>
Japanese,
/// <summary> /// <summary>
/// Indicating an English game client. /// Indicating an English game client.
/// </summary> /// </summary>
English, English,
/// <summary> /// <summary>
/// Indicating a German game client. /// Indicating a German game client.
/// </summary> /// </summary>
German, German,
/// <summary> /// <summary>
/// Indicating a French game client. /// Indicating a French game client.
/// </summary> /// </summary>
French, French,
}
} }

View file

@ -1,26 +1,27 @@
using System; using System;
namespace Dalamud; namespace Dalamud
/// <summary>
/// Extension methods for the <see cref="ClientLanguage"/> class.
/// </summary>
public static class ClientLanguageExtensions
{ {
/// <summary> /// <summary>
/// Converts a Dalamud ClientLanguage to the corresponding Lumina variant. /// Extension methods for the <see cref="ClientLanguage"/> class.
/// </summary> /// </summary>
/// <param name="language">Langauge to convert.</param> public static class ClientLanguageExtensions
/// <returns>Converted langauge.</returns>
public static Lumina.Data.Language ToLumina(this ClientLanguage language)
{ {
return language switch /// <summary>
/// Converts a Dalamud ClientLanguage to the corresponding Lumina variant.
/// </summary>
/// <param name="language">Langauge to convert.</param>
/// <returns>Converted langauge.</returns>
public static Lumina.Data.Language ToLumina(this ClientLanguage language)
{ {
ClientLanguage.Japanese => Lumina.Data.Language.Japanese, return language switch
ClientLanguage.English => Lumina.Data.Language.English, {
ClientLanguage.German => Lumina.Data.Language.German, ClientLanguage.Japanese => Lumina.Data.Language.Japanese,
ClientLanguage.French => Lumina.Data.Language.French, ClientLanguage.English => Lumina.Data.Language.English,
_ => throw new ArgumentOutOfRangeException(nameof(language)), ClientLanguage.German => Lumina.Data.Language.German,
}; ClientLanguage.French => Lumina.Data.Language.French,
_ => throw new ArgumentOutOfRangeException(nameof(language)),
};
}
} }
} }

View file

@ -1,12 +1,13 @@
namespace Dalamud.Configuration; namespace Dalamud.Configuration
/// <summary>
/// Configuration to store settings for a dalamud plugin.
/// </summary>
public interface IPluginConfiguration
{ {
/// <summary> /// <summary>
/// Gets or sets configuration version. /// Configuration to store settings for a dalamud plugin.
/// </summary> /// </summary>
int Version { get; set; } public interface IPluginConfiguration
{
/// <summary>
/// Gets or sets configuration version.
/// </summary>
int Version { get; set; }
}
} }

View file

@ -8,267 +8,268 @@ using Newtonsoft.Json;
using Serilog; using Serilog;
using Serilog.Events; using Serilog.Events;
namespace Dalamud.Configuration.Internal; namespace Dalamud.Configuration.Internal
/// <summary>
/// Class containing Dalamud settings.
/// </summary>
[Serializable]
internal sealed class DalamudConfiguration
{ {
private static readonly JsonSerializerSettings SerializerSettings = new() /// <summary>
/// Class containing Dalamud settings.
/// </summary>
[Serializable]
internal sealed class DalamudConfiguration
{ {
TypeNameHandling = TypeNameHandling.All, private static readonly JsonSerializerSettings SerializerSettings = new()
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
Formatting = Formatting.Indented,
};
[JsonIgnore]
private string configPath;
/// <summary>
/// Delegate for the <see cref="DalamudConfiguration.DalamudConfigurationSaved"/> event that occurs when the dalamud configuration is saved.
/// </summary>
/// <param name="dalamudConfiguration">The current dalamud configuration.</param>
public delegate void DalamudConfigurationSavedDelegate(DalamudConfiguration dalamudConfiguration);
/// <summary>
/// Event that occurs when dalamud configuration is saved.
/// </summary>
public event DalamudConfigurationSavedDelegate DalamudConfigurationSaved;
/// <summary>
/// Gets or sets a list of muted works.
/// </summary>
public List<string> BadWords { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not the taskbar should flash once a duty is found.
/// </summary>
public bool DutyFinderTaskbarFlash { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not a message should be sent in chat once a duty is found.
/// </summary>
public bool DutyFinderChatMessage { get; set; } = true;
/// <summary>
/// Gets or sets the language code to load Dalamud localization with.
/// </summary>
public string LanguageOverride { get; set; } = null;
/// <summary>
/// Gets or sets the last loaded Dalamud version.
/// </summary>
public string LastVersion { get; set; } = null;
/// <summary>
/// Gets or sets the last loaded Dalamud version.
/// </summary>
public string LastChangelogMajorMinor { get; set; } = null;
/// <summary>
/// Gets or sets the chat type used by default for plugin messages.
/// </summary>
public XivChatType GeneralChatType { get; set; } = XivChatType.Debug;
/// <summary>
/// Gets or sets a value indicating whether or not plugin testing builds should be shown.
/// </summary>
public bool DoPluginTest { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether or not Dalamud testing builds should be used.
/// </summary>
public bool DoDalamudTest { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether or not XL should download the Dalamud .NET runtime.
/// </summary>
public bool DoDalamudRuntime { get; set; } = false;
/// <summary>
/// Gets or sets a list of custom repos.
/// </summary>
public List<ThirdPartyRepoSettings> ThirdRepoList { get; set; } = new();
/// <summary>
/// Gets or sets a list of hidden plugins.
/// </summary>
public List<string> HiddenPluginInternalName { get; set; } = new();
/// <summary>
/// Gets or sets a list of seen plugins.
/// </summary>
public List<string> SeenPluginInternalName { get; set; } = new();
/// <summary>
/// Gets or sets a list of additional settings for devPlugins. The key is the absolute path
/// to the plugin DLL. This is automatically generated for any plugins in the devPlugins folder.
/// However by specifiying this value manually, you can add arbitrary files outside the normal
/// file paths.
/// </summary>
public Dictionary<string, DevPluginSettings> DevPluginSettings { get; set; } = new();
/// <summary>
/// Gets or sets a list of additional locations that dev plugins should be loaded from. This can
/// be either a DLL or folder, but should be the absolute path, or a path relative to the currently
/// injected Dalamud instance.
/// </summary>
public List<DevPluginLocationSettings> DevPluginLoadLocations { get; set; } = new();
/// <summary>
/// Gets or sets the global UI scale.
/// </summary>
public float GlobalUiScale { get; set; } = 1.0f;
/// <summary>
/// Gets or sets a value indicating whether or not plugin UI should be hidden.
/// </summary>
public bool ToggleUiHide { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not plugin UI should be hidden during cutscenes.
/// </summary>
public bool ToggleUiHideDuringCutscenes { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not plugin UI should be hidden during GPose.
/// </summary>
public bool ToggleUiHideDuringGpose { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not a message containing detailed plugin information should be sent at login.
/// </summary>
public bool PrintPluginsWelcomeMsg { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not plugins should be auto-updated.
/// </summary>
public bool AutoUpdatePlugins { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not Dalamud should add buttons to the system menu.
/// </summary>
public bool DoButtonsSystemMenu { get; set; } = true;
/// <summary>
/// Gets or sets the default Dalamud debug log level on startup.
/// </summary>
public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information;
/// <summary>
/// Gets or sets a value indicating whether or not the debug log should scroll automatically.
/// </summary>
public bool LogAutoScroll { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not the debug log should open at startup.
/// </summary>
public bool LogOpenAtStartup { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not ImGui asserts should be enabled at startup.
/// </summary>
public bool AssertsEnabledAtStartup { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not docking should be globally enabled in ImGui.
/// </summary>
public bool IsDocking { get; set; }
/// <summary>
/// Gets or sets a value indicating whether viewports should always be disabled.
/// </summary>
public bool IsDisableViewport { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not navigation via a gamepad should be globally enabled in ImGui.
/// </summary>
public bool IsGamepadNavigationEnabled { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not focus management is enabled.
/// </summary>
public bool IsFocusManagementEnabled { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not the anti-anti-debug check is enabled on startup.
/// </summary>
public bool IsAntiAntiDebugEnabled { get; set; } = false;
/// <summary>
/// Gets or sets the kind of beta to download when <see cref="DoDalamudTest"/> is set to true.
/// </summary>
public string DalamudBetaKind { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not all plugins, regardless of API level, should be loaded.
/// </summary>
public bool LoadAllApiLevels { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not banned plugins should be loaded.
/// </summary>
public bool LoadBannedPlugins { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not any plugin should be loaded when the game is started.
/// It is reset immediately when read.
/// </summary>
public bool PluginSafeMode { get; set; }
/// <summary>
/// Gets or sets a list of saved styles.
/// </summary>
[JsonProperty("SavedStyles")]
public List<StyleModelV1>? SavedStylesOld { get; set; }
/// <summary>
/// Gets or sets a list of saved styles.
/// </summary>
[JsonProperty("SavedStylesVersioned")]
public List<StyleModel>? SavedStyles { get; set; }
/// <summary>
/// Gets or sets the name of the currently chosen style.
/// </summary>
public string ChosenStyle { get; set; } = "Dalamud Standard";
/// <summary>
/// Gets or sets a value indicating whether or not Dalamud RMT filtering should be disabled.
/// </summary>
public bool DisableRmtFiltering { get; set; }
/// <summary>
/// Load a configuration from the provided path.
/// </summary>
/// <param name="path">The path to load the configuration file from.</param>
/// <returns>The deserialized configuration file.</returns>
public static DalamudConfiguration Load(string path)
{
DalamudConfiguration deserialized;
try
{ {
deserialized = JsonConvert.DeserializeObject<DalamudConfiguration>(File.ReadAllText(path), SerializerSettings); TypeNameHandling = TypeNameHandling.All,
} TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
catch (Exception ex) Formatting = Formatting.Indented,
};
[JsonIgnore]
private string configPath;
/// <summary>
/// Delegate for the <see cref="DalamudConfiguration.DalamudConfigurationSaved"/> event that occurs when the dalamud configuration is saved.
/// </summary>
/// <param name="dalamudConfiguration">The current dalamud configuration.</param>
public delegate void DalamudConfigurationSavedDelegate(DalamudConfiguration dalamudConfiguration);
/// <summary>
/// Event that occurs when dalamud configuration is saved.
/// </summary>
public event DalamudConfigurationSavedDelegate DalamudConfigurationSaved;
/// <summary>
/// Gets or sets a list of muted works.
/// </summary>
public List<string> BadWords { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not the taskbar should flash once a duty is found.
/// </summary>
public bool DutyFinderTaskbarFlash { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not a message should be sent in chat once a duty is found.
/// </summary>
public bool DutyFinderChatMessage { get; set; } = true;
/// <summary>
/// Gets or sets the language code to load Dalamud localization with.
/// </summary>
public string LanguageOverride { get; set; } = null;
/// <summary>
/// Gets or sets the last loaded Dalamud version.
/// </summary>
public string LastVersion { get; set; } = null;
/// <summary>
/// Gets or sets the last loaded Dalamud version.
/// </summary>
public string LastChangelogMajorMinor { get; set; } = null;
/// <summary>
/// Gets or sets the chat type used by default for plugin messages.
/// </summary>
public XivChatType GeneralChatType { get; set; } = XivChatType.Debug;
/// <summary>
/// Gets or sets a value indicating whether or not plugin testing builds should be shown.
/// </summary>
public bool DoPluginTest { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether or not Dalamud testing builds should be used.
/// </summary>
public bool DoDalamudTest { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether or not XL should download the Dalamud .NET runtime.
/// </summary>
public bool DoDalamudRuntime { get; set; } = false;
/// <summary>
/// Gets or sets a list of custom repos.
/// </summary>
public List<ThirdPartyRepoSettings> ThirdRepoList { get; set; } = new();
/// <summary>
/// Gets or sets a list of hidden plugins.
/// </summary>
public List<string> HiddenPluginInternalName { get; set; } = new();
/// <summary>
/// Gets or sets a list of seen plugins.
/// </summary>
public List<string> SeenPluginInternalName { get; set; } = new();
/// <summary>
/// Gets or sets a list of additional settings for devPlugins. The key is the absolute path
/// to the plugin DLL. This is automatically generated for any plugins in the devPlugins folder.
/// However by specifiying this value manually, you can add arbitrary files outside the normal
/// file paths.
/// </summary>
public Dictionary<string, DevPluginSettings> DevPluginSettings { get; set; } = new();
/// <summary>
/// Gets or sets a list of additional locations that dev plugins should be loaded from. This can
/// be either a DLL or folder, but should be the absolute path, or a path relative to the currently
/// injected Dalamud instance.
/// </summary>
public List<DevPluginLocationSettings> DevPluginLoadLocations { get; set; } = new();
/// <summary>
/// Gets or sets the global UI scale.
/// </summary>
public float GlobalUiScale { get; set; } = 1.0f;
/// <summary>
/// Gets or sets a value indicating whether or not plugin UI should be hidden.
/// </summary>
public bool ToggleUiHide { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not plugin UI should be hidden during cutscenes.
/// </summary>
public bool ToggleUiHideDuringCutscenes { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not plugin UI should be hidden during GPose.
/// </summary>
public bool ToggleUiHideDuringGpose { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not a message containing detailed plugin information should be sent at login.
/// </summary>
public bool PrintPluginsWelcomeMsg { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not plugins should be auto-updated.
/// </summary>
public bool AutoUpdatePlugins { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not Dalamud should add buttons to the system menu.
/// </summary>
public bool DoButtonsSystemMenu { get; set; } = true;
/// <summary>
/// Gets or sets the default Dalamud debug log level on startup.
/// </summary>
public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information;
/// <summary>
/// Gets or sets a value indicating whether or not the debug log should scroll automatically.
/// </summary>
public bool LogAutoScroll { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not the debug log should open at startup.
/// </summary>
public bool LogOpenAtStartup { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not ImGui asserts should be enabled at startup.
/// </summary>
public bool AssertsEnabledAtStartup { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not docking should be globally enabled in ImGui.
/// </summary>
public bool IsDocking { get; set; }
/// <summary>
/// Gets or sets a value indicating whether viewports should always be disabled.
/// </summary>
public bool IsDisableViewport { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not navigation via a gamepad should be globally enabled in ImGui.
/// </summary>
public bool IsGamepadNavigationEnabled { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not focus management is enabled.
/// </summary>
public bool IsFocusManagementEnabled { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not the anti-anti-debug check is enabled on startup.
/// </summary>
public bool IsAntiAntiDebugEnabled { get; set; } = false;
/// <summary>
/// Gets or sets the kind of beta to download when <see cref="DoDalamudTest"/> is set to true.
/// </summary>
public string DalamudBetaKind { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not all plugins, regardless of API level, should be loaded.
/// </summary>
public bool LoadAllApiLevels { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not banned plugins should be loaded.
/// </summary>
public bool LoadBannedPlugins { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not any plugin should be loaded when the game is started.
/// It is reset immediately when read.
/// </summary>
public bool PluginSafeMode { get; set; }
/// <summary>
/// Gets or sets a list of saved styles.
/// </summary>
[JsonProperty("SavedStyles")]
public List<StyleModelV1>? SavedStylesOld { get; set; }
/// <summary>
/// Gets or sets a list of saved styles.
/// </summary>
[JsonProperty("SavedStylesVersioned")]
public List<StyleModel>? SavedStyles { get; set; }
/// <summary>
/// Gets or sets the name of the currently chosen style.
/// </summary>
public string ChosenStyle { get; set; } = "Dalamud Standard";
/// <summary>
/// Gets or sets a value indicating whether or not Dalamud RMT filtering should be disabled.
/// </summary>
public bool DisableRmtFiltering { get; set; }
/// <summary>
/// Load a configuration from the provided path.
/// </summary>
/// <param name="path">The path to load the configuration file from.</param>
/// <returns>The deserialized configuration file.</returns>
public static DalamudConfiguration Load(string path)
{ {
Log.Warning(ex, "Failed to load DalamudConfiguration at {0}", path); DalamudConfiguration deserialized;
deserialized = new DalamudConfiguration(); try
{
deserialized = JsonConvert.DeserializeObject<DalamudConfiguration>(File.ReadAllText(path), SerializerSettings);
}
catch (Exception ex)
{
Log.Warning(ex, "Failed to load DalamudConfiguration at {0}", path);
deserialized = new DalamudConfiguration();
}
deserialized.configPath = path;
return deserialized;
} }
deserialized.configPath = path; /// <summary>
/// Save the configuration at the path it was loaded from.
return deserialized; /// </summary>
} public void Save()
{
/// <summary> File.WriteAllText(this.configPath, JsonConvert.SerializeObject(this, SerializerSettings));
/// Save the configuration at the path it was loaded from. this.DalamudConfigurationSaved?.Invoke(this);
/// </summary> }
public void Save()
{
File.WriteAllText(this.configPath, JsonConvert.SerializeObject(this, SerializerSettings));
this.DalamudConfigurationSaved?.Invoke(this);
} }
} }

View file

@ -1,23 +1,24 @@
namespace Dalamud.Configuration.Internal; namespace Dalamud.Configuration
/// <summary>
/// Additional locations to load dev plugins from.
/// </summary>
internal sealed class DevPluginLocationSettings
{ {
/// <summary> /// <summary>
/// Gets or sets the dev pluign path. /// Additional locations to load dev plugins from.
/// </summary> /// </summary>
public string Path { get; set; } internal sealed class DevPluginLocationSettings
{
/// <summary>
/// Gets or sets the dev pluign path.
/// </summary>
public string Path { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the third party repo is enabled. /// Gets or sets a value indicating whether the third party repo is enabled.
/// </summary> /// </summary>
public bool IsEnabled { get; set; } public bool IsEnabled { get; set; }
/// <summary> /// <summary>
/// Clone this object. /// Clone this object.
/// </summary> /// </summary>
/// <returns>A shallow copy of this object.</returns> /// <returns>A shallow copy of this object.</returns>
public DevPluginLocationSettings Clone() => this.MemberwiseClone() as DevPluginLocationSettings; public DevPluginLocationSettings Clone() => this.MemberwiseClone() as DevPluginLocationSettings;
}
} }

View file

@ -1,17 +1,18 @@
namespace Dalamud.Configuration.Internal; namespace Dalamud.Configuration.Internal
/// <summary>
/// Settings for DevPlugins.
/// </summary>
internal sealed class DevPluginSettings
{ {
/// <summary> /// <summary>
/// Gets or sets a value indicating whether this plugin should automatically start when Dalamud boots up. /// Settings for DevPlugins.
/// </summary> /// </summary>
public bool StartOnBoot { get; set; } = true; internal sealed class DevPluginSettings
{
/// <summary>
/// Gets or sets a value indicating whether this plugin should automatically start when Dalamud boots up.
/// </summary>
public bool StartOnBoot { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether this plugin should automatically reload on file change. /// Gets or sets a value indicating whether this plugin should automatically reload on file change.
/// </summary> /// </summary>
public bool AutomaticReloading { get; set; } = false; public bool AutomaticReloading { get; set; } = false;
}
} }

View file

@ -1,37 +1,38 @@
using System; using System;
namespace Dalamud.Configuration.Internal; namespace Dalamud.Configuration.Internal
/// <summary>
/// Environmental configuration settings.
/// </summary>
internal class EnvironmentConfiguration
{ {
/// <summary> /// <summary>
/// Gets a value indicating whether the XL_WINEONLINUX setting has been enabled. /// Environmental configuration settings.
/// </summary> /// </summary>
public static bool XlWineOnLinux { get; } = GetEnvironmentVariable("XL_WINEONLINUX"); internal class EnvironmentConfiguration
{
/// <summary>
/// Gets a value indicating whether the XL_WINEONLINUX setting has been enabled.
/// </summary>
public static bool XlWineOnLinux { get; } = GetEnvironmentVariable("XL_WINEONLINUX");
/// <summary> /// <summary>
/// Gets a value indicating whether the DALAMUD_NOT_HAVE_PLUGINS setting has been enabled. /// Gets a value indicating whether the DALAMUD_NOT_HAVE_PLUGINS setting has been enabled.
/// </summary> /// </summary>
public static bool DalamudNoPlugins { get; } = GetEnvironmentVariable("DALAMUD_NOT_HAVE_PLUGINS"); public static bool DalamudNoPlugins { get; } = GetEnvironmentVariable("DALAMUD_NOT_HAVE_PLUGINS");
/// <summary> /// <summary>
/// Gets a value indicating whether the DalamudForceReloaded setting has been enabled. /// Gets a value indicating whether the DalamudForceReloaded setting has been enabled.
/// </summary> /// </summary>
public static bool DalamudForceReloaded { get; } = GetEnvironmentVariable("DALAMUD_FORCE_RELOADED"); public static bool DalamudForceReloaded { get; } = GetEnvironmentVariable("DALAMUD_FORCE_RELOADED");
/// <summary> /// <summary>
/// Gets a value indicating whether the DalamudForceMinHook setting has been enabled. /// Gets a value indicating whether the DalamudForceMinHook setting has been enabled.
/// </summary> /// </summary>
public static bool DalamudForceMinHook { get; } = GetEnvironmentVariable("DALAMUD_FORCE_MINHOOK"); public static bool DalamudForceMinHook { get; } = GetEnvironmentVariable("DALAMUD_FORCE_MINHOOK");
/// <summary> /// <summary>
/// Gets a value indicating whether or not Dalamud should wait for a debugger to be attached when initializing. /// Gets a value indicating whether or not Dalamud should wait for a debugger to be attached when initializing.
/// </summary> /// </summary>
public static bool DalamudWaitForDebugger { get; } = GetEnvironmentVariable("DALAMUD_WAIT_DEBUGGER"); public static bool DalamudWaitForDebugger { get; } = GetEnvironmentVariable("DALAMUD_WAIT_DEBUGGER");
private static bool GetEnvironmentVariable(string name) private static bool GetEnvironmentVariable(string name)
=> bool.Parse(Environment.GetEnvironmentVariable(name) ?? "false"); => bool.Parse(Environment.GetEnvironmentVariable(name) ?? "false");
}
} }

View file

@ -1,28 +1,29 @@
namespace Dalamud.Configuration.Internal; namespace Dalamud.Configuration
/// <summary>
/// Third party repository for dalamud plugins.
/// </summary>
internal sealed class ThirdPartyRepoSettings
{ {
/// <summary> /// <summary>
/// Gets or sets the third party repo url. /// Third party repository for dalamud plugins.
/// </summary> /// </summary>
public string Url { get; set; } internal sealed class ThirdPartyRepoSettings
{
/// <summary>
/// Gets or sets the third party repo url.
/// </summary>
public string Url { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the third party repo is enabled. /// Gets or sets a value indicating whether the third party repo is enabled.
/// </summary> /// </summary>
public bool IsEnabled { get; set; } public bool IsEnabled { get; set; }
/// <summary> /// <summary>
/// Gets or sets a short name for the repo url. /// Gets or sets a short name for the repo url.
/// </summary> /// </summary>
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// Clone this object. /// Clone this object.
/// </summary> /// </summary>
/// <returns>A shallow copy of this object.</returns> /// <returns>A shallow copy of this object.</returns>
public ThirdPartyRepoSettings Clone() => this.MemberwiseClone() as ThirdPartyRepoSettings; public ThirdPartyRepoSettings Clone() => this.MemberwiseClone() as ThirdPartyRepoSettings;
}
} }

View file

@ -2,128 +2,129 @@ using System.IO;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud.Configuration; namespace Dalamud.Configuration
/// <summary>
/// Configuration to store settings for a dalamud plugin.
/// </summary>
public sealed class PluginConfigurations
{ {
private readonly DirectoryInfo configDirectory;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PluginConfigurations"/> class. /// Configuration to store settings for a dalamud plugin.
/// </summary> /// </summary>
/// <param name="storageFolder">Directory for storage of plugin configuration files.</param> public sealed class PluginConfigurations
public PluginConfigurations(string storageFolder)
{ {
this.configDirectory = new DirectoryInfo(storageFolder); private readonly DirectoryInfo configDirectory;
this.configDirectory.Create();
}
/// <summary> /// <summary>
/// Save/Load plugin configuration. /// Initializes a new instance of the <see cref="PluginConfigurations"/> class.
/// NOTE: Save/Load are still using Type information for now, /// </summary>
/// despite LoadForType superseding Load and not requiring or using it. /// <param name="storageFolder">Directory for storage of plugin configuration files.</param>
/// It might be worth removing the Type info from Save, to strip it from all future saved configs, public PluginConfigurations(string storageFolder)
/// and then Load() can probably be removed entirely.
/// </summary>
/// <param name="config">Plugin configuration.</param>
/// <param name="pluginName">Plugin name.</param>
public void Save(IPluginConfiguration config, string pluginName)
{
File.WriteAllText(this.GetConfigFile(pluginName).FullName, JsonConvert.SerializeObject(config, Formatting.Indented, new JsonSerializerSettings
{ {
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, this.configDirectory = new DirectoryInfo(storageFolder);
TypeNameHandling = TypeNameHandling.Objects, this.configDirectory.Create();
})); }
}
/// <summary> /// <summary>
/// Load plugin configuration. /// Save/Load plugin configuration.
/// </summary> /// NOTE: Save/Load are still using Type information for now,
/// <param name="pluginName">Plugin name.</param> /// despite LoadForType superseding Load and not requiring or using it.
/// <returns>Plugin configuration.</returns> /// It might be worth removing the Type info from Save, to strip it from all future saved configs,
public IPluginConfiguration? Load(string pluginName) /// and then Load() can probably be removed entirely.
{ /// </summary>
var path = this.GetConfigFile(pluginName); /// <param name="config">Plugin configuration.</param>
/// <param name="pluginName">Plugin name.</param>
if (!path.Exists) public void Save(IPluginConfiguration config, string pluginName)
return null; {
File.WriteAllText(this.GetConfigFile(pluginName).FullName, JsonConvert.SerializeObject(config, Formatting.Indented, new JsonSerializerSettings
return JsonConvert.DeserializeObject<IPluginConfiguration>(
File.ReadAllText(path.FullName),
new JsonSerializerSettings
{ {
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
TypeNameHandling = TypeNameHandling.Objects, TypeNameHandling = TypeNameHandling.Objects,
}); }));
} }
/// <summary> /// <summary>
/// Delete the configuration file and folder for the specified plugin. /// Load plugin configuration.
/// This will throw an <see cref="IOException"/> if the plugin did not correctly close its handles. /// </summary>
/// </summary> /// <param name="pluginName">Plugin name.</param>
/// <param name="pluginName">The name of the plugin.</param> /// <returns>Plugin configuration.</returns>
public void Delete(string pluginName) public IPluginConfiguration? Load(string pluginName)
{
var directory = this.GetDirectoryPath(pluginName);
if (directory.Exists)
directory.Delete(true);
var file = this.GetConfigFile(pluginName);
if (file.Exists)
file.Delete();
}
/// <summary>
/// Get plugin directory.
/// </summary>
/// <param name="pluginName">Plugin name.</param>
/// <returns>Plugin directory path.</returns>
public string GetDirectory(string pluginName)
{
try
{ {
var path = this.GetDirectoryPath(pluginName); var path = this.GetConfigFile(pluginName);
if (!path.Exists) if (!path.Exists)
{ return null;
path.Create();
}
return path.FullName; return JsonConvert.DeserializeObject<IPluginConfiguration>(
File.ReadAllText(path.FullName),
new JsonSerializerSettings
{
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
TypeNameHandling = TypeNameHandling.Objects,
});
} }
catch
/// <summary>
/// Delete the configuration file and folder for the specified plugin.
/// This will throw an <see cref="IOException"/> if the plugin did not correctly close its handles.
/// </summary>
/// <param name="pluginName">The name of the plugin.</param>
public void Delete(string pluginName)
{ {
return string.Empty; var directory = this.GetDirectoryPath(pluginName);
if (directory.Exists)
directory.Delete(true);
var file = this.GetConfigFile(pluginName);
if (file.Exists)
file.Delete();
} }
/// <summary>
/// Get plugin directory.
/// </summary>
/// <param name="pluginName">Plugin name.</param>
/// <returns>Plugin directory path.</returns>
public string GetDirectory(string pluginName)
{
try
{
var path = this.GetDirectoryPath(pluginName);
if (!path.Exists)
{
path.Create();
}
return path.FullName;
}
catch
{
return string.Empty;
}
}
/// <summary>
/// Load Plugin configuration. Parameterized deserialization.
/// Currently this is called via reflection from DalamudPluginInterface.GetPluginConfig().
/// Eventually there may be an additional pluginInterface method that can call this directly
/// without reflection - for now this is in support of the existing plugin api.
/// </summary>
/// <param name="pluginName">Plugin Name.</param>
/// <typeparam name="T">Configuration Type.</typeparam>
/// <returns>Plugin Configuration.</returns>
public T LoadForType<T>(string pluginName) where T : IPluginConfiguration
{
var path = this.GetConfigFile(pluginName);
return !path.Exists ? default : JsonConvert.DeserializeObject<T>(File.ReadAllText(path.FullName));
// intentionally no type handling - it will break when updating a plugin at runtime
// and turns out to be unnecessary when we fully qualify the object type
}
/// <summary>
/// Get FileInfo to plugin config file.
/// </summary>
/// <param name="pluginName">InternalName of the plugin.</param>
/// <returns>FileInfo of the config file.</returns>
public FileInfo GetConfigFile(string pluginName) => new(Path.Combine(this.configDirectory.FullName, $"{pluginName}.json"));
private DirectoryInfo GetDirectoryPath(string pluginName) => new(Path.Combine(this.configDirectory.FullName, pluginName));
} }
/// <summary>
/// Load Plugin configuration. Parameterized deserialization.
/// Currently this is called via reflection from DalamudPluginInterface.GetPluginConfig().
/// Eventually there may be an additional pluginInterface method that can call this directly
/// without reflection - for now this is in support of the existing plugin api.
/// </summary>
/// <param name="pluginName">Plugin Name.</param>
/// <typeparam name="T">Configuration Type.</typeparam>
/// <returns>Plugin Configuration.</returns>
public T LoadForType<T>(string pluginName) where T : IPluginConfiguration
{
var path = this.GetConfigFile(pluginName);
return !path.Exists ? default : JsonConvert.DeserializeObject<T>(File.ReadAllText(path.FullName));
// intentionally no type handling - it will break when updating a plugin at runtime
// and turns out to be unnecessary when we fully qualify the object type
}
/// <summary>
/// Get FileInfo to plugin config file.
/// </summary>
/// <param name="pluginName">InternalName of the plugin.</param>
/// <returns>FileInfo of the config file.</returns>
public FileInfo GetConfigFile(string pluginName) => new(Path.Combine(this.configDirectory.FullName, $"{pluginName}.json"));
private DirectoryInfo GetDirectoryPath(string pluginName) => new(Path.Combine(this.configDirectory.FullName, pluginName));
} }

View file

@ -33,378 +33,379 @@ using Serilog.Events;
[assembly: InternalsVisibleTo("Dalamud.Test")] [assembly: InternalsVisibleTo("Dalamud.Test")]
namespace Dalamud; namespace Dalamud
/// <summary>
/// The main Dalamud class containing all subsystems.
/// </summary>
internal sealed class Dalamud : IDisposable
{ {
#region Internals
private readonly ManualResetEvent unloadSignal;
private readonly ManualResetEvent finishUnloadSignal;
private MonoMod.RuntimeDetour.Hook processMonoHook;
private bool hasDisposedPlugins = false;
#endregion
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Dalamud"/> class. /// The main Dalamud class containing all subsystems.
/// </summary> /// </summary>
/// <param name="info">DalamudStartInfo instance.</param> internal sealed class Dalamud : IDisposable
/// <param name="loggingLevelSwitch">LoggingLevelSwitch to control Serilog level.</param>
/// <param name="finishSignal">Signal signalling shutdown.</param>
/// <param name="configuration">The Dalamud configuration.</param>
public Dalamud(DalamudStartInfo info, LoggingLevelSwitch loggingLevelSwitch, ManualResetEvent finishSignal, DalamudConfiguration configuration)
{ {
this.ApplyProcessPatch(); #region Internals
Service<Dalamud>.Set(this); private readonly ManualResetEvent unloadSignal;
Service<DalamudStartInfo>.Set(info); private readonly ManualResetEvent finishUnloadSignal;
Service<DalamudConfiguration>.Set(configuration); private MonoMod.RuntimeDetour.Hook processMonoHook;
private bool hasDisposedPlugins = false;
this.LogLevelSwitch = loggingLevelSwitch; #endregion
this.unloadSignal = new ManualResetEvent(false); /// <summary>
this.unloadSignal.Reset(); /// Initializes a new instance of the <see cref="Dalamud"/> class.
/// </summary>
this.finishUnloadSignal = finishSignal; /// <param name="info">DalamudStartInfo instance.</param>
this.finishUnloadSignal.Reset(); /// <param name="loggingLevelSwitch">LoggingLevelSwitch to control Serilog level.</param>
} /// <param name="finishSignal">Signal signalling shutdown.</param>
/// <param name="configuration">The Dalamud configuration.</param>
/// <summary> public Dalamud(DalamudStartInfo info, LoggingLevelSwitch loggingLevelSwitch, ManualResetEvent finishSignal, DalamudConfiguration configuration)
/// Gets LoggingLevelSwitch for Dalamud and Plugin logs.
/// </summary>
internal LoggingLevelSwitch LogLevelSwitch { get; private set; }
/// <summary>
/// Gets location of stored assets.
/// </summary>
internal DirectoryInfo AssetDirectory => new(Service<DalamudStartInfo>.Get().AssetDirectory);
/// <summary>
/// Runs tier 1 of the Dalamud initialization process.
/// </summary>
public void LoadTier1()
{
try
{ {
SerilogEventSink.Instance.LogLine += SerilogOnLogLine; this.ApplyProcessPatch();
Service<ServiceContainer>.Set(); Service<Dalamud>.Set(this);
Service<DalamudStartInfo>.Set(info);
Service<DalamudConfiguration>.Set(configuration);
// Initialize the process information. this.LogLevelSwitch = loggingLevelSwitch;
Service<SigScanner>.Set(new SigScanner(true));
Service<HookManager>.Set();
// Initialize FFXIVClientStructs function resolver this.unloadSignal = new ManualResetEvent(false);
FFXIVClientStructs.Resolver.Initialize(); this.unloadSignal.Reset();
Log.Information("[T1] FFXIVClientStructs initialized!");
// Initialize game subsystem this.finishUnloadSignal = finishSignal;
var framework = Service<Framework>.Set(); this.finishUnloadSignal.Reset();
Log.Information("[T1] Framework OK!");
#if DEBUG
Service<TaskTracker>.Set();
Log.Information("[T1] TaskTracker OK!");
#endif
Service<GameNetwork>.Set();
Service<GameGui>.Set();
framework.Enable();
Log.Information("[T1] Load complete!");
} }
catch (Exception ex)
{
Log.Error(ex, "Tier 1 load failed.");
this.Unload();
}
}
/// <summary> /// <summary>
/// Runs tier 2 of the Dalamud initialization process. /// Gets LoggingLevelSwitch for Dalamud and Plugin logs.
/// </summary> /// </summary>
/// <returns>Whether or not the load succeeded.</returns> internal LoggingLevelSwitch LogLevelSwitch { get; private set; }
public bool LoadTier2()
{
try
{
var configuration = Service<DalamudConfiguration>.Get();
var antiDebug = Service<AntiDebug>.Set(); /// <summary>
if (!antiDebug.IsEnabled) /// Gets location of stored assets.
/// </summary>
internal DirectoryInfo AssetDirectory => new(Service<DalamudStartInfo>.Get().AssetDirectory);
/// <summary>
/// Runs tier 1 of the Dalamud initialization process.
/// </summary>
public void LoadTier1()
{
try
{ {
SerilogEventSink.Instance.LogLine += SerilogOnLogLine;
Service<ServiceContainer>.Set();
// Initialize the process information.
Service<SigScanner>.Set(new SigScanner(true));
Service<HookManager>.Set();
// Initialize FFXIVClientStructs function resolver
FFXIVClientStructs.Resolver.Initialize();
Log.Information("[T1] FFXIVClientStructs initialized!");
// Initialize game subsystem
var framework = Service<Framework>.Set();
Log.Information("[T1] Framework OK!");
#if DEBUG #if DEBUG
antiDebug.Enable(); Service<TaskTracker>.Set();
Log.Information("[T1] TaskTracker OK!");
#endif
Service<GameNetwork>.Set();
Service<GameGui>.Set();
framework.Enable();
Log.Information("[T1] Load complete!");
}
catch (Exception ex)
{
Log.Error(ex, "Tier 1 load failed.");
this.Unload();
}
}
/// <summary>
/// Runs tier 2 of the Dalamud initialization process.
/// </summary>
/// <returns>Whether or not the load succeeded.</returns>
public bool LoadTier2()
{
try
{
var configuration = Service<DalamudConfiguration>.Get();
var antiDebug = Service<AntiDebug>.Set();
if (!antiDebug.IsEnabled)
{
#if DEBUG
antiDebug.Enable();
#else #else
if (configuration.IsAntiAntiDebugEnabled) if (configuration.IsAntiAntiDebugEnabled)
antiDebug.Enable(); antiDebug.Enable();
#endif #endif
}
Log.Information("[T2] AntiDebug OK!");
Service<WinSockHandlers>.Set();
Log.Information("[T2] WinSock OK!");
Service<NetworkHandlers>.Set();
Log.Information("[T2] NH OK!");
try
{
Service<DataManager>.Set().Initialize(this.AssetDirectory.FullName);
}
catch (Exception e)
{
Log.Error(e, "Could not initialize DataManager.");
this.Unload();
return false;
}
Log.Information("[T2] Data OK!");
var clientState = Service<ClientState>.Set();
Log.Information("[T2] CS OK!");
var localization = Service<Localization>.Set(new Localization(Path.Combine(this.AssetDirectory.FullName, "UIRes", "loc", "dalamud"), "dalamud_"));
if (!string.IsNullOrEmpty(configuration.LanguageOverride))
{
localization.SetupWithLangCode(configuration.LanguageOverride);
}
else
{
localization.SetupWithUiCulture();
}
Log.Information("[T2] LOC OK!");
// This is enabled in ImGuiScene setup
Service<DalamudIME>.Set();
Log.Information("[T2] IME OK!");
Service<InterfaceManager>.Set().Enable();
Log.Information("[T2] IM OK!");
#pragma warning disable CS0618 // Type or member is obsolete
Service<SeStringManager>.Set();
#pragma warning restore CS0618 // Type or member is obsolete
Log.Information("[T2] SeString OK!");
// Initialize managers. Basically handlers for the logic
Service<CommandManager>.Set();
Service<DalamudCommands>.Set().SetupCommands();
Log.Information("[T2] CM OK!");
Service<ChatHandlers>.Set();
Log.Information("[T2] CH OK!");
clientState.Enable();
Log.Information("[T2] CS ENABLE!");
Service<DalamudAtkTweaks>.Set().Enable();
Log.Information("[T2] Load complete!");
} }
catch (Exception ex)
Log.Information("[T2] AntiDebug OK!");
Service<WinSockHandlers>.Set();
Log.Information("[T2] WinSock OK!");
Service<NetworkHandlers>.Set();
Log.Information("[T2] NH OK!");
try
{ {
Service<DataManager>.Set().Initialize(this.AssetDirectory.FullName); Log.Error(ex, "Tier 2 load failed.");
}
catch (Exception e)
{
Log.Error(e, "Could not initialize DataManager.");
this.Unload(); this.Unload();
return false; return false;
} }
Log.Information("[T2] Data OK!"); return true;
var clientState = Service<ClientState>.Set();
Log.Information("[T2] CS OK!");
var localization = Service<Localization>.Set(new Localization(Path.Combine(this.AssetDirectory.FullName, "UIRes", "loc", "dalamud"), "dalamud_"));
if (!string.IsNullOrEmpty(configuration.LanguageOverride))
{
localization.SetupWithLangCode(configuration.LanguageOverride);
}
else
{
localization.SetupWithUiCulture();
}
Log.Information("[T2] LOC OK!");
// This is enabled in ImGuiScene setup
Service<DalamudIME>.Set();
Log.Information("[T2] IME OK!");
Service<InterfaceManager>.Set().Enable();
Log.Information("[T2] IM OK!");
#pragma warning disable CS0618 // Type or member is obsolete
Service<SeStringManager>.Set();
#pragma warning restore CS0618 // Type or member is obsolete
Log.Information("[T2] SeString OK!");
// Initialize managers. Basically handlers for the logic
Service<CommandManager>.Set();
Service<DalamudCommands>.Set().SetupCommands();
Log.Information("[T2] CM OK!");
Service<ChatHandlers>.Set();
Log.Information("[T2] CH OK!");
clientState.Enable();
Log.Information("[T2] CS ENABLE!");
Service<DalamudAtkTweaks>.Set().Enable();
Log.Information("[T2] Load complete!");
}
catch (Exception ex)
{
Log.Error(ex, "Tier 2 load failed.");
this.Unload();
return false;
} }
return true; /// <summary>
} /// Runs tier 3 of the Dalamud initialization process.
/// </summary>
/// <summary> /// <returns>Whether or not the load succeeded.</returns>
/// Runs tier 3 of the Dalamud initialization process. public bool LoadTier3()
/// </summary>
/// <returns>Whether or not the load succeeded.</returns>
public bool LoadTier3()
{
try
{ {
Log.Information("[T3] START!");
var pluginManager = Service<PluginManager>.Set();
Service<CallGate>.Set();
try try
{ {
_ = pluginManager.SetPluginReposFromConfigAsync(false); Log.Information("[T3] START!");
pluginManager.OnInstalledPluginsChanged += Troubleshooting.LogTroubleshooting; var pluginManager = Service<PluginManager>.Set();
Service<CallGate>.Set();
Log.Information("[T3] PM OK!"); try
{
_ = pluginManager.SetPluginReposFromConfigAsync(false);
pluginManager.CleanupPlugins(); pluginManager.OnInstalledPluginsChanged += Troubleshooting.LogTroubleshooting;
Log.Information("[T3] PMC OK!");
pluginManager.LoadAllPlugins(); Log.Information("[T3] PM OK!");
Log.Information("[T3] PML OK!");
pluginManager.CleanupPlugins();
Log.Information("[T3] PMC OK!");
pluginManager.LoadAllPlugins();
Log.Information("[T3] PML OK!");
}
catch (Exception ex)
{
Log.Error(ex, "Plugin load failed.");
}
Service<DalamudInterface>.Set();
Log.Information("[T3] DUI OK!");
Troubleshooting.LogTroubleshooting();
Log.Information("Dalamud is ready.");
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "Plugin load failed."); Log.Error(ex, "Tier 3 load failed.");
this.Unload();
return false;
} }
Service<DalamudInterface>.Set(); return true;
Log.Information("[T3] DUI OK!");
Troubleshooting.LogTroubleshooting();
Log.Information("Dalamud is ready.");
}
catch (Exception ex)
{
Log.Error(ex, "Tier 3 load failed.");
this.Unload();
return false;
} }
return true; /// <summary>
} /// Queue an unload of Dalamud when it gets the chance.
/// </summary>
/// <summary> public void Unload()
/// Queue an unload of Dalamud when it gets the chance.
/// </summary>
public void Unload()
{
Log.Information("Trigger unload");
this.unloadSignal.Set();
}
/// <summary>
/// Wait for an unload request to start.
/// </summary>
public void WaitForUnload()
{
this.unloadSignal.WaitOne();
}
/// <summary>
/// Wait for a queued unload to be finalized.
/// </summary>
public void WaitForUnloadFinish()
{
this.finishUnloadSignal?.WaitOne();
}
/// <summary>
/// Dispose subsystems related to plugin handling.
/// </summary>
public void DisposePlugins()
{
this.hasDisposedPlugins = true;
// this must be done before unloading interface manager, in order to do rebuild
// the correct cascaded WndProc (IME -> RawDX11Scene -> Game). Otherwise the game
// will not receive any windows messages
Service<DalamudIME>.GetNullable()?.Dispose();
// this must be done before unloading plugins, or it can cause a race condition
// due to rendering happening on another thread, where a plugin might receive
// a render call after it has been disposed, which can crash if it attempts to
// use any resources that it freed in its own Dispose method
Service<InterfaceManager>.GetNullable()?.Dispose();
Service<DalamudInterface>.GetNullable()?.Dispose();
Service<PluginManager>.GetNullable()?.Dispose();
}
/// <summary>
/// Dispose Dalamud subsystems.
/// </summary>
public void Dispose()
{
try
{ {
if (!this.hasDisposedPlugins) Log.Information("Trigger unload");
this.unloadSignal.Set();
}
/// <summary>
/// Wait for an unload request to start.
/// </summary>
public void WaitForUnload()
{
this.unloadSignal.WaitOne();
}
/// <summary>
/// Wait for a queued unload to be finalized.
/// </summary>
public void WaitForUnloadFinish()
{
this.finishUnloadSignal?.WaitOne();
}
/// <summary>
/// Dispose subsystems related to plugin handling.
/// </summary>
public void DisposePlugins()
{
this.hasDisposedPlugins = true;
// this must be done before unloading interface manager, in order to do rebuild
// the correct cascaded WndProc (IME -> RawDX11Scene -> Game). Otherwise the game
// will not receive any windows messages
Service<DalamudIME>.GetNullable()?.Dispose();
// this must be done before unloading plugins, or it can cause a race condition
// due to rendering happening on another thread, where a plugin might receive
// a render call after it has been disposed, which can crash if it attempts to
// use any resources that it freed in its own Dispose method
Service<InterfaceManager>.GetNullable()?.Dispose();
Service<DalamudInterface>.GetNullable()?.Dispose();
Service<PluginManager>.GetNullable()?.Dispose();
}
/// <summary>
/// Dispose Dalamud subsystems.
/// </summary>
public void Dispose()
{
try
{ {
this.DisposePlugins(); if (!this.hasDisposedPlugins)
Thread.Sleep(100); {
this.DisposePlugins();
Thread.Sleep(100);
}
Service<Framework>.GetNullable()?.Dispose();
Service<ClientState>.GetNullable()?.Dispose();
this.unloadSignal?.Dispose();
Service<WinSockHandlers>.GetNullable()?.Dispose();
Service<DataManager>.GetNullable()?.Dispose();
Service<AntiDebug>.GetNullable()?.Dispose();
Service<DalamudAtkTweaks>.GetNullable()?.Dispose();
Service<HookManager>.GetNullable()?.Dispose();
Service<SigScanner>.GetNullable()?.Dispose();
SerilogEventSink.Instance.LogLine -= SerilogOnLogLine;
this.processMonoHook?.Dispose();
Log.Debug("Dalamud::Dispose() OK!");
}
catch (Exception ex)
{
Log.Error(ex, "Dalamud::Dispose() failed.");
}
}
/// <summary>
/// Replace the built-in exception handler with a debug one.
/// </summary>
internal void ReplaceExceptionHandler()
{
var releaseSig = "40 55 53 56 48 8D AC 24 ?? ?? ?? ?? B8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 2B E0 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 48 83 3D ?? ?? ?? ?? ??";
var releaseFilter = Service<SigScanner>.Get().ScanText(releaseSig);
Log.Debug($"SE debug filter at {releaseFilter.ToInt64():X}");
var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(releaseFilter);
Log.Debug("Reset ExceptionFilter, old: {0}", oldFilter);
}
private static void SerilogOnLogLine(object? sender, (string Line, LogEventLevel Level, DateTimeOffset TimeStamp, Exception? Exception) e)
{
if (e.Exception == null)
return;
Troubleshooting.LogException(e.Exception, e.Line);
}
/// <summary>
/// Patch method for the class Process.Handle. This patch facilitates fixing Reloaded so that it
/// uses pseudo-handles to access memory, to prevent permission errors.
/// It should never be called manually.
/// </summary>
/// <param name="orig">A delegate that acts as the original method.</param>
/// <param name="self">The equivalent of `this`.</param>
/// <returns>A pseudo-handle for the current process, or the result from the original method.</returns>
private static IntPtr ProcessHandlePatch(Func<Process, IntPtr> orig, Process self)
{
var result = orig(self);
if (self.Id == Environment.ProcessId)
{
result = (IntPtr)0xFFFFFFFF;
} }
Service<Framework>.GetNullable()?.Dispose(); // Log.Verbose($"Process.Handle // {self.ProcessName} // {result:X}");
Service<ClientState>.GetNullable()?.Dispose(); return result;
this.unloadSignal?.Dispose();
Service<WinSockHandlers>.GetNullable()?.Dispose();
Service<DataManager>.GetNullable()?.Dispose();
Service<AntiDebug>.GetNullable()?.Dispose();
Service<DalamudAtkTweaks>.GetNullable()?.Dispose();
Service<HookManager>.GetNullable()?.Dispose();
Service<SigScanner>.GetNullable()?.Dispose();
SerilogEventSink.Instance.LogLine -= SerilogOnLogLine;
this.processMonoHook?.Dispose();
Log.Debug("Dalamud::Dispose() OK!");
} }
catch (Exception ex)
private void ApplyProcessPatch()
{ {
Log.Error(ex, "Dalamud::Dispose() failed."); var targetType = typeof(Process);
var handleTarget = targetType.GetProperty(nameof(Process.Handle)).GetGetMethod();
var handlePatch = typeof(Dalamud).GetMethod(nameof(Dalamud.ProcessHandlePatch), BindingFlags.NonPublic | BindingFlags.Static);
this.processMonoHook = new MonoMod.RuntimeDetour.Hook(handleTarget, handlePatch);
} }
} }
/// <summary>
/// Replace the built-in exception handler with a debug one.
/// </summary>
internal void ReplaceExceptionHandler()
{
var releaseSig = "40 55 53 56 48 8D AC 24 ?? ?? ?? ?? B8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 2B E0 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 48 83 3D ?? ?? ?? ?? ??";
var releaseFilter = Service<SigScanner>.Get().ScanText(releaseSig);
Log.Debug($"SE debug filter at {releaseFilter.ToInt64():X}");
var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(releaseFilter);
Log.Debug("Reset ExceptionFilter, old: {0}", oldFilter);
}
private static void SerilogOnLogLine(object? sender, (string Line, LogEventLevel Level, DateTimeOffset TimeStamp, Exception? Exception) e)
{
if (e.Exception == null)
return;
Troubleshooting.LogException(e.Exception, e.Line);
}
/// <summary>
/// Patch method for the class Process.Handle. This patch facilitates fixing Reloaded so that it
/// uses pseudo-handles to access memory, to prevent permission errors.
/// It should never be called manually.
/// </summary>
/// <param name="orig">A delegate that acts as the original method.</param>
/// <param name="self">The equivalent of `this`.</param>
/// <returns>A pseudo-handle for the current process, or the result from the original method.</returns>
private static IntPtr ProcessHandlePatch(Func<Process, IntPtr> orig, Process self)
{
var result = orig(self);
if (self.Id == Environment.ProcessId)
{
result = (IntPtr)0xFFFFFFFF;
}
// Log.Verbose($"Process.Handle // {self.ProcessName} // {result:X}");
return result;
}
private void ApplyProcessPatch()
{
var targetType = typeof(Process);
var handleTarget = targetType.GetProperty(nameof(Process.Handle)).GetGetMethod();
var handlePatch = typeof(Dalamud).GetMethod(nameof(Dalamud.ProcessHandlePatch), BindingFlags.NonPublic | BindingFlags.Static);
this.processMonoHook = new MonoMod.RuntimeDetour.Hook(handleTarget, handlePatch);
}
} }

View file

@ -3,57 +3,58 @@ using System;
using Dalamud.Game; using Dalamud.Game;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud; namespace Dalamud
/// <summary>
/// Struct containing information needed to initialize Dalamud.
/// </summary>
[Serializable]
public record DalamudStartInfo
{ {
/// <summary> /// <summary>
/// Gets or sets the working directory of the XIVLauncher installations. /// Struct containing information needed to initialize Dalamud.
/// </summary> /// </summary>
public string WorkingDirectory { get; set; } [Serializable]
public record DalamudStartInfo
{
/// <summary>
/// Gets or sets the working directory of the XIVLauncher installations.
/// </summary>
public string WorkingDirectory { get; set; }
/// <summary> /// <summary>
/// Gets the path to the configuration file. /// Gets the path to the configuration file.
/// </summary> /// </summary>
public string ConfigurationPath { get; init; } public string ConfigurationPath { get; init; }
/// <summary> /// <summary>
/// Gets the path to the directory for installed plugins. /// Gets the path to the directory for installed plugins.
/// </summary> /// </summary>
public string PluginDirectory { get; init; } public string PluginDirectory { get; init; }
/// <summary> /// <summary>
/// Gets the path to the directory for developer plugins. /// Gets the path to the directory for developer plugins.
/// </summary> /// </summary>
public string DefaultPluginDirectory { get; init; } public string DefaultPluginDirectory { get; init; }
/// <summary> /// <summary>
/// Gets the path to core Dalamud assets. /// Gets the path to core Dalamud assets.
/// </summary> /// </summary>
public string AssetDirectory { get; init; } public string AssetDirectory { get; init; }
/// <summary> /// <summary>
/// Gets the language of the game client. /// Gets the language of the game client.
/// </summary> /// </summary>
public ClientLanguage Language { get; init; } public ClientLanguage Language { get; init; }
/// <summary> /// <summary>
/// Gets the current game version code. /// Gets the current game version code.
/// </summary> /// </summary>
[JsonConverter(typeof(GameVersionConverter))] [JsonConverter(typeof(GameVersionConverter))]
public GameVersion GameVersion { get; init; } public GameVersion GameVersion { get; init; }
/// <summary> /// <summary>
/// Gets a value indicating whether or not market board information should be uploaded by default. /// Gets a value indicating whether or not market board information should be uploaded by default.
/// </summary> /// </summary>
public bool OptOutMbCollection { get; init; } public bool OptOutMbCollection { get; init; }
/// <summary> /// <summary>
/// Gets a value that specifies how much to wait before a new Dalamud session. /// Gets a value that specifies how much to wait before a new Dalamud session.
/// </summary> /// </summary>
public int DelayInitializeMs { get; init; } = 0; public int DelayInitializeMs { get; init; } = 0;
}
} }

View file

@ -18,329 +18,330 @@ using Lumina.Excel;
using Newtonsoft.Json; using Newtonsoft.Json;
using Serilog; using Serilog;
namespace Dalamud.Data; namespace Dalamud.Data
/// <summary>
/// This class provides data for Dalamud-internal features, but can also be used by plugins if needed.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed class DataManager : IDisposable
{ {
private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex";
private Thread luminaResourceThread;
private CancellationTokenSource luminaCancellationTokenSource;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DataManager"/> class. /// This class provides data for Dalamud-internal features, but can also be used by plugins if needed.
/// </summary> /// </summary>
internal DataManager() [PluginInterface]
[InterfaceVersion("1.0")]
public sealed class DataManager : IDisposable
{ {
this.Language = Service<DalamudStartInfo>.Get().Language; private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex";
// Set up default values so plugins do not null-reference when data is being loaded. private Thread luminaResourceThread;
this.ClientOpCodes = this.ServerOpCodes = new ReadOnlyDictionary<string, ushort>(new Dictionary<string, ushort>()); private CancellationTokenSource luminaCancellationTokenSource;
}
/// <summary> /// <summary>
/// Gets the current game client language. /// Initializes a new instance of the <see cref="DataManager"/> class.
/// </summary> /// </summary>
public ClientLanguage Language { get; private set; } internal DataManager()
/// <summary>
/// Gets the OpCodes sent by the server to the client.
/// </summary>
public ReadOnlyDictionary<string, ushort> ServerOpCodes { get; private set; }
/// <summary>
/// Gets the OpCodes sent by the client to the server.
/// </summary>
[UsedImplicitly]
public ReadOnlyDictionary<string, ushort> ClientOpCodes { get; private set; }
/// <summary>
/// Gets a <see cref="Lumina"/> object which gives access to any excel/game data.
/// </summary>
public GameData GameData { get; private set; }
/// <summary>
/// Gets an <see cref="ExcelModule"/> object which gives access to any of the game's sheet data.
/// </summary>
public ExcelModule Excel => this.GameData?.Excel;
/// <summary>
/// Gets a value indicating whether Game Data is ready to be read.
/// </summary>
public bool IsDataReady { get; private set; }
#region Lumina Wrappers
/// <summary>
/// Get an <see cref="ExcelSheet{T}"/> with the given Excel sheet row type.
/// </summary>
/// <typeparam name="T">The excel sheet type to get.</typeparam>
/// <returns>The <see cref="ExcelSheet{T}"/>, giving access to game rows.</returns>
public ExcelSheet<T>? GetExcelSheet<T>() where T : ExcelRow
{
return this.Excel.GetSheet<T>();
}
/// <summary>
/// Get an <see cref="ExcelSheet{T}"/> with the given Excel sheet row type with a specified language.
/// </summary>
/// <param name="language">Language of the sheet to get.</param>
/// <typeparam name="T">The excel sheet type to get.</typeparam>
/// <returns>The <see cref="ExcelSheet{T}"/>, giving access to game rows.</returns>
public ExcelSheet<T>? GetExcelSheet<T>(ClientLanguage language) where T : ExcelRow
{
return this.Excel.GetSheet<T>(language.ToLumina());
}
/// <summary>
/// Get a <see cref="FileResource"/> with the given path.
/// </summary>
/// <param name="path">The path inside of the game files.</param>
/// <returns>The <see cref="FileResource"/> of the file.</returns>
public FileResource? GetFile(string path)
{
return this.GetFile<FileResource>(path);
}
/// <summary>
/// Get a <see cref="FileResource"/> with the given path, of the given type.
/// </summary>
/// <typeparam name="T">The type of resource.</typeparam>
/// <param name="path">The path inside of the game files.</param>
/// <returns>The <see cref="FileResource"/> of the file.</returns>
public T? GetFile<T>(string path) where T : FileResource
{
var filePath = GameData.ParseFilePath(path);
if (filePath == null)
return default;
return this.GameData.Repositories.TryGetValue(filePath.Repository, out var repository) ? repository.GetFile<T>(filePath.Category, filePath) : default;
}
/// <summary>
/// Check if the file with the given path exists within the game's index files.
/// </summary>
/// <param name="path">The path inside of the game files.</param>
/// <returns>True if the file exists.</returns>
public bool FileExists(string path)
{
return this.GameData.FileExists(path);
}
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetIcon(uint iconId)
{
return this.GetIcon(this.Language, iconId);
}
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID, of the given quality.
/// </summary>
/// <param name="isHq">A value indicating whether the icon should be HQ.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetIcon(bool isHq, uint iconId)
{
var type = isHq ? "hq/" : string.Empty;
return this.GetIcon(type, iconId);
}
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID, of the given language.
/// </summary>
/// <param name="iconLanguage">The requested language.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId)
{
var type = iconLanguage switch
{ {
ClientLanguage.Japanese => "ja/", this.Language = Service<DalamudStartInfo>.Get().Language;
ClientLanguage.English => "en/",
ClientLanguage.German => "de/",
ClientLanguage.French => "fr/",
_ => throw new ArgumentOutOfRangeException(nameof(iconLanguage), $"Unknown Language: {iconLanguage}"),
};
return this.GetIcon(type, iconId); // Set up default values so plugins do not null-reference when data is being loaded.
} this.ClientOpCodes = this.ServerOpCodes = new ReadOnlyDictionary<string, ushort>(new Dictionary<string, ushort>());
}
/// <summary> /// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID, of the given type. /// Gets the current game client language.
/// </summary> /// </summary>
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param> public ClientLanguage Language { get; private set; }
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetIcon(string type, uint iconId)
{
type ??= string.Empty;
if (type.Length > 0 && !type.EndsWith("/"))
type += "/";
var filePath = string.Format(IconFileFormat, iconId / 1000, type, iconId); /// <summary>
var file = this.GetFile<TexFile>(filePath); /// Gets the OpCodes sent by the server to the client.
/// </summary>
public ReadOnlyDictionary<string, ushort> ServerOpCodes { get; private set; }
if (type == string.Empty || file != default) /// <summary>
return file; /// Gets the OpCodes sent by the client to the server.
/// </summary>
[UsedImplicitly]
public ReadOnlyDictionary<string, ushort> ClientOpCodes { get; private set; }
// Couldn't get specific type, try for generic version. /// <summary>
filePath = string.Format(IconFileFormat, iconId / 1000, string.Empty, iconId); /// Gets a <see cref="Lumina"/> object which gives access to any excel/game data.
file = this.GetFile<TexFile>(filePath); /// </summary>
return file; public GameData GameData { get; private set; }
}
/// <summary> /// <summary>
/// Get a <see cref="TexFile"/> containing the HQ icon with the given ID. /// Gets an <see cref="ExcelModule"/> object which gives access to any of the game's sheet data.
/// </summary> /// </summary>
/// <param name="iconId">The icon ID.</param> public ExcelModule Excel => this.GameData?.Excel;
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetHqIcon(uint iconId)
=> this.GetIcon(true, iconId);
/// <summary> /// <summary>
/// Get the passed <see cref="TexFile"/> as a drawable ImGui TextureWrap. /// Gets a value indicating whether Game Data is ready to be read.
/// </summary> /// </summary>
/// <param name="tex">The Lumina <see cref="TexFile"/>.</param> public bool IsDataReady { get; private set; }
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns>
public TextureWrap? GetImGuiTexture(TexFile? tex)
{
return tex == null ? null : Service<InterfaceManager>.Get().LoadImageRaw(tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height, 4);
}
/// <summary> #region Lumina Wrappers
/// Get the passed texture path as a drawable ImGui TextureWrap.
/// </summary>
/// <param name="path">The internal path to the texture.</param>
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns>
public TextureWrap? GetImGuiTexture(string path)
=> this.GetImGuiTexture(this.GetFile<TexFile>(path));
/// <summary> /// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID. /// Get an <see cref="ExcelSheet{T}"/> with the given Excel sheet row type.
/// </summary> /// </summary>
/// <param name="iconId">The icon ID.</param> /// <typeparam name="T">The excel sheet type to get.</typeparam>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns> /// <returns>The <see cref="ExcelSheet{T}"/>, giving access to game rows.</returns>
public TextureWrap? GetImGuiTextureIcon(uint iconId) public ExcelSheet<T>? GetExcelSheet<T>() where T : ExcelRow
=> this.GetImGuiTexture(this.GetIcon(iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given quality.
/// </summary>
/// <param name="isHq">A value indicating whether the icon should be HQ.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(isHq, iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given language.
/// </summary>
/// <param name="iconLanguage">The requested language.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(iconLanguage, iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given type.
/// </summary>
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(string type, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(type, iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the HQ icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureHqIcon(uint iconId)
=> this.GetImGuiTexture(this.GetHqIcon(iconId));
#endregion
/// <summary>
/// Dispose this DataManager.
/// </summary>
public void Dispose()
{
this.luminaCancellationTokenSource.Cancel();
}
/// <summary>
/// Initialize this data manager.
/// </summary>
/// <param name="baseDir">The directory to load data from.</param>
internal void Initialize(string baseDir)
{
try
{ {
Log.Verbose("Starting data load..."); return this.Excel.GetSheet<T>();
}
var zoneOpCodeDict = JsonConvert.DeserializeObject<Dictionary<string, ushort>>( /// <summary>
File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json"))); /// Get an <see cref="ExcelSheet{T}"/> with the given Excel sheet row type with a specified language.
this.ServerOpCodes = new ReadOnlyDictionary<string, ushort>(zoneOpCodeDict); /// </summary>
/// <param name="language">Language of the sheet to get.</param>
/// <typeparam name="T">The excel sheet type to get.</typeparam>
/// <returns>The <see cref="ExcelSheet{T}"/>, giving access to game rows.</returns>
public ExcelSheet<T>? GetExcelSheet<T>(ClientLanguage language) where T : ExcelRow
{
return this.Excel.GetSheet<T>(language.ToLumina());
}
Log.Verbose("Loaded {0} ServerOpCodes.", zoneOpCodeDict.Count); /// <summary>
/// Get a <see cref="FileResource"/> with the given path.
/// </summary>
/// <param name="path">The path inside of the game files.</param>
/// <returns>The <see cref="FileResource"/> of the file.</returns>
public FileResource? GetFile(string path)
{
return this.GetFile<FileResource>(path);
}
var clientOpCodeDict = JsonConvert.DeserializeObject<Dictionary<string, ushort>>( /// <summary>
File.ReadAllText(Path.Combine(baseDir, "UIRes", "clientopcode.json"))); /// Get a <see cref="FileResource"/> with the given path, of the given type.
this.ClientOpCodes = new ReadOnlyDictionary<string, ushort>(clientOpCodeDict); /// </summary>
/// <typeparam name="T">The type of resource.</typeparam>
/// <param name="path">The path inside of the game files.</param>
/// <returns>The <see cref="FileResource"/> of the file.</returns>
public T? GetFile<T>(string path) where T : FileResource
{
var filePath = GameData.ParseFilePath(path);
if (filePath == null)
return default;
return this.GameData.Repositories.TryGetValue(filePath.Repository, out var repository) ? repository.GetFile<T>(filePath.Category, filePath) : default;
}
Log.Verbose("Loaded {0} ClientOpCodes.", clientOpCodeDict.Count); /// <summary>
/// Check if the file with the given path exists within the game's index files.
/// </summary>
/// <param name="path">The path inside of the game files.</param>
/// <returns>True if the file exists.</returns>
public bool FileExists(string path)
{
return this.GameData.FileExists(path);
}
var luminaOptions = new LuminaOptions /// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetIcon(uint iconId)
{
return this.GetIcon(this.Language, iconId);
}
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID, of the given quality.
/// </summary>
/// <param name="isHq">A value indicating whether the icon should be HQ.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetIcon(bool isHq, uint iconId)
{
var type = isHq ? "hq/" : string.Empty;
return this.GetIcon(type, iconId);
}
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID, of the given language.
/// </summary>
/// <param name="iconLanguage">The requested language.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId)
{
var type = iconLanguage switch
{ {
CacheFileResources = true, ClientLanguage.Japanese => "ja/",
ClientLanguage.English => "en/",
ClientLanguage.German => "de/",
ClientLanguage.French => "fr/",
_ => throw new ArgumentOutOfRangeException(nameof(iconLanguage), $"Unknown Language: {iconLanguage}"),
};
return this.GetIcon(type, iconId);
}
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID, of the given type.
/// </summary>
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetIcon(string type, uint iconId)
{
type ??= string.Empty;
if (type.Length > 0 && !type.EndsWith("/"))
type += "/";
var filePath = string.Format(IconFileFormat, iconId / 1000, type, iconId);
var file = this.GetFile<TexFile>(filePath);
if (type == string.Empty || file != default)
return file;
// Couldn't get specific type, try for generic version.
filePath = string.Format(IconFileFormat, iconId / 1000, string.Empty, iconId);
file = this.GetFile<TexFile>(filePath);
return file;
}
/// <summary>
/// Get a <see cref="TexFile"/> containing the HQ icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetHqIcon(uint iconId)
=> this.GetIcon(true, iconId);
/// <summary>
/// Get the passed <see cref="TexFile"/> as a drawable ImGui TextureWrap.
/// </summary>
/// <param name="tex">The Lumina <see cref="TexFile"/>.</param>
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns>
public TextureWrap? GetImGuiTexture(TexFile? tex)
{
return tex == null ? null : Service<InterfaceManager>.Get().LoadImageRaw(tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height, 4);
}
/// <summary>
/// Get the passed texture path as a drawable ImGui TextureWrap.
/// </summary>
/// <param name="path">The internal path to the texture.</param>
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns>
public TextureWrap? GetImGuiTexture(string path)
=> this.GetImGuiTexture(this.GetFile<TexFile>(path));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(uint iconId)
=> this.GetImGuiTexture(this.GetIcon(iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given quality.
/// </summary>
/// <param name="isHq">A value indicating whether the icon should be HQ.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(isHq, iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given language.
/// </summary>
/// <param name="iconLanguage">The requested language.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(iconLanguage, iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given type.
/// </summary>
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(string type, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(type, iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the HQ icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureHqIcon(uint iconId)
=> this.GetImGuiTexture(this.GetHqIcon(iconId));
#endregion
/// <summary>
/// Dispose this DataManager.
/// </summary>
public void Dispose()
{
this.luminaCancellationTokenSource.Cancel();
}
/// <summary>
/// Initialize this data manager.
/// </summary>
/// <param name="baseDir">The directory to load data from.</param>
internal void Initialize(string baseDir)
{
try
{
Log.Verbose("Starting data load...");
var zoneOpCodeDict = JsonConvert.DeserializeObject<Dictionary<string, ushort>>(
File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json")));
this.ServerOpCodes = new ReadOnlyDictionary<string, ushort>(zoneOpCodeDict);
Log.Verbose("Loaded {0} ServerOpCodes.", zoneOpCodeDict.Count);
var clientOpCodeDict = JsonConvert.DeserializeObject<Dictionary<string, ushort>>(
File.ReadAllText(Path.Combine(baseDir, "UIRes", "clientopcode.json")));
this.ClientOpCodes = new ReadOnlyDictionary<string, ushort>(clientOpCodeDict);
Log.Verbose("Loaded {0} ClientOpCodes.", clientOpCodeDict.Count);
var luminaOptions = new LuminaOptions
{
CacheFileResources = true,
#if DEBUG #if DEBUG
PanicOnSheetChecksumMismatch = true, PanicOnSheetChecksumMismatch = true,
#else #else
PanicOnSheetChecksumMismatch = false, PanicOnSheetChecksumMismatch = false,
#endif #endif
DefaultExcelLanguage = this.Language.ToLumina(), DefaultExcelLanguage = this.Language.ToLumina(),
}; };
var processModule = Process.GetCurrentProcess().MainModule; var processModule = Process.GetCurrentProcess().MainModule;
if (processModule != null) if (processModule != null)
{
this.GameData = new GameData(Path.Combine(Path.GetDirectoryName(processModule.FileName), "sqpack"), luminaOptions);
}
Log.Information("Lumina is ready: {0}", this.GameData.DataPath);
this.IsDataReady = true;
this.luminaCancellationTokenSource = new();
var luminaCancellationToken = this.luminaCancellationTokenSource.Token;
this.luminaResourceThread = new(() =>
{
while (!luminaCancellationToken.IsCancellationRequested)
{ {
if (this.GameData.FileHandleManager.HasPendingFileLoads) this.GameData = new GameData(Path.Combine(Path.GetDirectoryName(processModule.FileName), "sqpack"), luminaOptions);
{
this.GameData.ProcessFileHandleQueue();
}
else
{
Thread.Sleep(5);
}
} }
});
this.luminaResourceThread.Start(); Log.Information("Lumina is ready: {0}", this.GameData.DataPath);
}
catch (Exception ex) this.IsDataReady = true;
{
Log.Error(ex, "Could not download data."); this.luminaCancellationTokenSource = new();
var luminaCancellationToken = this.luminaCancellationTokenSource.Token;
this.luminaResourceThread = new(() =>
{
while (!luminaCancellationToken.IsCancellationRequested)
{
if (this.GameData.FileHandleManager.HasPendingFileLoads)
{
this.GameData.ProcessFileHandleQueue();
}
else
{
Thread.Sleep(5);
}
}
});
this.luminaResourceThread.Start();
}
catch (Exception ex)
{
Log.Error(ex, "Could not download data.");
}
} }
} }
} }

View file

@ -21,261 +21,262 @@ using Serilog.Events;
using static Dalamud.NativeFunctions; using static Dalamud.NativeFunctions;
namespace Dalamud; namespace Dalamud
/// <summary>
/// The main entrypoint for the Dalamud system.
/// </summary>
public sealed class EntryPoint
{ {
/// <summary> /// <summary>
/// A delegate used during initialization of the CLR from Dalamud.Boot. /// The main entrypoint for the Dalamud system.
/// </summary> /// </summary>
/// <param name="infoPtr">Pointer to a serialized <see cref="DalamudStartInfo"/> data.</param> public sealed class EntryPoint
public delegate void InitDelegate(IntPtr infoPtr);
/// <summary>
/// Initialize Dalamud.
/// </summary>
/// <param name="infoPtr">Pointer to a serialized <see cref="DalamudStartInfo"/> data.</param>
public static void Initialize(IntPtr infoPtr)
{ {
var infoStr = Marshal.PtrToStringUTF8(infoPtr); /// <summary>
var info = JsonConvert.DeserializeObject<DalamudStartInfo>(infoStr); /// A delegate used during initialization of the CLR from Dalamud.Boot.
/// </summary>
/// <param name="infoPtr">Pointer to a serialized <see cref="DalamudStartInfo"/> data.</param>
public delegate void InitDelegate(IntPtr infoPtr);
new Thread(() => RunThread(info)).Start(); /// <summary>
} /// Initialize Dalamud.
/// </summary>
/// <summary> /// <param name="infoPtr">Pointer to a serialized <see cref="DalamudStartInfo"/> data.</param>
/// Initialize all Dalamud subsystems and start running on the main thread. public static void Initialize(IntPtr infoPtr)
/// </summary>
/// <param name="info">The <see cref="DalamudStartInfo"/> containing information needed to initialize Dalamud.</param>
private static void RunThread(DalamudStartInfo info)
{
if (EnvironmentConfiguration.DalamudWaitForDebugger)
{ {
while (!Debugger.IsAttached) var infoStr = Marshal.PtrToStringUTF8(infoPtr);
{ var info = JsonConvert.DeserializeObject<DalamudStartInfo>(infoStr);
Thread.Sleep(100);
} new Thread(() => RunThread(info)).Start();
} }
// Setup logger /// <summary>
var levelSwitch = InitLogging(info.WorkingDirectory); /// Initialize all Dalamud subsystems and start running on the main thread.
/// </summary>
/// <param name="info">The <see cref="DalamudStartInfo"/> containing information needed to initialize Dalamud.</param>
private static void RunThread(DalamudStartInfo info)
{
if (EnvironmentConfiguration.DalamudWaitForDebugger)
{
while (!Debugger.IsAttached)
{
Thread.Sleep(100);
}
}
// Load configuration first to get some early persistent state, like log level // Setup logger
var configuration = DalamudConfiguration.Load(info.ConfigurationPath); var levelSwitch = InitLogging(info.WorkingDirectory);
// Set the appropriate logging level from the configuration // Load configuration first to get some early persistent state, like log level
var configuration = DalamudConfiguration.Load(info.ConfigurationPath);
// Set the appropriate logging level from the configuration
#if !DEBUG #if !DEBUG
levelSwitch.MinimumLevel = configuration.LogLevel; levelSwitch.MinimumLevel = configuration.LogLevel;
#endif #endif
// Log any unhandled exception. // Log any unhandled exception.
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
var finishSignal = new ManualResetEvent(false); var finishSignal = new ManualResetEvent(false);
try try
{
if (info.DelayInitializeMs > 0)
{ {
Log.Information(string.Format("Waiting for {0}ms before starting a session.", info.DelayInitializeMs)); if (info.DelayInitializeMs > 0)
Thread.Sleep(info.DelayInitializeMs); {
Log.Information(string.Format("Waiting for {0}ms before starting a session.", info.DelayInitializeMs));
Thread.Sleep(info.DelayInitializeMs);
}
Log.Information(new string('-', 80));
Log.Information("Initializing a session..");
// 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.IsLinux())
InitSymbolHandler(info);
var dalamud = new Dalamud(info, levelSwitch, finishSignal, configuration);
Log.Information("Starting a session..");
// Run session
dalamud.LoadTier1();
dalamud.WaitForUnload();
dalamud.Dispose();
} }
catch (Exception ex)
{
Log.Fatal(ex, "Unhandled exception on main thread.");
}
finally
{
TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;
Log.Information(new string('-', 80)); Log.Information("Session has ended.");
Log.Information("Initializing a session.."); Log.CloseAndFlush();
// This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally finishSignal.Set();
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls; }
if (!Util.IsLinux())
InitSymbolHandler(info);
var dalamud = new Dalamud(info, levelSwitch, finishSignal, configuration);
Log.Information("Starting a session..");
// Run session
dalamud.LoadTier1();
dalamud.WaitForUnload();
dalamud.Dispose();
} }
catch (Exception ex)
private static void InitSymbolHandler(DalamudStartInfo info)
{ {
Log.Fatal(ex, "Unhandled exception on main thread."); try
{
if (string.IsNullOrEmpty(info.AssetDirectory))
return;
var symbolPath = Path.Combine(info.AssetDirectory, "UIRes", "pdb");
var searchPath = $".;{symbolPath}";
// Remove any existing Symbol Handler and Init a new one with our search path added
SymCleanup(GetCurrentProcess());
if (!SymInitialize(GetCurrentProcess(), searchPath, true))
throw new Win32Exception();
}
catch (Exception ex)
{
Log.Error(ex, "SymbolHandler Initialize Failed.");
}
} }
finally
private static LoggingLevelSwitch InitLogging(string baseDirectory)
{ {
TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;
Log.Information("Session has ended.");
Log.CloseAndFlush();
finishSignal.Set();
}
}
private static void InitSymbolHandler(DalamudStartInfo info)
{
try
{
if (string.IsNullOrEmpty(info.AssetDirectory))
return;
var symbolPath = Path.Combine(info.AssetDirectory, "UIRes", "pdb");
var searchPath = $".;{symbolPath}";
// Remove any existing Symbol Handler and Init a new one with our search path added
SymCleanup(GetCurrentProcess());
if (!SymInitialize(GetCurrentProcess(), searchPath, true))
throw new Win32Exception();
}
catch (Exception ex)
{
Log.Error(ex, "SymbolHandler Initialize Failed.");
}
}
private static LoggingLevelSwitch InitLogging(string baseDirectory)
{
#if DEBUG #if DEBUG
var logPath = Path.Combine(baseDirectory, "dalamud.log"); var logPath = Path.Combine(baseDirectory, "dalamud.log");
var oldPath = Path.Combine(baseDirectory, "dalamud.log.old"); var oldPath = Path.Combine(baseDirectory, "dalamud.log.old");
#else #else
var logPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log"); var logPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log");
var oldPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log.old"); var oldPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log.old");
#endif #endif
CullLogFile(logPath, oldPath, 1 * 1024 * 1024); CullLogFile(logPath, oldPath, 1 * 1024 * 1024);
CullLogFile(oldPath, null, 10 * 1024 * 1024); CullLogFile(oldPath, null, 10 * 1024 * 1024);
var levelSwitch = new LoggingLevelSwitch(LogEventLevel.Verbose); var levelSwitch = new LoggingLevelSwitch(LogEventLevel.Verbose);
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
.WriteTo.Async(a => a.File(logPath)) .WriteTo.Async(a => a.File(logPath))
.WriteTo.Sink(SerilogEventSink.Instance) .WriteTo.Sink(SerilogEventSink.Instance)
.MinimumLevel.ControlledBy(levelSwitch) .MinimumLevel.ControlledBy(levelSwitch)
.CreateLogger(); .CreateLogger();
return levelSwitch; return levelSwitch;
} }
private static void CullLogFile(string logPath, string? oldPath, int cullingFileSize) private static void CullLogFile(string logPath, string? oldPath, int cullingFileSize)
{
try
{ {
var bufferSize = 4096; try
var logFile = new FileInfo(logPath);
if (!logFile.Exists)
logFile.Create();
if (logFile.Length <= cullingFileSize)
return;
var amountToCull = logFile.Length - cullingFileSize;
if (amountToCull < bufferSize)
return;
if (oldPath != null)
{ {
var oldFile = new FileInfo(oldPath); var bufferSize = 4096;
if (!oldFile.Exists) var logFile = new FileInfo(logPath);
oldFile.Create().Close();
using var reader = new BinaryReader(logFile.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); if (!logFile.Exists)
using var writer = new BinaryWriter(oldFile.Open(FileMode.Append, FileAccess.Write, FileShare.ReadWrite)); logFile.Create();
var read = -1; if (logFile.Length <= cullingFileSize)
var total = 0; return;
var buffer = new byte[bufferSize];
while (read != 0 && total < amountToCull) var amountToCull = logFile.Length - cullingFileSize;
if (amountToCull < bufferSize)
return;
if (oldPath != null)
{ {
read = reader.Read(buffer, 0, buffer.Length); var oldFile = new FileInfo(oldPath);
writer.Write(buffer, 0, read);
total += read; if (!oldFile.Exists)
oldFile.Create().Close();
using var reader = new BinaryReader(logFile.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite));
using var writer = new BinaryWriter(oldFile.Open(FileMode.Append, FileAccess.Write, FileShare.ReadWrite));
var read = -1;
var total = 0;
var buffer = new byte[bufferSize];
while (read != 0 && total < amountToCull)
{
read = reader.Read(buffer, 0, buffer.Length);
writer.Write(buffer, 0, read);
total += read;
}
}
{
using var reader = new BinaryReader(logFile.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite));
using var writer = new BinaryWriter(logFile.Open(FileMode.Open, FileAccess.Write, FileShare.ReadWrite));
reader.BaseStream.Seek(amountToCull, SeekOrigin.Begin);
var read = -1;
var total = 0;
var buffer = new byte[bufferSize];
while (read != 0)
{
read = reader.Read(buffer, 0, buffer.Length);
writer.Write(buffer, 0, read);
total += read;
}
writer.BaseStream.SetLength(total);
} }
} }
catch (Exception ex)
{ {
using var reader = new BinaryReader(logFile.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite)); Log.Error(ex, "Log cull failed");
using var writer = new BinaryWriter(logFile.Open(FileMode.Open, FileAccess.Write, FileShare.ReadWrite));
reader.BaseStream.Seek(amountToCull, SeekOrigin.Begin); /*
var caption = "XIVLauncher Error";
var read = -1; var message = $"Log cull threw an exception: {ex.Message}\n{ex.StackTrace ?? string.Empty}";
var total = 0; _ = MessageBoxW(IntPtr.Zero, message, caption, MessageBoxType.IconError | MessageBoxType.Ok);
var buffer = new byte[bufferSize]; */
while (read != 0)
{
read = reader.Read(buffer, 0, buffer.Length);
writer.Write(buffer, 0, read);
total += read;
}
writer.BaseStream.SetLength(total);
} }
} }
catch (Exception ex)
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs args)
{ {
Log.Error(ex, "Log cull failed"); switch (args.ExceptionObject)
{
case Exception ex:
Log.Fatal(ex, "Unhandled exception on AppDomain");
Troubleshooting.LogException(ex, "DalamudUnhandled");
/* var info = "Further information could not be obtained";
var caption = "XIVLauncher Error"; if (ex.TargetSite != null && ex.TargetSite.DeclaringType != null)
var message = $"Log cull threw an exception: {ex.Message}\n{ex.StackTrace ?? string.Empty}"; {
_ = MessageBoxW(IntPtr.Zero, message, caption, MessageBoxType.IconError | MessageBoxType.Ok); info = $"{ex.TargetSite.DeclaringType.Assembly.GetName().Name}, {ex.TargetSite.DeclaringType.FullName}::{ex.TargetSite.Name}";
*/ }
const MessageBoxType flags = NativeFunctions.MessageBoxType.YesNo | NativeFunctions.MessageBoxType.IconError | NativeFunctions.MessageBoxType.SystemModal;
var result = MessageBoxW(
Process.GetCurrentProcess().MainWindowHandle,
$"An internal error in a Dalamud plugin occurred.\nThe game must close.\n\nType: {ex.GetType().Name}\n{info}\n\nMore information has been recorded separately, please contact us in our Discord or on GitHub.\n\nDo you want to disable all plugins the next time you start the game?",
"Dalamud",
flags);
if (result == (int)User32.MessageBoxResult.IDYES)
{
Log.Information("User chose to disable plugins on next launch...");
var config = Service<DalamudConfiguration>.Get();
config.PluginSafeMode = true;
config.Save();
}
Environment.Exit(-1);
break;
default:
Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject);
break;
}
} }
}
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs args) private static void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs args)
{
switch (args.ExceptionObject)
{ {
case Exception ex: if (!args.Observed)
Log.Fatal(ex, "Unhandled exception on AppDomain"); Log.Error(args.Exception, "Unobserved exception in Task.");
Troubleshooting.LogException(ex, "DalamudUnhandled");
var info = "Further information could not be obtained";
if (ex.TargetSite != null && ex.TargetSite.DeclaringType != null)
{
info = $"{ex.TargetSite.DeclaringType.Assembly.GetName().Name}, {ex.TargetSite.DeclaringType.FullName}::{ex.TargetSite.Name}";
}
const MessageBoxType flags = NativeFunctions.MessageBoxType.YesNo | NativeFunctions.MessageBoxType.IconError | NativeFunctions.MessageBoxType.SystemModal;
var result = MessageBoxW(
Process.GetCurrentProcess().MainWindowHandle,
$"An internal error in a Dalamud plugin occurred.\nThe game must close.\n\nType: {ex.GetType().Name}\n{info}\n\nMore information has been recorded separately, please contact us in our Discord or on GitHub.\n\nDo you want to disable all plugins the next time you start the game?",
"Dalamud",
flags);
if (result == (int)User32.MessageBoxResult.IDYES)
{
Log.Information("User chose to disable plugins on next launch...");
var config = Service<DalamudConfiguration>.Get();
config.PluginSafeMode = true;
config.Save();
}
Environment.Exit(-1);
break;
default:
Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject);
break;
} }
} }
private static void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs args)
{
if (!args.Observed)
Log.Error(args.Exception, "Unobserved exception in Task.");
}
} }

View file

@ -3,111 +3,112 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Dalamud.Game; namespace Dalamud.Game
/// <summary>
/// Base memory address resolver.
/// </summary>
public abstract class BaseAddressResolver
{ {
/// <summary> /// <summary>
/// Gets a list of memory addresses that were found, to list in /xldata. /// Base memory address resolver.
/// </summary> /// </summary>
public static Dictionary<string, List<(string ClassName, IntPtr Address)>> DebugScannedValues { get; } = new(); public abstract class BaseAddressResolver
/// <summary>
/// Gets or sets a value indicating whether the resolver has successfully run <see cref="Setup32Bit(SigScanner)"/> or <see cref="Setup64Bit(SigScanner)"/>.
/// </summary>
protected bool IsResolved { get; set; }
/// <summary>
/// Setup the resolver, calling the appopriate method based on the process architecture.
/// </summary>
public void Setup()
{ {
var scanner = Service<SigScanner>.Get(); /// <summary>
this.Setup(scanner); /// Gets a list of memory addresses that were found, to list in /xldata.
} /// </summary>
public static Dictionary<string, List<(string ClassName, IntPtr Address)>> DebugScannedValues { get; } = new();
/// <summary> /// <summary>
/// Setup the resolver, calling the appopriate method based on the process architecture. /// Gets or sets a value indicating whether the resolver has successfully run <see cref="Setup32Bit(SigScanner)"/> or <see cref="Setup64Bit(SigScanner)"/>.
/// </summary> /// </summary>
/// <param name="scanner">The SigScanner instance.</param> protected bool IsResolved { get; set; }
public void Setup(SigScanner scanner)
{
// Because C# don't allow to call virtual function while in ctor
// we have to do this shit :\
if (this.IsResolved) /// <summary>
/// Setup the resolver, calling the appopriate method based on the process architecture.
/// </summary>
public void Setup()
{ {
return; var scanner = Service<SigScanner>.Get();
this.Setup(scanner);
} }
if (scanner.Is32BitProcess) /// <summary>
/// Setup the resolver, calling the appopriate method based on the process architecture.
/// </summary>
/// <param name="scanner">The SigScanner instance.</param>
public void Setup(SigScanner scanner)
{ {
this.Setup32Bit(scanner); // Because C# don't allow to call virtual function while in ctor
} // we have to do this shit :\
else
{ if (this.IsResolved)
this.Setup64Bit(scanner); {
return;
}
if (scanner.Is32BitProcess)
{
this.Setup32Bit(scanner);
}
else
{
this.Setup64Bit(scanner);
}
this.SetupInternal(scanner);
var className = this.GetType().Name;
DebugScannedValues[className] = new List<(string, IntPtr)>();
foreach (var property in this.GetType().GetProperties().Where(x => x.PropertyType == typeof(IntPtr)))
{
DebugScannedValues[className].Add((property.Name, (IntPtr)property.GetValue(this)));
}
this.IsResolved = true;
} }
this.SetupInternal(scanner); /// <summary>
/// Fetch vfunc N from a pointer to the vtable and return a delegate function pointer.
var className = this.GetType().Name; /// </summary>
DebugScannedValues[className] = new List<(string, IntPtr)>(); /// <typeparam name="T">The delegate to marshal the function pointer to.</typeparam>
/// <param name="address">The address of the virtual table.</param>
foreach (var property in this.GetType().GetProperties().Where(x => x.PropertyType == typeof(IntPtr))) /// <param name="vtableOffset">The offset from address to the vtable pointer.</param>
/// <param name="count">The vfunc index.</param>
/// <returns>A delegate function pointer that can be invoked.</returns>
public T GetVirtualFunction<T>(IntPtr address, int vtableOffset, int count) where T : class
{ {
DebugScannedValues[className].Add((property.Name, (IntPtr)property.GetValue(this))); // Get vtable
var vtable = Marshal.ReadIntPtr(address, vtableOffset);
// Get an address to the function
var functionAddress = Marshal.ReadIntPtr(vtable, IntPtr.Size * count);
return Marshal.GetDelegateForFunctionPointer<T>(functionAddress);
} }
this.IsResolved = true; /// <summary>
} /// Setup the resolver by finding any necessary memory addresses.
/// </summary>
/// <param name="scanner">The SigScanner instance.</param>
protected virtual void Setup32Bit(SigScanner scanner)
{
throw new NotSupportedException("32 bit version is not supported.");
}
/// <summary> /// <summary>
/// Fetch vfunc N from a pointer to the vtable and return a delegate function pointer. /// Setup the resolver by finding any necessary memory addresses.
/// </summary> /// </summary>
/// <typeparam name="T">The delegate to marshal the function pointer to.</typeparam> /// <param name="scanner">The SigScanner instance.</param>
/// <param name="address">The address of the virtual table.</param> protected virtual void Setup64Bit(SigScanner scanner)
/// <param name="vtableOffset">The offset from address to the vtable pointer.</param> {
/// <param name="count">The vfunc index.</param> throw new NotSupportedException("64 bit version is not supported.");
/// <returns>A delegate function pointer that can be invoked.</returns> }
public T GetVirtualFunction<T>(IntPtr address, int vtableOffset, int count) where T : class
{
// Get vtable
var vtable = Marshal.ReadIntPtr(address, vtableOffset);
// Get an address to the function /// <summary>
var functionAddress = Marshal.ReadIntPtr(vtable, IntPtr.Size * count); /// Setup the resolver by finding any necessary memory addresses.
/// </summary>
return Marshal.GetDelegateForFunctionPointer<T>(functionAddress); /// <param name="scanner">The SigScanner instance.</param>
} protected virtual void SetupInternal(SigScanner scanner)
{
/// <summary> // Do nothing
/// Setup the resolver by finding any necessary memory addresses. }
/// </summary>
/// <param name="scanner">The SigScanner instance.</param>
protected virtual void Setup32Bit(SigScanner scanner)
{
throw new NotSupportedException("32 bit version is not supported.");
}
/// <summary>
/// Setup the resolver by finding any necessary memory addresses.
/// </summary>
/// <param name="scanner">The SigScanner instance.</param>
protected virtual void Setup64Bit(SigScanner scanner)
{
throw new NotSupportedException("64 bit version is not supported.");
}
/// <summary>
/// Setup the resolver by finding any necessary memory addresses.
/// </summary>
/// <param name="scanner">The SigScanner instance.</param>
protected virtual void SetupInternal(SigScanner scanner)
{
// Do nothing
} }
} }

View file

@ -21,298 +21,298 @@ using Dalamud.Plugin.Internal;
using Dalamud.Utility; using Dalamud.Utility;
using Serilog; using Serilog;
namespace Dalamud.Game; namespace Dalamud.Game
/// <summary>
/// Chat events and public helper functions.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public class ChatHandlers
{ {
// private static readonly Dictionary<string, string> UnicodeToDiscordEmojiDict = new() /// <summary>
// { /// Chat events and public helper functions.
// { "", "<:ffxive071:585847382210642069>" }, /// </summary>
// { "", "<:ffxive083:585848592699490329>" }, [PluginInterface]
// }; [InterfaceVersion("1.0")]
public class ChatHandlers
// private readonly Dictionary<XivChatType, Color> handledChatTypeColors = new()
// {
// { XivChatType.CrossParty, Color.DodgerBlue },
// { XivChatType.Party, Color.DodgerBlue },
// { XivChatType.FreeCompany, Color.DeepSkyBlue },
// { XivChatType.CrossLinkShell1, Color.ForestGreen },
// { XivChatType.CrossLinkShell2, Color.ForestGreen },
// { XivChatType.CrossLinkShell3, Color.ForestGreen },
// { XivChatType.CrossLinkShell4, Color.ForestGreen },
// { XivChatType.CrossLinkShell5, Color.ForestGreen },
// { XivChatType.CrossLinkShell6, Color.ForestGreen },
// { XivChatType.CrossLinkShell7, Color.ForestGreen },
// { XivChatType.CrossLinkShell8, Color.ForestGreen },
// { XivChatType.Ls1, Color.ForestGreen },
// { XivChatType.Ls2, Color.ForestGreen },
// { XivChatType.Ls3, Color.ForestGreen },
// { XivChatType.Ls4, Color.ForestGreen },
// { XivChatType.Ls5, Color.ForestGreen },
// { XivChatType.Ls6, Color.ForestGreen },
// { XivChatType.Ls7, Color.ForestGreen },
// { XivChatType.Ls8, Color.ForestGreen },
// { XivChatType.TellIncoming, Color.HotPink },
// { XivChatType.PvPTeam, Color.SandyBrown },
// { XivChatType.Urgent, Color.DarkViolet },
// { XivChatType.NoviceNetwork, Color.SaddleBrown },
// { XivChatType.Echo, Color.Gray },
// };
private readonly Regex rmtRegex = new(
@"4KGOLD|We have sufficient stock|VPK\.OM|Gil for free|www\.so9\.com|Fast & Convenient|Cheap & Safety Guarantee|【Code|A O A U E|igfans|4KGOLD\.COM|Cheapest Gil with|pvp and bank on google|Selling Cheap GIL|ff14mogstation\.com|Cheap Gil 1000k|gilsforyou|server 1000K =|gils_selling|E A S Y\.C O M|bonus code|mins delivery guarantee|Sell cheap|Salegm\.com|cheap Mog|Off Code:|FF14Mog.com|使用する5オ|Off Code( *):|offers Fantasia",
RegexOptions.Compiled);
private readonly Dictionary<ClientLanguage, Regex[]> retainerSaleRegexes = new()
{ {
// private static readonly Dictionary<string, string> UnicodeToDiscordEmojiDict = new()
// {
// { "", "<:ffxive071:585847382210642069>" },
// { "", "<:ffxive083:585848592699490329>" },
// };
// private readonly Dictionary<XivChatType, Color> handledChatTypeColors = new()
// {
// { XivChatType.CrossParty, Color.DodgerBlue },
// { XivChatType.Party, Color.DodgerBlue },
// { XivChatType.FreeCompany, Color.DeepSkyBlue },
// { XivChatType.CrossLinkShell1, Color.ForestGreen },
// { XivChatType.CrossLinkShell2, Color.ForestGreen },
// { XivChatType.CrossLinkShell3, Color.ForestGreen },
// { XivChatType.CrossLinkShell4, Color.ForestGreen },
// { XivChatType.CrossLinkShell5, Color.ForestGreen },
// { XivChatType.CrossLinkShell6, Color.ForestGreen },
// { XivChatType.CrossLinkShell7, Color.ForestGreen },
// { XivChatType.CrossLinkShell8, Color.ForestGreen },
// { XivChatType.Ls1, Color.ForestGreen },
// { XivChatType.Ls2, Color.ForestGreen },
// { XivChatType.Ls3, Color.ForestGreen },
// { XivChatType.Ls4, Color.ForestGreen },
// { XivChatType.Ls5, Color.ForestGreen },
// { XivChatType.Ls6, Color.ForestGreen },
// { XivChatType.Ls7, Color.ForestGreen },
// { XivChatType.Ls8, Color.ForestGreen },
// { XivChatType.TellIncoming, Color.HotPink },
// { XivChatType.PvPTeam, Color.SandyBrown },
// { XivChatType.Urgent, Color.DarkViolet },
// { XivChatType.NoviceNetwork, Color.SaddleBrown },
// { XivChatType.Echo, Color.Gray },
// };
private readonly Regex rmtRegex = new(
@"4KGOLD|We have sufficient stock|VPK\.OM|Gil for free|www\.so9\.com|Fast & Convenient|Cheap & Safety Guarantee|【Code|A O A U E|igfans|4KGOLD\.COM|Cheapest Gil with|pvp and bank on google|Selling Cheap GIL|ff14mogstation\.com|Cheap Gil 1000k|gilsforyou|server 1000K =|gils_selling|E A S Y\.C O M|bonus code|mins delivery guarantee|Sell cheap|Salegm\.com|cheap Mog|Off Code:|FF14Mog.com|使用する5オ|Off Code( *):|offers Fantasia",
RegexOptions.Compiled);
private readonly Dictionary<ClientLanguage, Regex[]> retainerSaleRegexes = new()
{ {
ClientLanguage.Japanese,
new Regex[]
{ {
ClientLanguage.Japanese,
new Regex[]
{
new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)×(?<count>[\d,.]+)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled), new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)×(?<count>[\d,.]+)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled),
new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled), new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled),
} }
}, },
{
ClientLanguage.English,
new Regex[]
{ {
ClientLanguage.English,
new Regex[]
{
new Regex(@"^(?<item>.+) you put up for sale in the (?:.+) markets (?:have|has) sold for (?<value>[\d,.]+) gil \(after fees\)\.$", RegexOptions.Compiled), new Regex(@"^(?<item>.+) you put up for sale in the (?:.+) markets (?:have|has) sold for (?<value>[\d,.]+) gil \(after fees\)\.$", RegexOptions.Compiled),
} }
}, },
{
ClientLanguage.German,
new Regex[]
{ {
ClientLanguage.German,
new Regex[]
{
new Regex(@"^Dein Gehilfe hat (?<item>.+) auf dem Markt von (?:.+) für (?<value>[\d,.]+) Gil verkauft\.$", RegexOptions.Compiled), new Regex(@"^Dein Gehilfe hat (?<item>.+) auf dem Markt von (?:.+) für (?<value>[\d,.]+) Gil verkauft\.$", RegexOptions.Compiled),
new Regex(@"^Dein Gehilfe hat (?<item>.+) auf dem Markt von (?:.+) verkauft und (?<value>[\d,.]+) Gil erhalten\.$", RegexOptions.Compiled), new Regex(@"^Dein Gehilfe hat (?<item>.+) auf dem Markt von (?:.+) verkauft und (?<value>[\d,.]+) Gil erhalten\.$", RegexOptions.Compiled),
} }
}, },
{
ClientLanguage.French,
new Regex[]
{ {
ClientLanguage.French,
new Regex[]
{
new Regex(@"^Un servant a vendu (?<item>.+) pour (?<value>[\d,.]+) gil à (?:.+)\.$", RegexOptions.Compiled), new Regex(@"^Un servant a vendu (?<item>.+) pour (?<value>[\d,.]+) gil à (?:.+)\.$", RegexOptions.Compiled),
} }
}, },
}; };
private readonly Regex urlRegex = new(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled); private readonly Regex urlRegex = new(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled);
private readonly DalamudLinkPayload openInstallerWindowLink; private readonly DalamudLinkPayload openInstallerWindowLink;
private bool hasSeenLoadingMsg; private bool hasSeenLoadingMsg;
private bool hasAutoUpdatedPlugins; private bool hasAutoUpdatedPlugins;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ChatHandlers"/> class. /// Initializes a new instance of the <see cref="ChatHandlers"/> class.
/// </summary> /// </summary>
internal ChatHandlers() internal ChatHandlers()
{
var chatGui = Service<ChatGui>.Get();
chatGui.CheckMessageHandled += this.OnCheckMessageHandled;
chatGui.ChatMessage += this.OnChatMessage;
this.openInstallerWindowLink = chatGui.AddChatLinkHandler("Dalamud", 1001, (i, m) =>
{ {
Service<DalamudInterface>.Get().OpenPluginInstaller(); var chatGui = Service<ChatGui>.Get();
});
}
/// <summary> chatGui.CheckMessageHandled += this.OnCheckMessageHandled;
/// Gets the last URL seen in chat. chatGui.ChatMessage += this.OnChatMessage;
/// </summary>
public string? LastLink { get; private set; }
/// <summary> this.openInstallerWindowLink = chatGui.AddChatLinkHandler("Dalamud", 1001, (i, m) =>
/// Convert a TextPayload to SeString and wrap in italics payloads.
/// </summary>
/// <param name="text">Text to convert.</param>
/// <returns>SeString payload of italicized text.</returns>
public static SeString MakeItalics(string text)
=> MakeItalics(new TextPayload(text));
/// <summary>
/// Convert a TextPayload to SeString and wrap in italics payloads.
/// </summary>
/// <param name="text">Text to convert.</param>
/// <returns>SeString payload of italicized text.</returns>
public static SeString MakeItalics(TextPayload text)
=> new(EmphasisItalicPayload.ItalicsOn, text, EmphasisItalicPayload.ItalicsOff);
private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled)
{
var configuration = Service<DalamudConfiguration>.Get();
var textVal = message.TextValue;
if (!configuration.DisableRmtFiltering)
{
var matched = this.rmtRegex.IsMatch(textVal);
if (matched)
{ {
// This seems to be a RMT ad - let's not show it Service<DalamudInterface>.Get().OpenPluginInstaller();
Log.Debug("Handled RMT ad: " + message.TextValue); });
}
/// <summary>
/// Gets the last URL seen in chat.
/// </summary>
public string? LastLink { get; private set; }
/// <summary>
/// Convert a TextPayload to SeString and wrap in italics payloads.
/// </summary>
/// <param name="text">Text to convert.</param>
/// <returns>SeString payload of italicized text.</returns>
public static SeString MakeItalics(string text)
=> MakeItalics(new TextPayload(text));
/// <summary>
/// Convert a TextPayload to SeString and wrap in italics payloads.
/// </summary>
/// <param name="text">Text to convert.</param>
/// <returns>SeString payload of italicized text.</returns>
public static SeString MakeItalics(TextPayload text)
=> new(EmphasisItalicPayload.ItalicsOn, text, EmphasisItalicPayload.ItalicsOff);
private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled)
{
var configuration = Service<DalamudConfiguration>.Get();
var textVal = message.TextValue;
if (!configuration.DisableRmtFiltering)
{
var matched = this.rmtRegex.IsMatch(textVal);
if (matched)
{
// This seems to be a RMT ad - let's not show it
Log.Debug("Handled RMT ad: " + message.TextValue);
isHandled = true;
return;
}
}
if (configuration.BadWords != null &&
configuration.BadWords.Any(x => !string.IsNullOrEmpty(x) && textVal.Contains(x)))
{
// This seems to be in the user block list - let's not show it
Log.Debug("Blocklist triggered");
isHandled = true; isHandled = true;
return; return;
} }
} }
if (configuration.BadWords != null && private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled)
configuration.BadWords.Any(x => !string.IsNullOrEmpty(x) && textVal.Contains(x)))
{ {
// This seems to be in the user block list - let's not show it var startInfo = Service<DalamudStartInfo>.Get();
Log.Debug("Blocklist triggered"); var clientState = Service<ClientState.ClientState>.Get();
isHandled = true;
return;
}
}
private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled) if (type == XivChatType.Notice && !this.hasSeenLoadingMsg)
{ this.PrintWelcomeMessage();
var startInfo = Service<DalamudStartInfo>.Get();
var clientState = Service<ClientState.ClientState>.Get();
if (type == XivChatType.Notice && !this.hasSeenLoadingMsg) // For injections while logged in
this.PrintWelcomeMessage(); if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg)
this.PrintWelcomeMessage();
// For injections while logged in if (!this.hasAutoUpdatedPlugins)
if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg) this.AutoUpdatePlugins();
this.PrintWelcomeMessage();
if (!this.hasAutoUpdatedPlugins)
this.AutoUpdatePlugins();
#if !DEBUG && false #if !DEBUG && false
if (!this.hasSeenLoadingMsg) if (!this.hasSeenLoadingMsg)
return; return;
#endif #endif
if (type == XivChatType.RetainerSale) if (type == XivChatType.RetainerSale)
{
foreach (var regex in this.retainerSaleRegexes[startInfo.Language])
{ {
var matchInfo = regex.Match(message.TextValue); foreach (var regex in this.retainerSaleRegexes[startInfo.Language])
// we no longer really need to do/validate the item matching since we read the id from the byte array
// but we'd be checking the main match anyway
var itemInfo = matchInfo.Groups["item"];
if (!itemInfo.Success)
continue;
var itemLink = message.Payloads.FirstOrDefault(x => x.Type == PayloadType.Item) as ItemPayload;
if (itemLink == default)
{ {
Log.Error("itemLink was null. Msg: {0}", BitConverter.ToString(message.Encode())); var matchInfo = regex.Match(message.TextValue);
// we no longer really need to do/validate the item matching since we read the id from the byte array
// but we'd be checking the main match anyway
var itemInfo = matchInfo.Groups["item"];
if (!itemInfo.Success)
continue;
var itemLink = message.Payloads.FirstOrDefault(x => x.Type == PayloadType.Item) as ItemPayload;
if (itemLink == default)
{
Log.Error("itemLink was null. Msg: {0}", BitConverter.ToString(message.Encode()));
break;
}
Log.Debug($"Probable retainer sale: {message}, decoded item {itemLink.Item.RowId}, HQ {itemLink.IsHQ}");
var valueInfo = matchInfo.Groups["value"];
// not sure if using a culture here would work correctly, so just strip symbols instead
if (!valueInfo.Success || !int.TryParse(valueInfo.Value.Replace(",", string.Empty).Replace(".", string.Empty), out var itemValue))
continue;
// Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemLink.Item.RowId, itemValue, itemLink.IsHQ));
break; break;
} }
Log.Debug($"Probable retainer sale: {message}, decoded item {itemLink.Item.RowId}, HQ {itemLink.IsHQ}");
var valueInfo = matchInfo.Groups["value"];
// not sure if using a culture here would work correctly, so just strip symbols instead
if (!valueInfo.Success || !int.TryParse(valueInfo.Value.Replace(",", string.Empty).Replace(".", string.Empty), out var itemValue))
continue;
// Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemLink.Item.RowId, itemValue, itemLink.IsHQ));
break;
}
}
var messageCopy = message;
var senderCopy = sender;
var linkMatch = this.urlRegex.Match(message.TextValue);
if (linkMatch.Value.Length > 0)
this.LastLink = linkMatch.Value;
}
private void PrintWelcomeMessage()
{
var chatGui = Service<ChatGui>.Get();
var configuration = Service<DalamudConfiguration>.Get();
var pluginManager = Service<PluginManager>.Get();
var dalamudInterface = Service<DalamudInterface>.Get();
var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString();
chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud vD{0} loaded."), assemblyVersion)
+ string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count));
if (configuration.PrintPluginsWelcomeMsg)
{
foreach (var plugin in pluginManager.InstalledPlugins.OrderBy(plugin => plugin.Name))
{
chatGui.Print(string.Format(Loc.Localize("DalamudPluginLoaded", " 》 {0} v{1} loaded."), plugin.Name, plugin.Manifest.AssemblyVersion));
}
}
if (string.IsNullOrEmpty(configuration.LastVersion) || !assemblyVersion.StartsWith(configuration.LastVersion))
{
chatGui.PrintChat(new XivChatEntry
{
Message = Loc.Localize("DalamudUpdated", "The In-Game addon has been updated or was reinstalled successfully! Please check the discord for a full changelog."),
Type = XivChatType.Notice,
});
if (string.IsNullOrEmpty(configuration.LastChangelogMajorMinor) || (!ChangelogWindow.WarrantsChangelogForMajorMinor.StartsWith(configuration.LastChangelogMajorMinor) && assemblyVersion.StartsWith(ChangelogWindow.WarrantsChangelogForMajorMinor)))
{
dalamudInterface.OpenChangelogWindow();
configuration.LastChangelogMajorMinor = ChangelogWindow.WarrantsChangelogForMajorMinor;
} }
configuration.LastVersion = assemblyVersion; var messageCopy = message;
configuration.Save(); var senderCopy = sender;
var linkMatch = this.urlRegex.Match(message.TextValue);
if (linkMatch.Value.Length > 0)
this.LastLink = linkMatch.Value;
} }
this.hasSeenLoadingMsg = true; private void PrintWelcomeMessage()
}
private void AutoUpdatePlugins()
{
var chatGui = Service<ChatGui>.Get();
var configuration = Service<DalamudConfiguration>.Get();
var pluginManager = Service<PluginManager>.Get();
var notifications = Service<NotificationManager>.Get();
if (!pluginManager.ReposReady || pluginManager.InstalledPlugins.Count == 0 || pluginManager.AvailablePlugins.Count == 0)
{ {
// Plugins aren't ready yet. var chatGui = Service<ChatGui>.Get();
return; var configuration = Service<DalamudConfiguration>.Get();
} var pluginManager = Service<PluginManager>.Get();
var dalamudInterface = Service<DalamudInterface>.Get();
this.hasAutoUpdatedPlugins = true; var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString();
Task.Run(() => pluginManager.UpdatePluginsAsync(!configuration.AutoUpdatePlugins)).ContinueWith(task => chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud vD{0} loaded."), assemblyVersion)
{ + string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count));
if (task.IsFaulted)
if (configuration.PrintPluginsWelcomeMsg)
{ {
Log.Error(task.Exception, Loc.Localize("DalamudPluginUpdateCheckFail", "Could not check for plugin updates.")); foreach (var plugin in pluginManager.InstalledPlugins.OrderBy(plugin => plugin.Name))
{
chatGui.Print(string.Format(Loc.Localize("DalamudPluginLoaded", " 》 {0} v{1} loaded."), plugin.Name, plugin.Manifest.AssemblyVersion));
}
}
if (string.IsNullOrEmpty(configuration.LastVersion) || !assemblyVersion.StartsWith(configuration.LastVersion))
{
chatGui.PrintChat(new XivChatEntry
{
Message = Loc.Localize("DalamudUpdated", "The In-Game addon has been updated or was reinstalled successfully! Please check the discord for a full changelog."),
Type = XivChatType.Notice,
});
if (string.IsNullOrEmpty(configuration.LastChangelogMajorMinor) || (!ChangelogWindow.WarrantsChangelogForMajorMinor.StartsWith(configuration.LastChangelogMajorMinor) && assemblyVersion.StartsWith(ChangelogWindow.WarrantsChangelogForMajorMinor)))
{
dalamudInterface.OpenChangelogWindow();
configuration.LastChangelogMajorMinor = ChangelogWindow.WarrantsChangelogForMajorMinor;
}
configuration.LastVersion = assemblyVersion;
configuration.Save();
}
this.hasSeenLoadingMsg = true;
}
private void AutoUpdatePlugins()
{
var chatGui = Service<ChatGui>.Get();
var configuration = Service<DalamudConfiguration>.Get();
var pluginManager = Service<PluginManager>.Get();
var notifications = Service<NotificationManager>.Get();
if (!pluginManager.ReposReady || pluginManager.InstalledPlugins.Count == 0 || pluginManager.AvailablePlugins.Count == 0)
{
// Plugins aren't ready yet.
return; return;
} }
var updatedPlugins = task.Result; this.hasAutoUpdatedPlugins = true;
if (updatedPlugins != null && updatedPlugins.Any())
{
if (configuration.AutoUpdatePlugins)
{
pluginManager.PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:"));
notifications.AddNotification(Loc.Localize("NotificationUpdatedPlugins", "{0} of your plugins were updated.").Format(updatedPlugins.Count), Loc.Localize("NotificationAutoUpdate", "Auto-Update"), NotificationType.Info);
}
else
{
var data = Service<DataManager>.Get();
chatGui.PrintChat(new XivChatEntry Task.Run(() => pluginManager.UpdatePluginsAsync(!configuration.AutoUpdatePlugins)).ContinueWith(task =>
{
if (task.IsFaulted)
{
Log.Error(task.Exception, Loc.Localize("DalamudPluginUpdateCheckFail", "Could not check for plugin updates."));
return;
}
var updatedPlugins = task.Result;
if (updatedPlugins != null && updatedPlugins.Any())
{
if (configuration.AutoUpdatePlugins)
{ {
Message = new SeString(new List<Payload>() pluginManager.PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:"));
{ notifications.AddNotification(Loc.Localize("NotificationUpdatedPlugins", "{0} of your plugins were updated.").Format(updatedPlugins.Count), Loc.Localize("NotificationAutoUpdate", "Auto-Update"), NotificationType.Info);
}
else
{
var data = Service<DataManager>.Get();
chatGui.PrintChat(new XivChatEntry
{
Message = new SeString(new List<Payload>()
{
new TextPayload(Loc.Localize("DalamudPluginUpdateRequired", "One or more of your plugins needs to be updated. Please use the /xlplugins command in-game to update them!")), new TextPayload(Loc.Localize("DalamudPluginUpdateRequired", "One or more of your plugins needs to be updated. Please use the /xlplugins command in-game to update them!")),
new TextPayload(" ["), new TextPayload(" ["),
new UIForegroundPayload(500), new UIForegroundPayload(500),
@ -321,11 +321,12 @@ public class ChatHandlers
RawPayload.LinkTerminator, RawPayload.LinkTerminator,
new UIForegroundPayload(0), new UIForegroundPayload(0),
new TextPayload("]"), new TextPayload("]"),
}), }),
Type = XivChatType.Urgent, Type = XivChatType.Urgent,
}); });
}
} }
} });
}); }
} }
} }

View file

@ -7,179 +7,180 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.Buddy; namespace Dalamud.Game.ClientState.Buddy
/// <summary>
/// This collection represents the buddies present in your squadron or trust party.
/// It does not include the local player.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed partial class BuddyList
{ {
private const uint InvalidObjectID = 0xE0000000;
private readonly ClientStateAddressResolver address;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BuddyList"/> class. /// This collection represents the buddies present in your squadron or trust party.
/// It does not include the local player.
/// </summary> /// </summary>
/// <param name="addressResolver">Client state address resolver.</param> [PluginInterface]
internal BuddyList(ClientStateAddressResolver addressResolver) [InterfaceVersion("1.0")]
public sealed partial class BuddyList
{ {
this.address = addressResolver; private const uint InvalidObjectID = 0xE0000000;
Log.Verbose($"Buddy list address 0x{this.address.BuddyList.ToInt64():X}"); private readonly ClientStateAddressResolver address;
}
/// <summary> /// <summary>
/// Gets the amount of battle buddies the local player has. /// Initializes a new instance of the <see cref="BuddyList"/> class.
/// </summary> /// </summary>
public int Length /// <param name="addressResolver">Client state address resolver.</param>
{ internal BuddyList(ClientStateAddressResolver addressResolver)
get
{ {
var i = 0; this.address = addressResolver;
for (; i < 3; i++)
Log.Verbose($"Buddy list address 0x{this.address.BuddyList.ToInt64():X}");
}
/// <summary>
/// Gets the amount of battle buddies the local player has.
/// </summary>
public int Length
{
get
{ {
var addr = this.GetBattleBuddyMemberAddress(i); var i = 0;
var member = this.CreateBuddyMemberReference(addr); for (; i < 3; i++)
if (member == null) {
break; var addr = this.GetBattleBuddyMemberAddress(i);
var member = this.CreateBuddyMemberReference(addr);
if (member == null)
break;
}
return i;
} }
return i;
} }
}
/// <summary> /// <summary>
/// Gets a value indicating whether the local player's companion is present. /// Gets a value indicating whether the local player's companion is present.
/// </summary> /// </summary>
public bool CompanionBuddyPresent => this.CompanionBuddy != null; public bool CompanionBuddyPresent => this.CompanionBuddy != null;
/// <summary> /// <summary>
/// Gets a value indicating whether the local player's pet is present. /// Gets a value indicating whether the local player's pet is present.
/// </summary> /// </summary>
public bool PetBuddyPresent => this.PetBuddy != null; public bool PetBuddyPresent => this.PetBuddy != null;
/// <summary> /// <summary>
/// Gets the active companion buddy. /// Gets the active companion buddy.
/// </summary> /// </summary>
public BuddyMember? CompanionBuddy public BuddyMember? CompanionBuddy
{
get
{ {
var addr = this.GetCompanionBuddyMemberAddress(); get
return this.CreateBuddyMemberReference(addr); {
var addr = this.GetCompanionBuddyMemberAddress();
return this.CreateBuddyMemberReference(addr);
}
} }
}
/// <summary> /// <summary>
/// Gets the active pet buddy. /// Gets the active pet buddy.
/// </summary> /// </summary>
public BuddyMember? PetBuddy public BuddyMember? PetBuddy
{
get
{ {
var addr = this.GetPetBuddyMemberAddress(); get
return this.CreateBuddyMemberReference(addr); {
var addr = this.GetPetBuddyMemberAddress();
return this.CreateBuddyMemberReference(addr);
}
} }
}
/// <summary> /// <summary>
/// Gets the address of the buddy list. /// Gets the address of the buddy list.
/// </summary> /// </summary>
internal IntPtr BuddyListAddress => this.address.BuddyList; internal IntPtr BuddyListAddress => this.address.BuddyList;
private static int BuddyMemberSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy>(); private static int BuddyMemberSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy>();
private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy*)this.BuddyListAddress; private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy*)this.BuddyListAddress;
/// <summary> /// <summary>
/// Gets a battle buddy at the specified spawn index. /// Gets a battle buddy at the specified spawn index.
/// </summary> /// </summary>
/// <param name="index">Spawn index.</param> /// <param name="index">Spawn index.</param>
/// <returns>A <see cref="BuddyMember"/> at the specified spawn index.</returns> /// <returns>A <see cref="BuddyMember"/> at the specified spawn index.</returns>
public BuddyMember? this[int index] public BuddyMember? this[int index]
{
get
{ {
var address = this.GetBattleBuddyMemberAddress(index); get
return this.CreateBuddyMemberReference(address); {
var address = this.GetBattleBuddyMemberAddress(index);
return this.CreateBuddyMemberReference(address);
}
}
/// <summary>
/// Gets the address of the companion buddy.
/// </summary>
/// <returns>The memory address of the companion buddy.</returns>
public unsafe IntPtr GetCompanionBuddyMemberAddress()
{
return (IntPtr)(&this.BuddyListStruct->Companion);
}
/// <summary>
/// Gets the address of the pet buddy.
/// </summary>
/// <returns>The memory address of the pet buddy.</returns>
public unsafe IntPtr GetPetBuddyMemberAddress()
{
return (IntPtr)(&this.BuddyListStruct->Pet);
}
/// <summary>
/// Gets the address of the battle buddy at the specified index of the buddy list.
/// </summary>
/// <param name="index">The index of the battle buddy.</param>
/// <returns>The memory address of the battle buddy.</returns>
public unsafe IntPtr GetBattleBuddyMemberAddress(int index)
{
if (index < 0 || index >= 3)
return IntPtr.Zero;
return (IntPtr)(this.BuddyListStruct->BattleBuddies + (index * BuddyMemberSize));
}
/// <summary>
/// Create a reference to a buddy.
/// </summary>
/// <param name="address">The address of the buddy in memory.</param>
/// <returns><see cref="BuddyMember"/> object containing the requested data.</returns>
public BuddyMember? CreateBuddyMemberReference(IntPtr address)
{
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
return null;
if (address == IntPtr.Zero)
return null;
var buddy = new BuddyMember(address);
if (buddy.ObjectId == InvalidObjectID)
return null;
return buddy;
} }
} }
/// <summary> /// <summary>
/// Gets the address of the companion buddy. /// This collection represents the buddies present in your squadron or trust party.
/// </summary> /// </summary>
/// <returns>The memory address of the companion buddy.</returns> public sealed partial class BuddyList : IReadOnlyCollection<BuddyMember>
public unsafe IntPtr GetCompanionBuddyMemberAddress()
{ {
return (IntPtr)(&this.BuddyListStruct->Companion); /// <inheritdoc/>
} int IReadOnlyCollection<BuddyMember>.Count => this.Length;
/// <summary> /// <inheritdoc/>
/// Gets the address of the pet buddy. public IEnumerator<BuddyMember> GetEnumerator()
/// </summary> {
/// <returns>The memory address of the pet buddy.</returns> for (var i = 0; i < this.Length; i++)
public unsafe IntPtr GetPetBuddyMemberAddress() {
{ yield return this[i];
return (IntPtr)(&this.BuddyListStruct->Pet); }
} }
/// <summary> /// <inheritdoc/>
/// Gets the address of the battle buddy at the specified index of the buddy list. IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
/// </summary>
/// <param name="index">The index of the battle buddy.</param>
/// <returns>The memory address of the battle buddy.</returns>
public unsafe IntPtr GetBattleBuddyMemberAddress(int index)
{
if (index < 0 || index >= 3)
return IntPtr.Zero;
return (IntPtr)(this.BuddyListStruct->BattleBuddies + (index * BuddyMemberSize));
}
/// <summary>
/// Create a reference to a buddy.
/// </summary>
/// <param name="address">The address of the buddy in memory.</param>
/// <returns><see cref="BuddyMember"/> object containing the requested data.</returns>
public BuddyMember? CreateBuddyMemberReference(IntPtr address)
{
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
return null;
if (address == IntPtr.Zero)
return null;
var buddy = new BuddyMember(address);
if (buddy.ObjectId == InvalidObjectID)
return null;
return buddy;
} }
} }
/// <summary>
/// This collection represents the buddies present in your squadron or trust party.
/// </summary>
public sealed partial class BuddyList : IReadOnlyCollection<BuddyMember>
{
/// <inheritdoc/>
int IReadOnlyCollection<BuddyMember>.Count => this.Length;
/// <inheritdoc/>
public IEnumerator<BuddyMember> GetEnumerator()
{
for (var i = 0; i < this.Length; i++)
{
yield return this[i];
}
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}

View file

@ -4,69 +4,70 @@ using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers; using Dalamud.Game.ClientState.Resolvers;
namespace Dalamud.Game.ClientState.Buddy; namespace Dalamud.Game.ClientState.Buddy
/// <summary>
/// This class represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
/// </summary>
public unsafe class BuddyMember
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BuddyMember"/> class. /// This class represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
/// </summary> /// </summary>
/// <param name="address">Buddy address.</param> public unsafe class BuddyMember
internal BuddyMember(IntPtr address)
{ {
this.Address = address; /// <summary>
/// Initializes a new instance of the <see cref="BuddyMember"/> class.
/// </summary>
/// <param name="address">Buddy address.</param>
internal BuddyMember(IntPtr address)
{
this.Address = address;
}
/// <summary>
/// Gets the address of the buddy in memory.
/// </summary>
public IntPtr Address { get; }
/// <summary>
/// Gets the object ID of this buddy.
/// </summary>
public uint ObjectId => this.Struct->ObjectID;
/// <summary>
/// Gets the actor associated with this buddy.
/// </summary>
/// <remarks>
/// This iterates the actor table, it should be used with care.
/// </remarks>
public GameObject? GameObject => Service<ObjectTable>.Get().SearchById(this.ObjectId);
/// <summary>
/// Gets the current health of this buddy.
/// </summary>
public uint CurrentHP => this.Struct->CurrentHealth;
/// <summary>
/// Gets the maximum health of this buddy.
/// </summary>
public uint MaxHP => this.Struct->MaxHealth;
/// <summary>
/// Gets the data ID of this buddy.
/// </summary>
public uint DataID => this.Struct->DataID;
/// <summary>
/// Gets the Mount data related to this buddy. It should only be used with companion buddies.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.Mount> MountData => new(this.DataID);
/// <summary>
/// Gets the Pet data related to this buddy. It should only be used with pet buddies.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.Pet> PetData => new(this.DataID);
/// <summary>
/// Gets the Trust data related to this buddy. It should only be used with battle buddies.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.DawnGrowMember> TrustData => new(this.DataID);
private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address;
} }
/// <summary>
/// Gets the address of the buddy in memory.
/// </summary>
public IntPtr Address { get; }
/// <summary>
/// Gets the object ID of this buddy.
/// </summary>
public uint ObjectId => this.Struct->ObjectID;
/// <summary>
/// Gets the actor associated with this buddy.
/// </summary>
/// <remarks>
/// This iterates the actor table, it should be used with care.
/// </remarks>
public GameObject? GameObject => Service<ObjectTable>.Get().SearchById(this.ObjectId);
/// <summary>
/// Gets the current health of this buddy.
/// </summary>
public uint CurrentHP => this.Struct->CurrentHealth;
/// <summary>
/// Gets the maximum health of this buddy.
/// </summary>
public uint MaxHP => this.Struct->MaxHealth;
/// <summary>
/// Gets the data ID of this buddy.
/// </summary>
public uint DataID => this.Struct->DataID;
/// <summary>
/// Gets the Mount data related to this buddy. It should only be used with companion buddies.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.Mount> MountData => new(this.DataID);
/// <summary>
/// Gets the Pet data related to this buddy. It should only be used with pet buddies.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.Pet> PetData => new(this.DataID);
/// <summary>
/// Gets the Trust data related to this buddy. It should only be used with battle buddies.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.DawnGrowMember> TrustData => new(this.DataID);
private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address;
} }

View file

@ -16,164 +16,165 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState; namespace Dalamud.Game.ClientState
/// <summary>
/// This class represents the state of the game client at the time of access.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed class ClientState : IDisposable
{ {
private readonly ClientStateAddressResolver address;
private readonly Hook<SetupTerritoryTypeDelegate> setupTerritoryTypeHook;
private bool lastConditionNone = true;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ClientState"/> class. /// This class represents the state of the game client at the time of access.
/// Set up client state access.
/// </summary> /// </summary>
internal ClientState() [PluginInterface]
[InterfaceVersion("1.0")]
public sealed class ClientState : IDisposable
{ {
this.address = new ClientStateAddressResolver(); private readonly ClientStateAddressResolver address;
this.address.Setup(); private readonly Hook<SetupTerritoryTypeDelegate> setupTerritoryTypeHook;
Log.Verbose("===== C L I E N T S T A T E ====="); private bool lastConditionNone = true;
this.ClientLanguage = Service<DalamudStartInfo>.Get().Language; /// <summary>
/// Initializes a new instance of the <see cref="ClientState"/> class.
Service<ObjectTable>.Set(this.address); /// Set up client state access.
/// </summary>
Service<FateTable>.Set(this.address); internal ClientState()
Service<PartyList>.Set(this.address);
Service<BuddyList>.Set(this.address);
Service<JobGauges>.Set(this.address);
Service<KeyState>.Set(this.address);
Service<GamepadState>.Set(this.address);
Service<Condition>.Set(this.address);
Service<TargetManager>.Set(this.address);
Log.Verbose($"SetupTerritoryType address 0x{this.address.SetupTerritoryType.ToInt64():X}");
this.setupTerritoryTypeHook = new Hook<SetupTerritoryTypeDelegate>(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour);
var framework = Service<Framework>.Get();
framework.Update += this.FrameworkOnOnUpdateEvent;
var networkHandlers = Service<NetworkHandlers>.Get();
networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr SetupTerritoryTypeDelegate(IntPtr manager, ushort terriType);
/// <summary>
/// Event that gets fired when the current Territory changes.
/// </summary>
public event EventHandler<ushort> TerritoryChanged;
/// <summary>
/// Event that fires when a character is logging in.
/// </summary>
public event EventHandler Login;
/// <summary>
/// Event that fires when a character is logging out.
/// </summary>
public event EventHandler Logout;
/// <summary>
/// Event that gets fired when a duty is ready.
/// </summary>
public event EventHandler<Lumina.Excel.GeneratedSheets.ContentFinderCondition> CfPop;
/// <summary>
/// Gets the language of the client.
/// </summary>
public ClientLanguage ClientLanguage { get; }
/// <summary>
/// Gets the current Territory the player resides in.
/// </summary>
public ushort TerritoryType { get; private set; }
/// <summary>
/// Gets the local player character, if one is present.
/// </summary>
public PlayerCharacter? LocalPlayer => Service<ObjectTable>.Get()[0] as PlayerCharacter;
/// <summary>
/// Gets the content ID of the local character.
/// </summary>
public ulong LocalContentId => (ulong)Marshal.ReadInt64(this.address.LocalContentId);
/// <summary>
/// Gets a value indicating whether a character is logged in.
/// </summary>
public bool IsLoggedIn { get; private set; }
/// <summary>
/// Enable this module.
/// </summary>
public void Enable()
{
Service<Condition>.Get().Enable();
Service<GamepadState>.Get().Enable();
this.setupTerritoryTypeHook.Enable();
}
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.setupTerritoryTypeHook.Dispose();
Service<Condition>.Get().Dispose();
Service<GamepadState>.Get().Dispose();
Service<Framework>.Get().Update -= this.FrameworkOnOnUpdateEvent;
Service<NetworkHandlers>.Get().CfPop -= this.NetworkHandlersOnCfPop;
}
private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType)
{
this.TerritoryType = terriType;
this.TerritoryChanged?.Invoke(this, terriType);
Log.Debug("TerritoryType changed: {0}", terriType);
return this.setupTerritoryTypeHook.Original(manager, terriType);
}
private void NetworkHandlersOnCfPop(object sender, Lumina.Excel.GeneratedSheets.ContentFinderCondition e)
{
this.CfPop?.Invoke(this, e);
}
private void FrameworkOnOnUpdateEvent(Framework framework)
{
var condition = Service<Condition>.Get();
if (condition.Any() && this.lastConditionNone == true)
{ {
Log.Debug("Is login"); this.address = new ClientStateAddressResolver();
this.lastConditionNone = false; this.address.Setup();
this.IsLoggedIn = true;
this.Login?.Invoke(this, null); Log.Verbose("===== C L I E N T S T A T E =====");
this.ClientLanguage = Service<DalamudStartInfo>.Get().Language;
Service<ObjectTable>.Set(this.address);
Service<FateTable>.Set(this.address);
Service<PartyList>.Set(this.address);
Service<BuddyList>.Set(this.address);
Service<JobGauges>.Set(this.address);
Service<KeyState>.Set(this.address);
Service<GamepadState>.Set(this.address);
Service<Condition>.Set(this.address);
Service<TargetManager>.Set(this.address);
Log.Verbose($"SetupTerritoryType address 0x{this.address.SetupTerritoryType.ToInt64():X}");
this.setupTerritoryTypeHook = new Hook<SetupTerritoryTypeDelegate>(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour);
var framework = Service<Framework>.Get();
framework.Update += this.FrameworkOnOnUpdateEvent;
var networkHandlers = Service<NetworkHandlers>.Get();
networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
} }
if (!condition.Any() && this.lastConditionNone == false) [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr SetupTerritoryTypeDelegate(IntPtr manager, ushort terriType);
/// <summary>
/// Event that gets fired when the current Territory changes.
/// </summary>
public event EventHandler<ushort> TerritoryChanged;
/// <summary>
/// Event that fires when a character is logging in.
/// </summary>
public event EventHandler Login;
/// <summary>
/// Event that fires when a character is logging out.
/// </summary>
public event EventHandler Logout;
/// <summary>
/// Event that gets fired when a duty is ready.
/// </summary>
public event EventHandler<Lumina.Excel.GeneratedSheets.ContentFinderCondition> CfPop;
/// <summary>
/// Gets the language of the client.
/// </summary>
public ClientLanguage ClientLanguage { get; }
/// <summary>
/// Gets the current Territory the player resides in.
/// </summary>
public ushort TerritoryType { get; private set; }
/// <summary>
/// Gets the local player character, if one is present.
/// </summary>
public PlayerCharacter? LocalPlayer => Service<ObjectTable>.Get()[0] as PlayerCharacter;
/// <summary>
/// Gets the content ID of the local character.
/// </summary>
public ulong LocalContentId => (ulong)Marshal.ReadInt64(this.address.LocalContentId);
/// <summary>
/// Gets a value indicating whether a character is logged in.
/// </summary>
public bool IsLoggedIn { get; private set; }
/// <summary>
/// Enable this module.
/// </summary>
public void Enable()
{ {
Log.Debug("Is logout"); Service<Condition>.Get().Enable();
this.lastConditionNone = true; Service<GamepadState>.Get().Enable();
this.IsLoggedIn = false; this.setupTerritoryTypeHook.Enable();
this.Logout?.Invoke(this, null); }
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.setupTerritoryTypeHook.Dispose();
Service<Condition>.Get().Dispose();
Service<GamepadState>.Get().Dispose();
Service<Framework>.Get().Update -= this.FrameworkOnOnUpdateEvent;
Service<NetworkHandlers>.Get().CfPop -= this.NetworkHandlersOnCfPop;
}
private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType)
{
this.TerritoryType = terriType;
this.TerritoryChanged?.Invoke(this, terriType);
Log.Debug("TerritoryType changed: {0}", terriType);
return this.setupTerritoryTypeHook.Original(manager, terriType);
}
private void NetworkHandlersOnCfPop(object sender, Lumina.Excel.GeneratedSheets.ContentFinderCondition e)
{
this.CfPop?.Invoke(this, e);
}
private void FrameworkOnOnUpdateEvent(Framework framework)
{
var condition = Service<Condition>.Get();
if (condition.Any() && this.lastConditionNone == true)
{
Log.Debug("Is login");
this.lastConditionNone = false;
this.IsLoggedIn = true;
this.Login?.Invoke(this, null);
}
if (!condition.Any() && this.lastConditionNone == false)
{
Log.Debug("Is logout");
this.lastConditionNone = true;
this.IsLoggedIn = false;
this.Logout?.Invoke(this, null);
}
} }
} }
} }

View file

@ -1,113 +1,114 @@
using System; using System;
namespace Dalamud.Game.ClientState; namespace Dalamud.Game.ClientState
/// <summary>
/// Client state memory address resolver.
/// </summary>
public sealed class ClientStateAddressResolver : BaseAddressResolver
{ {
// Static offsets
/// <summary> /// <summary>
/// Gets the address of the actor table. /// Client state memory address resolver.
/// </summary> /// </summary>
public IntPtr ObjectTable { get; private set; } public sealed class ClientStateAddressResolver : BaseAddressResolver
/// <summary>
/// Gets the address of the buddy list.
/// </summary>
public IntPtr BuddyList { get; private set; }
/// <summary>
/// Gets the address of a pointer to the fate table.
/// </summary>
/// <remarks>
/// This is a static address to a pointer, not the address of the table itself.
/// </remarks>
public IntPtr FateTablePtr { get; private set; }
/// <summary>
/// Gets the address of the Group Manager.
/// </summary>
public IntPtr GroupManager { get; private set; }
/// <summary>
/// Gets the address of the local content id.
/// </summary>
public IntPtr LocalContentId { get; private set; }
/// <summary>
/// Gets the address of job gauge data.
/// </summary>
public IntPtr JobGaugeData { get; private set; }
/// <summary>
/// Gets the address of the keyboard state.
/// </summary>
public IntPtr KeyboardState { get; private set; }
/// <summary>
/// Gets the address of the keyboard state index array which translates the VK enumeration to the key state.
/// </summary>
public IntPtr KeyboardStateIndexArray { get; private set; }
/// <summary>
/// Gets the address of the target manager.
/// </summary>
public IntPtr TargetManager { get; private set; }
/// <summary>
/// Gets the address of the condition flag array.
/// </summary>
public IntPtr ConditionFlags { get; private set; }
// Functions
/// <summary>
/// Gets the address of the method which sets the territory type.
/// </summary>
public IntPtr SetupTerritoryType { get; private set; }
/// <summary>
/// Gets the address of the method which polls the gamepads for data.
/// Called every frame, even when `Enable Gamepad` is off in the settings.
/// </summary>
public IntPtr GamepadPoll { get; private set; }
/// <summary>
/// Scan for and setup any configured address pointers.
/// </summary>
/// <param name="sig">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(SigScanner sig)
{ {
// We don't need those anymore, but maybe someone else will - let's leave them here for good measure // Static offsets
// ViewportActorTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? 85 ED", 0) + 0x148;
// SomeActorTableAccess = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 55 A0 48 8D 8E ?? ?? ?? ??");
this.ObjectTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 83"); /// <summary>
/// Gets the address of the actor table.
/// </summary>
public IntPtr ObjectTable { get; private set; }
this.BuddyList = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 45 84 E4 75 1A F6 45 12 04"); /// <summary>
/// Gets the address of the buddy list.
/// </summary>
public IntPtr BuddyList { get; private set; }
this.FateTablePtr = sig.GetStaticAddressFromSig("48 8B 15 ?? ?? ?? ?? 48 8B F9 44 0F B7 41 ??"); /// <summary>
/// Gets the address of a pointer to the fate table.
/// </summary>
/// <remarks>
/// This is a static address to a pointer, not the address of the table itself.
/// </remarks>
public IntPtr FateTablePtr { get; private set; }
this.GroupManager = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 80 B8 ?? ?? ?? ?? ?? 76 50"); /// <summary>
/// Gets the address of the Group Manager.
/// </summary>
public IntPtr GroupManager { get; private set; }
this.LocalContentId = sig.GetStaticAddressFromSig("48 0F 44 05 ?? ?? ?? ?? 48 39 07"); /// <summary>
this.JobGaugeData = sig.GetStaticAddressFromSig("48 8B 0D ?? ?? ?? ?? 48 85 C9 74 43") + 0x8; /// Gets the address of the local content id.
/// </summary>
public IntPtr LocalContentId { get; private set; }
this.SetupTerritoryType = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B F9 66 89 91 ?? ?? ?? ??"); /// <summary>
/// Gets the address of job gauge data.
/// </summary>
public IntPtr JobGaugeData { get; private set; }
// These resolve to fixed offsets only, without the base address added in, so GetStaticAddressFromSig() can't be used. /// <summary>
// lea rcx, ds:1DB9F74h[rax*4] KeyboardState /// Gets the address of the keyboard state.
// movzx edx, byte ptr [rbx+rsi+1D5E0E0h] KeyboardStateIndexArray /// </summary>
this.KeyboardState = sig.ScanText("48 8D 0C 85 ?? ?? ?? ?? 8B 04 31 85 C2 0F 85") + 0x4; public IntPtr KeyboardState { get; private set; }
this.KeyboardStateIndexArray = sig.ScanText("0F B6 94 33 ?? ?? ?? ?? 84 D2") + 0x4;
this.ConditionFlags = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? BA ?? ?? ?? ?? E8 ?? ?? ?? ?? B0 01 48 83 C4 30"); /// <summary>
/// Gets the address of the keyboard state index array which translates the VK enumeration to the key state.
/// </summary>
public IntPtr KeyboardStateIndexArray { get; private set; }
this.TargetManager = sig.GetStaticAddressFromSig("48 8B 05 ?? ?? ?? ?? 48 8D 0D ?? ?? ?? ?? FF 50 ?? 48 85 DB"); /// <summary>
/// Gets the address of the target manager.
/// </summary>
public IntPtr TargetManager { get; private set; }
this.GamepadPoll = sig.ScanText("40 ?? 57 41 ?? 48 81 EC ?? ?? ?? ?? 44 0F ?? ?? ?? ?? ?? ?? ?? 48 8B"); /// <summary>
/// Gets the address of the condition flag array.
/// </summary>
public IntPtr ConditionFlags { get; private set; }
// Functions
/// <summary>
/// Gets the address of the method which sets the territory type.
/// </summary>
public IntPtr SetupTerritoryType { get; private set; }
/// <summary>
/// Gets the address of the method which polls the gamepads for data.
/// Called every frame, even when `Enable Gamepad` is off in the settings.
/// </summary>
public IntPtr GamepadPoll { get; private set; }
/// <summary>
/// Scan for and setup any configured address pointers.
/// </summary>
/// <param name="sig">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(SigScanner sig)
{
// We don't need those anymore, but maybe someone else will - let's leave them here for good measure
// ViewportActorTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? 85 ED", 0) + 0x148;
// SomeActorTableAccess = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 55 A0 48 8D 8E ?? ?? ?? ??");
this.ObjectTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 83");
this.BuddyList = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 45 84 E4 75 1A F6 45 12 04");
this.FateTablePtr = sig.GetStaticAddressFromSig("48 8B 15 ?? ?? ?? ?? 48 8B F9 44 0F B7 41 ??");
this.GroupManager = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 80 B8 ?? ?? ?? ?? ?? 76 50");
this.LocalContentId = sig.GetStaticAddressFromSig("48 0F 44 05 ?? ?? ?? ?? 48 39 07");
this.JobGaugeData = sig.GetStaticAddressFromSig("48 8B 0D ?? ?? ?? ?? 48 85 C9 74 43") + 0x8;
this.SetupTerritoryType = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B F9 66 89 91 ?? ?? ?? ??");
// These resolve to fixed offsets only, without the base address added in, so GetStaticAddressFromSig() can't be used.
// lea rcx, ds:1DB9F74h[rax*4] KeyboardState
// movzx edx, byte ptr [rbx+rsi+1D5E0E0h] KeyboardStateIndexArray
this.KeyboardState = sig.ScanText("48 8D 0C 85 ?? ?? ?? ?? 8B 04 31 85 C2 0F 85") + 0x4;
this.KeyboardStateIndexArray = sig.ScanText("0F B6 94 33 ?? ?? ?? ?? 84 D2") + 0x4;
this.ConditionFlags = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? BA ?? ?? ?? ?? E8 ?? ?? ?? ?? B0 01 48 83 C4 30");
this.TargetManager = sig.GetStaticAddressFromSig("48 8B 05 ?? ?? ?? ?? 48 8D 0D ?? ?? ?? ?? FF 50 ?? 48 85 DB");
this.GamepadPoll = sig.ScanText("40 ?? 57 41 ?? 48 81 EC ?? ?? ?? ?? 44 0F ?? ?? ?? ?? ?? ?? ?? 48 8B");
}
} }
} }

View file

@ -4,154 +4,155 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.Conditions; namespace Dalamud.Game.ClientState.Conditions
/// <summary>
/// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed partial class Condition
{ {
/// <summary> /// <summary>
/// The current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has. /// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc.
/// </summary> /// </summary>
public const int MaxConditionEntries = 100; [PluginInterface]
[InterfaceVersion("1.0")]
private readonly bool[] cache = new bool[MaxConditionEntries]; public sealed partial class Condition
/// <summary>
/// Initializes a new instance of the <see cref="Condition"/> class.
/// </summary>
/// <param name="resolver">The ClientStateAddressResolver instance.</param>
internal Condition(ClientStateAddressResolver resolver)
{ {
this.Address = resolver.ConditionFlags; /// <summary>
} /// The current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has.
/// </summary>
public const int MaxConditionEntries = 100;
/// <summary> private readonly bool[] cache = new bool[MaxConditionEntries];
/// A delegate type used with the <see cref="ConditionChange"/> event.
/// </summary>
/// <param name="flag">The changed condition.</param>
/// <param name="value">The value the condition is set to.</param>
public delegate void ConditionChangeDelegate(ConditionFlag flag, bool value);
/// <summary> /// <summary>
/// Event that gets fired when a condition is set. /// Initializes a new instance of the <see cref="Condition"/> class.
/// Should only get fired for actual changes, so the previous value will always be !value. /// </summary>
/// </summary> /// <param name="resolver">The ClientStateAddressResolver instance.</param>
public event ConditionChangeDelegate? ConditionChange; internal Condition(ClientStateAddressResolver resolver)
/// <summary>
/// Gets the condition array base pointer.
/// </summary>
public IntPtr Address { get; private set; }
/// <summary>
/// Check the value of a specific condition/state flag.
/// </summary>
/// <param name="flag">The condition flag to check.</param>
public unsafe bool this[int flag]
{
get
{ {
if (flag < 0 || flag >= MaxConditionEntries) this.Address = resolver.ConditionFlags;
return false;
return *(bool*)(this.Address + flag);
}
}
/// <inheritdoc cref="this[int]"/>
public unsafe bool this[ConditionFlag flag]
=> this[(int)flag];
/// <summary>
/// Check if any condition flags are set.
/// </summary>
/// <returns>Whether any single flag is set.</returns>
public bool Any()
{
for (var i = 0; i < MaxConditionEntries; i++)
{
var cond = this[i];
if (cond)
return true;
} }
return false; /// <summary>
} /// A delegate type used with the <see cref="ConditionChange"/> event.
/// </summary>
/// <param name="flag">The changed condition.</param>
/// <param name="value">The value the condition is set to.</param>
public delegate void ConditionChangeDelegate(ConditionFlag flag, bool value);
/// <summary> /// <summary>
/// Enables the hooks of the Condition class function. /// Event that gets fired when a condition is set.
/// </summary> /// Should only get fired for actual changes, so the previous value will always be !value.
public void Enable() /// </summary>
{ public event ConditionChangeDelegate? ConditionChange;
// Initialization
for (var i = 0; i < MaxConditionEntries; i++)
this.cache[i] = this[i];
Service<Framework>.Get().Update += this.FrameworkUpdate; /// <summary>
} /// Gets the condition array base pointer.
/// </summary>
public IntPtr Address { get; private set; }
private void FrameworkUpdate(Framework framework) /// <summary>
{ /// Check the value of a specific condition/state flag.
for (var i = 0; i < MaxConditionEntries; i++) /// </summary>
/// <param name="flag">The condition flag to check.</param>
public unsafe bool this[int flag]
{ {
var value = this[i]; get
if (value != this.cache[i])
{ {
this.cache[i] = value; if (flag < 0 || flag >= MaxConditionEntries)
return false;
try return *(bool*)(this.Address + flag);
}
}
/// <inheritdoc cref="this[int]"/>
public unsafe bool this[ConditionFlag flag]
=> this[(int)flag];
/// <summary>
/// Check if any condition flags are set.
/// </summary>
/// <returns>Whether any single flag is set.</returns>
public bool Any()
{
for (var i = 0; i < MaxConditionEntries; i++)
{
var cond = this[i];
if (cond)
return true;
}
return false;
}
/// <summary>
/// Enables the hooks of the Condition class function.
/// </summary>
public void Enable()
{
// Initialization
for (var i = 0; i < MaxConditionEntries; i++)
this.cache[i] = this[i];
Service<Framework>.Get().Update += this.FrameworkUpdate;
}
private void FrameworkUpdate(Framework framework)
{
for (var i = 0; i < MaxConditionEntries; i++)
{
var value = this[i];
if (value != this.cache[i])
{ {
this.ConditionChange?.Invoke((ConditionFlag)i, value); this.cache[i] = value;
}
catch (Exception ex) try
{ {
Log.Error(ex, $"While invoking {nameof(this.ConditionChange)}, an exception was thrown."); this.ConditionChange?.Invoke((ConditionFlag)i, value);
}
catch (Exception ex)
{
Log.Error(ex, $"While invoking {nameof(this.ConditionChange)}, an exception was thrown.");
}
} }
} }
} }
} }
}
/// <summary>
/// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc.
/// </summary>
public sealed partial class Condition : IDisposable
{
private bool isDisposed;
/// <summary> /// <summary>
/// Finalizes an instance of the <see cref="Condition" /> class. /// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc.
/// </summary> /// </summary>
~Condition() public sealed partial class Condition : IDisposable
{ {
this.Dispose(false); private bool isDisposed;
}
/// <summary> /// <summary>
/// Disposes this instance, alongside its hooks. /// Finalizes an instance of the <see cref="Condition" /> class.
/// </summary> /// </summary>
public void Dispose() ~Condition()
{
GC.SuppressFinalize(this);
this.Dispose(true);
}
private void Dispose(bool disposing)
{
if (this.isDisposed)
return;
if (disposing)
{ {
Service<Framework>.Get().Update -= this.FrameworkUpdate; this.Dispose(false);
} }
this.isDisposed = true; /// <summary>
/// Disposes this instance, alongside its hooks.
/// </summary>
public void Dispose()
{
GC.SuppressFinalize(this);
this.Dispose(true);
}
private void Dispose(bool disposing)
{
if (this.isDisposed)
return;
if (disposing)
{
Service<Framework>.Get().Update -= this.FrameworkUpdate;
}
this.isDisposed = true;
}
} }
} }

View file

@ -1,452 +1,453 @@
namespace Dalamud.Game.ClientState.Conditions; namespace Dalamud.Game.ClientState.Conditions
/// <summary>
/// Possible state flags (or conditions as they're called internally) that can be set on the local client.
///
/// These come from LogMessage (somewhere) and directly map to each state field managed by the client. As of 5.25, it maps to
/// LogMessage row 7700 and onwards, which can be checked by looking at the Condition sheet and looking at what column 2 maps to.
/// </summary>
public enum ConditionFlag
{ {
/// <summary> /// <summary>
/// Unused. /// Possible state flags (or conditions as they're called internally) that can be set on the local client.
/// </summary> ///
None = 0, /// These come from LogMessage (somewhere) and directly map to each state field managed by the client. As of 5.25, it maps to
/// LogMessage row 7700 and onwards, which can be checked by looking at the Condition sheet and looking at what column 2 maps to.
/// <summary> /// </summary>
/// Unable to execute command under normal conditions. public enum ConditionFlag
/// </summary> {
NormalConditions = 1, /// <summary>
/// Unused.
/// <summary> /// </summary>
/// Unable to execute command while unconscious. None = 0,
/// </summary>
Unconscious = 2, /// <summary>
/// Unable to execute command under normal conditions.
/// <summary> /// </summary>
/// Unable to execute command during an emote. NormalConditions = 1,
/// </summary>
Emoting = 3, /// <summary>
/// Unable to execute command while unconscious.
/// <summary> /// </summary>
/// Unable to execute command while mounted. Unconscious = 2,
/// </summary>
Mounted = 4, /// <summary>
/// Unable to execute command during an emote.
/// <summary> /// </summary>
/// Unable to execute command while crafting. Emoting = 3,
/// </summary>
Crafting = 5, /// <summary>
/// Unable to execute command while mounted.
/// <summary> /// </summary>
/// Unable to execute command while gathering. Mounted = 4,
/// </summary>
Gathering = 6, /// <summary>
/// Unable to execute command while crafting.
/// <summary> /// </summary>
/// Unable to execute command while melding materia. Crafting = 5,
/// </summary>
MeldingMateria = 7, /// <summary>
/// Unable to execute command while gathering.
/// <summary> /// </summary>
/// Unable to execute command while operating a siege machine. Gathering = 6,
/// </summary>
OperatingSiegeMachine = 8, /// <summary>
/// Unable to execute command while melding materia.
/// <summary> /// </summary>
/// Unable to execute command while carrying an object. MeldingMateria = 7,
/// </summary>
CarryingObject = 9, /// <summary>
/// Unable to execute command while operating a siege machine.
/// <summary> /// </summary>
/// Unable to execute command while mounted. OperatingSiegeMachine = 8,
/// </summary>
Mounted2 = 10, /// <summary>
/// Unable to execute command while carrying an object.
/// <summary> /// </summary>
/// Unable to execute command while in that position. CarryingObject = 9,
/// </summary>
InThatPosition = 11, /// <summary>
/// Unable to execute command while mounted.
/// <summary> /// </summary>
/// Unable to execute command while chocobo racing. Mounted2 = 10,
/// </summary>
ChocoboRacing = 12, /// <summary>
/// Unable to execute command while in that position.
/// <summary> /// </summary>
/// Unable to execute command while playing a mini-game. InThatPosition = 11,
/// </summary>
PlayingMiniGame = 13, /// <summary>
/// Unable to execute command while chocobo racing.
/// <summary> /// </summary>
/// Unable to execute command while playing Lord of Verminion. ChocoboRacing = 12,
/// </summary>
PlayingLordOfVerminion = 14, /// <summary>
/// Unable to execute command while playing a mini-game.
/// <summary> /// </summary>
/// Unable to execute command while participating in a custom match. PlayingMiniGame = 13,
/// </summary>
ParticipatingInCustomMatch = 15, /// <summary>
/// Unable to execute command while playing Lord of Verminion.
/// <summary> /// </summary>
/// Unable to execute command while performing. PlayingLordOfVerminion = 14,
/// </summary>
Performing = 16, /// <summary>
/// Unable to execute command while participating in a custom match.
// Unknown17 = 17, /// </summary>
// Unknown18 = 18, ParticipatingInCustomMatch = 15,
// Unknown19 = 19,
// Unknown20 = 20, /// <summary>
// Unknown21 = 21, /// Unable to execute command while performing.
// Unknown22 = 22, /// </summary>
// Unknown23 = 23, Performing = 16,
// Unknown24 = 24,
// Unknown17 = 17,
/// <summary> // Unknown18 = 18,
/// Unable to execute command while occupied. // Unknown19 = 19,
/// </summary> // Unknown20 = 20,
Occupied = 25, // Unknown21 = 21,
// Unknown22 = 22,
/// <summary> // Unknown23 = 23,
/// Unable to execute command during combat. // Unknown24 = 24,
/// </summary>
InCombat = 26, /// <summary>
/// Unable to execute command while occupied.
/// <summary> /// </summary>
/// Unable to execute command while casting. Occupied = 25,
/// </summary>
Casting = 27, /// <summary>
/// Unable to execute command during combat.
/// <summary> /// </summary>
/// Unable to execute command while suffering status affliction. InCombat = 26,
/// </summary>
SufferingStatusAffliction = 28, /// <summary>
/// Unable to execute command while casting.
/// <summary> /// </summary>
/// Unable to execute command while suffering status affliction. Casting = 27,
/// </summary>
SufferingStatusAffliction2 = 29, /// <summary>
/// Unable to execute command while suffering status affliction.
/// <summary> /// </summary>
/// Unable to execute command while occupied. SufferingStatusAffliction = 28,
/// </summary>
Occupied30 = 30, /// <summary>
/// Unable to execute command while suffering status affliction.
/// <summary> /// </summary>
/// Unable to execute command while occupied. SufferingStatusAffliction2 = 29,
/// </summary>
// todo: not sure if this is used for other event states/??? /// <summary>
OccupiedInEvent = 31, /// Unable to execute command while occupied.
/// </summary>
/// <summary> Occupied30 = 30,
/// Unable to execute command while occupied.
/// </summary> /// <summary>
OccupiedInQuestEvent = 32, /// Unable to execute command while occupied.
/// </summary>
/// <summary> // todo: not sure if this is used for other event states/???
/// Unable to execute command while occupied. OccupiedInEvent = 31,
/// </summary>
Occupied33 = 33, /// <summary>
/// Unable to execute command while occupied.
/// <summary> /// </summary>
/// Unable to execute command while bound by duty. OccupiedInQuestEvent = 32,
/// </summary>
BoundByDuty = 34, /// <summary>
/// Unable to execute command while occupied.
/// <summary> /// </summary>
/// Unable to execute command while occupied. Occupied33 = 33,
/// </summary>
OccupiedInCutSceneEvent = 35, /// <summary>
/// Unable to execute command while bound by duty.
/// <summary> /// </summary>
/// Unable to execute command while in a dueling area. BoundByDuty = 34,
/// </summary>
InDuelingArea = 36, /// <summary>
/// Unable to execute command while occupied.
/// <summary> /// </summary>
/// Unable to execute command while a trade is open. OccupiedInCutSceneEvent = 35,
/// </summary>
TradeOpen = 37, /// <summary>
/// Unable to execute command while in a dueling area.
/// <summary> /// </summary>
/// Unable to execute command while occupied. InDuelingArea = 36,
/// </summary>
Occupied38 = 38, /// <summary>
/// Unable to execute command while a trade is open.
/// <summary> /// </summary>
/// Unable to execute command while occupied. TradeOpen = 37,
/// </summary>
Occupied39 = 39, /// <summary>
/// Unable to execute command while occupied.
/// <summary> /// </summary>
/// Unable to execute command while crafting. Occupied38 = 38,
/// </summary>
Crafting40 = 40, /// <summary>
/// Unable to execute command while occupied.
/// <summary> /// </summary>
/// Unable to execute command while preparing to craft. Occupied39 = 39,
/// </summary>
PreparingToCraft = 41, /// <summary>
/// Unable to execute command while crafting.
/// <summary> /// </summary>
/// Unable to execute command while gathering. Crafting40 = 40,
/// </summary>
Gathering42 = 42, /// <summary>
/// Unable to execute command while preparing to craft.
/// <summary> /// </summary>
/// Unable to execute command while fishing. PreparingToCraft = 41,
/// </summary>
Fishing = 43, /// <summary>
/// Unable to execute command while gathering.
// Unknown44 = 44, /// </summary>
Gathering42 = 42,
/// <summary>
/// Unable to execute command while between areas. /// <summary>
/// </summary> /// Unable to execute command while fishing.
BetweenAreas = 45, /// </summary>
Fishing = 43,
/// <summary>
/// Unable to execute command while stealthed. // Unknown44 = 44,
/// </summary>
Stealthed = 46, /// <summary>
/// Unable to execute command while between areas.
// Unknown47 = 47, /// </summary>
BetweenAreas = 45,
/// <summary>
/// Unable to execute command while jumping. /// <summary>
/// </summary> /// Unable to execute command while stealthed.
Jumping = 48, /// </summary>
Stealthed = 46,
/// <summary>
/// Unable to execute command while auto-run is active. // Unknown47 = 47,
/// </summary>
AutorunActive = 49, /// <summary>
/// Unable to execute command while jumping.
/// <summary> /// </summary>
/// Unable to execute command while occupied. Jumping = 48,
/// </summary>
// todo: used for other shits? /// <summary>
OccupiedSummoningBell = 50, /// Unable to execute command while auto-run is active.
/// </summary>
/// <summary> AutorunActive = 49,
/// Unable to execute command while between areas.
/// </summary> /// <summary>
BetweenAreas51 = 51, /// Unable to execute command while occupied.
/// </summary>
/// <summary> // todo: used for other shits?
/// Unable to execute command due to system error. OccupiedSummoningBell = 50,
/// </summary>
SystemError = 52, /// <summary>
/// Unable to execute command while between areas.
/// <summary> /// </summary>
/// Unable to execute command while logging out. BetweenAreas51 = 51,
/// </summary>
LoggingOut = 53, /// <summary>
/// Unable to execute command due to system error.
/// <summary> /// </summary>
/// Unable to execute command at this location. SystemError = 52,
/// </summary>
ConditionLocation = 54, /// <summary>
/// Unable to execute command while logging out.
/// <summary> /// </summary>
/// Unable to execute command while waiting for duty. LoggingOut = 53,
/// </summary>
WaitingForDuty = 55, /// <summary>
/// Unable to execute command at this location.
/// <summary> /// </summary>
/// Unable to execute command while bound by duty. ConditionLocation = 54,
/// </summary>
BoundByDuty56 = 56, /// <summary>
/// Unable to execute command while waiting for duty.
/// <summary> /// </summary>
/// Unable to execute command at this time. WaitingForDuty = 55,
/// </summary>
Unknown57 = 57, /// <summary>
/// Unable to execute command while bound by duty.
/// <summary> /// </summary>
/// Unable to execute command while watching a cutscene. BoundByDuty56 = 56,
/// </summary>
WatchingCutscene = 58, /// <summary>
/// Unable to execute command at this time.
/// <summary> /// </summary>
/// Unable to execute command while waiting for Duty Finder. Unknown57 = 57,
/// </summary>
WaitingForDutyFinder = 59, /// <summary>
/// Unable to execute command while watching a cutscene.
/// <summary> /// </summary>
/// Unable to execute command while creating a character. WatchingCutscene = 58,
/// </summary>
CreatingCharacter = 60, /// <summary>
/// Unable to execute command while waiting for Duty Finder.
/// <summary> /// </summary>
/// Unable to execute command while jumping. WaitingForDutyFinder = 59,
/// </summary>
Jumping61 = 61, /// <summary>
/// Unable to execute command while creating a character.
/// <summary> /// </summary>
/// Unable to execute command while the PvP display is active. CreatingCharacter = 60,
/// </summary>
PvPDisplayActive = 62, /// <summary>
/// Unable to execute command while jumping.
/// <summary> /// </summary>
/// Unable to execute command while suffering status affliction. Jumping61 = 61,
/// </summary>
SufferingStatusAffliction63 = 63, /// <summary>
/// Unable to execute command while the PvP display is active.
/// <summary> /// </summary>
/// Unable to execute command while mounting. PvPDisplayActive = 62,
/// </summary>
Mounting = 64, /// <summary>
/// Unable to execute command while suffering status affliction.
/// <summary> /// </summary>
/// Unable to execute command while carrying an item. SufferingStatusAffliction63 = 63,
/// </summary>
CarryingItem = 65, /// <summary>
/// Unable to execute command while mounting.
/// <summary> /// </summary>
/// Unable to execute command while using the Party Finder. Mounting = 64,
/// </summary>
UsingPartyFinder = 66, /// <summary>
/// Unable to execute command while carrying an item.
/// <summary> /// </summary>
/// Unable to execute command while using housing functions. CarryingItem = 65,
/// </summary>
UsingHousingFunctions = 67, /// <summary>
/// Unable to execute command while using the Party Finder.
/// <summary> /// </summary>
/// Unable to execute command while transformed. UsingPartyFinder = 66,
/// </summary>
Transformed = 68, /// <summary>
/// Unable to execute command while using housing functions.
/// <summary> /// </summary>
/// Unable to execute command while on the free trial. UsingHousingFunctions = 67,
/// </summary>
OnFreeTrial = 69, /// <summary>
/// Unable to execute command while transformed.
/// <summary> /// </summary>
/// Unable to execute command while being moved. Transformed = 68,
/// </summary>
BeingMoved = 70, /// <summary>
/// Unable to execute command while on the free trial.
/// <summary> /// </summary>
/// Unable to execute command while mounting. OnFreeTrial = 69,
/// </summary>
Mounting71 = 71, /// <summary>
/// Unable to execute command while being moved.
/// <summary> /// </summary>
/// Unable to execute command while suffering status affliction. BeingMoved = 70,
/// </summary>
SufferingStatusAffliction72 = 72, /// <summary>
/// Unable to execute command while mounting.
/// <summary> /// </summary>
/// Unable to execute command while suffering status affliction. Mounting71 = 71,
/// </summary>
SufferingStatusAffliction73 = 73, /// <summary>
/// Unable to execute command while suffering status affliction.
/// <summary> /// </summary>
/// Unable to execute command while registering for a race or match. SufferingStatusAffliction72 = 72,
/// </summary>
RegisteringForRaceOrMatch = 74, /// <summary>
/// Unable to execute command while suffering status affliction.
/// <summary> /// </summary>
/// Unable to execute command while waiting for a race or match. SufferingStatusAffliction73 = 73,
/// </summary>
WaitingForRaceOrMatch = 75, /// <summary>
/// Unable to execute command while registering for a race or match.
/// <summary> /// </summary>
/// Unable to execute command while waiting for a Triple Triad match. RegisteringForRaceOrMatch = 74,
/// </summary>
WaitingForTripleTriadMatch = 76, /// <summary>
/// Unable to execute command while waiting for a race or match.
/// <summary> /// </summary>
/// Unable to execute command while in flight. WaitingForRaceOrMatch = 75,
/// </summary>
InFlight = 77, /// <summary>
/// Unable to execute command while waiting for a Triple Triad match.
/// <summary> /// </summary>
/// Unable to execute command while watching a cutscene. WaitingForTripleTriadMatch = 76,
/// </summary>
WatchingCutscene78 = 78, /// <summary>
/// Unable to execute command while in flight.
/// <summary> /// </summary>
/// Unable to execute command while delving into a deep dungeon. InFlight = 77,
/// </summary>
InDeepDungeon = 79, /// <summary>
/// Unable to execute command while watching a cutscene.
/// <summary> /// </summary>
/// Unable to execute command while swimming. WatchingCutscene78 = 78,
/// </summary>
Swimming = 80, /// <summary>
/// Unable to execute command while delving into a deep dungeon.
/// <summary> /// </summary>
/// Unable to execute command while diving. InDeepDungeon = 79,
/// </summary>
Diving = 81, /// <summary>
/// Unable to execute command while swimming.
/// <summary> /// </summary>
/// Unable to execute command while registering for a Triple Triad match. Swimming = 80,
/// </summary>
RegisteringForTripleTriadMatch = 82, /// <summary>
/// Unable to execute command while diving.
/// <summary> /// </summary>
/// Unable to execute command while waiting for a Triple Triad match. Diving = 81,
/// </summary>
WaitingForTripleTriadMatch83 = 83, /// <summary>
/// Unable to execute command while registering for a Triple Triad match.
/// <summary> /// </summary>
/// Unable to execute command while participating in a cross-world party or alliance. RegisteringForTripleTriadMatch = 82,
/// </summary>
ParticipatingInCrossWorldPartyOrAlliance = 84, /// <summary>
/// Unable to execute command while waiting for a Triple Triad match.
// Unknown85 = 85, /// </summary>
WaitingForTripleTriadMatch83 = 83,
/// <summary>
/// Unable to execute command while playing duty record. /// <summary>
/// </summary> /// Unable to execute command while participating in a cross-world party or alliance.
DutyRecorderPlayback = 86, /// </summary>
ParticipatingInCrossWorldPartyOrAlliance = 84,
/// <summary>
/// Unable to execute command while casting. // Unknown85 = 85,
/// </summary>
Casting87 = 87, /// <summary>
/// Unable to execute command while playing duty record.
/// <summary> /// </summary>
/// Unable to execute command in this state. DutyRecorderPlayback = 86,
/// </summary>
InThisState88 = 88, /// <summary>
/// Unable to execute command while casting.
/// <summary> /// </summary>
/// Unable to execute command in this state. Casting87 = 87,
/// </summary>
InThisState89 = 89, /// <summary>
/// Unable to execute command in this state.
/// <summary> /// </summary>
/// Unable to execute command while role-playing. InThisState88 = 88,
/// </summary>
RolePlaying = 90, /// <summary>
/// Unable to execute command in this state.
/// <summary> /// </summary>
/// Unable to execute command while bound by duty. InThisState89 = 89,
/// </summary>
BoundToDuty97 = 91, /// <summary>
/// Unable to execute command while role-playing.
/// <summary> /// </summary>
/// Unable to execute command while readying to visit another World. RolePlaying = 90,
/// </summary>
ReadyingVisitOtherWorld = 92, /// <summary>
/// Unable to execute command while bound by duty.
/// <summary> /// </summary>
/// Unable to execute command while waiting to visit another World. BoundToDuty97 = 91,
/// </summary>
WaitingToVisitOtherWorld = 93, /// <summary>
/// Unable to execute command while readying to visit another World.
/// <summary> /// </summary>
/// Unable to execute command while using a parasol. ReadyingVisitOtherWorld = 92,
/// </summary>
UsingParasol = 94, /// <summary>
/// Unable to execute command while waiting to visit another World.
/// <summary> /// </summary>
/// Unable to execute command while bound by duty. WaitingToVisitOtherWorld = 93,
/// </summary>
BoundByDuty95 = 95, /// <summary>
/// Unable to execute command while using a parasol.
/// </summary>
UsingParasol = 94,
/// <summary>
/// Unable to execute command while bound by duty.
/// </summary>
BoundByDuty95 = 95,
}
} }

View file

@ -6,130 +6,131 @@ using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory; using Dalamud.Memory;
namespace Dalamud.Game.ClientState.Fates; namespace Dalamud.Game.ClientState.Fates
/// <summary>
/// This class represents an FFXIV Fate.
/// </summary>
public unsafe partial class Fate : IEquatable<Fate>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Fate"/> class. /// This class represents an FFXIV Fate.
/// </summary> /// </summary>
/// <param name="address">The address of this fate in memory.</param> public unsafe partial class Fate : IEquatable<Fate>
internal Fate(IntPtr address)
{ {
this.Address = address; /// <summary>
/// Initializes a new instance of the <see cref="Fate"/> class.
/// </summary>
/// <param name="address">The address of this fate in memory.</param>
internal Fate(IntPtr address)
{
this.Address = address;
}
/// <summary>
/// Gets the address of this Fate in memory.
/// </summary>
public IntPtr Address { get; }
private FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext*)this.Address;
public static bool operator ==(Fate fate1, Fate fate2)
{
if (fate1 is null || fate2 is null)
return Equals(fate1, fate2);
return fate1.Equals(fate2);
}
public static bool operator !=(Fate fate1, Fate fate2) => !(fate1 == fate2);
/// <summary>
/// Gets a value indicating whether this Fate is still valid in memory.
/// </summary>
/// <param name="fate">The fate to check.</param>
/// <returns>True or false.</returns>
public static bool IsValid(Fate fate)
{
var clientState = Service<ClientState>.Get();
if (fate == null)
return false;
if (clientState.LocalContentId == 0)
return false;
return true;
}
/// <summary>
/// Gets a value indicating whether this actor is still valid in memory.
/// </summary>
/// <returns>True or false.</returns>
public bool IsValid() => IsValid(this);
/// <inheritdoc/>
bool IEquatable<Fate>.Equals(Fate other) => this.FateId == other?.FateId;
/// <inheritdoc/>
public override bool Equals(object obj) => ((IEquatable<Fate>)this).Equals(obj as Fate);
/// <inheritdoc/>
public override int GetHashCode() => this.FateId.GetHashCode();
} }
/// <summary> /// <summary>
/// Gets the address of this Fate in memory. /// This class represents an FFXIV Fate.
/// </summary> /// </summary>
public IntPtr Address { get; } public unsafe partial class Fate
private FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext*)this.Address;
public static bool operator ==(Fate fate1, Fate fate2)
{ {
if (fate1 is null || fate2 is null) /// <summary>
return Equals(fate1, fate2); /// Gets the Fate ID of this <see cref="Fate" />.
/// </summary>
public ushort FateId => this.Struct->FateId;
return fate1.Equals(fate2); /// <summary>
/// Gets game data linked to this Fate.
/// </summary>
public Lumina.Excel.GeneratedSheets.Fate GameData => Service<DataManager>.Get().GetExcelSheet<Lumina.Excel.GeneratedSheets.Fate>().GetRow(this.FateId);
/// <summary>
/// Gets the time this <see cref="Fate"/> started.
/// </summary>
public int StartTimeEpoch => this.Struct->StartTimeEpoch;
/// <summary>
/// Gets how long this <see cref="Fate"/> will run.
/// </summary>
public short Duration => this.Struct->Duration;
/// <summary>
/// Gets the remaining time in seconds for this <see cref="Fate"/>.
/// </summary>
public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds();
/// <summary>
/// Gets the displayname of this <see cref="Fate" />.
/// </summary>
public SeString Name => MemoryHelper.ReadSeString(&this.Struct->Name);
/// <summary>
/// Gets the state of this <see cref="Fate"/> (Running, Ended, Failed, Preparation, WaitingForEnd).
/// </summary>
public FateState State => (FateState)this.Struct->State;
/// <summary>
/// Gets the progress amount of this <see cref="Fate"/>.
/// </summary>
public byte Progress => this.Struct->Progress;
/// <summary>
/// Gets the level of this <see cref="Fate"/>.
/// </summary>
public byte Level => this.Struct->Level;
/// <summary>
/// Gets the position of this <see cref="Fate"/>.
/// </summary>
public Vector3 Position => this.Struct->Location;
/// <summary>
/// Gets the territory this <see cref="Fate"/> is located in.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.TerritoryType> TerritoryType => new(this.Struct->TerritoryID);
} }
public static bool operator !=(Fate fate1, Fate fate2) => !(fate1 == fate2);
/// <summary>
/// Gets a value indicating whether this Fate is still valid in memory.
/// </summary>
/// <param name="fate">The fate to check.</param>
/// <returns>True or false.</returns>
public static bool IsValid(Fate fate)
{
var clientState = Service<ClientState>.Get();
if (fate == null)
return false;
if (clientState.LocalContentId == 0)
return false;
return true;
}
/// <summary>
/// Gets a value indicating whether this actor is still valid in memory.
/// </summary>
/// <returns>True or false.</returns>
public bool IsValid() => IsValid(this);
/// <inheritdoc/>
bool IEquatable<Fate>.Equals(Fate other) => this.FateId == other?.FateId;
/// <inheritdoc/>
public override bool Equals(object obj) => ((IEquatable<Fate>)this).Equals(obj as Fate);
/// <inheritdoc/>
public override int GetHashCode() => this.FateId.GetHashCode();
}
/// <summary>
/// This class represents an FFXIV Fate.
/// </summary>
public unsafe partial class Fate
{
/// <summary>
/// Gets the Fate ID of this <see cref="Fate" />.
/// </summary>
public ushort FateId => this.Struct->FateId;
/// <summary>
/// Gets game data linked to this Fate.
/// </summary>
public Lumina.Excel.GeneratedSheets.Fate GameData => Service<DataManager>.Get().GetExcelSheet<Lumina.Excel.GeneratedSheets.Fate>().GetRow(this.FateId);
/// <summary>
/// Gets the time this <see cref="Fate"/> started.
/// </summary>
public int StartTimeEpoch => this.Struct->StartTimeEpoch;
/// <summary>
/// Gets how long this <see cref="Fate"/> will run.
/// </summary>
public short Duration => this.Struct->Duration;
/// <summary>
/// Gets the remaining time in seconds for this <see cref="Fate"/>.
/// </summary>
public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds();
/// <summary>
/// Gets the displayname of this <see cref="Fate" />.
/// </summary>
public SeString Name => MemoryHelper.ReadSeString(&this.Struct->Name);
/// <summary>
/// Gets the state of this <see cref="Fate"/> (Running, Ended, Failed, Preparation, WaitingForEnd).
/// </summary>
public FateState State => (FateState)this.Struct->State;
/// <summary>
/// Gets the progress amount of this <see cref="Fate"/>.
/// </summary>
public byte Progress => this.Struct->Progress;
/// <summary>
/// Gets the level of this <see cref="Fate"/>.
/// </summary>
public byte Level => this.Struct->Level;
/// <summary>
/// Gets the position of this <see cref="Fate"/>.
/// </summary>
public Vector3 Position => this.Struct->Location;
/// <summary>
/// Gets the territory this <see cref="Fate"/> is located in.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.TerritoryType> TerritoryType => new(this.Struct->TerritoryID);
} }

View file

@ -1,32 +1,33 @@
namespace Dalamud.Game.ClientState.Fates; namespace Dalamud.Game.ClientState.Fates
/// <summary>
/// This represents the state of a single Fate.
/// </summary>
public enum FateState : byte
{ {
/// <summary> /// <summary>
/// The Fate is active. /// This represents the state of a single Fate.
/// </summary> /// </summary>
Running = 0x02, public enum FateState : byte
{
/// <summary>
/// The Fate is active.
/// </summary>
Running = 0x02,
/// <summary> /// <summary>
/// The Fate has ended. /// The Fate has ended.
/// </summary> /// </summary>
Ended = 0x04, Ended = 0x04,
/// <summary> /// <summary>
/// The player failed the Fate. /// The player failed the Fate.
/// </summary> /// </summary>
Failed = 0x05, Failed = 0x05,
/// <summary> /// <summary>
/// The Fate is preparing to run. /// The Fate is preparing to run.
/// </summary> /// </summary>
Preparation = 0x07, Preparation = 0x07,
/// <summary> /// <summary>
/// The Fate is preparing to end. /// The Fate is preparing to end.
/// </summary> /// </summary>
WaitingForEnd = 0x08, WaitingForEnd = 0x08,
}
} }

View file

@ -6,142 +6,143 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.Fates; namespace Dalamud.Game.ClientState.Fates
/// <summary>
/// This collection represents the currently available Fate events.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed partial class FateTable
{ {
private readonly ClientStateAddressResolver address;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="FateTable"/> class. /// This collection represents the currently available Fate events.
/// </summary> /// </summary>
/// <param name="addressResolver">Client state address resolver.</param> [PluginInterface]
internal FateTable(ClientStateAddressResolver addressResolver) [InterfaceVersion("1.0")]
public sealed partial class FateTable
{ {
this.address = addressResolver; private readonly ClientStateAddressResolver address;
Log.Verbose($"Fate table address 0x{this.address.FateTablePtr.ToInt64():X}"); /// <summary>
} /// Initializes a new instance of the <see cref="FateTable"/> class.
/// </summary>
/// <summary> /// <param name="addressResolver">Client state address resolver.</param>
/// Gets the address of the Fate table. internal FateTable(ClientStateAddressResolver addressResolver)
/// </summary>
public IntPtr Address => this.address.FateTablePtr;
/// <summary>
/// Gets the amount of currently active Fates.
/// </summary>
public unsafe int Length
{
get
{ {
var fateTable = this.FateTableAddress; this.address = addressResolver;
if (fateTable == IntPtr.Zero)
return 0;
// Sonar used this to check if the table was safe to read Log.Verbose($"Fate table address 0x{this.address.FateTablePtr.ToInt64():X}");
var check = Struct->Unk80.ToInt64();
if (check == 0)
return 0;
var start = Struct->FirstFatePtr.ToInt64();
var end = Struct->LastFatePtr.ToInt64();
if (start == 0 || end == 0)
return 0;
return (int)((end - start) / 8);
} }
}
/// <summary> /// <summary>
/// Gets the address of the Fate table. /// Gets the address of the Fate table.
/// </summary> /// </summary>
internal unsafe IntPtr FateTableAddress public IntPtr Address => this.address.FateTablePtr;
{
get /// <summary>
/// Gets the amount of currently active Fates.
/// </summary>
public unsafe int Length
{ {
if (this.address.FateTablePtr == IntPtr.Zero) get
{
var fateTable = this.FateTableAddress;
if (fateTable == IntPtr.Zero)
return 0;
// Sonar used this to check if the table was safe to read
var check = Struct->Unk80.ToInt64();
if (check == 0)
return 0;
var start = Struct->FirstFatePtr.ToInt64();
var end = Struct->LastFatePtr.ToInt64();
if (start == 0 || end == 0)
return 0;
return (int)((end - start) / 8);
}
}
/// <summary>
/// Gets the address of the Fate table.
/// </summary>
internal unsafe IntPtr FateTableAddress
{
get
{
if (this.address.FateTablePtr == IntPtr.Zero)
return IntPtr.Zero;
return *(IntPtr*)this.address.FateTablePtr;
}
}
private unsafe FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager*)this.FateTableAddress;
/// <summary>
/// Get an actor at the specified spawn index.
/// </summary>
/// <param name="index">Spawn index.</param>
/// <returns>A <see cref="Fate"/> at the specified spawn index.</returns>
public Fate? this[int index]
{
get
{
var address = this.GetFateAddress(index);
return this.CreateFateReference(address);
}
}
/// <summary>
/// Gets the address of the Fate at the specified index of the fate table.
/// </summary>
/// <param name="index">The index of the Fate.</param>
/// <returns>The memory address of the Fate.</returns>
public unsafe IntPtr GetFateAddress(int index)
{
if (index >= this.Length)
return IntPtr.Zero; return IntPtr.Zero;
return *(IntPtr*)this.address.FateTablePtr; var fateTable = this.FateTableAddress;
if (fateTable == IntPtr.Zero)
return IntPtr.Zero;
var firstFate = this.Struct->FirstFatePtr;
return *(IntPtr*)(firstFate + (8 * index));
} }
}
private unsafe FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager*)this.FateTableAddress; /// <summary>
/// Create a reference to a FFXIV actor.
/// <summary> /// </summary>
/// Get an actor at the specified spawn index. /// <param name="offset">The offset of the actor in memory.</param>
/// </summary> /// <returns><see cref="Fate"/> object containing requested data.</returns>
/// <param name="index">Spawn index.</param> public Fate? CreateFateReference(IntPtr offset)
/// <returns>A <see cref="Fate"/> at the specified spawn index.</returns>
public Fate? this[int index]
{
get
{ {
var address = this.GetFateAddress(index); var clientState = Service<ClientState>.Get();
return this.CreateFateReference(address);
if (clientState.LocalContentId == 0)
return null;
if (offset == IntPtr.Zero)
return null;
return new Fate(offset);
} }
} }
/// <summary> /// <summary>
/// Gets the address of the Fate at the specified index of the fate table. /// This collection represents the currently available Fate events.
/// </summary> /// </summary>
/// <param name="index">The index of the Fate.</param> public sealed partial class FateTable : IReadOnlyCollection<Fate>
/// <returns>The memory address of the Fate.</returns>
public unsafe IntPtr GetFateAddress(int index)
{ {
if (index >= this.Length) /// <inheritdoc/>
return IntPtr.Zero; int IReadOnlyCollection<Fate>.Count => this.Length;
var fateTable = this.FateTableAddress; /// <inheritdoc/>
if (fateTable == IntPtr.Zero) public IEnumerator<Fate> GetEnumerator()
return IntPtr.Zero; {
for (var i = 0; i < this.Length; i++)
{
yield return this[i];
}
}
var firstFate = this.Struct->FirstFatePtr; /// <inheritdoc/>
return *(IntPtr*)(firstFate + (8 * index)); IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}
/// <summary>
/// Create a reference to a FFXIV actor.
/// </summary>
/// <param name="offset">The offset of the actor in memory.</param>
/// <returns><see cref="Fate"/> object containing requested data.</returns>
public Fate? CreateFateReference(IntPtr offset)
{
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
return null;
if (offset == IntPtr.Zero)
return null;
return new Fate(offset);
} }
} }
/// <summary>
/// This collection represents the currently available Fate events.
/// </summary>
public sealed partial class FateTable : IReadOnlyCollection<Fate>
{
/// <inheritdoc/>
int IReadOnlyCollection<Fate>.Count => this.Length;
/// <inheritdoc/>
public IEnumerator<Fate> GetEnumerator()
{
for (var i = 0; i < this.Length; i++)
{
yield return this[i];
}
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}

View file

@ -1,95 +1,96 @@
using System; using System;
namespace Dalamud.Game.ClientState.GamePad; namespace Dalamud.Game.ClientState.GamePad
/// <summary>
/// Bitmask of the Button ushort used by the game.
/// </summary>
[Flags]
public enum GamepadButtons : ushort
{ {
/// <summary> /// <summary>
/// No buttons pressed. /// Bitmask of the Button ushort used by the game.
/// </summary> /// </summary>
None = 0, [Flags]
public enum GamepadButtons : ushort
{
/// <summary>
/// No buttons pressed.
/// </summary>
None = 0,
/// <summary> /// <summary>
/// Digipad up. /// Digipad up.
/// </summary> /// </summary>
DpadUp = 0x0001, DpadUp = 0x0001,
/// <summary> /// <summary>
/// Digipad down. /// Digipad down.
/// </summary> /// </summary>
DpadDown = 0x0002, DpadDown = 0x0002,
/// <summary> /// <summary>
/// Digipad left. /// Digipad left.
/// </summary> /// </summary>
DpadLeft = 0x0004, DpadLeft = 0x0004,
/// <summary> /// <summary>
/// Digipad right. /// Digipad right.
/// </summary> /// </summary>
DpadRight = 0x0008, DpadRight = 0x0008,
/// <summary> /// <summary>
/// North action button. Triangle on PS, Y on Xbox. /// North action button. Triangle on PS, Y on Xbox.
/// </summary> /// </summary>
North = 0x0010, North = 0x0010,
/// <summary> /// <summary>
/// South action button. Cross on PS, A on Xbox. /// South action button. Cross on PS, A on Xbox.
/// </summary> /// </summary>
South = 0x0020, South = 0x0020,
/// <summary> /// <summary>
/// West action button. Square on PS, X on Xbos. /// West action button. Square on PS, X on Xbos.
/// </summary> /// </summary>
West = 0x0040, West = 0x0040,
/// <summary> /// <summary>
/// East action button. Circle on PS, B on Xbox. /// East action button. Circle on PS, B on Xbox.
/// </summary> /// </summary>
East = 0x0080, East = 0x0080,
/// <summary> /// <summary>
/// First button on left shoulder side. /// First button on left shoulder side.
/// </summary> /// </summary>
L1 = 0x0100, L1 = 0x0100,
/// <summary> /// <summary>
/// Second button on left shoulder side. Analog input lost in this bitmask. /// Second button on left shoulder side. Analog input lost in this bitmask.
/// </summary> /// </summary>
L2 = 0x0200, L2 = 0x0200,
/// <summary> /// <summary>
/// Press on left analogue stick. /// Press on left analogue stick.
/// </summary> /// </summary>
L3 = 0x0400, L3 = 0x0400,
/// <summary> /// <summary>
/// First button on right shoulder. /// First button on right shoulder.
/// </summary> /// </summary>
R1 = 0x0800, R1 = 0x0800,
/// <summary> /// <summary>
/// Second button on right shoulder. Analog input lost in this bitmask. /// Second button on right shoulder. Analog input lost in this bitmask.
/// </summary> /// </summary>
R2 = 0x1000, R2 = 0x1000,
/// <summary> /// <summary>
/// Press on right analogue stick. /// Press on right analogue stick.
/// </summary> /// </summary>
R3 = 0x2000, R3 = 0x2000,
/// <summary> /// <summary>
/// Button on the right inner side of the controller. Options on PS, Start on Xbox. /// Button on the right inner side of the controller. Options on PS, Start on Xbox.
/// </summary> /// </summary>
Start = 0x8000, Start = 0x8000,
/// <summary> /// <summary>
/// Button on the left inner side of the controller. ??? on PS, Back on Xbox. /// Button on the left inner side of the controller. ??? on PS, Back on Xbox.
/// </summary> /// </summary>
Select = 0x4000, Select = 0x4000,
}
} }

View file

@ -1,75 +1,76 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Dalamud.Game.ClientState.GamePad; 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> /// <summary>
/// Left analogue stick's horizontal value, -99 for left, 99 for right. /// 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> /// </summary>
[FieldOffset(0x88)] [StructLayout(LayoutKind.Explicit)]
public int LeftStickX; public struct GamepadInput
{
/// <summary>
/// Left analogue stick's horizontal value, -99 for left, 99 for right.
/// </summary>
[FieldOffset(0x88)]
public int LeftStickX;
/// <summary> /// <summary>
/// Left analogue stick's vertical value, -99 for down, 99 for up. /// Left analogue stick's vertical value, -99 for down, 99 for up.
/// </summary> /// </summary>
[FieldOffset(0x8C)] [FieldOffset(0x8C)]
public int LeftStickY; public int LeftStickY;
/// <summary> /// <summary>
/// Right analogue stick's horizontal value, -99 for left, 99 for right. /// Right analogue stick's horizontal value, -99 for left, 99 for right.
/// </summary> /// </summary>
[FieldOffset(0x90)] [FieldOffset(0x90)]
public int RightStickX; public int RightStickX;
/// <summary> /// <summary>
/// Right analogue stick's vertical value, -99 for down, 99 for up. /// Right analogue stick's vertical value, -99 for down, 99 for up.
/// </summary> /// </summary>
[FieldOffset(0x94)] [FieldOffset(0x94)]
public int RightStickY; public int RightStickY;
/// <summary> /// <summary>
/// Raw input, set the whole time while a button is held. See <see cref="GamepadButtons"/> for the mapping. /// Raw input, set the whole time while a button is held. See <see cref="GamepadButtons"/> for the mapping.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is a bitfield. /// This is a bitfield.
/// </remarks> /// </remarks>
[FieldOffset(0x98)] [FieldOffset(0x98)]
public ushort ButtonsRaw; public ushort ButtonsRaw;
/// <summary> /// <summary>
/// Button pressed, set once when the button is pressed. See <see cref="GamepadButtons"/> for the mapping. /// Button pressed, set once when the button is pressed. See <see cref="GamepadButtons"/> for the mapping.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is a bitfield. /// This is a bitfield.
/// </remarks> /// </remarks>
[FieldOffset(0x9C)] [FieldOffset(0x9C)]
public ushort ButtonsPressed; public ushort ButtonsPressed;
/// <summary> /// <summary>
/// Button released input, set once right after the button is not hold anymore. See <see cref="GamepadButtons"/> for the mapping. /// Button released input, set once right after the button is not hold anymore. See <see cref="GamepadButtons"/> for the mapping.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is a bitfield. /// This is a bitfield.
/// </remarks> /// </remarks>
[FieldOffset(0xA0)] [FieldOffset(0xA0)]
public ushort ButtonsReleased; public ushort ButtonsReleased;
/// <summary> /// <summary>
/// Repeatedly emits the held button input in fixed intervals. See <see cref="GamepadButtons"/> for the mapping. /// Repeatedly emits the held button input in fixed intervals. See <see cref="GamepadButtons"/> for the mapping.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is a bitfield. /// This is a bitfield.
/// </remarks> /// </remarks>
[FieldOffset(0xA4)] [FieldOffset(0xA4)]
public ushort ButtonsRepeat; public ushort ButtonsRepeat;
}
} }

View file

@ -4,257 +4,258 @@ using Dalamud.Hooking;
using ImGuiNET; using ImGuiNET;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.GamePad; namespace Dalamud.Game.ClientState.GamePad
/// <summary>
/// Exposes the game gamepad state to dalamud.
///
/// Will block game's gamepad input if <see cref="ImGuiConfigFlags.NavEnableGamepad"/> is set.
/// </summary>
public unsafe class GamepadState : IDisposable
{ {
private readonly Hook<ControllerPoll> gamepadPoll;
private bool isDisposed;
private int leftStickX;
private int leftStickY;
private int rightStickX;
private int rightStickY;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GamepadState" /> class. /// Exposes the game gamepad state to dalamud.
///
/// Will block game's gamepad input if <see cref="ImGuiConfigFlags.NavEnableGamepad"/> is set.
/// </summary> /// </summary>
/// <param name="resolver">Resolver knowing the pointer to the GamepadPoll function.</param> public unsafe class GamepadState : IDisposable
public GamepadState(ClientStateAddressResolver resolver)
{ {
Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}"); private readonly Hook<ControllerPoll> gamepadPoll;
this.gamepadPoll = new Hook<ControllerPoll>(resolver.GamepadPoll, this.GamepadPollDetour);
}
/// <summary> private bool isDisposed;
/// Finalizes an instance of the <see cref="GamepadState" /> class.
/// </summary>
~GamepadState()
{
this.Dispose(false);
}
private delegate int ControllerPoll(IntPtr controllerInput); private int leftStickX;
private int leftStickY;
private int rightStickX;
private int rightStickY;
/// <summary> /// <summary>
/// Gets the pointer to the current instance of the GamepadInput struct. /// Initializes a new instance of the <see cref="GamepadState" /> class.
/// </summary> /// </summary>
public IntPtr GamepadInputAddress { get; private set; } /// <param name="resolver">Resolver knowing the pointer to the GamepadPoll function.</param>
public GamepadState(ClientStateAddressResolver resolver)
/// <summary>
/// Gets the state of the left analogue stick in the left direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float LeftStickLeft => this.leftStickX < 0 ? -this.leftStickX / 100f : 0;
/// <summary>
/// Gets the state of the left analogue stick in the right direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float LeftStickRight => this.leftStickX > 0 ? this.leftStickX / 100f : 0;
/// <summary>
/// Gets the state of the left analogue stick in the up direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float LeftStickUp => this.leftStickY > 0 ? this.leftStickY / 100f : 0;
/// <summary>
/// Gets the state of the left analogue stick in the down direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float LeftStickDown => this.leftStickY < 0 ? -this.leftStickY / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the left direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float RightStickLeft => this.rightStickX < 0 ? -this.rightStickX / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the right direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float RightStickRight => this.rightStickX > 0 ? this.rightStickX / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the up direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float RightStickUp => this.rightStickY > 0 ? this.rightStickY / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the down direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float RightStickDown => this.rightStickY < 0 ? -this.rightStickY / 100f : 0;
/// <summary>
/// Gets buttons pressed bitmask, set once when the button is pressed. See <see cref="GamepadButtons"/> for the mapping.
///
/// Exposed internally for Debug Data window.
/// </summary>
internal ushort 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; }
/// <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; }
/// <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; }
/// <summary>
/// Gets or sets a value indicating whether detour should block gamepad input for game.
///
/// Ideally, we would use
/// (ImGui.GetIO().ConfigFlags &amp; ImGuiConfigFlags.NavEnableGamepad) > 0
/// but this has a race condition during load with the detour which sets up ImGui
/// and throws if our detour gets called before the other.
/// </summary>
internal bool NavEnableGamepad { get; set; }
/// <summary>
/// Gets whether <paramref name="button"/> has been pressed.
///
/// Only true on first frame of the press.
/// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
/// </summary>
/// <param name="button">The button to check for.</param>
/// <returns>1 if pressed, 0 otherwise.</returns>
public float Pressed(GamepadButtons button) => (this.ButtonsPressed & (ushort)button) > 0 ? 1 : 0;
/// <summary>
/// Gets whether <paramref name="button"/> is being pressed.
///
/// True in intervals if button is held down.
/// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
/// </summary>
/// <param name="button">The button to check for.</param>
/// <returns>1 if still pressed during interval, 0 otherwise or in between intervals.</returns>
public float Repeat(GamepadButtons button) => (this.ButtonsRepeat & (ushort)button) > 0 ? 1 : 0;
/// <summary>
/// Gets whether <paramref name="button"/> has been released.
///
/// Only true the frame after release.
/// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
/// </summary>
/// <param name="button">The button to check for.</param>
/// <returns>1 if released, 0 otherwise.</returns>
public float Released(GamepadButtons button) => (this.ButtonsReleased & (ushort)button) > 0 ? 1 : 0;
/// <summary>
/// Gets the raw state of <paramref name="button"/>.
///
/// Is set the entire time a button is pressed down.
/// </summary>
/// <param name="button">The button to check for.</param>
/// <returns>1 the whole time button is pressed, 0 otherwise.</returns>
public float Raw(GamepadButtons button) => (this.ButtonsRaw & (ushort)button) > 0 ? 1 : 0;
/// <summary>
/// Enables the hook of the GamepadPoll function.
/// </summary>
public void Enable()
{
this.gamepadPoll.Enable();
}
/// <summary>
/// Disposes this instance, alongside its hooks.
/// </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
private int GamepadPollDetour(IntPtr gamepadInput)
{
var original = this.gamepadPoll.Original(gamepadInput);
try
{ {
this.GamepadInputAddress = gamepadInput; Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}");
var input = (GamepadInput*)gamepadInput; this.gamepadPoll = new Hook<ControllerPoll>(resolver.GamepadPoll, this.GamepadPollDetour);
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;
if (this.NavEnableGamepad) /// <summary>
/// Finalizes an instance of the <see cref="GamepadState" /> class.
/// </summary>
~GamepadState()
{
this.Dispose(false);
}
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; }
/// <summary>
/// Gets the state of the left analogue stick in the left direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float LeftStickLeft => this.leftStickX < 0 ? -this.leftStickX / 100f : 0;
/// <summary>
/// Gets the state of the left analogue stick in the right direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float LeftStickRight => this.leftStickX > 0 ? this.leftStickX / 100f : 0;
/// <summary>
/// Gets the state of the left analogue stick in the up direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float LeftStickUp => this.leftStickY > 0 ? this.leftStickY / 100f : 0;
/// <summary>
/// Gets the state of the left analogue stick in the down direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float LeftStickDown => this.leftStickY < 0 ? -this.leftStickY / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the left direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float RightStickLeft => this.rightStickX < 0 ? -this.rightStickX / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the right direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float RightStickRight => this.rightStickX > 0 ? this.rightStickX / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the up direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float RightStickUp => this.rightStickY > 0 ? this.rightStickY / 100f : 0;
/// <summary>
/// Gets the state of the right analogue stick in the down direction between 0 (not tilted) and 1 (max tilt).
/// </summary>
public float RightStickDown => this.rightStickY < 0 ? -this.rightStickY / 100f : 0;
/// <summary>
/// Gets buttons pressed bitmask, set once when the button is pressed. See <see cref="GamepadButtons"/> for the mapping.
///
/// Exposed internally for Debug Data window.
/// </summary>
internal ushort 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; }
/// <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; }
/// <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; }
/// <summary>
/// Gets or sets a value indicating whether detour should block gamepad input for game.
///
/// Ideally, we would use
/// (ImGui.GetIO().ConfigFlags &amp; ImGuiConfigFlags.NavEnableGamepad) > 0
/// but this has a race condition during load with the detour which sets up ImGui
/// and throws if our detour gets called before the other.
/// </summary>
internal bool NavEnableGamepad { get; set; }
/// <summary>
/// Gets whether <paramref name="button"/> has been pressed.
///
/// Only true on first frame of the press.
/// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
/// </summary>
/// <param name="button">The button to check for.</param>
/// <returns>1 if pressed, 0 otherwise.</returns>
public float Pressed(GamepadButtons button) => (this.ButtonsPressed & (ushort)button) > 0 ? 1 : 0;
/// <summary>
/// Gets whether <paramref name="button"/> is being pressed.
///
/// True in intervals if button is held down.
/// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
/// </summary>
/// <param name="button">The button to check for.</param>
/// <returns>1 if still pressed during interval, 0 otherwise or in between intervals.</returns>
public float Repeat(GamepadButtons button) => (this.ButtonsRepeat & (ushort)button) > 0 ? 1 : 0;
/// <summary>
/// Gets whether <paramref name="button"/> has been released.
///
/// Only true the frame after release.
/// If ImGuiConfigFlags.NavEnableGamepad is set, this is unreliable.
/// </summary>
/// <param name="button">The button to check for.</param>
/// <returns>1 if released, 0 otherwise.</returns>
public float Released(GamepadButtons button) => (this.ButtonsReleased & (ushort)button) > 0 ? 1 : 0;
/// <summary>
/// Gets the raw state of <paramref name="button"/>.
///
/// Is set the entire time a button is pressed down.
/// </summary>
/// <param name="button">The button to check for.</param>
/// <returns>1 the whole time button is pressed, 0 otherwise.</returns>
public float Raw(GamepadButtons button) => (this.ButtonsRaw & (ushort)button) > 0 ? 1 : 0;
/// <summary>
/// Enables the hook of the GamepadPoll function.
/// </summary>
public void Enable()
{
this.gamepadPoll.Enable();
}
/// <summary>
/// Disposes this instance, alongside its hooks.
/// </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
private int GamepadPollDetour(IntPtr gamepadInput)
{
var original = this.gamepadPoll.Original(gamepadInput);
try
{ {
input->LeftStickX = 0; this.GamepadInputAddress = gamepadInput;
input->LeftStickY = 0; var input = (GamepadInput*)gamepadInput;
input->RightStickX = 0; this.leftStickX = input->LeftStickX;
input->RightStickY = 0; 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;
// NOTE (Chiv) Zeroing `ButtonsRaw` destroys `ButtonPressed`, `ButtonReleased` if (this.NavEnableGamepad)
// and `ButtonRepeat` as the game uses the RAW input to determine those (apparently). {
// It does block, however, all input to the game. input->LeftStickX = 0;
// Leaving `ButtonsRaw` as it is and only zeroing the other leaves e.g. long-hold L2/R2 input->LeftStickY = 0;
// and the digipad (in some situations, but thankfully not in menus) functional. input->RightStickX = 0;
// We can either: input->RightStickY = 0;
// (a) Explicitly only set L2/R2/Digipad to 0 (and destroy their `ButtonPressed` field) => Needs to be documented, or
// (b) ignore it as so far it seems only a 'visual' error // NOTE (Chiv) Zeroing `ButtonsRaw` destroys `ButtonPressed`, `ButtonReleased`
// (L2/R2 being held down activates CrossHotBar but activating an ability is impossible because of the others blocked input, // and `ButtonRepeat` as the game uses the RAW input to determine those (apparently).
// Digipad is ignored in menus but without any menu's one still switches target or party members, but cannot interact with them // It does block, however, all input to the game.
// because of the other blocked input) // Leaving `ButtonsRaw` as it is and only zeroing the other leaves e.g. long-hold L2/R2
// `ButtonPressed` is pretty useful but its hella confusing to the user, so we do (a) and advise plugins do not rely on // and the digipad (in some situations, but thankfully not in menus) functional.
// `ButtonPressed` while ImGuiConfigFlags.NavEnableGamepad is set. // We can either:
// This is debatable. // (a) Explicitly only set L2/R2/Digipad to 0 (and destroy their `ButtonPressed` field) => Needs to be documented, or
// ImGui itself does not care either way as it uses the Raw values and does its own state handling. // (b) ignore it as so far it seems only a 'visual' error
const ushort deletionMask = (ushort)(~GamepadButtons.L2 // (L2/R2 being held down activates CrossHotBar but activating an ability is impossible because of the others blocked input,
& ~GamepadButtons.R2 // Digipad is ignored in menus but without any menu's one still switches target or party members, but cannot interact with them
& ~GamepadButtons.DpadDown // because of the other blocked input)
& ~GamepadButtons.DpadLeft // `ButtonPressed` is pretty useful but its hella confusing to the user, so we do (a) and advise plugins do not rely on
& ~GamepadButtons.DpadUp // `ButtonPressed` while ImGuiConfigFlags.NavEnableGamepad is set.
& ~GamepadButtons.DpadRight); // This is debatable.
input->ButtonsRaw &= deletionMask; // ImGui itself does not care either way as it uses the Raw values and does its own state handling.
input->ButtonsPressed = 0; const ushort deletionMask = (ushort)(~GamepadButtons.L2
input->ButtonsReleased = 0; & ~GamepadButtons.R2
input->ButtonsRepeat = 0; & ~GamepadButtons.DpadDown
return 0; & ~GamepadButtons.DpadLeft
& ~GamepadButtons.DpadUp
& ~GamepadButtons.DpadRight);
input->ButtonsRaw &= deletionMask;
input->ButtonsPressed = 0;
input->ButtonsReleased = 0;
input->ButtonsRepeat = 0;
return 0;
}
// NOTE (Chiv) Not so sure about the return value, does not seem to matter if we return the
// original, zero or do the work adjusting the bits.
return original;
}
catch (Exception e)
{
Log.Error(e, $"Gamepad Poll detour critical error! Gamepad navigation will not work!");
// NOTE (Chiv) Explicitly deactivate on error
ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.NavEnableGamepad;
return original;
}
}
private void Dispose(bool disposing)
{
if (this.isDisposed) return;
if (disposing)
{
this.gamepadPoll?.Disable();
this.gamepadPoll?.Dispose();
} }
// NOTE (Chiv) Not so sure about the return value, does not seem to matter if we return the this.isDisposed = true;
// original, zero or do the work adjusting the bits.
return original;
} }
catch (Exception e)
{
Log.Error(e, $"Gamepad Poll detour critical error! Gamepad navigation will not work!");
// NOTE (Chiv) Explicitly deactivate on error
ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.NavEnableGamepad;
return original;
}
}
private void Dispose(bool disposing)
{
if (this.isDisposed) return;
if (disposing)
{
this.gamepadPoll?.Disable();
this.gamepadPoll?.Dispose();
}
this.isDisposed = true;
} }
} }

View file

@ -1,22 +1,23 @@
namespace Dalamud.Game.ClientState.JobGauge.Enums; namespace Dalamud.Game.ClientState.JobGauge.Enums
/// <summary>
/// DRG Blood of the Dragon state types.
/// </summary>
public enum BOTDState : byte
{ {
/// <summary> /// <summary>
/// Inactive type. /// DRG Blood of the Dragon state types.
/// </summary> /// </summary>
NONE = 0, public enum BOTDState : byte
{
/// <summary>
/// Inactive type.
/// </summary>
NONE = 0,
/// <summary> /// <summary>
/// Blood of the Dragon is active. /// Blood of the Dragon is active.
/// </summary> /// </summary>
BOTD = 1, BOTD = 1,
/// <summary> /// <summary>
/// Life of the Dragon is active. /// Life of the Dragon is active.
/// </summary> /// </summary>
LOTD = 2, LOTD = 2,
}
} }

View file

@ -1,52 +1,53 @@
namespace Dalamud.Game.ClientState.JobGauge.Enums; namespace Dalamud.Game.ClientState.JobGauge.Enums
/// <summary>
/// AST Arcanum (card) types.
/// </summary>
public enum CardType : byte
{ {
/// <summary> /// <summary>
/// No card. /// AST Arcanum (card) types.
/// </summary> /// </summary>
NONE = 0, public enum CardType : byte
{
/// <summary>
/// No card.
/// </summary>
NONE = 0,
/// <summary> /// <summary>
/// The Balance card. /// The Balance card.
/// </summary> /// </summary>
BALANCE = 1, BALANCE = 1,
/// <summary> /// <summary>
/// The Bole card. /// The Bole card.
/// </summary> /// </summary>
BOLE = 2, BOLE = 2,
/// <summary> /// <summary>
/// The Arrow card. /// The Arrow card.
/// </summary> /// </summary>
ARROW = 3, ARROW = 3,
/// <summary> /// <summary>
/// The Spear card. /// The Spear card.
/// </summary> /// </summary>
SPEAR = 4, SPEAR = 4,
/// <summary> /// <summary>
/// The Ewer card. /// The Ewer card.
/// </summary> /// </summary>
EWER = 5, EWER = 5,
/// <summary> /// <summary>
/// The Spire card. /// The Spire card.
/// </summary> /// </summary>
SPIRE = 6, SPIRE = 6,
/// <summary> /// <summary>
/// The Lord of Crowns card. /// The Lord of Crowns card.
/// </summary> /// </summary>
LORD = 0x70, LORD = 0x70,
/// <summary> /// <summary>
/// The Lady of Crowns card. /// The Lady of Crowns card.
/// </summary> /// </summary>
LADY = 0x80, LADY = 0x80,
}
} }

View file

@ -1,17 +1,18 @@
namespace Dalamud.Game.ClientState.JobGauge.Enums; namespace Dalamud.Game.ClientState.JobGauge.Enums
/// <summary>
/// SCH Dismissed fairy types.
/// </summary>
public enum DismissedFairy : byte
{ {
/// <summary> /// <summary>
/// Dismissed fairy is Eos. /// SCH Dismissed fairy types.
/// </summary> /// </summary>
EOS = 6, public enum DismissedFairy : byte
{
/// <summary>
/// Dismissed fairy is Eos.
/// </summary>
EOS = 6,
/// <summary> /// <summary>
/// Dismissed fairy is Selene. /// Dismissed fairy is Selene.
/// </summary> /// </summary>
SELENE = 7, SELENE = 7,
}
} }

View file

@ -1,22 +1,23 @@
namespace Dalamud.Game.ClientState.JobGauge.Enums; namespace Dalamud.Game.ClientState.JobGauge.Enums
/// <summary>
/// NIN Mudra types.
/// </summary>
public enum Mudras : byte
{ {
/// <summary> /// <summary>
/// Ten mudra. /// NIN Mudra types.
/// </summary> /// </summary>
TEN = 1, public enum Mudras : byte
{
/// <summary>
/// Ten mudra.
/// </summary>
TEN = 1,
/// <summary> /// <summary>
/// Chi mudra. /// Chi mudra.
/// </summary> /// </summary>
CHI = 2, CHI = 2,
/// <summary> /// <summary>
/// Jin mudra. /// Jin mudra.
/// </summary> /// </summary>
JIN = 3, JIN = 3,
}
} }

View file

@ -1,27 +1,28 @@
namespace Dalamud.Game.ClientState.JobGauge.Enums; namespace Dalamud.Game.ClientState.JobGauge.Enums
/// <summary>
/// SMN summoned pet glam types.
/// </summary>
public enum PetGlam : byte
{ {
/// <summary> /// <summary>
/// No pet glam. /// SMN summoned pet glam types.
/// </summary> /// </summary>
NONE = 0, public enum PetGlam : byte
{
/// <summary>
/// No pet glam.
/// </summary>
NONE = 0,
/// <summary> /// <summary>
/// Emerald carbuncle pet glam. /// Emerald carbuncle pet glam.
/// </summary> /// </summary>
EMERALD = 1, EMERALD = 1,
/// <summary> /// <summary>
/// Topaz carbuncle pet glam. /// Topaz carbuncle pet glam.
/// </summary> /// </summary>
TOPAZ = 2, TOPAZ = 2,
/// <summary> /// <summary>
/// Ruby carbuncle pet glam. /// Ruby carbuncle pet glam.
/// </summary> /// </summary>
RUBY = 3, RUBY = 3,
}
} }

View file

@ -1,27 +1,28 @@
namespace Dalamud.Game.ClientState.JobGauge.Enums; namespace Dalamud.Game.ClientState.JobGauge.Enums
/// <summary>
/// AST Divination seal types.
/// </summary>
public enum SealType : byte
{ {
/// <summary> /// <summary>
/// No seal. /// AST Divination seal types.
/// </summary> /// </summary>
NONE = 0, public enum SealType : byte
{
/// <summary>
/// No seal.
/// </summary>
NONE = 0,
/// <summary> /// <summary>
/// Sun seal. /// Sun seal.
/// </summary> /// </summary>
SUN = 1, SUN = 1,
/// <summary> /// <summary>
/// Moon seal. /// Moon seal.
/// </summary> /// </summary>
MOON = 2, MOON = 2,
/// <summary> /// <summary>
/// Celestial seal. /// Celestial seal.
/// </summary> /// </summary>
CELESTIAL = 3, CELESTIAL = 3,
}
} }

View file

@ -1,30 +1,31 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Enums; namespace Dalamud.Game.ClientState.JobGauge.Enums
/// <summary>
/// Samurai Sen types.
/// </summary>
[Flags]
public enum Sen : byte
{ {
/// <summary> /// <summary>
/// No Sen. /// Samurai Sen types.
/// </summary> /// </summary>
NONE = 0, [Flags]
public enum Sen : byte
{
/// <summary>
/// No Sen.
/// </summary>
NONE = 0,
/// <summary> /// <summary>
/// Setsu Sen type. /// Setsu Sen type.
/// </summary> /// </summary>
SETSU = 1 << 0, SETSU = 1 << 0,
/// <summary> /// <summary>
/// Getsu Sen type. /// Getsu Sen type.
/// </summary> /// </summary>
GETSU = 1 << 1, GETSU = 1 << 1,
/// <summary> /// <summary>
/// Ka Sen type. /// Ka Sen type.
/// </summary> /// </summary>
KA = 1 << 2, KA = 1 << 2,
}
} }

View file

@ -1,27 +1,28 @@
namespace Dalamud.Game.ClientState.JobGauge.Enums; namespace Dalamud.Game.ClientState.JobGauge.Enums
/// <summary>
/// BRD Song types.
/// </summary>
public enum Song : byte
{ {
/// <summary> /// <summary>
/// No song is active type. /// BRD Song types.
/// </summary> /// </summary>
NONE = 0, public enum Song : byte
{
/// <summary>
/// No song is active type.
/// </summary>
NONE = 0,
/// <summary> /// <summary>
/// Mage's Ballad type. /// Mage's Ballad type.
/// </summary> /// </summary>
MAGE = 5, MAGE = 5,
/// <summary> /// <summary>
/// Army's Paeon type. /// Army's Paeon type.
/// </summary> /// </summary>
ARMY = 10, ARMY = 10,
/// <summary> /// <summary>
/// The Wanderer's Minuet type. /// The Wanderer's Minuet type.
/// </summary> /// </summary>
WANDERER = 15, WANDERER = 15,
}
} }

View file

@ -1,27 +1,28 @@
namespace Dalamud.Game.ClientState.JobGauge.Enums; namespace Dalamud.Game.ClientState.JobGauge.Enums
/// <summary>
/// SMN summoned pet types.
/// </summary>
public enum SummonPet : byte
{ {
/// <summary> /// <summary>
/// No pet. /// SMN summoned pet types.
/// </summary> /// </summary>
NONE = 0, public enum SummonPet : byte
{
/// <summary>
/// No pet.
/// </summary>
NONE = 0,
/// <summary> /// <summary>
/// The summoned pet Ifrit. /// The summoned pet Ifrit.
/// </summary> /// </summary>
IFRIT = 3, IFRIT = 3,
/// <summary> /// <summary>
/// The summoned pet Titan. /// The summoned pet Titan.
/// </summary> /// </summary>
TITAN = 4, TITAN = 4,
/// <summary> /// <summary>
/// The summoned pet Garuda. /// The summoned pet Garuda.
/// </summary> /// </summary>
GARUDA = 5, GARUDA = 5,
}
} }

View file

@ -7,47 +7,48 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.JobGauge; namespace Dalamud.Game.ClientState.JobGauge
/// <summary>
/// This class converts in-memory Job gauge data to structs.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public class JobGauges
{ {
private Dictionary<Type, JobGaugeBase> cache = new();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="JobGauges"/> class. /// This class converts in-memory Job gauge data to structs.
/// </summary> /// </summary>
/// <param name="addressResolver">Address resolver with the JobGauge memory location(s).</param> [PluginInterface]
public JobGauges(ClientStateAddressResolver addressResolver) [InterfaceVersion("1.0")]
public class JobGauges
{ {
this.Address = addressResolver.JobGaugeData; private Dictionary<Type, JobGaugeBase> cache = new();
Log.Verbose($"JobGaugeData address 0x{this.Address.ToInt64():X}"); /// <summary>
} /// Initializes a new instance of the <see cref="JobGauges"/> class.
/// </summary>
/// <summary> /// <param name="addressResolver">Address resolver with the JobGauge memory location(s).</param>
/// Gets the address of the JobGauge data. public JobGauges(ClientStateAddressResolver addressResolver)
/// </summary>
public IntPtr Address { get; }
/// <summary>
/// Get the JobGauge for a given job.
/// </summary>
/// <typeparam name="T">A JobGauge struct from ClientState.Structs.JobGauge.</typeparam>
/// <returns>A JobGauge.</returns>
public T Get<T>() where T : JobGaugeBase
{
// This is cached to mitigate the effects of using activator for instantiation.
// Since the gauge itself reads from live memory, there isn't much downside to doing this.
if (!this.cache.TryGetValue(typeof(T), out var gauge))
{ {
gauge = this.cache[typeof(T)] = (T)Activator.CreateInstance(typeof(T), BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { this.Address }, null); this.Address = addressResolver.JobGaugeData;
Log.Verbose($"JobGaugeData address 0x{this.Address.ToInt64():X}");
} }
return (T)gauge; /// <summary>
/// Gets the address of the JobGauge data.
/// </summary>
public IntPtr Address { get; }
/// <summary>
/// Get the JobGauge for a given job.
/// </summary>
/// <typeparam name="T">A JobGauge struct from ClientState.Structs.JobGauge.</typeparam>
/// <returns>A JobGauge.</returns>
public T Get<T>() where T : JobGaugeBase
{
// This is cached to mitigate the effects of using activator for instantiation.
// Since the gauge itself reads from live memory, there isn't much downside to doing this.
if (!this.cache.TryGetValue(typeof(T), out var gauge))
{
gauge = this.cache[typeof(T)] = (T)Activator.CreateInstance(typeof(T), BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { this.Address }, null);
}
return (T)gauge;
}
} }
} }

View file

@ -2,38 +2,39 @@ using System;
using Dalamud.Game.ClientState.JobGauge.Enums; using Dalamud.Game.ClientState.JobGauge.Enums;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// In-memory AST job gauge.
/// </summary>
public unsafe class ASTGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.AstrologianGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ASTGauge"/> class. /// In-memory AST job gauge.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> public unsafe class ASTGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.AstrologianGauge>
internal ASTGauge(IntPtr address)
: base(address)
{ {
} /// <summary>
/// Initializes a new instance of the <see cref="ASTGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal ASTGauge(IntPtr address)
: base(address)
{
}
/// <summary> /// <summary>
/// Gets the currently drawn <see cref="CardType"/>. /// Gets the currently drawn <see cref="CardType"/>.
/// </summary> /// </summary>
/// <returns>Currently drawn <see cref="CardType"/>.</returns> /// <returns>Currently drawn <see cref="CardType"/>.</returns>
public CardType DrawnCard => (CardType)this.Struct->Card; public CardType DrawnCard => (CardType)this.Struct->Card;
/// <summary> /// <summary>
/// Check if a <see cref="SealType"/> is currently active on the divination gauge. /// Check if a <see cref="SealType"/> is currently active on the divination gauge.
/// </summary> /// </summary>
/// <param name="seal">The <see cref="SealType"/> to check for.</param> /// <param name="seal">The <see cref="SealType"/> to check for.</param>
/// <returns>If the given Seal is currently divined.</returns> /// <returns>If the given Seal is currently divined.</returns>
public unsafe bool ContainsSeal(SealType seal) public unsafe bool ContainsSeal(SealType seal)
{ {
if (this.Struct->Seals[0] == (byte)seal) return true; if (this.Struct->Seals[0] == (byte)seal) return true;
if (this.Struct->Seals[1] == (byte)seal) return true; if (this.Struct->Seals[1] == (byte)seal) return true;
if (this.Struct->Seals[2] == (byte)seal) return true; if (this.Struct->Seals[2] == (byte)seal) return true;
return false; return false;
}
} }
} }

View file

@ -1,66 +1,67 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// In-memory BLM job gauge.
/// </summary>
public unsafe class BLMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.BlackMageGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BLMGauge"/> class. /// In-memory BLM job gauge.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> public unsafe class BLMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.BlackMageGauge>
internal BLMGauge(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="BLMGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal BLMGauge(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the time remaining for the Enochian time in milliseconds.
/// </summary>
public short EnochianTimer => this.Struct->EnochianTimer;
/// <summary>
/// Gets the time remaining for Astral Fire or Umbral Ice in milliseconds.
/// </summary>
public short ElementTimeRemaining => this.Struct->ElementTimeRemaining;
/// <summary>
/// Gets the number of Polyglot stacks remaining.
/// </summary>
public byte PolyglotStacks => this.Struct->PolyglotStacks;
/// <summary>
/// Gets the number of Umbral Hearts remaining.
/// </summary>
public byte UmbralHearts => this.Struct->UmbralHearts;
/// <summary>
/// Gets the amount of Umbral Ice stacks.
/// </summary>
public byte UmbralIceStacks => (byte)(this.InUmbralIce ? -this.Struct->ElementStance : 0);
/// <summary>
/// Gets the amount of Astral Fire stacks.
/// </summary>
public byte AstralFireStacks => (byte)(this.InAstralFire ? this.Struct->ElementStance : 0);
/// <summary>
/// Gets a value indicating whether if the player is in Umbral Ice.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool InUmbralIce => this.Struct->ElementStance < 0;
/// <summary>
/// Gets a value indicating whether if the player is in Astral fire.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool InAstralFire => this.Struct->ElementStance > 0;
/// <summary>
/// Gets a value indicating whether if Enochian is active.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsEnochianActive => this.Struct->Enochian != 0;
} }
/// <summary>
/// Gets the time remaining for the Enochian time in milliseconds.
/// </summary>
public short EnochianTimer => this.Struct->EnochianTimer;
/// <summary>
/// Gets the time remaining for Astral Fire or Umbral Ice in milliseconds.
/// </summary>
public short ElementTimeRemaining => this.Struct->ElementTimeRemaining;
/// <summary>
/// Gets the number of Polyglot stacks remaining.
/// </summary>
public byte PolyglotStacks => this.Struct->PolyglotStacks;
/// <summary>
/// Gets the number of Umbral Hearts remaining.
/// </summary>
public byte UmbralHearts => this.Struct->UmbralHearts;
/// <summary>
/// Gets the amount of Umbral Ice stacks.
/// </summary>
public byte UmbralIceStacks => (byte)(this.InUmbralIce ? -this.Struct->ElementStance : 0);
/// <summary>
/// Gets the amount of Astral Fire stacks.
/// </summary>
public byte AstralFireStacks => (byte)(this.InAstralFire ? this.Struct->ElementStance : 0);
/// <summary>
/// Gets a value indicating whether if the player is in Umbral Ice.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool InUmbralIce => this.Struct->ElementStance < 0;
/// <summary>
/// Gets a value indicating whether if the player is in Astral fire.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool InAstralFire => this.Struct->ElementStance > 0;
/// <summary>
/// Gets a value indicating whether if Enochian is active.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsEnochianActive => this.Struct->Enochian != 0;
} }

View file

@ -2,39 +2,40 @@ using System;
using Dalamud.Game.ClientState.JobGauge.Enums; using Dalamud.Game.ClientState.JobGauge.Enums;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// In-memory BRD job gauge.
/// </summary>
public unsafe class BRDGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.BardGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BRDGauge"/> class. /// In-memory BRD job gauge.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> public unsafe class BRDGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.BardGauge>
internal BRDGauge(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="BRDGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal BRDGauge(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the current song timer in milliseconds.
/// </summary>
public short SongTimer => this.Struct->SongTimer;
/// <summary>
/// Gets the amount of Repertoire accumulated.
/// </summary>
public byte Repertoire => this.Struct->Repertoire;
/// <summary>
/// Gets the amount of Soul Voice accumulated.
/// </summary>
public byte SoulVoice => this.Struct->SoulVoice;
/// <summary>
/// Gets the type of song that is active.
/// </summary>
public Song Song => (Song)this.Struct->Song;
} }
/// <summary>
/// Gets the current song timer in milliseconds.
/// </summary>
public short SongTimer => this.Struct->SongTimer;
/// <summary>
/// Gets the amount of Repertoire accumulated.
/// </summary>
public byte Repertoire => this.Struct->Repertoire;
/// <summary>
/// Gets the amount of Soul Voice accumulated.
/// </summary>
public byte SoulVoice => this.Struct->SoulVoice;
/// <summary>
/// Gets the type of song that is active.
/// </summary>
public Song Song => (Song)this.Struct->Song;
} }

View file

@ -1,59 +1,60 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// In-memory DNC job gauge.
/// </summary>
public unsafe class DNCGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.DancerGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DNCGauge"/> class. /// In-memory DNC job gauge.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> public unsafe class DNCGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.DancerGauge>
internal DNCGauge(IntPtr address)
: base(address)
{ {
} /// <summary>
/// Initializes a new instance of the <see cref="DNCGauge"/> class.
/// <summary> /// </summary>
/// Gets the number of feathers available. /// <param name="address">Address of the job gauge.</param>
/// </summary> internal DNCGauge(IntPtr address)
public byte Feathers => this.Struct->Feathers; : base(address)
/// <summary>
/// Gets the amount of Espirit available.
/// </summary>
public byte Esprit => this.Struct->Esprit;
/// <summary>
/// Gets the number of steps completed for the current dance.
/// </summary>
public byte CompletedSteps => this.Struct->StepIndex;
/// <summary>
/// Gets all the steps in the current dance.
/// </summary>
public unsafe uint[] Steps
{
get
{ {
var arr = new uint[4];
for (var i = 0; i < 4; i++)
arr[i] = this.Struct->DanceSteps[i] + 15999u - 1;
return arr;
} }
/// <summary>
/// Gets the number of feathers available.
/// </summary>
public byte Feathers => this.Struct->Feathers;
/// <summary>
/// Gets the amount of Espirit available.
/// </summary>
public byte Esprit => this.Struct->Esprit;
/// <summary>
/// Gets the number of steps completed for the current dance.
/// </summary>
public byte CompletedSteps => this.Struct->StepIndex;
/// <summary>
/// Gets all the steps in the current dance.
/// </summary>
public unsafe uint[] Steps
{
get
{
var arr = new uint[4];
for (var i = 0; i < 4; i++)
arr[i] = this.Struct->DanceSteps[i] + 15999u - 1;
return arr;
}
}
/// <summary>
/// Gets the next step in the current dance.
/// </summary>
/// <returns>The next dance step action ID.</returns>
public uint NextStep => 15999u + this.Struct->DanceSteps[this.Struct->StepIndex] - 1;
/// <summary>
/// Gets a value indicating whether the player is dancing or not.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsDancing => this.Struct->DanceSteps[0] != 0;
} }
/// <summary>
/// Gets the next step in the current dance.
/// </summary>
/// <returns>The next dance step action ID.</returns>
public uint NextStep => 15999u + this.Struct->DanceSteps[this.Struct->StepIndex] - 1;
/// <summary>
/// Gets a value indicating whether the player is dancing or not.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsDancing => this.Struct->DanceSteps[0] != 0;
} }

View file

@ -2,34 +2,35 @@ using System;
using Dalamud.Game.ClientState.JobGauge.Enums; using Dalamud.Game.ClientState.JobGauge.Enums;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// In-memory DRG job gauge.
/// </summary>
public unsafe class DRGGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.DragoonGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DRGGauge"/> class. /// In-memory DRG job gauge.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> public unsafe class DRGGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.DragoonGauge>
internal DRGGauge(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="DRGGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal DRGGauge(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the time remaining for Blood of the Dragon in milliseconds.
/// </summary>
public short BOTDTimer => this.Struct->BotdTimer;
/// <summary>
/// Gets the current state of Blood of the Dragon.
/// </summary>
public BOTDState BOTDState => (BOTDState)this.Struct->BotdState;
/// <summary>
/// Gets the count of eyes opened during Blood of the Dragon.
/// </summary>
public byte EyeCount => this.Struct->EyeCount;
} }
/// <summary>
/// Gets the time remaining for Blood of the Dragon in milliseconds.
/// </summary>
public short BOTDTimer => this.Struct->BotdTimer;
/// <summary>
/// Gets the current state of Blood of the Dragon.
/// </summary>
public BOTDState BOTDState => (BOTDState)this.Struct->BotdState;
/// <summary>
/// Gets the count of eyes opened during Blood of the Dragon.
/// </summary>
public byte EyeCount => this.Struct->EyeCount;
} }

View file

@ -1,39 +1,40 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// In-memory DRK job gauge.
/// </summary>
public unsafe class DRKGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.DarkKnightGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DRKGauge"/> class. /// In-memory DRK job gauge.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> public unsafe class DRKGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.DarkKnightGauge>
internal DRKGauge(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="DRKGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal DRKGauge(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the amount of blood accumulated.
/// </summary>
public byte Blood => this.Struct->Blood;
/// <summary>
/// Gets the Darkside time remaining in milliseconds.
/// </summary>
public ushort DarksideTimeRemaining => this.Struct->DarksideTimer;
/// <summary>
/// Gets the Shadow time remaining in milliseconds.
/// </summary>
public ushort ShadowTimeRemaining => this.Struct->ShadowTimer;
/// <summary>
/// Gets a value indicating whether the player has Dark Arts or not.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool HasDarkArts => this.Struct->DarkArtsState > 0;
} }
/// <summary>
/// Gets the amount of blood accumulated.
/// </summary>
public byte Blood => this.Struct->Blood;
/// <summary>
/// Gets the Darkside time remaining in milliseconds.
/// </summary>
public ushort DarksideTimeRemaining => this.Struct->DarksideTimer;
/// <summary>
/// Gets the Shadow time remaining in milliseconds.
/// </summary>
public ushort ShadowTimeRemaining => this.Struct->ShadowTimer;
/// <summary>
/// Gets a value indicating whether the player has Dark Arts or not.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool HasDarkArts => this.Struct->DarkArtsState > 0;
} }

View file

@ -1,33 +1,34 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// In-memory GNB job gauge.
/// </summary>
public unsafe class GNBGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.GunbreakerGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GNBGauge"/> class. /// In-memory GNB job gauge.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> public unsafe class GNBGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.GunbreakerGauge>
internal GNBGauge(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="GNBGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal GNBGauge(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the amount of ammo available.
/// </summary>
public byte Ammo => this.Struct->Ammo;
/// <summary>
/// Gets the max combo time of the Gnashing Fang combo.
/// </summary>
public short MaxTimerDuration => this.Struct->MaxTimerDuration;
/// <summary>
/// Gets the current step of the Gnashing Fang combo.
/// </summary>
public byte AmmoComboStep => this.Struct->AmmoComboStep;
} }
/// <summary>
/// Gets the amount of ammo available.
/// </summary>
public byte Ammo => this.Struct->Ammo;
/// <summary>
/// Gets the max combo time of the Gnashing Fang combo.
/// </summary>
public short MaxTimerDuration => this.Struct->MaxTimerDuration;
/// <summary>
/// Gets the current step of the Gnashing Fang combo.
/// </summary>
public byte AmmoComboStep => this.Struct->AmmoComboStep;
} }

View file

@ -1,23 +1,24 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// Base job gauge class.
/// </summary>
public abstract unsafe class JobGaugeBase
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="JobGaugeBase"/> class. /// Base job gauge class.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> public abstract unsafe class JobGaugeBase
internal JobGaugeBase(IntPtr address)
{ {
this.Address = address; /// <summary>
} /// Initializes a new instance of the <see cref="JobGaugeBase"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal JobGaugeBase(IntPtr address)
{
this.Address = address;
}
/// <summary> /// <summary>
/// Gets the address of this job gauge in memory. /// Gets the address of this job gauge in memory.
/// </summary> /// </summary>
public IntPtr Address { get; } public IntPtr Address { get; }
}
} }

View file

@ -1,24 +1,25 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// Base job gauge class.
/// </summary>
/// <typeparam name="T">The underlying FFXIVClientStructs type.</typeparam>
public unsafe class JobGaugeBase<T> : JobGaugeBase where T : unmanaged
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="JobGaugeBase{T}"/> class. /// Base job gauge class.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> /// <typeparam name="T">The underlying FFXIVClientStructs type.</typeparam>
internal JobGaugeBase(IntPtr address) public unsafe class JobGaugeBase<T> : JobGaugeBase where T : unmanaged
: base(address)
{ {
} /// <summary>
/// Initializes a new instance of the <see cref="JobGaugeBase{T}"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal JobGaugeBase(IntPtr address)
: base(address)
{
}
/// <summary> /// <summary>
/// Gets an unsafe struct pointer of this job gauge. /// Gets an unsafe struct pointer of this job gauge.
/// </summary> /// </summary>
private protected T* Struct => (T*)this.Address; private protected T* Struct => (T*)this.Address;
}
} }

View file

@ -1,55 +1,56 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// In-memory MCH job gauge.
/// </summary>
public unsafe class MCHGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.MachinistGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="MCHGauge"/> class. /// In-memory MCH job gauge.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> public unsafe class MCHGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.MachinistGauge>
internal MCHGauge(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="MCHGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal MCHGauge(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the time time remaining for Overheat in milliseconds.
/// </summary>
public short OverheatTimeRemaining => this.Struct->OverheatTimeRemaining;
/// <summary>
/// Gets the time remaining for the Rook or Queen in milliseconds.
/// </summary>
public short SummonTimeRemaining => this.Struct->SummonTimeRemaining;
/// <summary>
/// Gets the current Heat level.
/// </summary>
public byte Heat => this.Struct->Heat;
/// <summary>
/// Gets the current Battery level.
/// </summary>
public byte Battery => this.Struct->Battery;
/// <summary>
/// Gets the battery level of the last summon (robot).
/// </summary>
public byte LastSummonBatteryPower => this.Struct->LastSummonBatteryPower;
/// <summary>
/// Gets a value indicating whether the player is currently Overheated.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsOverheated => (this.Struct->TimerActive & 1) != 0;
/// <summary>
/// Gets a value indicating whether the player has an active Robot.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsRobotActive => (this.Struct->TimerActive & 2) != 0;
} }
/// <summary>
/// Gets the time time remaining for Overheat in milliseconds.
/// </summary>
public short OverheatTimeRemaining => this.Struct->OverheatTimeRemaining;
/// <summary>
/// Gets the time remaining for the Rook or Queen in milliseconds.
/// </summary>
public short SummonTimeRemaining => this.Struct->SummonTimeRemaining;
/// <summary>
/// Gets the current Heat level.
/// </summary>
public byte Heat => this.Struct->Heat;
/// <summary>
/// Gets the current Battery level.
/// </summary>
public byte Battery => this.Struct->Battery;
/// <summary>
/// Gets the battery level of the last summon (robot).
/// </summary>
public byte LastSummonBatteryPower => this.Struct->LastSummonBatteryPower;
/// <summary>
/// Gets a value indicating whether the player is currently Overheated.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsOverheated => (this.Struct->TimerActive & 1) != 0;
/// <summary>
/// Gets a value indicating whether the player has an active Robot.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsRobotActive => (this.Struct->TimerActive & 2) != 0;
} }

View file

@ -1,23 +1,24 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// In-memory MNK job gauge.
/// </summary>
public unsafe class MNKGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.MonkGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="MNKGauge"/> class. /// In-memory MNK job gauge.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> public unsafe class MNKGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.MonkGauge>
internal MNKGauge(IntPtr address)
: base(address)
{ {
} /// <summary>
/// Initializes a new instance of the <see cref="MNKGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal MNKGauge(IntPtr address)
: base(address)
{
}
/// <summary> /// <summary>
/// Gets the number of Chakra available. /// Gets the number of Chakra available.
/// </summary> /// </summary>
public byte Chakra => this.Struct->Chakra; public byte Chakra => this.Struct->Chakra;
}
} }

View file

@ -1,33 +1,34 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// In-memory NIN job gauge.
/// </summary>
public unsafe class NINGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.NinjaGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="NINGauge"/> class. /// In-memory NIN job gauge.
/// </summary> /// </summary>
/// <param name="address">The address of the gauge.</param> public unsafe class NINGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.NinjaGauge>
internal NINGauge(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="NINGauge"/> class.
/// </summary>
/// <param name="address">The address of the gauge.</param>
internal NINGauge(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the time left on Huton in milliseconds.
/// </summary>
public int HutonTimer => this.Struct->HutonTimer;
/// <summary>
/// Gets the amount of Ninki available.
/// </summary>
public byte Ninki => this.Struct->Ninki;
/// <summary>
/// Gets the number of times Huton has been cast manually.
/// </summary>
public byte HutonManualCasts => this.Struct->HutonManualCasts;
} }
/// <summary>
/// Gets the time left on Huton in milliseconds.
/// </summary>
public int HutonTimer => this.Struct->HutonTimer;
/// <summary>
/// Gets the amount of Ninki available.
/// </summary>
public byte Ninki => this.Struct->Ninki;
/// <summary>
/// Gets the number of times Huton has been cast manually.
/// </summary>
public byte HutonManualCasts => this.Struct->HutonManualCasts;
} }

View file

@ -1,23 +1,24 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// In-memory PLD job gauge.
/// </summary>
public unsafe class PLDGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.PaladinGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PLDGauge"/> class. /// In-memory PLD job gauge.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> public unsafe class PLDGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.PaladinGauge>
internal PLDGauge(IntPtr address)
: base(address)
{ {
} /// <summary>
/// Initializes a new instance of the <see cref="PLDGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal PLDGauge(IntPtr address)
: base(address)
{
}
/// <summary> /// <summary>
/// Gets the current level of the Oath gauge. /// Gets the current level of the Oath gauge.
/// </summary> /// </summary>
public byte OathGauge => this.Struct->OathGauge; public byte OathGauge => this.Struct->OathGauge;
}
} }

View file

@ -1,28 +1,29 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// In-memory RDM job gauge.
/// </summary>
public unsafe class RDMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.RedMageGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="RDMGauge"/> class. /// In-memory RDM job gauge.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> public unsafe class RDMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.RedMageGauge>
internal RDMGauge(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="RDMGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal RDMGauge(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the level of the White gauge.
/// </summary>
public byte WhiteMana => this.Struct->WhiteMana;
/// <summary>
/// Gets the level of the Black gauge.
/// </summary>
public byte BlackMana => this.Struct->BlackMana;
} }
/// <summary>
/// Gets the level of the White gauge.
/// </summary>
public byte WhiteMana => this.Struct->WhiteMana;
/// <summary>
/// Gets the level of the Black gauge.
/// </summary>
public byte BlackMana => this.Struct->BlackMana;
} }

View file

@ -2,52 +2,53 @@ using System;
using Dalamud.Game.ClientState.JobGauge.Enums; using Dalamud.Game.ClientState.JobGauge.Enums;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// In-memory SAM job gauge.
/// </summary>
public unsafe class SAMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.SamuraiGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SAMGauge"/> class. /// In-memory SAM job gauge.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> public unsafe class SAMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.SamuraiGauge>
internal SAMGauge(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="SAMGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal SAMGauge(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the current amount of Kenki available.
/// </summary>
public byte Kenki => this.Struct->Kenki;
/// <summary>
/// Gets the amount of Meditation stacks.
/// </summary>
public byte MeditationStacks => this.Struct->MeditationStacks;
/// <summary>
/// Gets the active Sen.
/// </summary>
public Sen Sen => (Sen)this.Struct->SenFlags;
/// <summary>
/// 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;
/// <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;
/// <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;
} }
/// <summary>
/// Gets the current amount of Kenki available.
/// </summary>
public byte Kenki => this.Struct->Kenki;
/// <summary>
/// Gets the amount of Meditation stacks.
/// </summary>
public byte MeditationStacks => this.Struct->MeditationStacks;
/// <summary>
/// Gets the active Sen.
/// </summary>
public Sen Sen => (Sen)this.Struct->SenFlags;
/// <summary>
/// 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;
/// <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;
/// <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;
} }

View file

@ -2,39 +2,40 @@ using System;
using Dalamud.Game.ClientState.JobGauge.Enums; using Dalamud.Game.ClientState.JobGauge.Enums;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// In-memory SCH job gauge.
/// </summary>
public unsafe class SCHGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.ScholarGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SCHGauge"/> class. /// In-memory SCH job gauge.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> public unsafe class SCHGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.ScholarGauge>
internal SCHGauge(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="SCHGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal SCHGauge(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the amount of Aetherflow stacks available.
/// </summary>
public byte Aetherflow => this.Struct->Aetherflow;
/// <summary>
/// Gets the current level of the Fairy Gauge.
/// </summary>
public byte FairyGauge => this.Struct->FairyGauge;
/// <summary>
/// Gets the Seraph time remaiSCHg in milliseconds.
/// </summary>
public short SeraphTimer => this.Struct->SeraphTimer;
/// <summary>
/// Gets the last dismissed fairy.
/// </summary>
public DismissedFairy DismissedFairy => (DismissedFairy)this.Struct->DismissedFairy;
} }
/// <summary>
/// Gets the amount of Aetherflow stacks available.
/// </summary>
public byte Aetherflow => this.Struct->Aetherflow;
/// <summary>
/// Gets the current level of the Fairy Gauge.
/// </summary>
public byte FairyGauge => this.Struct->FairyGauge;
/// <summary>
/// Gets the Seraph time remaiSCHg in milliseconds.
/// </summary>
public short SeraphTimer => this.Struct->SeraphTimer;
/// <summary>
/// Gets the last dismissed fairy.
/// </summary>
public DismissedFairy DismissedFairy => (DismissedFairy)this.Struct->DismissedFairy;
} }

View file

@ -2,58 +2,59 @@ using System;
using Dalamud.Game.ClientState.JobGauge.Enums; using Dalamud.Game.ClientState.JobGauge.Enums;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// In-memory SMN job gauge.
/// </summary>
public unsafe class SMNGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.SummonerGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SMNGauge"/> class. /// In-memory SMN job gauge.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> public unsafe class SMNGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.SummonerGauge>
internal SMNGauge(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="SMNGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal SMNGauge(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the time remaining for the current summon.
/// </summary>
public short TimerRemaining => this.Struct->TimerRemaining;
/// <summary>
/// Gets the summon that will return after the current summon expires.
/// </summary>
public SummonPet ReturnSummon => (SummonPet)this.Struct->ReturnSummon;
/// <summary>
/// Gets the summon glam for the <see cref="ReturnSummon"/>.
/// </summary>
public PetGlam ReturnSummonGlam => (PetGlam)this.Struct->ReturnSummonGlam;
/// <summary>
/// Gets the current aether flags.
/// Use the summon accessors instead.
/// </summary>
public byte AetherFlags => this.Struct->AetherFlags;
/// <summary>
/// Gets a value indicating whether if Phoenix is ready to be summoned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsPhoenixReady => (this.AetherFlags & 0x10) > 0;
/// <summary>
/// Gets a value indicating whether Bahamut is ready to be summoned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsBahamutReady => (this.AetherFlags & 8) > 0;
/// <summary>
/// Gets a value indicating whether there are any Aetherflow stacks available.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool HasAetherflowStacks => (this.AetherFlags & 3) > 0;
} }
/// <summary>
/// Gets the time remaining for the current summon.
/// </summary>
public short TimerRemaining => this.Struct->TimerRemaining;
/// <summary>
/// Gets the summon that will return after the current summon expires.
/// </summary>
public SummonPet ReturnSummon => (SummonPet)this.Struct->ReturnSummon;
/// <summary>
/// Gets the summon glam for the <see cref="ReturnSummon"/>.
/// </summary>
public PetGlam ReturnSummonGlam => (PetGlam)this.Struct->ReturnSummonGlam;
/// <summary>
/// Gets the current aether flags.
/// Use the summon accessors instead.
/// </summary>
public byte AetherFlags => this.Struct->AetherFlags;
/// <summary>
/// Gets a value indicating whether if Phoenix is ready to be summoned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsPhoenixReady => (this.AetherFlags & 0x10) > 0;
/// <summary>
/// Gets a value indicating whether Bahamut is ready to be summoned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsBahamutReady => (this.AetherFlags & 8) > 0;
/// <summary>
/// Gets a value indicating whether there are any Aetherflow stacks available.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool HasAetherflowStacks => (this.AetherFlags & 3) > 0;
} }

View file

@ -1,23 +1,24 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// In-memory WAR job gauge.
/// </summary>
public unsafe class WARGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.WarriorGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WARGauge"/> class. /// In-memory WAR job gauge.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> public unsafe class WARGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.WarriorGauge>
internal WARGauge(IntPtr address)
: base(address)
{ {
} /// <summary>
/// Initializes a new instance of the <see cref="WARGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal WARGauge(IntPtr address)
: base(address)
{
}
/// <summary> /// <summary>
/// Gets the amount of wrath in the Beast gauge. /// Gets the amount of wrath in the Beast gauge.
/// </summary> /// </summary>
public byte BeastGauge => this.Struct->BeastGauge; public byte BeastGauge => this.Struct->BeastGauge;
}
} }

View file

@ -1,33 +1,34 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types
/// <summary>
/// In-memory WHM job gauge.
/// </summary>
public unsafe class WHMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.WhiteMageGauge>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WHMGauge"/> class. /// In-memory WHM job gauge.
/// </summary> /// </summary>
/// <param name="address">Address of the job gauge.</param> public unsafe class WHMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.WhiteMageGauge>
internal WHMGauge(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="WHMGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal WHMGauge(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the time to next lily in milliseconds.
/// </summary>
public short LilyTimer => this.Struct->LilyTimer;
/// <summary>
/// Gets the number of Lilies.
/// </summary>
public byte Lily => this.Struct->Lily;
/// <summary>
/// Gets the number of times the blood lily has been nourished.
/// </summary>
public byte BloodLily => this.Struct->BloodLily;
} }
/// <summary>
/// Gets the time to next lily in milliseconds.
/// </summary>
public short LilyTimer => this.Struct->LilyTimer;
/// <summary>
/// Gets the number of Lilies.
/// </summary>
public byte Lily => this.Struct->Lily;
/// <summary>
/// Gets the number of times the blood lily has been nourished.
/// </summary>
public byte BloodLily => this.Struct->BloodLily;
} }

View file

@ -6,154 +6,155 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.Keys; namespace Dalamud.Game.ClientState.Keys
/// <summary>
/// Wrapper around the game keystate buffer, which contains the pressed state for all keyboard keys, indexed by virtual vkCode.
/// </summary>
/// <remarks>
/// The stored key state is actually a combination field, however the below ephemeral states are consumed each frame. Setting
/// the value may be mildly useful, however retrieving the value is largely pointless. In testing, it wasn't possible without
/// setting the statue manually.
/// index &amp; 0 = key pressed.
/// index &amp; 1 = key down (ephemeral).
/// index &amp; 2 = key up (ephemeral).
/// index &amp; 3 = short key press (ephemeral).
/// </remarks>
[PluginInterface]
[InterfaceVersion("1.0")]
public class KeyState
{ {
// The array is accessed in a way that this limit doesn't appear to exist
// but there is other state data past this point, and keys beyond here aren't
// generally valid for most things anyway
private const int MaxKeyCode = 0xF0;
private readonly IntPtr bufferBase;
private readonly IntPtr indexBase;
private VirtualKey[] validVirtualKeyCache = null;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="KeyState"/> class. /// Wrapper around the game keystate buffer, which contains the pressed state for all keyboard keys, indexed by virtual vkCode.
/// </summary> /// </summary>
/// <param name="addressResolver">The ClientStateAddressResolver instance.</param> /// <remarks>
public KeyState(ClientStateAddressResolver addressResolver) /// The stored key state is actually a combination field, however the below ephemeral states are consumed each frame. Setting
/// the value may be mildly useful, however retrieving the value is largely pointless. In testing, it wasn't possible without
/// setting the statue manually.
/// index &amp; 0 = key pressed.
/// index &amp; 1 = key down (ephemeral).
/// index &amp; 2 = key up (ephemeral).
/// index &amp; 3 = short key press (ephemeral).
/// </remarks>
[PluginInterface]
[InterfaceVersion("1.0")]
public class KeyState
{ {
var moduleBaseAddress = Service<SigScanner>.Get().Module.BaseAddress; // The array is accessed in a way that this limit doesn't appear to exist
// but there is other state data past this point, and keys beyond here aren't
// generally valid for most things anyway
private const int MaxKeyCode = 0xF0;
private readonly IntPtr bufferBase;
private readonly IntPtr indexBase;
private VirtualKey[] validVirtualKeyCache = null;
this.bufferBase = moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardState); /// <summary>
this.indexBase = moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardStateIndexArray); /// Initializes a new instance of the <see cref="KeyState"/> class.
/// </summary>
Log.Verbose($"Keyboard state buffer address 0x{this.bufferBase.ToInt64():X}"); /// <param name="addressResolver">The ClientStateAddressResolver instance.</param>
} public KeyState(ClientStateAddressResolver addressResolver)
/// <summary>
/// Get or set the key-pressed state for a given vkCode.
/// </summary>
/// <param name="vkCode">The virtual key to change.</param>
/// <returns>Whether the specified key is currently pressed.</returns>
/// <exception cref="ArgumentException">If the vkCode is not valid. Refer to <see cref="IsVirtualKeyValid(int)"/> or <see cref="GetValidVirtualKeys"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException">If the set value is non-zero.</exception>
public unsafe bool this[int vkCode]
{
get => this.GetRawValue(vkCode) != 0;
set => this.SetRawValue(vkCode, value ? 1 : 0);
}
/// <inheritdoc cref="this[int]"/>
public bool this[VirtualKey vkCode]
{
get => this[(int)vkCode];
set => this[(int)vkCode] = value;
}
/// <summary>
/// Gets the value in the index array.
/// </summary>
/// <param name="vkCode">The virtual key to change.</param>
/// <returns>The raw value stored in the index array.</returns>
/// <exception cref="ArgumentException">If the vkCode is not valid. Refer to <see cref="IsVirtualKeyValid(int)"/> or <see cref="GetValidVirtualKeys"/>.</exception>
public int GetRawValue(int vkCode)
=> this.GetRefValue(vkCode);
/// <inheritdoc cref="GetRawValue(int)"/>
public int GetRawValue(VirtualKey vkCode)
=> this.GetRawValue((int)vkCode);
/// <summary>
/// Sets the value in the index array.
/// </summary>
/// <param name="vkCode">The virtual key to change.</param>
/// <param name="value">The raw value to set in the index array.</param>
/// <exception cref="ArgumentException">If the vkCode is not valid. Refer to <see cref="IsVirtualKeyValid(int)"/> or <see cref="GetValidVirtualKeys"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException">If the set value is non-zero.</exception>
public void SetRawValue(int vkCode, int value)
{
if (value != 0)
throw new ArgumentOutOfRangeException(nameof(value), "Dalamud does not support pressing keys, only preventing them via zero or False. If you have a valid use-case for this, please contact the dev team.");
this.GetRefValue(vkCode) = value;
}
/// <inheritdoc cref="SetRawValue(int, int)"/>
public void SetRawValue(VirtualKey vkCode, int value)
=> this.SetRawValue((int)vkCode, value);
/// <summary>
/// Gets a value indicating whether the given VirtualKey code is regarded as valid input by the game.
/// </summary>
/// <param name="vkCode">Virtual key code.</param>
/// <returns>If the code is valid.</returns>
public bool IsVirtualKeyValid(int vkCode)
=> this.ConvertVirtualKey(vkCode) != 0;
/// <inheritdoc cref="IsVirtualKeyValid(int)"/>
public bool IsVirtualKeyValid(VirtualKey vkCode)
=> this.IsVirtualKeyValid((int)vkCode);
/// <summary>
/// Gets an array of virtual keys the game considers valid input.
/// </summary>
/// <returns>An array of valid virtual keys.</returns>
public VirtualKey[] GetValidVirtualKeys()
=> this.validVirtualKeyCache ??= Enum.GetValues<VirtualKey>().Where(vk => this.IsVirtualKeyValid(vk)).ToArray();
/// <summary>
/// Clears the pressed state for all keys.
/// </summary>
public void ClearAll()
{
foreach (var vk in this.GetValidVirtualKeys())
{ {
this[vk] = false; var moduleBaseAddress = Service<SigScanner>.Get().Module.BaseAddress;
this.bufferBase = moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardState);
this.indexBase = moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardStateIndexArray);
Log.Verbose($"Keyboard state buffer address 0x{this.bufferBase.ToInt64():X}");
}
/// <summary>
/// Get or set the key-pressed state for a given vkCode.
/// </summary>
/// <param name="vkCode">The virtual key to change.</param>
/// <returns>Whether the specified key is currently pressed.</returns>
/// <exception cref="ArgumentException">If the vkCode is not valid. Refer to <see cref="IsVirtualKeyValid(int)"/> or <see cref="GetValidVirtualKeys"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException">If the set value is non-zero.</exception>
public unsafe bool this[int vkCode]
{
get => this.GetRawValue(vkCode) != 0;
set => this.SetRawValue(vkCode, value ? 1 : 0);
}
/// <inheritdoc cref="this[int]"/>
public bool this[VirtualKey vkCode]
{
get => this[(int)vkCode];
set => this[(int)vkCode] = value;
}
/// <summary>
/// Gets the value in the index array.
/// </summary>
/// <param name="vkCode">The virtual key to change.</param>
/// <returns>The raw value stored in the index array.</returns>
/// <exception cref="ArgumentException">If the vkCode is not valid. Refer to <see cref="IsVirtualKeyValid(int)"/> or <see cref="GetValidVirtualKeys"/>.</exception>
public int GetRawValue(int vkCode)
=> this.GetRefValue(vkCode);
/// <inheritdoc cref="GetRawValue(int)"/>
public int GetRawValue(VirtualKey vkCode)
=> this.GetRawValue((int)vkCode);
/// <summary>
/// Sets the value in the index array.
/// </summary>
/// <param name="vkCode">The virtual key to change.</param>
/// <param name="value">The raw value to set in the index array.</param>
/// <exception cref="ArgumentException">If the vkCode is not valid. Refer to <see cref="IsVirtualKeyValid(int)"/> or <see cref="GetValidVirtualKeys"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException">If the set value is non-zero.</exception>
public void SetRawValue(int vkCode, int value)
{
if (value != 0)
throw new ArgumentOutOfRangeException(nameof(value), "Dalamud does not support pressing keys, only preventing them via zero or False. If you have a valid use-case for this, please contact the dev team.");
this.GetRefValue(vkCode) = value;
}
/// <inheritdoc cref="SetRawValue(int, int)"/>
public void SetRawValue(VirtualKey vkCode, int value)
=> this.SetRawValue((int)vkCode, value);
/// <summary>
/// Gets a value indicating whether the given VirtualKey code is regarded as valid input by the game.
/// </summary>
/// <param name="vkCode">Virtual key code.</param>
/// <returns>If the code is valid.</returns>
public bool IsVirtualKeyValid(int vkCode)
=> this.ConvertVirtualKey(vkCode) != 0;
/// <inheritdoc cref="IsVirtualKeyValid(int)"/>
public bool IsVirtualKeyValid(VirtualKey vkCode)
=> this.IsVirtualKeyValid((int)vkCode);
/// <summary>
/// Gets an array of virtual keys the game considers valid input.
/// </summary>
/// <returns>An array of valid virtual keys.</returns>
public VirtualKey[] GetValidVirtualKeys()
=> this.validVirtualKeyCache ??= Enum.GetValues<VirtualKey>().Where(vk => this.IsVirtualKeyValid(vk)).ToArray();
/// <summary>
/// Clears the pressed state for all keys.
/// </summary>
public void ClearAll()
{
foreach (var vk in this.GetValidVirtualKeys())
{
this[vk] = false;
}
}
/// <summary>
/// Converts a virtual key into the equivalent value that the game uses.
/// Valid values are non-zero.
/// </summary>
/// <param name="vkCode">Virtual key.</param>
/// <returns>Converted value.</returns>
private unsafe byte ConvertVirtualKey(int vkCode)
{
if (vkCode <= 0 || vkCode >= MaxKeyCode)
return 0;
return *(byte*)(this.indexBase + vkCode);
}
/// <summary>
/// Gets the raw value from the key state array.
/// </summary>
/// <param name="vkCode">Virtual key code.</param>
/// <returns>A reference to the indexed array.</returns>
private unsafe ref int GetRefValue(int vkCode)
{
vkCode = this.ConvertVirtualKey(vkCode);
if (vkCode == 0)
throw new ArgumentException($"Keycode state is only valid for certain values. Reference GetValidVirtualKeys for help.");
return ref *(int*)(this.bufferBase + (4 * vkCode));
} }
} }
/// <summary>
/// Converts a virtual key into the equivalent value that the game uses.
/// Valid values are non-zero.
/// </summary>
/// <param name="vkCode">Virtual key.</param>
/// <returns>Converted value.</returns>
private unsafe byte ConvertVirtualKey(int vkCode)
{
if (vkCode <= 0 || vkCode >= MaxKeyCode)
return 0;
return *(byte*)(this.indexBase + vkCode);
}
/// <summary>
/// Gets the raw value from the key state array.
/// </summary>
/// <param name="vkCode">Virtual key code.</param>
/// <returns>A reference to the indexed array.</returns>
private unsafe ref int GetRefValue(int vkCode)
{
vkCode = this.ConvertVirtualKey(vkCode);
if (vkCode == 0)
throw new ArgumentException($"Keycode state is only valid for certain values. Reference GetValidVirtualKeys for help.");
return ref *(int*)(this.bufferBase + (4 * vkCode));
}
} }

File diff suppressed because it is too large Load diff

View file

@ -1,27 +1,28 @@
namespace Dalamud.Game.ClientState.Objects.Enums; namespace Dalamud.Game.ClientState.Objects.Enums
/// <summary>
/// An Enum describing possible BattleNpc kinds.
/// </summary>
public enum BattleNpcSubKind : byte
{ {
/// <summary> /// <summary>
/// Invalid BattleNpc. /// An Enum describing possible BattleNpc kinds.
/// </summary> /// </summary>
None = 0, public enum BattleNpcSubKind : byte
{
/// <summary>
/// Invalid BattleNpc.
/// </summary>
None = 0,
/// <summary> /// <summary>
/// BattleNpc representing a Pet. /// BattleNpc representing a Pet.
/// </summary> /// </summary>
Pet = 2, Pet = 2,
/// <summary> /// <summary>
/// BattleNpc representing a Chocobo. /// BattleNpc representing a Chocobo.
/// </summary> /// </summary>
Chocobo = 3, Chocobo = 3,
/// <summary> /// <summary>
/// BattleNpc representing a standard enemy. /// BattleNpc representing a standard enemy.
/// </summary> /// </summary>
Enemy = 5, Enemy = 5,
}
} }

View file

@ -1,138 +1,139 @@
namespace Dalamud.Game.ClientState.Objects.Enums; namespace Dalamud.Game.ClientState.Objects.Enums
/// <summary>
/// This enum describes the indices of the Customize array.
/// </summary>
// TODO: This may need some rework since it may not be entirely accurate (stolen from Sapphire)
public enum CustomizeIndex
{ {
/// <summary> /// <summary>
/// The race of the character. /// This enum describes the indices of the Customize array.
/// </summary> /// </summary>
Race = 0x00, // TODO: This may need some rework since it may not be entirely accurate (stolen from Sapphire)
public enum CustomizeIndex
{
/// <summary>
/// The race of the character.
/// </summary>
Race = 0x00,
/// <summary> /// <summary>
/// The gender of the character. /// The gender of the character.
/// </summary> /// </summary>
Gender = 0x01, Gender = 0x01,
/// <summary> /// <summary>
/// The tribe of the character. /// The tribe of the character.
/// </summary> /// </summary>
Tribe = 0x04, Tribe = 0x04,
/// <summary> /// <summary>
/// The height of the character. /// The height of the character.
/// </summary> /// </summary>
Height = 0x03, Height = 0x03,
/// <summary> /// <summary>
/// The model type of the character. /// The model type of the character.
/// </summary> /// </summary>
ModelType = 0x02, // Au Ra: changes horns/tails, everything else: seems to drastically change appearance (flip between two sets, odd/even numbers). sometimes retains hairstyle and other features ModelType = 0x02, // Au Ra: changes horns/tails, everything else: seems to drastically change appearance (flip between two sets, odd/even numbers). sometimes retains hairstyle and other features
/// <summary> /// <summary>
/// The face type of the character. /// The face type of the character.
/// </summary> /// </summary>
FaceType = 0x05, FaceType = 0x05,
/// <summary> /// <summary>
/// The hair of the character. /// The hair of the character.
/// </summary> /// </summary>
HairStyle = 0x06, HairStyle = 0x06,
/// <summary> /// <summary>
/// Whether or not the character has hair highlights. /// Whether or not the character has hair highlights.
/// </summary> /// </summary>
HasHighlights = 0x07, // negative to enable, positive to disable HasHighlights = 0x07, // negative to enable, positive to disable
/// <summary> /// <summary>
/// The skin color of the character. /// The skin color of the character.
/// </summary> /// </summary>
SkinColor = 0x08, SkinColor = 0x08,
/// <summary> /// <summary>
/// The eye color of the character. /// The eye color of the character.
/// </summary> /// </summary>
EyeColor = 0x09, // color of character's right eye EyeColor = 0x09, // color of character's right eye
/// <summary> /// <summary>
/// The hair color of the character. /// The hair color of the character.
/// </summary> /// </summary>
HairColor = 0x0A, // main color HairColor = 0x0A, // main color
/// <summary> /// <summary>
/// The highlights hair color of the character. /// The highlights hair color of the character.
/// </summary> /// </summary>
HairColor2 = 0x0B, // highlights color HairColor2 = 0x0B, // highlights color
/// <summary> /// <summary>
/// The face features of the character. /// The face features of the character.
/// </summary> /// </summary>
FaceFeatures = 0x0C, // seems to be a toggle, (-odd and +even for large face covering), opposite for small FaceFeatures = 0x0C, // seems to be a toggle, (-odd and +even for large face covering), opposite for small
/// <summary> /// <summary>
/// The color of the face features of the character. /// The color of the face features of the character.
/// </summary> /// </summary>
FaceFeaturesColor = 0x0D, FaceFeaturesColor = 0x0D,
/// <summary> /// <summary>
/// The eyebrows of the character. /// The eyebrows of the character.
/// </summary> /// </summary>
Eyebrows = 0x0E, Eyebrows = 0x0E,
/// <summary> /// <summary>
/// The 2nd eye color of the character. /// The 2nd eye color of the character.
/// </summary> /// </summary>
EyeColor2 = 0x0F, // color of character's left eye EyeColor2 = 0x0F, // color of character's left eye
/// <summary> /// <summary>
/// The eye shape of the character. /// The eye shape of the character.
/// </summary> /// </summary>
EyeShape = 0x10, EyeShape = 0x10,
/// <summary> /// <summary>
/// The nose shape of the character. /// The nose shape of the character.
/// </summary> /// </summary>
NoseShape = 0x11, NoseShape = 0x11,
/// <summary> /// <summary>
/// The jaw shape of the character. /// The jaw shape of the character.
/// </summary> /// </summary>
JawShape = 0x12, JawShape = 0x12,
/// <summary> /// <summary>
/// The lip style of the character. /// The lip style of the character.
/// </summary> /// </summary>
LipStyle = 0x13, // lip colour depth and shape (negative values around -120 darker/more noticeable, positive no colour) LipStyle = 0x13, // lip colour depth and shape (negative values around -120 darker/more noticeable, positive no colour)
/// <summary> /// <summary>
/// The lip color of the character. /// The lip color of the character.
/// </summary> /// </summary>
LipColor = 0x14, LipColor = 0x14,
/// <summary> /// <summary>
/// The race feature size of the character. /// The race feature size of the character.
/// </summary> /// </summary>
RaceFeatureSize = 0x15, RaceFeatureSize = 0x15,
/// <summary> /// <summary>
/// The race feature type of the character. /// The race feature type of the character.
/// </summary> /// </summary>
RaceFeatureType = 0x16, // negative or out of range tail shapes for race result in no tail (e.g. Au Ra has max of 4 tail shapes), incorrect value can crash client RaceFeatureType = 0x16, // negative or out of range tail shapes for race result in no tail (e.g. Au Ra has max of 4 tail shapes), incorrect value can crash client
/// <summary> /// <summary>
/// The bust size of the character. /// The bust size of the character.
/// </summary> /// </summary>
BustSize = 0x17, // char creator allows up to max of 100, i set to 127 cause who wouldnt but no visible difference BustSize = 0x17, // char creator allows up to max of 100, i set to 127 cause who wouldnt but no visible difference
/// <summary> /// <summary>
/// The face paint of the character. /// The face paint of the character.
/// </summary> /// </summary>
Facepaint = 0x18, Facepaint = 0x18,
/// <summary> /// <summary>
/// The face paint color of the character. /// The face paint color of the character.
/// </summary> /// </summary>
FacepaintColor = 0x19, FacepaintColor = 0x19,
}
} }

View file

@ -1,82 +1,83 @@
namespace Dalamud.Game.ClientState.Objects.Enums; namespace Dalamud.Game.ClientState.Objects.Enums
/// <summary>
/// Enum describing possible entity kinds.
/// </summary>
public enum ObjectKind : byte
{ {
/// <summary> /// <summary>
/// Invalid character. /// Enum describing possible entity kinds.
/// </summary> /// </summary>
None = 0x00, public enum ObjectKind : byte
{
/// <summary>
/// Invalid character.
/// </summary>
None = 0x00,
/// <summary> /// <summary>
/// Objects representing player characters. /// Objects representing player characters.
/// </summary> /// </summary>
Player = 0x01, Player = 0x01,
/// <summary> /// <summary>
/// Objects representing battle NPCs. /// Objects representing battle NPCs.
/// </summary> /// </summary>
BattleNpc = 0x02, BattleNpc = 0x02,
/// <summary> /// <summary>
/// Objects representing event NPCs. /// Objects representing event NPCs.
/// </summary> /// </summary>
EventNpc = 0x03, EventNpc = 0x03,
/// <summary> /// <summary>
/// Objects representing treasures. /// Objects representing treasures.
/// </summary> /// </summary>
Treasure = 0x04, Treasure = 0x04,
/// <summary> /// <summary>
/// Objects representing aetherytes. /// Objects representing aetherytes.
/// </summary> /// </summary>
Aetheryte = 0x05, Aetheryte = 0x05,
/// <summary> /// <summary>
/// Objects representing gathering points. /// Objects representing gathering points.
/// </summary> /// </summary>
GatheringPoint = 0x06, GatheringPoint = 0x06,
/// <summary> /// <summary>
/// Objects representing event objects. /// Objects representing event objects.
/// </summary> /// </summary>
EventObj = 0x07, EventObj = 0x07,
/// <summary> /// <summary>
/// Objects representing mounts. /// Objects representing mounts.
/// </summary> /// </summary>
MountType = 0x08, MountType = 0x08,
/// <summary> /// <summary>
/// Objects representing minions. /// Objects representing minions.
/// </summary> /// </summary>
Companion = 0x09, // Minion Companion = 0x09, // Minion
/// <summary> /// <summary>
/// Objects representing retainers. /// Objects representing retainers.
/// </summary> /// </summary>
Retainer = 0x0A, Retainer = 0x0A,
/// <summary> /// <summary>
/// Objects representing area objects. /// Objects representing area objects.
/// </summary> /// </summary>
Area = 0x0B, Area = 0x0B,
/// <summary> /// <summary>
/// Objects representing housing objects. /// Objects representing housing objects.
/// </summary> /// </summary>
Housing = 0x0C, Housing = 0x0C,
/// <summary> /// <summary>
/// Objects representing cutscene objects. /// Objects representing cutscene objects.
/// </summary> /// </summary>
Cutscene = 0x0D, Cutscene = 0x0D,
/// <summary> /// <summary>
/// Objects representing card stand objects. /// Objects representing card stand objects.
/// </summary> /// </summary>
CardStand = 0x0E, CardStand = 0x0E,
}
} }

View file

@ -1,55 +1,56 @@
using System; using System;
namespace Dalamud.Game.ClientState.Objects.Enums; namespace Dalamud.Game.ClientState.Objects.Enums
/// <summary>
/// Enum describing possible status flags.
/// </summary>
[Flags]
public enum StatusFlags : byte
{ {
/// <summary> /// <summary>
/// No status flags set. /// Enum describing possible status flags.
/// </summary> /// </summary>
None = 0, [Flags]
public enum StatusFlags : byte
{
/// <summary>
/// No status flags set.
/// </summary>
None = 0,
/// <summary> /// <summary>
/// Hostile character. /// Hostile character.
/// </summary> /// </summary>
Hostile = 1, Hostile = 1,
/// <summary> /// <summary>
/// Character in combat. /// Character in combat.
/// </summary> /// </summary>
InCombat = 2, InCombat = 2,
/// <summary> /// <summary>
/// Character weapon is out. /// Character weapon is out.
/// </summary> /// </summary>
WeaponOut = 4, WeaponOut = 4,
/// <summary> /// <summary>
/// Character offhand is out. /// Character offhand is out.
/// </summary> /// </summary>
OffhandOut = 8, OffhandOut = 8,
/// <summary> /// <summary>
/// Character is a party member. /// Character is a party member.
/// </summary> /// </summary>
PartyMember = 16, PartyMember = 16,
/// <summary> /// <summary>
/// Character is a alliance member. /// Character is a alliance member.
/// </summary> /// </summary>
AllianceMember = 32, AllianceMember = 32,
/// <summary> /// <summary>
/// Character is in friend list. /// Character is in friend list.
/// </summary> /// </summary>
Friend = 64, Friend = 64,
/// <summary> /// <summary>
/// Character is casting. /// Character is casting.
/// </summary> /// </summary>
IsCasting = 128, IsCasting = 128,
}
} }

View file

@ -9,139 +9,140 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.Objects; namespace Dalamud.Game.ClientState.Objects
/// <summary>
/// This collection represents the currently spawned FFXIV game objects.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed partial class ObjectTable
{ {
private const int ObjectTableLength = 424;
private readonly ClientStateAddressResolver address;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ObjectTable"/> class. /// This collection represents the currently spawned FFXIV game objects.
/// </summary> /// </summary>
/// <param name="addressResolver">Client state address resolver.</param> [PluginInterface]
internal ObjectTable(ClientStateAddressResolver addressResolver) [InterfaceVersion("1.0")]
public sealed partial class ObjectTable
{ {
this.address = addressResolver; private const int ObjectTableLength = 424;
Log.Verbose($"Object table address 0x{this.address.ObjectTable.ToInt64():X}"); private readonly ClientStateAddressResolver address;
}
/// <summary> /// <summary>
/// Gets the address of the object table. /// Initializes a new instance of the <see cref="ObjectTable"/> class.
/// </summary> /// </summary>
public IntPtr Address => this.address.ObjectTable; /// <param name="addressResolver">Client state address resolver.</param>
internal ObjectTable(ClientStateAddressResolver addressResolver)
/// <summary>
/// Gets the length of the object table.
/// </summary>
public int Length => ObjectTableLength;
/// <summary>
/// Get an object at the specified spawn index.
/// </summary>
/// <param name="index">Spawn index.</param>
/// <returns>An <see cref="GameObject"/> at the specified spawn index.</returns>
public GameObject? this[int index]
{
get
{ {
var address = this.GetObjectAddress(index); this.address = addressResolver;
return this.CreateObjectReference(address);
Log.Verbose($"Object table address 0x{this.address.ObjectTable.ToInt64():X}");
}
/// <summary>
/// Gets the address of the object table.
/// </summary>
public IntPtr Address => this.address.ObjectTable;
/// <summary>
/// Gets the length of the object table.
/// </summary>
public int Length => ObjectTableLength;
/// <summary>
/// Get an object at the specified spawn index.
/// </summary>
/// <param name="index">Spawn index.</param>
/// <returns>An <see cref="GameObject"/> at the specified spawn index.</returns>
public GameObject? this[int index]
{
get
{
var address = this.GetObjectAddress(index);
return this.CreateObjectReference(address);
}
}
/// <summary>
/// Search for a game object by their Object ID.
/// </summary>
/// <param name="objectId">Object ID to find.</param>
/// <returns>A game object or null.</returns>
public GameObject? SearchById(uint objectId)
{
if (objectId is GameObject.InvalidGameObjectId or 0)
return null;
foreach (var obj in this)
{
if (obj == null)
continue;
if (obj.ObjectId == objectId)
return obj;
}
return null;
}
/// <summary>
/// Gets the address of the game object at the specified index of the object table.
/// </summary>
/// <param name="index">The index of the object.</param>
/// <returns>The memory address of the object.</returns>
public unsafe IntPtr GetObjectAddress(int index)
{
if (index < 0 || index >= ObjectTableLength)
return IntPtr.Zero;
return *(IntPtr*)(this.address.ObjectTable + (8 * index));
}
/// <summary>
/// Create a reference to an FFXIV game object.
/// </summary>
/// <param name="address">The address of the object in memory.</param>
/// <returns><see cref="GameObject"/> object or inheritor containing the requested data.</returns>
public unsafe GameObject? CreateObjectReference(IntPtr address)
{
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
return null;
if (address == IntPtr.Zero)
return null;
var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)address;
var objKind = (ObjectKind)obj->ObjectKind;
return objKind switch
{
ObjectKind.Player => new PlayerCharacter(address),
ObjectKind.BattleNpc => new BattleNpc(address),
ObjectKind.EventObj => new EventObj(address),
ObjectKind.Companion => new Npc(address),
_ => new GameObject(address),
};
} }
} }
/// <summary> /// <summary>
/// Search for a game object by their Object ID. /// This collection represents the currently spawned FFXIV game objects.
/// </summary> /// </summary>
/// <param name="objectId">Object ID to find.</param> public sealed partial class ObjectTable : IReadOnlyCollection<GameObject>
/// <returns>A game object or null.</returns>
public GameObject? SearchById(uint objectId)
{ {
if (objectId is GameObject.InvalidGameObjectId or 0) /// <inheritdoc/>
return null; int IReadOnlyCollection<GameObject>.Count => this.Length;
foreach (var obj in this) /// <inheritdoc/>
public IEnumerator<GameObject> GetEnumerator()
{ {
if (obj == null) for (var i = 0; i < ObjectTableLength; i++)
continue; {
var obj = this[i];
if (obj.ObjectId == objectId) if (obj == null)
return obj; continue;
yield return obj;
}
} }
return null; /// <inheritdoc/>
} IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
/// <summary>
/// Gets the address of the game object at the specified index of the object table.
/// </summary>
/// <param name="index">The index of the object.</param>
/// <returns>The memory address of the object.</returns>
public unsafe IntPtr GetObjectAddress(int index)
{
if (index < 0 || index >= ObjectTableLength)
return IntPtr.Zero;
return *(IntPtr*)(this.address.ObjectTable + (8 * index));
}
/// <summary>
/// Create a reference to an FFXIV game object.
/// </summary>
/// <param name="address">The address of the object in memory.</param>
/// <returns><see cref="GameObject"/> object or inheritor containing the requested data.</returns>
public unsafe GameObject? CreateObjectReference(IntPtr address)
{
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
return null;
if (address == IntPtr.Zero)
return null;
var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)address;
var objKind = (ObjectKind)obj->ObjectKind;
return objKind switch
{
ObjectKind.Player => new PlayerCharacter(address),
ObjectKind.BattleNpc => new BattleNpc(address),
ObjectKind.EventObj => new EventObj(address),
ObjectKind.Companion => new Npc(address),
_ => new GameObject(address),
};
} }
} }
/// <summary>
/// This collection represents the currently spawned FFXIV game objects.
/// </summary>
public sealed partial class ObjectTable : IReadOnlyCollection<GameObject>
{
/// <inheritdoc/>
int IReadOnlyCollection<GameObject>.Count => this.Length;
/// <inheritdoc/>
public IEnumerator<GameObject> GetEnumerator()
{
for (var i = 0; i < ObjectTableLength; i++)
{
var obj = this[i];
if (obj == null)
continue;
yield return obj;
}
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}

View file

@ -2,28 +2,29 @@ using System;
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
namespace Dalamud.Game.ClientState.Objects.Types; namespace Dalamud.Game.ClientState.Objects.Types
/// <summary>
/// This class represents a battle NPC.
/// </summary>
public unsafe class BattleNpc : BattleChara
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BattleNpc"/> class. /// This class represents a battle NPC.
/// Set up a new BattleNpc with the provided memory representation.
/// </summary> /// </summary>
/// <param name="address">The address of this actor in memory.</param> public unsafe class BattleNpc : BattleChara
internal BattleNpc(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="BattleNpc"/> class.
/// Set up a new BattleNpc with the provided memory representation.
/// </summary>
/// <param name="address">The address of this actor in memory.</param>
internal BattleNpc(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the BattleNpc <see cref="BattleNpcSubKind" /> of this BattleNpc.
/// </summary>
public BattleNpcSubKind BattleNpcKind => (BattleNpcSubKind)this.Struct->Character.GameObject.SubKind;
/// <inheritdoc/>
public override uint TargetObjectId => this.Struct->Character.TargetObjectID;
} }
/// <summary>
/// Gets the BattleNpc <see cref="BattleNpcSubKind" /> of this BattleNpc.
/// </summary>
public BattleNpcSubKind BattleNpcKind => (BattleNpcSubKind)this.Struct->Character.GameObject.SubKind;
/// <inheritdoc/>
public override uint TargetObjectId => this.Struct->Character.TargetObjectID;
} }

View file

@ -2,20 +2,21 @@ using System;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
namespace Dalamud.Game.ClientState.Objects.SubKinds; namespace Dalamud.Game.ClientState.Objects.SubKinds
/// <summary>
/// This class represents an EventObj.
/// </summary>
public unsafe class EventObj : GameObject
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="EventObj"/> class. /// This class represents an EventObj.
/// Set up a new EventObj with the provided memory representation.
/// </summary> /// </summary>
/// <param name="address">The address of this event object in memory.</param> public unsafe class EventObj : GameObject
internal EventObj(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="EventObj"/> class.
/// Set up a new EventObj with the provided memory representation.
/// </summary>
/// <param name="address">The address of this event object in memory.</param>
internal EventObj(IntPtr address)
: base(address)
{
}
} }
} }

View file

@ -2,20 +2,21 @@ using System;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
namespace Dalamud.Game.ClientState.Objects.SubKinds; namespace Dalamud.Game.ClientState.Objects.SubKinds
/// <summary>
/// This class represents a NPC.
/// </summary>
public unsafe class Npc : Character
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Npc"/> class. /// This class represents a NPC.
/// Set up a new NPC with the provided memory representation.
/// </summary> /// </summary>
/// <param name="address">The address of this actor in memory.</param> public unsafe class Npc : Character
internal Npc(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="Npc"/> class.
/// Set up a new NPC with the provided memory representation.
/// </summary>
/// <param name="address">The address of this actor in memory.</param>
internal Npc(IntPtr address)
: base(address)
{
}
} }
} }

View file

@ -3,35 +3,36 @@ using System;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers; using Dalamud.Game.ClientState.Resolvers;
namespace Dalamud.Game.ClientState.Objects.SubKinds; namespace Dalamud.Game.ClientState.Objects.SubKinds
/// <summary>
/// This class represents a player character.
/// </summary>
public unsafe class PlayerCharacter : BattleChara
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PlayerCharacter"/> class. /// This class represents a player character.
/// This represents a player character.
/// </summary> /// </summary>
/// <param name="address">The address of this actor in memory.</param> public unsafe class PlayerCharacter : BattleChara
internal PlayerCharacter(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="PlayerCharacter"/> class.
/// This represents a player character.
/// </summary>
/// <param name="address">The address of this actor in memory.</param>
internal PlayerCharacter(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the current <see cref="ExcelResolver{T}">world</see> of the character.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.World> CurrentWorld => new(this.Struct->Character.CurrentWorld);
/// <summary>
/// Gets the home <see cref="ExcelResolver{T}">world</see> of the character.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.World> HomeWorld => new(this.Struct->Character.HomeWorld);
/// <summary>
/// Gets the target actor ID of the PlayerCharacter.
/// </summary>
public override uint TargetObjectId => this.Struct->Character.PlayerTargetObjectID;
} }
/// <summary>
/// Gets the current <see cref="ExcelResolver{T}">world</see> of the character.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.World> CurrentWorld => new(this.Struct->Character.CurrentWorld);
/// <summary>
/// Gets the home <see cref="ExcelResolver{T}">world</see> of the character.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.World> HomeWorld => new(this.Struct->Character.HomeWorld);
/// <summary>
/// Gets the target actor ID of the PlayerCharacter.
/// </summary>
public override uint TargetObjectId => this.Struct->Character.PlayerTargetObjectID;
} }

View file

@ -4,160 +4,161 @@ using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
namespace Dalamud.Game.ClientState.Objects; namespace Dalamud.Game.ClientState.Objects
/// <summary>
/// Get and set various kinds of targets for the player.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed unsafe class TargetManager
{ {
private readonly ClientStateAddressResolver address;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="TargetManager"/> class. /// Get and set various kinds of targets for the player.
/// </summary> /// </summary>
/// <param name="addressResolver">The ClientStateAddressResolver instance.</param> [PluginInterface]
internal TargetManager(ClientStateAddressResolver addressResolver) [InterfaceVersion("1.0")]
public sealed unsafe class TargetManager
{ {
this.address = addressResolver; private readonly ClientStateAddressResolver address;
/// <summary>
/// Initializes a new instance of the <see cref="TargetManager"/> class.
/// </summary>
/// <param name="addressResolver">The ClientStateAddressResolver instance.</param>
internal TargetManager(ClientStateAddressResolver addressResolver)
{
this.address = addressResolver;
}
/// <summary>
/// Gets the address of the target manager.
/// </summary>
public IntPtr Address => this.address.TargetManager;
/// <summary>
/// Gets or sets the current target.
/// </summary>
public GameObject? Target
{
get => Service<ObjectTable>.Get().CreateObjectReference((IntPtr)Struct->Target);
set => this.SetTarget(value);
}
/// <summary>
/// Gets or sets the mouseover target.
/// </summary>
public GameObject? MouseOverTarget
{
get => Service<ObjectTable>.Get().CreateObjectReference((IntPtr)Struct->MouseOverTarget);
set => this.SetMouseOverTarget(value);
}
/// <summary>
/// Gets or sets the focus target.
/// </summary>
public GameObject? FocusTarget
{
get => Service<ObjectTable>.Get().CreateObjectReference((IntPtr)Struct->FocusTarget);
set => this.SetFocusTarget(value);
}
/// <summary>
/// Gets or sets the previous target.
/// </summary>
public GameObject? PreviousTarget
{
get => Service<ObjectTable>.Get().CreateObjectReference((IntPtr)Struct->PreviousTarget);
set => this.SetPreviousTarget(value);
}
/// <summary>
/// Gets or sets the soft target.
/// </summary>
public GameObject? SoftTarget
{
get => Service<ObjectTable>.Get().CreateObjectReference((IntPtr)Struct->SoftTarget);
set => this.SetSoftTarget(value);
}
private FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem*)this.Address;
/// <summary>
/// Sets the current target.
/// </summary>
/// <param name="actor">Actor to target.</param>
public void SetTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the mouseover target.
/// </summary>
/// <param name="actor">Actor to target.</param>
public void SetMouseOverTarget(GameObject? actor) => this.SetMouseOverTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the focus target.
/// </summary>
/// <param name="actor">Actor to target.</param>
public void SetFocusTarget(GameObject? actor) => this.SetFocusTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the previous target.
/// </summary>
/// <param name="actor">Actor to target.</param>
public void SetPreviousTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the soft target.
/// </summary>
/// <param name="actor">Actor to target.</param>
public void SetSoftTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the current target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
public void SetTarget(IntPtr actorAddress) => Struct->Target = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the mouseover target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
public void SetMouseOverTarget(IntPtr actorAddress) => Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the focus target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
public void SetFocusTarget(IntPtr actorAddress) => Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the previous target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
public void SetPreviousTarget(IntPtr actorAddress) => Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the soft target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
public void SetSoftTarget(IntPtr actorAddress) => Struct->SoftTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Clears the current target.
/// </summary>
public void ClearTarget() => this.SetTarget(IntPtr.Zero);
/// <summary>
/// Clears the mouseover target.
/// </summary>
public void ClearMouseOverTarget() => this.SetMouseOverTarget(IntPtr.Zero);
/// <summary>
/// Clears the focus target.
/// </summary>
public void ClearFocusTarget() => this.SetFocusTarget(IntPtr.Zero);
/// <summary>
/// Clears the previous target.
/// </summary>
public void ClearPreviousTarget() => this.SetPreviousTarget(IntPtr.Zero);
/// <summary>
/// Clears the soft target.
/// </summary>
public void ClearSoftTarget() => this.SetSoftTarget(IntPtr.Zero);
} }
/// <summary>
/// Gets the address of the target manager.
/// </summary>
public IntPtr Address => this.address.TargetManager;
/// <summary>
/// Gets or sets the current target.
/// </summary>
public GameObject? Target
{
get => Service<ObjectTable>.Get().CreateObjectReference((IntPtr)Struct->Target);
set => this.SetTarget(value);
}
/// <summary>
/// Gets or sets the mouseover target.
/// </summary>
public GameObject? MouseOverTarget
{
get => Service<ObjectTable>.Get().CreateObjectReference((IntPtr)Struct->MouseOverTarget);
set => this.SetMouseOverTarget(value);
}
/// <summary>
/// Gets or sets the focus target.
/// </summary>
public GameObject? FocusTarget
{
get => Service<ObjectTable>.Get().CreateObjectReference((IntPtr)Struct->FocusTarget);
set => this.SetFocusTarget(value);
}
/// <summary>
/// Gets or sets the previous target.
/// </summary>
public GameObject? PreviousTarget
{
get => Service<ObjectTable>.Get().CreateObjectReference((IntPtr)Struct->PreviousTarget);
set => this.SetPreviousTarget(value);
}
/// <summary>
/// Gets or sets the soft target.
/// </summary>
public GameObject? SoftTarget
{
get => Service<ObjectTable>.Get().CreateObjectReference((IntPtr)Struct->SoftTarget);
set => this.SetSoftTarget(value);
}
private FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem*)this.Address;
/// <summary>
/// Sets the current target.
/// </summary>
/// <param name="actor">Actor to target.</param>
public void SetTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the mouseover target.
/// </summary>
/// <param name="actor">Actor to target.</param>
public void SetMouseOverTarget(GameObject? actor) => this.SetMouseOverTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the focus target.
/// </summary>
/// <param name="actor">Actor to target.</param>
public void SetFocusTarget(GameObject? actor) => this.SetFocusTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the previous target.
/// </summary>
/// <param name="actor">Actor to target.</param>
public void SetPreviousTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the soft target.
/// </summary>
/// <param name="actor">Actor to target.</param>
public void SetSoftTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the current target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
public void SetTarget(IntPtr actorAddress) => Struct->Target = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the mouseover target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
public void SetMouseOverTarget(IntPtr actorAddress) => Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the focus target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
public void SetFocusTarget(IntPtr actorAddress) => Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the previous target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
public void SetPreviousTarget(IntPtr actorAddress) => Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the soft target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
public void SetSoftTarget(IntPtr actorAddress) => Struct->SoftTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Clears the current target.
/// </summary>
public void ClearTarget() => this.SetTarget(IntPtr.Zero);
/// <summary>
/// Clears the mouseover target.
/// </summary>
public void ClearMouseOverTarget() => this.SetMouseOverTarget(IntPtr.Zero);
/// <summary>
/// Clears the focus target.
/// </summary>
public void ClearFocusTarget() => this.SetFocusTarget(IntPtr.Zero);
/// <summary>
/// Clears the previous target.
/// </summary>
public void ClearPreviousTarget() => this.SetPreviousTarget(IntPtr.Zero);
/// <summary>
/// Clears the soft target.
/// </summary>
public void ClearSoftTarget() => this.SetSoftTarget(IntPtr.Zero);
} }

View file

@ -2,65 +2,66 @@ using System;
using Dalamud.Game.ClientState.Statuses; using Dalamud.Game.ClientState.Statuses;
namespace Dalamud.Game.ClientState.Objects.Types; namespace Dalamud.Game.ClientState.Objects.Types
/// <summary>
/// This class represents the battle characters.
/// </summary>
public unsafe class BattleChara : Character
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BattleChara"/> class. /// This class represents the battle characters.
/// This represents a battle character.
/// </summary> /// </summary>
/// <param name="address">The address of this character in memory.</param> public unsafe class BattleChara : Character
internal BattleChara(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="BattleChara"/> class.
/// This represents a battle character.
/// </summary>
/// <param name="address">The address of this character in memory.</param>
internal BattleChara(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the current status effects.
/// </summary>
public StatusList StatusList => new(&this.Struct->StatusManager);
/// <summary>
/// Gets a value indicating whether the chara is currently casting.
/// </summary>
public bool IsCasting => this.Struct->SpellCastInfo.IsCasting > 0;
/// <summary>
/// Gets a value indicating whether the cast is interruptible.
/// </summary>
public bool IsCastInterruptible => this.Struct->SpellCastInfo.Interruptible > 0;
/// <summary>
/// Gets the spell action type of the spell being cast by the actor.
/// </summary>
public byte CastActionType => (byte)this.Struct->SpellCastInfo.ActionType;
/// <summary>
/// Gets the spell action ID of the spell being cast by the actor.
/// </summary>
public uint CastActionId => this.Struct->SpellCastInfo.ActionID;
/// <summary>
/// Gets the object ID of the target currently being cast at by the chara.
/// </summary>
public uint CastTargetObjectId => this.Struct->SpellCastInfo.CastTargetID;
/// <summary>
/// Gets the current casting time of the spell being cast by the chara.
/// </summary>
public float CurrentCastTime => this.Struct->SpellCastInfo.CurrentCastTime;
/// <summary>
/// Gets the total casting time of the spell being cast by the chara.
/// </summary>
public float TotalCastTime => this.Struct->SpellCastInfo.TotalCastTime;
/// <summary>
/// Gets the underlying structure.
/// </summary>
private protected new FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)this.Address;
} }
/// <summary>
/// Gets the current status effects.
/// </summary>
public StatusList StatusList => new(&this.Struct->StatusManager);
/// <summary>
/// Gets a value indicating whether the chara is currently casting.
/// </summary>
public bool IsCasting => this.Struct->SpellCastInfo.IsCasting > 0;
/// <summary>
/// Gets a value indicating whether the cast is interruptible.
/// </summary>
public bool IsCastInterruptible => this.Struct->SpellCastInfo.Interruptible > 0;
/// <summary>
/// Gets the spell action type of the spell being cast by the actor.
/// </summary>
public byte CastActionType => (byte)this.Struct->SpellCastInfo.ActionType;
/// <summary>
/// Gets the spell action ID of the spell being cast by the actor.
/// </summary>
public uint CastActionId => this.Struct->SpellCastInfo.ActionID;
/// <summary>
/// Gets the object ID of the target currently being cast at by the chara.
/// </summary>
public uint CastTargetObjectId => this.Struct->SpellCastInfo.CastTargetID;
/// <summary>
/// Gets the current casting time of the spell being cast by the chara.
/// </summary>
public float CurrentCastTime => this.Struct->SpellCastInfo.CurrentCastTime;
/// <summary>
/// Gets the total casting time of the spell being cast by the chara.
/// </summary>
public float TotalCastTime => this.Struct->SpellCastInfo.TotalCastTime;
/// <summary>
/// Gets the underlying structure.
/// </summary>
private protected new FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)this.Address;
} }

View file

@ -5,101 +5,102 @@ using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory; using Dalamud.Memory;
namespace Dalamud.Game.ClientState.Objects.Types; namespace Dalamud.Game.ClientState.Objects.Types
/// <summary>
/// This class represents the base for non-static entities.
/// </summary>
public unsafe class Character : GameObject
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Character"/> class. /// This class represents the base for non-static entities.
/// This represents a non-static entity.
/// </summary> /// </summary>
/// <param name="address">The address of this character in memory.</param> public unsafe class Character : GameObject
internal Character(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="Character"/> class.
/// This represents a non-static entity.
/// </summary>
/// <param name="address">The address of this character in memory.</param>
internal Character(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the current HP of this Chara.
/// </summary>
public uint CurrentHp => this.Struct->Health;
/// <summary>
/// Gets the maximum HP of this Chara.
/// </summary>
public uint MaxHp => this.Struct->MaxHealth;
/// <summary>
/// Gets the current MP of this Chara.
/// </summary>
public uint CurrentMp => this.Struct->Mana;
/// <summary>
/// Gets the maximum MP of this Chara.
/// </summary>
public uint MaxMp => this.Struct->MaxMana;
/// <summary>
/// Gets the current GP of this Chara.
/// </summary>
public uint CurrentGp => this.Struct->GatheringPoints;
/// <summary>
/// Gets the maximum GP of this Chara.
/// </summary>
public uint MaxGp => this.Struct->MaxGatheringPoints;
/// <summary>
/// Gets the current CP of this Chara.
/// </summary>
public uint CurrentCp => this.Struct->CraftingPoints;
/// <summary>
/// Gets the maximum CP of this Chara.
/// </summary>
public uint MaxCp => this.Struct->MaxCraftingPoints;
/// <summary>
/// Gets the ClassJob of this Chara.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.ClassJob> ClassJob => new(this.Struct->ClassJob);
/// <summary>
/// Gets the level of this Chara.
/// </summary>
public byte Level => this.Struct->Level;
/// <summary>
/// Gets a byte array describing the visual appearance of this Chara.
/// Indexed by <see cref="CustomizeIndex"/>.
/// </summary>
public byte[] Customize => MemoryHelper.Read<byte>((IntPtr)this.Struct->CustomizeData, 28);
/// <summary>
/// Gets the Free Company tag of this chara.
/// </summary>
public SeString CompanyTag => MemoryHelper.ReadSeString((IntPtr)this.Struct->FreeCompanyTag, 6);
/// <summary>
/// Gets the target object ID of the character.
/// </summary>
public override uint TargetObjectId => this.Struct->TargetObjectID;
/// <summary>
/// Gets the name ID of the character.
/// </summary>
public uint NameId => this.Struct->NameID;
/// <summary>
/// Gets the status flags.
/// </summary>
public StatusFlags StatusFlags => (StatusFlags)this.Struct->StatusFlags;
/// <summary>
/// Gets the underlying structure.
/// </summary>
private protected new FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)this.Address;
} }
/// <summary>
/// Gets the current HP of this Chara.
/// </summary>
public uint CurrentHp => this.Struct->Health;
/// <summary>
/// Gets the maximum HP of this Chara.
/// </summary>
public uint MaxHp => this.Struct->MaxHealth;
/// <summary>
/// Gets the current MP of this Chara.
/// </summary>
public uint CurrentMp => this.Struct->Mana;
/// <summary>
/// Gets the maximum MP of this Chara.
/// </summary>
public uint MaxMp => this.Struct->MaxMana;
/// <summary>
/// Gets the current GP of this Chara.
/// </summary>
public uint CurrentGp => this.Struct->GatheringPoints;
/// <summary>
/// Gets the maximum GP of this Chara.
/// </summary>
public uint MaxGp => this.Struct->MaxGatheringPoints;
/// <summary>
/// Gets the current CP of this Chara.
/// </summary>
public uint CurrentCp => this.Struct->CraftingPoints;
/// <summary>
/// Gets the maximum CP of this Chara.
/// </summary>
public uint MaxCp => this.Struct->MaxCraftingPoints;
/// <summary>
/// Gets the ClassJob of this Chara.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.ClassJob> ClassJob => new(this.Struct->ClassJob);
/// <summary>
/// Gets the level of this Chara.
/// </summary>
public byte Level => this.Struct->Level;
/// <summary>
/// Gets a byte array describing the visual appearance of this Chara.
/// Indexed by <see cref="CustomizeIndex"/>.
/// </summary>
public byte[] Customize => MemoryHelper.Read<byte>((IntPtr)this.Struct->CustomizeData, 28);
/// <summary>
/// Gets the Free Company tag of this chara.
/// </summary>
public SeString CompanyTag => MemoryHelper.ReadSeString((IntPtr)this.Struct->FreeCompanyTag, 6);
/// <summary>
/// Gets the target object ID of the character.
/// </summary>
public override uint TargetObjectId => this.Struct->TargetObjectID;
/// <summary>
/// Gets the name ID of the character.
/// </summary>
public uint NameId => this.Struct->NameID;
/// <summary>
/// Gets the status flags.
/// </summary>
public StatusFlags StatusFlags => (StatusFlags)this.Struct->StatusFlags;
/// <summary>
/// Gets the underlying structure.
/// </summary>
private protected new FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)this.Address;
} }

View file

@ -5,170 +5,171 @@ using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory; using Dalamud.Memory;
namespace Dalamud.Game.ClientState.Objects.Types; namespace Dalamud.Game.ClientState.Objects.Types
/// <summary>
/// This class represents a GameObject in FFXIV.
/// </summary>
public unsafe partial class GameObject : IEquatable<GameObject>
{ {
/// <summary> /// <summary>
/// IDs of non-networked GameObjects. /// This class represents a GameObject in FFXIV.
/// </summary> /// </summary>
public const uint InvalidGameObjectId = 0xE0000000; public unsafe partial class GameObject : IEquatable<GameObject>
/// <summary>
/// Initializes a new instance of the <see cref="GameObject"/> class.
/// </summary>
/// <param name="address">The address of this game object in memory.</param>
internal GameObject(IntPtr address)
{ {
this.Address = address; /// <summary>
/// IDs of non-networked GameObjects.
/// </summary>
public const uint InvalidGameObjectId = 0xE0000000;
/// <summary>
/// Initializes a new instance of the <see cref="GameObject"/> class.
/// </summary>
/// <param name="address">The address of this game object in memory.</param>
internal GameObject(IntPtr address)
{
this.Address = address;
}
/// <summary>
/// Gets the address of the game object in memory.
/// </summary>
public IntPtr Address { get; }
/// <summary>
/// Gets the Dalamud instance.
/// </summary>
private protected Dalamud Dalamud { get; }
/// <summary>
/// This allows you to <c>if (obj) {...}</c> to check for validity.
/// </summary>
/// <param name="gameObject">The actor to check.</param>
/// <returns>True or false.</returns>
public static implicit operator bool(GameObject? gameObject) => IsValid(gameObject);
public static bool operator ==(GameObject? gameObject1, GameObject? gameObject2)
{
// Using == results in a stack overflow.
if (gameObject1 is null || gameObject2 is null)
return Equals(gameObject1, gameObject2);
return gameObject1.Equals(gameObject2);
}
public static bool operator !=(GameObject? actor1, GameObject? actor2) => !(actor1 == actor2);
/// <summary>
/// Gets a value indicating whether this actor is still valid in memory.
/// </summary>
/// <param name="actor">The actor to check.</param>
/// <returns>True or false.</returns>
public static bool IsValid(GameObject? actor)
{
var clientState = Service<ClientState>.Get();
if (actor is null)
return false;
if (clientState.LocalContentId == 0)
return false;
return true;
}
/// <summary>
/// Gets a value indicating whether this actor is still valid in memory.
/// </summary>
/// <returns>True or false.</returns>
public bool IsValid() => IsValid(this);
/// <inheritdoc/>
bool IEquatable<GameObject>.Equals(GameObject other) => this.ObjectId == other?.ObjectId;
/// <inheritdoc/>
public override bool Equals(object obj) => ((IEquatable<GameObject>)this).Equals(obj as GameObject);
/// <inheritdoc/>
public override int GetHashCode() => this.ObjectId.GetHashCode();
} }
/// <summary> /// <summary>
/// Gets the address of the game object in memory. /// This class represents a basic actor (GameObject) in FFXIV.
/// </summary> /// </summary>
public IntPtr Address { get; } public unsafe partial class GameObject
/// <summary>
/// Gets the Dalamud instance.
/// </summary>
private protected Dalamud Dalamud { get; }
/// <summary>
/// This allows you to <c>if (obj) {...}</c> to check for validity.
/// </summary>
/// <param name="gameObject">The actor to check.</param>
/// <returns>True or false.</returns>
public static implicit operator bool(GameObject? gameObject) => IsValid(gameObject);
public static bool operator ==(GameObject? gameObject1, GameObject? gameObject2)
{ {
// Using == results in a stack overflow. /// <summary>
if (gameObject1 is null || gameObject2 is null) /// Gets the name of this <see cref="GameObject" />.
return Equals(gameObject1, gameObject2); /// </summary>
public SeString Name => MemoryHelper.ReadSeString((IntPtr)this.Struct->Name, 64);
return gameObject1.Equals(gameObject2); /// <summary>
/// Gets the object ID of this <see cref="GameObject" />.
/// </summary>
public uint ObjectId => this.Struct->ObjectID;
/// <summary>
/// Gets the data ID for linking to other respective game data.
/// </summary>
public uint DataId => this.Struct->DataID;
/// <summary>
/// Gets the ID of this GameObject's owner.
/// </summary>
public uint OwnerId => this.Struct->OwnerID;
/// <summary>
/// Gets the entity kind of this <see cref="GameObject" />.
/// See <see cref="ObjectKind">the ObjectKind enum</see> for possible values.
/// </summary>
public ObjectKind ObjectKind => (ObjectKind)this.Struct->ObjectKind;
/// <summary>
/// Gets the sub kind of this Actor.
/// </summary>
public byte SubKind => this.Struct->SubKind;
/// <summary>
/// Gets the X distance from the local player in yalms.
/// </summary>
public byte YalmDistanceX => this.Struct->YalmDistanceFromPlayerX;
/// <summary>
/// Gets the Y distance from the local player in yalms.
/// </summary>
public byte YalmDistanceZ => this.Struct->YalmDistanceFromPlayerZ;
/// <summary>
/// Gets the position of this <see cref="GameObject" />.
/// </summary>
public Vector3 Position => new(this.Struct->Position.X, this.Struct->Position.Y, this.Struct->Position.Z);
/// <summary>
/// Gets the rotation of this <see cref="GameObject" />.
/// This ranges from -pi to pi radians.
/// </summary>
public float Rotation => this.Struct->Rotation;
/// <summary>
/// Gets the hitbox radius of this <see cref="GameObject" />.
/// </summary>
public float HitboxRadius => this.Struct->HitboxRadius;
/// <summary>
/// Gets the current target of the game object.
/// </summary>
public virtual uint TargetObjectId => 0;
/// <summary>
/// Gets the target object of the game object.
/// </summary>
/// <remarks>
/// This iterates the actor table, it should be used with care.
/// </remarks>
// TODO: Fix for non-networked GameObjects
public virtual GameObject? TargetObject => Service<ObjectTable>.Get().SearchById(this.TargetObjectId);
/// <summary>
/// Gets the underlying structure.
/// </summary>
private protected FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)this.Address;
/// <inheritdoc/>
public override string ToString() => $"{this.ObjectId:X}({this.Name.TextValue} - {this.ObjectKind}) at {this.Address:X}";
} }
public static bool operator !=(GameObject? actor1, GameObject? actor2) => !(actor1 == actor2);
/// <summary>
/// Gets a value indicating whether this actor is still valid in memory.
/// </summary>
/// <param name="actor">The actor to check.</param>
/// <returns>True or false.</returns>
public static bool IsValid(GameObject? actor)
{
var clientState = Service<ClientState>.Get();
if (actor is null)
return false;
if (clientState.LocalContentId == 0)
return false;
return true;
}
/// <summary>
/// Gets a value indicating whether this actor is still valid in memory.
/// </summary>
/// <returns>True or false.</returns>
public bool IsValid() => IsValid(this);
/// <inheritdoc/>
bool IEquatable<GameObject>.Equals(GameObject other) => this.ObjectId == other?.ObjectId;
/// <inheritdoc/>
public override bool Equals(object obj) => ((IEquatable<GameObject>)this).Equals(obj as GameObject);
/// <inheritdoc/>
public override int GetHashCode() => this.ObjectId.GetHashCode();
}
/// <summary>
/// This class represents a basic actor (GameObject) in FFXIV.
/// </summary>
public unsafe partial class GameObject
{
/// <summary>
/// Gets the name of this <see cref="GameObject" />.
/// </summary>
public SeString Name => MemoryHelper.ReadSeString((IntPtr)this.Struct->Name, 64);
/// <summary>
/// Gets the object ID of this <see cref="GameObject" />.
/// </summary>
public uint ObjectId => this.Struct->ObjectID;
/// <summary>
/// Gets the data ID for linking to other respective game data.
/// </summary>
public uint DataId => this.Struct->DataID;
/// <summary>
/// Gets the ID of this GameObject's owner.
/// </summary>
public uint OwnerId => this.Struct->OwnerID;
/// <summary>
/// Gets the entity kind of this <see cref="GameObject" />.
/// See <see cref="ObjectKind">the ObjectKind enum</see> for possible values.
/// </summary>
public ObjectKind ObjectKind => (ObjectKind)this.Struct->ObjectKind;
/// <summary>
/// Gets the sub kind of this Actor.
/// </summary>
public byte SubKind => this.Struct->SubKind;
/// <summary>
/// Gets the X distance from the local player in yalms.
/// </summary>
public byte YalmDistanceX => this.Struct->YalmDistanceFromPlayerX;
/// <summary>
/// Gets the Y distance from the local player in yalms.
/// </summary>
public byte YalmDistanceZ => this.Struct->YalmDistanceFromPlayerZ;
/// <summary>
/// Gets the position of this <see cref="GameObject" />.
/// </summary>
public Vector3 Position => new(this.Struct->Position.X, this.Struct->Position.Y, this.Struct->Position.Z);
/// <summary>
/// Gets the rotation of this <see cref="GameObject" />.
/// This ranges from -pi to pi radians.
/// </summary>
public float Rotation => this.Struct->Rotation;
/// <summary>
/// Gets the hitbox radius of this <see cref="GameObject" />.
/// </summary>
public float HitboxRadius => this.Struct->HitboxRadius;
/// <summary>
/// Gets the current target of the game object.
/// </summary>
public virtual uint TargetObjectId => 0;
/// <summary>
/// Gets the target object of the game object.
/// </summary>
/// <remarks>
/// This iterates the actor table, it should be used with care.
/// </remarks>
// TODO: Fix for non-networked GameObjects
public virtual GameObject? TargetObject => Service<ObjectTable>.Get().SearchById(this.TargetObjectId);
/// <summary>
/// Gets the underlying structure.
/// </summary>
private protected FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)this.Address;
/// <inheritdoc/>
public override string ToString() => $"{this.ObjectId:X}({this.Name.TextValue} - {this.ObjectKind}) at {this.Address:X}";
} }

View file

@ -7,177 +7,178 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.Party; namespace Dalamud.Game.ClientState.Party
/// <summary>
/// This collection represents the actors present in your party or alliance.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed unsafe partial class PartyList
{ {
private const int GroupLength = 8;
private const int AllianceLength = 20;
private readonly ClientStateAddressResolver address;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PartyList"/> class. /// This collection represents the actors present in your party or alliance.
/// </summary> /// </summary>
/// <param name="addressResolver">Client state address resolver.</param> [PluginInterface]
internal PartyList(ClientStateAddressResolver addressResolver) [InterfaceVersion("1.0")]
public sealed unsafe partial class PartyList
{ {
this.address = addressResolver; private const int GroupLength = 8;
private const int AllianceLength = 20;
Log.Verbose($"Group manager address 0x{this.address.GroupManager.ToInt64():X}"); private readonly ClientStateAddressResolver address;
}
/// <summary> /// <summary>
/// Gets the amount of party members the local player has. /// Initializes a new instance of the <see cref="PartyList"/> class.
/// </summary> /// </summary>
public int Length => this.GroupManagerStruct->MemberCount; /// <param name="addressResolver">Client state address resolver.</param>
internal PartyList(ClientStateAddressResolver addressResolver)
/// <summary>
/// Gets the index of the party leader.
/// </summary>
public uint PartyLeaderIndex => this.GroupManagerStruct->PartyLeaderIndex;
/// <summary>
/// Gets a value indicating whether this group is an alliance.
/// </summary>
public bool IsAlliance => this.GroupManagerStruct->IsAlliance;
/// <summary>
/// Gets the address of the Group Manager.
/// </summary>
public IntPtr GroupManagerAddress => this.address.GroupManager;
/// <summary>
/// Gets the address of the party list within the group manager.
/// </summary>
public IntPtr GroupListAddress => (IntPtr)GroupManagerStruct->PartyMembers;
/// <summary>
/// Gets the address of the alliance member list within the group manager.
/// </summary>
public IntPtr AllianceListAddress => (IntPtr)this.GroupManagerStruct->AllianceMembers;
private static int PartyMemberSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember>();
private FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager* GroupManagerStruct => (FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager*)this.GroupManagerAddress;
/// <summary>
/// Get a party member at the specified spawn index.
/// </summary>
/// <param name="index">Spawn index.</param>
/// <returns>A <see cref="PartyMember"/> at the specified spawn index.</returns>
public PartyMember? this[int index]
{
get
{ {
// Normally using Length results in a recursion crash, however we know the party size via ptr. this.address = addressResolver;
if (index < 0 || index >= this.Length)
Log.Verbose($"Group manager address 0x{this.address.GroupManager.ToInt64():X}");
}
/// <summary>
/// Gets the amount of party members the local player has.
/// </summary>
public int Length => this.GroupManagerStruct->MemberCount;
/// <summary>
/// Gets the index of the party leader.
/// </summary>
public uint PartyLeaderIndex => this.GroupManagerStruct->PartyLeaderIndex;
/// <summary>
/// Gets a value indicating whether this group is an alliance.
/// </summary>
public bool IsAlliance => this.GroupManagerStruct->IsAlliance;
/// <summary>
/// Gets the address of the Group Manager.
/// </summary>
public IntPtr GroupManagerAddress => this.address.GroupManager;
/// <summary>
/// Gets the address of the party list within the group manager.
/// </summary>
public IntPtr GroupListAddress => (IntPtr)GroupManagerStruct->PartyMembers;
/// <summary>
/// Gets the address of the alliance member list within the group manager.
/// </summary>
public IntPtr AllianceListAddress => (IntPtr)this.GroupManagerStruct->AllianceMembers;
private static int PartyMemberSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember>();
private FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager* GroupManagerStruct => (FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager*)this.GroupManagerAddress;
/// <summary>
/// Get a party member at the specified spawn index.
/// </summary>
/// <param name="index">Spawn index.</param>
/// <returns>A <see cref="PartyMember"/> at the specified spawn index.</returns>
public PartyMember? this[int index]
{
get
{
// Normally using Length results in a recursion crash, however we know the party size via ptr.
if (index < 0 || index >= this.Length)
return null;
if (this.Length > GroupLength)
{
var addr = this.GetAllianceMemberAddress(index);
return this.CreateAllianceMemberReference(addr);
}
else
{
var addr = this.GetPartyMemberAddress(index);
return this.CreatePartyMemberReference(addr);
}
}
}
/// <summary>
/// Gets the address of the party member at the specified index of the party list.
/// </summary>
/// <param name="index">The index of the party member.</param>
/// <returns>The memory address of the party member.</returns>
public IntPtr GetPartyMemberAddress(int index)
{
if (index < 0 || index >= GroupLength)
return IntPtr.Zero;
return this.GroupListAddress + (index * PartyMemberSize);
}
/// <summary>
/// Create a reference to an FFXIV party member.
/// </summary>
/// <param name="address">The address of the party member in memory.</param>
/// <returns>The party member object containing the requested data.</returns>
public PartyMember? CreatePartyMemberReference(IntPtr address)
{
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
return null; return null;
if (this.Length > GroupLength) if (address == IntPtr.Zero)
{ return null;
var addr = this.GetAllianceMemberAddress(index);
return this.CreateAllianceMemberReference(addr); return new PartyMember(address);
}
else
{
var addr = this.GetPartyMemberAddress(index);
return this.CreatePartyMemberReference(addr);
}
} }
}
/// <summary> /// <summary>
/// Gets the address of the party member at the specified index of the party list. /// Gets the address of the alliance member at the specified index of the alliance list.
/// </summary> /// </summary>
/// <param name="index">The index of the party member.</param> /// <param name="index">The index of the alliance member.</param>
/// <returns>The memory address of the party member.</returns> /// <returns>The memory address of the alliance member.</returns>
public IntPtr GetPartyMemberAddress(int index) public IntPtr GetAllianceMemberAddress(int index)
{
if (index < 0 || index >= GroupLength)
return IntPtr.Zero;
return this.GroupListAddress + (index * PartyMemberSize);
}
/// <summary>
/// Create a reference to an FFXIV party member.
/// </summary>
/// <param name="address">The address of the party member in memory.</param>
/// <returns>The party member object containing the requested data.</returns>
public PartyMember? CreatePartyMemberReference(IntPtr address)
{
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
return null;
if (address == IntPtr.Zero)
return null;
return new PartyMember(address);
}
/// <summary>
/// Gets the address of the alliance member at the specified index of the alliance list.
/// </summary>
/// <param name="index">The index of the alliance member.</param>
/// <returns>The memory address of the alliance member.</returns>
public IntPtr GetAllianceMemberAddress(int index)
{
if (index < 0 || index >= AllianceLength)
return IntPtr.Zero;
return this.AllianceListAddress + (index * PartyMemberSize);
}
/// <summary>
/// Create a reference to an FFXIV alliance member.
/// </summary>
/// <param name="address">The address of the alliance member in memory.</param>
/// <returns>The party member object containing the requested data.</returns>
public PartyMember? CreateAllianceMemberReference(IntPtr address)
{
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
return null;
if (address == IntPtr.Zero)
return null;
return new PartyMember(address);
}
}
/// <summary>
/// This collection represents the party members present in your party or alliance.
/// </summary>
public sealed partial class PartyList : IReadOnlyCollection<PartyMember>
{
/// <inheritdoc/>
int IReadOnlyCollection<PartyMember>.Count => this.Length;
/// <inheritdoc/>
public IEnumerator<PartyMember> GetEnumerator()
{
// Normally using Length results in a recursion crash, however we know the party size via ptr.
for (var i = 0; i < this.Length; i++)
{ {
var member = this[i]; if (index < 0 || index >= AllianceLength)
return IntPtr.Zero;
if (member == null) return this.AllianceListAddress + (index * PartyMemberSize);
break; }
yield return member; /// <summary>
/// Create a reference to an FFXIV alliance member.
/// </summary>
/// <param name="address">The address of the alliance member in memory.</param>
/// <returns>The party member object containing the requested data.</returns>
public PartyMember? CreateAllianceMemberReference(IntPtr address)
{
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
return null;
if (address == IntPtr.Zero)
return null;
return new PartyMember(address);
} }
} }
/// <inheritdoc/> /// <summary>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); /// This collection represents the party members present in your party or alliance.
/// </summary>
public sealed partial class PartyList : IReadOnlyCollection<PartyMember>
{
/// <inheritdoc/>
int IReadOnlyCollection<PartyMember>.Count => this.Length;
/// <inheritdoc/>
public IEnumerator<PartyMember> GetEnumerator()
{
// Normally using Length results in a recursion crash, however we know the party size via ptr.
for (var i = 0; i < this.Length; i++)
{
var member = this[i];
if (member == null)
break;
yield return member;
}
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}
} }

View file

@ -9,104 +9,105 @@ using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory; using Dalamud.Memory;
using JetBrains.Annotations; using JetBrains.Annotations;
namespace Dalamud.Game.ClientState.Party; namespace Dalamud.Game.ClientState.Party
/// <summary>
/// This class represents a party member in the group manager.
/// </summary>
public unsafe class PartyMember
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PartyMember"/> class. /// This class represents a party member in the group manager.
/// </summary> /// </summary>
/// <param name="address">Address of the party member.</param> public unsafe class PartyMember
internal PartyMember(IntPtr address)
{ {
this.Address = address; /// <summary>
/// Initializes a new instance of the <see cref="PartyMember"/> class.
/// </summary>
/// <param name="address">Address of the party member.</param>
internal PartyMember(IntPtr address)
{
this.Address = address;
}
/// <summary>
/// Gets the address of this party member in memory.
/// </summary>
public IntPtr Address { get; }
/// <summary>
/// Gets a list of buffs or debuffs applied to this party member.
/// </summary>
public StatusList Statuses => new(&this.Struct->StatusManager);
/// <summary>
/// Gets the position of the party member.
/// </summary>
public Vector3 Position => new(this.Struct->X, this.Struct->Y, this.Struct->Z);
/// <summary>
/// Gets the content ID of the party member.
/// </summary>
public long ContentId => this.Struct->ContentID;
/// <summary>
/// Gets the actor ID of this party member.
/// </summary>
public uint ObjectId => this.Struct->ObjectID;
/// <summary>
/// Gets the actor associated with this buddy.
/// </summary>
/// <remarks>
/// This iterates the actor table, it should be used with care.
/// </remarks>
public GameObject? GameObject => Service<ObjectTable>.Get().SearchById(this.ObjectId);
/// <summary>
/// Gets the current HP of this party member.
/// </summary>
public uint CurrentHP => this.Struct->CurrentHP;
/// <summary>
/// Gets the maximum HP of this party member.
/// </summary>
public uint MaxHP => this.Struct->MaxHP;
/// <summary>
/// Gets the current MP of this party member.
/// </summary>
public ushort CurrentMP => this.Struct->CurrentMP;
/// <summary>
/// Gets the maximum MP of this party member.
/// </summary>
public ushort MaxMP => this.Struct->MaxMP;
/// <summary>
/// Gets the territory this party member is located in.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.TerritoryType> Territory => new(this.Struct->TerritoryType);
/// <summary>
/// Gets the World this party member resides in.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.World> World => new(this.Struct->HomeWorld);
/// <summary>
/// Gets the displayname of this party member.
/// </summary>
public SeString Name => MemoryHelper.ReadSeString((IntPtr)Struct->Name, 0x40);
/// <summary>
/// Gets the sex of this party member.
/// </summary>
public byte Sex => this.Struct->Sex;
/// <summary>
/// Gets the classjob of this party member.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.ClassJob> ClassJob => new(this.Struct->ClassJob);
/// <summary>
/// Gets the level of this party member.
/// </summary>
public byte Level => this.Struct->Level;
private FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember*)this.Address;
} }
/// <summary>
/// Gets the address of this party member in memory.
/// </summary>
public IntPtr Address { get; }
/// <summary>
/// Gets a list of buffs or debuffs applied to this party member.
/// </summary>
public StatusList Statuses => new(&this.Struct->StatusManager);
/// <summary>
/// Gets the position of the party member.
/// </summary>
public Vector3 Position => new(this.Struct->X, this.Struct->Y, this.Struct->Z);
/// <summary>
/// Gets the content ID of the party member.
/// </summary>
public long ContentId => this.Struct->ContentID;
/// <summary>
/// Gets the actor ID of this party member.
/// </summary>
public uint ObjectId => this.Struct->ObjectID;
/// <summary>
/// Gets the actor associated with this buddy.
/// </summary>
/// <remarks>
/// This iterates the actor table, it should be used with care.
/// </remarks>
public GameObject? GameObject => Service<ObjectTable>.Get().SearchById(this.ObjectId);
/// <summary>
/// Gets the current HP of this party member.
/// </summary>
public uint CurrentHP => this.Struct->CurrentHP;
/// <summary>
/// Gets the maximum HP of this party member.
/// </summary>
public uint MaxHP => this.Struct->MaxHP;
/// <summary>
/// Gets the current MP of this party member.
/// </summary>
public ushort CurrentMP => this.Struct->CurrentMP;
/// <summary>
/// Gets the maximum MP of this party member.
/// </summary>
public ushort MaxMP => this.Struct->MaxMP;
/// <summary>
/// Gets the territory this party member is located in.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.TerritoryType> Territory => new(this.Struct->TerritoryType);
/// <summary>
/// Gets the World this party member resides in.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.World> World => new(this.Struct->HomeWorld);
/// <summary>
/// Gets the displayname of this party member.
/// </summary>
public SeString Name => MemoryHelper.ReadSeString((IntPtr)Struct->Name, 0x40);
/// <summary>
/// Gets the sex of this party member.
/// </summary>
public byte Sex => this.Struct->Sex;
/// <summary>
/// Gets the classjob of this party member.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.ClassJob> ClassJob => new(this.Struct->ClassJob);
/// <summary>
/// Gets the level of this party member.
/// </summary>
public byte Level => this.Struct->Level;
private FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember*)this.Address;
} }

View file

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

View file

@ -4,64 +4,65 @@ using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers; using Dalamud.Game.ClientState.Resolvers;
namespace Dalamud.Game.ClientState.Statuses; namespace Dalamud.Game.ClientState.Statuses
/// <summary>
/// This class represents a status effect an actor is afflicted by.
/// </summary>
public unsafe class Status
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Status"/> class. /// This class represents a status effect an actor is afflicted by.
/// </summary> /// </summary>
/// <param name="address">Status address.</param> public unsafe class Status
internal Status(IntPtr address)
{ {
this.Address = address; /// <summary>
/// Initializes a new instance of the <see cref="Status"/> class.
/// </summary>
/// <param name="address">Status address.</param>
internal Status(IntPtr address)
{
this.Address = address;
}
/// <summary>
/// Gets the address of the status in memory.
/// </summary>
public IntPtr Address { get; }
/// <summary>
/// Gets the status ID of this status.
/// </summary>
public uint StatusId => this.Struct->StatusID;
/// <summary>
/// Gets the GameData associated with this status.
/// </summary>
public Lumina.Excel.GeneratedSheets.Status GameData => new ExcelResolver<Lumina.Excel.GeneratedSheets.Status>(this.Struct->StatusID).GameData;
/// <summary>
/// Gets the parameter value of the status.
/// </summary>
public byte Param => this.Struct->Param;
/// <summary>
/// Gets the stack count of this status.
/// </summary>
public byte StackCount => this.Struct->StackCount;
/// <summary>
/// Gets the time remaining of this status.
/// </summary>
public float RemainingTime => this.Struct->RemainingTime;
/// <summary>
/// Gets the source ID of this status.
/// </summary>
public uint SourceID => this.Struct->SourceID;
/// <summary>
/// Gets the source actor associated with this status.
/// </summary>
/// <remarks>
/// This iterates the actor table, it should be used with care.
/// </remarks>
public GameObject? SourceObject => Service<ObjectTable>.Get().SearchById(this.SourceID);
private FFXIVClientStructs.FFXIV.Client.Game.Status* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Status*)this.Address;
} }
/// <summary>
/// Gets the address of the status in memory.
/// </summary>
public IntPtr Address { get; }
/// <summary>
/// Gets the status ID of this status.
/// </summary>
public uint StatusId => this.Struct->StatusID;
/// <summary>
/// Gets the GameData associated with this status.
/// </summary>
public Lumina.Excel.GeneratedSheets.Status GameData => new ExcelResolver<Lumina.Excel.GeneratedSheets.Status>(this.Struct->StatusID).GameData;
/// <summary>
/// Gets the parameter value of the status.
/// </summary>
public byte Param => this.Struct->Param;
/// <summary>
/// Gets the stack count of this status.
/// </summary>
public byte StackCount => this.Struct->StackCount;
/// <summary>
/// Gets the time remaining of this status.
/// </summary>
public float RemainingTime => this.Struct->RemainingTime;
/// <summary>
/// Gets the source ID of this status.
/// </summary>
public uint SourceID => this.Struct->SourceID;
/// <summary>
/// Gets the source actor associated with this status.
/// </summary>
/// <remarks>
/// This iterates the actor table, it should be used with care.
/// </remarks>
public GameObject? SourceObject => Service<ObjectTable>.Get().SearchById(this.SourceID);
private FFXIVClientStructs.FFXIV.Client.Game.Status* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Status*)this.Address;
} }

View file

@ -3,158 +3,159 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Dalamud.Game.ClientState.Statuses; namespace Dalamud.Game.ClientState.Statuses
/// <summary>
/// This collection represents the status effects an actor is afflicted by.
/// </summary>
public sealed unsafe partial class StatusList
{ {
private const int StatusListLength = 30;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StatusList"/> class. /// This collection represents the status effects an actor is afflicted by.
/// </summary> /// </summary>
/// <param name="address">Address of the status list.</param> public sealed unsafe partial class StatusList
internal StatusList(IntPtr address)
{ {
this.Address = address; private const int StatusListLength = 30;
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StatusList"/> class. /// Initializes a new instance of the <see cref="StatusList"/> class.
/// </summary> /// </summary>
/// <param name="pointer">Pointer to the status list.</param> /// <param name="address">Address of the status list.</param>
internal unsafe StatusList(void* pointer) internal StatusList(IntPtr address)
: this((IntPtr)pointer)
{
}
/// <summary>
/// Gets the address of the status list in memory.
/// </summary>
public IntPtr Address { get; }
/// <summary>
/// Gets the amount of status effect slots the actor has.
/// </summary>
public int Length => StatusListLength;
private static int StatusSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.Status>();
private FFXIVClientStructs.FFXIV.Client.Game.StatusManager* Struct => (FFXIVClientStructs.FFXIV.Client.Game.StatusManager*)this.Address;
/// <summary>
/// Get a status effect at the specified index.
/// </summary>
/// <param name="index">Status Index.</param>
/// <returns>The status at the specified index.</returns>
public Status? this[int index]
{
get
{ {
if (index < 0 || index > StatusListLength) this.Address = address;
}
/// <summary>
/// Initializes a new instance of the <see cref="StatusList"/> class.
/// </summary>
/// <param name="pointer">Pointer to the status list.</param>
internal unsafe StatusList(void* pointer)
: this((IntPtr)pointer)
{
}
/// <summary>
/// Gets the address of the status list in memory.
/// </summary>
public IntPtr Address { get; }
/// <summary>
/// Gets the amount of status effect slots the actor has.
/// </summary>
public int Length => StatusListLength;
private static int StatusSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.Status>();
private FFXIVClientStructs.FFXIV.Client.Game.StatusManager* Struct => (FFXIVClientStructs.FFXIV.Client.Game.StatusManager*)this.Address;
/// <summary>
/// Get a status effect at the specified index.
/// </summary>
/// <param name="index">Status Index.</param>
/// <returns>The status at the specified index.</returns>
public Status? this[int index]
{
get
{
if (index < 0 || index > StatusListLength)
return null;
var addr = this.GetStatusAddress(index);
return CreateStatusReference(addr);
}
}
/// <summary>
/// Create a reference to an FFXIV actor status list.
/// </summary>
/// <param name="address">The address of the status list in memory.</param>
/// <returns>The status object containing the requested data.</returns>
public static StatusList? CreateStatusListReference(IntPtr address)
{
// The use case for CreateStatusListReference and CreateStatusReference to be static is so
// fake status lists can be generated. Since they aren't exposed as services, it's either
// here or somewhere else.
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
return null; return null;
var addr = this.GetStatusAddress(index); if (address == IntPtr.Zero)
return CreateStatusReference(addr); return null;
return new StatusList(address);
}
/// <summary>
/// Create a reference to an FFXIV actor status.
/// </summary>
/// <param name="address">The address of the status effect in memory.</param>
/// <returns>The status object containing the requested data.</returns>
public static Status? CreateStatusReference(IntPtr address)
{
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
return null;
if (address == IntPtr.Zero)
return null;
return new Status(address);
}
/// <summary>
/// Gets the address of the party member at the specified index of the party list.
/// </summary>
/// <param name="index">The index of the party member.</param>
/// <returns>The memory address of the party member.</returns>
public IntPtr GetStatusAddress(int index)
{
if (index < 0 || index >= StatusListLength)
return IntPtr.Zero;
return (IntPtr)(this.Struct->Status + (index * StatusSize));
} }
} }
/// <summary> /// <summary>
/// Create a reference to an FFXIV actor status list. /// This collection represents the status effects an actor is afflicted by.
/// </summary> /// </summary>
/// <param name="address">The address of the status list in memory.</param> public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollection
/// <returns>The status object containing the requested data.</returns>
public static StatusList? CreateStatusListReference(IntPtr address)
{ {
// The use case for CreateStatusListReference and CreateStatusReference to be static is so /// <inheritdoc/>
// fake status lists can be generated. Since they aren't exposed as services, it's either int IReadOnlyCollection<Status>.Count => this.Length;
// here or somewhere else.
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0) /// <inheritdoc/>
return null; int ICollection.Count => this.Length;
if (address == IntPtr.Zero) /// <inheritdoc/>
return null; bool ICollection.IsSynchronized => false;
return new StatusList(address); /// <inheritdoc/>
} object ICollection.SyncRoot => this;
/// <summary> /// <inheritdoc/>
/// Create a reference to an FFXIV actor status. public IEnumerator<Status> GetEnumerator()
/// </summary>
/// <param name="address">The address of the status effect in memory.</param>
/// <returns>The status object containing the requested data.</returns>
public static Status? CreateStatusReference(IntPtr address)
{
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
return null;
if (address == IntPtr.Zero)
return null;
return new Status(address);
}
/// <summary>
/// Gets the address of the party member at the specified index of the party list.
/// </summary>
/// <param name="index">The index of the party member.</param>
/// <returns>The memory address of the party member.</returns>
public IntPtr GetStatusAddress(int index)
{
if (index < 0 || index >= StatusListLength)
return IntPtr.Zero;
return (IntPtr)(this.Struct->Status + (index * StatusSize));
}
}
/// <summary>
/// This collection represents the status effects an actor is afflicted by.
/// </summary>
public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollection
{
/// <inheritdoc/>
int IReadOnlyCollection<Status>.Count => this.Length;
/// <inheritdoc/>
int ICollection.Count => this.Length;
/// <inheritdoc/>
bool ICollection.IsSynchronized => false;
/// <inheritdoc/>
object ICollection.SyncRoot => this;
/// <inheritdoc/>
public IEnumerator<Status> GetEnumerator()
{
for (var i = 0; i < StatusListLength; i++)
{ {
var status = this[i]; for (var i = 0; i < StatusListLength; i++)
{
var status = this[i];
if (status == null || status.StatusId == 0) if (status == null || status.StatusId == 0)
continue; continue;
yield return status; yield return status;
}
} }
}
/// <inheritdoc/> /// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
/// <inheritdoc/> /// <inheritdoc/>
void ICollection.CopyTo(Array array, int index) void ICollection.CopyTo(Array array, int index)
{
for (var i = 0; i < this.Length; i++)
{ {
array.SetValue(this[i], index); for (var i = 0; i < this.Length; i++)
index++; {
array.SetValue(this[i], index);
index++;
}
} }
} }
} }

View file

@ -1,35 +1,36 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Dalamud.Game.ClientState.Structs; namespace Dalamud.Game.ClientState.Structs
/// <summary>
/// Native memory representation of a FFXIV status effect.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct StatusEffect
{ {
/// <summary> /// <summary>
/// The effect ID. /// Native memory representation of a FFXIV status effect.
/// </summary> /// </summary>
public short EffectId; [StructLayout(LayoutKind.Sequential)]
public struct StatusEffect
{
/// <summary>
/// The effect ID.
/// </summary>
public short EffectId;
/// <summary> /// <summary>
/// How many stacks are present. /// How many stacks are present.
/// </summary> /// </summary>
public byte StackCount; public byte StackCount;
/// <summary> /// <summary>
/// Additional parameters. /// Additional parameters.
/// </summary> /// </summary>
public byte Param; public byte Param;
/// <summary> /// <summary>
/// The duration remaining. /// The duration remaining.
/// </summary> /// </summary>
public float Duration; public float Duration;
/// <summary> /// <summary>
/// The ID of the actor that caused this effect. /// The ID of the actor that caused this effect.
/// </summary> /// </summary>
public int OwnerId; public int OwnerId;
}
} }

View file

@ -1,47 +1,48 @@
using System.Reflection; using System.Reflection;
namespace Dalamud.Game.Command; namespace Dalamud.Game.Command
/// <summary>
/// This class describes a registered command.
/// </summary>
public sealed class CommandInfo
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="CommandInfo"/> class. /// This class describes a registered command.
/// Create a new CommandInfo with the provided handler.
/// </summary> /// </summary>
/// <param name="handler">The method to call when the command is run.</param> public sealed class CommandInfo
public CommandInfo(HandlerDelegate handler)
{ {
this.Handler = handler; /// <summary>
this.LoaderAssemblyName = Assembly.GetCallingAssembly()?.GetName()?.Name; /// Initializes a new instance of the <see cref="CommandInfo"/> class.
/// Create a new CommandInfo with the provided handler.
/// </summary>
/// <param name="handler">The method to call when the command is run.</param>
public CommandInfo(HandlerDelegate handler)
{
this.Handler = handler;
this.LoaderAssemblyName = Assembly.GetCallingAssembly()?.GetName()?.Name;
}
/// <summary>
/// The function to be executed when the command is dispatched.
/// </summary>
/// <param name="command">The command itself.</param>
/// <param name="arguments">The arguments supplied to the command, ready for parsing.</param>
public delegate void HandlerDelegate(string command, string arguments);
/// <summary>
/// Gets a <see cref="HandlerDelegate"/> which will be called when the command is dispatched.
/// </summary>
public HandlerDelegate Handler { get; }
/// <summary>
/// Gets or sets the help message for this command.
/// </summary>
public string HelpMessage { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether if this command should be shown in the help output.
/// </summary>
public bool ShowInHelp { get; set; } = true;
/// <summary>
/// Gets or sets the name of the assembly responsible for this command.
/// </summary>
internal string LoaderAssemblyName { get; set; } = string.Empty;
} }
/// <summary>
/// The function to be executed when the command is dispatched.
/// </summary>
/// <param name="command">The command itself.</param>
/// <param name="arguments">The arguments supplied to the command, ready for parsing.</param>
public delegate void HandlerDelegate(string command, string arguments);
/// <summary>
/// Gets a <see cref="HandlerDelegate"/> which will be called when the command is dispatched.
/// </summary>
public HandlerDelegate Handler { get; }
/// <summary>
/// Gets or sets the help message for this command.
/// </summary>
public string HelpMessage { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether if this command should be shown in the help output.
/// </summary>
public bool ShowInHelp { get; set; } = true;
/// <summary>
/// Gets or sets the name of the assembly responsible for this command.
/// </summary>
internal string LoaderAssemblyName { get; set; } = string.Empty;
} }

View file

@ -10,166 +10,167 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Serilog; using Serilog;
namespace Dalamud.Game.Command; namespace Dalamud.Game.Command
/// <summary>
/// This class manages registered in-game slash commands.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed class CommandManager
{ {
private readonly Dictionary<string, CommandInfo> commandMap = new();
private readonly Regex commandRegexEn = new(@"^The command (?<command>.+) does not exist\.$", RegexOptions.Compiled);
private readonly Regex commandRegexJp = new(@"^そのコマンドはありません。: (?<command>.+)$", RegexOptions.Compiled);
private readonly Regex commandRegexDe = new(@"^„(?<command>.+)“ existiert nicht als Textkommando\.$", RegexOptions.Compiled);
private readonly Regex commandRegexFr = new(@"^La commande texte “(?<command>.+)” n'existe pas\.$", RegexOptions.Compiled);
private readonly Regex commandRegexCn = new(@"^“(?<command>.+)”(出现问题:该命令不存在|出現問題:該命令不存在)。$", RegexOptions.Compiled);
private readonly Regex currentLangCommandRegex;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="CommandManager"/> class. /// This class manages registered in-game slash commands.
/// </summary> /// </summary>
internal CommandManager() [PluginInterface]
[InterfaceVersion("1.0")]
public sealed class CommandManager
{ {
var startInfo = Service<DalamudStartInfo>.Get(); private readonly Dictionary<string, CommandInfo> commandMap = new();
private readonly Regex commandRegexEn = new(@"^The command (?<command>.+) does not exist\.$", RegexOptions.Compiled);
private readonly Regex commandRegexJp = new(@"^そのコマンドはありません。: (?<command>.+)$", RegexOptions.Compiled);
private readonly Regex commandRegexDe = new(@"^„(?<command>.+)“ existiert nicht als Textkommando\.$", RegexOptions.Compiled);
private readonly Regex commandRegexFr = new(@"^La commande texte “(?<command>.+)” n'existe pas\.$", RegexOptions.Compiled);
private readonly Regex commandRegexCn = new(@"^“(?<command>.+)”(出现问题:该命令不存在|出現問題:該命令不存在)。$", RegexOptions.Compiled);
private readonly Regex currentLangCommandRegex;
this.currentLangCommandRegex = startInfo.Language switch /// <summary>
/// Initializes a new instance of the <see cref="CommandManager"/> class.
/// </summary>
internal CommandManager()
{ {
ClientLanguage.Japanese => this.commandRegexJp, var startInfo = Service<DalamudStartInfo>.Get();
ClientLanguage.English => this.commandRegexEn,
ClientLanguage.German => this.commandRegexDe,
ClientLanguage.French => this.commandRegexFr,
_ => this.currentLangCommandRegex,
};
Service<ChatGui>.Get().CheckMessageHandled += this.OnCheckMessageHandled; this.currentLangCommandRegex = startInfo.Language switch
}
/// <summary>
/// Gets a read-only list of all registered commands.
/// </summary>
public ReadOnlyDictionary<string, CommandInfo> Commands => new(this.commandMap);
/// <summary>
/// Process a command in full.
/// </summary>
/// <param name="content">The full command string.</param>
/// <returns>True if the command was found and dispatched.</returns>
public bool ProcessCommand(string content)
{
string command;
string argument;
var separatorPosition = content.IndexOf(' ');
if (separatorPosition == -1 || separatorPosition + 1 >= content.Length)
{
// If no space was found or ends with the space. Process them as a no argument
if (separatorPosition + 1 >= content.Length)
{ {
// Remove the trailing space ClientLanguage.Japanese => this.commandRegexJp,
command = content.Substring(0, separatorPosition); ClientLanguage.English => this.commandRegexEn,
ClientLanguage.German => this.commandRegexDe,
ClientLanguage.French => this.commandRegexFr,
_ => this.currentLangCommandRegex,
};
Service<ChatGui>.Get().CheckMessageHandled += this.OnCheckMessageHandled;
}
/// <summary>
/// Gets a read-only list of all registered commands.
/// </summary>
public ReadOnlyDictionary<string, CommandInfo> Commands => new(this.commandMap);
/// <summary>
/// Process a command in full.
/// </summary>
/// <param name="content">The full command string.</param>
/// <returns>True if the command was found and dispatched.</returns>
public bool ProcessCommand(string content)
{
string command;
string argument;
var separatorPosition = content.IndexOf(' ');
if (separatorPosition == -1 || separatorPosition + 1 >= content.Length)
{
// If no space was found or ends with the space. Process them as a no argument
if (separatorPosition + 1 >= content.Length)
{
// Remove the trailing space
command = content.Substring(0, separatorPosition);
}
else
{
command = content;
}
argument = string.Empty;
} }
else else
{ {
command = content; // e.g.)
// /testcommand arg1
// => Total of 17 chars
// => command: 0-12 (12 chars)
// => argument: 13-17 (4 chars)
// => content.IndexOf(' ') == 12
command = content.Substring(0, separatorPosition);
var argStart = separatorPosition + 1;
argument = content[argStart..];
} }
argument = string.Empty; if (!this.commandMap.TryGetValue(command, out var handler)) // Commad was not found.
} return false;
else
{
// e.g.)
// /testcommand arg1
// => Total of 17 chars
// => command: 0-12 (12 chars)
// => argument: 13-17 (4 chars)
// => content.IndexOf(' ') == 12
command = content.Substring(0, separatorPosition);
var argStart = separatorPosition + 1; this.DispatchCommand(command, argument, handler);
argument = content[argStart..];
}
if (!this.commandMap.TryGetValue(command, out var handler)) // Commad was not found.
return false;
this.DispatchCommand(command, argument, handler);
return true;
}
/// <summary>
/// Dispatch the handling of a command.
/// </summary>
/// <param name="command">The command to dispatch.</param>
/// <param name="argument">The provided arguments.</param>
/// <param name="info">A <see cref="CommandInfo"/> object describing this command.</param>
public void DispatchCommand(string command, string argument, CommandInfo info)
{
try
{
info.Handler(command, argument);
}
catch (Exception ex)
{
Log.Error(ex, "Error while dispatching command {CommandName} (Argument: {Argument})", command, argument);
}
}
/// <summary>
/// Add a command handler, which you can use to add your own custom commands to the in-game chat.
/// </summary>
/// <param name="command">The command to register.</param>
/// <param name="info">A <see cref="CommandInfo"/> object describing the command.</param>
/// <returns>If adding was successful.</returns>
public bool AddHandler(string command, CommandInfo info)
{
if (info == null)
throw new ArgumentNullException(nameof(info), "Command handler is null.");
try
{
this.commandMap.Add(command, info);
return true; return true;
} }
catch (ArgumentException)
{
Log.Error("Command {CommandName} is already registered.", command);
return false;
}
}
/// <summary> /// <summary>
/// Remove a command from the command handlers. /// Dispatch the handling of a command.
/// </summary> /// </summary>
/// <param name="command">The command to remove.</param> /// <param name="command">The command to dispatch.</param>
/// <returns>If the removal was successful.</returns> /// <param name="argument">The provided arguments.</param>
public bool RemoveHandler(string command) /// <param name="info">A <see cref="CommandInfo"/> object describing this command.</param>
{ public void DispatchCommand(string command, string argument, CommandInfo info)
return this.commandMap.Remove(command);
}
private void OnCheckMessageHandled(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled)
{
if (type == XivChatType.ErrorMessage && senderId == 0)
{ {
var cmdMatch = this.currentLangCommandRegex.Match(message.TextValue).Groups["command"]; try
if (cmdMatch.Success)
{ {
// Yes, it's a chat command. info.Handler(command, argument);
var command = cmdMatch.Value;
if (this.ProcessCommand(command)) isHandled = true;
} }
else catch (Exception ex)
{ {
// Always match for china, since they patch in language files without changing the ClientLanguage. Log.Error(ex, "Error while dispatching command {CommandName} (Argument: {Argument})", command, argument);
cmdMatch = this.commandRegexCn.Match(message.TextValue).Groups["command"]; }
}
/// <summary>
/// Add a command handler, which you can use to add your own custom commands to the in-game chat.
/// </summary>
/// <param name="command">The command to register.</param>
/// <param name="info">A <see cref="CommandInfo"/> object describing the command.</param>
/// <returns>If adding was successful.</returns>
public bool AddHandler(string command, CommandInfo info)
{
if (info == null)
throw new ArgumentNullException(nameof(info), "Command handler is null.");
try
{
this.commandMap.Add(command, info);
return true;
}
catch (ArgumentException)
{
Log.Error("Command {CommandName} is already registered.", command);
return false;
}
}
/// <summary>
/// Remove a command from the command handlers.
/// </summary>
/// <param name="command">The command to remove.</param>
/// <returns>If the removal was successful.</returns>
public bool RemoveHandler(string command)
{
return this.commandMap.Remove(command);
}
private void OnCheckMessageHandled(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled)
{
if (type == XivChatType.ErrorMessage && senderId == 0)
{
var cmdMatch = this.currentLangCommandRegex.Match(message.TextValue).Groups["command"];
if (cmdMatch.Success) if (cmdMatch.Success)
{ {
// Yes, it's a Chinese fallback chat command. // Yes, it's a chat command.
var command = cmdMatch.Value; var command = cmdMatch.Value;
if (this.ProcessCommand(command)) isHandled = true; if (this.ProcessCommand(command)) isHandled = true;
} }
else
{
// Always match for china, since they patch in language files without changing the ClientLanguage.
cmdMatch = this.commandRegexCn.Match(message.TextValue).Groups["command"];
if (cmdMatch.Success)
{
// Yes, it's a Chinese fallback chat command.
var command = cmdMatch.Value;
if (this.ProcessCommand(command)) isHandled = true;
}
}
} }
} }
} }

View file

@ -16,300 +16,301 @@ using Dalamud.IoC.Internal;
using Dalamud.Utility; using Dalamud.Utility;
using Serilog; using Serilog;
namespace Dalamud.Game; namespace Dalamud.Game
/// <summary>
/// This class represents the Framework of the native game client and grants access to various subsystems.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed class Framework : IDisposable
{ {
private static Stopwatch statsStopwatch = new();
private Stopwatch updateStopwatch = new();
private bool tier2Initialized = false;
private bool tier3Initialized = false;
private bool tierInitError = false;
private Hook<OnUpdateDetour> updateHook;
private Hook<OnDestroyDetour> destroyHook;
private Hook<OnRealDestroyDelegate> realDestroyHook;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Framework"/> class. /// This class represents the Framework of the native game client and grants access to various subsystems.
/// </summary> /// </summary>
internal Framework() [PluginInterface]
[InterfaceVersion("1.0")]
public sealed class Framework : IDisposable
{ {
this.Address = new FrameworkAddressResolver(); private static Stopwatch statsStopwatch = new();
this.Address.Setup(); private Stopwatch updateStopwatch = new();
Log.Verbose($"Framework address 0x{this.Address.BaseAddress.ToInt64():X}"); private bool tier2Initialized = false;
if (this.Address.BaseAddress == IntPtr.Zero) private bool tier3Initialized = false;
private bool tierInitError = false;
private Hook<OnUpdateDetour> updateHook;
private Hook<OnDestroyDetour> destroyHook;
private Hook<OnRealDestroyDelegate> realDestroyHook;
/// <summary>
/// Initializes a new instance of the <see cref="Framework"/> class.
/// </summary>
internal Framework()
{ {
throw new InvalidOperationException("Framework is not initalized yet."); this.Address = new FrameworkAddressResolver();
this.Address.Setup();
Log.Verbose($"Framework address 0x{this.Address.BaseAddress.ToInt64():X}");
if (this.Address.BaseAddress == IntPtr.Zero)
{
throw new InvalidOperationException("Framework is not initalized yet.");
}
// Hook virtual functions
this.HookVTable();
} }
// Hook virtual functions /// <summary>
this.HookVTable(); /// A delegate type used with the <see cref="Update"/> event.
} /// </summary>
/// <param name="framework">The Framework instance.</param>
public delegate void OnUpdateDelegate(Framework framework);
/// <summary> /// <summary>
/// A delegate type used with the <see cref="Update"/> event. /// A delegate type used during the native Framework::destroy.
/// </summary> /// </summary>
/// <param name="framework">The Framework instance.</param> /// <param name="framework">The native Framework address.</param>
public delegate void OnUpdateDelegate(Framework framework); /// <returns>A value indicating if the call was successful.</returns>
public delegate bool OnRealDestroyDelegate(IntPtr framework);
/// <summary> /// <summary>
/// A delegate type used during the native Framework::destroy. /// A delegate type used during the native Framework::free.
/// </summary> /// </summary>
/// <param name="framework">The native Framework address.</param> /// <returns>The native Framework address.</returns>
/// <returns>A value indicating if the call was successful.</returns> public delegate IntPtr OnDestroyDelegate();
public delegate bool OnRealDestroyDelegate(IntPtr framework);
/// <summary> [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
/// A delegate type used during the native Framework::free. private delegate bool OnUpdateDetour(IntPtr framework);
/// </summary>
/// <returns>The native Framework address.</returns>
public delegate IntPtr OnDestroyDelegate();
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr OnDestroyDetour(); // OnDestroyDelegate
private delegate bool OnUpdateDetour(IntPtr framework);
private delegate IntPtr OnDestroyDetour(); // OnDestroyDelegate /// <summary>
/// Event that gets fired every time the game framework updates.
/// </summary>
public event OnUpdateDelegate Update;
/// <summary> /// <summary>
/// Event that gets fired every time the game framework updates. /// Gets or sets a value indicating whether the collection of stats is enabled.
/// </summary> /// </summary>
public event OnUpdateDelegate Update; public static bool StatsEnabled { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the collection of stats is enabled. /// Gets the stats history mapping.
/// </summary> /// </summary>
public static bool StatsEnabled { get; set; } public static Dictionary<string, List<double>> StatsHistory { get; } = new();
/// <summary> /// <summary>
/// Gets the stats history mapping. /// Gets a raw pointer to the instance of Client::Framework.
/// </summary> /// </summary>
public static Dictionary<string, List<double>> StatsHistory { get; } = new(); public FrameworkAddressResolver Address { get; }
/// <summary> /// <summary>
/// Gets a raw pointer to the instance of Client::Framework. /// Gets the last time that the Framework Update event was triggered.
/// </summary> /// </summary>
public FrameworkAddressResolver Address { get; } public DateTime LastUpdate { get; private set; } = DateTime.MinValue;
/// <summary> /// <summary>
/// Gets the last time that the Framework Update event was triggered. /// Gets the last time in UTC that the Framework Update event was triggered.
/// </summary> /// </summary>
public DateTime LastUpdate { get; private set; } = DateTime.MinValue; public DateTime LastUpdateUTC { get; private set; } = DateTime.MinValue;
/// <summary> /// <summary>
/// Gets the last time in UTC that the Framework Update event was triggered. /// Gets the delta between the last Framework Update and the currently executing one.
/// </summary> /// </summary>
public DateTime LastUpdateUTC { get; private set; } = DateTime.MinValue; public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero;
/// <summary> /// <summary>
/// Gets the delta between the last Framework Update and the currently executing one. /// Gets or sets a value indicating whether to dispatch update events.
/// </summary> /// </summary>
public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero; internal bool DispatchUpdateEvents { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether to dispatch update events. /// Enable this module.
/// </summary> /// </summary>
internal bool DispatchUpdateEvents { get; set; } = true; public void Enable()
/// <summary>
/// Enable this module.
/// </summary>
public void Enable()
{
Service<LibcFunction>.Set();
Service<GameGui>.Get().Enable();
Service<GameNetwork>.Get().Enable();
this.updateHook.Enable();
this.destroyHook.Enable();
this.realDestroyHook.Enable();
}
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
Service<GameGui>.GetNullable()?.Dispose();
Service<GameNetwork>.GetNullable()?.Dispose();
this.updateHook?.Disable();
this.destroyHook?.Disable();
this.realDestroyHook?.Disable();
Thread.Sleep(500);
this.updateHook?.Dispose();
this.destroyHook?.Dispose();
this.realDestroyHook?.Dispose();
this.updateStopwatch.Reset();
statsStopwatch.Reset();
}
private void HookVTable()
{
var vtable = Marshal.ReadIntPtr(this.Address.BaseAddress);
// Virtual function layout:
// .rdata:00000001411F1FE0 dq offset Xiv__Framework___dtor
// .rdata:00000001411F1FE8 dq offset Xiv__Framework__init
// .rdata:00000001411F1FF0 dq offset Xiv__Framework__destroy
// .rdata:00000001411F1FF8 dq offset Xiv__Framework__free
// .rdata:00000001411F2000 dq offset Xiv__Framework__update
var pUpdate = Marshal.ReadIntPtr(vtable, IntPtr.Size * 4);
this.updateHook = new Hook<OnUpdateDetour>(pUpdate, this.HandleFrameworkUpdate);
var pDestroy = Marshal.ReadIntPtr(vtable, IntPtr.Size * 3);
this.destroyHook = new Hook<OnDestroyDetour>(pDestroy, this.HandleFrameworkDestroy);
var pRealDestroy = Marshal.ReadIntPtr(vtable, IntPtr.Size * 2);
this.realDestroyHook = new Hook<OnRealDestroyDelegate>(pRealDestroy, this.HandleRealDestroy);
}
private bool HandleFrameworkUpdate(IntPtr framework)
{
// If any of the tier loads failed, just go to the original code.
if (this.tierInitError)
goto original;
var dalamud = Service<Dalamud>.Get();
// If this is the first time we are running this loop, we need to init Dalamud subsystems synchronously
if (!this.tier2Initialized)
{ {
this.tier2Initialized = dalamud.LoadTier2(); Service<LibcFunction>.Set();
Service<GameGui>.Get().Enable();
Service<GameNetwork>.Get().Enable();
this.updateHook.Enable();
this.destroyHook.Enable();
this.realDestroyHook.Enable();
}
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
Service<GameGui>.GetNullable()?.Dispose();
Service<GameNetwork>.GetNullable()?.Dispose();
this.updateHook?.Disable();
this.destroyHook?.Disable();
this.realDestroyHook?.Disable();
Thread.Sleep(500);
this.updateHook?.Dispose();
this.destroyHook?.Dispose();
this.realDestroyHook?.Dispose();
this.updateStopwatch.Reset();
statsStopwatch.Reset();
}
private void HookVTable()
{
var vtable = Marshal.ReadIntPtr(this.Address.BaseAddress);
// Virtual function layout:
// .rdata:00000001411F1FE0 dq offset Xiv__Framework___dtor
// .rdata:00000001411F1FE8 dq offset Xiv__Framework__init
// .rdata:00000001411F1FF0 dq offset Xiv__Framework__destroy
// .rdata:00000001411F1FF8 dq offset Xiv__Framework__free
// .rdata:00000001411F2000 dq offset Xiv__Framework__update
var pUpdate = Marshal.ReadIntPtr(vtable, IntPtr.Size * 4);
this.updateHook = new Hook<OnUpdateDetour>(pUpdate, this.HandleFrameworkUpdate);
var pDestroy = Marshal.ReadIntPtr(vtable, IntPtr.Size * 3);
this.destroyHook = new Hook<OnDestroyDetour>(pDestroy, this.HandleFrameworkDestroy);
var pRealDestroy = Marshal.ReadIntPtr(vtable, IntPtr.Size * 2);
this.realDestroyHook = new Hook<OnRealDestroyDelegate>(pRealDestroy, this.HandleRealDestroy);
}
private bool HandleFrameworkUpdate(IntPtr framework)
{
// If any of the tier loads failed, just go to the original code.
if (this.tierInitError)
goto original;
var dalamud = Service<Dalamud>.Get();
// If this is the first time we are running this loop, we need to init Dalamud subsystems synchronously
if (!this.tier2Initialized) if (!this.tier2Initialized)
this.tierInitError = true; {
this.tier2Initialized = dalamud.LoadTier2();
if (!this.tier2Initialized)
this.tierInitError = true;
goto original; goto original;
} }
// Plugins expect the interface to be available and ready, so we need to wait with plugins until we have init'd ImGui // Plugins expect the interface to be available and ready, so we need to wait with plugins until we have init'd ImGui
if (!this.tier3Initialized && Service<InterfaceManager>.GetNullable()?.IsReady == true) if (!this.tier3Initialized && Service<InterfaceManager>.GetNullable()?.IsReady == true)
{ {
this.tier3Initialized = dalamud.LoadTier3(); this.tier3Initialized = dalamud.LoadTier3();
if (!this.tier3Initialized) if (!this.tier3Initialized)
this.tierInitError = true; this.tierInitError = true;
goto original; goto original;
} }
try
{
Service<ChatGui>.Get().UpdateQueue();
Service<ToastGui>.Get().UpdateQueue();
Service<GameNetwork>.Get().UpdateQueue();
}
catch (Exception ex)
{
Log.Error(ex, "Exception while handling Framework::Update hook.");
}
if (this.DispatchUpdateEvents)
{
this.updateStopwatch.Stop();
this.UpdateDelta = TimeSpan.FromMilliseconds(this.updateStopwatch.ElapsedMilliseconds);
this.updateStopwatch.Restart();
this.LastUpdate = DateTime.Now;
this.LastUpdateUTC = DateTime.UtcNow;
try try
{ {
if (StatsEnabled && this.Update != null) Service<ChatGui>.Get().UpdateQueue();
{ Service<ToastGui>.Get().UpdateQueue();
// Stat Tracking for Framework Updates Service<GameNetwork>.Get().UpdateQueue();
var invokeList = this.Update.GetInvocationList();
var notUpdated = StatsHistory.Keys.ToList();
// Individually invoke OnUpdate handlers and time them.
foreach (var d in invokeList)
{
statsStopwatch.Restart();
d.Method.Invoke(d.Target, new object[] { this });
statsStopwatch.Stop();
var key = $"{d.Target}::{d.Method.Name}";
if (notUpdated.Contains(key))
notUpdated.Remove(key);
if (!StatsHistory.ContainsKey(key))
StatsHistory.Add(key, new List<double>());
StatsHistory[key].Add(statsStopwatch.Elapsed.TotalMilliseconds);
if (StatsHistory[key].Count > 1000)
{
StatsHistory[key].RemoveRange(0, StatsHistory[key].Count - 1000);
}
}
// Cleanup handlers that are no longer being called
foreach (var key in notUpdated)
{
if (StatsHistory[key].Count > 0)
{
StatsHistory[key].RemoveAt(0);
}
else
{
StatsHistory.Remove(key);
}
}
}
else
{
this.Update?.Invoke(this);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Error(ex, "Exception while dispatching Framework::Update event."); Log.Error(ex, "Exception while handling Framework::Update hook.");
} }
if (this.DispatchUpdateEvents)
{
this.updateStopwatch.Stop();
this.UpdateDelta = TimeSpan.FromMilliseconds(this.updateStopwatch.ElapsedMilliseconds);
this.updateStopwatch.Restart();
this.LastUpdate = DateTime.Now;
this.LastUpdateUTC = DateTime.UtcNow;
try
{
if (StatsEnabled && this.Update != null)
{
// Stat Tracking for Framework Updates
var invokeList = this.Update.GetInvocationList();
var notUpdated = StatsHistory.Keys.ToList();
// Individually invoke OnUpdate handlers and time them.
foreach (var d in invokeList)
{
statsStopwatch.Restart();
d.Method.Invoke(d.Target, new object[] { this });
statsStopwatch.Stop();
var key = $"{d.Target}::{d.Method.Name}";
if (notUpdated.Contains(key))
notUpdated.Remove(key);
if (!StatsHistory.ContainsKey(key))
StatsHistory.Add(key, new List<double>());
StatsHistory[key].Add(statsStopwatch.Elapsed.TotalMilliseconds);
if (StatsHistory[key].Count > 1000)
{
StatsHistory[key].RemoveRange(0, StatsHistory[key].Count - 1000);
}
}
// Cleanup handlers that are no longer being called
foreach (var key in notUpdated)
{
if (StatsHistory[key].Count > 0)
{
StatsHistory[key].RemoveAt(0);
}
else
{
StatsHistory.Remove(key);
}
}
}
else
{
this.Update?.Invoke(this);
}
}
catch (Exception ex)
{
Log.Error(ex, "Exception while dispatching Framework::Update event.");
}
}
original:
return this.updateHook.Original(framework);
} }
original: private bool HandleRealDestroy(IntPtr framework)
return this.updateHook.Original(framework);
}
private bool HandleRealDestroy(IntPtr framework)
{
if (this.DispatchUpdateEvents)
{ {
Log.Information("Framework::Destroy!"); if (this.DispatchUpdateEvents)
{
Log.Information("Framework::Destroy!");
var dalamud = Service<Dalamud>.Get();
dalamud.DisposePlugins();
Log.Information("Framework::Destroy OK!");
}
this.DispatchUpdateEvents = false;
return this.realDestroyHook.Original(framework);
}
private IntPtr HandleFrameworkDestroy()
{
Log.Information("Framework::Free!");
// Store the pointer to the original trampoline location
var originalPtr = Marshal.GetFunctionPointerForDelegate(this.destroyHook.Original);
var dalamud = Service<Dalamud>.Get(); var dalamud = Service<Dalamud>.Get();
dalamud.DisposePlugins(); dalamud.Unload();
dalamud.WaitForUnloadFinish();
Log.Information("Framework::Destroy OK!"); Log.Information("Framework::Free OK!");
// Return the original trampoline location to cleanly exit
return originalPtr;
} }
this.DispatchUpdateEvents = false;
return this.realDestroyHook.Original(framework);
}
private IntPtr HandleFrameworkDestroy()
{
Log.Information("Framework::Free!");
// Store the pointer to the original trampoline location
var originalPtr = Marshal.GetFunctionPointerForDelegate(this.destroyHook.Original);
var dalamud = Service<Dalamud>.Get();
dalamud.Unload();
dalamud.WaitForUnloadFinish();
Log.Information("Framework::Free OK!");
// Return the original trampoline location to cleanly exit
return originalPtr;
} }
} }

View file

@ -1,54 +1,57 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Dalamud.Game; using Dalamud.Game.Internal;
/// <summary> namespace Dalamud.Game
/// The address resolver for the <see cref="Framework"/> class.
/// </summary>
public sealed class FrameworkAddressResolver : BaseAddressResolver
{ {
/// <summary> /// <summary>
/// Gets the base address native Framework class. /// The address resolver for the <see cref="Framework"/> class.
/// </summary> /// </summary>
public IntPtr BaseAddress { get; private set; } public sealed class FrameworkAddressResolver : BaseAddressResolver
/// <summary>
/// Gets the address for the native GuiManager class.
/// </summary>
public IntPtr GuiManager { get; private set; }
/// <summary>
/// Gets the address for the native ScriptManager class.
/// </summary>
public IntPtr ScriptManager { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{ {
this.SetupFramework(sig); /// <summary>
/// Gets the base address native Framework class.
/// </summary>
public IntPtr BaseAddress { get; private set; }
// Xiv__Framework__GetGuiManager+8 000 mov rax, [rcx+2C00h] /// <summary>
// Xiv__Framework__GetGuiManager+F 000 retn /// Gets the address for the native GuiManager class.
this.GuiManager = Marshal.ReadIntPtr(this.BaseAddress, 0x2C08); /// </summary>
public IntPtr GuiManager { get; private set; }
// Called from Framework::Init /// <summary>
this.ScriptManager = this.BaseAddress + 0x2C68; // note that no deref here /// Gets the address for the native ScriptManager class.
} /// </summary>
public IntPtr ScriptManager { get; private set; }
private void SetupFramework(SigScanner scanner) /// <inheritdoc/>
{ protected override void Setup64Bit(SigScanner sig)
// Dissasembly of part of the .dtor {
// 00007FF701AD665A | 48 C7 05 ?? ?? ?? ?? 00 00 00 00 | MOV QWORD PTR DS:[g_mainFramework],0 this.SetupFramework(sig);
// 00007FF701AD6665 | E8 ?? ?? ?? ?? | CALL ffxiv_dx11.7FF701E27130
// 00007FF701AD666A | 48 8D ?? ?? ?? 00 00 | LEA RCX,QWORD PTR DS:[RBX + 2C38]
// 00007FF701AD6671 | E8 ?? ?? ?? ?? | CALL ffxiv_dx11.7FF701E2A7D0
// 00007FF701AD6676 | 48 8D ?? ?? ?? ?? ?? | LEA RAX,QWORD PTR DS:[7FF702C31F80
var fwDtor = scanner.ScanText("48 C7 05 ?? ?? ?? ?? 00 00 00 00 E8 ?? ?? ?? ?? 48 8D ?? ?? ?? 00 00 E8 ?? ?? ?? ?? 48 8D");
var fwOffset = Marshal.ReadInt32(fwDtor + 3);
var pFramework = scanner.ResolveRelativeAddress(fwDtor + 11, fwOffset);
// Framework does not change once initialized in startup so don't bother to deref again and again. // Xiv__Framework__GetGuiManager+8 000 mov rax, [rcx+2C00h]
this.BaseAddress = Marshal.ReadIntPtr(pFramework); // Xiv__Framework__GetGuiManager+F 000 retn
this.GuiManager = Marshal.ReadIntPtr(this.BaseAddress, 0x2C08);
// Called from Framework::Init
this.ScriptManager = this.BaseAddress + 0x2C68; // note that no deref here
}
private void SetupFramework(SigScanner scanner)
{
// Dissasembly of part of the .dtor
// 00007FF701AD665A | 48 C7 05 ?? ?? ?? ?? 00 00 00 00 | MOV QWORD PTR DS:[g_mainFramework],0
// 00007FF701AD6665 | E8 ?? ?? ?? ?? | CALL ffxiv_dx11.7FF701E27130
// 00007FF701AD666A | 48 8D ?? ?? ?? 00 00 | LEA RCX,QWORD PTR DS:[RBX + 2C38]
// 00007FF701AD6671 | E8 ?? ?? ?? ?? | CALL ffxiv_dx11.7FF701E2A7D0
// 00007FF701AD6676 | 48 8D ?? ?? ?? ?? ?? | LEA RAX,QWORD PTR DS:[7FF702C31F80
var fwDtor = scanner.ScanText("48 C7 05 ?? ?? ?? ?? 00 00 00 00 E8 ?? ?? ?? ?? 48 8D ?? ?? ?? 00 00 E8 ?? ?? ?? ?? 48 8D");
var fwOffset = Marshal.ReadInt32(fwDtor + 3);
var pFramework = scanner.ResolveRelativeAddress(fwDtor + 11, fwOffset);
// Framework does not change once initialized in startup so don't bother to deref again and again.
this.BaseAddress = Marshal.ReadIntPtr(pFramework);
}
} }
} }

View file

@ -5,404 +5,405 @@ using System.Text;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud.Game; namespace Dalamud.Game
/// <summary>
/// A GameVersion object contains give hierarchical numeric components: year, month,
/// day, major and minor. All components may be unspecified, which is represented
/// internally as a -1. By definition, an unspecified component matches anything
/// (both unspecified and specified), and an unspecified component is "less than" any
/// specified component. It will also equal the string "any" if all components are
/// unspecified. The value can be retrieved from the ffxivgame.ver file in your game
/// installation directory.
/// </summary>
[Serializable]
public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersion>, IEquatable<GameVersion>
{ {
private static readonly GameVersion AnyVersion = new();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GameVersion"/> class. /// A GameVersion object contains give hierarchical numeric components: year, month,
/// day, major and minor. All components may be unspecified, which is represented
/// internally as a -1. By definition, an unspecified component matches anything
/// (both unspecified and specified), and an unspecified component is "less than" any
/// specified component. It will also equal the string "any" if all components are
/// unspecified. The value can be retrieved from the ffxivgame.ver file in your game
/// installation directory.
/// </summary> /// </summary>
/// <param name="version">Version string to parse.</param> [Serializable]
[JsonConstructor] public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersion>, IEquatable<GameVersion>
public GameVersion(string version)
{ {
var ver = Parse(version); private static readonly GameVersion AnyVersion = new();
this.Year = ver.Year;
this.Month = ver.Month;
this.Day = ver.Day;
this.Major = ver.Major;
this.Minor = ver.Minor;
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GameVersion"/> class. /// Initializes a new instance of the <see cref="GameVersion"/> class.
/// </summary> /// </summary>
/// <param name="year">The year.</param> /// <param name="version">Version string to parse.</param>
/// <param name="month">The month.</param> [JsonConstructor]
/// <param name="day">The day.</param> public GameVersion(string version)
/// <param name="major">The major version.</param>
/// <param name="minor">The minor version.</param>
public GameVersion(int year, int month, int day, int major, int minor)
{
if ((this.Year = year) < 0)
throw new ArgumentOutOfRangeException(nameof(year));
if ((this.Month = month) < 0)
throw new ArgumentOutOfRangeException(nameof(month));
if ((this.Day = day) < 0)
throw new ArgumentOutOfRangeException(nameof(day));
if ((this.Major = major) < 0)
throw new ArgumentOutOfRangeException(nameof(major));
if ((this.Minor = minor) < 0)
throw new ArgumentOutOfRangeException(nameof(minor));
}
/// <summary>
/// Initializes a new instance of the <see cref="GameVersion"/> class.
/// </summary>
/// <param name="year">The year.</param>
/// <param name="month">The month.</param>
/// <param name="day">The day.</param>
/// <param name="major">The major version.</param>
public GameVersion(int year, int month, int day, int major)
{
if ((this.Year = year) < 0)
throw new ArgumentOutOfRangeException(nameof(year));
if ((this.Month = month) < 0)
throw new ArgumentOutOfRangeException(nameof(month));
if ((this.Day = day) < 0)
throw new ArgumentOutOfRangeException(nameof(day));
if ((this.Major = major) < 0)
throw new ArgumentOutOfRangeException(nameof(major));
}
/// <summary>
/// Initializes a new instance of the <see cref="GameVersion"/> class.
/// </summary>
/// <param name="year">The year.</param>
/// <param name="month">The month.</param>
/// <param name="day">The day.</param>
public GameVersion(int year, int month, int day)
{
if ((this.Year = year) < 0)
throw new ArgumentOutOfRangeException(nameof(year));
if ((this.Month = month) < 0)
throw new ArgumentOutOfRangeException(nameof(month));
if ((this.Day = day) < 0)
throw new ArgumentOutOfRangeException(nameof(day));
}
/// <summary>
/// Initializes a new instance of the <see cref="GameVersion"/> class.
/// </summary>
/// <param name="year">The year.</param>
/// <param name="month">The month.</param>
public GameVersion(int year, int month)
{
if ((this.Year = year) < 0)
throw new ArgumentOutOfRangeException(nameof(year));
if ((this.Month = month) < 0)
throw new ArgumentOutOfRangeException(nameof(month));
}
/// <summary>
/// Initializes a new instance of the <see cref="GameVersion"/> class.
/// </summary>
/// <param name="year">The year.</param>
public GameVersion(int year)
{
if ((this.Year = year) < 0)
throw new ArgumentOutOfRangeException(nameof(year));
}
/// <summary>
/// Initializes a new instance of the <see cref="GameVersion"/> class.
/// </summary>
public GameVersion()
{
}
/// <summary>
/// Gets the default "any" game version.
/// </summary>
public static GameVersion Any => AnyVersion;
/// <summary>
/// Gets the year component.
/// </summary>
public int Year { get; } = -1;
/// <summary>
/// Gets the month component.
/// </summary>
public int Month { get; } = -1;
/// <summary>
/// Gets the day component.
/// </summary>
public int Day { get; } = -1;
/// <summary>
/// Gets the major version component.
/// </summary>
public int Major { get; } = -1;
/// <summary>
/// Gets the minor version component.
/// </summary>
public int Minor { get; } = -1;
public static implicit operator GameVersion(string ver)
{
return Parse(ver);
}
public static bool operator ==(GameVersion v1, GameVersion v2)
{
if (v1 is null)
{ {
return v2 is null; var ver = Parse(version);
this.Year = ver.Year;
this.Month = ver.Month;
this.Day = ver.Day;
this.Major = ver.Major;
this.Minor = ver.Minor;
} }
return v1.Equals(v2); /// <summary>
} /// Initializes a new instance of the <see cref="GameVersion"/> class.
/// </summary>
public static bool operator !=(GameVersion v1, GameVersion v2) /// <param name="year">The year.</param>
{ /// <param name="month">The month.</param>
return !(v1 == v2); /// <param name="day">The day.</param>
} /// <param name="major">The major version.</param>
/// <param name="minor">The minor version.</param>
public static bool operator <(GameVersion v1, GameVersion v2) public GameVersion(int year, int month, int day, int major, int minor)
{
if (v1 is null)
throw new ArgumentNullException(nameof(v1));
return v1.CompareTo(v2) < 0;
}
public static bool operator <=(GameVersion v1, GameVersion v2)
{
if (v1 is null)
throw new ArgumentNullException(nameof(v1));
return v1.CompareTo(v2) <= 0;
}
public static bool operator >(GameVersion v1, GameVersion v2)
{
return v2 < v1;
}
public static bool operator >=(GameVersion v1, GameVersion v2)
{
return v2 <= v1;
}
public static GameVersion operator +(GameVersion v1, TimeSpan v2)
{
if (v1 == null)
throw new ArgumentNullException(nameof(v1));
if (v1.Year == -1 || v1.Month == -1 || v1.Day == -1)
return v1;
var date = new DateTime(v1.Year, v1.Month, v1.Day) + v2;
return new GameVersion(date.Year, date.Month, date.Day, v1.Major, v1.Minor);
}
public static GameVersion operator -(GameVersion v1, TimeSpan v2)
{
if (v1 == null)
throw new ArgumentNullException(nameof(v1));
if (v1.Year == -1 || v1.Month == -1 || v1.Day == -1)
return v1;
var date = new DateTime(v1.Year, v1.Month, v1.Day) - v2;
return new GameVersion(date.Year, date.Month, date.Day, v1.Major, v1.Minor);
}
/// <summary>
/// Parse a version string. YYYY.MM.DD.majr.minr or "any".
/// </summary>
/// <param name="input">Input to parse.</param>
/// <returns>GameVersion object.</returns>
public static GameVersion Parse(string input)
{
if (input == null)
throw new ArgumentNullException(nameof(input));
if (input.ToLower(CultureInfo.InvariantCulture) == "any")
return new GameVersion();
var parts = input.Split('.');
var tplParts = parts.Select(p =>
{ {
var result = int.TryParse(p, out var value); if ((this.Year = year) < 0)
return (result, value); throw new ArgumentOutOfRangeException(nameof(year));
}).ToArray();
if (tplParts.Any(t => !t.result)) if ((this.Month = month) < 0)
throw new FormatException("Bad formatting"); throw new ArgumentOutOfRangeException(nameof(month));
var intParts = tplParts.Select(t => t.value).ToArray(); if ((this.Day = day) < 0)
var len = intParts.Length; throw new ArgumentOutOfRangeException(nameof(day));
if (len == 1) if ((this.Major = major) < 0)
return new GameVersion(intParts[0]); throw new ArgumentOutOfRangeException(nameof(major));
else if (len == 2)
return new GameVersion(intParts[0], intParts[1]);
else if (len == 3)
return new GameVersion(intParts[0], intParts[1], intParts[2]);
else if (len == 4)
return new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3]);
else if (len == 5)
return new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3], intParts[4]);
else
throw new ArgumentException("Too many parts");
}
/// <summary> if ((this.Minor = minor) < 0)
/// Try to parse a version string. YYYY.MM.DD.majr.minr or "any". throw new ArgumentOutOfRangeException(nameof(minor));
/// </summary>
/// <param name="input">Input to parse.</param>
/// <param name="result">GameVersion object.</param>
/// <returns>Success or failure.</returns>
public static bool TryParse(string input, out GameVersion result)
{
try
{
result = Parse(input);
return true;
} }
catch
/// <summary>
/// Initializes a new instance of the <see cref="GameVersion"/> class.
/// </summary>
/// <param name="year">The year.</param>
/// <param name="month">The month.</param>
/// <param name="day">The day.</param>
/// <param name="major">The major version.</param>
public GameVersion(int year, int month, int day, int major)
{ {
result = null; if ((this.Year = year) < 0)
return false; throw new ArgumentOutOfRangeException(nameof(year));
if ((this.Month = month) < 0)
throw new ArgumentOutOfRangeException(nameof(month));
if ((this.Day = day) < 0)
throw new ArgumentOutOfRangeException(nameof(day));
if ((this.Major = major) < 0)
throw new ArgumentOutOfRangeException(nameof(major));
} }
}
/// <inheritdoc/> /// <summary>
public object Clone() => new GameVersion(this.Year, this.Month, this.Day, this.Major, this.Minor); /// Initializes a new instance of the <see cref="GameVersion"/> class.
/// </summary>
/// <inheritdoc/> /// <param name="year">The year.</param>
public int CompareTo(object obj) /// <param name="month">The month.</param>
{ /// <param name="day">The day.</param>
if (obj == null) public GameVersion(int year, int month, int day)
return 1;
if (obj is GameVersion value)
{ {
return this.CompareTo(value); if ((this.Year = year) < 0)
throw new ArgumentOutOfRangeException(nameof(year));
if ((this.Month = month) < 0)
throw new ArgumentOutOfRangeException(nameof(month));
if ((this.Day = day) < 0)
throw new ArgumentOutOfRangeException(nameof(day));
} }
else
/// <summary>
/// Initializes a new instance of the <see cref="GameVersion"/> class.
/// </summary>
/// <param name="year">The year.</param>
/// <param name="month">The month.</param>
public GameVersion(int year, int month)
{ {
throw new ArgumentException("Argument must be a GameVersion"); if ((this.Year = year) < 0)
throw new ArgumentOutOfRangeException(nameof(year));
if ((this.Month = month) < 0)
throw new ArgumentOutOfRangeException(nameof(month));
} }
}
/// <inheritdoc/> /// <summary>
public int CompareTo(GameVersion value) /// Initializes a new instance of the <see cref="GameVersion"/> class.
{ /// </summary>
if (value == null) /// <param name="year">The year.</param>
return 1; public GameVersion(int year)
{
if ((this.Year = year) < 0)
throw new ArgumentOutOfRangeException(nameof(year));
}
/// <summary>
/// Initializes a new instance of the <see cref="GameVersion"/> class.
/// </summary>
public GameVersion()
{
}
/// <summary>
/// Gets the default "any" game version.
/// </summary>
public static GameVersion Any => AnyVersion;
/// <summary>
/// Gets the year component.
/// </summary>
public int Year { get; } = -1;
/// <summary>
/// Gets the month component.
/// </summary>
public int Month { get; } = -1;
/// <summary>
/// Gets the day component.
/// </summary>
public int Day { get; } = -1;
/// <summary>
/// Gets the major version component.
/// </summary>
public int Major { get; } = -1;
/// <summary>
/// Gets the minor version component.
/// </summary>
public int Minor { get; } = -1;
public static implicit operator GameVersion(string ver)
{
return Parse(ver);
}
public static bool operator ==(GameVersion v1, GameVersion v2)
{
if (v1 is null)
{
return v2 is null;
}
return v1.Equals(v2);
}
public static bool operator !=(GameVersion v1, GameVersion v2)
{
return !(v1 == v2);
}
public static bool operator <(GameVersion v1, GameVersion v2)
{
if (v1 is null)
throw new ArgumentNullException(nameof(v1));
return v1.CompareTo(v2) < 0;
}
public static bool operator <=(GameVersion v1, GameVersion v2)
{
if (v1 is null)
throw new ArgumentNullException(nameof(v1));
return v1.CompareTo(v2) <= 0;
}
public static bool operator >(GameVersion v1, GameVersion v2)
{
return v2 < v1;
}
public static bool operator >=(GameVersion v1, GameVersion v2)
{
return v2 <= v1;
}
public static GameVersion operator +(GameVersion v1, TimeSpan v2)
{
if (v1 == null)
throw new ArgumentNullException(nameof(v1));
if (v1.Year == -1 || v1.Month == -1 || v1.Day == -1)
return v1;
var date = new DateTime(v1.Year, v1.Month, v1.Day) + v2;
return new GameVersion(date.Year, date.Month, date.Day, v1.Major, v1.Minor);
}
public static GameVersion operator -(GameVersion v1, TimeSpan v2)
{
if (v1 == null)
throw new ArgumentNullException(nameof(v1));
if (v1.Year == -1 || v1.Month == -1 || v1.Day == -1)
return v1;
var date = new DateTime(v1.Year, v1.Month, v1.Day) - v2;
return new GameVersion(date.Year, date.Month, date.Day, v1.Major, v1.Minor);
}
/// <summary>
/// Parse a version string. YYYY.MM.DD.majr.minr or "any".
/// </summary>
/// <param name="input">Input to parse.</param>
/// <returns>GameVersion object.</returns>
public static GameVersion Parse(string input)
{
if (input == null)
throw new ArgumentNullException(nameof(input));
if (input.ToLower(CultureInfo.InvariantCulture) == "any")
return new GameVersion();
var parts = input.Split('.');
var tplParts = parts.Select(p =>
{
var result = int.TryParse(p, out var value);
return (result, value);
}).ToArray();
if (tplParts.Any(t => !t.result))
throw new FormatException("Bad formatting");
var intParts = tplParts.Select(t => t.value).ToArray();
var len = intParts.Length;
if (len == 1)
return new GameVersion(intParts[0]);
else if (len == 2)
return new GameVersion(intParts[0], intParts[1]);
else if (len == 3)
return new GameVersion(intParts[0], intParts[1], intParts[2]);
else if (len == 4)
return new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3]);
else if (len == 5)
return new GameVersion(intParts[0], intParts[1], intParts[2], intParts[3], intParts[4]);
else
throw new ArgumentException("Too many parts");
}
/// <summary>
/// Try to parse a version string. YYYY.MM.DD.majr.minr or "any".
/// </summary>
/// <param name="input">Input to parse.</param>
/// <param name="result">GameVersion object.</param>
/// <returns>Success or failure.</returns>
public static bool TryParse(string input, out GameVersion result)
{
try
{
result = Parse(input);
return true;
}
catch
{
result = null;
return false;
}
}
/// <inheritdoc/>
public object Clone() => new GameVersion(this.Year, this.Month, this.Day, this.Major, this.Minor);
/// <inheritdoc/>
public int CompareTo(object obj)
{
if (obj == null)
return 1;
if (obj is GameVersion value)
{
return this.CompareTo(value);
}
else
{
throw new ArgumentException("Argument must be a GameVersion");
}
}
/// <inheritdoc/>
public int CompareTo(GameVersion value)
{
if (value == null)
return 1;
if (this == value)
return 0;
if (this == AnyVersion)
return 1;
if (value == AnyVersion)
return -1;
if (this.Year != value.Year)
return this.Year > value.Year ? 1 : -1;
if (this.Month != value.Month)
return this.Month > value.Month ? 1 : -1;
if (this.Day != value.Day)
return this.Day > value.Day ? 1 : -1;
if (this.Major != value.Major)
return this.Major > value.Major ? 1 : -1;
if (this.Minor != value.Minor)
return this.Minor > value.Minor ? 1 : -1;
if (this == value)
return 0; return 0;
if (this == AnyVersion)
return 1;
if (value == AnyVersion)
return -1;
if (this.Year != value.Year)
return this.Year > value.Year ? 1 : -1;
if (this.Month != value.Month)
return this.Month > value.Month ? 1 : -1;
if (this.Day != value.Day)
return this.Day > value.Day ? 1 : -1;
if (this.Major != value.Major)
return this.Major > value.Major ? 1 : -1;
if (this.Minor != value.Minor)
return this.Minor > value.Minor ? 1 : -1;
return 0;
}
/// <inheritdoc/>
public override bool Equals(object obj)
{
if (obj is not GameVersion value)
return false;
return this.Equals(value);
}
/// <inheritdoc/>
public bool Equals(GameVersion value)
{
if (value == null)
{
return false;
} }
return /// <inheritdoc/>
(this.Year == value.Year) && public override bool Equals(object obj)
(this.Month == value.Month) && {
(this.Day == value.Day) && if (obj is not GameVersion value)
(this.Major == value.Major) && return false;
(this.Minor == value.Minor);
}
/// <inheritdoc/> return this.Equals(value);
public override int GetHashCode() }
{
var accumulator = 0;
// This might be horribly wrong, but it isn't used heavily. /// <inheritdoc/>
accumulator |= this.Year.GetHashCode(); public bool Equals(GameVersion value)
accumulator |= this.Month.GetHashCode(); {
accumulator |= this.Day.GetHashCode(); if (value == null)
accumulator |= this.Major.GetHashCode(); {
accumulator |= this.Minor.GetHashCode(); return false;
}
return accumulator; return
} (this.Year == value.Year) &&
(this.Month == value.Month) &&
(this.Day == value.Day) &&
(this.Major == value.Major) &&
(this.Minor == value.Minor);
}
/// <inheritdoc/> /// <inheritdoc/>
public override string ToString() public override int GetHashCode()
{ {
if (this.Year == -1 && var accumulator = 0;
this.Month == -1 &&
this.Day == -1 &&
this.Major == -1 &&
this.Minor == -1)
return "any";
return new StringBuilder() // This might be horribly wrong, but it isn't used heavily.
.Append(string.Format("{0:D4}.", this.Year == -1 ? 0 : this.Year)) accumulator |= this.Year.GetHashCode();
.Append(string.Format("{0:D2}.", this.Month == -1 ? 0 : this.Month)) accumulator |= this.Month.GetHashCode();
.Append(string.Format("{0:D2}.", this.Day == -1 ? 0 : this.Day)) accumulator |= this.Day.GetHashCode();
.Append(string.Format("{0:D4}.", this.Major == -1 ? 0 : this.Major)) accumulator |= this.Major.GetHashCode();
.Append(string.Format("{0:D4}", this.Minor == -1 ? 0 : this.Minor)) accumulator |= this.Minor.GetHashCode();
.ToString();
return accumulator;
}
/// <inheritdoc/>
public override string ToString()
{
if (this.Year == -1 &&
this.Month == -1 &&
this.Day == -1 &&
this.Major == -1 &&
this.Minor == -1)
return "any";
return new StringBuilder()
.Append(string.Format("{0:D4}.", this.Year == -1 ? 0 : this.Year))
.Append(string.Format("{0:D2}.", this.Month == -1 ? 0 : this.Month))
.Append(string.Format("{0:D2}.", this.Day == -1 ? 0 : this.Day))
.Append(string.Format("{0:D4}.", this.Major == -1 ? 0 : this.Major))
.Append(string.Format("{0:D4}", this.Minor == -1 ? 0 : this.Minor))
.ToString();
}
} }
} }

View file

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

View file

@ -13,470 +13,471 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Serilog; using Serilog;
namespace Dalamud.Game.Gui; namespace Dalamud.Game.Gui
/// <summary>
/// This class handles interacting with the native chat UI.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed class ChatGui : IDisposable
{ {
private readonly ChatGuiAddressResolver address;
private readonly Queue<XivChatEntry> chatQueue = new();
private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = new();
private readonly Hook<PrintMessageDelegate> printMessageHook;
private readonly Hook<PopulateItemLinkDelegate> populateItemLinkHook;
private readonly Hook<InteractableLinkClickedDelegate> interactableLinkClickedHook;
private IntPtr baseAddress = IntPtr.Zero;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ChatGui"/> class. /// This class handles interacting with the native chat UI.
/// </summary> /// </summary>
/// <param name="baseAddress">The base address of the ChatManager.</param> [PluginInterface]
internal ChatGui(IntPtr baseAddress) [InterfaceVersion("1.0")]
public sealed class ChatGui : IDisposable
{ {
this.address = new ChatGuiAddressResolver(baseAddress); private readonly ChatGuiAddressResolver address;
this.address.Setup();
Log.Verbose($"Chat manager address 0x{this.address.BaseAddress.ToInt64():X}"); private readonly Queue<XivChatEntry> chatQueue = new();
private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = new();
this.printMessageHook = new Hook<PrintMessageDelegate>(this.address.PrintMessage, this.HandlePrintMessageDetour); private readonly Hook<PrintMessageDelegate> printMessageHook;
this.populateItemLinkHook = new Hook<PopulateItemLinkDelegate>(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour); private readonly Hook<PopulateItemLinkDelegate> populateItemLinkHook;
this.interactableLinkClickedHook = new Hook<InteractableLinkClickedDelegate>(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour); private readonly Hook<InteractableLinkClickedDelegate> interactableLinkClickedHook;
}
/// <summary> private IntPtr baseAddress = IntPtr.Zero;
/// A delegate type used with the <see cref="ChatGui.ChatMessage"/> event.
/// </summary>
/// <param name="type">The type of chat.</param>
/// <param name="senderId">The sender ID.</param>
/// <param name="sender">The sender name.</param>
/// <param name="message">The message sent.</param>
/// <param name="isHandled">A value indicating whether the message was handled or should be propagated.</param>
public delegate void OnMessageDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled);
/// <summary> /// <summary>
/// A delegate type used with the <see cref="ChatGui.CheckMessageHandled"/> event. /// Initializes a new instance of the <see cref="ChatGui"/> class.
/// </summary> /// </summary>
/// <param name="type">The type of chat.</param> /// <param name="baseAddress">The base address of the ChatManager.</param>
/// <param name="senderId">The sender ID.</param> internal ChatGui(IntPtr baseAddress)
/// <param name="sender">The sender name.</param>
/// <param name="message">The message sent.</param>
/// <param name="isHandled">A value indicating whether the message was handled or should be propagated.</param>
public delegate void OnCheckMessageHandledDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled);
/// <summary>
/// A delegate type used with the <see cref="ChatGui.ChatMessageHandled"/> event.
/// </summary>
/// <param name="type">The type of chat.</param>
/// <param name="senderId">The sender ID.</param>
/// <param name="sender">The sender name.</param>
/// <param name="message">The message sent.</param>
public delegate void OnMessageHandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message);
/// <summary>
/// A delegate type used with the <see cref="ChatGui.ChatMessageUnhandled"/> event.
/// </summary>
/// <param name="type">The type of chat.</param>
/// <param name="senderId">The sender ID.</param>
/// <param name="sender">The sender name.</param>
/// <param name="message">The message sent.</param>
public delegate void OnMessageUnhandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr PrintMessageDelegate(IntPtr manager, XivChatType chatType, IntPtr senderName, IntPtr message, uint senderId, IntPtr parameter);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void PopulateItemLinkDelegate(IntPtr linkObjectPtr, IntPtr itemInfoPtr);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void InteractableLinkClickedDelegate(IntPtr managerPtr, IntPtr messagePtr);
/// <summary>
/// Event that will be fired when a chat message is sent to chat by the game.
/// </summary>
public event OnMessageDelegate ChatMessage;
/// <summary>
/// Event that allows you to stop messages from appearing in chat by setting the isHandled parameter to true.
/// </summary>
public event OnCheckMessageHandledDelegate CheckMessageHandled;
/// <summary>
/// Event that will be fired when a chat message is handled by Dalamud or a Plugin.
/// </summary>
public event OnMessageHandledDelegate ChatMessageHandled;
/// <summary>
/// Event that will be fired when a chat message is not handled by Dalamud or a Plugin.
/// </summary>
public event OnMessageUnhandledDelegate ChatMessageUnhandled;
/// <summary>
/// Gets the ID of the last linked item.
/// </summary>
public int LastLinkedItemId { get; private set; }
/// <summary>
/// Gets the flags of the last linked item.
/// </summary>
public byte LastLinkedItemFlags { get; private set; }
/// <summary>
/// Enables this module.
/// </summary>
public void Enable()
{
this.printMessageHook.Enable();
this.populateItemLinkHook.Enable();
this.interactableLinkClickedHook.Enable();
}
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.printMessageHook.Dispose();
this.populateItemLinkHook.Dispose();
this.interactableLinkClickedHook.Dispose();
}
/// <summary>
/// Queue a chat message. While method is named as PrintChat, it only add a entry to the queue,
/// later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="chat">A message to send.</param>
public void PrintChat(XivChatEntry chat)
{
this.chatQueue.Enqueue(chat);
}
/// <summary>
/// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue,
/// later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="message">A message to send.</param>
public void Print(string message)
{
var configuration = Service<DalamudConfiguration>.Get();
// Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message);
this.PrintChat(new XivChatEntry
{ {
Message = message, this.address = new ChatGuiAddressResolver(baseAddress);
Type = configuration.GeneralChatType, this.address.Setup();
});
}
/// <summary> Log.Verbose($"Chat manager address 0x{this.address.BaseAddress.ToInt64():X}");
/// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue,
/// later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="message">A message to send.</param>
public void Print(SeString message)
{
var configuration = Service<DalamudConfiguration>.Get();
// Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue); this.printMessageHook = new Hook<PrintMessageDelegate>(this.address.PrintMessage, this.HandlePrintMessageDetour);
this.PrintChat(new XivChatEntry this.populateItemLinkHook = new Hook<PopulateItemLinkDelegate>(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
{ this.interactableLinkClickedHook = new Hook<InteractableLinkClickedDelegate>(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
Message = message,
Type = configuration.GeneralChatType,
});
}
/// <summary>
/// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to
/// the queue, later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="message">A message to send.</param>
public void PrintError(string message)
{
// Log.Verbose("[CHATGUI PRINT REGULAR ERROR]{0}", message);
this.PrintChat(new XivChatEntry
{
Message = message,
Type = XivChatType.Urgent,
});
}
/// <summary>
/// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to
/// the queue, later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="message">A message to send.</param>
public void PrintError(SeString message)
{
// Log.Verbose("[CHATGUI PRINT SESTRING ERROR]{0}", message.TextValue);
this.PrintChat(new XivChatEntry
{
Message = message,
Type = XivChatType.Urgent,
});
}
/// <summary>
/// Process a chat queue.
/// </summary>
public void UpdateQueue()
{
while (this.chatQueue.Count > 0)
{
var chat = this.chatQueue.Dequeue();
if (this.baseAddress == IntPtr.Zero)
{
continue;
}
var senderRaw = (chat.Name ?? string.Empty).Encode();
using var senderOwned = Service<LibcFunction>.Get().NewString(senderRaw);
var messageRaw = (chat.Message ?? string.Empty).Encode();
using var messageOwned = Service<LibcFunction>.Get().NewString(messageRaw);
this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters);
}
}
/// <summary>
/// Create a link handler.
/// </summary>
/// <param name="pluginName">The name of the plugin handling the link.</param>
/// <param name="commandId">The ID of the command to run.</param>
/// <param name="commandAction">The command action itself.</param>
/// <returns>A payload for handling.</returns>
internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action<uint, SeString> commandAction)
{
var payload = new DalamudLinkPayload() { Plugin = pluginName, CommandId = commandId };
this.dalamudLinkHandlers.Add((pluginName, commandId), commandAction);
return payload;
}
/// <summary>
/// Remove all handlers owned by a plugin.
/// </summary>
/// <param name="pluginName">The name of the plugin handling the links.</param>
internal void RemoveChatLinkHandler(string pluginName)
{
foreach (var handler in this.dalamudLinkHandlers.Keys.ToList().Where(k => k.PluginName == pluginName))
{
this.dalamudLinkHandlers.Remove(handler);
}
}
/// <summary>
/// Remove a registered link handler.
/// </summary>
/// <param name="pluginName">The name of the plugin handling the link.</param>
/// <param name="commandId">The ID of the command to be removed.</param>
internal void RemoveChatLinkHandler(string pluginName, uint commandId)
{
if (this.dalamudLinkHandlers.ContainsKey((pluginName, commandId)))
{
this.dalamudLinkHandlers.Remove((pluginName, commandId));
}
}
private static unsafe bool FastByteArrayCompare(byte[] a1, byte[] a2)
{
// Copyright (c) 2008-2013 Hafthor Stefansson
// Distributed under the MIT/X11 software license
// Ref: http://www.opensource.org/licenses/mit-license.php.
// https://stackoverflow.com/a/8808245
if (a1 == a2) return true;
if (a1 == null || a2 == null || a1.Length != a2.Length)
return false;
fixed (byte* p1 = a1, p2 = a2)
{
byte* x1 = p1, x2 = p2;
var l = a1.Length;
for (var i = 0; i < l / 8; i++, x1 += 8, x2 += 8)
{
if (*((long*)x1) != *((long*)x2))
return false;
}
if ((l & 4) != 0)
{
if (*((int*)x1) != *((int*)x2))
return false;
x1 += 4;
x2 += 4;
}
if ((l & 2) != 0)
{
if (*((short*)x1) != *((short*)x2))
return false;
x1 += 2;
x2 += 2;
}
if ((l & 1) != 0)
{
if (*((byte*)x1) != *((byte*)x2))
return false;
}
return true;
}
}
private void HandlePopulateItemLinkDetour(IntPtr linkObjectPtr, IntPtr itemInfoPtr)
{
try
{
this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr);
this.LastLinkedItemId = Marshal.ReadInt32(itemInfoPtr, 8);
this.LastLinkedItemFlags = Marshal.ReadByte(itemInfoPtr, 0x14);
// Log.Verbose($"HandlePopulateItemLinkDetour {linkObjectPtr} {itemInfoPtr} - linked:{this.LastLinkedItemId}");
}
catch (Exception ex)
{
Log.Error(ex, "Exception onPopulateItemLink hook.");
this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr);
}
}
private IntPtr HandlePrintMessageDetour(IntPtr manager, XivChatType chattype, IntPtr pSenderName, IntPtr pMessage, uint senderid, IntPtr parameter)
{
var retVal = IntPtr.Zero;
try
{
var sender = StdString.ReadFromPointer(pSenderName);
var parsedSender = SeString.Parse(sender.RawData);
var originalSenderData = (byte[])sender.RawData.Clone();
var oldEditedSender = parsedSender.Encode();
var senderPtr = pSenderName;
OwnedStdString allocatedString = null;
var message = StdString.ReadFromPointer(pMessage);
var parsedMessage = SeString.Parse(message.RawData);
var originalMessageData = (byte[])message.RawData.Clone();
var oldEdited = parsedMessage.Encode();
var messagePtr = pMessage;
OwnedStdString allocatedStringSender = null;
// Log.Verbose("[CHATGUI][{0}][{1}]", parsedSender.TextValue, parsedMessage.TextValue);
// Log.Debug($"HandlePrintMessageDetour {manager} - [{chattype}] [{BitConverter.ToString(message.RawData).Replace("-", " ")}] {message.Value} from {senderName.Value}");
// Call events
var isHandled = false;
this.CheckMessageHandled?.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled);
if (!isHandled)
{
this.ChatMessage?.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled);
}
var newEdited = parsedMessage.Encode();
if (!FastByteArrayCompare(oldEdited, newEdited))
{
Log.Verbose("SeString was edited, taking precedence over StdString edit.");
message.RawData = newEdited;
// Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}");
}
if (!FastByteArrayCompare(originalMessageData, message.RawData))
{
allocatedString = Service<LibcFunction>.Get().NewString(message.RawData);
Log.Debug($"HandlePrintMessageDetour String modified: {originalMessageData}({messagePtr}) -> {message}({allocatedString.Address})");
messagePtr = allocatedString.Address;
}
var newEditedSender = parsedSender.Encode();
if (!FastByteArrayCompare(oldEditedSender, newEditedSender))
{
Log.Verbose("SeString was edited, taking precedence over StdString edit.");
sender.RawData = newEditedSender;
// Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}");
}
if (!FastByteArrayCompare(originalSenderData, sender.RawData))
{
allocatedStringSender = Service<LibcFunction>.Get().NewString(sender.RawData);
Log.Debug(
$"HandlePrintMessageDetour Sender modified: {originalSenderData}({senderPtr}) -> {sender}({allocatedStringSender.Address})");
senderPtr = allocatedStringSender.Address;
}
// Print the original chat if it's handled.
if (isHandled)
{
this.ChatMessageHandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
}
else
{
retVal = this.printMessageHook.Original(manager, chattype, senderPtr, messagePtr, senderid, parameter);
this.ChatMessageUnhandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
}
if (this.baseAddress == IntPtr.Zero)
this.baseAddress = manager;
allocatedString?.Dispose();
allocatedStringSender?.Dispose();
}
catch (Exception ex)
{
Log.Error(ex, "Exception on OnChatMessage hook.");
retVal = this.printMessageHook.Original(manager, chattype, pSenderName, pMessage, senderid, parameter);
} }
return retVal; /// <summary>
} /// A delegate type used with the <see cref="ChatGui.ChatMessage"/> event.
/// </summary>
/// <param name="type">The type of chat.</param>
/// <param name="senderId">The sender ID.</param>
/// <param name="sender">The sender name.</param>
/// <param name="message">The message sent.</param>
/// <param name="isHandled">A value indicating whether the message was handled or should be propagated.</param>
public delegate void OnMessageDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled);
private void InteractableLinkClickedDetour(IntPtr managerPtr, IntPtr messagePtr) /// <summary>
{ /// A delegate type used with the <see cref="ChatGui.CheckMessageHandled"/> event.
try /// </summary>
/// <param name="type">The type of chat.</param>
/// <param name="senderId">The sender ID.</param>
/// <param name="sender">The sender name.</param>
/// <param name="message">The message sent.</param>
/// <param name="isHandled">A value indicating whether the message was handled or should be propagated.</param>
public delegate void OnCheckMessageHandledDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled);
/// <summary>
/// A delegate type used with the <see cref="ChatGui.ChatMessageHandled"/> event.
/// </summary>
/// <param name="type">The type of chat.</param>
/// <param name="senderId">The sender ID.</param>
/// <param name="sender">The sender name.</param>
/// <param name="message">The message sent.</param>
public delegate void OnMessageHandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message);
/// <summary>
/// A delegate type used with the <see cref="ChatGui.ChatMessageUnhandled"/> event.
/// </summary>
/// <param name="type">The type of chat.</param>
/// <param name="senderId">The sender ID.</param>
/// <param name="sender">The sender name.</param>
/// <param name="message">The message sent.</param>
public delegate void OnMessageUnhandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr PrintMessageDelegate(IntPtr manager, XivChatType chatType, IntPtr senderName, IntPtr message, uint senderId, IntPtr parameter);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void PopulateItemLinkDelegate(IntPtr linkObjectPtr, IntPtr itemInfoPtr);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void InteractableLinkClickedDelegate(IntPtr managerPtr, IntPtr messagePtr);
/// <summary>
/// Event that will be fired when a chat message is sent to chat by the game.
/// </summary>
public event OnMessageDelegate ChatMessage;
/// <summary>
/// Event that allows you to stop messages from appearing in chat by setting the isHandled parameter to true.
/// </summary>
public event OnCheckMessageHandledDelegate CheckMessageHandled;
/// <summary>
/// Event that will be fired when a chat message is handled by Dalamud or a Plugin.
/// </summary>
public event OnMessageHandledDelegate ChatMessageHandled;
/// <summary>
/// Event that will be fired when a chat message is not handled by Dalamud or a Plugin.
/// </summary>
public event OnMessageUnhandledDelegate ChatMessageUnhandled;
/// <summary>
/// Gets the ID of the last linked item.
/// </summary>
public int LastLinkedItemId { get; private set; }
/// <summary>
/// Gets the flags of the last linked item.
/// </summary>
public byte LastLinkedItemFlags { get; private set; }
/// <summary>
/// Enables this module.
/// </summary>
public void Enable()
{ {
var interactableType = (Payload.EmbeddedInfoType)(Marshal.ReadByte(messagePtr, 0x1B) + 1); this.printMessageHook.Enable();
this.populateItemLinkHook.Enable();
this.interactableLinkClickedHook.Enable();
}
if (interactableType != Payload.EmbeddedInfoType.DalamudLink) /// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.printMessageHook.Dispose();
this.populateItemLinkHook.Dispose();
this.interactableLinkClickedHook.Dispose();
}
/// <summary>
/// Queue a chat message. While method is named as PrintChat, it only add a entry to the queue,
/// later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="chat">A message to send.</param>
public void PrintChat(XivChatEntry chat)
{
this.chatQueue.Enqueue(chat);
}
/// <summary>
/// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue,
/// later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="message">A message to send.</param>
public void Print(string message)
{
var configuration = Service<DalamudConfiguration>.Get();
// Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message);
this.PrintChat(new XivChatEntry
{ {
this.interactableLinkClickedHook.Original(managerPtr, messagePtr); Message = message,
return; Type = configuration.GeneralChatType,
} });
}
Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}"); /// <summary>
/// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue,
/// later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="message">A message to send.</param>
public void Print(SeString message)
{
var configuration = Service<DalamudConfiguration>.Get();
var payloadPtr = Marshal.ReadIntPtr(messagePtr, 0x10); // Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue);
var messageSize = 0; this.PrintChat(new XivChatEntry
while (Marshal.ReadByte(payloadPtr, messageSize) != 0) messageSize++;
var payloadBytes = new byte[messageSize];
Marshal.Copy(payloadPtr, payloadBytes, 0, messageSize);
var seStr = SeString.Parse(payloadBytes);
var terminatorIndex = seStr.Payloads.IndexOf(RawPayload.LinkTerminator);
var payloads = terminatorIndex >= 0 ? seStr.Payloads.Take(terminatorIndex + 1).ToList() : seStr.Payloads;
if (payloads.Count == 0) return;
var linkPayload = payloads[0];
if (linkPayload is DalamudLinkPayload link)
{ {
if (this.dalamudLinkHandlers.ContainsKey((link.Plugin, link.CommandId))) Message = message,
Type = configuration.GeneralChatType,
});
}
/// <summary>
/// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to
/// the queue, later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="message">A message to send.</param>
public void PrintError(string message)
{
// Log.Verbose("[CHATGUI PRINT REGULAR ERROR]{0}", message);
this.PrintChat(new XivChatEntry
{
Message = message,
Type = XivChatType.Urgent,
});
}
/// <summary>
/// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to
/// the queue, later to be processed when UpdateQueue() is called.
/// </summary>
/// <param name="message">A message to send.</param>
public void PrintError(SeString message)
{
// Log.Verbose("[CHATGUI PRINT SESTRING ERROR]{0}", message.TextValue);
this.PrintChat(new XivChatEntry
{
Message = message,
Type = XivChatType.Urgent,
});
}
/// <summary>
/// Process a chat queue.
/// </summary>
public void UpdateQueue()
{
while (this.chatQueue.Count > 0)
{
var chat = this.chatQueue.Dequeue();
if (this.baseAddress == IntPtr.Zero)
{ {
Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}"); continue;
this.dalamudLinkHandlers[(link.Plugin, link.CommandId)].Invoke(link.CommandId, new SeString(payloads)); }
var senderRaw = (chat.Name ?? string.Empty).Encode();
using var senderOwned = Service<LibcFunction>.Get().NewString(senderRaw);
var messageRaw = (chat.Message ?? string.Empty).Encode();
using var messageOwned = Service<LibcFunction>.Get().NewString(messageRaw);
this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters);
}
}
/// <summary>
/// Create a link handler.
/// </summary>
/// <param name="pluginName">The name of the plugin handling the link.</param>
/// <param name="commandId">The ID of the command to run.</param>
/// <param name="commandAction">The command action itself.</param>
/// <returns>A payload for handling.</returns>
internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action<uint, SeString> commandAction)
{
var payload = new DalamudLinkPayload() { Plugin = pluginName, CommandId = commandId };
this.dalamudLinkHandlers.Add((pluginName, commandId), commandAction);
return payload;
}
/// <summary>
/// Remove all handlers owned by a plugin.
/// </summary>
/// <param name="pluginName">The name of the plugin handling the links.</param>
internal void RemoveChatLinkHandler(string pluginName)
{
foreach (var handler in this.dalamudLinkHandlers.Keys.ToList().Where(k => k.PluginName == pluginName))
{
this.dalamudLinkHandlers.Remove(handler);
}
}
/// <summary>
/// Remove a registered link handler.
/// </summary>
/// <param name="pluginName">The name of the plugin handling the link.</param>
/// <param name="commandId">The ID of the command to be removed.</param>
internal void RemoveChatLinkHandler(string pluginName, uint commandId)
{
if (this.dalamudLinkHandlers.ContainsKey((pluginName, commandId)))
{
this.dalamudLinkHandlers.Remove((pluginName, commandId));
}
}
private static unsafe bool FastByteArrayCompare(byte[] a1, byte[] a2)
{
// Copyright (c) 2008-2013 Hafthor Stefansson
// Distributed under the MIT/X11 software license
// Ref: http://www.opensource.org/licenses/mit-license.php.
// https://stackoverflow.com/a/8808245
if (a1 == a2) return true;
if (a1 == null || a2 == null || a1.Length != a2.Length)
return false;
fixed (byte* p1 = a1, p2 = a2)
{
byte* x1 = p1, x2 = p2;
var l = a1.Length;
for (var i = 0; i < l / 8; i++, x1 += 8, x2 += 8)
{
if (*((long*)x1) != *((long*)x2))
return false;
}
if ((l & 4) != 0)
{
if (*((int*)x1) != *((int*)x2))
return false;
x1 += 4;
x2 += 4;
}
if ((l & 2) != 0)
{
if (*((short*)x1) != *((short*)x2))
return false;
x1 += 2;
x2 += 2;
}
if ((l & 1) != 0)
{
if (*((byte*)x1) != *((byte*)x2))
return false;
}
return true;
}
}
private void HandlePopulateItemLinkDetour(IntPtr linkObjectPtr, IntPtr itemInfoPtr)
{
try
{
this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr);
this.LastLinkedItemId = Marshal.ReadInt32(itemInfoPtr, 8);
this.LastLinkedItemFlags = Marshal.ReadByte(itemInfoPtr, 0x14);
// Log.Verbose($"HandlePopulateItemLinkDetour {linkObjectPtr} {itemInfoPtr} - linked:{this.LastLinkedItemId}");
}
catch (Exception ex)
{
Log.Error(ex, "Exception onPopulateItemLink hook.");
this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr);
}
}
private IntPtr HandlePrintMessageDetour(IntPtr manager, XivChatType chattype, IntPtr pSenderName, IntPtr pMessage, uint senderid, IntPtr parameter)
{
var retVal = IntPtr.Zero;
try
{
var sender = StdString.ReadFromPointer(pSenderName);
var parsedSender = SeString.Parse(sender.RawData);
var originalSenderData = (byte[])sender.RawData.Clone();
var oldEditedSender = parsedSender.Encode();
var senderPtr = pSenderName;
OwnedStdString allocatedString = null;
var message = StdString.ReadFromPointer(pMessage);
var parsedMessage = SeString.Parse(message.RawData);
var originalMessageData = (byte[])message.RawData.Clone();
var oldEdited = parsedMessage.Encode();
var messagePtr = pMessage;
OwnedStdString allocatedStringSender = null;
// Log.Verbose("[CHATGUI][{0}][{1}]", parsedSender.TextValue, parsedMessage.TextValue);
// Log.Debug($"HandlePrintMessageDetour {manager} - [{chattype}] [{BitConverter.ToString(message.RawData).Replace("-", " ")}] {message.Value} from {senderName.Value}");
// Call events
var isHandled = false;
this.CheckMessageHandled?.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled);
if (!isHandled)
{
this.ChatMessage?.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled);
}
var newEdited = parsedMessage.Encode();
if (!FastByteArrayCompare(oldEdited, newEdited))
{
Log.Verbose("SeString was edited, taking precedence over StdString edit.");
message.RawData = newEdited;
// Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}");
}
if (!FastByteArrayCompare(originalMessageData, message.RawData))
{
allocatedString = Service<LibcFunction>.Get().NewString(message.RawData);
Log.Debug($"HandlePrintMessageDetour String modified: {originalMessageData}({messagePtr}) -> {message}({allocatedString.Address})");
messagePtr = allocatedString.Address;
}
var newEditedSender = parsedSender.Encode();
if (!FastByteArrayCompare(oldEditedSender, newEditedSender))
{
Log.Verbose("SeString was edited, taking precedence over StdString edit.");
sender.RawData = newEditedSender;
// Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}");
}
if (!FastByteArrayCompare(originalSenderData, sender.RawData))
{
allocatedStringSender = Service<LibcFunction>.Get().NewString(sender.RawData);
Log.Debug(
$"HandlePrintMessageDetour Sender modified: {originalSenderData}({senderPtr}) -> {sender}({allocatedStringSender.Address})");
senderPtr = allocatedStringSender.Address;
}
// Print the original chat if it's handled.
if (isHandled)
{
this.ChatMessageHandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
} }
else else
{ {
Log.Debug($"No DalamudLink registered for {link.Plugin} with ID of {link.CommandId}"); retVal = this.printMessageHook.Original(manager, chattype, senderPtr, messagePtr, senderid, parameter);
this.ChatMessageUnhandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
}
if (this.baseAddress == IntPtr.Zero)
this.baseAddress = manager;
allocatedString?.Dispose();
allocatedStringSender?.Dispose();
}
catch (Exception ex)
{
Log.Error(ex, "Exception on OnChatMessage hook.");
retVal = this.printMessageHook.Original(manager, chattype, pSenderName, pMessage, senderid, parameter);
}
return retVal;
}
private void InteractableLinkClickedDetour(IntPtr managerPtr, IntPtr messagePtr)
{
try
{
var interactableType = (Payload.EmbeddedInfoType)(Marshal.ReadByte(messagePtr, 0x1B) + 1);
if (interactableType != Payload.EmbeddedInfoType.DalamudLink)
{
this.interactableLinkClickedHook.Original(managerPtr, messagePtr);
return;
}
Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}");
var payloadPtr = Marshal.ReadIntPtr(messagePtr, 0x10);
var messageSize = 0;
while (Marshal.ReadByte(payloadPtr, messageSize) != 0) messageSize++;
var payloadBytes = new byte[messageSize];
Marshal.Copy(payloadPtr, payloadBytes, 0, messageSize);
var seStr = SeString.Parse(payloadBytes);
var terminatorIndex = seStr.Payloads.IndexOf(RawPayload.LinkTerminator);
var payloads = terminatorIndex >= 0 ? seStr.Payloads.Take(terminatorIndex + 1).ToList() : seStr.Payloads;
if (payloads.Count == 0) return;
var linkPayload = payloads[0];
if (linkPayload is DalamudLinkPayload link)
{
if (this.dalamudLinkHandlers.ContainsKey((link.Plugin, link.CommandId)))
{
Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}");
this.dalamudLinkHandlers[(link.Plugin, link.CommandId)].Invoke(link.CommandId, new SeString(payloads));
}
else
{
Log.Debug($"No DalamudLink registered for {link.Plugin} with ID of {link.CommandId}");
}
} }
} }
} catch (Exception ex)
catch (Exception ex) {
{ Log.Error(ex, "Exception on InteractableLinkClicked hook");
Log.Error(ex, "Exception on InteractableLinkClicked hook"); }
} }
} }
} }

View file

@ -1,117 +1,120 @@
using System; using System;
namespace Dalamud.Game.Gui; using Dalamud.Game.Internal;
/// <summary> namespace Dalamud.Game.Gui
/// The address resolver for the <see cref="ChatGui"/> class.
/// </summary>
public sealed class ChatGuiAddressResolver : BaseAddressResolver
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ChatGuiAddressResolver"/> class. /// The address resolver for the <see cref="ChatGui"/> class.
/// </summary> /// </summary>
/// <param name="baseAddress">The base address of the native ChatManager class.</param> public sealed class ChatGuiAddressResolver : BaseAddressResolver
public ChatGuiAddressResolver(IntPtr baseAddress)
{ {
this.BaseAddress = baseAddress; /// <summary>
} /// Initializes a new instance of the <see cref="ChatGuiAddressResolver"/> class.
/// </summary>
/// <param name="baseAddress">The base address of the native ChatManager class.</param>
public ChatGuiAddressResolver(IntPtr baseAddress)
{
this.BaseAddress = baseAddress;
}
/// <summary> /// <summary>
/// Gets the base address of the native ChatManager class. /// Gets the base address of the native ChatManager class.
/// </summary> /// </summary>
public IntPtr BaseAddress { get; } public IntPtr BaseAddress { get; }
/// <summary> /// <summary>
/// Gets the address of the native PrintMessage method. /// Gets the address of the native PrintMessage method.
/// </summary> /// </summary>
public IntPtr PrintMessage { get; private set; } public IntPtr PrintMessage { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the native PopulateItemLinkObject method. /// Gets the address of the native PopulateItemLinkObject method.
/// </summary> /// </summary>
public IntPtr PopulateItemLinkObject { get; private set; } public IntPtr PopulateItemLinkObject { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the native InteractableLinkClicked method. /// Gets the address of the native InteractableLinkClicked method.
/// </summary> /// </summary>
public IntPtr InteractableLinkClicked { get; private set; } public IntPtr InteractableLinkClicked { get; private set; }
/* /*
--- for reference: 4.57 --- --- for reference: 4.57 ---
.text:00000001405CD210 ; __int64 __fastcall Xiv::Gui::ChatGui::PrintMessage(__int64 handler, unsigned __int16 chatType, __int64 senderName, __int64 message, int senderActorId, char isLocal) .text:00000001405CD210 ; __int64 __fastcall Xiv::Gui::ChatGui::PrintMessage(__int64 handler, unsigned __int16 chatType, __int64 senderName, __int64 message, int senderActorId, char isLocal)
.text:00000001405CD210 Xiv__Gui__ChatGui__PrintMessage proc near .text:00000001405CD210 Xiv__Gui__ChatGui__PrintMessage proc near
.text:00000001405CD210 ; CODE XREF: sub_1401419F0+201p .text:00000001405CD210 ; CODE XREF: sub_1401419F0+201p
.text:00000001405CD210 ; sub_140141D10+220p ... .text:00000001405CD210 ; sub_140141D10+220p ...
.text:00000001405CD210 .text:00000001405CD210
.text:00000001405CD210 var_220 = qword ptr -220h .text:00000001405CD210 var_220 = qword ptr -220h
.text:00000001405CD210 var_218 = byte ptr -218h .text:00000001405CD210 var_218 = byte ptr -218h
.text:00000001405CD210 var_210 = word ptr -210h .text:00000001405CD210 var_210 = word ptr -210h
.text:00000001405CD210 var_208 = byte ptr -208h .text:00000001405CD210 var_208 = byte ptr -208h
.text:00000001405CD210 var_200 = word ptr -200h .text:00000001405CD210 var_200 = word ptr -200h
.text:00000001405CD210 var_1FC = dword ptr -1FCh .text:00000001405CD210 var_1FC = dword ptr -1FCh
.text:00000001405CD210 var_1F8 = qword ptr -1F8h .text:00000001405CD210 var_1F8 = qword ptr -1F8h
.text:00000001405CD210 var_1F0 = qword ptr -1F0h .text:00000001405CD210 var_1F0 = qword ptr -1F0h
.text:00000001405CD210 var_1E8 = qword ptr -1E8h .text:00000001405CD210 var_1E8 = qword ptr -1E8h
.text:00000001405CD210 var_1E0 = dword ptr -1E0h .text:00000001405CD210 var_1E0 = dword ptr -1E0h
.text:00000001405CD210 var_1DC = word ptr -1DCh .text:00000001405CD210 var_1DC = word ptr -1DCh
.text:00000001405CD210 var_1DA = word ptr -1DAh .text:00000001405CD210 var_1DA = word ptr -1DAh
.text:00000001405CD210 var_1D8 = qword ptr -1D8h .text:00000001405CD210 var_1D8 = qword ptr -1D8h
.text:00000001405CD210 var_1D0 = byte ptr -1D0h .text:00000001405CD210 var_1D0 = byte ptr -1D0h
.text:00000001405CD210 var_1C8 = qword ptr -1C8h .text:00000001405CD210 var_1C8 = qword ptr -1C8h
.text:00000001405CD210 var_1B0 = dword ptr -1B0h .text:00000001405CD210 var_1B0 = dword ptr -1B0h
.text:00000001405CD210 var_1AC = dword ptr -1ACh .text:00000001405CD210 var_1AC = dword ptr -1ACh
.text:00000001405CD210 var_1A8 = dword ptr -1A8h .text:00000001405CD210 var_1A8 = dword ptr -1A8h
.text:00000001405CD210 var_1A4 = dword ptr -1A4h .text:00000001405CD210 var_1A4 = dword ptr -1A4h
.text:00000001405CD210 var_1A0 = dword ptr -1A0h .text:00000001405CD210 var_1A0 = dword ptr -1A0h
.text:00000001405CD210 var_160 = dword ptr -160h .text:00000001405CD210 var_160 = dword ptr -160h
.text:00000001405CD210 var_15C = dword ptr -15Ch .text:00000001405CD210 var_15C = dword ptr -15Ch
.text:00000001405CD210 var_140 = dword ptr -140h .text:00000001405CD210 var_140 = dword ptr -140h
.text:00000001405CD210 var_138 = dword ptr -138h .text:00000001405CD210 var_138 = dword ptr -138h
.text:00000001405CD210 var_130 = byte ptr -130h .text:00000001405CD210 var_130 = byte ptr -130h
.text:00000001405CD210 var_C0 = byte ptr -0C0h .text:00000001405CD210 var_C0 = byte ptr -0C0h
.text:00000001405CD210 var_50 = qword ptr -50h .text:00000001405CD210 var_50 = qword ptr -50h
.text:00000001405CD210 var_38 = qword ptr -38h .text:00000001405CD210 var_38 = qword ptr -38h
.text:00000001405CD210 var_30 = qword ptr -30h .text:00000001405CD210 var_30 = qword ptr -30h
.text:00000001405CD210 var_28 = qword ptr -28h .text:00000001405CD210 var_28 = qword ptr -28h
.text:00000001405CD210 var_20 = qword ptr -20h .text:00000001405CD210 var_20 = qword ptr -20h
.text:00000001405CD210 senderActorId = dword ptr 30h .text:00000001405CD210 senderActorId = dword ptr 30h
.text:00000001405CD210 isLocal = byte ptr 38h .text:00000001405CD210 isLocal = byte ptr 38h
.text:00000001405CD210 .text:00000001405CD210
.text:00000001405CD210 ; __unwind { // __GSHandlerCheck .text:00000001405CD210 ; __unwind { // __GSHandlerCheck
.text:00000001405CD210 push rbp .text:00000001405CD210 push rbp
.text:00000001405CD212 push rdi .text:00000001405CD212 push rdi
.text:00000001405CD213 push r14 .text:00000001405CD213 push r14
.text:00000001405CD215 push r15 .text:00000001405CD215 push r15
.text:00000001405CD217 lea rbp, [rsp-128h] .text:00000001405CD217 lea rbp, [rsp-128h]
.text:00000001405CD21F sub rsp, 228h .text:00000001405CD21F sub rsp, 228h
.text:00000001405CD226 mov rax, cs:__security_cookie .text:00000001405CD226 mov rax, cs:__security_cookie
.text:00000001405CD22D xor rax, rsp .text:00000001405CD22D xor rax, rsp
.text:00000001405CD230 mov [rbp+140h+var_50], rax .text:00000001405CD230 mov [rbp+140h+var_50], rax
.text:00000001405CD237 xor r10b, r10b .text:00000001405CD237 xor r10b, r10b
.text:00000001405CD23A mov [rsp+240h+var_1F8], rcx .text:00000001405CD23A mov [rsp+240h+var_1F8], rcx
.text:00000001405CD23F xor eax, eax .text:00000001405CD23F xor eax, eax
.text:00000001405CD241 mov r11, r9 .text:00000001405CD241 mov r11, r9
.text:00000001405CD244 mov r14, r8 .text:00000001405CD244 mov r14, r8
.text:00000001405CD247 mov r9d, eax .text:00000001405CD247 mov r9d, eax
.text:00000001405CD24A movzx r15d, dx .text:00000001405CD24A movzx r15d, dx
.text:00000001405CD24E lea r8, [rcx+0C10h] .text:00000001405CD24E lea r8, [rcx+0C10h]
.text:00000001405CD255 mov rdi, rcx .text:00000001405CD255 mov rdi, rcx
*/ */
/// <inheritdoc/> /// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig) protected override void Setup64Bit(SigScanner sig)
{ {
// PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24D8FEFFFF 4881EC28020000 488B05???????? 4833C4 488985F0000000 4532D2 48894C2448"); LAST PART FOR 5.1??? // PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24D8FEFFFF 4881EC28020000 488B05???????? 4833C4 488985F0000000 4532D2 48894C2448"); LAST PART FOR 5.1???
this.PrintMessage = sig.ScanText("40 55 53 56 41 54 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC 20 02 00 00 48 8B 05"); this.PrintMessage = sig.ScanText("40 55 53 56 41 54 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC 20 02 00 00 48 8B 05");
// PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24E8FEFFFF 4881EC18020000 488B05???????? 4833C4 488985E0000000 4532D2 48894C2438"); old // PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24E8FEFFFF 4881EC18020000 488B05???????? 4833C4 488985E0000000 4532D2 48894C2438"); old
// PrintMessage = sig.ScanText("40 55 57 41 56 41 57 48 8D AC 24 D8 FE FF FF 48 81 EC 28 02 00 00 48 8B 05 63 47 4A 01 48 33 C4 48 89 85 F0 00 00 00 45 32 D2 48 89 4C 24 48 33"); // PrintMessage = sig.ScanText("40 55 57 41 56 41 57 48 8D AC 24 D8 FE FF FF 48 81 EC 28 02 00 00 48 8B 05 63 47 4A 01 48 33 C4 48 89 85 F0 00 00 00 45 32 D2 48 89 4C 24 48 33");
// PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 FA F2 B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); // PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 FA F2 B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A");
// PopulateItemLinkObject = sig.ScanText( "48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); 5.0 // PopulateItemLinkObject = sig.ScanText( "48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); 5.0
this.PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? ?? FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); this.PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? ?? FF 8B C8 EB 1D 0F B6 42 14 8B 4A");
this.InteractableLinkClicked = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 80 BB ?? ?? ?? ?? ?? 0F 85 ?? ?? ?? ?? 80 BB") + 9; this.InteractableLinkClicked = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 80 BB ?? ?? ?? ?? ?? 0F 85 ?? ?? ?? ?? 80 BB") + 9;
}
} }
} }

View file

@ -9,302 +9,303 @@ using Dalamud.IoC.Internal;
using Dalamud.Memory; using Dalamud.Memory;
using Serilog; using Serilog;
namespace Dalamud.Game.Gui.FlyText; namespace Dalamud.Game.Gui.FlyText
/// <summary>
/// This class facilitates interacting with and creating native in-game "fly text".
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed class FlyTextGui : IDisposable
{ {
/// <summary> /// <summary>
/// The native function responsible for adding fly text to the UI. See <see cref="FlyTextGuiAddressResolver.AddFlyText"/>. /// This class facilitates interacting with and creating native in-game "fly text".
/// </summary> /// </summary>
private readonly AddFlyTextDelegate addFlyTextNative; [PluginInterface]
[InterfaceVersion("1.0")]
/// <summary> public sealed class FlyTextGui : IDisposable
/// The hook that fires when the game creates a fly text element. See <see cref="FlyTextGuiAddressResolver.CreateFlyText"/>.
/// </summary>
private readonly Hook<CreateFlyTextDelegate> createFlyTextHook;
/// <summary>
/// Initializes a new instance of the <see cref="FlyTextGui"/> class.
/// </summary>
internal FlyTextGui()
{ {
this.Address = new FlyTextGuiAddressResolver(); /// <summary>
this.Address.Setup(); /// The native function responsible for adding fly text to the UI. See <see cref="FlyTextGuiAddressResolver.AddFlyText"/>.
/// </summary>
private readonly AddFlyTextDelegate addFlyTextNative;
this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer<AddFlyTextDelegate>(this.Address.AddFlyText); /// <summary>
this.createFlyTextHook = new Hook<CreateFlyTextDelegate>(this.Address.CreateFlyText, this.CreateFlyTextDetour); /// The hook that fires when the game creates a fly text element. See <see cref="FlyTextGuiAddressResolver.CreateFlyText"/>.
} /// </summary>
private readonly Hook<CreateFlyTextDelegate> createFlyTextHook;
/// <summary> /// <summary>
/// The delegate defining the type for the FlyText event. /// Initializes a new instance of the <see cref="FlyTextGui"/> class.
/// </summary> /// </summary>
/// <param name="kind">The FlyTextKind. See <see cref="FlyTextKind"/>.</param> internal FlyTextGui()
/// <param name="val1">Value1 passed to the native flytext function.</param>
/// <param name="val2">Value2 passed to the native flytext function. Seems unused.</param>
/// <param name="text1">Text1 passed to the native flytext function.</param>
/// <param name="text2">Text2 passed to the native flytext function.</param>
/// <param name="color">Color passed to the native flytext function. Changes flytext color.</param>
/// <param name="icon">Icon ID passed to the native flytext function. Only displays with select FlyTextKind.</param>
/// <param name="yOffset">The vertical offset to place the flytext at. 0 is default. Negative values result
/// in text appearing higher on the screen. This does not change where the element begins to fade.</param>
/// <param name="handled">Whether this flytext has been handled. If a subscriber sets this to true, the FlyText will not appear.</param>
public delegate void OnFlyTextCreatedDelegate(
ref FlyTextKind kind,
ref int val1,
ref int val2,
ref SeString text1,
ref SeString text2,
ref uint color,
ref uint icon,
ref float yOffset,
ref bool handled);
/// <summary>
/// Private delegate for the native CreateFlyText function's hook.
/// </summary>
private delegate IntPtr CreateFlyTextDelegate(
IntPtr addonFlyText,
FlyTextKind kind,
int val1,
int val2,
IntPtr text2,
uint color,
uint icon,
IntPtr text1,
float yOffset);
/// <summary>
/// Private delegate for the native AddFlyText function pointer.
/// </summary>
private delegate void AddFlyTextDelegate(
IntPtr addonFlyText,
uint actorIndex,
uint messageMax,
IntPtr numbers,
uint offsetNum,
uint offsetNumMax,
IntPtr strings,
uint offsetStr,
uint offsetStrMax,
int unknown);
/// <summary>
/// The FlyText event that can be subscribed to.
/// </summary>
public event OnFlyTextCreatedDelegate? FlyTextCreated;
private Dalamud Dalamud { get; }
private FlyTextGuiAddressResolver Address { get; }
/// <summary>
/// Disposes of managed and unmanaged resources.
/// </summary>
public void Dispose()
{
this.createFlyTextHook.Dispose();
}
/// <summary>
/// Displays a fly text in-game on the local player.
/// </summary>
/// <param name="kind">The FlyTextKind. See <see cref="FlyTextKind"/>.</param>
/// <param name="actorIndex">The index of the actor to place flytext on. Indexing unknown. 1 places flytext on local player.</param>
/// <param name="val1">Value1 passed to the native flytext function.</param>
/// <param name="val2">Value2 passed to the native flytext function. Seems unused.</param>
/// <param name="text1">Text1 passed to the native flytext function.</param>
/// <param name="text2">Text2 passed to the native flytext function.</param>
/// <param name="color">Color passed to the native flytext function. Changes flytext color.</param>
/// <param name="icon">Icon ID passed to the native flytext function. Only displays with select FlyTextKind.</param>
public unsafe void AddFlyText(FlyTextKind kind, uint actorIndex, uint val1, uint val2, SeString text1, SeString text2, uint color, uint icon)
{
// Known valid flytext region within the atk arrays
var numIndex = 28;
var strIndex = 25;
var numOffset = 147u;
var strOffset = 28u;
// Get the UI module and flytext addon pointers
var gameGui = Service<GameGui>.Get();
var ui = (FFXIVClientStructs.FFXIV.Client.UI.UIModule*)gameGui.GetUIModule();
var flytext = gameGui.GetAddonByName("_FlyText", 1);
if (ui == null || flytext == IntPtr.Zero)
return;
// Get the number and string arrays we need
var atkArrayDataHolder = ui->RaptureAtkModule.AtkModule.AtkArrayDataHolder;
var numArray = atkArrayDataHolder._NumberArrays[numIndex];
var strArray = atkArrayDataHolder._StringArrays[strIndex];
// Write the values to the arrays using a known valid flytext region
numArray->IntArray[numOffset + 0] = 1; // Some kind of "Enabled" flag for this section
numArray->IntArray[numOffset + 1] = (int)kind;
numArray->IntArray[numOffset + 2] = unchecked((int)val1);
numArray->IntArray[numOffset + 3] = unchecked((int)val2);
numArray->IntArray[numOffset + 4] = 5; // Unknown
numArray->IntArray[numOffset + 5] = unchecked((int)color);
numArray->IntArray[numOffset + 6] = unchecked((int)icon);
numArray->IntArray[numOffset + 7] = 0; // Unknown
numArray->IntArray[numOffset + 8] = 0; // Unknown, has something to do with yOffset
fixed (byte* pText1 = text1.Encode())
{ {
fixed (byte* pText2 = text2.Encode()) this.Address = new FlyTextGuiAddressResolver();
{ this.Address.Setup();
strArray->StringArray[strOffset + 0] = pText1;
strArray->StringArray[strOffset + 1] = pText2;
this.addFlyTextNative( this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer<AddFlyTextDelegate>(this.Address.AddFlyText);
flytext, this.createFlyTextHook = new Hook<CreateFlyTextDelegate>(this.Address.CreateFlyText, this.CreateFlyTextDetour);
actorIndex,
1,
(IntPtr)numArray,
numOffset,
9,
(IntPtr)strArray,
strOffset,
2,
0);
}
} }
}
/// <summary> /// <summary>
/// Enables this module. /// The delegate defining the type for the FlyText event.
/// </summary> /// </summary>
internal void Enable() /// <param name="kind">The FlyTextKind. See <see cref="FlyTextKind"/>.</param>
{ /// <param name="val1">Value1 passed to the native flytext function.</param>
this.createFlyTextHook.Enable(); /// <param name="val2">Value2 passed to the native flytext function. Seems unused.</param>
} /// <param name="text1">Text1 passed to the native flytext function.</param>
/// <param name="text2">Text2 passed to the native flytext function.</param>
/// <param name="color">Color passed to the native flytext function. Changes flytext color.</param>
/// <param name="icon">Icon ID passed to the native flytext function. Only displays with select FlyTextKind.</param>
/// <param name="yOffset">The vertical offset to place the flytext at. 0 is default. Negative values result
/// in text appearing higher on the screen. This does not change where the element begins to fade.</param>
/// <param name="handled">Whether this flytext has been handled. If a subscriber sets this to true, the FlyText will not appear.</param>
public delegate void OnFlyTextCreatedDelegate(
ref FlyTextKind kind,
ref int val1,
ref int val2,
ref SeString text1,
ref SeString text2,
ref uint color,
ref uint icon,
ref float yOffset,
ref bool handled);
private static byte[] Terminate(byte[] source) /// <summary>
{ /// Private delegate for the native CreateFlyText function's hook.
var terminated = new byte[source.Length + 1]; /// </summary>
Array.Copy(source, 0, terminated, 0, source.Length); private delegate IntPtr CreateFlyTextDelegate(
terminated[^1] = 0; IntPtr addonFlyText,
FlyTextKind kind,
int val1,
int val2,
IntPtr text2,
uint color,
uint icon,
IntPtr text1,
float yOffset);
return terminated; /// <summary>
} /// Private delegate for the native AddFlyText function pointer.
/// </summary>
private delegate void AddFlyTextDelegate(
IntPtr addonFlyText,
uint actorIndex,
uint messageMax,
IntPtr numbers,
uint offsetNum,
uint offsetNumMax,
IntPtr strings,
uint offsetStr,
uint offsetStrMax,
int unknown);
private IntPtr CreateFlyTextDetour( /// <summary>
IntPtr addonFlyText, /// The FlyText event that can be subscribed to.
FlyTextKind kind, /// </summary>
int val1, public event OnFlyTextCreatedDelegate? FlyTextCreated;
int val2,
IntPtr text2, private Dalamud Dalamud { get; }
uint color,
uint icon, private FlyTextGuiAddressResolver Address { get; }
IntPtr text1,
float yOffset) /// <summary>
{ /// Disposes of managed and unmanaged resources.
var retVal = IntPtr.Zero; /// </summary>
try public void Dispose()
{ {
Log.Verbose("[FlyText] Enter CreateFlyText detour!"); this.createFlyTextHook.Dispose();
}
var handled = false; /// <summary>
/// Displays a fly text in-game on the local player.
/// </summary>
/// <param name="kind">The FlyTextKind. See <see cref="FlyTextKind"/>.</param>
/// <param name="actorIndex">The index of the actor to place flytext on. Indexing unknown. 1 places flytext on local player.</param>
/// <param name="val1">Value1 passed to the native flytext function.</param>
/// <param name="val2">Value2 passed to the native flytext function. Seems unused.</param>
/// <param name="text1">Text1 passed to the native flytext function.</param>
/// <param name="text2">Text2 passed to the native flytext function.</param>
/// <param name="color">Color passed to the native flytext function. Changes flytext color.</param>
/// <param name="icon">Icon ID passed to the native flytext function. Only displays with select FlyTextKind.</param>
public unsafe void AddFlyText(FlyTextKind kind, uint actorIndex, uint val1, uint val2, SeString text1, SeString text2, uint color, uint icon)
{
// Known valid flytext region within the atk arrays
var numIndex = 28;
var strIndex = 25;
var numOffset = 147u;
var strOffset = 28u;
var tmpKind = kind; // Get the UI module and flytext addon pointers
var tmpVal1 = val1; var gameGui = Service<GameGui>.Get();
var tmpVal2 = val2; var ui = (FFXIVClientStructs.FFXIV.Client.UI.UIModule*)gameGui.GetUIModule();
var tmpText1 = text1 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(text1); var flytext = gameGui.GetAddonByName("_FlyText", 1);
var tmpText2 = text2 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(text2);
var tmpColor = color;
var tmpIcon = icon;
var tmpYOffset = yOffset;
var cmpText1 = tmpText1.ToString(); if (ui == null || flytext == IntPtr.Zero)
var cmpText2 = tmpText2.ToString(); return;
Log.Verbose($"[FlyText] Called with addonFlyText({addonFlyText.ToInt64():X}) " + // Get the number and string arrays we need
$"kind({kind}) val1({val1}) val2({val2}) " + var atkArrayDataHolder = ui->RaptureAtkModule.AtkModule.AtkArrayDataHolder;
$"text1({text1.ToInt64():X}, \"{tmpText1}\") text2({text2.ToInt64():X}, \"{tmpText2}\") " + var numArray = atkArrayDataHolder._NumberArrays[numIndex];
$"color({color:X}) icon({icon}) yOffset({yOffset})"); var strArray = atkArrayDataHolder._StringArrays[strIndex];
Log.Verbose("[FlyText] Calling flytext events!");
this.FlyTextCreated?.Invoke(
ref tmpKind,
ref tmpVal1,
ref tmpVal2,
ref tmpText1,
ref tmpText2,
ref tmpColor,
ref tmpIcon,
ref tmpYOffset,
ref handled);
// If handled, ignore the original call // Write the values to the arrays using a known valid flytext region
if (handled) numArray->IntArray[numOffset + 0] = 1; // Some kind of "Enabled" flag for this section
numArray->IntArray[numOffset + 1] = (int)kind;
numArray->IntArray[numOffset + 2] = unchecked((int)val1);
numArray->IntArray[numOffset + 3] = unchecked((int)val2);
numArray->IntArray[numOffset + 4] = 5; // Unknown
numArray->IntArray[numOffset + 5] = unchecked((int)color);
numArray->IntArray[numOffset + 6] = unchecked((int)icon);
numArray->IntArray[numOffset + 7] = 0; // Unknown
numArray->IntArray[numOffset + 8] = 0; // Unknown, has something to do with yOffset
fixed (byte* pText1 = text1.Encode())
{ {
Log.Verbose("[FlyText] FlyText was handled."); fixed (byte* pText2 = text2.Encode())
// Returning null to AddFlyText from CreateFlyText will result
// in the operation being dropped entirely.
return IntPtr.Zero;
}
// Check if any values have changed
var dirty = tmpKind != kind ||
tmpVal1 != val1 ||
tmpVal2 != val2 ||
tmpText1.ToString() != cmpText1 ||
tmpText2.ToString() != cmpText2 ||
tmpColor != color ||
tmpIcon != icon ||
Math.Abs(tmpYOffset - yOffset) > float.Epsilon;
// If not dirty, make the original call
if (!dirty)
{
Log.Verbose("[FlyText] Calling flytext with original args.");
return this.createFlyTextHook.Original(addonFlyText, kind, val1, val2, text2, color, icon, text1, yOffset);
}
var terminated1 = Terminate(tmpText1.Encode());
var terminated2 = Terminate(tmpText2.Encode());
var pText1 = Marshal.AllocHGlobal(terminated1.Length);
var pText2 = Marshal.AllocHGlobal(terminated2.Length);
Marshal.Copy(terminated1, 0, pText1, terminated1.Length);
Marshal.Copy(terminated2, 0, pText2, terminated2.Length);
Log.Verbose("[FlyText] Allocated and set strings.");
retVal = this.createFlyTextHook.Original(
addonFlyText,
tmpKind,
tmpVal1,
tmpVal2,
pText2,
tmpColor,
tmpIcon,
pText1,
tmpYOffset);
Log.Verbose("[FlyText] Returned from original. Delaying free task.");
Task.Delay(2000).ContinueWith(_ =>
{
try
{ {
Marshal.FreeHGlobal(pText1); strArray->StringArray[strOffset + 0] = pText1;
Marshal.FreeHGlobal(pText2); strArray->StringArray[strOffset + 1] = pText2;
Log.Verbose("[FlyText] Freed strings.");
this.addFlyTextNative(
flytext,
actorIndex,
1,
(IntPtr)numArray,
numOffset,
9,
(IntPtr)strArray,
strOffset,
2,
0);
} }
catch (Exception e) }
{
Log.Verbose(e, "[FlyText] Exception occurred freeing strings in task.");
}
});
}
catch (Exception e)
{
Log.Error(e, "Exception occurred in CreateFlyTextDetour!");
} }
return retVal; /// <summary>
/// Enables this module.
/// </summary>
internal void Enable()
{
this.createFlyTextHook.Enable();
}
private static byte[] Terminate(byte[] source)
{
var terminated = new byte[source.Length + 1];
Array.Copy(source, 0, terminated, 0, source.Length);
terminated[^1] = 0;
return terminated;
}
private IntPtr CreateFlyTextDetour(
IntPtr addonFlyText,
FlyTextKind kind,
int val1,
int val2,
IntPtr text2,
uint color,
uint icon,
IntPtr text1,
float yOffset)
{
var retVal = IntPtr.Zero;
try
{
Log.Verbose("[FlyText] Enter CreateFlyText detour!");
var handled = false;
var tmpKind = kind;
var tmpVal1 = val1;
var tmpVal2 = val2;
var tmpText1 = text1 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(text1);
var tmpText2 = text2 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(text2);
var tmpColor = color;
var tmpIcon = icon;
var tmpYOffset = yOffset;
var cmpText1 = tmpText1.ToString();
var cmpText2 = tmpText2.ToString();
Log.Verbose($"[FlyText] Called with addonFlyText({addonFlyText.ToInt64():X}) " +
$"kind({kind}) val1({val1}) val2({val2}) " +
$"text1({text1.ToInt64():X}, \"{tmpText1}\") text2({text2.ToInt64():X}, \"{tmpText2}\") " +
$"color({color:X}) icon({icon}) yOffset({yOffset})");
Log.Verbose("[FlyText] Calling flytext events!");
this.FlyTextCreated?.Invoke(
ref tmpKind,
ref tmpVal1,
ref tmpVal2,
ref tmpText1,
ref tmpText2,
ref tmpColor,
ref tmpIcon,
ref tmpYOffset,
ref handled);
// If handled, ignore the original call
if (handled)
{
Log.Verbose("[FlyText] FlyText was handled.");
// Returning null to AddFlyText from CreateFlyText will result
// in the operation being dropped entirely.
return IntPtr.Zero;
}
// Check if any values have changed
var dirty = tmpKind != kind ||
tmpVal1 != val1 ||
tmpVal2 != val2 ||
tmpText1.ToString() != cmpText1 ||
tmpText2.ToString() != cmpText2 ||
tmpColor != color ||
tmpIcon != icon ||
Math.Abs(tmpYOffset - yOffset) > float.Epsilon;
// If not dirty, make the original call
if (!dirty)
{
Log.Verbose("[FlyText] Calling flytext with original args.");
return this.createFlyTextHook.Original(addonFlyText, kind, val1, val2, text2, color, icon, text1, yOffset);
}
var terminated1 = Terminate(tmpText1.Encode());
var terminated2 = Terminate(tmpText2.Encode());
var pText1 = Marshal.AllocHGlobal(terminated1.Length);
var pText2 = Marshal.AllocHGlobal(terminated2.Length);
Marshal.Copy(terminated1, 0, pText1, terminated1.Length);
Marshal.Copy(terminated2, 0, pText2, terminated2.Length);
Log.Verbose("[FlyText] Allocated and set strings.");
retVal = this.createFlyTextHook.Original(
addonFlyText,
tmpKind,
tmpVal1,
tmpVal2,
pText2,
tmpColor,
tmpIcon,
pText1,
tmpYOffset);
Log.Verbose("[FlyText] Returned from original. Delaying free task.");
Task.Delay(2000).ContinueWith(_ =>
{
try
{
Marshal.FreeHGlobal(pText1);
Marshal.FreeHGlobal(pText2);
Log.Verbose("[FlyText] Freed strings.");
}
catch (Exception e)
{
Log.Verbose(e, "[FlyText] Exception occurred freeing strings in task.");
}
});
}
catch (Exception e)
{
Log.Error(e, "Exception occurred in CreateFlyTextDetour!");
}
return retVal;
}
} }
} }

View file

@ -1,31 +1,32 @@
using System; using System;
namespace Dalamud.Game.Gui.FlyText; namespace Dalamud.Game.Gui.FlyText
/// <summary>
/// An address resolver for the <see cref="FlyTextGui"/> class.
/// </summary>
public class FlyTextGuiAddressResolver : BaseAddressResolver
{ {
/// <summary> /// <summary>
/// Gets the address of the native AddFlyText method, which occurs /// An address resolver for the <see cref="FlyTextGui"/> class.
/// when the game adds fly text elements to the UI. Multiple fly text
/// elements can be added in a single AddFlyText call.
/// </summary> /// </summary>
public IntPtr AddFlyText { get; private set; } public class FlyTextGuiAddressResolver : BaseAddressResolver
/// <summary>
/// Gets the address of the native CreateFlyText method, which occurs
/// when the game creates a new fly text element. This method is called
/// once per fly text element, and can be called multiple times per
/// AddFlyText call.
/// </summary>
public IntPtr CreateFlyText { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{ {
this.AddFlyText = sig.ScanText("E8 ?? ?? ?? ?? FF C7 41 D1 C7"); /// <summary>
this.CreateFlyText = sig.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 40 48 63 FA"); /// Gets the address of the native AddFlyText method, which occurs
/// when the game adds fly text elements to the UI. Multiple fly text
/// elements can be added in a single AddFlyText call.
/// </summary>
public IntPtr AddFlyText { get; private set; }
/// <summary>
/// Gets the address of the native CreateFlyText method, which occurs
/// when the game creates a new fly text element. This method is called
/// once per fly text element, and can be called multiple times per
/// AddFlyText call.
/// </summary>
public IntPtr CreateFlyText { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
this.AddFlyText = sig.ScanText("E8 ?? ?? ?? ?? FF C7 41 D1 C7");
this.CreateFlyText = sig.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 40 48 63 FA");
}
} }
} }

View file

@ -1,282 +1,283 @@
namespace Dalamud.Game.Gui.FlyText; namespace Dalamud.Game.Gui.FlyText
/// <summary>
/// Enum of FlyTextKind values. Members suffixed with
/// a number seem to be a duplicate, or perform duplicate behavior.
/// </summary>
public enum FlyTextKind : int
{ {
/// <summary> /// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle. /// Enum of FlyTextKind values. Members suffixed with
/// Used for autos and incoming DoTs. /// a number seem to be a duplicate, or perform duplicate behavior.
/// </summary> /// </summary>
AutoAttack = 0, public enum FlyTextKind : int
{
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle.
/// Used for autos and incoming DoTs.
/// </summary>
AutoAttack = 0,
/// <summary> /// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle. /// Val1 in serif font, Text2 in sans-serif as subtitle.
/// Does a bounce effect on appearance. /// Does a bounce effect on appearance.
/// </summary> /// </summary>
DirectHit = 1, DirectHit = 1,
/// <summary> /// <summary>
/// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle.
/// Does a bigger bounce effect on appearance. /// Does a bigger bounce effect on appearance.
/// </summary> /// </summary>
CriticalHit = 2, CriticalHit = 2,
/// <summary> /// <summary>
/// Val1 in even larger serif font with 2 exclamations, Text2 in /// Val1 in even larger serif font with 2 exclamations, Text2 in
/// sans-serif as subtitle. Does a large bounce effect on appearance. /// sans-serif as subtitle. Does a large bounce effect on appearance.
/// Does not scroll up or down the screen. /// Does not scroll up or down the screen.
/// </summary> /// </summary>
CriticalDirectHit = 3, CriticalDirectHit = 3,
/// <summary> /// <summary>
/// AutoAttack with sans-serif Text1 to the left of the Val1. /// AutoAttack with sans-serif Text1 to the left of the Val1.
/// </summary> /// </summary>
NamedAttack = 4, NamedAttack = 4,
/// <summary> /// <summary>
/// DirectHit with sans-serif Text1 to the left of the Val1. /// DirectHit with sans-serif Text1 to the left of the Val1.
/// </summary> /// </summary>
NamedDirectHit = 5, NamedDirectHit = 5,
/// <summary> /// <summary>
/// CriticalHit with sans-serif Text1 to the left of the Val1. /// CriticalHit with sans-serif Text1 to the left of the Val1.
/// </summary> /// </summary>
NamedCriticalHit = 6, NamedCriticalHit = 6,
/// <summary> /// <summary>
/// CriticalDirectHit with sans-serif Text1 to the left of the Val1. /// CriticalDirectHit with sans-serif Text1 to the left of the Val1.
/// </summary> /// </summary>
NamedCriticalDirectHit = 7, NamedCriticalDirectHit = 7,
/// <summary> /// <summary>
/// All caps, serif MISS. /// All caps, serif MISS.
/// </summary> /// </summary>
Miss = 8, Miss = 8,
/// <summary> /// <summary>
/// Sans-serif Text1 next to all caps serif MISS. /// Sans-serif Text1 next to all caps serif MISS.
/// </summary> /// </summary>
NamedMiss = 9, NamedMiss = 9,
/// <summary> /// <summary>
/// All caps serif DODGE. /// All caps serif DODGE.
/// </summary> /// </summary>
Dodge = 10, Dodge = 10,
/// <summary> /// <summary>
/// Sans-serif Text1 next to all caps serif DODGE. /// Sans-serif Text1 next to all caps serif DODGE.
/// </summary> /// </summary>
NamedDodge = 11, NamedDodge = 11,
/// <summary> /// <summary>
/// Icon next to sans-serif Text1. /// Icon next to sans-serif Text1.
/// </summary> /// </summary>
NamedIcon = 12, NamedIcon = 12,
/// <summary> /// <summary>
/// Icon next to sans-serif Text1 (2). /// Icon next to sans-serif Text1 (2).
/// </summary> /// </summary>
NamedIcon2 = 13, NamedIcon2 = 13,
/// <summary> /// <summary>
/// Serif Val1 with all caps condensed font EXP with Text2 in sans-serif as subtitle. /// Serif Val1 with all caps condensed font EXP with Text2 in sans-serif as subtitle.
/// </summary> /// </summary>
Exp = 14, Exp = 14,
/// <summary> /// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle. /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle.
/// </summary> /// </summary>
NamedMp = 15, NamedMp = 15,
/// <summary> /// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle. /// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle.
/// </summary> /// </summary>
NamedTp = 16, NamedTp = 16,
/// <summary> /// <summary>
/// AutoAttack with sans-serif Text1 to the left of the Val1 (2). /// AutoAttack with sans-serif Text1 to the left of the Val1 (2).
/// </summary> /// </summary>
NamedAttack2 = 17, NamedAttack2 = 17,
/// <summary> /// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle (2). /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle (2).
/// </summary> /// </summary>
NamedMp2 = 18, NamedMp2 = 18,
/// <summary> /// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle (2). /// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle (2).
/// </summary> /// </summary>
NamedTp2 = 19, NamedTp2 = 19,
/// <summary> /// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font EP with Text2 in sans-serif as subtitle. /// Sans-serif Text1 next to serif Val1 with all caps condensed font EP with Text2 in sans-serif as subtitle.
/// </summary> /// </summary>
NamedEp = 20, NamedEp = 20,
/// <summary> /// <summary>
/// Displays nothing. /// Displays nothing.
/// </summary> /// </summary>
None = 21, None = 21,
/// <summary> /// <summary>
/// All caps serif INVULNERABLE. /// All caps serif INVULNERABLE.
/// </summary> /// </summary>
Invulnerable = 22, Invulnerable = 22,
/// <summary> /// <summary>
/// All caps sans-serif condensed font INTERRUPTED! /// All caps sans-serif condensed font INTERRUPTED!
/// Does a large bounce effect on appearance. /// Does a large bounce effect on appearance.
/// Does not scroll up or down the screen. /// Does not scroll up or down the screen.
/// </summary> /// </summary>
Interrupted = 23, Interrupted = 23,
/// <summary> /// <summary>
/// AutoAttack with no Text2. /// AutoAttack with no Text2.
/// </summary> /// </summary>
AutoAttackNoText = 24, AutoAttackNoText = 24,
/// <summary> /// <summary>
/// AutoAttack with no Text2 (2). /// AutoAttack with no Text2 (2).
/// </summary> /// </summary>
AutoAttackNoText2 = 25, AutoAttackNoText2 = 25,
/// <summary> /// <summary>
/// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. Does a bigger bounce effect on appearance (2). /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. Does a bigger bounce effect on appearance (2).
/// </summary> /// </summary>
CriticalHit2 = 26, CriticalHit2 = 26,
/// <summary> /// <summary>
/// AutoAttack with no Text2 (3). /// AutoAttack with no Text2 (3).
/// </summary> /// </summary>
AutoAttackNoText3 = 27, AutoAttackNoText3 = 27,
/// <summary> /// <summary>
/// CriticalHit with sans-serif Text1 to the left of the Val1 (2). /// CriticalHit with sans-serif Text1 to the left of the Val1 (2).
/// </summary> /// </summary>
NamedCriticalHit2 = 28, NamedCriticalHit2 = 28,
/// <summary> /// <summary>
/// Same as NamedCriticalHit with a green (cannot change) MP in condensed font to the right of Val1. /// Same as NamedCriticalHit with a green (cannot change) MP in condensed font to the right of Val1.
/// Does a jiggle effect to the right on appearance. /// Does a jiggle effect to the right on appearance.
/// </summary> /// </summary>
NamedCriticalHitWithMp = 29, NamedCriticalHitWithMp = 29,
/// <summary> /// <summary>
/// Same as NamedCriticalHit with a yellow (cannot change) TP in condensed font to the right of Val1. /// Same as NamedCriticalHit with a yellow (cannot change) TP in condensed font to the right of Val1.
/// Does a jiggle effect to the right on appearance. /// Does a jiggle effect to the right on appearance.
/// </summary> /// </summary>
NamedCriticalHitWithTp = 30, NamedCriticalHitWithTp = 30,
/// <summary> /// <summary>
/// Same as NamedIcon with sans-serif "has no effect!" to the right. /// Same as NamedIcon with sans-serif "has no effect!" to the right.
/// </summary> /// </summary>
NamedIconHasNoEffect = 31, NamedIconHasNoEffect = 31,
/// <summary> /// <summary>
/// Same as NamedIcon but Text1 is slightly faded. Used for buff expiration. /// Same as NamedIcon but Text1 is slightly faded. Used for buff expiration.
/// </summary> /// </summary>
NamedIconFaded = 32, NamedIconFaded = 32,
/// <summary> /// <summary>
/// Same as NamedIcon but Text1 is slightly faded (2). /// Same as NamedIcon but Text1 is slightly faded (2).
/// Used for buff expiration. /// Used for buff expiration.
/// </summary> /// </summary>
NamedIconFaded2 = 33, NamedIconFaded2 = 33,
/// <summary> /// <summary>
/// Text1 in sans-serif font. /// Text1 in sans-serif font.
/// </summary> /// </summary>
Named = 34, Named = 34,
/// <summary> /// <summary>
/// Same as NamedIcon with sans-serif "(fully resisted)" to the right. /// Same as NamedIcon with sans-serif "(fully resisted)" to the right.
/// </summary> /// </summary>
NamedIconFullyResisted = 35, NamedIconFullyResisted = 35,
/// <summary> /// <summary>
/// All caps serif 'INCAPACITATED!'. /// All caps serif 'INCAPACITATED!'.
/// </summary> /// </summary>
Incapacitated = 36, Incapacitated = 36,
/// <summary> /// <summary>
/// Text1 with sans-serif "(fully resisted)" to the right. /// Text1 with sans-serif "(fully resisted)" to the right.
/// </summary> /// </summary>
NamedFullyResisted = 37, NamedFullyResisted = 37,
/// <summary> /// <summary>
/// Text1 with sans-serif "has no effect!" to the right. /// Text1 with sans-serif "has no effect!" to the right.
/// </summary> /// </summary>
NamedHasNoEffect = 38, NamedHasNoEffect = 38,
/// <summary> /// <summary>
/// AutoAttack with sans-serif Text1 to the left of the Val1 (3). /// AutoAttack with sans-serif Text1 to the left of the Val1 (3).
/// </summary> /// </summary>
NamedAttack3 = 39, NamedAttack3 = 39,
/// <summary> /// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle (3). /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle (3).
/// </summary> /// </summary>
NamedMp3 = 40, NamedMp3 = 40,
/// <summary> /// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle (3). /// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle (3).
/// </summary> /// </summary>
NamedTp3 = 41, NamedTp3 = 41,
/// <summary> /// <summary>
/// Same as NamedIcon with serif "INVULNERABLE!" beneath the Text1. /// Same as NamedIcon with serif "INVULNERABLE!" beneath the Text1.
/// </summary> /// </summary>
NamedIconInvulnerable = 42, NamedIconInvulnerable = 42,
/// <summary> /// <summary>
/// All caps serif RESIST. /// All caps serif RESIST.
/// </summary> /// </summary>
Resist = 43, Resist = 43,
/// <summary> /// <summary>
/// Same as NamedIcon but places the given icon in the item icon outline. /// Same as NamedIcon but places the given icon in the item icon outline.
/// </summary> /// </summary>
NamedIconWithItemOutline = 44, NamedIconWithItemOutline = 44,
/// <summary> /// <summary>
/// AutoAttack with no Text2 (4). /// AutoAttack with no Text2 (4).
/// </summary> /// </summary>
AutoAttackNoText4 = 45, AutoAttackNoText4 = 45,
/// <summary> /// <summary>
/// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle (3). /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle (3).
/// Does a bigger bounce effect on appearance. /// Does a bigger bounce effect on appearance.
/// </summary> /// </summary>
CriticalHit3 = 46, CriticalHit3 = 46,
/// <summary> /// <summary>
/// All caps serif REFLECT. /// All caps serif REFLECT.
/// </summary> /// </summary>
Reflect = 47, Reflect = 47,
/// <summary> /// <summary>
/// All caps serif REFLECTED. /// All caps serif REFLECTED.
/// </summary> /// </summary>
Reflected = 48, Reflected = 48,
/// <summary> /// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle (2). /// Val1 in serif font, Text2 in sans-serif as subtitle (2).
/// Does a bounce effect on appearance. /// Does a bounce effect on appearance.
/// </summary> /// </summary>
DirectHit2 = 49, DirectHit2 = 49,
/// <summary> /// <summary>
/// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle (4). /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle (4).
/// Does a bigger bounce effect on appearance. /// Does a bigger bounce effect on appearance.
/// </summary> /// </summary>
CriticalHit4 = 50, CriticalHit4 = 50,
/// <summary> /// <summary>
/// Val1 in even larger serif font with 2 exclamations, Text2 in sans-serif as subtitle (2). /// Val1 in even larger serif font with 2 exclamations, Text2 in sans-serif as subtitle (2).
/// Does a large bounce effect on appearance. Does not scroll up or down the screen. /// Does a large bounce effect on appearance. Does not scroll up or down the screen.
/// </summary> /// </summary>
CriticalDirectHit2 = 51, CriticalDirectHit2 = 51,
}
} }

File diff suppressed because it is too large Load diff

View file

@ -1,103 +1,104 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Dalamud.Game.Gui; namespace Dalamud.Game.Gui
/// <summary>
/// The address resolver for the <see cref="GameGui"/> class.
/// </summary>
internal sealed class GameGuiAddressResolver : BaseAddressResolver
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GameGuiAddressResolver"/> class. /// The address resolver for the <see cref="GameGui"/> class.
/// </summary> /// </summary>
public GameGuiAddressResolver() internal sealed class GameGuiAddressResolver : BaseAddressResolver
{ {
this.BaseAddress = Service<Framework>.Get().Address.BaseAddress; /// <summary>
} /// Initializes a new instance of the <see cref="GameGuiAddressResolver"/> class.
/// </summary>
public GameGuiAddressResolver()
{
this.BaseAddress = Service<Framework>.Get().Address.BaseAddress;
}
/// <summary> /// <summary>
/// Gets the base address of the native GuiManager class. /// Gets the base address of the native GuiManager class.
/// </summary> /// </summary>
public IntPtr BaseAddress { get; private set; } public IntPtr BaseAddress { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the native ChatManager class. /// Gets the address of the native ChatManager class.
/// </summary> /// </summary>
public IntPtr ChatManager { get; private set; } public IntPtr ChatManager { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the native SetGlobalBgm method. /// Gets the address of the native SetGlobalBgm method.
/// </summary> /// </summary>
public IntPtr SetGlobalBgm { get; private set; } public IntPtr SetGlobalBgm { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the native HandleItemHover method. /// Gets the address of the native HandleItemHover method.
/// </summary> /// </summary>
public IntPtr HandleItemHover { get; private set; } public IntPtr HandleItemHover { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the native HandleItemOut method. /// Gets the address of the native HandleItemOut method.
/// </summary> /// </summary>
public IntPtr HandleItemOut { get; private set; } public IntPtr HandleItemOut { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the native HandleActionHover method. /// Gets the address of the native HandleActionHover method.
/// </summary> /// </summary>
public IntPtr HandleActionHover { get; private set; } public IntPtr HandleActionHover { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the native HandleActionOut method. /// Gets the address of the native HandleActionOut method.
/// </summary> /// </summary>
public IntPtr HandleActionOut { get; private set; } public IntPtr HandleActionOut { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the native HandleImm method. /// Gets the address of the native HandleImm method.
/// </summary> /// </summary>
public IntPtr HandleImm { get; private set; } public IntPtr HandleImm { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the native GetMatrixSingleton method. /// Gets the address of the native GetMatrixSingleton method.
/// </summary> /// </summary>
public IntPtr GetMatrixSingleton { get; private set; } public IntPtr GetMatrixSingleton { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the native ScreenToWorld method. /// Gets the address of the native ScreenToWorld method.
/// </summary> /// </summary>
public IntPtr ScreenToWorld { get; private set; } public IntPtr ScreenToWorld { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the native ToggleUiHide method. /// Gets the address of the native ToggleUiHide method.
/// </summary> /// </summary>
public IntPtr ToggleUiHide { get; private set; } public IntPtr ToggleUiHide { get; private set; }
/// <summary> /// <summary>
/// Gets the address of the native GetAgentModule method. /// Gets the address of the native GetAgentModule method.
/// </summary> /// </summary>
public IntPtr GetAgentModule { get; private set; } public IntPtr GetAgentModule { get; private set; }
/// <inheritdoc/> /// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig) protected override void Setup64Bit(SigScanner sig)
{ {
this.SetGlobalBgm = sig.ScanText("4C 8B 15 ?? ?? ?? ?? 4D 85 D2 74 58"); this.SetGlobalBgm = sig.ScanText("4C 8B 15 ?? ?? ?? ?? 4D 85 D2 74 58");
this.HandleItemHover = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 5C 24 ?? 48 89 AE ?? ?? ?? ??"); this.HandleItemHover = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 5C 24 ?? 48 89 AE ?? ?? ?? ??");
this.HandleItemOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 4D"); this.HandleItemOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 4D");
this.HandleActionHover = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 83 F8 0F"); this.HandleActionHover = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 83 F8 0F");
this.HandleActionOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B DA 48 8B F9 4D 85 C0 74 1F"); this.HandleActionOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B DA 48 8B F9 4D 85 C0 74 1F");
this.HandleImm = sig.ScanText("E8 ?? ?? ?? ?? 84 C0 75 10 48 83 FF 09"); this.HandleImm = sig.ScanText("E8 ?? ?? ?? ?? 84 C0 75 10 48 83 FF 09");
this.GetMatrixSingleton = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 4C 24 ?? 48 89 4c 24 ?? 4C 8D 4D ?? 4C 8D 44 24 ??"); this.GetMatrixSingleton = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 4C 24 ?? 48 89 4c 24 ?? 4C 8D 4D ?? 4C 8D 44 24 ??");
this.ScreenToWorld = sig.ScanText("48 83 EC 48 48 8B 05 ?? ?? ?? ?? 4D 8B D1"); this.ScreenToWorld = sig.ScanText("48 83 EC 48 48 8B 05 ?? ?? ?? ?? 4D 8B D1");
this.ToggleUiHide = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 0F B6 B9 ?? ?? ?? ?? B8 ?? ?? ?? ??"); this.ToggleUiHide = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 0F B6 B9 ?? ?? ?? ?? B8 ?? ?? ?? ??");
var uiModuleVtableSig = sig.GetStaticAddressFromSig("48 8D 05 ?? ?? ?? ?? 4C 89 61 28"); var uiModuleVtableSig = sig.GetStaticAddressFromSig("48 8D 05 ?? ?? ?? ?? 4C 89 61 28");
this.GetAgentModule = Marshal.ReadIntPtr(uiModuleVtableSig, 34 * IntPtr.Size); this.GetAgentModule = Marshal.ReadIntPtr(uiModuleVtableSig, 34 * IntPtr.Size);
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override void SetupInternal(SigScanner scanner) protected override void SetupInternal(SigScanner scanner)
{ {
// Xiv__UiManager__GetChatManager 000 lea rax, [rcx+13E0h] // Xiv__UiManager__GetChatManager 000 lea rax, [rcx+13E0h]
// Xiv__UiManager__GetChatManager+7 000 retn // Xiv__UiManager__GetChatManager+7 000 retn
this.ChatManager = this.BaseAddress + 0x13E0; this.ChatManager = this.BaseAddress + 0x13E0;
}
} }
} }

View file

@ -1,48 +1,49 @@
namespace Dalamud.Game.Gui; namespace Dalamud.Game.Gui
/// <summary>
/// ActionKinds used in AgentActionDetail.
/// These describe the possible kinds of actions being hovered.
/// </summary>
public enum HoverActionKind
{ {
/// <summary> /// <summary>
/// No action is hovered. /// ActionKinds used in AgentActionDetail.
/// These describe the possible kinds of actions being hovered.
/// </summary> /// </summary>
None = 0, public enum HoverActionKind
{
/// <summary>
/// No action is hovered.
/// </summary>
None = 0,
/// <summary> /// <summary>
/// A regular action is hovered. /// A regular action is hovered.
/// </summary> /// </summary>
Action = 21, Action = 21,
/// <summary> /// <summary>
/// A general action is hovered. /// A general action is hovered.
/// </summary> /// </summary>
GeneralAction = 23, GeneralAction = 23,
/// <summary> /// <summary>
/// A companion order type of action is hovered. /// A companion order type of action is hovered.
/// </summary> /// </summary>
CompanionOrder = 24, CompanionOrder = 24,
/// <summary> /// <summary>
/// A main command type of action is hovered. /// A main command type of action is hovered.
/// </summary> /// </summary>
MainCommand = 25, MainCommand = 25,
/// <summary> /// <summary>
/// An extras command type of action is hovered. /// An extras command type of action is hovered.
/// </summary> /// </summary>
ExtraCommand = 26, ExtraCommand = 26,
/// <summary> /// <summary>
/// A pet order type of action is hovered. /// A pet order type of action is hovered.
/// </summary> /// </summary>
PetOrder = 28, PetOrder = 28,
/// <summary> /// <summary>
/// A trait is hovered. /// A trait is hovered.
/// </summary> /// </summary>
Trait = 29, Trait = 29,
}
} }

View file

@ -1,22 +1,23 @@
namespace Dalamud.Game.Gui; namespace Dalamud.Game.Gui
/// <summary>
/// This class represents the hotbar action currently hovered over by the cursor.
/// </summary>
public class HoveredAction
{ {
/// <summary> /// <summary>
/// Gets or sets the base action ID. /// This class represents the hotbar action currently hovered over by the cursor.
/// </summary> /// </summary>
public uint BaseActionID { get; set; } = 0; public class HoveredAction
{
/// <summary>
/// Gets or sets the base action ID.
/// </summary>
public uint BaseActionID { get; set; } = 0;
/// <summary> /// <summary>
/// Gets or sets the action ID accounting for automatic upgrades. /// Gets or sets the action ID accounting for automatic upgrades.
/// </summary> /// </summary>
public uint ActionID { get; set; } = 0; public uint ActionID { get; set; } = 0;
/// <summary> /// <summary>
/// Gets or sets the type of action. /// Gets or sets the type of action.
/// </summary> /// </summary>
public HoverActionKind ActionKind { get; set; } = HoverActionKind.None; public HoverActionKind ActionKind { get; set; } = HoverActionKind.None;
}
} }

View file

@ -9,234 +9,235 @@ using ImGuiNET;
using static Dalamud.NativeFunctions; using static Dalamud.NativeFunctions;
namespace Dalamud.Game.Gui.Internal; namespace Dalamud.Game.Gui.Internal
/// <summary>
/// This class handles IME for non-English users.
/// </summary>
internal class DalamudIME : IDisposable
{ {
private static readonly ModuleLog Log = new("IME");
private IntPtr interfaceHandle;
private IntPtr wndProcPtr;
private IntPtr oldWndProcPtr;
private WndProcDelegate wndProcDelegate;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DalamudIME"/> class. /// This class handles IME for non-English users.
/// </summary> /// </summary>
internal DalamudIME() internal class DalamudIME : IDisposable
{ {
} private static readonly ModuleLog Log = new("IME");
private delegate long WndProcDelegate(IntPtr hWnd, uint msg, ulong wParam, long lParam); private IntPtr interfaceHandle;
private IntPtr wndProcPtr;
private IntPtr oldWndProcPtr;
private WndProcDelegate wndProcDelegate;
/// <summary> /// <summary>
/// Gets a value indicating whether the module is enabled. /// Initializes a new instance of the <see cref="DalamudIME"/> class.
/// </summary> /// </summary>
internal bool IsEnabled { get; private set; } internal DalamudIME()
/// <summary>
/// Gets the index of the first imm candidate in relation to the full list.
/// </summary>
internal CandidateList ImmCandNative { get; private set; } = default;
/// <summary>
/// Gets the imm candidates.
/// </summary>
internal List<string> ImmCand { get; private set; } = new();
/// <summary>
/// Gets the selected imm component.
/// </summary>
internal string ImmComp { get; private set; } = string.Empty;
/// <inheritdoc/>
public void Dispose()
{
if (this.oldWndProcPtr != IntPtr.Zero)
{ {
SetWindowLongPtrW(this.interfaceHandle, WindowLongType.WndProc, this.oldWndProcPtr);
this.oldWndProcPtr = IntPtr.Zero;
} }
}
/// <summary> private delegate long WndProcDelegate(IntPtr hWnd, uint msg, ulong wParam, long lParam);
/// Enables the IME module.
/// </summary>
internal void Enable()
{
try
{
this.wndProcDelegate = this.WndProcDetour;
this.interfaceHandle = Service<InterfaceManager>.Get().WindowHandlePtr;
this.wndProcPtr = Marshal.GetFunctionPointerForDelegate(this.wndProcDelegate);
this.oldWndProcPtr = SetWindowLongPtrW(this.interfaceHandle, WindowLongType.WndProc, this.wndProcPtr);
this.IsEnabled = true;
Log.Information("Enabled!");
}
catch (Exception ex)
{
Log.Information(ex, "Enable failed");
}
}
private void ToggleWindow(bool visible) /// <summary>
{ /// Gets a value indicating whether the module is enabled.
if (visible) /// </summary>
Service<DalamudInterface>.Get().OpenIMEWindow(); internal bool IsEnabled { get; private set; }
else
Service<DalamudInterface>.Get().CloseIMEWindow();
}
private long WndProcDetour(IntPtr hWnd, uint msg, ulong wParam, long lParam) /// <summary>
{ /// Gets the index of the first imm candidate in relation to the full list.
try /// </summary>
internal CandidateList ImmCandNative { get; private set; } = default;
/// <summary>
/// Gets the imm candidates.
/// </summary>
internal List<string> ImmCand { get; private set; } = new();
/// <summary>
/// Gets the selected imm component.
/// </summary>
internal string ImmComp { get; private set; } = string.Empty;
/// <inheritdoc/>
public void Dispose()
{ {
if (hWnd == this.interfaceHandle && ImGui.GetCurrentContext() != IntPtr.Zero && ImGui.GetIO().WantTextInput) if (this.oldWndProcPtr != IntPtr.Zero)
{ {
var io = ImGui.GetIO(); SetWindowLongPtrW(this.interfaceHandle, WindowLongType.WndProc, this.oldWndProcPtr);
var wmsg = (WindowsMessage)msg; this.oldWndProcPtr = IntPtr.Zero;
}
}
switch (wmsg) /// <summary>
/// Enables the IME module.
/// </summary>
internal void Enable()
{
try
{
this.wndProcDelegate = this.WndProcDetour;
this.interfaceHandle = Service<InterfaceManager>.Get().WindowHandlePtr;
this.wndProcPtr = Marshal.GetFunctionPointerForDelegate(this.wndProcDelegate);
this.oldWndProcPtr = SetWindowLongPtrW(this.interfaceHandle, WindowLongType.WndProc, this.wndProcPtr);
this.IsEnabled = true;
Log.Information("Enabled!");
}
catch (Exception ex)
{
Log.Information(ex, "Enable failed");
}
}
private void ToggleWindow(bool visible)
{
if (visible)
Service<DalamudInterface>.Get().OpenIMEWindow();
else
Service<DalamudInterface>.Get().CloseIMEWindow();
}
private long WndProcDetour(IntPtr hWnd, uint msg, ulong wParam, long lParam)
{
try
{
if (hWnd == this.interfaceHandle && ImGui.GetCurrentContext() != IntPtr.Zero && ImGui.GetIO().WantTextInput)
{ {
case WindowsMessage.WM_IME_NOTIFY: var io = ImGui.GetIO();
switch ((IMECommand)wParam) var wmsg = (WindowsMessage)msg;
{
case IMECommand.ChangeCandidate:
this.ToggleWindow(true);
if (hWnd == IntPtr.Zero) switch (wmsg)
return 0; {
case WindowsMessage.WM_IME_NOTIFY:
switch ((IMECommand)wParam)
{
case IMECommand.ChangeCandidate:
this.ToggleWindow(true);
if (hWnd == IntPtr.Zero)
return 0;
var hIMC = ImmGetContext(hWnd);
if (hIMC == IntPtr.Zero)
return 0;
var size = ImmGetCandidateListW(hIMC, 0, IntPtr.Zero, 0);
if (size == 0)
break;
var candlistPtr = Marshal.AllocHGlobal((int)size);
size = ImmGetCandidateListW(hIMC, 0, candlistPtr, (uint)size);
var candlist = this.ImmCandNative = Marshal.PtrToStructure<CandidateList>(candlistPtr);
var pageSize = candlist.PageSize;
var candCount = candlist.Count;
if (pageSize > 0 && candCount > 1)
{
var dwOffsets = new int[candCount];
for (var i = 0; i < candCount; i++)
{
dwOffsets[i] = Marshal.ReadInt32(candlistPtr + ((i + 6) * sizeof(int)));
}
var pageStart = candlist.PageStart;
var cand = new string[pageSize];
this.ImmCand.Clear();
for (var i = 0; i < pageSize; i++)
{
var offStart = dwOffsets[i + pageStart];
var offEnd = i + pageStart + 1 < candCount ? dwOffsets[i + pageStart + 1] : size;
var pStrStart = candlistPtr + (int)offStart;
var pStrEnd = candlistPtr + (int)offEnd;
var len = (int)(pStrEnd.ToInt64() - pStrStart.ToInt64());
if (len > 0)
{
var candBytes = new byte[len];
Marshal.Copy(pStrStart, candBytes, 0, len);
var candStr = Encoding.Unicode.GetString(candBytes);
cand[i] = candStr;
this.ImmCand.Add(candStr);
}
}
Marshal.FreeHGlobal(candlistPtr);
}
break;
case IMECommand.OpenCandidate:
this.ToggleWindow(true);
this.ImmCandNative = default;
this.ImmCand.Clear();
break;
case IMECommand.CloseCandidate:
this.ToggleWindow(false);
this.ImmCandNative = default;
this.ImmCand.Clear();
break;
default:
break;
}
break;
case WindowsMessage.WM_IME_COMPOSITION:
if ((lParam & (long)IMEComposition.ResultStr) > 0)
{
var hIMC = ImmGetContext(hWnd); var hIMC = ImmGetContext(hWnd);
if (hIMC == IntPtr.Zero) if (hIMC == IntPtr.Zero)
return 0; return 0;
var size = ImmGetCandidateListW(hIMC, 0, IntPtr.Zero, 0); var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, IntPtr.Zero, 0);
if (size == 0) var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize);
break; ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, unmanagedPointer, (uint)dwSize);
var candlistPtr = Marshal.AllocHGlobal((int)size); var bytes = new byte[dwSize];
size = ImmGetCandidateListW(hIMC, 0, candlistPtr, (uint)size); Marshal.Copy(unmanagedPointer, bytes, 0, (int)dwSize);
Marshal.FreeHGlobal(unmanagedPointer);
var candlist = this.ImmCandNative = Marshal.PtrToStructure<CandidateList>(candlistPtr); var lpstr = Encoding.Unicode.GetString(bytes);
var pageSize = candlist.PageSize; io.AddInputCharactersUTF8(lpstr);
var candCount = candlist.Count;
if (pageSize > 0 && candCount > 1) this.ImmComp = string.Empty;
{
var dwOffsets = new int[candCount];
for (var i = 0; i < candCount; i++)
{
dwOffsets[i] = Marshal.ReadInt32(candlistPtr + ((i + 6) * sizeof(int)));
}
var pageStart = candlist.PageStart;
var cand = new string[pageSize];
this.ImmCand.Clear();
for (var i = 0; i < pageSize; i++)
{
var offStart = dwOffsets[i + pageStart];
var offEnd = i + pageStart + 1 < candCount ? dwOffsets[i + pageStart + 1] : size;
var pStrStart = candlistPtr + (int)offStart;
var pStrEnd = candlistPtr + (int)offEnd;
var len = (int)(pStrEnd.ToInt64() - pStrStart.ToInt64());
if (len > 0)
{
var candBytes = new byte[len];
Marshal.Copy(pStrStart, candBytes, 0, len);
var candStr = Encoding.Unicode.GetString(candBytes);
cand[i] = candStr;
this.ImmCand.Add(candStr);
}
}
Marshal.FreeHGlobal(candlistPtr);
}
break;
case IMECommand.OpenCandidate:
this.ToggleWindow(true);
this.ImmCandNative = default; this.ImmCandNative = default;
this.ImmCand.Clear(); this.ImmCand.Clear();
break;
case IMECommand.CloseCandidate:
this.ToggleWindow(false); this.ToggleWindow(false);
this.ImmCandNative = default; }
this.ImmCand.Clear();
break;
default: if (((long)(IMEComposition.CompStr | IMEComposition.CompAttr | IMEComposition.CompClause |
break; IMEComposition.CompReadAttr | IMEComposition.CompReadClause | IMEComposition.CompReadStr) & lParam) > 0)
} {
var hIMC = ImmGetContext(hWnd);
if (hIMC == IntPtr.Zero)
return 0;
break; var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.CompStr, IntPtr.Zero, 0);
case WindowsMessage.WM_IME_COMPOSITION: var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize);
if ((lParam & (long)IMEComposition.ResultStr) > 0) ImmGetCompositionStringW(hIMC, IMEComposition.CompStr, unmanagedPointer, (uint)dwSize);
{
var hIMC = ImmGetContext(hWnd);
if (hIMC == IntPtr.Zero)
return 0;
var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, IntPtr.Zero, 0); var bytes = new byte[dwSize];
var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize); Marshal.Copy(unmanagedPointer, bytes, 0, (int)dwSize);
ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, unmanagedPointer, (uint)dwSize); Marshal.FreeHGlobal(unmanagedPointer);
var bytes = new byte[dwSize]; var lpstr = Encoding.Unicode.GetString(bytes);
Marshal.Copy(unmanagedPointer, bytes, 0, (int)dwSize); this.ImmComp = lpstr;
Marshal.FreeHGlobal(unmanagedPointer); if (lpstr == string.Empty)
this.ToggleWindow(false);
}
var lpstr = Encoding.Unicode.GetString(bytes); break;
io.AddInputCharactersUTF8(lpstr);
this.ImmComp = string.Empty; default:
this.ImmCandNative = default; break;
this.ImmCand.Clear(); }
this.ToggleWindow(false);
}
if (((long)(IMEComposition.CompStr | IMEComposition.CompAttr | IMEComposition.CompClause |
IMEComposition.CompReadAttr | IMEComposition.CompReadClause | IMEComposition.CompReadStr) & lParam) > 0)
{
var hIMC = ImmGetContext(hWnd);
if (hIMC == IntPtr.Zero)
return 0;
var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.CompStr, IntPtr.Zero, 0);
var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize);
ImmGetCompositionStringW(hIMC, IMEComposition.CompStr, unmanagedPointer, (uint)dwSize);
var bytes = new byte[dwSize];
Marshal.Copy(unmanagedPointer, bytes, 0, (int)dwSize);
Marshal.FreeHGlobal(unmanagedPointer);
var lpstr = Encoding.Unicode.GetString(bytes);
this.ImmComp = lpstr;
if (lpstr == string.Empty)
this.ToggleWindow(false);
}
break;
default:
break;
} }
} }
} catch (Exception ex)
catch (Exception ex) {
{ Log.Error(ex, "Prevented a crash in an IME hook");
Log.Error(ex, "Prevented a crash in an IME hook"); }
}
return CallWindowProcW(this.oldWndProcPtr, hWnd, msg, wParam, lParam); return CallWindowProcW(this.oldWndProcPtr, hWnd, msg, wParam, lParam);
}
} }
} }

View file

@ -1,27 +1,28 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Dalamud.Game.Gui.PartyFinder.Internal; namespace Dalamud.Game.Gui.PartyFinder.Internal
/// <summary>
/// The structure of the PartyFinder packet.
/// </summary>
[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Sequential struct marshaling.")]
[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "Sequential struct marshaling.")]
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the field usage.")]
[StructLayout(LayoutKind.Sequential)]
internal readonly struct PartyFinderPacket
{ {
/// <summary> /// <summary>
/// Gets the size of this packet. /// The structure of the PartyFinder packet.
/// </summary> /// </summary>
internal static int PacketSize { get; } = Marshal.SizeOf<PartyFinderPacket>(); [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Sequential struct marshaling.")]
[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "Sequential struct marshaling.")]
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the field usage.")]
[StructLayout(LayoutKind.Sequential)]
internal readonly struct PartyFinderPacket
{
/// <summary>
/// Gets the size of this packet.
/// </summary>
internal static int PacketSize { get; } = Marshal.SizeOf<PartyFinderPacket>();
internal readonly int BatchNumber; internal readonly int BatchNumber;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
private readonly byte[] padding1; private readonly byte[] padding1;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
internal readonly PartyFinderPacketListing[] Listings; internal readonly PartyFinderPacketListing[] Listings;
}
} }

View file

@ -2,97 +2,98 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Dalamud.Game.Gui.PartyFinder.Internal; namespace Dalamud.Game.Gui.PartyFinder.Internal
/// <summary>
/// The structure of an individual listing within a packet.
/// </summary>
[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "Sequential struct marshaling.")]
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the field usage.")]
[StructLayout(LayoutKind.Sequential)]
internal readonly struct PartyFinderPacketListing
{ {
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] /// <summary>
private readonly byte[] header1; /// The structure of an individual listing within a packet.
internal readonly uint Id; /// </summary>
[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:Elements should be ordered by access", Justification = "Sequential struct marshaling.")]
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Document the field usage.")]
private readonly byte[] header2; [StructLayout(LayoutKind.Sequential)]
internal readonly struct PartyFinderPacketListing
internal readonly uint ContentIdLower;
private readonly ushort unknownShort1;
private readonly ushort unknownShort2;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
private readonly byte[] header3;
internal readonly byte Category;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
private readonly byte[] header4;
internal readonly ushort Duty;
internal readonly byte DutyType;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 11)]
private readonly byte[] header5;
internal readonly ushort World;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
private readonly byte[] header6;
internal readonly byte Objective;
internal readonly byte BeginnersWelcome;
internal readonly byte Conditions;
internal readonly byte DutyFinderSettings;
internal readonly byte LootRules;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
private readonly byte[] header7; // all zero in every pf I've examined
internal readonly uint LastPatchHotfixTimestamp; // last time the servers were restarted?
internal readonly ushort SecondsRemaining;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
private readonly byte[] header8; // 00 00 01 00 00 00 in every pf I've examined
internal readonly ushort MinimumItemLevel;
internal readonly ushort HomeWorld;
internal readonly ushort CurrentWorld;
private readonly byte header9;
internal readonly byte NumSlots;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
private readonly byte[] header10;
internal readonly byte SearchArea;
private readonly byte header11;
internal readonly byte NumParties;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
private readonly byte[] header12; // 00 00 00 always. maybe numParties is a u32?
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
internal readonly uint[] Slots;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
internal readonly byte[] JobsPresent;
// Note that ByValTStr will not work here because the strings are UTF-8 and there's only a CharSet for UTF-16 in C#.
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
internal readonly byte[] Name;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 192)]
internal readonly byte[] Description;
internal bool IsNull()
{ {
// a valid party finder must have at least one slot set [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
return this.Slots.All(slot => slot == 0); private readonly byte[] header1;
internal readonly uint Id;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
private readonly byte[] header2;
internal readonly uint ContentIdLower;
private readonly ushort unknownShort1;
private readonly ushort unknownShort2;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)]
private readonly byte[] header3;
internal readonly byte Category;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
private readonly byte[] header4;
internal readonly ushort Duty;
internal readonly byte DutyType;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 11)]
private readonly byte[] header5;
internal readonly ushort World;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
private readonly byte[] header6;
internal readonly byte Objective;
internal readonly byte BeginnersWelcome;
internal readonly byte Conditions;
internal readonly byte DutyFinderSettings;
internal readonly byte LootRules;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
private readonly byte[] header7; // all zero in every pf I've examined
internal readonly uint LastPatchHotfixTimestamp; // last time the servers were restarted?
internal readonly ushort SecondsRemaining;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
private readonly byte[] header8; // 00 00 01 00 00 00 in every pf I've examined
internal readonly ushort MinimumItemLevel;
internal readonly ushort HomeWorld;
internal readonly ushort CurrentWorld;
private readonly byte header9;
internal readonly byte NumSlots;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
private readonly byte[] header10;
internal readonly byte SearchArea;
private readonly byte header11;
internal readonly byte NumParties;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
private readonly byte[] header12; // 00 00 00 always. maybe numParties is a u32?
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
internal readonly uint[] Slots;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
internal readonly byte[] JobsPresent;
// Note that ByValTStr will not work here because the strings are UTF-8 and there's only a CharSet for UTF-16 in C#.
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
internal readonly byte[] Name;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 192)]
internal readonly byte[] Description;
internal bool IsNull()
{
// a valid party finder must have at least one slot set
return this.Slots.All(slot => slot == 0);
}
} }
} }

View file

@ -1,20 +1,21 @@
using System; using System;
namespace Dalamud.Game.Gui.PartyFinder; namespace Dalamud.Game.Gui.PartyFinder
/// <summary>
/// The address resolver for the <see cref="PartyFinderGui"/> class.
/// </summary>
public class PartyFinderAddressResolver : BaseAddressResolver
{ {
/// <summary> /// <summary>
/// Gets the address of the native ReceiveListing method. /// The address resolver for the <see cref="PartyFinderGui"/> class.
/// </summary> /// </summary>
public IntPtr ReceiveListing { get; private set; } public class PartyFinderAddressResolver : BaseAddressResolver
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{ {
this.ReceiveListing = sig.ScanText("40 53 41 57 48 83 EC 28 48 8B D9"); /// <summary>
/// Gets the address of the native ReceiveListing method.
/// </summary>
public IntPtr ReceiveListing { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
this.ReceiveListing = sig.ScanText("40 53 41 57 48 83 EC 28 48 8B D9");
}
} }
} }

View file

@ -8,132 +8,133 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Serilog; using Serilog;
namespace Dalamud.Game.Gui.PartyFinder; namespace Dalamud.Game.Gui.PartyFinder
/// <summary>
/// This class handles interacting with the native PartyFinder window.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed class PartyFinderGui : IDisposable
{ {
private readonly PartyFinderAddressResolver address;
private readonly IntPtr memory;
private readonly Hook<ReceiveListingDelegate> receiveListingHook;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PartyFinderGui"/> class. /// This class handles interacting with the native PartyFinder window.
/// </summary> /// </summary>
internal PartyFinderGui() [PluginInterface]
[InterfaceVersion("1.0")]
public sealed class PartyFinderGui : IDisposable
{ {
this.address = new PartyFinderAddressResolver(); private readonly PartyFinderAddressResolver address;
this.address.Setup(); private readonly IntPtr memory;
this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize); private readonly Hook<ReceiveListingDelegate> receiveListingHook;
this.receiveListingHook = new Hook<ReceiveListingDelegate>(this.address.ReceiveListing, new ReceiveListingDelegate(this.HandleReceiveListingDetour)); /// <summary>
} /// Initializes a new instance of the <see cref="PartyFinderGui"/> class.
/// </summary>
/// <summary> internal PartyFinderGui()
/// Event type fired each time the game receives an individual Party Finder listing.
/// Cannot modify listings but can hide them.
/// </summary>
/// <param name="listing">The listings received.</param>
/// <param name="args">Additional arguments passed by the game.</param>
public delegate void PartyFinderListingEventDelegate(PartyFinderListing listing, PartyFinderListingEventArgs args);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void ReceiveListingDelegate(IntPtr managerPtr, IntPtr data);
/// <summary>
/// Event fired each time the game receives an individual Party Finder listing.
/// Cannot modify listings but can hide them.
/// </summary>
public event PartyFinderListingEventDelegate ReceiveListing;
/// <summary>
/// Enables this module.
/// </summary>
public void Enable()
{
this.receiveListingHook.Enable();
}
/// <summary>
/// Dispose of m anaged and unmanaged resources.
/// </summary>
public void Dispose()
{
this.receiveListingHook.Dispose();
try
{ {
Marshal.FreeHGlobal(this.memory); this.address = new PartyFinderAddressResolver();
} this.address.Setup();
catch (BadImageFormatException)
{
Log.Warning("Could not free PartyFinderGui memory.");
}
}
private void HandleReceiveListingDetour(IntPtr managerPtr, IntPtr data) this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize);
{
try this.receiveListingHook = new Hook<ReceiveListingDelegate>(this.address.ReceiveListing, new ReceiveListingDelegate(this.HandleReceiveListingDetour));
{
this.HandleListingEvents(data);
}
catch (Exception ex)
{
Log.Error(ex, "Exception on ReceiveListing hook.");
} }
this.receiveListingHook.Original(managerPtr, data); /// <summary>
} /// Event type fired each time the game receives an individual Party Finder listing.
/// Cannot modify listings but can hide them.
/// </summary>
/// <param name="listing">The listings received.</param>
/// <param name="args">Additional arguments passed by the game.</param>
public delegate void PartyFinderListingEventDelegate(PartyFinderListing listing, PartyFinderListingEventArgs args);
private void HandleListingEvents(IntPtr data) [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
{ private delegate void ReceiveListingDelegate(IntPtr managerPtr, IntPtr data);
var dataPtr = data + 0x10;
var packet = Marshal.PtrToStructure<PartyFinderPacket>(dataPtr); /// <summary>
/// Event fired each time the game receives an individual Party Finder listing.
/// Cannot modify listings but can hide them.
/// </summary>
public event PartyFinderListingEventDelegate ReceiveListing;
// rewriting is an expensive operation, so only do it if necessary /// <summary>
var needToRewrite = false; /// Enables this module.
/// </summary>
for (var i = 0; i < packet.Listings.Length; i++) public void Enable()
{ {
// these are empty slots that are not shown to the player this.receiveListingHook.Enable();
if (packet.Listings[i].IsNull()) }
/// <summary>
/// Dispose of m anaged and unmanaged resources.
/// </summary>
public void Dispose()
{
this.receiveListingHook.Dispose();
try
{ {
continue; Marshal.FreeHGlobal(this.memory);
}
catch (BadImageFormatException)
{
Log.Warning("Could not free PartyFinderGui memory.");
}
}
private void HandleReceiveListingDetour(IntPtr managerPtr, IntPtr data)
{
try
{
this.HandleListingEvents(data);
}
catch (Exception ex)
{
Log.Error(ex, "Exception on ReceiveListing hook.");
} }
var listing = new PartyFinderListing(packet.Listings[i]); this.receiveListingHook.Original(managerPtr, data);
var args = new PartyFinderListingEventArgs(packet.BatchNumber); }
this.ReceiveListing?.Invoke(listing, args);
if (args.Visible) private void HandleListingEvents(IntPtr data)
{
var dataPtr = data + 0x10;
var packet = Marshal.PtrToStructure<PartyFinderPacket>(dataPtr);
// rewriting is an expensive operation, so only do it if necessary
var needToRewrite = false;
for (var i = 0; i < packet.Listings.Length; i++)
{ {
continue; // these are empty slots that are not shown to the player
if (packet.Listings[i].IsNull())
{
continue;
}
var listing = new PartyFinderListing(packet.Listings[i]);
var args = new PartyFinderListingEventArgs(packet.BatchNumber);
this.ReceiveListing?.Invoke(listing, args);
if (args.Visible)
{
continue;
}
// hide the listing from the player by setting it to a null listing
packet.Listings[i] = default;
needToRewrite = true;
} }
// hide the listing from the player by setting it to a null listing if (!needToRewrite)
packet.Listings[i] = default; {
needToRewrite = true; return;
} }
if (!needToRewrite) // write our struct into the memory (doing this directly crashes the game)
{ Marshal.StructureToPtr(packet, this.memory, false);
return;
}
// write our struct into the memory (doing this directly crashes the game) // copy our new memory over the game's
Marshal.StructureToPtr(packet, this.memory, false); unsafe
{
// copy our new memory over the game's Buffer.MemoryCopy((void*)this.memory, (void*)dataPtr, PartyFinderPacket.PacketSize, PartyFinderPacket.PacketSize);
unsafe }
{
Buffer.MemoryCopy((void*)this.memory, (void*)dataPtr, PartyFinderPacket.PacketSize, PartyFinderPacket.PacketSize);
} }
} }
} }

View file

@ -1,25 +1,26 @@
using System; using System;
namespace Dalamud.Game.Gui.PartyFinder.Types; namespace Dalamud.Game.Gui.PartyFinder.Types
/// <summary>
/// Condition flags for the <see cref="PartyFinderGui"/> class.
/// </summary>
[Flags]
public enum ConditionFlags : uint
{ {
/// <summary> /// <summary>
/// No duty condition. /// Condition flags for the <see cref="PartyFinderGui"/> class.
/// </summary> /// </summary>
None = 1, [Flags]
public enum ConditionFlags : uint
{
/// <summary>
/// No duty condition.
/// </summary>
None = 1,
/// <summary> /// <summary>
/// The duty complete condition. /// The duty complete condition.
/// </summary> /// </summary>
DutyComplete = 2, DutyComplete = 2,
/// <summary> /// <summary>
/// The duty incomplete condition. /// The duty incomplete condition.
/// </summary> /// </summary>
DutyIncomplete = 4, DutyIncomplete = 4,
}
} }

View file

@ -1,47 +1,48 @@
namespace Dalamud.Game.Gui.PartyFinder.Types; namespace Dalamud.Game.Gui.PartyFinder.Types
/// <summary>
/// Category flags for the <see cref="PartyFinderGui"/> class.
/// </summary>
public enum DutyCategory
{ {
/// <summary> /// <summary>
/// The duty category. /// Category flags for the <see cref="PartyFinderGui"/> class.
/// </summary> /// </summary>
Duty = 0, public enum DutyCategory
{
/// <summary>
/// The duty category.
/// </summary>
Duty = 0,
/// <summary> /// <summary>
/// The quest battle category. /// The quest battle category.
/// </summary> /// </summary>
QuestBattles = 1 << 0, QuestBattles = 1 << 0,
/// <summary> /// <summary>
/// The fate category. /// The fate category.
/// </summary> /// </summary>
Fates = 1 << 1, Fates = 1 << 1,
/// <summary> /// <summary>
/// The treasure hunt category. /// The treasure hunt category.
/// </summary> /// </summary>
TreasureHunt = 1 << 2, TreasureHunt = 1 << 2,
/// <summary> /// <summary>
/// The hunt category. /// The hunt category.
/// </summary> /// </summary>
TheHunt = 1 << 3, TheHunt = 1 << 3,
/// <summary> /// <summary>
/// The gathering forays category. /// The gathering forays category.
/// </summary> /// </summary>
GatheringForays = 1 << 4, GatheringForays = 1 << 4,
/// <summary> /// <summary>
/// The deep dungeons category. /// The deep dungeons category.
/// </summary> /// </summary>
DeepDungeons = 1 << 5, DeepDungeons = 1 << 5,
/// <summary> /// <summary>
/// The adventuring forays category. /// The adventuring forays category.
/// </summary> /// </summary>
AdventuringForays = 1 << 6, AdventuringForays = 1 << 6,
}
} }

View file

@ -1,30 +1,31 @@
using System; using System;
namespace Dalamud.Game.Gui.PartyFinder.Types; namespace Dalamud.Game.Gui.PartyFinder.Types
/// <summary>
/// Duty finder settings flags for the <see cref="PartyFinderGui"/> class.
/// </summary>
[Flags]
public enum DutyFinderSettingsFlags : uint
{ {
/// <summary> /// <summary>
/// No duty finder settings. /// Duty finder settings flags for the <see cref="PartyFinderGui"/> class.
/// </summary> /// </summary>
None = 0, [Flags]
public enum DutyFinderSettingsFlags : uint
{
/// <summary>
/// No duty finder settings.
/// </summary>
None = 0,
/// <summary> /// <summary>
/// The undersized party setting. /// The undersized party setting.
/// </summary> /// </summary>
UndersizedParty = 1 << 0, UndersizedParty = 1 << 0,
/// <summary> /// <summary>
/// The minimum item level setting. /// The minimum item level setting.
/// </summary> /// </summary>
MinimumItemLevel = 1 << 1, MinimumItemLevel = 1 << 1,
/// <summary> /// <summary>
/// The silence echo setting. /// The silence echo setting.
/// </summary> /// </summary>
SilenceEcho = 1 << 2, SilenceEcho = 1 << 2,
}
} }

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