chore: convert Dalamud to file-scoped namespaces

This commit is contained in:
goat 2022-10-29 15:23:22 +02:00
parent b093323acc
commit 987ff8dc8f
No known key found for this signature in database
GPG key ID: 49E2AA8C6A76498B
368 changed files with 55081 additions and 55450 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> /// <summary>
/// Enum describing the language the game loads in. /// Indicating a Japanese game client.
/// </summary> /// </summary>
public enum ClientLanguage Japanese,
{
/// <summary>
/// Indicating a Japanese game client.
/// </summary>
Japanese,
/// <summary> /// <summary>
/// Indicating an English game client. /// Indicating an English game client.
/// </summary> /// </summary>
English, English,
/// <summary> /// <summary>
/// Indicating a German game client. /// Indicating a German game client.
/// </summary> /// </summary>
German, German,
/// <summary> /// <summary>
/// Indicating a French game client. /// Indicating a French game client.
/// </summary> /// </summary>
French, French,
}
} }

View file

@ -1,27 +1,26 @@
using System; using System;
namespace Dalamud namespace Dalamud;
/// <summary>
/// Extension methods for the <see cref="ClientLanguage"/> class.
/// </summary>
public static class ClientLanguageExtensions
{ {
/// <summary> /// <summary>
/// Extension methods for the <see cref="ClientLanguage"/> class. /// Converts a Dalamud ClientLanguage to the corresponding Lumina variant.
/// </summary> /// </summary>
public static class ClientLanguageExtensions /// <param name="language">Language to convert.</param>
/// <returns>Converted language.</returns>
public static Lumina.Data.Language ToLumina(this ClientLanguage language)
{ {
/// <summary> return language switch
/// Converts a Dalamud ClientLanguage to the corresponding Lumina variant.
/// </summary>
/// <param name="language">Language to convert.</param>
/// <returns>Converted language.</returns>
public static Lumina.Data.Language ToLumina(this ClientLanguage language)
{ {
return language switch ClientLanguage.Japanese => Lumina.Data.Language.Japanese,
{ ClientLanguage.English => Lumina.Data.Language.English,
ClientLanguage.Japanese => Lumina.Data.Language.Japanese, ClientLanguage.German => Lumina.Data.Language.German,
ClientLanguage.English => Lumina.Data.Language.English, ClientLanguage.French => Lumina.Data.Language.French,
ClientLanguage.German => Lumina.Data.Language.German, _ => throw new ArgumentOutOfRangeException(nameof(language)),
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> /// <summary>
/// Configuration to store settings for a dalamud plugin. /// Gets or sets configuration version.
/// </summary> /// </summary>
public interface IPluginConfiguration int Version { get; set; }
{
/// <summary>
/// Gets or sets configuration version.
/// </summary>
int Version { get; set; }
}
} }

View file

@ -10,368 +10,367 @@ using Newtonsoft.Json;
using Serilog; using Serilog;
using Serilog.Events; using Serilog.Events;
namespace Dalamud.Configuration.Internal namespace Dalamud.Configuration.Internal;
/// <summary>
/// Class containing Dalamud settings.
/// </summary>
[Serializable]
internal sealed class DalamudConfiguration : IServiceType
{ {
/// <summary> private static readonly JsonSerializerSettings SerializerSettings = new()
/// Class containing Dalamud settings.
/// </summary>
[Serializable]
internal sealed class DalamudConfiguration : IServiceType
{ {
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 key to opt into Dalamud staging builds.
/// </summary>
public string? DalamudBetaKey { get; set; } = null;
/// <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 to use AXIS fonts from the game.
/// </summary>
public bool UseAxisFontsFromGame { get; set; } = false;
/// <summary>
/// Gets or sets the gamma value to apply for Dalamud fonts. Effects text thickness.
///
/// Before gamma is applied...
/// * ...TTF fonts loaded with stb or FreeType are in linear space.
/// * ...the game's prebaked AXIS fonts are in gamma space with gamma value of 1.4.
/// </summary>
public float FontGammaLevel { get; set; } = 1.4f;
/// <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 to write to log files synchronously.
/// </summary>
public bool LogSynchronously { get; set; } = false;
/// <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 the dev bar should open at startup.
/// </summary>
public bool DevBarOpenAtStartup { 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 a value indicating whether to resume game main thread after plugins load.
/// </summary>
public bool IsResumeGameAfterPluginLoad { get; set; } = false;
/// <summary>
/// Gets or sets the kind of beta to download when <see cref="DalamudBetaKey"/> matches the server value.
/// </summary>
public string DalamudBetaKind { 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 value indicating the wait time between plugin unload and plugin assembly unload.
/// Uses default value that may change between versions if set to null.
/// </summary>
public int? PluginWaitBeforeFree { 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>
/// Gets or sets the order of DTR elements, by title.
/// </summary>
public List<string>? DtrOrder { get; set; }
/// <summary>
/// Gets or sets the list of ignored DTR elements, by title.
/// </summary>
public List<string>? DtrIgnore { get; set; }
/// <summary>
/// Gets or sets the spacing used for DTR entries.
/// </summary>
public int DtrSpacing { get; set; } = 10;
/// <summary>
/// Gets or sets a value indicating whether to swap the
/// direction in which elements are drawn in the DTR.
/// False indicates that elements will be drawn from the end of
/// the left side of the Server Info bar, and continue leftwards.
/// True indicates the opposite.
/// </summary>
public bool DtrSwapDirection { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether the title screen menu is shown.
/// </summary>
public bool ShowTsm { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not market board data should be uploaded.
/// </summary>
public bool IsMbCollect { get; set; } = true;
/// <summary>
/// Gets the ISO 639-1 two-letter code for the language of the effective Dalamud display language.
/// </summary>
public string EffectiveLanguage
{
get
{ {
TypeNameHandling = TypeNameHandling.All, var languages = Localization.ApplicableLangCodes.Prepend("en").ToArray();
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 key to opt into Dalamud staging builds.
/// </summary>
public string? DalamudBetaKey { get; set; } = null;
/// <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 to use AXIS fonts from the game.
/// </summary>
public bool UseAxisFontsFromGame { get; set; } = false;
/// <summary>
/// Gets or sets the gamma value to apply for Dalamud fonts. Effects text thickness.
///
/// Before gamma is applied...
/// * ...TTF fonts loaded with stb or FreeType are in linear space.
/// * ...the game's prebaked AXIS fonts are in gamma space with gamma value of 1.4.
/// </summary>
public float FontGammaLevel { get; set; } = 1.4f;
/// <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 to write to log files synchronously.
/// </summary>
public bool LogSynchronously { get; set; } = false;
/// <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 the dev bar should open at startup.
/// </summary>
public bool DevBarOpenAtStartup { 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 a value indicating whether to resume game main thread after plugins load.
/// </summary>
public bool IsResumeGameAfterPluginLoad { get; set; } = false;
/// <summary>
/// Gets or sets the kind of beta to download when <see cref="DalamudBetaKey"/> matches the server value.
/// </summary>
public string DalamudBetaKind { 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 value indicating the wait time between plugin unload and plugin assembly unload.
/// Uses default value that may change between versions if set to null.
/// </summary>
public int? PluginWaitBeforeFree { 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>
/// Gets or sets the order of DTR elements, by title.
/// </summary>
public List<string>? DtrOrder { get; set; }
/// <summary>
/// Gets or sets the list of ignored DTR elements, by title.
/// </summary>
public List<string>? DtrIgnore { get; set; }
/// <summary>
/// Gets or sets the spacing used for DTR entries.
/// </summary>
public int DtrSpacing { get; set; } = 10;
/// <summary>
/// Gets or sets a value indicating whether to swap the
/// direction in which elements are drawn in the DTR.
/// False indicates that elements will be drawn from the end of
/// the left side of the Server Info bar, and continue leftwards.
/// True indicates the opposite.
/// </summary>
public bool DtrSwapDirection { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether the title screen menu is shown.
/// </summary>
public bool ShowTsm { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not market board data should be uploaded.
/// </summary>
public bool IsMbCollect { get; set; } = true;
/// <summary>
/// Gets the ISO 639-1 two-letter code for the language of the effective Dalamud display language.
/// </summary>
public string EffectiveLanguage
{
get
{
var languages = Localization.ApplicableLangCodes.Prepend("en").ToArray();
try
{
if (string.IsNullOrEmpty(this.LanguageOverride))
{
var currentUiLang = CultureInfo.CurrentUICulture;
if (Localization.ApplicableLangCodes.Any(x => currentUiLang.TwoLetterISOLanguageName == x))
return currentUiLang.TwoLetterISOLanguageName;
else
return languages[0];
}
else
{
return this.LanguageOverride;
}
}
catch (Exception)
{
return languages[0];
}
}
}
/// <summary>
/// Gets or sets a value indicating whether or not to show info on dev bar.
/// </summary>
public bool ShowDevBarInfo { get; set; } = true;
/// <summary>
/// Gets or sets the last-used contact details for the plugin feedback form.
/// </summary>
public string LastFeedbackContactDetails { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a list of plugins that testing builds should be downloaded for.
/// </summary>
public List<PluginTestingOptIn>? PluginTestingOptIns { 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 = null;
try try
{ {
deserialized = JsonConvert.DeserializeObject<DalamudConfiguration>(File.ReadAllText(path), SerializerSettings); if (string.IsNullOrEmpty(this.LanguageOverride))
{
var currentUiLang = CultureInfo.CurrentUICulture;
if (Localization.ApplicableLangCodes.Any(x => currentUiLang.TwoLetterISOLanguageName == x))
return currentUiLang.TwoLetterISOLanguageName;
else
return languages[0];
}
else
{
return this.LanguageOverride;
}
} }
catch (Exception ex) catch (Exception)
{ {
Log.Warning(ex, "Failed to load DalamudConfiguration at {0}", path); return languages[0];
} }
deserialized ??= new DalamudConfiguration();
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);
} }
} }
/// <summary>
/// Gets or sets a value indicating whether or not to show info on dev bar.
/// </summary>
public bool ShowDevBarInfo { get; set; } = true;
/// <summary>
/// Gets or sets the last-used contact details for the plugin feedback form.
/// </summary>
public string LastFeedbackContactDetails { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a list of plugins that testing builds should be downloaded for.
/// </summary>
public List<PluginTestingOptIn>? PluginTestingOptIns { 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 = null;
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;
}
/// <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;
/// <summary>
/// Additional locations to load dev plugins from.
/// </summary>
internal sealed class DevPluginLocationSettings
{ {
/// <summary> /// <summary>
/// Additional locations to load dev plugins from. /// Gets or sets the dev pluign path.
/// </summary> /// </summary>
internal sealed class DevPluginLocationSettings public string Path { get; set; }
{
/// <summary>
/// Gets or sets the dev pluign path.
/// </summary>
public string Path { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the third party repo is enabled. /// Gets or sets a value indicating whether the third party repo is enabled.
/// </summary> /// </summary>
public bool IsEnabled { get; set; } public bool IsEnabled { get; set; }
/// <summary> /// <summary>
/// Clone this object. /// Clone this object.
/// </summary> /// </summary>
/// <returns>A shallow copy of this object.</returns> /// <returns>A shallow copy of this object.</returns>
public DevPluginLocationSettings Clone() => this.MemberwiseClone() as DevPluginLocationSettings; public DevPluginLocationSettings Clone() => this.MemberwiseClone() as DevPluginLocationSettings;
}
} }

View file

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

View file

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

View file

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

View file

@ -2,149 +2,148 @@ using System.IO;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud.Configuration namespace Dalamud.Configuration;
/// <summary>
/// Configuration to store settings for a dalamud plugin.
/// </summary>
public sealed class PluginConfigurations
{ {
private readonly DirectoryInfo configDirectory;
/// <summary> /// <summary>
/// Configuration to store settings for a dalamud plugin. /// Initializes a new instance of the <see cref="PluginConfigurations"/> class.
/// </summary> /// </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> /// <summary>
/// Initializes a new instance of the <see cref="PluginConfigurations"/> class. /// Save/Load plugin configuration.
/// </summary> /// NOTE: Save/Load are still using Type information for now,
/// <param name="storageFolder">Directory for storage of plugin configuration files.</param> /// despite LoadForType superseding Load and not requiring or using it.
public PluginConfigurations(string storageFolder) /// 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, SerializeConfig(config));
}
/// <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 DeserializeConfig(File.ReadAllText(path.FullName));
}
/// <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
{ {
this.configDirectory = new DirectoryInfo(storageFolder); var path = this.GetDirectoryPath(pluginName);
this.configDirectory.Create();
}
/// <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, SerializeConfig(config));
}
/// <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) if (!path.Exists)
return null;
return DeserializeConfig(File.ReadAllText(path.FullName));
}
/// <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); path.Create();
if (!path.Exists)
{
path.Create();
}
return path.FullName;
}
catch
{
return string.Empty;
} }
return path.FullName;
} }
catch
/// <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 string.Empty;
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> /// <summary>
/// Get FileInfo to plugin config file. /// Load Plugin configuration. Parameterized deserialization.
/// </summary> /// Currently this is called via reflection from DalamudPluginInterface.GetPluginConfig().
/// <param name="pluginName">InternalName of the plugin.</param> /// Eventually there may be an additional pluginInterface method that can call this directly
/// <returns>FileInfo of the config file.</returns> /// without reflection - for now this is in support of the existing plugin api.
public FileInfo GetConfigFile(string pluginName) => new(Path.Combine(this.configDirectory.FullName, $"{pluginName}.json")); /// </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);
/// <summary> return !path.Exists ? default : JsonConvert.DeserializeObject<T>(File.ReadAllText(path.FullName));
/// Serializes a plugin configuration object.
/// </summary> // intentionally no type handling - it will break when updating a plugin at runtime
/// <param name="config">The configuration object.</param> // and turns out to be unnecessary when we fully qualify the object type
/// <returns>A string representing the serialized configuration object.</returns> }
internal static string SerializeConfig(object? config)
/// <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"));
/// <summary>
/// Serializes a plugin configuration object.
/// </summary>
/// <param name="config">The configuration object.</param>
/// <returns>A string representing the serialized configuration object.</returns>
internal static string SerializeConfig(object? config)
{
return JsonConvert.SerializeObject(config, Formatting.Indented, new JsonSerializerSettings
{ {
return JsonConvert.SerializeObject(config, Formatting.Indented, new JsonSerializerSettings TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
TypeNameHandling = TypeNameHandling.Objects,
});
}
/// <summary>
/// Deserializes a plugin configuration from a string.
/// </summary>
/// <param name="data">The serialized configuration.</param>
/// <returns>The configuration object, or null.</returns>
internal static IPluginConfiguration? DeserializeConfig(string data)
{
return JsonConvert.DeserializeObject<IPluginConfiguration>(
data,
new JsonSerializerSettings
{ {
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
TypeNameHandling = TypeNameHandling.Objects, TypeNameHandling = TypeNameHandling.Objects,
}); });
}
/// <summary>
/// Deserializes a plugin configuration from a string.
/// </summary>
/// <param name="data">The serialized configuration.</param>
/// <returns>The configuration object, or null.</returns>
internal static IPluginConfiguration? DeserializeConfig(string data)
{
return JsonConvert.DeserializeObject<IPluginConfiguration>(
data,
new JsonSerializerSettings
{
TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
TypeNameHandling = TypeNameHandling.Objects,
});
}
private DirectoryInfo GetDirectoryPath(string pluginName) => new(Path.Combine(this.configDirectory.FullName, pluginName));
} }
private DirectoryInfo GetDirectoryPath(string pluginName) => new(Path.Combine(this.configDirectory.FullName, pluginName));
} }

View file

@ -19,131 +19,130 @@ using Serilog;
[assembly: InternalsVisibleTo("Dalamud.Test")] [assembly: InternalsVisibleTo("Dalamud.Test")]
[assembly: InternalsVisibleTo("Dalamud.DevHelpers")] [assembly: InternalsVisibleTo("Dalamud.DevHelpers")]
namespace Dalamud namespace Dalamud;
/// <summary>
/// The main Dalamud class containing all subsystems.
/// </summary>
internal sealed class Dalamud : IServiceType
{ {
#region Internals
private readonly ManualResetEvent unloadSignal;
#endregion
/// <summary> /// <summary>
/// The main Dalamud class containing all subsystems. /// Initializes a new instance of the <see cref="Dalamud"/> class.
/// </summary> /// </summary>
internal sealed class Dalamud : IServiceType /// <param name="info">DalamudStartInfo instance.</param>
/// <param name="configuration">The Dalamud configuration.</param>
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param>
public Dalamud(DalamudStartInfo info, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent)
{ {
#region Internals this.unloadSignal = new ManualResetEvent(false);
this.unloadSignal.Reset();
private readonly ManualResetEvent unloadSignal; ServiceManager.InitializeProvidedServicesAndClientStructs(this, info, configuration);
#endregion if (!configuration.IsResumeGameAfterPluginLoad)
/// <summary>
/// Initializes a new instance of the <see cref="Dalamud"/> class.
/// </summary>
/// <param name="info">DalamudStartInfo instance.</param>
/// <param name="configuration">The Dalamud configuration.</param>
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param>
public Dalamud(DalamudStartInfo info, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent)
{ {
this.unloadSignal = new ManualResetEvent(false); NativeFunctions.SetEvent(mainThreadContinueEvent);
this.unloadSignal.Reset(); try
{
ServiceManager.InitializeProvidedServicesAndClientStructs(this, info, configuration); _ = ServiceManager.InitializeEarlyLoadableServices();
}
if (!configuration.IsResumeGameAfterPluginLoad) catch (Exception e)
{
Log.Error(e, "Service initialization failure");
}
}
else
{
Task.Run(async () =>
{ {
NativeFunctions.SetEvent(mainThreadContinueEvent);
try try
{ {
_ = ServiceManager.InitializeEarlyLoadableServices(); var tasks = new[]
{
ServiceManager.InitializeEarlyLoadableServices(),
ServiceManager.BlockingResolved,
};
await Task.WhenAny(tasks);
var faultedTasks = tasks.Where(x => x.IsFaulted).Select(x => (Exception)x.Exception!).ToArray();
if (faultedTasks.Any())
throw new AggregateException(faultedTasks);
NativeFunctions.SetEvent(mainThreadContinueEvent);
await Task.WhenAll(tasks);
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e, "Service initialization failure"); Log.Error(e, "Service initialization failure");
} }
} finally
else
{
Task.Run(async () =>
{ {
try NativeFunctions.SetEvent(mainThreadContinueEvent);
{ }
var tasks = new[] });
{
ServiceManager.InitializeEarlyLoadableServices(),
ServiceManager.BlockingResolved,
};
await Task.WhenAny(tasks);
var faultedTasks = tasks.Where(x => x.IsFaulted).Select(x => (Exception)x.Exception!).ToArray();
if (faultedTasks.Any())
throw new AggregateException(faultedTasks);
NativeFunctions.SetEvent(mainThreadContinueEvent);
await Task.WhenAll(tasks);
}
catch (Exception e)
{
Log.Error(e, "Service initialization failure");
}
finally
{
NativeFunctions.SetEvent(mainThreadContinueEvent);
}
});
}
}
/// <summary>
/// Gets location of stored assets.
/// </summary>
internal DirectoryInfo AssetDirectory => new(Service<DalamudStartInfo>.Get().AssetDirectory!);
/// <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>
/// Dispose subsystems related to plugin handling.
/// </summary>
public void DisposePlugins()
{
// 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>
/// 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);
} }
} }
/// <summary>
/// Gets location of stored assets.
/// </summary>
internal DirectoryInfo AssetDirectory => new(Service<DalamudStartInfo>.Get().AssetDirectory!);
/// <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>
/// Dispose subsystems related to plugin handling.
/// </summary>
public void DisposePlugins()
{
// 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>
/// 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);
}
} }

View file

@ -4,167 +4,166 @@ using System.Collections.Generic;
using Dalamud.Game; using Dalamud.Game;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud namespace Dalamud;
/// <summary>
/// Struct containing information needed to initialize Dalamud.
/// </summary>
[Serializable]
public record DalamudStartInfo : IServiceType
{ {
/// <summary> /// <summary>
/// Struct containing information needed to initialize Dalamud. /// Initializes a new instance of the <see cref="DalamudStartInfo"/> class.
/// </summary> /// </summary>
[Serializable] public DalamudStartInfo()
public record DalamudStartInfo : IServiceType
{ {
/// <summary> // ignored
/// Initializes a new instance of the <see cref="DalamudStartInfo"/> class.
/// </summary>
public DalamudStartInfo()
{
// ignored
}
/// <summary>
/// Initializes a new instance of the <see cref="DalamudStartInfo"/> class.
/// </summary>
/// <param name="other">Object to copy values from.</param>
public DalamudStartInfo(DalamudStartInfo other)
{
this.WorkingDirectory = other.WorkingDirectory;
this.ConfigurationPath = other.ConfigurationPath;
this.PluginDirectory = other.PluginDirectory;
this.DefaultPluginDirectory = other.DefaultPluginDirectory;
this.AssetDirectory = other.AssetDirectory;
this.Language = other.Language;
this.GameVersion = other.GameVersion;
this.DelayInitializeMs = other.DelayInitializeMs;
this.TroubleshootingPackData = other.TroubleshootingPackData;
this.NoLoadPlugins = other.NoLoadPlugins;
this.NoLoadThirdPartyPlugins = other.NoLoadThirdPartyPlugins;
this.BootLogPath = other.BootLogPath;
this.BootShowConsole = other.BootShowConsole;
this.BootDisableFallbackConsole = other.BootDisableFallbackConsole;
this.BootWaitMessageBox = other.BootWaitMessageBox;
this.BootWaitDebugger = other.BootWaitDebugger;
this.BootVehEnabled = other.BootVehEnabled;
this.BootVehFull = other.BootVehFull;
this.BootEnableEtw = other.BootEnableEtw;
this.BootDotnetOpenProcessHookMode = other.BootDotnetOpenProcessHookMode;
this.BootEnabledGameFixes = other.BootEnabledGameFixes;
this.BootUnhookDlls = other.BootUnhookDlls;
this.CrashHandlerShow = other.CrashHandlerShow;
}
/// <summary>
/// Gets or sets the working directory of the XIVLauncher installations.
/// </summary>
public string? WorkingDirectory { get; set; }
/// <summary>
/// Gets or sets the path to the configuration file.
/// </summary>
public string? ConfigurationPath { get; set; }
/// <summary>
/// Gets or sets the path to the directory for installed plugins.
/// </summary>
public string? PluginDirectory { get; set; }
/// <summary>
/// Gets or sets the path to the directory for developer plugins.
/// </summary>
public string? DefaultPluginDirectory { get; set; }
/// <summary>
/// Gets or sets the path to core Dalamud assets.
/// </summary>
public string? AssetDirectory { get; set; }
/// <summary>
/// Gets or sets the language of the game client.
/// </summary>
public ClientLanguage Language { get; set; } = ClientLanguage.English;
/// <summary>
/// Gets or sets the current game version code.
/// </summary>
[JsonConverter(typeof(GameVersionConverter))]
public GameVersion? GameVersion { get; set; }
/// <summary>
/// Gets or sets troubleshooting information to attach when generating a tspack file.
/// </summary>
public string TroubleshootingPackData { get; set; }
/// <summary>
/// Gets or sets a value that specifies how much to wait before a new Dalamud session.
/// </summary>
public int DelayInitializeMs { get; set; }
/// <summary>
/// Gets or sets a value indicating whether no plugins should be loaded.
/// </summary>
public bool NoLoadPlugins { get; set; }
/// <summary>
/// Gets or sets a value indicating whether no third-party plugins should be loaded.
/// </summary>
public bool NoLoadThirdPartyPlugins { get; set; }
/// <summary>
/// Gets or sets the path the boot log file is supposed to be written to.
/// </summary>
public string? BootLogPath { get; set; }
/// <summary>
/// Gets or sets a value indicating whether a Boot console should be shown.
/// </summary>
public bool BootShowConsole { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the fallback console should be shown, if needed.
/// </summary>
public bool BootDisableFallbackConsole { get; set; }
/// <summary>
/// Gets or sets a flag indicating where Dalamud should wait with a message box.
/// </summary>
public int BootWaitMessageBox { get; set; }
/// <summary>
/// Gets or sets a value indicating whether Dalamud should wait for a debugger to be attached before initializing.
/// </summary>
public bool BootWaitDebugger { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the VEH should be enabled.
/// </summary>
public bool BootVehEnabled { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the VEH should be doing full crash dumps.
/// </summary>
public bool BootVehFull { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not ETW should be enabled.
/// </summary>
public bool BootEnableEtw { get; set; }
/// <summary>
/// Gets or sets a value choosing the OpenProcess hookmode.
/// </summary>
public int BootDotnetOpenProcessHookMode { get; set; }
/// <summary>
/// Gets or sets a list of enabled game fixes.
/// </summary>
public List<string>? BootEnabledGameFixes { get; set; }
/// <summary>
/// Gets or sets a list of DLLs that should be unhooked.
/// </summary>
public List<string>? BootUnhookDlls { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to show crash handler console window.
/// </summary>
public bool CrashHandlerShow { get; set; }
} }
/// <summary>
/// Initializes a new instance of the <see cref="DalamudStartInfo"/> class.
/// </summary>
/// <param name="other">Object to copy values from.</param>
public DalamudStartInfo(DalamudStartInfo other)
{
this.WorkingDirectory = other.WorkingDirectory;
this.ConfigurationPath = other.ConfigurationPath;
this.PluginDirectory = other.PluginDirectory;
this.DefaultPluginDirectory = other.DefaultPluginDirectory;
this.AssetDirectory = other.AssetDirectory;
this.Language = other.Language;
this.GameVersion = other.GameVersion;
this.DelayInitializeMs = other.DelayInitializeMs;
this.TroubleshootingPackData = other.TroubleshootingPackData;
this.NoLoadPlugins = other.NoLoadPlugins;
this.NoLoadThirdPartyPlugins = other.NoLoadThirdPartyPlugins;
this.BootLogPath = other.BootLogPath;
this.BootShowConsole = other.BootShowConsole;
this.BootDisableFallbackConsole = other.BootDisableFallbackConsole;
this.BootWaitMessageBox = other.BootWaitMessageBox;
this.BootWaitDebugger = other.BootWaitDebugger;
this.BootVehEnabled = other.BootVehEnabled;
this.BootVehFull = other.BootVehFull;
this.BootEnableEtw = other.BootEnableEtw;
this.BootDotnetOpenProcessHookMode = other.BootDotnetOpenProcessHookMode;
this.BootEnabledGameFixes = other.BootEnabledGameFixes;
this.BootUnhookDlls = other.BootUnhookDlls;
this.CrashHandlerShow = other.CrashHandlerShow;
}
/// <summary>
/// Gets or sets the working directory of the XIVLauncher installations.
/// </summary>
public string? WorkingDirectory { get; set; }
/// <summary>
/// Gets or sets the path to the configuration file.
/// </summary>
public string? ConfigurationPath { get; set; }
/// <summary>
/// Gets or sets the path to the directory for installed plugins.
/// </summary>
public string? PluginDirectory { get; set; }
/// <summary>
/// Gets or sets the path to the directory for developer plugins.
/// </summary>
public string? DefaultPluginDirectory { get; set; }
/// <summary>
/// Gets or sets the path to core Dalamud assets.
/// </summary>
public string? AssetDirectory { get; set; }
/// <summary>
/// Gets or sets the language of the game client.
/// </summary>
public ClientLanguage Language { get; set; } = ClientLanguage.English;
/// <summary>
/// Gets or sets the current game version code.
/// </summary>
[JsonConverter(typeof(GameVersionConverter))]
public GameVersion? GameVersion { get; set; }
/// <summary>
/// Gets or sets troubleshooting information to attach when generating a tspack file.
/// </summary>
public string TroubleshootingPackData { get; set; }
/// <summary>
/// Gets or sets a value that specifies how much to wait before a new Dalamud session.
/// </summary>
public int DelayInitializeMs { get; set; }
/// <summary>
/// Gets or sets a value indicating whether no plugins should be loaded.
/// </summary>
public bool NoLoadPlugins { get; set; }
/// <summary>
/// Gets or sets a value indicating whether no third-party plugins should be loaded.
/// </summary>
public bool NoLoadThirdPartyPlugins { get; set; }
/// <summary>
/// Gets or sets the path the boot log file is supposed to be written to.
/// </summary>
public string? BootLogPath { get; set; }
/// <summary>
/// Gets or sets a value indicating whether a Boot console should be shown.
/// </summary>
public bool BootShowConsole { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the fallback console should be shown, if needed.
/// </summary>
public bool BootDisableFallbackConsole { get; set; }
/// <summary>
/// Gets or sets a flag indicating where Dalamud should wait with a message box.
/// </summary>
public int BootWaitMessageBox { get; set; }
/// <summary>
/// Gets or sets a value indicating whether Dalamud should wait for a debugger to be attached before initializing.
/// </summary>
public bool BootWaitDebugger { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the VEH should be enabled.
/// </summary>
public bool BootVehEnabled { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the VEH should be doing full crash dumps.
/// </summary>
public bool BootVehFull { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not ETW should be enabled.
/// </summary>
public bool BootEnableEtw { get; set; }
/// <summary>
/// Gets or sets a value choosing the OpenProcess hookmode.
/// </summary>
public int BootDotnetOpenProcessHookMode { get; set; }
/// <summary>
/// Gets or sets a list of enabled game fixes.
/// </summary>
public List<string>? BootEnabledGameFixes { get; set; }
/// <summary>
/// Gets or sets a list of DLLs that should be unhooked.
/// </summary>
public List<string>? BootUnhookDlls { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to show crash handler console window.
/// </summary>
public bool CrashHandlerShow { get; set; }
} }

View file

@ -19,331 +19,330 @@ using Lumina.Excel;
using Newtonsoft.Json; using Newtonsoft.Json;
using Serilog; using Serilog;
namespace Dalamud.Data namespace Dalamud.Data;
/// <summary>
/// This class provides data for Dalamud-internal features, but can also be used by plugins if needed.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class DataManager : IDisposable, IServiceType
{ {
/// <summary> private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex";
/// This class provides data for Dalamud-internal features, but can also be used by plugins if needed.
/// </summary> private readonly Thread luminaResourceThread;
[PluginInterface] private readonly CancellationTokenSource luminaCancellationTokenSource;
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService] [ServiceManager.ServiceConstructor]
public sealed class DataManager : IDisposable, IServiceType private DataManager(DalamudStartInfo dalamudStartInfo, Dalamud dalamud)
{ {
private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex"; this.Language = dalamudStartInfo.Language;
private readonly Thread luminaResourceThread; // Set up default values so plugins do not null-reference when data is being loaded.
private readonly CancellationTokenSource luminaCancellationTokenSource; this.ClientOpCodes = this.ServerOpCodes = new ReadOnlyDictionary<string, ushort>(new Dictionary<string, ushort>());
[ServiceManager.ServiceConstructor] var baseDir = dalamud.AssetDirectory.FullName;
private DataManager(DalamudStartInfo dalamudStartInfo, Dalamud dalamud) try
{ {
this.Language = dalamudStartInfo.Language; Log.Verbose("Starting data load...");
// Set up default values so plugins do not null-reference when data is being loaded. var zoneOpCodeDict = JsonConvert.DeserializeObject<Dictionary<string, ushort>>(
this.ClientOpCodes = this.ServerOpCodes = new ReadOnlyDictionary<string, ushort>(new Dictionary<string, ushort>()); File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json")))!;
this.ServerOpCodes = new ReadOnlyDictionary<string, ushort>(zoneOpCodeDict);
var baseDir = dalamud.AssetDirectory.FullName; Log.Verbose("Loaded {0} ServerOpCodes.", zoneOpCodeDict.Count);
try
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);
using (Timings.Start("Lumina Init"))
{ {
Log.Verbose("Starting data load..."); var luminaOptions = new LuminaOptions
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);
using (Timings.Start("Lumina Init"))
{ {
var luminaOptions = new LuminaOptions LoadMultithreaded = true,
{ CacheFileResources = true,
LoadMultithreaded = true,
CacheFileResources = true,
#if DEBUG #if DEBUG
PanicOnSheetChecksumMismatch = true, PanicOnSheetChecksumMismatch = true,
#else #else
PanicOnSheetChecksumMismatch = false, PanicOnSheetChecksumMismatch = false,
#endif #endif
DefaultExcelLanguage = this.Language.ToLumina(), DefaultExcelLanguage = this.Language.ToLumina(),
}; };
var processModule = Process.GetCurrentProcess().MainModule; var processModule = Process.GetCurrentProcess().MainModule;
if (processModule != null) if (processModule != null)
{
this.GameData = new GameData(Path.Combine(Path.GetDirectoryName(processModule.FileName)!, "sqpack"), luminaOptions);
}
else
{
throw new Exception("Could not main module.");
}
Log.Information("Lumina is ready: {0}", this.GameData.DataPath);
}
this.IsDataReady = true;
this.luminaCancellationTokenSource = new();
var luminaCancellationToken = this.luminaCancellationTokenSource.Token;
this.luminaResourceThread = new(() =>
{
while (!luminaCancellationToken.IsCancellationRequested)
{
if (this.GameData.FileHandleManager.HasPendingFileLoads)
{ {
this.GameData = new GameData(Path.Combine(Path.GetDirectoryName(processModule.FileName)!, "sqpack"), luminaOptions); this.GameData.ProcessFileHandleQueue();
} }
else else
{ {
throw new Exception("Could not main module."); Thread.Sleep(5);
} }
Log.Information("Lumina is ready: {0}", this.GameData.DataPath);
} }
});
this.IsDataReady = true; this.luminaResourceThread.Start();
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.");
}
} }
catch (Exception ex)
/// <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>(); Log.Error(ex, "Could not download data.");
}
/// <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);
return file;
}
/// <summary>
/// Get a <see cref="TexFile"/> containing the HQ icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetHqIcon(uint iconId)
=> this.GetIcon(true, iconId);
/// <summary>
/// Get the passed <see cref="TexFile"/> as a drawable ImGui TextureWrap.
/// </summary>
/// <param name="tex">The Lumina <see cref="TexFile"/>.</param>
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns>
public TextureWrap? GetImGuiTexture(TexFile? tex)
{
return tex == null ? null : Service<InterfaceManager>.Get().LoadImageRaw(tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height, 4);
}
/// <summary>
/// Get the passed texture path as a drawable ImGui TextureWrap.
/// </summary>
/// <param name="path">The internal path to the texture.</param>
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns>
public TextureWrap? GetImGuiTexture(string path)
=> this.GetImGuiTexture(this.GetFile<TexFile>(path));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(uint iconId)
=> this.GetImGuiTexture(this.GetIcon(iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given quality.
/// </summary>
/// <param name="isHq">A value indicating whether the icon should be HQ.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(isHq, iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given language.
/// </summary>
/// <param name="iconLanguage">The requested language.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(iconLanguage, iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given type.
/// </summary>
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(string type, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(type, iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the HQ icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureHqIcon(uint iconId)
=> this.GetImGuiTexture(this.GetHqIcon(iconId));
#endregion
/// <summary>
/// Dispose this DataManager.
/// </summary>
void IDisposable.Dispose()
{
this.luminaCancellationTokenSource.Cancel();
} }
} }
/// <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
{
ClientLanguage.Japanese => "ja/",
ClientLanguage.English => "en/",
ClientLanguage.German => "de/",
ClientLanguage.French => "fr/",
_ => throw new ArgumentOutOfRangeException(nameof(iconLanguage), $"Unknown Language: {iconLanguage}"),
};
return this.GetIcon(type, iconId);
}
/// <summary>
/// Get a <see cref="TexFile"/> containing the icon with the given ID, of the given type.
/// </summary>
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetIcon(string? type, uint iconId)
{
type ??= string.Empty;
if (type.Length > 0 && !type.EndsWith("/"))
type += "/";
var filePath = string.Format(IconFileFormat, iconId / 1000, type, iconId);
var file = this.GetFile<TexFile>(filePath);
if (type == string.Empty || file != default)
return file;
// Couldn't get specific type, try for generic version.
filePath = string.Format(IconFileFormat, iconId / 1000, string.Empty, iconId);
file = this.GetFile<TexFile>(filePath);
return file;
}
/// <summary>
/// Get a <see cref="TexFile"/> containing the HQ icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TexFile"/> containing the icon.</returns>
public TexFile? GetHqIcon(uint iconId)
=> this.GetIcon(true, iconId);
/// <summary>
/// Get the passed <see cref="TexFile"/> as a drawable ImGui TextureWrap.
/// </summary>
/// <param name="tex">The Lumina <see cref="TexFile"/>.</param>
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns>
public TextureWrap? GetImGuiTexture(TexFile? tex)
{
return tex == null ? null : Service<InterfaceManager>.Get().LoadImageRaw(tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height, 4);
}
/// <summary>
/// Get the passed texture path as a drawable ImGui TextureWrap.
/// </summary>
/// <param name="path">The internal path to the texture.</param>
/// <returns>A <see cref="TextureWrap"/> that can be used to draw the texture.</returns>
public TextureWrap? GetImGuiTexture(string path)
=> this.GetImGuiTexture(this.GetFile<TexFile>(path));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(uint iconId)
=> this.GetImGuiTexture(this.GetIcon(iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given quality.
/// </summary>
/// <param name="isHq">A value indicating whether the icon should be HQ.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(isHq, iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given language.
/// </summary>
/// <param name="iconLanguage">The requested language.</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(iconLanguage, iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the icon with the given ID, of the given type.
/// </summary>
/// <param name="type">The type of the icon (e.g. 'hq' to get the HQ variant of an item icon).</param>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureIcon(string type, uint iconId)
=> this.GetImGuiTexture(this.GetIcon(type, iconId));
/// <summary>
/// Get a <see cref="TextureWrap"/> containing the HQ icon with the given ID.
/// </summary>
/// <param name="iconId">The icon ID.</param>
/// <returns>The <see cref="TextureWrap"/> containing the icon.</returns>
public TextureWrap? GetImGuiTextureHqIcon(uint iconId)
=> this.GetImGuiTexture(this.GetHqIcon(iconId));
#endregion
/// <summary>
/// Dispose this DataManager.
/// </summary>
void IDisposable.Dispose()
{
this.luminaCancellationTokenSource.Cancel();
}
} }

View file

@ -18,341 +18,340 @@ using Serilog.Events;
using static Dalamud.NativeFunctions; using static Dalamud.NativeFunctions;
namespace Dalamud namespace Dalamud;
/// <summary>
/// The main entrypoint for the Dalamud system.
/// </summary>
public sealed class EntryPoint
{ {
/// <summary> /// <summary>
/// The main entrypoint for the Dalamud system. /// Log level switch for runtime log level change.
/// </summary> /// </summary>
public sealed class EntryPoint public static readonly LoggingLevelSwitch LogLevelSwitch = new(LogEventLevel.Verbose);
/// <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>
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param>
public delegate void InitDelegate(IntPtr infoPtr, IntPtr mainThreadContinueEvent);
/// <summary>
/// A delegate used from VEH handler on exception which CoreCLR will fast fail by default.
/// </summary>
/// <returns>HGLOBAL for message.</returns>
public delegate IntPtr VehDelegate();
/// <summary>
/// Initialize Dalamud.
/// </summary>
/// <param name="infoPtr">Pointer to a serialized <see cref="DalamudStartInfo"/> data.</param>
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param>
public static void Initialize(IntPtr infoPtr, IntPtr mainThreadContinueEvent)
{ {
/// <summary> var infoStr = Marshal.PtrToStringUTF8(infoPtr)!;
/// Log level switch for runtime log level change. var info = JsonConvert.DeserializeObject<DalamudStartInfo>(infoStr)!;
/// </summary>
public static readonly LoggingLevelSwitch LogLevelSwitch = new(LogEventLevel.Verbose);
/// <summary> if ((info.BootWaitMessageBox & 4) != 0)
/// A delegate used during initialization of the CLR from Dalamud.Boot. MessageBoxW(IntPtr.Zero, "Press OK to continue (BeforeDalamudConstruct)", "Dalamud Boot", MessageBoxType.Ok);
/// </summary>
/// <param name="infoPtr">Pointer to a serialized <see cref="DalamudStartInfo"/> data.</param>
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param>
public delegate void InitDelegate(IntPtr infoPtr, IntPtr mainThreadContinueEvent);
/// <summary> new Thread(() => RunThread(info, mainThreadContinueEvent)).Start();
/// A delegate used from VEH handler on exception which CoreCLR will fast fail by default. }
/// </summary>
/// <returns>HGLOBAL for message.</returns>
public delegate IntPtr VehDelegate();
/// <summary> /// <summary>
/// Initialize Dalamud. /// Returns stack trace.
/// </summary> /// </summary>
/// <param name="infoPtr">Pointer to a serialized <see cref="DalamudStartInfo"/> data.</param> /// <returns>HGlobal to wchar_t* stack trace c-string.</returns>
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param> public static IntPtr VehCallback()
public static void Initialize(IntPtr infoPtr, IntPtr mainThreadContinueEvent) {
try
{ {
var infoStr = Marshal.PtrToStringUTF8(infoPtr)!; return Marshal.StringToHGlobalUni(Environment.StackTrace);
var info = JsonConvert.DeserializeObject<DalamudStartInfo>(infoStr)!;
if ((info.BootWaitMessageBox & 4) != 0)
MessageBoxW(IntPtr.Zero, "Press OK to continue (BeforeDalamudConstruct)", "Dalamud Boot", MessageBoxType.Ok);
new Thread(() => RunThread(info, mainThreadContinueEvent)).Start();
} }
catch (Exception e)
/// <summary>
/// Returns stack trace.
/// </summary>
/// <returns>HGlobal to wchar_t* stack trace c-string.</returns>
public static IntPtr VehCallback()
{ {
try return Marshal.StringToHGlobalUni("Fail: " + e);
{
return Marshal.StringToHGlobalUni(Environment.StackTrace);
}
catch (Exception e)
{
return Marshal.StringToHGlobalUni("Fail: " + e);
}
} }
}
/// <summary> /// <summary>
/// Sets up logging. /// Sets up logging.
/// </summary> /// </summary>
/// <param name="baseDirectory">Base directory.</param> /// <param name="baseDirectory">Base directory.</param>
/// <param name="logConsole">Whether to log to console.</param> /// <param name="logConsole">Whether to log to console.</param>
/// <param name="logSynchronously">Log synchronously.</param> /// <param name="logSynchronously">Log synchronously.</param>
internal static void InitLogging(string baseDirectory, bool logConsole, bool logSynchronously) internal static void InitLogging(string baseDirectory, bool logConsole, bool logSynchronously)
{ {
#if DEBUG #if DEBUG
var logPath = Path.Combine(baseDirectory, "dalamud.log"); var logPath = Path.Combine(baseDirectory, "dalamud.log");
var oldPath = Path.Combine(baseDirectory, "dalamud.old.log"); var oldPath = Path.Combine(baseDirectory, "dalamud.old.log");
var oldPathOld = Path.Combine(baseDirectory, "dalamud.log.old"); var oldPathOld = Path.Combine(baseDirectory, "dalamud.log.old");
#else #else
var logPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log"); var logPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log");
var oldPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.old.log"); var oldPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.old.log");
var oldPathOld = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log.old"); var oldPathOld = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log.old");
#endif #endif
Log.CloseAndFlush(); Log.CloseAndFlush();
var oldFileOld = new FileInfo(oldPathOld); var oldFileOld = new FileInfo(oldPathOld);
if (oldFileOld.Exists) if (oldFileOld.Exists)
{
var oldFile = new FileInfo(oldPath);
if (oldFile.Exists)
oldFileOld.Delete();
else
oldFileOld.MoveTo(oldPath);
}
CullLogFile(logPath, 1 * 1024 * 1024, oldPath, 10 * 1024 * 1024);
var config = new LoggerConfiguration()
.WriteTo.Sink(SerilogEventSink.Instance)
.MinimumLevel.ControlledBy(LogLevelSwitch);
if (logSynchronously)
{
config = config.WriteTo.File(logPath, fileSizeLimitBytes: null);
}
else
{
config = config.WriteTo.Async(a => a.File(
logPath,
fileSizeLimitBytes: null,
buffered: false,
flushToDiskInterval: TimeSpan.FromSeconds(1)));
}
if (logConsole)
config = config.WriteTo.Console();
Log.Logger = config.CreateLogger();
}
/// <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>
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param>
private static void RunThread(DalamudStartInfo info, IntPtr mainThreadContinueEvent)
{ {
// Setup logger
InitLogging(info.WorkingDirectory!, info.BootShowConsole, true);
SerilogEventSink.Instance.LogLine += SerilogOnLogLine;
// Load configuration first to get some early persistent state, like log level
var configuration = DalamudConfiguration.Load(info.ConfigurationPath!);
// Set the appropriate logging level from the configuration
#if !DEBUG
if (!configuration.LogSynchronously)
InitLogging(info.WorkingDirectory!, info.BootShowConsole, configuration.LogSynchronously);
LogLevelSwitch.MinimumLevel = configuration.LogLevel;
#endif
// Log any unhandled exception.
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
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, configuration, mainThreadContinueEvent);
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash}", Util.GetGitHash(), Util.GetGitHashClientStructs());
dalamud.WaitForUnload();
ServiceManager.UnloadAllServices();
}
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();
SerilogEventSink.Instance.LogLine -= SerilogOnLogLine;
}
}
private static void SerilogOnLogLine(object? sender, (string Line, LogEvent LogEvent) ev)
{
if (ev.LogEvent.Exception == null)
return;
// Don't pass verbose/debug/info exceptions to the troubleshooter, as the developer is probably doing
// something intentionally (or this is known).
if (ev.LogEvent.Level < LogEventLevel.Warning)
return;
Troubleshooting.LogException(ev.LogEvent.Exception, ev.Line);
}
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.");
}
}
/// <summary>
/// Trim existing log file to a specified length, and optionally move the excess data to another file.
/// </summary>
/// <param name="logPath">Target log file to trim.</param>
/// <param name="logMaxSize">Maximum size of target log file.</param>
/// <param name="oldPath">.old file to move excess data to.</param>
/// <param name="oldMaxSize">Maximum size of .old file.</param>
private static void CullLogFile(string logPath, int logMaxSize, string oldPath, int oldMaxSize)
{
var logFile = new FileInfo(logPath);
var oldFile = new FileInfo(oldPath); var oldFile = new FileInfo(oldPath);
var targetFiles = new[] if (oldFile.Exists)
{ oldFileOld.Delete();
(logFile, logMaxSize), else
(oldFile, oldMaxSize), oldFileOld.MoveTo(oldPath);
};
var buffer = new byte[4096];
try
{
if (!logFile.Exists)
logFile.Create().Close();
// 1. Move excess data from logFile to oldFile
if (logFile.Length > logMaxSize)
{
using var reader = logFile.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var writer = oldFile.Open(FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
var amountToMove = (int)Math.Min(logFile.Length - logMaxSize, oldMaxSize);
reader.Seek(-(logMaxSize + amountToMove), SeekOrigin.End);
for (var i = 0; i < amountToMove; i += buffer.Length)
writer.Write(buffer, 0, reader.Read(buffer, 0, Math.Min(buffer.Length, amountToMove - i)));
}
// 2. Cull each of .log and .old files
foreach (var (file, maxSize) in targetFiles)
{
if (!file.Exists || file.Length <= maxSize)
continue;
using var reader = file.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var writer = file.Open(FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
reader.Seek(file.Length - maxSize, SeekOrigin.Begin);
for (int read; (read = reader.Read(buffer, 0, buffer.Length)) > 0;)
writer.Write(buffer, 0, read);
writer.SetLength(maxSize);
}
}
catch (Exception ex)
{
if (ex is IOException)
{
foreach (var (file, _) in targetFiles)
{
try
{
if (file.Exists)
file.Delete();
}
catch (Exception ex2)
{
Log.Error(ex2, "Failed to delete {file}", file.FullName);
}
}
}
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);
*/
}
} }
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs args) CullLogFile(logPath, 1 * 1024 * 1024, oldPath, 10 * 1024 * 1024);
var config = new LoggerConfiguration()
.WriteTo.Sink(SerilogEventSink.Instance)
.MinimumLevel.ControlledBy(LogLevelSwitch);
if (logSynchronously)
{ {
switch (args.ExceptionObject) config = config.WriteTo.File(logPath, fileSizeLimitBytes: null);
{ }
case Exception ex: else
Log.Fatal(ex, "Unhandled exception on AppDomain"); {
Troubleshooting.LogException(ex, "DalamudUnhandled"); config = config.WriteTo.Async(a => a.File(
logPath,
var info = "Further information could not be obtained"; fileSizeLimitBytes: null,
if (ex.TargetSite != null && ex.TargetSite.DeclaringType != null) buffered: false,
{ flushToDiskInterval: TimeSpan.FromSeconds(1)));
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();
}
Log.CloseAndFlush();
Environment.Exit(-1);
break;
default:
Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject);
Log.CloseAndFlush();
Environment.Exit(-1);
break;
}
} }
private static void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs args) if (logConsole)
config = config.WriteTo.Console();
Log.Logger = config.CreateLogger();
}
/// <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>
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param>
private static void RunThread(DalamudStartInfo info, IntPtr mainThreadContinueEvent)
{
// Setup logger
InitLogging(info.WorkingDirectory!, info.BootShowConsole, true);
SerilogEventSink.Instance.LogLine += SerilogOnLogLine;
// Load configuration first to get some early persistent state, like log level
var configuration = DalamudConfiguration.Load(info.ConfigurationPath!);
// Set the appropriate logging level from the configuration
#if !DEBUG
if (!configuration.LogSynchronously)
InitLogging(info.WorkingDirectory!, info.BootShowConsole, configuration.LogSynchronously);
LogLevelSwitch.MinimumLevel = configuration.LogLevel;
#endif
// Log any unhandled exception.
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
try
{ {
if (!args.Observed) if (info.DelayInitializeMs > 0)
Log.Error(args.Exception, "Unobserved exception in Task."); {
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, configuration, mainThreadContinueEvent);
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash}", Util.GetGitHash(), Util.GetGitHashClientStructs());
dalamud.WaitForUnload();
ServiceManager.UnloadAllServices();
}
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();
SerilogEventSink.Instance.LogLine -= SerilogOnLogLine;
} }
} }
private static void SerilogOnLogLine(object? sender, (string Line, LogEvent LogEvent) ev)
{
if (ev.LogEvent.Exception == null)
return;
// Don't pass verbose/debug/info exceptions to the troubleshooter, as the developer is probably doing
// something intentionally (or this is known).
if (ev.LogEvent.Level < LogEventLevel.Warning)
return;
Troubleshooting.LogException(ev.LogEvent.Exception, ev.Line);
}
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.");
}
}
/// <summary>
/// Trim existing log file to a specified length, and optionally move the excess data to another file.
/// </summary>
/// <param name="logPath">Target log file to trim.</param>
/// <param name="logMaxSize">Maximum size of target log file.</param>
/// <param name="oldPath">.old file to move excess data to.</param>
/// <param name="oldMaxSize">Maximum size of .old file.</param>
private static void CullLogFile(string logPath, int logMaxSize, string oldPath, int oldMaxSize)
{
var logFile = new FileInfo(logPath);
var oldFile = new FileInfo(oldPath);
var targetFiles = new[]
{
(logFile, logMaxSize),
(oldFile, oldMaxSize),
};
var buffer = new byte[4096];
try
{
if (!logFile.Exists)
logFile.Create().Close();
// 1. Move excess data from logFile to oldFile
if (logFile.Length > logMaxSize)
{
using var reader = logFile.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var writer = oldFile.Open(FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
var amountToMove = (int)Math.Min(logFile.Length - logMaxSize, oldMaxSize);
reader.Seek(-(logMaxSize + amountToMove), SeekOrigin.End);
for (var i = 0; i < amountToMove; i += buffer.Length)
writer.Write(buffer, 0, reader.Read(buffer, 0, Math.Min(buffer.Length, amountToMove - i)));
}
// 2. Cull each of .log and .old files
foreach (var (file, maxSize) in targetFiles)
{
if (!file.Exists || file.Length <= maxSize)
continue;
using var reader = file.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var writer = file.Open(FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
reader.Seek(file.Length - maxSize, SeekOrigin.Begin);
for (int read; (read = reader.Read(buffer, 0, buffer.Length)) > 0;)
writer.Write(buffer, 0, read);
writer.SetLength(maxSize);
}
}
catch (Exception ex)
{
if (ex is IOException)
{
foreach (var (file, _) in targetFiles)
{
try
{
if (file.Exists)
file.Delete();
}
catch (Exception ex2)
{
Log.Error(ex2, "Failed to delete {file}", file.FullName);
}
}
}
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);
*/
}
}
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();
}
Log.CloseAndFlush();
Environment.Exit(-1);
break;
default:
Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject);
Log.CloseAndFlush();
Environment.Exit(-1);
break;
}
}
private static void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs args)
{
if (!args.Observed)
Log.Error(args.Exception, "Unobserved exception in Task.");
}
} }

View file

@ -5,114 +5,113 @@ using System.Runtime.InteropServices;
using JetBrains.Annotations; using JetBrains.Annotations;
namespace Dalamud.Game namespace Dalamud.Game;
/// <summary>
/// Base memory address resolver.
/// </summary>
public abstract class BaseAddressResolver
{ {
/// <summary> /// <summary>
/// Base memory address resolver. /// Gets a list of memory addresses that were found, to list in /xldata.
/// </summary> /// </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 appropriate method based on the process architecture,
/// using the default SigScanner.
///
/// For plugins. Not intended to be called from Dalamud Service{T} constructors.
/// </summary>
[UsedImplicitly]
public void Setup() => this.Setup(Service<SigScanner>.Get());
/// <summary>
/// Setup the resolver, calling the appropriate method based on the process architecture.
/// </summary>
/// <param name="scanner">The SigScanner instance.</param>
public void Setup(SigScanner scanner)
{ {
/// <summary> // Because C# don't allow to call virtual function while in ctor
/// Gets a list of memory addresses that were found, to list in /xldata. // we have to do this shit :\
/// </summary>
public static Dictionary<string, List<(string ClassName, IntPtr Address)>> DebugScannedValues { get; } = new();
/// <summary> if (this.IsResolved)
/// 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 appropriate method based on the process architecture,
/// using the default SigScanner.
///
/// For plugins. Not intended to be called from Dalamud Service{T} constructors.
/// </summary>
[UsedImplicitly]
public void Setup() => this.Setup(Service<SigScanner>.Get());
/// <summary>
/// Setup the resolver, calling the appropriate 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 return;
// 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;
var list = new List<(string, IntPtr)>();
lock (DebugScannedValues)
DebugScannedValues[className] = list;
foreach (var property in this.GetType().GetProperties().Where(x => x.PropertyType == typeof(IntPtr)))
{
list.Add((property.Name, (IntPtr)property.GetValue(this)));
}
this.IsResolved = true;
} }
/// <summary> if (scanner.Is32BitProcess)
/// 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 this.Setup32Bit(scanner);
var vtable = Marshal.ReadIntPtr(address, vtableOffset); }
else
// Get an address to the function {
var functionAddress = Marshal.ReadIntPtr(vtable, IntPtr.Size * count); this.Setup64Bit(scanner);
return Marshal.GetDelegateForFunctionPointer<T>(functionAddress);
} }
/// <summary> this.SetupInternal(scanner);
/// Setup the resolver by finding any necessary memory addresses.
/// </summary> var className = this.GetType().Name;
/// <param name="scanner">The SigScanner instance.</param> var list = new List<(string, IntPtr)>();
protected virtual void Setup32Bit(SigScanner scanner) lock (DebugScannedValues)
DebugScannedValues[className] = list;
foreach (var property in this.GetType().GetProperties().Where(x => x.PropertyType == typeof(IntPtr)))
{ {
throw new NotSupportedException("32 bit version is not supported."); list.Add((property.Name, (IntPtr)property.GetValue(this)));
} }
/// <summary> this.IsResolved = true;
/// 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> /// <summary>
/// Setup the resolver by finding any necessary memory addresses. /// Fetch vfunc N from a pointer to the vtable and return a delegate function pointer.
/// </summary> /// </summary>
/// <param name="scanner">The SigScanner instance.</param> /// <typeparam name="T">The delegate to marshal the function pointer to.</typeparam>
protected virtual void SetupInternal(SigScanner scanner) /// <param name="address">The address of the virtual table.</param>
{ /// <param name="vtableOffset">The offset from address to the vtable pointer.</param>
// Do nothing /// <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);
// 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

@ -20,314 +20,313 @@ using Dalamud.Plugin.Internal;
using Dalamud.Utility; using Dalamud.Utility;
using Serilog; using Serilog;
namespace Dalamud.Game namespace Dalamud.Game;
/// <summary>
/// Chat events and public helper functions.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public class ChatHandlers : IServiceType
{ {
/// <summary> // private static readonly Dictionary<string, string> UnicodeToDiscordEmojiDict = new()
/// Chat events and public helper functions. // {
/// </summary> // { "", "<:ffxive071:585847382210642069>" },
[PluginInterface] // { "", "<:ffxive083:585848592699490329>" },
[InterfaceVersion("1.0")] // };
[ServiceManager.BlockingEarlyLoadedService]
public class ChatHandlers : IServiceType // 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|[Gg]il for free|[Gg]il [Cc]heap|5GOLD|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オ|[Oo][Ff][Ff] [Cc]ode( *)[:;]|offers Fantasia",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
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|[Gg]il for free|[Gg]il [Cc]heap|5GOLD|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オ|[Oo][Ff][Ff] [Cc]ode( *)[:;]|offers Fantasia",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
private readonly Dictionary<ClientLanguage, Regex[]> retainerSaleRegexes = new()
{ {
ClientLanguage.Japanese,
new Regex[]
{ {
ClientLanguage.Japanese, new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)×(?<count>[\d,.]+)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled),
new Regex[] new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled),
{
new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)×(?<count>[\d,.]+)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled),
new Regex(@"^(?:.+)マーケットに(?<origValue>[\d,.]+)ギルで出品した(?<item>.*)が売れ、(?<value>[\d,.]+)ギルを入手しました。$", RegexOptions.Compiled),
}
},
{
ClientLanguage.English,
new Regex[]
{
new Regex(@"^(?<item>.+) you put up for sale in the (?:.+) markets (?:have|has) sold for (?<value>[\d,.]+) gil \(after fees\)\.$", RegexOptions.Compiled),
}
},
{
ClientLanguage.German,
new Regex[]
{
new Regex(@"^Dein Gehilfe hat (?<item>.+) auf dem Markt von (?:.+) für (?<value>[\d,.]+) Gil verkauft\.$", RegexOptions.Compiled),
new Regex(@"^Dein Gehilfe hat (?<item>.+) auf dem Markt von (?:.+) verkauft und (?<value>[\d,.]+) Gil erhalten\.$", RegexOptions.Compiled),
}
},
{
ClientLanguage.French,
new Regex[]
{
new Regex(@"^Un servant a vendu (?<item>.+) pour (?<value>[\d,.]+) gil à (?:.+)\.$", RegexOptions.Compiled),
}
},
};
private readonly Regex urlRegex = new(@"(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?", RegexOptions.Compiled);
private readonly DalamudLinkPayload openInstallerWindowLink;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private bool hasSeenLoadingMsg;
private bool hasAutoUpdatedPlugins;
[ServiceManager.ServiceConstructor]
private ChatHandlers(ChatGui chatGui)
{
chatGui.CheckMessageHandled += this.OnCheckMessageHandled;
chatGui.ChatMessage += this.OnChatMessage;
this.openInstallerWindowLink = chatGui.AddChatLinkHandler("Dalamud", 1001, (i, m) =>
{
Service<DalamudInterface>.GetNullable()?.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 textVal = message.TextValue;
if (!this.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 (this.configuration.BadWords != null && {
this.configuration.BadWords.Any(x => !string.IsNullOrEmpty(x) && textVal.Contains(x))) ClientLanguage.English,
new Regex[]
{ {
// This seems to be in the user block list - let's not show it new Regex(@"^(?<item>.+) you put up for sale in the (?:.+) markets (?:have|has) sold for (?<value>[\d,.]+) gil \(after fees\)\.$", RegexOptions.Compiled),
Log.Debug("Blocklist triggered"); }
},
{
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;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private bool hasSeenLoadingMsg;
private bool hasAutoUpdatedPlugins;
[ServiceManager.ServiceConstructor]
private ChatHandlers(ChatGui chatGui)
{
chatGui.CheckMessageHandled += this.OnCheckMessageHandled;
chatGui.ChatMessage += this.OnChatMessage;
this.openInstallerWindowLink = chatGui.AddChatLinkHandler("Dalamud", 1001, (i, m) =>
{
Service<DalamudInterface>.GetNullable()?.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 textVal = message.TextValue;
if (!this.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; isHandled = true;
return; return;
} }
} }
private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled) if (this.configuration.BadWords != null &&
this.configuration.BadWords.Any(x => !string.IsNullOrEmpty(x) && textVal.Contains(x)))
{ {
var startInfo = Service<DalamudStartInfo>.Get(); // This seems to be in the user block list - let's not show it
var clientState = Service<ClientState.ClientState>.GetNullable(); Log.Debug("Blocklist triggered");
if (clientState == null) isHandled = true;
return; return;
}
}
if (type == XivChatType.Notice && !this.hasSeenLoadingMsg) private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled)
this.PrintWelcomeMessage(); {
var startInfo = Service<DalamudStartInfo>.Get();
var clientState = Service<ClientState.ClientState>.GetNullable();
if (clientState == null)
return;
// For injections while logged in if (type == XivChatType.Notice && !this.hasSeenLoadingMsg)
if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg) this.PrintWelcomeMessage();
this.PrintWelcomeMessage();
if (!this.hasAutoUpdatedPlugins) // For injections while logged in
this.AutoUpdatePlugins(); if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg)
this.PrintWelcomeMessage();
if (!this.hasAutoUpdatedPlugins)
this.AutoUpdatePlugins();
#if !DEBUG && false #if !DEBUG && false
if (!this.hasSeenLoadingMsg) if (!this.hasSeenLoadingMsg)
return; return;
#endif #endif
if (type == XivChatType.RetainerSale) if (type == XivChatType.RetainerSale)
{
foreach (var regex in this.retainerSaleRegexes[startInfo.Language])
{ {
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); Log.Error("itemLink was null. Msg: {0}", BitConverter.ToString(message.Encode()));
// we no longer really need to do/validate the item matching since we read the id from the byte array
// but we'd be checking the main match anyway
var itemInfo = matchInfo.Groups["item"];
if (!itemInfo.Success)
continue;
var itemLink = message.Payloads.FirstOrDefault(x => x.Type == PayloadType.Item) as ItemPayload;
if (itemLink == default)
{
Log.Error("itemLink was null. Msg: {0}", BitConverter.ToString(message.Encode()));
break;
}
Log.Debug($"Probable retainer sale: {message}, decoded item {itemLink.Item.RowId}, HQ {itemLink.IsHQ}");
var valueInfo = matchInfo.Groups["value"];
// not sure if using a culture here would work correctly, so just strip symbols instead
if (!valueInfo.Success || !int.TryParse(valueInfo.Value.Replace(",", string.Empty).Replace(".", string.Empty), out var itemValue))
continue;
// Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemLink.Item.RowId, itemValue, itemLink.IsHQ));
break; break;
} }
Log.Debug($"Probable retainer sale: {message}, decoded item {itemLink.Item.RowId}, HQ {itemLink.IsHQ}");
var valueInfo = matchInfo.Groups["value"];
// not sure if using a culture here would work correctly, so just strip symbols instead
if (!valueInfo.Success || !int.TryParse(valueInfo.Value.Replace(",", string.Empty).Replace(".", string.Empty), out var itemValue))
continue;
// Task.Run(() => this.dalamud.BotManager.ProcessRetainerSale(itemLink.Item.RowId, itemValue, itemLink.IsHQ));
break;
} }
var messageCopy = message;
var senderCopy = sender;
var linkMatch = this.urlRegex.Match(message.TextValue);
if (linkMatch.Value.Length > 0)
this.LastLink = linkMatch.Value;
} }
private void PrintWelcomeMessage() var 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>.GetNullable();
var pluginManager = Service<PluginManager>.GetNullable();
var dalamudInterface = Service<DalamudInterface>.GetNullable();
if (chatGui == null || pluginManager == null || dalamudInterface == null)
return;
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(x => x.IsLoaded)));
if (this.configuration.PrintPluginsWelcomeMsg)
{ {
var chatGui = Service<ChatGui>.GetNullable(); foreach (var plugin in pluginManager.InstalledPlugins.OrderBy(plugin => plugin.Name).Where(x => x.IsLoaded))
var pluginManager = Service<PluginManager>.GetNullable();
var dalamudInterface = Service<DalamudInterface>.GetNullable();
if (chatGui == null || pluginManager == null || dalamudInterface == null)
return;
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(x => x.IsLoaded)));
if (this.configuration.PrintPluginsWelcomeMsg)
{ {
foreach (var plugin in pluginManager.InstalledPlugins.OrderBy(plugin => plugin.Name).Where(x => x.IsLoaded)) 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(this.configuration.LastVersion) || !assemblyVersion.StartsWith(this.configuration.LastVersion))
{
chatGui.PrintChat(new XivChatEntry
{
Message = Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully! Please check the discord for a full changelog."),
Type = XivChatType.Notice,
});
if (string.IsNullOrEmpty(this.configuration.LastChangelogMajorMinor) || (!ChangelogWindow.WarrantsChangelogForMajorMinor.StartsWith(this.configuration.LastChangelogMajorMinor) && assemblyVersion.StartsWith(ChangelogWindow.WarrantsChangelogForMajorMinor)))
{
dalamudInterface.OpenChangelogWindow();
this.configuration.LastChangelogMajorMinor = ChangelogWindow.WarrantsChangelogForMajorMinor;
}
this.configuration.LastVersion = assemblyVersion;
this.configuration.Save();
}
this.hasSeenLoadingMsg = true;
} }
private void AutoUpdatePlugins() if (string.IsNullOrEmpty(this.configuration.LastVersion) || !assemblyVersion.StartsWith(this.configuration.LastVersion))
{ {
var chatGui = Service<ChatGui>.GetNullable(); chatGui.PrintChat(new XivChatEntry
var pluginManager = Service<PluginManager>.GetNullable();
var notifications = Service<NotificationManager>.GetNullable();
if (chatGui == null || pluginManager == null || notifications == null)
return;
if (!pluginManager.ReposReady || pluginManager.InstalledPlugins.Count == 0 || pluginManager.AvailablePlugins.Count == 0)
{ {
// Plugins aren't ready yet. Message = Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully! Please check the discord for a full changelog."),
return; Type = XivChatType.Notice,
}
this.hasAutoUpdatedPlugins = true;
Task.Run(() => pluginManager.UpdatePluginsAsync(true, !this.configuration.AutoUpdatePlugins)).ContinueWith(task =>
{
if (task.IsFaulted)
{
Log.Error(task.Exception, Loc.Localize("DalamudPluginUpdateCheckFail", "Could not check for plugin updates."));
return;
}
var updatedPlugins = task.Result;
if (updatedPlugins.Any())
{
if (this.configuration.AutoUpdatePlugins)
{
Service<PluginManager>.Get().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
{
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),
this.openInstallerWindowLink,
new TextPayload(Loc.Localize("DalamudInstallerHelp", "Open the plugin installer")),
RawPayload.LinkTerminator,
new UIForegroundPayload(0),
new TextPayload("]"),
}),
Type = XivChatType.Urgent,
});
}
}
}); });
if (string.IsNullOrEmpty(this.configuration.LastChangelogMajorMinor) || (!ChangelogWindow.WarrantsChangelogForMajorMinor.StartsWith(this.configuration.LastChangelogMajorMinor) && assemblyVersion.StartsWith(ChangelogWindow.WarrantsChangelogForMajorMinor)))
{
dalamudInterface.OpenChangelogWindow();
this.configuration.LastChangelogMajorMinor = ChangelogWindow.WarrantsChangelogForMajorMinor;
}
this.configuration.LastVersion = assemblyVersion;
this.configuration.Save();
} }
this.hasSeenLoadingMsg = true;
}
private void AutoUpdatePlugins()
{
var chatGui = Service<ChatGui>.GetNullable();
var pluginManager = Service<PluginManager>.GetNullable();
var notifications = Service<NotificationManager>.GetNullable();
if (chatGui == null || pluginManager == null || notifications == null)
return;
if (!pluginManager.ReposReady || pluginManager.InstalledPlugins.Count == 0 || pluginManager.AvailablePlugins.Count == 0)
{
// Plugins aren't ready yet.
return;
}
this.hasAutoUpdatedPlugins = true;
Task.Run(() => pluginManager.UpdatePluginsAsync(true, !this.configuration.AutoUpdatePlugins)).ContinueWith(task =>
{
if (task.IsFaulted)
{
Log.Error(task.Exception, Loc.Localize("DalamudPluginUpdateCheckFail", "Could not check for plugin updates."));
return;
}
var updatedPlugins = task.Result;
if (updatedPlugins.Any())
{
if (this.configuration.AutoUpdatePlugins)
{
Service<PluginManager>.Get().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
{
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),
this.openInstallerWindowLink,
new TextPayload(Loc.Localize("DalamudInstallerHelp", "Open the plugin installer")),
RawPayload.LinkTerminator,
new UIForegroundPayload(0),
new TextPayload("]"),
}),
Type = XivChatType.Urgent,
});
}
}
});
} }
} }

