refactor(Dalamud): switch to file-scoped namespaces

This commit is contained in:
goat 2021-11-17 19:42:32 +01:00
parent 13cf3d93dc
commit b5f34c3199
No known key found for this signature in database
GPG key ID: 7773BB5B43BA52E5
325 changed files with 45878 additions and 46218 deletions

View file

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

View file

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

View file

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

View file

@ -8,268 +8,267 @@ using Newtonsoft.Json;
using Serilog;
using Serilog.Events;
namespace Dalamud.Configuration.Internal
namespace Dalamud.Configuration.Internal;
/// <summary>
/// Class containing Dalamud settings.
/// </summary>
[Serializable]
internal sealed class DalamudConfiguration
{
/// <summary>
/// Class containing Dalamud settings.
/// </summary>
[Serializable]
internal sealed class DalamudConfiguration
private static readonly JsonSerializerSettings SerializerSettings = new()
{
private static readonly JsonSerializerSettings SerializerSettings = new()
TypeNameHandling = TypeNameHandling.All,
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
{
TypeNameHandling = TypeNameHandling.All,
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)
deserialized = JsonConvert.DeserializeObject<DalamudConfiguration>(File.ReadAllText(path), SerializerSettings);
}
catch (Exception ex)
{
DalamudConfiguration deserialized;
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;
Log.Warning(ex, "Failed to load DalamudConfiguration at {0}", path);
deserialized = new DalamudConfiguration();
}
/// <summary>
/// Save the configuration at the path it was loaded from.
/// </summary>
public void Save()
{
File.WriteAllText(this.configPath, JsonConvert.SerializeObject(this, SerializerSettings));
this.DalamudConfigurationSaved?.Invoke(this);
}
deserialized.configPath = path;
return deserialized;
}
/// <summary>
/// Save the configuration at the path it was loaded from.
/// </summary>
public void Save()
{
File.WriteAllText(this.configPath, JsonConvert.SerializeObject(this, SerializerSettings));
this.DalamudConfigurationSaved?.Invoke(this);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -2,129 +2,128 @@ using System.IO;
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>
/// Configuration to store settings for a dalamud plugin.
/// Initializes a new instance of the <see cref="PluginConfigurations"/> class.
/// </summary>
public sealed class PluginConfigurations
/// <param name="storageFolder">Directory for storage of plugin configuration files.</param>
public PluginConfigurations(string storageFolder)
{
private readonly DirectoryInfo configDirectory;
this.configDirectory = new DirectoryInfo(storageFolder);
this.configDirectory.Create();
}
/// <summary>
/// Initializes a new instance of the <see cref="PluginConfigurations"/> class.
/// </summary>
/// <param name="storageFolder">Directory for storage of plugin configuration files.</param>
public PluginConfigurations(string storageFolder)
/// <summary>
/// Save/Load plugin configuration.
/// NOTE: Save/Load are still using Type information for now,
/// despite LoadForType superseding Load and not requiring or using it.
/// It might be worth removing the Type info from Save, to strip it from all future saved configs,
/// 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
{
this.configDirectory = new DirectoryInfo(storageFolder);
this.configDirectory.Create();
}
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
TypeNameHandling = TypeNameHandling.Objects,
}));
}
/// <summary>
/// Save/Load plugin configuration.
/// NOTE: Save/Load are still using Type information for now,
/// despite LoadForType superseding Load and not requiring or using it.
/// It might be worth removing the Type info from Save, to strip it from all future saved configs,
/// 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
/// <summary>
/// Load plugin configuration.
/// </summary>
/// <param name="pluginName">Plugin name.</param>
/// <returns>Plugin configuration.</returns>
public IPluginConfiguration? Load(string pluginName)
{
var path = this.GetConfigFile(pluginName);
if (!path.Exists)
return null;
return JsonConvert.DeserializeObject<IPluginConfiguration>(
File.ReadAllText(path.FullName),
new JsonSerializerSettings
{
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
TypeNameHandling = TypeNameHandling.Objects,
}));
}
/// <summary>
/// Load plugin configuration.
/// </summary>
/// <param name="pluginName">Plugin name.</param>
/// <returns>Plugin configuration.</returns>
public IPluginConfiguration? Load(string pluginName)
{
var path = this.GetConfigFile(pluginName);
if (!path.Exists)
return null;
return JsonConvert.DeserializeObject<IPluginConfiguration>(
File.ReadAllText(path.FullName),
new JsonSerializerSettings
{
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
TypeNameHandling = TypeNameHandling.Objects,
});
}
/// <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)
{
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>
/// 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)
{
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));
}

View file

@ -33,379 +33,378 @@ using Serilog.Events;
[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>
/// The main Dalamud class containing all subsystems.
/// Initializes a new instance of the <see cref="Dalamud"/> class.
/// </summary>
internal sealed class Dalamud : IDisposable
/// <param name="info">DalamudStartInfo instance.</param>
/// <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)
{
#region Internals
this.ApplyProcessPatch();
private readonly ManualResetEvent unloadSignal;
private readonly ManualResetEvent finishUnloadSignal;
private MonoMod.RuntimeDetour.Hook processMonoHook;
private bool hasDisposedPlugins = false;
Service<Dalamud>.Set(this);
Service<DalamudStartInfo>.Set(info);
Service<DalamudConfiguration>.Set(configuration);
#endregion
this.LogLevelSwitch = loggingLevelSwitch;
/// <summary>
/// Initializes a new instance of the <see cref="Dalamud"/> class.
/// </summary>
/// <param name="info">DalamudStartInfo instance.</param>
/// <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.unloadSignal = new ManualResetEvent(false);
this.unloadSignal.Reset();
this.finishUnloadSignal = finishSignal;
this.finishUnloadSignal.Reset();
}
/// <summary>
/// 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
{
this.ApplyProcessPatch();
SerilogEventSink.Instance.LogLine += SerilogOnLogLine;
Service<Dalamud>.Set(this);
Service<DalamudStartInfo>.Set(info);
Service<DalamudConfiguration>.Set(configuration);
Service<ServiceContainer>.Set();
this.LogLevelSwitch = loggingLevelSwitch;
// Initialize the process information.
Service<SigScanner>.Set(new SigScanner(true));
Service<HookManager>.Set();
this.unloadSignal = new ManualResetEvent(false);
this.unloadSignal.Reset();
// Initialize FFXIVClientStructs function resolver
FFXIVClientStructs.Resolver.Initialize();
Log.Information("[T1] FFXIVClientStructs initialized!");
this.finishUnloadSignal = finishSignal;
this.finishUnloadSignal.Reset();
}
/// <summary>
/// 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;
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!");
// Initialize game subsystem
var framework = Service<Framework>.Set();
Log.Information("[T1] Framework OK!");
#if DEBUG
Service<TaskTracker>.Set();
Log.Information("[T1] TaskTracker OK!");
Service<TaskTracker>.Set();
Log.Information("[T1] TaskTracker OK!");
#endif
Service<GameNetwork>.Set();
Service<GameGui>.Set();
Service<GameNetwork>.Set();
Service<GameGui>.Set();
framework.Enable();
framework.Enable();
Log.Information("[T1] Load complete!");
}
catch (Exception ex)
{
Log.Error(ex, "Tier 1 load failed.");
this.Unload();
}
Log.Information("[T1] Load complete!");
}
/// <summary>
/// Runs tier 2 of the Dalamud initialization process.
/// </summary>
/// <returns>Whether or not the load succeeded.</returns>
public bool LoadTier2()
catch (Exception ex)
{
try
{
var configuration = Service<DalamudConfiguration>.Get();
Log.Error(ex, "Tier 1 load failed.");
this.Unload();
}
}
var antiDebug = Service<AntiDebug>.Set();
if (!antiDebug.IsEnabled)
{
/// <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();
antiDebug.Enable();
#else
if (configuration.IsAntiAntiDebugEnabled)
antiDebug.Enable();
#endif
}
}
Log.Information("[T2] AntiDebug OK!");
Log.Information("[T2] AntiDebug OK!");
Service<WinSockHandlers>.Set();
Log.Information("[T2] WinSock OK!");
Service<WinSockHandlers>.Set();
Log.Information("[T2] WinSock OK!");
Service<NetworkHandlers>.Set();
Log.Information("[T2] NH 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;
}
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!");
Log.Information("[T2] Data OK!");
var clientState = Service<ClientState>.Set();
Log.Information("[T2] CS 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();
}
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!");
Log.Information("[T2] LOC OK!");
// This is enabled in ImGuiScene setup
Service<DalamudIME>.Set();
Log.Information("[T2] IME OK!");
// This is enabled in ImGuiScene setup
Service<DalamudIME>.Set();
Log.Information("[T2] IME OK!");
Service<InterfaceManager>.Set().Enable();
Log.Information("[T2] IM OK!");
Service<InterfaceManager>.Set().Enable();
Log.Information("[T2] IM OK!");
#pragma warning disable CS0618 // Type or member is obsolete
Service<SeStringManager>.Set();
Service<SeStringManager>.Set();
#pragma warning restore CS0618 // Type or member is obsolete
Log.Information("[T2] SeString OK!");
Log.Information("[T2] SeString OK!");
// Initialize managers. Basically handlers for the logic
Service<CommandManager>.Set();
// Initialize managers. Basically handlers for the logic
Service<CommandManager>.Set();
Service<DalamudCommands>.Set().SetupCommands();
Service<DalamudCommands>.Set().SetupCommands();
Log.Information("[T2] CM OK!");
Log.Information("[T2] CM OK!");
Service<ChatHandlers>.Set();
Service<ChatHandlers>.Set();
Log.Information("[T2] CH OK!");
Log.Information("[T2] CH OK!");
clientState.Enable();
Log.Information("[T2] CS ENABLE!");
clientState.Enable();
Log.Information("[T2] CS ENABLE!");
Service<DalamudAtkTweaks>.Set().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;
Log.Information("[T2] Load complete!");
}
catch (Exception ex)
{
Log.Error(ex, "Tier 2 load failed.");
this.Unload();
return false;
}
/// <summary>
/// Runs tier 3 of the Dalamud initialization process.
/// </summary>
/// <returns>Whether or not the load succeeded.</returns>
public bool LoadTier3()
return true;
}
/// <summary>
/// Runs tier 3 of the Dalamud initialization process.
/// </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
{
Log.Information("[T3] START!");
_ = pluginManager.SetPluginReposFromConfigAsync(false);
var pluginManager = Service<PluginManager>.Set();
Service<CallGate>.Set();
pluginManager.OnInstalledPluginsChanged += Troubleshooting.LogTroubleshooting;
try
{
_ = pluginManager.SetPluginReposFromConfigAsync(false);
Log.Information("[T3] PM OK!");
pluginManager.OnInstalledPluginsChanged += Troubleshooting.LogTroubleshooting;
pluginManager.CleanupPlugins();
Log.Information("[T3] PMC OK!");
Log.Information("[T3] PM 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.");
pluginManager.LoadAllPlugins();
Log.Information("[T3] PML OK!");
}
catch (Exception ex)
{
Log.Error(ex, "Tier 3 load failed.");
this.Unload();
return false;
Log.Error(ex, "Plugin load failed.");
}
return true;
Service<DalamudInterface>.Set();
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;
}
/// <summary>
/// Queue an unload of Dalamud when it gets the chance.
/// </summary>
public void Unload()
return true;
}
/// <summary>
/// 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
{
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)
{
if (!this.hasDisposedPlugins)
{
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;
this.DisposePlugins();
Thread.Sleep(100);
}
// Log.Verbose($"Process.Handle // {self.ProcessName} // {result:X}");
return result;
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!");
}
private void ApplyProcessPatch()
catch (Exception ex)
{
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);
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;
}
// 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,58 +3,57 @@ using System;
using Dalamud.Game;
using Newtonsoft.Json;
namespace Dalamud
namespace Dalamud;
/// <summary>
/// Struct containing information needed to initialize Dalamud.
/// </summary>
[Serializable]
public record DalamudStartInfo
{
/// <summary>
/// Struct containing information needed to initialize Dalamud.
/// Gets or sets the working directory of the XIVLauncher installations.
/// </summary>
[Serializable]
public record DalamudStartInfo
{
/// <summary>
/// Gets or sets the working directory of the XIVLauncher installations.
/// </summary>
public string WorkingDirectory { get; set; }
public string WorkingDirectory { get; set; }
/// <summary>
/// Gets the path to the configuration file.
/// </summary>
public string ConfigurationPath { get; init; }
/// <summary>
/// Gets the path to the configuration file.
/// </summary>
public string ConfigurationPath { get; init; }
/// <summary>
/// Gets the path to the directory for installed plugins.
/// </summary>
public string PluginDirectory { get; init; }
/// <summary>
/// Gets the path to the directory for installed plugins.
/// </summary>
public string PluginDirectory { get; init; }
/// <summary>
/// Gets the path to the directory for developer plugins.
/// </summary>
public string DefaultPluginDirectory { get; init; }
/// <summary>
/// Gets the path to the directory for developer plugins.
/// </summary>
public string DefaultPluginDirectory { get; init; }
/// <summary>
/// Gets the path to core Dalamud assets.
/// </summary>
public string AssetDirectory { get; init; }
/// <summary>
/// Gets the path to core Dalamud assets.
/// </summary>
public string AssetDirectory { get; init; }
/// <summary>
/// Gets the language of the game client.
/// </summary>
public ClientLanguage Language { get; init; }
/// <summary>
/// Gets the language of the game client.
/// </summary>
public ClientLanguage Language { get; init; }
/// <summary>
/// Gets the current game version code.
/// </summary>
[JsonConverter(typeof(GameVersionConverter))]
public GameVersion GameVersion { get; init; }
/// <summary>
/// Gets the current game version code.
/// </summary>
[JsonConverter(typeof(GameVersionConverter))]
public GameVersion GameVersion { get; init; }
/// <summary>
/// Gets a value indicating whether or not market board information should be uploaded by default.
/// </summary>
public bool OptOutMbCollection { get; init; }
/// <summary>
/// Gets a value indicating whether or not market board information should be uploaded by default.
/// </summary>
public bool OptOutMbCollection { get; init; }
/// <summary>
/// Gets a value that specifies how much to wait before a new Dalamud session.
/// </summary>
public int DelayInitializeMs { get; init; } = 0;
}
/// <summary>
/// Gets a value that specifies how much to wait before a new Dalamud session.
/// </summary>
public int DelayInitializeMs { get; init; } = 0;
}

View file

@ -18,330 +18,329 @@ using Lumina.Excel;
using Newtonsoft.Json;
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>
/// This class provides data for Dalamud-internal features, but can also be used by plugins if needed.
/// Initializes a new instance of the <see cref="DataManager"/> class.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed class DataManager : IDisposable
internal DataManager()
{
private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex";
this.Language = Service<DalamudStartInfo>.Get().Language;
private Thread luminaResourceThread;
private CancellationTokenSource luminaCancellationTokenSource;
// 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>
/// Initializes a new instance of the <see cref="DataManager"/> class.
/// </summary>
internal DataManager()
/// <summary>
/// Gets the current game client language.
/// </summary>
public ClientLanguage Language { get; private set; }
/// <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
{
this.Language = Service<DalamudStartInfo>.Get().Language;
ClientLanguage.Japanese => "ja/",
ClientLanguage.English => "en/",
ClientLanguage.German => "de/",
ClientLanguage.French => "fr/",
_ => throw new ArgumentOutOfRangeException(nameof(iconLanguage), $"Unknown Language: {iconLanguage}"),
};
// 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>());
}
return this.GetIcon(type, iconId);
}
/// <summary>
/// Gets the current game client language.
/// </summary>
public ClientLanguage Language { get; private set; }
/// <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 += "/";
/// <summary>
/// Gets the OpCodes sent by the server to the client.
/// </summary>
public ReadOnlyDictionary<string, ushort> ServerOpCodes { get; private set; }
var filePath = string.Format(IconFileFormat, iconId / 1000, type, iconId);
var file = this.GetFile<TexFile>(filePath);
/// <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/",
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);
if (type == string.Empty || file != default)
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);
// 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 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)
/// <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
{
return tex == null ? null : Service<InterfaceManager>.Get().LoadImageRaw(tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height, 4);
}
Log.Verbose("Starting data load...");
/// <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));
var zoneOpCodeDict = JsonConvert.DeserializeObject<Dictionary<string, ushort>>(
File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json")));
this.ServerOpCodes = new ReadOnlyDictionary<string, ushort>(zoneOpCodeDict);
/// <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));
Log.Verbose("Loaded {0} ServerOpCodes.", zoneOpCodeDict.Count);
/// <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));
var clientOpCodeDict = JsonConvert.DeserializeObject<Dictionary<string, ushort>>(
File.ReadAllText(Path.Combine(baseDir, "UIRes", "clientopcode.json")));
this.ClientOpCodes = new ReadOnlyDictionary<string, ushort>(clientOpCodeDict);
/// <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));
Log.Verbose("Loaded {0} ClientOpCodes.", clientOpCodeDict.Count);
/// <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
var luminaOptions = new LuminaOptions
{
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,
CacheFileResources = true,
#if DEBUG
PanicOnSheetChecksumMismatch = true,
PanicOnSheetChecksumMismatch = true,
#else
PanicOnSheetChecksumMismatch = false,
#endif
DefaultExcelLanguage = this.Language.ToLumina(),
};
DefaultExcelLanguage = this.Language.ToLumina(),
};
var processModule = Process.GetCurrentProcess().MainModule;
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.ProcessFileHandleQueue();
}
else
{
Thread.Sleep(5);
}
}
});
this.luminaResourceThread.Start();
}
catch (Exception ex)
var processModule = Process.GetCurrentProcess().MainModule;
if (processModule != null)
{
Log.Error(ex, "Could not download data.");
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.ProcessFileHandleQueue();
}
else
{
Thread.Sleep(5);
}
}
});
this.luminaResourceThread.Start();
}
catch (Exception ex)
{
Log.Error(ex, "Could not download data.");
}
}
}

View file