View file

@ -1,72 +1,71 @@
using Dalamud.Game.ClientState.Resolvers; using Dalamud.Game.ClientState.Resolvers;
using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Game.UI;
namespace Dalamud.Game.ClientState.Aetherytes namespace Dalamud.Game.ClientState.Aetherytes;
/// <summary>
/// This class represents an entry in the Aetheryte list.
/// </summary>
public sealed class AetheryteEntry
{ {
private readonly TeleportInfo data;
/// <summary> /// <summary>
/// This class represents an entry in the Aetheryte list. /// Initializes a new instance of the <see cref="AetheryteEntry"/> class.
/// </summary> /// </summary>
public sealed class AetheryteEntry /// <param name="data">Data read from the Aetheryte List.</param>
internal AetheryteEntry(TeleportInfo data)
{ {
private readonly TeleportInfo data; this.data = data;
/// <summary>
/// Initializes a new instance of the <see cref="AetheryteEntry"/> class.
/// </summary>
/// <param name="data">Data read from the Aetheryte List.</param>
internal AetheryteEntry(TeleportInfo data)
{
this.data = data;
}
/// <summary>
/// Gets the Aetheryte ID.
/// </summary>
public uint AetheryteId => this.data.AetheryteId;
/// <summary>
/// Gets the Territory ID.
/// </summary>
public uint TerritoryId => this.data.TerritoryId;
/// <summary>
/// Gets the SubIndex used when there can be multiple Aetherytes with the same ID (Private/Shared Estates etc.).
/// </summary>
public byte SubIndex => this.data.SubIndex;
/// <summary>
/// Gets the Ward. Zero if not a Shared Estate.
/// </summary>
public byte Ward => this.data.Ward;
/// <summary>
/// Gets the Plot. Zero if not a Shared Estate.
/// </summary>
public byte Plot => this.data.Plot;
/// <summary>
/// Gets the Cost in Gil to Teleport to this location.
/// </summary>
public uint GilCost => this.data.GilCost;
/// <summary>
/// Gets a value indicating whether the LocalPlayer has set this Aetheryte as Favorite or not.
/// </summary>
public bool IsFavourite => this.data.IsFavourite != 0;
/// <summary>
/// Gets a value indicating whether this Aetheryte is a Shared Estate or not.
/// </summary>
public bool IsSharedHouse => this.data.IsSharedHouse;
/// <summary>
/// Gets a value indicating whether this Aetheryte is an Appartment or not.
/// </summary>
public bool IsAppartment => this.data.IsAppartment;
/// <summary>
/// Gets the Aetheryte data related to this aetheryte.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.Aetheryte> AetheryteData => new(this.AetheryteId);
} }
/// <summary>
/// Gets the Aetheryte ID.
/// </summary>
public uint AetheryteId => this.data.AetheryteId;
/// <summary>
/// Gets the Territory ID.
/// </summary>
public uint TerritoryId => this.data.TerritoryId;
/// <summary>
/// Gets the SubIndex used when there can be multiple Aetherytes with the same ID (Private/Shared Estates etc.).
/// </summary>
public byte SubIndex => this.data.SubIndex;
/// <summary>
/// Gets the Ward. Zero if not a Shared Estate.
/// </summary>
public byte Ward => this.data.Ward;
/// <summary>
/// Gets the Plot. Zero if not a Shared Estate.
/// </summary>
public byte Plot => this.data.Plot;
/// <summary>
/// Gets the Cost in Gil to Teleport to this location.
/// </summary>
public uint GilCost => this.data.GilCost;
/// <summary>
/// Gets a value indicating whether the LocalPlayer has set this Aetheryte as Favorite or not.
/// </summary>
public bool IsFavourite => this.data.IsFavourite != 0;
/// <summary>
/// Gets a value indicating whether this Aetheryte is a Shared Estate or not.
/// </summary>
public bool IsSharedHouse => this.data.IsSharedHouse;
/// <summary>
/// Gets a value indicating whether this Aetheryte is an Appartment or not.
/// </summary>
public bool IsAppartment => this.data.IsAppartment;
/// <summary>
/// Gets the Aetheryte data related to this aetheryte.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.Aetheryte> AetheryteData => new(this.AetheryteId);
} }

View file

@ -7,105 +7,104 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.Aetherytes namespace Dalamud.Game.ClientState.Aetherytes;
/// <summary>
/// This collection represents the list of available Aetherytes in the Teleport window.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed partial class AetheryteList : IServiceType
{ {
/// <summary> [ServiceManager.ServiceDependency]
/// This collection represents the list of available Aetherytes in the Teleport window. private readonly ClientState clientState = Service<ClientState>.Get();
/// </summary> private readonly ClientStateAddressResolver address;
[PluginInterface] private readonly UpdateAetheryteListDelegate updateAetheryteListFunc;
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService] [ServiceManager.ServiceConstructor]
public sealed partial class AetheryteList : IServiceType private AetheryteList()
{ {
[ServiceManager.ServiceDependency] this.address = this.clientState.AddressResolver;
private readonly ClientState clientState = Service<ClientState>.Get(); this.updateAetheryteListFunc = Marshal.GetDelegateForFunctionPointer<UpdateAetheryteListDelegate>(this.address.UpdateAetheryteList);
private readonly ClientStateAddressResolver address;
private readonly UpdateAetheryteListDelegate updateAetheryteListFunc;
[ServiceManager.ServiceConstructor] Log.Verbose($"Teleport address 0x{this.address.Telepo.ToInt64():X}");
private AetheryteList() }
private delegate void UpdateAetheryteListDelegate(IntPtr telepo, byte arg1);
/// <summary>
/// Gets the amount of Aetherytes the local player has unlocked.
/// </summary>
public unsafe int Length
{
get
{ {
this.address = this.clientState.AddressResolver;
this.updateAetheryteListFunc = Marshal.GetDelegateForFunctionPointer<UpdateAetheryteListDelegate>(this.address.UpdateAetheryteList);
Log.Verbose($"Teleport address 0x{this.address.Telepo.ToInt64():X}");
}
private delegate void UpdateAetheryteListDelegate(IntPtr telepo, byte arg1);
/// <summary>
/// Gets the amount of Aetherytes the local player has unlocked.
/// </summary>
public unsafe int Length
{
get
{
if (this.clientState.LocalPlayer == null)
return 0;
this.Update();
if (TelepoStruct->TeleportList.First == TelepoStruct->TeleportList.Last)
return 0;
return (int)TelepoStruct->TeleportList.Size();
}
}
private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Telepo* TelepoStruct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Telepo*)this.address.Telepo;
/// <summary>
/// Gets a Aetheryte Entry at the specified index.
/// </summary>
/// <param name="index">Index.</param>
/// <returns>A <see cref="AetheryteEntry"/> at the specified index.</returns>
public unsafe AetheryteEntry? this[int index]
{
get
{
if (index < 0 || index >= this.Length)
{
return null;
}
if (this.clientState.LocalPlayer == null)
return null;
return new AetheryteEntry(TelepoStruct->TeleportList.Get((ulong)index));
}
}
private void Update()
{
// this is very very important as otherwise it crashes
if (this.clientState.LocalPlayer == null) if (this.clientState.LocalPlayer == null)
return; return 0;
this.updateAetheryteListFunc(this.address.Telepo, 0); this.Update();
if (TelepoStruct->TeleportList.First == TelepoStruct->TeleportList.Last)
return 0;
return (int)TelepoStruct->TeleportList.Size();
} }
} }
private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Telepo* TelepoStruct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Telepo*)this.address.Telepo;
/// <summary> /// <summary>
/// This collection represents the list of available Aetherytes in the Teleport window. /// Gets a Aetheryte Entry at the specified index.
/// </summary> /// </summary>
public sealed partial class AetheryteList : IReadOnlyCollection<AetheryteEntry> /// <param name="index">Index.</param>
/// <returns>A <see cref="AetheryteEntry"/> at the specified index.</returns>
public unsafe AetheryteEntry? this[int index]
{ {
/// <inheritdoc/> get
public int Count => this.Length;
/// <inheritdoc/>
public IEnumerator<AetheryteEntry> GetEnumerator()
{ {
for (var i = 0; i < this.Length; i++) if (index < 0 || index >= this.Length)
{ {
yield return this[i]; return null;
} }
}
/// <inheritdoc/> if (this.clientState.LocalPlayer == null)
IEnumerator IEnumerable.GetEnumerator() return null;
{
return this.GetEnumerator(); return new AetheryteEntry(TelepoStruct->TeleportList.Get((ulong)index));
} }
} }
private void Update()
{
// this is very very important as otherwise it crashes
if (this.clientState.LocalPlayer == null)
return;
this.updateAetheryteListFunc(this.address.Telepo, 0);
}
}
/// <summary>
/// This collection represents the list of available Aetherytes in the Teleport window.
/// </summary>
public sealed partial class AetheryteList : IReadOnlyCollection<AetheryteEntry>
{
/// <inheritdoc/>
public int Count => this.Length;
/// <inheritdoc/>
public IEnumerator<AetheryteEntry> GetEnumerator()
{
for (var i = 0; i < this.Length; i++)
{
yield return this[i];
}
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
} }

View file