@ -21,262 +21,261 @@ using Serilog.Events;
using static Dalamud.NativeFunctions;
namespace Dalamud
namespace Dalamud;
/// <summary>
/// The main entrypoint for the Dalamud system.
/// </summary>
public sealed class EntryPoint
{
/// <summary>
/// The main entrypoint for the Dalamud system.
/// A delegate used during initialization of the CLR from Dalamud.Boot.
/// </summary>
public sealed class EntryPoint
/// <param name="infoPtr">Pointer to a serialized <see cref="DalamudStartInfo"/> data.</param>
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)
{
/// <summary>
/// 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);
var infoStr = Marshal.PtrToStringUTF8(infoPtr);
var info = JsonConvert.DeserializeObject<DalamudStartInfo>(infoStr);
/// <summary>
/// Initialize Dalamud.
/// </summary>
/// <param name="infoPtr">Pointer to a serialized <see cref="DalamudStartInfo"/> data.</param>
public static void Initialize(IntPtr infoPtr)
new Thread(() => RunThread(info)).Start();
}
/// <summary>
/// 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)
{
var infoStr = Marshal.PtrToStringUTF8(infoPtr);
var info = JsonConvert.DeserializeObject<DalamudStartInfo>(infoStr);
new Thread(() => RunThread(info)).Start();
while (!Debugger.IsAttached)
{
Thread.Sleep(100);
}
}
/// <summary>
/// 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);
}
}
// Setup logger
var levelSwitch = InitLogging(info.WorkingDirectory);
// Setup logger
var levelSwitch = InitLogging(info.WorkingDirectory);
// Load configuration first to get some early persistent state, like log level
var configuration = DalamudConfiguration.Load(info.ConfigurationPath);
// 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
// Set the appropriate logging level from the configuration
#if !DEBUG
levelSwitch.MinimumLevel = configuration.LogLevel;
#endif
// Log any unhandled exception.
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
// Log any unhandled exception.
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
var finishSignal = new ManualResetEvent(false);
var finishSignal = new ManualResetEvent(false);
try
{
if (info.DelayInitializeMs > 0)
{
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("Session has ended.");
Log.CloseAndFlush();
finishSignal.Set();
}
}
private static void InitSymbolHandler(DalamudStartInfo info)
try
{
try
if (info.DelayInitializeMs > 0)
{
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.");
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();
}
private static LoggingLevelSwitch InitLogging(string baseDirectory)
catch (Exception ex)
{
Log.Fatal(ex, "Unhandled exception on main thread.");
}
finally
{
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
var logPath = Path.Combine(baseDirectory, "dalamud.log");
var oldPath = Path.Combine(baseDirectory, "dalamud.log.old");
var logPath = Path.Combine(baseDirectory, "dalamud.log");
var oldPath = Path.Combine(baseDirectory, "dalamud.log.old");
#else
var logPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log");
var oldPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log.old");
#endif
CullLogFile(logPath, oldPath, 1 * 1024 * 1024);
CullLogFile(oldPath, null, 10 * 1024 * 1024);
CullLogFile(logPath, oldPath, 1 * 1024 * 1024);
CullLogFile(oldPath, null, 10 * 1024 * 1024);
var levelSwitch = new LoggingLevelSwitch(LogEventLevel.Verbose);
Log.Logger = new LoggerConfiguration()
.WriteTo.Async(a => a.File(logPath))
.WriteTo.Sink(SerilogEventSink.Instance)
.MinimumLevel.ControlledBy(levelSwitch)
.CreateLogger();
var levelSwitch = new LoggingLevelSwitch(LogEventLevel.Verbose);
Log.Logger = new LoggerConfiguration()
.WriteTo.Async(a => a.File(logPath))
.WriteTo.Sink(SerilogEventSink.Instance)
.MinimumLevel.ControlledBy(levelSwitch)
.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
{
try
var bufferSize = 4096;
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 bufferSize = 4096;
var oldFile = new FileInfo(oldPath);
var logFile = new FileInfo(logPath);
if (!oldFile.Exists)
oldFile.Create().Close();
if (!logFile.Exists)
logFile.Create();
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));
if (logFile.Length <= cullingFileSize)
return;
var amountToCull = logFile.Length - cullingFileSize;
if (amountToCull < bufferSize)
return;
if (oldPath != null)
var read = -1;
var total = 0;
var buffer = new byte[bufferSize];
while (read != 0 && total < amountToCull)
{
var oldFile = new FileInfo(oldPath);
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);
read = reader.Read(buffer, 0, buffer.Length);
writer.Write(buffer, 0, read);
total += read;
}
}
catch (Exception ex)
{
Log.Error(ex, "Log cull failed");
/*
var caption = "XIVLauncher Error";
var message = $"Log cull threw an exception: {ex.Message}\n{ex.StackTrace ?? string.Empty}";
_ = MessageBoxW(IntPtr.Zero, message, caption, MessageBoxType.IconError | MessageBoxType.Ok);
*/
{
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);
}
}
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs args)
catch (Exception ex)
{
switch (args.ExceptionObject)
{
case Exception ex:
Log.Fatal(ex, "Unhandled exception on AppDomain");
Troubleshooting.LogException(ex, "DalamudUnhandled");
Log.Error(ex, "Log cull failed");
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.");
/*
var caption = "XIVLauncher Error";
var message = $"Log cull threw an exception: {ex.Message}\n{ex.StackTrace ?? string.Empty}";
_ = MessageBoxW(IntPtr.Zero, message, caption, MessageBoxType.IconError | MessageBoxType.Ok);
*/
}
}
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs args)
{
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";
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,112 +3,111 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
namespace Dalamud.Game
namespace Dalamud.Game;
/// <summary>
/// Base memory address resolver.
/// </summary>
public abstract class BaseAddressResolver
{
/// <summary>
/// Base memory address resolver.
/// Gets a list of memory addresses that were found, to list in /xldata.
/// </summary>
public abstract class BaseAddressResolver
public static Dictionary<string, List<(string ClassName, IntPtr Address)>> DebugScannedValues { get; } = new();
/// <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()
{
/// <summary>
/// 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();
var scanner = Service<SigScanner>.Get();
this.Setup(scanner);
}
/// <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>
/// <param name="scanner">The SigScanner instance.</param>
public void Setup(SigScanner scanner)
{
// Because C# don't allow to call virtual function while in ctor
// we have to do this shit :\
/// <summary>
/// Setup the resolver, calling the appopriate method based on the process architecture.
/// </summary>
public void Setup()
if (this.IsResolved)
{
var scanner = Service<SigScanner>.Get();
this.Setup(scanner);
return;
}
/// <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)
if (scanner.Is32BitProcess)
{
// Because C# don't allow to call virtual function while in ctor
// we have to do this shit :\
if (this.IsResolved)
{
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.Setup32Bit(scanner);
}
else
{
this.Setup64Bit(scanner);
}
/// <summary>
/// Fetch vfunc N from a pointer to the vtable and return a delegate function pointer.
/// </summary>
/// <typeparam name="T">The delegate to marshal the function pointer to.</typeparam>
/// <param name="address">The address of the virtual table.</param>
/// <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
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)))
{
// 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);
DebugScannedValues[className].Add((property.Name, (IntPtr)property.GetValue(this)));
}
/// <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.");
}
this.IsResolved = true;
}
/// <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>
/// Fetch vfunc N from a pointer to the vtable and return a delegate function pointer.
/// </summary>
/// <typeparam name="T">The delegate to marshal the function pointer to.</typeparam>
/// <param name="address">The address of the virtual table.</param>
/// <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
{
// Get vtable
var vtable = Marshal.ReadIntPtr(address, vtableOffset);
/// <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
}
// Get an address to the function
var functionAddress = Marshal.ReadIntPtr(vtable, IntPtr.Size * count);
return Marshal.GetDelegateForFunctionPointer<T>(functionAddress);
}
/// <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>
/// 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 Serilog;
namespace Dalamud.Game
namespace Dalamud.Game;
/// <summary>
/// Chat events and public helper functions.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public class ChatHandlers
{
/// <summary>
/// Chat events and public helper functions.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public class ChatHandlers
// 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()
{
// 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>.*)が売れ、(?<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),
}
},
}
},
{
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 (?:.+) verkauft und (?<value>[\d,.]+) Gil erhalten\.$", RegexOptions.Compiled),
}
},
{
ClientLanguage.French,
new Regex[]
{
new Regex(@"^Un servant a vendu (?<item>.+) pour (?<value>[\d,.]+) gil à (?:.+)\.$", RegexOptions.Compiled),
}
},
};
private readonly Regex urlRegex = new(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled);
private readonly DalamudLinkPayload openInstallerWindowLink;
private bool hasSeenLoadingMsg;
private bool hasAutoUpdatedPlugins;
/// <summary>
/// Initializes a new instance of the <see cref="ChatHandlers"/> class.
/// </summary>
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();
});
}
/// <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)))
},
{
ClientLanguage.French,
new Regex[]
{
// This seems to be in the user block list - let's not show it
Log.Debug("Blocklist triggered");
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 DalamudLinkPayload openInstallerWindowLink;
private bool hasSeenLoadingMsg;
private bool hasAutoUpdatedPlugins;
/// <summary>
/// Initializes a new instance of the <see cref="ChatHandlers"/> class.
/// </summary>
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();
});
}
/// <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;
}
}
private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled)
if (configuration.BadWords != null &&
configuration.BadWords.Any(x => !string.IsNullOrEmpty(x) && textVal.Contains(x)))
{
var startInfo = Service<DalamudStartInfo>.Get();
var clientState = Service<ClientState.ClientState>.Get();
// This seems to be in the user block list - let's not show it
Log.Debug("Blocklist triggered");
isHandled = true;
return;
}
}
if (type == XivChatType.Notice && !this.hasSeenLoadingMsg)
this.PrintWelcomeMessage();
private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled)
{
var startInfo = Service<DalamudStartInfo>.Get();
var clientState = Service<ClientState.ClientState>.Get();
// For injections while logged in
if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg)
this.PrintWelcomeMessage();
if (type == XivChatType.Notice && !this.hasSeenLoadingMsg)
this.PrintWelcomeMessage();
if (!this.hasAutoUpdatedPlugins)
this.AutoUpdatePlugins();
// For injections while logged in
if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg)
this.PrintWelcomeMessage();
if (!this.hasAutoUpdatedPlugins)
this.AutoUpdatePlugins();
#if !DEBUG && false
if (!this.hasSeenLoadingMsg)
return;
#endif
if (type == XivChatType.RetainerSale)
if (type == XivChatType.RetainerSale)
{
foreach (var regex in this.retainerSaleRegexes[startInfo.Language])
{
foreach (var regex in this.retainerSaleRegexes[startInfo.Language])
var matchInfo = regex.Match(message.TextValue);
// we no longer really need to do/validate the item matching since we read the id from the byte array
// but we'd be checking the main match anyway
var itemInfo = matchInfo.Groups["item"];
if (!itemInfo.Success)
continue;
var itemLink = message.Payloads.FirstOrDefault(x => x.Type == PayloadType.Item) as ItemPayload;
if (itemLink == default)
{
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));
Log.Error("itemLink was null. Msg: {0}", BitConverter.ToString(message.Encode()));
break;
}
Log.Debug($"Probable retainer sale: {message}, decoded item {itemLink.Item.RowId}, HQ {itemLink.IsHQ}");
var valueInfo = matchInfo.Groups["value"];
// not sure if using a culture here would work correctly, so just strip symbols instead
if (!valueInfo.Success || !int.TryParse(valueInfo.Value.Replace(",", string.Empty).Replace(".", string.Empty), out var itemValue))
continue;
// Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemLink.Item.RowId, itemValue, itemLink.IsHQ));
break;
}
var messageCopy = message;
var senderCopy = sender;
var linkMatch = this.urlRegex.Match(message.TextValue);
if (linkMatch.Value.Length > 0)
this.LastLink = linkMatch.Value;
}
private void PrintWelcomeMessage()
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)
{
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))
{
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));
}
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()
if (string.IsNullOrEmpty(configuration.LastVersion) || !assemblyVersion.StartsWith(configuration.LastVersion))
{
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)
chatGui.PrintChat(new XivChatEntry
{
// Plugins aren't ready yet.
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;
}
this.hasAutoUpdatedPlugins = true;
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;
}
this.hasAutoUpdatedPlugins = true;
Task.Run(() => pluginManager.UpdatePluginsAsync(!configuration.AutoUpdatePlugins)).ContinueWith(task =>
var updatedPlugins = task.Result;
if (updatedPlugins != null && updatedPlugins.Any())
{
if (task.IsFaulted)
if (configuration.AutoUpdatePlugins)
{
Log.Error(task.Exception, Loc.Localize("DalamudPluginUpdateCheckFail", "Could not check for plugin updates."));
return;
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);
}
var updatedPlugins = task.Result;
if (updatedPlugins != null && updatedPlugins.Any())
else
{
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();
var data = Service<DataManager>.Get();
chatGui.PrintChat(new XivChatEntry
{
Message = new SeString(new List<Payload>()
{
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(" ["),
new UIForegroundPayload(500),
@ -321,12 +321,11 @@ namespace Dalamud.Game
RawPayload.LinkTerminator,
new UIForegroundPayload(0),
new TextPayload("]"),
}),
Type = XivChatType.Urgent,
});
}
}),
Type = XivChatType.Urgent,
});
}
});
}
}
});
}
}

View file

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

View file

@ -16,165 +16,164 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
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>
/// This class represents the state of the game client at the time of access.
/// Initializes a new instance of the <see cref="ClientState"/> class.
/// Set up client state access.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed class ClientState : IDisposable
internal ClientState()
{
private readonly ClientStateAddressResolver address;
private readonly Hook<SetupTerritoryTypeDelegate> setupTerritoryTypeHook;
this.address = new ClientStateAddressResolver();
this.address.Setup();
private bool lastConditionNone = true;
Log.Verbose("===== C L I E N T S T A T E =====");
/// <summary>
/// Initializes a new instance of the <see cref="ClientState"/> class.
/// Set up client state access.
/// </summary>
internal ClientState()
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;
}
[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)
{
this.address = new ClientStateAddressResolver();
this.address.Setup();
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;
Log.Debug("Is login");
this.lastConditionNone = false;
this.IsLoggedIn = true;
this.Login?.Invoke(this, null);
}
[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()
if (!condition.Any() && this.lastConditionNone == false)
{
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.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);
}
Log.Debug("Is logout");
this.lastConditionNone = true;
this.IsLoggedIn = false;
this.Logout?.Invoke(this, null);
}
}
}

View file

@ -1,114 +1,113 @@
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>
/// Client state memory address resolver.
/// Gets the address of the actor table.
/// </summary>
public sealed class ClientStateAddressResolver : BaseAddressResolver
public IntPtr ObjectTable { get; private set; }
/// <summary>
/// Gets the address of the buddy list.
/// </summary>
public IntPtr BuddyList { get; private set; }
/// <summary>
/// Gets the address of a pointer to the fate table.
/// </summary>
/// <remarks>
/// This is a static address to a pointer, not the address of the table itself.
/// </remarks>
public IntPtr FateTablePtr { get; private set; }
/// <summary>
/// Gets the address of the Group Manager.
/// </summary>
public IntPtr GroupManager { get; private set; }
/// <summary>
/// Gets the address of the local content id.
/// </summary>
public IntPtr LocalContentId { get; private set; }
/// <summary>
/// Gets the address of job gauge data.
/// </summary>
public IntPtr JobGaugeData { get; private set; }
/// <summary>
/// Gets the address of the keyboard state.
/// </summary>
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)
{
// Static offsets
// 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 ?? ?? ?? ??");
/// <summary>
/// Gets the address of the actor table.
/// </summary>
public IntPtr ObjectTable { get; private set; }
this.ObjectTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 83");
/// <summary>
/// Gets the address of the buddy list.
/// </summary>
public IntPtr BuddyList { 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 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.FateTablePtr = sig.GetStaticAddressFromSig("48 8B 15 ?? ?? ?? ?? 48 8B F9 44 0F B7 41 ??");
/// <summary>
/// Gets the address of the Group Manager.
/// </summary>
public IntPtr GroupManager { get; private set; }
this.GroupManager = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 80 B8 ?? ?? ?? ?? ?? 76 50");
/// <summary>
/// Gets the address of the local content id.
/// </summary>
public IntPtr LocalContentId { get; private set; }
this.LocalContentId = sig.GetStaticAddressFromSig("48 0F 44 05 ?? ?? ?? ?? 48 39 07");
this.JobGaugeData = sig.GetStaticAddressFromSig("48 8B 0D ?? ?? ?? ?? 48 85 C9 74 43") + 0x8;
/// <summary>
/// Gets the address of job gauge data.
/// </summary>
public IntPtr JobGaugeData { 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 the keyboard state.
/// </summary>
public IntPtr KeyboardState { get; private set; }
// 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;
/// <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.ConditionFlags = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? BA ?? ?? ?? ?? E8 ?? ?? ?? ?? B0 01 48 83 C4 30");
/// <summary>
/// Gets the address of the target manager.
/// </summary>
public IntPtr TargetManager { get; private set; }
this.TargetManager = sig.GetStaticAddressFromSig("48 8B 05 ?? ?? ?? ?? 48 8D 0D ?? ?? ?? ?? FF 50 ?? 48 85 DB");
/// <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");
}
this.GamepadPoll = sig.ScanText("40 ?? 57 41 ?? 48 81 EC ?? ?? ?? ?? 44 0F ?? ?? ?? ?? ?? ?? ?? 48 8B");
}
}

View file

@ -4,155 +4,154 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
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>
/// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc.
/// The current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed partial class Condition
public const int MaxConditionEntries = 100;
private readonly bool[] cache = new bool[MaxConditionEntries];
/// <summary>
/// Initializes a new instance of the <see cref="Condition"/> class.
/// </summary>
/// <param name="resolver">The ClientStateAddressResolver instance.</param>
internal Condition(ClientStateAddressResolver resolver)
{
/// <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;
this.Address = resolver.ConditionFlags;
}
private readonly bool[] cache = new bool[MaxConditionEntries];
/// <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>
/// Initializes a new instance of the <see cref="Condition"/> class.
/// </summary>
/// <param name="resolver">The ClientStateAddressResolver instance.</param>
internal Condition(ClientStateAddressResolver resolver)
/// <summary>
/// Event that gets fired when a condition is set.
/// Should only get fired for actual changes, so the previous value will always be !value.
/// </summary>
public event ConditionChangeDelegate? ConditionChange;
/// <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
{
this.Address = resolver.ConditionFlags;
if (flag < 0 || flag >= MaxConditionEntries)
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;
}
/// <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);
return false;
}
/// <summary>
/// Event that gets fired when a condition is set.
/// Should only get fired for actual changes, so the previous value will always be !value.
/// </summary>
public event ConditionChangeDelegate? ConditionChange;
/// <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];
/// <summary>
/// Gets the condition array base pointer.
/// </summary>
public IntPtr Address { get; private set; }
Service<Framework>.Get().Update += this.FrameworkUpdate;
}
/// <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]
private void FrameworkUpdate(Framework framework)
{
for (var i = 0; i < MaxConditionEntries; i++)
{
get
var value = this[i];
if (value != this.cache[i])
{
if (flag < 0 || flag >= MaxConditionEntries)
return false;
this.cache[i] = value;
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])
try
{
this.cache[i] = value;
try
{
this.ConditionChange?.Invoke((ConditionFlag)i, value);
}
catch (Exception ex)
{
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>
/// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc.
/// Finalizes an instance of the <see cref="Condition" /> class.
/// </summary>
public sealed partial class Condition : IDisposable
~Condition()
{
private bool isDisposed;
this.Dispose(false);
}
/// <summary>
/// Finalizes an instance of the <see cref="Condition" /> class.
/// </summary>
~Condition()
/// <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)
{
this.Dispose(false);
Service<Framework>.Get().Update -= this.FrameworkUpdate;
}
/// <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;
}
this.isDisposed = true;
}
}

View file