@ -7,179 +7,178 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.Buddy namespace Dalamud.Game.ClientState.Buddy;
/// <summary>
/// This collection represents the buddies present in your squadron or trust party.
/// It does not include the local player.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed partial class BuddyList : IServiceType
{ {
/// <summary> private const uint InvalidObjectID = 0xE0000000;
/// This collection represents the buddies present in your squadron or trust party.
/// It does not include the local player. [ServiceManager.ServiceDependency]
/// </summary> private readonly ClientState clientState = Service<ClientState>.Get();
[PluginInterface]
[InterfaceVersion("1.0")] private readonly ClientStateAddressResolver address;
[ServiceManager.BlockingEarlyLoadedService]
public sealed partial class BuddyList : IServiceType [ServiceManager.ServiceConstructor]
private BuddyList()
{ {
private const uint InvalidObjectID = 0xE0000000; this.address = this.clientState.AddressResolver;
[ServiceManager.ServiceDependency] Log.Verbose($"Buddy list address 0x{this.address.BuddyList.ToInt64():X}");
private readonly ClientState clientState = Service<ClientState>.Get(); }
private readonly ClientStateAddressResolver address; /// <summary>
/// Gets the amount of battle buddies the local player has.
[ServiceManager.ServiceConstructor] /// </summary>
private BuddyList() public int Length
{
get
{ {
this.address = this.clientState.AddressResolver; var i = 0;
for (; i < 3; i++)
Log.Verbose($"Buddy list address 0x{this.address.BuddyList.ToInt64():X}");
}
/// <summary>
/// Gets the amount of battle buddies the local player has.
/// </summary>
public int Length
{
get
{ {
var i = 0; var addr = this.GetBattleBuddyMemberAddress(i);
for (; i < 3; i++) var member = this.CreateBuddyMemberReference(addr);
{ if (member == null)
var addr = this.GetBattleBuddyMemberAddress(i); break;
var member = this.CreateBuddyMemberReference(addr);
if (member == null)
break;
}
return i;
} }
}
/// <summary> return i;
/// 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)
{
if (this.clientState.LocalContentId == 0)
return null;
if (address == IntPtr.Zero)
return null;
var buddy = new BuddyMember(address);
if (buddy.ObjectId == InvalidObjectID)
return null;
return buddy;
} }
} }
/// <summary> /// <summary>
/// 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> /// </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/> get
int IReadOnlyCollection<BuddyMember>.Count => this.Length;
/// <inheritdoc/>
public IEnumerator<BuddyMember> GetEnumerator()
{ {
for (var i = 0; i < this.Length; i++) var addr = this.GetCompanionBuddyMemberAddress();
{ return this.CreateBuddyMemberReference(addr);
yield return this[i];
}
} }
}
/// <inheritdoc/> /// <summary>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); /// 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)
{
if (this.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,73 +4,72 @@ using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers; using Dalamud.Game.ClientState.Resolvers;
namespace Dalamud.Game.ClientState.Buddy namespace Dalamud.Game.ClientState.Buddy;
/// <summary>
/// This class represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
/// </summary>
public unsafe class BuddyMember
{ {
[ServiceManager.ServiceDependency]
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
/// <summary> /// <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> /// </summary>
public unsafe class BuddyMember /// <param name="address">Buddy address.</param>
internal BuddyMember(IntPtr address)
{ {
[ServiceManager.ServiceDependency] this.Address = address;
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
/// <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 => this.objectTable.SearchById(this.ObjectId);
/// <summary>
/// Gets the current health of this buddy.
/// </summary>
public uint CurrentHP => this.Struct->CurrentHealth;
/// <summary>
/// Gets the maximum health of this buddy.
/// </summary>
public uint MaxHP => this.Struct->MaxHealth;
/// <summary>
/// Gets the data ID of this buddy.
/// </summary>
public uint DataID => this.Struct->DataID;
/// <summary>
/// Gets the Mount data related to this buddy. It should only be used with companion buddies.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.Mount> MountData => new(this.DataID);
/// <summary>
/// Gets the Pet data related to this buddy. It should only be used with pet buddies.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.Pet> PetData => new(this.DataID);
/// <summary>
/// Gets the Trust data related to this buddy. It should only be used with battle buddies.
/// </summary>
public ExcelResolver<Lumina.Excel.GeneratedSheets.DawnGrowMember> TrustData => new(this.DataID);
private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address;
} }
/// <summary>
/// Gets the address of the buddy in memory.
/// </summary>
public IntPtr Address { get; }
/// <summary>
/// Gets the object ID of this buddy.
/// </summary>
public uint ObjectId => this.Struct->ObjectID;
/// <summary>
/// Gets the actor associated with this buddy.
/// </summary>
/// <remarks>
/// This iterates the actor table, it should be used with care.
/// </remarks>
public GameObject? GameObject => this.objectTable.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

@ -13,193 +13,192 @@ using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState namespace Dalamud.Game.ClientState;
/// <summary>
/// This class represents the state of the game client at the time of access.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class ClientState : IDisposable, IServiceType
{ {
/// <summary> private readonly ClientStateAddressResolver address;
/// This class represents the state of the game client at the time of access. private readonly Hook<SetupTerritoryTypeDelegate> setupTerritoryTypeHook;
/// </summary>
[PluginInterface] [ServiceManager.ServiceDependency]
[InterfaceVersion("1.0")] private readonly Framework framework = Service<Framework>.Get();
[ServiceManager.BlockingEarlyLoadedService]
public sealed class ClientState : IDisposable, IServiceType [ServiceManager.ServiceDependency]
private readonly NetworkHandlers networkHandlers = Service<NetworkHandlers>.Get();
private bool lastConditionNone = true;
private bool lastFramePvP = false;
[ServiceManager.ServiceConstructor]
private ClientState(SigScanner sigScanner, DalamudStartInfo startInfo)
{ {
private readonly ClientStateAddressResolver address; this.address = new ClientStateAddressResolver();
private readonly Hook<SetupTerritoryTypeDelegate> setupTerritoryTypeHook; this.address.Setup(sigScanner);
[ServiceManager.ServiceDependency] Log.Verbose("===== C L I E N T S T A T E =====");
private readonly Framework framework = Service<Framework>.Get();
[ServiceManager.ServiceDependency] this.ClientLanguage = startInfo.Language;
private readonly NetworkHandlers networkHandlers = Service<NetworkHandlers>.Get();
private bool lastConditionNone = true; Log.Verbose($"SetupTerritoryType address 0x{this.address.SetupTerritoryType.ToInt64():X}");
private bool lastFramePvP = false;
[ServiceManager.ServiceConstructor] this.setupTerritoryTypeHook = Hook<SetupTerritoryTypeDelegate>.FromAddress(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour);
private ClientState(SigScanner sigScanner, DalamudStartInfo startInfo)
this.framework.Update += this.FrameworkOnOnUpdateEvent;
this.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 fires when a character is entering PvP.
/// </summary>
public event Action EnterPvP;
/// <summary>
/// Event that fires when a character is leaving PvP.
/// </summary>
public event Action LeavePvP;
/// <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>.GetNullable()?[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>
/// Gets a value indicating whether or not the user is playing PvP.
/// </summary>
public bool IsPvP { get; private set; }
/// <summary>
/// Gets a value indicating whether or not the user is playing PvP, excluding the Wolves' Den.
/// </summary>
public bool IsPvPExcludingDen { get; private set; }
/// <summary>
/// Gets client state address resolver.
/// </summary>
internal ClientStateAddressResolver AddressResolver => this.address;
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
void IDisposable.Dispose()
{
this.setupTerritoryTypeHook.Dispose();
this.framework.Update -= this.FrameworkOnOnUpdateEvent;
this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop;
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
this.setupTerritoryTypeHook.Enable();
}
private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType)
{
this.TerritoryType = terriType;
this.TerritoryChanged?.InvokeSafely(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?.InvokeSafely(this, e);
}
private void FrameworkOnOnUpdateEvent(Framework framework1)
{
var condition = Service<Conditions.Condition>.GetNullable();
var gameGui = Service<GameGui>.GetNullable();
var data = Service<DataManager>.GetNullable();
if (condition == null || gameGui == null || data == null)
return;
if (condition.Any() && this.lastConditionNone == true)
{ {
this.address = new ClientStateAddressResolver(); Log.Debug("Is login");
this.address.Setup(sigScanner); this.lastConditionNone = false;
this.IsLoggedIn = true;
Log.Verbose("===== C L I E N T S T A T E ====="); this.Login?.InvokeSafely(this, null);
gameGui.ResetUiHideState();
this.ClientLanguage = startInfo.Language;
Log.Verbose($"SetupTerritoryType address 0x{this.address.SetupTerritoryType.ToInt64():X}");
this.setupTerritoryTypeHook = Hook<SetupTerritoryTypeDelegate>.FromAddress(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour);
this.framework.Update += this.FrameworkOnOnUpdateEvent;
this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
} }
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] if (!condition.Any() && this.lastConditionNone == false)
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 fires when a character is entering PvP.
/// </summary>
public event Action EnterPvP;
/// <summary>
/// Event that fires when a character is leaving PvP.
/// </summary>
public event Action LeavePvP;
/// <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>.GetNullable()?[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>
/// Gets a value indicating whether or not the user is playing PvP.
/// </summary>
public bool IsPvP { get; private set; }
/// <summary>
/// Gets a value indicating whether or not the user is playing PvP, excluding the Wolves' Den.
/// </summary>
public bool IsPvPExcludingDen { get; private set; }
/// <summary>
/// Gets client state address resolver.
/// </summary>
internal ClientStateAddressResolver AddressResolver => this.address;
/// <summary>
/// Dispose of managed and unmanaged resources.
/// </summary>
void IDisposable.Dispose()
{ {
this.setupTerritoryTypeHook.Dispose(); Log.Debug("Is logout");
this.framework.Update -= this.FrameworkOnOnUpdateEvent; this.lastConditionNone = true;
this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop; this.IsLoggedIn = false;
this.Logout?.InvokeSafely(this, null);
gameGui.ResetUiHideState();
} }
[ServiceManager.CallWhenServicesReady] this.IsPvP = GameMain.IsInPvPArea();
private void ContinueConstruction() this.IsPvPExcludingDen = this.IsPvP && this.TerritoryType != 250;
if (this.IsPvP != this.lastFramePvP)
{ {
this.setupTerritoryTypeHook.Enable(); this.lastFramePvP = this.IsPvP;
}
private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType) if (this.IsPvP)
{
this.TerritoryType = terriType;
this.TerritoryChanged?.InvokeSafely(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?.InvokeSafely(this, e);
}
private void FrameworkOnOnUpdateEvent(Framework framework1)
{
var condition = Service<Conditions.Condition>.GetNullable();
var gameGui = Service<GameGui>.GetNullable();
var data = Service<DataManager>.GetNullable();
if (condition == null || gameGui == null || data == null)
return;
if (condition.Any() && this.lastConditionNone == true)
{ {
Log.Debug("Is login"); this.EnterPvP?.InvokeSafely();
this.lastConditionNone = false;
this.IsLoggedIn = true;
this.Login?.InvokeSafely(this, null);
gameGui.ResetUiHideState();
} }
else
if (!condition.Any() && this.lastConditionNone == false)
{ {
Log.Debug("Is logout"); this.LeavePvP?.InvokeSafely();
this.lastConditionNone = true;
this.IsLoggedIn = false;
this.Logout?.InvokeSafely(this, null);
gameGui.ResetUiHideState();
}
this.IsPvP = GameMain.IsInPvPArea();
this.IsPvPExcludingDen = this.IsPvP && this.TerritoryType != 250;
if (this.IsPvP != this.lastFramePvP)
{
this.lastFramePvP = this.IsPvP;
if (this.IsPvP)
{
this.EnterPvP?.InvokeSafely();
}
else
{
this.LeavePvP?.InvokeSafely();
}
} }
} }
} }

View file

@ -1,128 +1,127 @@
using System; using System;
namespace Dalamud.Game.ClientState namespace Dalamud.Game.ClientState;
/// <summary>
/// Client state memory address resolver.
/// </summary>
public sealed class ClientStateAddressResolver : BaseAddressResolver
{ {
// Static offsets
/// <summary> /// <summary>
/// Client state memory address resolver. /// Gets the address of the actor table.
/// </summary> /// </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; }
/// <summary>
/// Gets the address of the Telepo instance.
/// </summary>
public IntPtr Telepo { 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>
/// Gets the address of the method which updates the list of available teleport locations.
/// </summary>
public IntPtr UpdateAetheryteList { 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> this.ObjectTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 83");
/// Gets the address of the actor table.
/// </summary>
public IntPtr ObjectTable { get; private set; }
/// <summary> this.BuddyList = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 45 84 E4 75 1A F6 45 12 04");
/// Gets the address of the buddy list.
/// </summary>
public IntPtr BuddyList { get; private set; }
/// <summary> this.FateTablePtr = sig.GetStaticAddressFromSig("48 8B 15 ?? ?? ?? ?? 48 8B F9 44 0F B7 41 ??");
/// 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> this.GroupManager = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 80 B8 ?? ?? ?? ?? ?? 76 50");
/// Gets the address of the Group Manager.
/// </summary>
public IntPtr GroupManager { get; private set; }
/// <summary> this.LocalContentId = sig.GetStaticAddressFromSig("48 0F 44 05 ?? ?? ?? ?? 48 39 07");
/// Gets the address of the local content id. this.JobGaugeData = sig.GetStaticAddressFromSig("48 8B 3D ?? ?? ?? ?? 33 ED") + 0x8;
/// </summary>
public IntPtr LocalContentId { get; private set; }
/// <summary> this.SetupTerritoryType = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B F9 66 89 91 ?? ?? ?? ??");
/// Gets the address of job gauge data.
/// </summary>
public IntPtr JobGaugeData { get; private set; }
/// <summary> // These resolve to fixed offsets only, without the base address added in, so GetStaticAddressFromSig() can't be used.
/// Gets the address of the keyboard state. // lea rcx, ds:1DB9F74h[rax*4] KeyboardState
/// </summary> // movzx edx, byte ptr [rbx+rsi+1D5E0E0h] KeyboardStateIndexArray
public IntPtr KeyboardState { get; private set; } 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> this.ConditionFlags = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? BA ?? ?? ?? ?? E8 ?? ?? ?? ?? B0 01 48 83 C4 30");
/// 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> this.TargetManager = sig.GetStaticAddressFromSig("48 8B 05 ?? ?? ?? ?? 48 8D 0D ?? ?? ?? ?? FF 50 ?? 48 85 DB");
/// Gets the address of the target manager.
/// </summary>
public IntPtr TargetManager { get; private set; }
/// <summary> this.GamepadPoll = sig.ScanText("40 ?? 57 41 ?? 48 81 EC ?? ?? ?? ?? 44 0F ?? ?? ?? ?? ?? ?? ?? 48 8B");
/// Gets the address of the condition flag array.
/// </summary>
public IntPtr ConditionFlags { get; private set; }
/// <summary> this.Telepo = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? 48 8B 12");
/// Gets the address of the Telepo instance.
/// </summary>
public IntPtr Telepo { get; private set; }
// Functions this.UpdateAetheryteList = sig.ScanText("E8 ?? ?? ?? ?? 48 89 46 68 4C 8D 45 50");
/// <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>
/// Gets the address of the method which updates the list of available teleport locations.
/// </summary>
public IntPtr UpdateAetheryteList { 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 3D ?? ?? ?? ?? 33 ED") + 0x8;
this.SetupTerritoryType = sig.ScanText("48 89 5C 24 ?? 48 89 6C 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.Telepo = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? 48 8B 12");
this.UpdateAetheryteList = sig.ScanText("E8 ?? ?? ?? ?? 48 89 46 68 4C 8D 45 50");
}
} }
} }

View file

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

View file

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

View file

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

View file

@ -6,137 +6,136 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.Fates namespace Dalamud.Game.ClientState.Fates;
/// <summary>
/// This collection represents the currently available Fate events.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed partial class FateTable : IServiceType
{ {
/// <summary> private readonly ClientStateAddressResolver address;
/// This collection represents the currently available Fate events.
/// </summary> [ServiceManager.ServiceConstructor]
[PluginInterface] private FateTable(ClientState clientState)
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed partial class FateTable : IServiceType
{ {
private readonly ClientStateAddressResolver address; this.address = clientState.AddressResolver;
[ServiceManager.ServiceConstructor] Log.Verbose($"Fate table address 0x{this.address.FateTablePtr.ToInt64():X}");
private FateTable(ClientState clientState) }
/// <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 = clientState.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
if (Struct->FateDirector == null)
return 0;
if (Struct->Fates.First == null || Struct->Fates.Last == null)
return 0;
return (int)Struct->Fates.Size();
}
}
/// <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; var fateTable = this.FateTableAddress;
if (fateTable == IntPtr.Zero) if (fateTable == IntPtr.Zero)
return IntPtr.Zero; return 0;
return (IntPtr)this.Struct->Fates.Get((ulong)index).Value; // Sonar used this to check if the table was safe to read
} if (Struct->FateDirector == null)
return 0;
/// <summary> if (Struct->Fates.First == null || Struct->Fates.Last == null)
/// Create a reference to a FFXIV actor. return 0;
/// </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 (int)Struct->Fates.Size();
return null;
if (offset == IntPtr.Zero)
return null;
return new Fate(offset);
} }
} }
/// <summary> /// <summary>
/// This collection represents the currently available Fate events. /// Gets the address of the Fate table.
/// </summary> /// </summary>
public sealed partial class FateTable : IReadOnlyCollection<Fate> internal unsafe IntPtr FateTableAddress
{ {
/// <inheritdoc/> get
int IReadOnlyCollection<Fate>.Count => this.Length;
/// <inheritdoc/>
public IEnumerator<Fate> GetEnumerator()
{ {
for (var i = 0; i < this.Length; i++) if (this.address.FateTablePtr == IntPtr.Zero)
{ return IntPtr.Zero;
yield return this[i];
}
}
/// <inheritdoc/> return *(IntPtr*)this.address.FateTablePtr;
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); }
}
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 (IntPtr)this.Struct->Fates.Get((ulong)index).Value;
}
/// <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; using System;
namespace Dalamud.Game.ClientState.GamePad namespace Dalamud.Game.ClientState.GamePad;
/// <summary>
/// Bitmask of the Button ushort used by the game.
/// </summary>
[Flags]
public enum GamepadButtons : ushort
{ {
/// <summary> /// <summary>
/// Bitmask of the Button ushort used by the game. /// No buttons pressed.
/// </summary> /// </summary>
[Flags] None = 0,
public enum GamepadButtons : ushort
{
/// <summary>
/// No buttons pressed.
/// </summary>
None = 0,
/// <summary> /// <summary>
/// Digipad up. /// Digipad up.
/// </summary> /// </summary>
DpadUp = 0x0001, DpadUp = 0x0001,
/// <summary> /// <summary>
/// Digipad down. /// Digipad down.
/// </summary> /// </summary>
DpadDown = 0x0002, DpadDown = 0x0002,
/// <summary> /// <summary>
/// Digipad left. /// Digipad left.
/// </summary> /// </summary>
DpadLeft = 0x0004, DpadLeft = 0x0004,
/// <summary> /// <summary>
/// Digipad right. /// Digipad right.
/// </summary> /// </summary>
DpadRight = 0x0008, DpadRight = 0x0008,
/// <summary> /// <summary>
/// North action button. Triangle on PS, Y on Xbox. /// North action button. Triangle on PS, Y on Xbox.
/// </summary> /// </summary>
North = 0x0010, North = 0x0010,
/// <summary> /// <summary>
/// South action button. Cross on PS, A on Xbox. /// South action button. Cross on PS, A on Xbox.
/// </summary> /// </summary>
South = 0x0020, South = 0x0020,
/// <summary> /// <summary>
/// West action button. Square on PS, X on Xbos. /// West action button. Square on PS, X on Xbos.
/// </summary> /// </summary>
West = 0x0040, West = 0x0040,
/// <summary> /// <summary>
/// East action button. Circle on PS, B on Xbox. /// East action button. Circle on PS, B on Xbox.
/// </summary> /// </summary>
East = 0x0080, East = 0x0080,
/// <summary> /// <summary>
/// First button on left shoulder side. /// First button on left shoulder side.
/// </summary> /// </summary>
L1 = 0x0100, L1 = 0x0100,
/// <summary> /// <summary>
/// Second button on left shoulder side. Analog input lost in this bitmask. /// Second button on left shoulder side. Analog input lost in this bitmask.
/// </summary> /// </summary>
L2 = 0x0200, L2 = 0x0200,
/// <summary> /// <summary>
/// Press on left analogue stick. /// Press on left analogue stick.
/// </summary> /// </summary>
L3 = 0x0400, L3 = 0x0400,
/// <summary> /// <summary>
/// First button on right shoulder. /// First button on right shoulder.
/// </summary> /// </summary>
R1 = 0x0800, R1 = 0x0800,
/// <summary> /// <summary>
/// Second button on right shoulder. Analog input lost in this bitmask. /// Second button on right shoulder. Analog input lost in this bitmask.
/// </summary> /// </summary>
R2 = 0x1000, R2 = 0x1000,
/// <summary> /// <summary>
/// Press on right analogue stick. /// Press on right analogue stick.
/// </summary> /// </summary>
R3 = 0x2000, R3 = 0x2000,
/// <summary> /// <summary>
/// Button on the right inner side of the controller. Options on PS, Start on Xbox. /// Button on the right inner side of the controller. Options on PS, Start on Xbox.
/// </summary> /// </summary>
Start = 0x8000, Start = 0x8000,
/// <summary> /// <summary>
/// Button on the left inner side of the controller. ??? on PS, Back on Xbox. /// Button on the left inner side of the controller. ??? on PS, Back on Xbox.
/// </summary> /// </summary>
Select = 0x4000, Select = 0x4000,
}
} }

View file

@ -1,76 +1,75 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Dalamud.Game.ClientState.GamePad namespace Dalamud.Game.ClientState.GamePad;
/// <summary>
/// Struct which gets populated by polling the gamepads.
///
/// Has an array of gamepads, among many other things (here not mapped).
/// All we really care about is the final data which the game uses to determine input.
///
/// The size is definitely bigger than only the following fields but I do not know how big.
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public struct GamepadInput
{ {
/// <summary> /// <summary>
/// Struct which gets populated by polling the gamepads. /// Left analogue stick's horizontal value, -99 for left, 99 for right.
///
/// Has an array of gamepads, among many other things (here not mapped).
/// All we really care about is the final data which the game uses to determine input.
///
/// The size is definitely bigger than only the following fields but I do not know how big.
/// </summary> /// </summary>
[StructLayout(LayoutKind.Explicit)] [FieldOffset(0x88)]
public struct GamepadInput public int LeftStickX;
{
/// <summary>
/// Left analogue stick's horizontal value, -99 for left, 99 for right.
/// </summary>
[FieldOffset(0x88)]
public int LeftStickX;
/// <summary> /// <summary>
/// Left analogue stick's vertical value, -99 for down, 99 for up. /// Left analogue stick's vertical value, -99 for down, 99 for up.
/// </summary> /// </summary>
[FieldOffset(0x8C)] [FieldOffset(0x8C)]
public int LeftStickY; public int LeftStickY;
/// <summary> /// <summary>
/// Right analogue stick's horizontal value, -99 for left, 99 for right. /// Right analogue stick's horizontal value, -99 for left, 99 for right.
/// </summary> /// </summary>
[FieldOffset(0x90)] [FieldOffset(0x90)]
public int RightStickX; public int RightStickX;
/// <summary> /// <summary>
/// Right analogue stick's vertical value, -99 for down, 99 for up. /// Right analogue stick's vertical value, -99 for down, 99 for up.
/// </summary> /// </summary>
[FieldOffset(0x94)] [FieldOffset(0x94)]
public int RightStickY; public int RightStickY;
/// <summary> /// <summary>
/// Raw input, set the whole time while a button is held. See <see cref="GamepadButtons"/> for the mapping. /// Raw input, set the whole time while a button is held. See <see cref="GamepadButtons"/> for the mapping.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is a bitfield. /// This is a bitfield.
/// </remarks> /// </remarks>
[FieldOffset(0x98)] [FieldOffset(0x98)]
public ushort ButtonsRaw; public ushort ButtonsRaw;
/// <summary> /// <summary>
/// Button pressed, set once when the button is pressed. See <see cref="GamepadButtons"/> for the mapping. /// Button pressed, set once when the button is pressed. See <see cref="GamepadButtons"/> for the mapping.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is a bitfield. /// This is a bitfield.
/// </remarks> /// </remarks>
[FieldOffset(0x9C)] [FieldOffset(0x9C)]
public ushort ButtonsPressed; public ushort ButtonsPressed;
/// <summary> /// <summary>
/// Button released input, set once right after the button is not hold anymore. See <see cref="GamepadButtons"/> for the mapping. /// Button released input, set once right after the button is not hold anymore. See <see cref="GamepadButtons"/> for the mapping.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is a bitfield. /// This is a bitfield.
/// </remarks> /// </remarks>
[FieldOffset(0xA0)] [FieldOffset(0xA0)]
public ushort ButtonsReleased; public ushort ButtonsReleased;
/// <summary> /// <summary>
/// Repeatedly emits the held button input in fixed intervals. See <see cref="GamepadButtons"/> for the mapping. /// Repeatedly emits the held button input in fixed intervals. See <see cref="GamepadButtons"/> for the mapping.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is a bitfield. /// This is a bitfield.
/// </remarks> /// </remarks>
[FieldOffset(0xA4)] [FieldOffset(0xA4)]
public ushort ButtonsRepeat; public ushort ButtonsRepeat;
}
} }

View file

@ -6,249 +6,248 @@ using Dalamud.IoC.Internal;
using ImGuiNET; using ImGuiNET;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.GamePad namespace Dalamud.Game.ClientState.GamePad;
/// <summary>
/// Exposes the game gamepad state to dalamud.
///
/// Will block game's gamepad input if <see cref="ImGuiConfigFlags.NavEnableGamepad"/> is set.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public unsafe class GamepadState : IDisposable, IServiceType
{ {
/// <summary> private readonly Hook<ControllerPoll> gamepadPoll;
/// Exposes the game gamepad state to dalamud.
/// private bool isDisposed;
/// Will block game's gamepad input if <see cref="ImGuiConfigFlags.NavEnableGamepad"/> is set.
/// </summary> private int leftStickX;
[PluginInterface] private int leftStickY;
[InterfaceVersion("1.0")] private int rightStickX;
[ServiceManager.BlockingEarlyLoadedService] private int rightStickY;
public unsafe class GamepadState : IDisposable, IServiceType
[ServiceManager.ServiceConstructor]
private GamepadState(ClientState clientState)
{ {
private readonly Hook<ControllerPoll> gamepadPoll; var resolver = clientState.AddressResolver;
Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}");
this.gamepadPoll = Hook<ControllerPoll>.FromAddress(resolver.GamepadPoll, this.GamepadPollDetour);
}
private bool isDisposed; private delegate int ControllerPoll(IntPtr controllerInput);
private int leftStickX; /// <summary>
private int leftStickY; /// Gets the pointer to the current instance of the GamepadInput struct.
private int rightStickX; /// </summary>
private int rightStickY; public IntPtr GamepadInputAddress { get; private set; }
[ServiceManager.ServiceConstructor] /// <summary>
private GamepadState(ClientState clientState) /// 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>
/// Disposes this instance, alongside its hooks.
/// </summary>
void IDisposable.Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
this.gamepadPoll.Enable();
}
private int GamepadPollDetour(IntPtr gamepadInput)
{
var original = this.gamepadPoll.Original(gamepadInput);
try
{ {
var resolver = clientState.AddressResolver; this.GamepadInputAddress = gamepadInput;
Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}"); var input = (GamepadInput*)gamepadInput;
this.gamepadPoll = Hook<ControllerPoll>.FromAddress(resolver.GamepadPoll, this.GamepadPollDetour); this.leftStickX = input->LeftStickX;
} this.leftStickY = input->LeftStickY;
this.rightStickX = input->RightStickX;
this.rightStickY = input->RightStickY;
this.ButtonsRaw = input->ButtonsRaw;
this.ButtonsPressed = input->ButtonsPressed;
this.ButtonsReleased = input->ButtonsReleased;
this.ButtonsRepeat = input->ButtonsRepeat;
private delegate int ControllerPoll(IntPtr controllerInput); if (this.NavEnableGamepad)
/// <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>
/// Disposes this instance, alongside its hooks.
/// </summary>
void IDisposable.Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
this.gamepadPoll.Enable();
}
private int GamepadPollDetour(IntPtr gamepadInput)
{
var original = this.gamepadPoll.Original(gamepadInput);
try
{ {
this.GamepadInputAddress = gamepadInput; input->LeftStickX = 0;
var input = (GamepadInput*)gamepadInput; input->LeftStickY = 0;
this.leftStickX = input->LeftStickX; input->RightStickX = 0;
this.leftStickY = input->LeftStickY; input->RightStickY = 0;
this.rightStickX = input->RightStickX;
this.rightStickY = input->RightStickY;
this.ButtonsRaw = input->ButtonsRaw;
this.ButtonsPressed = input->ButtonsPressed;
this.ButtonsReleased = input->ButtonsReleased;
this.ButtonsRepeat = input->ButtonsRepeat;
if (this.NavEnableGamepad) // NOTE (Chiv) Zeroing `ButtonsRaw` destroys `ButtonPressed`, `ButtonReleased`
{ // and `ButtonRepeat` as the game uses the RAW input to determine those (apparently).
input->LeftStickX = 0; // It does block, however, all input to the game.
input->LeftStickY = 0; // Leaving `ButtonsRaw` as it is and only zeroing the other leaves e.g. long-hold L2/R2
input->RightStickX = 0; // and the digipad (in some situations, but thankfully not in menus) functional.
input->RightStickY = 0; // We can either:
// (a) Explicitly only set L2/R2/Digipad to 0 (and destroy their `ButtonPressed` field) => Needs to be documented, or
// NOTE (Chiv) Zeroing `ButtonsRaw` destroys `ButtonPressed`, `ButtonReleased` // (b) ignore it as so far it seems only a 'visual' error
// and `ButtonRepeat` as the game uses the RAW input to determine those (apparently). // (L2/R2 being held down activates CrossHotBar but activating an ability is impossible because of the others blocked input,
// It does block, however, all input to the game. // Digipad is ignored in menus but without any menu's one still switches target or party members, but cannot interact with them
// Leaving `ButtonsRaw` as it is and only zeroing the other leaves e.g. long-hold L2/R2 // because of the other blocked input)
// and the digipad (in some situations, but thankfully not in menus) functional. // `ButtonPressed` is pretty useful but its hella confusing to the user, so we do (a) and advise plugins do not rely on
// We can either: // `ButtonPressed` while ImGuiConfigFlags.NavEnableGamepad is set.
// (a) Explicitly only set L2/R2/Digipad to 0 (and destroy their `ButtonPressed` field) => Needs to be documented, or // This is debatable.
// (b) ignore it as so far it seems only a 'visual' error // ImGui itself does not care either way as it uses the Raw values and does its own state handling.
// (L2/R2 being held down activates CrossHotBar but activating an ability is impossible because of the others blocked input, const ushort deletionMask = (ushort)(~GamepadButtons.L2
// Digipad is ignored in menus but without any menu's one still switches target or party members, but cannot interact with them & ~GamepadButtons.R2
// because of the other blocked input) & ~GamepadButtons.DpadDown
// `ButtonPressed` is pretty useful but its hella confusing to the user, so we do (a) and advise plugins do not rely on & ~GamepadButtons.DpadLeft
// `ButtonPressed` while ImGuiConfigFlags.NavEnableGamepad is set. & ~GamepadButtons.DpadUp
// This is debatable. & ~GamepadButtons.DpadRight);
// ImGui itself does not care either way as it uses the Raw values and does its own state handling. input->ButtonsRaw &= deletionMask;
const ushort deletionMask = (ushort)(~GamepadButtons.L2 input->ButtonsPressed = 0;
& ~GamepadButtons.R2 input->ButtonsReleased = 0;
& ~GamepadButtons.DpadDown input->ButtonsRepeat = 0;
& ~GamepadButtons.DpadLeft return 0;
& ~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();
} }
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,28 +1,27 @@
namespace Dalamud.Game.ClientState.JobGauge.Enums namespace Dalamud.Game.ClientState.JobGauge.Enums;
/// <summary>
/// MNK Beast Chakra types.
/// </summary>
public enum BeastChakra : byte
{ {
/// <summary> /// <summary>
/// MNK Beast Chakra types. /// No card.
/// </summary> /// </summary>
public enum BeastChakra : byte NONE = 0,
{
/// <summary>
/// No card.
/// </summary>
NONE = 0,
/// <summary> /// <summary>
/// The Coeurl chakra. /// The Coeurl chakra.
/// </summary> /// </summary>
COEURL = 1, COEURL = 1,
/// <summary> /// <summary>
/// The Opo-Opo chakra. /// The Opo-Opo chakra.
/// </summary> /// </summary>
OPOOPO = 2, OPOOPO = 2,
/// <summary> /// <summary>
/// The Raptor chakra. /// The Raptor chakra.
/// </summary> /// </summary>
RAPTOR = 3, RAPTOR = 3,
}
} }

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> /// <summary>
/// AST Arcanum (card) types. /// No card.
/// </summary> /// </summary>
public enum CardType : byte NONE = 0,
{
/// <summary>
/// No card.
/// </summary>
NONE = 0,
/// <summary> /// <summary>
/// The Balance card. /// The Balance card.
/// </summary> /// </summary>
BALANCE = 1, BALANCE = 1,
/// <summary> /// <summary>
/// The Bole card. /// The Bole card.
/// </summary> /// </summary>
BOLE = 2, BOLE = 2,
/// <summary> /// <summary>
/// The Arrow card. /// The Arrow card.
/// </summary> /// </summary>
ARROW = 3, ARROW = 3,
/// <summary> /// <summary>
/// The Spear card. /// The Spear card.
/// </summary> /// </summary>
SPEAR = 4, SPEAR = 4,
/// <summary> /// <summary>
/// The Ewer card. /// The Ewer card.
/// </summary> /// </summary>
EWER = 5, EWER = 5,
/// <summary> /// <summary>
/// The Spire card. /// The Spire card.
/// </summary> /// </summary>
SPIRE = 6, SPIRE = 6,
/// <summary> /// <summary>
/// The Lord of Crowns card. /// The Lord of Crowns card.
/// </summary> /// </summary>
LORD = 0x70, LORD = 0x70,
/// <summary> /// <summary>
/// The Lady of Crowns card. /// The Lady of Crowns card.
/// </summary> /// </summary>
LADY = 0x80, LADY = 0x80,
}
} }

View file

@ -1,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> /// <summary>
/// SCH Dismissed fairy types. /// Dismissed fairy is Eos.
/// </summary> /// </summary>
public enum DismissedFairy : byte EOS = 6,
{
/// <summary>
/// Dismissed fairy is Eos.
/// </summary>
EOS = 6,
/// <summary> /// <summary>
/// Dismissed fairy is Selene. /// Dismissed fairy is Selene.
/// </summary> /// </summary>
SELENE = 7, SELENE = 7,
}
} }

View file

@ -1,33 +1,32 @@
namespace Dalamud.Game.ClientState.JobGauge.Enums namespace Dalamud.Game.ClientState.JobGauge.Enums;
/// <summary>
/// SAM Kaeshi types.
/// </summary>
public enum Kaeshi : byte
{ {
/// <summary> /// <summary>
/// SAM Kaeshi types. /// No Kaeshi is active.
/// </summary> /// </summary>
public enum Kaeshi : byte NONE = 0,
{
/// <summary>
/// No Kaeshi is active.
/// </summary>
NONE = 0,
/// <summary> /// <summary>
/// Kaeshi: Higanbana type. /// Kaeshi: Higanbana type.
/// </summary> /// </summary>
HIGANBANA = 1, HIGANBANA = 1,
/// <summary> /// <summary>
/// Kaeshi: Goken type. /// Kaeshi: Goken type.
/// </summary> /// </summary>
GOKEN = 2, GOKEN = 2,
/// <summary> /// <summary>
/// Kaeshi: Setsugekka type. /// Kaeshi: Setsugekka type.
/// </summary> /// </summary>
SETSUGEKKA = 3, SETSUGEKKA = 3,
/// <summary> /// <summary>
/// Kaeshi: Namikiri type. /// Kaeshi: Namikiri type.
/// </summary> /// </summary>
NAMIKIRI = 4, NAMIKIRI = 4,
}
} }

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> /// <summary>
/// NIN Mudra types. /// Ten mudra.
/// </summary> /// </summary>
public enum Mudras : byte TEN = 1,
{
/// <summary>
/// Ten mudra.
/// </summary>
TEN = 1,
/// <summary> /// <summary>
/// Chi mudra. /// Chi mudra.
/// </summary> /// </summary>
CHI = 2, CHI = 2,
/// <summary> /// <summary>
/// Jin mudra. /// Jin mudra.
/// </summary> /// </summary>
JIN = 3, JIN = 3,
}
} }

View file

@ -1,26 +1,25 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Enums namespace Dalamud.Game.ClientState.JobGauge.Enums;
/// <summary>
/// MNK Nadi types.
/// </summary>
[Flags]
public enum Nadi : byte
{ {
/// <summary> /// <summary>
/// MNK Nadi types. /// No card.
/// </summary> /// </summary>
[Flags] NONE = 0,
public enum Nadi : byte
{
/// <summary>
/// No card.
/// </summary>
NONE = 0,
/// <summary> /// <summary>
/// The Lunar nadi. /// The Lunar nadi.
/// </summary> /// </summary>
LUNAR = 2, LUNAR = 2,
/// <summary> /// <summary>
/// The Solar nadi. /// The Solar nadi.
/// </summary> /// </summary>
SOLAR = 4, SOLAR = 4,
}
} }

View file

@ -1,48 +1,47 @@
namespace Dalamud.Game.ClientState.JobGauge.Enums namespace Dalamud.Game.ClientState.JobGauge.Enums;
/// <summary>
/// SMN summoned pet glam types.
/// </summary>
public enum PetGlam : byte
{ {
/// <summary> /// <summary>
/// SMN summoned pet glam types. /// No pet glam.
/// </summary> /// </summary>
public enum PetGlam : byte NONE = 0,
{
/// <summary>
/// No pet glam.
/// </summary>
NONE = 0,
/// <summary> /// <summary>
/// Emerald carbuncle pet glam. /// Emerald carbuncle pet glam.
/// </summary> /// </summary>
EMERALD = 1, EMERALD = 1,
/// <summary> /// <summary>
/// Topaz carbuncle pet glam. /// Topaz carbuncle pet glam.
/// </summary> /// </summary>
TOPAZ = 2, TOPAZ = 2,
/// <summary> /// <summary>
/// Ruby carbuncle pet glam. /// Ruby carbuncle pet glam.
/// </summary> /// </summary>
RUBY = 3, RUBY = 3,
/// <summary> /// <summary>
/// Normal carbuncle pet glam. /// Normal carbuncle pet glam.
/// </summary> /// </summary>
CARBUNCLE = 4, CARBUNCLE = 4,
/// <summary> /// <summary>
/// Ifrit Egi pet glam. /// Ifrit Egi pet glam.
/// </summary> /// </summary>
IFRIT = 5, IFRIT = 5,
/// <summary> /// <summary>
/// Titan Egi pet glam. /// Titan Egi pet glam.
/// </summary> /// </summary>
TITAN = 6, TITAN = 6,
/// <summary> /// <summary>
/// Garuda Egi pet glam. /// Garuda Egi pet glam.
/// </summary> /// </summary>
GARUDA = 7, GARUDA = 7,
}
} }

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> /// <summary>
/// AST Divination seal types. /// No seal.
/// </summary> /// </summary>
public enum SealType : byte NONE = 0,
{
/// <summary>
/// No seal.
/// </summary>
NONE = 0,
/// <summary> /// <summary>
/// Sun seal. /// Sun seal.
/// </summary> /// </summary>
SUN = 1, SUN = 1,
/// <summary> /// <summary>
/// Moon seal. /// Moon seal.
/// </summary> /// </summary>
MOON = 2, MOON = 2,
/// <summary> /// <summary>
/// Celestial seal. /// Celestial seal.
/// </summary> /// </summary>
CELESTIAL = 3, CELESTIAL = 3,
}
} }

View file

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

View file

@ -1,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> /// <summary>
/// BRD Song types. /// No song is active type.
/// </summary> /// </summary>
public enum Song : byte NONE = 0,
{
/// <summary>
/// No song is active type.
/// </summary>
NONE = 0,
/// <summary> /// <summary>
/// Mage's Ballad type. /// Mage's Ballad type.
/// </summary> /// </summary>
MAGE = 1, MAGE = 1,
/// <summary> /// <summary>
/// Army's Paeon type. /// Army's Paeon type.
/// </summary> /// </summary>
ARMY = 2, ARMY = 2,
/// <summary> /// <summary>
/// The Wanderer's Minuet type. /// The Wanderer's Minuet type.
/// </summary> /// </summary>
WANDERER = 3, WANDERER = 3,
}
} }

View file

@ -1,18 +1,17 @@
namespace Dalamud.Game.ClientState.JobGauge.Enums namespace Dalamud.Game.ClientState.JobGauge.Enums;
/// <summary>
/// SMN summoned pet types.
/// </summary>
public enum SummonPet : byte
{ {
/// <summary> /// <summary>
/// SMN summoned pet types. /// No pet.
/// </summary> /// </summary>
public enum SummonPet : byte NONE = 0,
{
/// <summary>
/// No pet.
/// </summary>
NONE = 0,
/// <summary> /// <summary>
/// The summoned pet Carbuncle. /// The summoned pet Carbuncle.
/// </summary> /// </summary>
CARBUNCLE = 23, CARBUNCLE = 23,
}
} }

View file

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

View file

@ -3,44 +3,43 @@ using System.Linq;
using Dalamud.Game.ClientState.JobGauge.Enums; using Dalamud.Game.ClientState.JobGauge.Enums;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// In-memory AST job gauge.
/// </summary>
public unsafe class ASTGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.AstrologianGauge>
{ {
/// <summary> /// <summary>
/// In-memory AST job gauge. /// Initializes a new instance of the <see cref="ASTGauge"/> class.
/// </summary> /// </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 & 0xF);
/// <summary>
/// Gets the currently drawn crown <see cref="CardType"/>.
/// </summary>
/// <returns>Currently drawn crown <see cref="CardType"/>.</returns>
public CardType DrawnCrownCard => this.Struct->Card - this.DrawnCard;
/// <summary>
/// Gets the <see cref="SealType"/>s currently active.
/// </summary>
public SealType[] Seals => this.Struct->CurrentSeals.Select(seal => (SealType)seal).ToArray();
/// <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) => this.Seals.Contains(seal);
} }
/// <summary>
/// Gets the currently drawn <see cref="CardType"/>.
/// </summary>
/// <returns>Currently drawn <see cref="CardType"/>.</returns>
public CardType DrawnCard => (CardType)(this.Struct->Card & 0xF);
/// <summary>
/// Gets the currently drawn crown <see cref="CardType"/>.
/// </summary>
/// <returns>Currently drawn crown <see cref="CardType"/>.</returns>
public CardType DrawnCrownCard => this.Struct->Card - this.DrawnCard;
/// <summary>
/// Gets the <see cref="SealType"/>s currently active.
/// </summary>
public SealType[] Seals => this.Struct->CurrentSeals.Select(seal => (SealType)seal).ToArray();
/// <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) => this.Seals.Contains(seal);
} }

View file

@ -1,73 +1,72 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// In-memory BLM job gauge.
/// </summary>
public unsafe class BLMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.BlackMageGauge>
{ {
/// <summary> /// <summary>
/// In-memory BLM job gauge. /// Initializes a new instance of the <see cref="BLMGauge"/> class.
/// </summary> /// </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 or not 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 or not 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 or not Enochian is active.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsEnochianActive => this.Struct->EnochianActive;
/// <summary>
/// Gets a value indicating whether Paradox is active.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsParadoxActive => this.Struct->ParadoxActive;
} }
/// <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 or not 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 or not 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 or not Enochian is active.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsEnochianActive => this.Struct->EnochianActive;
/// <summary>
/// Gets a value indicating whether Paradox is active.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsParadoxActive => this.Struct->ParadoxActive;
} }

View file

@ -3,94 +3,93 @@ using System;
using Dalamud.Game.ClientState.JobGauge.Enums; using Dalamud.Game.ClientState.JobGauge.Enums;
using FFXIVClientStructs.FFXIV.Client.Game.Gauge; using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// In-memory BRD job gauge.
/// </summary>
public unsafe class BRDGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.BardGauge>
{ {
/// <summary> /// <summary>
/// In-memory BRD job gauge. /// Initializes a new instance of the <see cref="BRDGauge"/> class.
/// </summary> /// </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> /// <summary>
/// <param name="address">Address of the job gauge.</param> /// Gets the current song timer in milliseconds.
internal BRDGauge(IntPtr address) /// </summary>
: base(address) public ushort 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
{
get
{ {
if (this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuet))
return Song.WANDERER;
if (this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeon))
return Song.ARMY;
if (this.Struct->SongFlags.HasFlag(SongFlags.MagesBallad))
return Song.MAGE;
return Song.NONE;
} }
}
/// <summary> /// <summary>
/// Gets the current song timer in milliseconds. /// Gets the type of song that was last played.
/// </summary> /// </summary>
public ushort SongTimer => this.Struct->SongTimer; public Song LastSong
{
/// <summary> get
/// 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
{ {
get if (this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuetLastPlayed))
{ return Song.WANDERER;
if (this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuet))
return Song.WANDERER;
if (this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeon)) if (this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeonLastPlayed))
return Song.ARMY; return Song.ARMY;
if (this.Struct->SongFlags.HasFlag(SongFlags.MagesBallad)) if (this.Struct->SongFlags.HasFlag(SongFlags.MagesBalladLastPlayed))
return Song.MAGE; return Song.MAGE;
return Song.NONE; return Song.NONE;
}
} }
}
/// <summary> /// <summary>
/// Gets the type of song that was last played. /// Gets the song Coda that are currently active.
/// </summary> /// </summary>
public Song LastSong /// <remarks>
/// This will always return an array of size 3, inactive Coda are represented by <see cref="Song.NONE"/>.
/// </remarks>
public Song[] Coda
{
get
{ {
get return new[]
{ {
if (this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuetLastPlayed)) this.Struct->SongFlags.HasFlag(SongFlags.MagesBalladCoda) ? Song.MAGE : Song.NONE,
return Song.WANDERER; this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeonCoda) ? Song.ARMY : Song.NONE,
this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuetCoda) ? Song.WANDERER : Song.NONE,
if (this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeonLastPlayed)) };
return Song.ARMY;
if (this.Struct->SongFlags.HasFlag(SongFlags.MagesBalladLastPlayed))
return Song.MAGE;
return Song.NONE;
}
}
/// <summary>
/// Gets the song Coda that are currently active.
/// </summary>
/// <remarks>
/// This will always return an array of size 3, inactive Coda are represented by <see cref="Song.NONE"/>.
/// </remarks>
public Song[] Coda
{
get
{
return new[]
{
this.Struct->SongFlags.HasFlag(SongFlags.MagesBalladCoda) ? Song.MAGE : Song.NONE,
this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeonCoda) ? Song.ARMY : Song.NONE,
this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuetCoda) ? Song.WANDERER : Song.NONE,
};
}
} }
} }
} }

View file

@ -1,60 +1,59 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// In-memory DNC job gauge.
/// </summary>
public unsafe class DNCGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.DancerGauge>
{ {
/// <summary> /// <summary>
/// In-memory DNC job gauge. /// Initializes a new instance of the <see cref="DNCGauge"/> class.
/// </summary> /// </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

@ -1,39 +1,38 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// In-memory DRG job gauge.
/// </summary>
public unsafe class DRGGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.DragoonGauge>
{ {
/// <summary> /// <summary>
/// In-memory DRG job gauge. /// Initializes a new instance of the <see cref="DRGGauge"/> class.
/// </summary> /// </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 Life of the Dragon in milliseconds.
/// </summary>
public short LOTDTimer => this.Struct->LotdTimer;
/// <summary>
/// Gets a value indicating whether Life of the Dragon is active.
/// </summary>
public bool IsLOTDActive => this.Struct->LotdState == 2;
/// <summary>
/// Gets the count of eyes opened during Blood of the Dragon.
/// </summary>
public byte EyeCount => this.Struct->EyeCount;
/// <summary>
/// Gets the amount of Firstminds' Focus available.
/// </summary>
public byte FirstmindsFocusCount => this.Struct->FirstmindsFocusCount;
} }
/// <summary>
/// Gets the time remaining for Life of the Dragon in milliseconds.
/// </summary>
public short LOTDTimer => this.Struct->LotdTimer;
/// <summary>
/// Gets a value indicating whether Life of the Dragon is active.
/// </summary>
public bool IsLOTDActive => this.Struct->LotdState == 2;
/// <summary>
/// Gets the count of eyes opened during Blood of the Dragon.
/// </summary>
public byte EyeCount => this.Struct->EyeCount;
/// <summary>
/// Gets the amount of Firstminds' Focus available.
/// </summary>
public byte FirstmindsFocusCount => this.Struct->FirstmindsFocusCount;
} }

View file

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

View file

@ -1,25 +1,24 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// Base job gauge class.
/// </summary>
/// <typeparam name="T">The underlying FFXIVClientStructs type.</typeparam>
public unsafe class JobGaugeBase<T> : JobGaugeBase where T : unmanaged
{ {
/// <summary> /// <summary>
/// Base job gauge class. /// Initializes a new instance of the <see cref="JobGaugeBase{T}"/> class.
/// </summary> /// </summary>
/// <typeparam name="T">The underlying FFXIVClientStructs type.</typeparam> /// <param name="address">Address of the job gauge.</param>
public unsafe class JobGaugeBase<T> : JobGaugeBase where T : unmanaged 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; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// In-memory MCH job gauge.
/// </summary>
public unsafe class MCHGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.MachinistGauge>
{ {
/// <summary> /// <summary>
/// In-memory MCH job gauge. /// Initializes a new instance of the <see cref="MCHGauge"/> class.
/// </summary> /// </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

@ -3,43 +3,42 @@ using System.Linq;
using Dalamud.Game.ClientState.JobGauge.Enums; using Dalamud.Game.ClientState.JobGauge.Enums;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// In-memory MNK job gauge.
/// </summary>
public unsafe class MNKGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.MonkGauge>
{ {
/// <summary> /// <summary>
/// In-memory MNK job gauge. /// Initializes a new instance of the <see cref="MNKGauge"/> class.
/// </summary> /// </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 amount of Chakra available.
/// </summary>
public byte Chakra => this.Struct->Chakra;
/// <summary>
/// Gets the types of Beast Chakra available.
/// </summary>
/// <remarks>
/// This will always return an array of size 3, inactive Beast Chakra are represented by <see cref="BeastChakra.NONE"/>.
/// </remarks>
public BeastChakra[] BeastChakra => this.Struct->BeastChakra.Select(c => (BeastChakra)c).ToArray();
/// <summary>
/// Gets the types of Nadi available.
/// </summary>
public Nadi Nadi => (Nadi)this.Struct->Nadi;
/// <summary>
/// Gets the time remaining that Blitz is active.
/// </summary>
public ushort BlitzTimeRemaining => this.Struct->BlitzTimeRemaining;
} }
/// <summary>
/// Gets the amount of Chakra available.
/// </summary>
public byte Chakra => this.Struct->Chakra;
/// <summary>
/// Gets the types of Beast Chakra available.
/// </summary>
/// <remarks>
/// This will always return an array of size 3, inactive Beast Chakra are represented by <see cref="BeastChakra.NONE"/>.
/// </remarks>
public BeastChakra[] BeastChakra => this.Struct->BeastChakra.Select(c => (BeastChakra)c).ToArray();
/// <summary>
/// Gets the types of Nadi available.
/// </summary>
public Nadi Nadi => (Nadi)this.Struct->Nadi;
/// <summary>
/// Gets the time remaining that Blitz is active.
/// </summary>
public ushort BlitzTimeRemaining => this.Struct->BlitzTimeRemaining;
} }

View file

@ -1,34 +1,33 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// In-memory NIN job gauge.
/// </summary>
public unsafe class NINGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.NinjaGauge>
{ {
/// <summary> /// <summary>
/// In-memory NIN job gauge. /// Initializes a new instance of the <see cref="NINGauge"/> class.
/// </summary> /// </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; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// In-memory PLD job gauge.
/// </summary>
public unsafe class PLDGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.PaladinGauge>
{ {
/// <summary> /// <summary>
/// In-memory PLD job gauge. /// Initializes a new instance of the <see cref="PLDGauge"/> class.
/// </summary> /// </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,34 +1,33 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// In-memory RDM job gauge.
/// </summary>
public unsafe class RDMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.RedMageGauge>
{ {
/// <summary> /// <summary>
/// In-memory RDM job gauge. /// Initializes a new instance of the <see cref="RDMGauge"/> class.
/// </summary> /// </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 amount of mana stacks.
/// </summary>
public byte ManaStacks => this.Struct->ManaStacks;
} }
/// <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 amount of mana stacks.
/// </summary>
public byte ManaStacks => this.Struct->ManaStacks;
} }

View file

@ -1,44 +1,43 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// In-memory RPR job gauge.
/// </summary>
public unsafe class RPRGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.ReaperGauge>
{ {
/// <summary> /// <summary>
/// In-memory RPR job gauge. /// Initializes a new instance of the <see cref="RPRGauge"/> class.
/// </summary> /// </summary>
public unsafe class RPRGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.ReaperGauge> /// <param name="address">Address of the job gauge.</param>
internal RPRGauge(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="RPRGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal RPRGauge(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the amount of Soul available.
/// </summary>
public byte Soul => this.Struct->Soul;
/// <summary>
/// Gets the amount of Shroud available.
/// </summary>
public byte Shroud => this.Struct->Shroud;
/// <summary>
/// Gets the time remaining that Enshrouded is active.
/// </summary>
public ushort EnshroudedTimeRemaining => this.Struct->EnshroudedTimeRemaining;
/// <summary>
/// Gets the amount of Lemure Shroud available.
/// </summary>
public byte LemureShroud => this.Struct->LemureShroud;
/// <summary>
/// Gets the amount of Void Shroud available.
/// </summary>
public byte VoidShroud => this.Struct->VoidShroud;
} }
/// <summary>
/// Gets the amount of Soul available.
/// </summary>
public byte Soul => this.Struct->Soul;
/// <summary>
/// Gets the amount of Shroud available.
/// </summary>
public byte Shroud => this.Struct->Shroud;
/// <summary>
/// Gets the time remaining that Enshrouded is active.
/// </summary>
public ushort EnshroudedTimeRemaining => this.Struct->EnshroudedTimeRemaining;
/// <summary>
/// Gets the amount of Lemure Shroud available.
/// </summary>
public byte LemureShroud => this.Struct->LemureShroud;
/// <summary>
/// Gets the amount of Void Shroud available.
/// </summary>
public byte VoidShroud => this.Struct->VoidShroud;
} }

View file

@ -2,58 +2,57 @@ using System;
using Dalamud.Game.ClientState.JobGauge.Enums; using Dalamud.Game.ClientState.JobGauge.Enums;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// In-memory SAM job gauge.
/// </summary>
public unsafe class SAMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.SamuraiGauge>
{ {
/// <summary> /// <summary>
/// In-memory SAM job gauge. /// Initializes a new instance of the <see cref="SAMGauge"/> class.
/// </summary> /// </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 currently active Kaeshi ability.
/// </summary>
public Kaeshi Kaeshi => (Kaeshi)this.Struct->Kaeshi;
/// <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 currently active Kaeshi ability.
/// </summary>
public Kaeshi Kaeshi => (Kaeshi)this.Struct->Kaeshi;
/// <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; using Dalamud.Game.ClientState.JobGauge.Enums;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// In-memory SCH job gauge.
/// </summary>
public unsafe class SCHGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.ScholarGauge>
{ {
/// <summary> /// <summary>
/// In-memory SCH job gauge. /// Initializes a new instance of the <see cref="SCHGauge"/> class.
/// </summary> /// </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 remaining time Seraph is active 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 remaining time Seraph is active 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

@ -1,40 +1,39 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// In-memory SGE job gauge.
/// </summary>
public unsafe class SGEGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.SageGauge>
{ {
/// <summary> /// <summary>
/// In-memory SGE job gauge. /// Initializes a new instance of the <see cref="SGEGauge"/> class.
/// </summary> /// </summary>
public unsafe class SGEGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.SageGauge> /// <param name="address">Address of the job gauge.</param>
internal SGEGauge(IntPtr address)
: base(address)
{ {
/// <summary>
/// Initializes a new instance of the <see cref="SGEGauge"/> class.
/// </summary>
/// <param name="address">Address of the job gauge.</param>
internal SGEGauge(IntPtr address)
: base(address)
{
}
/// <summary>
/// Gets the amount of milliseconds elapsed until the next Addersgall is available.
/// This counts from 0 to 20_000.
/// </summary>
public short AddersgallTimer => this.Struct->AddersgallTimer;
/// <summary>
/// Gets the amount of Addersgall available.
/// </summary>
public byte Addersgall => this.Struct->Addersgall;
/// <summary>
/// Gets the amount of Addersting available.
/// </summary>
public byte Addersting => this.Struct->Addersting;
/// <summary>
/// Gets a value indicating whether Eukrasia is activated.
/// </summary>
public bool Eukrasia => this.Struct->Eukrasia == 1;
} }
/// <summary>
/// Gets the amount of milliseconds elapsed until the next Addersgall is available.
/// This counts from 0 to 20_000.
/// </summary>
public short AddersgallTimer => this.Struct->AddersgallTimer;
/// <summary>
/// Gets the amount of Addersgall available.
/// </summary>
public byte Addersgall => this.Struct->Addersgall;
/// <summary>
/// Gets the amount of Addersting available.
/// </summary>
public byte Addersting => this.Struct->Addersting;
/// <summary>
/// Gets a value indicating whether Eukrasia is activated.
/// </summary>
public bool Eukrasia => this.Struct->Eukrasia == 1;
} }

View file

@ -3,112 +3,111 @@ using System;
using Dalamud.Game.ClientState.JobGauge.Enums; using Dalamud.Game.ClientState.JobGauge.Enums;
using FFXIVClientStructs.FFXIV.Client.Game.Gauge; using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// In-memory SMN job gauge.
/// </summary>
public unsafe class SMNGauge : JobGaugeBase<SummonerGauge>
{ {
/// <summary> /// <summary>
/// In-memory SMN job gauge. /// Initializes a new instance of the <see cref="SMNGauge"/> class.
/// </summary> /// </summary>
public unsafe class SMNGauge : JobGaugeBase<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 ushort SummonTimerRemaining => this.Struct->SummonTimer;
/// <summary>
/// Gets the time remaining for the current attunement.
/// </summary>
public ushort AttunmentTimerRemaining => this.Struct->AttunementTimer;
/// <summary>
/// Gets the summon that will return after the current summon expires.
/// This maps to the <see cref="Lumina.Excel.GeneratedSheets.Pet"/> sheet.
/// </summary>
public SummonPet ReturnSummon => (SummonPet)this.Struct->ReturnSummon;
/// <summary>
/// Gets the summon glam for the <see cref="ReturnSummon"/>.
/// This maps to the <see cref="Lumina.Excel.GeneratedSheets.PetMirage"/> sheet.
/// </summary>
public PetGlam ReturnSummonGlam => (PetGlam)this.Struct->ReturnSummonGlam;
/// <summary>
/// Gets the amount of aspected Attunment remaining.
/// </summary>
public byte Attunement => this.Struct->Attunement;
/// <summary>
/// Gets the current aether flags.
/// Use the summon accessors instead.
/// </summary>
public AetherFlags AetherFlags => this.Struct->AetherFlags;
/// <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.HasFlag(AetherFlags.PhoenixReady);
/// <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.HasFlag(AetherFlags.PhoenixReady);
/// <summary>
/// Gets a value indicating whether if Ifrit is ready to be summoned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsIfritReady => this.AetherFlags.HasFlag(AetherFlags.IfritReady);
/// <summary>
/// Gets a value indicating whether if Titan is ready to be summoned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsTitanReady => this.AetherFlags.HasFlag(AetherFlags.TitanReady);
/// <summary>
/// Gets a value indicating whether if Garuda is ready to be summoned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsGarudaReady => this.AetherFlags.HasFlag(AetherFlags.GarudaReady);
/// <summary>
/// Gets a value indicating whether if Ifrit is currently attuned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsIfritAttuned => this.AetherFlags.HasFlag(AetherFlags.IfritAttuned) && !this.AetherFlags.HasFlag(AetherFlags.GarudaAttuned);
/// <summary>
/// Gets a value indicating whether if Titan is currently attuned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsTitanAttuned => this.AetherFlags.HasFlag(AetherFlags.TitanAttuned) && !this.AetherFlags.HasFlag(AetherFlags.GarudaAttuned);
/// <summary>
/// Gets a value indicating whether if Garuda is currently attuned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsGarudaAttuned => this.AetherFlags.HasFlag(AetherFlags.GarudaAttuned);
/// <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.AetherflowStacks > 0;
/// <summary>
/// Gets the amount of Aetherflow available.
/// </summary>
public byte AetherflowStacks => (byte)(this.AetherFlags & AetherFlags.Aetherflow);
} }
/// <summary>
/// Gets the time remaining for the current summon.
/// </summary>
public ushort SummonTimerRemaining => this.Struct->SummonTimer;
/// <summary>
/// Gets the time remaining for the current attunement.
/// </summary>
public ushort AttunmentTimerRemaining => this.Struct->AttunementTimer;
/// <summary>
/// Gets the summon that will return after the current summon expires.
/// This maps to the <see cref="Lumina.Excel.GeneratedSheets.Pet"/> sheet.
/// </summary>
public SummonPet ReturnSummon => (SummonPet)this.Struct->ReturnSummon;
/// <summary>
/// Gets the summon glam for the <see cref="ReturnSummon"/>.
/// This maps to the <see cref="Lumina.Excel.GeneratedSheets.PetMirage"/> sheet.
/// </summary>
public PetGlam ReturnSummonGlam => (PetGlam)this.Struct->ReturnSummonGlam;
/// <summary>
/// Gets the amount of aspected Attunment remaining.
/// </summary>
public byte Attunement => this.Struct->Attunement;
/// <summary>
/// Gets the current aether flags.
/// Use the summon accessors instead.
/// </summary>
public AetherFlags AetherFlags => this.Struct->AetherFlags;
/// <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.HasFlag(AetherFlags.PhoenixReady);
/// <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.HasFlag(AetherFlags.PhoenixReady);
/// <summary>
/// Gets a value indicating whether if Ifrit is ready to be summoned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsIfritReady => this.AetherFlags.HasFlag(AetherFlags.IfritReady);
/// <summary>
/// Gets a value indicating whether if Titan is ready to be summoned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsTitanReady => this.AetherFlags.HasFlag(AetherFlags.TitanReady);
/// <summary>
/// Gets a value indicating whether if Garuda is ready to be summoned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsGarudaReady => this.AetherFlags.HasFlag(AetherFlags.GarudaReady);
/// <summary>
/// Gets a value indicating whether if Ifrit is currently attuned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsIfritAttuned => this.AetherFlags.HasFlag(AetherFlags.IfritAttuned) && !this.AetherFlags.HasFlag(AetherFlags.GarudaAttuned);
/// <summary>
/// Gets a value indicating whether if Titan is currently attuned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsTitanAttuned => this.AetherFlags.HasFlag(AetherFlags.TitanAttuned) && !this.AetherFlags.HasFlag(AetherFlags.GarudaAttuned);
/// <summary>
/// Gets a value indicating whether if Garuda is currently attuned.
/// </summary>
/// <returns><c>true</c> or <c>false</c>.</returns>
public bool IsGarudaAttuned => this.AetherFlags.HasFlag(AetherFlags.GarudaAttuned);
/// <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.AetherflowStacks > 0;
/// <summary>
/// Gets the amount of Aetherflow available.
/// </summary>
public byte AetherflowStacks => (byte)(this.AetherFlags & AetherFlags.Aetherflow);
} }

View file

@ -1,24 +1,23 @@
using System; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// In-memory WAR job gauge.
/// </summary>
public unsafe class WARGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.WarriorGauge>
{ {
/// <summary> /// <summary>
/// In-memory WAR job gauge. /// Initializes a new instance of the <see cref="WARGauge"/> class.
/// </summary> /// </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; using System;
namespace Dalamud.Game.ClientState.JobGauge.Types namespace Dalamud.Game.ClientState.JobGauge.Types;
/// <summary>
/// In-memory WHM job gauge.
/// </summary>
public unsafe class WHMGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game.Gauge.WhiteMageGauge>
{ {
/// <summary> /// <summary>
/// In-memory WHM job gauge. /// Initializes a new instance of the <see cref="WHMGauge"/> class.
/// </summary> /// </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,153 +6,152 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.Keys namespace Dalamud.Game.ClientState.Keys;
/// <summary>
/// Wrapper around the game keystate buffer, which contains the pressed state for all keyboard keys, indexed by virtual vkCode.
/// </summary>
/// <remarks>
/// The stored key state is actually a combination field, however the below ephemeral states are consumed each frame. Setting
/// the value may be mildly useful, however retrieving the value is largely pointless. In testing, it wasn't possible without
/// setting the statue manually.
/// index &amp; 0 = key pressed.
/// index &amp; 1 = key down (ephemeral).
/// index &amp; 2 = key up (ephemeral).
/// index &amp; 3 = short key press (ephemeral).
/// </remarks>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public class KeyState : IServiceType
{ {
/// <summary> // The array is accessed in a way that this limit doesn't appear to exist
/// Wrapper around the game keystate buffer, which contains the pressed state for all keyboard keys, indexed by virtual vkCode. // but there is other state data past this point, and keys beyond here aren't
/// </summary> // generally valid for most things anyway
/// <remarks> private const int MaxKeyCode = 0xF0;
/// The stored key state is actually a combination field, however the below ephemeral states are consumed each frame. Setting private readonly IntPtr bufferBase;
/// the value may be mildly useful, however retrieving the value is largely pointless. In testing, it wasn't possible without private readonly IntPtr indexBase;
/// setting the statue manually. private VirtualKey[] validVirtualKeyCache = null;
/// index &amp; 0 = key pressed.
/// index &amp; 1 = key down (ephemeral). [ServiceManager.ServiceConstructor]
/// index &amp; 2 = key up (ephemeral). private KeyState(SigScanner sigScanner, ClientState clientState)
/// index &amp; 3 = short key press (ephemeral).
/// </remarks>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public class KeyState : IServiceType
{ {
// The array is accessed in a way that this limit doesn't appear to exist var moduleBaseAddress = sigScanner.Module.BaseAddress;
// but there is other state data past this point, and keys beyond here aren't var addressResolver = clientState.AddressResolver;
// generally valid for most things anyway this.bufferBase = moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardState);
private const int MaxKeyCode = 0xF0; this.indexBase = moduleBaseAddress + Marshal.ReadInt32(addressResolver.KeyboardStateIndexArray);
private readonly IntPtr bufferBase;
private readonly IntPtr indexBase;
private VirtualKey[] validVirtualKeyCache = null;
[ServiceManager.ServiceConstructor] Log.Verbose($"Keyboard state buffer address 0x{this.bufferBase.ToInt64():X}");
private KeyState(SigScanner sigScanner, ClientState clientState) }
/// <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 = sigScanner.Module.BaseAddress; this[vk] = false;
var addressResolver = clientState.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())
{
this[vk] = false;
}
}
/// <summary>
/// Converts a virtual key into the equivalent value that the game uses.
/// Valid values are non-zero.
/// </summary>
/// <param name="vkCode">Virtual key.</param>
/// <returns>Converted value.</returns>
private unsafe byte ConvertVirtualKey(int vkCode)
{
if (vkCode <= 0 || vkCode >= MaxKeyCode)
return 0;
return *(byte*)(this.indexBase + vkCode);
}
/// <summary>
/// Gets the raw value from the key state array.
/// </summary>
/// <param name="vkCode">Virtual key code.</param>
/// <returns>A reference to the indexed array.</returns>
private unsafe ref int GetRefValue(int vkCode)
{
vkCode = this.ConvertVirtualKey(vkCode);
if (vkCode == 0)
throw new ArgumentException($"Keycode state is only valid for certain values. Reference GetValidVirtualKeys for help.");
return ref *(int*)(this.bufferBase + (4 * vkCode));
} }
} }
/// <summary>
/// Converts a virtual key into the equivalent value that the game uses.
/// Valid values are non-zero.
/// </summary>
/// <param name="vkCode">Virtual key.</param>
/// <returns>Converted value.</returns>
private unsafe byte ConvertVirtualKey(int vkCode)
{
if (vkCode <= 0 || vkCode >= MaxKeyCode)
return 0;
return *(byte*)(this.indexBase + vkCode);
}
/// <summary>
/// Gets the raw value from the key state array.
/// </summary>
/// <param name="vkCode">Virtual key code.</param>
/// <returns>A reference to the indexed array.</returns>
private unsafe ref int GetRefValue(int vkCode)
{
vkCode = this.ConvertVirtualKey(vkCode);
if (vkCode == 0)
throw new ArgumentException($"Keycode state is only valid for certain values. Reference GetValidVirtualKeys for help.");
return ref *(int*)(this.bufferBase + (4 * vkCode));
}
} }

File diff suppressed because it is too large Load diff

View file

@ -1,25 +1,24 @@
using System; using System;
namespace Dalamud.Game.ClientState.Keys namespace Dalamud.Game.ClientState.Keys;
/// <summary>
/// Attribute describing a VirtualKey.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
internal sealed class VirtualKeyAttribute : Attribute
{ {
/// <summary> /// <summary>
/// Attribute describing a VirtualKey. /// Initializes a new instance of the <see cref="VirtualKeyAttribute"/> class.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Field)] /// <param name="fancyName">Fancy name of this key.</param>
internal sealed class VirtualKeyAttribute : Attribute public VirtualKeyAttribute(string fancyName)
{ {
/// <summary> this.FancyName = fancyName;
/// Initializes a new instance of the <see cref="VirtualKeyAttribute"/> class.
/// </summary>
/// <param name="fancyName">Fancy name of this key.</param>
public VirtualKeyAttribute(string fancyName)
{
this.FancyName = fancyName;
}
/// <summary>
/// Gets the fancy name of this virtual key.
/// </summary>
public string FancyName { get; init; }
} }
/// <summary>
/// Gets the fancy name of this virtual key.
/// </summary>
public string FancyName { get; init; }
} }

View file

@ -1,20 +1,19 @@
using Dalamud.Utility; using Dalamud.Utility;
namespace Dalamud.Game.ClientState.Keys namespace Dalamud.Game.ClientState.Keys;
/// <summary>
/// Extension methods for <see cref="VirtualKey"/>.
/// </summary>
public static class VirtualKeyExtensions
{ {
/// <summary> /// <summary>
/// Extension methods for <see cref="VirtualKey"/>. /// Get the fancy name associated with this key.
/// </summary> /// </summary>
public static class VirtualKeyExtensions /// <param name="key">The they key to act on.</param>
/// <returns>The key's fancy name.</returns>
public static string GetFancyName(this VirtualKey key)
{ {
/// <summary> return key.GetAttribute<VirtualKeyAttribute>().FancyName;
/// Get the fancy name associated with this key.
/// </summary>
/// <param name="key">The they key to act on.</param>
/// <returns>The key's fancy name.</returns>
public static string GetFancyName(this VirtualKey key)
{
return key.GetAttribute<VirtualKeyAttribute>().FancyName;
}
} }
} }

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

View file

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

View file

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

View file

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

View file

@ -9,138 +9,137 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.Objects namespace Dalamud.Game.ClientState.Objects;
/// <summary>
/// This collection represents the currently spawned FFXIV game objects.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed partial class ObjectTable : IServiceType
{ {
/// <summary> private const int ObjectTableLength = 596;
/// This collection represents the currently spawned FFXIV game objects.
/// </summary> private readonly ClientStateAddressResolver address;
[PluginInterface]
[InterfaceVersion("1.0")] [ServiceManager.ServiceConstructor]
[ServiceManager.BlockingEarlyLoadedService] private ObjectTable(ClientState clientState)
public sealed partial class ObjectTable : IServiceType
{ {
private const int ObjectTableLength = 596; this.address = clientState.AddressResolver;
private readonly ClientStateAddressResolver address; Log.Verbose($"Object table address 0x{this.address.ObjectTable.ToInt64():X}");
}
[ServiceManager.ServiceConstructor] /// <summary>
private ObjectTable(ClientState clientState) /// 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 = clientState.AddressResolver; var address = this.GetObjectAddress(index);
return this.CreateObjectReference(address);
Log.Verbose($"Object table address 0x{this.address.ObjectTable.ToInt64():X}");
}
/// <summary>
/// Gets the address of the object table.
/// </summary>
public IntPtr Address => this.address.ObjectTable;
/// <summary>
/// Gets the length of the object table.
/// </summary>
public int Length => ObjectTableLength;
/// <summary>
/// Get an object at the specified spawn index.
/// </summary>
/// <param name="index">Spawn index.</param>
/// <returns>An <see cref="GameObject"/> at the specified spawn index.</returns>
public GameObject? this[int index]
{
get
{
var address = this.GetObjectAddress(index);
return this.CreateObjectReference(address);
}
}
/// <summary>
/// Search for a game object by their Object ID.
/// </summary>
/// <param name="objectId">Object ID to find.</param>
/// <returns>A game object or null.</returns>
public GameObject? SearchById(uint objectId)
{
if (objectId is GameObject.InvalidGameObjectId or 0)
return null;
foreach (var obj in this)
{
if (obj == null)
continue;
if (obj.ObjectId == objectId)
return obj;
}
return null;
}
/// <summary>
/// Gets the address of the game object at the specified index of the object table.
/// </summary>
/// <param name="index">The index of the object.</param>
/// <returns>The memory address of the object.</returns>
public unsafe IntPtr GetObjectAddress(int index)
{
if (index < 0 || index >= ObjectTableLength)
return IntPtr.Zero;
return *(IntPtr*)(this.address.ObjectTable + (8 * index));
}
/// <summary>
/// Create a reference to an FFXIV game object.
/// </summary>
/// <param name="address">The address of the object in memory.</param>
/// <returns><see cref="GameObject"/> object or inheritor containing the requested data.</returns>
public unsafe GameObject? CreateObjectReference(IntPtr address)
{
var clientState = Service<ClientState>.GetNullable();
if (clientState == null || clientState.LocalContentId == 0)
return null;
if (address == IntPtr.Zero)
return null;
var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)address;
var objKind = (ObjectKind)obj->ObjectKind;
return objKind switch
{
ObjectKind.Player => new PlayerCharacter(address),
ObjectKind.BattleNpc => new BattleNpc(address),
ObjectKind.EventObj => new EventObj(address),
ObjectKind.Companion => new Npc(address),
_ => new GameObject(address),
};
} }
} }
/// <summary> /// <summary>
/// This collection represents the currently spawned FFXIV game objects. /// Search for a game object by their Object ID.
/// </summary> /// </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/> if (objectId is GameObject.InvalidGameObjectId or 0)
int IReadOnlyCollection<GameObject>.Count => this.Length; return null;
/// <inheritdoc/> foreach (var obj in this)
public IEnumerator<GameObject> GetEnumerator()
{ {
for (var i = 0; i < ObjectTableLength; i++) if (obj == null)
{ continue;
var obj = this[i];
if (obj == null) if (obj.ObjectId == objectId)
continue; return obj;
yield return obj;
}
} }
/// <inheritdoc/> return null;
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); }
/// <summary>
/// Gets the address of the game object at the specified index of the object table.
/// </summary>
/// <param name="index">The index of the object.</param>
/// <returns>The memory address of the object.</returns>
public unsafe IntPtr GetObjectAddress(int index)
{
if (index < 0 || index >= ObjectTableLength)
return IntPtr.Zero;
return *(IntPtr*)(this.address.ObjectTable + (8 * index));
}
/// <summary>
/// Create a reference to an FFXIV game object.
/// </summary>
/// <param name="address">The address of the object in memory.</param>
/// <returns><see cref="GameObject"/> object or inheritor containing the requested data.</returns>
public unsafe GameObject? CreateObjectReference(IntPtr address)
{
var clientState = Service<ClientState>.GetNullable();
if (clientState == null || 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; using Dalamud.Game.ClientState.Objects.Enums;
namespace Dalamud.Game.ClientState.Objects.Types namespace Dalamud.Game.ClientState.Objects.Types;
/// <summary>
/// This class represents a battle NPC.
/// </summary>
public unsafe class BattleNpc : BattleChara
{ {
/// <summary> /// <summary>
/// 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> /// </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; using Dalamud.Game.ClientState.Objects.Types;
namespace Dalamud.Game.ClientState.Objects.SubKinds namespace Dalamud.Game.ClientState.Objects.SubKinds;
/// <summary>
/// This class represents an EventObj.
/// </summary>
public unsafe class EventObj : GameObject
{ {
/// <summary> /// <summary>
/// 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> /// </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; using Dalamud.Game.ClientState.Objects.Types;
namespace Dalamud.Game.ClientState.Objects.SubKinds namespace Dalamud.Game.ClientState.Objects.SubKinds;
/// <summary>
/// This class represents a NPC.
/// </summary>
public unsafe class Npc : Character
{ {
/// <summary> /// <summary>
/// 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> /// </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.Objects.Types;
using Dalamud.Game.ClientState.Resolvers; using Dalamud.Game.ClientState.Resolvers;
namespace Dalamud.Game.ClientState.Objects.SubKinds namespace Dalamud.Game.ClientState.Objects.SubKinds;
/// <summary>
/// This class represents a player character.
/// </summary>
public unsafe class PlayerCharacter : BattleChara
{ {
/// <summary> /// <summary>
/// This class represents a player character. /// Initializes a new instance of the <see cref="PlayerCharacter"/> class.
/// This represents a player character.
/// </summary> /// </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,165 +4,164 @@ using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
namespace Dalamud.Game.ClientState.Objects namespace Dalamud.Game.ClientState.Objects;
/// <summary>
/// Get and set various kinds of targets for the player.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed unsafe class TargetManager : IServiceType
{ {
/// <summary> [ServiceManager.ServiceDependency]
/// Get and set various kinds of targets for the player. private readonly ClientState clientState = Service<ClientState>.Get();
/// </summary>
[PluginInterface] [ServiceManager.ServiceDependency]
[InterfaceVersion("1.0")] private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
[ServiceManager.BlockingEarlyLoadedService]
public sealed unsafe class TargetManager : IServiceType private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor]
private TargetManager()
{ {
[ServiceManager.ServiceDependency] this.address = this.clientState.AddressResolver;
private readonly ClientState clientState = Service<ClientState>.Get();
[ServiceManager.ServiceDependency]
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor]
private TargetManager()
{
this.address = this.clientState.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 => this.objectTable.CreateObjectReference((IntPtr)Struct->Target);
set => this.SetTarget(value);
}
/// <summary>
/// Gets or sets the mouseover target.
/// </summary>
public GameObject? MouseOverTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverTarget);
set => this.SetMouseOverTarget(value);
}
/// <summary>
/// Gets or sets the focus target.
/// </summary>
public GameObject? FocusTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->FocusTarget);
set => this.SetFocusTarget(value);
}
/// <summary>
/// Gets or sets the previous target.
/// </summary>
public GameObject? PreviousTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->PreviousTarget);
set => this.SetPreviousTarget(value);
}
/// <summary>
/// Gets or sets the soft target.
/// </summary>
public GameObject? SoftTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->SoftTarget);
set => this.SetSoftTarget(value);
}
private FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem*)this.Address;
/// <summary>
/// Sets the current target.
/// </summary>
/// <param name="actor">Actor to target.</param>
public void SetTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the mouseover target.
/// </summary>
/// <param name="actor">Actor to target.</param>
public void SetMouseOverTarget(GameObject? actor) => this.SetMouseOverTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the focus target.
/// </summary>
/// <param name="actor">Actor to target.</param>
public void SetFocusTarget(GameObject? actor) => this.SetFocusTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the previous target.
/// </summary>
/// <param name="actor">Actor to target.</param>
public void SetPreviousTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the soft target.
/// </summary>
/// <param name="actor">Actor to target.</param>
public void SetSoftTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero);
/// <summary>
/// Sets the current target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
public void SetTarget(IntPtr actorAddress) => Struct->Target = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the mouseover target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
public void SetMouseOverTarget(IntPtr actorAddress) => Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the focus target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
public void SetFocusTarget(IntPtr actorAddress) => Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the previous target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
public void SetPreviousTarget(IntPtr actorAddress) => Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Sets the soft target.
/// </summary>
/// <param name="actorAddress">Actor (address) to target.</param>
public void SetSoftTarget(IntPtr actorAddress) => Struct->SoftTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress;
/// <summary>
/// Clears the current target.
/// </summary>
public void ClearTarget() => this.SetTarget(IntPtr.Zero);
/// <summary>
/// Clears the mouseover target.
/// </summary>
public void ClearMouseOverTarget() => this.SetMouseOverTarget(IntPtr.Zero);
/// <summary>
/// Clears the focus target.
/// </summary>
public void ClearFocusTarget() => this.SetFocusTarget(IntPtr.Zero);
/// <summary>
/// Clears the previous target.
/// </summary>
public void ClearPreviousTarget() => this.SetPreviousTarget(IntPtr.Zero);
/// <summary>
/// Clears the soft target.
/// </summary>
public void ClearSoftTarget() => this.SetSoftTarget(IntPtr.Zero);
} }
/// <summary>
/// Gets the address of the target manager.
/// </summary>
public IntPtr Address => this.address.TargetManager;
/// <summary>
/// Gets or sets the current target.
/// </summary>
public GameObject? Target
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->Target);
set => this.SetTarget(value);
}
/// <summary>
/// Gets or sets the mouseover target.
/// </summary>
public GameObject? MouseOverTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverTarget);
set => this.SetMouseOverTarget(value);
}
/// <summary>
/// Gets or sets the focus target.
/// </summary>
public GameObject? FocusTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->FocusTarget);
set => this.SetFocusTarget(value);
}
/// <summary>
/// Gets or sets the previous target.
/// </summary>
public GameObject? PreviousTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->PreviousTarget);
set => this.SetPreviousTarget(value);
}
/// <summary>
/// Gets or sets the soft target.
/// </summary>
public GameObject? SoftTarget
{
get => this.objectTable.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; using Dalamud.Game.ClientState.Statuses;
namespace Dalamud.Game.ClientState.Objects.Types namespace Dalamud.Game.ClientState.Objects.Types;
/// <summary>
/// This class represents the battle characters.
/// </summary>
public unsafe class BattleChara : Character
{ {
/// <summary> /// <summary>
/// This class represents the battle characters. /// Initializes a new instance of the <see cref="BattleChara"/> class.
/// This represents a battle character.
/// </summary> /// </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>
protected internal 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>
protected internal new FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Character.BattleChara*)this.Address;
} }