@ -1,453 +1,452 @@
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>
/// 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.
/// Unused.
/// </summary>
public enum ConditionFlag
{
/// <summary>
/// Unused.
/// </summary>
None = 0,
/// <summary>
/// Unable to execute command under normal conditions.
/// </summary>
NormalConditions = 1,
/// <summary>
/// Unable to execute command while unconscious.
/// </summary>
Unconscious = 2,
/// <summary>
/// Unable to execute command during an emote.
/// </summary>
Emoting = 3,
/// <summary>
/// Unable to execute command while mounted.
/// </summary>
Mounted = 4,
/// <summary>
/// Unable to execute command while crafting.
/// </summary>
Crafting = 5,
/// <summary>
/// Unable to execute command while gathering.
/// </summary>
Gathering = 6,
/// <summary>
/// Unable to execute command while melding materia.
/// </summary>
MeldingMateria = 7,
/// <summary>
/// Unable to execute command while operating a siege machine.
/// </summary>
OperatingSiegeMachine = 8,
/// <summary>
/// Unable to execute command while carrying an object.
/// </summary>
CarryingObject = 9,
/// <summary>
/// Unable to execute command while mounted.
/// </summary>
Mounted2 = 10,
/// <summary>
/// Unable to execute command while in that position.
/// </summary>
InThatPosition = 11,
/// <summary>
/// Unable to execute command while chocobo racing.
/// </summary>
ChocoboRacing = 12,
/// <summary>
/// Unable to execute command while playing a mini-game.
/// </summary>
PlayingMiniGame = 13,
/// <summary>
/// Unable to execute command while playing Lord of Verminion.
/// </summary>
PlayingLordOfVerminion = 14,
/// <summary>
/// Unable to execute command while participating in a custom match.
/// </summary>
ParticipatingInCustomMatch = 15,
/// <summary>
/// Unable to execute command while performing.
/// </summary>
Performing = 16,
// Unknown17 = 17,
// Unknown18 = 18,
// Unknown19 = 19,
// Unknown20 = 20,
// Unknown21 = 21,
// Unknown22 = 22,
// Unknown23 = 23,
// Unknown24 = 24,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
Occupied = 25,
/// <summary>
/// Unable to execute command during combat.
/// </summary>
InCombat = 26,
/// <summary>
/// Unable to execute command while casting.
/// </summary>
Casting = 27,
/// <summary>
/// Unable to execute command while suffering status affliction.
/// </summary>
SufferingStatusAffliction = 28,
/// <summary>
/// Unable to execute command while suffering status affliction.
/// </summary>
SufferingStatusAffliction2 = 29,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
Occupied30 = 30,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
// todo: not sure if this is used for other event states/???
OccupiedInEvent = 31,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
OccupiedInQuestEvent = 32,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
Occupied33 = 33,
/// <summary>
/// Unable to execute command while bound by duty.
/// </summary>
BoundByDuty = 34,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
OccupiedInCutSceneEvent = 35,
/// <summary>
/// Unable to execute command while in a dueling area.
/// </summary>
InDuelingArea = 36,
/// <summary>
/// Unable to execute command while a trade is open.
/// </summary>
TradeOpen = 37,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
Occupied38 = 38,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
Occupied39 = 39,
/// <summary>
/// Unable to execute command while crafting.
/// </summary>
Crafting40 = 40,
/// <summary>
/// Unable to execute command while preparing to craft.
/// </summary>
PreparingToCraft = 41,
/// <summary>
/// Unable to execute command while gathering.
/// </summary>
Gathering42 = 42,
/// <summary>
/// Unable to execute command while fishing.
/// </summary>
Fishing = 43,
// Unknown44 = 44,
/// <summary>
/// Unable to execute command while between areas.
/// </summary>
BetweenAreas = 45,
/// <summary>
/// Unable to execute command while stealthed.
/// </summary>
Stealthed = 46,
// Unknown47 = 47,
/// <summary>
/// Unable to execute command while jumping.
/// </summary>
Jumping = 48,
/// <summary>
/// Unable to execute command while auto-run is active.
/// </summary>
AutorunActive = 49,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
// todo: used for other shits?
OccupiedSummoningBell = 50,
/// <summary>
/// Unable to execute command while between areas.
/// </summary>
BetweenAreas51 = 51,
/// <summary>
/// Unable to execute command due to system error.
/// </summary>
SystemError = 52,
/// <summary>
/// Unable to execute command while logging out.
/// </summary>
LoggingOut = 53,
/// <summary>
/// Unable to execute command at this location.
/// </summary>
ConditionLocation = 54,
/// <summary>
/// Unable to execute command while waiting for duty.
/// </summary>
WaitingForDuty = 55,
/// <summary>
/// Unable to execute command while bound by duty.
/// </summary>
BoundByDuty56 = 56,
/// <summary>
/// Unable to execute command at this time.
/// </summary>
Unknown57 = 57,
/// <summary>
/// Unable to execute command while watching a cutscene.
/// </summary>
WatchingCutscene = 58,
/// <summary>
/// Unable to execute command while waiting for Duty Finder.
/// </summary>
WaitingForDutyFinder = 59,
/// <summary>
/// Unable to execute command while creating a character.
/// </summary>
CreatingCharacter = 60,
/// <summary>
/// Unable to execute command while jumping.
/// </summary>
Jumping61 = 61,
/// <summary>
/// Unable to execute command while the PvP display is active.
/// </summary>
PvPDisplayActive = 62,
/// <summary>
/// Unable to execute command while suffering status affliction.
/// </summary>
SufferingStatusAffliction63 = 63,
/// <summary>
/// Unable to execute command while mounting.
/// </summary>
Mounting = 64,
/// <summary>
/// Unable to execute command while carrying an item.
/// </summary>
CarryingItem = 65,
/// <summary>
/// Unable to execute command while using the Party Finder.
/// </summary>
UsingPartyFinder = 66,
/// <summary>
/// Unable to execute command while using housing functions.
/// </summary>
UsingHousingFunctions = 67,
/// <summary>
/// Unable to execute command while transformed.
/// </summary>
Transformed = 68,
/// <summary>
/// Unable to execute command while on the free trial.
/// </summary>
OnFreeTrial = 69,
/// <summary>
/// Unable to execute command while being moved.
/// </summary>
BeingMoved = 70,
/// <summary>
/// Unable to execute command while mounting.
/// </summary>
Mounting71 = 71,
/// <summary>
/// Unable to execute command while suffering status affliction.
/// </summary>
SufferingStatusAffliction72 = 72,
/// <summary>
/// Unable to execute command while suffering status affliction.
/// </summary>
SufferingStatusAffliction73 = 73,
/// <summary>
/// Unable to execute command while registering for a race or match.
/// </summary>
RegisteringForRaceOrMatch = 74,
/// <summary>
/// Unable to execute command while waiting for a race or match.
/// </summary>
WaitingForRaceOrMatch = 75,
/// <summary>
/// Unable to execute command while waiting for a Triple Triad match.
/// </summary>
WaitingForTripleTriadMatch = 76,
/// <summary>
/// Unable to execute command while in flight.
/// </summary>
InFlight = 77,
/// <summary>
/// Unable to execute command while watching a cutscene.
/// </summary>
WatchingCutscene78 = 78,
/// <summary>
/// Unable to execute command while delving into a deep dungeon.
/// </summary>
InDeepDungeon = 79,
/// <summary>
/// Unable to execute command while swimming.
/// </summary>
Swimming = 80,
/// <summary>
/// Unable to execute command while diving.
/// </summary>
Diving = 81,
/// <summary>
/// Unable to execute command while registering for a Triple Triad match.
/// </summary>
RegisteringForTripleTriadMatch = 82,
/// <summary>
/// Unable to execute command while waiting for a Triple Triad match.
/// </summary>
WaitingForTripleTriadMatch83 = 83,
/// <summary>
/// Unable to execute command while participating in a cross-world party or alliance.
/// </summary>
ParticipatingInCrossWorldPartyOrAlliance = 84,
// Unknown85 = 85,
/// <summary>
/// Unable to execute command while playing duty record.
/// </summary>
DutyRecorderPlayback = 86,
/// <summary>
/// Unable to execute command while casting.
/// </summary>
Casting87 = 87,
/// <summary>
/// Unable to execute command in this state.
/// </summary>
InThisState88 = 88,
/// <summary>
/// Unable to execute command in this state.
/// </summary>
InThisState89 = 89,
/// <summary>
/// Unable to execute command while role-playing.
/// </summary>
RolePlaying = 90,
/// <summary>
/// Unable to execute command while bound by duty.
/// </summary>
BoundToDuty97 = 91,
/// <summary>
/// Unable to execute command while readying to visit another World.
/// </summary>
ReadyingVisitOtherWorld = 92,
/// <summary>
/// Unable to execute command while waiting to visit another World.
/// </summary>
WaitingToVisitOtherWorld = 93,
/// <summary>
/// Unable to execute command while using a parasol.
/// </summary>
UsingParasol = 94,
/// <summary>
/// Unable to execute command while bound by duty.
/// </summary>
BoundByDuty95 = 95,
}
None = 0,
/// <summary>
/// Unable to execute command under normal conditions.
/// </summary>
NormalConditions = 1,
/// <summary>
/// Unable to execute command while unconscious.
/// </summary>
Unconscious = 2,
/// <summary>
/// Unable to execute command during an emote.
/// </summary>
Emoting = 3,
/// <summary>
/// Unable to execute command while mounted.
/// </summary>
Mounted = 4,
/// <summary>
/// Unable to execute command while crafting.
/// </summary>
Crafting = 5,
/// <summary>
/// Unable to execute command while gathering.
/// </summary>
Gathering = 6,
/// <summary>
/// Unable to execute command while melding materia.
/// </summary>
MeldingMateria = 7,
/// <summary>
/// Unable to execute command while operating a siege machine.
/// </summary>
OperatingSiegeMachine = 8,
/// <summary>
/// Unable to execute command while carrying an object.
/// </summary>
CarryingObject = 9,
/// <summary>
/// Unable to execute command while mounted.
/// </summary>
Mounted2 = 10,
/// <summary>
/// Unable to execute command while in that position.
/// </summary>
InThatPosition = 11,
/// <summary>
/// Unable to execute command while chocobo racing.
/// </summary>
ChocoboRacing = 12,
/// <summary>
/// Unable to execute command while playing a mini-game.
/// </summary>
PlayingMiniGame = 13,
/// <summary>
/// Unable to execute command while playing Lord of Verminion.
/// </summary>
PlayingLordOfVerminion = 14,
/// <summary>
/// Unable to execute command while participating in a custom match.
/// </summary>
ParticipatingInCustomMatch = 15,
/// <summary>
/// Unable to execute command while performing.
/// </summary>
Performing = 16,
// Unknown17 = 17,
// Unknown18 = 18,
// Unknown19 = 19,
// Unknown20 = 20,
// Unknown21 = 21,
// Unknown22 = 22,
// Unknown23 = 23,
// Unknown24 = 24,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
Occupied = 25,
/// <summary>
/// Unable to execute command during combat.
/// </summary>
InCombat = 26,
/// <summary>
/// Unable to execute command while casting.
/// </summary>
Casting = 27,
/// <summary>
/// Unable to execute command while suffering status affliction.
/// </summary>
SufferingStatusAffliction = 28,
/// <summary>
/// Unable to execute command while suffering status affliction.
/// </summary>
SufferingStatusAffliction2 = 29,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
Occupied30 = 30,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
// todo: not sure if this is used for other event states/???
OccupiedInEvent = 31,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
OccupiedInQuestEvent = 32,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
Occupied33 = 33,
/// <summary>
/// Unable to execute command while bound by duty.
/// </summary>
BoundByDuty = 34,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
OccupiedInCutSceneEvent = 35,
/// <summary>
/// Unable to execute command while in a dueling area.
/// </summary>
InDuelingArea = 36,
/// <summary>
/// Unable to execute command while a trade is open.
/// </summary>
TradeOpen = 37,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
Occupied38 = 38,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
Occupied39 = 39,
/// <summary>
/// Unable to execute command while crafting.
/// </summary>
Crafting40 = 40,
/// <summary>
/// Unable to execute command while preparing to craft.
/// </summary>
PreparingToCraft = 41,
/// <summary>
/// Unable to execute command while gathering.
/// </summary>
Gathering42 = 42,
/// <summary>
/// Unable to execute command while fishing.
/// </summary>
Fishing = 43,
// Unknown44 = 44,
/// <summary>
/// Unable to execute command while between areas.
/// </summary>
BetweenAreas = 45,
/// <summary>
/// Unable to execute command while stealthed.
/// </summary>
Stealthed = 46,
// Unknown47 = 47,
/// <summary>
/// Unable to execute command while jumping.
/// </summary>
Jumping = 48,
/// <summary>
/// Unable to execute command while auto-run is active.
/// </summary>
AutorunActive = 49,
/// <summary>
/// Unable to execute command while occupied.
/// </summary>
// todo: used for other shits?
OccupiedSummoningBell = 50,
/// <summary>
/// Unable to execute command while between areas.
/// </summary>
BetweenAreas51 = 51,
/// <summary>
/// Unable to execute command due to system error.
/// </summary>
SystemError = 52,
/// <summary>
/// Unable to execute command while logging out.
/// </summary>
LoggingOut = 53,
/// <summary>
/// Unable to execute command at this location.
/// </summary>
ConditionLocation = 54,
/// <summary>
/// Unable to execute command while waiting for duty.
/// </summary>
WaitingForDuty = 55,
/// <summary>
/// Unable to execute command while bound by duty.
/// </summary>
BoundByDuty56 = 56,
/// <summary>
/// Unable to execute command at this time.
/// </summary>
Unknown57 = 57,
/// <summary>
/// Unable to execute command while watching a cutscene.
/// </summary>
WatchingCutscene = 58,
/// <summary>
/// Unable to execute command while waiting for Duty Finder.
/// </summary>
WaitingForDutyFinder = 59,
/// <summary>
/// Unable to execute command while creating a character.
/// </summary>
CreatingCharacter = 60,
/// <summary>
/// Unable to execute command while jumping.
/// </summary>
Jumping61 = 61,
/// <summary>
/// Unable to execute command while the PvP display is active.
/// </summary>
PvPDisplayActive = 62,
/// <summary>
/// Unable to execute command while suffering status affliction.
/// </summary>
SufferingStatusAffliction63 = 63,
/// <summary>
/// Unable to execute command while mounting.
/// </summary>
Mounting = 64,
/// <summary>
/// Unable to execute command while carrying an item.
/// </summary>
CarryingItem = 65,
/// <summary>
/// Unable to execute command while using the Party Finder.
/// </summary>
UsingPartyFinder = 66,
/// <summary>
/// Unable to execute command while using housing functions.
/// </summary>
UsingHousingFunctions = 67,
/// <summary>
/// Unable to execute command while transformed.
/// </summary>
Transformed = 68,
/// <summary>
/// Unable to execute command while on the free trial.
/// </summary>
OnFreeTrial = 69,
/// <summary>
/// Unable to execute command while being moved.
/// </summary>
BeingMoved = 70,
/// <summary>
/// Unable to execute command while mounting.
/// </summary>
Mounting71 = 71,
/// <summary>
/// Unable to execute command while suffering status affliction.
/// </summary>
SufferingStatusAffliction72 = 72,
/// <summary>
/// Unable to execute command while suffering status affliction.
/// </summary>
SufferingStatusAffliction73 = 73,
/// <summary>
/// Unable to execute command while registering for a race or match.
/// </summary>
RegisteringForRaceOrMatch = 74,
/// <summary>
/// Unable to execute command while waiting for a race or match.
/// </summary>
WaitingForRaceOrMatch = 75,
/// <summary>
/// Unable to execute command while waiting for a Triple Triad match.
/// </summary>
WaitingForTripleTriadMatch = 76,
/// <summary>
/// Unable to execute command while in flight.
/// </summary>
InFlight = 77,
/// <summary>
/// Unable to execute command while watching a cutscene.
/// </summary>
WatchingCutscene78 = 78,
/// <summary>
/// Unable to execute command while delving into a deep dungeon.
/// </summary>
InDeepDungeon = 79,
/// <summary>
/// Unable to execute command while swimming.
/// </summary>
Swimming = 80,
/// <summary>
/// Unable to execute command while diving.
/// </summary>
Diving = 81,
/// <summary>
/// Unable to execute command while registering for a Triple Triad match.
/// </summary>
RegisteringForTripleTriadMatch = 82,
/// <summary>
/// Unable to execute command while waiting for a Triple Triad match.
/// </summary>
WaitingForTripleTriadMatch83 = 83,
/// <summary>
/// Unable to execute command while participating in a cross-world party or alliance.
/// </summary>
ParticipatingInCrossWorldPartyOrAlliance = 84,
// Unknown85 = 85,
/// <summary>
/// Unable to execute command while playing duty record.
/// </summary>
DutyRecorderPlayback = 86,
/// <summary>
/// Unable to execute command while casting.
/// </summary>
Casting87 = 87,
/// <summary>
/// Unable to execute command in this state.
/// </summary>
InThisState88 = 88,
/// <summary>
/// Unable to execute command in this state.
/// </summary>
InThisState89 = 89,
/// <summary>
/// Unable to execute command while role-playing.
/// </summary>
RolePlaying = 90,
/// <summary>
/// Unable to execute command while bound by duty.
/// </summary>
BoundToDuty97 = 91,
/// <summary>
/// Unable to execute command while readying to visit another World.
/// </summary>
ReadyingVisitOtherWorld = 92,
/// <summary>
/// Unable to execute command while waiting to visit another World.
/// </summary>
WaitingToVisitOtherWorld = 93,
/// <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,131 +6,130 @@ using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.Text.SeStringHandling;
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>
/// This class represents an FFXIV Fate.
/// Initializes a new instance of the <see cref="Fate"/> class.
/// </summary>
public unsafe partial class Fate : IEquatable<Fate>
/// <param name="address">The address of this fate in memory.</param>
internal Fate(IntPtr 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();
this.Address = address;
}
/// <summary>
/// This class represents an FFXIV Fate.
/// Gets the address of this Fate in memory.
/// </summary>
public unsafe partial class Fate
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)
{
/// <summary>
/// Gets the Fate ID of this <see cref="Fate" />.
/// </summary>
public ushort FateId => this.Struct->FateId;
if (fate1 is null || fate2 is null)
return Equals(fate1, 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);
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>
/// 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,33 +1,32 @@
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>
/// This represents the state of a single Fate.
/// The Fate is active.
/// </summary>
public enum FateState : byte
{
/// <summary>
/// The Fate is active.
/// </summary>
Running = 0x02,
Running = 0x02,
/// <summary>
/// The Fate has ended.
/// </summary>
Ended = 0x04,
/// <summary>
/// The Fate has ended.
/// </summary>
Ended = 0x04,
/// <summary>
/// The player failed the Fate.
/// </summary>
Failed = 0x05,
/// <summary>
/// The player failed the Fate.
/// </summary>
Failed = 0x05,
/// <summary>
/// The Fate is preparing to run.
/// </summary>
Preparation = 0x07,
/// <summary>
/// The Fate is preparing to run.
/// </summary>
Preparation = 0x07,
/// <summary>
/// The Fate is preparing to end.
/// </summary>
WaitingForEnd = 0x08,
}
/// <summary>
/// The Fate is preparing to end.
/// </summary>
WaitingForEnd = 0x08,
}

View file