View file

@ -6,107 +6,106 @@ using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory; using Dalamud.Memory;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
namespace Dalamud.Game.ClientState.Objects.Types namespace Dalamud.Game.ClientState.Objects.Types;
/// <summary>
/// This class represents the base for non-static entities.
/// </summary>
public unsafe class Character : GameObject
{ {
/// <summary> /// <summary>
/// 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> /// </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<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 current online status of the character.
/// </summary>
public ExcelResolver<OnlineStatus> OnlineStatus => new(this.Struct->OnlineStatus);
/// <summary>
/// Gets the status flags.
/// </summary>
public StatusFlags StatusFlags => (StatusFlags)this.Struct->StatusFlags;
/// <summary>
/// Gets the underlying structure.
/// </summary>
protected internal 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<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 current online status of the character.
/// </summary>
public ExcelResolver<OnlineStatus> OnlineStatus => new(this.Struct->OnlineStatus);
/// <summary>
/// Gets the status flags.
/// </summary>
public StatusFlags StatusFlags => (StatusFlags)this.Struct->StatusFlags;
/// <summary>
/// Gets the underlying structure.
/// </summary>
protected internal 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.Game.Text.SeStringHandling;
using Dalamud.Memory; using Dalamud.Memory;
namespace Dalamud.Game.ClientState.Objects.Types namespace Dalamud.Game.ClientState.Objects.Types;
/// <summary>
/// This class represents a GameObject in FFXIV.
/// </summary>
public unsafe partial class GameObject : IEquatable<GameObject>
{ {
/// <summary> /// <summary>
/// This class represents a GameObject in FFXIV. /// IDs of non-networked GameObjects.
/// </summary> /// </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> this.Address = address;
/// 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>.GetNullable();
if (actor is null || clientState == null)
return false;
if (clientState.LocalContentId == 0)
return false;
return true;
}
/// <summary>
/// Gets a value indicating whether this actor is still valid in memory.
/// </summary>
/// <returns>True or false.</returns>
public bool IsValid() => IsValid(this);
/// <inheritdoc/>
bool IEquatable<GameObject>.Equals(GameObject other) => this.ObjectId == other?.ObjectId;
/// <inheritdoc/>
public override bool Equals(object obj) => ((IEquatable<GameObject>)this).Equals(obj as GameObject);
/// <inheritdoc/>
public override int GetHashCode() => this.ObjectId.GetHashCode();
} }
/// <summary> /// <summary>
/// This class represents a basic actor (GameObject) in FFXIV. /// Gets the address of the game object in memory.
/// </summary> /// </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> // Using == results in a stack overflow.
/// Gets the name of this <see cref="GameObject" />. if (gameObject1 is null || gameObject2 is null)
/// </summary> return Equals(gameObject1, gameObject2);
public SeString Name => MemoryHelper.ReadSeString((IntPtr)this.Struct->Name, 64);
/// <summary> return gameObject1.Equals(gameObject2);
/// 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>
protected internal FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)this.Address;
/// <inheritdoc/>
public override string ToString() => $"{this.ObjectId:X}({this.Name.TextValue} - {this.ObjectKind}) at {this.Address:X}";
} }
public static bool operator !=(GameObject? actor1, GameObject? actor2) => !(actor1 == actor2);
/// <summary>
/// Gets a value indicating whether this actor is still valid in memory.
/// </summary>
/// <param name="actor">The actor to check.</param>
/// <returns>True or false.</returns>
public static bool IsValid(GameObject? actor)
{
var clientState = Service<ClientState>.GetNullable();
if (actor is null || clientState == 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>
protected internal 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,180 +7,179 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.Party namespace Dalamud.Game.ClientState.Party;
/// <summary>
/// This collection represents the actors present in your party or alliance.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed unsafe partial class PartyList : IServiceType
{ {
/// <summary> private const int GroupLength = 8;
/// This collection represents the actors present in your party or alliance. private const int AllianceLength = 20;
/// </summary>
[PluginInterface] [ServiceManager.ServiceDependency]
[InterfaceVersion("1.0")] private readonly ClientState clientState = Service<ClientState>.Get();
[ServiceManager.BlockingEarlyLoadedService]
public sealed unsafe partial class PartyList : IServiceType private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor]
private PartyList()
{ {
private const int GroupLength = 8; this.address = this.clientState.AddressResolver;
private const int AllianceLength = 20;
[ServiceManager.ServiceDependency] Log.Verbose($"Group manager address 0x{this.address.GroupManager.ToInt64():X}");
private readonly ClientState clientState = Service<ClientState>.Get();
private readonly ClientStateAddressResolver address;
[ServiceManager.ServiceConstructor]
private PartyList()
{
this.address = this.clientState.AddressResolver;
Log.Verbose($"Group manager address 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;
/// <summary>
/// Gets the ID of the party.
/// </summary>
public long PartyId => this.GroupManagerStruct->PartyId;
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)
{
if (this.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)
{
if (this.clientState.LocalContentId == 0)
return null;
if (address == IntPtr.Zero)
return null;
return new PartyMember(address);
}
} }
/// <summary> /// <summary>
/// This collection represents the party members present in your party or alliance. /// Gets the amount of party members the local player has.
/// </summary> /// </summary>
public sealed partial class PartyList : IReadOnlyCollection<PartyMember> public int Length => this.GroupManagerStruct->MemberCount;
{
/// <inheritdoc/>
int IReadOnlyCollection<PartyMember>.Count => this.Length;
/// <inheritdoc/> /// <summary>
public IEnumerator<PartyMember> GetEnumerator() /// 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;
/// <summary>
/// Gets the ID of the party.
/// </summary>
public long PartyId => this.GroupManagerStruct->PartyId;
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. // 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]; var addr = this.GetAllianceMemberAddress(index);
return this.CreateAllianceMemberReference(addr);
if (member == null) }
break; else
{
yield return member; var addr = this.GetPartyMemberAddress(index);
return this.CreatePartyMemberReference(addr);
} }
} }
}
/// <inheritdoc/> /// <summary>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); /// 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)
{
if (this.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)
{
if (this.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

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

View file

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

View file

@ -4,65 +4,64 @@ using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Resolvers; using Dalamud.Game.ClientState.Resolvers;
namespace Dalamud.Game.ClientState.Statuses namespace Dalamud.Game.ClientState.Statuses;
/// <summary>
/// This class represents a status effect an actor is afflicted by.
/// </summary>
public unsafe class Status
{ {
/// <summary> /// <summary>
/// This class represents a status effect an actor is afflicted by. /// Initializes a new instance of the <see cref="Status"/> class.
/// </summary> /// </summary>
public unsafe class Status /// <param name="address">Status address.</param>
internal Status(IntPtr address)
{ {
/// <summary> this.Address = address;
/// 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 ushort Param => this.Struct->Param;
/// <summary>
/// Gets the stack count of this status.
/// </summary>
public byte StackCount => this.Struct->StackCount;
/// <summary>
/// Gets the time remaining of this status.
/// </summary>
public float RemainingTime => this.Struct->RemainingTime;
/// <summary>
/// Gets the source ID of this status.
/// </summary>
public uint SourceId => this.Struct->SourceID;
/// <summary>
/// Gets the source actor associated with this status.
/// </summary>
/// <remarks>
/// This iterates the actor table, it should be used with care.
/// </remarks>
public GameObject? SourceObject => Service<ObjectTable>.Get().SearchById(this.SourceId);
private FFXIVClientStructs.FFXIV.Client.Game.Status* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Status*)this.Address;
} }
/// <summary>
/// Gets the address of the status in memory.
/// </summary>
public IntPtr Address { get; }
/// <summary>
/// Gets the status ID of this status.
/// </summary>
public uint StatusId => this.Struct->StatusID;
/// <summary>
/// Gets the GameData associated with this status.
/// </summary>
public Lumina.Excel.GeneratedSheets.Status GameData => new ExcelResolver<Lumina.Excel.GeneratedSheets.Status>(this.Struct->StatusID).GameData;
/// <summary>
/// Gets the parameter value of the status.
/// </summary>
public ushort 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.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Dalamud.Game.ClientState.Statuses namespace Dalamud.Game.ClientState.Statuses;
/// <summary>
/// This collection represents the status effects an actor is afflicted by.
/// </summary>
public sealed unsafe partial class StatusList
{ {
private const int StatusListLength = 30;
/// <summary> /// <summary>
/// This collection represents the status effects an actor is afflicted by. /// Initializes a new instance of the <see cref="StatusList"/> class.
/// </summary> /// </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> /// <summary>
/// Initializes a new instance of the <see cref="StatusList"/> class. /// Initializes a new instance of the <see cref="StatusList"/> class.
/// </summary> /// </summary>
/// <param name="address">Address of the status list.</param> /// <param name="pointer">Pointer to the status list.</param>
internal StatusList(IntPtr address) 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; if (index < 0 || index > StatusListLength)
}
/// <summary>
/// Initializes a new instance of the <see cref="StatusList"/> class.
/// </summary>
/// <param name="pointer">Pointer to the status list.</param>
internal unsafe StatusList(void* pointer)
: this((IntPtr)pointer)
{
}
/// <summary>
/// Gets the address of the status list in memory.
/// </summary>
public IntPtr Address { get; }
/// <summary>
/// Gets the amount of status effect slots the actor has.
/// </summary>
public int Length => StatusListLength;
private static int StatusSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.Status>();
private FFXIVClientStructs.FFXIV.Client.Game.StatusManager* Struct => (FFXIVClientStructs.FFXIV.Client.Game.StatusManager*)this.Address;
/// <summary>
/// Get a status effect at the specified index.
/// </summary>
/// <param name="index">Status Index.</param>
/// <returns>The status at the specified index.</returns>
public Status? this[int index]
{
get
{
if (index < 0 || index > StatusListLength)
return null;
var addr = this.GetStatusAddress(index);
return CreateStatusReference(addr);
}
}
/// <summary>
/// Create a reference to an FFXIV actor status list.
/// </summary>
/// <param name="address">The address of the status list in memory.</param>
/// <returns>The status object containing the requested data.</returns>
public static StatusList? CreateStatusListReference(IntPtr address)
{
// The use case for CreateStatusListReference and CreateStatusReference to be static is so
// fake status lists can be generated. Since they aren't exposed as services, it's either
// here or somewhere else.
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
return null; return null;
if (address == IntPtr.Zero) var addr = this.GetStatusAddress(index);
return null; return CreateStatusReference(addr);
return new StatusList(address);
}
/// <summary>
/// Create a reference to an FFXIV actor status.
/// </summary>
/// <param name="address">The address of the status effect in memory.</param>
/// <returns>The status object containing the requested data.</returns>
public static Status? CreateStatusReference(IntPtr address)
{
var clientState = Service<ClientState>.Get();
if (clientState.LocalContentId == 0)
return null;
if (address == IntPtr.Zero)
return null;
return new Status(address);
}
/// <summary>
/// Gets the address of the party member at the specified index of the party list.
/// </summary>
/// <param name="index">The index of the party member.</param>
/// <returns>The memory address of the party member.</returns>
public IntPtr GetStatusAddress(int index)
{
if (index < 0 || index >= StatusListLength)
return IntPtr.Zero;
return (IntPtr)(this.Struct->Status + (index * StatusSize));
} }
} }
/// <summary> /// <summary>
/// This collection represents the status effects an actor is afflicted by. /// Create a reference to an FFXIV actor status list.
/// </summary> /// </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/> // The use case for CreateStatusListReference and CreateStatusReference to be static is so
int IReadOnlyCollection<Status>.Count => this.Length; // 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/> if (clientState.LocalContentId == 0)
int ICollection.Count => this.Length; return null;
/// <inheritdoc/> if (address == IntPtr.Zero)
bool ICollection.IsSynchronized => false; return null;
/// <inheritdoc/> return new StatusList(address);
object ICollection.SyncRoot => this; }
/// <inheritdoc/> /// <summary>
public IEnumerator<Status> GetEnumerator() /// 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) if (status == null || status.StatusId == 0)
continue; continue;
yield return status; yield return status;
}
} }
}
/// <inheritdoc/> /// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
/// <inheritdoc/> /// <inheritdoc/>
void ICollection.CopyTo(Array array, int index) void ICollection.CopyTo(Array array, int index)
{
for (var i = 0; i < this.Length; i++)
{ {
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; using System.Runtime.InteropServices;
namespace Dalamud.Game.ClientState.Structs namespace Dalamud.Game.ClientState.Structs;
/// <summary>
/// Native memory representation of a FFXIV status effect.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct StatusEffect
{ {
/// <summary> /// <summary>
/// Native memory representation of a FFXIV status effect. /// The effect ID.
/// </summary> /// </summary>
[StructLayout(LayoutKind.Sequential)] public short EffectId;
public struct StatusEffect
{
/// <summary>
/// The effect ID.
/// </summary>
public short EffectId;
/// <summary> /// <summary>
/// How many stacks are present. /// How many stacks are present.
/// </summary> /// </summary>
public byte StackCount; public byte StackCount;
/// <summary> /// <summary>
/// Additional parameters. /// Additional parameters.
/// </summary> /// </summary>
public byte Param; public byte Param;
/// <summary> /// <summary>
/// The duration remaining. /// The duration remaining.
/// </summary> /// </summary>
public float Duration; public float Duration;
/// <summary> /// <summary>
/// The ID of the actor that caused this effect. /// The ID of the actor that caused this effect.
/// </summary> /// </summary>
public int OwnerId; public int OwnerId;
}
} }

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,49 +1,48 @@
using System; using System;
namespace Dalamud.Game namespace Dalamud.Game;
/// <summary>
/// The address resolver for the <see cref="Framework"/> class.
/// </summary>
public sealed unsafe class FrameworkAddressResolver : BaseAddressResolver
{ {
/// <summary> /// <summary>
/// The address resolver for the <see cref="Framework"/> class. /// Gets the base address of the Framework object.
/// </summary> /// </summary>
public sealed unsafe class FrameworkAddressResolver : BaseAddressResolver [Obsolete("Please use FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance() instead.")]
public IntPtr BaseAddress => new(FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance());
/// <summary>
/// Gets the address for the function that is called once the Framework is destroyed.
/// </summary>
public IntPtr DestroyAddress { get; private set; }
/// <summary>
/// Gets the address for the function that is called once the Framework is free'd.
/// </summary>
public IntPtr FreeAddress { get; private set; }
/// <summary>
/// Gets the function that is called every tick.
/// </summary>
public IntPtr TickAddress { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{ {
/// <summary> this.SetupFramework(sig);
/// Gets the base address of the Framework object. }
/// </summary>
[Obsolete("Please use FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance() instead.")]
public IntPtr BaseAddress => new(FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance());
/// <summary> private void SetupFramework(SigScanner scanner)
/// Gets the address for the function that is called once the Framework is destroyed. {
/// </summary> this.DestroyAddress =
public IntPtr DestroyAddress { get; private set; } scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B 3D ?? ?? ?? ?? 48 8B D9 48 85 FF");
/// <summary> this.FreeAddress =
/// Gets the address for the function that is called once the Framework is free'd. scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B D9 48 8B 0D ?? ?? ?? ??");
/// </summary>
public IntPtr FreeAddress { get; private set; }
/// <summary> this.TickAddress =
/// Gets the function that is called every tick. scanner.ScanText("40 53 48 83 EC 20 FF 81 ?? ?? ?? ?? 48 8B D9 48 8D 4C 24 ??");
/// </summary>
public IntPtr TickAddress { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{
this.SetupFramework(sig);
}
private void SetupFramework(SigScanner scanner)
{
this.DestroyAddress =
scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B 3D ?? ?? ?? ?? 48 8B D9 48 85 FF");
this.FreeAddress =
scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B D9 48 8B 0D ?? ?? ?? ??");
this.TickAddress =
scanner.ScanText("40 53 48 83 EC 20 FF 81 ?? ?? ?? ?? 48 8B D9 48 8D 4C 24 ??");
}
} }
} }

View file

@ -5,405 +5,404 @@ using System.Text;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud.Game namespace Dalamud.Game;
/// <summary>
/// A GameVersion object contains give hierarchical numeric components: year, month,
/// day, major and minor. All components may be unspecified, which is represented
/// internally as a -1. By definition, an unspecified component matches anything
/// (both unspecified and specified), and an unspecified component is "less than" any
/// specified component. It will also equal the string "any" if all components are
/// unspecified. The value can be retrieved from the ffxivgame.ver file in your game
/// installation directory.
/// </summary>
[Serializable]
public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersion>, IEquatable<GameVersion>
{ {
private static readonly GameVersion AnyVersion = new();
/// <summary> /// <summary>
/// A GameVersion object contains give hierarchical numeric components: year, month, /// Initializes a new instance of the <see cref="GameVersion"/> class.
/// day, major and minor. All components may be unspecified, which is represented
/// internally as a -1. By definition, an unspecified component matches anything
/// (both unspecified and specified), and an unspecified component is "less than" any
/// specified component. It will also equal the string "any" if all components are
/// unspecified. The value can be retrieved from the ffxivgame.ver file in your game
/// installation directory.
/// </summary> /// </summary>
[Serializable] /// <param name="version">Version string to parse.</param>
public sealed class GameVersion : ICloneable, IComparable, IComparable<GameVersion>, IEquatable<GameVersion> [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> /// <summary>
/// Initializes a new instance of the <see cref="GameVersion"/> class. /// Initializes a new instance of the <see cref="GameVersion"/> class.
/// </summary> /// </summary>
/// <param name="version">Version string to parse.</param> /// <param name="year">The year.</param>
[JsonConstructor] /// <param name="month">The month.</param>
public GameVersion(string version) /// <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); return v2 is null;
this.Year = ver.Year;
this.Month = ver.Month;
this.Day = ver.Day;
this.Major = ver.Major;
this.Minor = ver.Minor;
} }
/// <summary> return v1.Equals(v2);
/// Initializes a new instance of the <see cref="GameVersion"/> class. }
/// </summary>
/// <param name="year">The year.</param> public static bool operator !=(GameVersion v1, GameVersion v2)
/// <param name="month">The month.</param> {
/// <param name="day">The day.</param> return !(v1 == v2);
/// <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) 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) var result = int.TryParse(p, out var value);
throw new ArgumentOutOfRangeException(nameof(year)); return (result, value);
}).ToArray();
if ((this.Month = month) < 0) if (tplParts.Any(t => !t.result))
throw new ArgumentOutOfRangeException(nameof(month)); throw new FormatException("Bad formatting");
if ((this.Day = day) < 0) var intParts = tplParts.Select(t => t.value).ToArray();
throw new ArgumentOutOfRangeException(nameof(day)); var len = intParts.Length;
if ((this.Major = major) < 0) if (len == 1)
throw new ArgumentOutOfRangeException(nameof(major)); 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) /// <summary>
throw new ArgumentOutOfRangeException(nameof(minor)); /// 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
/// <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) result = null;
throw new ArgumentOutOfRangeException(nameof(year)); return false;
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();
} }
} }
/// <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; using Newtonsoft.Json;
namespace Dalamud.Game namespace Dalamud.Game;
/// <summary>
/// Converts a <see cref="GameVersion"/> to and from a string (e.g. <c>"2010.01.01.1234.5678"</c>).
/// </summary>
public sealed class GameVersionConverter : JsonConverter
{ {
/// <summary> /// <summary>
/// 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> /// </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> if (value == null)
/// Writes the JSON representation of the object.
/// </summary>
/// <param name="writer">The <see cref="JsonWriter"/> to write to.</param>
/// <param name="value">The value.</param>
/// <param name="serializer">The calling serializer.</param>
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{ {
if (value == null) writer.WriteNull();
{
writer.WriteNull();
}
else if (value is GameVersion)
{
writer.WriteValue(value.ToString());
}
else
{
throw new JsonSerializationException("Expected GameVersion object value");
}
} }
else if (value is GameVersion)
/// <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) writer.WriteValue(value.ToString());
{
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}");
}
}
} }
else
/// <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); 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

@ -14,446 +14,445 @@ using Dalamud.IoC.Internal;
using Dalamud.Utility; using Dalamud.Utility;
using Serilog; using Serilog;
namespace Dalamud.Game.Gui namespace Dalamud.Game.Gui;
/// <summary>
/// This class handles interacting with the native chat UI.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class ChatGui : IDisposable, IServiceType
{ {
/// <summary> private readonly ChatGuiAddressResolver address;
/// This class handles interacting with the native chat UI.
/// </summary> private readonly Queue<XivChatEntry> chatQueue = new();
[PluginInterface] private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = new();
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService] private readonly Hook<PrintMessageDelegate> printMessageHook;
public sealed class ChatGui : IDisposable, IServiceType private readonly Hook<PopulateItemLinkDelegate> populateItemLinkHook;
private readonly Hook<InteractableLinkClickedDelegate> interactableLinkClickedHook;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
[ServiceManager.ServiceDependency]
private readonly LibcFunction libcFunction = Service<LibcFunction>.Get();
private IntPtr baseAddress = IntPtr.Zero;
[ServiceManager.ServiceConstructor]
private ChatGui(SigScanner sigScanner)
{ {
private readonly ChatGuiAddressResolver address; this.address = new ChatGuiAddressResolver();
this.address.Setup(sigScanner);
private readonly Queue<XivChatEntry> chatQueue = new(); this.printMessageHook = Hook<PrintMessageDelegate>.FromAddress(this.address.PrintMessage, this.HandlePrintMessageDetour);
private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = new(); this.populateItemLinkHook = Hook<PopulateItemLinkDelegate>.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
this.interactableLinkClickedHook = Hook<InteractableLinkClickedDelegate>.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
}
private readonly Hook<PrintMessageDelegate> printMessageHook; /// <summary>
private readonly Hook<PopulateItemLinkDelegate> populateItemLinkHook; /// A delegate type used with the <see cref="ChatGui.ChatMessage"/> event.
private readonly Hook<InteractableLinkClickedDelegate> interactableLinkClickedHook; /// </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);
[ServiceManager.ServiceDependency] /// <summary>
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get(); /// 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);
[ServiceManager.ServiceDependency] /// <summary>
private readonly LibcFunction libcFunction = Service<LibcFunction>.Get(); /// 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);
private IntPtr baseAddress = IntPtr.Zero; /// <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);
[ServiceManager.ServiceConstructor] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private ChatGui(SigScanner sigScanner) 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>
/// Dispose of managed and unmanaged resources.
/// </summary>
void IDisposable.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)
{
// Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message);
this.PrintChat(new XivChatEntry
{ {
this.address = new ChatGuiAddressResolver(); Message = message,
this.address.Setup(sigScanner); Type = this.configuration.GeneralChatType,
});
}
this.printMessageHook = Hook<PrintMessageDelegate>.FromAddress(this.address.PrintMessage, this.HandlePrintMessageDetour); /// <summary>
this.populateItemLinkHook = Hook<PopulateItemLinkDelegate>.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour); /// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue,
this.interactableLinkClickedHook = Hook<InteractableLinkClickedDelegate>.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour); /// later to be processed when UpdateQueue() is called.
} /// </summary>
/// <param name="message">A message to send.</param>
/// <summary> public void Print(SeString message)
/// A delegate type used with the <see cref="ChatGui.ChatMessage"/> event. {
/// </summary> // Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue);
/// <param name="type">The type of chat.</param> this.PrintChat(new XivChatEntry
/// <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>
/// Dispose of managed and unmanaged resources.
/// </summary>
void IDisposable.Dispose()
{ {
this.printMessageHook.Dispose(); Message = message,
this.populateItemLinkHook.Dispose(); Type = this.configuration.GeneralChatType,
this.interactableLinkClickedHook.Dispose(); });
} }
/// <summary> /// <summary>
/// Queue a chat message. While method is named as PrintChat, it only add a entry to the queue, /// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to
/// later to be processed when UpdateQueue() is called. /// the queue, later to be processed when UpdateQueue() is called.
/// </summary> /// </summary>
/// <param name="chat">A message to send.</param> /// <param name="message">A message to send.</param>
public void PrintChat(XivChatEntry chat) public void PrintError(string message)
{
// Log.Verbose("[CHATGUI PRINT REGULAR ERROR]{0}", message);
this.PrintChat(new XivChatEntry
{ {
this.chatQueue.Enqueue(chat); Message = message,
} Type = XivChatType.Urgent,
});
}
/// <summary> /// <summary>
/// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue, /// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to
/// later to be processed when UpdateQueue() is called. /// the queue, later to be processed when UpdateQueue() is called.
/// </summary> /// </summary>
/// <param name="message">A message to send.</param> /// <param name="message">A message to send.</param>
public void Print(string message) public void PrintError(SeString message)
{
// Log.Verbose("[CHATGUI PRINT SESTRING ERROR]{0}", message.TextValue);
this.PrintChat(new XivChatEntry
{ {
// Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message); Message = message,
this.PrintChat(new XivChatEntry 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)
{ {
Message = message, continue;
Type = this.configuration.GeneralChatType, }
});
var senderRaw = (chat.Name ?? string.Empty).Encode();
using var senderOwned = this.libcFunction.NewString(senderRaw);
var messageRaw = (chat.Message ?? string.Empty).Encode();
using var messageOwned = this.libcFunction.NewString(messageRaw);
this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters);
} }
}
/// <summary> /// <summary>
/// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue, /// Create a link handler.
/// later to be processed when UpdateQueue() is called. /// </summary>
/// </summary> /// <param name="pluginName">The name of the plugin handling the link.</param>
/// <param name="message">A message to send.</param> /// <param name="commandId">The ID of the command to run.</param>
public void Print(SeString message) /// <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))
{ {
// Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue); this.dalamudLinkHandlers.Remove(handler);
this.PrintChat(new XivChatEntry
{
Message = message,
Type = this.configuration.GeneralChatType,
});
} }
}
/// <summary> /// <summary>
/// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to /// Remove a registered link handler.
/// the queue, later to be processed when UpdateQueue() is called. /// </summary>
/// </summary> /// <param name="pluginName">The name of the plugin handling the link.</param>
/// <param name="message">A message to send.</param> /// <param name="commandId">The ID of the command to be removed.</param>
public void PrintError(string message) internal void RemoveChatLinkHandler(string pluginName, uint commandId)
{
if (this.dalamudLinkHandlers.ContainsKey((pluginName, commandId)))
{ {
// Log.Verbose("[CHATGUI PRINT REGULAR ERROR]{0}", message); this.dalamudLinkHandlers.Remove((pluginName, commandId));
this.PrintChat(new XivChatEntry
{
Message = message,
Type = XivChatType.Urgent,
});
} }
}
/// <summary> [ServiceManager.CallWhenServicesReady]
/// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to private void ContinueConstruction(GameGui gameGui, LibcFunction libcFunction)
/// the queue, later to be processed when UpdateQueue() is called. {
/// </summary> this.printMessageHook.Enable();
/// <param name="message">A message to send.</param> this.populateItemLinkHook.Enable();
public void PrintError(SeString message) this.interactableLinkClickedHook.Enable();
}
private void HandlePopulateItemLinkDetour(IntPtr linkObjectPtr, IntPtr itemInfoPtr)
{
try
{ {
// Log.Verbose("[CHATGUI PRINT SESTRING ERROR]{0}", message.TextValue); this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr);
this.PrintChat(new XivChatEntry
{ this.LastLinkedItemId = Marshal.ReadInt32(itemInfoPtr, 8);
Message = message, this.LastLinkedItemFlags = Marshal.ReadByte(itemInfoPtr, 0x14);
Type = XivChatType.Urgent,
}); // Log.Verbose($"HandlePopulateItemLinkDetour {linkObjectPtr} {itemInfoPtr} - linked:{this.LastLinkedItemId}");
} }
catch (Exception ex)
/// <summary>
/// Process a chat queue.
/// </summary>
public void UpdateQueue()
{ {
while (this.chatQueue.Count > 0) Log.Error(ex, "Exception onPopulateItemLink hook.");
{ this.populateItemLinkHook.Original(linkObjectPtr, itemInfoPtr);
var chat = this.chatQueue.Dequeue(); }
}
if (this.baseAddress == IntPtr.Zero) 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;
var invocationList = this.CheckMessageHandled.GetInvocationList();
foreach (var @delegate in invocationList)
{
try
{ {
continue; var messageHandledDelegate = @delegate as OnCheckMessageHandledDelegate;
messageHandledDelegate!.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled);
}
catch (Exception e)
{
Log.Error(e, "Could not invoke registered OnCheckMessageHandledDelegate for {Name}", @delegate.Method.Name);
} }
var senderRaw = (chat.Name ?? string.Empty).Encode();
using var senderOwned = this.libcFunction.NewString(senderRaw);
var messageRaw = (chat.Message ?? string.Empty).Encode();
using var messageOwned = this.libcFunction.NewString(messageRaw);
this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters);
} }
}
/// <summary> if (!isHandled)
/// Create a link handler.
/// </summary>
/// <param name="pluginName">The name of the plugin handling the link.</param>
/// <param name="commandId">The ID of the command to run.</param>
/// <param name="commandAction">The command action itself.</param>
/// <returns>A payload for handling.</returns>
internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action<uint, SeString> commandAction)
{
var payload = new DalamudLinkPayload() { Plugin = pluginName, CommandId = commandId };
this.dalamudLinkHandlers.Add((pluginName, commandId), commandAction);
return payload;
}
/// <summary>
/// Remove all handlers owned by a plugin.
/// </summary>
/// <param name="pluginName">The name of the plugin handling the links.</param>
internal void RemoveChatLinkHandler(string pluginName)
{
foreach (var handler in this.dalamudLinkHandlers.Keys.ToList().Where(k => k.PluginName == pluginName))
{ {
this.dalamudLinkHandlers.Remove(handler); invocationList = this.ChatMessage.GetInvocationList();
}
}
/// <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));
}
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction(GameGui gameGui, LibcFunction libcFunction)
{
this.printMessageHook.Enable();
this.populateItemLinkHook.Enable();
this.interactableLinkClickedHook.Enable();
}
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;
var invocationList = this.CheckMessageHandled.GetInvocationList();
foreach (var @delegate in invocationList) foreach (var @delegate in invocationList)
{ {
try try
{ {
var messageHandledDelegate = @delegate as OnCheckMessageHandledDelegate; var messageHandledDelegate = @delegate as OnMessageDelegate;
messageHandledDelegate!.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled); messageHandledDelegate!.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled);
} }
catch (Exception e) catch (Exception e)
{ {
Log.Error(e, "Could not invoke registered OnCheckMessageHandledDelegate for {Name}", @delegate.Method.Name); Log.Error(e, "Could not invoke registered OnMessageDelegate for {Name}", @delegate.Method.Name);
} }
} }
}
if (!isHandled) var newEdited = parsedMessage.Encode();
{ if (!Util.FastByteArrayCompare(oldEdited, newEdited))
invocationList = this.ChatMessage.GetInvocationList(); {
foreach (var @delegate in invocationList) Log.Verbose("SeString was edited, taking precedence over StdString edit.");
{ message.RawData = newEdited;
try // Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}");
{ }
var messageHandledDelegate = @delegate as OnMessageDelegate;
messageHandledDelegate!.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled);
}
catch (Exception e)
{
Log.Error(e, "Could not invoke registered OnMessageDelegate for {Name}", @delegate.Method.Name);
}
}
}
var newEdited = parsedMessage.Encode(); if (!Util.FastByteArrayCompare(originalMessageData, message.RawData))
if (!Util.FastByteArrayCompare(oldEdited, newEdited)) {
{ allocatedString = this.libcFunction.NewString(message.RawData);
Log.Verbose("SeString was edited, taking precedence over StdString edit."); Log.Debug($"HandlePrintMessageDetour String modified: {originalMessageData}({messagePtr}) -> {message}({allocatedString.Address})");
message.RawData = newEdited; messagePtr = allocatedString.Address;
// Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}"); }
}
if (!Util.FastByteArrayCompare(originalMessageData, message.RawData)) var newEditedSender = parsedSender.Encode();
{ if (!Util.FastByteArrayCompare(oldEditedSender, newEditedSender))
allocatedString = this.libcFunction.NewString(message.RawData); {
Log.Debug($"HandlePrintMessageDetour String modified: {originalMessageData}({messagePtr}) -> {message}({allocatedString.Address})"); Log.Verbose("SeString was edited, taking precedence over StdString edit.");
messagePtr = allocatedString.Address; sender.RawData = newEditedSender;
} // Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}");
}
var newEditedSender = parsedSender.Encode(); if (!Util.FastByteArrayCompare(originalSenderData, sender.RawData))
if (!Util.FastByteArrayCompare(oldEditedSender, newEditedSender)) {
{ allocatedStringSender = this.libcFunction.NewString(sender.RawData);
Log.Verbose("SeString was edited, taking precedence over StdString edit."); Log.Debug(
sender.RawData = newEditedSender; $"HandlePrintMessageDetour Sender modified: {originalSenderData}({senderPtr}) -> {sender}({allocatedStringSender.Address})");
// Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}"); senderPtr = allocatedStringSender.Address;
} }
if (!Util.FastByteArrayCompare(originalSenderData, sender.RawData)) // Print the original chat if it's handled.
{ if (isHandled)
allocatedStringSender = this.libcFunction.NewString(sender.RawData); {
Log.Debug( this.ChatMessageHandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
$"HandlePrintMessageDetour Sender modified: {originalSenderData}({senderPtr}) -> {sender}({allocatedStringSender.Address})"); }
senderPtr = allocatedStringSender.Address; else
} {
retVal = this.printMessageHook.Original(manager, chattype, senderPtr, messagePtr, senderid, parameter);
this.ChatMessageUnhandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
}
// Print the original chat if it's handled. if (this.baseAddress == IntPtr.Zero)
if (isHandled) this.baseAddress = manager;
allocatedString?.Dispose();
allocatedStringSender?.Dispose();
}
catch (Exception ex)
{
Log.Error(ex, "Exception on OnChatMessage hook.");
retVal = this.printMessageHook.Original(manager, chattype, pSenderName, pMessage, senderid, parameter);
}
return retVal;
}
private void InteractableLinkClickedDetour(IntPtr managerPtr, IntPtr messagePtr)
{
try
{
var interactableType = (Payload.EmbeddedInfoType)(Marshal.ReadByte(messagePtr, 0x1B) + 1);
if (interactableType != Payload.EmbeddedInfoType.DalamudLink)
{
this.interactableLinkClickedHook.Original(managerPtr, messagePtr);
return;
}
Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}");
var payloadPtr = Marshal.ReadIntPtr(messagePtr, 0x10);
var messageSize = 0;
while (Marshal.ReadByte(payloadPtr, messageSize) != 0) messageSize++;
var payloadBytes = new byte[messageSize];
Marshal.Copy(payloadPtr, payloadBytes, 0, messageSize);
var seStr = SeString.Parse(payloadBytes);
var terminatorIndex = seStr.Payloads.IndexOf(RawPayload.LinkTerminator);
var payloads = terminatorIndex >= 0 ? seStr.Payloads.Take(terminatorIndex + 1).ToList() : seStr.Payloads;
if (payloads.Count == 0) return;
var linkPayload = payloads[0];
if (linkPayload is DalamudLinkPayload link)
{
if (this.dalamudLinkHandlers.ContainsKey((link.Plugin, link.CommandId)))
{ {
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 else
{ {
retVal = this.printMessageHook.Original(manager, chattype, senderPtr, messagePtr, senderid, parameter); Log.Debug($"No DalamudLink registered for {link.Plugin} with ID of {link.CommandId}");
this.ChatMessageUnhandled?.Invoke(chattype, senderid, parsedSender, parsedMessage);
} }
if (this.baseAddress == IntPtr.Zero)
this.baseAddress = manager;
allocatedString?.Dispose();
allocatedStringSender?.Dispose();
} }
catch (Exception ex)
{
Log.Error(ex, "Exception on OnChatMessage hook.");
retVal = this.printMessageHook.Original(manager, chattype, pSenderName, pMessage, senderid, parameter);
}
return retVal;
} }
catch (Exception ex)
private void InteractableLinkClickedDetour(IntPtr managerPtr, IntPtr messagePtr)
{ {
try Log.Error(ex, "Exception on InteractableLinkClicked hook");
{
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");
}
} }
} }
} }

View file