@ -6,143 +6,142 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
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>
/// This collection represents the currently available Fate events.
/// Initializes a new instance of the <see cref="FateTable"/> class.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed partial class FateTable
/// <param name="addressResolver">Client state address resolver.</param>
internal FateTable(ClientStateAddressResolver addressResolver)
{
private readonly ClientStateAddressResolver address;
this.address = addressResolver;
/// <summary>
/// Initializes a new instance of the <see cref="FateTable"/> class.
/// </summary>
/// <param name="addressResolver">Client state address resolver.</param>
internal FateTable(ClientStateAddressResolver addressResolver)
Log.Verbose($"Fate table address 0x{this.address.FateTablePtr.ToInt64():X}");
}
/// <summary>
/// Gets the address of the Fate table.
/// </summary>
public IntPtr Address => this.address.FateTablePtr;
/// <summary>
/// Gets the amount of currently active Fates.
/// </summary>
public unsafe int Length
{
get
{
this.address = addressResolver;
Log.Verbose($"Fate table address 0x{this.address.FateTablePtr.ToInt64():X}");
}
/// <summary>
/// Gets the address of the Fate table.
/// </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;
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;
var fateTable = this.FateTableAddress;
if (fateTable == IntPtr.Zero)
return IntPtr.Zero;
return 0;
var firstFate = this.Struct->FirstFatePtr;
return *(IntPtr*)(firstFate + (8 * index));
}
// Sonar used this to check if the table was safe to read
var check = Struct->Unk80.ToInt64();
if (check == 0)
return 0;
/// <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();
var start = Struct->FirstFatePtr.ToInt64();
var end = Struct->LastFatePtr.ToInt64();
if (start == 0 || end == 0)
return 0;
if (clientState.LocalContentId == 0)
return null;
if (offset == IntPtr.Zero)
return null;
return new Fate(offset);
return (int)((end - start) / 8);
}
}
/// <summary>
/// This collection represents the currently available Fate events.
/// Gets the address of the Fate table.
/// </summary>
public sealed partial class FateTable : IReadOnlyCollection<Fate>
internal unsafe IntPtr FateTableAddress
{
/// <inheritdoc/>
int IReadOnlyCollection<Fate>.Count => this.Length;
/// <inheritdoc/>
public IEnumerator<Fate> GetEnumerator()
get
{
for (var i = 0; i < this.Length; i++)
{
yield return this[i];
}
}
if (this.address.FateTablePtr == IntPtr.Zero)
return IntPtr.Zero;
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
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;
var fateTable = this.FateTableAddress;
if (fateTable == IntPtr.Zero)
return IntPtr.Zero;
var firstFate = this.Struct->FirstFatePtr;
return *(IntPtr*)(firstFate + (8 * index));
}
/// <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,96 +1,95 @@
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>
/// Bitmask of the Button ushort used by the game.
/// No buttons pressed.
/// </summary>
[Flags]
public enum GamepadButtons : ushort
{
/// <summary>
/// No buttons pressed.
/// </summary>
None = 0,
None = 0,
/// <summary>
/// Digipad up.
/// </summary>
DpadUp = 0x0001,
/// <summary>
/// Digipad up.
/// </summary>
DpadUp = 0x0001,
/// <summary>
/// Digipad down.
/// </summary>
DpadDown = 0x0002,
/// <summary>
/// Digipad down.
/// </summary>
DpadDown = 0x0002,
/// <summary>
/// Digipad left.
/// </summary>
DpadLeft = 0x0004,
/// <summary>
/// Digipad left.
/// </summary>
DpadLeft = 0x0004,
/// <summary>
/// Digipad right.
/// </summary>
DpadRight = 0x0008,
/// <summary>
/// Digipad right.
/// </summary>
DpadRight = 0x0008,
/// <summary>
/// North action button. Triangle on PS, Y on Xbox.
/// </summary>
North = 0x0010,
/// <summary>
/// North action button. Triangle on PS, Y on Xbox.
/// </summary>
North = 0x0010,
/// <summary>
/// South action button. Cross on PS, A on Xbox.
/// </summary>
South = 0x0020,
/// <summary>
/// South action button. Cross on PS, A on Xbox.
/// </summary>
South = 0x0020,
/// <summary>
/// West action button. Square on PS, X on Xbos.
/// </summary>
West = 0x0040,
/// <summary>
/// West action button. Square on PS, X on Xbos.
/// </summary>
West = 0x0040,
/// <summary>
/// East action button. Circle on PS, B on Xbox.
/// </summary>
East = 0x0080,
/// <summary>
/// East action button. Circle on PS, B on Xbox.
/// </summary>
East = 0x0080,
/// <summary>
/// First button on left shoulder side.
/// </summary>
L1 = 0x0100,
/// <summary>
/// First button on left shoulder side.
/// </summary>
L1 = 0x0100,
/// <summary>
/// Second button on left shoulder side. Analog input lost in this bitmask.
/// </summary>
L2 = 0x0200,
/// <summary>
/// Second button on left shoulder side. Analog input lost in this bitmask.
/// </summary>
L2 = 0x0200,
/// <summary>
/// Press on left analogue stick.
/// </summary>
L3 = 0x0400,
/// <summary>
/// Press on left analogue stick.
/// </summary>
L3 = 0x0400,
/// <summary>
/// First button on right shoulder.
/// </summary>
R1 = 0x0800,
/// <summary>
/// First button on right shoulder.
/// </summary>
R1 = 0x0800,
/// <summary>
/// Second button on right shoulder. Analog input lost in this bitmask.
/// </summary>
R2 = 0x1000,
/// <summary>
/// Second button on right shoulder. Analog input lost in this bitmask.
/// </summary>
R2 = 0x1000,
/// <summary>
/// Press on right analogue stick.
/// </summary>
R3 = 0x2000,
/// <summary>
/// Press on right analogue stick.
/// </summary>
R3 = 0x2000,
/// <summary>
/// Button on the right inner side of the controller. Options on PS, Start on Xbox.
/// </summary>
Start = 0x8000,
/// <summary>
/// Button on the right inner side of the controller. Options on PS, Start on Xbox.
/// </summary>
Start = 0x8000,
/// <summary>
/// Button on the left inner side of the controller. ??? on PS, Back on Xbox.
/// </summary>
Select = 0x4000,
}
/// <summary>
/// Button on the left inner side of the controller. ??? on PS, Back on Xbox.
/// </summary>
Select = 0x4000,
}

View file

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

View file

@ -4,258 +4,257 @@ using Dalamud.Hooking;
using ImGuiNET;
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>
/// Exposes the game gamepad state to dalamud.
///
/// Will block game's gamepad input if <see cref="ImGuiConfigFlags.NavEnableGamepad"/> is set.
/// Initializes a new instance of the <see cref="GamepadState" /> class.
/// </summary>
public unsafe class GamepadState : IDisposable
/// <param name="resolver">Resolver knowing the pointer to the GamepadPoll function.</param>
public GamepadState(ClientStateAddressResolver resolver)
{
private readonly Hook<ControllerPoll> gamepadPoll;
Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}");
this.gamepadPoll = new Hook<ControllerPoll>(resolver.GamepadPoll, this.GamepadPollDetour);
}
private bool isDisposed;
/// <summary>
/// Finalizes an instance of the <see cref="GamepadState" /> class.
/// </summary>
~GamepadState()
{
this.Dispose(false);
}
private int leftStickX;
private int leftStickY;
private int rightStickX;
private int rightStickY;
private delegate int ControllerPoll(IntPtr controllerInput);
/// <summary>
/// Initializes a new instance of the <see cref="GamepadState" /> class.
/// </summary>
/// <param name="resolver">Resolver knowing the pointer to the GamepadPoll function.</param>
public GamepadState(ClientStateAddressResolver resolver)
/// <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
{
Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}");
this.gamepadPoll = new Hook<ControllerPoll>(resolver.GamepadPoll, this.GamepadPollDetour);
}
this.GamepadInputAddress = gamepadInput;
var input = (GamepadInput*)gamepadInput;
this.leftStickX = input->LeftStickX;
this.leftStickY = input->LeftStickY;
this.rightStickX = input->RightStickX;
this.rightStickY = input->RightStickY;
this.ButtonsRaw = input->ButtonsRaw;
this.ButtonsPressed = input->ButtonsPressed;
this.ButtonsReleased = input->ButtonsReleased;
this.ButtonsRepeat = input->ButtonsRepeat;
/// <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
if (this.NavEnableGamepad)
{
this.GamepadInputAddress = gamepadInput;
var input = (GamepadInput*)gamepadInput;
this.leftStickX = input->LeftStickX;
this.leftStickY = input->LeftStickY;
this.rightStickX = input->RightStickX;
this.rightStickY = input->RightStickY;
this.ButtonsRaw = input->ButtonsRaw;
this.ButtonsPressed = input->ButtonsPressed;
this.ButtonsReleased = input->ButtonsReleased;
this.ButtonsRepeat = input->ButtonsRepeat;
input->LeftStickX = 0;
input->LeftStickY = 0;
input->RightStickX = 0;
input->RightStickY = 0;
if (this.NavEnableGamepad)
{
input->LeftStickX = 0;
input->LeftStickY = 0;
input->RightStickX = 0;
input->RightStickY = 0;
// NOTE (Chiv) Zeroing `ButtonsRaw` destroys `ButtonPressed`, `ButtonReleased`
// and `ButtonRepeat` as the game uses the RAW input to determine those (apparently).
// It does block, however, all input to the game.
// Leaving `ButtonsRaw` as it is and only zeroing the other leaves e.g. long-hold L2/R2
// and the digipad (in some situations, but thankfully not in menus) functional.
// We can either:
// (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
// (L2/R2 being held down activates CrossHotBar but activating an ability is impossible because of the others blocked input,
// Digipad is ignored in menus but without any menu's one still switches target or party members, but cannot interact with them
// because of the other blocked input)
// `ButtonPressed` is pretty useful but its hella confusing to the user, so we do (a) and advise plugins do not rely on
// `ButtonPressed` while ImGuiConfigFlags.NavEnableGamepad is set.
// This is debatable.
// ImGui itself does not care either way as it uses the Raw values and does its own state handling.
const ushort deletionMask = (ushort)(~GamepadButtons.L2
& ~GamepadButtons.R2
& ~GamepadButtons.DpadDown
& ~GamepadButtons.DpadLeft
& ~GamepadButtons.DpadUp
& ~GamepadButtons.DpadRight);
input->ButtonsRaw &= deletionMask;
input->ButtonsPressed = 0;
input->ButtonsReleased = 0;
input->ButtonsRepeat = 0;
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) Zeroing `ButtonsRaw` destroys `ButtonPressed`, `ButtonReleased`
// and `ButtonRepeat` as the game uses the RAW input to determine those (apparently).
// It does block, however, all input to the game.
// Leaving `ButtonsRaw` as it is and only zeroing the other leaves e.g. long-hold L2/R2
// and the digipad (in some situations, but thankfully not in menus) functional.
// We can either:
// (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
// (L2/R2 being held down activates CrossHotBar but activating an ability is impossible because of the others blocked input,
// Digipad is ignored in menus but without any menu's one still switches target or party members, but cannot interact with them
// because of the other blocked input)
// `ButtonPressed` is pretty useful but its hella confusing to the user, so we do (a) and advise plugins do not rely on
// `ButtonPressed` while ImGuiConfigFlags.NavEnableGamepad is set.
// This is debatable.
// ImGui itself does not care either way as it uses the Raw values and does its own state handling.
const ushort deletionMask = (ushort)(~GamepadButtons.L2
& ~GamepadButtons.R2
& ~GamepadButtons.DpadDown
& ~GamepadButtons.DpadLeft
& ~GamepadButtons.DpadUp
& ~GamepadButtons.DpadRight);
input->ButtonsRaw &= deletionMask;
input->ButtonsPressed = 0;
input->ButtonsReleased = 0;
input->ButtonsRepeat = 0;
return 0;
}
this.isDisposed = true;
// 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();
}
this.isDisposed = true;
}
}

View file

@ -1,23 +1,22 @@
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>
/// DRG Blood of the Dragon state types.
/// Inactive type.
/// </summary>
public enum BOTDState : byte
{
/// <summary>
/// Inactive type.
/// </summary>
NONE = 0,
NONE = 0,
/// <summary>
/// Blood of the Dragon is active.
/// </summary>
BOTD = 1,
/// <summary>
/// Blood of the Dragon is active.
/// </summary>
BOTD = 1,
/// <summary>
/// Life of the Dragon is active.
/// </summary>
LOTD = 2,
}
/// <summary>
/// Life of the Dragon is active.
/// </summary>
LOTD = 2,
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,31 +1,30 @@
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>
/// Samurai Sen types.
/// No Sen.
/// </summary>
[Flags]
public enum Sen : byte
{
/// <summary>
/// No Sen.
/// </summary>
NONE = 0,
NONE = 0,
/// <summary>
/// Setsu Sen type.
/// </summary>
SETSU = 1 << 0,
/// <summary>
/// Setsu Sen type.
/// </summary>
SETSU = 1 << 0,
/// <summary>
/// Getsu Sen type.
/// </summary>
GETSU = 1 << 1,
/// <summary>
/// Getsu Sen type.
/// </summary>
GETSU = 1 << 1,
/// <summary>
/// Ka Sen type.
/// </summary>
KA = 1 << 2,
}
/// <summary>
/// Ka Sen type.
/// </summary>
KA = 1 << 2,
}

View file

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

View file

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

View file

@ -7,48 +7,47 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
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>
/// This class converts in-memory Job gauge data to structs.
/// Initializes a new instance of the <see cref="JobGauges"/> class.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public class JobGauges
/// <param name="addressResolver">Address resolver with the JobGauge memory location(s).</param>
public JobGauges(ClientStateAddressResolver addressResolver)
{
private Dictionary<Type, JobGaugeBase> cache = new();
this.Address = addressResolver.JobGaugeData;
/// <summary>
/// Initializes a new instance of the <see cref="JobGauges"/> class.
/// </summary>
/// <param name="addressResolver">Address resolver with the JobGauge memory location(s).</param>
public JobGauges(ClientStateAddressResolver addressResolver)
Log.Verbose($"JobGaugeData address 0x{this.Address.ToInt64():X}");
}
/// <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))
{
this.Address = addressResolver.JobGaugeData;
Log.Verbose($"JobGaugeData address 0x{this.Address.ToInt64():X}");
gauge = this.cache[typeof(T)] = (T)Activator.CreateInstance(typeof(T), BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { this.Address }, null);
}
/// <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;
}
return (T)gauge;
}
}

View file

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

View file

@ -1,67 +1,66 @@
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>
/// In-memory BLM job gauge.
/// Initializes a new instance of the <see cref="BLMGauge"/> class.
/// </summary>
public unsafe class BLMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.BlackMageGauge>
/// <param name="address">Address of the job gauge.</param>
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,40 +2,39 @@ using System;
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>
/// In-memory BRD job gauge.
/// Initializes a new instance of the <see cref="BRDGauge"/> class.
/// </summary>
public unsafe class BRDGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.BardGauge>
/// <param name="address">Address of the job gauge.</param>
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,60 +1,59 @@
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>
/// In-memory DNC job gauge.
/// Initializes a new instance of the <see cref="DNCGauge"/> class.
/// </summary>
public unsafe class DNCGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.DancerGauge>
/// <param name="address">Address of the job gauge.</param>
internal DNCGauge(IntPtr address)
: base(address)
{
/// <summary>
/// Initializes a new instance of the <see cref="DNCGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal DNCGauge(IntPtr address)
: base(address)
{
}
/// <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 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;
}

View file

@ -2,35 +2,34 @@ using System;
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>
/// In-memory DRG job gauge.
/// Initializes a new instance of the <see cref="DRGGauge"/> class.
/// </summary>
public unsafe class DRGGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.DragoonGauge>
/// <param name="address">Address of the job gauge.</param>
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,40 +1,39 @@
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>
/// In-memory DRK job gauge.
/// Initializes a new instance of the <see cref="DRKGauge"/> class.
/// </summary>
public unsafe class DRKGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.DarkKnightGauge>
/// <param name="address">Address of the job gauge.</param>
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,34 +1,33 @@
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>
/// In-memory GNB job gauge.
/// Initializes a new instance of the <see cref="GNBGauge"/> class.
/// </summary>
public unsafe class GNBGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.GunbreakerGauge>
/// <param name="address">Address of the job gauge.</param>
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,24 +1,23 @@
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>
/// Base job gauge class.
/// Initializes a new instance of the <see cref="JobGaugeBase"/> class.
/// </summary>
public abstract unsafe class JobGaugeBase
/// <param name="address">Address of the job gauge.</param>
internal JobGaugeBase(IntPtr 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>
/// Gets the address of this job gauge in memory.
/// </summary>
public IntPtr Address { get; }
this.Address = address;
}
/// <summary>
/// Gets the address of this job gauge in memory.
/// </summary>
public IntPtr Address { get; }
}

View file

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

View file

@ -1,56 +1,55 @@
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>
/// In-memory MCH job gauge.
/// Initializes a new instance of the <see cref="MCHGauge"/> class.
/// </summary>
public unsafe class MCHGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.MachinistGauge>
/// <param name="address">Address of the job gauge.</param>
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,24 +1,23 @@
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>
/// In-memory MNK job gauge.
/// Initializes a new instance of the <see cref="MNKGauge"/> class.
/// </summary>
public unsafe class MNKGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.MonkGauge>
/// <param name="address">Address of the job gauge.</param>
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>
/// Gets the number of Chakra available.
/// </summary>
public byte Chakra => this.Struct->Chakra;
}
/// <summary>
/// Gets the number of Chakra available.
/// </summary>
public byte Chakra => this.Struct->Chakra;
}

View file

@ -1,34 +1,33 @@
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>
/// In-memory NIN job gauge.
/// Initializes a new instance of the <see cref="NINGauge"/> class.
/// </summary>
public unsafe class NINGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.NinjaGauge>
/// <param name="address">The address of the gauge.</param>
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,24 +1,23 @@
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>
/// In-memory PLD job gauge.
/// Initializes a new instance of the <see cref="PLDGauge"/> class.
/// </summary>
public unsafe class PLDGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.PaladinGauge>
/// <param name="address">Address of the job gauge.</param>
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>
/// Gets the current level of the Oath gauge.
/// </summary>
public byte OathGauge => this.Struct->OathGauge;
}
/// <summary>
/// Gets the current level of the Oath gauge.
/// </summary>
public byte OathGauge => this.Struct->OathGauge;
}

View file

@ -1,29 +1,28 @@
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>
/// In-memory RDM job gauge.
/// Initializes a new instance of the <see cref="RDMGauge"/> class.
/// </summary>
public unsafe class RDMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.RedMageGauge>
/// <param name="address">Address of the job gauge.</param>
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,53 +2,52 @@ using System;
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>
/// In-memory SAM job gauge.
/// Initializes a new instance of the <see cref="SAMGauge"/> class.
/// </summary>
public unsafe class SAMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.SamuraiGauge>
/// <param name="address">Address of the job gauge.</param>
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,40 +2,39 @@ using System;
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>
/// In-memory SCH job gauge.
/// Initializes a new instance of the <see cref="SCHGauge"/> class.
/// </summary>
public unsafe class SCHGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.ScholarGauge>
/// <param name="address">Address of the job gauge.</param>
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,59 +2,58 @@ using System;
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>
/// In-memory SMN job gauge.
/// Initializes a new instance of the <see cref="SMNGauge"/> class.
/// </summary>
public unsafe class SMNGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.SummonerGauge>
/// <param name="address">Address of the job gauge.</param>
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,24 +1,23 @@
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>
/// In-memory WAR job gauge.
/// Initializes a new instance of the <see cref="WARGauge"/> class.
/// </summary>
public unsafe class WARGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.WarriorGauge>
/// <param name="address">Address of the job gauge.</param>
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>
/// Gets the amount of wrath in the Beast gauge.
/// </summary>
public byte BeastGauge => this.Struct->BeastGauge;
}
/// <summary>
/// Gets the amount of wrath in the Beast gauge.
/// </summary>
public byte BeastGauge => this.Struct->BeastGauge;
}

View file

@ -1,34 +1,33 @@
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>
/// In-memory WHM job gauge.
/// Initializes a new instance of the <see cref="WHMGauge"/> class.
/// </summary>
public unsafe class WHMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.WhiteMageGauge>
/// <param name="address">Address of the job gauge.</param>
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,155 +6,154 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
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>
/// Wrapper around the game keystate buffer, which contains the pressed state for all keyboard keys, indexed by virtual vkCode.
/// Initializes a new instance of the <see cref="KeyState"/> class.
/// </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
/// <param name="addressResolver">The ClientStateAddressResolver instance.</param>
public KeyState(ClientStateAddressResolver addressResolver)
{
// 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;
var moduleBaseAddress = Service<SigScanner>.Get().Module.BaseAddress;
/// <summary>
/// Initializes a new instance of the <see cref="KeyState"/> class.
/// </summary>
/// <param name="addressResolver">The ClientStateAddressResolver instance.</param>
public KeyState(ClientStateAddressResolver addressResolver)
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())
{
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));
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));
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,28 +1,27 @@
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>
/// An Enum describing possible BattleNpc kinds.
/// Invalid BattleNpc.
/// </summary>
public enum BattleNpcSubKind : byte
{
/// <summary>
/// Invalid BattleNpc.
/// </summary>
None = 0,
None = 0,
/// <summary>
/// BattleNpc representing a Pet.
/// </summary>
Pet = 2,
/// <summary>
/// BattleNpc representing a Pet.
/// </summary>
Pet = 2,
/// <summary>
/// BattleNpc representing a Chocobo.
/// </summary>
Chocobo = 3,
/// <summary>
/// BattleNpc representing a Chocobo.
/// </summary>
Chocobo = 3,
/// <summary>
/// BattleNpc representing a standard enemy.
/// </summary>
Enemy = 5,
}
/// <summary>
/// BattleNpc representing a standard enemy.
/// </summary>
Enemy = 5,
}

View file

@ -1,139 +1,138 @@
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>
/// This enum describes the indices of the Customize array.
/// The race of the character.
/// </summary>
// 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,
Race = 0x00,
/// <summary>
/// The gender of the character.
/// </summary>
Gender = 0x01,
/// <summary>
/// The gender of the character.
/// </summary>
Gender = 0x01,
/// <summary>
/// The tribe of the character.
/// </summary>
Tribe = 0x04,
/// <summary>
/// The tribe of the character.
/// </summary>
Tribe = 0x04,
/// <summary>
/// The height of the character.
/// </summary>
Height = 0x03,
/// <summary>
/// The height of the character.
/// </summary>
Height = 0x03,
/// <summary>
/// The model type of the character.
/// </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
/// <summary>
/// The model type of the character.
/// </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
/// <summary>
/// The face type of the character.
/// </summary>
FaceType = 0x05,
/// <summary>
/// The face type of the character.
/// </summary>
FaceType = 0x05,
/// <summary>
/// The hair of the character.
/// </summary>
HairStyle = 0x06,
/// <summary>
/// The hair of the character.
/// </summary>
HairStyle = 0x06,
/// <summary>
/// Whether or not the character has hair highlights.
/// </summary>
HasHighlights = 0x07, // negative to enable, positive to disable
/// <summary>
/// Whether or not the character has hair highlights.
/// </summary>
HasHighlights = 0x07, // negative to enable, positive to disable
/// <summary>
/// The skin color of the character.
/// </summary>
SkinColor = 0x08,
/// <summary>
/// The skin color of the character.
/// </summary>
SkinColor = 0x08,
/// <summary>
/// The eye color of the character.
/// </summary>
EyeColor = 0x09, // color of character's right eye
/// <summary>
/// The eye color of the character.
/// </summary>
EyeColor = 0x09, // color of character's right eye
/// <summary>
/// The hair color of the character.
/// </summary>
HairColor = 0x0A, // main color
/// <summary>
/// The hair color of the character.
/// </summary>
HairColor = 0x0A, // main color
/// <summary>
/// The highlights hair color of the character.
/// </summary>
HairColor2 = 0x0B, // highlights color
/// <summary>
/// The highlights hair color of the character.
/// </summary>
HairColor2 = 0x0B, // highlights color
/// <summary>
/// The face features of the character.
/// </summary>
FaceFeatures = 0x0C, // seems to be a toggle, (-odd and +even for large face covering), opposite for small
/// <summary>
/// The face features of the character.
/// </summary>
FaceFeatures = 0x0C, // seems to be a toggle, (-odd and +even for large face covering), opposite for small
/// <summary>
/// The color of the face features of the character.
/// </summary>
FaceFeaturesColor = 0x0D,
/// <summary>
/// The color of the face features of the character.
/// </summary>
FaceFeaturesColor = 0x0D,
/// <summary>
/// The eyebrows of the character.
/// </summary>
Eyebrows = 0x0E,
/// <summary>
/// The eyebrows of the character.
/// </summary>
Eyebrows = 0x0E,
/// <summary>
/// The 2nd eye color of the character.
/// </summary>
EyeColor2 = 0x0F, // color of character's left eye
/// <summary>
/// The 2nd eye color of the character.
/// </summary>
EyeColor2 = 0x0F, // color of character's left eye
/// <summary>
/// The eye shape of the character.
/// </summary>
EyeShape = 0x10,
/// <summary>
/// The eye shape of the character.
/// </summary>
EyeShape = 0x10,
/// <summary>
/// The nose shape of the character.
/// </summary>
NoseShape = 0x11,
/// <summary>
/// The nose shape of the character.
/// </summary>
NoseShape = 0x11,
/// <summary>
/// The jaw shape of the character.
/// </summary>
JawShape = 0x12,
/// <summary>
/// The jaw shape of the character.
/// </summary>
JawShape = 0x12,
/// <summary>
/// The lip style of the character.
/// </summary>
LipStyle = 0x13, // lip colour depth and shape (negative values around -120 darker/more noticeable, positive no colour)
/// <summary>
/// The lip style of the character.
/// </summary>
LipStyle = 0x13, // lip colour depth and shape (negative values around -120 darker/more noticeable, positive no colour)
/// <summary>
/// The lip color of the character.
/// </summary>
LipColor = 0x14,
/// <summary>
/// The lip color of the character.
/// </summary>
LipColor = 0x14,
/// <summary>
/// The race feature size of the character.
/// </summary>
RaceFeatureSize = 0x15,
/// <summary>
/// The race feature size of the character.
/// </summary>
RaceFeatureSize = 0x15,
/// <summary>
/// The race feature type of the character.
/// </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
/// <summary>
/// The race feature type of the character.
/// </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
/// <summary>
/// The bust size of the character.
/// </summary>
BustSize = 0x17, // char creator allows up to max of 100, i set to 127 cause who wouldnt but no visible difference
/// <summary>
/// The bust size of the character.
/// </summary>
BustSize = 0x17, // char creator allows up to max of 100, i set to 127 cause who wouldnt but no visible difference
/// <summary>
/// The face paint of the character.
/// </summary>
Facepaint = 0x18,
/// <summary>
/// The face paint of the character.
/// </summary>
Facepaint = 0x18,
/// <summary>
/// The face paint color of the character.
/// </summary>
FacepaintColor = 0x19,
}
/// <summary>
/// The face paint color of the character.
/// </summary>
FacepaintColor = 0x19,
}

View file

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

View file

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

View file

@ -9,140 +9,139 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
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>
/// This collection represents the currently spawned FFXIV game objects.
/// Initializes a new instance of the <see cref="ObjectTable"/> class.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed partial class ObjectTable
/// <param name="addressResolver">Client state address resolver.</param>
internal ObjectTable(ClientStateAddressResolver addressResolver)
{
private const int ObjectTableLength = 424;
this.address = addressResolver;
private readonly ClientStateAddressResolver address;
Log.Verbose($"Object table address 0x{this.address.ObjectTable.ToInt64():X}");
}
/// <summary>
/// Initializes a new instance of the <see cref="ObjectTable"/> class.
/// </summary>
/// <param name="addressResolver">Client state address resolver.</param>
internal ObjectTable(ClientStateAddressResolver addressResolver)
/// <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
{
this.address = addressResolver;
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),
};
var address = this.GetObjectAddress(index);
return this.CreateObjectReference(address);
}
}
/// <summary>
/// This collection represents the currently spawned FFXIV game objects.
/// Search for a game object by their Object ID.
/// </summary>
public sealed partial class ObjectTable : IReadOnlyCollection<GameObject>
/// <param name="objectId">Object ID to find.</param>
/// <returns>A game object or null.</returns>
public GameObject? SearchById(uint objectId)
{
/// <inheritdoc/>
int IReadOnlyCollection<GameObject>.Count => this.Length;
if (objectId is GameObject.InvalidGameObjectId or 0)
return null;
/// <inheritdoc/>
public IEnumerator<GameObject> GetEnumerator()
foreach (var obj in this)
{
for (var i = 0; i < ObjectTableLength; i++)
{
var obj = this[i];
if (obj == null)
continue;
if (obj == null)
continue;
yield return obj;
}
if (obj.ObjectId == objectId)
return obj;
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
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>
/// 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,29 +2,28 @@ using System;
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>
/// This class represents a battle NPC.
/// Initializes a new instance of the <see cref="BattleNpc"/> class.
/// Set up a new BattleNpc with the provided memory representation.
/// </summary>
public unsafe class BattleNpc : BattleChara
/// <param name="address">The address of this actor in memory.</param>
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,21 +2,20 @@ using System;
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>
/// This class represents an EventObj.
/// Initializes a new instance of the <see cref="EventObj"/> class.
/// Set up a new EventObj with the provided memory representation.
/// </summary>
public unsafe class EventObj : GameObject
/// <param name="address">The address of this event object in memory.</param>
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,21 +2,20 @@ using System;
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>
/// This class represents a NPC.
/// Initializes a new instance of the <see cref="Npc"/> class.
/// Set up a new NPC with the provided memory representation.
/// </summary>
public unsafe class Npc : Character
/// <param name="address">The address of this actor in memory.</param>
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,36 +3,35 @@ using System;
using Dalamud.Game.ClientState.Objects.Types;
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>
/// This class represents a player character.
/// Initializes a new instance of the <see cref="PlayerCharacter"/> class.
/// This represents a player character.
/// </summary>
public unsafe class PlayerCharacter : BattleChara
/// <param name="address">The address of this actor in memory.</param>
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,161 +4,160 @@ using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.IoC;
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>
/// Get and set various kinds of targets for the player.
/// Initializes a new instance of the <see cref="TargetManager"/> class.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed unsafe class TargetManager
/// <param name="addressResolver">The ClientStateAddressResolver instance.</param>
internal TargetManager(ClientStateAddressResolver 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);
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);
}

View file

@ -2,66 +2,65 @@ using System;
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>
/// This class represents the battle characters.
/// Initializes a new instance of the <see cref="BattleChara"/> class.
/// This represents a battle character.
/// </summary>
public unsafe class BattleChara : Character
/// <param name="address">The address of this character in memory.</param>
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,102 +5,101 @@ using Dalamud.Game.ClientState.Resolvers;
using Dalamud.Game.Text.SeStringHandling;
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>
/// This class represents the base for non-static entities.
/// Initializes a new instance of the <see cref="Character"/> class.
/// This represents a non-static entity.
/// </summary>
public unsafe class Character : GameObject
/// <param name="address">The address of this character in memory.</param>
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,171 +5,170 @@ using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.Text.SeStringHandling;
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>
/// This class represents a GameObject in FFXIV.
/// IDs of non-networked GameObjects.
/// </summary>
public unsafe partial class GameObject : IEquatable<GameObject>
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)
{
/// <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();
this.Address = address;
}
/// <summary>
/// This class represents a basic actor (GameObject) in FFXIV.
/// Gets the address of the game object in memory.
/// </summary>
public unsafe partial class GameObject
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)
{
/// <summary>
/// Gets the name of this <see cref="GameObject" />.
/// </summary>
public SeString Name => MemoryHelper.ReadSeString((IntPtr)this.Struct->Name, 64);
// Using == results in a stack overflow.
if (gameObject1 is null || gameObject2 is null)
return Equals(gameObject1, 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}";
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>
/// 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,178 +7,177 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
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>
/// This collection represents the actors present in your party or alliance.
/// Initializes a new instance of the <see cref="PartyList"/> class.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed unsafe partial class PartyList
/// <param name="addressResolver">Client state address resolver.</param>
internal PartyList(ClientStateAddressResolver addressResolver)
{
private const int GroupLength = 8;
private const int AllianceLength = 20;
this.address = addressResolver;
private readonly ClientStateAddressResolver address;
/// <summary>
/// Initializes a new instance of the <see cref="PartyList"/> class.
/// </summary>
/// <param name="addressResolver">Client state address resolver.</param>
internal PartyList(ClientStateAddressResolver addressResolver)
{
this.address = addressResolver;
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;
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);
}
Log.Verbose($"Group manager address 0x{this.address.GroupManager.ToInt64():X}");
}
/// <summary>
/// This collection represents the party members present in your party or alliance.
/// Gets the amount of party members the local player has.
/// </summary>
public sealed partial class PartyList : IReadOnlyCollection<PartyMember>
{
/// <inheritdoc/>
int IReadOnlyCollection<PartyMember>.Count => this.Length;
public int Length => this.GroupManagerStruct->MemberCount;
/// <inheritdoc/>
public IEnumerator<PartyMember> GetEnumerator()
/// <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.
for (var i = 0; i < this.Length; i++)
if (index < 0 || index >= this.Length)
return null;
if (this.Length > GroupLength)
{
var member = this[i];
if (member == null)
break;
yield return member;
var addr = this.GetAllianceMemberAddress(index);
return this.CreateAllianceMemberReference(addr);
}
else
{
var addr = this.GetPartyMemberAddress(index);
return this.CreatePartyMemberReference(addr);
}
}
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
/// <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;
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 (member == null)
break;
yield return member;
}
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}

View file

@ -9,105 +9,104 @@ using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory;
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>
/// This class represents a party member in the group manager.
/// Initializes a new instance of the <see cref="PartyMember"/> class.
/// </summary>
public unsafe class PartyMember
/// <param name="address">Address of the party member.</param>
internal PartyMember(IntPtr 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;
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;
}

View file

@ -1,31 +1,30 @@
using Dalamud.Data;
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>
/// This object resolves a rowID within an Excel sheet.
/// Initializes a new instance of the <see cref="ExcelResolver{T}"/> class.
/// </summary>
/// <typeparam name="T">The type of Lumina sheet to resolve.</typeparam>
public class ExcelResolver<T> where T : ExcelRow
/// <param name="id">The ID of the classJob.</param>
internal ExcelResolver(uint 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);
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);
}

View file

@ -4,65 +4,64 @@ using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
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>
/// This class represents a status effect an actor is afflicted by.
/// Initializes a new instance of the <see cref="Status"/> class.
/// </summary>
public unsafe class Status
/// <param name="address">Status address.</param>
internal Status(IntPtr 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;
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;
}

View file

@ -3,159 +3,158 @@ using System.Collections;
using System.Collections.Generic;
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>
/// This collection represents the status effects an actor is afflicted by.
/// Initializes a new instance of the <see cref="StatusList"/> class.
/// </summary>
public sealed unsafe partial class StatusList
/// <param name="address">Address of the status list.</param>
internal StatusList(IntPtr address)
{
private const int StatusListLength = 30;
this.Address = address;
}
/// <summary>
/// Initializes a new instance of the <see cref="StatusList"/> class.
/// </summary>
/// <param name="address">Address of the status list.</param>
internal StatusList(IntPtr 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
{
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)
if (index < 0 || index > StatusListLength)
return null;
if (address == IntPtr.Zero)
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));
var addr = this.GetStatusAddress(index);
return CreateStatusReference(addr);
}
}
/// <summary>
/// This collection represents the status effects an actor is afflicted by.
/// Create a reference to an FFXIV actor status list.
/// </summary>
public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollection
/// <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)
{
/// <inheritdoc/>
int IReadOnlyCollection<Status>.Count => this.Length;
// 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();
/// <inheritdoc/>
int ICollection.Count => this.Length;
if (clientState.LocalContentId == 0)
return null;
/// <inheritdoc/>
bool ICollection.IsSynchronized => false;
if (address == IntPtr.Zero)
return null;
/// <inheritdoc/>
object ICollection.SyncRoot => this;
return new StatusList(address);
}
/// <inheritdoc/>
public IEnumerator<Status> GetEnumerator()
/// <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>
/// 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++)
{
for (var i = 0; i < StatusListLength; i++)
{
var status = this[i];
var status = this[i];
if (status == null || status.StatusId == 0)
continue;
if (status == null || status.StatusId == 0)
continue;
yield return status;
}
yield return status;
}
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
/// <inheritdoc/>
void ICollection.CopyTo(Array array, int index)
/// <inheritdoc/>
void ICollection.CopyTo(Array array, int index)
{
for (var i = 0; i < this.Length; i++)
{
for (var i = 0; i < this.Length; i++)
{
array.SetValue(this[i], index);
index++;
}
array.SetValue(this[i], index);
index++;
}
}
}

View file

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

View file