@ -1,104 +1,103 @@
using System; using System;
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> /// <summary>
/// The address resolver for the <see cref="ChatGui"/> class. /// Gets the address of the native PrintMessage method.
/// </summary> /// </summary>
public sealed class ChatGuiAddressResolver : BaseAddressResolver 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 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
*/
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{ {
/// <summary> // PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24D8FEFFFF 4881EC28020000 488B05???????? 4833C4 488985F0000000 4532D2 48894C2448"); LAST PART FOR 5.1???
/// Gets the address of the native PrintMessage method. 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");
/// </summary> // PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24E8FEFFFF 4881EC18020000 488B05???????? 4833C4 488985E0000000 4532D2 48894C2438"); old
public IntPtr PrintMessage { get; private set; }
/// <summary> // 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");
/// Gets the address of the native PopulateItemLinkObject method.
/// </summary>
public IntPtr PopulateItemLinkObject { get; private set; }
/// <summary> // 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");
/// Gets the address of the native InteractableLinkClicked method.
/// </summary>
public IntPtr InteractableLinkClicked { get; private set; }
/* // 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
--- for reference: 4.57 --- 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");
.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/> this.InteractableLinkClicked = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 80 BB ?? ?? ?? ?? ?? 0F 85 ?? ?? ?? ?? 80 BB") + 9;
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");
// 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");
this.InteractableLinkClicked = sig.ScanText("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 80 BB ?? ?? ?? ?? ?? 0F 85 ?? ?? ?? ?? 80 BB") + 9;
}
} }
} }

View file

@ -10,314 +10,313 @@ using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using Serilog; using Serilog;
namespace Dalamud.Game.Gui.Dtr namespace Dalamud.Game.Gui.Dtr;
/// <summary>
/// Class used to interface with the server info bar.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed unsafe class DtrBar : IDisposable, IServiceType
{ {
/// <summary> private const uint BaseNodeId = 1000;
/// Class used to interface with the server info bar.
/// </summary> [ServiceManager.ServiceDependency]
[PluginInterface] private readonly Framework framework = Service<Framework>.Get();
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService] [ServiceManager.ServiceDependency]
public sealed unsafe class DtrBar : IDisposable, IServiceType private readonly GameGui gameGui = Service<GameGui>.Get();
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private List<DtrBarEntry> entries = new();
private uint runningNodeIds = BaseNodeId;
[ServiceManager.ServiceConstructor]
private DtrBar()
{ {
private const uint BaseNodeId = 1000; this.framework.Update += this.Update;
[ServiceManager.ServiceDependency] this.configuration.DtrOrder ??= new List<string>();
private readonly Framework framework = Service<Framework>.Get(); this.configuration.DtrIgnore ??= new List<string>();
this.configuration.Save();
}
[ServiceManager.ServiceDependency] /// <summary>
private readonly GameGui gameGui = Service<GameGui>.Get(); /// Get a DTR bar entry.
/// This allows you to add your own text, and users to sort it.
/// </summary>
/// <param name="title">A user-friendly name for sorting.</param>
/// <param name="text">The text the entry shows.</param>
/// <returns>The entry object used to update, hide and remove the entry.</returns>
/// <exception cref="ArgumentException">Thrown when an entry with the specified title exists.</exception>
public DtrBarEntry Get(string title, SeString? text = null)
{
if (this.entries.Any(x => x.Title == title))
throw new ArgumentException("An entry with the same title already exists.");
[ServiceManager.ServiceDependency] var node = this.MakeNode(++this.runningNodeIds);
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get(); var entry = new DtrBarEntry(title, node);
entry.Text = text;
private List<DtrBarEntry> entries = new(); // Add the entry to the end of the order list, if it's not there already.
private uint runningNodeIds = BaseNodeId; if (!this.configuration.DtrOrder!.Contains(title))
this.configuration.DtrOrder!.Add(title);
this.entries.Add(entry);
this.ApplySort();
[ServiceManager.ServiceConstructor] return entry;
private DtrBar() }
/// <inheritdoc/>
void IDisposable.Dispose()
{
foreach (var entry in this.entries)
this.RemoveNode(entry.TextNode);
this.entries.Clear();
this.framework.Update -= this.Update;
}
/// <summary>
/// Remove nodes marked as "should be removed" from the bar.
/// </summary>
internal void HandleRemovedNodes()
{
foreach (var data in this.entries.Where(d => d.ShouldBeRemoved))
{ {
this.framework.Update += this.Update; this.RemoveNode(data.TextNode);
this.configuration.DtrOrder ??= new List<string>();
this.configuration.DtrIgnore ??= new List<string>();
this.configuration.Save();
} }
/// <summary> this.entries.RemoveAll(d => d.ShouldBeRemoved);
/// Get a DTR bar entry. }
/// This allows you to add your own text, and users to sort it.
/// </summary>
/// <param name="title">A user-friendly name for sorting.</param>
/// <param name="text">The text the entry shows.</param>
/// <returns>The entry object used to update, hide and remove the entry.</returns>
/// <exception cref="ArgumentException">Thrown when an entry with the specified title exists.</exception>
public DtrBarEntry Get(string title, SeString? text = null)
{
if (this.entries.Any(x => x.Title == title))
throw new ArgumentException("An entry with the same title already exists.");
var node = this.MakeNode(++this.runningNodeIds); /// <summary>
var entry = new DtrBarEntry(title, node); /// Check whether an entry with the specified title exists.
entry.Text = text; /// </summary>
/// <param name="title">The title to check for.</param>
// Add the entry to the end of the order list, if it's not there already. /// <returns>Whether or not an entry with that title is registered.</returns>
if (!this.configuration.DtrOrder!.Contains(title)) internal bool HasEntry(string title) => this.entries.Any(x => x.Title == title);
this.configuration.DtrOrder!.Add(title);
this.entries.Add(entry);
this.ApplySort();
return entry;
}
/// <inheritdoc/>
void IDisposable.Dispose()
{
foreach (var entry in this.entries)
this.RemoveNode(entry.TextNode);
this.entries.Clear();
this.framework.Update -= this.Update;
}
/// <summary>
/// Remove nodes marked as "should be removed" from the bar.
/// </summary>
internal void HandleRemovedNodes()
{
foreach (var data in this.entries.Where(d => d.ShouldBeRemoved))
{
this.RemoveNode(data.TextNode);
}
this.entries.RemoveAll(d => d.ShouldBeRemoved);
}
/// <summary>
/// Check whether an entry with the specified title exists.
/// </summary>
/// <param name="title">The title to check for.</param>
/// <returns>Whether or not an entry with that title is registered.</returns>
internal bool HasEntry(string title) => this.entries.Any(x => x.Title == title);
/// <summary>
/// Dirty the DTR bar entry with the specified title.
/// </summary>
/// <param name="title">Title of the entry to dirty.</param>
/// <returns>Whether the entry was found.</returns>
internal bool MakeDirty(string title)
{
var entry = this.entries.FirstOrDefault(x => x.Title == title);
if (entry == null)
return false;
entry.Dirty = true;
return true;
}
/// <summary>
/// Reapply the DTR entry ordering from <see cref="DalamudConfiguration"/>.
/// </summary>
internal void ApplySort()
{
// Sort the current entry list, based on the order in the configuration.
var positions = this.configuration
.DtrOrder!
.Select(entry => (entry, index: this.configuration.DtrOrder!.IndexOf(entry)))
.ToDictionary(x => x.entry, x => x.index);
this.entries.Sort((x, y) =>
{
var xPos = positions.TryGetValue(x.Title, out var xIndex) ? xIndex : int.MaxValue;
var yPos = positions.TryGetValue(y.Title, out var yIndex) ? yIndex : int.MaxValue;
return xPos.CompareTo(yPos);
});
}
private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR", 1).ToPointer();
private void Update(Framework unused)
{
this.HandleRemovedNodes();
var dtr = this.GetDtr();
if (dtr == null) return;
// The collision node on the DTR element is always the width of its content
if (dtr->UldManager.NodeList == null) return;
// If we have an unmodified DTR but still have entries, we need to
// work to reset our state.
if (!this.CheckForDalamudNodes())
this.RecreateNodes();
var collisionNode = dtr->UldManager.NodeList[1];
if (collisionNode == null) return;
// If we are drawing backwards, we should start from the right side of the collision node. That is,
// collisionNode->X + collisionNode->Width.
var runningXPos = this.configuration.DtrSwapDirection
? collisionNode->X + collisionNode->Width
: collisionNode->X;
for (var i = 0; i < this.entries.Count; i++)
{
var data = this.entries[i];
var isHide = this.configuration.DtrIgnore!.Any(x => x == data.Title) || !data.Shown;
if (data.Dirty && data.Added && data.Text != null && data.TextNode != null)
{
var node = data.TextNode;
node->SetText(data.Text?.Encode());
ushort w = 0, h = 0;
if (isHide)
{
node->AtkResNode.ToggleVisibility(false);
}
else
{
node->AtkResNode.ToggleVisibility(true);
node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr);
node->AtkResNode.SetWidth(w);
}
data.Dirty = false;
}
if (!data.Added)
{
data.Added = this.AddNode(data.TextNode);
}
if (!isHide)
{
var elementWidth = data.TextNode->AtkResNode.Width + this.configuration.DtrSpacing;
if (this.configuration.DtrSwapDirection)
{
data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2);
runningXPos += elementWidth;
}
else
{
runningXPos -= elementWidth;
data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2);
}
}
this.entries[i] = data;
}
}
/// <summary>
/// Checks if there are any Dalamud nodes in the DTR.
/// </summary>
/// <returns>True if there are nodes with an ID > 1000.</returns>
private bool CheckForDalamudNodes()
{
var dtr = this.GetDtr();
if (dtr == null || dtr->RootNode == null) return false;
for (var i = 0; i < dtr->UldManager.NodeListCount; i++)
{
if (dtr->UldManager.NodeList[i]->NodeID > 1000)
return true;
}
/// <summary>
/// Dirty the DTR bar entry with the specified title.
/// </summary>
/// <param name="title">Title of the entry to dirty.</param>
/// <returns>Whether the entry was found.</returns>
internal bool MakeDirty(string title)
{
var entry = this.entries.FirstOrDefault(x => x.Title == title);
if (entry == null)
return false; return false;
}
private void RecreateNodes() entry.Dirty = true;
return true;
}
/// <summary>
/// Reapply the DTR entry ordering from <see cref="DalamudConfiguration"/>.
/// </summary>
internal void ApplySort()
{
// Sort the current entry list, based on the order in the configuration.
var positions = this.configuration
.DtrOrder!
.Select(entry => (entry, index: this.configuration.DtrOrder!.IndexOf(entry)))
.ToDictionary(x => x.entry, x => x.index);
this.entries.Sort((x, y) =>
{ {
this.runningNodeIds = BaseNodeId; var xPos = positions.TryGetValue(x.Title, out var xIndex) ? xIndex : int.MaxValue;
foreach (var entry in this.entries) var yPos = positions.TryGetValue(y.Title, out var yIndex) ? yIndex : int.MaxValue;
return xPos.CompareTo(yPos);
});
}
private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR", 1).ToPointer();
private void Update(Framework unused)
{
this.HandleRemovedNodes();
var dtr = this.GetDtr();
if (dtr == null) return;
// The collision node on the DTR element is always the width of its content
if (dtr->UldManager.NodeList == null) return;
// If we have an unmodified DTR but still have entries, we need to
// work to reset our state.
if (!this.CheckForDalamudNodes())
this.RecreateNodes();
var collisionNode = dtr->UldManager.NodeList[1];
if (collisionNode == null) return;
// If we are drawing backwards, we should start from the right side of the collision node. That is,
// collisionNode->X + collisionNode->Width.
var runningXPos = this.configuration.DtrSwapDirection
? collisionNode->X + collisionNode->Width
: collisionNode->X;
for (var i = 0; i < this.entries.Count; i++)
{
var data = this.entries[i];
var isHide = this.configuration.DtrIgnore!.Any(x => x == data.Title) || !data.Shown;
if (data.Dirty && data.Added && data.Text != null && data.TextNode != null)
{ {
entry.TextNode = this.MakeNode(++this.runningNodeIds); var node = data.TextNode;
entry.Added = false; node->SetText(data.Text?.Encode());
} ushort w = 0, h = 0;
}
private bool AddNode(AtkTextNode* node) if (isHide)
{ {
var dtr = this.GetDtr(); node->AtkResNode.ToggleVisibility(false);
if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; }
else
{
node->AtkResNode.ToggleVisibility(true);
node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr);
node->AtkResNode.SetWidth(w);
}
var lastChild = dtr->RootNode->ChildNode; data.Dirty = false;
while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode;
Log.Debug($"Found last sibling: {(ulong)lastChild:X}");
lastChild->PrevSiblingNode = (AtkResNode*)node;
node->AtkResNode.ParentNode = lastChild->ParentNode;
node->AtkResNode.NextSiblingNode = lastChild;
dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount + 1);
Log.Debug("Set last sibling of DTR and updated child count");
dtr->UldManager.UpdateDrawNodeList();
Log.Debug("Updated node draw list");
return true;
}
private bool RemoveNode(AtkTextNode* node)
{
var dtr = this.GetDtr();
if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false;
var tmpPrevNode = node->AtkResNode.PrevSiblingNode;
var tmpNextNode = node->AtkResNode.NextSiblingNode;
// if (tmpNextNode != null)
tmpNextNode->PrevSiblingNode = tmpPrevNode;
if (tmpPrevNode != null)
tmpPrevNode->NextSiblingNode = tmpNextNode;
node->AtkResNode.Destroy(true);
dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount - 1);
Log.Debug("Set last sibling of DTR and updated child count");
dtr->UldManager.UpdateDrawNodeList();
Log.Debug("Updated node draw list");
return true;
}
private AtkTextNode* MakeNode(uint nodeId)
{
var newTextNode = (AtkTextNode*)IMemorySpace.GetUISpace()->Malloc((ulong)sizeof(AtkTextNode), 8);
if (newTextNode == null)
{
Log.Debug("Failed to allocate memory for text node");
return null;
} }
IMemorySpace.Memset(newTextNode, 0, (ulong)sizeof(AtkTextNode)); if (!data.Added)
newTextNode->Ctor(); {
data.Added = this.AddNode(data.TextNode);
}
newTextNode->AtkResNode.NodeID = nodeId; if (!isHide)
newTextNode->AtkResNode.Type = NodeType.Text; {
newTextNode->AtkResNode.Flags = (short)(NodeFlags.AnchorLeft | NodeFlags.AnchorTop); var elementWidth = data.TextNode->AtkResNode.Width + this.configuration.DtrSpacing;
newTextNode->AtkResNode.DrawFlags = 12;
newTextNode->AtkResNode.SetWidth(22);
newTextNode->AtkResNode.SetHeight(22);
newTextNode->AtkResNode.SetPositionFloat(-200, 2);
newTextNode->LineSpacing = 12; if (this.configuration.DtrSwapDirection)
newTextNode->AlignmentFontType = 5; {
newTextNode->FontSize = 14; data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2);
newTextNode->TextFlags = (byte)TextFlags.Edge; runningXPos += elementWidth;
newTextNode->TextFlags2 = 0; }
else
{
runningXPos -= elementWidth;
data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2);
}
}
newTextNode->SetText(" "); this.entries[i] = data;
newTextNode->TextColor.R = 255;
newTextNode->TextColor.G = 255;
newTextNode->TextColor.B = 255;
newTextNode->TextColor.A = 255;
newTextNode->EdgeColor.R = 142;
newTextNode->EdgeColor.G = 106;
newTextNode->EdgeColor.B = 12;
newTextNode->EdgeColor.A = 255;
return newTextNode;
} }
} }
/// <summary>
/// Checks if there are any Dalamud nodes in the DTR.
/// </summary>
/// <returns>True if there are nodes with an ID > 1000.</returns>
private bool CheckForDalamudNodes()
{
var dtr = this.GetDtr();
if (dtr == null || dtr->RootNode == null) return false;
for (var i = 0; i < dtr->UldManager.NodeListCount; i++)
{
if (dtr->UldManager.NodeList[i]->NodeID > 1000)
return true;
}
return false;
}
private void RecreateNodes()
{
this.runningNodeIds = BaseNodeId;
foreach (var entry in this.entries)
{
entry.TextNode = this.MakeNode(++this.runningNodeIds);
entry.Added = false;
}
}
private bool AddNode(AtkTextNode* node)
{
var dtr = this.GetDtr();
if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false;
var lastChild = dtr->RootNode->ChildNode;
while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode;
Log.Debug($"Found last sibling: {(ulong)lastChild:X}");
lastChild->PrevSiblingNode = (AtkResNode*)node;
node->AtkResNode.ParentNode = lastChild->ParentNode;
node->AtkResNode.NextSiblingNode = lastChild;
dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount + 1);
Log.Debug("Set last sibling of DTR and updated child count");
dtr->UldManager.UpdateDrawNodeList();
Log.Debug("Updated node draw list");
return true;
}
private bool RemoveNode(AtkTextNode* node)
{
var dtr = this.GetDtr();
if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false;
var tmpPrevNode = node->AtkResNode.PrevSiblingNode;
var tmpNextNode = node->AtkResNode.NextSiblingNode;
// if (tmpNextNode != null)
tmpNextNode->PrevSiblingNode = tmpPrevNode;
if (tmpPrevNode != null)
tmpPrevNode->NextSiblingNode = tmpNextNode;
node->AtkResNode.Destroy(true);
dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount - 1);
Log.Debug("Set last sibling of DTR and updated child count");
dtr->UldManager.UpdateDrawNodeList();
Log.Debug("Updated node draw list");
return true;
}
private AtkTextNode* MakeNode(uint nodeId)
{
var newTextNode = (AtkTextNode*)IMemorySpace.GetUISpace()->Malloc((ulong)sizeof(AtkTextNode), 8);
if (newTextNode == null)
{
Log.Debug("Failed to allocate memory for text node");
return null;
}
IMemorySpace.Memset(newTextNode, 0, (ulong)sizeof(AtkTextNode));
newTextNode->Ctor();
newTextNode->AtkResNode.NodeID = nodeId;
newTextNode->AtkResNode.Type = NodeType.Text;
newTextNode->AtkResNode.Flags = (short)(NodeFlags.AnchorLeft | NodeFlags.AnchorTop);
newTextNode->AtkResNode.DrawFlags = 12;
newTextNode->AtkResNode.SetWidth(22);
newTextNode->AtkResNode.SetHeight(22);
newTextNode->AtkResNode.SetPositionFloat(-200, 2);
newTextNode->LineSpacing = 12;
newTextNode->AlignmentFontType = 5;
newTextNode->FontSize = 14;
newTextNode->TextFlags = (byte)TextFlags.Edge;
newTextNode->TextFlags2 = 0;
newTextNode->SetText(" ");
newTextNode->TextColor.R = 255;
newTextNode->TextColor.G = 255;
newTextNode->TextColor.B = 255;
newTextNode->TextColor.A = 255;
newTextNode->EdgeColor.R = 142;
newTextNode->EdgeColor.G = 106;
newTextNode->EdgeColor.B = 12;
newTextNode->EdgeColor.A = 255;
return newTextNode;
}
} }

View file

@ -3,91 +3,90 @@
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Gui.Dtr namespace Dalamud.Game.Gui.Dtr;
/// <summary>
/// Class representing an entry in the server info bar.
/// </summary>
public sealed unsafe class DtrBarEntry : IDisposable
{ {
private bool shownBacking = true;
private SeString? textBacking = null;
/// <summary> /// <summary>
/// Class representing an entry in the server info bar. /// Initializes a new instance of the <see cref="DtrBarEntry"/> class.
/// </summary> /// </summary>
public sealed unsafe class DtrBarEntry : IDisposable /// <param name="title">The title of the bar entry.</param>
/// <param name="textNode">The corresponding text node.</param>
internal DtrBarEntry(string title, AtkTextNode* textNode)
{ {
private bool shownBacking = true; this.Title = title;
private SeString? textBacking = null; this.TextNode = textNode;
}
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DtrBarEntry"/> class. /// Gets the title of this entry.
/// </summary> /// </summary>
/// <param name="title">The title of the bar entry.</param> public string Title { get; init; }
/// <param name="textNode">The corresponding text node.</param>
internal DtrBarEntry(string title, AtkTextNode* textNode) /// <summary>
/// Gets or sets the text of this entry.
/// </summary>
public SeString? Text
{
get => this.textBacking;
set
{ {
this.Title = title; this.textBacking = value;
this.TextNode = textNode; this.Dirty = true;
}
/// <summary>
/// Gets the title of this entry.
/// </summary>
public string Title { get; init; }
/// <summary>
/// Gets or sets the text of this entry.
/// </summary>
public SeString? Text
{
get => this.textBacking;
set
{
this.textBacking = value;
this.Dirty = true;
}
}
/// <summary>
/// Gets or sets a value indicating whether this entry is visible.
/// </summary>
public bool Shown
{
get => this.shownBacking;
set
{
this.shownBacking = value;
this.Dirty = true;
}
}
/// <summary>
/// Gets or sets the internal text node of this entry.
/// </summary>
internal AtkTextNode* TextNode { get; set; }
/// <summary>
/// Gets a value indicating whether this entry should be removed.
/// </summary>
internal bool ShouldBeRemoved { get; private set; } = false;
/// <summary>
/// Gets or sets a value indicating whether this entry is dirty.
/// </summary>
internal bool Dirty { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether this entry has just been added.
/// </summary>
internal bool Added { get; set; } = false;
/// <summary>
/// Remove this entry from the bar.
/// You will need to re-acquire it from DtrBar to reuse it.
/// </summary>
public void Remove()
{
this.ShouldBeRemoved = true;
}
/// <inheritdoc/>
public void Dispose()
{
this.Remove();
} }
} }
/// <summary>
/// Gets or sets a value indicating whether this entry is visible.
/// </summary>
public bool Shown
{
get => this.shownBacking;
set
{
this.shownBacking = value;
this.Dirty = true;
}
}
/// <summary>
/// Gets or sets the internal text node of this entry.
/// </summary>
internal AtkTextNode* TextNode { get; set; }
/// <summary>
/// Gets a value indicating whether this entry should be removed.
/// </summary>
internal bool ShouldBeRemoved { get; private set; } = false;
/// <summary>
/// Gets or sets a value indicating whether this entry is dirty.
/// </summary>
internal bool Dirty { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether this entry has just been added.
/// </summary>
internal bool Added { get; set; } = false;
/// <summary>
/// Remove this entry from the bar.
/// You will need to re-acquire it from DtrBar to reuse it.
/// </summary>
public void Remove()
{
this.ShouldBeRemoved = true;
}
/// <inheritdoc/>
public void Dispose()
{
this.Remove();
}
} }

View file

@ -9,303 +9,302 @@ using Dalamud.IoC.Internal;
using Dalamud.Memory; using Dalamud.Memory;
using Serilog; using Serilog;
namespace Dalamud.Game.Gui.FlyText namespace Dalamud.Game.Gui.FlyText;
/// <summary>
/// This class facilitates interacting with and creating native in-game "fly text".
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public sealed class FlyTextGui : IDisposable, IServiceType
{ {
/// <summary> /// <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> /// </summary>
[PluginInterface] private readonly AddFlyTextDelegate addFlyTextNative;
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService] /// <summary>
public sealed class FlyTextGui : IDisposable, IServiceType /// The hook that fires when the game creates a fly text element. See <see cref="FlyTextGuiAddressResolver.CreateFlyText"/>.
/// </summary>
private readonly Hook<CreateFlyTextDelegate> createFlyTextHook;
[ServiceManager.ServiceConstructor]
private FlyTextGui(SigScanner sigScanner)
{ {
/// <summary> this.Address = new FlyTextGuiAddressResolver();
/// The native function responsible for adding fly text to the UI. See <see cref="FlyTextGuiAddressResolver.AddFlyText"/>. this.Address.Setup(sigScanner);
/// </summary>
private readonly AddFlyTextDelegate addFlyTextNative;
/// <summary> this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer<AddFlyTextDelegate>(this.Address.AddFlyText);
/// The hook that fires when the game creates a fly text element. See <see cref="FlyTextGuiAddressResolver.CreateFlyText"/>. this.createFlyTextHook = Hook<CreateFlyTextDelegate>.FromAddress(this.Address.CreateFlyText, this.CreateFlyTextDetour);
/// </summary> }
private readonly Hook<CreateFlyTextDelegate> createFlyTextHook;
[ServiceManager.ServiceConstructor] /// <summary>
private FlyTextGui(SigScanner sigScanner) /// 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>.GetNullable();
if (gameGui == null)
return;
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->GetRaptureAtkModule()->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(); fixed (byte* pText2 = text2.Encode())
this.Address.Setup(sigScanner);
this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer<AddFlyTextDelegate>(this.Address.AddFlyText);
this.createFlyTextHook = Hook<CreateFlyTextDelegate>.FromAddress(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>.GetNullable();
if (gameGui == null)
return;
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->GetRaptureAtkModule()->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()) strArray->StringArray[strOffset + 0] = pText1;
{ strArray->StringArray[strOffset + 1] = pText2;
strArray->StringArray[strOffset + 0] = pText1;
strArray->StringArray[strOffset + 1] = pText2;
this.addFlyTextNative( this.addFlyTextNative(
flytext, flytext,
actorIndex, actorIndex,
1, 1,
(IntPtr)numArray, (IntPtr)numArray,
numOffset, numOffset,
9, 9,
(IntPtr)strArray, (IntPtr)strArray,
strOffset, strOffset,
2, 2,
0); 0);
}
} }
} }
}
private static byte[] Terminate(byte[] source)
{ private static byte[] Terminate(byte[] source)
var terminated = new byte[source.Length + 1]; {
Array.Copy(source, 0, terminated, 0, source.Length); var terminated = new byte[source.Length + 1];
terminated[^1] = 0; Array.Copy(source, 0, terminated, 0, source.Length);
terminated[^1] = 0;
return terminated;
} return terminated;
}
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction(GameGui gameGui) [ServiceManager.CallWhenServicesReady]
{ private void ContinueConstruction(GameGui gameGui)
this.createFlyTextHook.Enable(); {
} this.createFlyTextHook.Enable();
}
private IntPtr CreateFlyTextDetour(
IntPtr addonFlyText, private IntPtr CreateFlyTextDetour(
FlyTextKind kind, IntPtr addonFlyText,
int val1, FlyTextKind kind,
int val2, int val1,
IntPtr text2, int val2,
uint color, IntPtr text2,
uint icon, uint color,
IntPtr text1, uint icon,
float yOffset) IntPtr text1,
{ float yOffset)
var retVal = IntPtr.Zero; {
try var retVal = IntPtr.Zero;
{ try
Log.Verbose("[FlyText] Enter CreateFlyText detour!"); {
Log.Verbose("[FlyText] Enter CreateFlyText detour!");
var handled = false;
var handled = false;
var tmpKind = kind;
var tmpVal1 = val1; var tmpKind = kind;
var tmpVal2 = val2; var tmpVal1 = val1;
var tmpText1 = text1 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(text1); var tmpVal2 = val2;
var tmpText2 = text2 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(text2); var tmpText1 = text1 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(text1);
var tmpColor = color; var tmpText2 = text2 == IntPtr.Zero ? string.Empty : MemoryHelper.ReadSeStringNullTerminated(text2);
var tmpIcon = icon; var tmpColor = color;
var tmpYOffset = yOffset; var tmpIcon = icon;
var tmpYOffset = yOffset;
var cmpText1 = tmpText1.ToString();
var cmpText2 = tmpText2.ToString(); var cmpText1 = tmpText1.ToString();
var cmpText2 = tmpText2.ToString();
Log.Verbose($"[FlyText] Called with addonFlyText({addonFlyText.ToInt64():X}) " +
$"kind({kind}) val1({val1}) val2({val2}) " + Log.Verbose($"[FlyText] Called with addonFlyText({addonFlyText.ToInt64():X}) " +
$"text1({text1.ToInt64():X}, \"{tmpText1}\") text2({text2.ToInt64():X}, \"{tmpText2}\") " + $"kind({kind}) val1({val1}) val2({val2}) " +
$"color({color:X}) icon({icon}) yOffset({yOffset})"); $"text1({text1.ToInt64():X}, \"{tmpText1}\") text2({text2.ToInt64():X}, \"{tmpText2}\") " +
Log.Verbose("[FlyText] Calling flytext events!"); $"color({color:X}) icon({icon}) yOffset({yOffset})");
this.FlyTextCreated?.Invoke( Log.Verbose("[FlyText] Calling flytext events!");
ref tmpKind, this.FlyTextCreated?.Invoke(
ref tmpVal1, ref tmpKind,
ref tmpVal2, ref tmpVal1,
ref tmpText1, ref tmpVal2,
ref tmpText2, ref tmpText1,
ref tmpColor, ref tmpText2,
ref tmpIcon, ref tmpColor,
ref tmpYOffset, ref tmpIcon,
ref handled); ref tmpYOffset,
ref handled);
// If handled, ignore the original call
if (handled) // If handled, ignore the original call
{ if (handled)
Log.Verbose("[FlyText] FlyText was handled."); {
Log.Verbose("[FlyText] FlyText was handled.");
// Returning null to AddFlyText from CreateFlyText will result
// in the operation being dropped entirely. // Returning null to AddFlyText from CreateFlyText will result
return IntPtr.Zero; // in the operation being dropped entirely.
} return IntPtr.Zero;
}
// Check if any values have changed
var dirty = tmpKind != kind || // Check if any values have changed
tmpVal1 != val1 || var dirty = tmpKind != kind ||
tmpVal2 != val2 || tmpVal1 != val1 ||
tmpText1.ToString() != cmpText1 || tmpVal2 != val2 ||
tmpText2.ToString() != cmpText2 || tmpText1.ToString() != cmpText1 ||
tmpColor != color || tmpText2.ToString() != cmpText2 ||
tmpIcon != icon || tmpColor != color ||
Math.Abs(tmpYOffset - yOffset) > float.Epsilon; tmpIcon != icon ||
Math.Abs(tmpYOffset - yOffset) > float.Epsilon;
// If not dirty, make the original call
if (!dirty) // 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); 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 terminated1 = Terminate(tmpText1.Encode());
var pText1 = Marshal.AllocHGlobal(terminated1.Length); var terminated2 = Terminate(tmpText2.Encode());
var pText2 = Marshal.AllocHGlobal(terminated2.Length); var pText1 = Marshal.AllocHGlobal(terminated1.Length);
Marshal.Copy(terminated1, 0, pText1, terminated1.Length); var pText2 = Marshal.AllocHGlobal(terminated2.Length);
Marshal.Copy(terminated2, 0, pText2, terminated2.Length); Marshal.Copy(terminated1, 0, pText1, terminated1.Length);
Log.Verbose("[FlyText] Allocated and set strings."); Marshal.Copy(terminated2, 0, pText2, terminated2.Length);
Log.Verbose("[FlyText] Allocated and set strings.");
retVal = this.createFlyTextHook.Original(
addonFlyText, retVal = this.createFlyTextHook.Original(
tmpKind, addonFlyText,
tmpVal1, tmpKind,
tmpVal2, tmpVal1,
pText2, tmpVal2,
tmpColor, pText2,
tmpIcon, tmpColor,
pText1, tmpIcon,
tmpYOffset); pText1,
tmpYOffset);
Log.Verbose("[FlyText] Returned from original. Delaying free task.");
Log.Verbose("[FlyText] Returned from original. Delaying free task.");
Task.Delay(2000).ContinueWith(_ =>
{ Task.Delay(2000).ContinueWith(_ =>
try {
{ try
Marshal.FreeHGlobal(pText1); {
Marshal.FreeHGlobal(pText2); Marshal.FreeHGlobal(pText1);
Log.Verbose("[FlyText] Freed strings."); Marshal.FreeHGlobal(pText2);
} Log.Verbose("[FlyText] Freed strings.");
catch (Exception e) }
{ catch (Exception e)
Log.Verbose(e, "[FlyText] Exception occurred freeing strings in task."); {
} Log.Verbose(e, "[FlyText] Exception occurred freeing strings in task.");
}); }
} });
catch (Exception e) }
{ catch (Exception e)
Log.Error(e, "Exception occurred in CreateFlyTextDetour!"); {
} Log.Error(e, "Exception occurred in CreateFlyTextDetour!");
}
return retVal;
} return retVal;
} }
} }

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,80 +1,79 @@
using System; using System;
namespace Dalamud.Game.Gui namespace Dalamud.Game.Gui;
/// <summary>
/// The address resolver for the <see cref="GameGui"/> class.
/// </summary>
internal sealed class GameGuiAddressResolver : BaseAddressResolver
{ {
/// <summary> /// <summary>
/// The address resolver for the <see cref="GameGui"/> class. /// Gets the base address of the native GuiManager class.
/// </summary> /// </summary>
internal sealed class GameGuiAddressResolver : BaseAddressResolver public IntPtr BaseAddress { 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 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 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 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 ToggleUiHide method.
/// </summary>
public IntPtr ToggleUiHide { get; private set; }
/// <summary>
/// Gets the address of the native Utf8StringFromSequence method.
/// </summary>
public IntPtr Utf8StringFromSequence { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(SigScanner sig)
{ {
/// <summary> this.SetGlobalBgm = sig.ScanText("4C 8B 15 ?? ?? ?? ?? 4D 85 D2 74 58");
/// Gets the base address of the native GuiManager class. this.HandleItemHover = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 5C 24 ?? 48 89 AE ?? ?? ?? ??");
/// </summary> this.HandleItemOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B FA 48 8B D9 4D");
public IntPtr BaseAddress { get; private set; } 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");
/// <summary> this.HandleImm = sig.ScanText("E8 ?? ?? ?? ?? 84 C0 75 10 48 83 FF 09");
/// Gets the address of the native SetGlobalBgm method. this.GetMatrixSingleton = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 4C 24 ?? 48 89 4c 24 ?? 4C 8D 4D ?? 4C 8D 44 24 ??");
/// </summary> this.ScreenToWorld = sig.ScanText("48 83 EC 48 48 8B 05 ?? ?? ?? ?? 4D 8B D1");
public IntPtr SetGlobalBgm { get; private set; } this.ToggleUiHide = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 0F B6 B9 ?? ?? ?? ?? B8 ?? ?? ?? ??");
this.Utf8StringFromSequence = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8D 41 22 66 C7 41 ?? ?? ?? 48 89 01 49 8B D8");
/// <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 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 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 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 Utf8StringFromSequence method.
/// </summary>
public IntPtr Utf8StringFromSequence { 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 ?? ?? ?? ??");
this.Utf8StringFromSequence = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8D 41 22 66 C7 41 ?? ?? ?? 48 89 01 49 8B D8");
}
} }
} }

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