@ -1,48 +1,47 @@
using System.Reflection;
namespace Dalamud.Game.Command
namespace Dalamud.Game.Command;
/// <summary>
/// This class describes a registered command.
/// </summary>
public sealed class CommandInfo
{
/// <summary>
/// This class describes a registered command.
/// Initializes a new instance of the <see cref="CommandInfo"/> class.
/// Create a new CommandInfo with the provided handler.
/// </summary>
public sealed class CommandInfo
/// <param name="handler">The method to call when the command is run.</param>
public CommandInfo(HandlerDelegate handler)
{
/// <summary>
/// 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;
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;
}

View file

@ -10,167 +10,166 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
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>
/// This class manages registered in-game slash commands.
/// Initializes a new instance of the <see cref="CommandManager"/> class.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed class CommandManager
internal 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;
var startInfo = Service<DalamudStartInfo>.Get();
/// <summary>
/// Initializes a new instance of the <see cref="CommandManager"/> class.
/// </summary>
internal CommandManager()
this.currentLangCommandRegex = startInfo.Language switch
{
var startInfo = Service<DalamudStartInfo>.Get();
ClientLanguage.Japanese => this.commandRegexJp,
ClientLanguage.English => this.commandRegexEn,
ClientLanguage.German => this.commandRegexDe,
ClientLanguage.French => this.commandRegexFr,
_ => this.currentLangCommandRegex,
};
this.currentLangCommandRegex = startInfo.Language switch
{
ClientLanguage.Japanese => this.commandRegexJp,
ClientLanguage.English => this.commandRegexEn,
ClientLanguage.German => this.commandRegexDe,
ClientLanguage.French => this.commandRegexFr,
_ => this.currentLangCommandRegex,
};
Service<ChatGui>.Get().CheckMessageHandled += this.OnCheckMessageHandled;
}
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>
/// 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;
/// <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)
var separatorPosition = content.IndexOf(' ');
if (separatorPosition == -1 || separatorPosition + 1 >= content.Length)
{
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)
{
// 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;
// Remove the trailing space
command = content.Substring(0, separatorPosition);
}
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;
argument = content[argStart..];
command = content;
}
if (!this.commandMap.TryGetValue(command, out var handler)) // Commad was not found.
return false;
argument = string.Empty;
}
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);
this.DispatchCommand(command, argument, handler);
var argStart = separatorPosition + 1;
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;
}
/// <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)
catch (ArgumentException)
{
try
{
info.Handler(command, argument);
}
catch (Exception ex)
{
Log.Error(ex, "Error while dispatching command {CommandName} (Argument: {Argument})", command, argument);
}
Log.Error("Command {CommandName} is already registered.", command);
return false;
}
}
/// <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)
/// <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)
{
if (info == null)
throw new ArgumentNullException(nameof(info), "Command handler is null.");
try
var cmdMatch = this.currentLangCommandRegex.Match(message.TextValue).Groups["command"];
if (cmdMatch.Success)
{
this.commandMap.Add(command, info);
return true;
// Yes, it's a chat command.
var command = cmdMatch.Value;
if (this.ProcessCommand(command)) isHandled = true;
}
catch (ArgumentException)
else
{
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"];
// 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 chat command.
// Yes, it's a Chinese fallback chat command.
var command = cmdMatch.Value;
if (this.ProcessCommand(command)) isHandled = true;
}
else
{
// Always match for china, since they patch in language files without changing the ClientLanguage.
cmdMatch = this.commandRegexCn.Match(message.TextValue).Groups["command"];
if (cmdMatch.Success)
{
// Yes, it's a Chinese fallback chat command.
var command = cmdMatch.Value;
if (this.ProcessCommand(command)) isHandled = true;
}
}
}
}
}

View file

@ -16,301 +16,300 @@ using Dalamud.IoC.Internal;
using Dalamud.Utility;
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>
/// This class represents the Framework of the native game client and grants access to various subsystems.
/// Initializes a new instance of the <see cref="Framework"/> class.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed class Framework : IDisposable
internal Framework()
{
private static Stopwatch statsStopwatch = new();
private Stopwatch updateStopwatch = new();
this.Address = new FrameworkAddressResolver();
this.Address.Setup();
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>
/// Initializes a new instance of the <see cref="Framework"/> class.
/// </summary>
internal Framework()
Log.Verbose($"Framework address 0x{this.Address.BaseAddress.ToInt64():X}");
if (this.Address.BaseAddress == IntPtr.Zero)
{
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();
throw new InvalidOperationException("Framework is not initalized yet.");
}
/// <summary>
/// A delegate type used with the <see cref="Update"/> event.
/// </summary>
/// <param name="framework">The Framework instance.</param>
public delegate void OnUpdateDelegate(Framework framework);
// Hook virtual functions
this.HookVTable();
}
/// <summary>
/// A delegate type used during the native Framework::destroy.
/// </summary>
/// <param name="framework">The native Framework address.</param>
/// <returns>A value indicating if the call was successful.</returns>
public delegate bool OnRealDestroyDelegate(IntPtr framework);
/// <summary>
/// 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>
/// A delegate type used during the native Framework::free.
/// </summary>
/// <returns>The native Framework address.</returns>
public delegate IntPtr OnDestroyDelegate();
/// <summary>
/// A delegate type used during the native Framework::destroy.
/// </summary>
/// <param name="framework">The native Framework address.</param>
/// <returns>A value indicating if the call was successful.</returns>
public delegate bool OnRealDestroyDelegate(IntPtr framework);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate bool OnUpdateDetour(IntPtr framework);
/// <summary>
/// A delegate type used during the native Framework::free.
/// </summary>
/// <returns>The native Framework address.</returns>
public delegate IntPtr OnDestroyDelegate();
private delegate IntPtr OnDestroyDetour(); // OnDestroyDelegate
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate bool OnUpdateDetour(IntPtr framework);
/// <summary>
/// Event that gets fired every time the game framework updates.
/// </summary>
public event OnUpdateDelegate Update;
private delegate IntPtr OnDestroyDetour(); // OnDestroyDelegate
/// <summary>
/// Gets or sets a value indicating whether the collection of stats is enabled.
/// </summary>
public static bool StatsEnabled { get; set; }
/// <summary>
/// Event that gets fired every time the game framework updates.
/// </summary>
public event OnUpdateDelegate Update;
/// <summary>
/// Gets the stats history mapping.
/// </summary>
public static Dictionary<string, List<double>> StatsHistory { get; } = new();
/// <summary>
/// Gets or sets a value indicating whether the collection of stats is enabled.
/// </summary>
public static bool StatsEnabled { get; set; }
/// <summary>
/// Gets a raw pointer to the instance of Client::Framework.
/// </summary>
public FrameworkAddressResolver Address { get; }
/// <summary>
/// Gets the stats history mapping.
/// </summary>
public static Dictionary<string, List<double>> StatsHistory { get; } = new();
/// <summary>
/// Gets the last time that the Framework Update event was triggered.
/// </summary>
public DateTime LastUpdate { get; private set; } = DateTime.MinValue;
/// <summary>
/// Gets a raw pointer to the instance of Client::Framework.
/// </summary>
public FrameworkAddressResolver Address { get; }
/// <summary>
/// Gets the last time in UTC that the Framework Update event was triggered.
/// </summary>
public DateTime LastUpdateUTC { get; private set; } = DateTime.MinValue;
/// <summary>
/// Gets the last time that the Framework Update event was triggered.
/// </summary>
public DateTime LastUpdate { get; private set; } = DateTime.MinValue;
/// <summary>
/// Gets the delta between the last Framework Update and the currently executing one.
/// </summary>
public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero;
/// <summary>
/// Gets the last time in UTC that the Framework Update event was triggered.
/// </summary>
public DateTime LastUpdateUTC { get; private set; } = DateTime.MinValue;
/// <summary>
/// Gets or sets a value indicating whether to dispatch update events.
/// </summary>
internal bool DispatchUpdateEvents { get; set; } = true;
/// <summary>
/// Gets the delta between the last Framework Update and the currently executing one.
/// </summary>
public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero;
/// <summary>
/// Enable this module.
/// </summary>
public void Enable()
/// <summary>
/// Gets or sets a value indicating whether to dispatch update events.
/// </summary>
internal bool DispatchUpdateEvents { get; set; } = true;
/// <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)
{
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
this.tier2Initialized = dalamud.LoadTier2();
if (!this.tier2Initialized)
{
this.tier2Initialized = dalamud.LoadTier2();
if (!this.tier2Initialized)
this.tierInitError = true;
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
if (!this.tier3Initialized && Service<InterfaceManager>.GetNullable()?.IsReady == true)
{
this.tier3Initialized = dalamud.LoadTier3();
if (!this.tier3Initialized)
this.tierInitError = true;
// 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)
{
this.tier3Initialized = dalamud.LoadTier3();
if (!this.tier3Initialized)
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
{
Service<ChatGui>.Get().UpdateQueue();
Service<ToastGui>.Get().UpdateQueue();
Service<GameNetwork>.Get().UpdateQueue();
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 handling Framework::Update hook.");
Log.Error(ex, "Exception while dispatching Framework::Update event.");
}
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);
}
private bool HandleRealDestroy(IntPtr framework)
original:
return this.updateHook.Original(framework);
}
private bool HandleRealDestroy(IntPtr framework)
{
if (this.DispatchUpdateEvents)
{
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);
Log.Information("Framework::Destroy!");
var dalamud = Service<Dalamud>.Get();
dalamud.Unload();
dalamud.WaitForUnloadFinish();
dalamud.DisposePlugins();
Log.Information("Framework::Free OK!");
// Return the original trampoline location to cleanly exit
return originalPtr;
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();
dalamud.Unload();
dalamud.WaitForUnloadFinish();
Log.Information("Framework::Free OK!");
// Return the original trampoline location to cleanly exit
return originalPtr;
}
}

View file

@ -1,57 +1,54 @@
using System;
using System.Runtime.InteropServices;
using Dalamud.Game.Internal;
namespace Dalamud.Game;
namespace Dalamud.Game
/// <summary>
/// The address resolver for the <see cref="Framework"/> class.
/// </summary>
public sealed class FrameworkAddressResolver : BaseAddressResolver
{
/// <summary>
/// The address resolver for the <see cref="Framework"/> class.
/// Gets the base address native Framework class.
/// </summary>
public sealed class FrameworkAddressResolver : BaseAddressResolver
public IntPtr BaseAddress { get; private set; }
/// <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)
{
/// <summary>
/// Gets the base address native Framework class.
/// </summary>
public IntPtr BaseAddress { get; private set; }
this.SetupFramework(sig);
/// <summary>
/// Gets the address for the native GuiManager class.
/// </summary>
public IntPtr GuiManager { get; private set; }
// Xiv__Framework__GetGuiManager+8 000 mov rax, [rcx+2C00h]
// Xiv__Framework__GetGuiManager+F 000 retn
this.GuiManager = Marshal.ReadIntPtr(this.BaseAddress, 0x2C08);
/// <summary>
/// Gets the address for the native ScriptManager class.
/// </summary>
public IntPtr ScriptManager { get; private set; }
// Called from Framework::Init
this.ScriptManager = this.BaseAddress + 0x2C68; // note that no deref here
}
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
this.SetupFramework(sig);
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);
// Xiv__Framework__GetGuiManager+8 000 mov rax, [rcx+2C00h]
// 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);
}
// 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,405 +5,404 @@ using System.Text;
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>
/// 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.
/// Initializes a new instance of the <see cref="GameVersion"/> class.
/// </summary>
[Serializable]
public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersion>, IEquatable<GameVersion>
/// <param name="version">Version string to parse.</param>
[JsonConstructor]
public GameVersion(string version)
{
private static readonly GameVersion AnyVersion = new();
var ver = Parse(version);
this.Year = ver.Year;
this.Month = ver.Month;
this.Day = ver.Day;
this.Major = ver.Major;
this.Minor = ver.Minor;
}
/// <summary>
/// Initializes a new instance of the <see cref="GameVersion"/> class.
/// </summary>
/// <param name="version">Version string to parse.</param>
[JsonConstructor]
public GameVersion(string version)
/// <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>
/// <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)
{
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 v2 is null;
}
/// <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>
/// <param name="minor">The minor version.</param>
public GameVersion(int year, int month, int day, int major, int minor)
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 =>
{
if ((this.Year = year) < 0)
throw new ArgumentOutOfRangeException(nameof(year));
var result = int.TryParse(p, out var value);
return (result, value);
}).ToArray();
if ((this.Month = month) < 0)
throw new ArgumentOutOfRangeException(nameof(month));
if (tplParts.Any(t => !t.result))
throw new FormatException("Bad formatting");
if ((this.Day = day) < 0)
throw new ArgumentOutOfRangeException(nameof(day));
var intParts = tplParts.Select(t => t.value).ToArray();
var len = intParts.Length;
if ((this.Major = major) < 0)
throw new ArgumentOutOfRangeException(nameof(major));
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");
}
if ((this.Minor = minor) < 0)
throw new ArgumentOutOfRangeException(nameof(minor));
/// <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;
}
/// <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)
catch
{
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;
}
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;
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
(this.Year == value.Year) &&
(this.Month == value.Month) &&
(this.Day == value.Day) &&
(this.Major == value.Major) &&
(this.Minor == value.Minor);
}
/// <inheritdoc/>
public override int GetHashCode()
{
var accumulator = 0;
// This might be horribly wrong, but it isn't used heavily.
accumulator |= this.Year.GetHashCode();
accumulator |= this.Month.GetHashCode();
accumulator |= this.Day.GetHashCode();
accumulator |= this.Major.GetHashCode();
accumulator |= this.Minor.GetHashCode();
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();
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;
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
(this.Year == value.Year) &&
(this.Month == value.Month) &&
(this.Day == value.Day) &&
(this.Major == value.Major) &&
(this.Minor == value.Minor);
}
/// <inheritdoc/>
public override int GetHashCode()
{
var accumulator = 0;
// This might be horribly wrong, but it isn't used heavily.
accumulator |= this.Year.GetHashCode();
accumulator |= this.Month.GetHashCode();
accumulator |= this.Day.GetHashCode();
accumulator |= this.Major.GetHashCode();
accumulator |= this.Minor.GetHashCode();
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,79 +2,78 @@ using System;
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>
/// Converts a <see cref="GameVersion"/> to and from a string (e.g. <c>"2010.01.01.1234.5678"</c>).
/// Writes the JSON representation of the object.
/// </summary>
public sealed class GameVersionConverter : JsonConverter
/// <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)
{
/// <summary>
/// Writes the JSON representation of the object.
/// </summary>
/// <param name="writer">The <see cref="JsonWriter"/> to write to.</param>
/// <param name="value">The value.</param>
/// <param name="serializer">The calling serializer.</param>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
if (value == null)
{
if (value == null)
{
writer.WriteNull();
}
else if (value is GameVersion)
{
writer.WriteValue(value.ToString());
}
else
{
throw new JsonSerializationException("Expected GameVersion object value");
}
writer.WriteNull();
}
/// <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)
else if (value is 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}");
}
}
writer.WriteValue(value.ToString());
}
/// <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)
else
{
return objectType == typeof(GameVersion);
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
{
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,471 +13,470 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
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>
/// This class handles interacting with the native chat UI.
/// Initializes a new instance of the <see cref="ChatGui"/> class.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed class ChatGui : IDisposable
/// <param name="baseAddress">The base address of the ChatManager.</param>
internal ChatGui(IntPtr baseAddress)
{
private readonly ChatGuiAddressResolver address;
this.address = new ChatGuiAddressResolver(baseAddress);
this.address.Setup();
private readonly Queue<XivChatEntry> chatQueue = new();
private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = new();
Log.Verbose($"Chat manager address 0x{this.address.BaseAddress.ToInt64():X}");
private readonly Hook<PrintMessageDelegate> printMessageHook;
private readonly Hook<PopulateItemLinkDelegate> populateItemLinkHook;
private readonly Hook<InteractableLinkClickedDelegate> interactableLinkClickedHook;
this.printMessageHook = new Hook<PrintMessageDelegate>(this.address.PrintMessage, this.HandlePrintMessageDetour);
this.populateItemLinkHook = new Hook<PopulateItemLinkDelegate>(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
this.interactableLinkClickedHook = new Hook<InteractableLinkClickedDelegate>(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
}
private IntPtr baseAddress = IntPtr.Zero;
/// <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);
/// <summary>
/// Initializes a new instance of the <see cref="ChatGui"/> class.
/// </summary>
/// <param name="baseAddress">The base address of the ChatManager.</param>
internal ChatGui(IntPtr baseAddress)
/// <summary>
/// A delegate type used with the <see cref="ChatGui.CheckMessageHandled"/> 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 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
{
this.address = new ChatGuiAddressResolver(baseAddress);
this.address.Setup();
Message = message,
Type = configuration.GeneralChatType,
});
}
Log.Verbose($"Chat manager address 0x{this.address.BaseAddress.ToInt64():X}");
/// <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();
this.printMessageHook = new Hook<PrintMessageDelegate>(this.address.PrintMessage, this.HandlePrintMessageDetour);
this.populateItemLinkHook = new Hook<PopulateItemLinkDelegate>(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
this.interactableLinkClickedHook = new Hook<InteractableLinkClickedDelegate>(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
}
/// <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);
/// <summary>
/// A delegate type used with the <see cref="ChatGui.CheckMessageHandled"/> 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 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()
// Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue);
this.PrintChat(new XivChatEntry
{
this.printMessageHook.Enable();
this.populateItemLinkHook.Enable();
this.interactableLinkClickedHook.Enable();
}
Message = message,
Type = configuration.GeneralChatType,
});
}
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
public void Dispose()
/// <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
{
this.printMessageHook.Dispose();
this.populateItemLinkHook.Dispose();
this.interactableLinkClickedHook.Dispose();
}
Message = message,
Type = XivChatType.Urgent,
});
}
/// <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)
/// <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
{
this.chatQueue.Enqueue(chat);
}
Message = message,
Type = XivChatType.Urgent,
});
}
/// <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)
/// <summary>
/// Process a chat queue.
/// </summary>
public void UpdateQueue()
{
while (this.chatQueue.Count > 0)
{
var configuration = Service<DalamudConfiguration>.Get();
var chat = this.chatQueue.Dequeue();
// Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message);
this.PrintChat(new XivChatEntry
if (this.baseAddress == IntPtr.Zero)
{
Message = message,
Type = configuration.GeneralChatType,
});
}
/// <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();
// Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue);
this.PrintChat(new XivChatEntry
{
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);
continue;
}
}
/// <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;
}
var senderRaw = (chat.Name ?? string.Empty).Encode();
using var senderOwned = Service<LibcFunction>.Get().NewString(senderRaw);
/// <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)
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))
{
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)
{
this.dalamudLinkHandlers.Remove(handler);
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);
}
/// <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)
return retVal;
}
private void InteractableLinkClickedDetour(IntPtr managerPtr, IntPtr messagePtr)
{
try
{
if (this.dalamudLinkHandlers.ContainsKey((pluginName, commandId)))
var interactableType = (Payload.EmbeddedInfoType)(Marshal.ReadByte(messagePtr, 0x1B) + 1);
if (interactableType != Payload.EmbeddedInfoType.DalamudLink)
{
this.dalamudLinkHandlers.Remove((pluginName, commandId));
this.interactableLinkClickedHook.Original(managerPtr, messagePtr);
return;
}
}
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
Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}");
if (a1 == a2) return true;
if (a1 == null || a2 == null || a1.Length != a2.Length)
return false;
fixed (byte* p1 = a1, p2 = a2)
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)
{
byte* x1 = p1, x2 = p2;
var l = a1.Length;
for (var i = 0; i < l / 8; i++, x1 += 8, x2 += 8)
if (this.dalamudLinkHandlers.ContainsKey((link.Plugin, link.CommandId)))
{
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);
Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}");
this.dalamudLinkHandlers[(link.Plugin, link.CommandId)].Invoke(link.CommandId, new SeString(payloads));
}
else
{
retVal = this.printMessageHook.Original(manager, chattype, senderPtr, messagePtr, senderid, parameter);
this.ChatMessageUnhandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
Log.Debug($"No DalamudLink registered for {link.Plugin} with ID of {link.CommandId}");
}
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)
catch (Exception ex)
{
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)
{
Log.Error(ex, "Exception on InteractableLinkClicked hook");
}
Log.Error(ex, "Exception on InteractableLinkClicked hook");
}
}
}

View file

@ -1,120 +1,117 @@
using System;
using Dalamud.Game.Internal;
namespace Dalamud.Game.Gui;
namespace Dalamud.Game.Gui
/// <summary>
/// The address resolver for the <see cref="ChatGui"/> class.
/// </summary>
public sealed class ChatGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// The address resolver for the <see cref="ChatGui"/> class.
/// Initializes a new instance of the <see cref="ChatGuiAddressResolver"/> class.
/// </summary>
public sealed class ChatGuiAddressResolver : BaseAddressResolver
/// <param name="baseAddress">The base address of the native ChatManager class.</param>
public ChatGuiAddressResolver(IntPtr 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;
}
this.BaseAddress = baseAddress;
}
/// <summary>
/// Gets the base address of the native ChatManager class.
/// </summary>
public IntPtr BaseAddress { get; }
/// <summary>
/// Gets the base address of the native ChatManager class.
/// </summary>
public IntPtr BaseAddress { get; }
/// <summary>
/// Gets the address of the native PrintMessage method.
/// </summary>
public IntPtr PrintMessage { get; private set; }
/// <summary>
/// Gets the address of the native PrintMessage method.
/// </summary>
public IntPtr PrintMessage { get; private set; }
/// <summary>
/// Gets the address of the native PopulateItemLinkObject method.
/// </summary>
public IntPtr PopulateItemLinkObject { get; private set; }
/// <summary>
/// Gets the address of the native PopulateItemLinkObject method.
/// </summary>
public IntPtr PopulateItemLinkObject { get; private set; }
/// <summary>
/// Gets the address of the native InteractableLinkClicked method.
/// </summary>
public IntPtr InteractableLinkClicked { get; private set; }
/// <summary>
/// Gets the address of the native InteractableLinkClicked method.
/// </summary>
public IntPtr InteractableLinkClicked { get; private set; }
/*
--- 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 Xiv__Gui__ChatGui__PrintMessage proc near
.text:00000001405CD210 ; CODE XREF: sub_1401419F0+201p
.text:00000001405CD210 ; sub_140141D10+220p ...
.text:00000001405CD210
.text:00000001405CD210 var_220 = qword ptr -220h
.text:00000001405CD210 var_218 = byte ptr -218h
.text:00000001405CD210 var_210 = word ptr -210h
.text:00000001405CD210 var_208 = byte ptr -208h
.text:00000001405CD210 var_200 = word ptr -200h
.text:00000001405CD210 var_1FC = dword ptr -1FCh
.text:00000001405CD210 var_1F8 = qword ptr -1F8h
.text:00000001405CD210 var_1F0 = qword ptr -1F0h
.text:00000001405CD210 var_1E8 = qword ptr -1E8h
.text:00000001405CD210 var_1E0 = dword ptr -1E0h
.text:00000001405CD210 var_1DC = word ptr -1DCh
.text:00000001405CD210 var_1DA = word ptr -1DAh
.text:00000001405CD210 var_1D8 = qword ptr -1D8h
.text:00000001405CD210 var_1D0 = byte ptr -1D0h
.text:00000001405CD210 var_1C8 = qword ptr -1C8h
.text:00000001405CD210 var_1B0 = dword ptr -1B0h
.text:00000001405CD210 var_1AC = dword ptr -1ACh
.text:00000001405CD210 var_1A8 = dword ptr -1A8h
.text:00000001405CD210 var_1A4 = dword ptr -1A4h
.text:00000001405CD210 var_1A0 = dword ptr -1A0h
.text:00000001405CD210 var_160 = dword ptr -160h
.text:00000001405CD210 var_15C = dword ptr -15Ch
.text:00000001405CD210 var_140 = dword ptr -140h
.text:00000001405CD210 var_138 = dword ptr -138h
.text:00000001405CD210 var_130 = byte ptr -130h
.text:00000001405CD210 var_C0 = byte ptr -0C0h
.text:00000001405CD210 var_50 = qword ptr -50h
.text:00000001405CD210 var_38 = qword ptr -38h
.text:00000001405CD210 var_30 = qword ptr -30h
.text:00000001405CD210 var_28 = qword ptr -28h
.text:00000001405CD210 var_20 = qword ptr -20h
.text:00000001405CD210 senderActorId = dword ptr 30h
.text:00000001405CD210 isLocal = byte ptr 38h
.text:00000001405CD210
.text:00000001405CD210 ; __unwind { // __GSHandlerCheck
.text:00000001405CD210 push rbp
.text:00000001405CD212 push rdi
.text:00000001405CD213 push r14
.text:00000001405CD215 push r15
.text:00000001405CD217 lea rbp, [rsp-128h]
.text:00000001405CD21F sub rsp, 228h
.text:00000001405CD226 mov rax, cs:__security_cookie
.text:00000001405CD22D xor rax, rsp
.text:00000001405CD230 mov [rbp+140h+var_50], rax
.text:00000001405CD237 xor r10b, r10b
.text:00000001405CD23A mov [rsp+240h+var_1F8], rcx
.text:00000001405CD23F xor eax, eax
.text:00000001405CD241 mov r11, r9
.text:00000001405CD244 mov r14, r8
.text:00000001405CD247 mov r9d, eax
.text:00000001405CD24A movzx r15d, dx
.text:00000001405CD24E lea r8, [rcx+0C10h]
.text:00000001405CD255 mov rdi, rcx
*/
/*
--- 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 Xiv__Gui__ChatGui__PrintMessage proc near
.text:00000001405CD210 ; CODE XREF: sub_1401419F0+201p
.text:00000001405CD210 ; sub_140141D10+220p ...
.text:00000001405CD210
.text:00000001405CD210 var_220 = qword ptr -220h
.text:00000001405CD210 var_218 = byte ptr -218h
.text:00000001405CD210 var_210 = word ptr -210h
.text:00000001405CD210 var_208 = byte ptr -208h
.text:00000001405CD210 var_200 = word ptr -200h
.text:00000001405CD210 var_1FC = dword ptr -1FCh
.text:00000001405CD210 var_1F8 = qword ptr -1F8h
.text:00000001405CD210 var_1F0 = qword ptr -1F0h
.text:00000001405CD210 var_1E8 = qword ptr -1E8h
.text:00000001405CD210 var_1E0 = dword ptr -1E0h
.text:00000001405CD210 var_1DC = word ptr -1DCh
.text:00000001405CD210 var_1DA = word ptr -1DAh
.text:00000001405CD210 var_1D8 = qword ptr -1D8h
.text:00000001405CD210 var_1D0 = byte ptr -1D0h
.text:00000001405CD210 var_1C8 = qword ptr -1C8h
.text:00000001405CD210 var_1B0 = dword ptr -1B0h
.text:00000001405CD210 var_1AC = dword ptr -1ACh
.text:00000001405CD210 var_1A8 = dword ptr -1A8h
.text:00000001405CD210 var_1A4 = dword ptr -1A4h
.text:00000001405CD210 var_1A0 = dword ptr -1A0h
.text:00000001405CD210 var_160 = dword ptr -160h
.text:00000001405CD210 var_15C = dword ptr -15Ch
.text:00000001405CD210 var_140 = dword ptr -140h
.text:00000001405CD210 var_138 = dword ptr -138h
.text:00000001405CD210 var_130 = byte ptr -130h
.text:00000001405CD210 var_C0 = byte ptr -0C0h
.text:00000001405CD210 var_50 = qword ptr -50h
.text:00000001405CD210 var_38 = qword ptr -38h
.text:00000001405CD210 var_30 = qword ptr -30h
.text:00000001405CD210 var_28 = qword ptr -28h
.text:00000001405CD210 var_20 = qword ptr -20h
.text:00000001405CD210 senderActorId = dword ptr 30h
.text:00000001405CD210 isLocal = byte ptr 38h
.text:00000001405CD210
.text:00000001405CD210 ; __unwind { // __GSHandlerCheck
.text:00000001405CD210 push rbp
.text:00000001405CD212 push rdi
.text:00000001405CD213 push r14
.text:00000001405CD215 push r15
.text:00000001405CD217 lea rbp, [rsp-128h]
.text:00000001405CD21F sub rsp, 228h
.text:00000001405CD226 mov rax, cs:__security_cookie
.text:00000001405CD22D xor rax, rsp
.text:00000001405CD230 mov [rbp+140h+var_50], rax
.text:00000001405CD237 xor r10b, r10b
.text:00000001405CD23A mov [rsp+240h+var_1F8], rcx
.text:00000001405CD23F xor eax, eax
.text:00000001405CD241 mov r11, r9
.text:00000001405CD244 mov r14, r8
.text:00000001405CD247 mov r9d, eax
.text:00000001405CD24A movzx r15d, dx
.text:00000001405CD24E lea r8, [rcx+0C10h]
.text:00000001405CD255 mov rdi, rcx
*/
/// <inheritdoc/>
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???
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
/// <inheritdoc/>
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???
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("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
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");
// 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.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,303 +9,302 @@ using Dalamud.IoC.Internal;
using Dalamud.Memory;
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>
/// This class facilitates interacting with and creating native in-game "fly text".
/// The native function responsible for adding fly text to the UI. See <see cref="FlyTextGuiAddressResolver.AddFlyText"/>.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed class FlyTextGui : IDisposable
private readonly AddFlyTextDelegate addFlyTextNative;
/// <summary>
/// The hook that fires when the game creates a fly text element. See <see cref="FlyTextGuiAddressResolver.CreateFlyText"/>.
/// </summary>
private readonly Hook<CreateFlyTextDelegate> createFlyTextHook;
/// <summary>
/// Initializes a new instance of the <see cref="FlyTextGui"/> class.
/// </summary>
internal FlyTextGui()
{
/// <summary>
/// The native function responsible for adding fly text to the UI. See <see cref="FlyTextGuiAddressResolver.AddFlyText"/>.
/// </summary>
private readonly AddFlyTextDelegate addFlyTextNative;
this.Address = new FlyTextGuiAddressResolver();
this.Address.Setup();
/// <summary>
/// The hook that fires when the game creates a fly text element. See <see cref="FlyTextGuiAddressResolver.CreateFlyText"/>.
/// </summary>
private readonly Hook<CreateFlyTextDelegate> createFlyTextHook;
this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer<AddFlyTextDelegate>(this.Address.AddFlyText);
this.createFlyTextHook = new Hook<CreateFlyTextDelegate>(this.Address.CreateFlyText, this.CreateFlyTextDetour);
}
/// <summary>
/// Initializes a new instance of the <see cref="FlyTextGui"/> class.
/// </summary>
internal FlyTextGui()
/// <summary>
/// The delegate defining the type for the FlyText event.
/// </summary>
/// <param name="kind">The FlyTextKind. See <see cref="FlyTextKind"/>.</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>
/// <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())
{
this.Address = new FlyTextGuiAddressResolver();
this.Address.Setup();
this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer<AddFlyTextDelegate>(this.Address.AddFlyText);
this.createFlyTextHook = new Hook<CreateFlyTextDelegate>(this.Address.CreateFlyText, this.CreateFlyTextDetour);
}
/// <summary>
/// The delegate defining the type for the FlyText event.
/// </summary>
/// <param name="kind">The FlyTextKind. See <see cref="FlyTextKind"/>.</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>
/// <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())
{
fixed (byte* pText2 = text2.Encode())
{
strArray->StringArray[strOffset + 0] = pText1;
strArray->StringArray[strOffset + 1] = pText2;
strArray->StringArray[strOffset + 0] = pText1;
strArray->StringArray[strOffset + 1] = pText2;
this.addFlyTextNative(
flytext,
actorIndex,
1,
(IntPtr)numArray,
numOffset,
9,
(IntPtr)strArray,
strOffset,
2,
0);
}
this.addFlyTextNative(
flytext,
actorIndex,
1,
(IntPtr)numArray,
numOffset,
9,
(IntPtr)strArray,
strOffset,
2,
0);
}
}
/// <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;
}
}
/// <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,32 +1,31 @@
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>
/// An address resolver for the <see cref="FlyTextGui"/> class.
/// 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 class FlyTextGuiAddressResolver : BaseAddressResolver
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)
{
/// <summary>
/// Gets the address of the native AddFlyText method, which occurs
/// when the game adds fly text elements to the UI. Multiple fly text
/// elements can be added in a single AddFlyText call.
/// </summary>
public IntPtr AddFlyText { get; private set; }
/// <summary>
/// Gets the address of the native CreateFlyText method, which occurs
/// when the game creates a new fly text element. This method is called
/// once per fly text element, and can be called multiple times per
/// AddFlyText call.
/// </summary>
public IntPtr CreateFlyText { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(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");
}
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,283 +1,282 @@
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>
/// Enum of FlyTextKind values. Members suffixed with
/// a number seem to be a duplicate, or perform duplicate behavior.
/// Val1 in serif font, Text2 in sans-serif as subtitle.
/// Used for autos and incoming DoTs.
/// </summary>
public enum FlyTextKind : int
{
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle.
/// Used for autos and incoming DoTs.
/// </summary>
AutoAttack = 0,
AutoAttack = 0,
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle.
/// Does a bounce effect on appearance.
/// </summary>
DirectHit = 1,
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle.
/// Does a bounce effect on appearance.
/// </summary>
DirectHit = 1,
/// <summary>
/// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle.
/// Does a bigger bounce effect on appearance.
/// </summary>
CriticalHit = 2,
/// <summary>
/// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle.
/// Does a bigger bounce effect on appearance.
/// </summary>
CriticalHit = 2,
/// <summary>
/// Val1 in even larger serif font with 2 exclamations, Text2 in
/// sans-serif as subtitle. Does a large bounce effect on appearance.
/// Does not scroll up or down the screen.
/// </summary>
CriticalDirectHit = 3,
/// <summary>
/// Val1 in even larger serif font with 2 exclamations, Text2 in
/// sans-serif as subtitle. Does a large bounce effect on appearance.
/// Does not scroll up or down the screen.
/// </summary>
CriticalDirectHit = 3,
/// <summary>
/// AutoAttack with sans-serif Text1 to the left of the Val1.
/// </summary>
NamedAttack = 4,
/// <summary>
/// AutoAttack with sans-serif Text1 to the left of the Val1.
/// </summary>
NamedAttack = 4,
/// <summary>
/// DirectHit with sans-serif Text1 to the left of the Val1.
/// </summary>
NamedDirectHit = 5,
/// <summary>
/// DirectHit with sans-serif Text1 to the left of the Val1.
/// </summary>
NamedDirectHit = 5,
/// <summary>
/// CriticalHit with sans-serif Text1 to the left of the Val1.
/// </summary>
NamedCriticalHit = 6,
/// <summary>
/// CriticalHit with sans-serif Text1 to the left of the Val1.
/// </summary>
NamedCriticalHit = 6,
/// <summary>
/// CriticalDirectHit with sans-serif Text1 to the left of the Val1.
/// </summary>
NamedCriticalDirectHit = 7,
/// <summary>
/// CriticalDirectHit with sans-serif Text1 to the left of the Val1.
/// </summary>
NamedCriticalDirectHit = 7,
/// <summary>
/// All caps, serif MISS.
/// </summary>
Miss = 8,
/// <summary>
/// All caps, serif MISS.
/// </summary>
Miss = 8,
/// <summary>
/// Sans-serif Text1 next to all caps serif MISS.
/// </summary>
NamedMiss = 9,
/// <summary>
/// Sans-serif Text1 next to all caps serif MISS.
/// </summary>
NamedMiss = 9,
/// <summary>
/// All caps serif DODGE.
/// </summary>
Dodge = 10,
/// <summary>
/// All caps serif DODGE.
/// </summary>
Dodge = 10,
/// <summary>
/// Sans-serif Text1 next to all caps serif DODGE.
/// </summary>
NamedDodge = 11,
/// <summary>
/// Sans-serif Text1 next to all caps serif DODGE.
/// </summary>
NamedDodge = 11,
/// <summary>
/// Icon next to sans-serif Text1.
/// </summary>
NamedIcon = 12,
/// <summary>
/// Icon next to sans-serif Text1.
/// </summary>
NamedIcon = 12,
/// <summary>
/// Icon next to sans-serif Text1 (2).
/// </summary>
NamedIcon2 = 13,
/// <summary>
/// Icon next to sans-serif Text1 (2).
/// </summary>
NamedIcon2 = 13,
/// <summary>
/// Serif Val1 with all caps condensed font EXP with Text2 in sans-serif as subtitle.
/// </summary>
Exp = 14,
/// <summary>
/// Serif Val1 with all caps condensed font EXP with Text2 in sans-serif as subtitle.
/// </summary>
Exp = 14,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle.
/// </summary>
NamedMp = 15,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle.
/// </summary>
NamedMp = 15,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle.
/// </summary>
NamedTp = 16,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle.
/// </summary>
NamedTp = 16,
/// <summary>
/// AutoAttack with sans-serif Text1 to the left of the Val1 (2).
/// </summary>
NamedAttack2 = 17,
/// <summary>
/// AutoAttack with sans-serif Text1 to the left of the Val1 (2).
/// </summary>
NamedAttack2 = 17,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle (2).
/// </summary>
NamedMp2 = 18,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle (2).
/// </summary>
NamedMp2 = 18,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle (2).
/// </summary>
NamedTp2 = 19,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle (2).
/// </summary>
NamedTp2 = 19,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font EP with Text2 in sans-serif as subtitle.
/// </summary>
NamedEp = 20,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font EP with Text2 in sans-serif as subtitle.
/// </summary>
NamedEp = 20,
/// <summary>
/// Displays nothing.
/// </summary>
None = 21,
/// <summary>
/// Displays nothing.
/// </summary>
None = 21,
/// <summary>
/// All caps serif INVULNERABLE.
/// </summary>
Invulnerable = 22,
/// <summary>
/// All caps serif INVULNERABLE.
/// </summary>
Invulnerable = 22,
/// <summary>
/// All caps sans-serif condensed font INTERRUPTED!
/// Does a large bounce effect on appearance.
/// Does not scroll up or down the screen.
/// </summary>
Interrupted = 23,
/// <summary>
/// All caps sans-serif condensed font INTERRUPTED!
/// Does a large bounce effect on appearance.
/// Does not scroll up or down the screen.
/// </summary>
Interrupted = 23,
/// <summary>
/// AutoAttack with no Text2.
/// </summary>
AutoAttackNoText = 24,
/// <summary>
/// AutoAttack with no Text2.
/// </summary>
AutoAttackNoText = 24,
/// <summary>
/// AutoAttack with no Text2 (2).
/// </summary>
AutoAttackNoText2 = 25,
/// <summary>
/// AutoAttack with no Text2 (2).
/// </summary>
AutoAttackNoText2 = 25,
/// <summary>
/// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. Does a bigger bounce effect on appearance (2).
/// </summary>
CriticalHit2 = 26,
/// <summary>
/// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. Does a bigger bounce effect on appearance (2).
/// </summary>
CriticalHit2 = 26,
/// <summary>
/// AutoAttack with no Text2 (3).
/// </summary>
AutoAttackNoText3 = 27,
/// <summary>
/// AutoAttack with no Text2 (3).
/// </summary>
AutoAttackNoText3 = 27,
/// <summary>
/// CriticalHit with sans-serif Text1 to the left of the Val1 (2).
/// </summary>
NamedCriticalHit2 = 28,
/// <summary>
/// CriticalHit with sans-serif Text1 to the left of the Val1 (2).
/// </summary>
NamedCriticalHit2 = 28,
/// <summary>
/// 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.
/// </summary>
NamedCriticalHitWithMp = 29,
/// <summary>
/// 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.
/// </summary>
NamedCriticalHitWithMp = 29,
/// <summary>
/// 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.
/// </summary>
NamedCriticalHitWithTp = 30,
/// <summary>
/// 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.
/// </summary>
NamedCriticalHitWithTp = 30,
/// <summary>
/// Same as NamedIcon with sans-serif "has no effect!" to the right.
/// </summary>
NamedIconHasNoEffect = 31,
/// <summary>
/// Same as NamedIcon with sans-serif "has no effect!" to the right.
/// </summary>
NamedIconHasNoEffect = 31,
/// <summary>
/// Same as NamedIcon but Text1 is slightly faded. Used for buff expiration.
/// </summary>
NamedIconFaded = 32,
/// <summary>
/// Same as NamedIcon but Text1 is slightly faded. Used for buff expiration.
/// </summary>
NamedIconFaded = 32,
/// <summary>
/// Same as NamedIcon but Text1 is slightly faded (2).
/// Used for buff expiration.
/// </summary>
NamedIconFaded2 = 33,
/// <summary>
/// Same as NamedIcon but Text1 is slightly faded (2).
/// Used for buff expiration.
/// </summary>
NamedIconFaded2 = 33,
/// <summary>
/// Text1 in sans-serif font.
/// </summary>
Named = 34,
/// <summary>
/// Text1 in sans-serif font.
/// </summary>
Named = 34,
/// <summary>
/// Same as NamedIcon with sans-serif "(fully resisted)" to the right.
/// </summary>
NamedIconFullyResisted = 35,
/// <summary>
/// Same as NamedIcon with sans-serif "(fully resisted)" to the right.
/// </summary>
NamedIconFullyResisted = 35,
/// <summary>
/// All caps serif 'INCAPACITATED!'.
/// </summary>
Incapacitated = 36,
/// <summary>
/// All caps serif 'INCAPACITATED!'.
/// </summary>
Incapacitated = 36,
/// <summary>
/// Text1 with sans-serif "(fully resisted)" to the right.
/// </summary>
NamedFullyResisted = 37,
/// <summary>
/// Text1 with sans-serif "(fully resisted)" to the right.
/// </summary>
NamedFullyResisted = 37,
/// <summary>
/// Text1 with sans-serif "has no effect!" to the right.
/// </summary>
NamedHasNoEffect = 38,
/// <summary>
/// Text1 with sans-serif "has no effect!" to the right.
/// </summary>
NamedHasNoEffect = 38,
/// <summary>
/// AutoAttack with sans-serif Text1 to the left of the Val1 (3).
/// </summary>
NamedAttack3 = 39,
/// <summary>
/// AutoAttack with sans-serif Text1 to the left of the Val1 (3).
/// </summary>
NamedAttack3 = 39,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle (3).
/// </summary>
NamedMp3 = 40,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle (3).
/// </summary>
NamedMp3 = 40,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle (3).
/// </summary>
NamedTp3 = 41,
/// <summary>
/// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle (3).
/// </summary>
NamedTp3 = 41,
/// <summary>
/// Same as NamedIcon with serif "INVULNERABLE!" beneath the Text1.
/// </summary>
NamedIconInvulnerable = 42,
/// <summary>
/// Same as NamedIcon with serif "INVULNERABLE!" beneath the Text1.
/// </summary>
NamedIconInvulnerable = 42,
/// <summary>
/// All caps serif RESIST.
/// </summary>
Resist = 43,
/// <summary>
/// All caps serif RESIST.
/// </summary>
Resist = 43,
/// <summary>
/// Same as NamedIcon but places the given icon in the item icon outline.
/// </summary>
NamedIconWithItemOutline = 44,
/// <summary>
/// Same as NamedIcon but places the given icon in the item icon outline.
/// </summary>
NamedIconWithItemOutline = 44,
/// <summary>
/// AutoAttack with no Text2 (4).
/// </summary>
AutoAttackNoText4 = 45,
/// <summary>
/// AutoAttack with no Text2 (4).
/// </summary>
AutoAttackNoText4 = 45,
/// <summary>
/// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle (3).
/// Does a bigger bounce effect on appearance.
/// </summary>
CriticalHit3 = 46,
/// <summary>
/// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle (3).
/// Does a bigger bounce effect on appearance.
/// </summary>
CriticalHit3 = 46,
/// <summary>
/// All caps serif REFLECT.
/// </summary>
Reflect = 47,
/// <summary>
/// All caps serif REFLECT.
/// </summary>
Reflect = 47,
/// <summary>
/// All caps serif REFLECTED.
/// </summary>
Reflected = 48,
/// <summary>
/// All caps serif REFLECTED.
/// </summary>
Reflected = 48,
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle (2).
/// Does a bounce effect on appearance.
/// </summary>
DirectHit2 = 49,
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle (2).
/// Does a bounce effect on appearance.
/// </summary>
DirectHit2 = 49,
/// <summary>
/// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle (4).
/// Does a bigger bounce effect on appearance.
/// </summary>
CriticalHit4 = 50,
/// <summary>
/// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle (4).
/// Does a bigger bounce effect on appearance.
/// </summary>
CriticalHit4 = 50,
/// <summary>
/// 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.
/// </summary>
CriticalDirectHit2 = 51,
}
/// <summary>
/// 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.
/// </summary>
CriticalDirectHit2 = 51,
}

File diff suppressed because it is too large Load diff

View file

@ -1,104 +1,103 @@
using System;
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>
/// The address resolver for the <see cref="GameGui"/> class.
/// Initializes a new instance of the <see cref="GameGuiAddressResolver"/> class.
/// </summary>
internal sealed class GameGuiAddressResolver : BaseAddressResolver
public GameGuiAddressResolver()
{
/// <summary>
/// Initializes a new instance of the <see cref="GameGuiAddressResolver"/> class.
/// </summary>
public GameGuiAddressResolver()
{
this.BaseAddress = Service<Framework>.Get().Address.BaseAddress;
}
this.BaseAddress = Service<Framework>.Get().Address.BaseAddress;
}
/// <summary>
/// Gets the base address of the native GuiManager class.
/// </summary>
public IntPtr BaseAddress { get; private set; }
/// <summary>
/// Gets the base address of the native GuiManager class.
/// </summary>
public IntPtr BaseAddress { get; private set; }
/// <summary>
/// Gets the address of the native ChatManager class.
/// </summary>
public IntPtr ChatManager { get; private set; }
/// <summary>
/// Gets the address of the native ChatManager class.
/// </summary>
public IntPtr ChatManager { get; private set; }
/// <summary>
/// Gets the address of the native SetGlobalBgm method.
/// </summary>
public IntPtr SetGlobalBgm { get; private set; }
/// <summary>
/// Gets the address of the native SetGlobalBgm method.
/// </summary>
public IntPtr SetGlobalBgm { get; private set; }
/// <summary>
/// Gets the address of the native HandleItemHover method.
/// </summary>
public IntPtr HandleItemHover { get; private set; }
/// <summary>
/// Gets the address of the native HandleItemHover method.
/// </summary>
public IntPtr HandleItemHover { get; private set; }
/// <summary>
/// Gets the address of the native HandleItemOut method.
/// </summary>
public IntPtr HandleItemOut { get; private set; }
/// <summary>
/// Gets the address of the native HandleItemOut method.
/// </summary>
public IntPtr HandleItemOut { get; private set; }
/// <summary>
/// Gets the address of the native HandleActionHover method.
/// </summary>
public IntPtr HandleActionHover { get; private set; }
/// <summary>
/// Gets the address of the native HandleActionHover method.
/// </summary>
public IntPtr HandleActionHover { get; private set; }
/// <summary>
/// Gets the address of the native HandleActionOut method.
/// </summary>
public IntPtr HandleActionOut { get; private set; }
/// <summary>
/// Gets the address of the native HandleActionOut method.
/// </summary>
public IntPtr HandleActionOut { get; private set; }
/// <summary>
/// Gets the address of the native HandleImm method.
/// </summary>
public IntPtr HandleImm { get; private set; }
/// <summary>
/// Gets the address of the native HandleImm method.
/// </summary>
public IntPtr HandleImm { get; private set; }
/// <summary>
/// Gets the address of the native GetMatrixSingleton method.
/// </summary>
public IntPtr GetMatrixSingleton { get; private set; }
/// <summary>
/// Gets the address of the native GetMatrixSingleton method.
/// </summary>
public IntPtr GetMatrixSingleton { get; private set; }
/// <summary>
/// Gets the address of the native ScreenToWorld method.
/// </summary>
public IntPtr ScreenToWorld { get; private set; }
/// <summary>
/// Gets the address of the native ScreenToWorld method.
/// </summary>
public IntPtr ScreenToWorld { get; private set; }
/// <summary>
/// Gets the address of the native ToggleUiHide method.
/// </summary>
public IntPtr ToggleUiHide { get; private set; }
/// <summary>
/// Gets the address of the native ToggleUiHide method.
/// </summary>
public IntPtr ToggleUiHide { get; private set; }
/// <summary>
/// Gets the address of the native GetAgentModule method.
/// </summary>
public IntPtr GetAgentModule { get; private set; }
/// <summary>
/// Gets the address of the native GetAgentModule method.
/// </summary>
public IntPtr GetAgentModule { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
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.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.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.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.ToggleUiHide = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 0F B6 B9 ?? ?? ?? ?? B8 ?? ?? ?? ??");
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
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.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.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.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.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");
this.GetAgentModule = Marshal.ReadIntPtr(uiModuleVtableSig, 34 * IntPtr.Size);
}
var uiModuleVtableSig = sig.GetStaticAddressFromSig("48 8D 05 ?? ?? ?? ?? 4C 89 61 28");
this.GetAgentModule = Marshal.ReadIntPtr(uiModuleVtableSig, 34 * IntPtr.Size);
}
/// <inheritdoc/>
protected override void SetupInternal(SigScanner scanner)
{
// Xiv__UiManager__GetChatManager 000 lea rax, [rcx+13E0h]
// Xiv__UiManager__GetChatManager+7 000 retn
this.ChatManager = this.BaseAddress + 0x13E0;
}
/// <inheritdoc/>
protected override void SetupInternal(SigScanner scanner)
{
// Xiv__UiManager__GetChatManager 000 lea rax, [rcx+13E0h]
// Xiv__UiManager__GetChatManager+7 000 retn
this.ChatManager = this.BaseAddress + 0x13E0;
}
}

View file

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

View file

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

View file

@ -9,235 +9,234 @@ using ImGuiNET;
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>
/// This class handles IME for non-English users.
/// Initializes a new instance of the <see cref="DalamudIME"/> class.
/// </summary>
internal class DalamudIME : IDisposable
internal DalamudIME()
{
private static readonly ModuleLog Log = new("IME");
}
private IntPtr interfaceHandle;
private IntPtr wndProcPtr;
private IntPtr oldWndProcPtr;
private WndProcDelegate wndProcDelegate;
private delegate long WndProcDelegate(IntPtr hWnd, uint msg, ulong wParam, long lParam);
/// <summary>
/// Initializes a new instance of the <see cref="DalamudIME"/> class.
/// </summary>
internal DalamudIME()
/// <summary>
/// Gets a value indicating whether the module is enabled.
/// </summary>
internal bool IsEnabled { get; private set; }
/// <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)
{
}
private delegate long WndProcDelegate(IntPtr hWnd, uint msg, ulong wParam, long lParam);
/// <summary>
/// Gets a value indicating whether the module is enabled.
/// </summary>
internal bool IsEnabled { get; private set; }
/// <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>
/// 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)
{
var io = ImGui.GetIO();
var wmsg = (WindowsMessage)msg;
switch (wmsg)
{
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);
if (hIMC == IntPtr.Zero)
return 0;
var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, IntPtr.Zero, 0);
var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize);
ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, unmanagedPointer, (uint)dwSize);
var bytes = new byte[dwSize];
Marshal.Copy(unmanagedPointer, bytes, 0, (int)dwSize);
Marshal.FreeHGlobal(unmanagedPointer);
var lpstr = Encoding.Unicode.GetString(bytes);
io.AddInputCharactersUTF8(lpstr);
this.ImmComp = string.Empty;
this.ImmCandNative = default;
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)
{
Log.Error(ex, "Prevented a crash in an IME hook");
}
return CallWindowProcW(this.oldWndProcPtr, hWnd, msg, wParam, lParam);
SetWindowLongPtrW(this.interfaceHandle, WindowLongType.WndProc, this.oldWndProcPtr);
this.oldWndProcPtr = IntPtr.Zero;
}
}
/// <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)
{
var io = ImGui.GetIO();
var wmsg = (WindowsMessage)msg;
switch (wmsg)
{
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);
if (hIMC == IntPtr.Zero)
return 0;
var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, IntPtr.Zero, 0);
var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize);
ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, unmanagedPointer, (uint)dwSize);
var bytes = new byte[dwSize];
Marshal.Copy(unmanagedPointer, bytes, 0, (int)dwSize);
Marshal.FreeHGlobal(unmanagedPointer);
var lpstr = Encoding.Unicode.GetString(bytes);
io.AddInputCharactersUTF8(lpstr);
this.ImmComp = string.Empty;
this.ImmCandNative = default;
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)
{
Log.Error(ex, "Prevented a crash in an IME hook");
}
return CallWindowProcW(this.oldWndProcPtr, hWnd, msg, wParam, lParam);
}
}

View file

@ -1,28 +1,27 @@
using System.Diagnostics.CodeAnalysis;
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>
/// The structure of the PartyFinder packet.
/// Gets the size of this 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>
/// Gets the size of this packet.
/// </summary>
internal static int PacketSize { get; } = Marshal.SizeOf<PartyFinderPacket>();
internal static int PacketSize { get; } = Marshal.SizeOf<PartyFinderPacket>();
internal readonly int BatchNumber;
internal readonly int BatchNumber;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
private readonly byte[] padding1;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
private readonly byte[] padding1;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
internal readonly PartyFinderPacketListing[] Listings;
}
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
internal readonly PartyFinderPacketListing[] Listings;
}

View file

@ -2,98 +2,97 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
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
{
/// <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)]
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()
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
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);
}
// a valid party finder must have at least one slot set
return this.Slots.All(slot => slot == 0);
}
}

View file

@ -1,21 +1,20 @@
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>
/// The address resolver for the <see cref="PartyFinderGui"/> class.
/// Gets the address of the native ReceiveListing method.
/// </summary>
public class PartyFinderAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the native ReceiveListing method.
/// </summary>
public IntPtr ReceiveListing { get; private set; }
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");
}
/// <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,133 +8,132 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
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>
/// This class handles interacting with the native PartyFinder window.
/// Initializes a new instance of the <see cref="PartyFinderGui"/> class.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public sealed class PartyFinderGui : IDisposable
internal PartyFinderGui()
{
private readonly PartyFinderAddressResolver address;
private readonly IntPtr memory;
this.address = new PartyFinderAddressResolver();
this.address.Setup();
private readonly Hook<ReceiveListingDelegate> receiveListingHook;
this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize);
/// <summary>
/// Initializes a new instance of the <see cref="PartyFinderGui"/> class.
/// </summary>
internal PartyFinderGui()
this.receiveListingHook = new Hook<ReceiveListingDelegate>(this.address.ReceiveListing, new ReceiveListingDelegate(this.HandleReceiveListingDetour));
}
/// <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);
[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
{
this.address = new PartyFinderAddressResolver();
this.address.Setup();
Marshal.FreeHGlobal(this.memory);
}
catch (BadImageFormatException)
{
Log.Warning("Could not free PartyFinderGui memory.");
}
}
this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize);
this.receiveListingHook = new Hook<ReceiveListingDelegate>(this.address.ReceiveListing, new ReceiveListingDelegate(this.HandleReceiveListingDetour));
private void HandleReceiveListingDetour(IntPtr managerPtr, IntPtr data)
{
try
{
this.HandleListingEvents(data);
}
catch (Exception ex)
{
Log.Error(ex, "Exception on ReceiveListing hook.");
}
/// <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);
this.receiveListingHook.Original(managerPtr, data);
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate void ReceiveListingDelegate(IntPtr managerPtr, IntPtr data);
private void HandleListingEvents(IntPtr data)
{
var dataPtr = data + 0x10;
/// <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;
var packet = Marshal.PtrToStructure<PartyFinderPacket>(dataPtr);
/// <summary>
/// Enables this module.
/// </summary>
public void Enable()
// rewriting is an expensive operation, so only do it if necessary
var needToRewrite = false;
for (var i = 0; i < packet.Listings.Length; i++)
{
this.receiveListingHook.Enable();
// 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;
}
/// <summary>
/// Dispose of m anaged and unmanaged resources.
/// </summary>
public void Dispose()
if (!needToRewrite)
{
this.receiveListingHook.Dispose();
try
{
Marshal.FreeHGlobal(this.memory);
}
catch (BadImageFormatException)
{
Log.Warning("Could not free PartyFinderGui memory.");
}
return;
}
private void HandleReceiveListingDetour(IntPtr managerPtr, IntPtr data)
// write our struct into the memory (doing this directly crashes the game)
Marshal.StructureToPtr(packet, this.memory, false);
// copy our new memory over the game's
unsafe
{
try
{
this.HandleListingEvents(data);
}
catch (Exception ex)
{
Log.Error(ex, "Exception on ReceiveListing hook.");
}
this.receiveListingHook.Original(managerPtr, data);
}
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++)
{
// 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;
}
if (!needToRewrite)
{
return;
}
// write our struct into the memory (doing this directly crashes the game)
Marshal.StructureToPtr(packet, this.memory, false);
// copy our new memory over the game's
unsafe
{
Buffer.MemoryCopy((void*)this.memory, (void*)dataPtr, PartyFinderPacket.PacketSize, PartyFinderPacket.PacketSize);
}
Buffer.MemoryCopy((void*)this.memory, (void*)dataPtr, PartyFinderPacket.PacketSize, PartyFinderPacket.PacketSize);
}
}
}

View file

@ -1,26 +1,25 @@
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>
/// Condition flags for the <see cref="PartyFinderGui"/> class.
/// No duty condition.
/// </summary>
[Flags]
public enum ConditionFlags : uint
{
/// <summary>
/// No duty condition.
/// </summary>
None = 1,
None = 1,
/// <summary>
/// The duty complete condition.
/// </summary>
DutyComplete = 2,
/// <summary>
/// The duty complete condition.
/// </summary>
DutyComplete = 2,
/// <summary>
/// The duty incomplete condition.
/// </summary>
DutyIncomplete = 4,
}
/// <summary>
/// The duty incomplete condition.
/// </summary>
DutyIncomplete = 4,
}

View file

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

View file

@ -1,31 +1,30 @@
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>
/// Duty finder settings flags for the <see cref="PartyFinderGui"/> class.
/// No duty finder settings.
/// </summary>
[Flags]
public enum DutyFinderSettingsFlags : uint
{
/// <summary>
/// No duty finder settings.
/// </summary>
None = 0,
None = 0,
/// <summary>
/// The undersized party setting.
/// </summary>
UndersizedParty = 1 << 0,
/// <summary>
/// The undersized party setting.
/// </summary>
UndersizedParty = 1 << 0,
/// <summary>
/// The minimum item level setting.
/// </summary>
MinimumItemLevel = 1 << 1,
/// <summary>
/// The minimum item level setting.
/// </summary>
MinimumItemLevel = 1 << 1,
/// <summary>
/// The silence echo setting.
/// </summary>
SilenceEcho = 1 << 2,
}
/// <summary>
/// The silence echo setting.
/// </summary>
SilenceEcho = 1 << 2,
}

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